@yemi33/minions 0.1.2144 → 0.1.2146
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 +85 -0
- package/bin/minions.js.rej +16 -0
- package/dashboard/js/refresh.js +14 -0
- package/dashboard/js/render-pinned.js +119 -3
- package/dashboard/js/utils.js +20 -0
- package/dashboard/layout.html +1 -0
- package/dashboard/slim/body.html +113 -1
- package/dashboard/slim/body.html.rej +11 -0
- package/dashboard/slim/js/command-send.js.rej +12 -0
- package/dashboard/slim/js/helpers.js +9 -0
- package/dashboard/slim/js/history.js +153 -88
- package/dashboard/slim/js/history.js.rej +26 -0
- package/dashboard/slim/js/modals-tiles.js +8 -2
- package/dashboard/slim/js/pinned.js +182 -0
- package/dashboard/slim/js/settings.js +126 -6
- package/dashboard/slim/js/status.js +9 -6
- package/dashboard/slim/layout.html +1 -0
- package/dashboard/slim/styles.css +77 -2
- package/dashboard/slim/styles.css.rej +124 -0
- package/dashboard/styles.css +19 -0
- package/dashboard-build.js +9 -2
- package/dashboard.js +44 -1
- package/docs/README.md.rej +9 -0
- package/docs/auto-discovery.md +2 -2
- package/docs/constellation-style-telemetry.md +161 -0
- package/docs/engine-restart.md +1 -1
- package/docs/kb-sweep.md +2 -2
- package/docs/managed-spawn.md +1 -1
- package/docs/watches.md +11 -11
- package/engine/cleanup.js +9 -0
- package/engine/cli.js +57 -12
- package/engine/features.js +11 -0
- package/engine/shared.js +290 -37
- package/engine/watchdog.js +458 -0
- package/engine/worktree-gc.js +9 -1
- package/engine.js +57 -8
- package/package.json +1 -1
package/engine/shared.js
CHANGED
|
@@ -2906,6 +2906,51 @@ function _resetLegacyCcModelMigrationFlag() {
|
|
|
2906
2906
|
_legacyCcModelMigrationLogged = false;
|
|
2907
2907
|
}
|
|
2908
2908
|
|
|
2909
|
+
/**
|
|
2910
|
+
* One-time force-on for the CC worker pool.
|
|
2911
|
+
*
|
|
2912
|
+
* The pool has been the resolved default for copilot CC since PR #2492
|
|
2913
|
+
* (`resolveCcUseWorkerPool` returns true when the config has no explicit
|
|
2914
|
+
* value). But configs that carried an explicit `ccUseWorkerPool: false` —
|
|
2915
|
+
* set before the default flipped, or copied from an old template — stay
|
|
2916
|
+
* opted out forever. This migration flips those explicit-false opt-outs to
|
|
2917
|
+
* `true` ONCE, on both surfaces the value lives in (`engine.ccUseWorkerPool`
|
|
2918
|
+
* which the resolver reads, and `features.ccUseWorkerPool` which drives the
|
|
2919
|
+
* Settings toggle / `isFeatureOn`), so the two stay consistent.
|
|
2920
|
+
*
|
|
2921
|
+
* Idempotency: records `engine._ccPoolForcedOnV1` after running. Once that
|
|
2922
|
+
* marker is set the migration never touches the value again — so an operator
|
|
2923
|
+
* who *deliberately* turns the pool off afterward is never re-forced. The
|
|
2924
|
+
* marker is persisted even when nothing needed flipping, which is what lets a
|
|
2925
|
+
* later opt-out be distinguished from a never-migrated config.
|
|
2926
|
+
*
|
|
2927
|
+
* Pure + idempotent: safe to call in-memory then re-apply to the on-disk copy
|
|
2928
|
+
* under a lock (mirrors backfillProjectWorkSourceDefaults). Returns
|
|
2929
|
+
* `{ changed, flipped }` — `changed` true means the config was mutated (marker
|
|
2930
|
+
* set and/or values flipped) and should be persisted; `flipped` lists which
|
|
2931
|
+
* surfaces ('engine'/'features') were actually turned on.
|
|
2932
|
+
*/
|
|
2933
|
+
function applyCcWorkerPoolForceOnMigration(config) {
|
|
2934
|
+
const result = { changed: false, flipped: [] };
|
|
2935
|
+
if (!config || typeof config !== 'object') return result;
|
|
2936
|
+
const engine = (config.engine && typeof config.engine === 'object') ? config.engine : null;
|
|
2937
|
+
if (!engine) return result; // no engine section — retry next start, harmless
|
|
2938
|
+
if (engine._ccPoolForcedOnV1) return result; // already forced once; respect later opt-out
|
|
2939
|
+
|
|
2940
|
+
if (engine.ccUseWorkerPool === false) {
|
|
2941
|
+
engine.ccUseWorkerPool = true;
|
|
2942
|
+
result.flipped.push('engine');
|
|
2943
|
+
}
|
|
2944
|
+
const features = (config.features && typeof config.features === 'object') ? config.features : null;
|
|
2945
|
+
if (features && features.ccUseWorkerPool === false) {
|
|
2946
|
+
features.ccUseWorkerPool = true;
|
|
2947
|
+
result.flipped.push('features');
|
|
2948
|
+
}
|
|
2949
|
+
engine._ccPoolForcedOnV1 = true; // marker — never force again
|
|
2950
|
+
result.changed = true; // marker always needs persisting on first run
|
|
2951
|
+
return result;
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2909
2954
|
// ─── Runtime Config Preflight Warnings ──────────────────────────────────────
|
|
2910
2955
|
//
|
|
2911
2956
|
// Emit non-fatal warnings about runtime/CLI configuration drift. Consumed by
|
|
@@ -5547,39 +5592,17 @@ function normalizePrLinkItems(value) {
|
|
|
5547
5592
|
return [...new Set(items.filter(item => typeof item === 'string' && item))];
|
|
5548
5593
|
}
|
|
5549
5594
|
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
* sourcePlan/itemType cases. See W-mq5rs2eq000da8a9 for the bug repro.
|
|
5560
|
-
*
|
|
5561
|
-
* A PR is auto-managed when ANY of the following hold (and `_contextOnly` is
|
|
5562
|
-
* not explicitly true — context-only always wins):
|
|
5563
|
-
* - It carries one or more `prdItems` (linked to work items)
|
|
5564
|
-
* - It was explicitly opted-in via `_autoObserve: true` (manual link with
|
|
5565
|
-
* autoObserve, or per-row toggle via POST /api/pull-requests/observe)
|
|
5566
|
-
* - It was created from a plan or has an itemType (`sourcePlan`/`itemType`)
|
|
5567
|
-
* - It has a non-empty agent author other than the literal string `'human'`
|
|
5568
|
-
* (the helper deliberately does NOT require the agent be in
|
|
5569
|
-
* `config.agents` — once `_autoObserve` is the explicit opt-in for
|
|
5570
|
-
* non-agent authors, the loosened agent-string check has no operational
|
|
5571
|
-
* downside for managed flows and avoids the silent-skip bug.)
|
|
5572
|
-
*
|
|
5573
|
-
* @param {object} pr Pull request record
|
|
5574
|
-
* @returns {boolean} true when the engine should drive review/fix for this PR
|
|
5575
|
-
*/
|
|
5595
|
+
// W-mq5s5ttx000j7ab8-a — canonical `contextOnly` gate. The 6-clause legacy
|
|
5596
|
+
// body (prdItems / _autoObserve / sourcePlan / itemType / agent-heuristic) is
|
|
5597
|
+
// replaced by a single read of the canonical field. The one-shot boot
|
|
5598
|
+
// migration (migratePrGateFlags) projects the legacy signals onto
|
|
5599
|
+
// `contextOnly` so every on-disk record has the canonical value before any
|
|
5600
|
+
// tick fires. Anything new written via upsertPullRequestRecord (and the
|
|
5601
|
+
// dashboard link/observe paths consolidated in PR #3137 / `-c`) also lands
|
|
5602
|
+
// on `contextOnly` directly. See W-mq5rs2eq000da8a9 for the bug repro that
|
|
5603
|
+
// motivated consolidating onto a single helper.
|
|
5576
5604
|
function isAutoManagedPrRecord(pr) {
|
|
5577
|
-
|
|
5578
|
-
if (normalizePrLinkItems(pr.prdItems).length > 0) return true;
|
|
5579
|
-
if (pr._autoObserve === true) return true;
|
|
5580
|
-
if (pr.sourcePlan || pr.itemType) return true;
|
|
5581
|
-
const agent = String(pr.agent || '').trim().toLowerCase();
|
|
5582
|
-
return !!agent && agent !== 'human';
|
|
5605
|
+
return !!pr && typeof pr === 'object' && pr.contextOnly !== true;
|
|
5583
5606
|
}
|
|
5584
5607
|
|
|
5585
5608
|
function mergePrLinkItems(links, prId, itemIds) {
|
|
@@ -5757,12 +5780,24 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
5757
5780
|
target[key] = normalizedEntry[key];
|
|
5758
5781
|
}
|
|
5759
5782
|
}
|
|
5760
|
-
|
|
5783
|
+
// W-mq5s5ttx000j7ab8-a — `_manual` and `_autoObserve` are no longer
|
|
5784
|
+
// copied through; the engine reads gate state from the canonical
|
|
5785
|
+
// `contextOnly` field (see isAutoManagedPrRecord above + the boot
|
|
5786
|
+
// migration migratePrGateFlags). `_context`/`_projectResolution` are
|
|
5787
|
+
// unrelated breadcrumbs and stay.
|
|
5788
|
+
for (const key of ['_context', '_projectResolution']) {
|
|
5761
5789
|
if (normalizedEntry[key] != null) target[key] = normalizedEntry[key];
|
|
5762
5790
|
}
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5791
|
+
// Accept either the canonical `contextOnly` (preferred) or the legacy
|
|
5792
|
+
// `_contextOnly` from callers that haven't been migrated yet
|
|
5793
|
+
// (dashboard.js manual-link path, lifecycle.js oneShot tagging — items
|
|
5794
|
+
// (b)/(c) of the decomposition). Persist as canonical `contextOnly`.
|
|
5795
|
+
const incomingContextOnly = normalizedEntry.contextOnly != null
|
|
5796
|
+
? normalizedEntry.contextOnly
|
|
5797
|
+
: normalizedEntry._contextOnly;
|
|
5798
|
+
if (incomingContextOnly != null) {
|
|
5799
|
+
const wouldDemoteManagedPr = incomingContextOnly === true && targetWasAutoManaged;
|
|
5800
|
+
if (!wouldDemoteManagedPr) target.contextOnly = incomingContextOnly === true;
|
|
5766
5801
|
}
|
|
5767
5802
|
}
|
|
5768
5803
|
target.prdItems = normalizePrLinkItems(target.prdItems || []);
|
|
@@ -5785,6 +5820,105 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
5785
5820
|
return { id: canonicalId, prNumber, created, linked, skipped, record };
|
|
5786
5821
|
}
|
|
5787
5822
|
|
|
5823
|
+
// ─── PR Gate Migration (W-mq5s5ttx000j7ab8-a) ───────────────────────────────
|
|
5824
|
+
//
|
|
5825
|
+
// One-shot boot migration that projects the legacy gate signals
|
|
5826
|
+
// (`_contextOnly`, `_autoObserve`, `_manual`) onto the canonical
|
|
5827
|
+
// `contextOnly` field on every `projects/<name>/pull-requests.json` record.
|
|
5828
|
+
// Wired from `engine/cli.js#start()` before the first tick fires so the new
|
|
5829
|
+
// `isAutoManagedPrRecord` (which only reads `contextOnly`) returns the same
|
|
5830
|
+
// verdict as the legacy 6-clause helper for pre-existing data.
|
|
5831
|
+
//
|
|
5832
|
+
// Per-record decision:
|
|
5833
|
+
// 1. contextOnly := (_contextOnly === true)
|
|
5834
|
+
// 2. legacyManaged := prdItems.length > 0 || _autoObserve === true ||
|
|
5835
|
+
// sourcePlan || itemType || (agent is a Minions persona)
|
|
5836
|
+
// "Minions persona" = non-empty kebab-case identifier other than 'human'.
|
|
5837
|
+
// Anything with whitespace (human display names like "yemi shin", which
|
|
5838
|
+
// github poller writes into `agent` from `prData.user.login`) is treated
|
|
5839
|
+
// as a human author, not a managed agent.
|
|
5840
|
+
// 3. If !contextOnly && !legacyManaged, set contextOnly = true and stamp
|
|
5841
|
+
// `_migrationNote: 'auto-set-contextOnly-by-pr-gate-simplification'`.
|
|
5842
|
+
// 4. Persist `contextOnly`; delete `_autoObserve`, `_manual`, `_contextOnly`.
|
|
5843
|
+
//
|
|
5844
|
+
// Idempotent: records that already have `contextOnly` and none of the legacy
|
|
5845
|
+
// keys are skipped (no rewrite, no log line, JSON mtime unchanged).
|
|
5846
|
+
const _PR_GATE_MIGRATION_NOTE = 'auto-set-contextOnly-by-pr-gate-simplification';
|
|
5847
|
+
const _AGENT_PERSONA_RE = /^[a-z0-9_-]+$/;
|
|
5848
|
+
|
|
5849
|
+
function _prRecordHasLegacyGateKey(record) {
|
|
5850
|
+
return Object.prototype.hasOwnProperty.call(record, '_contextOnly')
|
|
5851
|
+
|| Object.prototype.hasOwnProperty.call(record, '_autoObserve')
|
|
5852
|
+
|| Object.prototype.hasOwnProperty.call(record, '_manual');
|
|
5853
|
+
}
|
|
5854
|
+
|
|
5855
|
+
function _prRecordIsLegacyManaged(record) {
|
|
5856
|
+
const prdItemsCount = Array.isArray(record.prdItems) ? record.prdItems.length : 0;
|
|
5857
|
+
if (prdItemsCount > 0) return true;
|
|
5858
|
+
if (record._autoObserve === true) return true;
|
|
5859
|
+
if (record.sourcePlan) return true;
|
|
5860
|
+
if (record.itemType) return true;
|
|
5861
|
+
if (typeof record.agent === 'string') {
|
|
5862
|
+
const agent = record.agent.trim().toLowerCase();
|
|
5863
|
+
if (agent && agent !== 'human' && _AGENT_PERSONA_RE.test(agent)) return true;
|
|
5864
|
+
}
|
|
5865
|
+
return false;
|
|
5866
|
+
}
|
|
5867
|
+
|
|
5868
|
+
function migratePrGateFlags(projectsRoot) {
|
|
5869
|
+
const summary = { projectsScanned: 0, projectsMigrated: 0, totalRecords: 0, totalMigrated: 0 };
|
|
5870
|
+
if (!projectsRoot || typeof projectsRoot !== 'string') return summary;
|
|
5871
|
+
let entries;
|
|
5872
|
+
try {
|
|
5873
|
+
entries = fs.readdirSync(projectsRoot, { withFileTypes: true });
|
|
5874
|
+
} catch {
|
|
5875
|
+
return summary;
|
|
5876
|
+
}
|
|
5877
|
+
for (const entry of entries) {
|
|
5878
|
+
if (!entry.isDirectory()) continue;
|
|
5879
|
+
const projectName = entry.name;
|
|
5880
|
+
const prPath = path.join(projectsRoot, projectName, 'pull-requests.json');
|
|
5881
|
+
if (!fs.existsSync(prPath)) continue;
|
|
5882
|
+
summary.projectsScanned++;
|
|
5883
|
+
|
|
5884
|
+
let migrated = 0;
|
|
5885
|
+
let stamped = 0;
|
|
5886
|
+
let alreadyMigrated = 0;
|
|
5887
|
+
|
|
5888
|
+
mutatePullRequests(prPath, (prs) => {
|
|
5889
|
+
if (!Array.isArray(prs)) return prs;
|
|
5890
|
+
for (const record of prs) {
|
|
5891
|
+
if (!record || typeof record !== 'object') continue;
|
|
5892
|
+
const hasLegacy = _prRecordHasLegacyGateKey(record);
|
|
5893
|
+
const hasCanonical = Object.prototype.hasOwnProperty.call(record, 'contextOnly');
|
|
5894
|
+
if (hasCanonical && !hasLegacy) { alreadyMigrated++; continue; }
|
|
5895
|
+
|
|
5896
|
+
let contextOnly = (record._contextOnly === true);
|
|
5897
|
+
if (!contextOnly && !_prRecordIsLegacyManaged(record)) {
|
|
5898
|
+
contextOnly = true;
|
|
5899
|
+
record._migrationNote = _PR_GATE_MIGRATION_NOTE;
|
|
5900
|
+
stamped++;
|
|
5901
|
+
}
|
|
5902
|
+
record.contextOnly = contextOnly;
|
|
5903
|
+
delete record._autoObserve;
|
|
5904
|
+
delete record._manual;
|
|
5905
|
+
delete record._contextOnly;
|
|
5906
|
+
migrated++;
|
|
5907
|
+
}
|
|
5908
|
+
return prs;
|
|
5909
|
+
});
|
|
5910
|
+
|
|
5911
|
+
summary.totalRecords += (migrated + alreadyMigrated);
|
|
5912
|
+
summary.totalMigrated += migrated;
|
|
5913
|
+
if (migrated > 0) {
|
|
5914
|
+
summary.projectsMigrated++;
|
|
5915
|
+
// One line per project that actually moved. Keep idempotent runs silent.
|
|
5916
|
+
console.log(`[pr-gate-migration] ${projectName}: migrated ${migrated} records (${stamped} stamped contextOnly, ${alreadyMigrated} already-migrated)`);
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
return summary;
|
|
5920
|
+
}
|
|
5921
|
+
|
|
5788
5922
|
// ─── Cross-Platform Process Kill Helpers ─────────────────────────────────────
|
|
5789
5923
|
|
|
5790
5924
|
function normalizeKillPid(proc) {
|
|
@@ -6442,13 +6576,126 @@ function _purgeReservedFiles(dirPath) {
|
|
|
6442
6576
|
}
|
|
6443
6577
|
}
|
|
6444
6578
|
|
|
6445
|
-
|
|
6579
|
+
// ── Live-worktree guard (W-mq5rwwss000f30a7) ─────────────────────────────────
|
|
6580
|
+
// Single source of truth for "is some non-terminal dispatch currently using
|
|
6581
|
+
// this worktree?" Every code path that wants to wipe / reset / recycle /
|
|
6582
|
+
// quarantine a worktree MUST call isWorktreePathLive() first and skip on
|
|
6583
|
+
// true. Without this guard the engine has wiped agents mid-task four times
|
|
6584
|
+
// in a row (W-mq5n1zx5000hcfb5 post-mortem) by reaping a worktree whose
|
|
6585
|
+
// dispatch was still active.
|
|
6586
|
+
//
|
|
6587
|
+
// Fail-open semantics: when SQLite is unreachable or the query throws, the
|
|
6588
|
+
// helper returns true (assume live). Better to leak a worktree than nuke
|
|
6589
|
+
// an agent's unpushed work.
|
|
6590
|
+
|
|
6591
|
+
function _normalizeWorktreePath(p) {
|
|
6592
|
+
if (!p || typeof p !== 'string') return '';
|
|
6593
|
+
let resolved;
|
|
6594
|
+
try { resolved = path.resolve(p); }
|
|
6595
|
+
catch { return ''; }
|
|
6596
|
+
resolved = resolved.replace(/\\/g, '/').replace(/\/+$/g, '');
|
|
6597
|
+
if (process.platform === 'win32') resolved = resolved.toLowerCase();
|
|
6598
|
+
return resolved;
|
|
6599
|
+
}
|
|
6600
|
+
|
|
6601
|
+
function isWorktreePathLive(worktreePath, opts = {}) {
|
|
6602
|
+
if (!worktreePath) return false;
|
|
6603
|
+
const target = _normalizeWorktreePath(worktreePath);
|
|
6604
|
+
if (!target) return false;
|
|
6605
|
+
const excludeDispatchId = opts.excludeDispatchId ? String(opts.excludeDispatchId) : null;
|
|
6606
|
+
let db = opts.db || null;
|
|
6607
|
+
if (!db) {
|
|
6608
|
+
try { db = require('./db').getDb(); }
|
|
6609
|
+
catch (e) {
|
|
6610
|
+
log('warn', `isWorktreePathLive: SQL unavailable for ${worktreePath} (${e.message}) — fail-open (assume live)`);
|
|
6611
|
+
return true;
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6614
|
+
if (!db) {
|
|
6615
|
+
log('warn', `isWorktreePathLive: no db handle for ${worktreePath} — fail-open (assume live)`);
|
|
6616
|
+
return true;
|
|
6617
|
+
}
|
|
6618
|
+
let rows;
|
|
6619
|
+
try {
|
|
6620
|
+
rows = db.prepare(`
|
|
6621
|
+
SELECT id,
|
|
6622
|
+
json_extract(data, '$.worktreePath') AS top_wt,
|
|
6623
|
+
json_extract(data, '$.meta.worktreePath') AS meta_wt
|
|
6624
|
+
FROM dispatches
|
|
6625
|
+
WHERE status IN ('pending', 'active')
|
|
6626
|
+
`).all();
|
|
6627
|
+
} catch (e) {
|
|
6628
|
+
log('warn', `isWorktreePathLive: query threw for ${worktreePath} (${e.message}) — fail-open (assume live)`);
|
|
6629
|
+
return true;
|
|
6630
|
+
}
|
|
6631
|
+
for (const row of rows || []) {
|
|
6632
|
+
if (excludeDispatchId && String(row.id) === excludeDispatchId) continue;
|
|
6633
|
+
if (row.top_wt && _normalizeWorktreePath(row.top_wt) === target) return true;
|
|
6634
|
+
if (row.meta_wt && _normalizeWorktreePath(row.meta_wt) === target) return true;
|
|
6635
|
+
}
|
|
6636
|
+
return false;
|
|
6637
|
+
}
|
|
6638
|
+
|
|
6639
|
+
// Drop a deduped inbox note when a wipe site skips due to the live guard so
|
|
6640
|
+
// operators can see when the guard fires. Filename is keyed on basename +
|
|
6641
|
+
// UTC date — a single skip per worktree per day produces one note; further
|
|
6642
|
+
// skips that day silently no-op.
|
|
6643
|
+
function _writeWorktreeSkipLiveInboxNote(worktreePath, callerTag) {
|
|
6644
|
+
try {
|
|
6645
|
+
const base = path.basename(String(worktreePath || '').replace(/[\\/]+$/g, '')) || 'unknown';
|
|
6646
|
+
const safeBase = base.replace(/[^a-zA-Z0-9._-]+/g, '-').slice(0, 80);
|
|
6647
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
6648
|
+
const fname = `engine-worktree-skip-live-${safeBase}-${date}.md`;
|
|
6649
|
+
const inboxDir = path.join(MINIONS_DIR, 'notes', 'inbox');
|
|
6650
|
+
try { fs.mkdirSync(inboxDir, { recursive: true }); } catch { /* exists */ }
|
|
6651
|
+
const fpath = path.join(inboxDir, fname);
|
|
6652
|
+
if (fs.existsSync(fpath)) return; // deduped
|
|
6653
|
+
const body = [
|
|
6654
|
+
'---',
|
|
6655
|
+
`id: NOTE-${crypto.randomBytes(8).toString('hex')}`,
|
|
6656
|
+
'agent: engine',
|
|
6657
|
+
`date: ${date}`,
|
|
6658
|
+
'---',
|
|
6659
|
+
'',
|
|
6660
|
+
`# Engine skipped worktree wipe — live dispatch guard fired (W-mq5rwwss000f30a7)`,
|
|
6661
|
+
'',
|
|
6662
|
+
`- caller: ${callerTag || 'unknown'}`,
|
|
6663
|
+
`- worktree: ${worktreePath}`,
|
|
6664
|
+
`- timestamp: ${new Date().toISOString()}`,
|
|
6665
|
+
'',
|
|
6666
|
+
'A non-terminal dispatch row still claims this worktree. The wipe was skipped to',
|
|
6667
|
+
'protect agent state. If this fires repeatedly, inspect engine/state.db dispatches',
|
|
6668
|
+
"table to find which dispatch is stuck claiming the path.",
|
|
6669
|
+
'',
|
|
6670
|
+
].join('\n');
|
|
6671
|
+
fs.writeFileSync(fpath, body);
|
|
6672
|
+
} catch { /* best-effort — never throw from the skip-note writer */ }
|
|
6673
|
+
}
|
|
6674
|
+
|
|
6675
|
+
function removeWorktree(wtPath, gitRoot, worktreeRoot, opts = {}) {
|
|
6446
6676
|
const resolved = path.resolve(wtPath);
|
|
6447
6677
|
const resolvedRoot = path.resolve(worktreeRoot) + path.sep;
|
|
6448
6678
|
if (!resolved.startsWith(resolvedRoot)) {
|
|
6449
6679
|
log('warn', `removeWorktree: refusing to remove ${wtPath} — not under ${worktreeRoot}`);
|
|
6450
6680
|
return false;
|
|
6451
6681
|
}
|
|
6682
|
+
// W-mq5rwwss000f30a7 — never wipe a worktree while an agent is actively
|
|
6683
|
+
// dispatched inside it. isWorktreePathLive fails OPEN (returns true) when
|
|
6684
|
+
// the dispatches table is unreachable, so we err on the side of leaking
|
|
6685
|
+
// the worktree rather than destroying agent work.
|
|
6686
|
+
//
|
|
6687
|
+
// PR #3133 review: callers that are themselves the owning dispatch (e.g.
|
|
6688
|
+
// the W-mpbqhstz001lf518 dispatch-end orphan GC, or the pool-return
|
|
6689
|
+
// chain) can pass `excludeDispatchId` so their OWN active row — which
|
|
6690
|
+
// legitimately claims the worktreePath via the pending→active persistence
|
|
6691
|
+
// at engine.js — is ignored by the guard. Any OTHER non-terminal row
|
|
6692
|
+
// still blocks the wipe.
|
|
6693
|
+
const excludeDispatchId = opts && opts.excludeDispatchId ? String(opts.excludeDispatchId) : null;
|
|
6694
|
+
if (isWorktreePathLive(resolved, excludeDispatchId ? { excludeDispatchId } : undefined)) {
|
|
6695
|
+
log('warn', `removeWorktree: skip — live dispatch in ${wtPath}`);
|
|
6696
|
+
_writeWorktreeSkipLiveInboxNote(wtPath, 'shared.removeWorktree');
|
|
6697
|
+
return false;
|
|
6698
|
+
}
|
|
6452
6699
|
_pruneRemoveWorktreeFailures();
|
|
6453
6700
|
// Skip paths that failed 3+ times — retry after 1 hour cooldown
|
|
6454
6701
|
const prior = _removeWorktreeFailures.get(resolved);
|
|
@@ -6772,6 +7019,7 @@ module.exports = {
|
|
|
6772
7019
|
resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentModel, resolveCcModel,
|
|
6773
7020
|
resolveAgentMaxBudget, resolveAgentBareMode,
|
|
6774
7021
|
applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
|
|
7022
|
+
applyCcWorkerPoolForceOnMigration,
|
|
6775
7023
|
runtimeConfigWarnings,
|
|
6776
7024
|
projectWorkSourceWarnings,
|
|
6777
7025
|
backfillProjectWorkSourceDefaults,
|
|
@@ -6841,6 +7089,8 @@ module.exports = {
|
|
|
6841
7089
|
mergePrLinkItems, // exported for testing
|
|
6842
7090
|
isAutoManagedPrRecord,
|
|
6843
7091
|
upsertPullRequestRecord,
|
|
7092
|
+
isAutoManagedPrRecord, // W-mq5s5ttx000j7ab8-a — exported for engine + watch-plugin gate consolidation
|
|
7093
|
+
migratePrGateFlags, // W-mq5s5ttx000j7ab8-a — boot migration wired from engine/cli.js
|
|
6844
7094
|
nextWorkItemId,
|
|
6845
7095
|
getProjectOrg,
|
|
6846
7096
|
getAdoOrgBase,
|
|
@@ -6893,6 +7143,9 @@ module.exports = {
|
|
|
6893
7143
|
listProcessDescendants,
|
|
6894
7144
|
listProcessReachable,
|
|
6895
7145
|
removeWorktree,
|
|
7146
|
+
isWorktreePathLive,
|
|
7147
|
+
_normalizeWorktreePath, // exported for testing
|
|
7148
|
+
_writeWorktreeSkipLiveInboxNote, // exported for testing
|
|
6896
7149
|
_retryFsOp, // exported for testing (W-mq5o6bvy000x7191)
|
|
6897
7150
|
bumpWorktreeGcMetric, // exported for testing (W-mq5o6bvy000x7191)
|
|
6898
7151
|
_WORKTREE_RETRYABLE_CODES, // exported for testing (W-mq5o6bvy000x7191)
|