@yemi33/minions 0.1.813 → 0.1.814
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/CHANGELOG.md +4 -3
- package/dashboard/js/command-center.js +7 -4
- package/dashboard.js +111 -0
- package/docs/teams-production.md +368 -0
- package/docs/teams-setup.md +300 -0
- package/engine/ado.js +7 -0
- package/engine/cli.js +24 -4
- package/engine/github.js +7 -0
- package/engine/lifecycle.js +35 -13
- package/engine/preflight.js +13 -0
- package/engine/shared.js +9 -0
- package/engine/teams-cards.js +137 -0
- package/engine/teams.js +589 -0
- package/package.json +4 -2
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Teams Integration Setup
|
|
2
|
+
|
|
3
|
+
End-to-end guide for connecting Minions to Microsoft Teams via Azure Bot Framework. After completing these steps, messages sent to a Teams channel will be routed to the Minions Command Center, and agent events (completions, PR merges, plan updates) will be posted back to Teams.
|
|
4
|
+
|
|
5
|
+
**Prerequisites:**
|
|
6
|
+
|
|
7
|
+
- An Azure subscription (free tier works)
|
|
8
|
+
- Azure CLI installed (`az --version`) or access to the [Azure Portal](https://portal.azure.com)
|
|
9
|
+
- Dev Tunnel CLI installed (`devtunnel --version`) — see [Step 5](#step-5-set-up-dev-tunnel) for installation
|
|
10
|
+
- Minions dashboard running locally (`minions dash`)
|
|
11
|
+
- Admin or owner permissions on the target Teams team (for side-loading)
|
|
12
|
+
|
|
13
|
+
## Step 1: Create an Azure Bot Resource
|
|
14
|
+
|
|
15
|
+
1. Open the [Azure Portal](https://portal.azure.com) and search for **Azure Bot** in the top search bar.
|
|
16
|
+
2. Click **Create**.
|
|
17
|
+
3. Fill in the basics:
|
|
18
|
+
- **Bot handle**: A unique name (e.g., `minions-bot`). This is the internal identifier — it doesn't appear in Teams.
|
|
19
|
+
- **Subscription**: Select your Azure subscription.
|
|
20
|
+
- **Resource group**: Create a new one (e.g., `rg-minions`) or use an existing one.
|
|
21
|
+
- **Data residency**: Choose **Global** unless you have regional compliance requirements.
|
|
22
|
+
- **Pricing tier**: Select **F0 (Free)** for development.
|
|
23
|
+
4. Under **Microsoft App ID**, select **Single Tenant**.
|
|
24
|
+
- Choose **Create new Microsoft App ID**.
|
|
25
|
+
- This creates a new Entra ID (Azure AD) app registration for the bot.
|
|
26
|
+
|
|
27
|
+
> **Why Single Tenant?** Single-tenant bots only accept tokens from your Azure AD tenant, which is more secure for an internal tool like Minions. Multi-tenant is needed only if the bot must work across organizations.
|
|
28
|
+
|
|
29
|
+
5. Click **Review + create**, then **Create**.
|
|
30
|
+
6. Wait for deployment to complete (usually under 1 minute), then click **Go to resource**.
|
|
31
|
+
|
|
32
|
+
## Step 2: Configure the Teams Channel
|
|
33
|
+
|
|
34
|
+
1. In your Azure Bot resource, click **Channels** in the left sidebar.
|
|
35
|
+
2. Click the **Microsoft Teams** icon in the available channels list.
|
|
36
|
+
3. Accept the Terms of Service.
|
|
37
|
+
4. Leave the channel settings at defaults:
|
|
38
|
+
- **Messaging** tab: Enabled (default).
|
|
39
|
+
- **Calling** and **Group Chat** tabs: Leave disabled unless needed.
|
|
40
|
+
5. Click **Apply**.
|
|
41
|
+
|
|
42
|
+
The Teams channel should now appear as **Running** in the channels list.
|
|
43
|
+
|
|
44
|
+
## Step 3: Obtain App ID and App Password
|
|
45
|
+
|
|
46
|
+
You need two credentials: the **App ID** (also called Microsoft App ID or Client ID) and the **App Password** (a client secret).
|
|
47
|
+
|
|
48
|
+
### Get the App ID
|
|
49
|
+
|
|
50
|
+
1. In your Azure Bot resource, click **Configuration** in the left sidebar.
|
|
51
|
+
2. Copy the **Microsoft App ID** value. This is a GUID like `a1b2c3d4-e5f6-7890-abcd-ef1234567890`.
|
|
52
|
+
3. Save it — you'll add it to your Minions config.
|
|
53
|
+
|
|
54
|
+
### Create an App Password (Client Secret)
|
|
55
|
+
|
|
56
|
+
1. On the same **Configuration** page, click **Manage Password** next to the Microsoft App ID. This opens the Entra ID app registration.
|
|
57
|
+
2. In the app registration, click **Certificates & secrets** in the left sidebar.
|
|
58
|
+
3. Click **New client secret**.
|
|
59
|
+
4. Enter a description (e.g., `minions-bot-secret`) and choose an expiry (e.g., 24 months).
|
|
60
|
+
5. Click **Add**.
|
|
61
|
+
6. **Immediately copy the secret Value** (not the Secret ID). It is shown only once — if you navigate away, you cannot retrieve it again.
|
|
62
|
+
|
|
63
|
+
### Add Credentials to Minions Config
|
|
64
|
+
|
|
65
|
+
Add a `teams` section to your `config.json`:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"projects": [ ... ],
|
|
70
|
+
"agents": { ... },
|
|
71
|
+
"engine": { ... },
|
|
72
|
+
"teams": {
|
|
73
|
+
"enabled": true,
|
|
74
|
+
"appId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
75
|
+
"appPassword": "your-client-secret-value-here",
|
|
76
|
+
"notifyEvents": ["pr-merged", "agent-completed", "plan-completed", "agent-failed"],
|
|
77
|
+
"ccMirror": true,
|
|
78
|
+
"inboxPollInterval": 15000
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Field | Description |
|
|
84
|
+
|-------|-------------|
|
|
85
|
+
| `enabled` | Master switch — `true` to activate Teams integration |
|
|
86
|
+
| `appId` | Microsoft App ID from the Azure Bot Configuration page |
|
|
87
|
+
| `appPassword` | Client secret value from Entra ID Certificates & secrets |
|
|
88
|
+
| `notifyEvents` | Which events trigger Teams notifications (see below) |
|
|
89
|
+
| `ccMirror` | Mirror CC dashboard responses to Teams (`true`/`false`) |
|
|
90
|
+
| `inboxPollInterval` | How often to check for new Teams messages, in ms (default: 15000) |
|
|
91
|
+
|
|
92
|
+
**Available notification events:** `pr-merged`, `agent-completed`, `plan-completed`, `agent-failed`, `pr-abandoned`, `pr-approved`, `pr-build-failed`, `plan-approved`, `plan-rejected`, `verify-created`
|
|
93
|
+
|
|
94
|
+
> **Security note:** `config.json` is gitignored by default and should never be committed. For shared machines, consider setting the app password via an environment variable and reading it in your config setup.
|
|
95
|
+
|
|
96
|
+
## Step 4: Set the Messaging Endpoint
|
|
97
|
+
|
|
98
|
+
The messaging endpoint is the URL that Azure Bot Framework sends incoming messages to. It must point to your Minions dashboard's `/api/bot` route.
|
|
99
|
+
|
|
100
|
+
1. In your Azure Bot resource, click **Configuration** in the left sidebar.
|
|
101
|
+
2. Set the **Messaging endpoint** to:
|
|
102
|
+
```
|
|
103
|
+
https://<your-tunnel-url>/api/bot
|
|
104
|
+
```
|
|
105
|
+
For local development, this will be your Dev Tunnel URL (see [Step 5](#step-5-set-up-dev-tunnel)).
|
|
106
|
+
|
|
107
|
+
For example: `https://abc123.devtunnels.ms/api/bot`
|
|
108
|
+
|
|
109
|
+
3. Click **Apply** to save.
|
|
110
|
+
|
|
111
|
+
> The endpoint must be **HTTPS**. Bot Framework will not send messages to plain HTTP URLs. Dev Tunnels and production hosting (Azure App Service, etc.) both provide HTTPS by default.
|
|
112
|
+
|
|
113
|
+
> Changes to the messaging endpoint take effect immediately — no need to reinstall the bot in Teams.
|
|
114
|
+
|
|
115
|
+
## Step 5: Set Up Dev Tunnel
|
|
116
|
+
|
|
117
|
+
[Dev Tunnels](https://learn.microsoft.com/en-us/azure/developer/dev-tunnels/overview) create a public HTTPS URL that forwards traffic to your local machine. This lets Azure Bot Framework reach your locally running dashboard.
|
|
118
|
+
|
|
119
|
+
### Install Dev Tunnel CLI
|
|
120
|
+
|
|
121
|
+
If you don't have it installed:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Windows (winget)
|
|
125
|
+
winget install Microsoft.devtunnel
|
|
126
|
+
|
|
127
|
+
# macOS (Homebrew)
|
|
128
|
+
brew install --cask devtunnel
|
|
129
|
+
|
|
130
|
+
# Linux (curl)
|
|
131
|
+
curl -sL https://aka.ms/DevTunnelCliInstall | bash
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Authenticate
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
devtunnel user login
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This opens a browser for Microsoft account authentication. Sign in with the same account that has access to your Azure subscription.
|
|
141
|
+
|
|
142
|
+
### Start the Tunnel
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
devtunnel host -p 7331 --allow-anonymous
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- `-p 7331` forwards to the Minions dashboard port.
|
|
149
|
+
- `--allow-anonymous` allows Bot Framework to reach the endpoint without additional auth (the Bot Framework adapter handles its own authentication via the App ID and Password).
|
|
150
|
+
|
|
151
|
+
You'll see output like:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
Connect via browser: https://abc123.devtunnels.ms
|
|
155
|
+
Inspect network activity: https://abc123-7331.devtunnels.ms
|
|
156
|
+
|
|
157
|
+
Hosting port: 7331
|
|
158
|
+
Connect via browser: https://abc123-7331.devtunnels.ms
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Copy the Forwarding URL to Azure Bot
|
|
162
|
+
|
|
163
|
+
1. Copy the **port-specific** tunnel URL from the output (e.g., `https://abc123-7331.devtunnels.ms`).
|
|
164
|
+
2. Go back to your Azure Bot resource > **Configuration**.
|
|
165
|
+
3. Set the **Messaging endpoint** to:
|
|
166
|
+
```
|
|
167
|
+
https://abc123-7331.devtunnels.ms/api/bot
|
|
168
|
+
```
|
|
169
|
+
4. Click **Apply**.
|
|
170
|
+
|
|
171
|
+
### Persistent Tunnel (Optional)
|
|
172
|
+
|
|
173
|
+
By default, the tunnel URL changes each time you restart `devtunnel host`. To get a persistent URL:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# Create a named tunnel
|
|
177
|
+
devtunnel create minions-tunnel
|
|
178
|
+
|
|
179
|
+
# Add the port
|
|
180
|
+
devtunnel port create minions-tunnel -p 7331
|
|
181
|
+
|
|
182
|
+
# Host with anonymous access
|
|
183
|
+
devtunnel host minions-tunnel --allow-anonymous
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The named tunnel keeps the same URL across restarts — you won't need to update the Azure Bot messaging endpoint each time.
|
|
187
|
+
|
|
188
|
+
### Verify Connectivity
|
|
189
|
+
|
|
190
|
+
With the tunnel running and the dashboard started (`minions dash`), open the tunnel URL in a browser:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
https://abc123-7331.devtunnels.ms/api/routes
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
You should see the Minions API route list as JSON. If you get a connection error, check that:
|
|
197
|
+
- The dashboard is running (`minions dash`)
|
|
198
|
+
- The tunnel is active (`devtunnel host` is still running)
|
|
199
|
+
- The port number matches (7331)
|
|
200
|
+
|
|
201
|
+
## Step 6: Install the Bot in Teams
|
|
202
|
+
|
|
203
|
+
There are two ways to add the bot to a Teams channel: via App Studio / Teams Developer Portal (recommended for development) or via the Teams Admin Center (for org-wide deployment).
|
|
204
|
+
|
|
205
|
+
### Option A: Teams Developer Portal (Recommended for Development)
|
|
206
|
+
|
|
207
|
+
1. Open [Teams Developer Portal](https://dev.teams.microsoft.com/apps) in a browser.
|
|
208
|
+
2. Click **New app**.
|
|
209
|
+
3. Fill in the app details:
|
|
210
|
+
- **Name**: `Minions Bot` (or any display name)
|
|
211
|
+
- **Short description**: `Minions agent orchestration`
|
|
212
|
+
- **Developer name**: Your name or team name
|
|
213
|
+
- **Website**: `https://github.com/yemi33/minions` (or any URL)
|
|
214
|
+
- **Privacy policy** and **Terms of use**: Can be any URL for development
|
|
215
|
+
4. Under **App features**, click **Bot**.
|
|
216
|
+
5. Select **Enter a bot ID manually** and paste the **App ID** from [Step 3](#step-3-obtain-app-id-and-app-password).
|
|
217
|
+
6. Check the scopes:
|
|
218
|
+
- **Team** — enables the bot in team channels
|
|
219
|
+
- **Group chat** — optional, enables the bot in group chats
|
|
220
|
+
7. Click **Save**.
|
|
221
|
+
8. Click **Publish** > **Publish to your org** (or **Download app package** to side-load manually).
|
|
222
|
+
|
|
223
|
+
### Side-Load the App Package
|
|
224
|
+
|
|
225
|
+
If your organization doesn't allow direct publishing:
|
|
226
|
+
|
|
227
|
+
1. In the Developer Portal, click **Download app package** to get a `.zip` file.
|
|
228
|
+
2. Open Microsoft Teams.
|
|
229
|
+
3. Click **Apps** in the left sidebar.
|
|
230
|
+
4. Click **Manage your apps** > **Upload an app**.
|
|
231
|
+
5. Select **Upload a custom app** (or **Upload a custom app for [org name]** if you have admin rights).
|
|
232
|
+
6. Choose the downloaded `.zip` file.
|
|
233
|
+
7. In the app details dialog, click **Add to a team**.
|
|
234
|
+
8. Select the team and channel where you want the bot.
|
|
235
|
+
9. Click **Set up a bot**.
|
|
236
|
+
|
|
237
|
+
### Option B: Teams Admin Center (Org-Wide Deployment)
|
|
238
|
+
|
|
239
|
+
For deploying to all users or specific groups:
|
|
240
|
+
|
|
241
|
+
1. Open [Teams Admin Center](https://admin.teams.microsoft.com/).
|
|
242
|
+
2. Go to **Teams apps** > **Manage apps**.
|
|
243
|
+
3. Click **Upload new app** and upload the app package `.zip`.
|
|
244
|
+
4. Go to **Teams apps** > **Setup policies**.
|
|
245
|
+
5. Edit the relevant policy and add the Minions Bot to the **Installed apps** list.
|
|
246
|
+
|
|
247
|
+
### Verify the Bot Works
|
|
248
|
+
|
|
249
|
+
1. Open the Teams channel where you installed the bot.
|
|
250
|
+
2. Send a message that @mentions the bot:
|
|
251
|
+
```
|
|
252
|
+
@Minions Bot hello
|
|
253
|
+
```
|
|
254
|
+
3. If everything is configured correctly, the message will:
|
|
255
|
+
- Reach your dashboard via the Dev Tunnel
|
|
256
|
+
- Be written to `engine/teams-inbox.json`
|
|
257
|
+
- Be processed by the CC on the next poll cycle
|
|
258
|
+
- The CC response will appear as a thread reply in Teams
|
|
259
|
+
|
|
260
|
+
If you don't see a response, check:
|
|
261
|
+
- **Dashboard logs**: Look for incoming `/api/bot` requests
|
|
262
|
+
- **Dev Tunnel**: Ensure it's still running and the URL matches the Azure Bot messaging endpoint
|
|
263
|
+
- **Azure Bot**: Test the bot connection in the Azure Portal via **Test in Web Chat** (Configuration page)
|
|
264
|
+
- **Config**: Verify `config.json` has `teams.enabled: true` and correct `appId`/`appPassword`
|
|
265
|
+
|
|
266
|
+
## Troubleshooting
|
|
267
|
+
|
|
268
|
+
### "Unauthorized" or 401 Errors
|
|
269
|
+
|
|
270
|
+
The App ID or App Password in `config.json` doesn't match what's registered in Azure. Double-check:
|
|
271
|
+
- The App ID matches the Azure Bot's **Microsoft App ID** on the Configuration page.
|
|
272
|
+
- The App Password is the client secret **Value** (not the Secret ID) from Entra ID.
|
|
273
|
+
- The client secret hasn't expired.
|
|
274
|
+
|
|
275
|
+
### "Endpoint not reachable" in Azure Bot Test
|
|
276
|
+
|
|
277
|
+
- Verify the Dev Tunnel is running: `devtunnel host -p 7331 --allow-anonymous`
|
|
278
|
+
- Verify the messaging endpoint URL ends with `/api/bot`
|
|
279
|
+
- Verify the dashboard is running: `minions dash`
|
|
280
|
+
- Try opening the tunnel URL in a browser to confirm it's accessible
|
|
281
|
+
|
|
282
|
+
### Bot Doesn't Respond in Teams
|
|
283
|
+
|
|
284
|
+
- Check `engine/teams-inbox.json` — if messages appear there, the webhook is working but the CC processing loop may not be running.
|
|
285
|
+
- Check that the engine is running: `minions start`
|
|
286
|
+
- Check the engine logs for Teams-related errors.
|
|
287
|
+
- Ensure `config.teams.enabled` is `true`.
|
|
288
|
+
|
|
289
|
+
### "App not found" When Side-Loading
|
|
290
|
+
|
|
291
|
+
- Your Teams admin may have disabled custom app side-loading. Contact your Teams admin to enable **Upload custom apps** in the Teams Admin Center > Setup policies.
|
|
292
|
+
- Alternatively, ask an admin to upload the app via the Admin Center (Option B above).
|
|
293
|
+
|
|
294
|
+
## What's Next
|
|
295
|
+
|
|
296
|
+
Once the bot is responding to messages in Teams:
|
|
297
|
+
|
|
298
|
+
- **Agent notifications** will appear in the Teams channel when agents complete work, PRs are merged, or plans are finished.
|
|
299
|
+
- **CC mirror** mode (enabled by default) posts Command Center responses from the dashboard to Teams so the whole team sees orchestration activity.
|
|
300
|
+
- For production deployment (replacing Dev Tunnel with a stable public URL), see [docs/teams-production.md](teams-production.md).
|
package/engine/ado.js
CHANGED
|
@@ -441,6 +441,13 @@ async function pollPrStatus(config) {
|
|
|
441
441
|
pr.buildErrorLog = logParts.join('\n\n');
|
|
442
442
|
log('info', `PR ${pr.id}: fetched error logs from ${logParts.length} failing pipeline(s)`);
|
|
443
443
|
}
|
|
444
|
+
|
|
445
|
+
// Teams notification for build failure — non-blocking
|
|
446
|
+
try {
|
|
447
|
+
const teams = require('./teams');
|
|
448
|
+
const prFilePath = shared.projectPrPath(project);
|
|
449
|
+
teams.teamsNotifyPrEvent(pr, 'build-failed', project, prFilePath).catch(() => {});
|
|
450
|
+
} catch {}
|
|
444
451
|
}
|
|
445
452
|
}
|
|
446
453
|
|
package/engine/cli.js
CHANGED
|
@@ -372,6 +372,19 @@ const commands = {
|
|
|
372
372
|
}
|
|
373
373
|
}, 3000);
|
|
374
374
|
|
|
375
|
+
// Teams inbox poll timer — process incoming Teams messages through CC
|
|
376
|
+
const teams = require('./teams');
|
|
377
|
+
const teamsInboxInterval = config.teams?.inboxPollInterval ?? shared.ENGINE_DEFAULTS.teams.inboxPollInterval;
|
|
378
|
+
const teamsInboxTimer = teams.isTeamsEnabled() ? setInterval(() => {
|
|
379
|
+
try {
|
|
380
|
+
const ctrl = getControl();
|
|
381
|
+
if (ctrl.state !== 'running') return;
|
|
382
|
+
teams.processTeamsInbox().catch(err => {
|
|
383
|
+
shared.log('warn', `Teams inbox poll error: ${err.message}`);
|
|
384
|
+
});
|
|
385
|
+
} catch {}
|
|
386
|
+
}, teamsInboxInterval) : null;
|
|
387
|
+
|
|
375
388
|
console.log(`Tick interval: ${interval / 1000}s | Max concurrent: ${config.engine?.maxConcurrent || 5}`);
|
|
376
389
|
console.log('Press Ctrl+C to stop');
|
|
377
390
|
|
|
@@ -422,6 +435,7 @@ const commands = {
|
|
|
422
435
|
console.log(`\n${signal} received — initiating graceful shutdown...`);
|
|
423
436
|
clearInterval(tickTimer);
|
|
424
437
|
clearInterval(fastPollTimer);
|
|
438
|
+
if (teamsInboxTimer) clearInterval(teamsInboxTimer);
|
|
425
439
|
for (const f of _watchedFiles) { try { fs.unwatchFile(f); } catch { /* cleanup */ } }
|
|
426
440
|
safeWrite(CONTROL_PATH, { state: 'stopping', pid: process.pid, stopping_at: e.ts() });
|
|
427
441
|
e.log('info', `Graceful shutdown initiated (${signal})`);
|
|
@@ -894,9 +908,9 @@ const commands = {
|
|
|
894
908
|
const e = engine();
|
|
895
909
|
console.log('\n=== Kill All Active Work ===\n');
|
|
896
910
|
const config = getConfig();
|
|
897
|
-
const dispatch = getDispatch();
|
|
898
911
|
const shared = require('./shared');
|
|
899
912
|
|
|
913
|
+
// Kill processes via PID files (expensive — outside dispatch lock)
|
|
900
914
|
const pidFiles = fs.readdirSync(ENGINE_DIR).filter(f => f.startsWith('pid-'));
|
|
901
915
|
for (const f of pidFiles) {
|
|
902
916
|
const pid = safeRead(path.join(ENGINE_DIR, f)).trim();
|
|
@@ -904,7 +918,15 @@ const commands = {
|
|
|
904
918
|
fs.unlinkSync(path.join(ENGINE_DIR, f));
|
|
905
919
|
}
|
|
906
920
|
|
|
907
|
-
|
|
921
|
+
// Atomically read and clear dispatch.active (locked read-modify-write)
|
|
922
|
+
let killed = [];
|
|
923
|
+
e.mutateDispatch((dispatch) => {
|
|
924
|
+
killed = dispatch.active || [];
|
|
925
|
+
dispatch.active = [];
|
|
926
|
+
return dispatch;
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// Reset work items outside the dispatch lock (work-items.json has its own lock)
|
|
908
930
|
for (const item of killed) {
|
|
909
931
|
if (item.meta) {
|
|
910
932
|
e.updateWorkItemStatus(item.meta, WI_STATUS.PENDING, '');
|
|
@@ -932,8 +954,6 @@ const commands = {
|
|
|
932
954
|
|
|
933
955
|
console.log(`Killed dispatch: ${item.id} (${item.agent}) — work item reset to pending`);
|
|
934
956
|
}
|
|
935
|
-
dispatch.active = [];
|
|
936
|
-
safeWrite(DISPATCH_PATH, dispatch);
|
|
937
957
|
|
|
938
958
|
// Agent status derived from dispatch.json — clearing dispatch.active is sufficient.
|
|
939
959
|
console.log('All agents reset to idle (dispatch cleared)');
|
package/engine/github.js
CHANGED
|
@@ -411,6 +411,13 @@ async function pollPrStatus(config) {
|
|
|
411
411
|
pr.buildErrorLog = errorLog;
|
|
412
412
|
log('info', `PR ${pr.id}: fetched ${errorLog.split('\n').length} lines of build error log`);
|
|
413
413
|
}
|
|
414
|
+
|
|
415
|
+
// Teams notification for build failure — non-blocking
|
|
416
|
+
try {
|
|
417
|
+
const teams = require('./teams');
|
|
418
|
+
const prFilePath = shared.projectPrPath(project);
|
|
419
|
+
teams.teamsNotifyPrEvent(pr, 'build-failed', project, prFilePath).catch(() => {});
|
|
420
|
+
} catch {}
|
|
414
421
|
}
|
|
415
422
|
}
|
|
416
423
|
}
|
package/engine/lifecycle.js
CHANGED
|
@@ -277,10 +277,25 @@ function checkPlanCompletion(meta, config) {
|
|
|
277
277
|
});
|
|
278
278
|
});
|
|
279
279
|
log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
|
|
280
|
+
|
|
281
|
+
// Teams notification for verify creation — non-blocking
|
|
282
|
+
try {
|
|
283
|
+
const teams = require('./teams');
|
|
284
|
+
teams.teamsNotifyPlanEvent({ name: plan.plan_summary || planFile, file: planFile }, 'verify-created').catch(() => {});
|
|
285
|
+
} catch {}
|
|
280
286
|
}
|
|
281
287
|
|
|
282
288
|
// Archive deferred until verify completes
|
|
283
289
|
|
|
290
|
+
// Teams notification for plan completion — non-blocking
|
|
291
|
+
try {
|
|
292
|
+
const teams = require('./teams');
|
|
293
|
+
teams.teamsNotifyPlanEvent({
|
|
294
|
+
name: plan.plan_summary || planFile, file: planFile, project: plan.project,
|
|
295
|
+
doneCount: doneItems.length, totalCount: planFeatureIds.size,
|
|
296
|
+
}, 'plan-completed').catch(() => {});
|
|
297
|
+
} catch {}
|
|
298
|
+
|
|
284
299
|
log('info', `PRD ${planFile} completed: ${doneItems.length} done, ${failedItems.length} failed, runtime ${runtimeMin}m`);
|
|
285
300
|
return true;
|
|
286
301
|
}
|
|
@@ -751,9 +766,13 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary)
|
|
|
751
766
|
if (!Array.isArray(prs)) return prs;
|
|
752
767
|
const target = prs.find(p => p.id === pr.id);
|
|
753
768
|
if (!target) return prs;
|
|
754
|
-
// Once approved, stays approved
|
|
755
|
-
if (postReviewStatus
|
|
756
|
-
target.reviewStatus
|
|
769
|
+
// Once approved, stays approved — only changes-requested can override
|
|
770
|
+
if (postReviewStatus) {
|
|
771
|
+
if (target.reviewStatus === 'approved' && postReviewStatus !== 'changes-requested') {
|
|
772
|
+
// Keep approved — don't downgrade
|
|
773
|
+
} else {
|
|
774
|
+
target.reviewStatus = postReviewStatus;
|
|
775
|
+
}
|
|
757
776
|
}
|
|
758
777
|
target.lastReviewedAt = ts();
|
|
759
778
|
target.minionsReview = {
|
|
@@ -1019,16 +1038,13 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
1019
1038
|
});
|
|
1020
1039
|
}
|
|
1021
1040
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
});
|
|
1030
|
-
} catch (err) { log('warn', `Teams post-merge notify failed: ${err.message}`); }
|
|
1031
|
-
}
|
|
1041
|
+
// Teams PR lifecycle notification — non-blocking
|
|
1042
|
+
try {
|
|
1043
|
+
const teams = require('./teams');
|
|
1044
|
+
const prEvent = newStatus === PR_STATUS.MERGED ? 'pr-merged' : 'pr-abandoned';
|
|
1045
|
+
const prFilePath = project ? projectPrPath(project) : null;
|
|
1046
|
+
teams.teamsNotifyPrEvent(pr, prEvent, project, prFilePath).catch(() => {});
|
|
1047
|
+
} catch {}
|
|
1032
1048
|
|
|
1033
1049
|
log('info', `Post-merge hooks completed for ${pr.id}`);
|
|
1034
1050
|
}
|
|
@@ -1630,6 +1646,12 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
1630
1646
|
const metricsResult = isAutoRetry ? 'retry' : finalResult;
|
|
1631
1647
|
updateMetrics(agentId, dispatchItem, metricsResult, taskUsage, prsCreatedCount, model);
|
|
1632
1648
|
|
|
1649
|
+
// Teams notification — non-blocking
|
|
1650
|
+
try {
|
|
1651
|
+
const teams = require('./teams');
|
|
1652
|
+
teams.teamsNotifyCompletion(dispatchItem, finalResult, agentId).catch(() => {});
|
|
1653
|
+
} catch {}
|
|
1654
|
+
|
|
1633
1655
|
return { resultSummary, taskUsage, autoRecovered, structuredCompletion };
|
|
1634
1656
|
}
|
|
1635
1657
|
|
package/engine/preflight.js
CHANGED
|
@@ -192,6 +192,19 @@ function doctor(minionsHome) {
|
|
|
192
192
|
} else {
|
|
193
193
|
runtimeResults.push({ name: 'Agents configured', ok: false, message: 'no agents in config.json' });
|
|
194
194
|
}
|
|
195
|
+
|
|
196
|
+
// Check Teams integration
|
|
197
|
+
const teams = config.teams;
|
|
198
|
+
if (teams && teams.enabled === true) {
|
|
199
|
+
if (!teams.appId || !teams.appPassword) {
|
|
200
|
+
const missing = [!teams.appId && 'appId', !teams.appPassword && 'appPassword'].filter(Boolean).join(', ');
|
|
201
|
+
runtimeResults.push({ name: 'Teams integration', ok: 'warn', message: `enabled but missing: ${missing}` });
|
|
202
|
+
} else {
|
|
203
|
+
runtimeResults.push({ name: 'Teams integration', ok: true, message: 'configured' });
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
runtimeResults.push({ name: 'Teams integration', ok: 'warn', message: 'disabled — see docs/teams-setup.md' });
|
|
207
|
+
}
|
|
195
208
|
} catch {
|
|
196
209
|
runtimeResults.push({ name: 'Config', ok: false, message: `missing or invalid — run: minions init` });
|
|
197
210
|
}
|
package/engine/shared.js
CHANGED
|
@@ -553,6 +553,15 @@ const ENGINE_DEFAULTS = {
|
|
|
553
553
|
ccModel: 'sonnet', // model for Command Center and doc-chat (sonnet, haiku, opus)
|
|
554
554
|
ccEffort: null, // effort level for CC/doc-chat (null, 'low', 'medium', 'high')
|
|
555
555
|
ccMaxTurns: 50, // max tool-use turns for CC/doc-chat before CLI stops
|
|
556
|
+
// Teams integration — config.teams shape: { enabled, appId, appPassword, notifyEvents, ccMirror, inboxPollInterval }
|
|
557
|
+
teams: {
|
|
558
|
+
enabled: false,
|
|
559
|
+
appId: '',
|
|
560
|
+
appPassword: '',
|
|
561
|
+
notifyEvents: ['pr-merged', 'agent-completed', 'plan-completed', 'agent-failed'],
|
|
562
|
+
ccMirror: true,
|
|
563
|
+
inboxPollInterval: 15000,
|
|
564
|
+
},
|
|
556
565
|
};
|
|
557
566
|
|
|
558
567
|
// ─── Status & Type Constants ─────────────────────────────────────────────────
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/teams-cards.js — Adaptive Card templates for Teams notifications.
|
|
3
|
+
* All cards use schema version 1.4 and include fallback text.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const SCHEMA = 'http://adaptivecards.io/schemas/adaptive-card.json';
|
|
7
|
+
const VERSION = '1.4';
|
|
8
|
+
const DASHBOARD_URL = 'http://localhost:7331';
|
|
9
|
+
|
|
10
|
+
function wrapCard(body, actions, fallbackText) {
|
|
11
|
+
return {
|
|
12
|
+
type: 'AdaptiveCard',
|
|
13
|
+
$schema: SCHEMA,
|
|
14
|
+
version: VERSION,
|
|
15
|
+
fallbackText: fallbackText || 'Minions notification',
|
|
16
|
+
body,
|
|
17
|
+
actions: actions || [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Agent completion card — shows agent name, task title, result, PR link.
|
|
23
|
+
* @param {string} agent — agent name/id
|
|
24
|
+
* @param {object} item — { title, id }
|
|
25
|
+
* @param {string} result — 'success' or 'error'
|
|
26
|
+
* @param {string} [prUrl] — PR URL if available
|
|
27
|
+
*/
|
|
28
|
+
function buildCompletionCard(agent, item, result, prUrl) {
|
|
29
|
+
const isSuccess = result === 'success';
|
|
30
|
+
const badge = isSuccess ? 'Done' : 'Failed';
|
|
31
|
+
const color = isSuccess ? 'good' : 'attention';
|
|
32
|
+
const title = item?.title || item?.id || 'Unknown task';
|
|
33
|
+
|
|
34
|
+
const body = [
|
|
35
|
+
{ type: 'TextBlock', text: `${badge} — ${agent}`, weight: 'bolder', size: 'medium', color },
|
|
36
|
+
{ type: 'TextBlock', text: title, wrap: true },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const actions = [
|
|
40
|
+
{ type: 'Action.OpenUrl', title: 'Open Dashboard', url: DASHBOARD_URL },
|
|
41
|
+
];
|
|
42
|
+
if (prUrl) {
|
|
43
|
+
actions.unshift({ type: 'Action.OpenUrl', title: 'View PR', url: prUrl });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return wrapCard(body, actions, `${badge}: ${agent} — ${title}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* PR lifecycle card — shows PR title, event, author, project.
|
|
51
|
+
* @param {object} pr — { id, title, url, agent }
|
|
52
|
+
* @param {string} event — 'pr-merged', 'pr-abandoned', 'build-failed', etc.
|
|
53
|
+
* @param {object} [project] — { name }
|
|
54
|
+
*/
|
|
55
|
+
function buildPrCard(pr, event, project) {
|
|
56
|
+
const title = pr?.title || pr?.id || 'Unknown PR';
|
|
57
|
+
const agent = pr?.agent || 'unknown';
|
|
58
|
+
const projectName = project?.name || '';
|
|
59
|
+
|
|
60
|
+
const body = [
|
|
61
|
+
{ type: 'TextBlock', text: `${event}`, weight: 'bolder', size: 'medium' },
|
|
62
|
+
{ type: 'ColumnSet', columns: [
|
|
63
|
+
{ type: 'Column', width: 'stretch', items: [
|
|
64
|
+
{ type: 'TextBlock', text: title, wrap: true, weight: 'bolder' },
|
|
65
|
+
{ type: 'TextBlock', text: `${agent}${projectName ? ' | ' + projectName : ''}`, isSubtle: true, spacing: 'none' },
|
|
66
|
+
]},
|
|
67
|
+
]},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const actions = [
|
|
71
|
+
{ type: 'Action.OpenUrl', title: 'Open Dashboard', url: DASHBOARD_URL },
|
|
72
|
+
];
|
|
73
|
+
if (pr?.url) {
|
|
74
|
+
actions.unshift({ type: 'Action.OpenUrl', title: 'View PR', url: pr.url });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return wrapCard(body, actions, `${event}: ${title} (${agent})`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Plan lifecycle card — shows plan name, event, item counts.
|
|
82
|
+
* @param {object} plan — { name, file, doneCount, totalCount }
|
|
83
|
+
* @param {string} event — 'plan-completed', 'plan-approved', 'plan-rejected', 'verify-created'
|
|
84
|
+
*/
|
|
85
|
+
function buildPlanCard(plan, event) {
|
|
86
|
+
const name = plan?.name || plan?.file || 'Unknown plan';
|
|
87
|
+
const hasCounts = plan?.doneCount != null && plan?.totalCount != null;
|
|
88
|
+
|
|
89
|
+
const body = [
|
|
90
|
+
{ type: 'TextBlock', text: `${event}`, weight: 'bolder', size: 'medium' },
|
|
91
|
+
{ type: 'TextBlock', text: name, wrap: true },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
if (hasCounts) {
|
|
95
|
+
body.push({ type: 'TextBlock', text: `${plan.doneCount}/${plan.totalCount} items completed`, isSubtle: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const actions = [
|
|
99
|
+
{ type: 'Action.OpenUrl', title: 'Open Dashboard', url: `${DASHBOARD_URL}/prd` },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
return wrapCard(body, actions, `${event}: ${name}${hasCounts ? ` (${plan.doneCount}/${plan.totalCount})` : ''}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* CC response mirror card — shows user question and CC answer.
|
|
107
|
+
* @param {string} question — user's CC input
|
|
108
|
+
* @param {string} answer — CC response (truncated if needed)
|
|
109
|
+
*/
|
|
110
|
+
function buildCCResponseCard(question, answer) {
|
|
111
|
+
const maxLen = 3000;
|
|
112
|
+
const truncated = answer.length > maxLen
|
|
113
|
+
? answer.slice(0, maxLen) + '...'
|
|
114
|
+
: answer;
|
|
115
|
+
|
|
116
|
+
const body = [
|
|
117
|
+
{ type: 'TextBlock', text: 'Command Center', weight: 'bolder', size: 'medium' },
|
|
118
|
+
{ type: 'TextBlock', text: `> ${question.slice(0, 200)}`, wrap: true, isSubtle: true },
|
|
119
|
+
{ type: 'TextBlock', text: truncated, wrap: true },
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const actions = [
|
|
123
|
+
{ type: 'Action.OpenUrl', title: 'Open Dashboard', url: DASHBOARD_URL },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
return wrapCard(body, actions, `CC: ${question.slice(0, 100)} — ${answer.slice(0, 200)}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = {
|
|
130
|
+
buildCompletionCard,
|
|
131
|
+
buildPrCard,
|
|
132
|
+
buildPlanCard,
|
|
133
|
+
buildCCResponseCard,
|
|
134
|
+
SCHEMA,
|
|
135
|
+
VERSION,
|
|
136
|
+
DASHBOARD_URL,
|
|
137
|
+
};
|