@yemi33/minions 0.1.2228 → 0.1.2230

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/bin/minions.js CHANGED
@@ -1168,6 +1168,7 @@ if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
1168
1168
  minions cleanup Clean temp files, worktrees, zombies
1169
1169
  minions pr comment <repo> <n> Post a marker-prepended PR comment via gh
1170
1170
  --agent <id> --kind <k> [--wi <id>] (--body-file <f> | --body <text>)
1171
+ [--harness-file <f> | --harness-json <j>] folds in a "Harnesses used" section
1171
1172
  minions nuke --confirm Factory reset (delete state, reset config to defaults)
1172
1173
  minions uninstall --confirm Remove everything + uninstall npm package
1173
1174
 
package/dashboard.js CHANGED
@@ -6965,9 +6965,28 @@ const server = http.createServer(async (req, res) => {
6965
6965
  // Replay-on-connect: if a terminal event for this agent landed within
6966
6966
  // the ring's TTL, push it immediately so a viewer who opens the live
6967
6967
  // view AFTER completion still sees the verdict.
6968
+ //
6969
+ // W-mqprx2px00076668 — but a STALE terminal failure must not be replayed
6970
+ // as the active blocking banner when the agent has since started a newer
6971
+ // dispatch (e.g. a successful retry of the same work item). Gate the
6972
+ // replay on the agent's live dispatch state: suppress the failure banner
6973
+ // when the agent is actively working a different (newer) dispatch than the
6974
+ // one that produced the event. History is preserved — the raw event still
6975
+ // lives in the JSONL ring and the WI/event history; only the prominent
6976
+ // active-agent banner is gated.
6968
6977
  try {
6969
6978
  const recent = dispatchEvents.getLastTerminalForAgent(agentId);
6970
- if (recent) sendTerminalEvent(recent);
6979
+ if (recent) {
6980
+ let agentStatus = null;
6981
+ try { agentStatus = queries.getAgentStatus(agentId); } catch { agentStatus = null; }
6982
+ if (dispatchEvents.isStaleTerminalForActiveDispatch(recent, agentStatus)) {
6983
+ // Mark as already-handled so a later file-rotation rewind in the
6984
+ // terminal watcher cannot re-broadcast this suppressed stale banner.
6985
+ if (recent.dispatchId) _sentTerminalDispatchIds.add(recent.dispatchId);
6986
+ } else {
6987
+ sendTerminalEvent(recent);
6988
+ }
6989
+ }
6971
6990
  } catch { /* best-effort replay */ }
6972
6991
 
6973
6992
  const eventsFile = dispatchEvents._eventsFile();
@@ -110,11 +110,15 @@ evaluation pass) can see what tooling drove a dispatch:
110
110
  and ⚠️-marked), and returns `''` for an empty/absent record. The GitHub
111
111
  comment path folds it in via `engine/gh-comment.js#buildMinionsCommentBody`
112
112
  (optional `harnessUsed` arg, threaded through `postPrComment` /
113
- `postPrReviewComment` / `postPrReview`); the Azure DevOps path has no
114
- engine-side comment poster (agents post via `az repos pr comment` / REST),
115
- so review/fix agents append the **byte-identical** section themselves per
116
- `playbooks/shared-rules.md` "Harness transparency / self-report". Both
117
- surfaces consume the one renderer, so there is no second formatter to drift.
113
+ `postPrReviewComment` / `postPrReview`); the Azure DevOps path mirrors it via
114
+ `engine/ado-comment.js#postAdoPrComment` (same builder, same `harnessUsed`
115
+ arg). Both posters are reached from the `minions pr comment` CLI, which turns
116
+ `--harness-file` / `--harness-json` into the grounded record and folds the
117
+ **byte-identical** section in for the agent. Only the raw `gh pr comment` /
118
+ `az repos pr comment` / REST fallbacks bypass that chokepoint, so on those
119
+ paths review/fix agents append the section themselves per
120
+ `playbooks/shared-rules.md` → "Harness transparency / self-report". Every
121
+ surface consumes the one renderer, so there is no second formatter to drift.
118
122
  2. **notes/inbox digest** — harness usage is summarized into the learnings /
