@yemi33/minions 0.1.2235 → 0.1.2237

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.
@@ -147,7 +147,7 @@ const RENDER_VERSIONS = {
147
147
  dispatch: 2,
148
148
  engineLog: 2,
149
149
  metrics: 1,
150
- workItems: 3,
150
+ workItems: 4,
151
151
  skills: 1,
152
152
  commands: 1,
153
153
  mcpServers: 1,
@@ -238,6 +238,10 @@ function wiRow(item) {
238
238
  })() +
239
239
  '</td>' +
240
240
  '<td style="white-space:nowrap">' +
241
+ // W-mqqa76pi — "View live output" opens the agent slide-in panel (live-output
242
+ // tab auto-selected when the agent is working). Only for genuinely active
243
+ // dispatches with a resolvable assigned agent id.
244
+ ((item.status === 'dispatched' && (item.dispatched_to || item.agent)) ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--green);border-color:var(--green);margin-right:4px" onclick="event.stopPropagation();openArtifact(\'agent\',\'' + escapeHtml(item.dispatched_to || item.agent) + '\')" title="View live output">&#x1F4FA;</button>' : '') +
241
245
  ((item.status === 'pending' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--blue);border-color:var(--blue);margin-right:4px" onclick="event.stopPropagation();editWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Edit work item">&#x270E;</button>' : '') +
242
246
  ((item.status === 'done' || item.status === 'failed') ? '<button class="pr-pager-btn" style="font-size:var(--text-xs);padding:1px 6px;color:var(--muted);border-color:var(--border);margin-right:4px" onclick="event.stopPropagation();archiveWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Archive work item">&#x1F4E6;</button>' : '') +
243
247
  ((item.status === 'done' || item.status === 'failed') && !item._humanFeedback ? '<button class="btn-action" style="margin-right:4px" onclick="event.stopPropagation();feedbackWorkItem(\'' + escapeHtml(item.id) + '\',\'' + escapeHtml(item._source || '') + '\')" title="Give feedback">&#x1F44D;&#x1F44E;</button>' : (item._humanFeedback ? '<span style="font-size:var(--text-xs)" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '&#x1F44D;' : '&#x1F44E;') + '</span> ' : '')) +
@@ -699,6 +703,14 @@ function _wiRenderDetail(item) {
699
703
  return ' <span class="pr-badge needs-attention" style="font-size:var(--text-xs);margin-left:4px" title="See &quot;Why this is blocked&quot; below">&#x26A0; Needs attention</span>';
700
704
  })() +
701
705
  '</div>';
706
+ // W-mqqa76pi — Prominent "View live output" button for an actively-dispatched
707
+ // WI. Opens the agent slide-in panel via openArtifact('agent', id), which
708
+ // resets the modal stack and selects the live-output tab when the agent is
709
+ // working. Gated on status==='dispatched' AND a resolvable assigned agent id.
710
+ if (item.status === 'dispatched' && (item.dispatched_to || item.agent)) {
711
+ var _wiLiveAgentId = item.dispatched_to || item.agent;
712
+ html += '<div style="margin-bottom:12px"><button class="pr-pager-btn" style="font-size:var(--text-base);padding:4px 12px;color:var(--green);border-color:var(--green)" onclick="openArtifact(\'agent\',\'' + escapeHtml(_wiLiveAgentId) + '\')" title="View live output">&#x1F4FA; View live output</button></div>';
713
+ }
702
714
  // Work item id — shown in the modal body (not just the title) so operators can
703
715
  // read/copy the canonical id (W-… or sched-…) when deeplinking in from a note.
704
716
  html += field('ID', '<code style="font-size:var(--text-sm);background:var(--surface2);padding:2px 6px;border-radius:var(--radius-sm)">' + escapeHtml(item.id || '—') + '</code>');
@@ -932,14 +944,44 @@ function _wiRenderDetail(item) {
932
944
  // (work_item: / wi: / sourceItem:). Slim string[] field set by
933
945
  // engine/queries.js#getWorkItems from _buildNotesByWiMap. Each entry is
934
946
  // either a bare inbox filename or `archive:<filename>` for archive notes.
947
+ //
948
+ // #308 — repeated attempts for one WI used to render a flat pile of
949
+ // indistinguishable note chips (every per-attempt failure report + legacy
950
+ // harness-usage note shown beside the final agent report). Partition the
951
+ // notes: per-attempt failure/partial reports and legacy harness-usage notes
952
+ // are GROUPED into a collapsed "Prior attempts" disclosure, leaving the final
953
+ // agent findings note(s) as the primary, always-visible chips. Backward
954
+ // compatible — every grouped note still opens via the same renderArtifactLink
955
+ // chip, just behind a <details> instead of in the flat list.
935
956
  if (Array.isArray(item._notes) && item._notes.length > 0) {
936
- var mentionPills = item._notes.map(function(token) {
957
+ var _mentionPill = function(token) {
937
958
  var isArchive = token.indexOf('archive:') === 0;
938
959
  var fname = isArchive ? token.slice(8) : token;
939
960
  var label = fname.replace(/\.md$/, '').slice(0, 30) + (isArchive ? ' (archived)' : '');
940
961
  return renderArtifactLink({ type: 'note', id: fname, label: label, title: 'Note: ' + fname });
941
- }).join(' ');
942
- html += field('Mentions', '<div style="display:flex;flex-wrap:wrap;gap:4px">' + mentionPills + '</div>');
962
+ };
963
+ // A note is a "prior attempt" chip when its filename slug marks it as an
964
+ // engine-emitted per-attempt failure/partial report or a legacy
965
+ // harness-usage note. These are keyed off the writeToInbox slug embedded in
966
+ // the filename (agent-failure-/agent-failed-/agent-partial-/harness-usage-),
967
+ // so the test is a pure string check independent of frontmatter.
968
+ var _isPriorAttemptNote = function(token) {
969
+ var fname = token.indexOf('archive:') === 0 ? token.slice(8) : token;
970
+ return /(?:^|-)(?:agent-fail(?:ure|ed)|agent-partial|harness-usage)-/.test(fname);
971
+ };
972
+ var primaryNotes = item._notes.filter(function(t) { return !_isPriorAttemptNote(t); });
973
+ var priorNotes = item._notes.filter(_isPriorAttemptNote);
974
+ var mentionHtml = '';
975
+ if (primaryNotes.length > 0) {
976
+ mentionHtml += '<div style="display:flex;flex-wrap:wrap;gap:4px">' + primaryNotes.map(_mentionPill).join(' ') + '</div>';
977
+ }
978
+ if (priorNotes.length > 0) {
979
+ mentionHtml += '<details style="margin-top:' + (primaryNotes.length > 0 ? '6px' : '0') + '">'
980
+ + '<summary style="cursor:pointer;color:var(--muted);font-size:var(--text-sm)">Prior attempts (' + priorNotes.length + ')</summary>'
981
+ + '<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px">' + priorNotes.map(_mentionPill).join(' ') + '</div>'
982
+ + '</details>';
983
+ }
984
+ if (mentionHtml) html += field('Mentions', mentionHtml);
943
985
  }
944
986
 
945
987
  if (item._totalCostUsd != null) html += field('Cumulative Cost', '$' + Number(item._totalCostUsd).toFixed(4));
package/dashboard.js CHANGED
@@ -1977,6 +1977,61 @@ setInterval(() => checkNpmVersion().catch(() => {}), _getVersionCheckInterval())
1977
1977
  let _diskVersionCache = null;
1978
1978
  let _diskVersionCacheTs = 0;
1979
1979
  const DISK_VERSION_TTL = 60000; // re-check every 60s
1980
+
1981
+ // Read the short HEAD commit by parsing `.git` directly — NO `git` subprocess.
1982
+ // The old `execSync('git rev-parse --short HEAD', { timeout: 5000 })` spawned a
1983
+ // synchronous child process on the dashboard's single event loop; under
1984
+ // concurrent agent git activity (worktree add/merge holding repo locks) that
1985
+ // subprocess could take its full 5s timeout, and because getDiskVersion runs
1986
+ // inside the /api/status rebuild, every in-flight poll blocked for that whole
1987
+ // window. Reading `.git/HEAD` + the resolved ref (loose ref, then packed-refs
1988
+ // fallback) is a couple of tiny synchronous file reads with zero process spawn.
1989
+ // Returns { commit, isGitRepo }: commit is the 7-char short SHA or null.
1990
+ function _readGitHeadShort(repoDir) {
1991
+ try {
1992
+ const gitPath = path.join(repoDir, '.git');
1993
+ let gitDir = gitPath;
1994
+ const st = fs.statSync(gitPath); // throws if no `.git` → caught → not a repo
1995
+ if (st.isFile()) {
1996
+ // Linked worktree: `.git` is a file whose first line reads `gitdir: <abs>`.
1997
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(fs.readFileSync(gitPath, 'utf8'));
1998
+ if (!m) return { commit: null, isGitRepo: false };
1999
+ gitDir = path.isAbsolute(m[1]) ? m[1] : path.resolve(repoDir, m[1]);
2000
+ }
2001
+ const head = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim();
2002
+ let sha = null;
2003
+ const refM = /^ref:\s*(.+?)\s*$/.exec(head);
2004
+ if (refM) {
2005
+ const ref = refM[1];
2006
+ try { sha = fs.readFileSync(path.join(gitDir, ref), 'utf8').trim() || null; } catch {}
2007
+ if (!sha) {
2008
+ // packed-refs fallback (git gc packs loose refs). For the main repo
2009
+ // gitDir IS the common gitdir, so packed-refs lives directly under it.
2010
+ try {
2011
+ const line = fs.readFileSync(path.join(gitDir, 'packed-refs'), 'utf8')
2012
+ .split('\n').find(l => l.endsWith(' ' + ref));
2013
+ if (line) sha = line.slice(0, 40);
2014
+ } catch {}
2015
+ }
2016
+ } else if (/^[0-9a-f]{40}$/i.test(head)) {
2017
+ sha = head; // detached HEAD: the SHA is written directly into HEAD
2018
+ }
2019
+ return { commit: sha ? sha.slice(0, 7) : null, isGitRepo: true };
2020
+ } catch {
2021
+ return { commit: null, isGitRepo: false };
2022
+ }
2023
+ }
2024
+
2025
+ // Two git short-SHA abbreviations name the same commit when one is a prefix of
2026
+ // the other. `git rev-parse --short` picks the minimum-unique length (often 8),
2027
+ // while _readGitHeadShort emits a fixed 7 — a strict `!==` between them would
2028
+ // false-positive into a spurious "version stale" banner. Prefix comparison is
2029
+ // the robust check across abbreviation-length differences.
2030
+ function _commitsDiffer(a, b) {
2031
+ if (!a || !b) return false;
2032
+ return !(a.startsWith(b) || b.startsWith(a));
2033
+ }
2034
+
1980
2035
  function getDiskVersion() {
1981
2036
  const now = Date.now();
1982
2037
  if (_diskVersionCache && (now - _diskVersionCacheTs) < DISK_VERSION_TTL) return _diskVersionCache;
@@ -1990,10 +2045,11 @@ function getDiskVersion() {
1990
2045
  if (!diskVersion) {
1991
2046
  try { diskVersion = require('@yemi33/minions/package.json').version; } catch {}
1992
2047
  }
1993
- let diskCommit = null;
1994
- let isGitRepo = false;
1995
- // Prefer git (authoritative for repo-based dev), fall back to .minions-commit (installed copies)
1996
- try { diskCommit = require('child_process').execSync('git rev-parse --short HEAD', { cwd: MINIONS_DIR, encoding: 'utf8', timeout: 5000, windowsHide: true }).trim(); isGitRepo = true; } catch {}
2048
+ // Prefer git (authoritative for repo-based dev), fall back to .minions-commit (installed copies).
2049
+ // Parse `.git` directly instead of spawning `git rev-parse` — see _readGitHeadShort.
2050
+ const head = _readGitHeadShort(MINIONS_DIR);
2051
+ let diskCommit = head.commit;
2052
+ let isGitRepo = head.isGitRepo;
1997
2053
  if (!diskCommit) {
1998
2054
  try { diskCommit = fs.readFileSync(path.join(MINIONS_DIR, '.minions-commit'), 'utf8').trim() || null; } catch {}
1999
2055
  }
@@ -2450,9 +2506,9 @@ function _buildStatusSlowState() {
2450
2506
  const engine = getEngineState();
2451
2507
  const { diskVersion, diskCommit, isGitRepo } = getDiskVersion();
2452
2508
  const engineStale = !!(engine.codeVersion && diskVersion && engine.codeVersion !== diskVersion) ||
2453
- !!(engine.codeCommit && diskCommit && engine.codeCommit !== diskCommit);
2509
+ _commitsDiffer(engine.codeCommit, diskCommit);
2454
2510
  const dashboardStale = !!(diskVersion && _dashboardVersion.codeVersion && diskVersion !== _dashboardVersion.codeVersion) ||
2455
- !!(diskCommit && _dashboardVersion.codeCommit && diskCommit !== _dashboardVersion.codeCommit);
2511
+ _commitsDiffer(diskCommit, _dashboardVersion.codeCommit);
2456
2512
  return {
2457
2513
  running: engine.codeVersion || null,
2458
2514
  runningCommit: engine.codeCommit || null,
@@ -3056,17 +3112,34 @@ async function _handleStatusRequest(req, res) {
3056
3112
  try {
3057
3113
  _recordDashboardBrowserPresenceFromRequest(req);
3058
3114
 
3059
- // Ensure cache is fresh + yields the event loop mid-rebuild so CC SSE
3060
- // heartbeats keep flowing during slow rebuilds. Synchronous getStatus()
3061
- // still works for SSE-periodic-push callers that can't await.
3062
- try { await refreshStatusAsync(); } catch { /* fall through getStatusJson will sync-rebuild */ }
3063
-
3064
- // Race-guard fallback: if refresh discarded its result because
3065
- // invalidateStatusCache() fired mid-rebuild, _statusCache is still null.
3066
- // Trigger a synchronous rebuild here so the ETag we compute reflects
3067
- // freshly-built post-invalidate state, not the stale pre-invalidate one.
3068
- if (!_statusCache) {
3069
- try { getStatus(); } catch { /* getStatusJson below will retry */ }
3115
+ // Stale-while-revalidate (perf, W-status-swr). Two staleness modes exist:
3116
+ // HARD invalidation invalidateStatusCache() sets _statusCache = null
3117
+ // because a mutation MUST be reflected before we answer. There is
3118
+ // nothing valid to serve, so we await one rebuild (cold path below).
3119
+ // • SOFT staleness — the event-version / mtime signal advanced (e.g. an
3120
+ // agent emitted a state event) but _statusCache still holds a valid,
3121
+ // seconds-old snapshot. Under active-agent load this fires on nearly
3122
+ // every 4s poll. Awaiting the rebuild here is what stalled every
3123
+ // in-flight poll for 6–15s, because the single-flight rebuild runs
3124
+ // synchronous fs/git probes to completion before resolving.
3125
+ // When we have ANY snapshot, never block the request: kick the refresh in
3126
+ // the background (its result lands in _statusCache and bumps the ETag for
3127
+ // the NEXT poll) and serve the current cache now. A 4s-old envelope for a
3128
+ // 4s poller is fine; a 15s-blocked request is not.
3129
+ if (_statusCache) {
3130
+ Promise.resolve().then(refreshStatusAsync).catch(() => { /* next poll + SSE push retry */ });
3131
+ } else {
3132
+ // Cold start or post-hard-invalidation: nothing to serve → await one
3133
+ // rebuild. refreshStatusAsync yields the event loop mid-rebuild so CC
3134
+ // SSE heartbeats keep flowing during a slow build.
3135
+ try { await refreshStatusAsync(); } catch { /* fall through — getStatusJson will sync-rebuild */ }
3136
+ // Race-guard fallback: if refresh discarded its result because
3137
+ // invalidateStatusCache() fired mid-rebuild, _statusCache is still null.
3138
+ // Trigger a synchronous rebuild here so the ETag we compute reflects
3139
+ // freshly-built post-invalidate state, not the stale pre-invalidate one.
3140
+ if (!_statusCache) {
3141
+ try { getStatus(); } catch { /* getStatusJson below will retry */ }
3142
+ }
3070
3143
  }
3071
3144
 
3072
3145
  // ETag = monotonic cache version. Bumped on every successful rebuild and
@@ -12530,9 +12603,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
12530
12603
  const { diskVersion, diskCommit, isGitRepo } = getDiskVersion();
12531
12604
  const engine = getEngineState();
12532
12605
  const engineStale = !!(engine.codeVersion && diskVersion && engine.codeVersion !== diskVersion) ||
12533
- !!(engine.codeCommit && diskCommit && engine.codeCommit !== diskCommit);
12606
+ _commitsDiffer(engine.codeCommit, diskCommit);
12534
12607
  const dashboardStale = !!(diskVersion && _dashboardVersion.codeVersion && diskVersion !== _dashboardVersion.codeVersion) ||
12535
- !!(diskCommit && _dashboardVersion.codeCommit && diskCommit !== _dashboardVersion.codeCommit);
12608
+ _commitsDiffer(diskCommit, _dashboardVersion.codeCommit);
12536
12609
  return jsonReply(res, 200, {
12537
12610
  current: diskVersion,
12538
12611
  currentCommit: diskCommit,
@@ -14060,6 +14133,11 @@ module.exports = {
14060
14133
  _setStatusRefreshHook,
14061
14134
  _resetStatusCacheForTesting,
14062
14135
  _ifNoneMatchHasEtag,
14136
+ // W-status-swr — exported for direct unit tests of the subprocess-free git
14137
+ // HEAD parser and the prefix-tolerant short-SHA comparison. No production
14138
+ // caller imports these; they are test seams.
14139
+ _readGitHeadShort,
14140
+ _commitsDiffer,
14063
14141
  // Exported for the engine-stale-two-signal test (W-mpof1xxe000ac689) — the
14064
14142
  // staleness verdict it stamps on engine.heartbeatStale is the contract under
14065
14143
  // test. No production caller imports this; it is a test seam.
package/docs/README.md CHANGED
@@ -29,7 +29,7 @@ Architecture, design proposals, and lifecycle references for people working on t
29
29
  - [design-state-storage.md](design-state-storage.md) — Design proposal evaluating five database options for replacing Minions' file-based JSON state; recommends `node:sqlite` as the medium-term target (accepted; implementation tracked in CHANGELOG.md Phases 0–9).
30
30
  - [harness-mode.md](harness-mode.md) — Tri-Agent Harness Mode (`harness_mode: "tri_agent"` on scheduled tasks): Planner → Generator → Evaluator loop that iterates a shared on-disk artifact until a rubric passes or the iteration cap fires.
31
31
  - [harness-propagation.md](harness-propagation.md) — How user-level and project-local harness assets (skills, slash-commands, MCP config, `CLAUDE.md` / `AGENTS.md`) propagate into an agent's worktree via `--add-dir`, the `harnessPropagateProjectLocal` flag, and the project-local-on-main worktree-visibility footgun.
32
- - [harness-transparency.md](harness-transparency.md) — The `harnessUsed` self-report contract: capture (agent reports the skills / MCPs / commands / docs it used) → ground (engine cross-checks against `_harnessPropagated` and annotates `grounded:true\|false`, never dropping) → surface (PR comment, notes/inbox digest, work-item modal).
32
+ - [harness-transparency.md](harness-transparency.md) — The `harnessUsed` self-report contract: capture (agent reports the skills / MCPs / commands / docs it used) → ground (engine cross-checks against `_harnessPropagated` and annotates `grounded:true\|false`, never dropping) → surface (PR comment, final agent note, work-item modal).
33
33
  - [kb-sweep.md](kb-sweep.md) — Knowledge-base consolidation sweep (hash dedup → LLM batch dedup/reclassify → per-entry compress) and the detached runner that keeps it alive across `minions restart`.
34
34
  - [keep-processes.md](keep-processes.md) — `meta.keep_processes` sidecar contract: when to use it vs managed-spawn, sidecar schema, caps, and the [`engine/keep-process-sweep.js`](../engine/keep-process-sweep.js) lifecycle.
35
35
  - [live-checkout-mode.md](live-checkout-mode.md) — Per-project opt-in `checkoutMode: 'live'`: skips `git worktree add` and dispatches in-place inside `project.localPath` for `repo`-managed trees, submodule-heavy repos, deep Windows paths, and native build state. Includes the refuse-on-dirty contract and the per-project mutating-concurrency cap of 1.
@@ -119,10 +119,13 @@ evaluation pass) can see what tooling drove a dispatch:
119
119
  paths review/fix agents append the section themselves per
120
120
  `playbooks/shared-rules.md` → "Harness transparency / self-report". Every
121
121
  surface consumes the one renderer, so there is no second formatter to drift.
122
- 2. **notes/inbox digest** — harness usage is summarized into the learnings /
123
- inbox stream that feeds consolidation, so cross-task patterns ("everyone
124
- reaches for skill X on Android fixes") become visible to the team-memory
125
- layer.
122
+ 2. **Final agent note** — for a non-clean completion (failure / partial) the
123
+ single final agent report (`engine/lifecycle.js#writeNonCleanAgentReport`)
124
+ folds the grounded harness footprint in as the same `buildHarnessUsedSection`
125
+ block, so the learnings / inbox stream that feeds consolidation still carries
126
+ it without a separate per-attempt `harness-usage-*` note. Clean completions
127
+ rely on the work-item modal surface (#3) instead — they write no inbox note
128
+ at all (#308 removed the standalone harness-usage digest to cut note spam).
126
129
  3. **Work-item detail modal** — the dashboard work-item modal shows the
127
130
  grounded harness list alongside the completion artifacts, with the
128
131
  `grounded: false` entries visually distinguished (P-d5a6f7c4). On
@@ -4094,6 +4094,11 @@ function writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredComp
4094
4094
  const structuredLines = structuredCompletion
4095
4095
  ? Object.entries(structuredCompletion).map(([key, value]) => `- ${key}: ${value}`).join('\n')
4096
4096
  : '- none';
4097
+ // #308 — fold the agent's grounded harness footprint into THIS final agent
4098
+ // report instead of emitting a separate `harness-usage-*` inbox note per
4099
+ // attempt. buildHarnessUsedSection returns '' for an absent/empty/malformed
4100
+ // record, so the section is appended only when there is something to show.
4101
+ const harnessSection = buildHarnessUsedSection(structuredCompletion?.harnessUsed);
4097
4102
  const content = [
4098
4103
  `# Agent ${outcome === 'partial' ? 'Partially Completed' : 'Reported Failure'}: ${title}`,
4099
4104
  '',
@@ -4108,65 +4113,11 @@ function writeNonCleanAgentReport(dispatchItem, agentId, outcome, structuredComp
4108
4113
  structuredLines,
4109
4114
  '',
4110
4115
  resultSummary ? `## Summary\n${resultSummary}` : '## Summary\n(no agent summary captured)',
4116
+ harnessSection ? `\n${harnessSection}` : '',
4111
4117
  ].filter(Boolean).join('\n');
4112
4118
  shared.writeToInbox(agentId || 'engine', `agent-${outcome}-${dispatchItem.id}`, content, null, metadata);
4113
4119
  }
4114
4120
 
4115
- /**
4116
- * P-f3c8b5e6 — Harness Transparency, Stage-3 readout #3: notes/inbox digest.
4117
- *
4118
- * Routes a compact, one-block-per-dispatch harness-usage summary into
4119
- * notes/inbox/ so engine/consolidation.js#consolidateInbox folds it into the
4120
- * notes.md digest alongside the other inbox findings. Clean dispatches don't
4121
- * otherwise write an inbox note (only failures/non-clean outcomes do, via
4122
- * writeNonCleanAgentReport / dispatch.js writeFailedAgentReport), so without
4123
- * this the harness footprint never reaches the digest for successful runs.
4124
- *
4125
- * Gating: writes nothing unless the GROUNDED harnessUsed (the canonical
4126
- * { skills, mcpServers, commands, docs } shape produced by
4127
- * shared.groundHarnessUsed) renders at least one entry. The render check is
4128
- * delegated to buildHarnessUsedSection (the same platform-neutral renderer the
4129
- * PR-comment surface uses — one renderer, no drift), which returns '' for
4130
- * absent/malformed/all-empty records.
4131
- *
4132
- * Dedup: the inbox slug is keyed on the work-item id (falling back to the
4133
- * dispatch id), so retries of the same WI — which get a fresh dispatch id —
4134
- * collapse onto one note per day (writeToInbox skips when a same-prefix file
4135
- * already exists). This keeps busy fleets from flooding the digest.
4136
- *
4137
- * Best-effort: never throws into the completion path. Returns the note id on
4138
- * write, or false when nothing was written (empty footprint, dedup hit, or no
4139
- * dispatch id).
4140
- */
4141
- function writeHarnessUsageDigest(dispatchItem, agentId, harnessUsed) {
4142
- try {
4143
- if (!dispatchItem?.id) return false;
4144
- const section = buildHarnessUsedSection(harnessUsed);
4145
- if (!section) return false; // empty / absent / malformed → write nothing
4146
- const itemId = dispatchItem.meta?.item?.id || '';
4147
- const dedupKey = itemId || dispatchItem.id;
4148
- const title = dispatchItem.meta?.item?.title || dispatchItem.task || dispatchItem.id;
4149
- const metadata = {
4150
- dispatchId: dispatchItem.id,
4151
- sourceItem: itemId || null,
4152
- kind: 'harness-usage',
4153
- };
4154
- const content = [
4155
- `# Harnesses used: ${title}`,
4156
- '',
4157
- `**Agent:** ${agentId || 'engine'}`,
4158
- `**Dispatch:** \`${dispatchItem.id}\``,
4159
- itemId ? `**Work Item:** \`${itemId}\`` : '',
4160
- `**Type:** ${dispatchItem.type || 'unknown'}`,
4161
- '',
4162
- section,
4163
- ].filter(Boolean).join('\n');
4164
- return shared.writeToInbox(agentId || 'engine', `harness-usage-${dedupKey}`, content, null, metadata);
4165
- } catch (err) {
4166
- log('warn', `Harness-usage inbox digest write failed for ${dispatchItem?.id} (non-fatal): ${err.message}`);
4167
- return false;
4168
- }
4169
- }
4170
4121
 
4171
4122
  /**
4172
4123
  * Permissively pull all assistant-message content out of a stream-json log.
@@ -4753,9 +4704,15 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
4753
4704
  // The grounded result replaces `structuredCompletion.harnessUsed` IN PLACE, so
4754
4705
  // it rides the existing `structuredCompletion` storage onto the completed
4755
4706
  // dispatch record (completeDispatch persists `item.structuredCompletion`) for
4756
- // the Stage-3 surfaces (PR comment / inbox digest / WI modal) to read. Pure,
4757
- // non-destructive (grounded:false entries are kept + flagged, never dropped),
4758
- // and best-effort: any failure here must never block completion.
4707
+ // the Stage-3 surfaces (PR comment / final agent note / WI modal) to read.
4708
+ // Pure, non-destructive (grounded:false entries are kept + flagged, never
4709
+ // dropped), and best-effort: any failure here must never block completion.
4710
+ //
4711
+ // #308 — the grounded footprint is no longer routed to its own
4712
+ // `harness-usage-*` inbox note (that produced one duplicate-looking note chip
4713
+ // per attempt). For clean completions the structured `_harnessUsed` WI-modal
4714
+ // UI surfaces it; for non-clean outcomes writeNonCleanAgentReport folds the
4715
+ // same buildHarnessUsedSection block into the single final agent note.
4759
4716
  if (structuredCompletion && structuredCompletion.harnessUsed) {
4760
4717
  try {
4761
4718
  const propagated = resolveHarnessPropagated(dispatchItem);
@@ -4764,11 +4721,6 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
4764
4721
  } catch (err) {
4765
4722
  log('warn', `Harness grounding cross-check failed for ${dispatchItem.id} (non-fatal): ${err.message}`);
4766
4723
  }
4767
- // P-f3c8b5e6 — Stage-3 readout #3: route the grounded footprint into the
4768
- // notes/inbox digest. Gated on a non-empty render + deduped per WI inside
4769
- // writeHarnessUsageDigest, so this is safe to call unconditionally for
4770
- // every dispatch (clean or not) that self-reported a harness footprint.
4771
- writeHarnessUsageDigest(dispatchItem, agentId, structuredCompletion.harnessUsed);
4772
4724
  }
4773
4725
 
4774
4726
  const completionGateSummary = resultSummary || (typeof stdout === 'string' && !stdout.includes('"type":') ? stdout : '');
@@ -5903,8 +5855,10 @@ module.exports = {
5903
5855
  markMissingPrAttachment,
5904
5856
  parseCompletionReportFile,
5905
5857
  resolveHarnessPropagated,
5906
- // P-f3c8b5e6notes/inbox harness-usage digest (exported for unit testing).
5907
- writeHarnessUsageDigest,
5858
+ // #308 — exported for unit testing: the final non-clean agent note now folds
5859
+ // the grounded harness footprint in as a section instead of emitting a
5860
+ // standalone harness-usage-* inbox note.
5861
+ writeNonCleanAgentReport,
5908
5862
  normalizeCompletionArtifacts,
5909
5863
  completionArtifactToNoteEntry,
5910
5864
  mergeArtifactNotes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.2235",
3
+ "version": "0.1.2237",
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"