@yemi33/minions 0.1.934 → 0.1.936

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 CHANGED
@@ -1,8 +1,10 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.934 (2026-04-14)
3
+ ## 0.1.936 (2026-04-14)
4
4
 
5
5
  ### Features
6
+ - make ADO poll frequency configurable and ungate reconcilePrs
7
+ - certificate-based auth for Teams integration (#1027)
6
8
  - add adoPollEnabled/ghPollEnabled engine settings
7
9
  - doc-chat abort kills LLM process + queued messages auto-process
8
10
  - add /api/work-items/reopen endpoint and reopen-work-item CC action (#982)
@@ -14,6 +14,7 @@ async function openSettings() {
14
14
  const e = data.engine || {};
15
15
  const c = data.claude || {};
16
16
  const agents = data.agents || {};
17
+ const t = data.teams || {};
17
18
 
18
19
  const agentRows = Object.entries(agents).map(function([id, a]) {
19
20
  return '<tr>' +
@@ -48,8 +49,12 @@ async function openSettings() {
48
49
  settingsToggle('Auto-decompose', 'set-autoDecompose', e.autoDecompose !== false, 'Large implement items are auto-split into sub-tasks') +
49
50
  settingsToggle('Allow Temp Agents', 'set-allowTempAgents', !!e.allowTempAgents, 'Spawn ephemeral agents when all permanent agents are busy') +
50
51
  settingsToggle('Auto-archive Plans', 'set-autoArchive', !!e.autoArchive, 'Automatically archive plans after verify completes (off = manual archive via dashboard)') +
51
- settingsToggle('ADO Polling', 'set-adoPollEnabled', e.adoPollEnabled !== false, 'Poll ADO PR status, comments, and reconciliation on each tick cycle') +
52
- settingsToggle('GitHub Polling', 'set-ghPollEnabled', e.ghPollEnabled !== false, 'Poll GitHub PR status, comments, and reconciliation on each tick cycle') +
52
+ settingsToggle('ADO Polling', 'set-adoPollEnabled', e.adoPollEnabled !== false, 'Poll ADO PR status and comments each tick (reconciliation always runs)') +
53
+ settingsToggle('GitHub Polling', 'set-ghPollEnabled', e.ghPollEnabled !== false, 'Poll GitHub PR status and comments each tick (reconciliation always runs)') +
54
+ '</div>' +
55
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
56
+ settingsField('ADO Status Poll Frequency', 'set-adoPollStatusEvery', e.adoPollStatusEvery || 6, 'ticks', 'Poll ADO PR build/review/merge status every N ticks (~6 min at default tick rate)') +
57
+ settingsField('ADO Comments Poll Frequency', 'set-adoPollCommentsEvery', e.adoPollCommentsEvery || 12, 'ticks', 'Poll ADO PR human comments every N ticks (~12 min at default tick rate)') +
53
58
  '</div>' +
54
59
  '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
55
60
  settingsField('Eval Max Iterations', 'set-evalMaxIterations', e.evalMaxIterations || 3, '', 'Max review→fix cycles before escalating (1-10)') +
@@ -96,6 +101,28 @@ async function openSettings() {
96
101
  '</div>' +
97
102
  '</div>' +
98
103
 
104
+ '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Teams Integration</h3>' +
105
+ '<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:8px">' +
106
+ settingsToggle('Enable Teams', 'set-teams-enabled', !!t.enabled, 'Connect Minions to Microsoft Teams') +
107
+ '</div>' +
108
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">' +
109
+ settingsField('App ID', 'set-teams-appId', t.appId || '', '', 'Microsoft App ID from Azure Bot Configuration') +
110
+ settingsField('App Password', 'set-teams-appPassword', t.appPassword || '', '', 'Client secret (leave blank for certificate auth)') +
111
+ '</div>' +
112
+ '<div style="font-size:10px;color:var(--muted);margin-bottom:4px;font-weight:600">Certificate Auth (alternative to client secret)</div>' +
113
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">' +
114
+ settingsField('Certificate Path', 'set-teams-certPath', t.certPath || '', '', 'Path to PEM certificate file') +
115
+ settingsField('Private Key Path', 'set-teams-privateKeyPath', t.privateKeyPath || '', '', 'Path to PEM private key file') +
116
+ settingsField('Tenant ID', 'set-teams-tenantId', t.tenantId || '', '', 'Azure AD tenant ID (required for cert auth)') +
117
+ '</div>' +
118
+ '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
119
+ settingsField('Notify Events', 'set-teams-notifyEvents', (t.notifyEvents || []).join(', '), '', 'Comma-separated event types to notify') +
120
+ settingsField('Inbox Poll Interval', 'set-teams-inboxPollInterval', t.inboxPollInterval || 15000, 'ms', 'How often to check for Teams messages') +
121
+ '</div>' +
122
+ '<div style="display:flex;flex-direction:column;gap:6px;margin-bottom:16px">' +
123
+ settingsToggle('CC Mirror', 'set-teams-ccMirror', t.ccMirror !== false, 'Mirror Command Center responses to Teams') +
124
+ '</div>' +
125
+
99
126
  '<h3 style="font-size:13px;color:var(--blue);margin-bottom:8px">Claude CLI</h3>' +
100
127
  '<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px">' +
101
128
  settingsField('Output Format', 'set-outputFormat', c.outputFormat || 'stream-json', '', '') +
@@ -212,6 +239,8 @@ async function saveSettings() {
212
239
  autoArchive: document.getElementById('set-autoArchive').checked,
213
240
  adoPollEnabled: document.getElementById('set-adoPollEnabled').checked,
214
241
  ghPollEnabled: document.getElementById('set-ghPollEnabled').checked,
242
+ adoPollStatusEvery: document.getElementById('set-adoPollStatusEvery').value,
243
+ adoPollCommentsEvery: document.getElementById('set-adoPollCommentsEvery').value,
215
244
  evalMaxIterations: document.getElementById('set-evalMaxIterations').value,
216
245
  evalMaxCost: document.getElementById('set-evalMaxCost').value || null,
217
246
  maxBuildFixAttempts: document.getElementById('set-maxBuildFixAttempts').value,
@@ -236,6 +265,18 @@ async function saveSettings() {
236
265
  permissionMode: document.getElementById('set-permissionMode').value,
237
266
  };
238
267
 
268
+ const teamsPayload = {
269
+ enabled: document.getElementById('set-teams-enabled').checked,
270
+ appId: document.getElementById('set-teams-appId').value,
271
+ appPassword: document.getElementById('set-teams-appPassword').value,
272
+ certPath: document.getElementById('set-teams-certPath').value,
273
+ privateKeyPath: document.getElementById('set-teams-privateKeyPath').value,
274
+ tenantId: document.getElementById('set-teams-tenantId').value,
275
+ notifyEvents: document.getElementById('set-teams-notifyEvents').value,
276
+ inboxPollInterval: document.getElementById('set-teams-inboxPollInterval').value,
277
+ ccMirror: document.getElementById('set-teams-ccMirror').checked,
278
+ };
279
+
239
280
  const agentsPayload = {};
240
281
  document.querySelectorAll('[data-agent][data-field]').forEach(function(el) {
241
282
  const id = el.dataset.agent;
@@ -247,7 +288,7 @@ async function saveSettings() {
247
288
  // Save config
248
289
  const res = await fetch('/api/settings', {
249
290
  method: 'POST', headers: { 'Content-Type': 'application/json' },
250
- body: JSON.stringify({ engine: enginePayload, claude: claudePayload, agents: agentsPayload })
291
+ body: JSON.stringify({ engine: enginePayload, claude: claudePayload, agents: agentsPayload, teams: teamsPayload })
251
292
  });
252
293
  const result = await res.json();
253
294
  if (!res.ok) throw new Error(result.error);
package/dashboard.js CHANGED
@@ -3702,6 +3702,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3702
3702
  engine: { ...shared.ENGINE_DEFAULTS, ...(config.engine || {}) },
3703
3703
  claude: { ...shared.DEFAULT_CLAUDE, ...(config.claude || {}) },
3704
3704
  agents: config.agents || {},
3705
+ teams: { ...shared.ENGINE_DEFAULTS.teams, ...(config.teams || {}) },
3705
3706
  routing,
3706
3707
  });
3707
3708
  } catch (e) { return jsonReply(res, 500, { error: e.message }); }
@@ -3729,6 +3730,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3729
3730
  meetingRoundTimeout: [60000],
3730
3731
  versionCheckInterval: [60000],
3731
3732
  maxBuildFixAttempts: [1, 10],
3733
+ adoPollStatusEvery: [1], adoPollCommentsEvery: [1],
3732
3734
  };
3733
3735
  for (const [key, [min, max]] of Object.entries(numericFields)) {
3734
3736
  if (e[key] !== undefined) {
@@ -3795,6 +3797,22 @@ What would you like to discuss or change? When you're happy, say "approve" and I
3795
3797
  }
3796
3798
  }
3797
3799
 
3800
+ if (body.teams) {
3801
+ if (!config.teams) config.teams = {};
3802
+ const tm = body.teams;
3803
+ if (tm.enabled !== undefined) config.teams.enabled = !!tm.enabled;
3804
+ for (const key of ['appId', 'appPassword', 'certPath', 'privateKeyPath', 'tenantId']) {
3805
+ if (tm[key] !== undefined) config.teams[key] = String(tm[key] || '');
3806
+ }
3807
+ if (tm.notifyEvents !== undefined) {
3808
+ config.teams.notifyEvents = Array.isArray(tm.notifyEvents) ? tm.notifyEvents : String(tm.notifyEvents || '').split(',').map(s => s.trim()).filter(Boolean);
3809
+ }
3810
+ if (tm.inboxPollInterval !== undefined) config.teams.inboxPollInterval = Math.max(5000, Number(tm.inboxPollInterval) || 15000);
3811
+ if (tm.ccMirror !== undefined) config.teams.ccMirror = !!tm.ccMirror;
3812
+ // Invalidate cached adapter so credential changes take effect
3813
+ teams._resetAdapter();
3814
+ }
3815
+
3798
3816
  safeWrite(configPath, config);
3799
3817
  // Refresh in-memory CONFIG so subsequent reads see the update
3800
3818
  reloadConfig();
@@ -4571,7 +4589,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4571
4589
 
4572
4590
  // Settings
4573
4591
  { method: 'GET', path: '/api/settings', desc: 'Return current engine + claude + routing config', handler: handleSettingsRead },
4574
- { method: 'POST', path: '/api/settings', desc: 'Update engine + claude + agent config', params: 'engine?, claude?, agents?', handler: handleSettingsUpdate },
4592
+ { method: 'POST', path: '/api/settings', desc: 'Update engine + claude + agent + teams config', params: 'engine?, claude?, agents?, teams?', handler: handleSettingsUpdate },
4575
4593
  { method: 'POST', path: '/api/settings/routing', desc: 'Update routing.md', params: 'content', handler: handleSettingsRouting },
4576
4594
  { method: 'POST', path: '/api/settings/reset', desc: 'Reset engine + claude + agent settings to defaults', handler: handleSettingsReset },
4577
4595
 
@@ -84,7 +84,10 @@ Add a `teams` section to your `config.json`:
84
84
  |-------|-------------|
85
85
  | `enabled` | Master switch — `true` to activate Teams integration |
86
86
  | `appId` | Microsoft App ID from the Azure Bot Configuration page |
87
- | `appPassword` | Client secret value from Entra ID Certificates & secrets |
87
+ | `appPassword` | Client secret value from Entra ID Certificates & secrets (leave blank for certificate auth) |
88
+ | `certPath` | Path to PEM certificate file (certificate auth only) |
89
+ | `privateKeyPath` | Path to PEM private key file (certificate auth only) |
90
+ | `tenantId` | Azure AD tenant ID (required for certificate auth) |
88
91
  | `notifyEvents` | Which events trigger Teams notifications (see below) |
89
92
  | `ccMirror` | Mirror CC dashboard responses to Teams (`true`/`false`) |
90
93
  | `inboxPollInterval` | How often to check for new Teams messages, in ms (default: 15000) |
@@ -93,6 +96,55 @@ Add a `teams` section to your `config.json`:
93
96
 
94
97
  > **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
98
 
99
+ ### Certificate Auth (Alternative to Client Secret)
100
+
101
+ Some tenants prohibit creating client secrets in Entra ID. Use certificate-based authentication instead — no client secret needed.
102
+
103
+ #### Generate a Self-Signed Certificate
104
+
105
+ ```bash
106
+ # Generate a private key and self-signed certificate (valid for 1 year)
107
+ openssl req -x509 -newkey rsa:2048 -keyout bot-private-key.pem -out bot-cert.pem -days 365 -nodes -subj "/CN=minions-bot"
108
+
109
+ # Verify the certificate
110
+ openssl x509 -in bot-cert.pem -text -noout
111
+ ```
112
+
113
+ This creates two files:
114
+ - `bot-cert.pem` — the public certificate (upload to Entra ID)
115
+ - `bot-private-key.pem` — the private key (keep secret, do not commit)
116
+
117
+ #### Upload the Certificate to Entra ID
118
+
119
+ 1. In the [Azure Portal](https://portal.azure.com), go to **Entra ID** > **App registrations**.
120
+ 2. Find and click the app registration for your bot (same App ID from Step 3).
121
+ 3. Click **Certificates & secrets** in the left sidebar.
122
+ 4. Click the **Certificates** tab, then **Upload certificate**.
123
+ 5. Select your `bot-cert.pem` file and click **Add**.
124
+
125
+ #### Configure Minions for Certificate Auth
126
+
127
+ Use certificate fields instead of `appPassword` in your `config.json`:
128
+
129
+ ```json
130
+ {
131
+ "teams": {
132
+ "enabled": true,
133
+ "appId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
134
+ "certPath": "/path/to/bot-cert.pem",
135
+ "privateKeyPath": "/path/to/bot-private-key.pem",
136
+ "tenantId": "your-azure-ad-tenant-id",
137
+ "notifyEvents": ["pr-merged", "agent-completed", "plan-completed", "agent-failed"],
138
+ "ccMirror": true,
139
+ "inboxPollInterval": 15000
140
+ }
141
+ }
142
+ ```
143
+
144
+ > **Finding your Tenant ID:** In the Azure Portal, go to **Entra ID** > **Overview**. The **Tenant ID** is listed under Basic Information.
145
+
146
+ > When both `appPassword` and cert fields are configured, certificate auth takes precedence.
147
+
96
148
  ## Step 4: Set the Messaging Endpoint
97
149
 
98
150
  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.
@@ -193,14 +193,20 @@ function doctor(minionsHome) {
193
193
  runtimeResults.push({ name: 'Agents configured', ok: false, message: 'no agents in config.json' });
194
194
  }
195
195
 
196
- // Check Teams integration
196
+ // Check Teams integration — supports client secret OR certificate auth
197
197
  const teams = config.teams;
198
198
  if (teams && teams.enabled === true) {
199
- if (!teams.appId || !teams.appPassword) {
200
- const missing = [!teams.appId && 'appId', !teams.appPassword && 'appPassword'].filter(Boolean).join(', ');
199
+ const hasSecret = !!teams.appId && !!teams.appPassword;
200
+ const hasCert = !!teams.appId && !!teams.certPath && !!teams.privateKeyPath && !!teams.tenantId;
201
+ if (!hasSecret && !hasCert) {
202
+ const missing = [
203
+ !teams.appId && 'appId',
204
+ !teams.appPassword && !teams.certPath && 'appPassword or certPath+privateKeyPath+tenantId',
205
+ ].filter(Boolean).join(', ');
201
206
  runtimeResults.push({ name: 'Teams integration', ok: 'warn', message: `enabled but missing: ${missing}` });
202
207
  } else {
203
- runtimeResults.push({ name: 'Teams integration', ok: true, message: 'configured' });
208
+ const authMode = hasCert ? 'certificate' : 'client secret';
209
+ runtimeResults.push({ name: 'Teams integration', ok: true, message: `configured (${authMode})` });
204
210
  }
205
211
  } else {
206
212
  runtimeResults.push({ name: 'Teams integration', ok: 'warn', message: 'disabled — see docs/teams-setup.md' });
package/engine/shared.js CHANGED
@@ -549,6 +549,8 @@ const ENGINE_DEFAULTS = {
549
549
  buildFixGracePeriod: 600000, // 10min — wait for CI to run after build fix before re-dispatching
550
550
  adoPollEnabled: true, // poll ADO PR status, comments, and reconciliation on each tick cycle
551
551
  ghPollEnabled: true, // poll GitHub PR status, comments, and reconciliation on each tick cycle
552
+ adoPollStatusEvery: 6, // poll ADO PR build/review/merge status every N ticks (~6 min at default interval)
553
+ adoPollCommentsEvery: 12, // poll ADO PR human comments every N ticks (~12 min at default interval)
552
554
  autoCompletePrs: false, // auto-merge PRs when builds green + review approved (opt-in)
553
555
  prMergeMethod: 'squash', // merge method: squash, merge, rebase
554
556
  ignoredCommentAuthors: [], // comments from these authors are auto-closed and never trigger fixes
@@ -556,11 +558,15 @@ const ENGINE_DEFAULTS = {
556
558
  ccEffort: null, // effort level for CC/doc-chat (null, 'low', 'medium', 'high')
557
559
  heartbeatTimeouts: {}, // populated after WORK_TYPE is defined (below)
558
560
  ccMaxTurns: 50, // max tool-use turns for CC/doc-chat before CLI stops
559
- // Teams integration — config.teams shape: { enabled, appId, appPassword, notifyEvents, ccMirror, inboxPollInterval }
561
+ // Teams integration — config.teams shape: { enabled, appId, appPassword, certPath, privateKeyPath, tenantId, notifyEvents, ccMirror, inboxPollInterval }
562
+ // Auth modes: (1) appId + appPassword (client secret), or (2) appId + certPath + privateKeyPath + tenantId (certificate)
560
563
  teams: {
561
564
  enabled: false,
562
565
  appId: '',
563
566
  appPassword: '',
567
+ certPath: '', // PEM certificate file path (certificate auth)
568
+ privateKeyPath: '', // PEM private key file path (certificate auth)
569
+ tenantId: '', // Azure AD tenant ID (required for certificate auth)
564
570
  notifyEvents: ['pr-merged', 'agent-completed', 'plan-completed', 'agent-failed'],
565
571
  ccMirror: true,
566
572
  inboxPollInterval: 15000,
package/engine/teams.js CHANGED
@@ -8,7 +8,7 @@ const path = require('path');
8
8
  const shared = require('./shared');
9
9
  const queries = require('./queries');
10
10
 
11
- const { log, safeJson, mutateJsonFileLocked, ENGINE_DEFAULTS } = shared;
11
+ const { log, safeRead, safeJson, mutateJsonFileLocked, ENGINE_DEFAULTS } = shared;
12
12
  const { ENGINE_DIR, getConfig } = queries;
13
13
  const cards = require('./teams-cards');
14
14
 
@@ -50,10 +50,16 @@ function getTeamsConfig() {
50
50
 
51
51
  /**
52
52
  * Returns true if Teams integration is enabled and has required credentials.
53
+ * Supports two auth modes:
54
+ * (1) Client secret: appId + appPassword
55
+ * (2) Certificate: appId + certPath + privateKeyPath + tenantId
53
56
  */
54
57
  function isTeamsEnabled() {
55
58
  const cfg = getTeamsConfig();
56
- return cfg.enabled === true && !!cfg.appId && !!cfg.appPassword;
59
+ if (cfg.enabled !== true || !cfg.appId) return false;
60
+ const hasSecret = !!cfg.appPassword;
61
+ const hasCert = !!cfg.certPath && !!cfg.privateKeyPath && !!cfg.tenantId;
62
+ return hasSecret || hasCert;
57
63
  }
58
64
 
59
65
  // Cached adapter instance — created once per process
@@ -62,6 +68,9 @@ let _adapter = null;
62
68
  /**
63
69
  * Create and return a BotFrameworkAdapter instance.
64
70
  * Returns null when Teams is disabled or botbuilder is not installed.
71
+ * Supports two auth modes:
72
+ * (1) Client secret: uses ConfigurationBotFrameworkAuthentication with appPassword
73
+ * (2) Certificate: uses CertificateServiceClientCredentialsFactory with PEM cert + key
65
74
  */
66
75
  function createAdapter() {
67
76
  if (_adapter) return _adapter;
@@ -75,15 +84,43 @@ function createAdapter() {
75
84
  if (!botbuilder) return null;
76
85
 
77
86
  const cfg = getTeamsConfig();
87
+ const useCert = !!cfg.certPath && !!cfg.privateKeyPath && !!cfg.tenantId;
88
+
78
89
  try {
79
- _adapter = new botbuilder.CloudAdapter(
80
- new botbuilder.ConfigurationBotFrameworkAuthentication({
81
- MicrosoftAppId: cfg.appId,
82
- MicrosoftAppPassword: cfg.appPassword,
83
- MicrosoftAppType: 'SingleTenant',
84
- })
85
- );
86
- log('info', 'Teams adapter created successfully');
90
+ if (useCert) {
91
+ let connector;
92
+ try {
93
+ connector = require('botframework-connector');
94
+ } catch {
95
+ log('warn', 'botframework-connector not installed — certificate auth unavailable. Install via: npm install botframework-connector');
96
+ return null;
97
+ }
98
+ const cert = safeRead(cfg.certPath);
99
+ const privateKey = safeRead(cfg.privateKeyPath);
100
+ if (!cert || !privateKey) {
101
+ log('warn', `Teams cert auth failed — could not read cert (${cfg.certPath}) or key (${cfg.privateKeyPath})`);
102
+ return null;
103
+ }
104
+ const credentialsFactory = new connector.CertificateServiceClientCredentialsFactory(
105
+ cfg.appId, cert, privateKey, cfg.tenantId
106
+ );
107
+ _adapter = new botbuilder.CloudAdapter(
108
+ new botbuilder.ConfigurationBotFrameworkAuthentication(
109
+ { MicrosoftAppId: cfg.appId, MicrosoftAppType: 'SingleTenant', MicrosoftAppTenantId: cfg.tenantId },
110
+ credentialsFactory
111
+ )
112
+ );
113
+ log('info', 'Teams adapter created (certificate auth)');
114
+ } else {
115
+ _adapter = new botbuilder.CloudAdapter(
116
+ new botbuilder.ConfigurationBotFrameworkAuthentication({
117
+ MicrosoftAppId: cfg.appId,
118
+ MicrosoftAppPassword: cfg.appPassword,
119
+ MicrosoftAppType: 'SingleTenant',
120
+ })
121
+ );
122
+ log('info', 'Teams adapter created (client secret auth)');
123
+ }
87
124
  return _adapter;
88
125
  } catch (err) {
89
126
  log('warn', `Teams adapter creation failed: ${err.message}`);
package/engine.js CHANGED
@@ -3122,11 +3122,13 @@ async function tickInner() {
3122
3122
 
3123
3123
  const adoPollEnabled = config.engine?.adoPollEnabled ?? DEFAULTS.adoPollEnabled;
3124
3124
  const ghPollEnabled = config.engine?.ghPollEnabled ?? DEFAULTS.ghPollEnabled;
3125
+ const adoPollStatusEvery = Math.max(1, Number(config.engine?.adoPollStatusEvery) || DEFAULTS.adoPollStatusEvery);
3126
+ const adoPollCommentsEvery = Math.max(1, Number(config.engine?.adoPollCommentsEvery) || DEFAULTS.adoPollCommentsEvery);
3125
3127
 
3126
- // 2.6. Poll PR status: build, review, merge (every 6 ticks = ~3 minutes)
3128
+ // 2.6. Poll PR status: build, review, merge (every adoPollStatusEvery ticks, default ~6 minutes)
3127
3129
  // Awaited so PR state is consistent before discoverWork reads it
3128
3130
  // Also re-polls early if previous tick had ADO auth failures (stale build status recovery)
3129
- if (tickCount % 6 === 0 || needsAdoPollRetry()) {
3131
+ if (tickCount % adoPollStatusEvery === 0 || needsAdoPollRetry()) {
3130
3132
  if (adoPollEnabled) {
3131
3133
  try { await pollPrStatus(config); } catch (err) { log('warn', `ADO PR status poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
3132
3134
  }
@@ -3152,16 +3154,17 @@ async function tickInner() {
3152
3154
  } catch (err) { log('warn', `Plan completion check error: ${err?.message || err}`); }
3153
3155
  }
3154
3156
 
3155
- // 2.7. Poll PR threads for human comments (every 12 ticks = ~6 minutes)
3156
- if (tickCount % 12 === 0) {
3157
+ // 2.7. Poll PR threads for human comments (every adoPollCommentsEvery ticks, default ~12 minutes)
3158
+ if (tickCount % adoPollCommentsEvery === 0) {
3157
3159
  if (adoPollEnabled) {
3158
3160
  try { await pollPrHumanComments(config); } catch (err) { log('warn', `ADO PR comment poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
3159
- try { await reconcilePrs(config); } catch (err) { log('warn', `ADO PR reconciliation error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
3160
3161
  }
3161
3162
  if (ghPollEnabled) {
3162
3163
  try { await ghPollPrHumanComments(config); } catch (err) { log('warn', `GitHub PR comment poll error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
3163
- try { await ghReconcilePrs(config); } catch (err) { log('warn', `GitHub PR reconciliation error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
3164
3164
  }
3165
+ // Reconciliation runs regardless of poll flags — it's a recovery sweep, not a convenience poll
3166
+ try { await reconcilePrs(config); } catch (err) { log('warn', `ADO PR reconciliation error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
3167
+ try { await ghReconcilePrs(config); } catch (err) { log('warn', `GitHub PR reconciliation error: ${err?.message || err}${err?.stack ? ' | ' + err.stack.split('\n')[1]?.trim() : ''}`); }
3165
3168
  }
3166
3169
 
3167
3170
  // 2.9. Stalled dispatch detection — auto-retry failed items blocking the graph (every 20 ticks = ~10 min)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.934",
3
+ "version": "0.1.936",
4
4
  "description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
5
5
  "bin": {
6
6
  "minions": "bin/minions.js"