@adaptic/maestro 1.5.2 → 1.6.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.
@@ -1,8 +1,15 @@
1
1
  #!/bin/bash
2
- # send-email.sh — Send HTML email via SMTP as Sophie Nguyen
2
+ # send-email.sh — Send HTML email via SMTP as the running agent
3
3
  # Usage: ./scripts/send-email.sh <to> <subject> <body> [cc] [in-reply-to] [references]
4
4
  set -e
5
5
 
6
+ SCRIPT_DIR_INIT="$(cd "$(dirname "$0")" && pwd)"
7
+ AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$SCRIPT_DIR_INIT/.." && pwd)}"
8
+ if [ -f "$AGENT_REPO_DIR/config/agent.env" ]; then
9
+ # shellcheck disable=SC1091
10
+ source "$AGENT_REPO_DIR/config/agent.env"
11
+ fi
12
+
6
13
  TO="$1"
7
14
  SUBJECT="$2"
8
15
  BODY="$3"
@@ -25,7 +32,7 @@ if [ -x "$SCRIPT_DIR/outbound-dedup.sh" ]; then
25
32
  # Generate content-hash key and acquire lock
26
33
  CONTENT_KEY=$("$SCRIPT_DIR/outbound-dedup.sh" generate-key email "$TO" "$SUBJECT" "$BODY" 2>/dev/null) || true
27
34
  if [ -n "$CONTENT_KEY" ]; then
28
- ACQUIRE_RESULT=$("$SCRIPT_DIR/outbound-dedup.sh" acquire email "$CONTENT_KEY" "${SOPHIE_SESSION_ID:-$$}" 2>/dev/null) || true
35
+ ACQUIRE_RESULT=$("$SCRIPT_DIR/outbound-dedup.sh" acquire email "$CONTENT_KEY" "${AGENT_SESSION_ID:-${SOPHIE_SESSION_ID:-${RAVI_SESSION_ID:-$$}}}" 2>/dev/null) || true
29
36
  if [ "$ACQUIRE_RESULT" = "DEDUP_SKIP" ]; then
30
37
  echo "DEDUP_SKIP"
31
38
  exit 0
@@ -39,7 +46,7 @@ fi
39
46
 
40
47
  # Build headers
41
48
  HEADERS="To: ${TO}
42
- From: Sophie Nguyen <sophie@adaptic.ai>
49
+ From: ${AGENT_FULL_NAME:-Agent} <${AGENT_EMAIL:-agent@example.com}>
43
50
  Subject: ${SUBJECT}
44
51
  MIME-Version: 1.0
45
52
  Content-Type: text/html; charset=UTF-8"
@@ -61,23 +68,29 @@ References: <${IN_REPLY_TO}>"
61
68
  fi
62
69
  fi
63
70
 
64
- LOGO_URL="https://ci3.googleusercontent.com/mail-sig/AIorK4xNDnPN6a0MjSHV5urb9g11u2CzHIzwdJbwN21quGjzQFdbOdegCq1Wp6lq3NCqsARbi-ooqPfE9E1D"
71
+ LOGO_URL="${EMAIL_SIGNATURE_LOGO_URL:-https://ci3.googleusercontent.com/mail-sig/AIorK4xNDnPN6a0MjSHV5urb9g11u2CzHIzwdJbwN21quGjzQFdbOdegCq1Wp6lq3NCqsARbi-ooqPfE9E1D}"
72
+ SIG_ADDRESS="${EMAIL_SIGNATURE_ADDRESS:-Level 1, Innovation One, Dubai International Financial Centre, Dubai, UAE}"
73
+ SIG_PHONE_HTML=""
74
+ if [ -n "${AGENT_PHONE:-}" ]; then
75
+ PHONE_TEL=$(echo "$AGENT_PHONE" | tr -d ' ')
76
+ SIG_PHONE_HTML="<div style=\"font-size:small;\"><a href=\"tel:${PHONE_TEL}\" style=\"color:#1155cc;\">${AGENT_PHONE}</a></div>
77
+ <div><br></div>
78
+ "
79
+ fi
65
80
 
66
- # Build HTML body — signature matches Mehran's format exactly
81
+ # Build HTML body
67
82
  HTML_BODY="<html><body>
68
83
  <div style=\"font-family:Arial,Helvetica,sans-serif;font-size:small;color:#000;\">
