@yemi33/minions 0.1.2370 → 0.1.2371

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.
Files changed (71) hide show
  1. package/bin/minions.js +38 -11
  2. package/dashboard/js/render-other.js +2 -30
  3. package/dashboard/js/render-work-items.js +0 -34
  4. package/dashboard/js/settings.js +12 -27
  5. package/dashboard/pages/tools.html +1 -1
  6. package/dashboard.js +80 -257
  7. package/docs/README.md +2 -3
  8. package/docs/architecture.excalidraw +2 -2
  9. package/docs/auto-discovery.md +3 -3
  10. package/docs/completion-reports.md +0 -121
  11. package/docs/copilot-cli-schema.md +9 -17
  12. package/docs/deprecated.json +0 -51
  13. package/docs/design-state-storage.md +4 -4
  14. package/docs/harness-propagation.md +33 -263
  15. package/docs/human-vs-automated.md +1 -1
  16. package/docs/named-agents.md +0 -2
  17. package/docs/plan-lifecycle.md +5 -6
  18. package/docs/qa-runbook-lifecycle.md +10 -25
  19. package/docs/shared-lifecycle-module-map.md +2 -10
  20. package/engine/ado-comment.js +5 -11
  21. package/engine/ado.js +10 -5
  22. package/engine/cleanup.js +16 -36
  23. package/engine/cli.js +13 -175
  24. package/engine/comment-format.js +8 -182
  25. package/engine/db/index.js +60 -10
  26. package/engine/db/migrations/018-sql-only-cutover.js +56 -0
  27. package/engine/dispatch-store.js +0 -114
  28. package/engine/dispatch.js +1 -6
  29. package/engine/gh-comment.js +2 -10
  30. package/engine/github.js +8 -6
  31. package/engine/lifecycle.js +58 -173
  32. package/engine/llm.js +9 -11
  33. package/engine/managed-spawn.js +1 -6
  34. package/engine/memory-store.js +8 -6
  35. package/engine/metrics-store.js +4 -128
  36. package/engine/pipeline.js +4 -7
  37. package/engine/playbook.js +8 -196
  38. package/engine/prd-store.js +115 -141
  39. package/engine/preflight.js +8 -55
  40. package/engine/projects.js +34 -41
  41. package/engine/pull-requests-store.js +9 -234
  42. package/engine/qa-runs.js +4 -15
  43. package/engine/qa-sessions.js +5 -17
  44. package/engine/queries.js +23 -103
  45. package/engine/routing.js +1 -1
  46. package/engine/runtimes/claude.js +1 -2
  47. package/engine/runtimes/codex.js +0 -2
  48. package/engine/runtimes/copilot.js +1 -4
  49. package/engine/shared-branch-pr-reconcile.js +2 -5
  50. package/engine/shared.js +174 -533
  51. package/engine/small-state-store.js +12 -647
  52. package/engine/spawn-agent.js +5 -96
  53. package/engine/state-operations.js +166 -0
  54. package/engine/supervisor.js +0 -1
  55. package/engine/watch-actions.js +2 -3
  56. package/engine/watches-store.js +2 -128
  57. package/engine/work-items-store.js +3 -254
  58. package/engine.js +102 -334
  59. package/package.json +2 -2
  60. package/playbooks/build-fix-complex.md +0 -4
  61. package/playbooks/fix.md +0 -4
  62. package/playbooks/implement-shared.md +0 -4
  63. package/playbooks/implement.md +0 -4
  64. package/playbooks/plan-to-prd.md +16 -11
  65. package/playbooks/plan.md +0 -4
  66. package/playbooks/review.md +0 -6
  67. package/playbooks/shared-rules.md +0 -65
  68. package/docs/harness-transparency.md +0 -199
  69. package/docs/project-skills.md +0 -193
  70. package/engine/discover-project-skills.js +0 -673
  71. package/engine/playbook-intents.js +0 -76
