@yemi33/minions 0.1.1937 → 0.1.1939

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/README.md CHANGED
@@ -676,8 +676,6 @@ To move to a new machine: `npm install -g @yemi33/minions && minions init --forc
676
676
  github.js issues.js
677
677
  ado.js ado-token.js ado-mcp-wrapper.js
678
678
  ado-status.js check-status.js
679
- # Notifications
680
- teams.js teams-cards.js
681
679
  # Runtime state (generated, gitignored)
682
680
  control.json <- running/paused/stopped
683
681
  dispatch.json <- pending/active/completed queue
@@ -60,7 +60,9 @@ function formatToolSummary(name, input) {
60
60
  }
61
61
  default: {
62
62
  var keys = Object.keys(inp);
63
- if (keys.length === 0) return escHtml(name) + '()';
63
+ // No-input case (e.g. ACP chips that carry their full label in `name`
64
+ // like "Fetch /api/status") — render the name verbatim, no parens.
65
+ if (keys.length === 0) return escHtml(name);
64
66
  var firstKey = keys[0];
65
67
  var firstVal = String(inp[firstKey] || '');
66
68
  if (firstVal.length > 40) firstVal = firstVal.slice(0, 37) + '...';
@@ -45,7 +45,6 @@ async function openSettings() {
45
45
  const e = data.engine || {};
46
46
  const c = data.claude || {};
47
47
  const agents = data.agents || {};
48
- const t = data.teams || {};
49
48
 
50
49
  // Per-agent override placeholders surface the inherited fleet defaults as
51
50
  // muted text — operators see exactly what each agent will resolve to without
@@ -242,28 +241,6 @@ async function openSettings() {
242
241
  '</details>' +
243
242
  '</div>' +
244
243
 
245
- '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Teams Integration</h3>' +
246
- '<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:8px">' +
247
- settingsToggle('Enable Teams', 'set-teams-enabled', !!t.enabled, 'Connect Minions to Microsoft Teams') +
248
- '</div>' +
249
- '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">' +
250
- settingsField('App ID', 'set-teams-appId', t.appId || '', '', 'Microsoft App ID from Azure Bot Configuration') +
251
- settingsField('App Password', 'set-teams-appPassword', t.appPassword || '', '', 'Client secret (leave blank for certificate auth)') +
252
- '</div>' +
253
- '<div style="font-size:10px;color:var(--muted);margin-bottom:4px;font-weight:600">Certificate Auth (alternative to client secret)</div>' +
254
- '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">' +
255
- settingsField('Certificate Path', 'set-teams-certPath', t.certPath || '', '', 'Path to PEM certificate file') +
256
- settingsField('Private Key Path', 'set-teams-privateKeyPath', t.privateKeyPath || '', '', 'Path to PEM private key file') +
257
- settingsField('Tenant ID', 'set-teams-tenantId', t.tenantId || '', '', 'Azure AD tenant ID (required for cert auth)') +
258
- '</div>' +
259
- '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
260
- settingsField('Notify Events', 'set-teams-notifyEvents', (t.notifyEvents || []).join(', '), '', 'Comma-separated event types to notify') +
261
- settingsField('Inbox Poll Interval', 'set-teams-inboxPollInterval', t.inboxPollInterval || 15000, 'ms', 'How often to check for Teams messages') +
262
- '</div>' +
263
- '<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:16px">' +
264
- settingsToggle('CC Mirror', 'set-teams-ccMirror', t.ccMirror !== false, 'Mirror Command Center responses to Teams') +
265
- '</div>' +
266
-
267
244
  '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Claude CLI</h3>' +
268
245
  '<div style="display:grid;grid-template-columns:1fr;gap:8px;margin-bottom:8px">' +
269
246
  settingsField('Allowed Tools', 'set-allowedTools', c.allowedTools || '', '', 'Claude allow-list passed through for compatibility; runtime bypass flags are adapter-owned.') +
@@ -637,18 +614,6 @@ async function saveSettings() {
637
614
  allowedTools: document.getElementById('set-allowedTools').value,
638
615
  };
639
616
 
640
- const teamsPayload = {
641
- enabled: document.getElementById('set-teams-enabled').checked,
642
- appId: document.getElementById('set-teams-appId').value,
643
- appPassword: document.getElementById('set-teams-appPassword').value,
644
- certPath: document.getElementById('set-teams-certPath').value,
645
- privateKeyPath: document.getElementById('set-teams-privateKeyPath').value,
646
- tenantId: document.getElementById('set-teams-tenantId').value,
647
- notifyEvents: document.getElementById('set-teams-notifyEvents').value,
648
- inboxPollInterval: document.getElementById('set-teams-inboxPollInterval').value,
649
- ccMirror: document.getElementById('set-teams-ccMirror').checked,
650
- };
651
-
652
617
  const agentsPayload = {};
653
618
  document.querySelectorAll('[data-agent][data-field]').forEach(function(el) {
654
619
  const id = el.dataset.agent;
@@ -671,7 +636,7 @@ async function saveSettings() {
671
636
  // Save config
672
637
  const res = await fetch('/api/settings', {
673
638
  method: 'POST', headers: { 'Content-Type': 'application/json' },
674
- body: JSON.stringify({ engine: enginePayload, claude: claudePayload, agents: agentsPayload, teams: teamsPayload, projects: projectsPayload })
639
+ body: JSON.stringify({ engine: enginePayload, claude: claudePayload, agents: agentsPayload, projects: projectsPayload })
675
640
  });
676
641
  const result = await res.json();
677
642
  if (!res.ok) throw new Error(result.error);
package/dashboard.js CHANGED
@@ -21,7 +21,6 @@ const _dashboardVersion = {
21
21
  };
22
22
  const shared = require('./engine/shared');
23
23
  const queries = require('./engine/queries');
24
- const teams = require('./engine/teams');
25
24
  const ado = require('./engine/ado');
26
25
  const gh = require('./engine/github');
27
26
  const issues = require('./engine/issues');
@@ -121,10 +120,6 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
121
120
  if (candidate.agents && candidate.agents[id]) current.agents[id] = candidate.agents[id];
122
121
  }
123
122
  }
124
- if (body.teams) {
125
- if (candidate.teams) current.teams = candidate.teams;
126
- else delete current.teams;
127
- }
128
123
  if (body.projects && Array.isArray(body.projects)) {
129
124
  if (!Array.isArray(current.projects)) current.projects = [];
130
125
  for (const update of body.projects) {
@@ -804,7 +799,6 @@ function _steeringDeliveryState(agentId) {
804
799
  }
805
800
 
806
801
  const PLANS_DIR = path.join(MINIONS_DIR, 'plans');
807
- const TEAMS_INBOX_PATH = path.join(ENGINE_DIR, 'teams-inbox.json');
808
802
 
809
803
  // Resolve a plan/PRD file path: .json files live in prd/, .md files in plans/
810
804
  // Validates that the file stays within the expected directory to prevent path traversal.
@@ -2018,7 +2012,6 @@ const CC_API_FALLBACK_METHODS = new Set(['GET', 'POST', 'DELETE']);
2018
2012
  const CC_API_FALLBACK_BLOCKED_PREFIXES = [
2019
2013
  '/api/command-center',
2020
2014
  '/api/doc-chat',
2021
- '/api/bot',
2022
2015
  ];
2023
2016
 
2024
2017
  // SoT for CC's runtime API index. Captured lazily on the first HTTP request
@@ -5039,9 +5032,6 @@ const server = http.createServer(async (req, res) => {
5039
5032
  }
5040
5033
  }
5041
5034
 
5042
- // Teams notification for plan approval — non-blocking
5043
- try { teams.teamsNotifyPlanEvent({ name: plan.plan_summary || body.file, file: body.file }, 'plan-approved').catch(() => {}); } catch {}
5044
-
5045
5035
  invalidateStatusCache();
5046
5036
  return jsonReply(res, 200, { ok: true, status: 'approved', resumedWorkItems: resumed, diffAwareUpdate: diffAwareQueued });
5047
5037
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
@@ -5209,9 +5199,6 @@ const server = http.createServer(async (req, res) => {
5209
5199
  return data;
5210
5200
  }, { defaultValue: {} });
5211
5201
 
5212
- // Teams notification for plan rejection — non-blocking
5213
- try { teams.teamsNotifyPlanEvent({ name: plan.plan_summary || body.file, file: body.file }, 'plan-rejected').catch(() => {}); } catch {}
5214
-
5215
5202
  return jsonReply(res, 200, { ok: true, status: 'rejected' });
5216
5203
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
5217
5204
  }
@@ -6273,10 +6260,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6273
6260
  sessionId: ccSession.sessionId,
6274
6261
  newSession: !wasResume,
6275
6262
  };
6276
- // Mirror user-facing text to Teams (skip Teams-originated turns).
6277
- if (!tabId.startsWith('teams-')) {
6278
- teams.teamsPostCCResponse(body.message, result.text).catch(() => {});
6279
- }
6280
6263
  if (sessionReset) replyBody.sessionReset = true;
6281
6264
  return jsonReply(res, 200, replyBody);
6282
6265
  } finally {
@@ -6759,12 +6742,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6759
6742
  liveState.donePayload = donePayload;
6760
6743
  if (liveState.writer) liveState.writer(donePayload);
6761
6744
 
6762
- // Mirror CC response to Teams (non-blocking, skip Teams-originated)
6763
- const _streamTabId = body.tabId || 'default';
6764
- if (!_streamTabId.startsWith('teams-')) {
6765
- teams.teamsPostCCResponse(body.message, displayText).catch(() => {});
6766
- }
6767
-
6768
6745
  if (liveState.endResponse) liveState.endResponse();
6769
6746
  _scheduleCcLiveCleanup(tabId);
6770
6747
  } finally {
@@ -7100,7 +7077,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7100
7077
  engine,
7101
7078
  claude: settingsClaudeConfig(config),
7102
7079
  agents: config.agents || {},
7103
- teams: { ...shared.ENGINE_DEFAULTS.teams, ...(config.teams || {}) },
7104
7080
  projects: (config.projects || []).map(p => ({
7105
7081
  name: p.name,
7106
7082
  workSources: {
@@ -7388,22 +7364,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7388
7364
  }
7389
7365
  }
7390
7366
 
7391
- if (body.teams) {
7392
- if (!config.teams) config.teams = {};
7393
- const tm = body.teams;
7394
- if (tm.enabled !== undefined) config.teams.enabled = !!tm.enabled;
7395
- for (const key of ['appId', 'appPassword', 'certPath', 'privateKeyPath', 'tenantId']) {
7396
- if (tm[key] !== undefined) config.teams[key] = String(tm[key] || '');
7397
- }
7398
- if (tm.notifyEvents !== undefined) {
7399
- config.teams.notifyEvents = Array.isArray(tm.notifyEvents) ? tm.notifyEvents : String(tm.notifyEvents || '').split(',').map(s => s.trim()).filter(Boolean);
7400
- }
7401
- if (tm.inboxPollInterval !== undefined) config.teams.inboxPollInterval = Math.max(5000, Number(tm.inboxPollInterval) || 15000);
7402
- if (tm.ccMirror !== undefined) config.teams.ccMirror = !!tm.ccMirror;
7403
- // Invalidate cached adapter so credential changes take effect
7404
- teams._resetAdapter();
7405
- }
7406
-
7407
7367
  if (body.projects && Array.isArray(body.projects)) {
7408
7368
  if (!config.projects) config.projects = [];
7409
7369
  for (const update of body.projects) {
@@ -7551,93 +7511,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7551
7511
  }
7552
7512
  }
7553
7513
 
7554
- // ── Teams Bot Handler ─────────────────────────────────────────────────────
7555
-
7556
- async function handleTeamsBot(req, res) {
7557
- if (!teams.isTeamsEnabled()) {
7558
- return jsonReply(res, 503, { error: 'Teams integration disabled' }, req);
7559
- }
7560
- const adapter = teams.createAdapter();
7561
- if (!adapter) {
7562
- return jsonReply(res, 503, { error: 'Teams adapter unavailable' }, req);
7563
- }
7564
- try {
7565
- await adapter.process(req, res, async (context) => {
7566
- const activity = context.activity;
7567
- const cfg = teams.getTeamsConfig();
7568
-
7569
- // Save conversation reference on install/member events
7570
- if (activity.type === 'conversationUpdate' && activity.membersAdded?.length) {
7571
- const ref = context.activity.conversation?.id;
7572
- if (ref) {
7573
- const convRef = {
7574
- activityId: activity.id,
7575
- user: activity.from,
7576
- bot: activity.recipient,
7577
- conversation: activity.conversation,
7578
- channelId: activity.channelId,
7579
- locale: activity.locale,
7580
- serviceUrl: activity.serviceUrl,
7581
- };
7582
- teams.saveConversationRef(activity.conversation.id, convRef);
7583
- shared.log('info', `Teams conversationUpdate: saved ref for ${activity.conversation.id}`);
7584
- }
7585
- }
7586
-
7587
- if (activity.type === 'installationUpdate') {
7588
- const convRef = {
7589
- activityId: activity.id,
7590
- user: activity.from,
7591
- bot: activity.recipient,
7592
- conversation: activity.conversation,
7593
- channelId: activity.channelId,
7594
- locale: activity.locale,
7595
- serviceUrl: activity.serviceUrl,
7596
- };
7597
- if (activity.conversation?.id) {
7598
- teams.saveConversationRef(activity.conversation.id, convRef);
7599
- shared.log('info', `Teams installationUpdate: saved ref for ${activity.conversation.id}`);
7600
- }
7601
- }
7602
-
7603
- // Handle incoming messages
7604
- if (activity.type === 'message' && activity.text) {
7605
- // Filter bot's own echo messages
7606
- if (activity.from?.id === cfg.appId) return;
7607
-
7608
- const msgId = `teams-${Date.now()}-${shared.uid()}`;
7609
- const convRef = {
7610
- activityId: activity.id,
7611
- user: activity.from,
7612
- bot: activity.recipient,
7613
- conversation: activity.conversation,
7614
- channelId: activity.channelId,
7615
- locale: activity.locale,
7616
- serviceUrl: activity.serviceUrl,
7617
- };
7618
- mutateJsonFileLocked(TEAMS_INBOX_PATH, (inbox) => {
7619
- if (!Array.isArray(inbox)) inbox = [];
7620
- inbox.push({
7621
- id: msgId,
7622
- text: activity.text,
7623
- from: activity.from?.name || activity.from?.id || 'unknown',
7624
- conversationRef: convRef,
7625
- receivedAt: new Date().toISOString(),
7626
- _processedAt: null,
7627
- });
7628
- return inbox;
7629
- }, { defaultValue: [] });
7630
- shared.log('info', `Teams message received from ${activity.from?.name || 'unknown'}: ${activity.text.slice(0, 80)}`);
7631
- }
7632
- });
7633
- } catch (err) {
7634
- shared.log('warn', `Teams bot handler error: ${err.message}`);
7635
- if (!res.headersSent) {
7636
- return jsonReply(res, 500, { error: 'Bot processing failed' }, req);
7637
- }
7638
- }
7639
- }
7640
-
7641
7514
  // ── Route Registry ──────────────────────────────────────────────────────────
7642
7515
  // Order matters: specific routes before general ones (e.g., /api/plans/approve before /api/plans/:file)
7643
7516
 
@@ -8322,16 +8195,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
8322
8195
 
8323
8196
  // Settings
8324
8197
  { method: 'GET', path: '/api/settings', desc: 'Return current engine + claude + routing config', handler: handleSettingsRead },
8325
- { method: 'POST', path: '/api/settings', desc: 'Update engine + claude + agent + teams + projects config', params: 'engine?, claude?, agents?, teams?, projects?', handler: handleSettingsUpdate },
8198
+ { method: 'POST', path: '/api/settings', desc: 'Update engine + claude + agent + projects config', params: 'engine?, claude?, agents?, projects?', handler: handleSettingsUpdate },
8326
8199
  { method: 'POST', path: '/api/settings/routing', desc: 'Update routing.md', params: 'content', handler: handleSettingsRouting },
8327
8200
  { method: 'POST', path: '/api/settings/reset', desc: 'Reset engine + claude + agent settings to defaults', handler: handleSettingsReset },
8328
8201
 
8329
8202
  // Feature flags (experimental / in-progress UX gates — see engine/features.js)
8330
8203
  { method: 'GET', path: '/api/features', desc: 'List registered feature flags with current enabled state', handler: handleFeaturesList },
8331
8204
  { method: 'POST', path: '/api/features/toggle', desc: 'Enable/disable a registered feature flag', params: 'id, enabled', handler: handleFeaturesToggle },
8332
-
8333
- // Teams Bot Framework webhook
8334
- { method: 'POST', path: '/api/bot', desc: 'Bot Framework webhook for Teams integration', handler: handleTeamsBot },
8335
8205
  ];
8336
8206
 
8337
8207
  // ── Route Dispatcher ────────────────────────────────────────────────────────
package/docs/README.md CHANGED
@@ -23,13 +23,11 @@ Architecture, design proposals, and lifecycle references for people working on t
23
23
 
24
24
  ## Operations
25
25
 
26
- Operational runbooks for engine operators, fleet maintainers, and Teams integrators.
26
+ Operational runbooks for engine operators and fleet maintainers.
27
27
 
28
28
  - [auto-discovery.md](auto-discovery.md) — Auto-discovery and execution pipeline: the per-tick orchestration loop and the four work-discovery sources.
29
29
  - [engine-restart.md](engine-restart.md) — How agents survive an engine restart: state persistence, the 20-minute startup grace period, and orphan reattachment via PID files and `live-output.log`.
30
30
  - [human-vs-automated.md](human-vs-automated.md) — Quick reference table of which features humans start, run, decide, and recover, and the two human approval gates.
31
- - [teams-production.md](teams-production.md) — Three deployment options (Azure App Service, Container Apps, self-hosted VM) for migrating the Teams bot from a Dev Tunnel to a stable HTTPS endpoint.
32
- - [teams-setup.md](teams-setup.md) — End-to-end guide for connecting Minions to Microsoft Teams via Azure Bot Framework and a Dev Tunnel.
33
31
 
34
32
  ---
35
33
 
@@ -14,6 +14,30 @@
14
14
  "reason": "Read-side tolerance: cleanup sweep auto-migrates four obsolete work-item / PRD status strings ('in-pr', 'implemented', 'complete', 'needs-human-review') to the canonical 'done' / 'failed' values. The aliases are no longer written anywhere in the engine; the constants exist only to repair stale on-disk values from old engine versions.",
15
15
  "targetRemovalDate": null,
16
16
  "notes": "Keep indefinitely until telemetry / a sweep log shows zero migrations performed for 30 consecutive days across all known projects (work-items.json + prd/*.json). At that point the constants and both _migrateLegacyItem branches in engine/cleanup.js (definitions at :799-800; usage at :803-815 for work items and :880-887 for PRD missing_features) can be deleted. Total cost on disk today: 4 strings."
17
+ },
18
+ {
19
+ "id": "native-teams-integration",
20
+ "removedAt": "2026-05-14",
21
+ "reason": "Native Microsoft Teams Bot Framework integration removed end-to-end. The Teams MCP server (teams-* tools, configured in the CC client outside this repo) supersedes the in-repo Bot Framework path. Removal closes the dual-implementation gap and drops the botbuilder dependency.",
22
+ "removedLocations": [
23
+ "engine/teams.js",
24
+ "engine/teams-cards.js",
25
+ "engine/teams-state.json (runtime state)",
26
+ "engine/teams-inbox.json (runtime state, generated by the deleted /api/bot handler)",
27
+ "dashboard.js: POST /api/bot route + handleTeamsBot, TEAMS_INBOX_PATH constant, CC mirror hooks (teamsPostCCResponse), plan-approval/rejection Teams notifications, settings GET/POST teams block",
28
+ "dashboard/js/settings.js: Teams Integration settings UI + teamsPayload submit",
29
+ "engine/lifecycle.js: teamsNotifyCompletion, teamsNotifyPlanEvent (verify-created + plan-completed), teamsNotifyPrEvent (post-merge)",
30
+ "engine/github.js + engine/ado.js: teamsNotifyPrEvent on pr-approved and build-failed",
31
+ "engine/preflight.js: Teams integration doctor check",
32
+ "engine/cli.js: teamsInboxTimer + clearInterval on shutdown",
33
+ "engine/shared.js: ENGINE_DEFAULTS.teams block",
34
+ "package.json: botbuilder dependency (4.23.3)",
35
+ "docs/teams-setup.md, docs/teams-production.md",
36
+ "test/unit/auto-recovery.test.js: ~58 Teams test cases",
37
+ "test/unit/preflight-behavioral.test.js: 4 doctor Teams checks + teams field in the docs-link coverage scenario",
38
+ "README.md / CLAUDE.md / docs/README.md / TODO.md / docs/rfc-completion-json.md: prose references"
39
+ ],
40
+ "notes": "The Teams MCP (teams-* tools) lives outside this repo in the CC client config and is NOT affected. If Teams-style notifications are needed again, route them through the MCP layer or an external webhook watch action — do not re-introduce the Bot Framework SDK in-process."
17
41
  }
18
42
  ]
19
43
 
@@ -1,6 +1,37 @@
1
1
  # Distribution & Publishing
2
2
 
3
- Minions is distributed as an npm package (`@yemi33/minions`) from a sanitized package boundary.
3
+ Minions is distributed as an npm package (`@yemi33/minions`) from a sanitized package boundary. The source lives across three GitHub remotes, with a strict one-way port direction (see [Three-Remote Topology](#three-remote-topology)).
4
+
5
+ ## Three-Remote Topology
6
+
7
+ | Remote (local name on `D:/squad`) | URL | Role |
8
+ |-----------------------------------|-----|------|
9
+ | `origin` | `https://github.com/yemi33/minions` | **Authoring source of truth.** All normal code changes (engine, dashboard, playbooks, agents, etc.) land here first. Triggers npm publish + downstream mirrors. |
10
+ | `emu` | `https://github.com/yemishin_microsoft/minions` | Microsoft-identity mirror of `yemi33/minions`. Automated via `.github/workflows/mirror-to-emu.yml`. |
11
+ | `opg` | `https://github.com/opg-microsoft/minions` | **Compliant store** under the `opg-microsoft` org. Automated mirror to a sync branch via `.github/workflows/mirror-to-opg.yml`; `main` is reconciled by a maintainer PR because it carries enterprise/compliance-only commits (see below). |
12
+
13
+ ### Port direction
14
+
15
+ **Strictly one-way: `yemi33` → `emu`, `yemi33` → `opg`.**
16
+
17
+ - **Do NOT port from `emu` or `opg` back into `yemi33`.** Their `main` branches may carry org-specific compliance content (`.github/policies/*`, `.github/acl/*`, `.github/compliance/*`, JIT access policy, internal access lists, MS-org issue templates) that has no business in the personal-fork authoring source.
18
+ - **Direct pushes to `opg-microsoft/minions` are allowed for enterprise/compliance-specific changes only.** Examples: editing `.github/policies/jit.yml`, updating `.github/acl/access.yml`, modifying `.github/compliance/inventory.yml`. These changes stay on `opg` and are not backported.
19
+ - Other teams (Microsoft infra, compliance review) may also push to `opg-microsoft/minions` directly. Treat its `main` as potentially ahead of `yemi33`/`emu` on policy files.
20
+
21
+ ### Why opg can't use the same direct-mirror pattern as emu
22
+
23
+ `mirror-to-emu.yml` does `git push emu HEAD:master` — direct overwrite, because `emu/master` is meant to be a 1:1 reflection of `yemi33/master`. `opg-microsoft/minions` is different: its `main` carries enterprise-only commits that don't exist on `yemi33/master`, so a force-push to `opg/main` would destroy them. Instead, `mirror-to-opg.yml` force-pushes `yemi33/master` to `opg/sync/yemi33-master` (a dedicated bot-owned branch), and a maintainer opens or updates a PR from `sync/yemi33-master` → `main` for review. The first such PR uses `--allow-unrelated-histories` to bridge the initial divergence; subsequent ones merge incrementally.
24
+
25
+ ### Required secrets on `yemi33/minions`
26
+
27
+ | Secret | Purpose | Workflow |
28
+ |--------|---------|----------|
29
+ | `NPM_TOKEN` | npm publish | `publish.yml` |
30
+ | `EMU_PUSH_TOKEN` | Push to `yemishin_microsoft/minions` | `mirror-to-emu.yml` |
31
+ | `EMU_PACKAGES_TOKEN` | Publish to `@yemishin_microsoft` internal registry | `publish-internal-github-packages.yml` |
32
+ | `OPG_PUSH_TOKEN` | Push to `opg-microsoft/minions` sync branch | `mirror-to-opg.yml` |
33
+
34
+ Each mirror workflow gracefully skips with a warning if its token secret is unset, so unconfigured downstreams never block the primary publish path.
4
35
 
