@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.
- package/dashboard/js/refresh.js +1 -1
- package/dashboard/js/render-work-items.js +45 -3
- package/dashboard.js +97 -19
- package/docs/README.md +1 -1
- package/docs/harness-transparency.md +7 -4
- package/engine/lifecycle.js +19 -65
- package/package.json +1 -1
package/dashboard/js/refresh.js
CHANGED
|
@@ -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">📺</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">✎</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">📦</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">👍👎</button>' : (item._humanFeedback ? '<span style="font-size:var(--text-xs)" title="Feedback given">' + (item._humanFeedback.rating === 'up' ? '👍' : '👎') + '</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 "Why this is blocked" below">⚠ 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">📺 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
|
|
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
|
-
}
|
|
942
|
-
|
|
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
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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
|
-
|
|
2509
|
+
_commitsDiffer(engine.codeCommit, diskCommit);
|
|
2454
2510
|
const dashboardStale = !!(diskVersion && _dashboardVersion.codeVersion && diskVersion !== _dashboardVersion.codeVersion) ||
|
|
2455
|
-
|
|
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
|
-
//
|
|
3060
|
-
//
|
|
3061
|
-
//
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
//
|
|
3065
|
-
//
|
|
3066
|
-
//
|
|
3067
|
-
//
|
|
3068
|
-
|
|
3069
|
-
|
|
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
|
-
|
|
12606
|
+
_commitsDiffer(engine.codeCommit, diskCommit);
|
|
12534
12607
|
const dashboardStale = !!(diskVersion && _dashboardVersion.codeVersion && diskVersion !== _dashboardVersion.codeVersion) ||
|
|
12535
|
-
|
|
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,
|
|
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. **
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
package/engine/lifecycle.js
CHANGED
|
@@ -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 /
|
|
4757
|
-
// non-destructive (grounded:false entries are kept + flagged, never
|
|
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
|
-
//
|
|
5907
|
-
|
|
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.
|
|
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"
|