package/engine/shared.js CHANGED
@@ -471,17 +471,6 @@ function log(level, msg, meta = {}) {
471
471
  // Console output remains immediate (also redacted)
472
472
  console.log(`[${logTs()}] [${level}] ${safeMsg}`);
473
473
 
474
- // Capture the resolved log file path AT WRITE TIME (not flush time).
475
- // Stops test pollution: a test sets MINIONS_TEST_DIR, calls log(), the test
476
- // ends and clears MINIONS_TEST_DIR, then the buffer flushes — without
477
- // capture-at-write-time the entry would land in the production log.json.
478
- // Stripped before persisting (see _flushLogBuffer).
479
- Object.defineProperty(entry, '_logPath', {
480
- value: _currentLogPath(),
481
- enumerable: false,
482
- writable: true,
483
- configurable: true,
484
- });
485
474
  // Capture the SQL state.db path too — same write-time semantics so a
486
475
  // deferred buffer flush after the test ends still routes the entry to
487
476
  // the test's state.db (not the production one). Path resolution mirrors
@@ -534,22 +523,7 @@ function log(level, msg, meta = {}) {
534
523
  * Lazy resolution honors the *current* env at flush time, so even unbussed
535
524
  * dependents write to the test dir while the test owns it.
536
525
  */
537
- function _currentLogPath() {
538
- if (process.env.MINIONS_TEST_DIR) {
539
- return path.join(path.resolve(process.env.MINIONS_TEST_DIR), 'engine', 'log.json');
540
- }
541
- if (process.env.MINIONS_LOG_PATH) {
542
- return path.resolve(process.env.MINIONS_LOG_PATH);
543
- }
544
- const root = process.env.MINIONS_HOME ? path.resolve(process.env.MINIONS_HOME) : path.resolve(__dirname, '..');
545
- return path.join(root, 'engine', 'log.json');
546
- }
547
-
548
- // Resolve the SQL state.db path at write time — same isolation contract
549
- // as _currentLogPath. MINIONS_LOG_PATH only redirects the JSON sidecar;
550
- // the SQL store stays attached to MINIONS_TEST_DIR / MINIONS_HOME because
551
- // it shares the file with the rest of the engine state (dispatches,
552
- // work_items, pull_requests).
526
+ // Resolve the SQL state.db path at write time.
553
527
  function _currentLogDbPath() {
554
528
  if (process.env.MINIONS_TEST_DIR) {
555
529
  return path.join(path.resolve(process.env.MINIONS_TEST_DIR), 'engine', 'state.db');
@@ -673,21 +647,15 @@ function _flushLogBuffer() {
673
647
  // has been cleared by the time we flush. Entries without captured paths
674
648
  // fall back to the current resolved values (eg. direct _logBuffer.push()
675
649
  // from tests).
676
- const fallbackJsonPath = _currentLogPath();
677
650
  const fallbackDbPath = _currentLogDbPath();
678
- const byJsonPath = new Map();
679
651
  const byDbPath = new Map();
680
652
  for (const raw of drained) {
681
- const jsonTarget = raw._logPath || fallbackJsonPath;
682
653
  const dbTarget = raw._dbPath || fallbackDbPath;
683
654
  // SEC-09 defense-in-depth: redact again at flush time so any direct
684
655
  // `_logBuffer.push(entry)` callers (tests, future paths) can't leak secrets.
685
656
  const entry = redactSecrets(raw);
686
657
  // Strip the routing-only metadata before persisting.
687
- delete entry._logPath;
688
658
  delete entry._dbPath;
689
- if (!byJsonPath.has(jsonTarget)) byJsonPath.set(jsonTarget, []);
690
- byJsonPath.get(jsonTarget).push(entry);
691
659
  if (!byDbPath.has(dbTarget)) byDbPath.set(dbTarget, []);
692
660
  byDbPath.get(dbTarget).push(entry);
693
661
  }
@@ -702,19 +670,6 @@ function _flushLogBuffer() {
702
670
  }
703
671
  } catch { /* logs-store unavailable (node<22.5) — JSON mirror still works */ }
704
672
 
705
- // JSON mirror — kept for backward compat with test fixtures that
706
- // safeJson(log.json) directly, and the legacy getEngineLog fallback.
707
- // Phase 4.5 will retire this once readers move to readRecentLogs().
708
- for (const [target, entries] of byJsonPath) {
709
- try {
710
- mutateJsonFileLocked(target, (logData) => {
711
- if (!Array.isArray(logData)) logData = logData?.entries || [];
712
- logData.push(...entries);
713
- if (logData.length >= 2500) logData.splice(0, logData.length - 2000);
714
- return logData;
715
- }, { defaultValue: [] });
716
- } catch { /* logging should never crash the caller */ }
717
- }
718
673
  }
719
674
 
720
675
  /** Flush buffered log entries to disk. Call during graceful shutdown to drain the buffer. */
@@ -729,6 +684,11 @@ function flushLogs() {
729
684
  // ── File I/O ─────────────────────────────────────────────────────────────────
730
685
 
731
686
  function safeRead(p) {
687
+ const prdStore = require('./prd-store');
688
+ if (prdStore.parsePrdPath(p)) {
689
+ const prd = prdStore.readPrd(p);
690
+ return prd == null ? '' : JSON.stringify(prd, null, 2);
691
+ }
732
692
  try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
733
693
  }
734
694
 
@@ -738,10 +698,24 @@ function safeRead(p) {
738
698
  // the client-sent document body with '' when the file was deleted/moved
739
699
  // between panel open and send.
740
700
  function safeReadOrNull(p) {
701
+ const prdStore = require('./prd-store');
702
+ if (prdStore.parsePrdPath(p)) {
703
+ const prd = prdStore.readPrd(p);
704
+ return prd == null ? null : JSON.stringify(prd, null, 2);
705
+ }
741
706
  try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
742
707
  }
743
708
 
744
709
  function safeReadDir(dir) {
710
+ const resolved = path.resolve(dir);
711
+ const prdRoot = path.resolve(MINIONS_DIR, 'prd');
712
+ const prdArchive = path.join(prdRoot, 'archive');
713
+ if (resolved === prdRoot || resolved === prdArchive) {
714
+ const archived = resolved === prdArchive;
715
+ return require('./prd-store').listPrdRows()
716
+ .filter(row => row.archived === archived)
717
+ .map(row => row.filename);
718
+ }
745
719
  try { return fs.readdirSync(dir); } catch { return []; }
746
720
  }
747
721
 
@@ -884,7 +858,7 @@ function isSourcePlanContentStale(plansDir, plan) {
884
858
  // Matching is by path suffix rather than full-prefix against MINIONS_DIR,
885
859
  // because tests using MINIONS_TEST_DIR re-resolve the dir but use the same
886
860
  // suffix pattern. Returns `{value}` on hit, `null` on miss; callers fall
887
- // through to the legacy disk read.
861
+ // through to ordinary file-backed JSON handling.
888
862
  //
889
863
  // Per-project work-items / pull-requests need scope extraction from the
890
864
  // path. `<MINIONS_DIR>/projects/<name>/work-items.json` → scope `<name>`;
@@ -956,23 +930,15 @@ function _routeJsonReadToSql(p) {
956
930
  const store = require('./small-state-store');
957
931
  return { value: store.readDocSessions() };
958
932
  }
959
- // Per-project work-items.json — match `/projects/<name>/work-items.json`.
960
- // When SQL has no rows for the scope AND the JSON file is absent on
961
- // disk, preserve the legacy "file missing → null" semantic. This guards
962
- // the resurrection-bug regression (engine-helper-coverage.test.js):
963
- // safeJson(path-to-removed-project) must return null, not `[]`.
933
+ // Per-project work-items.json — the path is now only a scope identifier.
964
934
  if ((m = norm.match(/\/projects\/([^/]+)\/work-items\.json$/))) {
965
935
  const store = require('./work-items-store');
966
- const v = store.readWorkItemsForScope(m[1]);
967
- if ((!Array.isArray(v) || v.length === 0) && !fs.existsSync(p)) return null;
968
- return { value: v };
936
+ return { value: store.readWorkItemsForScope(m[1]) };
969
937
  }
970
938
  // Per-project pull-requests.json
971
939
  if ((m = norm.match(/\/projects\/([^/]+)\/pull-requests\.json$/))) {
972
940
  const store = require('./pull-requests-store');
973
- const v = store.readPullRequestsForScope(m[1]);
974
- if ((!Array.isArray(v) || v.length === 0) && !fs.existsSync(p)) return null;
975
- return { value: v };
941
+ return { value: store.readPullRequestsForScope(m[1]) };
976
942
  }
977
943
  // Central work-items.json — `<MINIONS_DIR>/work-items.json`. Must not
978
944
  // collide with `/projects/<name>/work-items.json` (handled above).
@@ -1040,6 +1006,8 @@ function safeJson(p) {
1040
1006
  // bytes as the source of truth (so the read+write pair is atomic).
1041
1007
  const skipSqlRouting = arguments.length > 1 && arguments[1] && arguments[1].skipSqlRouting;
1042
1008
  if (!skipSqlRouting) {
1009
+ const prdStore = require('./prd-store');
1010
+ if (prdStore.parsePrdPath(p)) return prdStore.readPrd(p);
1043
1011
  const routed = _routeJsonReadToSql(p);
1044
1012
  if (routed) return routed.value;
1045
1013
  }
@@ -1137,6 +1105,10 @@ function safeJsonArr(p) { return safeJson(p) || []; }
1137
1105
  * @returns {*} Parsed JSON on success, otherwise `defaultValue`.
1138
1106
  */
1139
1107
  function safeJsonNoRestore(p, defaultValue = null) {
1108
+ const prdStore = require('./prd-store');
1109
+ if (prdStore.parsePrdPath(p)) return prdStore.readPrd(p) ?? defaultValue;
1110
+ const routed = _routeJsonReadToSql(p);
1111
+ if (routed) return routed.value ?? defaultValue;
1140
1112
  let raw;
1141
1113
  try {
1142
1114
  raw = fs.readFileSync(p, 'utf8');
@@ -1163,6 +1135,12 @@ function safeJsonNoRestore(p, defaultValue = null) {
1163
1135
  let _tmpCounter = 0;
1164
1136
 
1165
1137
  function safeWrite(p, data) {
1138
+ const prdStore = require('./prd-store');
1139
+ if (prdStore.parsePrdPath(p)) {
1140
+ if (typeof data === 'string') data = JSON.parse(data);
1141
+ prdStore.writePrd(p, data);
1142
+ return;
1143
+ }
1166
1144
  const dir = path.dirname(p);
1167
1145
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1168
1146
  const content = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
@@ -1824,18 +1802,27 @@ function withFileLock(lockPath, fn, {
1824
1802
  // Route table for small-state files migrated to SQL stores. Each entry
1825
1803
  // declares the SQL store function to invoke and the expected default JSON
1826
1804
  // shape (for the "ensure file exists" fallback below).
1827
- const _SMALL_STATE_MUTATE_ROUTES = {
1828
- 'cooldowns.json': { fn: 'applyCooldownsMutation', mirror: '_mirrorCooldownsJson', defaultShape: 'object' },
1829
- 'pending-rebases.json': { fn: 'applyPendingRebasesMutation', mirror: '_mirrorPendingRebasesJson', defaultShape: 'array' },
1830
- 'cc-sessions.json': { fn: 'applyCcSessionsMutation', mirror: '_mirrorCcSessionsJson', defaultShape: 'array' },
1831
- 'doc-sessions.json': { fn: 'applyDocSessionsMutation', mirror: '_mirrorDocSessionsJson', defaultShape: 'object' },
1832
- 'pr-links.json': { fn: 'applyPrLinksMutation', mirror: '_mirrorPrLinksJson', defaultShape: 'object' },
1805
+ const _STATE_MUTATE_ROUTES = {
1806
+ 'dispatch.json': { module: './dispatch-store', fn: 'applyDispatchMutation' },
1807
+ 'metrics.json': { module: './metrics-store', fn: 'applyMetricsMutation' },
1808
+ 'watches.json': { module: './watches-store', fn: 'applyWatchesMutation' },
1809
+ 'schedule-runs.json': { module: './small-state-store', fn: 'applyScheduleRunsMutation' },
1810
+ 'pipeline-runs.json': { module: './small-state-store', fn: 'applyPipelineRunsMutation' },
1811
+ 'managed-processes.json': { module: './small-state-store', fn: 'applyManagedProcessesMutation' },
1812
+ 'worktree-pool.json': { module: './small-state-store', fn: 'applyWorktreePoolMutation' },
1813
+ 'qa-runs.json': { module: './small-state-store', fn: 'applyQaRunsMutation' },
1814
+ 'qa-sessions.json': { module: './small-state-store', fn: 'applyQaSessionsMutation' },
1815
+ 'cooldowns.json': { module: './small-state-store', fn: 'applyCooldownsMutation' },
1816
+ 'pending-rebases.json': { module: './small-state-store', fn: 'applyPendingRebasesMutation' },
1817
+ 'cc-sessions.json': { module: './small-state-store', fn: 'applyCcSessionsMutation' },
1818
+ 'doc-sessions.json': { module: './small-state-store', fn: 'applyDocSessionsMutation' },
1819
+ 'pr-links.json': { module: './small-state-store', fn: 'applyPrLinksMutation' },
1833
1820
  };
1834
1821
 
1835
1822
  function _tryRouteMutateToSql(filePath, mutateFn, onWrote) {
1836
1823
  const baseName = path.basename(filePath);
1837
- const smallRoute = _SMALL_STATE_MUTATE_ROUTES[baseName];
1838
- if (baseName !== 'work-items.json' && baseName !== 'pull-requests.json' && !smallRoute) return null;
1824
+ const stateRoute = _STATE_MUTATE_ROUTES[baseName];
1825
+ if (baseName !== 'work-items.json' && baseName !== 'pull-requests.json' && !stateRoute) return null;
1839
1826
  const fpNorm = String(filePath).replace(/\\/g, '/');
1840
1827
  const minionsNorm = String(MINIONS_DIR).replace(/\\/g, '/');
1841
1828
  const insideMinionsDir = fpNorm.startsWith(minionsNorm + '/') || fpNorm === minionsNorm + '/' + baseName;
@@ -1843,57 +1830,21 @@ function _tryRouteMutateToSql(filePath, mutateFn, onWrote) {
1843
1830
 
1844
1831
  // Small-state files live exclusively under <MINIONS_DIR>/engine/<baseName>.
1845
1832
  // Don't hijack writes to ad-hoc paths that happen to share the basename.
1846
- if (smallRoute) {
1833
+ if (stateRoute) {
1847
1834
  if (!fpNorm.endsWith('/engine/' + baseName)) return null;
1848
- const store = require('./small-state-store');
1849
- // Pre-warm the SQLite DB BEFORE acquiring the file lock. On the very first
1850
- // call from a process (or after a test-isolation MINIONS_TEST_DIR swap),
1851
- // getDb() opens the DB file and runs migrations — which can take tens of
1852
- // milliseconds on a loaded box. When 5+ concurrent processes all hit this
1853
- // path simultaneously, whichever one wins the file lock holds it for the
1854
- // entire DB-init duration. The other 4 processes each have only a 5-second
1855
- // timeout window to acquire the lock; if DB init exceeds that, they throw
1856
- // "Lock timeout" and exit 1 (observed in CI after opg#552 backport which
1857
- // added a new test that increases system load during parallel runs).
1858
- // Pre-warming here means getDb() returns the cached _db instantly when
1859
- // called again inside the lock, so the lock is held only for the fast
1860
- // SQL transaction + mirror write — not for DB initialization.
1835
+ const store = require(stateRoute.module);
1861
1836
  try {
1862
1837
  require('./db').getDb();
1863
1838
  } catch (e) {
1864
1839
  throw new Error(`small-state-store: SQLite unavailable — ${e.message}`);
1865
1840
  }
1866
- // Hold the JSON file lock across the SQL transaction AND the mirror
1867
- // write. The SQL transaction itself is cross-process serialized via
1868
- // BEGIN IMMEDIATE inside the SQL store, but the JSON mirror is a
1869
- // separate read-from-SQL → atomic-rename that can race across
1870
- // processes: an earlier mirror's stale snapshot can land AFTER a
1871
- // later mirror's complete snapshot and lose committed rows from the
1872
- // on-disk JSON file. Pre-Phase-9.4 this was masked by the JSON
1873
- // fallback (concurrent writers serialized through the same lock at
1874
- // the bottom of mutateJsonFileLocked); after the fallback removal,
1875
- // the bare mirror races. Locking around the SQL+mirror block puts
1876
- // those two operations back into one cross-process critical section.
1877
- const lockPath = `${filePath}.lock`;
1878
- return withFileLock(lockPath, () => {
1879
- const out = store[smallRoute.fn]((data) => {
1880
- const next = mutateFn(data);
1881
- if (onWrote) onWrote();
1882
- return next;
1883
- });
1884
- const result = out && Object.prototype.hasOwnProperty.call(out, 'result') ? out.result : undefined;
1885
- try { store[smallRoute.mirror](filePath); } catch { /* mirror best-effort */ }
1886
- if (!fs.existsSync(filePath)) {
1887
- try {
1888
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
1889
- const fallback = smallRoute.defaultShape === 'array'
1890
- ? (Array.isArray(result) ? result : [])
1891
- : (result && typeof result === 'object' && !Array.isArray(result) ? result : {});
1892
- safeWrite(filePath, fallback);
1893
- } catch { /* best-effort */ }
1894
- }
1895
- return { routed: result };
1841
+ const out = store[stateRoute.fn]((data) => {
1842
+ const next = mutateFn(data);
1843
+ if (onWrote) onWrote();
1844
+ return next;
1896
1845
  });
1846
+ const result = out && Object.prototype.hasOwnProperty.call(out, 'result') ? out.result : undefined;
1847
+ return { routed: result };
1897
1848
  }
1898
1849
 
1899
1850
  let result;
@@ -1915,15 +1866,6 @@ function _tryRouteMutateToSql(filePath, mutateFn, onWrote) {
1915
1866
  return out;
1916
1867
  });
1917
1868
  }
1918
- // SQL-store mirror is gated on `wrote`; an empty-no-op mutator leaves the
1919
- // JSON file uncreated. The JSON path always wrote (including for empty
1920
- // arrays via ensureProjectStateFiles), so guarantee a JSON file exists.
1921
- if (!fs.existsSync(filePath)) {
1922
- try {
1923
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
1924
- safeWrite(filePath, Array.isArray(result) ? result : []);
1925
- } catch { /* best-effort */ }
1926
- }
1927
1869
  return { routed: result };
1928
1870
  }
1929
1871
 
@@ -1938,12 +1880,11 @@ function mutateJsonFileLocked(filePath, mutateFn, {
1938
1880
  const lockPath = `${filePath}.lock`;
1939
1881
  const retries = lockRetries ?? ENGINE_DEFAULTS.lockRetries;
1940
1882
  const retryBackoffMs = lockRetryBackoffMs ?? ENGINE_DEFAULTS.lockRetryBackoffMs;
1941
- // Phase 9 SQL-routing: when targeting work-items.json or pull-requests.json
1942
- // under MINIONS_DIR, delegate to the SQL-store wrappers so SQL stays
1943
- // canonical. Without this, JSON-only edits leak past the SQL mirror and
1944
- // divergence-detection short-circuits leave SQL readers stale.
1945
- // `_skipSqlRouting: true` is the internal opt-out used by the SQL store's
1946
- // own SQLite-failure fallback to avoid recursion.
1883
+ const prdStore = require('./prd-store');
1884
+ if (!_skipSqlRouting && prdStore.parsePrdPath(filePath)) {
1885
+ return prdStore.mutatePrd(filePath, mutateFn, { defaultValue });
1886
+ }
1887
+ // Route migrated path-shaped state APIs to SQL.
1947
1888
  if (!_skipSqlRouting) {
1948
1889
  const routed = _tryRouteMutateToSql(filePath, mutateFn, onWrote);
1949
1890
  if (routed) return routed.routed;
@@ -1980,17 +1921,6 @@ function mutateJsonFileLocked(filePath, mutateFn, {
1980
1921
  try { if (fileExists) fs.copyFileSync(filePath, backupPath); } catch { /* backup is best-effort */ }
1981
1922
  }
1982
1923
  safeWrite(filePath, finalData);
1983
- // Phase 10 step 2 — dual-write PRD content to SQL. The single chokepoint:
1984
- // every prd/*.json mutation through mutateJsonFileLocked mirrors into the
1985
- // prds/prd_items/prd_verify_prs tables. JSON stays canonical (nothing
1986
- // reads SQL yet); this keeps the mirror trustworthy for the read-flip.
1987
- // BEST-EFFORT: parsePrdPath fast-rejects non-PRD paths, and a SQLite
1988
- // failure must never break the JSON write. mirrorPrdToSql swallows
1989
- // internally; the require is guarded for the (engine-agnostic) load order.
1990
- try {
1991
- const prdStore = require('./prd-store');
1992
- if (prdStore.parsePrdPath(filePath)) prdStore.mirrorPrdToSql(filePath, finalData);
1993
- } catch { /* mirror is best-effort — never break the canonical JSON write */ }
1994
1924
  // Side-effect hook fired only when an actual write happened. Callers
1995
1925
  // use this to emit cache-invalidation signals (events table row) so
1996
1926
  // skip-write-unchanged paths (dedup, idempotent no-op POSTs) don't
@@ -3533,46 +3463,18 @@ const ENGINE_DEFAULTS = {
3533
3463
  // Engine-scoped per the CLAUDE.md rule against `runtime.name === ...` branches at call sites — the runtime check
3534
3464
  // lives inside the helper (engine/spawn-agent.js#preApproveWorkspaceMcps) which no-ops for non-Claude runtimes.
3535
3465
  claudePreApproveWorkspaceMcps: true,
3536
- // P-08b62d49 — propagate project-local-on-main harness dirs (uncommitted
3537
- // <repo>/.claude/skills, <repo>/.copilot/commands, etc.) into the worktree
3538
- // dispatch via additional `--add-dir` entries. Defaults TRUE so the
3539
- // worktree-uncommitted footgun described in docs/harness-propagation.md is
3540
- // closed by default. Set to false to fall back to the legacy
3541
- // "only committed assets are visible" behavior.
3542
- harnessPropagateProjectLocal: true,
3543
- // P-7c1a9e42 — cap on how many skill packs / command files
3544
- // engine/discover-project-skills.js reads per surface (root `.claude/skills`,
3545
- // each `<area>/.claude/skills`, etc.). Raised well above the module-level
3546
- // DEFAULTS.maxFilesPerSurface fallback (50) so large monorepo sub-projects (e.g.
3547
- // office/src/ocm/.claude/skills with ~80 packages) don't silently drop the
3548
- // most-relevant skills past the cap. engine/playbook.js threads this into the
3549
- // discovery `opts`; when no override is supplied the module falls back to its
3550
- // own DEFAULTS.maxFilesPerSurface. See docs/project-skills.md.
3551
- projectSkillMaxFilesPerSurface: 200,
3552
- // P-49e1c8b7 — hermetic harness opt-out. When TRUE the spawned agent runs
3553
- // with a known-empty harness surface around the worktree:
3554
- // - `--add-dir` is exactly `[minionsDir]` (user-scope skill/command/MCP
3555
- // roots from `runtime.getUserAssetDirs(...)` are dropped);
3556
- // - project-local-on-main harness propagation (`harnessPropagateProjectLocal`)
3557
- // is skipped — no `--project-harness-dir` flags emitted;
3558
- // - Claude workspace `.mcp.json` pre-approval (`claudePreApproveWorkspaceMcps`)
3559
- // is skipped.
3560
- // Independent of `copilotDisableBuiltinMcps` and `copilotSuppressAgentsMd` —
3561
- // those keep their existing semantics so operators can mix-and-match. Use
3562
- // per-agent override `agent.hermeticHarness` for targeted runs. Resolved
3563
- // through `shared.resolveAgentHermeticHarness(agent, engine)` (mirrors
3564
- // `resolveAgentBareMode`).
3565
- hermeticHarness: false,
3566
- copilotSuppressAgentsMd: true, // Copilot --no-custom-instructions: stop AGENTS.md auto-load from fighting Minions playbook prompts
3567
3466
  copilotStreamMode: 'on', // Copilot --stream <on|off>: 'on' streams assistant.message_delta events live; 'off' batches them
3568
3467
  copilotReasoningSummaries: false, // Copilot --enable-reasoning-summaries (Anthropic-family models only)
3569
- // P-mcp-storm Copilot SPAWNS every server in its `mcp-config.json` on every
3570
- // process start and authenticates it. `--disable-mcp-server <name>` only hides
3571
- // a server's TOOLS from the model; it does NOT stop the server process (proven
3572
- // empirically agents spawned azmcp/workiq even with the flag set), so any
3573
- // auth-requiring server pops a Microsoft sign-in window on EVERY agent spawn.
3574
- // The ONLY real lever is the CONFIG: a server absent from the config can't be
3575
- // spawned. The engine therefore points agents at a private COPILOT_HOME
3468
+ ccUseWorkerPool: false, // Sub-task C of W-mp2w003600196c51 (CC perf): when true AND CC runtime is copilot, _invokeCcStream routes through engine/cc-worker-pool.js (persistent `copilot --acp` per CC tab) instead of spawning a fresh CLI per turn. Off by default — opt-in feature flag. **Structurally copilot-only**: the pool spawns `copilot --acp` (Agent Client Protocol); Claude Code does not implement ACP, so resolveCcUseWorkerPool returns false on non-copilot CC runtimes even with explicit-true (W-mphlriic00095f69 — prevents silent runtime switch). Engine/agent dispatch path stays per-process regardless.
3469
+ // PR #321 "kill auth-popup storm". Copilot spawns and authenticates EVERY
3470
+ // MCP server in `$COPILOT_HOME/mcp-config.json` on every process start, even
3471
+ // when the corresponding tool is hidden from the model via
3472
+ // `--disable-mcp-server`. If left unfiltered, every auth-requiring user MCP
3473
+ // (azure-kusto/azmcp, DevBox, workiq, loop, teams, …) boots and pops a
3474
+ // Microsoft sign-in window on EVERY agent spawn — at minions' continuous
3475
+ // spawn cadence that is an infinite popup storm.
3476
+ //
3477
+ // The engine therefore points agents at a private COPILOT_HOME
3576
3478
  // (`shared.resolveAgentCopilotHome`) whose mcp-config is a copy of the operator's
3577
3479
  // `~/.copilot/mcp-config.json` MINUS the names in this list (see engine.js
3578
3480
  // `_applyAgentCopilotHome`). Default [] = inherit ALL of the operator's MCP
@@ -3581,7 +3483,6 @@ const ENGINE_DEFAULTS = {
3581
3483
  // `agent.copilotAgentDisabledMcpServers`. Resolved through
3582
3484
  // `shared.resolveCopilotAgentDisabledMcpServers(agent, engine)`.
3583
3485
  copilotAgentDisabledMcpServers: [],
3584
- ccUseWorkerPool: false, // Sub-task C of W-mp2w003600196c51 (CC perf): when true AND CC runtime is copilot, _invokeCcStream routes through engine/cc-worker-pool.js (persistent `copilot --acp` per CC tab) instead of spawning a fresh CLI per turn. Off by default — opt-in feature flag. **Structurally copilot-only**: the pool spawns `copilot --acp` (Agent Client Protocol); Claude Code does not implement ACP, so resolveCcUseWorkerPool returns false on non-copilot CC runtimes even with explicit-true (W-mphlriic00095f69 — prevents silent runtime switch). Engine/agent dispatch path stays per-process regardless.
3585
3486
  maxBudgetUsd: undefined, // fleet USD ceiling for --max-budget-usd (per-agent override: agents.<id>.maxBudgetUsd). Honors 0 via ?? so a literal cap of $0 works
3586
3487
  disableModelDiscovery: false, // skip runtime.listModels() REST calls fleet-wide (settings UI falls back to free-text)
3587
3488
  // Phase 8 (qa-runs.json + qa-sessions.json → SQL). When true, every QA
@@ -4046,31 +3947,6 @@ function resolveCopilotAgentDisabledMcpServers(agent, engine) {
4046
3947
  return [];
4047
3948
  }
4048
3949
 
4049
- /**
4050
- * P-49e1c8b7 — Resolve whether this agent should run with a hermetic harness.
4051
- * Priority (mirrors `resolveAgentBareMode`):
4052
- * 1. `agent.hermeticHarness` — per-agent override (boolean)
4053
- * 2. `engine.hermeticHarness` — fleet default
4054
- * 3. `false` — hardcoded fallback
4055
- *
4056
- * Strict undefined/null check (not falsy) so a per-agent `false` correctly
4057
- * overrides an engine `true`. Truthy/falsy non-bool values are coerced via
4058
- * `!!` for config tolerance.
4059
- *
4060
- * When TRUE, engine.js (a) skips Claude workspace .mcp.json pre-approval,
4061
- * (b) skips project-local-on-main `--project-harness-dir` propagation, and
4062
- * (c) forwards `--hermetic-harness` to spawn-agent.js so `computeAddDirs`
4063
- * returns `[minionsDir]` only (user-scope skill/command/MCP roots stripped).
4064
- * Independent of `copilotDisableBuiltinMcps` and `copilotSuppressAgentsMd`.
4065
- */
4066
- function resolveAgentHermeticHarness(agent, engine) {
4067
- const a = agent ? agent.hermeticHarness : undefined;
4068
- if (a !== undefined && a !== null) return !!a;
4069
- const e = engine ? engine.hermeticHarness : undefined;
4070
- if (e !== undefined && e !== null) return !!e;
4071
- return false;
4072
- }
4073
-
4074
3950
 
4075
3951
  // ─── Legacy ccModel → defaultModel Migration ─────────────────────────────────
4076
3952
  //
@@ -4653,12 +4529,11 @@ const WATCH_ACTION_TYPE = {
4653
4529
  * propagate; the CLI shim in bin/minions.js guarantees `node:sqlite` is
4654
4530
  * loadable on every supported Node version.
4655
4531
  */
4656
- function _smallStateMutator({ filePath, applyMutation, mirror, topic }) {
4532
+ function _smallStateMutator({ applyMutation, topic }) {
4657
4533
  return (mutator) => {
4658
4534
  const store = require('./small-state-store');
4659
4535
  const { wrote, result } = store[applyMutation]((obj) => mutator(obj) || obj);
4660
4536
  if (wrote) {
4661
- try { if (mirror && typeof store[mirror] === 'function') store[mirror](filePath); } catch { /* mirror best-effort */ }
4662
4537
  try { require('./db-events').emitStateEvent(topic); } catch { /* optional */ }
4663
4538
  }
4664
4539
  return result;
@@ -4666,84 +4541,51 @@ function _smallStateMutator({ filePath, applyMutation, mirror, topic }) {
4666
4541
  }
4667
4542
 
4668
4543
  const mutateScheduleRuns = _smallStateMutator({
4669
- filePath: path.join(MINIONS_DIR, 'engine', 'schedule-runs.json'),
4670
4544
  applyMutation: 'applyScheduleRunsMutation',
4671
- mirror: '_mirrorScheduleRunsJson',
4672
4545
  topic: 'schedule_runs',
4673
4546
  defaultValue: () => ({}),
4674
4547
  });
4675
4548
 
4676
4549
  const mutatePipelineRuns = _smallStateMutator({
4677
- filePath: path.join(MINIONS_DIR, 'engine', 'pipeline-runs.json'),
4678
4550
  applyMutation: 'applyPipelineRunsMutation',
4679
- mirror: '_mirrorPipelineRunsJson',
4680
4551
  topic: 'pipeline_runs',
4681
4552
  defaultValue: () => ({}),
4682
4553
  });
4683
4554
 
4684
4555
  const mutateManagedProcesses = _smallStateMutator({
4685
- filePath: path.join(MINIONS_DIR, 'engine', 'managed-processes.json'),
4686
4556
  applyMutation: 'applyManagedProcessesMutation',
4687
- mirror: '_mirrorManagedProcessesJson',
4688
4557
  topic: 'managed_processes',
4689
4558
  defaultValue: () => ({ specs: [] }),
4690
4559
  });
4691
4560
 
4692
4561
  const mutateWorktreePool = _smallStateMutator({
4693
- filePath: path.join(MINIONS_DIR, 'engine', 'worktree-pool.json'),
4694
4562
  applyMutation: 'applyWorktreePoolMutation',
4695
- mirror: '_mirrorWorktreePoolJson',
4696
4563
  topic: 'worktree_pool',
4697
4564
  defaultValue: () => ({ entries: [] }),
4698
4565
  });
4699
4566
 
4700
- /**
4701
- * Phase 8 QA state mutators. Both qa-runs and qa-sessions are top-level
4702
- * JSON arrays (mutator receives the array; mutates in place or returns a
4703
- * replacement). The store diffs by `id`. Falls back to mutateJsonFileLocked
4704
- * on SQLite failure so a node:sqlite-broken install keeps recording QA
4705
- * state. The JSON mirror is gated by `engine.qaDualWriteJson` (default true)
4706
- * — turn off once operators trust SQL as the source of truth.
4707
- */
4708
- function _qaDualWriteEnabled() {
4709
- try {
4710
- const cfg = safeJson(path.join(MINIONS_DIR, 'config.json')) || {};
4711
- const flag = cfg && cfg.engine && cfg.engine.qaDualWriteJson;
4712
- if (flag === false) return false;
4713
- return true;
4714
- } catch { return true; }
4715
- }
4716
-
4717
- function _qaMutator({ filePath, applyMutation, mirror, topic }) {
4567
+ /** Route QA array mutations through their SQL stores. */
4568
+ function _qaMutator({ applyMutation, topic }) {
4718
4569
  return (mutator) => {
4719
- return withFileLock(filePath + '.lock', () => {
4720
- const store = require('./small-state-store');
4721
- const { wrote, result } = store[applyMutation]((arr) => {
4722
- if (!Array.isArray(arr)) arr = [];
4723
- return mutator(arr) || arr;
4724
- });
4725
- if (wrote) {
4726
- if (_qaDualWriteEnabled()) {
4727
- try { if (mirror && typeof store[mirror] === 'function') store[mirror](filePath); } catch { /* mirror best-effort */ }
4728
- }
4729
- try { require('./db-events').emitStateEvent(topic); } catch { /* optional */ }
4730
- }
4731
- return result;
4732
- }, { timeoutMs: 5000, retries: 3 });
4570
+ const store = require('./small-state-store');
4571
+ const { wrote, result } = store[applyMutation]((arr) => {
4572
+ if (!Array.isArray(arr)) arr = [];
4573
+ return mutator(arr) || arr;
4574
+ });
4575
+ if (wrote) {
4576
+ try { require('./db-events').emitStateEvent(topic); } catch { /* optional */ }
4577
+ }
4578
+ return result;
4733
4579
  };
4734
4580
  }
4735
4581
 
4736
4582
  const mutateQaRuns = _qaMutator({
4737
- filePath: path.join(MINIONS_DIR, 'engine', 'qa-runs.json'),
4738
4583
  applyMutation: 'applyQaRunsMutation',
4739
- mirror: '_mirrorQaRunsJson',
4740
4584
  topic: 'qa_runs',
4741
4585
  });
4742
4586
 
4743
4587
  const mutateQaSessions = _qaMutator({
4744
- filePath: path.join(MINIONS_DIR, 'engine', 'qa-sessions.json'),
4745
4588
  applyMutation: 'applyQaSessionsMutation',
4746
- mirror: '_mirrorQaSessionsJson',
4747
4589
  topic: 'qa_sessions',
4748
4590
  });
4749
4591
 
@@ -4754,41 +4596,24 @@ const mutateQaSessions = _qaMutator({
4754
4596
  * diffs by id. SQL-canonical (Phase 9.4); SQLite failures propagate.
4755
4597
  */
4756
4598
  function mutateWatches(mutator) {
4757
- const watchesPath = path.join(MINIONS_DIR, 'engine', 'watches.json');
4758
4599
  const store = require('./watches-store');
4759
4600
  const { wrote, result } = store.applyWatchesMutation((arr) => {
4760
4601
  if (!Array.isArray(arr)) arr = [];
4761
4602
  return mutator(arr) || arr;
4762
4603
  });
4763
4604
  if (wrote) {
4764
- try { store._mirrorJsonFromSql(watchesPath); } catch { /* mirror best-effort */ }
4765
4605
  try { require('./db-events').emitStateEvent('watches'); } catch { /* optional */ }
4766
4606
  }
4767
4607
  return result;
4768
4608
  }
4769
4609
 
4770
- /**
4771
- * Route a metrics mutation through the SQL store with a JSON dual-write
4772
- * mirror. Same shape as mutateWorkItems / mutatePullRequests: mutator
4773
- * receives the full legacy-shape metrics object, mutates in place or
4774
- * returns a replacement, and the store diffs by (kind, key) row.
4775
- * SQL-canonical (Phase 9.4); SQLite failures propagate.
4776
- */
4610
+ /** Route a metrics object mutation through the SQL store. */
4777
4611
  function mutateMetrics(mutator) {
4778
- const metricsPath = path.join(MINIONS_DIR, 'engine', 'metrics.json');
4779
4612
  const store = require('./metrics-store');
4780
- // W-mradg74d000rfd45 — the JSON mirror is now written INSIDE
4781
- // applyMetricsMutation's SQL transaction (before commit), not here
4782
- // after the fact. Mirroring post-commit left a cross-process window
4783
- // where a sibling process (dashboard vs. engine — separate Node
4784
- // processes, each with its own in-memory mirror-hash cache) could
4785
- // observe the newly committed SQL rows before the JSON file caught up
4786
- // and read stale/missing metrics off the sidecar. See
4787
- // metrics-store.js#applyMetricsMutation for the full rationale.
4788
4613
  const { wrote, result } = store.applyMetricsMutation((m) => {
4789
4614
  if (!m || typeof m !== 'object') m = {};
4790
4615
  return mutator(m) || m;
4791
- }, { mirrorPath: metricsPath });
4616
+ });
4792
4617
  if (wrote) {
4793
4618
  try { require('./db-events').emitStateEvent('metrics'); } catch { /* optional */ }
4794
4619
  }
@@ -4981,132 +4806,6 @@ const FAILURE_CLASS = {
4981
4806
  // Structured completion protocol — fields agents must produce in ```completion blocks
4982
4807
  const COMPLETION_FIELDS = ['status', 'summary', 'files_changed', 'tests', 'pr', 'not_changed', 'failure_class', 'retryable', 'needs_rerun', 'verdict', 'artifacts', 'nonce', 'securityFlags'];
4983
4808
 
4984
- // ── Harness transparency (P-a8f3c2d1) ──────────────────────────────────────
4985
- // Canonical vocabulary for harness-usage observability. Both the agent
4986
- // self-report (completionReport.harnessUsed) and the engine-side grounding
4987
- // manifest (_harnessPropagated) speak this shape. Each kind declares the only
4988
- // fields that survive normalization plus the single field that MUST be present
4989
- // for an entry to be kept:
4990
- // skills: { name, source, path } (required: name)
4991
- // mcpServers: { name, scope } (required: name)
4992
- // commands: { name, scope } (required: name)
4993
- // docs: { path, why } (required: path)
4994
- const HARNESS_USED_KINDS = {
4995
- skills: { fields: ['name', 'source', 'path'], required: 'name' },
4996
- mcpServers: { fields: ['name', 'scope'], required: 'name' },
4997
- commands: { fields: ['name', 'scope'], required: 'name' },
4998
- docs: { fields: ['path', 'why'], required: 'path' },
4999
- };
5000
- const HARNESS_USED_MAX_ENTRIES = 50; // per-kind array cap (anti-spam / anti-DoS)
5001
- const HARNESS_USED_MAX_FIELD_LEN = 512; // per-field string clamp
5002
-
5003
- // normalizeHarnessUsed(raw) — validate/clamp an untrusted harnessUsed-shaped
5004
- // object into the canonical { skills, mcpServers, commands, docs } form.
5005
- // Drops malformed entries (non-objects, or missing the kind's required field),
5006
- // keeps only whitelisted string fields (trimmed + clamped), and caps each array
5007
- // at HARNESS_USED_MAX_ENTRIES. Returns null when nothing survives so callers can
5008
- // treat "no harness data" as a single falsy case. When non-null, all four kind
5009
- // keys are always present (empty arrays for kinds with no entries) for a stable
5010
- // downstream shape.
5011
- function normalizeHarnessUsed(raw) {
5012
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
5013
- const out = {};
5014
- let any = false;
5015
- for (const [kind, spec] of Object.entries(HARNESS_USED_KINDS)) {
5016
- const arr = Array.isArray(raw[kind]) ? raw[kind] : [];
5017
- const normed = [];
5018
- for (const entry of arr) {
5019
- if (normed.length >= HARNESS_USED_MAX_ENTRIES) break;
5020
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
5021
- const clean = {};
5022
- for (const field of spec.fields) {
5023
- const val = entry[field];
5024
- if (typeof val !== 'string') continue;
5025
- const trimmed = val.trim();
5026
- if (!trimmed) continue;
5027
- clean[field] = trimmed.slice(0, HARNESS_USED_MAX_FIELD_LEN);
5028
- }
5029
- if (!clean[spec.required]) continue; // required field missing/empty → drop
5030
- normed.push(clean);
5031
- }
5032
- out[kind] = normed;
5033
- if (normed.length) any = true;
5034
- }
5035
- return any ? out : null;
5036
- }
5037
-
5038
- // groundHarnessUsed(harnessUsed, propagated) — Stage 2 grounding cross-check
5039
- // (P-c6f5e8b3). Normalizes an agent's self-reported `harnessUsed` and annotates
5040
- // every surviving entry with a strict-boolean `grounded` field by cross-checking
5041
- // it against the engine-captured `_harnessPropagated` manifest persisted at
5042
- // spawn time:
5043
- // { skills:[{name,source,path}], mcpServers:[{name,scope}],
5044
- // commands:[{name,scope}], addDirs:[...] }
5045
- //
5046
- // Grounding is intentionally NON-DESTRUCTIVE: a `grounded:false` entry is KEPT
5047
- // and flagged — it may be a genuine out-of-band tool, a stale path, or a
5048
- // hallucination, and a human (not the engine) adjudicates. Returns null when
5049
- // the self-report normalizes to nothing (same falsy contract as
5050
- // normalizeHarnessUsed) so callers treat "no harness data" as one case.
5051
- //
5052
- // Matching signals per entry (OR'd to grounded:true):
5053
- // - identity: the entry's `name` equals a propagated entry's name of the same
5054
- // kind, OR (skills/docs) the entry's `path` is lexically contained under a
5055
- // propagated skill root or `--add-dir`, OR basename(path) equals a
5056
- // propagated skill name.
5057
- // - scope: the entry's scope (skills use `source`) matches a propagated
5058
- // entry's scope of the same kind. The capture manifest records harness
5059
- // ROOTS (skill/command dirs, MCP config files) rather than individual
5060
- // affordances, so a per-affordance self-report often can't match by name;
5061
- // scope membership is the coarse-but-honest "the engine exposed this
5062
- // surface" signal the manifest can attest. `docs` carry no scope, so path
5063
- // containment is their only signal.
5064
- function groundHarnessUsed(harnessUsed, propagated) {
5065
- const normalized = normalizeHarnessUsed(harnessUsed);
5066
- if (!normalized) return null;
5067
- const m = (propagated && typeof propagated === 'object' && !Array.isArray(propagated)) ? propagated : {};
5068
- const arr = (v) => (Array.isArray(v) ? v : []);
5069
- const lc = (v) => (typeof v === 'string' ? v.trim().toLowerCase() : '');
5070
- const normPath = (p) => String(p || '').replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
5071
- const basename = (p) => normPath(p).split('/').filter(Boolean).pop() || '';
5072
-
5073
- // Container paths an affordance may legitimately live under: every propagated
5074
- // skill root + every propagated `--add-dir`.
5075
- const containers = [
5076
- ...arr(m.skills).map(s => s && s.path).filter(Boolean),
5077
- ...arr(m.addDirs).filter(p => typeof p === 'string' && p),
5078
- ].map(normPath).filter(Boolean);
5079
- const isUnder = (p) => {
5080
- const np = normPath(p);
5081
- if (!np) return false;
5082
- return containers.some(c => np === c || np.startsWith(c + '/'));
5083
- };
5084
-
5085
- const setOf = (list, key) => new Set(arr(list).map(e => e && lc(e[key])).filter(Boolean));
5086
- const skillNames = setOf(m.skills, 'name');
5087
- const skillSources = setOf(m.skills, 'source');
5088
- const cmdNames = setOf(m.commands, 'name');
5089
- const cmdScopes = setOf(m.commands, 'scope');
5090
- const mcpNames = setOf(m.mcpServers, 'name');
5091
- const mcpScopes = setOf(m.mcpServers, 'scope');
5092
-
5093
- const groundSkill = (e) => !!(
5094
- (e.name && skillNames.has(lc(e.name)))
5095
- || (e.path && (isUnder(e.path) || skillNames.has(basename(e.path))))
5096
- || (e.source && skillSources.has(lc(e.source)))
5097
- );
5098
- const groundCommand = (e) => !!((e.name && cmdNames.has(lc(e.name))) || (e.scope && cmdScopes.has(lc(e.scope))));
5099
- const groundMcp = (e) => !!((e.name && mcpNames.has(lc(e.name))) || (e.scope && mcpScopes.has(lc(e.scope))));
5100
- const groundDoc = (e) => !!(e.path && isUnder(e.path));
5101
-
5102
- return {
5103
- skills: normalized.skills.map(e => ({ ...e, grounded: groundSkill(e) })),
5104
- mcpServers: normalized.mcpServers.map(e => ({ ...e, grounded: groundMcp(e) })),
5105
- commands: normalized.commands.map(e => ({ ...e, grounded: groundCommand(e) })),
5106
- docs: normalized.docs.map(e => ({ ...e, grounded: groundDoc(e) })),
5107
- };
5108
- }
5109
-
5110
4809
  const DEFAULT_AGENT_METRICS = {
5111
4810
  tasksCompleted: 0, tasksErrored: 0,
5112
4811
  prsCreated: 0, prsApproved: 0, prsRejected: 0, prsMerged: 0,
@@ -5600,19 +5299,12 @@ function sameResolvedPath(a, b) {
5600
5299
  }
5601
5300
 
5602
5301
  function ensureProjectStateFiles(project) {
5603
- const files = [
5604
- { name: 'pull-requests.json', centralPath: projectPrPath(project) },
5605
- { name: 'work-items.json', centralPath: projectWorkItemsPath(project) },
5606
- ];
5607
5302
  const result = { created: [] };
5608
-
5609
5303
  projectStateDirEnsure(project);
5610
- for (const file of files) {
5611
- if (!fs.existsSync(file.centralPath)) {
5612
- mutateJsonFileLocked(file.centralPath, data => Array.isArray(data) ? data : [], { defaultValue: [] });
5613
- result.created.push(file.name);
5614
- }
5615
- }
5304
+ // SQL scopes need no empty sentinel rows. Touch both stores so migrations
5305
+ // and schema validation still happen at project setup time.
5306
+ require('./work-items-store').readWorkItemsForScope(project.name);
5307
+ require('./pull-requests-store').readPullRequestsForScope(project.name);
5616
5308
  return result;
5617
5309
  }
5618
5310
 
@@ -6262,50 +5954,6 @@ function resolveProjectRootDir(localPath, minionsDir) {
6262
5954
  return fallback;
6263
5955
  }
6264
5956
 
6265
- /**
6266
- * Resolve a stable, repo-EXTERNAL COPILOT_HOME for spawned copilot agents.
6267
- *
6268
- * Copilot loads `$COPILOT_HOME/mcp-config.json` and SPAWNS every server in it on
6269
- * every process start. `--disable-mcp-server <name>` only hides a server's tools
6270
- * from the model — it does NOT stop the server PROCESS from launching, so any
6271
- * server that does interactive auth (azure-kusto/azmcp, DevBox, workiq, loop, …)
6272
- * still pops a Microsoft sign-in window on EVERY agent spawn. With minions
6273
- * spawning agents continuously that is an unbounded popup storm.
6274
- *
6275
- * The only effective lever is to give agents a COPILOT_HOME whose mcp-config has
6276
- * NO user servers (the engine seeds `{"mcpServers":{}}` there). Copilot auth is
6277
- * unaffected — it uses GH_TOKEN / the OS credential store, not files under the
6278
- * home — and the operator's real `~/.copilot` (interactive copilot) is untouched.
6279
- *
6280
- * Placed OUTSIDE the repo so concurrent git branch switches in the working tree
6281
- * can never delete or dirty it, on the MINIONS_DIR volume (copilot's session-store
6282
- * can grow to GBs, so keep it off a possibly-full system drive). Stable path so
6283
- * copilot session resume (`--resume`) stays consistent across spawns. Falls back
6284
- * to the user home when no minionsDir is given.
6285
- *
6286
- * Base selection is cross-platform (W-copilot-home-posix-root): on Windows the
6287
- * MINIONS_DIR drive root (`C:\`, `D:\`, or a UNC share root) is user-writable, so
6288
- * we anchor there. On POSIX the filesystem root (`/`) is NOT user-writable — a
6289
- * home placed there can never be created (`mkdir` EACCES), `ensureAgentCopilotHome`
6290
- * fail-opens and returns the uncreatable path anyway, and every spawned
6291
- * `copilot --acp` / copilot agent then inherits a `COPILOT_HOME` it can't use and
6292
- * exits code 1 silently (surfaced in CC as "Load failed"). So when the drive root
6293
- * is the bare POSIX root we fall back to the user home — repo-external, writable,
6294
- * and on the user's own volume.
6295
- */
6296
- function resolveAgentCopilotHome(minionsDir) {
6297
- let base;
6298
- if (minionsDir) {
6299
- const root = path.parse(path.resolve(String(minionsDir))).root;
6300
- // The POSIX filesystem root ('/') is not user-writable; Windows drive/UNC
6301
- // roots are. Anchor at the user home in the unwritable-root case.
6302
- base = (root === '/' || root === path.sep) ? os.homedir() : root;
6303
- } else {
6304
- base = os.homedir();
6305
- }
6306
- return path.join(base, '.minions-agent-copilot-home');
6307
- }
6308
-
6309
5957
  /**
6310
5958
  * Derive a scratch/temp base dir on the same volume agent work already lives on
6311
5959
  * — the configured worktree location (engine.worktreeRoot, default
@@ -6330,6 +5978,55 @@ function resolveAgentTempBaseDir(project, engine, minionsDir) {
6330
5978
  return path.join(anchor, '.agent-temp');
6331
5979
  }
6332
5980
 
5981
+ /**
5982
+ * Resolve the stable, repo-external COPILOT_HOME used to isolate spawned
5983
+ * Copilot agents from the operator's interactive `~/.copilot` MCP servers
5984
+ * (W-copilot-auth-popup-storm / PR #321 — "give copilot agents an MCP-free
5985
+ * COPILOT_HOME (kill auth-popup storm)"). Copilot spawns and authenticates
5986
+ * EVERY server in `$COPILOT_HOME/mcp-config.json` on every process start;
5987
+ * `--disable-mcp-server <name>` only hides that server's tools from the model,
5988
+ * it does NOT stop the server process from launching (verified live: agents
5989
+ * spawned azmcp.exe/workiq.exe despite those servers being in
5990
+ * `copilotAgentDisabledMcpServers`). The only effective lever is to point
5991
+ * agents at a COPILOT_HOME whose mcp-config has NO disabled-list servers — see
5992
+ * `ensureAgentCopilotHome` below, which the engine seeds at boot and re-stamps
5993
+ * per dispatch (`_applyAgentCopilotHome` in engine.js).
5994
+ *
5995
+ * `COPILOT_HOME` overrides `$HOME/.copilot`; copilot auth is unaffected (it
5996
+ * uses GH_TOKEN / the OS credential store, not files under the home), and the
5997
+ * operator's real `~/.copilot` — i.e. their interactive copilot — is
5998
+ * completely untouched.
5999
+ *
6000
+ * Placed OUTSIDE the repo so concurrent git branch switches in the working
6001
+ * tree can never delete or dirty it, on the MINIONS_DIR volume (copilot's
6002
+ * session-store can grow to GBs, so keep it off a possibly-full system
6003
+ * drive). Stable path so copilot session resume (`--resume`) stays
6004
+ * consistent across spawns. Falls back to the user home when no minionsDir
6005
+ * is given.
6006
+ *
6007
+ * Base selection is cross-platform (W-copilot-home-posix-root): on Windows
6008
+ * the MINIONS_DIR drive root (`C:\`, `D:\`, or a UNC share root) is
6009
+ * user-writable, so we anchor there. On POSIX the filesystem root (`/`) is
6010
+ * NOT user-writable — a home placed there can never be created (`mkdir`
6011
+ * EACCES), `ensureAgentCopilotHome` fail-opens and returns the uncreatable
6012
+ * path anyway, and every spawned `copilot --acp` / copilot agent then
6013
+ * inherits a `COPILOT_HOME` it can't use and exits code 1 silently. So when
6014
+ * the drive root is the bare POSIX root we fall back to the user home —
6015
+ * repo-external, writable, and on the user's own volume.
6016
+ */
6017
+ function resolveAgentCopilotHome(minionsDir) {
6018
+ let base;
6019
+ if (minionsDir) {
6020
+ const root = path.parse(path.resolve(String(minionsDir))).root;
6021
+ // The POSIX filesystem root ('/') is not user-writable; Windows drive/UNC
6022
+ // roots are. Anchor at the user home in the unwritable-root case.
6023
+ base = (root === '/' || root === path.sep) ? os.homedir() : root;
6024
+ } else {
6025
+ base = os.homedir();
6026
+ }
6027
+ return path.join(base, '.minions-agent-copilot-home');
6028
+ }
6029
+
6333
6030
  // Seed the agent COPILOT_HOME's mcp-config (copy of `~/.copilot/mcp-config.json`
6334
6031
  // MINUS `engine.copilotAgentDisabledMcpServers`) and return the home path. This
6335
6032
  // is the source of truth for "what MCP servers may an agent-dispatch copilot
@@ -6503,13 +6200,7 @@ function resolveSpawnPaths(project, type, minionsDir, options) {
6503
6200
  // (project.localPath for read-only / live; worktree root for mutating).
6504
6201
  // When set, the engine lands the agent at <base>/<workdir> so monorepo
6505
6202
  // subpackage dispatches can use the runtime CLI's native cwd-rooted skill
6506
- // discovery (`<workdir>/.claude/skills/<name>/SKILL.md`) without engine
6507
- // awareness of which package is which. Cross-package skill aggregation is
6508
- // explicitly out of scope (per PRD open_question) — when workdir is set,
6509
- // the harness propagation block in engine.js also CLIPS its candidate
6510
- // `--project-harness-dir` set to dirs that live under the workdir mirror
6511
- // in `project.localPath`, so a `packages/foo` dispatch never sees
6512
- // `packages/bar/.claude/skills/...`.
6203
+ // discovery without engine awareness of which package is which.
6513
6204
  //
6514
6205
  // Three pure helpers, all unit-tested in test/unit/work-item-workdir.test.js:
6515
6206
  //
@@ -6530,13 +6221,6 @@ function resolveSpawnPaths(project, type, minionsDir, options) {
6530
6221
  // already rejects bare `..` / absolute strings). `base` MUST be
6531
6222
  // absolute. Empty/null workdir returns the base unchanged.
6532
6223
  //
6533
- // 3. filterProjectHarnessDirsForWorkdir(dirs, opts) — clips the
6534
- // candidate `--project-harness-dir` set to entries under the resolved
6535
- // cwd anchor in `project.localPath`, excluding entries under the
6536
- // worktree mirror (already discoverable via the agent's cwd). When
6537
- // `workdir` is null/empty the behavior matches P-08b62d49 unchanged
6538
- // (filter to project.localPath, exclude worktree).
6539
- //
6540
6224
  // The private `_applyWorkdirOrThrow(base, workdir)` is the throw-on-error
6541
6225
  // adapter used inside resolveSpawnPaths so live/read-only callers don't
6542
6226
  // need to forward the `error` field.
@@ -6622,48 +6306,6 @@ function applyWorkdir(base, workdir) {
6622
6306
  return { cwd: joined };
6623
6307
  }
6624
6308
 
6625
- function filterProjectHarnessDirsForWorkdir(dirs, opts) {
6626
- if (!Array.isArray(dirs) || dirs.length === 0) return [];
6627
- const projectLocalPath = opts && opts.projectLocalPath;
6628
- const worktreePath = opts && opts.worktreePath;
6629
- const workdir = opts && opts.workdir;
6630
- if (!projectLocalPath || !worktreePath) return [];
6631
-
6632
- const projectLocalAbs = path.resolve(String(projectLocalPath));
6633
- const worktreeAbs = path.resolve(String(worktreePath));
6634
-
6635
- // When workdir is set, the harness propagation anchors shift to the
6636
- // subpath mirror in BOTH the project root and the worktree root, so a
6637
- // `packages/foo` dispatch only sees `<projectLocal>/packages/foo/...`
6638
- // harness dirs (its actual cwd in the operator's main checkout) and
6639
- // excludes `<worktree>/packages/foo/...` (already discoverable via cwd).
6640
- let projectAnchor = projectLocalAbs;
6641
- let worktreeAnchor = worktreeAbs;
6642
- if (workdir && typeof workdir === 'string' && workdir) {
6643
- const pr = applyWorkdir(projectLocalAbs, workdir);
6644
- const wr = applyWorkdir(worktreeAbs, workdir);
6645
- // If either join fails containment, drop everything — better empty than wrong.
6646
- if (pr.error || wr.error) return [];
6647
- projectAnchor = pr.cwd;
6648
- worktreeAnchor = wr.cwd;
6649
- }
6650
-
6651
- const seen = new Set();
6652
- const out = [];
6653
- for (const dir of dirs) {
6654
- if (typeof dir !== 'string' || !dir) continue;
6655
- const abs = path.resolve(dir);
6656
- if (seen.has(abs)) continue;
6657
- seen.add(abs);
6658
- const insideProject = abs === projectAnchor || abs.startsWith(projectAnchor + path.sep);
6659
- if (!insideProject) continue;
6660
- const insideWorktree = abs === worktreeAnchor || abs.startsWith(worktreeAnchor + path.sep);
6661
- if (insideWorktree) continue;
6662
- out.push(abs);
6663
- }
6664
- return out;
6665
- }
6666
-
6667
6309
  // ── HTTP Origin Allowlist & Security Headers ─────────────────────────────────
6668
6310
  // Pure helpers used by dashboard.js to gate mutating requests against an
6669
6311
  // explicit allowlist of local origins and to attach uniform security response
@@ -7710,18 +7352,12 @@ function getPrLinks() {
7710
7352
  mergePrLinkItems(links, pr.id, pr.prdItems || []);
7711
7353
  }
7712
7354
  } catch { /* SQL unavailable — skip primary source */ }
7713
- // Fallback: pr_links SQL store (was static pr-links.json file). Phase 9.1
7714
- // routes both reads and writes through small-state-store; the JSON file is
7715
- // kept as a dual-write mirror. SQL failures fall through to the JSON file.
7355
+ // Static PR links are stored in SQLite.
7716
7356
  let static_ = null;
7717
7357
  try {
7718
7358
  const store = require('./small-state-store');
7719
7359
  static_ = store.readPrLinks();
7720
7360
  } catch { static_ = null; }
7721
- if (!static_ || Object.keys(static_).length === 0) {
7722
- try { static_ = JSON.parse(fs.readFileSync(PR_LINKS_PATH, 'utf8')); }
7723
- catch { static_ = null; }
7724
- }
7725
7361
  if (static_ && typeof static_ === 'object' && !Array.isArray(static_)) {
7726
7362
  for (const [k, v] of Object.entries(static_)) {
7727
7363
  const canonical = parseCanonicalPrId(k);
@@ -7920,6 +7556,32 @@ function _clearPrTombstonesForIdentity(identity) {
7920
7556
  return cleared;
7921
7557
  }
7922
7558
 
7559
+ /**
7560
+ * W-mrezh0yb0007f733 — decide whether a poll should adopt the platform's real
7561
+ * PR creation timestamp over the currently-stored `created` value. The platform
7562
+ * (GitHub `created_at` / ADO `creationDate`) is the source of truth for when a
7563
+ * PR was opened; the stored value may be missing, a legacy date-only stamp, or
7564
+ * a link/attach-time `now()` default (the bug this fixes). Returns true when the
7565
+ * stored value should be replaced.
7566
+ *
7567
+ * Rules (fail-safe — never trades a real timestamp for a worse one):
7568
+ * - platform absent/unparseable → false (keep whatever we have).
7569
+ * - stored absent/empty → true (nothing to lose; backfill).
7570
+ * - stored unparseable → true (replace garbage with the real instant).
7571
+ * - both parse → replace only when they denote a DIFFERENT instant, so two
7572
+ * equivalent representations (e.g. `…T00:00:00Z` vs `…T00:00:00.000Z`) don't
7573
+ * churn a rewrite every poll.
7574
+ */
7575
+ function shouldAdoptPlatformCreated(storedCreated, platformCreated) {
7576
+ if (!platformCreated) return false;
7577
+ const p = Date.parse(platformCreated);
7578
+ if (!Number.isFinite(p)) return false;
7579
+ if (!storedCreated) return true;
7580
+ const s = Date.parse(storedCreated);
7581
+ if (!Number.isFinite(s)) return true;
7582
+ return s !== p;
7583
+ }
7584
+
7923
7585
  /**
7924
7586
  * Canonical PR-producing work contract helper.
7925
7587
  *
@@ -8238,19 +7900,10 @@ function mutateWorkItems(filePath, mutator) {
8238
7900
  if (insideMinionsDir) {
8239
7901
  const store = require('./work-items-store');
8240
7902
  const scope = store.scopeForFilePath(filePath);
8241
- // W-mr9vcxvy000cdb4e — the JSON mirror is now written INSIDE
8242
- // applyWorkItemsMutation's SQL transaction (before commit), not here
8243
- // after the fact. Mirroring post-commit left a cross-process window
8244
- // where a sibling process (dashboard vs. engine — separate Node
8245
- // processes, each with its own in-memory mirror-hash cache) could
8246
- // observe the new SQL row before the JSON file caught up and
8247
- // destructively rehydrate the scope from that stale file, dropping
8248
- // the just-created row (the "phantom work item" dispatch race). See
8249
- // work-items-store.js#applyWorkItemsMutation for the full rationale.
8250
7903
  const { wrote, result } = store.applyWorkItemsMutation(scope, (items) => {
8251
7904
  if (!Array.isArray(items)) items = [];
8252
7905
  return mutator(items) || items;
8253
- }, { mirrorPath: filePath });
7906
+ });
8254
7907
  if (wrote) {
8255
7908
  try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
8256
7909
  try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
@@ -8302,20 +7955,10 @@ function mutatePullRequests(filePath, mutator) {
8302
7955
  if (insideMinionsDir) {
8303
7956
  const store = require('./pull-requests-store');
8304
7957
  const scope = store.scopeForFilePath(filePath);
8305
- // W-mrcg7j9k000ja233 (#759) — the JSON mirror is now written INSIDE
8306
- // applyPullRequestsMutation's SQL transaction (before commit), not here
8307
- // after the fact. Mirroring post-commit left a cross-process window
8308
- // where a sibling process (dashboard vs. engine — separate Node
8309
- // processes) could observe the new SQL row before the JSON file caught
8310
- // up and destructively rehydrate the scope from that stale file,
8311
- // dropping the just-written row. See
8312
- // pull-requests-store.js#applyPullRequestsMutation for the full
8313
- // rationale (mirrors the identical work-items-store.js fix,
8314
- // W-mr9vcxvy000cdb4e).
8315
7958
  const { wrote, result } = store.applyPullRequestsMutation(scope, (prs) => {
8316
7959
  if (!Array.isArray(prs)) prs = [];
8317
7960
  return mutator(prs) || prs;
8318
- }, { mirrorPath: filePath });
7961
+ });
8319
7962
  if (wrote) {
8320
7963
  try { require('./db-events').emitStateEvent('pull_requests'); } catch { /* optional */ }
8321
7964
  }
@@ -9341,7 +8984,6 @@ module.exports = {
9341
8984
  resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentModel, resolveCcModel,
9342
8985
  resolveAgentMaxBudget, resolveAgentBareMode,
9343
8986
  resolveCopilotAgentDisabledMcpServers,
9344
- resolveAgentHermeticHarness,
9345
8987
  applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
9346
8988
  runtimeConfigWarnings,
9347
8989
  projectWorkSourceWarnings,
@@ -9351,7 +8993,6 @@ module.exports = {
9351
8993
  WATCH_STALLED_DEFAULT_TICKS, WATCH_STUCK_STAGE_DEFAULT_TICKS,
9352
8994
  PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
9353
8995
  FAILURE_CLASS, COMPLETION_FIELDS,
9354
- HARNESS_USED_KINDS, HARNESS_USED_MAX_ENTRIES, HARNESS_USED_MAX_FIELD_LEN, normalizeHarnessUsed, groundHarnessUsed,
9355
8996
  DEFAULT_AGENT_METRICS,
9356
8997
  DEFAULT_AGENTS,
9357
8998
  DEFAULT_CLAUDE,
@@ -9419,6 +9060,7 @@ module.exports = {
9419
9060
  normalizePrLinkItems, // exported for testing
9420
9061
  mergePrLinkItems, // exported for testing
9421
9062
  isContextOnlyPrRecord,
9063
+ shouldAdoptPlatformCreated, // W-mrezh0yb0007f733 — platform creationDate authority
9422
9064
  upsertPullRequestRecord,
9423
9065
  collapseDuplicatePrRecords, // P-e9f0a2b4 — one-time repair helper
9424
9066
  isAutoManagedPrRecord, // W-mq5s5ttx000j7ab8-a — exported for engine + watch-plugin gate consolidation
@@ -9453,7 +9095,6 @@ module.exports = {
9453
9095
  resolveSpawnPaths,
9454
9096
  validateWorkItemWorkdir,
9455
9097
  applyWorkdir,
9456
- filterProjectHarnessDirsForWorkdir,
9457
9098
  READ_ONLY_ROOT_TASK_TYPES,
9458
9099
  isLiveCommandCenterPath,
9459
9100
  describeCcProtectedPaths,