69
84
  $(echo "$BODY" | sed 's/$/<br>/g')
70
85
  </div>
71
86
  <br>
72
87
  <div style=\"font-family:Arial,Helvetica,sans-serif;color:#000;\">
73
- <div style=\"font-size:small;\"><b>Sophie Nguyen</b></div>
74
- <div style=\"font-size:small;\">Chief of Staff</div>
88
+ <div style=\"font-size:small;\"><b>${AGENT_FULL_NAME:-Agent}</b></div>
89
+ <div style=\"font-size:small;\">${AGENT_TITLE:-}</div>
75
90
  <div><br></div>
76
91
  <div><img src=\"${LOGO_URL}\" width=\"125\"></div>
77
92
  <div><br></div>
78
- <div style=\"font-size:small;\"><a href=\"tel:+16282656712\" style=\"color:#1155cc;\">+1 628 265 6712</a> (USA)</div>
79
- <div><br></div>
80
- <div style=\"font-size:x-small;color:#000;\">Level 1, Innovation One, Dubai International Financial Centre, Dubai, UAE</div>
93
+ ${SIG_PHONE_HTML}<div style=\"font-size:x-small;color:#000;\">${SIG_ADDRESS}</div>
81
94
  <div style=\"font-size:x-small;color:#666;\">_________________________________________</div>
82
95
  <div><br></div>
