@adaptic/maestro 1.1.7 → 1.4.1

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 (60) hide show
  1. package/.claude/commands/init-maestro.md +502 -260
  2. package/README.md +47 -2
  3. package/bin/maestro.mjs +1 -1
  4. package/docs/guides/agents-observe-setup.md +64 -0
  5. package/docs/guides/ccxray-diagnostics.md +65 -0
  6. package/docs/guides/claude-mem-setup.md +79 -0
  7. package/docs/guides/claude-pace-setup.md +56 -0
  8. package/docs/guides/claudraband-sessions.md +98 -0
  9. package/docs/guides/clawteam-swarm.md +116 -0
  10. package/docs/guides/code-review-graph-setup.md +86 -0
  11. package/docs/guides/email-setup.md +399 -0
  12. package/docs/guides/media-generation-setup.md +349 -0
  13. package/docs/guides/outbound-governance-setup.md +438 -0
  14. package/docs/guides/pdf-generation-setup.md +315 -0
  15. package/docs/guides/poller-daemon-setup.md +550 -0
  16. package/docs/guides/rag-context-setup.md +459 -0
  17. package/docs/guides/self-optimization-pattern.md +82 -0
  18. package/docs/guides/slack-setup.md +350 -0
  19. package/docs/guides/twilio-subaccounts-setup.md +223 -0
  20. package/docs/guides/voice-sms-setup.md +698 -0
  21. package/docs/guides/webhook-relay-setup.md +349 -0
  22. package/docs/guides/whatsapp-setup.md +282 -0
  23. package/docs/runbooks/mac-mini-bootstrap.md +21 -0
  24. package/package.json +2 -1
  25. package/plugins/maestro-skills/plugin.json +16 -0
  26. package/plugins/maestro-skills/skills/agents-observe.md +110 -0
  27. package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
  28. package/plugins/maestro-skills/skills/claude-pace.md +61 -0
  29. package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
  30. package/scaffold/CLAUDE.md +64 -0
  31. package/scaffold/config/agent.ts.example +2 -1
  32. package/scaffold/config/caller-id-map.yaml +46 -0
  33. package/scaffold/config/known-agents.json +35 -0
  34. package/scripts/daemon/classifier.mjs +264 -50
  35. package/scripts/daemon/dispatcher.mjs +109 -5
  36. package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
  37. package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
  38. package/scripts/daemon/launchd-wrapper.sh +91 -0
  39. package/scripts/daemon/lib/session-router.mjs +274 -0
  40. package/scripts/daemon/lib/session-router.test.mjs +295 -0
  41. package/scripts/daemon/prompt-builder.mjs +51 -11
  42. package/scripts/daemon/responder.mjs +234 -19
  43. package/scripts/daemon/session-lock.mjs +194 -0
  44. package/scripts/daemon/sophie-daemon.mjs +16 -2
  45. package/scripts/email-signature.html +20 -4
  46. package/scripts/local-triggers/generate-plists.sh +62 -10
  47. package/scripts/media-generation/README.md +2 -0
  48. package/scripts/pdf-generation/README.md +2 -0
  49. package/scripts/poller/imap-client.mjs +4 -2
  50. package/scripts/poller/slack-poller.mjs +126 -59
  51. package/scripts/poller/trigger.mjs +12 -1
  52. package/scripts/setup/init-agent.sh +91 -1
  53. package/scripts/setup/install-dev-tools.sh +150 -0
  54. package/scripts/spawn-session.sh +21 -6
  55. package/workflows/continuous/backlog-executor.yaml +141 -0
  56. package/workflows/daily/evening-wrap.yaml +41 -1
  57. package/workflows/daily/morning-brief.yaml +17 -0
  58. package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
  59. package/workflows/event-driven/pr-review.yaml +104 -0
  60. package/workflows/weekly/engineering-health.yaml +154 -0
