@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/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 === 'approved' || pr.status === 'merged') prApprovedByAgent[agent] = (prApprovedByAgent[agent] || 0) + 1;
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 projects = getProjects(config);
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
- const files = [
2445
- // prd/*.json (surfaced by getPrdInfo) — engine writes via syncPrdFromPrs,
2446
- // the materializer, and plan-to-prd outputs.
2447
- PRD_DIR,
2448
- // prd/archive/*.json manual archive moves PRDs here.
2449
- path.join(PRD_DIR, 'archive'),
2450
- // prd/guides/*.md — verify agent writes new files here on E2E completion.
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 === 'failing', message: pr.buildStatus === 'failing' ? `PR ${target} build is failing` : '' };
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 === 'passing', message: pr.buildStatus === 'passing' ? `PR ${target} build is passing` : '' };
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 === 'active'
763
- && pr.reviewStatus === 'approved'
764
- && pr.buildStatus === 'passing'
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 } = require('./engine/lifecycle');
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
- const artWiPath = resolveWorkItemPath(dispatchItem.meta);
3306
- if (artWiPath) {
3307
- // Collect inbox notes written by this agent today (with structured IDs if available)
3308
- const _artToday = shared.dateStamp();
3309
- const _artInboxDir = path.join(MINIONS_DIR, 'notes', 'inbox');
3310
- let _artNotes = [];
3311
- try {
3312
- const noteFiles = shared.safeReadDir(_artInboxDir).filter(f => f.startsWith(agentId + '-') && f.includes(_artToday));
3313
- for (const f of noteFiles) {
3314
- const content = shared.safeRead(path.join(_artInboxDir, f));
3315
- const noteId = shared.parseNoteId(content);
3316
- _artNotes.push({ file: f, id: noteId || f.replace(/\.md$/, '') });
3317
- }
3318
- } catch {}
3319
-
3320
- mutateJsonFileLocked(artWiPath, data => {
3321
- if (!Array.isArray(data)) return data;
3322
- const wi = data.find(i => i.id === dispatchItem.meta.item.id);
3323
- if (!wi) return data;
3324
- const arts = wi._artifacts || {};
3325
- arts.outputLog = `agents/${agentId}/output-${id}.log`;
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.2087",
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",