83
96
  <div style=\"font-size:x-small;color:#999;\">This email and any files transmitted with it are confidential and intended solely for the use of the individual or entity to whom they are addressed. If you have received this email in error please notify the sender. This message contains confidential information and is intended only for the individual named. If you are not the named addressee you should not disseminate, distribute or copy this email. Please notify the sender immediately by email if you have received this email by mistake and delete this email from your system. If you are not the intended recipient you are notified that disclosing, copying, distributing, or taking any action in reliance on the contents of this information is strictly prohibited.</div>
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * generate-agent-env.mjs — Generate config/agent.env from config/agent.json
4
+ *
5
+ * Shell scripts can't import TypeScript or parse JSON cheaply, so we
6
+ * pre-derive a flat env file that any bash script can source:
7
+ *
8
+ * source "$AGENT_DIR/config/agent.env"
9
+ * echo "Hello, $AGENT_FIRST_NAME"
10
+ *
11
+ * The generator is invoked:
12
+ * - During `npm run init-agent` (initial setup)
13
+ * - At the end of every `maestro upgrade` run
14
+ * - On demand: `node scripts/setup/generate-agent-env.mjs`
15
+ *
16
+ * No TypeScript runtime required — we read agent.json directly.
17
+ */
18
+
19
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
20
+ import { resolve, dirname } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const AGENT_DIR = process.env.AGENT_DIR || resolve(__dirname, "../..");
25
+ const JSON_PATH = resolve(AGENT_DIR, "config/agent.json");
26
+ const ENV_PATH = resolve(AGENT_DIR, "config/agent.env");
27
+
28
+ if (!existsSync(JSON_PATH)) {
29
+ console.error(`[generate-agent-env] config/agent.json not found at ${JSON_PATH}`);
30
+ console.error("[generate-agent-env] Run /init-maestro to configure identity, or migrate from config/agent.ts");
31
+ process.exit(1);
32
+ }
33
+
34
+ const agent = JSON.parse(readFileSync(JSON_PATH, "utf-8"));
35
+
36
+ // Shell-quote: wrap in single quotes; replace ' with '\'' (close, escape, reopen).
37
+ const q = (v) => {
38
+ const s = v === null || v === undefined ? "" : String(v);
39
+ return `'${s.replace(/'/g, "'\\''")}'`;
40
+ };
41
+
42
+ const repoDir = `$HOME/${agent.repoSlug}`;
43
+ const firstUpper = (agent.firstName || "").toUpperCase();
44
+ const firstLower = (agent.firstName || "").toLowerCase();
45
+ const signature = `${agent.fullName} | ${agent.title} | ${agent.company}`;
46
+
47
+ const lines = [
48
+ `# Auto-generated from config/agent.json by @adaptic/maestro.`,
49
+ `# DO NOT EDIT — regenerate with: node scripts/setup/generate-agent-env.mjs`,
50
+ `# Sourced by every shell script that needs agent identity.`,
51
+ ``,
52
+ `# ── Agent identity ─────────────────────────────────────────────────`,
53
+ `export AGENT_FIRST_NAME=${q(agent.firstName)}`,
54
+ `export AGENT_LAST_NAME=${q(agent.lastName)}`,
55
+ `export AGENT_FULL_NAME=${q(agent.fullName)}`,
56
+ `export AGENT_TITLE=${q(agent.title)}`,
57
+ `export AGENT_ARCHETYPE=${q(agent.archetype)}`,
58
+ `export AGENT_EMAIL=${q(agent.email)}`,
59
+ `export AGENT_PHONE=${q(agent.phone)}`,
60
+ `export AGENT_SLACK_USER_ID=${q(agent.slackMemberId)}`,
61
+ `export AGENT_REPO_SLUG=${q(agent.repoSlug)}`,
62
+ `export AGENT_REPO_DIR="${repoDir}"`,
63
+ `export AGENT_MACHINE_NAME=${q(agent.machineName)}`,
64
+ `export AGENT_LAUNCHD_PREFIX=${q(agent.launchdLabelPrefix)}`,
65
+ `export AGENT_TIMEZONE=${q(agent.timezone)}`,
66
+ `export AGENT_LOCALE=${q(agent.locale)}`,
67
+ `export AGENT_UPPER=${q(firstUpper)}`,
68
+ `export AGENT_LOWER=${q(firstLower)}`,
69
+ `export AGENT_EMAIL_SIGNATURE=${q(signature)}`,
70
+ ``,
71
+ `# ── Principal ──────────────────────────────────────────────────────`,
72
+ `export PRINCIPAL_FIRST_NAME=${q(agent.principal?.firstName)}`,
73
+ `export PRINCIPAL_LAST_NAME=${q(agent.principal?.lastName)}`,
74
+ `export PRINCIPAL_FULL_NAME=${q(agent.principal?.fullName)}`,
75
+ `export PRINCIPAL_TITLE=${q(agent.principal?.title)}`,
76
+ `export PRINCIPAL_EMAIL=${q(agent.principal?.email)}`,
77
+ `export PRINCIPAL_SLACK_USER_ID=${q(agent.principal?.slackMemberId)}`,
78
+ ``,
79
+ `# ── Company ────────────────────────────────────────────────────────`,
80
+ `export COMPANY_NAME=${q(agent.company)}`,
81
+ `export COMPANY_DOMAIN=${q(agent.companyDomain)}`,
82
+ `export COMPANY_DESCRIPTION=${q(agent.companyDescription)}`,
83
+ ``,
84
+ `# ── Schedule ───────────────────────────────────────────────────────`,
85
+ `export SCHEDULE_MORNING_BRIEF=${q(agent.schedule?.morningBrief)}`,
86
+ `export SCHEDULE_EVENING_WRAP=${q(agent.schedule?.eveningWrap)}`,
87
+ `export SCHEDULE_OVERNIGHT_MONITORING=${q(agent.schedule?.overnightMonitoring)}`,
88
+ ``,
89
+ ];
90
+
91
+ writeFileSync(ENV_PATH, lines.join("\n"));
92
+ console.log(`[generate-agent-env] wrote ${ENV_PATH}`);
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * migrate-agent-to-sot.mjs — Migrate an existing maestro agent repo to the
4
+ * Source-of-Truth layout (`config/agent.json` canonical + `config/agent.ts`
5
+ * thin wrapper + `config/agent.env` generated).
6
+ *
7
+ * Before:
8
+ * config/agent.ts ← contains inline `export const agent = { firstName: "Ravi", ... }`
9
+ *
10
+ * After:
11
+ * config/agent.json ← canonical data extracted from agent.ts
12
+ * config/agent.ts ← thin wrapper that imports agent.json + provides types
13
+ * config/agent.env ← generated for shell scripts
14
+ *
15
+ * Run from inside an agent repo:
16
+ * npx tsx scripts/setup/migrate-agent-to-sot.mjs
17
+ *
18
+ * Requires `tsx` in the agent's devDependencies (it is — the scaffold sets it).
19
+ *
20
+ * Safety:
21
+ * - Backs up the existing config/agent.ts to config/agent.ts.bak
22
+ * - Refuses to run if config/agent.json already exists
23
+ * - Refuses to run if config/agent.ts can't be imported
24
+ */
25
+
26
+ import { readFileSync, writeFileSync, copyFileSync, existsSync } from "node:fs";
27
+ import { resolve, dirname } from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+ import { execFileSync } from "node:child_process";
30
+
31
+ const __dirname = dirname(fileURLToPath(import.meta.url));
32
+ const AGENT_DIR = process.env.AGENT_DIR || resolve(__dirname, "../..");
33
+ const TS_PATH = resolve(AGENT_DIR, "config/agent.ts");
34
+ const JSON_PATH = resolve(AGENT_DIR, "config/agent.json");
35
+ const BAK_PATH = resolve(AGENT_DIR, "config/agent.ts.pre-sot-migration.bak");
36
+
37
+ if (!existsSync(TS_PATH)) {
38
+ console.error(`[migrate] config/agent.ts not found at ${TS_PATH}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ if (existsSync(JSON_PATH)) {
43
+ console.error(`[migrate] config/agent.json already exists — already migrated?`);
44
+ console.error(`[migrate] Delete it manually if you want to re-run.`);
45
+ process.exit(1);
46
+ }
47
+
48
+ // Use tsx to load the existing TS module. This is the most reliable way to
49
+ // extract the data without writing a fragile parser.
50
+ let agent;
51
+ try {
52
+ const tsxBin = execFileSync("npx", ["--yes", "tsx", "-e", "console.log(0)"], {
53
+ encoding: "utf-8",
54
+ stdio: ["pipe", "pipe", "ignore"],
55
+ });
56
+ // tsx is available — load via inline eval.
57
+ const json = execFileSync(
58
+ "npx",
59
+ [
60
+ "--yes",
61
+ "tsx",
62
+ "-e",
63
+ `import("${TS_PATH}").then(m => { console.log(JSON.stringify(m.agent)); }).catch(e => { console.error(e.message); process.exit(1); });`,
64
+ ],
65
+ { encoding: "utf-8" }
66
+ );
67
+ agent = JSON.parse(json.trim());
68
+ } catch (e) {
69
+ console.error(`[migrate] failed to import config/agent.ts: ${e.message}`);
70
+ console.error(`[migrate] ensure tsx is installed (npm install --save-dev tsx) and agent.ts has a default \`export const agent = ...\``);
71
+ process.exit(1);
72
+ }
73
+
74
+ // Sanity check
75
+ if (!agent || typeof agent !== "object" || !agent.firstName) {
76
+ console.error("[migrate] extracted config doesn't look right (missing firstName)");
77
+ console.error("[migrate] received:", JSON.stringify(agent).slice(0, 200));
78
+ process.exit(1);
79
+ }
80
+
81
+ // Backup
82
+ copyFileSync(TS_PATH, BAK_PATH);
83
+ console.log(`[migrate] backed up agent.ts → ${BAK_PATH}`);
84
+
85
+ // Write canonical JSON
86
+ writeFileSync(JSON_PATH, JSON.stringify(agent, null, 2) + "\n");
87
+ console.log(`[migrate] wrote config/agent.json (canonical)`);
88
+
89
+ // Replace agent.ts with thin wrapper template (read from scaffold).
90
+ // The wrapper is shipped alongside this migration script in the maestro
91
+ // package, but we also fall back to writing it inline so the migration
92
+ // works even without access to the scaffold.
93
+ const wrapperTemplate = inlineWrapper();
94
+ writeFileSync(TS_PATH, wrapperTemplate);
95
+ console.log(`[migrate] rewrote config/agent.ts as thin JSON wrapper`);
96
+
97
+ // Generate agent.env
98
+ try {
99
+ execFileSync(
100
+ "node",
101
+ [resolve(__dirname, "generate-agent-env.mjs")],
102
+ { encoding: "utf-8", stdio: "inherit" }
103
+ );
104
+ } catch (e) {
105
+ console.warn(`[migrate] could not auto-generate agent.env: ${e.message}`);
106
+ console.warn(`[migrate] run manually: node scripts/setup/generate-agent-env.mjs`);
107
+ }
108
+
109
+ console.log();
110
+ console.log("[migrate] ✓ migration complete");
111
+ console.log(`[migrate] config/agent.json canonical identity data`);
112
+ console.log(`[migrate] config/agent.ts thin TS wrapper (imports agent.json)`);
113
+ console.log(`[migrate] config/agent.env shell-sourceable identity vars`);
114
+ console.log(`[migrate] ${BAK_PATH.split("/").slice(-2).join("/")} original (delete after verifying)`);
115
+
116
+ function inlineWrapper() {
117
+ return `/**
118
+ * Maestro — Agent Configuration (TypeScript wrapper)
119
+ *
120
+ * The canonical source of truth is \`config/agent.json\`. Edit that file,
121
+ * then run \`node scripts/setup/generate-agent-env.mjs\` to regenerate the
122
+ * shell environment file.
123
+ *
124
+ * This wrapper exposes typed access plus a few computed convenience values.
125
+ */
126
+
127
+ import agentData from './agent.json' with { type: 'json' };
128
+
129
+ export interface VoiceMode {
130
+ id: string;
131
+ label: string;
132
+ description: string;
133
+ }
134
+
135
+ export interface PrincipalConfig {
136
+ firstName: string;
137
+ lastName: string;
138
+ fullName: string;
139
+ title: string;
140
+ email: string;
141
+ slackMemberId: string;
142
+ }
143
+
144
+ export interface AgentConfig {
145
+ firstName: string;
146
+ lastName: string;
147
+ fullName: string;
148
+ title: string;
149
+ archetype: string;
150
+
151
+ email: string;
152
+ phone: string;
153
+ slackMemberId: string;
154
+
155
+ company: string;
156
+ companyDomain: string;
157
+ companyDescription: string;
158
+
159
+ principal: PrincipalConfig;
160
+
161
+ machineName: string;
162
+ repoSlug: string;
163
+ launchdLabelPrefix: string;
164
+
165
+ timezone: string;
166
+ locale: string;
167
+
168
+ schedule: {
169
+ morningBrief: string;
170
+ commsTriage: string[];
171
+ eveningWrap: string;
172
+ overnightMonitoring: string;
173
+ };
174
+
175
+ communication: {
176
+ defaultTone: string;
177
+ externalTone: string;
178
+ voiceModes: VoiceMode[];
179
+ };
180
+
181
+ responsibilities: string[];
182
+ operatingPrinciples: string[];
183
+ }
184
+
185
+ export const agent: AgentConfig = agentData as AgentConfig;
186
+
187
+ export const AGENT_UPPER = agent.firstName.toUpperCase();
188
+ export const AGENT_LOWER = agent.firstName.toLowerCase();
189
+ export const REPO_DIR = \`~/\${agent.repoSlug}\`;
190
+ export const EMAIL_SIGNATURE = \`\${agent.fullName} | \${agent.title} | \${agent.company}\`;
191
+ `;
192
+ }
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Slack Events API Server — Real-Time Event Receiver for Sophie
3
+ * Slack Events API Server — Real-Time Event Receiver
4
4
  *
