@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.
- package/bin/minions.js +38 -11
- package/dashboard/js/render-other.js +2 -30
- package/dashboard/js/render-work-items.js +0 -34
- package/dashboard/js/settings.js +12 -27
- package/dashboard/pages/tools.html +1 -1
- package/dashboard.js +80 -257
- 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/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/db/index.js +60 -10
- 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/gh-comment.js +2 -10
- package/engine/github.js +8 -6
- package/engine/lifecycle.js +58 -173
- package/engine/llm.js +9 -11
- package/engine/managed-spawn.js +1 -6
- package/engine/memory-store.js +8 -6
- package/engine/metrics-store.js +4 -128
- package/engine/pipeline.js +4 -7
- package/engine/playbook.js +8 -196
- 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 +174 -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
|
|
@@ -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({
|
|
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
|
-
|
|
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
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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,
|