@yemi33/minions 0.1.2087 → 0.1.2088
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 +598 -160
- package/dashboard/js/render-dispatch.js +77 -0
- package/dashboard/js/render-inbox.js +72 -0
- package/dashboard/js/render-meetings.js +55 -0
- package/dashboard/js/render-plans.js +14 -9
- package/dashboard/js/render-prd.js +13 -6
- package/dashboard/js/render-prs.js +55 -0
- package/dashboard/js/render-watches.js +16 -0
- package/dashboard/js/render-work-items.js +70 -0
- package/dashboard/js/settings.js +1 -5
- package/dashboard/js/state.js +9 -3
- package/dashboard.js +400 -358
- package/docs/security.md +23 -0
- package/engine/ado.js +54 -54
- package/engine/cli.js +3 -38
- package/engine/db/index.js +1 -1
- package/engine/db/migrations/002-dispatches.js +1 -1
- package/engine/db/migrations/003-work-items.js +1 -1
- package/engine/db/migrations/004-pull-requests.js +1 -1
- package/engine/dispatch.js +8 -2
- package/engine/github.js +38 -38
- package/engine/lifecycle.js +192 -18
- package/engine/projects.js +92 -0
- package/engine/queries.js +61 -129
- package/engine/shared.js +85 -89
- package/engine/watches.js +5 -5
- package/engine.js +23 -34
- package/package.json +2 -2
package/engine/queries.js
CHANGED
|
@@ -210,6 +210,40 @@ function _findDispatchEntry(dispatch, id) {
|
|
|
210
210
|
return null;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
function _mergeArtifactNotes(existing, additions) {
|
|
214
|
+
const merged = [];
|
|
215
|
+
const seen = new Set();
|
|
216
|
+
function normalize(note) {
|
|
217
|
+
if (shared.isPlainObject(note)) {
|
|
218
|
+
const file = String(note.file || '').trim();
|
|
219
|
+
if (!file) return null;
|
|
220
|
+
const out = { file };
|
|
221
|
+
for (const key of ['id', 'title', 'path']) {
|
|
222
|
+
if (note[key] == null) continue;
|
|
223
|
+
const value = String(note[key]).trim();
|
|
224
|
+
if (value) out[key] = value;
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
const file = String(note || '').trim();
|
|
229
|
+
return file ? file : null;
|
|
230
|
+
}
|
|
231
|
+
function keyFor(note) {
|
|
232
|
+
return shared.isPlainObject(note)
|
|
233
|
+
? String(note.file || '')
|
|
234
|
+
: String(note || '');
|
|
235
|
+
}
|
|
236
|
+
for (const note of [...(Array.isArray(existing) ? existing : []), ...(Array.isArray(additions) ? additions : [])]) {
|
|
237
|
+
const normalized = normalize(note);
|
|
238
|
+
if (!normalized) continue;
|
|
239
|
+
const key = keyFor(normalized);
|
|
240
|
+
if (seen.has(key)) continue;
|
|
241
|
+
seen.add(key);
|
|
242
|
+
merged.push(normalized);
|
|
243
|
+
}
|
|
244
|
+
return merged;
|
|
245
|
+
}
|
|
246
|
+
|
|
213
247
|
function _completionReportPathForEntry(entryOrId) {
|
|
214
248
|
const id = typeof entryOrId === 'string' ? entryOrId : entryOrId?.id;
|
|
215
249
|
return (typeof entryOrId === 'object' && entryOrId?.meta?.completionReportPath)
|
|
@@ -376,7 +410,7 @@ function getMetrics() {
|
|
|
376
410
|
const agent = (pr.agent || '').toLowerCase();
|
|
377
411
|
if (!agent || agent.startsWith('temp-')) continue;
|
|
378
412
|
prCountByAgent[agent] = (prCountByAgent[agent] || 0) + 1;
|
|
379
|
-
if (pr.reviewStatus ===
|
|
413
|
+
if (pr.reviewStatus === shared.REVIEW_STATUS.APPROVED || pr.status === shared.PR_STATUS.MERGED) prApprovedByAgent[agent] = (prApprovedByAgent[agent] || 0) + 1;
|
|
380
414
|
if (pr.reviewStatus === 'rejected') prRejectedByAgent[agent] = (prRejectedByAgent[agent] || 0) + 1;
|
|
381
415
|
}
|
|
382
416
|
|
|
@@ -1436,7 +1470,7 @@ function getWorkItems(config) {
|
|
|
1436
1470
|
// Best-effort enrichment for work item _artifacts.notes, not correctness-critical.
|
|
1437
1471
|
const _kbEntries = getKnowledgeBaseEntriesSnapshot();
|
|
1438
1472
|
for (const item of allItems) {
|
|
1439
|
-
const arts = {};
|
|
1473
|
+
const arts = shared.isPlainObject(item._artifacts) ? { ...item._artifacts } : {};
|
|
1440
1474
|
const agentId = item.dispatched_to || item.agent;
|
|
1441
1475
|
if (agentId) {
|
|
1442
1476
|
// Output log — match by dispatch ID (output-{dispatchId}.log)
|
|
@@ -1461,7 +1495,7 @@ function getWorkItems(config) {
|
|
|
1461
1495
|
const matchArchive = _archiveFiles.filter(f => f.includes(agentId) && f.includes(itemId));
|
|
1462
1496
|
for (const f of matchArchive) allNotes.push('archive:' + f);
|
|
1463
1497
|
}
|
|
1464
|
-
if (allNotes.length > 0) arts.notes = allNotes;
|
|
1498
|
+
if (allNotes.length > 0) arts.notes = _mergeArtifactNotes(arts.notes, allNotes);
|
|
1465
1499
|
}
|
|
1466
1500
|
if (item.branch || item.featureBranch) arts.branch = item.branch || item.featureBranch;
|
|
1467
1501
|
if (item.sourcePlan) arts.sourcePlan = item.sourcePlan;
|
|
@@ -2269,99 +2303,26 @@ function _invalidateMtimePathsCache() {
|
|
|
2269
2303
|
}
|
|
2270
2304
|
|
|
2271
2305
|
function getStatusFastStateMtimePaths(config) {
|
|
2306
|
+
// Issue #2949 — /api/status was slimmed to just engine/throttle state +
|
|
2307
|
+
// skills/mcp/projects/version. dispatch.json, work-items.json,
|
|
2308
|
+
// pull-requests.json, watches.json, notes.md, INBOX_DIR, qa-runs.json,
|
|
2309
|
+
// and the meetings dir all moved to dedicated /api/<x> endpoints which
|
|
2310
|
+
// have their OWN input-mtime ETags. Continuing to track those files in
|
|
2311
|
+
// the fast-state registry would bust the /api/status outer cache on
|
|
2312
|
+
// every engine write to them — pure wasted rebuild for slices that no
|
|
2313
|
+
// longer appear in the payload.
|
|
2314
|
+
//
|
|
2315
|
+
// Fast-state inputs are now exclusively in-memory (engine heartbeat,
|
|
2316
|
+
// ADO/GH throttle counters) so no file-mtime tracking is required.
|
|
2317
|
+
// Returning [] makes the fast tracker a no-op; the slow tracker still
|
|
2318
|
+
// catches the few file-derived slices that remain (skills, mcp,
|
|
2319
|
+
// projects/git, autoMode/config). The cache version is bumped by
|
|
2320
|
+
// `emitStateEvent` (db-events) when the slow registry detects a
|
|
2321
|
+
// change OR when explicit invalidateStatusCache() callers fire.
|
|
2272
2322
|
config = config || getConfig();
|
|
2273
2323
|
const cacheKey = _mtimePathsCacheKey(config);
|
|
2274
2324
|
if (_fastMtimePathsCache && _fastMtimePathsCacheKey === cacheKey) return _fastMtimePathsCache;
|
|
2275
|
-
const
|
|
2276
|
-
const files = [
|
|
2277
|
-
// Engine-level state surfaced by getDispatchQueue. `control.json`,
|
|
2278
|
-
// `log.json`, and `metrics.json` are intentionally omitted — see the
|
|
2279
|
-
// "Files intentionally NOT tracked" section above
|
|
2280
|
-
// (W-mpg8aapw001d7e0c, W-mphejzct00065d8c). dispatch.json stays
|
|
2281
|
-
// tracked because the home + engine sidebar activity dots and
|
|
2282
|
-
// renderDispatch() consume dispatch transitions at sub-FAST_STATE_TTL
|
|
2283
|
-
// cadence (active → completed flips must light up within one SPA
|
|
2284
|
-
// poll, not wait up to 10 s for the periodic SSE backstop).
|
|
2285
|
-
DISPATCH_PATH,
|
|
2286
|
-
// Watches surfaced by watchesMod.getWatches() (W-mpftp7na000td0f4 fix).
|
|
2287
|
-
path.join(ENGINE_DIR, 'watches.json'),
|
|
2288
|
-
// Central work-items.json surfaced by getWorkItems().
|
|
2289
|
-
path.join(MINIONS_DIR, 'work-items.json'),
|
|
2290
|
-
// notes.md (surfaced by getNotesWithMeta) — consolidation writes this
|
|
2291
|
-
// when an inbox batch is processed. Single file, mtime advances on every
|
|
2292
|
-
// write. Without this, the dashboard's notes view sat stale for up to
|
|
2293
|
-
// FAST_STATE_TTL (10 s) after each consolidation cycle.
|
|
2294
|
-
NOTES_PATH,
|
|
2295
|
-
// notes/inbox/ (surfaced by getInbox) — every writeToInbox call CREATES
|
|
2296
|
-
// a new file (engine/shared.js#writeToInbox always uses a uid'd path,
|
|
2297
|
-
// never an in-place edit), so the directory's mtime is a reliable
|
|
2298
|
-
// entry-add/remove signal even on Windows NTFS. Without it, PR-comment
|
|
2299
|
-
// notifications, agent-failure summaries, follow-up build alerts, and
|
|
2300
|
-
// meeting-transcript dumps all lagged up to 10 s before appearing on
|
|
2301
|
-
// the dashboard's inbox view.
|
|
2302
|
-
INBOX_DIR,
|
|
2303
|
-
// engine/qa-runs.json (surfaced by listRuns via fast-state qaRuns slice)
|
|
2304
|
-
// — new QA runs and status flips need to light the sidebar activity dot
|
|
2305
|
-
// within one SPA poll cycle. Single file, mtime advances on each write.
|
|
2306
|
-
path.join(ENGINE_DIR, 'qa-runs.json'),
|
|
2307
|
-
];
|
|
2308
|
-
// meetings/<id>.json (surfaced by meeting.getMeetings) — round transitions
|
|
2309
|
-
// edit each file in-place via mutateMeeting, so the parent dir's mtime
|
|
2310
|
-
// does NOT advance on Windows. Tracking each file individually catches
|
|
2311
|
-
// in-file edits. Bounded by active meeting count; safeWrite's `.json.backup`
|
|
2312
|
-
// tempfile sidecars are excluded by the `.json` suffix check (a path
|
|
2313
|
-
// ending in `.json.backup` does not end in `.json`).
|
|
2314
|
-
try {
|
|
2315
|
-
const meetingsDir = path.join(MINIONS_DIR, 'meetings');
|
|
2316
|
-
for (const f of fs.readdirSync(meetingsDir)) {
|
|
2317
|
-
if (f.endsWith('.json')) files.push(path.join(meetingsDir, f));
|
|
2318
|
-
}
|
|
2319
|
-
} catch { /* meetings dir absent → no meetings to track */ }
|
|
2320
|
-
// Per-project work-items (surfaced by getWorkItems) and pull-requests
|
|
2321
|
-
// (surfaced by getPullRequests). The PR file was the biggest miss in the
|
|
2322
|
-
// original tracked list — PR status flips (running → passing, waiting →
|
|
2323
|
-
// approved) were waiting on the 10 s SSE backstop instead of the next
|
|
2324
|
-
// 4 s SPA poll.
|
|
2325
|
-
//
|
|
2326
|
-
// Per-project `.git/logs/HEAD` (the reflog) — added to cut the project-
|
|
2327
|
-
// branch-state lag (was 15–25 s typical). The reflog is appended on every
|
|
2328
|
-
// HEAD-changing operation (checkout, commit, reset, merge, rebase), so
|
|
2329
|
-
// its mtime is a perfect signal that getProjectGitStatus's cached value
|
|
2330
|
-
// is stale. Unlike control.json (intentionally excluded above), it is
|
|
2331
|
-
// NOT advanced on a timer — it only moves on user-initiated git
|
|
2332
|
-
// operations, so it can't dominate legitimate state changes. Cheap (one
|
|
2333
|
-
// statSync per project per cache miss).
|
|
2334
|
-
//
|
|
2335
|
-
// Per-project `.git/FETCH_HEAD` — bare `git fetch` advances FETCH_HEAD
|
|
2336
|
-
// without touching `.git/logs/HEAD` (no HEAD move) or `.git/index` (no
|
|
2337
|
-
// working-tree change). Without this entry, an external `git fetch`
|
|
2338
|
-
// (or another tool background-fetching) leaves the dashboard's
|
|
2339
|
-
// _statusCache and getProjectGitStatus's inner cache both serving the
|
|
2340
|
-
// pre-fetch ahead/behind counts until BOTH the 10s FAST_STATE_TTL and
|
|
2341
|
-
// the 15s probe TTL expire (W-mphdmr8c00030124).
|
|
2342
|
-
//
|
|
2343
|
-
// For linked worktrees (`<localPath>/.git` is a file pointing to
|
|
2344
|
-
// `<main>/.git/worktrees/<name>/`), `_resolveGitDir` walks the pointer
|
|
2345
|
-
// so logs/HEAD and FETCH_HEAD are tracked at the actual gitdir; the
|
|
2346
|
-
// statSync in dashboard's `_getMtimes` tolerates ENOENT, so falling
|
|
2347
|
-
// back to `<localPath>/.git/...` for non-linked-worktree repos is safe.
|
|
2348
|
-
for (const p of projects) {
|
|
2349
|
-
files.push(shared.projectWorkItemsPath(p));
|
|
2350
|
-
files.push(shared.projectPrPath(p));
|
|
2351
|
-
if (p && p.localPath) {
|
|
2352
|
-
const gitDir = _resolveGitDir(p.localPath) || path.join(p.localPath, '.git');
|
|
2353
|
-
// logs/HEAD is per-worktree (HEAD moves, commits, checkouts).
|
|
2354
|
-
// FETCH_HEAD lives in the COMMON gitdir — `git fetch` from a linked
|
|
2355
|
-
// worktree writes to `<main>/.git/FETCH_HEAD`, not to the
|
|
2356
|
-
// per-worktree subdir. Tracking only the per-worktree path here
|
|
2357
|
-
// would leave linked-worktree projects stuck after `git fetch`
|
|
2358
|
-
// (the file at `<main>/.git/worktrees/<name>/FETCH_HEAD` never
|
|
2359
|
-
// exists — verified empirically).
|
|
2360
|
-
const commonGitDir = _resolveCommonGitDir(gitDir);
|
|
2361
|
-
files.push(path.join(gitDir, 'logs', 'HEAD'));
|
|
2362
|
-
files.push(path.join(commonGitDir, 'FETCH_HEAD'));
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2325
|
+
const files = [];
|
|
2365
2326
|
_fastMtimePathsCache = files;
|
|
2366
2327
|
_fastMtimePathsCacheKey = cacheKey;
|
|
2367
2328
|
return files;
|
|
@@ -2441,35 +2402,13 @@ function getStatusSlowStateMtimePaths(config) {
|
|
|
2441
2402
|
if (_slowMtimePathsCache && _slowMtimePathsCacheKey === cacheKey) return _slowMtimePathsCache;
|
|
2442
2403
|
const projects = getProjects(config);
|
|
2443
2404
|
const homeDir = os.homedir();
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
path.join(MINIONS_DIR, 'prd', 'guides'),
|
|
2452
|
-
// engine/schedule-runs.json — scheduler rewrites this on every cron fire.
|
|
2453
|
-
path.join(ENGINE_DIR, 'schedule-runs.json'),
|
|
2454
|
-
// engine/pipeline-runs.json — pipeline executor rewrites this on each
|
|
2455
|
-
// stage transition (the most user-visible slow-state lag pre-fix).
|
|
2456
|
-
path.join(ENGINE_DIR, 'pipeline-runs.json'),
|
|
2457
|
-
// pipelines/*.json — pipeline definitions, edited by humans + plan agents.
|
|
2458
|
-
path.join(MINIONS_DIR, 'pipelines'),
|
|
2459
|
-
// pinned.md — single file, dashboard-side writes already call
|
|
2460
|
-
// invalidateStatusCache({includeSlow:true}); tracker entry catches any
|
|
2461
|
-
// CLI/editor edit that bypasses the API.
|
|
2462
|
-
path.join(MINIONS_DIR, 'pinned.md'),
|
|
2463
|
-
// work-items.json — central + per-project files (per-project pushed below
|
|
2464
|
-
// alongside the PR paths). The PRD progress slice in slow-state is
|
|
2465
|
-
// *derived* from work-item statuses via getPrdInfo's input-hash, so a
|
|
2466
|
-
// WI flipping dispatched→done changes prdProgress without touching any
|
|
2467
|
-
// file in this tracker. Without this entry the slow-state cache hangs
|
|
2468
|
-
// on stale PRD progress for up to 60s after a WI completes (user
|
|
2469
|
-
// report: visit /plans, see all items active; switch to /home + hard
|
|
2470
|
-
// refresh; the WIs are done; return to /plans, now items show as done).
|
|
2471
|
-
path.join(MINIONS_DIR, 'work-items.json'),
|
|
2472
|
-
];
|
|
2405
|
+
// Issue #2949 — slow-state now contains ONLY skills, mcpServers, projects,
|
|
2406
|
+
// autoMode, initialized, installId, version. PRD/PRDProgress/verifyGuides/
|
|
2407
|
+
// archivedPrds/schedules/pipelines/pinned/work-items all moved to dedicated
|
|
2408
|
+
// /api/<x> endpoints which carry their own ETags; tracking their backing
|
|
2409
|
+
// files here would bust the /api/status outer cache for slices that aren't
|
|
2410
|
+
// in the payload anymore.
|
|
2411
|
+
const files = [];
|
|
2473
2412
|
|
|
2474
2413
|
// Skill discovery roots (surfaced by _buildStatusSlowState → getSkills).
|
|
2475
2414
|
// Mirrors collectSkillFiles' source enumeration so adding a new runtime
|
|
@@ -2519,13 +2458,6 @@ function getStatusSlowStateMtimePaths(config) {
|
|
|
2519
2458
|
const commonGitDir = _resolveCommonGitDir(gitDir);
|
|
2520
2459
|
files.push(path.join(gitDir, 'logs', 'HEAD'));
|
|
2521
2460
|
files.push(path.join(commonGitDir, 'FETCH_HEAD'));
|
|
2522
|
-
// Per-project work-items.json + pull-requests.json — same reason as the
|
|
2523
|
-
// central work-items.json above: the prdProgress slice is derived from
|
|
2524
|
-
// their contents via getPrdInfo's input hash. A WI completion in a
|
|
2525
|
-
// project that uses its own work-items.json file would otherwise hang
|
|
2526
|
-
// slow-state until TTL.
|
|
2527
|
-
try { files.push(projectWorkItemsPath(project)); } catch { /* path helper optional */ }
|
|
2528
|
-
try { files.push(projectPrPath(project)); } catch { /* path helper optional */ }
|
|
2529
2461
|
}
|
|
2530
2462
|
|
|
2531
2463
|
_slowMtimePathsCache = files;
|
package/engine/shared.js
CHANGED
|
@@ -1843,7 +1843,6 @@ const KB_READABLE_CATEGORIES = Object.freeze([
|
|
|
1843
1843
|
'consolidated', 'consolidation', 'consolidations',
|
|
1844
1844
|
'team-memory', 'general', 'patterns',
|
|
1845
1845
|
]);
|
|
1846
|
-
|
|
1847
1846
|
/**
|
|
1848
1847
|
* Classify an inbox item into a knowledge base category.
|
|
1849
1848
|
* Single source of truth — used by consolidation.js (both LLM and regex paths).
|
|
@@ -1913,6 +1912,14 @@ const ENGINE_DEFAULTS = {
|
|
|
1913
1912
|
prNoOpFixPauseAttempts: 2, // pause one PR automation cause after repeated no-op fixes for unchanged evidence
|
|
1914
1913
|
completionReportRetentionDays: 90, // retain completion report sidecars beyond capped dispatch history
|
|
1915
1914
|
completionReportMaxFiles: 5000, // hard cap for completion report sidecars during cleanup
|
|
1915
|
+
// P-bfa2c-cors-wildcard: extra Origins permitted to receive an
|
|
1916
|
+
// `Access-Control-Allow-Origin` echo on GET/HEAD dashboard responses
|
|
1917
|
+
// beyond the default `http://localhost:7331`. Default empty (security
|
|
1918
|
+
// default). Reverse-proxy or alternate-port deployments set this to
|
|
1919
|
+
// opt those origins in. Entries are matched verbatim against the
|
|
1920
|
+
// request's `Origin` header (scheme + host + port — no path/wildcards).
|
|
1921
|
+
// See `shared.isAllowedDashboardOrigin()` and the dashboard CORS prelude.
|
|
1922
|
+
allowedDashboardOrigins: [],
|
|
1916
1923
|
meetingRoundTimeout: 900000, // 15min per meeting round — soft signal; logs a "still waiting" warning each tick
|
|
1917
1924
|
meetingRoundHardTimeout: 3600000, // 60min hard backstop — non-terminal participants are marked failed and the round advances. Prevents permanent stalls if an agent's dispatch never spawns or its completion gets dropped.
|
|
1918
1925
|
evalLoop: true, // enable review→fix loop after implementation completes
|
|
@@ -2183,33 +2190,6 @@ const ENGINE_DEFAULTS = {
|
|
|
2183
2190
|
// Settings UI exposes this as a free-text input; clearing the field deletes
|
|
2184
2191
|
// the override and falls back to auto-resolution.
|
|
2185
2192
|
operatorLogin: null,
|
|
2186
|
-
// ── /api/status workItems retention (W-mphejzmj000718bf) ────────────────────
|
|
2187
|
-
// Optional age-based trim for done/failed/cancelled work items in the
|
|
2188
|
-
// /api/status workItems slice. Default 0 = no trim (full list shipped). The
|
|
2189
|
-
// bulk of the payload savings (~3MB → ~500KB) comes from _slimWorkItemForStatus
|
|
2190
|
-
// dropping description / full acceptanceCriteria / references — that slim
|
|
2191
|
-
// projection runs unconditionally. The date filter on top was a second-tier
|
|
2192
|
-
// optimization that surfaced as data loss to users (completed items vanishing
|
|
2193
|
-
// from /api/status after 7 days) so it now opts in via a positive integer.
|
|
2194
|
-
// Active items (pending/dispatched/queued) are ALWAYS shipped regardless of
|
|
2195
|
-
// age. The detail modal fetches the full record on demand via
|
|
2196
|
-
// GET /api/work-items/<id> when description/references/AC are needed.
|
|
2197
|
-
statusWorkItemsRetentionDays: 0,
|
|
2198
|
-
|
|
2199
|
-
// ── /api/status meetings retention (W-mphlrxx6000a8760) ─────────────────────
|
|
2200
|
-
// Same shape as statusWorkItemsRetentionDays — optional age-based trim for
|
|
2201
|
-
// completed/archived meetings in the /api/status meetings slice. Default 0
|
|
2202
|
-
// = no trim (full list shipped). The slim projection (which collapses
|
|
2203
|
-
// ~95KB+ per-round findings/debate/transcript bodies down to {agentId: true}
|
|
2204
|
-
// sentinels) delivers the bulk of the payload savings and always runs.
|
|
2205
|
-
// The date filter on top was demoted to opt-in for the same reason as the
|
|
2206
|
-
// workItems trim: vanishing completed meetings read as data loss. Active
|
|
2207
|
-
// meetings (investigating/debating/concluding) are ALWAYS shipped regardless
|
|
2208
|
-
// of age. The detail modal fetches the full record (findings, debate,
|
|
2209
|
-
// conclusion, transcript bodies) on demand via GET /api/meetings/<id>.
|
|
2210
|
-
// A top-level meetingsTotal field is synthesized so the sidebar activity
|
|
2211
|
-
// dot still fires when ANY meeting gains a new round.
|
|
2212
|
-
statusMeetingsRetentionDays: 0,
|
|
2213
2193
|
};
|
|
2214
2194
|
|
|
2215
2195
|
// ─── Runtime Fleet Resolution (P-3b8e5f1d) ──────────────────────────────────
|
|
@@ -2406,64 +2386,6 @@ function _resetLegacyCcModelMigrationFlag() {
|
|
|
2406
2386
|
_legacyCcModelMigrationLogged = false;
|
|
2407
2387
|
}
|
|
2408
2388
|
|
|
2409
|
-
// ─── Stale statusWorkItemsRetentionDays Default Migration ────────────────────
|
|
2410
|
-
//
|
|
2411
|
-
// The retention default was 7 from W-mphejzmj000718bf until users reported the
|
|
2412
|
-
// trim hid completed work items from /api/status, which read as data loss.
|
|
2413
|
-
// We flipped the baked-in default to 0 (no trim). Installs that opened the
|
|
2414
|
-
// Settings page while the default was 7 have `engine.statusWorkItemsRetentionDays: 7`
|
|
2415
|
-
// persisted in their config.json — the resolver would return 7 and they'd
|
|
2416
|
-
// still see the trim. This shim drops a literal `7` at load time so the new
|
|
2417
|
-
// default of 0 applies. Operators who explicitly set a non-7 value (e.g. 14
|
|
2418
|
-
// or 30) are left untouched. No on-disk rewrite.
|
|
2419
|
-
|
|
2420
|
-
let _staleRetentionMigrationLogged = false;
|
|
2421
|
-
|
|
2422
|
-
function applyStatusWorkItemsRetentionMigration(config, { logger = log } = {}) {
|
|
2423
|
-
if (!config || !config.engine || typeof config.engine !== 'object') return false;
|
|
2424
|
-
const e = config.engine;
|
|
2425
|
-
if (e.statusWorkItemsRetentionDays !== 7) return false;
|
|
2426
|
-
delete e.statusWorkItemsRetentionDays;
|
|
2427
|
-
if (!_staleRetentionMigrationLogged) {
|
|
2428
|
-
_staleRetentionMigrationLogged = true;
|
|
2429
|
-
try {
|
|
2430
|
-
logger('warn', 'statusWorkItemsRetentionDays=7 was the previous default — clearing in-memory so the new default (0, no trim) applies. Re-save Settings to persist or set a positive value to opt back in.');
|
|
2431
|
-
} catch { /* logger may not be wired during tests — best-effort */ }
|
|
2432
|
-
}
|
|
2433
|
-
return true;
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
/** Test helper: reset the dedup flag so repeated tests can re-trigger the log. */
|
|
2437
|
-
function _resetStaleRetentionMigrationFlag() {
|
|
2438
|
-
_staleRetentionMigrationLogged = false;
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
// Same shape as applyStatusWorkItemsRetentionMigration above, for the meetings
|
|
2442
|
-
// slice. The prior baked-in default of 7 caused completed/archived meetings to
|
|
2443
|
-
// vanish from /api/status after a week; we flipped the default to 0 and strip
|
|
2444
|
-
// the literal 7 from persisted configs so the new behavior applies.
|
|
2445
|
-
|
|
2446
|
-
let _staleMeetingsRetentionMigrationLogged = false;
|
|
2447
|
-
|
|
2448
|
-
function applyStatusMeetingsRetentionMigration(config, { logger = log } = {}) {
|
|
2449
|
-
if (!config || !config.engine || typeof config.engine !== 'object') return false;
|
|
2450
|
-
const e = config.engine;
|
|
2451
|
-
if (e.statusMeetingsRetentionDays !== 7) return false;
|
|
2452
|
-
delete e.statusMeetingsRetentionDays;
|
|
2453
|
-
if (!_staleMeetingsRetentionMigrationLogged) {
|
|
2454
|
-
_staleMeetingsRetentionMigrationLogged = true;
|
|
2455
|
-
try {
|
|
2456
|
-
logger('warn', 'statusMeetingsRetentionDays=7 was the previous default — clearing in-memory so the new default (0, no trim) applies. Re-save Settings to persist or set a positive value to opt back in.');
|
|
2457
|
-
} catch { /* logger may not be wired during tests — best-effort */ }
|
|
2458
|
-
}
|
|
2459
|
-
return true;
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2462
|
-
/** Test helper: reset the dedup flag so repeated tests can re-trigger the log. */
|
|
2463
|
-
function _resetStaleMeetingsRetentionMigrationFlag() {
|
|
2464
|
-
_staleMeetingsRetentionMigrationLogged = false;
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
2389
|
// ─── Runtime Config Preflight Warnings ──────────────────────────────────────
|
|
2468
2390
|
//
|
|
2469
2391
|
// Emit non-fatal warnings about runtime/CLI configuration drift. Consumed by
|
|
@@ -2732,6 +2654,44 @@ const PR_POLLABLE_STATUSES = new Set([PR_STATUS.ACTIVE, PR_STATUS.LINKED]);
|
|
|
2732
2654
|
const PR_PENDING_REASON = {
|
|
2733
2655
|
MISSING_BRANCH: 'missing_pr_branch',
|
|
2734
2656
|
};
|
|
2657
|
+
// PR build-status enum — single source of truth for the literal strings written to
|
|
2658
|
+
// pull-requests.json `buildStatus`. Previously drifted across engine/ado.js,
|
|
2659
|
+
// engine/github.js, engine/lifecycle.js, engine/watches.js, engine/queries.js, engine/cli.js
|
|
2660
|
+
// (P-bfa3d-constants-eslint, audit items #68-#82).
|
|
2661
|
+
const BUILD_STATUS = {
|
|
2662
|
+
PASSING: 'passing',
|
|
2663
|
+
FAILING: 'failing',
|
|
2664
|
+
RUNNING: 'running',
|
|
2665
|
+
NONE: 'none',
|
|
2666
|
+
};
|
|
2667
|
+
// PR review-status enum — single source of truth for the literal strings written to
|
|
2668
|
+
// pull-requests.json `reviewStatus`. Previously drifted across engine/ado.js,
|
|
2669
|
+
// engine/github.js, engine/lifecycle.js, engine/watches.js, engine/queries.js, engine/cli.js
|
|
2670
|
+
// (P-bfa3d-constants-eslint, audit items #68-#82).
|
|
2671
|
+
const REVIEW_STATUS = {
|
|
2672
|
+
APPROVED: 'approved',
|
|
2673
|
+
CHANGES_REQUESTED: 'changes-requested',
|
|
2674
|
+
WAITING: 'waiting',
|
|
2675
|
+
PENDING: 'pending',
|
|
2676
|
+
};
|
|
2677
|
+
// Named fetch-timeout constants — previously hard-coded as `timeout: 4000` /
|
|
2678
|
+
// `timeout: 15000` etc. across ADO and GitHub integrations. Per-call-class names so
|
|
2679
|
+
// the migrated sites read self-documentingly and drift is grep-able.
|
|
2680
|
+
// (P-bfa3d-constants-eslint, audit items #68-#82).
|
|
2681
|
+
const FETCH_TIMEOUT_MS = {
|
|
2682
|
+
ADO_API: 4000, // engine/ado.js — ADO REST API single-shot fetches
|
|
2683
|
+
GH_CLI: 15000, // engine/github.js, engine/lifecycle.js — `gh pr/api` shell-outs
|
|
2684
|
+
GH_COMMENT: 30000, // engine/gh-comment.js — comment/review posts (slower endpoint)
|
|
2685
|
+
};
|
|
2686
|
+
// Retry delay between auto-link fallback attempts in engine/lifecycle.js#resolvePrLinkFallback.
|
|
2687
|
+
// Previously a bare `3000` magic number; named so the cadence is grep-able and consistent
|
|
2688
|
+
// if other retry paths need the same backoff. (P-bfa3d-constants-eslint).
|
|
2689
|
+
const RETRY_DELAY_MS = 3000;
|
|
2690
|
+
// Max retries for ADO token-refresh inside engine/ado.js#adoFetch.
|
|
2691
|
+
// Distinct from ENGINE_DEFAULTS.maxRetries (dispatch-level cap) — this is the
|
|
2692
|
+
// per-request token-refresh ceiling and is intentionally separate.
|
|
2693
|
+
// (P-bfa3d-constants-eslint).
|
|
2694
|
+
const ADO_TOKEN_REFRESH_MAX_RETRIES = 1;
|
|
2735
2695
|
|
|
2736
2696
|
// Watch statuses — engine-level persistent watches that survive restarts
|
|
2737
2697
|
const WATCH_STATUS = { ACTIVE: 'active', PAUSED: 'paused', TRIGGERED: 'triggered', EXPIRED: 'expired' };
|
|
@@ -4105,6 +4065,42 @@ function isAllowedOrigin(origin) {
|
|
|
4105
4065
|
return _ALLOWED_ORIGIN_HOSTS.has(parsed.hostname);
|
|
4106
4066
|
}
|
|
4107
4067
|
|
|
4068
|
+
// Canonical dashboard origin — the (scheme+host+port) value echoed on
|
|
4069
|
+
// `Access-Control-Allow-Origin` when a browser request originates from the
|
|
4070
|
+
// dashboard's own served page. Mirrors the bind addr + port in dashboard.js.
|
|
4071
|
+
const DASHBOARD_ACAO_DEFAULT_ORIGIN = 'http://localhost:7331';
|
|
4072
|
+
|
|
4073
|
+
/**
|
|
4074
|
+
* Returns true if the given `Origin` header value should receive an
|
|
4075
|
+
* `Access-Control-Allow-Origin` echo on GET/HEAD responses. STRICTER than
|
|
4076
|
+
* `isAllowedOrigin` — that helper is the mutating-request gate (allows any
|
|
4077
|
+
* localhost port the browser may originate from, as defense-in-depth against
|
|
4078
|
+
* CSRF/DNS-rebinding). This helper decides which origins may *cross-origin
|
|
4079
|
+
* READ* dashboard JSON responses, so it admits only:
|
|
4080
|
+
*
|
|
4081
|
+
* - The dashboard's own served origin (`http://localhost:7331`)
|
|
4082
|
+
* - Exact strings in `config.engine.allowedDashboardOrigins` (default [])
|
|
4083
|
+
*
|
|
4084
|
+
* Everything else, including other `http://localhost:<port>` URLs, returns
|
|
4085
|
+
* false — the dashboard never opts into cross-origin browser reads for them.
|
|
4086
|
+
* Reverse-proxy or alternate-port deployments can opt in via the config knob;
|
|
4087
|
+
* see CLAUDE.md and docs/security.md (P-bfa2c-cors-wildcard).
|
|
4088
|
+
*
|
|
4089
|
+
* @param {string|null|undefined} origin Raw `Origin` request header value.
|
|
4090
|
+
* @param {object} [config] Engine config (`config.engine.allowedDashboardOrigins`).
|
|
4091
|
+
* @returns {boolean}
|
|
4092
|
+
*/
|
|
4093
|
+
function isAllowedDashboardOrigin(origin, config) {
|
|
4094
|
+
if (!origin || typeof origin !== 'string') return false;
|
|
4095
|
+
const trimmed = origin.trim();
|
|
4096
|
+
if (!trimmed) return false;
|
|
4097
|
+
if (trimmed === DASHBOARD_ACAO_DEFAULT_ORIGIN) return true;
|
|
4098
|
+
const extras = config && config.engine && Array.isArray(config.engine.allowedDashboardOrigins)
|
|
4099
|
+
? config.engine.allowedDashboardOrigins
|
|
4100
|
+
: [];
|
|
4101
|
+
return extras.includes(trimmed);
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4108
4104
|
/**
|
|
4109
4105
|
* Returns the baseline set of security response headers to apply on every HTTP
|
|
4110
4106
|
* response from the dashboard. Values match OWASP defaults for a same-origin
|
|
@@ -5517,12 +5513,10 @@ module.exports = {
|
|
|
5517
5513
|
resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentModel, resolveCcModel,
|
|
5518
5514
|
resolveAgentMaxBudget, resolveAgentBareMode,
|
|
5519
5515
|
applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
|
|
5520
|
-
applyStatusWorkItemsRetentionMigration, _resetStaleRetentionMigrationFlag,
|
|
5521
|
-
applyStatusMeetingsRetentionMigration, _resetStaleMeetingsRetentionMigrationFlag,
|
|
5522
5516
|
runtimeConfigWarnings,
|
|
5523
5517
|
projectWorkSourceWarnings,
|
|
5524
5518
|
backfillProjectWorkSourceDefaults,
|
|
5525
|
-
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, WORKTREE_REQUIRING_TYPES, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, DISPATCH_RESULT, mutateMetrics, mutateWatches, mutateScheduleRuns, mutatePipelineRuns, mutateManagedProcesses, mutateWorktreePool, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject,
|
|
5519
|
+
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, WORKTREE_REQUIRING_TYPES, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, RETRY_DELAY_MS, ADO_TOKEN_REFRESH_MAX_RETRIES, DISPATCH_RESULT, mutateMetrics, mutateWatches, mutateScheduleRuns, mutatePipelineRuns, mutateManagedProcesses, mutateWorktreePool, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject,
|
|
5526
5520
|
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS, WATCH_ACTION_TYPE,
|
|
5527
5521
|
WATCH_STALLED_DEFAULT_TICKS, WATCH_STUCK_STAGE_DEFAULT_TICKS,
|
|
5528
5522
|
PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
|
|
@@ -5597,6 +5591,8 @@ module.exports = {
|
|
|
5597
5591
|
_CC_PROTECTED_FILE_GLOBS, // exported for testing
|
|
5598
5592
|
_CC_PROTECTED_PREFIXES, // exported for testing
|
|
5599
5593
|
isAllowedOrigin,
|
|
5594
|
+
isAllowedDashboardOrigin,
|
|
5595
|
+
DASHBOARD_ACAO_DEFAULT_ORIGIN,
|
|
5600
5596
|
buildSecurityHeaders,
|
|
5601
5597
|
hasDangerousKey,
|
|
5602
5598
|
HAS_DANGEROUS_KEY_MAX_DEPTH,
|
package/engine/watches.js
CHANGED
|
@@ -706,9 +706,9 @@ registerTargetType(WATCH_TARGET_TYPE.PR, {
|
|
|
706
706
|
case WATCH_CONDITION.MERGED:
|
|
707
707
|
return { triggered: pr.status === 'merged', message: pr.status === 'merged' ? `PR ${target} was merged` : '' };
|
|
708
708
|
case WATCH_CONDITION.BUILD_FAIL:
|
|
709
|
-
return { triggered: pr.buildStatus ===
|
|
709
|
+
return { triggered: pr.buildStatus === shared.BUILD_STATUS.FAILING, message: pr.buildStatus === shared.BUILD_STATUS.FAILING ? `PR ${target} build is failing` : '' };
|
|
710
710
|
case WATCH_CONDITION.BUILD_PASS:
|
|
711
|
-
return { triggered: pr.buildStatus ===
|
|
711
|
+
return { triggered: pr.buildStatus === shared.BUILD_STATUS.PASSING, message: pr.buildStatus === shared.BUILD_STATUS.PASSING ? `PR ${target} build is passing` : '' };
|
|
712
712
|
case WATCH_CONDITION.STATUS_CHANGE: {
|
|
713
713
|
const changed = prevState.status !== undefined && prevState.status !== pr.status;
|
|
714
714
|
return { triggered: changed, message: changed ? `PR ${target} status changed: ${prevState.status} → ${pr.status}` : '' };
|
|
@@ -759,9 +759,9 @@ registerTargetType(WATCH_TARGET_TYPE.PR, {
|
|
|
759
759
|
// WATCH_ABSOLUTE_CONDITIONS) so it auto-expires on first trigger
|
|
760
760
|
// when stopAfter=0. Treat isDraft !== true (catches null/undefined
|
|
761
761
|
// legacy PRs that don't expose the field).
|
|
762
|
-
const ready = pr.status ===
|
|
763
|
-
&& pr.reviewStatus ===
|
|
764
|
-
&& pr.buildStatus ===
|
|
762
|
+
const ready = pr.status === shared.PR_STATUS.ACTIVE
|
|
763
|
+
&& pr.reviewStatus === shared.REVIEW_STATUS.APPROVED
|
|
764
|
+
&& pr.buildStatus === shared.BUILD_STATUS.PASSING
|
|
765
765
|
&& pr.mergeable === true
|
|
766
766
|
&& pr.isDraft !== true;
|
|
767
767
|
return { triggered: ready, message: ready ? `PR ${target} is ready for merge` : '' };
|
package/engine.js
CHANGED
|
@@ -161,7 +161,8 @@ const ghToken = require('./engine/gh-token');
|
|
|
161
161
|
const { runPostCompletionHooks, updateWorkItemStatus, syncPrdItemStatus, reconcilePrdStatuses, handlePostMerge, checkPlanCompletion,
|
|
162
162
|
syncPrsFromOutput, updatePrAfterReview, updatePrAfterFix, checkForLearnings, extractSkillsFromOutput,
|
|
163
163
|
updateAgentHistory, updateMetrics, createReviewFeedbackForAuthor, parseAgentOutput, syncPrdFromPrs,
|
|
164
|
-
isItemCompleted, classifyFailure: classifyFailureFallback, diagnoseEmptyOutput, processPendingRebases, resolveWorkItemPath
|
|
164
|
+
isItemCompleted, classifyFailure: classifyFailureFallback, diagnoseEmptyOutput, processPendingRebases, resolveWorkItemPath,
|
|
165
|
+
mergeArtifactNotes, promoteCompletionArtifacts } = require('./engine/lifecycle');
|
|
165
166
|
|
|
166
167
|
// ─── Agent Spawner ──────────────────────────────────────────────────────────
|
|
167
168
|
|
|
@@ -3302,39 +3303,27 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3302
3303
|
// Track artifacts on the work item for dashboard display
|
|
3303
3304
|
if (dispatchItem.meta?.item?.id) {
|
|
3304
3305
|
try {
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
if (dispatchItem.meta.branch) arts.branch = dispatchItem.meta.branch;
|
|
3327
|
-
if (wi._pr) arts.pr = wi._pr;
|
|
3328
|
-
if (wi._prUrl) arts.prUrl = wi._prUrl;
|
|
3329
|
-
if (_artNotes.length > 0) arts.notes = _artNotes;
|
|
3330
|
-
// Track plan/PRD artifacts from dispatch metadata
|
|
3331
|
-
if (dispatchItem.meta.item?.planFile) arts.plan = dispatchItem.meta.item.planFile;
|
|
3332
|
-
if (dispatchItem.meta.item?._prdFilename) arts.prd = dispatchItem.meta.item._prdFilename;
|
|
3333
|
-
if (dispatchItem.meta.item?.sourcePlan) arts.sourcePlan = dispatchItem.meta.item.sourcePlan;
|
|
3334
|
-
wi._artifacts = arts;
|
|
3335
|
-
return data;
|
|
3336
|
-
});
|
|
3337
|
-
}
|
|
3306
|
+
// Structured completion artifacts are authoritative. The legacy
|
|
3307
|
+
// agent-prefixed inbox scan remains only as a best-effort augmentation
|
|
3308
|
+
// for older runtimes that did not write completion_report.artifacts.
|
|
3309
|
+
const _artToday = shared.dateStamp();
|
|
3310
|
+
const _artInboxDir = path.join(MINIONS_DIR, 'notes', 'inbox');
|
|
3311
|
+
let _artNotes = [];
|
|
3312
|
+
try {
|
|
3313
|
+
const noteFiles = shared.safeReadDir(_artInboxDir).filter(f => f.startsWith(agentId + '-') && f.includes(_artToday));
|
|
3314
|
+
for (const f of noteFiles) {
|
|
3315
|
+
const content = shared.safeRead(path.join(_artInboxDir, f));
|
|
3316
|
+
const noteId = shared.parseNoteId(content);
|
|
3317
|
+
_artNotes.push({ file: f, id: noteId || f.replace(/\.md$/, '') });
|
|
3318
|
+
}
|
|
3319
|
+
} catch {}
|
|
3320
|
+
_artNotes = mergeArtifactNotes([], _artNotes);
|
|
3321
|
+
promoteCompletionArtifacts(dispatchItem.meta, agentId, id, structuredCompletion, {
|
|
3322
|
+
outputLog: `agents/${agentId}/output-${id}.log`,
|
|
3323
|
+
branch: dispatchItem.meta.branch,
|
|
3324
|
+
resultSummary,
|
|
3325
|
+
additionalNotes: _artNotes,
|
|
3326
|
+
});
|
|
3338
3327
|
} catch (err) { log('warn', `Artifact tracking: ${err.message}`); }
|
|
3339
3328
|
}
|
|
3340
3329
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2088",
|
|
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"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"test:e2e:accept": "node test/playwright/accept-baseline.js",
|
|
25
25
|
"test:e2e:accept-force": "node test/playwright/accept-baseline.js --force",
|
|
26
26
|
"test:setup": "npx playwright install chromium",
|
|
27
|
-
"lint": "eslint dashboard/"
|
|
27
|
+
"lint": "eslint dashboard/ engine/ado.js engine/github.js engine/lifecycle.js engine/queries.js engine/watches.js engine/cli.js"
|
|
28
28
|
},
|
|
29
29
|
"keywords": [
|
|
30
30
|
"ai",
|