5
5
  * Receives real-time events from Slack via the bot's Event Subscriptions.
6
6
  * Writes inbound events to state/inbox/slack/ in YAML format (matching poller output).
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * Events handled:
12
12
  * - message (DM, channel messages, thread replies — all channel types)
13
- * - app_mention (Sophie @mentioned)
13
+ * - app_mention (the agent @mentioned)
14
14
  * - reaction_added (emoji reactions)
15
15
  *
16
16
  * Deduplication:
@@ -72,15 +72,45 @@ const POLLER_TRACKER_DIR = path.join(ROOT, "state/slack-thread-tracker");
72
72
  const TRIGGER_DIR = path.join(ROOT, "state/triggers/priority");
73
73
  const LOG_DIR = path.join(ROOT, "logs/polling");
74
74
 
75
- const SOPHIE_USER_ID = "U099N1JFPRQ";
76
- const MEHRAN_USER_ID = "U097N5R0M7U";
77
- const MEHRAN_DM_CHANNEL = "D099N1JGKRQ";
75
+ // Load the running agent's identity (canonical SOT) so we can filter
76
+ // self-messages and resolve the principal's user ID without hardcoding.
77
+ let _agent = null;
78
+ function loadAgent() {
79
+ if (_agent) return _agent;
80
+ try {
81
+ _agent = JSON.parse(fs.readFileSync(path.join(ROOT, "config/agent.json"), "utf-8"));
82
+ } catch {
83
+ _agent = { firstName: "Agent", slackMemberId: "", fullName: "Agent", principal: {} };
84
+ }
85
+ return _agent;
86
+ }
78
87
 
