@yemi33/minions 0.1.2236 → 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 +12 -0
- package/dashboard.js +97 -19
- 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>');
|
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/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"
|