@adaptic/maestro 1.6.0 → 1.7.0

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 (33) hide show
  1. package/package.json +1 -1
  2. package/scripts/continuous-monitor.sh +11 -6
  3. package/scripts/daemon/context-compiler.mjs +8 -8
  4. package/scripts/daemon/health.mjs +2 -2
  5. package/scripts/daemon/maestro-daemon.mjs +4 -3
  6. package/scripts/email_thread_dedup.py +4 -3
  7. package/scripts/huddle/huddle-server.mjs +50 -29
  8. package/scripts/llm_email_dedup.py +23 -15
  9. package/scripts/local-triggers/generate-plists.sh +2 -1
  10. package/scripts/media-generation/README.md +1 -1
  11. package/scripts/outbound-dedup-cleanup.sh +4 -4
  12. package/scripts/outbound-dedup.sh +4 -3
  13. package/scripts/pdf-generation/README.md +1 -1
  14. package/scripts/pdf-generation/templates/memo.latex +1 -1
  15. package/scripts/poll-slack-events.sh +4 -2
  16. package/scripts/poller/imap-client.mjs +11 -10
  17. package/scripts/poller/index.mjs +6 -6
  18. package/scripts/poller/intra-session-check.mjs +35 -18
  19. package/scripts/poller/mehran-gmail-poller.mjs +63 -29
  20. package/scripts/poller/slack-poller.mjs +45 -31
  21. package/scripts/poller/trigger.mjs +22 -5
  22. package/scripts/pre-draft-context.py +2 -2
  23. package/scripts/rag-indexer.py +3 -3
  24. package/scripts/send-sms.sh +7 -7
  25. package/scripts/send-whatsapp.sh +11 -11
  26. package/scripts/setup/configure-macos.sh +4 -2
  27. package/scripts/setup/init-agent.sh +1 -1
  28. package/scripts/slack-react.mjs +1 -1
  29. package/scripts/slack-typing.mjs +3 -3
  30. package/scripts/system-verify.sh +28 -15
  31. package/scripts/user-context-search.py +4 -4
  32. package/scripts/validate-outbound.py +29 -18
  33. package/scripts/sophie-inbox-poller.py +0 -406
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adaptic/maestro",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Maestro — Autonomous AI agent operating system. Deploy AI employees on dedicated Mac minis.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,18 +8,23 @@
8
8
  set -e
9
9
 
10
10
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
11
- SOPHIE_AI_DIR="$(dirname "$SCRIPT_DIR")"
11
+ AGENT_REPO_DIR="${AGENT_DIR:-$(dirname "$SCRIPT_DIR")}"
12
+ # Load agent identity for the LLM prompt
13
+ if [ -f "$AGENT_REPO_DIR/config/agent.env" ]; then
14
+ # shellcheck disable=SC1091
15
+ source "$AGENT_REPO_DIR/config/agent.env"
16
+ fi
12
17
  CHANNEL="${1:---channel all}"
13
18
  CHANNEL="${CHANNEL#--channel }"
14
- LOG_DIR="$SOPHIE_AI_DIR/logs/monitor"
15
- QUEUE_DIR="$SOPHIE_AI_DIR/outputs/inbound/queue"
19
+ LOG_DIR="$AGENT_REPO_DIR/logs/monitor"
20
+ QUEUE_DIR="$AGENT_REPO_DIR/outputs/inbound/queue"
16
21
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
17
22
 
18
23
  mkdir -p "$LOG_DIR" "$QUEUE_DIR"
19
24
 
20
25
  # Check emergency stop