@@ -0,0 +1,350 @@
1
+ # Slack Setup Guide
2
+
3
+ How to enable Slack integration for a Maestro agent: dedicated app creation, token configuration, message sending (with typing indicators), file uploads, emoji reactions, real-time events via Railway relay, and Slack CDP for huddle automation.
4
+
5
+ > **Hard requirement: every agent gets its own dedicated Slack app.** Do NOT share a Slack app across agents. Each app has its own bot user, signing secret, OAuth tokens, and scope set. Sharing breaks identity (messages appear as the wrong agent), confuses signature verification on the webhook relay, and prevents per-agent revocation. The init-maestro wizard creates a new app for each agent via the Slack manifest API.
6
+
7
+ **Prerequisites**: Complete the [Mac Mini Bootstrap](../runbooks/mac-mini-bootstrap.md). The agent needs a Slack workspace account with admin rights to create apps.
8
+
9
+ ---
10
+
11
+ ## Architecture Overview
12
+
13
+ ```
14
+ ┌──────────────────────────────────────────────────────────────────────┐
15
+ │ INBOUND │
16
+ │ │
17
+ │ Slack workspace ──▶ slack-poller.mjs ──▶ state/inbox/slack/*.yaml │
18
+ │ (via xoxp- token) (every 60s) (inbox processor routes) │
19
+ │ │
20
+ │ OR (real-time): │
21
+ │ Slack Events API ──▶ slack-events-server.mjs ──▶ inbox / daemon │
22
+ │ (webhook) (Socket Mode or HTTP) │
23
+ ├──────────────────────────────────────────────────────────────────────┤
24
+ │ OUTBOUND │
25
+ │ │
26
+ │ ┌───────────────┐ ┌─────────────────┐ ┌──────────────────┐ │
27
+ │ │ slack-send.sh │ │ slack-upload- │ │ slack-react.mjs │ │
28
+ │ │ (messages) │ │ v2.py (files) │ │ (emoji reactions)│ │
29
+ │ └──────┬────────┘ └────────┬────────┘ └──────────────────┘ │
30
+ │ │ │ │
31
+ │ ┌──────▼────────────────────▼──────────────────────────────────┐ │
32
+ │ │ Pre-send pipeline: │ │
33
+ │ │ 1. validate-outbound.py (factual checks) │ │
34
+ │ │ 2. slack-responded.sh (atomic response dedup) │ │
35
+ │ │ 3. pre_draft_lookup.py (context enrichment) │ │
36
+ │ │ 4. slack-typing.mjs (typing indicator) │ │
37
+ │ │ 5. markdown → mrkdwn (format conversion) │ │
38
+ │ └──────────────────────────────────────────────────────────────┘ │
39
+ │ │ │
40
+ │ ▼ │
41
+ │ Slack Web API (chat.postMessage) via xoxp- User Token │
42
+ └──────────────────────────────────────────────────────────────────────┘
43
+ ```
44
+
45
+ **Critical design decision**: All Slack messages are sent via the **User OAuth Token (xoxp-)**, not the Bot Token (xoxb-). Messages sent with the user token appear as the agent's Slack user account (e.g. "Sophie Nguyen"), not as a bot app. The `block-mcp-slack-send.sh` hook enforces this — MCP Slack sends are blocked because they add a "Sent using @Claude" label.
46
+
47
+ ---
48
+
49
+ ## 1. Slack App Creation
50
+
51
+ ### 1.1 Create a Slack App
52
+
53
+ 1. Go to https://api.slack.com/apps → **Create New App**
54
+ 2. Choose "From scratch"
55
+ 3. Name it (e.g. "Adaptic Agent") and select your workspace
56
+ 4. Note the **App ID** from Basic Information
57
+
58
+ ### 1.2 Configure OAuth Scopes
59
+
60
+ Under **OAuth & Permissions** → **Scopes**, add:
61
+
62
+ **Bot Token Scopes** (xoxb-):
63
+
64
+ | Scope | Purpose |
65
+ |---|---|
66
+ | `chat:write` | Send messages |
67
+ | `channels:read` | List channels |
68
+ | `groups:read` | List private channels |
69
+ | `im:read` | List DM conversations |
70
+ | `users:read` | Look up user info |
71
+ | `files:write` | Upload files |
72
+ | `reactions:write` | Add emoji reactions |
73
+
74
+ **User Token Scopes** (xoxp-):
75
+
76
+ | Scope | Purpose |
77
+ |---|---|
78
+ | `channels:history` | Read channel messages |
79
+ | `groups:history` | Read private channel messages |
80
+ | `im:history` | Read DM messages |
81
+ | `search:read` | Search messages |
82
+ | `chat:write` | Send as user (primary send method) |
83
+ | `files:write` | Upload as user |
84
+ | `reactions:write` | React as user |
85
+
86
+ ### 1.3 Install the App
87
+
88
+ 1. Click **Install to Workspace**
89
+ 2. Authorize the requested permissions
90
+ 3. Copy both tokens:
91
+ - **Bot User OAuth Token** (starts with `xoxb-`)
92
+ - **User OAuth Token** (starts with `xoxp-`)
93
+
94
+ ### 1.4 Configure Environment Variables
95
+
96
+ Add to `.env`:
97
+
98
+ ```bash
99
+ # User token (xoxp-) — used for sending messages as the agent's user account
100
+ SLACK_USER_TOKEN=xoxp-...
101
+
102
+ # Bot token (xoxb-) — used for API calls that don't need user identity
103
+ SLACK_BOT_TOKEN=xoxb-...
104
+
105
+ # Optional: signing secret for Events API webhook verification
106
+ SLACK_SIGNING_SECRET=...
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 2. Sending Messages (`slack-send.sh`)
112
+
113
+ The primary script for all Slack message sending.
114
+
115
+ ### 2.1 Usage
116
+
117
+ ```bash
118
+ # Send to a channel
119
+ ./scripts/slack-send.sh "C1234567890" "Hello from the agent"
120
+
121
+ # Reply in a thread
122
+ ./scripts/slack-send.sh "C1234567890" "Thread reply" --thread_ts "1234567890.123456"
123
+
124
+ # With response dedup (prevents double-replying to the same message)
125
+ ./scripts/slack-send.sh "C1234567890" "Reply" --responding_to "1234567890.123456"
126
+
127
+ # Skip typing indicator
128
+ ./scripts/slack-send.sh "C1234567890" "Quick message" --no-typing
129
+ ```
130
+
131
+ ### 2.2 Pre-Send Pipeline
132
+
133
+ Every Slack send goes through this pipeline (in order):
134
+
135
+ 1. **Response dedup** (`slack-responded.sh`): If `--responding_to` is set, acquires an atomic mkdir-based lock to prevent concurrent sessions from replying to the same message
136
+ 2. **Typing indicator** (`slack-typing.mjs`): Shows "Agent is typing..." for a duration proportional to message length (1.5s–4s)
137
+ 3. **Pre-draft context lookup** (`pre_draft_lookup.py`): Looks up recipient in entity index (advisory, never blocks)
138
+ 4. **Factual validation** (`validate-outbound.py`): Checks for relationship claims, AI disclosure, scheduling errors — **blocks if critical issues found**
139
+ 5. **Markdown → mrkdwn conversion**: Converts standard Markdown (`**bold**`, `[link](url)`, `## heading`) to Slack's mrkdwn format (`*bold*`, `<url|link>`, `*heading*`)
140
+ 6. **Send via `chat.postMessage`**: Uses User OAuth Token
141
+
142
+ ### 2.3 Typing Indicators (`slack-typing.mjs`)
143
+
144
+ Creates a natural feel by showing typing dots before messages arrive:
145
+
146
+ - Duration scales with message length: <50 chars → 1.5s, 50-200 → 2.5s, 200-500 → 3s, >500 → 4s
147
+ - Connects via WebSocket RTM API using the user token
148
+ - Requires `rtm:stream` scope (degrades gracefully if missing — message sends without dots)
149
+ - Disable globally: `SLACK_TYPING_ENABLED=0` in `.env`
150
+
151
+ ### 2.4 Response Deduplication (`slack-responded.sh`)
152
+
153
+ Prevents the agent from sending multiple replies to the same message:
154
+
155
+ - Uses atomic `mkdir` for race-safe locking (POSIX-guaranteed atomic)
156
+ - Lock path: `state/locks/slack-responded/{channel}/{message_ts}/`
157
+ - After send succeeds, records a preview of the sent message for audit
158
+ - Lock TTL handled by `outbound-dedup-cleanup.sh`
159
+
160
+ ---
161
+
162
+ ## 3. File Uploads (`slack-upload-v2.py`)
163
+
164
+ Uploads files to Slack channels or threads:
165
+
166
+ ```bash
167
+ python3 scripts/slack-upload-v2.py --channel "C1234567890" --file /path/to/document.pdf --title "Q2 Board Pack"
168
+
169
+ # Upload to a thread
170
+ python3 scripts/slack-upload-v2.py --channel "C1234567890" --file report.pdf --thread_ts "1234567890.123456"
171
+ ```
172
+
173
+ Uses Slack's v2 file upload API (`files.uploadV2`) which supports larger files and better threading.
174
+
175
+ ---
176
+
177
+ ## 4. Emoji Reactions (`slack-react.mjs`)
178
+
179
+ Adds emoji reactions to messages:
180
+
181
+ ```bash
182
+ node scripts/slack-react.mjs --channel "C1234567890" --timestamp "1234567890.123456" --emoji "white_check_mark"
183
+ ```
184
+
185
+ Used by the agent to acknowledge messages (e.g. ✅ after processing a request).
186
+
187
+ ---
188
+
189
+ ## 5. Event Polling (`slack-poller.mjs`)
190
+
191
+ ### 5.1 How It Works
192
+
193
+ The Slack poller runs as part of the main poller loop (every 60 seconds):
194
+
195
+ 1. Reads recent messages from monitored channels and DMs using `conversations.history`
196
+ 2. Filters for new messages since last poll (tracks last-seen timestamps)
197
+ 3. Writes new messages to `state/inbox/slack/` as YAML files
198
+ 4. Inbox processor classifies and routes each message
199
+
200
+ ### 5.2 Monitored Channels
201
+
202
+ The poller monitors channels configured in the agent's operational setup. Typically:
203
+ - All DM conversations
204
+ - Channels where the agent is mentioned
205
+ - Priority channels (CEO DM, team channels)
206
+
207
+ ### 5.3 Priority Detection
208
+
209
+ CEO DMs and messages containing urgent keywords trigger immediate processing rather than waiting for the next inbox processor cycle.
210
+
211
+ ---
212
+
213
+ ## 6. Real-Time Events Server (`slack-events-server.mjs`)
214
+
215
+ ### 6.1 Overview
216
+
217
+ For faster event detection than polling, the events server receives Slack events in real-time:
218
+
219
+ ```bash
220
+ node scripts/slack-events-server.mjs
221
+ ```
222
+
223
+ ### 6.2 Control Script
224
+
225
+ ```bash
226
+ # Start the events server
227
+ ./scripts/slack-events-ctl.sh start
228
+
229
+ # Stop it
230
+ ./scripts/slack-events-ctl.sh stop
231
+
232
+ # Check status
233
+ ./scripts/slack-events-ctl.sh status
234
+ ```
235
+
236
+ ### 6.3 Configuration
237
+
238
+ Requires the Slack app to have **Event Subscriptions** enabled:
239
+
240
+ 1. Go to Slack App settings → Event Subscriptions → Enable Events
241
+ 2. Request URL: `https://your-tunnel-url/slack/events`
242
+ 3. Subscribe to events: `message.channels`, `message.groups`, `message.im`, `app_mention`
243
+ 4. The events server verifies requests using `SLACK_SIGNING_SECRET`
244
+
245
+ ---
246
+
247
+ ## 7. MCP Slack Send Block
248
+
249
+ The `block-mcp-slack-send.sh` hook prevents sending via MCP Slack:
250
+
251
+ ```
252
+ BLOCKED: Per CEO directive si-010 — do NOT use MCP Slack for sending.
253
+ Use scripts/slack-send.sh with SLACK_USER_TOKEN instead.
254
+ ```
255
+
256
+ **Why**: MCP Slack sends add a "Sent using @Claude" label to messages, breaking the agent's identity. All sends must go through `slack-send.sh` using the User OAuth Token so messages appear as the agent's Slack user account.
257
+
258
+ This hook is registered in `.claude/settings.json` as a `PreToolUse` hook matching MCP Slack send tools.
259
+
260
+ ---
261
+
262
+ ## 8. Slack CDP (Chrome DevTools Protocol)
263
+
264
+ For advanced Slack UI automation (huddle participation, keyboard shortcuts), the Slack desktop app is launched with CDP enabled:
265
+
266
+ ```bash
267
+ ./scripts/huddle/launch-slack.sh
268
+ # or manually:
269
+ /Applications/Slack.app/Contents/MacOS/Slack --remote-debugging-port=9222
270
+ ```
271
+
272
+ See [Voice & SMS Setup](voice-sms-setup.md) for the full huddle voice infrastructure that builds on Slack CDP.
273
+
274
+ ---
275
+
276
+ ## 9. Testing
277
+
278
+ | # | Test | How to Verify |
279
+ |---|---|---|
280
+ | 1 | Token validation | `curl -s -H "Authorization: Bearer $SLACK_USER_TOKEN" https://slack.com/api/auth.test \| jq .` |
281
+ | 2 | Simple send | `./scripts/slack-send.sh "C_TEST_CHANNEL" "Test message"` |
282
+ | 3 | Thread reply | Send with `--thread_ts` and verify it threads in Slack |
283
+ | 4 | Typing indicator | Watch Slack while sending — "Agent is typing..." should appear |
284
+ | 5 | Response dedup | Send same `--responding_to` twice; second should `DEDUP_SKIP` |
285
+ | 6 | File upload | `python3 scripts/slack-upload-v2.py --channel C_TEST --file test.txt` |
286
+ | 7 | Emoji reaction | `node scripts/slack-react.mjs --channel C_TEST --timestamp TS --emoji thumbsup` |
287
+ | 8 | MCP block | Attempt MCP Slack send — should be blocked by hook |
288
+ | 9 | Poller | Send a DM to the agent; check `state/inbox/slack/` |
289
+ | 10 | Markdown conversion | Send message with `**bold**` and `[link](url)` — verify Slack renders correctly |
290
+
291
+ ---
292
+
293
+ ## 10. Troubleshooting
294
+
295
+ ### "invalid_auth" or "token_revoked" errors
296
+
297
+ 1. Regenerate tokens: Slack App → OAuth & Permissions → Reinstall to Workspace
298
+ 2. Verify token type: user sends need `xoxp-` token, not `xoxb-`
299
+ 3. Update `.env` with new tokens
300
+
301
+ ### Messages appearing as bot (not user)
302
+
303
+ 1. Verify `SLACK_USER_TOKEN` starts with `xoxp-` (not `xoxb-`)
304
+ 2. Check `slack-send.sh` is using `SLACK_USER_TOKEN` (line 12)
305
+ 3. Verify `block-mcp-slack-send.sh` hook is active in `.claude/settings.json`
306
+
307
+ ### Typing indicator not showing
308
+
309
+ 1. Check `rtm:stream` scope on user token (not available on all Slack plans)
310
+ 2. Verify `SLACK_TYPING_ENABLED` is not set to `0`
311
+ 3. Check `slack-typing.mjs` can connect: `node scripts/slack-typing.mjs C_TEST 2000`
312
+ 4. Degrades gracefully — messages still send without typing dots
313
+
314
+ ### Poller missing messages
315
+
316
+ 1. Verify channel access: agent's Slack account must be a member of monitored channels
317
+ 2. Check `conversations.history` scope is granted
318
+ 3. Check last-poll timestamp in poller state isn't in the future (clock sync issue)
319
+
320
+ ### Response dedup false positives
321
+
322
+ 1. Check lock directory: `ls state/locks/slack-responded/CHANNEL/`
323
+ 2. Clean stale locks: `./scripts/outbound-dedup-cleanup.sh`
324
+ 3. Locks should auto-expire — check TTL in `outbound-dedup.sh`
325
+
326
+ ---
327
+
328
+ ## Key Files
329
+
330
+ | File | Purpose |
331
+ |---|---|
332
+ | `scripts/slack-send.sh` | Primary message sender (user token, typing, dedup, markdown) |
333
+ | `scripts/slack-typing.mjs` | WebSocket RTM typing indicator |
334
+ | `scripts/slack-react.mjs` | Add emoji reactions to messages |
335
+ | `scripts/slack-upload-v2.py` | File upload (v2 API) |
336
+ | `scripts/slack-responded.sh` | Atomic response deduplication |
337
+ | `scripts/slack-events-server.mjs` | Real-time Slack events receiver |
338
+ | `scripts/slack-events-ctl.sh` | Events server control (start/stop/status) |
339
+ | `scripts/poll-slack-events.sh` | Legacy event polling script |
340
+ | `scripts/hooks/block-mcp-slack-send.sh` | Blocks MCP Slack sends (enforces user token) |
341
+ | `scripts/poller/slack-poller.mjs` | Slack channel/DM polling |
342
+
343
+ ---
344
+
345
+ ## Related Documents
346
+
347
+ - [Voice & SMS Setup](voice-sms-setup.md) — Slack huddle voice via CDP
348
+ - [Outbound Governance Setup](outbound-governance-setup.md) — Dedup and validation pipeline
349
+ - [Poller & Daemon Setup](poller-daemon-setup.md) — How Slack polling integrates with the event loop
350
+ - [Communications Policy](../governance/communications-policy.md) — Voice modes and approval rules for Slack
@@ -0,0 +1,223 @@
1
+ # Twilio Sub-Accounts Setup Guide
2
+
3
+ How to create a dedicated Twilio sub-account for each Maestro agent under a single parent Twilio account. This is required for per-agent WhatsApp segregation because the **Twilio WhatsApp Sandbox webhook is account-wide** — only one URL per Twilio account.
4
+
5
+ **Prerequisites**: A parent Twilio account already exists (the company's root account). You have its `Account SID` and `Auth Token`.
6
+
7
+ ---
8
+
9
+ ## Why sub-accounts?
10
+
11
+ Each agent runs as a separate identity (Lucas, Jacob, Sophie, etc.) and needs:
12
+
13
+ - Its own phone number (for SMS)
14
+ - Its own WhatsApp sandbox webhook URL (the constraint)
15
+ - Its own auth token (so the agent's webhook relay can verify Twilio HMAC signatures independently)
16
+ - Its own usage logs and cost attribution
17
+
18
+ Sub-accounts give you all of this while still rolling up to the parent account for billing.
19
+
20
+ | Constraint | Sharing parent account | Per-agent sub-account |
21
+ |---|---|---|
22
+ | WhatsApp sandbox webhook | One URL for ALL agents — only the latest agent's webhook works | Each agent has their own sandbox + webhook |
23
+ | Phone numbers | Mixed across agents on one account | Each agent has their own |
24
+ | Auth Token | Shared — every agent's relay can verify everything (security risk) | Per-agent token, isolated verification |
25
+ | Cost attribution | Aggregated — hard to attribute to a specific agent | Sub-account usage rolls up but is separately reportable |
26
+ | Suspension | Suspending the parent kills all agents | Suspend just the affected agent |
27
+
28
+ ---
29
+
30
+ ## 1. Create the sub-account
31
+
32
+ Run from the agent's repo (or anywhere with `TWILIO_PARENT_ACCOUNT_SID` and `TWILIO_PARENT_AUTH_TOKEN` set):
33
+
34
+ ```bash
35
+ TWILIO_PARENT_ACCOUNT_SID=AC... # parent
36
+ TWILIO_PARENT_AUTH_TOKEN=... # parent
37
+ AGENT_NAME="lucas-ferreira-adaptic" # whatever friendly name you want
38
+
39
+ curl -s -u "$TWILIO_PARENT_ACCOUNT_SID:$TWILIO_PARENT_AUTH_TOKEN" -X POST \
40
+ "https://api.twilio.com/2010-04-01/Accounts.json" \
41
+ --data-urlencode "FriendlyName=$AGENT_NAME" \
42
+ | python3 -m json.tool
43
+ ```
44
+
45
+ Capture from the response:
46
+
47
+ - `sid` — the new sub-account SID (starts with `AC...`)
48
+ - `auth_token` — the sub-account's auth token (use this for HMAC verification on the agent's webhook relay)
49
+ - `owner_account_sid` — confirms it's a child of the parent account
50
+
51
+ Save both to the agent's `.env`:
52
+
53
+ ```bash
54
+ # Lucas's dedicated Twilio sub-account
55
+ TWILIO_ACCOUNT_SID=ACf2... # the new sub-account SID
56
+ TWILIO_AUTH_TOKEN=c1da... # the new sub-account auth token
57
+
58
+ # Parent account — only used for sub-account management API calls
59
+ TWILIO_PARENT_ACCOUNT_SID=AC36...
60
+ TWILIO_PARENT_AUTH_TOKEN=af86...
61
+ ```
62
+
63
+ ---
64
+
65
+ ## 2. Buy a phone number under the sub-account
66
+
67
+ Search for available numbers:
68
+
69
+ ```bash
70
+ source .env
71
+ curl -s -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
72
+ "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/AvailablePhoneNumbers/US/Local.json?AreaCode=628&SmsEnabled=true&VoiceEnabled=true&PageSize=5"
73
+ ```
74
+
75
+ Buy one (irreversible — costs ~$1.15/mo + per-message charges):
76
+
77
+ ```bash
78
+ PHONE="+16283454599"
79
+ curl -s -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" -X POST \
80
+ "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/IncomingPhoneNumbers.json" \
81
+ --data-urlencode "PhoneNumber=$PHONE" \
82
+ --data-urlencode "FriendlyName=Lucas Ferreira - Adaptic"
83
+ ```
84
+
85
+ Save the returned `sid` (starts with `PN...`) to `.env` as `TWILIO_PHONE_SID`.
86
+
87
+ ### Transferring an existing number from the parent
88
+
89
+ If the agent already has a number under the parent account that you want to move to the sub-account:
90
+
91
+ ```bash
92
+ PARENT_SID="AC36..." # parent account SID
93
+ PARENT_AUTH="af86..." # parent account auth token
94
+ PHONE_SID="PN1eb30e..." # the number to move
95
+ SUBACCOUNT_SID="ACf2..." # destination sub-account
96
+
97
+ curl -s -u "$PARENT_SID:$PARENT_AUTH" -X POST \
98
+ "https://api.twilio.com/2010-04-01/Accounts/$PARENT_SID/IncomingPhoneNumbers/$PHONE_SID.json" \
99
+ --data-urlencode "AccountSid=$SUBACCOUNT_SID"
100
+ ```
101
+
102
+ The number, all its webhook configuration, and message history move to the sub-account in one API call.
103
+
104
+ ---
105
+
106
+ ## 3. Configure the SMS webhook
107
+
108
+ After the sub-account owns the number, configure the SMS webhook to point at the agent's Railway webhook relay:
109
+
110
+ ```bash
111
+ source .env
112
+ RELAY_URL="https://${AGENT_FIRSTNAME_LOWER}-webhook-relay-production.up.railway.app"
113
+ curl -s -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" -X POST \
114
+ "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/IncomingPhoneNumbers/$TWILIO_PHONE_SID.json" \
115
+ --data-urlencode "SmsUrl=$RELAY_URL/sms" \
116
+ --data-urlencode "SmsMethod=POST"
117
+ ```
118
+
119
+ ---
120
+
121
+ ## 4. Configure the WhatsApp sandbox
122
+
123
+ This is the whole reason we use sub-accounts. Each sub-account has its own independent WhatsApp sandbox.
124
+
125
+ **Important**: WhatsApp sandbox webhook configuration is **UI-only** — Twilio does not expose an API for this. You must use Playwright or the user must configure it manually.
126
+
127
+ 1. Log in to the Twilio Console (the sandbox UI uses a different login session than the API)
128
+ 2. **Switch to the sub-account** via the account selector top-left (the dropdown shows all sub-accounts under the parent)
129
+ 3. Navigate to: `https://console.twilio.com/us1/develop/sms/try-it-out/whatsapp-learn?frameUrl=%2Fconsole%2Fsms%2Fwhatsapp%2Flearn`
130
+ 4. Click **Sandbox settings** tab
131
+ 5. Set the inbound webhook URL: `https://{firstname}-webhook-relay-production.up.railway.app/whatsapp`
132
+ 6. Set the status callback URL: `https://{firstname}-webhook-relay-production.up.railway.app/whatsapp/status`
133
+ 7. Set both methods to HTTP POST
134
+ 8. Save
135
+
136
+ Note the **sandbox join code** shown on the page (e.g. `join follow-arm`). Users must send `join {code}` from their WhatsApp to `+1 415 523 8886` to enroll their number for testing with this agent's sandbox.
137
+
138
+ ### Production WhatsApp (post-sandbox)
139
+
140
+ For production WhatsApp (real WABA business number, not the shared sandbox number), each sub-account can register its own WABA. This requires Facebook Business verification per sub-account — Twilio provides a self-sign-up flow at https://www.twilio.com/docs/whatsapp/self-sign-up. Same pattern: each agent's sub-account → its own WABA → its own webhook URL.
141
+
142
+ ---
143
+
144
+ ## 5. Update the Railway webhook relay
145
+
146
+ The agent's relay verifies Twilio HMAC signatures using the sub-account's auth token (NOT the parent's). After creating the sub-account, update the Railway env var:
147
+
148
+ ```bash
149
+ source .env
150
+ cd services/webhook-relay
151
+ railway variables --service ${AGENT_FIRSTNAME_LOWER}-webhook-relay \
152
+ --set "TWILIO_AUTH_TOKEN=$TWILIO_AUTH_TOKEN"
153
+ railway up --service ${AGENT_FIRSTNAME_LOWER}-webhook-relay --detach
154
+ ```
155
+
156
+ Wait until `/health` reports `twilio_signature: true` again.
157
+
158
+ ---
159
+
160
+ ## 6. Add to init-maestro Phase 4
161
+
162
+ When `/init-maestro` runs Phase 4 Service Configuration, the Twilio step should:
163
+
164
+ 1. Check whether `TWILIO_PARENT_ACCOUNT_SID` is set in `.env` (from a previous agent's setup or org config)
165
+ 2. If yes: create a sub-account for this agent via the API (Step 1 above), then bypass the "buy new account" flow
166
+ 3. If no: this is the first agent in the org — use `TWILIO_ACCOUNT_SID` as both parent and main, and document that future agents will need sub-accounts under it
167
+ 4. Buy a phone number under the sub-account
168
+ 5. Configure SMS webhook → Railway
169
+ 6. (Manual or Playwright) Configure WhatsApp sandbox → Railway
170
+
171
+ ---
172
+
173
+ ## Verification
174
+
175
+ ```bash
176
+ # 1. Sub-account exists and is active
177
+ source .env
178
+ curl -s -u "$TWILIO_PARENT_ACCOUNT_SID:$TWILIO_PARENT_AUTH_TOKEN" \
179
+ "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID.json" \
180
+ | python3 -m json.tool
181
+
182
+ # 2. Phone number is owned by the sub-account
183
+ curl -s -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
184
+ "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/IncomingPhoneNumbers.json"
185
+
186
+ # 3. SMS webhook points at Railway
187
+ curl -s -u "$TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN" \
188
+ "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/IncomingPhoneNumbers/$TWILIO_PHONE_SID.json" \
189
+ | python3 -c "import sys,json; d=json.load(sys.stdin); print('SMS URL:', d.get('sms_url'))"
190
+
191
+ # 4. Send a test SMS using the sub-account credentials
192
+ bash scripts/send-sms.sh --to "+1...your_test_number" --body "Sub-account test from {AgentName}"
193
+
194
+ # 5. Send a WhatsApp message after joining the sandbox
195
+ bash scripts/send-whatsapp.sh --to "whatsapp:+1...your_number" --body "WhatsApp test from {AgentName}"
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Closing / suspending a sub-account
201
+
202
+ When an agent is decommissioned:
203
+
204
+ ```bash
205
+ source .env
206
+ curl -s -u "$TWILIO_PARENT_ACCOUNT_SID:$TWILIO_PARENT_AUTH_TOKEN" -X POST \
207
+ "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID.json" \
208
+ --data-urlencode "Status=closed"
209
+ ```
210
+
211
+ Closing is irreversible. Use `Status=suspended` for temporary suspension (can be reactivated).
212
+
213
+ Before closing, decide what to do with the agent's phone number:
214
+ - Transfer back to the parent account (if you want to retain the number for re-use)
215
+ - Release it (Twilio will free up the number, you stop being charged)
216
+
217
+ ---
218
+
219
+ ## Related guides
220
+
221
+ - [Webhook Relay Setup](webhook-relay-setup.md) — How the Railway relay uses `TWILIO_AUTH_TOKEN` to verify HMAC signatures
222
+ - [Voice & SMS Setup](voice-sms-setup.md) — End-to-end SMS pipeline
223
+ - [WhatsApp Setup](whatsapp-setup.md) — Sandbox vs production WhatsApp