@yemi33/minions 0.1.2369 → 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.
- package/bin/minions.js +38 -11
- package/dashboard/js/memory-search.js +112 -0
- package/dashboard/js/render-kb.js +15 -0
- package/dashboard/js/render-other.js +2 -30
- package/dashboard/js/render-work-items.js +0 -34
- package/dashboard/js/settings.js +27 -27
- package/dashboard/pages/inbox.html +1 -1
- package/dashboard/pages/tools.html +1 -1
- package/dashboard.js +148 -258
- package/docs/README.md +2 -3
- package/docs/architecture.excalidraw +2 -2
- package/docs/auto-discovery.md +3 -3
- package/docs/completion-reports.md +0 -121
- package/docs/copilot-cli-schema.md +9 -17
- package/docs/deprecated.json +0 -51
- package/docs/design-state-storage.md +4 -4
- package/docs/harness-propagation.md +33 -263
- package/docs/human-vs-automated.md +1 -1
- package/docs/named-agents.md +0 -2
- package/docs/plan-lifecycle.md +5 -6
- package/docs/qa-runbook-lifecycle.md +10 -25
- package/docs/shared-lifecycle-module-map.md +2 -10
- package/docs/team-memory.md +18 -4
- package/engine/ado-comment.js +5 -11
- package/engine/ado.js +10 -5
- package/engine/cleanup.js +16 -36
- package/engine/cli.js +13 -175
- package/engine/comment-format.js +8 -182
- package/engine/consolidation.js +72 -1
- package/engine/db/index.js +60 -10
- package/engine/db/migrations/017-agent-memory.js +208 -0
- package/engine/db/migrations/018-sql-only-cutover.js +56 -0
- package/engine/dispatch-store.js +0 -114
- package/engine/dispatch.js +1 -6
- package/engine/features.js +6 -0
- package/engine/gh-comment.js +2 -10
- package/engine/github.js +8 -6
- package/engine/lifecycle.js +141 -173
- package/engine/llm.js +9 -11
- package/engine/managed-spawn.js +1 -6
- package/engine/memory-retrieval.js +165 -0
- package/engine/memory-store.js +367 -0
- package/engine/metrics-store.js +4 -128
- package/engine/pipeline.js +4 -7
- package/engine/playbook.js +64 -198
- package/engine/prd-store.js +115 -141
- package/engine/preflight.js +8 -55
- package/engine/projects.js +34 -41
- package/engine/pull-requests-store.js +9 -234
- package/engine/qa-runs.js +4 -15
- package/engine/qa-sessions.js +5 -17
- package/engine/queries.js +23 -103
- package/engine/routing.js +1 -1
- package/engine/runtimes/claude.js +1 -2
- package/engine/runtimes/codex.js +0 -2
- package/engine/runtimes/copilot.js +1 -4
- package/engine/shared-branch-pr-reconcile.js +2 -5
- package/engine/shared.js +179 -533
- package/engine/small-state-store.js +12 -647
- package/engine/spawn-agent.js +5 -96
- package/engine/state-operations.js +166 -0
- package/engine/supervisor.js +0 -1
- package/engine/watch-actions.js +2 -3
- package/engine/watches-store.js +2 -128
- package/engine/work-items-store.js +3 -254
- package/engine.js +102 -334
- package/package.json +2 -2
- package/playbooks/build-fix-complex.md +0 -4
- package/playbooks/fix.md +0 -4
- package/playbooks/implement-shared.md +0 -4
- package/playbooks/implement.md +0 -4
- package/playbooks/plan-to-prd.md +16 -11
- package/playbooks/plan.md +0 -4
- package/playbooks/review.md +0 -6
- package/playbooks/shared-rules.md +0 -65
- package/docs/harness-transparency.md +0 -199
- package/docs/project-skills.md +0 -193
- package/engine/discover-project-skills.js +0 -673
- 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
|
-
|
|
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
|
|
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 —
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1828
|
-
'
|
|
1829
|
-
'
|
|
1830
|
-
'
|
|
1831
|
-
'
|
|
1832
|
-
'
|
|
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
|
|
1838
|
-
if (baseName !== 'work-items.json' && baseName !== 'pull-requests.json' && !
|
|
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 (
|
|
1833
|
+
if (stateRoute) {
|
|
1847
1834
|
if (!fpNorm.endsWith('/engine/' + baseName)) return null;
|
|
1848
|
-
const store = require(
|
|
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
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
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
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
//
|
|
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
|
-
//
|
|
3570
|
-
//
|
|
3571
|
-
//
|
|
3572
|
-
//
|
|
3573
|
-
//
|
|
3574
|
-
//
|
|
3575
|
-
//
|
|
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
|
|
@@ -3683,6 +3584,11 @@ const ENGINE_DEFAULTS = {
|
|
|
3683
3584
|
agentMemorySummaryEnabled: false, // opt-in: when true, eviction batches go through an LLM-compressed summary before being dropped. Default off to mirror the conservative gating on the existing reconcile pass (LLM cost + test stability). Operators flip via engine.agentMemorySummaryEnabled.
|
|
3684
3585
|
agentMemorySummaryThreshold: 30, // batch window: when summary is enabled and a prune evicts entries, fold at least this many oldest sections into one summary. Means "summary every ~30 entries" in steady state (the original PRD intent).
|
|
3685
3586
|
agentMemorySummaryDays: 30, // age trigger: when the oldest section is older than this and >= agentMemorySummaryThreshold entries exist, summarize the oldest window even if the file is under the entry cap.
|
|
3587
|
+
memoryRetrievalShadowMode: true, // compute + measure FTS5 retrieval but preserve legacy prompt injection until explicitly disabled
|
|
3588
|
+
memoryRetrievalTopK: 8, // maximum structured memory records injected into one dispatch
|
|
3589
|
+
memoryRetrievalMaxBytes: 8 * 1024, // hard byte budget for the Relevant Memory prompt appendix
|
|
3590
|
+
memoryRetrievalCandidateLimit: 50, // bounded FTS5 candidate pool before deterministic re-ranking
|
|
3591
|
+
memoryEpisodicCapture: false, // opt-in capture of compact task outcomes; never stores transcripts or chain-of-thought
|
|
3686
3592
|
untrustedFenceMaxBytes: 64 * 1024, // F5 (W-mpeklod3000we69c): per-block cap for `<UNTRUSTED-INPUT>` fences in engine/untrusted-fence.js. 64KB is long enough for realistic PR comments / pinned notes / agent memory sections, short enough that a megabyte-bomb comment cannot blow up the prompt. Content above the cap is truncated INSIDE the fence with a `[truncated N more bytes]` marker so the agent still sees the provenance attribute.
|
|
3687
3593
|
maxMeetingPromptBytes: 16 * 1024, // cap meeting findings/debate context injected into prompts
|
|
3688
3594
|
maxMeetingHumanNotesBytes: 2 * 1024, // cap human note bullet lists injected into meeting prompts
|
|
@@ -4041,31 +3947,6 @@ function resolveCopilotAgentDisabledMcpServers(agent, engine) {
|
|
|
4041
3947
|
return [];
|
|
4042
3948
|
}
|
|
4043
3949
|
|
|
4044
|
-
/**
|
|
4045
|
-
* P-49e1c8b7 — Resolve whether this agent should run with a hermetic harness.
|
|
4046
|
-
* Priority (mirrors `resolveAgentBareMode`):
|
|
4047
|
-
* 1. `agent.hermeticHarness` — per-agent override (boolean)
|
|
4048
|
-
* 2. `engine.hermeticHarness` — fleet default
|
|
4049
|
-
* 3. `false` — hardcoded fallback
|
|
4050
|
-
*
|
|
4051
|
-
* Strict undefined/null check (not falsy) so a per-agent `false` correctly
|
|
4052
|
-
* overrides an engine `true`. Truthy/falsy non-bool values are coerced via
|
|
4053
|
-
* `!!` for config tolerance.
|
|
4054
|
-
*
|
|
4055
|
-
* When TRUE, engine.js (a) skips Claude workspace .mcp.json pre-approval,
|
|
4056
|
-
* (b) skips project-local-on-main `--project-harness-dir` propagation, and
|
|
4057
|
-
* (c) forwards `--hermetic-harness` to spawn-agent.js so `computeAddDirs`
|
|
4058
|
-
* returns `[minionsDir]` only (user-scope skill/command/MCP roots stripped).
|
|
4059
|
-
* Independent of `copilotDisableBuiltinMcps` and `copilotSuppressAgentsMd`.
|
|
4060
|
-
*/
|
|
4061
|
-
function resolveAgentHermeticHarness(agent, engine) {
|
|
4062
|
-
const a = agent ? agent.hermeticHarness : undefined;
|
|
4063
|
-
if (a !== undefined && a !== null) return !!a;
|
|
4064
|
-
const e = engine ? engine.hermeticHarness : undefined;
|
|
4065
|
-
if (e !== undefined && e !== null) return !!e;
|
|
4066
|
-
return false;
|
|
4067
|
-
}
|
|
4068
|
-
|
|
4069
3950
|
|
|
4070
3951
|
// ─── Legacy ccModel → defaultModel Migration ─────────────────────────────────
|
|
4071
3952
|
//
|
|
@@ -4648,12 +4529,11 @@ const WATCH_ACTION_TYPE = {
|
|
|
4648
4529
|
* propagate; the CLI shim in bin/minions.js guarantees `node:sqlite` is
|
|
4649
4530
|
* loadable on every supported Node version.
|
|
4650
4531
|
*/
|
|
4651
|
-
function _smallStateMutator({
|
|
4532
|
+
function _smallStateMutator({ applyMutation, topic }) {
|
|
4652
4533
|
return (mutator) => {
|
|
4653
4534
|
const store = require('./small-state-store');
|
|
4654
4535
|
const { wrote, result } = store[applyMutation]((obj) => mutator(obj) || obj);
|
|
4655
4536
|
if (wrote) {
|
|
4656
|
-
try { if (mirror && typeof store[mirror] === 'function') store[mirror](filePath); } catch { /* mirror best-effort */ }
|
|
4657
4537
|
try { require('./db-events').emitStateEvent(topic); } catch { /* optional */ }
|
|
4658
4538
|
}
|
|
4659
4539
|
return result;
|
|
@@ -4661,84 +4541,51 @@ function _smallStateMutator({ filePath, applyMutation, mirror, topic }) {
|
|
|
4661
4541
|
}
|
|
4662
4542
|
|
|
4663
4543
|
const mutateScheduleRuns = _smallStateMutator({
|
|
4664
|
-
filePath: path.join(MINIONS_DIR, 'engine', 'schedule-runs.json'),
|
|
4665
4544
|
applyMutation: 'applyScheduleRunsMutation',
|
|
4666
|
-
mirror: '_mirrorScheduleRunsJson',
|
|
4667
4545
|
topic: 'schedule_runs',
|
|
4668
4546
|
defaultValue: () => ({}),
|
|
4669
4547
|
});
|
|
4670
4548
|
|
|
4671
4549
|
const mutatePipelineRuns = _smallStateMutator({
|
|
4672
|
-
filePath: path.join(MINIONS_DIR, 'engine', 'pipeline-runs.json'),
|
|
4673
4550
|
applyMutation: 'applyPipelineRunsMutation',
|
|
4674
|
-
mirror: '_mirrorPipelineRunsJson',
|
|
4675
4551
|
topic: 'pipeline_runs',
|
|
4676
4552
|
defaultValue: () => ({}),
|
|
4677
4553
|
});
|
|
4678
4554
|
|
|
4679
4555
|
const mutateManagedProcesses = _smallStateMutator({
|
|
4680
|
-
filePath: path.join(MINIONS_DIR, 'engine', 'managed-processes.json'),
|
|
4681
4556
|
applyMutation: 'applyManagedProcessesMutation',
|
|
4682
|
-
mirror: '_mirrorManagedProcessesJson',
|
|
4683
4557
|
topic: 'managed_processes',
|
|
4684
4558
|
defaultValue: () => ({ specs: [] }),
|
|
4685
4559
|
});
|
|
4686
4560
|
|
|
4687
4561
|
const mutateWorktreePool = _smallStateMutator({
|
|
4688
|
-
filePath: path.join(MINIONS_DIR, 'engine', 'worktree-pool.json'),
|
|
4689
4562
|
applyMutation: 'applyWorktreePoolMutation',
|
|
4690
|
-
mirror: '_mirrorWorktreePoolJson',
|
|
4691
4563
|
topic: 'worktree_pool',
|
|
4692
4564
|
defaultValue: () => ({ entries: [] }),
|
|
4693
4565
|
});
|
|
4694
4566
|
|
|
4695
|
-
/**
|
|
4696
|
-
|
|
4697
|
-
* JSON arrays (mutator receives the array; mutates in place or returns a
|
|
4698
|
-
* replacement). The store diffs by `id`. Falls back to mutateJsonFileLocked
|
|
4699
|
-
* on SQLite failure so a node:sqlite-broken install keeps recording QA
|
|
4700
|
-
* state. The JSON mirror is gated by `engine.qaDualWriteJson` (default true)
|
|
4701
|
-
* — turn off once operators trust SQL as the source of truth.
|
|
4702
|
-
*/
|
|
4703
|
-
function _qaDualWriteEnabled() {
|
|
4704
|
-
try {
|
|
4705
|
-
const cfg = safeJson(path.join(MINIONS_DIR, 'config.json')) || {};
|
|
4706
|
-
const flag = cfg && cfg.engine && cfg.engine.qaDualWriteJson;
|
|
4707
|
-
if (flag === false) return false;
|
|
4708
|
-
return true;
|
|
4709
|
-
} catch { return true; }
|
|
4710
|
-
}
|
|
4711
|
-
|
|
4712
|
-
function _qaMutator({ filePath, applyMutation, mirror, topic }) {
|
|
4567
|
+
/** Route QA array mutations through their SQL stores. */
|
|
4568
|
+
function _qaMutator({ applyMutation, topic }) {
|
|
4713
4569
|
return (mutator) => {
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
}
|
|
4724
|
-
try { require('./db-events').emitStateEvent(topic); } catch { /* optional */ }
|
|
4725
|
-
}
|
|
4726
|
-
return result;
|
|
4727
|
-
}, { 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;
|
|
4728
4579
|
};
|
|
4729
4580
|
}
|
|
4730
4581
|
|
|
4731
4582
|
const mutateQaRuns = _qaMutator({
|
|
4732
|
-
filePath: path.join(MINIONS_DIR, 'engine', 'qa-runs.json'),
|
|
4733
4583
|
applyMutation: 'applyQaRunsMutation',
|
|
4734
|
-
mirror: '_mirrorQaRunsJson',
|
|
4735
4584
|
topic: 'qa_runs',
|
|
4736
4585
|
});
|
|
4737
4586
|
|
|
4738
4587
|
const mutateQaSessions = _qaMutator({
|
|
4739
|
-
filePath: path.join(MINIONS_DIR, 'engine', 'qa-sessions.json'),
|
|
4740
4588
|
applyMutation: 'applyQaSessionsMutation',
|
|
4741
|
-
mirror: '_mirrorQaSessionsJson',
|
|
4742
4589
|
topic: 'qa_sessions',
|
|
4743
4590
|
});
|
|
4744
4591
|
|
|
@@ -4749,41 +4596,24 @@ const mutateQaSessions = _qaMutator({
|
|
|
4749
4596
|
* diffs by id. SQL-canonical (Phase 9.4); SQLite failures propagate.
|
|
4750
4597
|
*/
|
|
4751
4598
|
function mutateWatches(mutator) {
|
|
4752
|
-
const watchesPath = path.join(MINIONS_DIR, 'engine', 'watches.json');
|
|
4753
4599
|
const store = require('./watches-store');
|
|
4754
4600
|
const { wrote, result } = store.applyWatchesMutation((arr) => {
|
|
4755
4601
|
if (!Array.isArray(arr)) arr = [];
|
|
4756
4602
|
return mutator(arr) || arr;
|
|
4757
4603
|
});
|
|
4758
4604
|
if (wrote) {
|
|
4759
|
-
try { store._mirrorJsonFromSql(watchesPath); } catch { /* mirror best-effort */ }
|
|
4760
4605
|
try { require('./db-events').emitStateEvent('watches'); } catch { /* optional */ }
|
|
4761
4606
|
}
|
|
4762
4607
|
return result;
|
|
4763
4608
|
}
|
|
4764
4609
|
|
|
4765
|
-
/**
|
|
4766
|
-
* Route a metrics mutation through the SQL store with a JSON dual-write
|
|
4767
|
-
* mirror. Same shape as mutateWorkItems / mutatePullRequests: mutator
|
|
4768
|
-
* receives the full legacy-shape metrics object, mutates in place or
|
|
4769
|
-
* returns a replacement, and the store diffs by (kind, key) row.
|
|
4770
|
-
* SQL-canonical (Phase 9.4); SQLite failures propagate.
|
|
4771
|
-
*/
|
|
4610
|
+
/** Route a metrics object mutation through the SQL store. */
|
|
4772
4611
|
function mutateMetrics(mutator) {
|
|
4773
|
-
const metricsPath = path.join(MINIONS_DIR, 'engine', 'metrics.json');
|
|
4774
4612
|
const store = require('./metrics-store');
|
|
4775
|
-
// W-mradg74d000rfd45 — the JSON mirror is now written INSIDE
|
|
4776
|
-
// applyMetricsMutation's SQL transaction (before commit), not here
|
|
4777
|
-
// after the fact. Mirroring post-commit left a cross-process window
|
|
4778
|
-
// where a sibling process (dashboard vs. engine — separate Node
|
|
4779
|
-
// processes, each with its own in-memory mirror-hash cache) could
|
|
4780
|
-
// observe the newly committed SQL rows before the JSON file caught up
|
|
4781
|
-
// and read stale/missing metrics off the sidecar. See
|
|
4782
|
-
// metrics-store.js#applyMetricsMutation for the full rationale.
|
|
4783
4613
|
const { wrote, result } = store.applyMetricsMutation((m) => {
|
|
4784
4614
|
if (!m || typeof m !== 'object') m = {};
|
|
4785
4615
|
return mutator(m) || m;
|
|
4786
|
-
}
|
|
4616
|
+
});
|
|
4787
4617
|
if (wrote) {
|
|
4788
4618
|
try { require('./db-events').emitStateEvent('metrics'); } catch { /* optional */ }
|
|
4789
4619
|
}
|
|
@@ -4976,132 +4806,6 @@ const FAILURE_CLASS = {
|
|
|
4976
4806
|
// Structured completion protocol — fields agents must produce in ```completion blocks
|
|
4977
4807
|
const COMPLETION_FIELDS = ['status', 'summary', 'files_changed', 'tests', 'pr', 'not_changed', 'failure_class', 'retryable', 'needs_rerun', 'verdict', 'artifacts', 'nonce', 'securityFlags'];
|
|
4978
4808
|
|
|
4979
|
-
// ── Harness transparency (P-a8f3c2d1) ──────────────────────────────────────
|
|
4980
|
-
// Canonical vocabulary for harness-usage observability. Both the agent
|
|
4981
|
-
// self-report (completionReport.harnessUsed) and the engine-side grounding
|
|
4982
|
-
// manifest (_harnessPropagated) speak this shape. Each kind declares the only
|
|
4983
|
-
// fields that survive normalization plus the single field that MUST be present
|
|
4984
|
-
// for an entry to be kept:
|
|
4985
|
-
// skills: { name, source, path } (required: name)
|
|
4986
|
-
// mcpServers: { name, scope } (required: name)
|
|
4987
|
-
// commands: { name, scope } (required: name)
|
|
4988
|
-
// docs: { path, why } (required: path)
|
|
4989
|
-
const HARNESS_USED_KINDS = {
|
|
4990
|
-
skills: { fields: ['name', 'source', 'path'], required: 'name' },
|
|
4991
|
-
mcpServers: { fields: ['name', 'scope'], required: 'name' },
|
|
4992
|
-
commands: { fields: ['name', 'scope'], required: 'name' },
|
|
4993
|
-
docs: { fields: ['path', 'why'], required: 'path' },
|
|
4994
|
-
};
|
|
4995
|
-
const HARNESS_USED_MAX_ENTRIES = 50; // per-kind array cap (anti-spam / anti-DoS)
|
|
4996
|
-
const HARNESS_USED_MAX_FIELD_LEN = 512; // per-field string clamp
|
|
4997
|
-
|
|
4998
|
-
// normalizeHarnessUsed(raw) — validate/clamp an untrusted harnessUsed-shaped
|
|
4999
|
-
// object into the canonical { skills, mcpServers, commands, docs } form.
|
|
5000
|
-
// Drops malformed entries (non-objects, or missing the kind's required field),
|
|
5001
|
-
// keeps only whitelisted string fields (trimmed + clamped), and caps each array
|
|
5002
|
-
// at HARNESS_USED_MAX_ENTRIES. Returns null when nothing survives so callers can
|
|
5003
|
-
// treat "no harness data" as a single falsy case. When non-null, all four kind
|
|
5004
|
-
// keys are always present (empty arrays for kinds with no entries) for a stable
|
|
5005
|
-
// downstream shape.
|
|
5006
|
-
function normalizeHarnessUsed(raw) {
|
|
5007
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
5008
|
-
const out = {};
|
|
5009
|
-
let any = false;
|
|
5010
|
-
for (const [kind, spec] of Object.entries(HARNESS_USED_KINDS)) {
|
|
5011
|
-
const arr = Array.isArray(raw[kind]) ? raw[kind] : [];
|
|
5012
|
-
const normed = [];
|
|
5013
|
-
for (const entry of arr) {
|
|
5014
|
-
if (normed.length >= HARNESS_USED_MAX_ENTRIES) break;
|
|
5015
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
|
|
5016
|
-
const clean = {};
|
|
5017
|
-
for (const field of spec.fields) {
|
|
5018
|
-
const val = entry[field];
|
|
5019
|
-
if (typeof val !== 'string') continue;
|
|
5020
|
-
const trimmed = val.trim();
|
|
5021
|
-
if (!trimmed) continue;
|
|
5022
|
-
clean[field] = trimmed.slice(0, HARNESS_USED_MAX_FIELD_LEN);
|
|
5023
|
-
}
|
|
5024
|
-
if (!clean[spec.required]) continue; // required field missing/empty → drop
|
|
5025
|
-
normed.push(clean);
|
|
5026
|
-
}
|
|
5027
|
-
out[kind] = normed;
|
|
5028
|
-
if (normed.length) any = true;
|
|
5029
|
-
}
|
|
5030
|
-
return any ? out : null;
|
|
5031
|
-
}
|
|
5032
|
-
|
|
5033
|
-
// groundHarnessUsed(harnessUsed, propagated) — Stage 2 grounding cross-check
|
|
5034
|
-
// (P-c6f5e8b3). Normalizes an agent's self-reported `harnessUsed` and annotates
|
|
5035
|
-
// every surviving entry with a strict-boolean `grounded` field by cross-checking
|
|
5036
|
-
// it against the engine-captured `_harnessPropagated` manifest persisted at
|
|
5037
|
-
// spawn time:
|
|
5038
|
-
// { skills:[{name,source,path}], mcpServers:[{name,scope}],
|
|
5039
|
-
// commands:[{name,scope}], addDirs:[...] }
|
|
5040
|
-
//
|
|
5041
|
-
// Grounding is intentionally NON-DESTRUCTIVE: a `grounded:false` entry is KEPT
|
|
5042
|
-
// and flagged — it may be a genuine out-of-band tool, a stale path, or a
|
|
5043
|
-
// hallucination, and a human (not the engine) adjudicates. Returns null when
|
|
5044
|
-
// the self-report normalizes to nothing (same falsy contract as
|
|
5045
|
-
// normalizeHarnessUsed) so callers treat "no harness data" as one case.
|
|
5046
|
-
//
|
|
5047
|
-
// Matching signals per entry (OR'd to grounded:true):
|
|
5048
|
-
// - identity: the entry's `name` equals a propagated entry's name of the same
|
|
5049
|
-
// kind, OR (skills/docs) the entry's `path` is lexically contained under a
|
|
5050
|
-
// propagated skill root or `--add-dir`, OR basename(path) equals a
|
|
5051
|
-
// propagated skill name.
|
|
5052
|
-
// - scope: the entry's scope (skills use `source`) matches a propagated
|
|
5053
|
-
// entry's scope of the same kind. The capture manifest records harness
|
|
5054
|
-
// ROOTS (skill/command dirs, MCP config files) rather than individual
|
|
5055
|
-
// affordances, so a per-affordance self-report often can't match by name;
|
|
5056
|
-
// scope membership is the coarse-but-honest "the engine exposed this
|
|
5057
|
-
// surface" signal the manifest can attest. `docs` carry no scope, so path
|
|
5058
|
-
// containment is their only signal.
|
|
5059
|
-
function groundHarnessUsed(harnessUsed, propagated) {
|
|
5060
|
-
const normalized = normalizeHarnessUsed(harnessUsed);
|
|
5061
|
-
if (!normalized) return null;
|
|
5062
|
-
const m = (propagated && typeof propagated === 'object' && !Array.isArray(propagated)) ? propagated : {};
|
|
5063
|
-
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
5064
|
-
const lc = (v) => (typeof v === 'string' ? v.trim().toLowerCase() : '');
|
|
5065
|
-
const normPath = (p) => String(p || '').replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
5066
|
-
const basename = (p) => normPath(p).split('/').filter(Boolean).pop() || '';
|
|
5067
|
-
|
|
5068
|
-
// Container paths an affordance may legitimately live under: every propagated
|
|
5069
|
-
// skill root + every propagated `--add-dir`.
|
|
5070
|
-
const containers = [
|
|
5071
|
-
...arr(m.skills).map(s => s && s.path).filter(Boolean),
|
|
5072
|
-
...arr(m.addDirs).filter(p => typeof p === 'string' && p),
|
|
5073
|
-
].map(normPath).filter(Boolean);
|
|
5074
|
-
const isUnder = (p) => {
|
|
5075
|
-
const np = normPath(p);
|
|
5076
|
-
if (!np) return false;
|
|
5077
|
-
return containers.some(c => np === c || np.startsWith(c + '/'));
|
|
5078
|
-
};
|
|
5079
|
-
|
|
5080
|
-
const setOf = (list, key) => new Set(arr(list).map(e => e && lc(e[key])).filter(Boolean));
|
|
5081
|
-
const skillNames = setOf(m.skills, 'name');
|
|
5082
|
-
const skillSources = setOf(m.skills, 'source');
|
|
5083
|
-
const cmdNames = setOf(m.commands, 'name');
|
|
5084
|
-
const cmdScopes = setOf(m.commands, 'scope');
|
|
5085
|
-
const mcpNames = setOf(m.mcpServers, 'name');
|
|
5086
|
-
const mcpScopes = setOf(m.mcpServers, 'scope');
|
|
5087
|
-
|
|
5088
|
-
const groundSkill = (e) => !!(
|
|
5089
|
-
(e.name && skillNames.has(lc(e.name)))
|
|
5090
|
-
|| (e.path && (isUnder(e.path) || skillNames.has(basename(e.path))))
|
|
5091
|
-
|| (e.source && skillSources.has(lc(e.source)))
|
|
5092
|
-
);
|
|
5093
|
-
const groundCommand = (e) => !!((e.name && cmdNames.has(lc(e.name))) || (e.scope && cmdScopes.has(lc(e.scope))));
|
|
5094
|
-
const groundMcp = (e) => !!((e.name && mcpNames.has(lc(e.name))) || (e.scope && mcpScopes.has(lc(e.scope))));
|
|
5095
|
-
const groundDoc = (e) => !!(e.path && isUnder(e.path));
|
|
5096
|
-
|
|
5097
|
-
return {
|
|
5098
|
-
skills: normalized.skills.map(e => ({ ...e, grounded: groundSkill(e) })),
|
|
5099
|
-
mcpServers: normalized.mcpServers.map(e => ({ ...e, grounded: groundMcp(e) })),
|
|
5100
|
-
commands: normalized.commands.map(e => ({ ...e, grounded: groundCommand(e) })),
|
|
5101
|
-
docs: normalized.docs.map(e => ({ ...e, grounded: groundDoc(e) })),
|
|
5102
|
-
};
|
|
5103
|
-
}
|
|
5104
|
-
|
|
5105
4809
|
const DEFAULT_AGENT_METRICS = {
|
|
5106
4810
|
tasksCompleted: 0, tasksErrored: 0,
|
|
5107
4811
|
prsCreated: 0, prsApproved: 0, prsRejected: 0, prsMerged: 0,
|
|
@@ -5595,19 +5299,12 @@ function sameResolvedPath(a, b) {
|
|
|
5595
5299
|
}
|
|
5596
5300
|
|
|
5597
5301
|
function ensureProjectStateFiles(project) {
|
|
5598
|
-
const files = [
|
|
5599
|
-
{ name: 'pull-requests.json', centralPath: projectPrPath(project) },
|
|
5600
|
-
{ name: 'work-items.json', centralPath: projectWorkItemsPath(project) },
|
|
5601
|
-
];
|
|
5602
5302
|
const result = { created: [] };
|
|
5603
|
-
|
|
5604
5303
|
projectStateDirEnsure(project);
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
}
|
|
5610
|
-
}
|
|
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);
|
|
5611
5308
|
return result;
|
|
5612
5309
|
}
|
|
5613
5310
|
|
|
@@ -6257,50 +5954,6 @@ function resolveProjectRootDir(localPath, minionsDir) {
|
|
|
6257
5954
|
return fallback;
|
|
6258
5955
|
}
|
|
6259
5956
|
|
|
6260
|
-
/**
|
|
6261
|
-
* Resolve a stable, repo-EXTERNAL COPILOT_HOME for spawned copilot agents.
|
|
6262
|
-
*
|
|
6263
|
-
* Copilot loads `$COPILOT_HOME/mcp-config.json` and SPAWNS every server in it on
|
|
6264
|
-
* every process start. `--disable-mcp-server <name>` only hides a server's tools
|
|
6265
|
-
* from the model — it does NOT stop the server PROCESS from launching, so any
|
|
6266
|
-
* server that does interactive auth (azure-kusto/azmcp, DevBox, workiq, loop, …)
|
|
6267
|
-
* still pops a Microsoft sign-in window on EVERY agent spawn. With minions
|
|
6268
|
-
* spawning agents continuously that is an unbounded popup storm.
|
|
6269
|
-
*
|
|
6270
|
-
* The only effective lever is to give agents a COPILOT_HOME whose mcp-config has
|
|
6271
|
-
* NO user servers (the engine seeds `{"mcpServers":{}}` there). Copilot auth is
|
|
6272
|
-
* unaffected — it uses GH_TOKEN / the OS credential store, not files under the
|
|
6273
|
-
* home — and the operator's real `~/.copilot` (interactive copilot) is untouched.
|
|
6274
|
-
*
|
|
6275
|
-
* Placed OUTSIDE the repo so concurrent git branch switches in the working tree
|
|
6276
|
-
* can never delete or dirty it, on the MINIONS_DIR volume (copilot's session-store
|
|
6277
|
-
* can grow to GBs, so keep it off a possibly-full system drive). Stable path so
|
|
6278
|
-
* copilot session resume (`--resume`) stays consistent across spawns. Falls back
|
|
6279
|
-
* to the user home when no minionsDir is given.
|
|
6280
|
-
*
|
|
6281
|
-
* Base selection is cross-platform (W-copilot-home-posix-root): on Windows the
|
|
6282
|
-
* MINIONS_DIR drive root (`C:\`, `D:\`, or a UNC share root) is user-writable, so
|
|
6283
|
-
* we anchor there. On POSIX the filesystem root (`/`) is NOT user-writable — a
|
|
6284
|
-
* home placed there can never be created (`mkdir` EACCES), `ensureAgentCopilotHome`
|
|
6285
|
-
* fail-opens and returns the uncreatable path anyway, and every spawned
|
|
6286
|
-
* `copilot --acp` / copilot agent then inherits a `COPILOT_HOME` it can't use and
|
|
6287
|
-
* exits code 1 silently (surfaced in CC as "Load failed"). So when the drive root
|
|
6288
|
-
* is the bare POSIX root we fall back to the user home — repo-external, writable,
|
|
6289
|
-
* and on the user's own volume.
|
|
6290
|
-
*/
|
|
6291
|
-
function resolveAgentCopilotHome(minionsDir) {
|
|
6292
|
-
let base;
|
|
6293
|
-
if (minionsDir) {
|
|
6294
|
-
const root = path.parse(path.resolve(String(minionsDir))).root;
|
|
6295
|
-
// The POSIX filesystem root ('/') is not user-writable; Windows drive/UNC
|
|
6296
|
-
// roots are. Anchor at the user home in the unwritable-root case.
|
|
6297
|
-
base = (root === '/' || root === path.sep) ? os.homedir() : root;
|
|
6298
|
-
} else {
|
|
6299
|
-
base = os.homedir();
|
|
6300
|
-
}
|
|
6301
|
-
return path.join(base, '.minions-agent-copilot-home');
|
|
6302
|
-
}
|
|
6303
|
-
|
|
6304
5957
|
/**
|
|
6305
5958
|
* Derive a scratch/temp base dir on the same volume agent work already lives on
|
|
6306
5959
|
* — the configured worktree location (engine.worktreeRoot, default
|
|
@@ -6325,6 +5978,55 @@ function resolveAgentTempBaseDir(project, engine, minionsDir) {
|
|
|
6325
5978
|
return path.join(anchor, '.agent-temp');
|
|
6326
5979
|
}
|
|
6327
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
|
+
|
|
6328
6030
|
// Seed the agent COPILOT_HOME's mcp-config (copy of `~/.copilot/mcp-config.json`
|
|
6329
6031
|
// MINUS `engine.copilotAgentDisabledMcpServers`) and return the home path. This
|
|
6330
6032
|
// is the source of truth for "what MCP servers may an agent-dispatch copilot
|
|
@@ -6498,13 +6200,7 @@ function resolveSpawnPaths(project, type, minionsDir, options) {
|
|
|
6498
6200
|
// (project.localPath for read-only / live; worktree root for mutating).
|
|
6499
6201
|
// When set, the engine lands the agent at <base>/<workdir> so monorepo
|
|
6500
6202
|
// subpackage dispatches can use the runtime CLI's native cwd-rooted skill
|
|
6501
|
-
// discovery
|
|
6502
|
-
// awareness of which package is which. Cross-package skill aggregation is
|
|
6503
|
-
// explicitly out of scope (per PRD open_question) — when workdir is set,
|
|
6504
|
-
// the harness propagation block in engine.js also CLIPS its candidate
|
|
6505
|
-
// `--project-harness-dir` set to dirs that live under the workdir mirror
|
|
6506
|
-
// in `project.localPath`, so a `packages/foo` dispatch never sees
|
|
6507
|
-
// `packages/bar/.claude/skills/...`.
|
|
6203
|
+
// discovery without engine awareness of which package is which.
|
|
6508
6204
|
//
|
|
6509
6205
|
// Three pure helpers, all unit-tested in test/unit/work-item-workdir.test.js:
|
|
6510
6206
|
//
|
|
@@ -6525,13 +6221,6 @@ function resolveSpawnPaths(project, type, minionsDir, options) {
|
|
|
6525
6221
|
// already rejects bare `..` / absolute strings). `base` MUST be
|
|
6526
6222
|
// absolute. Empty/null workdir returns the base unchanged.
|
|
6527
6223
|
//
|
|
6528
|
-
// 3. filterProjectHarnessDirsForWorkdir(dirs, opts) — clips the
|
|
6529
|
-
// candidate `--project-harness-dir` set to entries under the resolved
|
|
6530
|
-
// cwd anchor in `project.localPath`, excluding entries under the
|
|
6531
|
-
// worktree mirror (already discoverable via the agent's cwd). When
|
|
6532
|
-
// `workdir` is null/empty the behavior matches P-08b62d49 unchanged
|
|
6533
|
-
// (filter to project.localPath, exclude worktree).
|
|
6534
|
-
//
|
|
6535
6224
|
// The private `_applyWorkdirOrThrow(base, workdir)` is the throw-on-error
|
|
6536
6225
|
// adapter used inside resolveSpawnPaths so live/read-only callers don't
|
|
6537
6226
|
// need to forward the `error` field.
|
|
@@ -6617,48 +6306,6 @@ function applyWorkdir(base, workdir) {
|
|
|
6617
6306
|
return { cwd: joined };
|
|
6618
6307
|
}
|
|
6619
6308
|
|
|
6620
|
-
function filterProjectHarnessDirsForWorkdir(dirs, opts) {
|
|
6621
|
-
if (!Array.isArray(dirs) || dirs.length === 0) return [];
|
|
6622
|
-
const projectLocalPath = opts && opts.projectLocalPath;
|
|
6623
|
-
const worktreePath = opts && opts.worktreePath;
|
|
6624
|
-
const workdir = opts && opts.workdir;
|
|
6625
|
-
if (!projectLocalPath || !worktreePath) return [];
|
|
6626
|
-
|
|
6627
|
-
const projectLocalAbs = path.resolve(String(projectLocalPath));
|
|
6628
|
-
const worktreeAbs = path.resolve(String(worktreePath));
|
|
6629
|
-
|
|
6630
|
-
// When workdir is set, the harness propagation anchors shift to the
|
|
6631
|
-
// subpath mirror in BOTH the project root and the worktree root, so a
|
|
6632
|
-
// `packages/foo` dispatch only sees `<projectLocal>/packages/foo/...`
|
|
6633
|
-
// harness dirs (its actual cwd in the operator's main checkout) and
|
|
6634
|
-
// excludes `<worktree>/packages/foo/...` (already discoverable via cwd).
|
|
6635
|
-
let projectAnchor = projectLocalAbs;
|
|
6636
|
-
let worktreeAnchor = worktreeAbs;
|
|
6637
|
-
if (workdir && typeof workdir === 'string' && workdir) {
|
|
6638
|
-
const pr = applyWorkdir(projectLocalAbs, workdir);
|
|
6639
|
-
const wr = applyWorkdir(worktreeAbs, workdir);
|
|
6640
|
-
// If either join fails containment, drop everything — better empty than wrong.
|
|
6641
|
-
if (pr.error || wr.error) return [];
|
|
6642
|
-
projectAnchor = pr.cwd;
|
|
6643
|
-
worktreeAnchor = wr.cwd;
|
|
6644
|
-
}
|
|
6645
|
-
|
|
6646
|
-
const seen = new Set();
|
|
6647
|
-
const out = [];
|
|
6648
|
-
for (const dir of dirs) {
|
|
6649
|
-
if (typeof dir !== 'string' || !dir) continue;
|
|
6650
|
-
const abs = path.resolve(dir);
|
|
6651
|
-
if (seen.has(abs)) continue;
|
|
6652
|
-
seen.add(abs);
|
|
6653
|
-
const insideProject = abs === projectAnchor || abs.startsWith(projectAnchor + path.sep);
|
|
6654
|
-
if (!insideProject) continue;
|
|
6655
|
-
const insideWorktree = abs === worktreeAnchor || abs.startsWith(worktreeAnchor + path.sep);
|
|
6656
|
-
if (insideWorktree) continue;
|
|
6657
|
-
out.push(abs);
|
|
6658
|
-
}
|
|
6659
|
-
return out;
|
|
6660
|
-
}
|
|
6661
|
-
|
|
6662
6309
|
// ── HTTP Origin Allowlist & Security Headers ─────────────────────────────────
|
|
6663
6310
|
// Pure helpers used by dashboard.js to gate mutating requests against an
|
|
6664
6311
|
// explicit allowlist of local origins and to attach uniform security response
|
|
@@ -7705,18 +7352,12 @@ function getPrLinks() {
|
|
|
7705
7352
|
mergePrLinkItems(links, pr.id, pr.prdItems || []);
|
|
7706
7353
|
}
|
|
7707
7354
|
} catch { /* SQL unavailable — skip primary source */ }
|
|
7708
|
-
//
|
|
7709
|
-
// routes both reads and writes through small-state-store; the JSON file is
|
|
7710
|
-
// kept as a dual-write mirror. SQL failures fall through to the JSON file.
|
|
7355
|
+
// Static PR links are stored in SQLite.
|
|
7711
7356
|
let static_ = null;
|
|
7712
7357
|
try {
|
|
7713
7358
|
const store = require('./small-state-store');
|
|
7714
7359
|
static_ = store.readPrLinks();
|
|
7715
7360
|
} catch { static_ = null; }
|
|
7716
|
-
if (!static_ || Object.keys(static_).length === 0) {
|
|
7717
|
-
try { static_ = JSON.parse(fs.readFileSync(PR_LINKS_PATH, 'utf8')); }
|
|
7718
|
-
catch { static_ = null; }
|
|
7719
|
-
}
|
|
7720
7361
|
if (static_ && typeof static_ === 'object' && !Array.isArray(static_)) {
|
|
7721
7362
|
for (const [k, v] of Object.entries(static_)) {
|
|
7722
7363
|
const canonical = parseCanonicalPrId(k);
|
|
@@ -7915,6 +7556,32 @@ function _clearPrTombstonesForIdentity(identity) {
|
|
|
7915
7556
|
return cleared;
|
|
7916
7557
|
}
|
|
7917
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
|
+
|
|
7918
7585
|
/**
|
|
7919
7586
|
* Canonical PR-producing work contract helper.
|
|
7920
7587
|
*
|
|
@@ -8233,19 +7900,10 @@ function mutateWorkItems(filePath, mutator) {
|
|
|
8233
7900
|
if (insideMinionsDir) {
|
|
8234
7901
|
const store = require('./work-items-store');
|
|
8235
7902
|
const scope = store.scopeForFilePath(filePath);
|
|
8236
|
-
// W-mr9vcxvy000cdb4e — the JSON mirror is now written INSIDE
|
|
8237
|
-
// applyWorkItemsMutation's SQL transaction (before commit), not here
|
|
8238
|
-
// after the fact. Mirroring post-commit left a cross-process window
|
|
8239
|
-
// where a sibling process (dashboard vs. engine — separate Node
|
|
8240
|
-
// processes, each with its own in-memory mirror-hash cache) could
|
|
8241
|
-
// observe the new SQL row before the JSON file caught up and
|
|
8242
|
-
// destructively rehydrate the scope from that stale file, dropping
|
|
8243
|
-
// the just-created row (the "phantom work item" dispatch race). See
|
|
8244
|
-
// work-items-store.js#applyWorkItemsMutation for the full rationale.
|
|
8245
7903
|
const { wrote, result } = store.applyWorkItemsMutation(scope, (items) => {
|
|
8246
7904
|
if (!Array.isArray(items)) items = [];
|
|
8247
7905
|
return mutator(items) || items;
|
|
8248
|
-
}
|
|
7906
|
+
});
|
|
8249
7907
|
if (wrote) {
|
|
8250
7908
|
try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
8251
7909
|
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
@@ -8297,20 +7955,10 @@ function mutatePullRequests(filePath, mutator) {
|
|
|
8297
7955
|
if (insideMinionsDir) {
|
|
8298
7956
|
const store = require('./pull-requests-store');
|
|
8299
7957
|
const scope = store.scopeForFilePath(filePath);
|
|
8300
|
-
// W-mrcg7j9k000ja233 (#759) — the JSON mirror is now written INSIDE
|
|
8301
|
-
// applyPullRequestsMutation's SQL transaction (before commit), not here
|
|
8302
|
-
// after the fact. Mirroring post-commit left a cross-process window
|
|
8303
|
-
// where a sibling process (dashboard vs. engine — separate Node
|
|
8304
|
-
// processes) could observe the new SQL row before the JSON file caught
|
|
8305
|
-
// up and destructively rehydrate the scope from that stale file,
|
|
8306
|
-
// dropping the just-written row. See
|
|
8307
|
-
// pull-requests-store.js#applyPullRequestsMutation for the full
|
|
8308
|
-
// rationale (mirrors the identical work-items-store.js fix,
|
|
8309
|
-
// W-mr9vcxvy000cdb4e).
|
|
8310
7958
|
const { wrote, result } = store.applyPullRequestsMutation(scope, (prs) => {
|
|
8311
7959
|
if (!Array.isArray(prs)) prs = [];
|
|
8312
7960
|
return mutator(prs) || prs;
|
|
8313
|
-
}
|
|
7961
|
+
});
|
|
8314
7962
|
if (wrote) {
|
|
8315
7963
|
try { require('./db-events').emitStateEvent('pull_requests'); } catch { /* optional */ }
|
|
8316
7964
|
}
|
|
@@ -9336,7 +8984,6 @@ module.exports = {
|
|
|
9336
8984
|
resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentModel, resolveCcModel,
|
|
9337
8985
|
resolveAgentMaxBudget, resolveAgentBareMode,
|
|
9338
8986
|
resolveCopilotAgentDisabledMcpServers,
|
|
9339
|
-
resolveAgentHermeticHarness,
|
|
9340
8987
|
applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
|
|
9341
8988
|
runtimeConfigWarnings,
|
|
9342
8989
|
projectWorkSourceWarnings,
|
|
@@ -9346,7 +8993,6 @@ module.exports = {
|
|
|
9346
8993
|
WATCH_STALLED_DEFAULT_TICKS, WATCH_STUCK_STAGE_DEFAULT_TICKS,
|
|
9347
8994
|
PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
|
|
9348
8995
|
FAILURE_CLASS, COMPLETION_FIELDS,
|
|
9349
|
-
HARNESS_USED_KINDS, HARNESS_USED_MAX_ENTRIES, HARNESS_USED_MAX_FIELD_LEN, normalizeHarnessUsed, groundHarnessUsed,
|
|
9350
8996
|
DEFAULT_AGENT_METRICS,
|
|
9351
8997
|
DEFAULT_AGENTS,
|
|
9352
8998
|
DEFAULT_CLAUDE,
|
|
@@ -9414,6 +9060,7 @@ module.exports = {
|
|
|
9414
9060
|
normalizePrLinkItems, // exported for testing
|
|
9415
9061
|
mergePrLinkItems, // exported for testing
|
|
9416
9062
|
isContextOnlyPrRecord,
|
|
9063
|
+
shouldAdoptPlatformCreated, // W-mrezh0yb0007f733 — platform creationDate authority
|
|
9417
9064
|
upsertPullRequestRecord,
|
|
9418
9065
|
collapseDuplicatePrRecords, // P-e9f0a2b4 — one-time repair helper
|
|
9419
9066
|
isAutoManagedPrRecord, // W-mq5s5ttx000j7ab8-a — exported for engine + watch-plugin gate consolidation
|
|
@@ -9448,7 +9095,6 @@ module.exports = {
|
|
|
9448
9095
|
resolveSpawnPaths,
|
|
9449
9096
|
validateWorkItemWorkdir,
|
|
9450
9097
|
applyWorkdir,
|
|
9451
|
-
filterProjectHarnessDirsForWorkdir,
|
|
9452
9098
|
READ_ONLY_ROOT_TASK_TYPES,
|
|
9453
9099
|
isLiveCommandCenterPath,
|
|
9454
9100
|
describeCcProtectedPaths,
|