79
- // Known users for name/privilege resolution (matches poller/utils.mjs)
80
- const KNOWN_USERS = {
81
- U097N5R0M7U: { name: "mehran-granfar", privilege: "ceo" },
82
- U099N1JFPRQ: { name: "sophie-nguyen", privilege: "system" },
83
- };
88
+ const AGENT_USER_ID = loadAgent().slackMemberId || "";
89
+ const PRINCIPAL_USER_ID = loadAgent().principal?.slackMemberId || "";
90
+ // DM channel between the principal and the agent — resolved at runtime from
91
+ // Slack API on first DM event. The hardcoded MEHRAN_DM_CHANNEL constant was
92
+ // removed; the server detects DM channels via the Slack channel ID prefix
93
+ // (channels starting with "D" are IM channels).
94
+
95
+ // Known users for name/privilege resolution — built from agent.json so each
96
+ // agent's principal gets `privilege: ceo` and the agent itself gets `system`.
97
+ const KNOWN_USERS = (() => {
98
+ const a = loadAgent();
99
+ const map = {};
100
+ if (a.principal?.slackMemberId) {
101
+ map[a.principal.slackMemberId] = {
102
+ name: (a.principal.fullName || "").toLowerCase().replace(/\s+/g, "-"),
103
+ privilege: "ceo",
104
+ };
105
+ }
106
+ if (a.slackMemberId) {
107
+ map[a.slackMemberId] = {
108
+ name: (a.fullName || "").toLowerCase().replace(/\s+/g, "-"),
109
+ privilege: "system",
110
+ };
111
+ }
112
+ return map;
113
+ })();
84
114
 
85
115
  // Monitored channels (matches poller/slack-poller.mjs)
86
116
  const CHANNEL_INFO = {
@@ -385,7 +415,7 @@ priority_signals:
385
415
  from_ceo: ${item.priority_signals?.from_ceo || false}
386
416
  tagged_urgent: ${item.priority_signals?.tagged_urgent || false}
387
417
  contains_deadline: ${item.priority_signals?.contains_deadline || false}
388
- mentions_sophie: ${item.priority_signals?.mentions_sophie || false}
418
+ mentions_agent: ${item.priority_signals?.mentions_agent || false}
389
419
  raw_ref: "${item.raw_ref || ""}"
390
420
  source: "events-api"
391
421
  `;
@@ -446,20 +476,24 @@ content: |
446
476
  */
447
477
  function handleMessageEvent(event) {
448
478
  const userId = event.user || "";
449
- // Skip Sophie's own messages — defensive check (outer dispatch also filters,
479
+ // Skip the agent's own messages — defensive check (outer dispatch also filters,
450
480
  // but message_changed normalization can bypass it)
451
- if (userId === SOPHIE_USER_ID) return;
481
+ if (userId === AGENT_USER_ID) return;
452
482
 
453
483
  const msgText = event.text || "";
454
484
  const channelId = event.channel || "";
455
485
  const channelType = event.channel_type || "";
456
486
  const isThread = !!event.thread_ts && event.thread_ts !== event.ts;
457
487
  const isDM = channelType === "im";
458
- const isMehran = userId === MEHRAN_USER_ID;
459
- const isMehranDmChannel = channelId === MEHRAN_DM_CHANNEL;
460
- const mentionsSophie =
461
- msgText.toLowerCase().includes("sophie") ||
462
- msgText.includes(`<@${SOPHIE_USER_ID}>`);
488
+ const isPrincipal = userId === PRINCIPAL_USER_ID;
489
+ // The principal's DM channel is whichever IM channel they happen to use to
490
+ // contact the agent — Slack assigns DM channel IDs dynamically. Detect
491
+ // principal-DM context from `isPrincipal && isDM` rather than a hardcode.
492
+ const isPrincipalDmChannel = isPrincipal && isDM;
493
+ const agentFirstName = (loadAgent().firstName || "").toLowerCase();
494
+ const mentionsAgent =
495
+ (agentFirstName && msgText.toLowerCase().includes(agentFirstName)) ||
496
+ (AGENT_USER_ID && msgText.includes(`<@${AGENT_USER_ID}>`));
463
497
  const isUrgent = /\b(urgent|emergency|asap|blocker|critical)\b/i.test(
464
498
  msgText,
465
499
  );
@@ -482,8 +516,8 @@ function handleMessageEvent(event) {
482
516
  channel: channelName,
483
517
  isDM,
484
518
  isThread,
485
- isMehran,
486
- mentionsSophie,
519
+ isPrincipal,
520
+ mentionsAgent,
487
521
  isUrgent,
488
522
  });
489
523
 
@@ -505,10 +539,10 @@ function handleMessageEvent(event) {
505
539
  is_reply: isThread,
506
540
  attachments: attachments.length > 0 ? attachments : undefined,
507
541
  priority_signals: {
508
- from_ceo: isMehran,
542
+ from_ceo: isPrincipal,
509
543
  tagged_urgent: isUrgent,
510
544
  contains_deadline: false,
511
- mentions_sophie: mentionsSophie || isDM,
545
+ mentions_agent: mentionsAgent || isDM,
512
546
  },
513
547
  raw_ref: `slack:${channelId}:${msgTs}`,
514
548
  _eventType: isThread ? "thread_reply" : isDM ? "dm" : "channel_message",
@@ -520,25 +554,25 @@ function handleMessageEvent(event) {
520
554
  markInPollerTracker(msgTs);
521
555
 
522
556
  // Write priority trigger for CEO DMs, @mentions, and urgent messages
523
- if (isMehran && (isDM || isMehranDmChannel)) {
557
+ if (isPrincipal && (isDM || isPrincipalDmChannel)) {
524
558
  writePriorityTrigger(event, "CEO DM via Events API");
525
- } else if (isMehran && !isDM) {
559
+ } else if (isPrincipal && !isDM) {
526
560
  writePriorityTrigger(event, "CEO channel message via Events API");
527
- } else if (mentionsSophie) {
528
- writePriorityTrigger(event, "Sophie @mentioned via Events API");
561
+ } else if (mentionsAgent) {
562
+ writePriorityTrigger(event, "Agent @mentioned via Events API");
529
563
  } else if (isUrgent) {
530
564
  writePriorityTrigger(event, "Urgent message via Events API");
531
565
  }
532
566
  }
533
567
 
534
568
  /**
535
- * Process an app_mention event (@Sophie or @Adaptic COO).
569
+ * Process an app_mention event (@-mention of the running agent).
536
570
  */
537
571
  function handleMentionEvent(event) {
538
572
  const msgText = event.text || "";
539
573
  const userId = event.user || "";
540
574
  const channelId = event.channel || "";
541
- const isMehran = userId === MEHRAN_USER_ID;
575
+ const isPrincipal = userId === PRINCIPAL_USER_ID;
542
576
  const isUrgent = /\b(urgent|emergency|asap|blocker|critical)\b/i.test(
543
577
  msgText,
544
578
  );
@@ -557,15 +591,15 @@ function handleMentionEvent(event) {
557
591
  sender: resolveName(userId),
558
592
  sender_privilege: resolvePrivilege(userId),
559
593
  timestamp: new Date(parseFloat(msgTs) * 1000).toISOString(),
560
- subject: `@Sophie in #${channelName}`,
594
+ subject: `@${loadAgent().firstName || "Agent"} in #${channelName}`,
561
595
  content: msgText,
562
596
  thread_id: event.thread_ts || "",
563
597
  is_reply: !!event.thread_ts && event.thread_ts !== event.ts,
564
598
  priority_signals: {
565
- from_ceo: isMehran,
599
+ from_ceo: isPrincipal,
566
600
  tagged_urgent: isUrgent,
567
601
  contains_deadline: false,
568
- mentions_sophie: true,
602
+ mentions_agent: true,
569
603
  },
570
604
  raw_ref: `slack:${channelId}:${msgTs}`,
571
605
  _eventType: "mention",
@@ -577,9 +611,9 @@ function handleMentionEvent(event) {
577
611
  // Mentions always get a priority trigger
578
612
  writePriorityTrigger(
579
613
  event,
580
- isMehran
614
+ isPrincipal
581
615
  ? "CEO @mention via Events API"
582
- : "Sophie @mentioned via Events API",
616
+ : "Agent @mentioned via Events API",
583
617
  );
584
618
  }
585
619
 
@@ -591,7 +625,7 @@ function handleReactionEvent(event) {
591
625
  const reaction = event.reaction || "";
592
626
  const itemTs = event.item?.ts || "";
593
627
  const itemChannel = event.item?.channel || "";
594
- const isMehran = userId === MEHRAN_USER_ID;
628
+ const isPrincipal = userId === PRINCIPAL_USER_ID;
595
629
  const channelName = resolveChannelName(itemChannel, "channel");
596
630
 
597
631
  log("INFO", `Reaction event: :${reaction}: from ${resolveName(userId)}`, {
@@ -612,10 +646,10 @@ function handleReactionEvent(event) {
612
646
  thread_id: "",
613
647
  is_reply: false,
614
648
  priority_signals: {
615
- from_ceo: isMehran,
649
+ from_ceo: isPrincipal,
616
650
  tagged_urgent: false,
617
651
  contains_deadline: false,
618
- mentions_sophie: false,
652
+ mentions_agent: false,
619
653
  },
620
654
  raw_ref: `slack:${itemChannel}:${itemTs}:reaction:${reaction}`,
621
655
  _eventType: "reaction",
@@ -634,8 +668,8 @@ function handleHuddleEvent(event) {
634
668
  const userId = event.user || "";
635
669
  const huddleState = event.huddle_state || "";
636
670
 
637
- // Don't react to Sophie's own huddle changes
638
- if (userId === SOPHIE_USER_ID) return;
671
+ // Don't react to the agent's own huddle changes
672
+ if (userId === AGENT_USER_ID) return;
639
673
 
640
674
  const userName = resolveName(userId);
641
675
 
@@ -690,7 +724,7 @@ function handleEvent(payload) {
690
724
  stats.events_received++;
691
725
  stats.last_event_at = new Date().toISOString();
692
726
 
693
- // Skip bot messages (including Sophie's own messages)
727
+ // Skip bot messages (including the agent's own messages)
694
728
  if (event.bot_id || event.subtype === "bot_message") {
695
729
  stats.events_skipped_bot++;
696
730
  log("DEBUG", `Bot message skipped`, {
@@ -701,8 +735,8 @@ function handleEvent(payload) {
701
735
  return;
702
736
  }
703
737
 
704
- // Skip Sophie's own messages
705
- if (event.user === SOPHIE_USER_ID) {
738
+ // Skip the agent's own messages
739
+ if (event.user === AGENT_USER_ID) {
706
740
  stats.events_skipped_bot++;
707
741
  markProcessed(eventId);
708
742
  return;
@@ -743,9 +777,9 @@ function handleEvent(payload) {
743
777
 
744
778
  // Re-check after normalization: the original event.user for
745
779
  // message_changed is often empty/system; the real author is in
746
- // event.message.user. Without this, Sophie's own edited messages
747
- // bypass the earlier SOPHIE_USER_ID filter and trigger self-replies.
748
- if (event.user === SOPHIE_USER_ID) {
780
+ // event.message.user. Without this, the agent's own edited messages
781
+ // bypass the earlier AGENT_USER_ID filter and trigger self-replies.
782
+ if (event.user === AGENT_USER_ID) {
749
783
  stats.events_skipped_bot++;
750
784
  markProcessed(eventId);
751
785
  return;
@@ -24,8 +24,11 @@
24
24
  # → purge entries older than 24h
25
25
  set -e
26
26
 
27
- LOCK_BASE_DIR="/Users/sophie/sophie-ai/state/locks/slack-response"
28
- LEGACY_DIR="/Users/sophie/sophie-ai/state/slack-responded"
27
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
28
+ AGENT_REPO_DIR="${AGENT_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
29
+
30
+ LOCK_BASE_DIR="$AGENT_REPO_DIR/state/locks/slack-response"
31
+ LEGACY_DIR="$AGENT_REPO_DIR/state/slack-responded"
29
32
  LOCK_TTL_MINUTES=1440 # 24 hours — thread-level dedup needs long TTL to prevent cross-session duplicates
30
33
 
31
34
  mkdir -p "$LOCK_BASE_DIR"