@yemi33/minions 0.1.1670 → 0.1.1671

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,9 +1,21 @@
1
1
  # Changelog
2
2
 
3
- ## 0.1.1670 (2026-05-02)
3
+ ## 0.1.1671 (2026-05-02)
4
+
5
+ ### Features
6
+ - durable meeting artifact ingestion (#1972)
7
+
8
+ ### Fixes
9
+ - shared-rules.md — declare done after push, do not wait for remote pipelines (#1970)
10
+ - sync pipeline card with modal after retrigger/abort/continue
11
+ - harden maxConcurrent parsing, routing case, and runtime exit codes
12
+
13
+ ### Other
14
+ - docs: condense CLAUDE.md
15
+
16
+ ## 0.1.1669 (2026-05-02)
4
17
 
5
18
  ### Features
6
- - make ADO PR titles authoritative (#1965)
7
19
  - gate pendingFix clear (#1964)
8
20
 
9
21
  ## 0.1.1668 (2026-05-01)
@@ -434,6 +434,7 @@ async function _refreshPipelineDetail(id) {
434
434
  if (fresh) {
435
435
  _pipelinesData = _pipelinesData.map(function(x) { return x.id === id ? fresh : x; });
436
436
  _pipelinePollHash = JSON.stringify({ runs: fresh.runs || [], enabled: fresh.enabled, _stoppedBy: fresh._stoppedBy, _stopReason: fresh._stopReason });
437
+ renderPipelines(_pipelinesData);
437
438
  openPipelineDetail(id);
438
439
  }
439
440
  } catch (e) { /* silent — next poll will catch up */ }
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-02T00:03:38.474Z"
4
+ "cachedAt": "2026-05-02T02:37:22.632Z"
5
5
  }
@@ -2529,7 +2529,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
2529
2529
  if (type === WORK_TYPE.MEETING && meta?.meetingId) {
2530
2530
  try {
2531
2531
  const { collectMeetingFindings } = require('./meeting');
2532
- collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout);
2532
+ collectMeetingFindings(meta.meetingId, agentId, meta.roundName, stdout, structuredCompletion);
2533
2533
  } catch (err) { log('warn', `Meeting collect: ${err.message}`); }
2534
2534
  }
2535
2535
 
package/engine/meeting.js CHANGED
@@ -19,6 +19,86 @@ const EMPTY_OUTPUT_PATTERNS = ['(no output)', '(no findings)', '(no response)'];
19
19
  // Derive from shared.MINIONS_DIR so createTestMinionsDir()/MINIONS_TEST_DIR
20
20
  // tests can redirect the meetings directory without patching module internals.
21
21
  const MEETINGS_DIR = path.join(shared.MINIONS_DIR, 'meetings');
22
+ const MEETING_NOTE_ARTIFACT_ROOT = path.join(shared.MINIONS_DIR, 'notes', 'inbox');
23
+
24
+ function isEmptyMeetingContent(text) {
25
+ const value = String(text || '').trim();
26
+ return !value || EMPTY_OUTPUT_PATTERNS.includes(value);
27
+ }
28
+
29
+ function isSuccessfulStructuredCompletion(completion) {
30
+ const status = String(completion?.status || completion?.outcome || '').trim().toLowerCase();
31
+ return ['success', 'succeeded', 'complete', 'completed', 'done', 'ok', 'passed'].includes(status);
32
+ }
33
+
34
+ function getStructuredNoteArtifacts(structuredCompletion) {
35
+ const artifacts = structuredCompletion?.artifacts;
36
+ if (!Array.isArray(artifacts)) return [];
37
+ return artifacts.filter(artifact =>
38
+ artifact &&
39
+ typeof artifact === 'object' &&
40
+ String(artifact.type || '').toLowerCase() === 'note' &&
41
+ typeof artifact.path === 'string' &&
42
+ artifact.path.trim()
43
+ );
44
+ }
45
+
46
+ function isPathInside(parent, child) {
47
+ const rel = path.relative(parent, child);
48
+ return rel && !rel.startsWith('..') && !path.isAbsolute(rel);
49
+ }
50
+
51
+ function resolveMeetingNoteArtifactPath(artifactPath) {
52
+ const raw = String(artifactPath || '').trim();
53
+ if (!raw || raw.includes('\0')) return null;
54
+ const resolved = path.resolve(path.isAbsolute(raw) ? raw : path.join(shared.MINIONS_DIR, raw));
55
+ const root = path.resolve(MEETING_NOTE_ARTIFACT_ROOT);
56
+ if (!isPathInside(root, resolved)) return null;
57
+ if (path.extname(resolved).toLowerCase() !== '.md') return null;
58
+ return resolved;
59
+ }
60
+
61
+ function readMeetingNoteArtifact(artifactPath) {
62
+ const resolved = resolveMeetingNoteArtifactPath(artifactPath);
63
+ if (!resolved) {
64
+ log('warn', `Ignoring unsafe meeting note artifact path: ${artifactPath || '(empty)'}`);
65
+ return '';
66
+ }
67
+ try {
68
+ const realRoot = fs.realpathSync(MEETING_NOTE_ARTIFACT_ROOT);
69
+ const realPath = fs.realpathSync(resolved);
70
+ if (!isPathInside(realRoot, realPath)) {
71
+ log('warn', `Ignoring meeting note artifact outside notes/inbox: ${artifactPath}`);
72
+ return '';
73
+ }
74
+ const content = fs.readFileSync(realPath, 'utf8');
75
+ return isEmptyMeetingContent(content) ? '' : content;
76
+ } catch (err) {
77
+ log('warn', `Meeting note artifact unreadable (${artifactPath}): ${err.message}`);
78
+ return '';
79
+ }
80
+ }
81
+
82
+ function resolveStructuredMeetingContent(structuredCompletion) {
83
+ if (!isSuccessfulStructuredCompletion(structuredCompletion)) return '';
84
+ const noteArtifacts = getStructuredNoteArtifacts(structuredCompletion);
85
+ if (noteArtifacts.length === 0) return '';
86
+
87
+ for (const artifact of noteArtifacts) {
88
+ const content = readMeetingNoteArtifact(artifact.path);
89
+ if (content) return content;
90
+ }
91
+
92
+ const summary = String(structuredCompletion.summary || '').trim();
93
+ return isEmptyMeetingContent(summary) ? '' : summary;
94
+ }
95
+
96
+ function resolveMeetingContributionContent(output, structuredCompletion) {
97
+ const { text } = shared.parseStreamJsonOutput(output, { maxTextLength: 50000 });
98
+ const rawContent = (text || '').trim();
99
+ if (!isEmptyMeetingContent(rawContent)) return rawContent;
100
+ return resolveStructuredMeetingContent(structuredCompletion);
101
+ }
22
102
 
23
103
  function truncateMeetingContext(text, maxBytes, label) {
24
104
  return shared.truncateTextBytes(text, maxBytes, `\n\n_...${label} truncated — review the meeting transcript if needed._`);
@@ -323,7 +403,7 @@ function discoverMeetingWork(config) {
323
403
  * Collect findings from a completed meeting agent.
324
404
  * Called from runPostCompletionHooks when type === 'meeting'.
325
405
  */
326
- function collectMeetingFindings(meetingId, agentId, roundName, output) {
406
+ function collectMeetingFindings(meetingId, agentId, roundName, output, structuredCompletion = null) {
327
407
  const meeting = getMeeting(meetingId);
328
408
  if (!meeting) return;
329
409
  if (meeting.status === 'completed' || meeting.status === 'archived') {
@@ -331,17 +411,15 @@ function collectMeetingFindings(meetingId, agentId, roundName, output) {
331
411
  return;
332
412
  }
333
413
 
334
- const { text } = shared.parseStreamJsonOutput(output, { maxTextLength: 50000 });
335
- const rawContent = (text || '').trim();
414
+ const content = resolveMeetingContributionContent(output, structuredCompletion);
336
415
 
337
416
  // Validate output — reject empty or placeholder responses
338
- if (!rawContent || EMPTY_OUTPUT_PATTERNS.includes(rawContent)) {
417
+ if (isEmptyMeetingContent(content)) {
339
418
  log('warn', `Meeting ${meetingId}: agent ${agentId} returned empty output for ${roundName} — rejecting`);
340
419
  // Don't record it — agent will be re-dispatched on next tick
341
420
  saveMeeting(meeting);
342
421
  return;
343
422
  }
344
- const content = rawContent;
345
423
 
346
424
  if (roundName === 'investigate') {
347
425
  meeting.findings[agentId] = { content, submittedAt: ts() };
package/engine/routing.js CHANGED
@@ -125,7 +125,8 @@ function normalizeWorkType(workType, fallback = WORK_TYPE.IMPLEMENT) {
125
125
 
126
126
  function routeForWorkType(workType) {
127
127
  const routes = getRoutingTableCached();
128
- return routes[normalizeWorkType(workType)] || routes[WORK_TYPE.IMPLEMENT] || { preferred: ANY_AGENT, fallback: ANY_AGENT };
128
+ const routeKey = normalizeWorkType(workType).toLowerCase();
129
+ return routes[routeKey] || routes[WORK_TYPE.IMPLEMENT] || { preferred: ANY_AGENT, fallback: ANY_AGENT };
129
130
  }
130
131
 
131
132
  function isAgentHardPinned(item) {
package/engine/shared.js CHANGED
@@ -445,12 +445,15 @@ function mutateCooldowns(mutator) {
445
445
  }, { defaultValue: {}, skipWriteIfUnchanged: true });
446
446
  }
447
447
 
448
+ let _uidCounter = 0;
449
+
448
450
  /**
449
- * Generate a unique ID suffix: timestamp + 4 random chars.
451
+ * Generate a unique ID suffix: timestamp + monotonic counter + random chars.
450
452
  * Use for filenames that could collide (dispatch IDs, temp files, etc.)
451
453
  */
452
454
  function uid() {
453
- return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
455
+ _uidCounter = (_uidCounter + 1) % 0x1000000;
456
+ return Date.now().toString(36) + _uidCounter.toString(36).padStart(4, '0') + crypto.randomBytes(2).toString('hex');
454
457
  }
455
458
 
456
459
  /**
@@ -122,6 +122,12 @@ function buildSpawnInvocation({ runtime, resolved, promptText, sysPromptText, op
122
122
  };
123
123
  }
124
124
 
125
+ function normalizeRuntimeExit(code, signal) {
126
+ if (Number.isInteger(code)) return code;
127
+ if (signal) return 128;
128
+ return 1;
129
+ }
130
+
125
131
  // ─── Main script execution ──────────────────────────────────────────────────
126
132
 
127
133
  function _installHint(name, runtime) {
@@ -278,12 +284,13 @@ function main() {
278
284
  }, MCP_STARTUP_TIMEOUT);
279
285
  proc.stdout.once('data', () => { gotFirstOutput = true; clearTimeout(startupTimer); });
280
286
 
281
- proc.on('close', (code) => {
287
+ proc.on('close', (code, signal) => {
282
288
  clearTimeout(startupTimer);
289
+ const exitCode = normalizeRuntimeExit(code, signal);
283
290
  // Write process-exit sentinel to stdout so the engine can detect completion (#716).
284
- try { process.stdout.write(`\n[process-exit] code=${code}\n`); } catch { /* stdout may be closed */ }
285
- fs.appendFileSync(debugPath, `EXIT: code=${code}\nSTDERR: ${stderrBuf.slice(0, 500)}\n`);
286
- process.exit(code || 0);
291
+ try { process.stdout.write(`\n[process-exit] code=${exitCode}${signal ? ` signal=${signal}` : ''}\n`); } catch { /* stdout may be closed */ }
292
+ fs.appendFileSync(debugPath, `EXIT: code=${exitCode}${signal ? ` signal=${signal}` : ''}\nSTDERR: ${stderrBuf.slice(0, 500)}\n`);
293
+ process.exit(exitCode);
287
294
  });
288
295
  proc.on('error', (err) => {
289
296
  fs.appendFileSync(debugPath, `ERROR: ${err.message}\n`);
@@ -291,6 +298,6 @@ function main() {
291
298
  });
292
299
  }
293
300
 
294
- module.exports = { parseSpawnArgs, buildSpawnInvocation };
301
+ module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit };
295
302
 
296
303
  if (require.main === module) main();
package/engine.js CHANGED
@@ -3543,7 +3543,7 @@ async function discoverWork(config) {
3543
3543
  );
3544
3544
  if (hasIncompleteImplements) {
3545
3545
  const activeCount = (getDispatch().active || []).length;
3546
- const maxConcurrent = config.engine?.maxConcurrent ?? ENGINE_DEFAULTS.maxConcurrent;
3546
+ const maxConcurrent = resolveMaxConcurrent(config);
3547
3547
  const freeSlots = Math.max(0, maxConcurrent - activeCount);
3548
3548
  if (freeSlots === 0) {
3549
3549
  if (allReviews.length > 0) {
@@ -3630,6 +3630,13 @@ function isSoftFixDispatch(item) {
3630
3630
  return item?.type === WORK_TYPE.FIX && !routing.isAgentHardPinned(item.meta?.item);
3631
3631
  }
3632
3632
 
3633
+ function resolveMaxConcurrent(config) {
3634
+ const raw = config?.engine?.maxConcurrent;
3635
+ if (raw === undefined || raw === null || raw === '') return ENGINE_DEFAULTS.maxConcurrent;
3636
+ const value = Number(raw);
3637
+ return Number.isFinite(value) && value >= 0 ? value : ENGINE_DEFAULTS.maxConcurrent;
3638
+ }
3639
+
3633
3640
  // ─── Main Tick ──────────────────────────────────────────────────────────────
3634
3641
 
3635
3642
  let tickCount = 0;
@@ -3936,7 +3943,7 @@ async function tickInner() {
3936
3943
  {
3937
3944
  const dispatchPre = getDispatch();
3938
3945
  const activeCountPre = (dispatchPre.active || []).length;
3939
- const maxC = config.engine?.maxConcurrent ?? ENGINE_DEFAULTS.maxConcurrent;
3946
+ const maxC = resolveMaxConcurrent(config);
3940
3947
  setTempBudget(Math.max(0, maxC - activeCountPre));
3941
3948
  }
3942
3949
  try { pruneStalePrDispatches(config); } catch (e) { log('warn', 'prune stale PR dispatches: ' + e.message); }
@@ -3954,7 +3961,7 @@ async function tickInner() {
3954
3961
  // 5. Process pending dispatches — auto-spawn agents
3955
3962
  const dispatch = getDispatch();
3956
3963
  const activeCount = (dispatch.active || []).length;
3957
- const maxConcurrent = config.engine?.maxConcurrent || 5;
3964
+ const maxConcurrent = resolveMaxConcurrent(config);
3958
3965
 
3959
3966
  const slotsAvailable = Math.max(0, maxConcurrent - activeCount);
3960
3967
 
@@ -4254,7 +4261,7 @@ module.exports = {
4254
4261
 
4255
4262
  // Tick
4256
4263
  tick,
4257
- _pollIntervalMsFromTicks, _shouldRunPeriodicPhase, // exported for testing
4264
+ resolveMaxConcurrent, _pollIntervalMsFromTicks, _shouldRunPeriodicPhase, // exported for testing
4258
4265
  };
4259
4266
 
4260
4267
  // ─── Entrypoint ─────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1670",
3
+ "version": "0.1.1671",
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"
@@ -64,6 +64,18 @@ Use `status: "failed"` plus an accurate `failure_class`, `retryable`, and `needs
64
64
 
65
65
  Builds, dependency installs, tests, and local servers can be quiet for long periods. Run the repo's normal CLI commands and let them finish; do not add artificial progress output, heartbeat loops, or command-specific workarounds just to keep Minions active.
66
66
 
67
+ ## Done = pushed + local validation. Do NOT wait for remote pipelines.
68
+
69
+ Your dispatch is **done** the moment (1) your fix is pushed to the branch and (2) any local validation you ran has finished. Write the completion JSON and exit immediately. **Do not** wait for the remote PR pipeline (Android OCM PR build, Espresso CloudTest, GitHub Actions, etc.) to finish before declaring done.
70
+
71
+ This applies to **every** fix dispatch, including `build-fix` tasks. Pipeline failures route back through separate engine paths (a new `build-fix` dispatch will be queued if the remote build fails); your job ends at push.
72
+
73
+ Concretely:
74
+ - After `git push`, write the completion report and exit. Do not start a `monitor`/`read_powershell`/`watch` loop on the pipeline.
75
+ - Do not sleep or busy-wait for `mergeStatus`, `buildStatus`, or any ADO/GitHub API to flip from `running` to `passing`.
76
+ - If you skipped local validation, say so in the completion JSON (e.g. `tests: skipped — relying on PR pipeline`) and still exit.
77
+ - Holding a slot to watch a pipeline is wasted capacity; the engine has its own pipeline-monitoring path.
78
+
67
79
  ## Checking PR and Build Status
68
80
 
69
81
  When asked to check build status, CI results, or review state for a PR: