@adaptic/maestro 1.8.2 → 1.8.4

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 (55) hide show
  1. package/bin/maestro.mjs +15 -3
  2. package/package.json +1 -1
  3. package/plugins/maestro-skills/skills/board-deck.md +2 -2
  4. package/plugins/maestro-skills/skills/decision-brief.md +6 -6
  5. package/plugins/maestro-skills/skills/draft-comms.md +9 -9
  6. package/plugins/maestro-skills/skills/evening-wrap.md +2 -2
  7. package/plugins/maestro-skills/skills/hiring-triage.md +4 -4
  8. package/plugins/maestro-skills/skills/inbox-triage.md +5 -5
  9. package/plugins/maestro-skills/skills/morning-brief.md +4 -4
  10. package/plugins/maestro-skills/skills/pipeline-review.md +2 -2
  11. package/plugins/maestro-skills/skills/regulatory-status.md +2 -2
  12. package/plugins/maestro-skills/skills/schedule-meeting.md +3 -3
  13. package/plugins/maestro-skills/skills/slack-followup.md +5 -5
  14. package/plugins/maestro-skills/skills/weekly-memo.md +5 -5
  15. package/scaffold/CLAUDE.md +21 -0
  16. package/scripts/daemon/classifier.mjs +21 -5
  17. package/scripts/hooks/block-mcp-slack-send.sh +1 -1
  18. package/scripts/huddle/audio-bridge.mjs +17 -17
  19. package/scripts/huddle/boot-slack-cdp.sh +1 -1
  20. package/scripts/huddle/huddle-controller.mjs +3 -3
  21. package/scripts/huddle/huddle-server.mjs +21 -7
  22. package/scripts/huddle/launch-slack.sh +2 -2
  23. package/scripts/huddle/package-lock.json +2 -2
  24. package/scripts/huddle/package.json +2 -2
  25. package/scripts/huddle/setup-audio.sh +6 -6
  26. package/scripts/huddle/start-call.mjs +2 -2
  27. package/scripts/huddle/test-pipeline.mjs +2 -2
  28. package/scripts/parse-voice-transcript.mjs +4 -9
  29. package/scripts/poller/gmail-poller.mjs +8 -2
  30. package/scripts/poller/intra-session-check.mjs +4 -3
  31. package/scripts/poller-launchd/install.sh +48 -10
  32. package/scripts/pre_draft_lookup.py +2 -2
  33. package/scripts/self-optimization/compute-metrics.py +23 -2
  34. package/scripts/setup/boot-claude-session.sh +14 -5
  35. package/scripts/setup/render-environment-yaml.mjs +133 -0
  36. package/scripts/watchdog/ai.maestro.memory-watchdog.plist +3 -3
  37. package/scripts/watchdog/force-reboot.sh +3 -3
  38. package/scripts/watchdog/memory-watchdog.sh +11 -5
  39. package/workflows/continuous/backlog-executor.yaml +1 -1
  40. package/workflows/continuous/inbound-monitor.yaml +10 -10
  41. package/workflows/daily/applicant-triage.yaml +4 -4
  42. package/workflows/daily/comms-triage.yaml +3 -3
  43. package/workflows/daily/evening-wrap.yaml +1 -1
  44. package/workflows/daily/morning-brief.yaml +2 -2
  45. package/workflows/daily/slack-followup-sweep.yaml +3 -3
  46. package/workflows/event-driven/README.md +5 -5
  47. package/workflows/event-driven/agent-failure-investigation.yaml +1 -1
  48. package/workflows/event-driven/pr-review.yaml +6 -3
  49. package/workflows/monthly/board-readiness.yaml +1 -1
  50. package/workflows/quarterly/strategic-scenario-analysis.yaml +1 -1
  51. package/workflows/session-protocol.md +7 -7
  52. package/workflows/weekly/engineering-health.yaml +1 -1
  53. package/workflows/weekly/hiring-review.yaml +4 -4
  54. package/workflows/weekly/rollup-pipeline-review.yaml +1 -1
  55. package/workflows/weekly/strategic-memo.yaml +1 -1
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Sophie Huddle — Controller
3
+ * Maestro Huddle — Controller
4
4
  *
5
5
  * Controls the Slack desktop app via Chrome DevTools Protocol for huddle
6
6
  * participation. Connects to Slack launched with --remote-debugging-port
@@ -430,7 +430,7 @@ export class HuddleController extends EventEmitter {
430
430
  }
431
431
 
