@yemi33/minions 0.1.1644 → 0.1.1646

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,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1646 (2026-05-01)
4
+
5
+ ### Fixes
6
+ - scope syncPrsFromOutput inbox fallback to empty stdout
7
+ - execAsync no longer unref's child, fixes silent test exit
8
+
9
+ ### Other
10
+ - Prevent CLI dispatch from orphaning agents
11
+ - Harden eval loop gating audit
12
+ - chore: gitignore agent runtime state and stray repo-root artifacts
13
+ - revert: remove CC hallucination detector — too many false positives
14
+
15
+ ## 0.1.1645 (2026-04-30)
16
+
17
+ ### Other
18
+ - test(queries): add unit tests for timeSince, readHeadTail, detectInFlightTool, getInboxFiles (#1903)
19
+
3
20
  ## 0.1.1644 (2026-04-30)
4
21
 
5
22
  ### Fixes
package/README.md CHANGED
@@ -545,7 +545,7 @@ Engine behavior is controlled via `config.json`. Key settings:
545
545
  | `autoDecompose` | true | Auto-decompose `implement:large` items into sub-tasks before dispatch |
546
546
  | `autoApprovePlans` | false | Auto-approve PRDs without waiting for human approval |
547
547
  | `evalLoop` | true | Auto-dispatch review → fix cycles after implementation completes |
548
- | `evalMaxIterations` | 3 | Max review → fix cycles before escalating to human |
548
+ | `evalMaxIterations` | 3 | Max minion review → fix cycles before pausing minion review automation; human feedback, build fixes, and conflict fixes continue |
549
549
  | `evalMaxCost` | null | USD ceiling per work item across all eval iterations (null = no limit) |
550
550
  | `meetingRoundTimeout` | 600000 (10min) | Timeout per meeting round before auto-advance |
551
551
  | `ccModel` | `sonnet` | Model for Command Center and doc-chat (sonnet/haiku/opus) |
@@ -706,10 +706,6 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
706
706
  // Surface as an inline warning so the user knows actions were dropped
707
707
  // (was previously silent — appeared as "actions failed" with no signal).
708
708
  addMsg('system', '<div style="padding:6px 12px;font-size:11px;color:var(--red);background:var(--surface2);border-radius:6px;margin:4px 0">⚠️ Actions block emitted but JSON could not be parsed — no actions were executed. Resend or rephrase. (' + escHtml(String(evt.actionParseError).slice(0, 200)) + ')</div>', false, activeTabId);
709
- } else if (evt.hallucinationWarning) {
710
- // CC said it dispatched/queued/assigned something but emitted no actions block —
711
- // surface the false-claim guard so the user knows nothing actually ran.
712
- addMsg('system', '<div style="padding:6px 12px;font-size:11px;color:var(--orange);background:var(--surface2);border-radius:6px;margin:4px 0">⚠️ ' + escHtml(evt.hallucinationWarning) + '</div>', false, activeTabId);
713
709
  }
714
710
  } else if (evt.type === 'error') {
715
711
  terminalEventSeen = true;
package/dashboard.js CHANGED
@@ -1345,16 +1345,6 @@ function _ccValidateAction(action) {
1345
1345
  }
1346
1346
  }
1347
1347
 
