@yemi33/minions 0.1.2379 → 0.1.2381
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 +3 -4
- package/dashboard/js/render-other.js +43 -23
- package/dashboard/js/render-prd.js +1 -1
- package/dashboard/js/render-work-items.js +13 -6
- package/dashboard/js/settings.js +40 -17
- package/dashboard.js +435 -768
- 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/live-checkout-mode.md +45 -26
- 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 +39 -44
- package/engine/github.js +20 -27
- package/engine/lifecycle.js +279 -354
- package/engine/live-checkout.js +193 -149
- 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 +13 -16
- 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 +4 -6
- package/engine/scheduler.js +0 -4
- package/engine/shared-branch-pr-reconcile.js +2 -3
- package/engine/shared.js +268 -699
- 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 +197 -358
- 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 +9 -9
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) {
|
|
@@ -2856,6 +2481,80 @@ function classifyInboxItem(name, content) {
|
|
|
2856
2481
|
// config.json keep working without an on-disk rewrite. Tracked in
|
|
2857
2482
|
// docs/deprecated.json (id: worktreemode-field-rename).
|
|
2858
2483
|
const CHECKOUT_MODES = Object.freeze({ WORKTREE: 'worktree', LIVE: 'live' });
|
|
2484
|
+
const LEGACY_LIVE_VALIDATION_TYPE_ALIASES = Object.freeze({
|
|
2485
|
+
'build-and-test': 'test',
|
|
2486
|
+
});
|
|
2487
|
+
|
|
2488
|
+
function normalizeLiveValidationWorkType(value) {
|
|
2489
|
+
if (typeof value !== 'string') return '';
|
|
2490
|
+
const type = value.trim();
|
|
2491
|
+
return LEGACY_LIVE_VALIDATION_TYPE_ALIASES[type] || type;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
function inspectLiveValidation(value) {
|
|
2495
|
+
if (value === undefined || value === null || value === '') {
|
|
2496
|
+
return { configured: false, types: [], normalizedType: undefined, error: null };
|
|
2497
|
+
}
|
|
2498
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
2499
|
+
return {
|
|
2500
|
+
configured: true,
|
|
2501
|
+
types: [],
|
|
2502
|
+
normalizedType: undefined,
|
|
2503
|
+
error: 'Invalid liveValidation: must be an object { type, autoDispatch }.',
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
const rawType = value.type;
|
|
2508
|
+
const rawTypes = Array.isArray(rawType) ? rawType : [rawType];
|
|
2509
|
+
if (
|
|
2510
|
+
(typeof rawType !== 'string' && !Array.isArray(rawType))
|
|
2511
|
+
|| rawTypes.length === 0
|
|
2512
|
+
|| !rawTypes.every(type => typeof type === 'string' && type.trim())
|
|
2513
|
+
) {
|
|
2514
|
+
const accepted = [...LIVE_VALIDATION_WORK_TYPES].sort().join(', ');
|
|
2515
|
+
return {
|
|
2516
|
+
configured: true,
|
|
2517
|
+
types: [],
|
|
2518
|
+
normalizedType: undefined,
|
|
2519
|
+
error: `Invalid liveValidation: "type" is required and must be a non-empty string, or a non-empty array of live-capable work-item types. Accepted values: ${accepted}.`,
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
const normalizedTypes = [...new Set(rawTypes.map(normalizeLiveValidationWorkType))];
|
|
2524
|
+
const types = normalizedTypes.filter(type => LIVE_VALIDATION_WORK_TYPES.has(type));
|
|
2525
|
+
const invalidTypes = normalizedTypes.filter(type => !LIVE_VALIDATION_WORK_TYPES.has(type));
|
|
2526
|
+
let error = null;
|
|
2527
|
+
if (invalidTypes.length > 0) {
|
|
2528
|
+
const accepted = [...LIVE_VALIDATION_WORK_TYPES].sort().join(', ');
|
|
2529
|
+
error = `Invalid liveValidation type(s): ${invalidTypes.join(', ')}. Accepted values: ${accepted}.`;
|
|
2530
|
+
}
|
|
2531
|
+
if (!error && Object.prototype.hasOwnProperty.call(value, 'autoDispatch')) {
|
|
2532
|
+
if (typeof value.autoDispatch !== 'boolean') {
|
|
2533
|
+
error = 'Invalid liveValidation: "autoDispatch" must be a boolean.';
|
|
2534
|
+
} else if (value.autoDispatch && !normalizedTypes.includes(WORK_TYPE.TEST)) {
|
|
2535
|
+
error = 'Invalid liveValidation: autoDispatch requires "test" in liveValidation.type so the generated validation work item runs on the live checkout.';
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
return {
|
|
2539
|
+
configured: true,
|
|
2540
|
+
types,
|
|
2541
|
+
normalizedType: Array.isArray(rawType) ? normalizedTypes : normalizedTypes[0],
|
|
2542
|
+
error,
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// Return the canonical, live-capable work-item types configured for a hybrid
|
|
2547
|
+
// project. Raw config is read defensively because config.json can predate the
|
|
2548
|
+
// validator; invalid entries are ignored rather than turning hybrid into full
|
|
2549
|
+
// live mode. `build-and-test` is retained as a read-compat alias for `test`.
|
|
2550
|
+
function resolveLiveValidationTypes(project) {
|
|
2551
|
+
return inspectLiveValidation(project && project.liveValidation).types;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
function isLiveValidationConfigInvalid(project) {
|
|
2555
|
+
const inspection = inspectLiveValidation(project && project.liveValidation);
|
|
2556
|
+
return inspection.configured && !!inspection.error;
|
|
2557
|
+
}
|
|
2859
2558
|
|
|
2860
2559
|
// Resolve a project's effective checkout mode, honoring the legacy
|
|
2861
2560
|
// `worktreeMode` field for back-compat. Reading order:
|
|
@@ -2867,37 +2566,37 @@ const CHECKOUT_MODES = Object.freeze({ WORKTREE: 'worktree', LIVE: 'live' });
|
|
|
2867
2566
|
//
|
|
2868
2567
|
// Optional second argument `workItemType` enables liveValidation routing:
|
|
2869
2568
|
// If checkoutMode === 'live' AND project.liveValidation is set:
|
|
2870
|
-
// - workItemType matches liveValidation.type → 'live'
|
|
2871
|
-
// - otherwise → 'worktree'
|
|
2569
|
+
// - workItemType matches liveValidation.type → 'live'
|
|
2570
|
+
// - otherwise → 'worktree'
|
|
2872
2571
|
// liveValidation without checkoutMode: 'live' is ignored with a warn log.
|
|
2873
2572
|
function resolveCheckoutMode(project, workItemType) {
|
|
2874
2573
|
if (!project || typeof project !== 'object') return CHECKOUT_MODES.WORKTREE;
|
|
2875
2574
|
const canonical = project.checkoutMode;
|
|
2575
|
+
const legacy = project.worktreeMode;
|
|
2576
|
+
let effectiveMode = CHECKOUT_MODES.WORKTREE;
|
|
2577
|
+
if (canonical === CHECKOUT_MODES.LIVE || canonical === CHECKOUT_MODES.WORKTREE) {
|
|
2578
|
+
effectiveMode = canonical;
|
|
2579
|
+
} else if (legacy === 'live') {
|
|
2580
|
+
effectiveMode = CHECKOUT_MODES.LIVE;
|
|
2581
|
+
}
|
|
2876
2582
|
// liveValidation without checkoutMode: 'live' is a misconfiguration — warn and ignore.
|
|
2877
|
-
if (project.liveValidation &&
|
|
2583
|
+
if (project.liveValidation && effectiveMode !== CHECKOUT_MODES.LIVE) {
|
|
2878
2584
|
log('warn', 'resolveCheckoutMode: liveValidation is set but checkoutMode is not "live" — liveValidation ignored',
|
|
2879
2585
|
{ projectName: project.name });
|
|
2880
2586
|
}
|
|
2881
|
-
if (
|
|
2587
|
+
if (effectiveMode === CHECKOUT_MODES.LIVE) {
|
|
2882
2588
|
// Apply liveValidation routing when the block is present and workItemType is provided.
|
|
2883
2589
|
// liveValidation.type may be a single string (legacy / back-compat) or an
|
|
2884
2590
|
// array of strings (W-mr2m1ute000a9c01, multi-select hybrid types) —
|
|
2885
2591
|
// normalize to an array and do a membership check so either shape works.
|
|
2886
2592
|
if (project.liveValidation && workItemType !== undefined) {
|
|
2887
|
-
const lvTypes =
|
|
2888
|
-
|
|
2889
|
-
: [project.liveValidation.type];
|
|
2890
|
-
return lvTypes.includes(workItemType)
|
|
2593
|
+
const lvTypes = resolveLiveValidationTypes(project);
|
|
2594
|
+
return lvTypes.includes(normalizeLiveValidationWorkType(workItemType))
|
|
2891
2595
|
? CHECKOUT_MODES.LIVE
|
|
2892
2596
|
: CHECKOUT_MODES.WORKTREE;
|
|
2893
2597
|
}
|
|
2894
2598
|
return CHECKOUT_MODES.LIVE;
|
|
2895
2599
|
}
|
|
2896
|
-
if (canonical === CHECKOUT_MODES.WORKTREE) return CHECKOUT_MODES.WORKTREE;
|
|
2897
|
-
// Legacy field fallback (only consulted when checkoutMode is absent/unknown).
|
|
2898
|
-
const legacy = project.worktreeMode;
|
|
2899
|
-
if (legacy === 'live') return CHECKOUT_MODES.LIVE;
|
|
2900
|
-
// legacy 'isolated' (and anything else) → the default worktree behavior.
|
|
2901
2600
|
return CHECKOUT_MODES.WORKTREE;
|
|
2902
2601
|
}
|
|
2903
2602
|
|
|
@@ -2913,9 +2612,10 @@ function isLiveCheckoutProject(project) {
|
|
|
2913
2612
|
// anything else (undefined / null / string) is treated as unset so the next
|
|
2914
2613
|
// tier (or the `false` default) decides. When ON and a live-checkout tree is
|
|
2915
2614
|
// dirty, `engine/live-checkout.js#prepareLiveCheckout` runs
|
|
2916
|
-
// `git
|
|
2917
|
-
//
|
|
2918
|
-
// so it is opt-in
|
|
2615
|
+
// `git reset --hard HEAD` + `git clean -fd` instead of refusing with
|
|
2616
|
+
// LIVE_CHECKOUT_DIRTY. This discards uncommitted tracked and untracked work but
|
|
2617
|
+
// preserves the current branch and committed history, so it is opt-in. Sibling
|
|
2618
|
+
// of `resolveCheckoutMode` — pure, no I/O.
|
|
2919
2619
|
function resolveLiveCheckoutAutoReset(project, engine) {
|
|
2920
2620
|
if (project && typeof project === 'object' && typeof project.liveCheckoutAutoReset === 'boolean') {
|
|
2921
2621
|
return project.liveCheckoutAutoReset;
|
|
@@ -2980,16 +2680,14 @@ function validateCheckoutMode(value) {
|
|
|
2980
2680
|
}
|
|
2981
2681
|
|
|
2982
2682
|
// Validate + normalize a per-project `liveValidation` block (hybrid mode).
|
|
2983
|
-
// Hybrid = checkoutMode:'live' + liveValidation:{ type, autoDispatch }:
|
|
2984
|
-
// work items
|
|
2985
|
-
//
|
|
2986
|
-
// checkout. See resolveCheckoutMode (which routes per work-item type) and
|
|
2987
|
-
// docs/live-checkout-mode.md.
|
|
2683
|
+
// Hybrid = checkoutMode:'live' + liveValidation:{ type, autoDispatch }: only
|
|
2684
|
+
// work items whose type is IN liveValidation.type run in-place; all other types
|
|
2685
|
+
// use isolated worktrees. See resolveCheckoutMode and docs/live-checkout-mode.md.
|
|
2988
2686
|
//
|
|
2989
2687
|
// `type` may be a single work-item-type string (legacy / back-compat) OR a
|
|
2990
2688
|
// non-empty array of non-empty-string work-item types
|
|
2991
2689
|
// (W-mr2m1ute000a9c01 — multi-select hybrid types, e.g. both "implement" and
|
|
2992
|
-
// "fix"
|
|
2690
|
+
// "fix" run live while other types stay in worktrees). Every reader of
|
|
2993
2691
|
// liveValidation.type (resolveCheckoutMode, lifecycle.autoDispatchLiveValidationWi,
|
|
2994
2692
|
// dashboard project mapper / pill / CC label) must handle BOTH shapes —
|
|
2995
2693
|
// existing config.json entries with a plain string `type` continue to work
|
|
@@ -2998,10 +2696,8 @@ function validateCheckoutMode(value) {
|
|
|
2998
2696
|
// Returns:
|
|
2999
2697
|
// - undefined → caller should clear the field (empty / null / '' input)
|
|
3000
2698
|
// - { type, autoDispatch? } normalized object on success, where `type` is
|
|
3001
|
-
//
|
|
3002
|
-
//
|
|
3003
|
-
// into an array so already-persisted single-type configs round-trip
|
|
3004
|
-
// byte-identical through a Settings save that doesn't touch this field.
|
|
2699
|
+
// the caller's input shape (string stays a string; array stays an array),
|
|
2700
|
+
// with values trimmed, normalized, and deduplicated.
|
|
3005
2701
|
// Throws HTTP 400 (via _httpError) on malformed input, or when the effective
|
|
3006
2702
|
// checkout mode is not 'live' (liveValidation is meaningless without it — it is
|
|
3007
2703
|
// silently ignored at resolve time, so we refuse to persist a misconfiguration).
|
|
@@ -3011,45 +2707,15 @@ function validateCheckoutMode(value) {
|
|
|
3011
2707
|
// applied so the gate sees the intended final mode.
|
|
3012
2708
|
function validateLiveValidation(value, opts) {
|
|
3013
2709
|
if (value === undefined || value === null || value === '') return undefined;
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
}
|
|
2710
|
+
const inspection = inspectLiveValidation(value);
|
|
2711
|
+
if (inspection.error) throw _httpError(400, inspection.error);
|
|
3017
2712
|
const checkoutMode = opts && opts.checkoutMode;
|
|
3018
2713
|
if (checkoutMode !== CHECKOUT_MODES.LIVE) {
|
|
3019
2714
|
throw _httpError(400, 'liveValidation requires checkoutMode "live" (hybrid mode). Set checkoutMode to "live" first, or clear liveValidation.');
|
|
3020
2715
|
}
|
|
3021
|
-
const
|
|
3022
|
-
const rawType = value.type;
|
|
3023
|
-
let out;
|
|
3024
|
-
if (typeof rawType === 'string') {
|
|
3025
|
-
const trimmed = rawType.trim();
|
|
3026
|
-
if (!trimmed) throw _httpError(400, typeErrorMsg);
|
|
3027
|
-
out = { type: trimmed };
|
|
3028
|
-
} else if (Array.isArray(rawType)) {
|
|
3029
|
-
if (!rawType.every(t => typeof t === 'string' && t.trim().length > 0)) {
|
|
3030
|
-
throw _httpError(400, typeErrorMsg);
|
|
3031
|
-
}
|
|
3032
|
-
// Dedupe (preserving first-seen order) and cap at the number of distinct
|
|
3033
|
-
// work-item types the engine recognizes — a longer array can only be
|
|
3034
|
-
// full of duplicates or invented types.
|
|
3035
|
-
const deduped = [...new Set(rawType.map(t => t.trim()))];
|
|
3036
|
-
if (deduped.length === 0) throw _httpError(400, typeErrorMsg);
|
|
3037
|
-
if (deduped.length > VALID_WORK_TYPES.size) {
|
|
3038
|
-
throw _httpError(400, `Invalid liveValidation: "type" array has ${deduped.length} entries, more than the ${VALID_WORK_TYPES.size} distinct work-item types the engine recognizes — remove duplicates or invalid types.`);
|
|
3039
|
-
}
|
|
3040
|
-
out = { type: deduped };
|
|
3041
|
-
} else {
|
|
3042
|
-
throw _httpError(400, typeErrorMsg);
|
|
3043
|
-
}
|
|
3044
|
-
// autoDispatch (optional): when true, lifecycle auto-creates a SINGLE
|
|
3045
|
-
// validation WI after each coding WI completes with a PR
|
|
3046
|
-
// (engine/lifecycle.js autoDispatchLiveValidationWi). Even when `type` is a
|
|
3047
|
-
// multi-entry array (which drives checkout ROUTING — the types that run live),
|
|
3048
|
-
// exactly one validation WI is filed, of the canonical `test` type when
|
|
3049
|
-
// configured else the first configured live type. Coerce to a strict
|
|
3050
|
-
// boolean; absent leaves it off so the operator opts in explicitly.
|
|
2716
|
+
const out = { type: inspection.normalizedType };
|
|
3051
2717
|
if (Object.prototype.hasOwnProperty.call(value, 'autoDispatch')) {
|
|
3052
|
-
out.autoDispatch =
|
|
2718
|
+
out.autoDispatch = value.autoDispatch;
|
|
3053
2719
|
}
|
|
3054
2720
|
return out;
|
|
3055
2721
|
}
|
|
@@ -3351,8 +3017,8 @@ const ENGINE_DEFAULTS = {
|
|
|
3351
3017
|
// shared.resolvePollFlag is intentionally NOT used here because no
|
|
3352
3018
|
// legacy macro bundle ever covered these phases.
|
|
3353
3019
|
prDiscoveryEnabled: true, // discoverFromPrs (per-project PR-driven fix/review/test discovery)
|
|
3354
|
-
workItemsDiscoveryEnabled: true, // discoverFromWorkItems (
|
|
3355
|
-
centralWorkDiscoveryEnabled: true, // discoverCentralWorkItems (
|
|
3020
|
+
workItemsDiscoveryEnabled: true, // discoverFromWorkItems (project SQL scope)
|
|
3021
|
+
centralWorkDiscoveryEnabled: true, // discoverCentralWorkItems (central SQL scope)
|
|
3356
3022
|
scheduledWorkDiscoveryEnabled: true, // discoverScheduledWork (cron-style scheduled tasks + meetings)
|
|
3357
3023
|
planMaterializationEnabled: true, // reconcilePrdStatuses + materializePlansAsWorkItems pair
|
|
3358
3024
|
prPollStatusEvery: 72, // poll PR build/review/merge status every N ticks for both ADO and GitHub (~12 min at default 10s tick)
|
|
@@ -3394,8 +3060,8 @@ const ENGINE_DEFAULTS = {
|
|
|
3394
3060
|
prAbandonConfirmCount: 3,
|
|
3395
3061
|
// W-mp60tw0u000j3931: One-shot startup reconciliation pass for `abandoned` PRs runs
|
|
3396
3062
|
// exactly once per bump of this constant. The engine compares this value against
|
|
3397
|
-
//
|
|
3398
|
-
// is lower, every project's
|
|
3063
|
+
// the persisted lastAbandonedReconciliationVersion at boot; if the version
|
|
3064
|
+
// is lower, every project's PR scope is scanned, every abandoned PR is
|
|
3399
3065
|
// re-probed, and any false-flipped PRs (e.g. transient 404 victims from before the
|
|
3400
3066
|
// hardening in W-mp5trwh60008386d shipped) are flipped back to active/merged/closed
|
|
3401
3067
|
// based on their live API state. Bump this when reconciliation logic itself changes
|
|
@@ -3459,7 +3125,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3459
3125
|
// re-queued into review again.
|
|
3460
3126
|
preDispatchEvalMaxRejections: 5,
|
|
3461
3127
|
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, //
|
|
3128
|
+
autoApplyReviewVote: false, // Master gate for platform vote mutations; local verdicts remain informational when false.
|
|
3463
3129
|
|
|
3464
3130
|
// ── Runtime fleet (P-3b8e5f1d) ──────────────────────────────────────────────
|
|
3465
3131
|
// Single source of truth for which CLI runtime + model every spawn uses.
|
|
@@ -3473,7 +3139,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3473
3139
|
claudeBareMode: false, // Claude --bare: suppress CLAUDE.md auto-discovery (per-agent override: agents.<id>.bareMode)
|
|
3474
3140
|
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
3141
|
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, //
|
|
3142
|
+
copilotDisableBuiltinMcps: true, // Keep github-mcp-server from bypassing Minions PR tracking.
|
|
3477
3143
|
// P-7d31a06b — Claude pre-approves workspace .mcp.json servers in ~/.claude.json on every spawn into a fresh
|
|
3478
3144
|
// worktree cwd. Without this, the first Claude invocation in a brand-new worktree shows a project-scoped MCP
|
|
3479
3145
|
// "trust this server?" prompt that blocks until a human accepts — invisible behind --dangerously-skip-permissions
|
|
@@ -3489,13 +3155,6 @@ const ENGINE_DEFAULTS = {
|
|
|
3489
3155
|
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
3156
|
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
3157
|
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
3158
|
// W-mpmwxkrw000872ec — dashboard global font-size scale. Drives the
|
|
3500
3159
|
// [data-font-size] attribute on <html> via the inline bootstrap script in
|
|
3501
3160
|
// layout.html (localStorage fast path) and is reconciled from the server
|
|
@@ -3503,10 +3162,9 @@ const ENGINE_DEFAULTS = {
|
|
|
3503
3162
|
// 'xlarge'. Allowlist validation lives in handleSettingsUpdate so invalid
|
|
3504
3163
|
// values are clamped rather than silently breaking the bootstrap.
|
|
3505
3164
|
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)
|
|
3165
|
+
maxPendingContexts: 20, // cap pendingContexts arrays to prevent unbounded growth
|
|
3166
|
+
maxPendingContextEntryBytes: 256 * 1024, // cap each pending context entry
|
|
3167
|
+
maxDispatchPromptBytes: 1024 * 1024, // sidecar oversized prompts under engine/contexts/
|
|
3510
3168
|
mainBranchCacheTtlMs: 300000, // 5min — cache git default-branch detection, then prune expired entries
|
|
3511
3169
|
mainBranchCacheMaxEntries: 100, // bound repo/branch detection cache in long-lived dashboard/engine processes
|
|
3512
3170
|
removeWorktreeFailureTtlMs: 24 * 60 * 60 * 1000, // stale failed paths are forgotten after a day
|
|
@@ -3565,7 +3223,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3565
3223
|
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
3224
|
docSessionMaxEntries: 200, // cap doc-chat session map/disk store by least-recent activity (LRU; sessions are non-expiring otherwise)
|
|
3567
3225
|
ccLiveStreamMaxAgeMs: 30 * 60 * 1000, // hard cap reconnect buffers if abort/cleanup stalls
|
|
3568
|
-
metricsFlushIntervalMs: 10000, // batch trackEngineUsage writes
|
|
3226
|
+
metricsFlushIntervalMs: 10000, // batch trackEngineUsage SQL writes
|
|
3569
3227
|
maxLlmRawBytes: 256 * 1024, // keep only a bounded stdout tail from direct Claude calls
|
|
3570
3228
|
maxLlmStderrBytes: 64 * 1024, // keep only a bounded stderr tail from direct Claude calls
|
|
3571
3229
|
maxLlmLineBufferBytes: 128 * 1024, // cap the incremental JSON line buffer to avoid malformed-stream OOMs
|
|
@@ -4285,6 +3943,15 @@ const WORK_TYPE = {
|
|
|
4285
3943
|
BUILD_FIX_COMPLEX: 'build-fix-complex',
|
|
4286
3944
|
};
|
|
4287
3945
|
|
|
3946
|
+
const FIX_LIKE_WORK_TYPES = new Set([
|
|
3947
|
+
WORK_TYPE.FIX,
|
|
3948
|
+
WORK_TYPE.BUILD_FIX_COMPLEX,
|
|
3949
|
+
]);
|
|
3950
|
+
|
|
3951
|
+
function isFixLikeWorkType(type) {
|
|
3952
|
+
return FIX_LIKE_WORK_TYPES.has(type);
|
|
3953
|
+
}
|
|
3954
|
+
|
|
4288
3955
|
// Work types whose dispatch path requires a per-project git worktree. The
|
|
4289
3956
|
// engine's spawnAgent uses the project's `localPath` as the worktree root —
|
|
4290
3957
|
// without an owning project the rootDir falls back to MINIONS_DIR's parent,
|
|
@@ -4323,6 +3990,21 @@ const WORKTREE_REQUIRING_TYPES = new Set([
|
|
|
4323
3990
|
WORK_TYPE.DOCS,
|
|
4324
3991
|
]);
|
|
4325
3992
|
|
|
3993
|
+
// Hybrid routing is meaningful only for work types that would otherwise need
|
|
3994
|
+
// an isolated project worktree. Read-only root tasks already run from the
|
|
3995
|
+
// project/root path and therefore gain nothing from a live-validation entry.
|
|
3996
|
+
const LIVE_VALIDATION_WORK_TYPES = new Set(WORKTREE_REQUIRING_TYPES);
|
|
3997
|
+
|
|
3998
|
+
function resolveLiveValidationAutoDispatchType(project) {
|
|
3999
|
+
if (!project?.liveValidation || project.liveValidation.autoDispatch !== true) return null;
|
|
4000
|
+
if (resolveCheckoutMode(project) !== CHECKOUT_MODES.LIVE) return null;
|
|
4001
|
+
const inspection = inspectLiveValidation(project.liveValidation);
|
|
4002
|
+
if (inspection.error) return null;
|
|
4003
|
+
return inspection.types.includes(WORK_TYPE.TEST)
|
|
4004
|
+
? WORK_TYPE.TEST
|
|
4005
|
+
: null;
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4326
4008
|
// W-mpxqkkn300121d21 — Valid `type` strings the materializer will honor when a
|
|
4327
4009
|
// PRD missing_feature carries one. Excludes orchestration-only types that the
|
|
4328
4010
|
// plan→PRD→materializer pipeline never produces (PLAN, PLAN_TO_PRD, MEETING)
|
|
@@ -4402,7 +4084,7 @@ const PR_PENDING_REASON = {
|
|
|
4402
4084
|
MISSING_BRANCH: 'missing_pr_branch',
|
|
4403
4085
|
};
|
|
4404
4086
|
// PR build-status enum — single source of truth for the literal strings written to
|
|
4405
|
-
//
|
|
4087
|
+
// PR `buildStatus`. Previously drifted across engine/ado.js,
|
|
4406
4088
|
// engine/github.js, engine/lifecycle.js, engine/watches.js, engine/queries.js, engine/cli.js
|
|
4407
4089
|
// (P-bfa3d-constants-eslint, audit items #68-#82).
|
|
4408
4090
|
const BUILD_STATUS = {
|
|
@@ -4412,7 +4094,7 @@ const BUILD_STATUS = {
|
|
|
4412
4094
|
NONE: 'none',
|
|
4413
4095
|
};
|
|
4414
4096
|
// PR review-status enum — single source of truth for the literal strings written to
|
|
4415
|
-
//
|
|
4097
|
+
// PR `reviewStatus`. Previously drifted across engine/ado.js,
|
|
4416
4098
|
// engine/github.js, engine/lifecycle.js, engine/watches.js, engine/queries.js, engine/cli.js
|
|
4417
4099
|
// (P-bfa3d-constants-eslint, audit items #68-#82).
|
|
4418
4100
|
const REVIEW_STATUS = {
|
|
@@ -4666,14 +4348,10 @@ function trackReviewMetric(pr, newReviewStatus, config) {
|
|
|
4666
4348
|
* just a boolean caused the toast to render "queued undefined" (issue #2375).
|
|
4667
4349
|
*/
|
|
4668
4350
|
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
4351
|
let queued = false;
|
|
4674
4352
|
let id = null;
|
|
4675
4353
|
let item = null;
|
|
4676
|
-
|
|
4354
|
+
mutateWorkItems('central', items => {
|
|
4677
4355
|
if (!Array.isArray(items)) items = [];
|
|
4678
4356
|
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
4357
|
if (existing) {
|
|
@@ -4820,7 +4498,7 @@ const FAILURE_CLASS = {
|
|
|
4820
4498
|
WORKSPACE_MANIFEST_URL: 'workspace-manifest-url-forbidden', // W-mq07avbk000m5543: out-of-scope external URL fetch. Non-retryable as-is.
|
|
4821
4499
|
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
4500
|
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', //
|
|
4501
|
+
VERIFY_MISSING_PR: 'verify-missing-pr', // Verify completed without an attached or tracked PR.
|
|
4824
4502
|
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
4503
|
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
4504
|
UNKNOWN: 'unknown', // Unclassified failure
|
|
@@ -4857,9 +4535,8 @@ const DEFAULT_CLAUDE = {
|
|
|
4857
4535
|
// engine/cleanup.js:1200-1213 _engine.legacyStatusMigrations.
|
|
4858
4536
|
function _bumpPruneDefaultClaudeStrip() {
|
|
4859
4537
|
try {
|
|
4860
|
-
const metricsPath = path.join(ENGINE_DIR, 'metrics.json');
|
|
4861
4538
|
const dateKey = new Date().toISOString().slice(0, 10);
|
|
4862
|
-
|
|
4539
|
+
mutateMetrics((metrics) => {
|
|
4863
4540
|
metrics = metrics || {};
|
|
4864
4541
|
if (!metrics._engine) metrics._engine = {};
|
|
4865
4542
|
if (!metrics._engine.pruneDefaultClaudeConfigStrips) metrics._engine.pruneDefaultClaudeConfigStrips = {};
|
|
@@ -5168,18 +4845,9 @@ function resolveConfiguredProject(projectName, projectsOrConfig, options = {}) {
|
|
|
5168
4845
|
return { project: null, explicit: false, value: '' };
|
|
5169
4846
|
}
|
|
5170
4847
|
|
|
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
4848
|
function _projectSourceRawValue(source) {
|
|
5180
4849
|
if (source && typeof source === 'object') {
|
|
5181
4850
|
return source.name ?? source.project ?? source._project ?? source.source ?? source._source ??
|
|
5182
|
-
source.wiPath ?? source.workItemsPath ?? source.prPath ?? source.pullRequestsPath ??
|
|
5183
4851
|
source.stateDir ?? source.path ?? source.localPath ?? '';
|
|
5184
4852
|
}
|
|
5185
4853
|
return source;
|
|
@@ -5193,18 +4861,15 @@ function _sameSourcePath(value, targetPath, minionsDir = MINIONS_DIR) {
|
|
|
5193
4861
|
}
|
|
5194
4862
|
|
|
5195
4863
|
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
4864
|
const stateDir = project ? projectStateDir(project) : minionsDir;
|
|
5199
4865
|
return {
|
|
5200
4866
|
project: project || null,
|
|
5201
4867
|
explicit: !!explicit,
|
|
5202
4868
|
value: value || '',
|
|
5203
4869
|
sourceName: project?.name || 'central',
|
|
4870
|
+
scope: project?.name || 'central',
|
|
5204
4871
|
isCentral: !project,
|
|
5205
4872
|
stateDir,
|
|
5206
|
-
wiPath,
|
|
5207
|
-
prPath,
|
|
5208
4873
|
};
|
|
5209
4874
|
}
|
|
5210
4875
|
|
|
@@ -5221,17 +4886,13 @@ function resolveProjectSource(source, projectsOrConfig, options = {}) {
|
|
|
5221
4886
|
return _projectSourceDescriptor(projects[0], '', false, minionsDir);
|
|
5222
4887
|
}
|
|
5223
4888
|
if (allowCentral) return _projectSourceDescriptor(null, '', false, minionsDir);
|
|
5224
|
-
return { project: null, explicit: false, value: '', sourceName: '', isCentral: false,
|
|
4889
|
+
return { project: null, explicit: false, value: '', sourceName: '', isCentral: false, scope: null, stateDir: null };
|
|
5225
4890
|
}
|
|
5226
4891
|
|
|
5227
|
-
const centralWorkPath = centralWorkItemsPath(minionsDir);
|
|
5228
|
-
const centralPrPath = centralPullRequestsPath(minionsDir);
|
|
5229
4892
|
const centralNames = new Set(['central', 'root']);
|
|
5230
4893
|
const lowerValue = value.toLowerCase();
|
|
5231
4894
|
const isCentral = centralNames.has(lowerValue) ||
|
|
5232
|
-
_sameSourcePath(value, minionsDir, minionsDir)
|
|
5233
|
-
_sameSourcePath(value, centralWorkPath, minionsDir) ||
|
|
5234
|
-
_sameSourcePath(value, centralPrPath, minionsDir);
|
|
4895
|
+
_sameSourcePath(value, minionsDir, minionsDir);
|
|
5235
4896
|
if (isCentral) {
|
|
5236
4897
|
if (allowCentral) return _projectSourceDescriptor(null, value, true, minionsDir);
|
|
5237
4898
|
return {
|
|
@@ -5240,8 +4901,7 @@ function resolveProjectSource(source, projectsOrConfig, options = {}) {
|
|
|
5240
4901
|
value,
|
|
5241
4902
|
sourceName: '',
|
|
5242
4903
|
isCentral: false,
|
|
5243
|
-
|
|
5244
|
-
prPath: null,
|
|
4904
|
+
scope: null,
|
|
5245
4905
|
stateDir: null,
|
|
5246
4906
|
error: 'central source is not allowed here',
|
|
5247
4907
|
};
|
|
@@ -5255,8 +4915,6 @@ function resolveProjectSource(source, projectsOrConfig, options = {}) {
|
|
|
5255
4915
|
const candidates = [
|
|
5256
4916
|
project.localPath,
|
|
5257
4917
|
projectStateDir(project),
|
|
5258
|
-
projectWorkItemsPath(project),
|
|
5259
|
-
projectPrPath(project),
|
|
5260
4918
|
].filter(Boolean);
|
|
5261
4919
|
if (candidates.some(candidate => _sameSourcePath(value, candidate, minionsDir))) {
|
|
5262
4920
|
return _projectSourceDescriptor(project, value, explicit, minionsDir);
|
|
@@ -5269,8 +4927,7 @@ function resolveProjectSource(source, projectsOrConfig, options = {}) {
|
|
|
5269
4927
|
value,
|
|
5270
4928
|
sourceName: '',
|
|
5271
4929
|
isCentral: false,
|
|
5272
|
-
|
|
5273
|
-
prPath: null,
|
|
4930
|
+
scope: null,
|
|
5274
4931
|
stateDir: null,
|
|
5275
4932
|
error: formatUnknownProjectError(value, projects),
|
|
5276
4933
|
};
|
|
@@ -5280,15 +4937,8 @@ function projectRoot(project) {
|
|
|
5280
4937
|
return path.resolve(project.localPath);
|
|
5281
4938
|
}
|
|
5282
4939
|
|
|
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.
|
|
4940
|
+
// Project-local definitions and artifacts live under projects/{name}/.
|
|
4941
|
+
// Runtime records use the project's SQL scope.
|
|
5292
4942
|
function projectStateDir(project) {
|
|
5293
4943
|
const name = project.name || path.basename(project.localPath);
|
|
5294
4944
|
return path.join(MINIONS_DIR, 'projects', name);
|
|
@@ -5302,14 +4952,6 @@ function projectStateDirEnsure(project) {
|
|
|
5302
4952
|
return dir;
|
|
5303
4953
|
}
|
|
5304
4954
|
|
|
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
4955
|
function sameResolvedPath(a, b) {
|
|
5314
4956
|
if (!a || !b) return false;
|
|
5315
4957
|
try {
|
|
@@ -5321,7 +4963,7 @@ function sameResolvedPath(a, b) {
|
|
|
5321
4963
|
}
|
|
5322
4964
|
}
|
|
5323
4965
|
|
|
5324
|
-
function
|
|
4966
|
+
function initializeProjectState(project) {
|
|
5325
4967
|
const result = { created: [] };
|
|
5326
4968
|
projectStateDirEnsure(project);
|
|
5327
4969
|
// SQL scopes need no empty sentinel rows. Touch both stores so migrations
|
|
@@ -5355,33 +4997,6 @@ function realPathForComparison(filePath) {
|
|
|
5355
4997
|
}
|
|
5356
4998
|
}
|
|
5357
4999
|
|
|
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
5000
|
// ── ID Generation ────────────────────────────────────────────────────────────
|
|
5386
5001
|
|
|
5387
5002
|
function nextWorkItemId(items, prefix) {
|
|
@@ -5421,7 +5036,7 @@ function getAdoOrgBase(project) {
|
|
|
5421
5036
|
*
|
|
5422
5037
|
* - "basenames": exact relative paths under the live root (engine.js, dashboard.js,
|
|
5423
5038
|
* minions.js, config.json — and the runtime state files engine/control.json
|
|
5424
|
-
* and engine/
|
|
5039
|
+
* and engine/control.json).
|
|
5425
5040
|
* - "globs": direct-child JS files under protected live directories
|
|
5426
5041
|
* (engine/*.js, bin/*.js).
|
|
5427
5042
|
* - "prefixes": relative directory prefixes whose entire subtree is read-only
|
|
@@ -5443,7 +5058,6 @@ const _CC_PROTECTED_BASENAMES = Object.freeze([
|
|
|
5443
5058
|
'minions.js',
|
|
5444
5059
|
'config.json',
|
|
5445
5060
|
'engine/control.json',
|
|
5446
|
-
'engine/dispatch.json',
|
|
5447
5061
|
]);
|
|
5448
5062
|
const _CC_PROTECTED_FILE_GLOBS = Object.freeze([
|
|
5449
5063
|
'engine/*.js',
|
|
@@ -6581,8 +6195,8 @@ function extractStructuredWorkItemPrRef(item) {
|
|
|
6581
6195
|
// W-mq1j85cj00055a8f — when an inbox note (notes/inbox/<name>) leaves the
|
|
6582
6196
|
// inbox (persisted to notes.md, promoted to knowledge/, or auto-archived by
|
|
6583
6197
|
// consolidation), any work-item reference pointing at the old inbox path
|
|
6584
|
-
// turns into a broken link. Rewrite those references across every
|
|
6585
|
-
//
|
|
6198
|
+
// turns into a broken link. Rewrite those references across every work-item
|
|
6199
|
+
// scope to the new canonical location.
|
|
6586
6200
|
//
|
|
6587
6201
|
// Match semantics (intentionally narrow per the WI scope):
|
|
6588
6202
|
// - item.references[]: string 'notes/inbox/<inboxName>' (anchored on start
|
|
@@ -6600,15 +6214,14 @@ function extractStructuredWorkItemPrRef(item) {
|
|
|
6600
6214
|
// - Trailing `?query` or `#fragment` is tolerated; substring overlaps in
|
|
6601
6215
|
// unrelated path segments (e.g. .../notes/inbox/foobar for foo.md) are
|
|
6602
6216
|
// NOT rewritten.
|
|
6603
|
-
// - Archived work
|
|
6604
|
-
// surfaced by getProjects() + the central path are touched.
|
|
6217
|
+
// - Archived work items are skipped.
|
|
6605
6218
|
//
|
|
6606
6219
|
// Idempotent: re-running with the same inboxName after the rewrite is a
|
|
6607
6220
|
// no-op (the references already read newLocation, which won't match the
|
|
6608
6221
|
// notes/inbox/<inboxName> regex).
|
|
6609
6222
|
//
|
|
6610
|
-
// Returns the count of references rewritten across all
|
|
6611
|
-
// Each
|
|
6223
|
+
// Returns the count of references rewritten across all scopes (for logging).
|
|
6224
|
+
// Each scope's mutate is wrapped in try/catch so one corrupt project can't
|
|
6612
6225
|
// block the others.
|
|
6613
6226
|
//
|
|
6614
6227
|
// `opts._mutate` is an undocumented test seam — production callers always
|
|
@@ -6645,14 +6258,14 @@ function rewriteInboxRefsAcrossProjects(inboxName, newLocation, opts = {}) {
|
|
|
6645
6258
|
const candidates = [];
|
|
6646
6259
|
try {
|
|
6647
6260
|
for (const project of getProjects(config)) {
|
|
6648
|
-
|
|
6261
|
+
if (project?.name) candidates.push(project);
|
|
6649
6262
|
}
|
|
6650
6263
|
} catch { /* getProjects failed — fall back to central only */ }
|
|
6651
|
-
candidates.push(
|
|
6264
|
+
candidates.push('central');
|
|
6652
6265
|
|
|
6653
|
-
for (const
|
|
6266
|
+
for (const scope of candidates) {
|
|
6654
6267
|
try {
|
|
6655
|
-
_mutate(
|
|
6268
|
+
_mutate(scope, items => {
|
|
6656
6269
|
if (!Array.isArray(items)) return items;
|
|
6657
6270
|
for (const item of items) {
|
|
6658
6271
|
if (!item) continue;
|
|
@@ -6709,7 +6322,7 @@ function rewriteInboxRefsAcrossProjects(inboxName, newLocation, opts = {}) {
|
|
|
6709
6322
|
return items;
|
|
6710
6323
|
});
|
|
6711
6324
|
} catch (e) {
|
|
6712
|
-
try { console.warn(`rewriteInboxRefsAcrossProjects(${
|
|
6325
|
+
try { console.warn(`rewriteInboxRefsAcrossProjects(${stateScope(scope)}): ${e.message}`); } catch { /* logging best-effort */ }
|
|
6713
6326
|
}
|
|
6714
6327
|
}
|
|
6715
6328
|
return rewritten;
|
|
@@ -7347,18 +6960,14 @@ function addPrLink(prId, itemId, { project = null, url = '', prNumber = null } =
|
|
|
7347
6960
|
links[effectivePrId] = [...mergedCurrent];
|
|
7348
6961
|
return links;
|
|
7349
6962
|
};
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
mutateJsonFileLocked(PR_LINKS_PATH, mutator, { defaultValue: {} });
|
|
6963
|
+
const { wrote } = require('./small-state-store').applyPrLinksMutation(mutator);
|
|
6964
|
+
if (wrote) {
|
|
6965
|
+
try { require('./db-events').emitStateEvent('pr_links'); } catch { /* optional */ }
|
|
6966
|
+
}
|
|
7355
6967
|
|
|
7356
6968
|
if (!project) return;
|
|
7357
|
-
const prPath = projectPrPath(project);
|
|
7358
6969
|
const effectivePrNumber = getPrNumber(prNumber ?? effectivePrId);
|
|
7359
|
-
|
|
7360
|
-
// so SQL stays canonical; the JSON mirror is regenerated by the store.
|
|
7361
|
-
mutatePullRequests(prPath, (prs) => {
|
|
6970
|
+
mutatePullRequests(project, (prs) => {
|
|
7362
6971
|
if (!Array.isArray(prs) || prs.length === 0) return prs;
|
|
7363
6972
|
normalizePrRecords(prs, project);
|
|
7364
6973
|
const existingPr = prs.find(pr =>
|
|
@@ -7428,13 +7037,13 @@ function _mergeDuplicatePrInto(winner, loser) {
|
|
|
7428
7037
|
* • scalar fields: filled only when the winner's field is empty
|
|
7429
7038
|
* • prdItems: union-merged
|
|
7430
7039
|
*
|
|
7431
|
-
* @param {string}
|
|
7040
|
+
* @param {object|string|null} source Project, scope name, or central.
|
|
7432
7041
|
* @param {{ project?: object|null }} opts Optional project config for normalization.
|
|
7433
7042
|
* @returns {{ collapsed: number }} Number of duplicate records removed.
|
|
7434
7043
|
*/
|
|
7435
|
-
function collapseDuplicatePrRecords(
|
|
7044
|
+
function collapseDuplicatePrRecords(source, { project = null } = {}) {
|
|
7436
7045
|
let collapsed = 0;
|
|
7437
|
-
mutatePullRequests(
|
|
7046
|
+
mutatePullRequests(source, (prs) => {
|
|
7438
7047
|
normalizePrRecords(prs, project);
|
|
7439
7048
|
// PR numbers are only unique within a repository. Grouping by number alone
|
|
7440
7049
|
// merges unrelated GitHub/ADO repositories and can discard a tombstone.
|
|
@@ -7476,12 +7085,12 @@ function _clearPrTombstonesForIdentity(identity) {
|
|
|
7476
7085
|
? projectsByName.get(pr._scope) || null
|
|
7477
7086
|
: null;
|
|
7478
7087
|
if (getPrIdentityKey(pr, project) !== identity) continue;
|
|
7479
|
-
const
|
|
7480
|
-
scopes.set(
|
|
7088
|
+
const scope = pr._scope || 'central';
|
|
7089
|
+
scopes.set(scope, { scope, project });
|
|
7481
7090
|
}
|
|
7482
7091
|
let cleared = 0;
|
|
7483
|
-
for (const {
|
|
7484
|
-
mutatePullRequests(
|
|
7092
|
+
for (const { scope, project } of scopes.values()) {
|
|
7093
|
+
mutatePullRequests(scope, (prs) => {
|
|
7485
7094
|
for (const pr of prs) {
|
|
7486
7095
|
if (pr?.userDeleted !== true || getPrIdentityKey(pr, project) !== identity) continue;
|
|
7487
7096
|
delete pr.userDeleted;
|
|
@@ -7523,13 +7132,10 @@ function shouldAdoptPlatformCreated(storedCreated, platformCreated) {
|
|
|
7523
7132
|
/**
|
|
7524
7133
|
* Canonical PR-producing work contract helper.
|
|
7525
7134
|
*
|
|
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.
|
|
7135
|
+
* Any path that discovers or manually records a PR for a work item must use
|
|
7136
|
+
* this helper so the PR record and work-item attachment stay consistent.
|
|
7530
7137
|
*/
|
|
7531
|
-
function upsertPullRequestRecord(
|
|
7532
|
-
if (!prPath) throw new Error('prPath required');
|
|
7138
|
+
function upsertPullRequestRecord(source, entry, { project = null, itemId = null, itemIds = null, beforeInsert = null, allowResurrect = false } = {}) {
|
|
7533
7139
|
if (!entry || typeof entry !== 'object') throw new Error('entry required');
|
|
7534
7140
|
|
|
7535
7141
|
const linkedItemIds = normalizePrLinkItems([
|
|
@@ -7553,9 +7159,7 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
7553
7159
|
prNumber: prNumber ?? entry.prNumber ?? null,
|
|
7554
7160
|
prdItems: linkedItemIds,
|
|
7555
7161
|
};
|
|
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.
|
|
7162
|
+
// Backfill `project` from the resolved project when the entry omits it.
|
|
7559
7163
|
// Without this, PRs linked via /api/pull-requests/link had project:null on
|
|
7560
7164
|
// the record, which broke any downstream path that reads pr.project to
|
|
7561
7165
|
// resolve the owning project.
|
|
@@ -7572,7 +7176,7 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
7572
7176
|
let resurrected = allowResurrect && _clearPrTombstonesForIdentity(canonicalId) > 0;
|
|
7573
7177
|
let record = null;
|
|
7574
7178
|
|
|
7575
|
-
mutatePullRequests(
|
|
7179
|
+
mutatePullRequests(source, (prs) => {
|
|
7576
7180
|
normalizePrRecords(prs, project);
|
|
7577
7181
|
const identityMatches = prs.filter(pr => getPrIdentityKey(pr, project) === canonicalId);
|
|
7578
7182
|
let target = identityMatches.length > 0 ? _pickBestPrRecord(identityMatches) : null;
|
|
@@ -7736,7 +7340,7 @@ function classifyPrRefForVerification(prRef, project = null) {
|
|
|
7736
7340
|
return null;
|
|
7737
7341
|
}
|
|
7738
7342
|
|
|
7739
|
-
// W-mq5wfh1v000e0da9 — Auto-enroll a PR into
|
|
7343
|
+
// W-mq5wfh1v000e0da9 — Auto-enroll a PR into tracked state when a
|
|
7740
7344
|
// `type: fix` work item is created with a structured PR pointer. Without
|
|
7741
7345
|
// this, fix WIs against untracked PRs bypass the polling / review / build
|
|
7742
7346
|
// pipelines (the engine `pr_not_found` gate blocks dispatch entirely, and
|
|
@@ -7754,17 +7358,22 @@ function classifyPrRefForVerification(prRef, project = null) {
|
|
|
7754
7358
|
//
|
|
7755
7359
|
// Returns:
|
|
7756
7360
|
// { skipped: true, reason } — not a pr-requiring type / no ref / no URL / upsert error
|
|
7757
|
-
// { alreadyEnrolled: true, id } — PR already
|
|
7758
|
-
// { enrolled: true, id,
|
|
7759
|
-
const _PR_REQUIRING_TYPES = new Set([
|
|
7361
|
+
// { alreadyEnrolled: true, id } — PR already tracked
|
|
7362
|
+
// { enrolled: true, id, scope } — newly enrolled
|
|
7363
|
+
const _PR_REQUIRING_TYPES = new Set([
|
|
7364
|
+
WORK_TYPE.FIX,
|
|
7365
|
+
WORK_TYPE.BUILD_FIX_COMPLEX,
|
|
7366
|
+
WORK_TYPE.REVIEW,
|
|
7367
|
+
WORK_TYPE.TEST,
|
|
7368
|
+
]);
|
|
7760
7369
|
function autoEnrollPrFromWorkItem(item, project, minionsDir) {
|
|
7761
7370
|
if (!item || !_PR_REQUIRING_TYPES.has(item.type)) return { skipped: true, reason: 'not-pr-requiring-type' };
|
|
7762
7371
|
const prRef = extractStructuredWorkItemPrRef(item);
|
|
7763
7372
|
if (!prRef) return { skipped: true, reason: 'no-structured-ref' };
|
|
7764
7373
|
const url = deriveUrlForPrRef(prRef, project);
|
|
7765
7374
|
if (!url) return { skipped: true, reason: 'no-url' };
|
|
7766
|
-
const
|
|
7767
|
-
const existing =
|
|
7375
|
+
const source = project || 'central';
|
|
7376
|
+
const existing = readPullRequests(source);
|
|
7768
7377
|
if (findPrRecord(existing, prRef, project)) {
|
|
7769
7378
|
return { alreadyEnrolled: true, id: getCanonicalPrId(project, prRef, url) };
|
|
7770
7379
|
}
|
|
@@ -7772,7 +7381,7 @@ function autoEnrollPrFromWorkItem(item, project, minionsDir) {
|
|
|
7772
7381
|
const prNum = parsedUrl ? parsedUrl.prNumber : null;
|
|
7773
7382
|
const prId = getCanonicalPrId(project, prRef, url);
|
|
7774
7383
|
try {
|
|
7775
|
-
const result = upsertPullRequestRecord(
|
|
7384
|
+
const result = upsertPullRequestRecord(source, {
|
|
7776
7385
|
id: prId,
|
|
7777
7386
|
prNumber: prNum,
|
|
7778
7387
|
title: `PR #${prNum != null ? prNum : '?'} (polling...)`,
|
|
@@ -7786,7 +7395,7 @@ function autoEnrollPrFromWorkItem(item, project, minionsDir) {
|
|
|
7786
7395
|
contextOnly: false,
|
|
7787
7396
|
}, { project, itemId: item.id });
|
|
7788
7397
|
return result.created
|
|
7789
|
-
? { enrolled: true, id: result.id,
|
|
7398
|
+
? { enrolled: true, id: result.id, scope: stateScope(source) }
|
|
7790
7399
|
: { alreadyEnrolled: true, id: result.id };
|
|
7791
7400
|
} catch (e) {
|
|
7792
7401
|
log('warn', `autoEnrollPrFromWorkItem ${item.id}: ${e.message}`);
|
|
@@ -7820,47 +7429,35 @@ function clearWorktreeFailureCache(wtPath) {
|
|
|
7820
7429
|
|
|
7821
7430
|
// ─── Work Items & Pull Requests Mutation Helpers ────────────────────────────
|
|
7822
7431
|
|
|
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;
|
|
7432
|
+
function stateScope(source) {
|
|
7433
|
+
if (source == null || source === '') return 'central';
|
|
7434
|
+
if (source && typeof source === 'object') {
|
|
7435
|
+
if (source.isCentral) return 'central';
|
|
7436
|
+
source = source.scope ?? source.name ?? source.project;
|
|
7437
|
+
}
|
|
7438
|
+
if (typeof source !== 'string') throw new TypeError('state scope requires a project, scope name, or null');
|
|
7439
|
+
const scope = source.trim();
|
|
7440
|
+
if (!scope || scope === 'central') return 'central';
|
|
7441
|
+
if (/[\\/]/.test(scope) || /\.json$/i.test(scope)) {
|
|
7442
|
+
throw new TypeError('state scope must be a project name, not a file path');
|
|
7850
7443
|
}
|
|
7444
|
+
return scope;
|
|
7445
|
+
}
|
|
7851
7446
|
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
7862
|
-
},
|
|
7447
|
+
function readWorkItems(source = null) {
|
|
7448
|
+
return require('./work-items-store').readWorkItemsForScope(stateScope(source));
|
|
7449
|
+
}
|
|
7450
|
+
|
|
7451
|
+
function mutateWorkItems(source, mutator) {
|
|
7452
|
+
const store = require('./work-items-store');
|
|
7453
|
+
const { wrote, result } = store.applyWorkItemsMutation(stateScope(source), (items) => {
|
|
7454
|
+
if (!Array.isArray(items)) items = [];
|
|
7455
|
+
return mutator(items) || items;
|
|
7863
7456
|
});
|
|
7457
|
+
if (wrote) {
|
|
7458
|
+
try { require('./db-events').emitStateEvent('work_items'); } catch { /* optional */ }
|
|
7459
|
+
try { require('./queries').invalidateWorkItemsCache(); } catch { /* queries not loaded */ }
|
|
7460
|
+
}
|
|
7864
7461
|
return result;
|
|
7865
7462
|
}
|
|
7866
7463
|
|
|
@@ -7877,43 +7474,20 @@ function reopenWorkItem(wi) {
|
|
|
7877
7474
|
wi._retryCount = 0;
|
|
7878
7475
|
}
|
|
7879
7476
|
|
|
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
|
-
}
|
|
7477
|
+
function readPullRequests(source = null) {
|
|
7478
|
+
return require('./pull-requests-store').readPullRequestsForScope(stateScope(source));
|
|
7479
|
+
}
|
|
7905
7480
|
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
skipWriteIfUnchanged: true,
|
|
7912
|
-
_skipSqlRouting: true,
|
|
7913
|
-
onWrote: () => {
|
|
7914
|
-
try { require('./db-events').emitStateEvent('pull_requests'); } catch { /* optional */ }
|
|
7915
|
-
},
|
|
7481
|
+
function mutatePullRequests(source, mutator) {
|
|
7482
|
+
const store = require('./pull-requests-store');
|
|
7483
|
+
const { wrote, result } = store.applyPullRequestsMutation(stateScope(source), (prs) => {
|
|
7484
|
+
if (!Array.isArray(prs)) prs = [];
|
|
7485
|
+
return mutator(prs) || prs;
|
|
7916
7486
|
});
|
|
7487
|
+
if (wrote) {
|
|
7488
|
+
try { require('./db-events').emitStateEvent('pull_requests'); } catch { /* optional */ }
|
|
7489
|
+
}
|
|
7490
|
+
return result;
|
|
7917
7491
|
}
|
|
7918
7492
|
|
|
7919
7493
|
/**
|
|
@@ -8818,11 +8392,7 @@ module.exports = {
|
|
|
8818
8392
|
writeStopIntent,
|
|
8819
8393
|
clearStopIntent,
|
|
8820
8394
|
isStopIntentSet,
|
|
8821
|
-
COOLDOWNS_PATH,
|
|
8822
|
-
ENGINE_STATE_PATH, // W-mp60tw0u000j3931
|
|
8823
|
-
PR_LINKS_PATH,
|
|
8824
8395
|
PINNED_ITEMS_PATH,
|
|
8825
|
-
LOG_PATH,
|
|
8826
8396
|
CONSTELLATION_BRIDGE_MARKER_PATH,
|
|
8827
8397
|
CONSTELLATION_BRIDGE_MARKER_SCHEMA_VERSION,
|
|
8828
8398
|
DEFAULT_DASHBOARD_PORT,
|
|
@@ -8860,14 +8430,12 @@ module.exports = {
|
|
|
8860
8430
|
forEachPidFile,
|
|
8861
8431
|
resolveMinionsHome,
|
|
8862
8432
|
saveMinionsRootPointer,
|
|
8863
|
-
neutralizeJsonBackupSidecar,
|
|
8864
8433
|
PROMPT_CONTEXTS_DIR,
|
|
8865
8434
|
dispatchPromptSidecarPath,
|
|
8866
8435
|
dispatchCompletionReportPath,
|
|
8867
8436
|
sidecarDispatchPrompt,
|
|
8868
8437
|
resolveDispatchPrompt,
|
|
8869
8438
|
deleteDispatchPromptSidecar,
|
|
8870
|
-
assertStateFileSize,
|
|
8871
8439
|
withFileLock,
|
|
8872
8440
|
mutateJsonFileLocked,
|
|
8873
8441
|
mutateControl, recordEngineRespawn,
|
|
@@ -8878,8 +8446,11 @@ module.exports = {
|
|
|
8878
8446
|
mutateKbSwept, // Phase 9.3
|
|
8879
8447
|
mutateKbSweepState, // Phase 9.3
|
|
8880
8448
|
mutateTestResults, // Phase 9.3
|
|
8449
|
+
stateScope,
|
|
8450
|
+
readWorkItems,
|
|
8881
8451
|
mutateWorkItems,
|
|
8882
8452
|
reopenWorkItem,
|
|
8453
|
+
readPullRequests,
|
|
8883
8454
|
mutatePullRequests,
|
|
8884
8455
|
uid,
|
|
8885
8456
|
uniquePath,
|
|
@@ -8925,7 +8496,7 @@ module.exports = {
|
|
|
8925
8496
|
runtimeConfigWarnings,
|
|
8926
8497
|
projectWorkSourceWarnings,
|
|
8927
8498
|
backfillProjectWorkSourceDefaults,
|
|
8928
|
-
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, isPrdArchived, isDefunctPrd, WORK_TYPE, WORKTREE_REQUIRING_TYPES, VALID_WORK_TYPES, resolveWorkItemTypeFromPrdItem, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, RETRY_DELAY_MS, ADO_TOKEN_REFRESH_MAX_RETRIES, DISPATCH_RESULT, mutateMetrics, mutateWatches, mutateScheduleRuns, mutatePipelineRuns, mutateManagedProcesses, mutateWorktreePool, mutateQaRuns, mutateQaSessions, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject, extractPlanTargetProjects,
|
|
8499
|
+
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, isPrdArchived, isDefunctPrd, WORK_TYPE, isFixLikeWorkType, WORKTREE_REQUIRING_TYPES, LIVE_VALIDATION_WORK_TYPES, VALID_WORK_TYPES, resolveWorkItemTypeFromPrdItem, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, RETRY_DELAY_MS, ADO_TOKEN_REFRESH_MAX_RETRIES, DISPATCH_RESULT, mutateMetrics, mutateWatches, mutateScheduleRuns, mutatePipelineRuns, mutateManagedProcesses, mutateWorktreePool, mutateQaRuns, mutateQaSessions, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject, extractPlanTargetProjects,
|
|
8929
8500
|
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS, WATCH_ACTION_TYPE,
|
|
8930
8501
|
WATCH_STALLED_DEFAULT_TICKS, WATCH_STUCK_STAGE_DEFAULT_TICKS,
|
|
8931
8502
|
PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
|
|
@@ -8950,17 +8521,11 @@ module.exports = {
|
|
|
8950
8521
|
resolveConfiguredProject,
|
|
8951
8522
|
resolveProjectSource,
|
|
8952
8523
|
projectRoot,
|
|
8953
|
-
centralWorkItemsPath,
|
|
8954
|
-
centralPullRequestsPath,
|
|
8955
8524
|
projectStateDir,
|
|
8956
8525
|
projectStateDirEnsure,
|
|
8957
|
-
|
|
8958
|
-
projectPrPath,
|
|
8959
|
-
ensureProjectStateFiles,
|
|
8526
|
+
initializeProjectState,
|
|
8960
8527
|
sameResolvedPath,
|
|
8961
8528
|
realPathForComparison, // exported for testing
|
|
8962
|
-
prPathComparisonCandidates, // exported for testing
|
|
8963
|
-
resolveProjectForPrPath, // exported for testing
|
|
8964
8529
|
getPrLinks,
|
|
8965
8530
|
addPrLink,
|
|
8966
8531
|
normalizePrScopeSegment, // exported for testing
|
|
@@ -9051,6 +8616,10 @@ module.exports = {
|
|
|
9051
8616
|
CHECKOUT_MODES,
|
|
9052
8617
|
validateCheckoutMode,
|
|
9053
8618
|
validateLiveValidation,
|
|
8619
|
+
normalizeLiveValidationWorkType,
|
|
8620
|
+
resolveLiveValidationTypes,
|
|
8621
|
+
isLiveValidationConfigInvalid,
|
|
8622
|
+
resolveLiveValidationAutoDispatchType,
|
|
9054
8623
|
resolveCheckoutMode,
|
|
9055
8624
|
isLiveCheckoutProject,
|
|
9056
8625
|
resolveLiveCheckoutAutoReset,
|