5
36
  ## Distribution Boundary
6
37
 
@@ -101,8 +101,8 @@ The agent must not write the file in pieces. Empty, truncated, or malformed JSON
101
101
 
102
102
  // ── Always required ──────────────────────────────────────────────────────
103
103
  "status": "done", // see §4.2
104
- "summary": "Added /api/bot endpoint and wired Teams inbox.", // ≤500 chars
105
- "filesChanged": ["engine/teams.js", "dashboard.js"], // optional, hint only
104
+ "summary": "Added /api/example endpoint and wired example integration.", // ≤500 chars
105
+ "filesChanged": ["engine/example.js", "dashboard.js"], // optional, hint only
106
106
 
107
107
  // ── PR control plane (replaces sites 1, 4 for fix/implement/verify) ─────
108
108
  "prs": [
@@ -271,7 +271,7 @@ These paths stay on stdout / live-output.log:
271
271
 
272
272
  1. **`engine/timeout.js` completion-via-output detection** (`timeout.js:189-219`). The signal is the engine-written `[process-exit]` sentinel, emitted even if the agent crashed before writing completion.json. Removing it would mean orphans that finished during process-handle loss are never reconciled.
273
273
  2. **Stale-orphan cleanup via `live-output.log` mtime** (`timeout.js:178`). Completion.json is written once at exit, so `live-output.log` remains the best indirect signal after the engine loses process tracking.
274
- 3. **`parseStreamJsonOutput` for `resultSummary`** in `parseAgentOutput` (`lifecycle.js:1483`). This extracts the human-readable summary from the CLI's stream-json. Even after the flip, `completion.summary` is *also* extracted, but the stream-json text remains the canonical "what did the agent say last" — used in dashboards, agent history, Teams notifications. The two coexist: `completion.summary` is for routing decisions, the stream-json text is for display.
274
+ 3. **`parseStreamJsonOutput` for `resultSummary`** in `parseAgentOutput` (`lifecycle.js:1483`). This extracts the human-readable summary from the CLI's stream-json. Even after the flip, `completion.summary` is *also* extracted, but the stream-json text remains the canonical "what did the agent say last" — used in dashboards, agent history, and external notifications. The two coexist: `completion.summary` is for routing decisions, the stream-json text is for display.
275
275
  4. **Inbox-file skill scan** (`lifecycle.js:2013-2024`). Some agents write skills into their inbox findings file (a deliberate human-discoverable artifact). The completion file deprecates inline ` ```skill ` blocks in stdout, but the inbox file scan is opt-in and stays — it's a different surface (a real file the agent intentionally wrote, not regex-scraped from stdout).
276
276
 
277
277
  ### 5.5 Backward Compatibility
@@ -355,7 +355,7 @@ engine falls back to stdout parsing during the dual-mode period. After the
355
355
  flip date, missing/invalid completion.json marks your dispatch failed.
356
356
 
357
357
  Do NOT include sensitive data (tokens, API keys) — completion.json is read
358
- by the engine and may surface in dashboard views and Teams notifications.
358
+ by the engine and may surface in dashboard views and external notifications.
359
359
  ```
360
360
 
361
361
  ### 7.2 Per-Playbook Removals
package/engine/ado.js CHANGED
@@ -860,14 +860,6 @@ async function pollPrStatus(config) {
860
860
  pr.reviewStatus = newReviewStatus;
861
861
  updated = true;
862
862
  shared.trackReviewMetric(pr, newReviewStatus, config);
863
- if (newReviewStatus === 'approved') {
864
- // Teams notification for PR approval — non-blocking, edge-triggered (only on transition)
865
- try {
866
- const teams = require('./teams');
867
- const prFilePath = shared.projectPrPath(project);
868
- teams.teamsNotifyPrEvent(pr, 'pr-approved', project, prFilePath).catch(() => {});
869
- } catch {}
870
- }
871
863
  }
872
864
 
873
865
  if (newStatus !== PR_STATUS.ACTIVE) return updated;
@@ -976,15 +968,6 @@ async function pollPrStatus(config) {
976
968
  }
977
969
  }
