@yemi33/minions 0.1.2379 → 0.1.2380
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 +19 -9
- package/dashboard/js/refresh.js +2 -3
- package/dashboard/js/render-prd.js +1 -1
- package/dashboard/js/render-work-items.js +13 -6
- package/dashboard.js +393 -721
- package/docs/architecture-review-2026-07-09.md +2 -4
- package/docs/auto-discovery.md +36 -49
- package/docs/blog-first-successful-dispatch.md +1 -1
- package/docs/branch-derivation.md +2 -2
- package/docs/command-center.md +1 -1
- package/docs/completion-reports.md +14 -4
- package/docs/constellation-bridge.md +59 -10
- package/docs/constellation-style-telemetry.md +6 -6
- package/docs/cooldown-merge-semantics.md +4 -0
- package/docs/copilot-cli-schema.md +3 -3
- package/docs/cross-repo-plans.md +17 -17
- package/docs/deprecated.json +2 -2
- package/docs/design-state-storage.md +1 -1
- package/docs/documentation-audit-2026-07-09.md +2 -2
- package/docs/engine-restart.md +20 -8
- package/docs/harness-mode.md +1 -1
- package/docs/managed-spawn.md +4 -4
- package/docs/onboarding.md +1 -2
- package/docs/pr-comment-followup.md +3 -3
- package/docs/pr-review-fix-loop.md +1 -1
- package/docs/proposals/repo-pool-for-live-checkout.md +1 -2
- package/docs/qa-runbook-lifecycle.md +4 -4
- package/docs/qa-runbooks.md +2 -2
- package/docs/rfc-completion-json.md +4 -1
- package/docs/runtime-adapters.md +1 -1
- package/docs/self-improvement.md +4 -5
- package/docs/shared-lifecycle-module-map.md +3 -1
- package/docs/slim-ux/architecture-suggestions.md +5 -6
- package/docs/slim-ux/concepts.md +23 -25
- package/docs/watches.md +7 -7
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +1 -1
- package/engine/abandoned-pr-reconciliation.js +4 -5
- package/engine/ado-status.js +5 -5
- package/engine/ado.js +20 -25
- package/engine/agent-worker-pool.js +58 -1
- package/engine/bridge.js +260 -5
- package/engine/cleanup.js +48 -131
- package/engine/cli.js +125 -83
- package/engine/cooldown.js +9 -16
- package/engine/db/index.js +22 -9
- package/engine/db/migrations/009-qa.js +1 -1
- package/engine/db/migrations/020-qa-session-scopes.js +23 -0
- package/engine/db/migrations/021-archived-work-items.js +72 -0
- package/engine/db/migrations/022-global-cc-session.js +31 -0
- package/engine/db/migrations/023-engine-state.js +30 -0
- package/engine/db/migrations/024-prd-ghost-collisions.js +53 -0
- package/engine/db/migrations/025-malformed-work-item-phantoms.js +33 -0
- package/engine/dispatch-store.js +2 -7
- package/engine/dispatch.js +34 -41
- package/engine/github.js +20 -27
- package/engine/lifecycle.js +221 -283
- package/engine/llm.js +1 -1
- package/engine/logs-store.js +2 -2
- package/engine/managed-spawn.js +2 -23
- package/engine/meeting.js +6 -6
- package/engine/metrics-store.js +2 -2
- package/engine/note-link-backfill.js +6 -11
- package/engine/pipeline.js +18 -36
- package/engine/playbook.js +1 -1
- package/engine/prd-store.js +73 -54
- package/engine/preflight.js +2 -5
- package/engine/projects.js +15 -62
- package/engine/pull-requests-store.js +0 -17
- package/engine/qa-runbooks.js +2 -2
- package/engine/qa-runs.js +1 -8
- package/engine/qa-sessions.js +41 -64
- package/engine/queries.js +120 -219
- package/engine/routing.js +3 -5
- package/engine/scheduler.js +0 -4
- package/engine/shared-branch-pr-reconcile.js +2 -3
- package/engine/shared.js +132 -637
- package/engine/small-state-store.js +89 -10
- package/engine/state-operations.js +16 -4
- package/engine/stdio-timestamps.js +1 -1
- package/engine/timeout.js +5 -12
- package/engine/watch-actions.js +20 -22
- package/engine/watches-store.js +1 -1
- package/engine/watches.js +6 -10
- package/engine/work-item-validation.js +52 -0
- package/engine/work-items-store.js +127 -29
- package/engine/worktree-gc.js +2 -2
- package/engine/worktree-pool.js +8 -18
- package/engine.js +147 -343
- package/minions.js +2 -2
- package/package.json +1 -1
- package/playbooks/plan-to-prd.md +2 -2
- package/playbooks/shared-rules.md +3 -3
- package/playbooks/templates/followup-dispatch.md +1 -1
- package/playbooks/verify.md +1 -1
- package/prompts/cc-system.md +2 -2
package/engine/shared.js
CHANGED
|
@@ -71,25 +71,16 @@ const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
|
|
|
71
71
|
// don't collide with heap-snapshot pruning.
|
|
72
72
|
const CRASH_REPORTS_DIR = path.join(ENGINE_DIR, 'diagnostics', 'crash-reports');
|
|
73
73
|
const CONTROL_PATH = path.join(ENGINE_DIR, 'control.json');
|
|
74
|
-
const COOLDOWNS_PATH = path.join(ENGINE_DIR, 'cooldowns.json');
|
|
75
74
|
// W-mp60tw0u000j3931: Persistent cross-restart engine state (migration markers,
|
|
76
75
|
// one-shot reconciliation versions, etc.). Distinct from CONTROL_PATH which is
|
|
77
76
|
// process-lifetime (state/pid/ownerToken). See ENGINE_DEFAULTS.abandonedReconciliationVersion
|
|
78
77
|
// for the first consumer.
|
|
79
|
-
|
|
80
|
-
// Phase 9.3: wrap raw safeWrite callers for kb-checkpoint, kb-swept,
|
|
81
|
-
// kb-sweep-state, and test-results so they go through mutateJsonFileLocked
|
|
82
|
-
// (the same path mutateControl / mutateEngineState / mutateCooldowns use).
|
|
83
|
-
// This sets them up for SQL routing via _SMALL_STATE_MUTATE_ROUTES later, and
|
|
84
|
-
// in the case of kb-sweep-state.json closes a real cross-process race (the
|
|
85
|
-
// dashboard process and the detached kb-sweep-runner child both write it).
|
|
78
|
+
// File-backed coordination and diagnostic state uses lock-safe mutators.
|
|
86
79
|
const KB_CHECKPOINT_PATH = path.join(ENGINE_DIR, 'kb-checkpoint.json');
|
|
87
80
|
const KB_SWEPT_PATH = path.join(ENGINE_DIR, 'kb-swept.json');
|
|
88
81
|
const KB_SWEEP_STATE_PATH = path.join(ENGINE_DIR, 'kb-sweep-state.json');
|
|
89
82
|
const TEST_RESULTS_PATH = path.join(ENGINE_DIR, 'test-results.json');
|
|
90
|
-
const PR_LINKS_PATH = path.join(MINIONS_DIR, 'engine', 'pr-links.json');
|
|
91
83
|
const PINNED_ITEMS_PATH = path.join(MINIONS_DIR, 'engine', 'kb-pins.json');
|
|
92
|
-
const LOG_PATH = path.join(MINIONS_DIR, 'engine', 'log.json');
|
|
93
84
|
// Constellation-bridge cross-repo marker (P-wi1-bridge-readonly). Written by the
|
|
94
85
|
// Constellation agent's bridge on every successful poll; read by `minions bridge
|
|
95
86
|
// status` to surface the last-seen timestamp. The engine itself never writes
|
|
@@ -340,7 +331,7 @@ function buildEditsSeen(comments, prev, cap) {
|
|
|
340
331
|
|
|
341
332
|
// ── Secret Redaction (SEC-09) ──────────────────────────────────────────────
|
|
342
333
|
// Pure, side-effect-free redactor applied to every entry on the log write path
|
|
343
|
-
// so ADO tokens, JWTs, and azureauth stdout dumps never land in
|
|
334
|
+
// so ADO tokens, JWTs, and azureauth stdout dumps never land in persisted logs
|
|
344
335
|
// (2500-entry ring buffer readable by any local process).
|
|
345
336
|
//
|
|
346
337
|
// Replacements (order matters — azureauth first, then Bearer, then bare JWT):
|
|
@@ -464,7 +455,7 @@ let _logFlushTimer = null;
|
|
|
464
455
|
function log(level, msg, meta = {}) {
|
|
465
456
|
// SEC-09: redact sensitive patterns (ADO tokens, JWTs, azureauth stdout)
|
|
466
457
|
// before both the in-memory buffer push and the console echo — ensures
|
|
467
|
-
// nothing sensitive is persisted to
|
|
458
|
+
// nothing sensitive is persisted to the log store or engine stdout logs.
|
|
468
459
|
const safeMsg = typeof msg === 'string' ? _redactString(msg) : msg;
|
|
469
460
|
const safeMeta = redactSecrets(meta) || {};
|
|
470
461
|
const entry = { timestamp: ts(), level, message: safeMsg, ...safeMeta };
|
|
@@ -500,30 +491,7 @@ function log(level, msg, meta = {}) {
|
|
|
500
491
|
}
|
|
501
492
|
}
|
|
502
493
|
|
|
503
|
-
|
|
504
|
-
* Resolve the log file path at write time, not at module load time.
|
|
505
|
-
*
|
|
506
|
-
* `LOG_PATH` is captured eagerly when shared.js first loads. In production this
|
|
507
|
-
* is fine — there's a single shared.js instance per process. In tests the
|
|
508
|
-
* require cache is busted to swap MINIONS_DIR, but modules NOT in
|
|
509
|
-
* ISOLATED_MODULES (engine/github.js, engine/ado.js, etc.) keep a reference to
|
|
510
|
-
* the OLD shared.js whose `LOG_PATH` still points at `D:/squad/engine/log.json`.
|
|
511
|
-
* That leaked test pollution into the live engine log (e.g. `_test/backoff-*`,
|
|
512
|
-
* `this-playbook-does-not-exist-xyz`, `TEST-EXT-*` meeting IDs).
|
|
513
|
-
*
|
|
514
|
-
* Resolution order:
|
|
515
|
-
* 1. `MINIONS_TEST_DIR` — per-test isolation root (regression tests rely
|
|
516
|
-
* on this — log goes to `<tmpdir>/engine/log.json`).
|
|
517
|
-
* 2. `MINIONS_LOG_PATH` — default test log target. Set at the top of
|
|
518
|
-
* `test/unit.test.js` so tests that exercise log() without explicit
|
|
519
|
-
* isolation (e.g. github backoff helpers, cooldown helpers) still land
|
|
520
|
-
* in a benign tmp file instead of the live `D:/squad/engine/log.json`.
|
|
521
|
-
* 3. `MINIONS_HOME` / repo root fallback — production behavior.
|
|
522
|
-
*
|
|
523
|
-
* Lazy resolution honors the *current* env at flush time, so even unbussed
|
|
524
|
-
* dependents write to the test dir while the test owns it.
|
|
525
|
-
*/
|
|
526
|
-
// Resolve the SQL state.db path at write time.
|
|
494
|
+
// Resolve the SQL database at write time so test roots stay isolated.
|
|
527
495
|
function _currentLogDbPath() {
|
|
528
496
|
if (process.env.MINIONS_TEST_DIR) {
|
|
529
497
|
return path.join(path.resolve(process.env.MINIONS_TEST_DIR), 'engine', 'state.db');
|
|
@@ -547,9 +515,9 @@ function resolveEngineCacheDir(fallbackEngineDir) {
|
|
|
547
515
|
// try/catch and the caller sees them. Dashboard self-open and `minions dash`
|
|
548
516
|
// / `minions restart` post-health open all funnel through here.
|
|
549
517
|
//
|
|
550
|
-
// Every call writes a structured `event: 'browser-open'` entry
|
|
518
|
+
// Every call writes a structured `event: 'browser-open'` log entry
|
|
551
519
|
// (via shared.log) and bumps a per-reason counter in `_engine.browserOpens`
|
|
552
|
-
// in metrics.
|
|
520
|
+
// in metrics state. Callers MUST pass `opts.reason` so the log/metric is
|
|
553
521
|
// attributable; a missing reason is logged at `warn` level with
|
|
554
522
|
// `reason: 'unknown'` to make a regression loud rather than silent.
|
|
555
523
|
//
|
|
@@ -620,7 +588,7 @@ function openUrlInBrowser(url, opts = {}) {
|
|
|
620
588
|
}
|
|
621
589
|
|
|
622
590
|
// Bump per-reason counter under metrics._engine.browserOpens. Best-effort —
|
|
623
|
-
//
|
|
591
|
+
// Metrics writes must never throw out of openUrlInBrowser. Schema:
|
|
624
592
|
// _engine.browserOpens.<reason> = { count, suppressedCount, lastAt }
|
|
625
593
|
function _bumpBrowserOpenMetric(reason, { suppressed = false } = {}) {
|
|
626
594
|
try {
|
|
@@ -668,7 +636,7 @@ function _flushLogBuffer() {
|
|
|
668
636
|
for (const [dbPath, entries] of byDbPath) {
|
|
669
637
|
store.appendLogsToDbFile(dbPath, entries);
|
|
670
638
|
}
|
|
671
|
-
} catch { /*
|
|
639
|
+
} catch { /* logging is best-effort */ }
|
|
672
640
|
|
|
673
641
|
}
|
|
674
642
|
|
|
@@ -684,11 +652,6 @@ function flushLogs() {
|
|
|
684
652
|
// ── File I/O ─────────────────────────────────────────────────────────────────
|
|
685
653
|
|
|
686
654
|
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
|
-
}
|
|
692
655
|
try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
|
|
693
656
|
}
|
|
694
657
|
|
|
@@ -698,24 +661,10 @@ function safeRead(p) {
|
|
|
698
661
|
// the client-sent document body with '' when the file was deleted/moved
|
|
699
662
|
// between panel open and send.
|
|
700
663
|
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
|
-
}
|
|
706
664
|
try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
|
|
707
665
|
}
|
|
708
666
|
|
|
709
667
|
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
|
-
}
|
|
719
668
|
try { return fs.readdirSync(dir); } catch { return []; }
|
|
720
669
|
}
|
|
721
670
|
|
|
@@ -845,125 +794,9 @@ function isSourcePlanContentStale(plansDir, plan) {
|
|
|
845
794
|
return out;
|
|
846
795
|
}
|
|
847
796
|
|
|
848
|
-
// ── SQL-routing shim for migrated state files ──────────────────────────────
|
|
849
|
-
//
|
|
850
|
-
// Phase 9 (post-Phase 8 cleanup): every state file that has a SQL backing
|
|
851
|
-
// (dispatches, work-items, pull-requests, metrics, watches, schedule-runs,
|
|
852
|
-
// pipeline-runs, managed-processes, worktree-pool, qa-runs, qa-sessions)
|
|
853
|
-
// becomes SQL-only at the read API surface. The JSON sidecar files stop
|
|
854
|
-
// being mirror-written; this shim makes existing `safeJson*` callers
|
|
855
|
-
// transparently read from SQL so we don't have to convert ~40 call sites
|
|
856
|
-
// in engine.js / dashboard.js / cleanup.js / etc.
|
|
857
|
-
//
|
|
858
|
-
// Matching is by path suffix rather than full-prefix against MINIONS_DIR,
|
|
859
|
-
// because tests using MINIONS_TEST_DIR re-resolve the dir but use the same
|
|
860
|
-
// suffix pattern. Returns `{value}` on hit, `null` on miss; callers fall
|
|
861
|
-
// through to ordinary file-backed JSON handling.
|
|
862
|
-
//
|
|
863
|
-
// Per-project work-items / pull-requests need scope extraction from the
|
|
864
|
-
// path. `<MINIONS_DIR>/projects/<name>/work-items.json` → scope `<name>`;
|
|
865
|
-
// `<MINIONS_DIR>/work-items.json` → scope `central`.
|
|
866
|
-
function _routeJsonReadToSql(p) {
|
|
867
|
-
if (!p || typeof p !== 'string') return null;
|
|
868
|
-
const norm = p.replace(/\\/g, '/');
|
|
869
|
-
// Only route reads from paths inside MINIONS_DIR. Tests that use
|
|
870
|
-
// createTmpDir() write to ad-hoc temp paths that shouldn't be hijacked
|
|
871
|
-
// by the SQL stores (the path-suffix patterns below would otherwise
|
|
872
|
-
// match e.g. `<tmp>/pull-requests.json` and return empty SQL data).
|
|
873
|
-
const minionsNorm = String(MINIONS_DIR).replace(/\\/g, '/');
|
|
874
|
-
if (!norm.startsWith(minionsNorm + '/') && norm !== minionsNorm) return null;
|
|
875
|
-
let m;
|
|
876
|
-
try {
|
|
877
|
-
if (norm.endsWith('/engine/dispatch.json')) {
|
|
878
|
-
const store = require('./dispatch-store');
|
|
879
|
-
return { value: store.readDispatchSectioned() };
|
|
880
|
-
}
|
|
881
|
-
if (norm.endsWith('/engine/metrics.json')) {
|
|
882
|
-
const store = require('./metrics-store');
|
|
883
|
-
return { value: store.readMetrics() };
|
|
884
|
-
}
|
|
885
|
-
if (norm.endsWith('/engine/watches.json')) {
|
|
886
|
-
const store = require('./watches-store');
|
|
887
|
-
return { value: store.readWatches() };
|
|
888
|
-
}
|
|
889
|
-
if (norm.endsWith('/engine/schedule-runs.json')) {
|
|
890
|
-
const store = require('./small-state-store');
|
|
891
|
-
return { value: store.readScheduleRuns() };
|
|
892
|
-
}
|
|
893
|
-
if (norm.endsWith('/engine/pipeline-runs.json')) {
|
|
894
|
-
const store = require('./small-state-store');
|
|
895
|
-
return { value: store.readPipelineRuns() };
|
|
896
|
-
}
|
|
897
|
-
if (norm.endsWith('/engine/managed-processes.json')) {
|
|
898
|
-
const store = require('./small-state-store');
|
|
899
|
-
return { value: store.readManagedProcesses() };
|
|
900
|
-
}
|
|
901
|
-
if (norm.endsWith('/engine/worktree-pool.json')) {
|
|
902
|
-
const store = require('./small-state-store');
|
|
903
|
-
return { value: store.readWorktreePool() };
|
|
904
|
-
}
|
|
905
|
-
if (norm.endsWith('/engine/qa-runs.json')) {
|
|
906
|
-
const store = require('./small-state-store');
|
|
907
|
-
return { value: store.readQaRuns() };
|
|
908
|
-
}
|
|
909
|
-
if (norm.endsWith('/engine/qa-sessions.json')) {
|
|
910
|
-
const store = require('./small-state-store');
|
|
911
|
-
return { value: store.readQaSessions() };
|
|
912
|
-
}
|
|
913
|
-
if (norm.endsWith('/engine/pr-links.json')) {
|
|
914
|
-
const store = require('./small-state-store');
|
|
915
|
-
return { value: store.readPrLinks() };
|
|
916
|
-
}
|
|
917
|
-
if (norm.endsWith('/engine/cooldowns.json')) {
|
|
918
|
-
const store = require('./small-state-store');
|
|
919
|
-
return { value: store.readCooldowns() };
|
|
920
|
-
}
|
|
921
|
-
if (norm.endsWith('/engine/pending-rebases.json')) {
|
|
922
|
-
const store = require('./small-state-store');
|
|
923
|
-
return { value: store.readPendingRebases() };
|
|
924
|
-
}
|
|
925
|
-
if (norm.endsWith('/engine/cc-sessions.json')) {
|
|
926
|
-
const store = require('./small-state-store');
|
|
927
|
-
return { value: store.readCcSessions() };
|
|
928
|
-
}
|
|
929
|
-
if (norm.endsWith('/engine/doc-sessions.json')) {
|
|
930
|
-
const store = require('./small-state-store');
|
|
931
|
-
return { value: store.readDocSessions() };
|
|
932
|
-
}
|
|
933
|
-
// Per-project work-items.json — the path is now only a scope identifier.
|
|
934
|
-
if ((m = norm.match(/\/projects\/([^/]+)\/work-items\.json$/))) {
|
|
935
|
-
const store = require('./work-items-store');
|
|
936
|
-
return { value: store.readWorkItemsForScope(m[1]) };
|
|
937
|
-
}
|
|
938
|
-
// Per-project pull-requests.json
|
|
939
|
-
if ((m = norm.match(/\/projects\/([^/]+)\/pull-requests\.json$/))) {
|
|
940
|
-
const store = require('./pull-requests-store');
|
|
941
|
-
return { value: store.readPullRequestsForScope(m[1]) };
|
|
942
|
-
}
|
|
943
|
-
// Central work-items.json — `<MINIONS_DIR>/work-items.json`. Must not
|
|
944
|
-
// collide with `/projects/<name>/work-items.json` (handled above).
|
|
945
|
-
if (/(^|\/)work-items\.json$/.test(norm) && !/\/projects\//.test(norm)) {
|
|
946
|
-
const store = require('./work-items-store');
|
|
947
|
-
return { value: store.readWorkItemsForScope('central') };
|
|
948
|
-
}
|
|
949
|
-
// Central pull-requests.json
|
|
950
|
-
if (/(^|\/)pull-requests\.json$/.test(norm) && !/\/projects\//.test(norm)) {
|
|
951
|
-
const store = require('./pull-requests-store');
|
|
952
|
-
return { value: store.readPullRequestsForScope('central') };
|
|
953
|
-
}
|
|
954
|
-
} catch (e) {
|
|
955
|
-
// Phase 9.4: store/load failures (not SQLite-unavailable — the CLI shim
|
|
956
|
-
// in bin/minions.js guarantees node:sqlite is loadable) propagate up so
|
|
957
|
-
// the caller can decide whether to retry or surface.
|
|
958
|
-
throw e;
|
|
959
|
-
}
|
|
960
|
-
return null;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
797
|
/**
|
|
964
798
|
* Read a JSON file with **automatic restore from `.backup` sidecar** on
|
|
965
|
-
* missing/corrupt primary. Intended for
|
|
966
|
-
* (work-items.json, dispatch.json, pull-requests.json, etc.) that are paired
|
|
799
|
+
* missing/corrupt primary. Intended for intentionally file-backed data paired
|
|
967
800
|
* with a `.backup` sidecar written by `safeWrite`. Returns the parsed JSON,
|
|
968
801
|
* or null when both primary and backup are missing/unparseable.
|
|
969
802
|
*
|
|
@@ -972,45 +805,12 @@ function _routeJsonReadToSql(p) {
|
|
|
972
805
|
* to the primary path (best-effort). This protects live state from torn
|
|
973
806
|
* writes / interrupted saves.
|
|
974
807
|
*
|
|
975
|
-
* **SQL routing (Phase 9):** when the path matches a migrated state file
|
|
976
|
-
* (work-items, pull-requests, dispatch, metrics, watches, schedule-runs,
|
|
977
|
-
* pipeline-runs, managed-processes, worktree-pool, qa-runs, qa-sessions,
|
|
978
|
-
* pr-links), the read is served from SQLite via `_routeJsonReadToSql` — the
|
|
979
|
-
* JSON file on disk is no longer authoritative.
|
|
980
|
-
*
|
|
981
808
|
* Counterpart: `safeJsonNoRestore` for terminal artifacts and "missing == gone"
|
|
982
809
|
* reads (cooldowns, archived PRDs, ephemeral session state) where reviving a
|
|
983
810
|
* stale `.backup` is actively harmful. See its JSDoc for selection guidance.
|
|
984
811
|
*/
|
|
985
812
|
|
|
986
|
-
// PL-prd-no-backup (W-mqub65ez0004b3bd) — PRD `.json` files must NOT auto-restore
|
|
987
|
-
// from a stale `.backup` sidecar: a removed/archived PRD coming back and
|
|
988
|
-
// re-dispatching work is the original resurrection footgun (W-mouptdh1000h9f39).
|
|
989
|
-
// Phase 10 step 4.3 — BROADENED from prd/archive/ to ALL prd/*.json (root +
|
|
990
|
-
// archive). The root-level backup lifecycle previously survived ONLY to recover
|
|
991
|
-
// the enforceDeclaredPlanProject rename-race (a concurrent sweep deleting a
|
|
992
|
-
// just-renamed PRD); that rename is now retired and SQL is the canonical store,
|
|
993
|
-
// so the sidecar is pure resurrection fuel with no remaining benefit:
|
|
994
|
-
// - prd/*.json AND prd/archive/*.json → no backup write + no safeJson restore.
|
|
995
|
-
// Matches a single filename segment under prd/ (optionally one archive/ level),
|
|
996
|
-
// so nested non-PRD paths and plans/ (which are .md) are unaffected.
|
|
997
|
-
const _NO_BACKUP_JSON_RE = /(?:^|[\\/])prd[\\/](?:archive[\\/])?[^\\/]+\.json$/i;
|
|
998
|
-
function _isNoBackupJsonPath(p) {
|
|
999
|
-
return typeof p === 'string' && _NO_BACKUP_JSON_RE.test(p);
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
813
|
function safeJson(p) {
|
|
1003
|
-
// Internal opt-out (positional second arg from mutateJsonFileLocked):
|
|
1004
|
-
// when truthy, skip the SQL-routing shim and do a raw disk read. Used
|
|
1005
|
-
// when the caller already holds the file lock and needs the on-disk
|
|
1006
|
-
// bytes as the source of truth (so the read+write pair is atomic).
|
|
1007
|
-
const skipSqlRouting = arguments.length > 1 && arguments[1] && arguments[1].skipSqlRouting;
|
|
1008
|
-
if (!skipSqlRouting) {
|
|
1009
|
-
const prdStore = require('./prd-store');
|
|
1010
|
-
if (prdStore.parsePrdPath(p)) return prdStore.readPrd(p);
|
|
1011
|
-
const routed = _routeJsonReadToSql(p);
|
|
1012
|
-
if (routed) return routed.value;
|
|
1013
|
-
}
|
|
1014
814
|
// Split the read from the parse so we can distinguish "file missing" (normal
|
|
1015
815
|
// pre-create state — silent) from "file present but corrupt JSON" (real
|
|
1016
816
|
// integrity failure — must log). Without this split a `JSON.parse(read)` in
|
|
@@ -1034,8 +834,6 @@ function safeJson(p) {
|
|
|
1034
834
|
console.error(`[safeJson] parse failure for ${path.basename(p)}: ${parseErr.message}`);
|
|
1035
835
|
}
|
|
1036
836
|
}
|
|
1037
|
-
// Archived PRDs (prd/archive/) are permanently gone — skip .backup restore.
|
|
1038
|
-
if (_isNoBackupJsonPath(p)) return null;
|
|
1039
837
|
// Primary missing or corrupted — try restoring from .backup sidecar.
|
|
1040
838
|
const backupPath = p + '.backup';
|
|
1041
839
|
try {
|
|
@@ -1070,23 +868,16 @@ function safeJsonArr(p) { return safeJson(p) || []; }
|
|
|
1070
868
|
|
|
1071
869
|
/**
|
|
1072
870
|
* Sibling of safeJson for terminal-artifact and "missing == gone" reads
|
|
1073
|
-
* (
|
|
1074
|
-
*
|
|
871
|
+
* (archived plans and any other data where a missing primary should not
|
|
872
|
+
* auto-restore from a stale
|
|
1075
873
|
* `.backup` sidecar). Returns the parsed JSON on success, or `defaultValue`
|
|
1076
874
|
* (default `null`) on **any** failure: missing file, unparseable JSON, or
|
|
1077
875
|
* IO error. The `.backup` sidecar is never consulted.
|
|
1078
876
|
*
|
|
1079
877
|
* Why a separate primitive: safeJson's restore-on-miss is correct for live
|
|
1080
|
-
*
|
|
878
|
+
* file-backed data, but
|
|
1081
879
|
* actively harmful for terminal artifacts. Examples of misuse and the bugs
|
|
1082
880
|
* they hide:
|
|
1083
|
-
* - Archived PRDs leave a `.backup` sidecar in `prd/`; reading the active
|
|
1084
|
-
* path with safeJson silently restores it and the dashboard sees a
|
|
1085
|
-
* phantom "active" PRD (W-mouptdh1000h9f39). PRDs are end-state — no
|
|
1086
|
-
* resurrection.
|
|
1087
|
-
* - Cooldowns are time-bounded ephemeral state (24h TTL). Restoring a
|
|
1088
|
-
* stale `cooldowns.json.backup` could resurrect expired entries that
|
|
1089
|
-
* should already have been pruned, suppressing legitimate dispatches.
|
|
1090
881
|
* - Restoring corrupt-primary scenarios from `.backup` masks the underlying
|
|
1091
882
|
* write integrity failure and breaks State Integrity tests.
|
|
1092
883
|
*
|
|
@@ -1105,10 +896,6 @@ function safeJsonArr(p) { return safeJson(p) || []; }
|
|
|
1105
896
|
* @returns {*} Parsed JSON on success, otherwise `defaultValue`.
|
|
1106
897
|
*/
|
|
1107
898
|
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;
|
|
1112
899
|
let raw;
|
|
1113
900
|
try {
|
|
1114
901
|
raw = fs.readFileSync(p, 'utf8');
|
|
@@ -1135,12 +922,6 @@ function safeJsonNoRestore(p, defaultValue = null) {
|
|
|
1135
922
|
let _tmpCounter = 0;
|
|
1136
923
|
|
|
1137
924
|
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
|
-
}
|
|
1144
925
|
const dir = path.dirname(p);
|
|
1145
926
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
1146
927
|
const content = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
@@ -1235,7 +1016,7 @@ function createDispatchTmpDir(dispatchId) {
|
|
|
1235
1016
|
}
|
|
1236
1017
|
|
|
1237
1018
|
// Validate a tmpDir path before destructive ops (rmSync). Defends against a
|
|
1238
|
-
// corrupted/poisoned dispatch
|
|
1019
|
+
// corrupted/poisoned dispatch `entry.tmpDir` field pointing at an
|
|
1239
1020
|
// arbitrary directory: must resolve under <MINIONS_DIR>/engine/tmp/, basename
|
|
1240
1021
|
// must match `dispatch-*`, and lstat must be a real directory (not a symlink).
|
|
1241
1022
|
function validateDispatchTmpDir(dirPath) {
|
|
@@ -1348,42 +1129,10 @@ function forEachPidFile(callback) {
|
|
|
1348
1129
|
}
|
|
1349
1130
|
}
|
|
1350
1131
|
|
|
1351
|
-
function neutralizeJsonBackupSidecar(filePath, inertData = { status: 'archived' }) {
|
|
1352
|
-
const backupPath = filePath + '.backup';
|
|
1353
|
-
try {
|
|
1354
|
-
// Retry the unlink — on Windows a transient EPERM/EBUSY (AV scanner, lagging
|
|
1355
|
-
// OS handle, indexer) otherwise leaves the .backup sidecar in place, and the
|
|
1356
|
-
// next by-name safeJson() resurrects the just-deleted PRD from it. _retryFsOp
|
|
1357
|
-
// rethrows non-retryable codes (e.g. ENOENT) immediately, so the absent path
|
|
1358
|
-
// below still fires for an already-missing sidecar.
|
|
1359
|
-
_retryFsOp(() => fs.unlinkSync(backupPath), `neutralize backup ${path.basename(backupPath)}`);
|
|
1360
|
-
return { ok: true, action: 'removed', backupPath };
|
|
1361
|
-
} catch (unlinkErr) {
|
|
1362
|
-
if (unlinkErr.code === 'ENOENT') return { ok: true, action: 'absent', backupPath };
|
|
1363
|
-
try {
|
|
1364
|
-
safeWrite(backupPath, inertData);
|
|
1365
|
-
return {
|
|
1366
|
-
ok: true,
|
|
1367
|
-
action: 'neutralized',
|
|
1368
|
-
backupPath,
|
|
1369
|
-
unlinkError: unlinkErr.message,
|
|
1370
|
-
};
|
|
1371
|
-
} catch (writeErr) {
|
|
1372
|
-
return {
|
|
1373
|
-
ok: false,
|
|
1374
|
-
action: 'failed',
|
|
1375
|
-
backupPath,
|
|
1376
|
-
unlinkError: unlinkErr.message,
|
|
1377
|
-
writeError: writeErr.message,
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
1132
|
// ── Dispatch Prompt Sidecar (#1167) ─────────────────────────────────────────
|
|
1384
1133
|
// Large prompts (PR diffs, build error logs, coalesced human feedback) inlined
|
|
1385
|
-
// into dispatch
|
|
1386
|
-
//
|
|
1134
|
+
// into dispatch state caused hundreds-of-MB bloat per entry and eventual V8 OOM.
|
|
1135
|
+
// Sidecar files keep dispatch rows small while preserving full
|
|
1387
1136
|
// content for the agent at spawn time.
|
|
1388
1137
|
|
|
1389
1138
|
// Resolve lazily so MINIONS_TEST_DIR overrides work in tests.
|
|
@@ -1441,7 +1190,7 @@ function sidecarDispatchPrompt(item, thresholdBytes) {
|
|
|
1441
1190
|
/**
|
|
1442
1191
|
* Read the effective prompt for a dispatch item. Prefers the sidecar file when
|
|
1443
1192
|
* `_promptFile` is set so spawnAgent always sees the full prompt even though
|
|
1444
|
-
* dispatch
|
|
1193
|
+
* dispatch state only stores a small stub.
|
|
1445
1194
|
*/
|
|
1446
1195
|
function resolveDispatchPrompt(item) {
|
|
1447
1196
|
if (!item) return '';
|
|
@@ -1472,13 +1221,6 @@ function deleteDispatchPromptSidecar(item) {
|
|
|
1472
1221
|
for (const p of paths) safeUnlink(p);
|
|
1473
1222
|
}
|
|
1474
1223
|
|
|
1475
|
-
/**
|
|
1476
|
-
* Startup guard: throw a clear error when a state file has grown past
|
|
1477
|
-
* maxStateFileBytes. Without this the dashboard silently OOMs on JSON.parse
|
|
1478
|
-
* (seen on a 491 MB dispatch.json + 509 MB cooldowns.json — #1167).
|
|
1479
|
-
* The thrown error points at the bloated file so operators can act instead
|
|
1480
|
-
* of chasing V8 heap traces.
|
|
1481
|
-
*/
|
|
1482
1224
|
// ── Stop-intent flag (engine/stop-intent.json) ──────────────────────────────
|
|
1483
1225
|
//
|
|
1484
1226
|
// File-presence signal used by the external supervisor (engine/supervisor.js)
|
|
@@ -1517,25 +1259,6 @@ function isStopIntentSet() {
|
|
|
1517
1259
|
return fs.existsSync(STOP_INTENT_PATH);
|
|
1518
1260
|
}
|
|
1519
1261
|
|
|
1520
|
-
function assertStateFileSize(filePath, maxBytes) {
|
|
1521
|
-
const limit = Number(maxBytes) > 0 ? Number(maxBytes) : ENGINE_DEFAULTS.maxStateFileBytes;
|
|
1522
|
-
try {
|
|
1523
|
-
const stat = fs.statSync(filePath);
|
|
1524
|
-
if (stat.size > limit) {
|
|
1525
|
-
throw new Error(
|
|
1526
|
-
`State file too large: ${filePath} is ${Math.round(stat.size / (1024 * 1024))} MB ` +
|
|
1527
|
-
`(limit ${Math.round(limit / (1024 * 1024))} MB). ` +
|
|
1528
|
-
`This usually means dispatch prompts or cooldown contexts were inlined and not sidecarred. ` +
|
|
1529
|
-
`Inspect/trim the file manually, then restart. See engine/contexts/ for sidecar files.`
|
|
1530
|
-
);
|
|
1531
|
-
}
|
|
1532
|
-
} catch (e) {
|
|
1533
|
-
if (e.code === 'ENOENT') return; // file absent is fine
|
|
1534
|
-
if (e.message && e.message.startsWith('State file too large:')) throw e;
|
|
1535
|
-
// Other stat errors (permission etc.) — do not block startup
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
1262
|
function sleepMs(ms) {
|
|
1540
1263
|
try {
|
|
1541
1264
|
const ab = new SharedArrayBuffer(4);
|
|
@@ -1799,127 +1522,28 @@ function withFileLock(lockPath, fn, {
|
|
|
1799
1522
|
throw lastErr;
|
|
1800
1523
|
}
|
|
1801
1524
|
|
|
1802
|
-
// Route table for small-state files migrated to SQL stores. Each entry
|
|
1803
|
-
// declares the SQL store function to invoke and the expected default JSON
|
|
1804
|
-
// shape (for the "ensure file exists" fallback below).
|
|
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' },
|
|
1820
|
-
};
|
|
1821
|
-
|
|
1822
|
-
function _tryRouteMutateToSql(filePath, mutateFn, onWrote) {
|
|
1823
|
-
const baseName = path.basename(filePath);
|
|
1824
|
-
const stateRoute = _STATE_MUTATE_ROUTES[baseName];
|
|
1825
|
-
if (baseName !== 'work-items.json' && baseName !== 'pull-requests.json' && !stateRoute) return null;
|
|
1826
|
-
const fpNorm = String(filePath).replace(/\\/g, '/');
|
|
1827
|
-
const minionsNorm = String(MINIONS_DIR).replace(/\\/g, '/');
|
|
1828
|
-
const insideMinionsDir = fpNorm.startsWith(minionsNorm + '/') || fpNorm === minionsNorm + '/' + baseName;
|
|
1829
|
-
if (!insideMinionsDir) return null;
|
|
1830
|
-
|
|
1831
|
-
// Small-state files live exclusively under <MINIONS_DIR>/engine/<baseName>.
|
|
1832
|
-
// Don't hijack writes to ad-hoc paths that happen to share the basename.
|
|
1833
|
-
if (stateRoute) {
|
|
1834
|
-
if (!fpNorm.endsWith('/engine/' + baseName)) return null;
|
|
1835
|
-
const store = require(stateRoute.module);
|
|
1836
|
-
try {
|
|
1837
|
-
require('./db').getDb();
|
|
1838
|
-
} catch (e) {
|
|
1839
|
-
throw new Error(`small-state-store: SQLite unavailable — ${e.message}`);
|
|
1840
|
-
}
|
|
1841
|
-
const out = store[stateRoute.fn]((data) => {
|
|
1842
|
-
const next = mutateFn(data);
|
|
1843
|
-
if (onWrote) onWrote();
|
|
1844
|
-
return next;
|
|
1845
|
-
});
|
|
1846
|
-
const result = out && Object.prototype.hasOwnProperty.call(out, 'result') ? out.result : undefined;
|
|
1847
|
-
return { routed: result };
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
let result;
|
|
1851
|
-
if (baseName === 'work-items.json') {
|
|
1852
|
-
result = mutateWorkItems(filePath, (arr) => {
|
|
1853
|
-
const out = mutateFn(arr);
|
|
1854
|
-
if (onWrote) onWrote();
|
|
1855
|
-
return out;
|
|
1856
|
-
});
|
|
1857
|
-
} else {
|
|
1858
|
-
// pull-requests.json: preserve legacy-ID normalization the JSON path
|
|
1859
|
-
// applied before invoking the mutator (parity with mutateJsonFileLocked
|
|
1860
|
-
// body at the pull-requests.json normalization site).
|
|
1861
|
-
const project = resolveProjectForPrPath(filePath);
|
|
1862
|
-
result = mutatePullRequests(filePath, (arr) => {
|
|
1863
|
-
if (Array.isArray(arr)) normalizePrRecords(arr, project);
|
|
1864
|
-
const out = mutateFn(arr);
|
|
1865
|
-
if (onWrote) onWrote();
|
|
1866
|
-
return out;
|
|
1867
|
-
});
|
|
1868
|
-
}
|
|
1869
|
-
return { routed: result };
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
1525
|
function mutateJsonFileLocked(filePath, mutateFn, {
|
|
1873
1526
|
defaultValue = {},
|
|
1874
1527
|
lockRetries,
|
|
1875
1528
|
lockRetryBackoffMs,
|
|
1876
1529
|
skipWriteIfUnchanged = false,
|
|
1877
1530
|
onWrote = null,
|
|
1878
|
-
_skipSqlRouting = false,
|
|
1879
1531
|
} = {}) {
|
|
1880
1532
|
const lockPath = `${filePath}.lock`;
|
|
1881
1533
|
const retries = lockRetries ?? ENGINE_DEFAULTS.lockRetries;
|
|
1882
1534
|
const retryBackoffMs = lockRetryBackoffMs ?? ENGINE_DEFAULTS.lockRetryBackoffMs;
|
|
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.
|
|
1888
|
-
if (!_skipSqlRouting) {
|
|
1889
|
-
const routed = _tryRouteMutateToSql(filePath, mutateFn, onWrote);
|
|
1890
|
-
if (routed) return routed.routed;
|
|
1891
|
-
}
|
|
1892
1535
|
return withFileLock(lockPath, () => {
|
|
1893
1536
|
const fileExists = fs.existsSync(filePath);
|
|
1894
|
-
let data = safeJson(filePath
|
|
1537
|
+
let data = safeJson(filePath);
|
|
1895
1538
|
const parsedInvalid = fileExists && data === null;
|
|
1896
1539
|
if (data === null || typeof data !== 'object') data = Array.isArray(defaultValue) ? [...defaultValue] : { ...defaultValue };
|
|
1897
|
-
// Normalize BEFORE taking the baseline snapshot so that both `beforeSerialized`
|
|
1898
|
-
// and the post-mutator snapshot reflect post-normalize state. Capturing the
|
|
1899
|
-
// baseline before normalize breaks the `skipWriteIfUnchanged` optimization for
|
|
1900
|
-
// pull-requests.json files: a no-op mutator on a denormalized file would
|
|
1901
|
-
// always trip the write path because normalization itself shifted serialized
|
|
1902
|
-
// bytes between the two snapshots (P-bfa1c-skipwrite-timing). The trade-off
|
|
1903
|
-
// is intentional: when normalization is the ONLY change, we deliberately
|
|
1904
|
-
// leave the on-disk file denormalized — readers re-run normalizePrRecords on
|
|
1905
|
-
// load (see getPrLinks, engine/queries.js:670-674), so the in-memory contract
|
|
1906
|
-
// is preserved without the per-poll mtime bump.
|
|
1907
|
-
if (path.basename(filePath) === 'pull-requests.json' && Array.isArray(data)) {
|
|
1908
|
-
normalizePrRecords(data, resolveProjectForPrPath(filePath));
|
|
1909
|
-
}
|
|
1910
1540
|
const beforeSerialized = skipWriteIfUnchanged ? JSON.stringify(data) : null;
|
|
1911
1541
|
const next = mutateFn(data);
|
|
1912
1542
|
const finalData = next === undefined ? data : next;
|
|
1913
1543
|
const shouldWrite = !skipWriteIfUnchanged || parsedInvalid || JSON.stringify(finalData) !== beforeSerialized;
|
|
1914
1544
|
if (shouldWrite) {
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
// (PL-prd-no-backup, W-mqub65ez0004b3bd). Root-level prd/*.json retains
|
|
1918
|
-
// the backup lifecycle — concurrent-sweep loss must be recoverable.
|
|
1919
|
-
if (!_isNoBackupJsonPath(filePath)) {
|
|
1920
|
-
const backupPath = filePath + '.backup';
|
|
1921
|
-
try { if (fileExists) fs.copyFileSync(filePath, backupPath); } catch { /* backup is best-effort */ }
|
|
1922
|
-
}
|
|
1545
|
+
const backupPath = filePath + '.backup';
|
|
1546
|
+
try { if (fileExists) fs.copyFileSync(filePath, backupPath); } catch { /* backup is best-effort */ }
|
|
1923
1547
|
safeWrite(filePath, finalData);
|
|
1924
1548
|
// Side-effect hook fired only when an actual write happened. Callers
|
|
1925
1549
|
// use this to emit cache-invalidation signals (events table row) so
|
|
@@ -2020,33 +1644,34 @@ function recordEngineRespawn(source) {
|
|
|
2020
1644
|
return { count, crashLoop };
|
|
2021
1645
|
}
|
|
2022
1646
|
|
|
2023
|
-
// W-mp60tw0u000j3931: Lock-safe read-modify-write for engine/state.json — the
|
|
2024
|
-
// persistent cross-restart engine-level state file (migration markers,
|
|
2025
|
-
// one-shot reconciliation versions, etc.). Mirrors mutateControl's shape.
|
|
2026
1647
|
function mutateEngineState(mutator) {
|
|
2027
|
-
|
|
1648
|
+
const store = require('./small-state-store');
|
|
1649
|
+
const { result } = store.applyEngineStateMutation((data) => {
|
|
2028
1650
|
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
2029
1651
|
return mutator(data) || data;
|
|
2030
|
-
}
|
|
1652
|
+
});
|
|
1653
|
+
return result;
|
|
2031
1654
|
}
|
|
2032
1655
|
|
|
2033
1656
|
function readEngineState() {
|
|
2034
|
-
const data =
|
|
1657
|
+
const data = require('./small-state-store').readEngineState();
|
|
2035
1658
|
if (!data || typeof data !== 'object' || Array.isArray(data)) return {};
|
|
2036
1659
|
return data;
|
|
2037
1660
|
}
|
|
2038
1661
|
|
|
2039
1662
|
function mutateCooldowns(mutator) {
|
|
2040
|
-
|
|
1663
|
+
const store = require('./small-state-store');
|
|
1664
|
+
const { wrote, result } = store.applyCooldownsMutation((data) => {
|
|
2041
1665
|
if (!data || typeof data !== 'object' || Array.isArray(data)) data = {};
|
|
2042
1666
|
return mutator(data) || data;
|
|
2043
|
-
}
|
|
1667
|
+
});
|
|
1668
|
+
if (wrote) {
|
|
1669
|
+
try { require('./db-events').emitStateEvent('cooldowns'); } catch { /* optional */ }
|
|
1670
|
+
}
|
|
1671
|
+
return result;
|
|
2044
1672
|
}
|
|
2045
1673
|
|
|
2046
|
-
//
|
|
2047
|
-
// for the small JSON state files that were still going through raw safeWrite.
|
|
2048
|
-
// Pattern mirrors mutateControl / mutateEngineState / mutateCooldowns — so a
|
|
2049
|
-
// future SQL migration is a mechanical _SMALL_STATE_MUTATE_ROUTES addition.
|
|
1674
|
+
// Lock-safe wrappers for intentionally file-backed coordination state.
|
|
2050
1675
|
|
|
2051
1676
|
// KB classification checkpoint counter (consolidation.js post-classify).
|
|
2052
1677
|
function mutateKbCheckpoint(mutator) {
|
|
@@ -3351,8 +2976,8 @@ const ENGINE_DEFAULTS = {
|
|
|
3351
2976
|
// shared.resolvePollFlag is intentionally NOT used here because no
|
|
3352
2977
|
// legacy macro bundle ever covered these phases.
|
|
3353
2978
|
prDiscoveryEnabled: true, // discoverFromPrs (per-project PR-driven fix/review/test discovery)
|
|
3354
|
-
workItemsDiscoveryEnabled: true, // discoverFromWorkItems (
|
|
3355
|
-
centralWorkDiscoveryEnabled: true, // discoverCentralWorkItems (
|
|
2979
|
+
workItemsDiscoveryEnabled: true, // discoverFromWorkItems (project SQL scope)
|
|
2980
|
+
centralWorkDiscoveryEnabled: true, // discoverCentralWorkItems (central SQL scope)
|
|
3356
2981
|
scheduledWorkDiscoveryEnabled: true, // discoverScheduledWork (cron-style scheduled tasks + meetings)
|
|
3357
2982
|
planMaterializationEnabled: true, // reconcilePrdStatuses + materializePlansAsWorkItems pair
|
|
3358
2983
|
prPollStatusEvery: 72, // poll PR build/review/merge status every N ticks for both ADO and GitHub (~12 min at default 10s tick)
|
|
@@ -3394,8 +3019,8 @@ const ENGINE_DEFAULTS = {
|
|
|
3394
3019
|
prAbandonConfirmCount: 3,
|
|
3395
3020
|
// W-mp60tw0u000j3931: One-shot startup reconciliation pass for `abandoned` PRs runs
|
|
3396
3021
|
// exactly once per bump of this constant. The engine compares this value against
|
|
3397
|
-
//
|
|
3398
|
-
// is lower, every project's
|
|
3022
|
+
// the persisted lastAbandonedReconciliationVersion at boot; if the version
|
|
3023
|
+
// is lower, every project's PR scope is scanned, every abandoned PR is
|
|
3399
3024
|
// re-probed, and any false-flipped PRs (e.g. transient 404 victims from before the
|
|
3400
3025
|
// hardening in W-mp5trwh60008386d shipped) are flipped back to active/merged/closed
|
|
3401
3026
|
// based on their live API state. Bump this when reconciliation logic itself changes
|
|
@@ -3459,7 +3084,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3459
3084
|
// re-queued into review again.
|
|
3460
3085
|
preDispatchEvalMaxRejections: 5,
|
|
3461
3086
|
completionNonceRequired: false, // P-d2a8f6c1 (agent trust boundary F8): when true, a missing `nonce` field in the completion JSON hard-fails the dispatch with failure_class:'completion-nonce-mismatch'. Default false for one release so older agents/runtime caches that haven't picked up the prompt change degrade with a warning instead of breaking. Mismatched nonces hard-fail regardless of this flag. See docs/completion-reports.md → "Trust boundary".
|
|
3462
|
-
autoApplyReviewVote: false, //
|
|
3087
|
+
autoApplyReviewVote: false, // Master gate for platform vote mutations; local verdicts remain informational when false.
|
|
3463
3088
|
|
|
3464
3089
|
// ── Runtime fleet (P-3b8e5f1d) ──────────────────────────────────────────────
|
|
3465
3090
|
// Single source of truth for which CLI runtime + model every spawn uses.
|
|
@@ -3473,7 +3098,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3473
3098
|
claudeBareMode: false, // Claude --bare: suppress CLAUDE.md auto-discovery (per-agent override: agents.<id>.bareMode)
|
|
3474
3099
|
claudeFallbackModel: undefined,// Claude --fallback-model — Claude CLI honors this on rate-limit (429) only; engine retry on FAILURE_CLASS.MODEL_UNAVAILABLE keeps the flag passed so the CLI can swap internally
|
|
3475
3100
|
copilotFallbackModel: undefined,// W-mpg6isvy000xca4d: Copilot has no --fallback-model flag; engine retry on FAILURE_CLASS.MODEL_UNAVAILABLE OVERRIDES --model directly with this value (separate knob from claudeFallbackModel because model namespaces differ across runtimes)
|
|
3476
|
-
copilotDisableBuiltinMcps: true, //
|
|
3101
|
+
copilotDisableBuiltinMcps: true, // Keep github-mcp-server from bypassing Minions PR tracking.
|
|
3477
3102
|
// P-7d31a06b — Claude pre-approves workspace .mcp.json servers in ~/.claude.json on every spawn into a fresh
|
|
3478
3103
|
// worktree cwd. Without this, the first Claude invocation in a brand-new worktree shows a project-scoped MCP
|
|
3479
3104
|
// "trust this server?" prompt that blocks until a human accepts — invisible behind --dangerously-skip-permissions
|
|
@@ -3489,13 +3114,6 @@ const ENGINE_DEFAULTS = {
|
|
|
3489
3114
|
agentUseWorkerPool: false, // P-9b3d5f61: mirrors ccUseWorkerPool's shape but for the fleet agent-dispatch path (engine/agent-worker-pool.js). Opt-in, default OFF — see resolveAgentUseWorkerPool in engine/shared.js for the full priority chain and rationale.
|
|
3490
3115
|
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
|
|
3491
3116
|
disableModelDiscovery: false, // skip runtime.listModels() REST calls fleet-wide (settings UI falls back to free-text)
|
|
3492
|
-
// Phase 8 (qa-runs.json + qa-sessions.json → SQL). When true, every QA
|
|
3493
|
-
// mutation also writes the JSON sidecar at engine/qa-runs.json and
|
|
3494
|
-
// engine/qa-sessions.json for back-compat with external tooling. SQL is
|
|
3495
|
-
// always the source of truth — flip this OFF once operators trust the
|
|
3496
|
-
// SQL store and want to drop the dual-write cost. Sunset tracked in
|
|
3497
|
-
// docs/deprecated.json (qa-json-sidecars).
|
|
3498
|
-
qaDualWriteJson: true,
|
|
3499
3117
|
// W-mpmwxkrw000872ec — dashboard global font-size scale. Drives the
|
|
3500
3118
|
// [data-font-size] attribute on <html> via the inline bootstrap script in
|
|
3501
3119
|
// layout.html (localStorage fast path) and is reconciled from the server
|
|
@@ -3503,10 +3121,9 @@ const ENGINE_DEFAULTS = {
|
|
|
3503
3121
|
// 'xlarge'. Allowlist validation lives in handleSettingsUpdate so invalid
|
|
3504
3122
|
// values are clamped rather than silently breaking the bootstrap.
|
|
3505
3123
|
fontSize: 'small',
|
|
3506
|
-
maxPendingContexts: 20, // cap pendingContexts arrays
|
|
3507
|
-
maxPendingContextEntryBytes: 256 * 1024, //
|
|
3508
|
-
maxDispatchPromptBytes: 1024 * 1024, //
|
|
3509
|
-
maxStateFileBytes: 100 * 1024 * 1024, // 100 MB — fail startup with a clear error when dispatch.json / cooldowns.json exceed this, rather than silently OOMing on JSON.parse (#1167)
|
|
3124
|
+
maxPendingContexts: 20, // cap pendingContexts arrays to prevent unbounded growth
|
|
3125
|
+
maxPendingContextEntryBytes: 256 * 1024, // cap each pending context entry
|
|
3126
|
+
maxDispatchPromptBytes: 1024 * 1024, // sidecar oversized prompts under engine/contexts/
|
|
3510
3127
|
mainBranchCacheTtlMs: 300000, // 5min — cache git default-branch detection, then prune expired entries
|
|
3511
3128
|
mainBranchCacheMaxEntries: 100, // bound repo/branch detection cache in long-lived dashboard/engine processes
|
|
3512
3129
|
removeWorktreeFailureTtlMs: 24 * 60 * 60 * 1000, // stale failed paths are forgotten after a day
|
|
@@ -3565,7 +3182,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3565
3182
|
ccTurnTimeoutMs: 300000, // W-mpmwxni2000c25c7-b/-d: 5min per-turn no-progress watchdog. The window resets on every liveness signal — token chunk, tool-call notification, tool-update — so an actively-streaming CC/doc-chat turn (long shell command, deep search, sub-agent loop) survives indefinitely up to the outer CC_CALL_TIMEOUT_MS (~1h) ceiling. Only true silence past this window with no progress fires the cancel: the in-flight LLM call is aborted and the handler surfaces `{code:'cc-turn-timeout', retryable:true}` via the typed error envelope so the UI can stop the spinner and offer Retry. Clamped to [10000, 3600000] in the settings POST handler. Independent of CC_CALL_TIMEOUT_MS. Non-streaming doc-chat is the lone wall-clock exception (no progress hooks); see _raceCcDocChatTimeout in dashboard.js for the dual factory/promise shape.
|
|
3566
3183
|
docSessionMaxEntries: 200, // cap doc-chat session map/disk store by least-recent activity (LRU; sessions are non-expiring otherwise)
|
|
3567
3184
|
ccLiveStreamMaxAgeMs: 30 * 60 * 1000, // hard cap reconnect buffers if abort/cleanup stalls
|
|
3568
|
-
metricsFlushIntervalMs: 10000, // batch trackEngineUsage writes
|
|
3185
|
+
metricsFlushIntervalMs: 10000, // batch trackEngineUsage SQL writes
|
|
3569
3186
|
maxLlmRawBytes: 256 * 1024, // keep only a bounded stdout tail from direct Claude calls
|
|
3570
3187
|
maxLlmStderrBytes: 64 * 1024, // keep only a bounded stderr tail from direct Claude calls
|
|
3571
3188
|
maxLlmLineBufferBytes: 128 * 1024, // cap the incremental JSON line buffer to avoid malformed-stream OOMs
|
|
@@ -4402,7 +4019,7 @@ const PR_PENDING_REASON = {
|
|
|
4402
4019
|
MISSING_BRANCH: 'missing_pr_branch',
|
|
4403
4020
|
};
|
|
4404
4021
|
// PR build-status enum — single source of truth for the literal strings written to
|
|
4405
|
-
//
|
|
4022
|
+
// PR `buildStatus`. Previously drifted across engine/ado.js,
|
|
4406
4023
|
// engine/github.js, engine/lifecycle.js, engine/watches.js, engine/queries.js, engine/cli.js
|
|
4407
4024
|
// (P-bfa3d-constants-eslint, audit items #68-#82).
|
|
4408
4025
|
const BUILD_STATUS = {
|
|
@@ -4412,7 +4029,7 @@ const BUILD_STATUS = {
|
|
|
4412
4029
|
NONE: 'none',
|
|
4413
4030
|
};
|
|
4414
4031
|
// PR review-status enum — single source of truth for the literal strings written to
|
|
4415
|
-
//
|
|
4032
|
+
// PR `reviewStatus`. Previously drifted across engine/ado.js,
|
|
4416
4033
|
// engine/github.js, engine/lifecycle.js, engine/watches.js, engine/queries.js, engine/cli.js
|
|
4417
4034
|
// (P-bfa3d-constants-eslint, audit items #68-#82).
|
|
4418
4035
|
const REVIEW_STATUS = {
|
|
@@ -4666,14 +4283,10 @@ function trackReviewMetric(pr, newReviewStatus, config) {
|
|
|
4666
4283
|
* just a boolean caused the toast to render "queued undefined" (issue #2375).
|
|
4667
4284
|
*/
|
|
4668
4285
|
function queuePlanToPrd({ planFile, prdFile, title, description, project, createdBy, extra }) {
|
|
4669
|
-
// Use MINIONS_DIR (honors MINIONS_TEST_DIR override) instead of resolving from
|
|
4670
|
-
// __dirname — otherwise tests that exercise this helper leak work items into
|
|
4671
|
-
// the real package-root work-items.json even after createTestMinionsDir().
|
|
4672
|
-
const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4673
4286
|
let queued = false;
|
|
4674
4287
|
let id = null;
|
|
4675
4288
|
let item = null;
|
|
4676
|
-
|
|
4289
|
+
mutateWorkItems('central', items => {
|
|
4677
4290
|
if (!Array.isArray(items)) items = [];
|
|
4678
4291
|
const existing = items.find(w => w.type === WORK_TYPE.PLAN_TO_PRD && w.planFile === planFile && (w.status === WI_STATUS.PENDING || w.status === WI_STATUS.DISPATCHED));
|
|
4679
4292
|
if (existing) {
|
|
@@ -4820,7 +4433,7 @@ const FAILURE_CLASS = {
|
|
|
4820
4433
|
WORKSPACE_MANIFEST_URL: 'workspace-manifest-url-forbidden', // W-mq07avbk000m5543: out-of-scope external URL fetch. Non-retryable as-is.
|
|
4821
4434
|
SPAWN_PHASE_STALL: 'spawn-phase-stall', // W-mq0e2dae000a003d: process spawned and ran startup-only events (MCP init / hooks) but never emitted real task progress; CPU usage stayed below threshold past the grace window. Engine kills the wedged child and treats this as retryable (fresh-session) so a re-spawn can clear a transient MCP wedge.
|
|
4822
4435
|
OUTPUT_TRUNCATED: 'output-truncated', // P-8e4c2a17: the agent streamed more stdout than the engine's hard capture cap (engine.js AGENT_OUTPUT_CAP_BYTES, 1MB) BEFORE the terminal `result` event arrived. The result (session id, completion block, final text) lives at the END of the stream, so it fell outside the captured window and parseOutput found nothing — the dispatch would otherwise fail as an opaque UNKNOWN and retry to death. Surfaced loudly with an actionable message; non-retryable (mechanical retry just reproduces the overflow — the agent must reduce output volume or the task must be split).
|
|
4823
|
-
VERIFY_MISSING_PR: 'verify-missing-pr', //
|
|
4436
|
+
VERIFY_MISSING_PR: 'verify-missing-pr', // Verify completed without an attached or tracked PR.
|
|
4824
4437
|
PROJECT_NOT_FOUND: 'project_not_found', // W-mqv2wsy30002e090: a work item's `project` field names a project that is not configured (resolveConfiguredProject / formatUnknownProjectError → `Project "<name>" not found. Known projects: …`). Stamped at discovery time so the dashboard PR-column renderer surfaces the red failReason snippet like other non-retryable failures instead of burying it in the Agent column. Non-retryable — operator must fix the WI's project field or configure the project.
|
|
4825
4438
|
PRE_DISPATCH_EVAL_STUCK: 'pre-dispatch-eval-stuck', // W-mrcrh5hj0017d22f: pre-dispatch-eval rejected the SAME (unchanged) description ENGINE_DEFAULTS.preDispatchEvalMaxRejections times in a row without the WI status ever leaving pending — previously this burned an LLM validation call every discovery tick forever with nothing surfaced to the dashboard (constant-bug-bash's file-fixes stage wedged 16+ consecutive rejections, one/minute, until a human manually cancelled it). Engine now flips the WI to failed with this class so the stalled item is visible and stops silently re-evaluating; non-retryable until the description changes.
|
|
4826
4439
|
UNKNOWN: 'unknown', // Unclassified failure
|
|
@@ -4857,9 +4470,8 @@ const DEFAULT_CLAUDE = {
|
|
|
4857
4470
|
// engine/cleanup.js:1200-1213 _engine.legacyStatusMigrations.
|
|
4858
4471
|
function _bumpPruneDefaultClaudeStrip() {
|
|
4859
4472
|
try {
|
|
4860
|
-
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
4861
4473
|
const dateKey = new Date().toISOString().slice(0, 10);
|
|
4862
|
-
|
|
4474
|
+
mutateMetrics((metrics) => {
|
|
4863
4475
|
metrics = metrics || {};
|
|
4864
4476
|
if (!metrics._engine) metrics._engine = {};
|
|
4865
4477
|
if (!metrics._engine.pruneDefaultClaudeConfigStrips) metrics._engine.pruneDefaultClaudeConfigStrips = {};
|
|
@@ -5168,18 +4780,9 @@ function resolveConfiguredProject(projectName, projectsOrConfig, options = {}) {
|
|
|
5168
4780
|
return { project: null, explicit: false, value: '' };
|
|
5169
4781
|
}
|
|
5170
4782
|
|
|
5171
|
-
function centralWorkItemsPath(minionsDir = MINIONS_DIR) {
|
|
5172
|
-
return path.join(minionsDir, 'work-items.json');
|
|
5173
|
-
}
|
|
5174
|
-
|
|
5175
|
-
function centralPullRequestsPath(minionsDir = MINIONS_DIR) {
|
|
5176
|
-
return path.join(minionsDir, 'pull-requests.json');
|
|
5177
|
-
}
|
|
5178
|
-
|
|
5179
4783
|
function _projectSourceRawValue(source) {
|
|
5180
4784
|
if (source && typeof source === 'object') {
|
|
5181
4785
|
return source.name ?? source.project ?? source._project ?? source.source ?? source._source ??
|
|
5182
|
-
source.wiPath ?? source.workItemsPath ?? source.prPath ?? source.pullRequestsPath ??
|
|
5183
4786
|
source.stateDir ?? source.path ?? source.localPath ?? '';
|
|
5184
4787
|
}
|
|
5185
4788
|
return source;
|
|
@@ -5193,18 +4796,15 @@ function _sameSourcePath(value, targetPath, minionsDir = MINIONS_DIR) {
|
|
|
5193
4796
|
}
|
|
5194
4797
|
|
|
5195
4798
|
function _projectSourceDescriptor(project, value, explicit, minionsDir = MINIONS_DIR) {
|
|
5196
|
-
const wiPath = project ? projectWorkItemsPath(project) : centralWorkItemsPath(minionsDir);
|
|
5197
|
-
const prPath = project ? projectPrPath(project) : centralPullRequestsPath(minionsDir);
|
|
5198
4799
|
const stateDir = project ? projectStateDir(project) : minionsDir;
|
|
5199
4800
|
return {
|
|
5200
4801
|
project: project || null,
|
|
5201
4802
|
explicit: !!explicit,
|
|
5202
4803
|
value: value || '',
|
|
5203
4804
|
sourceName: project?.name || 'central',
|
|
4805
|
+
scope: project?.name || 'central',
|
|
5204
4806
|
isCentral: !project,
|
|
5205
4807
|
stateDir,
|
|
5206
|
-
wiPath,
|
|
5207
|
-
prPath,
|
|
5208
4808
|
};
|
|
5209
4809
|
}
|
|
5210
4810
|
|
|
@@ -5221,17 +4821,13 @@ function resolveProjectSource(source, projectsOrConfig, options = {}) {
|
|
|
5221
4821
|
return _projectSourceDescriptor(projects[0], '', false, minionsDir);
|
|
5222
4822
|
}
|
|
5223
4823
|
if (allowCentral) return _projectSourceDescriptor(null, '', false, minionsDir);
|
|
5224
|
-
return { project: null, explicit: false, value: '', sourceName: '', isCentral: false,
|
|
4824
|
+
return { project: null, explicit: false, value: '', sourceName: '', isCentral: false, scope: null, stateDir: null };
|
|
5225
4825
|
}
|
|
5226
4826
|
|
|
5227
|
-
const centralWorkPath = centralWorkItemsPath(minionsDir);
|
|
5228
|
-
const centralPrPath = centralPullRequestsPath(minionsDir);
|
|
5229
4827
|
const centralNames = new Set(['central', 'root']);
|
|
5230
4828
|
const lowerValue = value.toLowerCase();
|
|
5231
4829
|
const isCentral = centralNames.has(lowerValue) ||
|
|
5232
|
-
_sameSourcePath(value, minionsDir, minionsDir)
|
|
5233
|
-
_sameSourcePath(value, centralWorkPath, minionsDir) ||
|
|
5234
|
-
_sameSourcePath(value, centralPrPath, minionsDir);
|
|
4830
|
+
_sameSourcePath(value, minionsDir, minionsDir);
|
|
5235
4831
|
if (isCentral) {
|
|
5236
4832
|
if (allowCentral) return _projectSourceDescriptor(null, value, true, minionsDir);
|
|
5237
4833
|
return {
|
|
@@ -5240,8 +4836,7 @@ function resolveProjectSource(source, projectsOrConfig, options = {}) {
|
|
|
5240
4836
|
value,
|
|
5241
4837
|
sourceName: '',
|
|
5242
4838
|
isCentral: false,
|
|
5243
|
-
|
|
5244
|
-
prPath: null,
|
|
4839
|
+
scope: null,
|
|
5245
4840
|
stateDir: null,
|
|
5246
4841
|
error: 'central source is not allowed here',
|
|
5247
4842
|
};
|
|
@@ -5255,8 +4850,6 @@ function resolveProjectSource(source, projectsOrConfig, options = {}) {
|
|
|
5255
4850
|
const candidates = [
|
|
5256
4851
|
project.localPath,
|
|
5257
4852
|
projectStateDir(project),
|
|
5258
|
-
projectWorkItemsPath(project),
|
|
5259
|
-
projectPrPath(project),
|
|
5260
4853
|
].filter(Boolean);
|
|
5261
4854
|
if (candidates.some(candidate => _sameSourcePath(value, candidate, minionsDir))) {
|
|
5262
4855
|
return _projectSourceDescriptor(project, value, explicit, minionsDir);
|
|
@@ -5269,8 +4862,7 @@ function resolveProjectSource(source, projectsOrConfig, options = {}) {
|
|
|
5269
4862
|
value,
|
|
5270
4863
|
sourceName: '',
|
|
5271
4864
|
isCentral: false,
|
|
5272
|
-
|
|
5273
|
-
prPath: null,
|
|
4865
|
+
scope: null,
|
|
5274
4866
|
stateDir: null,
|
|
5275
4867
|
error: formatUnknownProjectError(value, projects),
|
|
5276
4868
|
};
|
|
@@ -5280,15 +4872,8 @@ function projectRoot(project) {
|
|
|
5280
4872
|
return path.resolve(project.localPath);
|
|
5281
4873
|
}
|
|
5282
4874
|
|
|
5283
|
-
//
|
|
5284
|
-
//
|
|
5285
|
-
//
|
|
5286
|
-
// projectStateDir is path-only (no fs side effects) — safe to call with stale
|
|
5287
|
-
// project references after `removeProject` archived the data dir. Write paths
|
|
5288
|
-
// (safeWrite, withFileLock, mutateJsonFileLocked) already mkdir the parent dir
|
|
5289
|
-
// at write time, so the dir is created lazily only when something is actually
|
|
5290
|
-
// written. Use projectStateDirEnsure() when a caller specifically needs the
|
|
5291
|
-
// directory to exist before doing its own fs ops.
|
|
4875
|
+
// Project-local definitions and artifacts live under projects/{name}/.
|
|
4876
|
+
// Runtime records use the project's SQL scope.
|
|
5292
4877
|
function projectStateDir(project) {
|
|
5293
4878
|
const name = project.name || path.basename(project.localPath);
|
|
5294
4879
|
return path.join(MINIONS_DIR, 'projects', name);
|
|
@@ -5302,14 +4887,6 @@ function projectStateDirEnsure(project) {
|
|
|
5302
4887
|
return dir;
|
|
5303
4888
|
}
|
|
5304
4889
|
|
|
5305
|
-
function projectWorkItemsPath(project) {
|
|
5306
|
-
return path.join(projectStateDir(project), 'work-items.json');
|
|
5307
|
-
}
|
|
5308
|
-
|
|
5309
|
-
function projectPrPath(project) {
|
|
5310
|
-
return path.join(projectStateDir(project), 'pull-requests.json');
|
|
5311
|
-
}
|
|
5312
|
-
|
|
5313
4890
|
function sameResolvedPath(a, b) {
|
|
5314
4891
|
if (!a || !b) return false;
|
|
5315
4892
|
try {
|
|
@@ -5321,7 +4898,7 @@ function sameResolvedPath(a, b) {
|
|
|
5321
4898
|
}
|
|
5322
4899
|
}
|
|
5323
4900
|
|
|
5324
|
-
function
|
|
4901
|
+
function initializeProjectState(project) {
|
|
5325
4902
|
const result = { created: [] };
|
|
5326
4903
|
projectStateDirEnsure(project);
|
|
5327
4904
|
// SQL scopes need no empty sentinel rows. Touch both stores so migrations
|
|
@@ -5355,33 +4932,6 @@ function realPathForComparison(filePath) {
|
|
|
5355
4932
|
}
|
|
5356
4933
|
}
|
|
5357
4934
|
|
|
5358
|
-
function prPathComparisonCandidates(filePath) {
|
|
5359
|
-
const candidates = new Set();
|
|
5360
|
-
const addCandidate = (candidate) => {
|
|
5361
|
-
const resolved = path.resolve(candidate);
|
|
5362
|
-
candidates.add(resolved);
|
|
5363
|
-
candidates.add(realPathForComparison(resolved));
|
|
5364
|
-
};
|
|
5365
|
-
|
|
5366
|
-
addCandidate(filePath);
|
|
5367
|
-
if (!path.isAbsolute(filePath)) {
|
|
5368
|
-
addCandidate(path.resolve(MINIONS_DIR, filePath));
|
|
5369
|
-
}
|
|
5370
|
-
return candidates;
|
|
5371
|
-
}
|
|
5372
|
-
|
|
5373
|
-
function resolveProjectForPrPath(filePath, config = null) {
|
|
5374
|
-
const fileCandidates = prPathComparisonCandidates(filePath);
|
|
5375
|
-
const projects = getProjects(config);
|
|
5376
|
-
for (const project of projects) {
|
|
5377
|
-
for (const projectPath of prPathComparisonCandidates(projectPrPath(project))) {
|
|
5378
|
-
if (fileCandidates.has(projectPath)) return project;
|
|
5379
|
-
}
|
|
5380
|
-
}
|
|
5381
|
-
if (projects.length === 1) return projects[0];
|
|
5382
|
-
return null;
|
|
5383
|
-
}
|
|
5384
|
-
|
|
5385
4935
|
// ── ID Generation ────────────────────────────────────────────────────────────
|
|
5386
4936
|
|
|
5387
4937
|
function nextWorkItemId(items, prefix) {
|
|
@@ -5421,7 +4971,7 @@ function getAdoOrgBase(project) {
|
|
|
5421
4971
|
*
|
|
5422
4972
|
* - "basenames": exact relative paths under the live root (engine.js, dashboard.js,
|
|
5423
4973
|
* minions.js, config.json — and the runtime state files engine/control.json
|
|
5424
|
-
* and engine/
|
|
4974
|
+
* and engine/control.json).
|
|
5425
4975
|
* - "globs": direct-child JS files under protected live directories
|
|
5426
4976
|
* (engine/*.js, bin/*.js).
|
|
5427
4977
|
* - "prefixes": relative directory prefixes whose entire subtree is read-only
|
|
@@ -5443,7 +4993,6 @@ const _CC_PROTECTED_BASENAMES = Object.freeze([
|
|
|
5443
4993
|
'minions.js',
|
|
5444
4994
|
'config.json',
|
|
5445
4995
|
'engine/control.json',
|
|
5446
|
-
'engine/dispatch.json',
|
|
5447
4996
|
]);
|
|
5448
4997
|
const _CC_PROTECTED_FILE_GLOBS = Object.freeze([
|
|
5449
4998
|
'engine/*.js',
|
|
@@ -6581,8 +6130,8 @@ function extractStructuredWorkItemPrRef(item) {
|
|
|
6581
6130
|
// W-mq1j85cj00055a8f — when an inbox note (notes/inbox/<name>) leaves the
|
|
6582
6131
|
// inbox (persisted to notes.md, promoted to knowledge/, or auto-archived by
|
|
6583
6132
|
// consolidation), any work-item reference pointing at the old inbox path
|
|
6584
|
-
// turns into a broken link. Rewrite those references across every
|
|
6585
|
-
//
|
|
6133
|
+
// turns into a broken link. Rewrite those references across every work-item
|
|
6134
|
+
// scope to the new canonical location.
|
|
6586
6135
|
//
|
|
6587
6136
|
// Match semantics (intentionally narrow per the WI scope):
|
|
6588
6137
|
// - item.references[]: string 'notes/inbox/<inboxName>' (anchored on start
|
|
@@ -6600,15 +6149,14 @@ function extractStructuredWorkItemPrRef(item) {
|
|
|
6600
6149
|
// - Trailing `?query` or `#fragment` is tolerated; substring overlaps in
|
|
6601
6150
|
// unrelated path segments (e.g. .../notes/inbox/foobar for foo.md) are
|
|
6602
6151
|
// NOT rewritten.
|
|
6603
|
-
// - Archived work
|
|
6604
|
-
// surfaced by getProjects() + the central path are touched.
|
|
6152
|
+
// - Archived work items are skipped.
|
|
6605
6153
|
//
|
|
6606
6154
|
// Idempotent: re-running with the same inboxName after the rewrite is a
|
|
6607
6155
|
// no-op (the references already read newLocation, which won't match the
|
|
6608
6156
|
// notes/inbox/<inboxName> regex).
|
|
6609
6157
|
//
|
|
6610
|
-
// Returns the count of references rewritten across all
|
|
6611
|
-
// Each
|
|
6158
|
+
// Returns the count of references rewritten across all scopes (for logging).
|
|
6159
|
+
// Each scope's mutate is wrapped in try/catch so one corrupt project can't
|
|
6612
6160
|
// block the others.
|
|
6613
6161
|
//
|
|
6614
6162
|
// `opts._mutate` is an undocumented test seam — production callers always
|
|
@@ -6645,14 +6193,14 @@ function rewriteInboxRefsAcrossProjects(inboxName, newLocation, opts = {}) {
|
|
|
6645
6193
|
const candidates = [];
|
|
6646
6194
|
try {
|
|
6647
6195
|
for (const project of getProjects(config)) {
|
|
6648
|
-
|
|
6196
|
+
if (project?.name) candidates.push(project);
|
|
6649
6197
|
}
|
|
6650
6198
|
} catch { /* getProjects failed — fall back to central only */ }
|
|
6651
|
-
candidates.push(
|
|
6199
|
+
candidates.push('central');
|
|
6652
6200
|
|
|
6653
|
-
for (const
|
|
6201
|
+
for (const scope of candidates) {
|
|
6654
6202
|
try {
|
|
6655
|
-
_mutate(
|
|
6203
|
+
_mutate(scope, items => {
|
|
6656
6204
|
if (!Array.isArray(items)) return items;
|
|
6657
6205
|
for (const item of items) {
|
|
6658
6206
|
if (!item) continue;
|
|
@@ -6709,7 +6257,7 @@ function rewriteInboxRefsAcrossProjects(inboxName, newLocation, opts = {}) {
|
|
|
6709
6257
|
return items;
|
|
6710
6258
|
});
|
|
6711
6259
|
} catch (e) {
|
|
6712
|
-
try { console.warn(`rewriteInboxRefsAcrossProjects(${
|
|
6260
|
+
try { console.warn(`rewriteInboxRefsAcrossProjects(${stateScope(scope)}): ${e.message}`); } catch { /* logging best-effort */ }
|
|
6713
6261
|
}
|
|
6714
6262
|
}
|
|
6715
6263
|
return rewritten;
|
|
@@ -7347,18 +6895,14 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
|
|
|
7347
6895
|
links[effectivePrId] = [...mergedCurrent];
|
|
7348
6896
|
return links;
|
|
7349
6897
|
};
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
mutateJsonFileLocked(PR_LINKS_PATH, mutator, { defaultValue: {} });
|
|
6898
|
+
const { wrote } = require('./small-state-store').applyPrLinksMutation(mutator);
|
|
6899
|
+
if (wrote) {
|
|
6900
|
+
try { require('./db-events').emitStateEvent('pr_links'); } catch { /* optional */ }
|
|
6901
|
+
}
|
|
7355
6902
|
|
|
7356
6903
|
if (!project) return;
|
|
7357
|
-
const prPath = projectPrPath(project);
|
|
7358
6904
|
const effectivePrNumber = getPrNumber(prNumber ?? effectivePrId);
|
|
7359
|
-
|
|
7360
|
-
// so SQL stays canonical; the JSON mirror is regenerated by the store.
|
|
7361
|
-
mutatePullRequests(prPath, (prs) => {
|
|
6905
|
+
mutatePullRequests(project, (prs) => {
|
|
7362
6906
|
if (!Array.isArray(prs) || prs.length === 0) return prs;
|
|
7363
6907
|
normalizePrRecords(prs, project);
|
|
7364
6908
|
const existingPr = prs.find(pr =>
|
|
@@ -7428,13 +6972,13 @@ function _mergeDuplicatePrInto(winner, loser) {
|
|
|
7428
6972
|
* • scalar fields: filled only when the winner's field is empty
|
|
7429
6973
|
* • prdItems: union-merged
|
|
7430
6974
|
*
|
|
7431
|
-
* @param {string}
|
|
6975
|
+
* @param {object|string|null} source Project, scope name, or central.
|
|
7432
6976
|
* @param {{ project?: object|null }} opts Optional project config for normalization.
|
|
7433
6977
|
* @returns {{ collapsed: number }} Number of duplicate records removed.
|
|
7434
6978
|
*/
|
|
7435
|
-
function collapseDuplicatePrRecords(
|
|
6979
|
+
function collapseDuplicatePrRecords(source, { project = null } = {}) {
|
|
7436
6980
|
let collapsed = 0;
|
|
7437
|
-
mutatePullRequests(
|
|
6981
|
+
mutatePullRequests(source, (prs) => {
|
|
7438
6982
|
normalizePrRecords(prs, project);
|
|
7439
6983
|
// PR numbers are only unique within a repository. Grouping by number alone
|
|
7440
6984
|
// merges unrelated GitHub/ADO repositories and can discard a tombstone.
|
|
@@ -7476,12 +7020,12 @@ function _clearPrTombstonesForIdentity(identity) {
|
|
|
7476
7020
|
? projectsByName.get(pr._scope) || null
|
|
7477
7021
|
: null;
|
|
7478
7022
|
if (getPrIdentityKey(pr, project) !== identity) continue;
|
|
7479
|
-
const
|
|
7480
|
-
scopes.set(
|
|
7023
|
+
const scope = pr._scope || 'central';
|
|
7024
|
+
scopes.set(scope, { scope, project });
|
|
7481
7025
|
}
|
|
7482
7026
|
let cleared = 0;
|
|
7483
|
-
for (const {
|
|
7484
|
-
mutatePullRequests(
|
|
7027
|
+
for (const { scope, project } of scopes.values()) {
|
|
7028
|
+
mutatePullRequests(scope, (prs) => {
|
|
7485
7029
|
for (const pr of prs) {
|
|
7486
7030
|
if (pr?.userDeleted !== true || getPrIdentityKey(pr, project) !== identity) continue;
|
|
7487
7031
|
delete pr.userDeleted;
|
|
@@ -7523,13 +7067,10 @@ function shouldAdoptPlatformCreated(storedCreated, platformCreated) {
|
|
|
7523
7067
|
/**
|
|
7524
7068
|
* Canonical PR-producing work contract helper.
|
|
7525
7069
|
*
|
|
7526
|
-
*
|
|
7527
|
-
*
|
|
7528
|
-
* manually records a PR for a work item must use this helper so the PR record
|
|
7529
|
-
* and the canonical work-item attachment are created together and idempotently.
|
|
7070
|
+
* Any path that discovers or manually records a PR for a work item must use
|
|
7071
|
+
* this helper so the PR record and work-item attachment stay consistent.
|
|
7530
7072
|
*/
|
|
7531
|
-
function upsertPullRequestRecord(
|
|
7532
|
-
if (!prPath) throw new Error('prPath required');
|
|
7073
|
+
function upsertPullRequestRecord(source, entry, { project = null, itemId = null, itemIds = null, beforeInsert = null, allowResurrect = false } = {}) {
|
|
7533
7074
|
if (!entry || typeof entry !== 'object') throw new Error('entry required');
|
|
7534
7075
|
|
|
7535
7076
|
const linkedItemIds = normalizePrLinkItems([
|
|
@@ -7553,9 +7094,7 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
7553
7094
|
prNumber: prNumber ?? entry.prNumber ?? null,
|
|
7554
7095
|
prdItems: linkedItemIds,
|
|
7555
7096
|
};
|
|
7556
|
-
//
|
|
7557
|
-
// doesn't carry one. The file path already encodes the project name (e.g.
|
|
7558
|
-
// projects/<name>/pull-requests.json), so it is always deterministic.
|
|
7097
|
+
// Backfill `project` from the resolved project when the entry omits it.
|
|
7559
7098
|
// Without this, PRs linked via /api/pull-requests/link had project:null on
|
|
7560
7099
|
// the record, which broke any downstream path that reads pr.project to
|
|
7561
7100
|
// resolve the owning project.
|
|
@@ -7572,7 +7111,7 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
7572
7111
|
let resurrected = allowResurrect && _clearPrTombstonesForIdentity(canonicalId) > 0;
|
|
7573
7112
|
let record = null;
|
|
7574
7113
|
|
|
7575
|
-
mutatePullRequests(
|
|
7114
|
+
mutatePullRequests(source, (prs) => {
|
|
7576
7115
|
normalizePrRecords(prs, project);
|
|
7577
7116
|
const identityMatches = prs.filter(pr => getPrIdentityKey(pr, project) === canonicalId);
|
|
7578
7117
|
let target = identityMatches.length > 0 ? _pickBestPrRecord(identityMatches) : null;
|
|
@@ -7736,7 +7275,7 @@ function classifyPrRefForVerification(prRef, project = null) {
|
|
|
7736
7275
|
return null;
|
|
7737
7276
|
}
|
|
7738
7277
|
|
|
7739
|
-
// W-mq5wfh1v000e0da9 — Auto-enroll a PR into
|
|
7278
|
+
// W-mq5wfh1v000e0da9 — Auto-enroll a PR into tracked state when a
|
|
7740
7279
|
// `type: fix` work item is created with a structured PR pointer. Without
|
|
7741
7280
|
// this, fix WIs against untracked PRs bypass the polling / review / build
|
|
7742
7281
|
// pipelines (the engine `pr_not_found` gate blocks dispatch entirely, and
|
|
@@ -7754,8 +7293,8 @@ function classifyPrRefForVerification(prRef, project = null) {
|
|
|
7754
7293
|
//
|
|
7755
7294
|
// Returns:
|
|
7756
7295
|
// { skipped: true, reason } — not a pr-requiring type / no ref / no URL / upsert error
|
|
7757
|
-
// { alreadyEnrolled: true, id } — PR already
|
|
7758
|
-
// { enrolled: true, id,
|
|
7296
|
+
// { alreadyEnrolled: true, id } — PR already tracked
|
|
7297
|
+
// { enrolled: true, id, scope } — newly enrolled
|
|
7759
7298
|
const _PR_REQUIRING_TYPES = new Set([WORK_TYPE.FIX, WORK_TYPE.REVIEW, WORK_TYPE.TEST]);
|
|
7760
7299
|
function autoEnrollPrFromWorkItem(item, project, minionsDir) {
|
|
7761
7300
|
if (!item || !_PR_REQUIRING_TYPES.has(item.type)) return { skipped: true, reason: 'not-pr-requiring-type' };
|
|
@@ -7763,8 +7302,8 @@ function autoEnrollPrFromWorkItem(item, project, minionsDir) {
|
|
|
7763
7302
|
if (!prRef) return { skipped: true, reason: 'no-structured-ref' };
|
|
7764
7303
|
const url = deriveUrlForPrRef(prRef, project);
|
|
7765
7304
|
if (!url) return { skipped: true, reason: 'no-url' };
|
|
7766
|
-
const
|
|
7767
|
-
const existing =
|
|
7305
|
+
const source = project || 'central';
|
|
7306
|
+
const existing = readPullRequests(source);
|
|
7768
7307
|
if (findPrRecord(existing, prRef, project)) {
|
|
7769
7308
|
return { alreadyEnrolled: true, id: getCanonicalPrId(project, prRef, url) };
|
|
7770
7309
|
}
|
|
@@ -7772,7 +7311,7 @@ function autoEnrollPrFromWorkItem(item, project, minionsDir) {
|
|
|
7772
7311
|
const prNum = parsedUrl ? parsedUrl.prNumber : null;
|
|
7773
7312
|
const prId = getCanonicalPrId(project, prRef, url);
|
|
7774
7313
|
try {
|
|
7775
|
-
const result = upsertPullRequestRecord(
|
|
7314
|
+
const result = upsertPullRequestRecord(source, {
|
|
7776
7315
|
id: prId,
|
|
7777
7316
|
prNumber: prNum,
|
|
7778
7317
|
title: `PR #${prNum != null ? prNum : '?'} (polling...)`,
|
|
@@ -7786,7 +7325,7 @@ function autoEnrollPrFromWorkItem(item, project, minionsDir) {
|
|
|
7786
7325
|
contextOnly: false,
|
|
7787
7326
|
}, { project, itemId: item.id });
|
|
7788
7327
|
return result.created
|
|
7789
|
-
? { enrolled: true, id: result.id,
|
|
7328
|
+
? { enrolled: true, id: result.id, scope: stateScope(source) }
|
|
7790
7329
|
: { alreadyEnrolled: true, id: result.id };
|
|
7791
7330
|
} catch (e) {
|
|
7792
7331
|
log('warn', `autoEnrollPrFromWorkItem ${item.id}: ${e.message}`);
|
|
@@ -7820,47 +7359,35 @@ function clearWorktreeFailureCache(wtPath) {
|
|
|
7820
7359
|
|
|
7821
7360
|
// ─── Work Items & Pull Requests Mutation Helpers ────────────────────────────
|
|
7822
7361
|
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
// callers always use shared.projectWorkItemsPath(p) / MINIONS_DIR/work-items.json.
|
|
7835
|
-
const fpNorm = String(filePath).replace(/\\/g, '/');
|
|
7836
|
-
const minionsNorm = String(MINIONS_DIR).replace(/\\/g, '/');
|
|
7837
|
-
const insideMinionsDir = fpNorm.startsWith(minionsNorm + '/') || fpNorm === minionsNorm + '/work-items.json';
|
|
7838
|
-
if (insideMinionsDir) {
|
|
7839
|
-
const store = require('./work-items-store');
|
|
7840
|
-
const scope = store.scopeForFilePath(filePath);
|
|
7841
|
-
const { wrote, result } = store.applyWorkItemsMutation(scope, (items) => {
|
|
7842
|
-
if (!Array.isArray(items)) items = [];
|
|
7843
|
-
return mutator(items) || items;
|
|
7844
|
-
});
|
|
7845
|
-
if (wrote) {
|
|
7846
|
-
try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
7847
|
-
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
7848
|
-
}
|
|
7849
|
-
return result;
|
|
7362
|
+
function stateScope(source) {
|
|
7363
|
+
if (source == null || source === '') return 'central';
|
|
7364
|
+
if (source && typeof source === 'object') {
|
|
7365
|
+
if (source.isCentral) return 'central';
|
|
7366
|
+
source = source.scope ?? source.name ?? source.project;
|
|
7367
|
+
}
|
|
7368
|
+
if (typeof source !== 'string') throw new TypeError('state scope requires a project, scope name, or null');
|
|
7369
|
+
const scope = source.trim();
|
|
7370
|
+
if (!scope || scope === 'central') return 'central';
|
|
7371
|
+
if (/[\\/]/.test(scope) || /\.json$/i.test(scope)) {
|
|
7372
|
+
throw new TypeError('state scope must be a project name, not a file path');
|
|
7850
7373
|
}
|
|
7374
|
+
return scope;
|
|
7375
|
+
}
|
|
7851
7376
|
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
7862
|
-
},
|
|
7377
|
+
function readWorkItems(source = null) {
|
|
7378
|
+
return require('./work-items-store').readWorkItemsForScope(stateScope(source));
|
|
7379
|
+
}
|
|
7380
|
+
|
|
7381
|
+
function mutateWorkItems(source, mutator) {
|
|
7382
|
+
const store = require('./work-items-store');
|
|
7383
|
+
const { wrote, result } = store.applyWorkItemsMutation(stateScope(source), (items) => {
|
|
7384
|
+
if (!Array.isArray(items)) items = [];
|
|
7385
|
+
return mutator(items) || items;
|
|
7863
7386
|
});
|
|
7387
|
+
if (wrote) {
|
|
7388
|
+
try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
7389
|
+
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
7390
|
+
}
|
|
7864
7391
|
return result;
|
|
7865
7392
|
}
|
|
7866
7393
|
|
|
@@ -7877,43 +7404,20 @@ function reopenWorkItem(wi) {
|
|
|
7877
7404
|
wi._retryCount = 0;
|
|
7878
7405
|
}
|
|
7879
7406
|
|
|
7880
|
-
|
|
7881
|
-
|
|
7882
|
-
|
|
7883
|
-
* @param {string} filePath - Path to the pull-requests JSON file
|
|
7884
|
-
* @param {Function} mutator - Receives the array, mutates in place or returns new value
|
|
7885
|
-
*/
|
|
7886
|
-
function mutatePullRequests(filePath, mutator) {
|
|
7887
|
-
// Phase 9.4 SQL-only. Route through pull-requests-store when filePath sits
|
|
7888
|
-
// under MINIONS_DIR. Ad-hoc tmp paths (legacy tests using createTmpDir)
|
|
7889
|
-
// still fall through to the JSON path.
|
|
7890
|
-
const fpNorm = String(filePath).replace(/\\/g, '/');
|
|
7891
|
-
const minionsNorm = String(MINIONS_DIR).replace(/\\/g, '/');
|
|
7892
|
-
const insideMinionsDir = fpNorm.startsWith(minionsNorm + '/') || fpNorm === minionsNorm + '/pull-requests.json';
|
|
7893
|
-
if (insideMinionsDir) {
|
|
7894
|
-
const store = require('./pull-requests-store');
|
|
7895
|
-
const scope = store.scopeForFilePath(filePath);
|
|
7896
|
-
const { wrote, result } = store.applyPullRequestsMutation(scope, (prs) => {
|
|
7897
|
-
if (!Array.isArray(prs)) prs = [];
|
|
7898
|
-
return mutator(prs) || prs;
|
|
7899
|
-
});
|
|
7900
|
-
if (wrote) {
|
|
7901
|
-
try { require('./db-events').emitStateEvent('pull_requests'); } catch { /* optional */ }
|
|
7902
|
-
}
|
|
7903
|
-
return result;
|
|
7904
|
-
}
|
|
7407
|
+
function readPullRequests(source = null) {
|
|
7408
|
+
return require('./pull-requests-store').readPullRequestsForScope(stateScope(source));
|
|
7409
|
+
}
|
|
7905
7410
|
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
skipWriteIfUnchanged: true,
|
|
7912
|
-
_skipSqlRouting: true,
|
|
7913
|
-
onWrote: () => {
|
|
7914
|
-
try { require('./db-events').emitStateEvent('pull_requests'); } catch { /* optional */ }
|
|
7915
|
-
},
|
|
7411
|
+
function mutatePullRequests(source, mutator) {
|
|
7412
|
+
const store = require('./pull-requests-store');
|
|
7413
|
+
const { wrote, result } = store.applyPullRequestsMutation(stateScope(source), (prs) => {
|
|
7414
|
+
if (!Array.isArray(prs)) prs = [];
|
|
7415
|
+
return mutator(prs) || prs;
|
|
7916
7416
|
});
|
|
7417
|
+
if (wrote) {
|
|
7418
|
+
try { require('./db-events').emitStateEvent('pull_requests'); } catch { /* optional */ }
|
|
7419
|
+
}
|
|
7420
|
+
return result;
|
|
7917
7421
|
}
|
|
7918
7422
|
|
|
7919
7423
|
/**
|
|
@@ -8818,11 +8322,7 @@ module.exports = {
|
|
|
8818
8322
|
writeStopIntent,
|
|
8819
8323
|
clearStopIntent,
|
|
8820
8324
|
isStopIntentSet,
|
|
8821
|
-
COOLDOWNS_PATH,
|
|
8822
|
-
ENGINE_STATE_PATH, // W-mp60tw0u000j3931
|
|
8823
|
-
PR_LINKS_PATH,
|
|
8824
8325
|
PINNED_ITEMS_PATH,
|
|
8825
|
-
LOG_PATH,
|
|
8826
8326
|
CONSTELLATION_BRIDGE_MARKER_PATH,
|
|
8827
8327
|
CONSTELLATION_BRIDGE_MARKER_SCHEMA_VERSION,
|
|
8828
8328
|
DEFAULT_DASHBOARD_PORT,
|
|
@@ -8860,14 +8360,12 @@ module.exports = {
|
|
|
8860
8360
|
forEachPidFile,
|
|
8861
8361
|
resolveMinionsHome,
|
|
8862
8362
|
saveMinionsRootPointer,
|
|
8863
|
-
neutralizeJsonBackupSidecar,
|
|
8864
8363
|
PROMPT_CONTEXTS_DIR,
|
|
8865
8364
|
dispatchPromptSidecarPath,
|
|
8866
8365
|
dispatchCompletionReportPath,
|
|
8867
8366
|
sidecarDispatchPrompt,
|
|
8868
8367
|
resolveDispatchPrompt,
|
|
8869
8368
|
deleteDispatchPromptSidecar,
|
|
8870
|
-
assertStateFileSize,
|
|
8871
8369
|
withFileLock,
|
|
8872
8370
|
mutateJsonFileLocked,
|
|
8873
8371
|
mutateControl, recordEngineRespawn,
|
|
@@ -8878,8 +8376,11 @@ module.exports = {
|
|
|
8878
8376
|
mutateKbSwept, // Phase 9.3
|
|
8879
8377
|
mutateKbSweepState, // Phase 9.3
|
|
8880
8378
|
mutateTestResults, // Phase 9.3
|
|
8379
|
+
stateScope,
|
|
8380
|
+
readWorkItems,
|
|
8881
8381
|
mutateWorkItems,
|
|
8882
8382
|
reopenWorkItem,
|
|
8383
|
+
readPullRequests,
|
|
8883
8384
|
mutatePullRequests,
|
|
8884
8385
|
uid,
|
|
8885
8386
|
uniquePath,
|
|
@@ -8950,17 +8451,11 @@ module.exports = {
|
|
|
8950
8451
|
resolveConfiguredProject,
|
|
8951
8452
|
resolveProjectSource,
|
|
8952
8453
|
projectRoot,
|
|
8953
|
-
centralWorkItemsPath,
|
|
8954
|
-
centralPullRequestsPath,
|
|
8955
8454
|
projectStateDir,
|
|
8956
8455
|
projectStateDirEnsure,
|
|
8957
|
-
|
|
8958
|
-
projectPrPath,
|
|
8959
|
-
ensureProjectStateFiles,
|
|
8456
|
+
initializeProjectState,
|
|
8960
8457
|
sameResolvedPath,
|
|
8961
8458
|
realPathForComparison, // exported for testing
|
|
8962
|
-
prPathComparisonCandidates, // exported for testing
|
|
8963
|
-
resolveProjectForPrPath, // exported for testing
|
|
8964
8459
|
getPrLinks,
|
|
8965
8460
|
addPrLink,
|
|
8966
8461
|
normalizePrScopeSegment, // exported for testing
|