@yemi33/minions 0.1.2356 → 0.1.2358
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/dashboard/js/render-pipelines.js +12 -3
- package/dashboard/js/render-prs.js +55 -1
- package/dashboard.js +78 -1
- package/docs/README.md +1 -0
- package/docs/live-checkout-mode.md +6 -0
- package/docs/worktree-lifecycle.md +77 -1
- package/engine/ado.js +1 -1
- package/engine/cleanup.js +4 -4
- package/engine/dispatch.js +37 -5
- package/engine/lifecycle.js +72 -0
- package/engine/live-checkout.js +141 -0
- package/engine/pipeline.js +95 -32
- package/engine/playbook.js +6 -5
- package/engine/queries.js +34 -22
- package/engine/shared.js +53 -1
- package/engine.js +486 -80
- package/package.json +1 -1
package/engine/live-checkout.js
CHANGED
|
@@ -128,6 +128,21 @@
|
|
|
128
128
|
* (signature `(absPath:string) => boolean`, defaulting to fs.existsSync).
|
|
129
129
|
* Production callers omit both and the helper falls back to
|
|
130
130
|
* `require('./shared').shellSafeGit` and `fs.existsSync` respectively.
|
|
131
|
+
*
|
|
132
|
+
* `checkLiveCheckoutDirty` / `checkLiveCheckoutStaleBase` (W-mrcifup2000c218a):
|
|
133
|
+
* ALLOCATION-TIME pre-dispatch counterparts to the dirty / stale-base checks
|
|
134
|
+
* above. Both are cheap, read-only, non-mutating probes engine.js calls
|
|
135
|
+
* BEFORE a live-mode work item is dispatched (in the same allocation loop
|
|
136
|
+
* that already gates on `live_checkout_busy`), so a dirty tree or a
|
|
137
|
+
* contaminated local base leaves the item pending
|
|
138
|
+
* (`_pendingReason: 'live_checkout_dirty'` / `'live_checkout_stale_base'`)
|
|
139
|
+
* instead of burning a retry (dirty, pre-fix behavior) or failing
|
|
140
|
+
* non-retryably on the first hit (stale-base, pre-fix behavior). The
|
|
141
|
+
* in-spawn guards documented above (inside `prepareLiveCheckout`, surfaced by
|
|
142
|
+
* spawnAgent) remain in place as a defense-in-depth fallback for the race
|
|
143
|
+
* window between this probe and the actual git operations inside spawnAgent
|
|
144
|
+
* — they should now fire rarely, as the exception path rather than the
|
|
145
|
+
* common path.
|
|
131
146
|
*/
|
|
132
147
|
|
|
133
148
|
'use strict';
|
|
@@ -1205,6 +1220,130 @@ async function prepareLiveCheckout(opts = {}) {
|
|
|
1205
1220
|
return { ok: true, branch: branchName, created: true, originalRef, originalRefType };
|
|
1206
1221
|
}
|
|
1207
1222
|
|
|
1223
|
+
/**
|
|
1224
|
+
* checkLiveCheckoutDirty — W-mrcifup2000c218a
|
|
1225
|
+
*
|
|
1226
|
+
* Cheap, read-only, NON-MUTATING pre-dispatch probe for a dirty live-checkout
|
|
1227
|
+
* working tree. Mirrors prepareLiveCheckout's Step 1 dirty check (same
|
|
1228
|
+
* `git status --porcelain=v1 -b` command, same porcelain parsing, same
|
|
1229
|
+
* auto-cleanable-artifact allowance) but never deletes anything and never
|
|
1230
|
+
* throws on a git failure — callers use this at ALLOCATION time (before a WI
|
|
1231
|
+
* is dispatched) to decide whether to leave the item pending
|
|
1232
|
+
* (`_pendingReason: 'live_checkout_dirty'`) instead of spawning it only to
|
|
1233
|
+
* have prepareLiveCheckout refuse it after the fact.
|
|
1234
|
+
*
|
|
1235
|
+
* Intentionally permissive on ambiguity: if the `git status` probe itself
|
|
1236
|
+
* fails (transient error), or every dirty entry is an auto-cleanable build
|
|
1237
|
+
* artifact (which prepareLiveCheckout's spawn-time auto-clean step will
|
|
1238
|
+
* delete before it ever re-checks dirtiness), this reports `dirty:false` so
|
|
1239
|
+
* allocation does not block on a condition that resolves itself inside the
|
|
1240
|
+
* spawn path. The in-spawn guard (engine.js ~line 2617) remains the
|
|
1241
|
+
* authoritative check for the race window between this probe and the actual
|
|
1242
|
+
* git operations inside spawnAgent.
|
|
1243
|
+
*
|
|
1244
|
+
* @param {object} opts
|
|
1245
|
+
* @param {string} opts.localPath operator checkout root (git cwd)
|
|
1246
|
+
* @param {object} [opts.gitOpts] extra execFile opts merged into cwd
|
|
1247
|
+
* @param {boolean} [opts.skipDirtyCheck] #522: opt out entirely (GVFS repos)
|
|
1248
|
+
* @param {function} [opts._git] test seam; defaults to shellSafeGit
|
|
1249
|
+
* @returns {Promise<{dirty:boolean, dirtyFiles?:string[], branchInfo?:string, autoCleanable?:boolean, error?:string}>}
|
|
1250
|
+
*/
|
|
1251
|
+
async function checkLiveCheckoutDirty(opts = {}) {
|
|
1252
|
+
const { localPath, gitOpts, skipDirtyCheck, _git } = opts;
|
|
1253
|
+
if (skipDirtyCheck) return { dirty: false };
|
|
1254
|
+
if (!localPath || typeof localPath !== 'string') return { dirty: false };
|
|
1255
|
+
const git = (typeof _git === 'function') ? _git : shared.shellSafeGit;
|
|
1256
|
+
const baseOpts = { cwd: localPath, maxBuffer: LIVE_CHECKOUT_GIT_MAX_BUFFER, ...(gitOpts || {}) };
|
|
1257
|
+
let statusRaw;
|
|
1258
|
+
try {
|
|
1259
|
+
statusRaw = await git(['status', '--porcelain=v1', '-b'], baseOpts);
|
|
1260
|
+
} catch (e) {
|
|
1261
|
+
// A probe failure is not proof of a dirty tree — don't block allocation
|
|
1262
|
+
// on it. spawnAgent's own dirty check (with its LIVE_CHECKOUT_FAILED
|
|
1263
|
+
// classification) is the authoritative fallback for a real git failure.
|
|
1264
|
+
return { dirty: false, error: e.message };
|
|
1265
|
+
}
|
|
1266
|
+
const lines = (typeof statusRaw === 'string' ? statusRaw : '')
|
|
1267
|
+
.split(/\r?\n/)
|
|
1268
|
+
.map((line) => line.replace(/\s+$/, ''))
|
|
1269
|
+
.filter((line) => line.length > 0);
|
|
1270
|
+
const branchInfo = lines.find((line) => line.startsWith('## ')) || '';
|
|
1271
|
+
const dirtyFiles = lines.filter((line) => !line.startsWith('## '));
|
|
1272
|
+
if (dirtyFiles.length === 0) return { dirty: false, branchInfo };
|
|
1273
|
+
const cleanablePaths = _collectAutoCleanablePaths(dirtyFiles);
|
|
1274
|
+
if (cleanablePaths) return { dirty: false, branchInfo, autoCleanable: true };
|
|
1275
|
+
return { dirty: true, dirtyFiles, branchInfo };
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
/**
|
|
1279
|
+
* checkLiveCheckoutStaleBase — W-mrcifup2000c218a
|
|
1280
|
+
*
|
|
1281
|
+
* Cheap, read-only, NON-MUTATING, fetch-free pre-dispatch probe for the
|
|
1282
|
+
* STALE-BASE GUARD condition (W-mr98op8w000ma4ad). Mirrors prepareLiveCheckout's
|
|
1283
|
+
* Step 4d divergence check (same `git rev-list --count origin/<mainRef>..<mainRef>`
|
|
1284
|
+
* comparison) so allocation-time callers can detect a contaminated local base
|
|
1285
|
+
* BEFORE dispatching, instead of burning a non-retryable terminal failure after
|
|
1286
|
+
* the fact. Never fetches, never resets, never mutates anything.
|
|
1287
|
+
*
|
|
1288
|
+
* Scope mirrors prepareLiveCheckout exactly: only relevant when a NEW branch
|
|
1289
|
+
* would be forked (an existing local `branchName` checkout is unaffected by
|
|
1290
|
+
* this class of contamination), when HEAD sits on `mainRef` itself, and when
|
|
1291
|
+
* `origin/<mainRef>` is present locally to compare against. `allowNonMainBase`
|
|
1292
|
+
* (PR-targeted / shared-branch continuations) short-circuits to `stale:false`
|
|
1293
|
+
* — those dispatches intentionally build on a non-main branch and opt out of
|
|
1294
|
+
* this guard, same as prepareLiveCheckout.
|
|
1295
|
+
*
|
|
1296
|
+
* @param {object} opts
|
|
1297
|
+
* @param {string} opts.localPath operator checkout root (git cwd)
|
|
1298
|
+
* @param {string} opts.mainRef project base branch name
|
|
1299
|
+
* @param {object} [opts.gitOpts] extra execFile opts merged into cwd
|
|
1300
|
+
* @param {boolean} [opts.allowNonMainBase] opt out (PR-targeted/shared-branch)
|
|
1301
|
+
* @param {function} [opts._git] test seam; defaults to shellSafeGit
|
|
1302
|
+
* @returns {Promise<{stale:boolean, ahead?:number, mainRef?:string, localMainSha?:string, originMainSha?:string}>}
|
|
1303
|
+
*/
|
|
1304
|
+
async function checkLiveCheckoutStaleBase(opts = {}) {
|
|
1305
|
+
const { localPath, mainRef, gitOpts, allowNonMainBase, _git } = opts;
|
|
1306
|
+
if (allowNonMainBase) return { stale: false };
|
|
1307
|
+
if (!localPath || typeof localPath !== 'string') return { stale: false };
|
|
1308
|
+
if (!mainRef || typeof mainRef !== 'string') return { stale: false };
|
|
1309
|
+
const git = (typeof _git === 'function') ? _git : shared.shellSafeGit;
|
|
1310
|
+
const baseOpts = { cwd: localPath, maxBuffer: LIVE_CHECKOUT_GIT_MAX_BUFFER, ...(gitOpts || {}) };
|
|
1311
|
+
let curBranch = '';
|
|
1312
|
+
try {
|
|
1313
|
+
const raw = await git(['symbolic-ref', '-q', '--short', 'HEAD'], baseOpts);
|
|
1314
|
+
curBranch = (typeof raw === 'string' ? raw : '').trim();
|
|
1315
|
+
} catch {
|
|
1316
|
+
// Detached HEAD or probe error — out of scope for this guard (the
|
|
1317
|
+
// mid-operation/detached-HEAD preflight is a separate concern, still
|
|
1318
|
+
// handled in-spawn only).
|
|
1319
|
+
return { stale: false };
|
|
1320
|
+
}
|
|
1321
|
+
if (!curBranch || curBranch !== mainRef) return { stale: false };
|
|
1322
|
+
let originMainSha = '';
|
|
1323
|
+
try {
|
|
1324
|
+
const originRaw = await git(['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${mainRef}`], baseOpts);
|
|
1325
|
+
originMainSha = (typeof originRaw === 'string' ? originRaw : '').trim();
|
|
1326
|
+
} catch {
|
|
1327
|
+
return { stale: false }; // no remote-tracking ref → ambiguous, skip (local-only/no-remote repo)
|
|
1328
|
+
}
|
|
1329
|
+
if (!originMainSha) return { stale: false };
|
|
1330
|
+
let aheadCount = -1;
|
|
1331
|
+
try {
|
|
1332
|
+
const cntRaw = await git(['rev-list', '--count', `refs/remotes/origin/${mainRef}..${mainRef}`], baseOpts);
|
|
1333
|
+
aheadCount = parseInt((typeof cntRaw === 'string' ? cntRaw : '').trim(), 10);
|
|
1334
|
+
if (Number.isNaN(aheadCount)) aheadCount = -1;
|
|
1335
|
+
} catch {
|
|
1336
|
+
aheadCount = -1; // rev-list failed → transient/ambiguous; don't block on it
|
|
1337
|
+
}
|
|
1338
|
+
if (aheadCount <= 0) return { stale: false };
|
|
1339
|
+
let localMainSha = '';
|
|
1340
|
+
try {
|
|
1341
|
+
const localRaw = await git(['rev-parse', '--verify', '--quiet', `refs/heads/${mainRef}`], baseOpts);
|
|
1342
|
+
localMainSha = (typeof localRaw === 'string' ? localRaw : '').trim();
|
|
1343
|
+
} catch { /* best-effort — informational only */ }
|
|
1344
|
+
return { stale: true, ahead: aheadCount, mainRef, localMainSha, originMainSha };
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1208
1347
|
/**
|
|
1209
1348
|
* restoreLiveCheckoutAtDispatchEnd — P-d9e6b2c4
|
|
1210
1349
|
*
|
|
@@ -1723,6 +1862,8 @@ async function maybeRestoreLiveCheckoutFromRecord(opts = {}) {
|
|
|
1723
1862
|
|
|
1724
1863
|
module.exports = {
|
|
1725
1864
|
prepareLiveCheckout,
|
|
1865
|
+
checkLiveCheckoutDirty,
|
|
1866
|
+
checkLiveCheckoutStaleBase,
|
|
1726
1867
|
restoreLiveCheckoutAtDispatchEnd,
|
|
1727
1868
|
resolveLiveCheckoutAutoStash,
|
|
1728
1869
|
performLiveCheckoutAutoStash,
|
package/engine/pipeline.js
CHANGED
|
@@ -77,6 +77,23 @@ function getPipelineRuns() {
|
|
|
77
77
|
return safeJsonObj(PIPELINE_RUNS_PATH);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// W-mrcrh5hj0017d22f: SQL is the canonical (and only) work-items store post
|
|
81
|
+
// Phase 9.4 — the JSON files (central work-items.json + each
|
|
82
|
+
// projects/<name>/work-items.json) are passive, best-effort mirrors that can
|
|
83
|
+
// legitimately lag behind a just-completed write (mirror failures are
|
|
84
|
+
// swallowed by design so they never block the SQL write; see
|
|
85
|
+
// work-items-store.js's `_mirrorJsonFromSql` comments). Reading those JSON
|
|
86
|
+
// files directly (via safeJson/safeJsonArr) can therefore observe a work
|
|
87
|
+
// item whose `status` mirrored to DONE but whose `resultSummary` write
|
|
88
|
+
// hasn't landed in the mirror yet — which silently degraded stage-output
|
|
89
|
+
// template substitution ({{stages.<id>.output}}) to the stage's own work
|
|
90
|
+
// item id instead of its real completion text. Route every work-item read
|
|
91
|
+
// in this module through the SQL-backed store so pipeline state always sees
|
|
92
|
+
// the authoritative record.
|
|
93
|
+
function _allWorkItems() {
|
|
94
|
+
return require('./work-items-store').readAllWorkItems();
|
|
95
|
+
}
|
|
96
|
+
|
|
80
97
|
function getActiveRun(pipelineId) {
|
|
81
98
|
const runs = getPipelineRuns();
|
|
82
99
|
const pipelineRuns = runs[pipelineId] || [];
|
|
@@ -338,12 +355,7 @@ function evaluateCondition(condition, ctx) {
|
|
|
338
355
|
case 'noFailedItems': {
|
|
339
356
|
// True when all work items created by the pipeline are done (not failed)
|
|
340
357
|
if (!run) return false;
|
|
341
|
-
const
|
|
342
|
-
const workItems = safeJsonArr(wiPath);
|
|
343
|
-
const allProjectWi = shared.getProjects(config).reduce((acc, p) => {
|
|
344
|
-
return acc.concat(safeJson(shared.projectWorkItemsPath(p)) || []);
|
|
345
|
-
}, []);
|
|
346
|
-
const all = [...workItems, ...allProjectWi];
|
|
358
|
+
const all = _allWorkItems();
|
|
347
359
|
const pipelineWis = all.filter(w => w._pipelineRun === run.runId);
|
|
348
360
|
if (pipelineWis.length === 0) return true; // no items = nothing failed
|
|
349
361
|
return pipelineWis.every(w => w.status !== WI_STATUS.FAILED);
|
|
@@ -501,11 +513,16 @@ function _findMeetingsInRun(run) {
|
|
|
501
513
|
return meetings;
|
|
502
514
|
}
|
|
503
515
|
|
|
504
|
-
// Check if a plan already exists for a given meeting (created manually via
|
|
505
|
-
|
|
516
|
+
// Check if a plan already exists for a given meeting (created manually via
|
|
517
|
+
// dashboard, or previously by this same pipeline stage). `pipelineSlugPrefix`
|
|
518
|
+
// (optional) is the slug a pipeline plan stage writes its files under
|
|
519
|
+
// (`slugify(stage.title)`) — matching on it catches pre-existing pipeline
|
|
520
|
+
// plans that predate the `**Source Meeting:**` marker below.
|
|
521
|
+
function _findExistingPlanForMeeting(meetingIds, plansDir, pipelineSlugPrefix) {
|
|
506
522
|
const files = safeReadDir(plansDir).filter(f => f.endsWith('.md'));
|
|
507
|
-
// Build slug prefixes for
|
|
523
|
+
// Build slug prefixes for dashboard, and (optionally) pipeline naming conventions
|
|
508
524
|
const slugPrefixes = [];
|
|
525
|
+
if (pipelineSlugPrefix) slugPrefixes.push(pipelineSlugPrefix);
|
|
509
526
|
for (const mid of meetingIds) {
|
|
510
527
|
const mtg = safeJson(path.join(MEETINGS_DIR, mid + '.json'));
|
|
511
528
|
if (mtg?.title) {
|
|
@@ -575,6 +592,18 @@ function _findExistingPrdForPlan(planFile, prdDir) {
|
|
|
575
592
|
return null;
|
|
576
593
|
}
|
|
577
594
|
|
|
595
|
+
// A well-formed plan (LLM path or fallback path) always starts with a markdown
|
|
596
|
+
// title and has substantive body content. Guards against `maxTurns: 1` LLM
|
|
597
|
+
// calls that spend their single turn on tool-oriented preamble/narration
|
|
598
|
+
// instead of the plan itself (W-mrbn0eex000265d7) — that kind of response is a
|
|
599
|
+
// short mid-task fragment, not a plan, and must not be written verbatim.
|
|
600
|
+
function _looksLikePlanContent(content) {
|
|
601
|
+
if (!content) return false;
|
|
602
|
+
const trimmed = content.trim();
|
|
603
|
+
if (trimmed.length < 200) return false;
|
|
604
|
+
return trimmed.startsWith('#');
|
|
605
|
+
}
|
|
606
|
+
|
|
578
607
|
async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
579
608
|
const plansDir = PLANS_DIR;
|
|
580
609
|
if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
|
|
@@ -591,7 +620,7 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
591
620
|
// ── Reconciliation: check if a plan already exists for a meeting in this run ──
|
|
592
621
|
const meetingIds = _findMeetingsInRun(run);
|
|
593
622
|
if (meetingIds.length > 0) {
|
|
594
|
-
const existingPlanFile = _findExistingPlanForMeeting(meetingIds, plansDir);
|
|
623
|
+
const existingPlanFile = _findExistingPlanForMeeting(meetingIds, plansDir, slug);
|
|
595
624
|
if (existingPlanFile) {
|
|
596
625
|
log('info', `Pipeline ${run.pipelineId}: reconciling plan stage — adopting existing plan "${existingPlanFile}"`);
|
|
597
626
|
|
|
@@ -686,9 +715,11 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
686
715
|
});
|
|
687
716
|
if (result.missingRuntime) {
|
|
688
717
|
log('warn', `Pipeline plan LLM skipped: ${result.text || result.stderr || 'runtime unavailable'} — falling back to raw meeting context`);
|
|
689
|
-
} else if (result.text) {
|
|
718
|
+
} else if (result.text && _looksLikePlanContent(result.text)) {
|
|
690
719
|
content = result.text;
|
|
691
720
|
log('info', `Pipeline plan: LLM generated ${content.length} chars from meeting context`);
|
|
721
|
+
} else if (result.text) {
|
|
722
|
+
log('warn', `Pipeline plan LLM response looked like a fragment, not a plan (${result.text.length} chars, doesn't start with a title) — discarding and falling back to raw meeting context`);
|
|
692
723
|
}
|
|
693
724
|
} catch (e) { log('warn', `Pipeline plan LLM failed: ${e.message} — falling back to raw meeting context`); }
|
|
694
725
|
|
|
@@ -701,6 +732,14 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
701
732
|
if (stage.description) content += stage.description + '\n';
|
|
702
733
|
}
|
|
703
734
|
|
|
735
|
+
// Stamp every source meeting so a future/duplicate invocation for this run
|
|
736
|
+
// can find this plan via `_findExistingPlanForMeeting`'s content scan — the
|
|
737
|
+
// reconciliation guard above only matches on this literal marker or a slug
|
|
738
|
+
// prefix, and generated content (LLM or fallback) never included it before.
|
|
739
|
+
if (meetingIds.length > 0) {
|
|
740
|
+
content += '\n\n---\n\n' + meetingIds.map(mid => `**Source Meeting:** ${mid}`).join('\n') + '\n';
|
|
741
|
+
}
|
|
742
|
+
|
|
704
743
|
const filename = `${slug}-${dateStamp()}.md`;
|
|
705
744
|
const filePath = shared.uniquePath(path.join(plansDir, filename));
|
|
706
745
|
safeWrite(filePath, content);
|
|
@@ -918,13 +957,8 @@ function isStageComplete(stage, stageState, run, config) {
|
|
|
918
957
|
|
|
919
958
|
switch (stage.type) {
|
|
920
959
|
case STAGE_TYPE.TASK: {
|
|
921
|
-
// Check
|
|
922
|
-
const
|
|
923
|
-
const workItems = safeJsonArr(wiPath);
|
|
924
|
-
const allProjectWi = shared.getProjects(config).reduce((acc, p) => {
|
|
925
|
-
return acc.concat(safeJson(shared.projectWorkItemsPath(p)) || []);
|
|
926
|
-
}, []);
|
|
927
|
-
const all = [...workItems, ...allProjectWi];
|
|
960
|
+
// Check central + all project work items (WIs may live in project scopes)
|
|
961
|
+
const all = _allWorkItems();
|
|
928
962
|
const ids = artifacts.workItems || [];
|
|
929
963
|
if (ids.length === 0) return false;
|
|
930
964
|
return ids.every(id => {
|
|
@@ -943,12 +977,7 @@ function isStageComplete(stage, stageState, run, config) {
|
|
|
943
977
|
}
|
|
944
978
|
case STAGE_TYPE.PLAN: {
|
|
945
979
|
// Plan stage completion: PRD conversion done + all materialized work items done
|
|
946
|
-
const
|
|
947
|
-
const workItems = safeJsonArr(wiPath);
|
|
948
|
-
const allProjectWi = shared.getProjects(config).reduce((acc, p) => {
|
|
949
|
-
return acc.concat(safeJson(shared.projectWorkItemsPath(p)) || []);
|
|
950
|
-
}, []);
|
|
951
|
-
const all = [...workItems, ...allProjectWi];
|
|
980
|
+
const all = _allWorkItems();
|
|
952
981
|
|
|
953
982
|
// Check if plan-to-prd work item is done
|
|
954
983
|
const prdWiIds = artifacts.workItems || [];
|
|
@@ -1021,7 +1050,7 @@ function isStageComplete(stage, stageState, run, config) {
|
|
|
1021
1050
|
if (prIds.length === 0) return true; // nothing to merge
|
|
1022
1051
|
const projects = shared.getProjects(config);
|
|
1023
1052
|
for (const project of projects) {
|
|
1024
|
-
const prs =
|
|
1053
|
+
const prs = safeJsonArr(shared.projectPrPath(project));
|
|
1025
1054
|
for (const prId of prIds) {
|
|
1026
1055
|
const pr = shared.findPrRecord(prs, prId, project);
|
|
1027
1056
|
if (pr && pr.status !== PR_STATUS.MERGED && pr.status !== PR_STATUS.ABANDONED) return false;
|
|
@@ -1111,13 +1140,20 @@ async function discoverPipelineWork(config) {
|
|
|
1111
1140
|
// Collect output
|
|
1112
1141
|
let output = '';
|
|
1113
1142
|
if (stage.type === STAGE_TYPE.TASK) {
|
|
1114
|
-
const
|
|
1115
|
-
const workItems = safeJsonArr(wiPath);
|
|
1116
|
-
const projWi = shared.getProjects(config).reduce((acc, p) => acc.concat(safeJson(shared.projectWorkItemsPath(p)) || []), []);
|
|
1117
|
-
const allWi = [...workItems, ...projWi];
|
|
1143
|
+
const allWi = _allWorkItems();
|
|
1118
1144
|
output = (stageState.artifacts?.workItems || []).map(id => {
|
|
1119
1145
|
const wi = allWi.find(w => w.id === id);
|
|
1120
|
-
|
|
1146
|
+
// W-mrcrh5hj0017d22f: never fall back to the stage's own work
|
|
1147
|
+
// item id — a bare id substituted into {{stages.<id>.output}}
|
|
1148
|
+
// produces a nonsensical downstream description (e.g.
|
|
1149
|
+
// "Confirmed bugs:\nPL-mrcqpzit-verify-0-minions-opg") that
|
|
1150
|
+
// pre-dispatch-eval correctly (but silently, forever) rejects.
|
|
1151
|
+
// If the real resultSummary/title text isn't available yet
|
|
1152
|
+
// (WI missing, or a lagged/failed mirror momentarily hides a
|
|
1153
|
+
// just-completed resultSummary), fall back to an empty string
|
|
1154
|
+
// — a blank substitution is harmless where an opaque id is
|
|
1155
|
+
// actively misleading.
|
|
1156
|
+
return wi?.resultSummary || wi?.title || '';
|
|
1121
1157
|
}).join('\n');
|
|
1122
1158
|
} else if (stage.type === STAGE_TYPE.MEETING) {
|
|
1123
1159
|
const { getMeeting } = require('./meeting');
|
|
@@ -1173,7 +1209,7 @@ async function discoverPipelineWork(config) {
|
|
|
1173
1209
|
if (meetingIds.length > 0) {
|
|
1174
1210
|
const plansDir = PLANS_DIR;
|
|
1175
1211
|
if (fs.existsSync(plansDir)) {
|
|
1176
|
-
const existingPlan = _findExistingPlanForMeeting(meetingIds, plansDir);
|
|
1212
|
+
const existingPlan = _findExistingPlanForMeeting(meetingIds, plansDir, slugify(nextPlanStage.title || 'pipeline-plan'));
|
|
1177
1213
|
if (existingPlan) {
|
|
1178
1214
|
updateRunStage(pipeline.id, activeRun.runId, stage.id, {
|
|
1179
1215
|
status: PIPELINE_STATUS.COMPLETED, completedAt: ts(),
|
|
@@ -1207,7 +1243,34 @@ async function discoverPipelineWork(config) {
|
|
|
1207
1243
|
stageState.status = PIPELINE_STATUS.FAILED;
|
|
1208
1244
|
anyFailed = true;
|
|
1209
1245
|
} else if (depsReady) {
|
|
1210
|
-
|
|
1246
|
+
// W-mrbn0eex000265d7: persist RUNNING BEFORE the (potentially slow,
|
|
1247
|
+
// up-to-120s LLM-backed) executeStage call resolves. Plan stages in
|
|
1248
|
+
// particular kick off an LLM call; the stage's persisted status
|
|
1249
|
+
// previously stayed PENDING for the whole call, so a concurrent or
|
|
1250
|
+
// retried invocation of discoverPipelineWork (overlapping tick, or
|
|
1251
|
+
// resumed run after an engine restart mid-await) would see PENDING
|
|
1252
|
+
// again and re-execute the stage, writing a second duplicate plan
|
|
1253
|
+
// file. Marking RUNNING first means a re-entrant call takes the
|
|
1254
|
+
// RUNNING branch above (isStageComplete) instead of re-executing.
|
|
1255
|
+
updateRunStage(pipeline.id, activeRun.runId, stage.id, { status: PIPELINE_STATUS.RUNNING, startedAt: ts() });
|
|
1256
|
+
stageState.status = PIPELINE_STATUS.RUNNING;
|
|
1257
|
+
// W-mrbn0eex000265d7 (review follow-up): the RUNNING status above is
|
|
1258
|
+
// already persisted, so if executeStage throws instead of resolving
|
|
1259
|
+
// (e.g. a lock-timeout in mutateWorkItems, an fs error), an uncaught
|
|
1260
|
+
// rejection would propagate out of discoverPipelineWork to the
|
|
1261
|
+
// engine.js tick-phase catch, which only logs a warning — leaving
|
|
1262
|
+
// this stage's persisted status stuck at RUNNING forever with no
|
|
1263
|
+
// follow-up transition (previously it stayed PENDING and simply
|
|
1264
|
+
// retried next tick). Catch here and route the failure through the
|
|
1265
|
+
// normal FAILED path so dependents fail fast instead of the run
|
|
1266
|
+
// stalling silently.
|
|
1267
|
+
let result;
|
|
1268
|
+
try {
|
|
1269
|
+
result = await executeStage(stage, activeRun, pipeline, config);
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
log('warn', `Pipeline ${pipeline.id}: stage ${stage.id} threw during execution: ${e.message}`);
|
|
1272
|
+
result = { status: PIPELINE_STATUS.FAILED, error: e.message };
|
|
1273
|
+
}
|
|
1211
1274
|
updateRunStage(pipeline.id, activeRun.runId, stage.id, { ...result, startedAt: ts() });
|
|
1212
1275
|
Object.assign(stageState, result, { startedAt: ts() });
|
|
1213
1276
|
if (result.status === PIPELINE_STATUS.RUNNING) {
|
package/engine/playbook.js
CHANGED
|
@@ -12,7 +12,7 @@ const queries = require('./queries');
|
|
|
12
12
|
const { wrapUntrusted, buildSource } = require('./untrusted-fence');
|
|
13
13
|
const { MINIONS_BRAND_URL } = require('./comment-format');
|
|
14
14
|
|
|
15
|
-
const {
|
|
15
|
+
const { safeJsonArr, safeRead, getProjects, log, ts, dateStamp, truncateTextBytes, ENGINE_DEFAULTS, WI_STATUS, WORK_TYPE, PR_STATUS, DISPATCH_RESULT, getProjectOrg } = shared;
|
|
16
16
|
const { getConfig, getDispatch, getNotes, getPrs, getKnowledgeBaseIndex, AGENTS_DIR } = queries;
|
|
17
17
|
|
|
18
18
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
@@ -1060,11 +1060,12 @@ function renderPlaybook(type, vars) {
|
|
|
1060
1060
|
// ─── Playbook Section Validator ──────────────────────────────────────────────
|
|
1061
1061
|
|
|
1062
1062
|
// Required structural section patterns — warn (do not throw) when absent.
|
|
1063
|
-
//
|
|
1063
|
+
// Only '## Your Task' is checked: it's the one section every playbook template
|
|
1064
|
+
// actually implements. '## Tools' / '## Constraints' were removed (#775) —
|
|
1065
|
+
// no template in playbooks/ defines those headers, so the checks always
|
|
1066
|
+
// failed and produced 100%-failure-rate warn-level noise in engine/log.json.
|
|
1064
1067
|
const _REQUIRED_PROMPT_SECTIONS = [
|
|
1065
1068
|
{ pattern: /^## Your Task\b/m, label: '## Your Task' },
|
|
1066
|
-
{ pattern: /^## Tools?\b/m, label: '## Tools' },
|
|
1067
|
-
{ pattern: /^## Constraints\b/m, label: '## Constraints' },
|
|
1068
1069
|
];
|
|
1069
1070
|
|
|
1070
1071
|
/**
|
|
@@ -1266,7 +1267,7 @@ function buildAgentContext(agentId, config, project) {
|
|
|
1266
1267
|
}
|
|
1267
1268
|
// Also check central pull-requests.json
|
|
1268
1269
|
try {
|
|
1269
|
-
const centralPrs =
|
|
1270
|
+
const centralPrs = safeJsonArr(path.join(MINIONS_DIR, 'pull-requests.json'));
|
|
1270
1271
|
for (const pr of centralPrs.filter(pr => pr.status === PR_STATUS.ACTIVE)) {
|
|
1271
1272
|
if (!allPrs.some(p => p.id === pr.id)) allPrs.push({ ...pr, _project: 'central' });
|
|
1272
1273
|
}
|
package/engine/queries.js
CHANGED
|
@@ -72,6 +72,25 @@ function _resetPrEnrichmentCacheForTest() {
|
|
|
72
72
|
_prEnrichmentInFlight.clear();
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// W-mrcs8yqk001o6893 — shared enrichment-fallback resolver. Both the prLinks
|
|
76
|
+
// loop and the wi._pr fallback loop in getPrdInfo() need the SAME defensive
|
|
77
|
+
// behavior when the canonical PR id has no live record in pull-requests.json
|
|
78
|
+
// (e.g. it was merged and then swept by the merged-PR cleanup routine): check
|
|
79
|
+
// the enrichment cache first, else synthesize PR_STATUS.ACTIVE as a
|
|
80
|
+
// placeholder and kick off a fire-and-forget enrollment so the next poll
|
|
81
|
+
// (within PR_ENRICHMENT_TTL_MS) resolves the real status. Previously only the
|
|
82
|
+
// wi._pr loop did this (W-mq5uzmc6001d708f); the prLinks loop hardcoded
|
|
83
|
+
// `pr?.status || PR_STATUS.ACTIVE` with no self-heal, so PRs reached only via
|
|
84
|
+
// pr_links stayed 'active' forever once removed from pull-requests.json.
|
|
85
|
+
function _resolvePrStatusWithEnrichment(pr, canonicalPrId, project, itemId) {
|
|
86
|
+
if (pr?.status) return pr.status;
|
|
87
|
+
if (!canonicalPrId) return PR_STATUS.ACTIVE;
|
|
88
|
+
const cachedStatus = _getCachedPrEnrichment(canonicalPrId);
|
|
89
|
+
if (cachedStatus) return cachedStatus;
|
|
90
|
+
_scheduleAsyncPrEnrollment(canonicalPrId, project, itemId);
|
|
91
|
+
return PR_STATUS.ACTIVE;
|
|
92
|
+
}
|
|
93
|
+
|
|
75
94
|
/**
|
|
76
95
|
* Read the first `bytes` and last `bytes` of a file efficiently using byte offsets.
|
|
77
96
|
* For files <= 2*bytes, reads the whole file. Returns { head, tail } strings.
|
|
@@ -572,15 +591,6 @@ function _buildArchiveNotesByWiMap() {
|
|
|
572
591
|
return out;
|
|
573
592
|
}
|
|
574
593
|
|
|
575
|
-
// Test/maintenance hook — drop the archive notes cache (mirrors the other
|
|
576
|
-
// invalidate* helpers). Not normally needed: the cache self-heals via the
|
|
577
|
-
// dir-mtime gate + TTL.
|
|
578
|
-
function invalidateArchiveNotesByWiCache() {
|
|
579
|
-
_archiveNotesByWiCache = null;
|
|
580
|
-
_archiveNotesByWiCacheAt = 0;
|
|
581
|
-
_archiveNotesByWiCacheMtime = null;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
594
|
// Build a wiId → [filename, ...] map by scanning the inbox + archive dirs.
|
|
585
595
|
// Archive entries are prefixed with `archive:` so the dashboard renderer can
|
|
586
596
|
// tell them apart (matches the convention already used by _artifacts.notes).
|
|
@@ -2363,7 +2373,18 @@ function getPrdInfo(config) {
|
|
|
2363
2373
|
const url = buildPrUrlFromId(prId, pr, projects);
|
|
2364
2374
|
for (const itemId of (itemIds || [])) {
|
|
2365
2375
|
if (!prdToPr[itemId]) prdToPr[itemId] = [];
|
|
2366
|
-
|
|
2376
|
+
// W-mrcs8yqk001o6893 — when the pr_links mapping outlives the PR record
|
|
2377
|
+
// (merged-PR sweep removes it from pull-requests.json but pr_links is
|
|
2378
|
+
// never pruned), resolve the item's project so we can self-heal the
|
|
2379
|
+
// status the same way the wi._pr fallback loop below does.
|
|
2380
|
+
let itemProject = null;
|
|
2381
|
+
if (!pr) {
|
|
2382
|
+
const itemSource = (wiById[itemId] || allWiById[itemId] || {}).project
|
|
2383
|
+
|| (wiById[itemId] || allWiById[itemId] || {})._source || null;
|
|
2384
|
+
itemProject = itemSource ? shared.resolveProjectSource(itemSource, projects, { allowCentral: false }).project || null : null;
|
|
2385
|
+
}
|
|
2386
|
+
const status = _resolvePrStatusWithEnrichment(pr, prId, itemProject, itemId);
|
|
2387
|
+
prdToPr[itemId].push({ id: prId, url, title: pr?.title || '', status, _project: pr?._project || '' });
|
|
2367
2388
|
}
|
|
2368
2389
|
}
|
|
2369
2390
|
// Fallback: work item _pr field for anything still missing
|
|
@@ -2379,17 +2400,8 @@ function getPrdInfo(config) {
|
|
|
2379
2400
|
// exists (orphan _pr pointer), use a cached status from a prior async
|
|
2380
2401
|
// enrollment if available, else synthesize ACTIVE and schedule a
|
|
2381
2402
|
// fire-and-forget enrollment so the next status poll renders correctly.
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
const cachedStatus = _getCachedPrEnrichment(canonicalPrId);
|
|
2385
|
-
if (cachedStatus) {
|
|
2386
|
-
synthesizedStatus = cachedStatus;
|
|
2387
|
-
} else {
|
|
2388
|
-
synthesizedStatus = PR_STATUS.ACTIVE;
|
|
2389
|
-
_scheduleAsyncPrEnrollment(canonicalPrId, project, wi.id);
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
prdToPr[wi.id] = [{ id: pr?.id || canonicalPrId || wi._pr, url, title: pr?.title || '', status: synthesizedStatus || PR_STATUS.ACTIVE, _project: project?.name || '' }];
|
|
2403
|
+
const synthesizedStatus = _resolvePrStatusWithEnrichment(pr, canonicalPrId, project, wi.id);
|
|
2404
|
+
prdToPr[wi.id] = [{ id: pr?.id || canonicalPrId || wi._pr, url, title: pr?.title || '', status: synthesizedStatus, _project: project?.name || '' }];
|
|
2393
2405
|
}
|
|
2394
2406
|
// Aggregate sub-task PRs to decomposed parent (sub-tasks aren't PRD items but their PRs should show)
|
|
2395
2407
|
for (const pr of allPrs) {
|
|
@@ -3422,7 +3434,7 @@ module.exports = {
|
|
|
3422
3434
|
getKnowledgeBaseEntries, getKnowledgeBaseEntriesSnapshot, getKnowledgeBaseIndex,
|
|
3423
3435
|
|
|
3424
3436
|
// Work items & PRD
|
|
3425
|
-
getWorkItems, invalidateWorkItemsCache,
|
|
3437
|
+
getWorkItems, invalidateWorkItemsCache, getPrdInfo,
|
|
3426
3438
|
|
|
3427
3439
|
// W-mq5uzmc6001d708f — test hooks for the defensive PR enrichment cache.
|
|
3428
3440
|
_resetPrEnrichmentCacheForTest,
|
package/engine/shared.js
CHANGED
|
@@ -2306,8 +2306,47 @@ function runFile(file, args, opts = {}) {
|
|
|
2306
2306
|
return _spawn(file, args, { windowsHide: true, ...opts });
|
|
2307
2307
|
}
|
|
2308
2308
|
|
|
2309
|
+
// True when `p` is a UNC path (`\\server\share\...` or its forward-slash
|
|
2310
|
+
// form `//server/share/...`, e.g. `\\wsl.localhost\Ubuntu\...` / `\\wsl$\...`
|
|
2311
|
+
// for a WSL-hosted project). Windows' cmd.exe — the default shell
|
|
2312
|
+
// `execSync`/`exec` spawn under — refuses a UNC path as its working
|
|
2313
|
+
// directory ("UNC paths are not supported. Defaulting to Windows
|
|
2314
|
+
// directory."), which silently redirects the child's actual cwd elsewhere.
|
|
2315
|
+
function isUncPath(p) {
|
|
2316
|
+
return typeof p === 'string' && (p.startsWith('\\\\') || p.startsWith('//'));
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// Minimal argv tokenizer for the small, hardcoded `git ...` command strings
|
|
2320
|
+
// built elsewhere in this file — splits on whitespace, honoring
|
|
2321
|
+
// double-quoted segments (the only quoting these callers use, e.g. a branch
|
|
2322
|
+
// name interpolated as `"refs/heads/${branch}"`). Not a general shell
|
|
2323
|
+
// parser; only ever fed strings this codebase constructs itself.
|
|
2324
|
+
function _splitGitCommandLine(cmd) {
|
|
2325
|
+
const argv = [];
|
|
2326
|
+
const re = /"([^"]*)"|(\S+)/g;
|
|
2327
|
+
let m;
|
|
2328
|
+
while ((m = re.exec(cmd)) !== null) argv.push(m[1] !== undefined ? m[1] : m[2]);
|
|
2329
|
+
return argv;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2309
2332
|
function execSilent(cmd, opts = {}) {
|
|
2310
|
-
|
|
2333
|
+
const merged = { stdio: 'pipe', windowsHide: true, ...opts };
|
|
2334
|
+
// W-mrcv3ky7000c2338 / opg#772 — a `git ...` shellout whose `cwd` is a UNC
|
|
2335
|
+
// path (WSL-hosted project, or any other UNC-rooted checkout) would
|
|
2336
|
+
// otherwise run through cmd.exe, which can't chdir into a UNC path and
|
|
2337
|
+
// silently defaults to the Windows directory instead — the subsequent git
|
|
2338
|
+
// command then fails with "fatal: not a git repository", and callers like
|
|
2339
|
+
// cleanup.js's orphan/quarantine worktree-dir reaper have no ground truth
|
|
2340
|
+
// for which worktrees are registered, so quarantined worktree husks are
|
|
2341
|
+
// never reaped (unbounded disk growth). Re-parse into argv and run via
|
|
2342
|
+
// execFileSync (shell:false) instead — that bypasses cmd.exe entirely and
|
|
2343
|
+
// Windows' CreateProcess honors a UNC cwd correctly, exactly like
|
|
2344
|
+
// shared.shellSafeGitSync / shared.removeWorktree already do.
|
|
2345
|
+
if (typeof cmd === 'string' && /^git\b/.test(cmd.trim()) && isUncPath(merged.cwd)) {
|
|
2346
|
+
const argv = _splitGitCommandLine(cmd);
|
|
2347
|
+
return _execFileSync(argv[0], argv.slice(1), merged);
|
|
2348
|
+
}
|
|
2349
|
+
return _execSync(cmd, merged);
|
|
2311
2350
|
}
|
|
2312
2351
|
|
|
2313
2352
|
/**
|
|
@@ -3458,6 +3497,17 @@ const ENGINE_DEFAULTS = {
|
|
|
3458
3497
|
// requested/completed PRDs still flow through the normal validator path so a
|
|
3459
3498
|
// stale PRD reopen can't smuggle in unvetted items.
|
|
3460
3499
|
preDispatchEvalSkipPrdSourced: true,
|
|
3500
|
+
// W-mrcrh5hj0017d22f — repeat-rejection cap. addToDispatchWithValidation
|
|
3501
|
+
// re-runs the LLM validator on EVERY discovery tick for a WI that's still
|
|
3502
|
+
// `pending` — routing to the review queue (_routeToReviewQueue) dedupes
|
|
3503
|
+
// that queue but never changes the WI's own status, so an item whose
|
|
3504
|
+
// description the validator will never accept (see stage-output
|
|
3505
|
+
// substitution bug above) got re-evaluated forever, once per tick,
|
|
3506
|
+
// invisible to the dashboard. Once the SAME (unchanged) description is
|
|
3507
|
+
// rejected this many consecutive times, the WI is flipped to `failed`
|
|
3508
|
+
// with FAILURE_CLASS.PRE_DISPATCH_EVAL_STUCK instead of being silently
|
|
3509
|
+
// re-queued into review again.
|
|
3510
|
+
preDispatchEvalMaxRejections: 5,
|
|
3461
3511
|
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
3512
|
autoApplyReviewVote: false, // W-mpea9fyb0010febf / P-f7splitgate: master gate for ALL engine platform vote mutations. When true, the engine (a) mirrors live platform vote state into local pull-requests.json reviewStatus, (b) re-applies its own ADO +10 approval when ADO resets votes on target-branch advance (pollPrStatus), (c) reconciles its own prior negative on a verdict flip to APPROVE (ADO resetReviewerNegativeVote / GitHub dismissPriorViewerChangesRequestedReviews), and (d) instructs ADO review agents to cast `az repos pr set-vote`. When false (default), the verdict is recorded in pull-requests.json reviewStatus from the agent's completion ONLY — informational — and Minions never cast, re-cast, or clear a platform vote/review (ADO agents post the verdict as a comment only, parity with GitHub self-approval block).
|
|
3463
3513
|
|
|
@@ -4919,6 +4969,7 @@ const FAILURE_CLASS = {
|
|
|
4919
4969
|
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).
|
|
4920
4970
|
VERIFY_MISSING_PR: 'verify-missing-pr', // W-mqsk1ip00006cbae: a verify WI exited done but no PR was attached (neither _prUrl/_pr on the item nor a matching pull-requests.json entry for the plan). Flipped to failed so the plan doesn't silently advance without an E2E PR. Retryable — agent may have phantom-crashed before pushing the branch.
|
|
4921
4971
|
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.
|
|
4972
|
+
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.
|
|
4922
4973
|
UNKNOWN: 'unknown', // Unclassified failure
|
|
4923
4974
|
};
|
|
4924
4975
|
|
|
@@ -9174,6 +9225,7 @@ module.exports = {
|
|
|
9174
9225
|
exec,
|
|
9175
9226
|
execAsync,
|
|
9176
9227
|
execSilent,
|
|
9228
|
+
isUncPath,
|
|
9177
9229
|
shellSafeGh,
|
|
9178
9230
|
shellSafeGit,
|
|
9179
9231
|
removeStaleIndexLock,
|