978
970
  updated = true;
979
-
980
- if (buildStatus === 'failing') {
981
- // Teams notification for build failure — non-blocking
982
- try {
983
- const teams = require('./teams');
984
- const prFilePath = shared.projectPrPath(project);
985
- teams.teamsNotifyPrEvent(pr, 'build-failed', project, prFilePath).catch(() => {});
986
- } catch {}
987
- }
988
971
  }
989
972
  if (buildStatus === 'failing') {
990
973
  if (buildFailReason && pr.buildFailReason !== buildFailReason) {
@@ -252,9 +252,7 @@ class Worker {
252
252
  try { this.inflight.onChunk(text); } catch { /* swallow */ }
253
253
  }
254
254
  } else if (update.sessionUpdate === 'tool_call' && this.inflight.onToolUse) {
255
- // ACP `tool_call` (pending) → Claude-style {name, input} via
256
- // _mapAcpToolCallToToolUse so the dashboard's formatToolSummary
257
- // formatters (Bash → "$ <cmd>", etc.) work unchanged.
255
+ // ACP `tool_call` (pending) → chip label via _mapAcpToolCallToToolUse.
258
256
  const mapped = _mapAcpToolCallToToolUse(update);
259
257
  if (mapped) {
260
258
  try { this.inflight.onToolUse(mapped.name, mapped.input, update.toolCallId); }
@@ -447,50 +445,40 @@ function _extractChunkText(content) {
447
445
  // (Bash → "$ <cmd>", Read → "Reading <path>", etc.). Unknown kinds fall back
448
446
  // to ACP's human-readable `title` with the raw input attached, which renders
449
447
  // through the default `<title>(<key>: <val>)` formatter.
448
+ // Map an ACP `tool_call` notification to a chip label. Strategy:
449
+ //
450
+ // 1. Detect well-known shapes by rawInput field presence and route through
451
+ // formatToolSummary's Claude-tool formatters (Bash → "$ <cmd>",
452
+ // Grep → "Searching `pat` in path", Read → "Reading <path>"). Preserves
453
+ // the granular detail you actually want when reading the chip list.
454
+ //
455
+ // 2. Fall back to ACP's human-curated `title` for anything that doesn't
456
+ // match. Title is always populated and immune to field-name drift, so
457
+ // the empty-`$` failure mode (kind:execute with no `command`) becomes
458
+ // a clean "Find work item W-..." chip instead of a broken stub.
459
+ //
460
+ // Kind-based routing is intentionally NOT used — ACP overloads `kind:read`
461
+ // for both file-view and grep, and `kind:execute` sometimes arrives without
462
+ // a `command`. Field-detection on rawInput is more reliable.
450
463
  function _mapAcpToolCallToToolUse(update) {
451
464
  if (!update || update.sessionUpdate !== 'tool_call') return null;
452
465
  const rawInput = (update.rawInput && typeof update.rawInput === 'object') ? update.rawInput : {};
453
- const kind = String(update.kind || '').toLowerCase();
454
- const title = update.title || '';
455
-
456
- // Field-name normalization: Copilot ACP and Claude tool_use use different
457
- // keys for the same concept (Copilot `path`, Claude `file_path`). Normalize
458
- // here so the dashboard's formatToolSummary — written against Claude's
459
- // names — produces the same chip text on both runtimes.
460
- const filePath = rawInput.file_path || rawInput.path || rawInput.filePath || '';
461
-
462
- // Pattern (Grep) — Copilot uses `paths` (plural) for the search scope where
463
- // Claude's Grep takes `path`.
464
- if (typeof rawInput.pattern === 'string') {
465
- return {
466
- name: 'Grep',
467
- input: { pattern: rawInput.pattern, path: rawInput.paths || rawInput.path || '.' },
468
- };
469
- }
466
+ const title = update.title || update.kind || 'Tool';
470
467
 
471
- switch (kind) {
472
- case 'execute':
473
- return { name: 'Bash', input: rawInput };
474
- case 'read':
475
- // ACP overloads `read` for both file-view and grep the pattern check
476
- // above already handled grep, so this branch is the file-view case.
477
- return { name: 'Read', input: { file_path: filePath } };
478
- case 'edit':
479
- return { name: 'Edit', input: { file_path: filePath } };
480
- case 'search':
481
- // Pattern check above handled the grep case; arriving here means glob.
482
- return { name: 'Glob', input: rawInput };
483
- case 'fetch':
484
- return { name: 'WebFetch', input: rawInput };
485
- case 'think':
486
- return { name: title || 'Think', input: rawInput };
487
- default:
488
- // Unknown kind — use ACP's human-readable title as the chip label and
489
- // drop rawInput so formatToolSummary's default branch shows just the
490
- // title (avoids `<title>(<key>: <val>)` clutter when the input shape
491
- // is unfamiliar).
492
- return { name: title || kind || 'Tool', input: {} };
468
+ if (typeof rawInput.command === 'string' && rawInput.command) {
469
+ return { name: 'Bash', input: { command: rawInput.command } };
470
+ }
471
+ if (typeof rawInput.pattern === 'string' && rawInput.pattern) {
472
+ return { name: 'Grep', input: { pattern: rawInput.pattern, path: rawInput.paths || rawInput.path || '.' } };
473
+ }
474
+ if (typeof rawInput.path === 'string' && rawInput.path) {
475
+ const isEdit = String(update.kind || '').toLowerCase() === 'edit';
476
+ return { name: isEdit ? 'Edit' : 'Read', input: { file_path: rawInput.path } };
477
+ }
478
+ if (typeof rawInput.url === 'string' && rawInput.url) {
479
+ return { name: 'WebFetch', input: { url: rawInput.url } };
493
480
  }
481
+ return { name: title, input: {} };
494
482
  }
495
483
 
496
484
  // ── Public API ────────────────────────────────────────────────────────────
package/engine/cli.js CHANGED
@@ -731,19 +731,6 @@ const commands = {
731
731
  }
732
732
  }, 1000);
733
733
 
734
- // Teams inbox poll timer — process incoming Teams messages through CC
735
- const teams = require('./teams');
736
- const teamsInboxInterval = config.teams?.inboxPollInterval ?? shared.ENGINE_DEFAULTS.teams.inboxPollInterval;
737
- const teamsInboxTimer = teams.isTeamsEnabled() ? setInterval(() => {
738
- try {
739
- const ctrl = getControl();
740
- if (ctrl.state !== 'running') return;
741
- teams.processTeamsInbox().catch(err => {
742
- shared.log('warn', `Teams inbox poll error: ${err.message}`);
743
- });
744
- } catch {}
745
- }, teamsInboxInterval) : null;
746
-
747
734
  console.log(`Tick interval: ${interval / 1000}s | Max concurrent: ${config.engine?.maxConcurrent || 5}`);
748
735
  console.log('Press Ctrl+C to stop');
749
736
 
@@ -804,7 +791,6 @@ const commands = {
804
791
  console.log(`\n${signal} received — initiating graceful shutdown...`);
805
792
  clearInterval(tickTimer);
806
793
  clearInterval(fastPollTimer);
807
- if (teamsInboxTimer) clearInterval(teamsInboxTimer);
808
794
  for (const f of _watchedFiles) { try { fs.unwatchFile(f); } catch { /* cleanup */ } }
809
795
  const stoppingAt = e.ts();
810
796
  const stoppingWrite = markControlStoppingForOwner(controlOwner, stoppingAt);
package/engine/github.js CHANGED
@@ -646,14 +646,6 @@ async function pollPrStatus(config) {
646
646
  pr.reviewStatus = newReviewStatus;
647
647
  updated = true;
648
648
  shared.trackReviewMetric(pr, newReviewStatus, config);
649
- if (newReviewStatus === 'approved') {
650
- // Teams notification for PR approval — non-blocking, edge-triggered (only on transition)
651
- try {
652
- const teams = require('./teams');
653
- const prFilePath = shared.projectPrPath(project);
654
- teams.teamsNotifyPrEvent(pr, 'pr-approved', project, prFilePath).catch(() => {});
655
- } catch {}
656
- }
657
649
  }
658
650
  }
659
651
 
@@ -717,15 +709,6 @@ async function pollPrStatus(config) {
717
709
  }
718
710
  }
719
711
  updated = true;
720
-
721
- if (buildStatus === 'failing') {
722
- // Teams notification for build failure — non-blocking
723
- try {
724
- const teams = require('./teams');
725
- const prFilePath = shared.projectPrPath(project);
726
- teams.teamsNotifyPrEvent(pr, 'build-failed', project, prFilePath).catch(() => {});
727
- } catch {}
728
- }
729
712
  }
730
713
  if (buildStatus === 'failing') {
731
714
  if (buildFailReason && pr.buildFailReason !== buildFailReason) {