1348
- // Hallucination guard: detect prose like "I dispatched ..." when no ===ACTIONS=== block was emitted.
1349
- // The regex is intentionally narrow — we only want affirmative claims about completed work, not
1350
- // hypotheticals like "I would dispatch this" or "consider dispatching X".
1351
- function _detectClaimedActionWithoutBlock(displayText, actions) {
1352
- if (Array.isArray(actions) && actions.length > 0) return null; // there are actions, no false claim
1353
- const triggers = /\b(dispatched|enqueued|queued|created (?:a |the )?work item|assigned (?:this |it )?(?:to|for)|spun up|kicked off|i'?ll dispatch|i (?:have )?(?:just )?dispatched)\b/i;
1354
- if (!triggers.test(displayText || '')) return null;
1355
- return 'CC described an action ("dispatched", "assigned", etc.) but no ===ACTIONS=== block was emitted. No work was actually queued. Resend or rephrase the request.';
1356
- }
1357
-
1358
1348
  async function executeCCActions(actions) {
1359
1349
  const results = [];
1360
1350
  for (const action of actions) {
@@ -4864,8 +4854,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
4864
4854
  const { _actionParseError, ...parsedReply } = parsed;
4865
4855
  const reply = { ...parsedReply, sessionId: ccSession.sessionId, newSession: !wasResume };
4866
4856
  if (_actionParseError) reply.actionParseError = _actionParseError;
4867
- const hallucinationWarning = _detectClaimedActionWithoutBlock(parsed.text, parsed.actions);
4868
- if (hallucinationWarning) reply.hallucinationWarning = hallucinationWarning;
4869
4857
  if (sessionReset) reply.sessionReset = true;
4870
4858
  return jsonReply(res, 200, reply);
4871
4859
  } finally {
@@ -5149,8 +5137,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5149
5137
  // Issue #1834: surface action JSON parse failures so the UI can warn
5150
5138
  // instead of silently dropping. Client renders this as a small notice.
5151
5139
  if (_actionParseError) donePayload.actionParseError = _actionParseError;
5152
- const hallucinationWarning = _detectClaimedActionWithoutBlock(displayText, actions);
5153
- if (hallucinationWarning) donePayload.hallucinationWarning = hallucinationWarning;
5154
5140
  if (sessionReset) donePayload.sessionReset = true;
5155
5141
  liveState.donePayload = donePayload;
5156
5142
  if (liveState.writer) liveState.writer(donePayload);
@@ -6550,7 +6536,6 @@ module.exports = {
6550
6536
  _resolveSkillReadPath,
6551
6537
  DOC_CHAT_DOCUMENT_DELIMITER,
6552
6538
  _ccValidateAction,
6553
- _detectClaimedActionWithoutBlock,
6554
6539
  executeCCActions,
6555
6540
  };
6556
6541
 
@@ -37,7 +37,7 @@ Before scanning, the engine materializes plans and specs into project work items
37
37
  | `_mergeConflict: true` | Route to author for conflict resolution | `fix` |
38
38
  Skips PRs where `status !== "active"`.
39
39
 
40
- PR fix triggers are evaluated in this source order inside `discoverFromPrs()`: review feedback first (`engine.js:2166-2180`), human feedback second (`engine.js:2191-2226`), build failure third (`engine.js:2229-2271`), and merge conflict fourth (`engine.js:2299-2317`). Conflict fixes are additionally gated by `!fixDispatched` (`engine.js:2301`), so any earlier successful fix dispatch in the same PR discovery pass suppresses the conflict fix until a later pass.
40
+ Inside `discoverFromPrs()`, `evalLoop` / `evalMaxIterations` only gate the minion review loop: initial minion reviews, minion re-reviews, and minion review-feedback fixes. Human-feedback fixes are evaluated outside that gate, build failures use the separate `maxBuildFixAttempts` cap, and merge conflicts use the separate `autoFixConflicts` gate. Conflict fixes are additionally gated by `!fixDispatched`, so an earlier successful human/review/build fix dispatch in the same PR discovery pass suppresses the conflict fix until a later pass.
41
41
 
42
42
  ### Source 2: PRD Gap Analysis (via `materializePlansAsWorkItems`)
43
43
 
@@ -23,30 +23,34 @@ How the engine manages the lifecycle of a PR from creation through review, fix,
23
23
 
24
24
  ## 4. Fix dispatch trigger order
25
25
 
26
- `discoverFromPrs()` evaluates PR auto-fix triggers in a fixed order during each discovery pass:
26
+ `discoverFromPrs()` evaluates PR review/fix triggers in a fixed order during each discovery pass:
27
27
 
28
- 1. Review feedback (`changes-requested`) `engine.js:2166-2180`
29
- 2. Human feedback (`humanFeedback.pendingFix` or coalesced feedback) — `engine.js:2191-2226`
30
- 3. Build failure (`buildStatus === 'failing'`) `engine.js:2229-2271`
31
- 4. Merge conflict (`_mergeConflict`) — `engine.js:2299-2317`
28
+ 1. Initial minion review (`reviewStatus === 'pending'`)
29
+ 2. Human feedback (`humanFeedback.pendingFix` or coalesced feedback)
30
+ 3. Minion re-review (`reviewStatus === 'waiting'` after a fix)
31
+ 4. Minion review feedback (`changes-requested`)
32
+ 5. Build failure (`buildStatus === 'failing'`)
33
+ 6. Merge conflict (`_mergeConflict`)
32
34
 
33
- When multiple problems coexist, earlier triggers get the first chance to enqueue work. The local `fixDispatched` flag is declared before the fix triggers (`engine.js:2168`) and set after review-feedback, human-feedback, and build-failure dispatches (`engine.js:2180`, `engine.js:2226`, `engine.js:2271`). Conflict fixes run last and explicitly require `!fixDispatched` (`engine.js:2301`), so any earlier successful fix dispatch suppresses the conflict fix for that PR in the same discovery pass. Build fixes are evaluated after review and human feedback, but the build-fix condition itself is not gated by `!fixDispatched` (`engine.js:2238`).
35
+ When multiple problems coexist, earlier triggers get the first chance to enqueue work. The local `fixDispatched` flag is declared before the fix triggers and set after human-feedback, review-feedback, and build-failure dispatches. Conflict fixes run last and explicitly require `!fixDispatched`, so any earlier successful fix dispatch suppresses the conflict fix for that PR in the same discovery pass. Build fixes are evaluated after human and minion review feedback, but the build-fix condition itself is not gated by `!fixDispatched`.
34
36
 
35
- ### A. Review feedback (`changes-requested`)
37
+ `evalMaxIterations` only applies to the minion review loop: initial minion review, minion re-review, and minion review-feedback fixes. It does not gate human-feedback fixes, build-failure fixes, or merge-conflict fixes.
36
38
 
37
- - Gate: `reviewStatus === 'changes-requested'` + `!awaitingReReview` + `!evalEscalated` + not dispatched + not on cooldown
38
- - Routes to PR author via `_author_` routing token
39
- - `review_note` = reviewer's feedback
40
- - Sets `fixDispatched = true` — prevents human-feedback and conflict fixes from also firing this pass
41
- - **Review-loop escalation**: after `evalMaxIterations` review→fix cycles (default 3), `_evalEscalated` is set on the PR and *only this trigger plus review/re-review* stop. Triggers B (human comments), C (build failures), and the merge-conflict fix path keep running. The dashboard PR row distinguishes the two states with separate badges (review badge `review-escalated` vs. build badge `build-escalated`).
39
+ ### A. Human comments (`humanFeedback.pendingFix`)
42
40
 
43
- ### B. Human comments (`humanFeedback.pendingFix`)
44
-
45
- - Gate: `pendingFix || coalescedFeedback` + `!awaitingReReview` + `!fixDispatched`
41
+ - Gate: `pendingFix || coalescedFeedback` + not already dispatched/on cooldown
46
42
  - Agent comments filtered out via `/\bMinions\s*\(/i` regex on comment body
47
43
  - Coalesces multiple comments arriving during cooldown into single fix
48
44
  - Routes to author
49
- - Not gated by `_evalEscalated` — humans can always force more fixes via PR comments even after the review loop escalates.
45
+ - Not gated by `_evalEscalated` — humans can always force more fixes via PR comments even after the minion review loop escalates.
46
+
47
+ ### B. Review feedback (`changes-requested`)
48
+
49
+ - Gate: `reviewStatus === 'changes-requested'` + `!awaitingReReview` + `!evalEscalated` + not dispatched + not on cooldown
50
+ - Routes to PR author via `_author_` routing token
51
+ - `review_note` = reviewer's feedback
52
+ - Sets `fixDispatched = true` — prevents the later conflict fix from also firing this pass
53
+ - **Review-loop escalation**: after `evalMaxIterations` review→fix cycles (default 3), `_evalEscalated` is set on the PR and *only this trigger plus minion review/re-review* stop. Triggers A (human comments), C (build failures), and D (merge conflicts) keep running. The dashboard PR row distinguishes the two states with separate badges (review badge `review-escalated` vs. build badge `build-escalated`).
50
54
 
51
55
  ### C. Build failures (`buildStatus === 'failing'`)
52
56
 
@@ -90,7 +94,7 @@ When multiple problems coexist, earlier triggers get the first chance to enqueue
90
94
  | Scenario | Guard |
91
95
  |---|---|
92
96
  | Simultaneous review + fix | `activePrIds` — skip PR if any dispatch in-flight |
93
- | Duplicate fix (review + human + conflict) | `fixDispatched` flag — later human/conflict triggers skip after earlier fix dispatches in the same PR pass |
97
+ | Duplicate fix (human/review/build + conflict) | `fixDispatched` flag — conflict triggers skip after earlier fix dispatches in the same PR pass |
94
98
  | Branch write conflict | `isBranchActive()` mutex |
95
99
  | Fix while awaiting re-review | `awaitingReReview` (waiting + fixedAt) |
96
100
  | Build fix before CI runs | `_buildFixPushedAt` grace period (10min) |
package/engine/cli.js CHANGED
@@ -21,6 +21,26 @@ function engine() {
21
21
  let _dispatchModule = null;
22
22
  function dispatchModule() { if (!_dispatchModule) _dispatchModule = require('./dispatch'); return _dispatchModule; }
23
23
 
24
+ function isEngineProcessAlive(control) {
25
+ if (!control?.pid) return false;
26
+ if (control.pid === process.pid) return true;
27
+ try {
28
+ if (process.platform === 'win32') {
29
+ const { execSync } = require('child_process');
30
+ const out = execSync(`tasklist /FI "PID eq ${control.pid}" /NH`, {
31
+ encoding: 'utf8',
32
+ windowsHide: true,
33
+ timeout: 3000,
34
+ });
35
+ return new RegExp(`\\b${control.pid}\\b`).test(out) && out.toLowerCase().includes('node');
36
+ }
37
+ process.kill(control.pid, 0);
38
+ return true;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
24
44
  function handleCommand(cmd, args) {
25
45
  if (!cmd) {
26
46
  return commands.start();
@@ -910,16 +930,22 @@ const commands = {
910
930
  },
911
931
 
912
932
  dispatch() {
913
- const e = engine();
914
- console.log('Forcing dispatch cycle...');
915
933
  const control = getControl();
916
- const prevState = control.state;
917
- safeWrite(CONTROL_PATH, { ...control, state: 'running' });
918
- e.tick();
919
- if (prevState !== 'running') {
920
- safeWrite(CONTROL_PATH, { ...control, state: prevState });
934
+ if (control.state === 'running' && isEngineProcessAlive(control)) {
935
+ safeWrite(CONTROL_PATH, { ...control, _wakeupAt: Date.now() });
936
+ console.log(`Dispatch wakeup requested from running engine (PID ${control.pid}).`);
937
+ return;
921
938
  }
922
- console.log('Dispatch cycle complete.');
939
+
940
+ const activeCount = (getDispatch().active || []).length;
941
+ if (activeCount > 0) {
942
+ console.log(`Engine is not running, but ${activeCount} dispatch(es) are active.`);
943
+ console.log('Refusing to run a local dispatch tick because it cannot track live agent processes.');
944
+ console.log('Start the engine to re-attach or recover: node engine.js start');
945
+ return;
946
+ }
947
+
948
+ console.log('Engine is not running. Start it to dispatch work: node engine.js start');
923
949
  },
924
950
 
925
951
  spawn(agentId, ...promptParts) {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-04-30T21:06:17.577Z"
4
+ "cachedAt": "2026-05-01T00:51:54.243Z"
5
5
  }
@@ -200,6 +200,36 @@ function readLiveWorkItem(meta) {
200
200
  return Array.isArray(items) ? items.find(i => i.id === itemId) || null : null;
201
201
  }
202
202
 
203
+ function writeFailedAgentReport(item, reason, resultSummary, failureClass) {
204
+ if (!item?.id) {
205
+ log('warn', 'Cannot write failed agent report without dispatch id');
206
+ return;
207
+ }
208
+ const agentId = item.agent || 'engine';
209
+ const itemId = item.meta?.item?.id || '';
210
+ const title = item.meta?.item?.title || item.task || item.id;
211
+ const metadata = {
212
+ dispatchId: item.id,
213
+ sourceItem: itemId || null,
214
+ result: DISPATCH_RESULT.ERROR,
215
+ failureClass: failureClass || null,
216
+ };
217
+ const content = [
218
+ `# Agent Failed: ${title}`,
219
+ '',
220
+ `**Agent:** ${agentId}`,
221
+ `**Dispatch:** \`${item.id}\``,
222
+ itemId ? `**Work Item:** \`${itemId}\`` : '',
223
+ `**Type:** ${item.type || 'unknown'}`,
224
+ `**Result:** ${DISPATCH_RESULT.ERROR}`,
225
+ failureClass ? `**Failure Class:** ${failureClass}` : '',
226
+ reason ? `**Reason:** ${reason}` : '',
227
+ '',
228
+ resultSummary ? `## Summary\n${resultSummary}` : '## Summary\n(no agent summary captured)',
229
+ ].filter(Boolean).join('\n');
230
+ shared.writeToInbox(agentId, `agent-failure-${item.id}`, content, null, metadata);
231
+ }
232
+
203
233
  // ─── Complete Dispatch ───────────────────────────────────────────────────────
204
234
 
205
235
  function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', resultSummary = '', opts = {}) {
@@ -238,6 +268,9 @@ function completeDispatch(id, result = DISPATCH_RESULT.SUCCESS, reason = '', res
238
268
 
239
269
  if (item) {
240
270
  log('info', `Completed dispatch: ${id} (${result}${reason ? ': ' + reason : ''})`);
271
+ if (result === DISPATCH_RESULT.ERROR) {
272
+ writeFailedAgentReport(item, reason, resultSummary, failureClass);
273
+ }
241
274
 
242
275
  // Update source work item status on failure + auto-retry with backoff
243
276
  const retryableFailure = isRetryableFailureReason(reason, failureClass);
@@ -778,16 +778,22 @@ function syncPrsFromOutput(output, agentId, meta, config) {
778
778
  }
779
779
  } catch {}
780
780
 
781
- // Accept inbox fallback only when the agent wrote the explicit PR-created
782
- // protocol line; generic PR mentions in findings/review notes are not evidence.
783
- const today = dateStamp();
784
- const inboxFiles = getInboxFiles().filter(f => f.includes(agentId) && f.includes(today));
785
- for (const f of inboxFiles) {
786
- const content = safeRead(path.join(INBOX_DIR, f));
787
- if (!content) continue;
788
- const prHeaderPattern = /(?:^|\n)\s*\*{0,2}(?:PR|Pull\s+Request|E2E\s+PR)\s+(?:created|opened|submitted)\*{0,2}\s*[:\-]\s*([^\n]+)/gi;
789
- while ((match = prHeaderPattern.exec(content)) !== null) {
790
- addPrUrlEvidence(match[1]);
781
+ // Accept inbox fallback ONLY when the agent's stdout is empty (rotated/lost).
782
+ // The inbox note is the durable artifact for the "gh pr create ran in a sibling
783
+ // dispatch whose stdout was rotated" case. When stdout has actual content (even
784
+ // without PR evidence e.g. the agent ran gh issue view but didn't create a PR),
785
+ // we must NOT pull in PR URLs from leftover inbox files of prior dispatches —
786
+ // those would falsely attribute unrelated PRs to this run.
787
+ if (!output || !String(output).trim()) {
788
+ const today = dateStamp();
789
+ const inboxFiles = getInboxFiles().filter(f => f.includes(agentId) && f.includes(today));
790
+ for (const f of inboxFiles) {
791
+ const content = safeRead(path.join(INBOX_DIR, f));
792
+ if (!content) continue;
793
+ const prHeaderPattern = /(?:^|\n)\s*\*{0,2}(?:PR|Pull\s+Request|E2E\s+PR)\s+(?:created|opened|submitted)\*{0,2}\s*[:\-]\s*([^\n]+)/gi;
794
+ while ((match = prHeaderPattern.exec(content)) !== null) {
795
+ addPrUrlEvidence(match[1]);
796
+ }
791
797
  }
792
798
  }
793
799
 
@@ -1688,6 +1694,44 @@ function parseStructuredCompletion(stdout, runtimeName) {
1688
1694
  return result;
1689
1695
  }
1690
1696
 
1697
+ function normalizeCompletionStatus(status) {
1698
+ return String(status || '').trim().toLowerCase().replace(/[\s_]+/g, '-');
1699
+ }
1700
+
1701
+ function writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredCompletion, resultSummary, exitCode) {
1702
+ if (!dispatchItem?.id || !outcome) {
1703
+ log('warn', 'Cannot write non-clean agent report without dispatch id and outcome');
1704
+ return;
1705
+ }
1706
+ const itemId = dispatchItem.meta?.item?.id || '';
1707
+ const title = dispatchItem.meta?.item?.title || dispatchItem.task || dispatchItem.id;
1708
+ const metadata = {
1709
+ dispatchId: dispatchItem.id,
1710
+ sourceItem: itemId || null,
1711
+ result: outcome,
1712
+ completionStatus: structuredCompletion?.status || null,
1713
+ };
1714
+ const structuredLines = structuredCompletion
1715
+ ? Object.entries(structuredCompletion).map(([key, value]) => `- ${key}: ${value}`).join('\n')
1716
+ : '- none';
1717
+ const content = [
1718
+ `# Agent ${outcome === 'partial' ? 'Partially Completed' : 'Reported Failure'}: ${title}`,
1719
+ '',
1720
+ `**Agent:** ${agentId}`,
1721
+ `**Dispatch:** \`${dispatchItem.id}\``,
1722
+ itemId ? `**Work Item:** \`${itemId}\`` : '',
1723
+ `**Type:** ${dispatchItem.type || 'unknown'}`,
1724
+ `**Exit Code:** ${exitCode}`,
1725
+ `**Outcome:** ${outcome}`,
1726
+ '',
1727
+ `## Structured Completion`,
1728
+ structuredLines,
1729
+ '',
1730
+ resultSummary ? `## Summary\n${resultSummary}` : '## Summary\n(no agent summary captured)',
1731
+ ].filter(Boolean).join('\n');
1732
+ shared.writeToInbox(agentId || 'engine', `agent-${outcome}-${dispatchItem.id}`, content, null, metadata);
1733
+ }
1734
+
1691
1735
  /**
1692
1736
  * Handle decomposition result — parse sub-items from agent output and create child work items.
1693
1737
  * Called from runPostCompletionHooks when type === 'decompose'.
@@ -1827,6 +1871,12 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
1827
1871
  }
1828
1872
  const effectiveSuccess = isSuccess || autoRecovered;
1829
1873
 
1874
+ const completionStatus = normalizeCompletionStatus(structuredCompletion?.status);
1875
+ if (completionStatus.startsWith('partial') || autoRecovered || (completionStatus.startsWith('fail') && isSuccess)) {
1876
+ const outcome = completionStatus.startsWith('fail') ? 'failure' : 'partial';
1877
+ writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredCompletion, resultSummary, code);
1878
+ }
1879
+
1830
1880
  // Handle decomposition results — create sub-items from decompose agent output
1831
1881
  let skipDoneStatus = false;
1832
1882
  if (type === WORK_TYPE.DECOMPOSE && effectiveSuccess && meta?.item?.id) {
package/engine/queries.js CHANGED
@@ -49,7 +49,7 @@ function readHeadTail(filePath, bytes = 1024) {
49
49
  * Returns { description, taskId } for the most recent in-flight tool, or null.
50
50
  */
51
51
  function detectInFlightTool(tail) {
52
- if (!tail) return null;
52
+ if (typeof tail !== 'string' || !tail) return null;
53
53
  const lines = tail.split('\n');
54
54
  const completed = new Set();
55
55
 
@@ -92,10 +92,12 @@ const NOTES_PATH = path.join(MINIONS_DIR, 'notes.md');
92
92
  // ── Helpers ─────────────────────────────────────────────────────────────────
93
93
 
94
94
  function timeSince(ms) {
95
- const s = Math.floor((Date.now() - ms) / 1000);
95
+ if (typeof ms !== 'number' || !Number.isFinite(ms)) return 'unknown';
96
+ const s = Math.floor(Math.max(0, Date.now() - ms) / 1000);
96
97
  if (s < 60) return `${s}s ago`;
97
98
  if (s < 3600) return `${Math.floor(s / 60)}m ago`;
98
- return `${Math.floor(s / 3600)}h ago`;
99
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
100
+ return `${Math.floor(s / 86400)}d ago`;
99
101
  }
100
102
 
101
103
  function readJsonNoRestore(filePath) {
package/engine/shared.js CHANGED
@@ -563,7 +563,7 @@ function execSilent(cmd, opts = {}) {
563
563
  function execAsync(cmd, opts = {}) {
564
564
  const { timeout, ...rest } = opts;
565
565
  return new Promise((resolve, reject) => {
566
- const child = _cbExec(cmd, { windowsHide: true, encoding: 'utf8', ...rest, timeout: timeout || 30000 }, (err, stdout, stderr) => {
566
+ _cbExec(cmd, { windowsHide: true, encoding: 'utf8', ...rest, timeout: timeout || 30000 }, (err, stdout, stderr) => {
567
567
  if (err) {
568
568
  err.stderr = stderr;
569
569
  err.stdout = stdout;
@@ -571,8 +571,6 @@ function execAsync(cmd, opts = {}) {
571
571
  }
572
572
  resolve(stdout);
573
573
  });
574
- // Safety: ensure child is killed if parent process exits
575
- child.unref && child.unref();
576
574
  });
577
575
  }
578
576
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1644",
3
+ "version": "0.1.1646",
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"
@@ -79,8 +79,6 @@ I'll dispatch dallas to fix that bug.
79
79
  - `note`: `title` and `content` (or `description`) REQUIRED.
80
80
  - `knowledge`: `title`, `content`, and `category` REQUIRED. Valid categories: architecture, conventions, project-notes, build-reports, reviews.
81
81
 
82
- If you describe an action in prose ("I'll dispatch this..."), you MUST emit a matching `===ACTIONS===` block. The server detects prose claims without action blocks and surfaces a warning to the user — i.e., your false claim becomes visible. Either dispatch or don't promise to.
83
-
84
82
  Core action types:
85
83
  - **dispatch**: title (REQUIRED), workType, priority (low/medium/high), agents[] or agent (optional — both shapes accepted), project (REQUIRED when multi-project), description
86
84
  workTypes: `explore` (research/report only, NO PR), `ask` (answer/report, NO PR), `implement` (new code, PR REQUIRED), `fix` (bug fix, PR REQUIRED), `review` (code review, NO PR), `test` (tests, PR if new), `verify` (merge/build/maintenance, NO PR)