432
432
  /**
433
- * Mute/unmute Sophie's microphone in the huddle.
433
+ * Mute/unmute the agent's microphone in the huddle.
434
434
  * @param {boolean} muted - true to mute, false to unmute
435
435
  */
436
436
  async setMute(muted) {
@@ -449,7 +449,7 @@ export class HuddleController extends EventEmitter {
449
449
  // -------------------------------------------------------------------------
450
450
 
451
451
  /**
452
- * Check if Sophie is currently in a huddle.
452
+ * Check if the agent is currently in a huddle.
453
453
  * @returns {Promise<boolean>}
454
454
  */
455
455
  async isInHuddle() {
@@ -17,7 +17,7 @@
17
17
  * Usage:
18
18
  * node huddle-server.mjs # Start and listen for huddles
19
19
  * node huddle-server.mjs --join #general # Join huddle in #general
20
- * node huddle-server.mjs --call mehran # Initiate huddle with Mehran
20
+ * node huddle-server.mjs --call principal # Initiate huddle with the agent's principal
21
21
  */
22
22
 
23
23
  import dotenv from "dotenv";
@@ -635,14 +635,28 @@ class HuddleServer extends EventEmitter {
635
635
  * Used for access control (CEO gets write tools) and personalized greetings.
636
636
  */
637
637
  _resolveHuddleParticipant(identifier) {
638
- // Known mappings channel names and person names to access levels
638
+ // Seed `knownParticipants` with the agent's PRINCIPAL from
639
+ // config/agent.json (SOT) — replaces the previously-hardcoded "mehran"
640
+ // entry. Falls back to common operational handles for back-compat.
639
641
  const knownParticipants = {
640
- "mehran": { slug: "mehran-granfar", name: "Mehran Granfar", accessLevel: "ceo" },
641
- "mehran-granfar": { slug: "mehran-granfar", name: "Mehran Granfar", accessLevel: "ceo" },
642
- "hootan": { slug: "hootan-yazhari", name: "Hootan Yazhari", accessLevel: "leadership" },
643
- "nima": { slug: "nima-masroori", name: "Nima Masroori", accessLevel: "leadership" },
644
642
  "#ceo-office": { slug: "ceo-office", name: "CEO Office Channel", accessLevel: "ceo" },
645
643
  };
644
+ try {
645
+ const agentPath = join(AGENT_REPO_DIR, "config/agent.json");
646
+ const agentJson = JSON.parse(readFileSync(agentPath, "utf-8"));
647
+ const principal = agentJson?.principal;
648
+ if (principal?.firstName) {
649
+ const slug = `${principal.firstName}-${principal.lastName || ""}`.toLowerCase().replace(/\s+/g, "-").replace(/-+$/, "");
650
+ const entry = {
651
+ slug,
652
+ name: principal.fullName || `${principal.firstName} ${principal.lastName || ""}`.trim(),
653
+ accessLevel: "ceo",
654
+ };
655
+ knownParticipants[principal.firstName.toLowerCase()] = entry;
656
+ knownParticipants[slug] = entry;
657
+ knownParticipants["principal"] = entry;
658
+ }
659
+ } catch { /* agent.json unavailable — only #ceo-office remains */ }
646
660
 
647
661
  const key = (identifier || "").toLowerCase().replace(/^#/, "");
648
662
  const match = knownParticipants[key];
@@ -1015,7 +1029,7 @@ PERSONALITY:
1015
1029
  ABOUT ADAPTIC:
1016
1030
  - Adaptic is a global AI-native institutional asset management group
1017
1031
  - Headquartered in DIFC, Dubai with entities across seven plus jurisdictions
1018
- - CEO and founder: Mehran Granfar
1032
+ - CEO and founder: the principal (resolved from config/agent.json)
1019
1033
  - Building Adaptic OS, an algorithmic trading platform
1020
1034
  - Currently in regulatory licensing phase
1021
1035
 
@@ -2,7 +2,7 @@
2
2
  set -euo pipefail
3
3
 
4
4
  # ---------------------------------------------------------------------------
5
- # Sophie Huddle — Slack Launcher with CDP
5
+ # Maestro Huddle — Slack Launcher with CDP
6
6
  #
7
7
  # Launches (or relaunches) the Slack desktop app with Chrome DevTools Protocol
8
8
  # enabled on port 9222. This allows Playwright/Puppeteer to connect and
@@ -207,7 +207,7 @@ show_status() {
207
207
  main() {
208
208
  log ""
209
209
  log "=========================================="
210
- log " Sophie Huddle — Slack Launcher"
210
+ log " Maestro Huddle — Slack Launcher"
211
211
  log " $(date)"
212
212
  log "=========================================="
213
213
 
@@ -1,11 +1,11 @@
1
1
  {
2
- "name": "sophie-huddle",
2
+ "name": "maestro-huddle",
3
3
  "version": "1.0.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
- "name": "sophie-huddle",
8
+ "name": "maestro-huddle",
9
9
  "version": "1.0.0",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.52.0",
@@ -1,7 +1,7 @@
1
1
  {
2
- "name": "sophie-huddle",
2
+ "name": "maestro-huddle",
3
3
  "version": "1.0.0",
4
- "description": "Sophie Nguyen — Slack Huddle Voice Participation",
4
+ "description": "Maestro — Slack huddle voice participation (agent-neutral)",
5
5
  "main": "huddle-server.mjs",
6
6
  "type": "module",
7
7
  "scripts": {
@@ -2,13 +2,13 @@
2
2
  set -euo pipefail
3
3
 
4
4
  # ---------------------------------------------------------------------------
5
- # Sophie Huddle — Audio Setup
5
+ # Maestro Huddle — Audio Setup
6
6
  #
7
7
  # Installs and configures BlackHole virtual audio devices for Slack huddle
8
8
  # participation. Two separate virtual devices prevent feedback loops:
9
9
  #
10
10
  # BlackHole 2ch → Slack's Speaker output (captures what others say)
11
- # BlackHole 16ch → Slack's Mic input (carries Sophie's synthesized voice)
11
+ # BlackHole 16ch → Slack's Mic input (carries the agent's synthesized voice)
12
12
  #
13
13
  # Usage:
14
14
  # ./setup-audio.sh # Full install + verify
@@ -81,7 +81,7 @@ install_blackhole_16ch() {
81
81
  return 0
82
82
  fi
83
83
 
84
- log "Installing BlackHole 16ch (Sophie mic input)..."
84
+ log "Installing BlackHole 16ch (agent mic input)..."
85
85
  brew install blackhole-16ch
86
86
 
87
87
  sleep 2
@@ -144,7 +144,7 @@ verify_audio_devices() {
144
144
  log " Speaker: BlackHole 2ch"
145
145
  log " Microphone: BlackHole 16ch"
146
146
  log ""
147
- log "This routes huddle audio through Sophie's capture pipeline."
147
+ log "This routes huddle audio through the agent's capture pipeline."
148
148
  log "System audio remains on built-in devices (no disruption)."
149
149
  return 0
150
150
  else
@@ -163,7 +163,7 @@ test_audio_capture() {
163
163
  log ""
164
164
  log "Recording 3 seconds from BlackHole 2ch..."
165
165
 
166
- local test_file="/tmp/sophie-audio-test-$(date +%s).wav"
166
+ local test_file="/tmp/agent-audio-test-$(date +%s).wav"
167
167
 
168
168
  # Record 3 seconds from BlackHole 2ch
169
169
  if sox -t coreaudio "BlackHole 2ch" -r 16000 -c 1 -b 16 "$test_file" trim 0 3 2>/dev/null; then
@@ -207,7 +207,7 @@ test_audio_playback() {
207
207
  main() {
208
208
  log ""
209
209
  log "=========================================="
210
- log " Sophie Huddle — Audio Setup"
210
+ log " Maestro Huddle — Audio Setup"
211
211
  log " $(date)"
212
212
  log "=========================================="
213
213
  log ""
@@ -8,7 +8,7 @@
8
8
  * 3. Launch the huddle server to connect audio bridge
9
9
  *
10
10
  * Usage:
11
- * node start-call.mjs mehran
11
+ * node start-call.mjs principal
12
12
  * node start-call.mjs hootan
13
13
  */
14
14
 
@@ -24,7 +24,7 @@ dotenv.config({ path: join(__dirname, "../..", ".env") });
24
24
 
25
25
  const execFileAsync = promisify(execFile);
26
26
  const CDP_URL = "http://localhost:9222";
27
- const target = process.argv[2] || "mehran";
27
+ const target = process.argv[2] || "principal";
28
28
 
29
29
  // ---------------------------------------------------------------------------
30
30
  // Helper: send CDP command
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Sophie Huddle — Pipeline Test
3
+ * Maestro Huddle — Pipeline Test
4
4
  *
5
5
  * Verifies each component of the huddle pipeline:
6
6
  * 1. CDP connection to Slack
@@ -38,7 +38,7 @@ function skip(name, detail = "") {
38
38
 
39
39
  async function main() {
40
40
  console.log("\n==========================================");
41
- console.log(" Sophie Huddle — Pipeline Test");
41
+ console.log(" Maestro Huddle — Pipeline Test");
42
42
  console.log(" " + new Date().toISOString());
43
43
  console.log("==========================================\n");
44
44
 
@@ -31,17 +31,12 @@
31
31
  */
32
32
 
33
33
  // ---------------------------------------------------------------------------
34
- // Known callers — maps phone numbers to identities
35
- // Keep in sync with voice-ai/server.js CALLER_GREETINGS
34
+ // Known callers — phone numbers to identities. Loaded from
35
+ // config/caller-id-map.yaml at runtime; falls back to an empty map. Keep
36
+ // caller-id-map.yaml in sync with voice-ai/server.js CALLER_GREETINGS.
36
37
  // ---------------------------------------------------------------------------
37
38
 
38
- const KNOWN_CALLERS = {
39
- "+971585291799": {
40
- name: "mehran-granfar",
41
- role: "ceo",
42
- privilege: "ceo",
43
- },
44
- };
39
+ const KNOWN_CALLERS = {}; // populated lazily from config/caller-id-map.yaml on first use
45
40
 
46
41
  // ---------------------------------------------------------------------------
47
42
  // Action item detection patterns
@@ -62,11 +62,17 @@ function extractSenderEmail(from) {
62
62
  }
63
63
 
64
64
  /**
65
- * Determine sender privilege from email address.
65
+ * Determine sender privilege from email address. The principal (CEO) is
66
+ * recognised by matching against the agent's `principal.email` or
67
+ * `principal.firstName` from config/agent.json — never hardcode names.
66
68
  */
67
69
  function resolveSenderPrivilege(email) {
68
70
  const lower = email.toLowerCase();
69
- if (lower.includes("mehran")) return "ceo";
71
+ const me = loadAgent();
72
+ const principalEmail = (me.principal?.email || "").toLowerCase();
73
+ const principalFirst = (me.principal?.firstName || "").toLowerCase();
74
+ if (principalEmail && lower === principalEmail) return "ceo";
75
+ if (principalFirst && lower.includes(principalFirst)) return "ceo";
70
76
  if (lower.includes("adaptic.ai")) return "internal";
71
77
  return "external";
72
78
  }
@@ -167,15 +167,16 @@ async function checkCriticalChannels() {
167
167
  : false,
168
168
  });
169
169
 
170
- // If CEO posted in a critical channel, also create a trigger
170
+ // If the principal posted in a critical channel, also create a trigger.
171
171
  const ceoMsgs = relevant.filter((m) => m.user === CEO_USER_ID);
172
172
  if (ceoMsgs.length > 0) {
173
+ const principalSlug = (_agent.principal?.fullName || "principal").toLowerCase().replace(/\s+/g, "-");
173
174
  writePriorityTrigger(
174
175
  `slack:${ch.id}:${ceoMsgs[0].ts}`,
175
- "mehran-granfar",
176
+ principalSlug,
176
177
  ch.name,
177
178
  ceoMsgs[0].text,
178
- `CEO message in #${ch.name} during intra-session poll`,
179
+ `Principal message in #${ch.name} during intra-session poll`,
179
180
  );
180
181
  }
181
182
  }
@@ -1,27 +1,65 @@
1
1
  #!/bin/bash
2
- # Install the Sophie poller as a macOS launchd daemon
2
+ # Install the agent's poller as a macOS launchd daemon
3
3
  # Usage: ./scripts/poller-launchd/install.sh
4
+ #
5
+ # Agent identity is read from config/agent.json (SOT). The first-name slug
6
+ # (lowercase) is used to build the launchd Label so multiple agents can run
7
+ # their poller daemons side by side on the same Mac.
4
8
 
5
9
  set -e
6
10
 
7
- PLIST_NAME="ai.adaptic.sophie-poller"
8
- PLIST_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$PLIST_NAME.plist"
11
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12
+ AGENT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
13
+ AGENT_JSON="$AGENT_DIR/config/agent.json"
14
+
15
+ # Resolve the agent's first name from the SOT. Prefer jq, fall back to awk.
16
+ AGENT_FIRST=""
17
+ if [ -f "$AGENT_JSON" ]; then
18
+ if command -v jq >/dev/null 2>&1; then
19
+ AGENT_FIRST=$(jq -r '.firstName // empty' "$AGENT_JSON" | tr '[:upper:]' '[:lower:]')
20
+ else
21
+ AGENT_FIRST=$(awk -F'"' '/"firstName"[[:space:]]*:/ { print tolower($4); exit }' "$AGENT_JSON")
22
+ fi
23
+ fi
24
+ # Fallback chain: directory name (strip -ai suffix) → "agent".
25
+ if [ -z "$AGENT_FIRST" ]; then
26
+ AGENT_FIRST=$(basename "$AGENT_DIR" | sed 's/-ai$//')
27
+ fi
28
+ [ -z "$AGENT_FIRST" ] && AGENT_FIRST="agent"
29
+
30
+ PLIST_NAME="ai.adaptic.${AGENT_FIRST}-poller"
31
+ PLIST_SRC="$SCRIPT_DIR/$PLIST_NAME.plist"
9
32
  PLIST_DST="$HOME/Library/LaunchAgents/$PLIST_NAME.plist"
10
33
 
11
- echo "Installing Sophie poller daemon..."
34
+ # Back-compat: the canonical plist on disk is still ai.adaptic.sophie-poller
35
+ # until the per-agent generator catches up. If a $AGENT_FIRST-poller plist is
36
+ # absent, fall back to the sophie one so the installer still works during
37
+ # the rename transition.
38
+ if [ ! -f "$PLIST_SRC" ]; then
39
+ SOPHIE_PLIST="$SCRIPT_DIR/ai.adaptic.sophie-poller.plist"
40
+ if [ -f "$SOPHIE_PLIST" ]; then
41
+ PLIST_SRC="$SOPHIE_PLIST"
42
+ echo "[install] using sophie poller plist as template (no ${AGENT_FIRST}-specific plist found)"
43
+ else
44
+ echo "[install] FATAL: no poller plist template found at $PLIST_SRC" >&2
45
+ exit 1
46
+ fi
47
+ fi
48
+
49
+ echo "Installing ${AGENT_FIRST} poller daemon..."
12
50
 
13
- # Copy plist to LaunchAgents
51
+ # Copy plist to LaunchAgents and rewrite its Label so the installed copy
52
+ # matches the agent's identity (handles the sophie-fallback case above).
14
53
  cp "$PLIST_SRC" "$PLIST_DST"
54
+ sed -i '' "s|<string>ai.adaptic.[a-z][a-z0-9-]*-poller</string>|<string>$PLIST_NAME</string>|g" "$PLIST_DST"
15
55
 
16
56
  # Update node path to actual location
17
57
  NODE_PATH=$(which node)
18
58
  sed -i '' "s|/usr/local/bin/node|$NODE_PATH|g" "$PLIST_DST"
19
59
 
20
60
  # Inject environment variables from .env if it exists
21
- SOPHIE_AI_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
22
- ENV_FILE="$SOPHIE_AI_DIR/.env"
61
+ ENV_FILE="$AGENT_DIR/.env"
23
62
  if [ -f "$ENV_FILE" ]; then
24
- # Read SLACK_USER_TOKEN from .env and inject into plist
25
63
  SLACK_TOKEN=$(grep '^SLACK_USER_TOKEN=' "$ENV_FILE" | cut -d= -f2-)
26
64
  if [ -n "$SLACK_TOKEN" ]; then
27
65
  sed -i '' "s|<string>production</string>|<string>production</string>\n <key>SLACK_USER_TOKEN</key>\n <string>$SLACK_TOKEN</string>|g" "$PLIST_DST"
@@ -33,6 +71,6 @@ fi
33
71
  launchctl unload "$PLIST_DST" 2>/dev/null || true
34
72
  launchctl load "$PLIST_DST"
35
73
 
36
- echo "Sophie poller daemon installed and running."
37
- echo "Check status: launchctl list | grep sophie"
74
+ echo "${AGENT_FIRST} poller daemon installed and running."
75
+ echo "Check status: launchctl list | grep ${AGENT_FIRST}"
38
76
  echo "Uninstall: launchctl unload $PLIST_DST && rm $PLIST_DST"
@@ -11,10 +11,10 @@ logs the error and returns None — never blocks a send.
11
11
 
12
12
  Usage (Python import):
13
13
  from pre_draft_lookup import pre_draft_lookup
14
- context = pre_draft_lookup("Mehran Granfar", message_type="slack")
14
+ context = pre_draft_lookup("<recipient name>", message_type="slack")
15
15
 
16
16
  Usage (CLI — for shell scripts):
17
- python3 scripts/pre_draft_lookup.py --recipient "Mehran Granfar" --type slack --channel C099ABC
17
+ python3 scripts/pre_draft_lookup.py --recipient "<recipient name>" --type slack --channel C099ABC
18
18
 
19
19
  Created: 2026-04-04 — Memory Enhancement Phase 7
20
20
  """
@@ -33,6 +33,26 @@ from collections import defaultdict
33
33
  REPO_ROOT = Path(__file__).resolve().parent.parent.parent
34
34
 
35
35
 
36
+ def _load_principal_slug():
37
+ """Load the principal's slug from config/agent.json (SOT).
38
+
39
+ Falls back to "principal" if agent.json is unavailable; metrics tagged
40
+ with the literal string "principal" then no-op gracefully rather than
41
+ being matched against a hardcoded name.
42
+ """
43
+ try:
44
+ with open(REPO_ROOT / "config" / "agent.json") as fh:
45
+ agent = json.load(fh)
46
+ except Exception:
47
+ return "principal"
48
+ p = agent.get("principal") or {}
49
+ full = p.get("fullName") or f"{p.get('firstName', '')} {p.get('lastName', '')}".strip()
50
+ return full.lower().replace(" ", "-") if full else "principal"
51
+
52
+
53
+ PRINCIPAL_SLUG = _load_principal_slug()
54
+
55
+
36
56
  def parse_jsonl(filepath):
37
57
  """Parse a JSONL file, skipping malformed lines."""
38
58
  entries = []
@@ -216,8 +236,9 @@ def compute_metrics(date_str):
216
236
 
217
237
  # --- CEO Satisfaction ---
218
238
 
219
- # ceo_correction_rate & ceo_positive_signal_rate: from interaction patterns
220
- ceo_responses = [r for r in responses if r.get('sender') == 'mehran-granfar']
239
+ # ceo_correction_rate & ceo_positive_signal_rate: from interaction patterns.
240
+ # Principal slug resolved from config/agent.json (SOT) never hardcoded.
241
+ ceo_responses = [r for r in responses if r.get('sender') == PRINCIPAL_SLUG]
221
242
  metrics['ceo_correction_rate'] = {
222
243
  'value': len(ceo_responses),
223
244
  'unit': 'ceo_interactions',
@@ -6,12 +6,12 @@
6
6
  # Opens Terminal.app with a Claude Code interactive session in the agent's
7
7
  # working directory. Called via launchd at login.
8
8
  #
9
- # This is separate from the sophie-daemon (which runs headless as a Node.js
9
+ # This is separate from the maestro daemon (which runs headless as a Node.js
10
10
  # process for polling/dispatching). This script provides a visible, interactive
11
11
  # Claude Code session that the operator can observe and interact with.
12
12
  #
13
13
  # Usage:
14
- # ./scripts/setup/boot-claude-session.sh [agent-dir]
14
+ # ./scripts/setup/boot-claude-session.sh <agent-dir>
15
15
  #
16
16
  # =============================================================================
17
17
 
@@ -20,14 +20,23 @@ set -euo pipefail
20
20
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
21
  MAESTRO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
22
22
 
23
- # Detect agent directory prefer sophie-ai if it exists
24
- AGENT_DIR="${1:?Agent directory required as first argument (e.g. ~/sophie-ai)}"
23
+ # Agent directory required as first argument (e.g. ~/<firstname>-ai).
24
+ AGENT_DIR="${1:?Agent directory required as first argument (e.g. ~/<firstname>-ai)}"
25
25
  if [ ! -d "$AGENT_DIR" ]; then
26
26
  echo "ERROR: Agent directory does not exist: $AGENT_DIR" >&2
27
27
  exit 1
28
28
  fi
29
29
 
30
30
  AGENT_NAME=$(basename "$AGENT_DIR")
31
+ # Resolve the agent's display name from config/agent.json (SOT), falling
32
+ # back to the directory basename. Used for the Terminal window banner.
33
+ AGENT_DISPLAY="$AGENT_NAME"
34
+ if [ -f "$AGENT_DIR/config/agent.json" ]; then
35
+ if command -v jq >/dev/null 2>&1; then
36
+ fn=$(jq -r '.fullName // empty' "$AGENT_DIR/config/agent.json")
37
+ [ -n "$fn" ] && AGENT_DISPLAY="$fn"
38
+ fi
39
+ fi
31
40
  LOG_FILE="$MAESTRO_DIR/logs/watchdog/$(date +%Y-%m-%d)-boot-session.log"
32
41
  mkdir -p "$(dirname "$LOG_FILE")"
33
42
 
@@ -76,7 +85,7 @@ osascript <<APPLESCRIPT
76
85
  tell application "Terminal"
77
86
  activate
78
87
  -- Open a new window with claude running in the agent directory
79
- do script "cd '$AGENT_DIR' && clear && echo '╔══════════════════════════════════════════════════════════╗' && echo '║ Sophie AI — Boot Session ($(date +%Y-%m-%d))' && echo '╠══════════════════════════════════════════════════════════╣' && echo '║ Agent dir: $AGENT_DIR' && echo '║ Claude: $CLAUDE_PATH' && echo '╚══════════════════════════════════════════════════════════╝' && echo '' && '$CLAUDE_PATH'"
88
+ do script "cd '$AGENT_DIR' && clear && echo '╔══════════════════════════════════════════════════════════╗' && echo '║ $AGENT_DISPLAY — Boot Session ($(date +%Y-%m-%d))' && echo '╠══════════════════════════════════════════════════════════╣' && echo '║ Agent dir: $AGENT_DIR' && echo '║ Claude: $CLAUDE_PATH' && echo '╚══════════════════════════════════════════════════════════╝' && echo '' && '$CLAUDE_PATH'"
80
89
  -- Set the window title
81
90
  set custom title of front window to "$AGENT_NAME — Claude Code"
82
91
  end tell
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * render-environment-yaml.mjs
4
+ *
5
+ * Derive `config/environment.yaml` from `config/agent.json` (the SOT).
6
+ * Replaces the previously-static, agent-name-hardcoded environment.yaml so
7
+ * that operator_persona / operator_role / ceo / hostname / paths / agent
8
+ * blocks always reflect what agent.json says.
9
+ *
10
+ * Called by `maestro upgrade` (post-merge) and safe to run by hand:
11
+ *
12
+ * node scripts/setup/render-environment-yaml.mjs # write
13
+ * node scripts/setup/render-environment-yaml.mjs --dry-run # preview
14
+ * node scripts/setup/render-environment-yaml.mjs --check # exit 1 if drift
15
+ *
16
+ * Output is canonical (deterministic). If the existing file already matches,
17
+ * nothing is written. The first 5 lines of the rendered file always include
18
+ * a `# regenerated-from: config/agent.json` banner so operators editing it
19
+ * directly know they're editing a derived file.
20
+ */
21
+
22
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
23
+ import { resolve, join } from "node:path";
24
+
25
+ const AGENT_DIR = process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd();
26
+ const AGENT_JSON = join(AGENT_DIR, "config", "agent.json");
27
+ const TARGET = join(AGENT_DIR, "config", "environment.yaml");
28
+
29
+ function fail(msg) {
30
+ process.stderr.write(`[render-environment-yaml] ${msg}\n`);
31
+ process.exit(1);
32
+ }
33
+
34
+ if (!existsSync(AGENT_JSON)) {
35
+ fail(`config/agent.json not found at ${AGENT_JSON} — nothing to derive from.`);
36
+ }
37
+
38
+ let agent;
39
+ try {
40
+ agent = JSON.parse(readFileSync(AGENT_JSON, "utf-8"));
41
+ } catch (err) {
42
+ fail(`failed to parse agent.json: ${err.message}`);
43
+ }
44
+
45
+ // Render a stable, predictable YAML. We don't use a YAML library to keep
46
+ // this script dep-free and to control the exact formatting & comment
47
+ // placement. Mind the trailing newline on every value.
48
+ function quote(s) {
49
+ if (typeof s !== "string") return `"${s ?? ""}"`;
50
+ // No double-quotes in agent identity strings expected; if any appear,
51
+ // escape them properly.
52
+ return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
53
+ }
54
+
55
+ function yamlValue(v) {
56
+ if (v === null || v === undefined) return '""';
57
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
58
+ return quote(String(v));
59
+ }
60
+
61
+ function render(agent) {
62
+ const repoSlug = agent.repoSlug || `${(agent.firstName || "agent").toLowerCase()}-ai`;
63
+ const home = `~/${repoSlug}`;
64
+ const lines = [
65
+ "# Environment Configuration",
66
+ "# regenerated-from: config/agent.json by scripts/setup/render-environment-yaml.mjs",
67
+ "# DO NOT EDIT BY HAND — changes will be overwritten on every upgrade.",
68
+ "# Edit config/agent.json (the SOT) and re-run the renderer instead.",
69
+ "",
70
+ "system:",
71
+ ` name: maestro`,
72
+ ` version: 1.0.0`,
73
+ ` operator_persona: ${yamlValue(agent.fullName)}`,
74
+ ` operator_role: ${yamlValue(agent.title)}`,
75
+ ` company: ${yamlValue(agent.company)}`,
76
+ ` ceo: ${yamlValue(agent.principal?.fullName || "")}`,
77
+ ` timezone: ${yamlValue(agent.timezone || "UTC")}`,
78
+ "",
79
+ "machine:",
80
+ ` type: mac-mini`,
81
+ ` os: macOS`,
82
+ ` hostname: ${yamlValue(agent.machineName || "")}`,
83
+ ` purpose: Autonomous agent operations node`,
84
+ "",
85
+ "paths:",
86
+ ` agent_home: ${home}`,
87
+ ` logs: ${home}/logs`,
88
+ ` outputs: ${home}/outputs`,
89
+ "",
90
+ "agent:",
91
+ ` name: ${yamlValue(agent.fullName)}`,
92
+ ` email: ${yamlValue(agent.email)}`,
93
+ ` phone: ${yamlValue(agent.phone || "")}`,
94
+ "",
95
+ ];
96
+ return lines.join("\n");
97
+ }
98
+
99
+ const rendered = render(agent);
100
+ const args = process.argv.slice(2);
101
+ const dryRun = args.includes("--dry-run") || args.includes("-n");
102
+ const check = args.includes("--check");
103
+
104
+ if (check) {
105
+ if (!existsSync(TARGET)) {
106
+ process.stdout.write("config/environment.yaml missing\n");
107
+ process.exit(1);
108
+ }
109
+ const current = readFileSync(TARGET, "utf-8");
110
+ if (current === rendered) {
111
+ process.stdout.write("environment.yaml is up to date with agent.json\n");
112
+ process.exit(0);
113
+ }
114
+ process.stdout.write("environment.yaml drifts from agent.json — rerun the renderer\n");
115
+ process.exit(1);
116
+ }
117
+
118
+ if (dryRun) {
119
+ process.stdout.write(rendered);
120
+ process.exit(0);
121
+ }
122
+
123
+ // Skip the write if already up to date — keeps `upgrade` idempotent.
124
+ if (existsSync(TARGET)) {
125
+ const current = readFileSync(TARGET, "utf-8");
126
+ if (current === rendered) {
127
+ process.stdout.write("environment.yaml already up to date\n");
128
+ process.exit(0);
129
+ }
130
+ }
131
+
132
+ writeFileSync(TARGET, rendered);
133
+ process.stdout.write(`environment.yaml regenerated from agent.json (${rendered.split("\n").length} lines)\n`);
@@ -8,7 +8,7 @@
8
8
  <key>ProgramArguments</key>
9
9
  <array>
10
10
  <string>/bin/bash</string>
11
- <string>/Users/sophie/maestro/scripts/watchdog/memory-watchdog.sh</string>
11
+ <string>__AGENT_DIR__/scripts/watchdog/memory-watchdog.sh</string>
12
12
  </array>
13
13
 
14
14
  <key>StartInterval</key>
@@ -18,10 +18,10 @@
18
18
  <true/>
19
19
 
20
20
  <key>StandardOutPath</key>
21
- <string>/Users/sophie/maestro/logs/watchdog/launchd-stdout.log</string>
21
+ <string>__AGENT_DIR__/logs/watchdog/launchd-stdout.log</string>
22
22
 
23
23
  <key>StandardErrorPath</key>
24
- <string>/Users/sophie/maestro/logs/watchdog/launchd-stderr.log</string>
24
+ <string>__AGENT_DIR__/logs/watchdog/launchd-stderr.log</string>
25
25
 
26
26
  <key>EnvironmentVariables</key>
27
27
  <dict>
@@ -12,7 +12,7 @@
12
12
  # ./scripts/watchdog/force-reboot.sh --status # Check heartbeat and uptime
13
13
  #
14
14
  # Remote usage (from another machine via SSH):
15
- # ssh sophie@mac-mini.local "~/maestro/scripts/watchdog/force-reboot.sh --graceful"
15
+ # ssh <user>@<mac-mini> "~/maestro/scripts/watchdog/force-reboot.sh --graceful"
16
16
  #
17
17
  # =============================================================================
18
18
  #
@@ -43,8 +43,8 @@
43
43
  # - With `pmset autorestart 1`, it will boot automatically
44
44
  #
45
45
  # 5. SSH + SYSDIAGNOSE RESET (partial freeze, SSH still works):
46
- # - ssh sophie@mac-mini.local "sudo reboot"
47
- # - Or: ssh sophie@mac-mini.local "sudo shutdown -r now"
46
+ # - ssh <user>@<mac-mini> "sudo reboot"
47
+ # - Or: ssh <user>@<mac-mini> "sudo shutdown -r now"
48
48
  #
49
49
  # =============================================================================
50
50