119
123
  inbox stream that feeds consolidation, so cross-task patterns ("everyone
120
124
  reaches for skill X on Android fixes") become visible to the team-memory
package/engine/cli.js CHANGED
@@ -250,7 +250,7 @@ const CLI_COMMAND_DOCS = Object.freeze({
250
250
  'mcp-sync': { args: '', summary: 'Print harness propagation diagnostic (same source as `minions doctor --harness`; read-only, no writes)' },
251
251
  doctor: { args: '[--harness]', summary: 'Check prerequisites and runtime health (--harness: print harness propagation diagnostic)' },
252
252
  config: { args: 'set-cli <R> [--model M]', summary: 'Persist defaultCli/defaultModel without starting' },
253
- pr: { args: 'comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] [--body-file <f>|--body <text>]', summary: 'Post a marker-prepended PR comment via gh' },
253
+ pr: { args: 'comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] [--harness-file <f>|--harness-json <j>] [--body-file <f>|--body <text>]', summary: 'Post a marker-prepended PR comment via gh' },
254
254
  bridge: { args: 'status|health|enable|disable', summary: 'Constellation bridge: toggle and inspect the read-only cross-repo feed' },
255
255
  });
256
256
 
@@ -413,6 +413,68 @@ function _applyRuntimeFlags({ cli, model, modelExplicit }) {
413
413
  return { warnings, applied: true };
414
414
  }
415
415
 
416
+ // resolveHarnessUsedForComment(flags) — P-7a3c9e21. Turn the `minions pr comment`
417
+ // harness flags into a grounded `harnessUsed` record suitable for the posters'
418
+ // `harnessUsed` param (engine/gh-comment.js / engine/ado-comment.js), which fold
419
+ // it via buildMinionsCommentBody -> buildHarnessUsedSection.
420
+ //
421
+ // Source precedence: `--harness-file <path>` (JSON on disk) wins over the inline
422
+ // `--harness-json <json>` convenience; neither supplied -> returns undefined so
423
+ // the comment body is byte-identical to today (no section). Read/parse failures
424
+ // are HARD errors (process.exit(2) with the offending message) — never a silent
425
+ // drop. Grounding uses the spawn-time `_harnessPropagated` manifest located by
426
+ // dispatch id (explicit `--dispatch-id` -> else basename(MINIONS_COMPLETION_REPORT,
427
+ // '.json')); an unresolvable manifest passes null, marking every entry
428
+ // grounded:false — the honest "no record" state, not a fatal condition.
429
+ function resolveHarnessUsedForComment(flags) {
430
+ const fileArg = flags['harness-file'];
431
+ const jsonArg = flags['harness-json'];
432
+
433
+ let raw;
434
+ if (fileArg !== undefined) {
435
+ try {
436
+ raw = fs.readFileSync(fileArg, 'utf8');
437
+ } catch (e) {
438
+ console.error(`error: could not read --harness-file ${fileArg}: ${e.message}`);
439
+ process.exit(2);
440
+ }
441
+ } else if (jsonArg !== undefined) {
442
+ raw = String(jsonArg);
443
+ } else {
444
+ return undefined;
445
+ }
446
+
447
+ let parsed;
448
+ try {
449
+ parsed = JSON.parse(raw);
450
+ } catch (e) {
451
+ const src = fileArg !== undefined ? `--harness-file ${fileArg}` : '--harness-json';
452
+ console.error(`error: invalid JSON in ${src}: ${e.message}`);
453
+ process.exit(2);
454
+ }
455
+
456
+ // Resolve the grounding manifest best-effort: explicit id -> completion-report
457
+ // basename. A missing/unresolvable manifest is non-fatal (groundHarnessUsed
458
+ // with null propagated marks everything grounded:false).
459
+ const dispatchId = flags['dispatch-id']
460
+ || path.basename(process.env.MINIONS_COMPLETION_REPORT || '', '.json')
461
+ || undefined;
462
+ let propagated = null;
463
+ if (dispatchId) {
464
+ try {
465
+ const dispatch = getDispatch();
466
+ for (const queue of ['active', 'completed', 'pending']) {
467
+ const list = Array.isArray(dispatch?.[queue]) ? dispatch[queue] : null;
468
+ if (!list) continue;
469
+ const found = list.find(d => d && d.id === dispatchId);
470
+ if (found && found._harnessPropagated) { propagated = found._harnessPropagated; break; }
471
+ }
472
+ } catch { propagated = null; }
473
+ }
474
+
475
+ return shared.groundHarnessUsed(parsed, propagated);
476
+ }
477
+
416
478
  const commands = {
417
479
  start(...startArgs) {
418
480
  // Apply --cli / --model fleet flags before any engine wiring touches
@@ -1982,6 +2044,11 @@ const commands = {
1982
2044
  console.log(' GitHub: minions pr comment <repo> <prNumber> --agent <id> --kind <k> [--wi <id>] (--body-file <path> | --body <text>)');
1983
2045
  console.log(' ADO: minions pr comment <prNumber> --host ado --ado-org <org> --ado-project <proj> --repo-id <id> --agent <id> --kind <k> [--wi <id>] (--body-file <path> | --body <text>)');
1984
2046
  console.log('');
2047
+ console.log('Optional harness self-report (folded into the collapsible "Harnesses used" section):');
2048
+ console.log(' --harness-file <path> JSON file with the { skills, mcpServers, commands, docs } record (wins over --harness-json)');
2049
+ console.log(' --harness-json <json> same record inline as a JSON string');
2050
+ console.log(' --dispatch-id <id> grounding manifest override (else basename of $MINIONS_COMPLETION_REPORT)');
2051
+ console.log('');
1985
2052
  console.log('Posts a PR comment with the hidden minions marker, the collapsible');
1986
2053
  console.log('"Harnesses used" section, and the brand link folded in by the shared');
1987
2054
  console.log('builder — so engine classifiers identify agent-authored comments by');
@@ -2040,6 +2107,12 @@ const commands = {
2040
2107
  process.exit(2);
2041
2108
  }
2042
2109
 
2110
+ // P-7a3c9e21 — thread the consulted-sources footprint into both posters so
2111
+ // the auto-folded "Harnesses used" <details> section the help already
2112
+ // promises is actually rendered. Resolved once (host-neutral); absent flags
2113
+ // -> undefined -> body byte-identical to today.
2114
+ const harnessUsed = resolveHarnessUsedForComment(flags);
2115
+
2043
2116
  // ── Azure DevOps: <prNumber> --host ado --ado-org --ado-project --repo-id ──
2044
2117
  if (isAdo) {
2045
2118
  const [prNumberRaw] = positional;
@@ -2059,7 +2132,7 @@ const commands = {
2059
2132
  const orgBase = flags['org-base'] || `https://dev.azure.com/${adoOrg}`;
2060
2133
  const adoComment = require('./ado-comment');
2061
2134
  adoComment.postAdoPrComment({
2062
- orgBase, project, repositoryId, prNumber, body, agentId, kind, workItemId,
2135
+ orgBase, project, repositoryId, prNumber, body, agentId, kind, workItemId, harnessUsed,
2063
2136
  }).then((result) => {
2064
2137
  if (result && result.threadId) console.log(`ADO thread ${result.threadId} created`);
2065
2138
  }).catch((e) => {
@@ -2084,7 +2157,7 @@ const commands = {
2084
2157
 
2085
2158
  try {
2086
2159
  const result = ghComment.postPrComment({
2087
- repo, prNumber, body, agentId, kind, workItemId,
2160
+ repo, prNumber, body, agentId, kind, workItemId, harnessUsed,
2088
2161
  });
2089
2162
  if (result.output) console.log(result.output);
2090
2163
  } catch (e) {
@@ -2223,6 +2296,8 @@ module.exports = {
2223
2296
  _readDispatchPid: readDispatchPid,
2224
2297
  _normalizeSessionBranch: normalizeSessionBranch,
2225
2298
  _dispatchSessionBranch: dispatchSessionBranch,
2299
+ // P-7a3c9e21 — harness-flag resolver exported for CLI-threading tests.
2300
+ _resolveHarnessUsedForComment: resolveHarnessUsedForComment,
2226
2301
  // W-mpcyvff6000pf828 (#2653) — heartbeat writer + factory exported for tests
2227
2302
  _writeHeartbeatNow: writeHeartbeatNow,
2228
2303
  _createHeartbeatInterval: createHeartbeatInterval,
@@ -141,6 +141,45 @@ function getLastTerminalForAgent(agentId) {
141
141
  return null;
142
142
  }
143
143
 
144
+ /**
145
+ * W-mqprx2px00076668 — gate a terminal-FAILURE banner against the agent's
146
+ * live dispatch state. A stale terminal failure (result !== 'success', e.g.
147
+ * `error` / `timeout`) MUST NOT be surfaced as the active blocking banner
148
+ * once the SAME agent is actively working a NEWER dispatch than the one that
149
+ * produced the event. The active-dispatch state wins over any older terminal
150
+ * error whose `dispatchId` is not the agent's current dispatch.
151
+ *
152
+ * This is a GENERIC rule across all failureClasses — keyed solely on "does
153
+ * this terminal event belong to the agent's CURRENT dispatch?" — not a
154
+ * special-case for any one failureClass. Pure (no I/O): the caller passes the
155
+ * agent's status snapshot so the predicate stays unit-testable and reusable
156
+ * by both the dashboard replay-on-connect path and the client renderer.
157
+ *
158
+ * Returns true when the event should be suppressed as the active banner.
159
+ * Does NOT delete history — the raw event still lives in the JSONL ring and
160
+ * the WI/event history; only the prominent active-agent banner is gated.
161
+ *
162
+ * @param {object} event - dispatch.terminal event ({ result, dispatchId, ... }).
163
+ * @param {object} agentStatus - agent status snapshot, e.g. the
164
+ * `{ status, dispatch_id }` shape returned by queries.getAgentStatus().
165
+ * @returns {boolean}
166
+ */
167
+ function isStaleTerminalForActiveDispatch(event, agentStatus) {
168
+ if (!event || !event.dispatchId) return false;
169
+ // Success banners never imply the run is blocked — leave them to the
170
+ // dispatch.started clear path. Only failure/timeout banners are gated.
171
+ if (event.result === 'success') return false;
172
+ // No active work → keep the failure banner. An agent that is genuinely
173
+ // idle (or in the post-completion `error`/`done` window) after a terminal
174
+ // failure STILL shows its banner; don't over-suppress.
175
+ if (!agentStatus || agentStatus.status !== 'working') return false;
176
+ const current = agentStatus.dispatch_id || null;
177
+ if (!current) return false;
178
+ // Agent is actively working a DIFFERENT (newer) dispatch than the one that
179
+ // produced this terminal failure → suppress it as the active banner.
180
+ return current !== event.dispatchId;
181
+ }
182
+
144
183
 
145
184
  /**
146
185
  * Build the terminal event payload from a freshly-completed dispatch entry +
@@ -237,6 +276,7 @@ module.exports = {
237
276
  onDispatchTerminal,
238
277
  readRecentEvents,
239
278
  getLastTerminalForAgent,
279
+ isStaleTerminalForActiveDispatch,
240
280
  buildTerminalEvent,
241
281
  // dispatch.started (W-mq1m1wo5000nd78d)
242
282
  emitDispatchStarted,
package/engine/github.js CHANGED
@@ -633,11 +633,11 @@ async function forEachActiveGhPr(config, callback) {
633
633
  if (updated) {
634
634
  // Also update title/author/branch if still placeholder
635
635
  const currentTitle = pr.title || '';
636
- if (!currentTitle || currentTitle.includes('polling...') || pr.agent === 'human' || pr.description === undefined) {
636
+ if (shared.isPlaceholderPrTitle(currentTitle) || pr.agent === 'human' || pr.description === undefined) {
637
637
  const prData = await ghApi(`/pulls/${prNum}`, slug);
638
638
  if (prData) {
639
639
  const latestTitle = pr.title || '';
640
- if (!latestTitle || latestTitle.includes('polling...') || /[{}"\[\]]/.test(latestTitle) || /^[0-9a-f-]{8,}$/i.test(latestTitle)) {
640
+ if (shared.isPlaceholderPrTitle(latestTitle)) {
641
641
  pr.title = (prData.title || latestTitle).slice(0, 120);
642
642
  }
643
643
  if (pr.description === undefined) pr.description = (prData.body || '').slice(0, 500);
@@ -752,10 +752,7 @@ async function pollPrStatus(config) {
752
752
  // stay stuck on "PR #N (polling...)" forever. `prData` is already in
753
753
  // hand from line 622 — no extra API call needed.
754
754
  const currentTitleForBackfill = pr.title || '';
755
- if (!currentTitleForBackfill
756
- || currentTitleForBackfill.includes('polling...')
757
- || /[{}"\[\]]/.test(currentTitleForBackfill)
758
- || /^[0-9a-f-]{8,}$/i.test(currentTitleForBackfill)) {
755
+ if (shared.isPlaceholderPrTitle(currentTitleForBackfill)) {
759
756
  if (prData.title) {
760
757
  const nextTitle = String(prData.title).slice(0, 120);
761
758
  if (pr.title !== nextTitle) {
package/engine/shared.js CHANGED
@@ -6553,6 +6553,32 @@ function findPrRecord(prs, prRef, project = null) {
6553
6553
  return numberMatches.length === 1 ? numberMatches[0] : null;
6554
6554
  }
6555
6555
 
6556
+ // Issue #289: single source of truth for "is this stored PR title a
6557
+ // placeholder/fallback that should be backfilled from the live platform
6558
+ // title on the next poll?". Recognizes:
6559
+ // - empty / missing titles
6560
+ // - the link-time "...(polling...)" placeholder
6561
+ // - serialized-JSON / agent-output leakage (contains {}"[] chars)
6562
+ // - bare hex/uuid-ish ids (>= 8 hex/dash chars)
6563
+ // - the fallback WRITER shapes "PR created by <agent>" (engine/lifecycle.js)
6564
+ // and bare "PR #<n>" (engine/ado.js central poller)
6565
+ // The WRITER (engine/lifecycle.js) and the DETECTORS (engine/github.js,
6566
+ // engine/ado.js) MUST agree — route every site through this helper so they
6567
+ // cannot drift apart again (the github poller previously never backfilled a
6568
+ // frozen "PR created by Ripley" title because its detector didn't know the shape).
6569
+ function isPlaceholderPrTitle(title) {
6570
+ const t = typeof title === 'string'
6571
+ ? title.trim()
6572
+ : (title == null ? '' : String(title).trim());
6573
+ if (!t) return true;
6574
+ if (t.includes('polling...')) return true;
6575
+ if (/[{}"\[\]]/.test(t)) return true;
6576
+ if (/^[0-9a-f-]{8,}$/i.test(t)) return true;
6577
+ if (/^PR created by /i.test(t)) return true;
6578
+ if (/^PR #\d+$/i.test(t)) return true;
6579
+ return false;
6580
+ }
6581
+
6556
6582
  function snapshotPrRecord(pr) {
6557
6583
  if (pr === undefined) return undefined;
6558
6584
  return JSON.parse(JSON.stringify(pr));
@@ -8834,6 +8860,7 @@ module.exports = {
8834
8860
  isAdoPrScopeCompatible,
8835
8861
  getCanonicalPrId,
8836
8862
  findPrRecord,
8863
+ isPlaceholderPrTitle,
8837
8864
  snapshotPrRecord,
8838
8865
  applyPrFieldDelta,
8839
8866
  normalizePrRecord,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2228",
3
+ "version": "0.1.2230",
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"
@@ -153,7 +153,11 @@ Copyable worked example:
153
153
 
154
154
  Entries you report are **cross-checked** against what the engine propagated into your worktree and annotated `grounded: true | false` — never dropped — so a `grounded: false` entry surfaces a discrepancy for a human rather than being hidden. Full contract: `docs/harness-transparency.md`.
155
155
 
156
- When you author a **PR review / fix summary comment** (GitHub `minions pr comment` / `gh pr comment`, or Azure DevOps `az repos pr comment`), fold the same harness footprint into the body as a collapsible section so reviewers see which skills / MCPs / docs informed the change. Render it **identically on both hosts** — append this exact `<details>` block at the end of your comment body (omit it entirely when you used no affordances):
156
+ When you author a **PR review / fix summary comment**, the same harness footprint belongs in the comment body as a collapsible "Harnesses used" section so reviewers see which skills / MCPs / docs informed the change.
157
+
158
+ **Preferred — `minions pr comment` (GitHub and Azure DevOps `--host ado`): do NOT hand-render the section.** Write your `harnessUsed` record — the same `{ skills, mcpServers, commands, docs }` shape as the completion field above — to a JSON file and pass `--harness-file <path>` (or inline via `--harness-json <json>`). The shared builder (`engine/comment-format.js#buildHarnessUsedSection`, consumed by both `engine/gh-comment.js` and `engine/ado-comment.js`) folds the byte-identical `<details>` section into the body for you, so agent-authored and engine-authored comments match. Each entry is cross-checked against the harness manifest the engine propagated into your worktree and annotated `grounded: true | false` (never dropped); a `grounded: false`, ⚠️-marked bullet means **the engine couldn't confirm that affordance against the propagated manifest — not that you did anything wrong**.
159
+
160
+ **Raw fallbacks only — `gh pr comment` / `az repos pr comment` / ADO REST:** these bypass the CLI chokepoint, so you MUST hand-render the section yourself. Render it **identically on both hosts** — append this exact `<details>` block at the end of your comment body (omit it entirely when you used no affordances):
157
161
 
158
162
  ```markdown
159
163
  <details>
@@ -167,7 +171,7 @@ When you author a **PR review / fix summary comment** (GitHub `minions pr commen
167
171
  </details>
168
172
  ```
169
173
 
170
- One bullet per affordance, in the order skills → MCP servers → commands → docs; `N` is the bullet count. This is the human-readable mirror of your `harnessUsed` completion field keep the two consistent. The engine renders the byte-identical section via `engine/comment-format.js#buildHarnessUsedSection` when it composes a comment, so agent-authored and engine-authored sections match.
174
+ One bullet per affordance, in the order skills → MCP servers → commands → docs; `N` is the bullet count. Whether the CLI folds it in or you hand-render it on a raw fallback, the section is the human-readable mirror of your `harnessUsed` completion field keep the two consistent.
171
175
 
172
176
  ## Minions API access
173
177