21
26
  check_stop() {
22
- if [ -f "$SOPHIE_AI_DIR/.emergency-stop" ]; then
27
+ if [ -f "$AGENT_REPO_DIR/.emergency-stop" ]; then
23
28
  echo "[$TIMESTAMP] Monitor halted: emergency stop active" >> "$LOG_DIR/monitor.log"
24
29
  exit 0
25
30
  fi
@@ -40,14 +45,14 @@ monitor_channel() {
40
45
 
41
46
  # Use Claude to observe the channel via desktop control
42
47
  # This invokes the appropriate operator agent in observe mode
43
- claude -p "You are the $channel monitoring agent for Sophie Nguyen at Adaptic.ai.
48
+ claude -p "You are the $channel monitoring agent for ${AGENT_FULL_NAME:-the agent} at ${COMPANY_NAME:-the company}.
44
49
  OBSERVE ONLY — do not send any messages.
45
50
  Check for new messages since the last check.
46
51
  If there are new messages, write a YAML summary to: $QUEUE_DIR/${channel}-$(date +%s).yaml
47
52
  Include: sender, timestamp, channel/thread, subject/preview, urgency_hint.
48
53
  If no new messages, do nothing.
49
54
  Check the desktop $channel app using screenshot observation.
50
- Working directory: $SOPHIE_AI_DIR" \
55
+ Working directory: $AGENT_REPO_DIR" \
51
56
  --output-format text \
52
57
  >> "$log_file" 2>&1 || true
53
58
 
@@ -14,8 +14,8 @@
14
14
  import { readFileSync, readdirSync } from "fs";
15
15
  import { join } from "path";
16
16
 
17
- const SOPHIE_AI_DIR = process.env.__TEST_SOPHIE_DIR || join(new URL(".", import.meta.url).pathname, "../..");
18
- const SESSION_DIR = process.env.__TEST_SESSION_DIR || join(SOPHIE_AI_DIR, "state", "sessions");
17
+ const AGENT_REPO_DIR = process.env.__TEST_AGENT_DIR || process.env.AGENT_DIR || join(new URL(".", import.meta.url).pathname, "../..");
18
+ const SESSION_DIR = process.env.__TEST_SESSION_DIR || join(AGENT_REPO_DIR, "state", "sessions");
19
19
  const CHARS_PER_TOKEN = 4;
20
20
 
21
21
  // ── Token budgets by priority ──────────────────────────────────────
@@ -60,7 +60,7 @@ function extractChannelId(item) {
60
60
  function loadUserProfile(senderSlug, budgetTokens) {
61
61
  if (!senderSlug) return null;
62
62
  try {
63
- const filePath = join(SOPHIE_AI_DIR, "memory", "profiles", "users", `${senderSlug}.yaml`);
63
+ const filePath = join(AGENT_REPO_DIR, "memory", "profiles", "users", `${senderSlug}.yaml`);
64
64
  let raw = readFileSync(filePath, "utf-8");
65
65
  const charBudget = budgetTokens * CHARS_PER_TOKEN;
66
66
  if (raw.length > charBudget) {
@@ -80,7 +80,7 @@ function loadUserProfile(senderSlug, budgetTokens) {
80
80
  */
81
81
  function loadDomainDescriptions() {
82
82
  try {
83
- const filePath = join(SOPHIE_AI_DIR, "policies", "information-barriers.yaml");
83
+ const filePath = join(AGENT_REPO_DIR, "policies", "information-barriers.yaml");
84
84
  const raw = readFileSync(filePath, "utf-8");
85
85
  const map = new Map();
86
86
  const regex = /^\s{2}([\w-]+):\s*\n\s+description:\s*"(.+?)"/gm;
@@ -164,7 +164,7 @@ function loadDisclosureBoundaries(senderSlug, budgetTokens) {
164
164
 
165
165
  if (senderSlug) {
166
166
  try {
167
- const filePath = join(SOPHIE_AI_DIR, "memory", "profiles", "users", `${senderSlug}.yaml`);
167
+ const filePath = join(AGENT_REPO_DIR, "memory", "profiles", "users", `${senderSlug}.yaml`);
168
168
  const raw = readFileSync(filePath, "utf-8");
169
169
 
170
170
  // Extract privilege_level via regex
@@ -226,10 +226,10 @@ function loadDisclosureBoundaries(senderSlug, budgetTokens) {
226
226
  */
227
227
  function loadConversationHistory(senderSlug, channelId, budgetTokens, forbiddenDomains) {
228
228
  const candidateDirs = [];
229
- if (channelId) candidateDirs.push(join(SOPHIE_AI_DIR, "memory", "interactions", "slack", channelId));
229
+ if (channelId) candidateDirs.push(join(AGENT_REPO_DIR, "memory", "interactions", "slack", channelId));
230
230
  if (senderSlug) {
231
- candidateDirs.push(join(SOPHIE_AI_DIR, "memory", "interactions", "slack", `dm-${senderSlug}`));
232
- candidateDirs.push(join(SOPHIE_AI_DIR, "memory", "interactions", "slack", senderSlug));
231
+ candidateDirs.push(join(AGENT_REPO_DIR, "memory", "interactions", "slack", `dm-${senderSlug}`));
232
+ candidateDirs.push(join(AGENT_REPO_DIR, "memory", "interactions", "slack", senderSlug));
233
233
  }
234
234
 
235
235
  const entries = [];
@@ -5,7 +5,7 @@ import { writeFileSync, mkdirSync } from "fs";
5
5
  import { join } from "path";
6
6
  import { getStatus } from "./dispatcher.mjs";
7
7
 
8
- const SOPHIE_AI_DIR = join(new URL(".", import.meta.url).pathname, "../..");
8
+ const AGENT_REPO_DIR = process.env.AGENT_DIR || join(new URL(".", import.meta.url).pathname, "../..");
9
9
 
10
10
  let stats = {
11
11
  daemon_start: new Date().toISOString(),
@@ -51,7 +51,7 @@ export function writeHealthDashboard() {
51
51
  dispatcher,
52
52
  };
53
53
 
54
- const dir = join(SOPHIE_AI_DIR, "state", "dashboards");
54
+ const dir = join(AGENT_REPO_DIR, "state", "dashboards");
55
55
  mkdirSync(dir, { recursive: true });
56
56
  const path = join(dir, "daemon-health.yaml");
57
57
 
@@ -10,15 +10,16 @@
10
10
  // Install: launchd plist with KeepAlive: true
11
11
  // =============================================================================
12
12
 
13
- // The core daemon is in sophie-daemon.mjs for backward compatibility.
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).
14
16
  // It uses AGENT_DIR (resolved from this file's location) as the base path.
15
- // The init-maestro wizard renames sophie references in that file.
16
17
 
17
18
  import { resolve, dirname } from "node:path";
18
19
  import { fileURLToPath } from "node:url";
19
20
 
20
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
21
- const AGENT_DIR = resolve(__dirname, "../..");
22
+ const AGENT_DIR = process.env.AGENT_DIR || resolve(__dirname, "../..");
22
23
 
23
24
  // Set AGENT_DIR as env var so all modules can use it
24
25
  process.env.AGENT_DIR = AGENT_DIR;
@@ -34,9 +34,10 @@ import sys
34
34
  import time
35
35
  from datetime import datetime, timezone
36
36
 
37
- SOPHIE_AI = "/Users/sophie/sophie-ai"
38
- LOCK_DIR = os.path.join(SOPHIE_AI, "state", "locks", "outbound", "email-threads")
39
- AUDIT_DIR = os.path.join(SOPHIE_AI, "logs", "audit")
37
+ from pathlib import Path
38
+ AGENT_REPO_DIR = os.environ.get("AGENT_DIR", str(Path(__file__).resolve().parent.parent))
39
+ LOCK_DIR = os.path.join(AGENT_REPO_DIR, "state", "locks", "outbound", "email-threads")
40
+ AUDIT_DIR = os.path.join(AGENT_REPO_DIR, "logs", "audit")
40
41
  TTL_SECONDS = 24 * 60 * 60 # 24 hours
41
42
 
42
43
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Sophie Huddle — Main Server
3
+ * Agent Huddle — Main Server
4
4
  *
5
5
  * Orchestrates Slack huddle voice participation by coordinating:
6
6
  * - HuddleController (CDP-based Slack UI automation)
@@ -24,7 +24,7 @@ import dotenv from "dotenv";
24
24
  import { fileURLToPath as _fu } from "node:url";
25
25
  import { dirname as _dn, join as _jn } from "node:path";
26
26
 
27
- // Load .env from sophie-ai root (where all API keys live), then huddle-specific .env
27
+ // Load .env from agent repo root (where all API keys live), then huddle-specific .env
28
28
  const _d = _dn(_fu(import.meta.url));
29
29
  dotenv.config({ path: _jn(_d, "../..", ".env") });
30
30
  dotenv.config({ path: _jn(_d, ".env"), override: true });
@@ -42,8 +42,21 @@ import { HuddleController } from "./huddle-controller.mjs";
42
42
  import { AudioBridge } from "./audio-bridge.mjs";
43
43
 
44
44
  const __dirname = dirname(fileURLToPath(import.meta.url));
45
- const SOPHIE_ROOT = join(__dirname, "../..");
46
- const VOICE_AI_ROOT = join(process.env.HOME || "/Users/sophie", "voice-ai");
45
+ const AGENT_REPO_DIR = process.env.AGENT_DIR || join(__dirname, "../..");
46
+ const VOICE_AI_ROOT = process.env.VOICE_AI_ROOT || join(process.env.HOME || "", "voice-ai");
47
+
48
+ // Load agent identity for transcript speaker tags + display names.
49
+ let _agent = null;
50
+ function loadAgent() {
51
+ if (_agent) return _agent;
52
+ try {
53
+ _agent = JSON.parse(readFileSync(join(AGENT_REPO_DIR, "config/agent.json"), "utf-8"));
54
+ } catch {
55
+ _agent = { firstName: "Agent" };
56
+ }
57
+ return _agent;
58
+ }
59
+ const AGENT_SLUG = (loadAgent().firstName || "agent").toLowerCase();
47
60
 
48
61
  // ---------------------------------------------------------------------------
49
62
  // Configuration
@@ -90,7 +103,7 @@ const HuddleState = {
90
103
  };
91
104
 
92
105
  /**
93
- * HuddleServer is the top-level orchestrator for Sophie's huddle participation.
106
+ * HuddleServer is the top-level orchestrator for the agent's huddle participation.
94
107
  */
95
108
  class HuddleServer extends EventEmitter {
96
109
  constructor() {
@@ -114,7 +127,7 @@ class HuddleServer extends EventEmitter {
114
127
 
115
128
  async start() {
116
129
  console.log("\n==========================================");
117
- console.log(" Sophie Huddle Server");
130
+ console.log(` ${loadAgent().firstName || "Agent"} Huddle Server`);
118
131
  console.log(" " + new Date().toISOString());
119
132
  console.log("==========================================\n");
120
133
 
@@ -265,7 +278,7 @@ class HuddleServer extends EventEmitter {
265
278
  lastThreadTs: null, // Last seen thread message timestamp
266
279
  };
267
280
 
268
- // Load live context from sophie-ai for Claude
281
+ // Load live context from the agent repo for Claude
269
282
  this.session.liveContext = this._loadLiveContext();
270
283
 
271
284
  // In webaudio capture mode, set Slack's speaker to physical output
@@ -289,7 +302,7 @@ class HuddleServer extends EventEmitter {
289
302
  await this.bridge.speak(greeting);
290
303
  if (this.session) {
291
304
  this.session.messages.push({ role: "assistant", content: greeting });
292
- this.session.transcript.push({ speaker: "sophie", text: greeting, ts: Date.now() });
305
+ this.session.transcript.push({ speaker: AGENT_SLUG, text: greeting, ts: Date.now() });
293
306
  }
294
307
 
295
308
  // Start polling the huddle thread for text messages
@@ -329,7 +342,7 @@ class HuddleServer extends EventEmitter {
329
342
  // -------------------------------------------------------------------------
330
343
 
331
344
  /**
332
- * Process a transcript from the huddle and generate Sophie's response.
345
+ * Process a transcript from the huddle and generate the agent's response.
333
346
  * @param {string} userText - What someone said in the huddle
334
347
  */
335
348
  async _handleTranscript(userText) {
@@ -356,7 +369,7 @@ class HuddleServer extends EventEmitter {
356
369
 
357
370
  // Add to history
358
371
  this.session.messages.push({ role: "assistant", content: cleanResponse });
359
- this.session.transcript.push({ speaker: "sophie", text: cleanResponse, ts: Date.now() });
372
+ this.session.transcript.push({ speaker: AGENT_SLUG, text: cleanResponse, ts: Date.now() });
360
373
 
361
374
  // Speak the response via TTS → BlackHole 16ch → Slack mic
362
375
  await this.bridge.speak(cleanResponse);
@@ -380,7 +393,7 @@ class HuddleServer extends EventEmitter {
380
393
  /**
381
394
  * Start polling the huddle thread for text messages via the Slack API.
382
395
  * Text messages in the huddle thread are injected into the conversation
383
- * so Sophie can respond to both voice and text.
396
+ * so the agent can respond to both voice and text.
384
397
  */
385
398
  _startThreadPolling() {
386
399
  const slackToken = process.env.SLACK_USER_TOKEN || process.env.SLACK_BOT_TOKEN;
@@ -431,7 +444,7 @@ class HuddleServer extends EventEmitter {
431
444
 
432
445
  /**
433
446
  * Poll the Slack channel for recent messages in the huddle thread.
434
- * Only processes text messages from other users (not Sophie's bot).
447
+ * Only processes text messages from other users (not the agent's bot).
435
448
  */
436
449
  async _pollThreadMessages(channelId, token) {
437
450
  if (!this.session) return;
@@ -445,12 +458,14 @@ class HuddleServer extends EventEmitter {
445
458
  const data = await resp.json();
446
459
  if (!data.ok || !data.messages || data.messages.length === 0) return;
447
460
 
448
- // Sophie's user ID (skip her own messages)
449
- const sophieUserId = process.env.SLACK_BOT_USER_ID || "U099N1JFPRQ";
461
+ // The agent's Slack user ID (skip its own messages). Hardcoded fallback
462
+ // removed agents must set SLACK_BOT_USER_ID in .env if they want this
463
+ // filter active, otherwise the agent's slackMemberId from agent.json is used.
464
+ const agentUserId = process.env.SLACK_BOT_USER_ID || loadAgent().slackMemberId || "";
450
465
 
451
466
  for (const msg of data.messages.reverse()) { // oldest first
452
- // Skip bot messages, Sophie's messages, and non-text messages
453
- if (msg.user === sophieUserId) continue;
467
+ // Skip bot messages, the agent's own messages, and non-text messages
468
+ if (agentUserId && msg.user === agentUserId) continue;
454
469
  if (msg.subtype) continue; // Skip system messages
455
470
  if (!msg.text || !msg.text.trim()) continue;
456
471
 
@@ -495,7 +510,7 @@ class HuddleServer extends EventEmitter {
495
510
  // Build a single prompt from system + conversation history
496
511
  const parts = [`[System]\n${systemPrompt}\n`];
497
512
  for (const msg of messages) {
498
- const role = msg.role === "assistant" ? "Sophie" : "Participant";
513
+ const role = msg.role === "assistant" ? (loadAgent().firstName || "Agent") : "Participant";
499
514
  const content = typeof msg.content === "string"
500
515
  ? msg.content
501
516
  : msg.content?.map((b) => b.text || "").join("") || "";
@@ -503,7 +518,7 @@ class HuddleServer extends EventEmitter {
503
518
  parts.push(`[${role}]\n${content}`);
504
519
  }
505
520
  }
506
- parts.push("[Sophie]");
521
+ parts.push(`[${loadAgent().firstName || "Agent"}]`);
507
522
 
508
523
  const prompt = parts.join("\n\n");
509
524
 
@@ -568,7 +583,7 @@ class HuddleServer extends EventEmitter {
568
583
  if (filler) {
569
584
  await this.bridge.speak(filler);
570
585
  if (this.session) {
571
- this.session.transcript.push({ speaker: "sophie", text: filler, ts: Date.now() });
586
+ this.session.transcript.push({ speaker: AGENT_SLUG, text: filler, ts: Date.now() });
572
587
  }
573
588
  }
574
589
  }
@@ -616,7 +631,7 @@ class HuddleServer extends EventEmitter {
616
631
  // -------------------------------------------------------------------------
617
632
 
618
633
  /**
619
- * Resolve a huddle participant's identity from sophie-ai contacts/profiles.
634
+ * Resolve a huddle participant's identity from the agent repo contacts/profiles.
620
635
  * Used for access control (CEO gets write tools) and personalized greetings.
621
636
  */
622
637
  _resolveHuddleParticipant(identifier) {
@@ -636,7 +651,7 @@ class HuddleServer extends EventEmitter {
636
651
 
637
652
  // Try loading from contacts.yaml
638
653
  try {
639
- const contactsPath = join(SOPHIE_ROOT, "config/contacts.yaml");
654
+ const contactsPath = join(AGENT_REPO_DIR, "config/contacts.yaml");
640
655
  const contacts = readFileSync(contactsPath, "utf8");
641
656
  // Simple search — look for the identifier in the contacts file
642
657
  if (contacts.toLowerCase().includes(key)) {
@@ -695,7 +710,7 @@ class HuddleServer extends EventEmitter {
695
710
  console.log(`[EVENTS] Huddle event: ${user_name || user} → ${huddle_state} in ${channel || "unknown"}`);
696
711
  this._logEvent("events-api-huddle", event);
697
712
 
698
- // Someone started a huddle and Sophie isn't in one yet
713
+ // Someone started a huddle and the agent isn't in one yet
699
714
  if (huddle_state === "in_a_huddle" && this.state === HuddleState.IDLE && this.autoJoin) {
700
715
  // Determine the channel to join
701
716
  const targetChannel = channel || user_name;
@@ -719,7 +734,7 @@ class HuddleServer extends EventEmitter {
719
734
  }
720
735
 
721
736
  // -------------------------------------------------------------------------
722
- // Live context loader (reads from sophie-ai state files)
737
+ // Live context loader (reads from the agent repo state files)
723
738
  // -------------------------------------------------------------------------
724
739
 
725
740
  _loadLiveContext() {
@@ -727,13 +742,13 @@ class HuddleServer extends EventEmitter {
727
742
 
728
743
  // Load priorities
729
744
  try {
730
- const priorities = readFileSync(join(SOPHIE_ROOT, "config/priorities.yaml"), "utf8");
745
+ const priorities = readFileSync(join(AGENT_REPO_DIR, "config/priorities.yaml"), "utf8");
731
746
  contextParts.push(`PRIORITIES:\n${priorities.slice(0, 2000)}`);
732
747
  } catch { /* optional */ }
733
748
 
734
749
  // Load executive summary
735
750
  try {
736
- const summary = readFileSync(join(SOPHIE_ROOT, "state/dashboards/executive-summary.yaml"), "utf8");
751
+ const summary = readFileSync(join(AGENT_REPO_DIR, "state/dashboards/executive-summary.yaml"), "utf8");
737
752
  contextParts.push(`EXECUTIVE SUMMARY:\n${summary.slice(0, 3000)}`);
738
753
  } catch { /* optional */ }
739
754
 
@@ -835,7 +850,7 @@ class HuddleServer extends EventEmitter {
835
850
  // -------------------------------------------------------------------------
836
851
 
837
852
  /**
838
- * Toggle Sophie's Slack mic mute via CDP on the huddle window.
853
+ * Toggle the agent's Slack mic mute via CDP on the huddle window.
839
854
  * Keeps mic muted between TTS outputs to prevent echo from the
840
855
  * virtual audio loopback.
841
856
  * @param {boolean} muted - true to mute, false to unmute
@@ -946,7 +961,7 @@ class HuddleServer extends EventEmitter {
946
961
  // -------------------------------------------------------------------------
947
962
 
948
963
  _logEvent(event, data = {}) {
949
- const logDir = join(SOPHIE_ROOT, "logs/huddle");
964
+ const logDir = join(AGENT_REPO_DIR, "logs/huddle");
950
965
  mkdirSync(logDir, { recursive: true });
951
966
 
952
967
  const date = new Date().toISOString().split("T")[0];
@@ -964,7 +979,7 @@ class HuddleServer extends EventEmitter {
964
979
  _saveTranscript() {
965
980
  if (!this.session || this.session.transcript.length === 0) return;
966
981
 
967
- const logDir = join(SOPHIE_ROOT, "logs/huddle");
982
+ const logDir = join(AGENT_REPO_DIR, "logs/huddle");
968
983
  mkdirSync(logDir, { recursive: true });
969
984
 
970
985
  const date = new Date().toISOString().split("T")[0];
@@ -984,7 +999,10 @@ class HuddleServer extends EventEmitter {
984
999
  // Huddle-specific system prompt
985
1000
  // ---------------------------------------------------------------------------
986
1001
 
987
- const HUDDLE_SYSTEM_PROMPT = `You are Sophie Nguyen, Chief of Staff to Mehran Granfar, CEO of Adaptic.ai.
1002
+ function buildHuddleSystemPrompt() {
1003
+ const a = loadAgent();
1004
+ const principal = a.principal || {};
1005
+ return `You are ${a.fullName}, ${a.title} to ${principal.fullName || "the principal"}, ${principal.title || "the principal"} of ${a.company}.
988
1006
  You are participating in a Slack huddle — a real-time voice conversation.
989
1007
 
990
1008
  PERSONALITY:
@@ -1026,6 +1044,9 @@ CRITICAL — DO NOT REPEAT YOURSELF:
1026
1044
  - Never re-introduce yourself after the initial greeting
1027
1045
  - Each response should add new information or move the conversation forward
1028
1046
  - If you already answered a question, do not repeat the answer`;
1047
+ }
1048
+
1049
+ const HUDDLE_SYSTEM_PROMPT = buildHuddleSystemPrompt();
1029
1050
 
1030
1051
  // ---------------------------------------------------------------------------
1031
1052
  // Main
@@ -35,22 +35,30 @@ import time
35
35
  from datetime import datetime, timedelta, timezone
36
36
  from email.utils import parsedate_to_datetime
37
37
 
38
- SOPHIE_AI = "/Users/sophie/sophie-ai"
39
- AUDIT_DIR = os.path.join(SOPHIE_AI, "logs", "audit")
38
+ from pathlib import Path
39
+ import json as _json
40
+ AGENT_REPO_DIR = os.environ.get("AGENT_DIR", str(Path(__file__).resolve().parent.parent))
41
+ AUDIT_DIR = os.path.join(AGENT_REPO_DIR, "logs", "audit")
40
42
 
41
- # Gmail credentials same pattern as other send scripts
42
- SOPHIE_EMAIL = "sophie@adaptic.ai"
43
- SOPHIE_PASSWORD = os.environ.get("GMAIL_APP_PASSWORD", "")
43
+ # Load agent identity (canonical SOT)
44
+ try:
45
+ with open(os.path.join(AGENT_REPO_DIR, "config", "agent.json")) as _f:
46
+ _AGENT = _json.load(_f)
47
+ except Exception:
48
+ _AGENT = {"email": "agent@example.com", "firstName": "agent"}
49
+
50
+ AGENT_EMAIL = _AGENT.get("email", "agent@example.com")
51
+ AGENT_GMAIL_PASSWORD = os.environ.get("GMAIL_APP_PASSWORD", "")
44
52
 
45
53
  # Load from .env if not in environment
46
- if not SOPHIE_PASSWORD:
47
- _env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
54
+ if not AGENT_GMAIL_PASSWORD:
55
+ _env_path = os.path.join(AGENT_REPO_DIR, ".env")
48
56
  if os.path.exists(_env_path):
49
57
  with open(_env_path) as _f:
50
58
  for _line in _f:
51
59
  _line = _line.strip()
52
60
  if _line.startswith("GMAIL_APP_PASSWORD="):
53
- SOPHIE_PASSWORD = _line.split("=", 1)[1].strip().strip('"').strip("'")
61
+ AGENT_GMAIL_PASSWORD = _line.split("=", 1)[1].strip().strip('"').strip("'")
54
62
  break
55
63
 
56
64
  # Anthropic API key
@@ -112,9 +120,9 @@ def fetch_recent_thread_messages(to: str, subject: str, sender_email: str = "",
112
120
  List of message dicts sorted by date (newest first), or empty list on error.
113
121
  """
114
122
  if not sender_email:
115
- sender_email = SOPHIE_EMAIL
123
+ sender_email = AGENT_EMAIL
116
124
  if not sender_password:
117
- sender_password = SOPHIE_PASSWORD
125
+ sender_password = AGENT_GMAIL_PASSWORD
118
126
 
119
127
  if not sender_email or not sender_password:
120
128
  return []
@@ -283,7 +291,7 @@ def llm_dedup_check(to: str, subject: str, body: str,
283
291
  to: Recipient email address
284
292
  subject: Proposed email subject
285
293
  body: Proposed email body
286
- sender_email: Sender's Gmail address (default: sophie@adaptic.ai)
294
+ sender_email: Sender's Gmail address (default: agent.email from config/agent.json)
287
295
  sender_password: Sender's app password (default: from env/config)
288
296
 
289
297
  Returns:
@@ -298,8 +306,8 @@ def llm_dedup_check(to: str, subject: str, body: str,
298
306
  # Fetch recent conversation history
299
307
  recent = fetch_recent_thread_messages(
300
308
  to, subject,
301
- sender_email=sender_email or SOPHIE_EMAIL,
302
- sender_password=sender_password or SOPHIE_PASSWORD,
309
+ sender_email=sender_email or AGENT_EMAIL,
310
+ sender_password=sender_password or AGENT_GMAIL_PASSWORD,
303
311
  )
304
312
 
305
313
  if not recent:
@@ -315,7 +323,7 @@ def llm_dedup_check(to: str, subject: str, body: str,
315
323
  # Build context for LLM assessment
316
324
  conversation_summary = []
317
325
  for i, msg in enumerate(recent):
318
- direction = "SENT" if SOPHIE_EMAIL.lower() in msg.get("from", "").lower() or (sender_email and sender_email.lower() in msg.get("from", "").lower()) else "RECEIVED"
326
+ direction = "SENT" if AGENT_EMAIL.lower() in msg.get("from", "").lower() or (sender_email and sender_email.lower() in msg.get("from", "").lower()) else "RECEIVED"
319
327
  conversation_summary.append(
320
328
  f"[{i+1}] {direction} | Date: {msg['date']} | Subject: {msg['subject']}\n"
321
329
  f" Preview: {msg['body_preview'][:300]}"
@@ -404,7 +412,7 @@ def main():
404
412
  parser.add_argument("to", help="Recipient email address")
405
413
  parser.add_argument("subject", help="Proposed email subject")
406
414
  parser.add_argument("body", help="Proposed email body")
407
- parser.add_argument("--sender-email", default="", help="Sender email (default: sophie@adaptic.ai)")
415
+ parser.add_argument("--sender-email", default="", help="Sender email (default: agent.email from config/agent.json)")
408
416
  parser.add_argument("--sender-password", default="", help="Sender app password (default: from env)")
409
417
  parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed output")
410
418
 
@@ -40,7 +40,8 @@ echo "[generate-plists] User: $AGENT_USER"
40
40
  mkdir -p "$PLIST_DIR"
41
41
 
42
42
  # Clear old plists before generating — prevents stale plists from a previous
43
- # agent identity (e.g. sophie-*) from being installed alongside new ones
43
+ # agent identity (e.g. an old AGENT_LAUNCHD_PREFIX) from being installed
44
+ # alongside new ones
44
45
  rm -f "$PLIST_DIR"/*.plist 2>/dev/null
45
46
 
46
47
  # ── Helper: generate a single plist ──────────────────────────────────────────
@@ -9,7 +9,7 @@ Ported from `~/adapticai/app/scripts/media-generation/` and `~/ai-born/`.
9
9
 
10
10
  ```bash
11
11
  # Install dependencies
12
- cd ~/sophie-ai && npm install
12
+ cd ~/<agent-name> && npm install
13
13
 
14
14
  # Set API key in .env or config/environment.yaml
15
15
  echo "GEMINI_API_KEY=your-key-here" >> .env
@@ -5,14 +5,14 @@
5
5
  # Also cleans the legacy Slack locks in state/locks/slack-response/
6
6
  #
7
7
  # Intended to be run by launchd or cron every 5 minutes:
8
- # */5 * * * * /Users/sophie/sophie-ai/scripts/outbound-dedup-cleanup.sh
8
+ # */5 * * * * <AGENT_REPO_DIR>/scripts/outbound-dedup-cleanup.sh
9
9
  #
10
10
  # Or via launchd plist with StartInterval: 300
11
11
  set -e
12
12
 
13
- SOPHIE_AI="/Users/sophie/sophie-ai"
14
- SCRIPT_DIR="$SOPHIE_AI/scripts"
15
- LOG_DIR="$SOPHIE_AI/logs/audit"
13
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
14
+ AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
15
+ LOG_DIR="$AGENT_REPO_DIR/logs/audit"
16
16
  TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
17
17
 
18
18
  mkdir -p "$LOG_DIR"
@@ -44,10 +44,11 @@
44
44
  # - Full audit trail to logs/audit/
45
45
  set -e
46
46
 
47
- SOPHIE_AI="/Users/sophie/sophie-ai"
48
- LOCK_BASE_DIR="$SOPHIE_AI/state/locks/outbound"
47
+ SCRIPT_DIR_INIT="$(cd "$(dirname "$0")" && pwd)"
48
+ AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$SCRIPT_DIR_INIT/.." && pwd)}"
49
+ LOCK_BASE_DIR="$AGENT_REPO_DIR/state/locks/outbound"
49
50
  LOCK_TTL_MINUTES=720 # 12 hours — email dedup needs long TTL to prevent cross-session duplicates
50
- AUDIT_DIR="$SOPHIE_AI/logs/audit"
51
+ AUDIT_DIR="$AGENT_REPO_DIR/logs/audit"
51
52
  TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
52
53
 
53
54
  # Ensure base directories exist
@@ -15,7 +15,7 @@ brew install pandoc
15
15
  brew install --cask mactex-no-gui
16
16
 
17
17
  # Install Node.js dependencies
18
- cd ~/sophie-ai && npm install
18
+ cd ~/<agent-name> && npm install
19
19
  ```
20
20
 
21
21
  ## Usage
@@ -49,7 +49,7 @@
49
49
  \renewcommand{\footrulewidth}{0.3pt}
50
50
  \fancyhead[L]{\includegraphics[height=10mm]{public/assets/adaptic-icon-dark.png}}
51
51
  \fancyhead[R]{\footnotesize\sffamily\color{adaptic-text-secondary} Internal — $if(title)$$title$$endif$}
52
- \fancyfoot[L]{\footnotesize\color{adaptic-text-secondary} Prepared by Sophie Nguyen, Chief of Staff}
52
+ \fancyfoot[L]{\footnotesize\color{adaptic-text-secondary} $if(author)$Prepared by $author$$endif$}
53
53
  \fancyfoot[R]{\footnotesize\color{adaptic-text-secondary} \thepage}
54
54
 
55
55
  % ─── Links ───
@@ -4,8 +4,10 @@
4
4
  # Run via cron every 5-10 seconds for near-instant response
5
5
  set -e
6
6
 
7
- EVENTS_URL="https://slack-events-server-production.up.railway.app/events"
8
- INBOX_DIR="/Users/sophie/sophie-ai/state/inbox/slack"
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
9
+ EVENTS_URL="${SLACK_EVENTS_RELAY_URL:-https://slack-events-server-production.up.railway.app/events}"
10
+ INBOX_DIR="$AGENT_REPO_DIR/state/inbox/slack"
9
11
  mkdir -p "$INBOX_DIR"
10
12
 
11
13
  RESPONSE=$(curl -s --max-time 5 "$EVENTS_URL" 2>/dev/null)
@@ -1,4 +1,5 @@
1
- // imap-client.mjs — Shared native IMAP polling for Sophie and Mehran inboxes
1
+ // imap-client.mjs — Shared native IMAP polling for the agent's inbox and
2
+ // any other inbox the agent has explicit access to (e.g. principal-gmail).
2
3
  // Replaces external Python scripts with direct Node.js IMAP via imapflow.
3
4
  //
4
5
  // Each inbox gets its own credentials and file-naming convention, but the
@@ -9,10 +10,10 @@ import { ImapFlow } from "imapflow";
9
10
  import { writeFileSync, existsSync, readFileSync, mkdirSync } from "fs";
10
11
  import { join, dirname } from "path";
11
12
  import { createHash } from "crypto";
12
- import { SOPHIE_AI_DIR } from "./utils.mjs";
13
+ import { AGENT_REPO_DIR } from "./utils.mjs";
13
14
 
14
- const INBOX_DIR = join(SOPHIE_AI_DIR, "state", "inbox", "gmail");
15
- const ATTACHMENTS_DIR = join(SOPHIE_AI_DIR, "state", "inbox", "attachments");
15
+ const INBOX_DIR = join(AGENT_REPO_DIR, "state", "inbox", "gmail");
16
+ const ATTACHMENTS_DIR = join(AGENT_REPO_DIR, "state", "inbox", "attachments");
16
17
  const MAX_EMAILS_PER_CYCLE = 20;
17
18
  const SNIPPET_LENGTH = 500;
18
19
  const IMAP_TIMEOUT_MS = 25_000;
@@ -55,7 +56,7 @@ function isAlreadyProcessed(midHash, suffix) {
55
56
  // ── Cursor Management ───────────────────────────────────────────────────
56
57
 
57
58
  function cursorPath(account) {
58
- return join(SOPHIE_AI_DIR, "state", "polling", `${account}-gmail-cursor.yaml`);
59
+ return join(AGENT_REPO_DIR, "state", "polling", `${account}-gmail-cursor.yaml`);
59
60
  }
60
61
 
61
62
  function readImapCursor(account) {
@@ -128,12 +129,12 @@ function saveAttachments(parsed, midHash) {
128
129
  * Poll a Gmail inbox via IMAP and write new messages as JSON to the inbox dir.
129
130
  *
130
131
  * @param {object} opts
131
- * @param {string} opts.email — Email address (e.g. sophie@adaptic.ai)
132
+ * @param {string} opts.email — Email address to poll
132
133
  * @param {string} opts.password — Gmail app password
133
- * @param {string} opts.account — Account slug for cursor/file naming (e.g. "sophie" or "mehran")
134
- * @param {string} opts.fileSuffix — JSON file suffix (e.g. "email" or "mehran-email")
135
- * @param {string} opts.eventType — Event type tag (e.g. "sophie_email" or "mehran_email")
136
- * @param {string} opts.logPrefix — Console log prefix (e.g. "[sophie-gmail]")
134
+ * @param {string} opts.account — Account slug for cursor/file naming (e.g. agent's first name)
135
+ * @param {string} opts.fileSuffix — JSON file suffix
136
+ * @param {string} opts.eventType — Event type tag
137
+ * @param {string} opts.logPrefix — Console log prefix
137
138
  * @returns {Promise<{newCount: number, errors: string[]}>}
138
139
  */
139
140
  export async function pollImapInbox({