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