@yemi33/minions 0.1.2086 → 0.1.2088
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/refresh.js +598 -160
- package/dashboard/js/render-dispatch.js +77 -0
- package/dashboard/js/render-inbox.js +72 -0
- package/dashboard/js/render-meetings.js +55 -0
- package/dashboard/js/render-plans.js +14 -9
- package/dashboard/js/render-prd.js +13 -6
- package/dashboard/js/render-prs.js +55 -0
- package/dashboard/js/render-watches.js +16 -0
- package/dashboard/js/render-work-items.js +70 -0
- package/dashboard/js/settings.js +1 -5
- package/dashboard/js/state.js +9 -3
- package/dashboard.js +557 -351
- package/docs/security.md +23 -0
- package/engine/ado.js +54 -54
- package/engine/cli.js +3 -38
- package/engine/db/index.js +1 -1
- package/engine/db/migrations/002-dispatches.js +1 -1
- package/engine/db/migrations/003-work-items.js +1 -1
- package/engine/db/migrations/004-pull-requests.js +1 -1
- package/engine/dispatch.js +8 -2
- package/engine/github.js +38 -38
- package/engine/lifecycle.js +192 -18
- package/engine/projects.js +92 -0
- package/engine/queries.js +61 -129
- package/engine/shared.js +85 -89
- package/engine/watches.js +5 -5
- package/engine.js +23 -34
- package/package.json +2 -2
package/engine/github.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const shared = require('./shared');
|
|
8
|
-
const { exec, execAsync, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeJsonArr, safeWrite, mutateJsonFileLocked, mutatePullRequests, MINIONS_DIR, getPrLinks, backfillPrPrdItems, log, ts, dateStamp, PR_STATUS, PR_POLLABLE_STATUSES, ENGINE_DEFAULTS, createThrottleTracker, getProjectOrg } = shared;
|
|
8
|
+
const { exec, execAsync, getProjects, projectPrPath, projectWorkItemsPath, safeJson, safeJsonArr, safeWrite, mutateJsonFileLocked, mutatePullRequests, MINIONS_DIR, getPrLinks, backfillPrPrdItems, log, ts, dateStamp, PR_STATUS, PR_POLLABLE_STATUSES, BUILD_STATUS, REVIEW_STATUS, FETCH_TIMEOUT_MS, ENGINE_DEFAULTS, createThrottleTracker, getProjectOrg } = shared;
|
|
9
9
|
const { getPrs } = require('./queries');
|
|
10
10
|
const { isPreviewStatusBody, hasMinionsMarker, hasVerdictPrefix } = require('./comment-classifier');
|
|
11
11
|
const { wrapUntrusted, buildSource } = require('./untrusted-fence');
|
|
@@ -392,7 +392,7 @@ async function fetchGhBuildErrorLog(slug, failedRuns) {
|
|
|
392
392
|
const args = ['api', `repos/${shared.validateGhSlug(slug)}/actions/jobs/${jobId}/logs`];
|
|
393
393
|
const token = ghToken.resolveTokenForSlug(slug);
|
|
394
394
|
const env = token ? { ...process.env, GH_TOKEN: token } : undefined;
|
|
395
|
-
const result = await _runShellSafeGh(args, { timeout:
|
|
395
|
+
const result = await _runShellSafeGh(args, { timeout: FETCH_TIMEOUT_MS.GH_CLI, maxBuffer: GH_MAX_BUFFER, env });
|
|
396
396
|
if (result && !result.includes('Not Found')) {
|
|
397
397
|
logParts.push(`--- ${run.name || 'Check'} (log) ---\n${result}`);
|
|
398
398
|
}
|
|
@@ -485,8 +485,8 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
485
485
|
const idx = currentPrs.findIndex(p => p.id === after.id);
|
|
486
486
|
if (idx >= 0) {
|
|
487
487
|
// Never downgrade reviewStatus from 'approved' — it's a permanent terminal state
|
|
488
|
-
if (currentPrs[idx].reviewStatus ===
|
|
489
|
-
after.reviewStatus =
|
|
488
|
+
if (currentPrs[idx].reviewStatus === REVIEW_STATUS.APPROVED && after.reviewStatus !== REVIEW_STATUS.APPROVED) {
|
|
489
|
+
after.reviewStatus = REVIEW_STATUS.APPROVED;
|
|
490
490
|
}
|
|
491
491
|
// W-mp5trwh60008386d: never downgrade `status: merged` — terminal state. A stale
|
|
492
492
|
// 404 reaching `prAbandonConfirmCount` could otherwise overwrite a concurrent
|
|
@@ -812,12 +812,12 @@ async function pollPrStatus(config) {
|
|
|
812
812
|
|
|
813
813
|
if (newStatus === PR_STATUS.MERGED || newStatus === PR_STATUS.ABANDONED) {
|
|
814
814
|
// Resolve stale 'waiting' review status — won't be polled again after this
|
|
815
|
-
if (pr.reviewStatus ===
|
|
816
|
-
pr.reviewStatus = newStatus === PR_STATUS.MERGED ?
|
|
815
|
+
if (pr.reviewStatus === REVIEW_STATUS.WAITING) {
|
|
816
|
+
pr.reviewStatus = newStatus === PR_STATUS.MERGED ? REVIEW_STATUS.APPROVED : REVIEW_STATUS.PENDING;
|
|
817
817
|
log('info', `PR ${pr.id} reviewStatus: waiting → ${pr.reviewStatus} (${newStatus})`);
|
|
818
818
|
}
|
|
819
819
|
// Clear stale build status — checks won't be polled after close
|
|
820
|
-
if (pr.buildStatus && pr.buildStatus !==
|
|
820
|
+
if (pr.buildStatus && pr.buildStatus !== BUILD_STATUS.NONE) {
|
|
821
821
|
delete pr.buildStatus;
|
|
822
822
|
delete pr.buildFailReason;
|
|
823
823
|
delete pr.buildErrorLog;
|
|
@@ -857,7 +857,7 @@ async function pollPrStatus(config) {
|
|
|
857
857
|
// F6 not yet shipped (field absent).
|
|
858
858
|
delete pr.humanFeedback.editsSeen;
|
|
859
859
|
pr.fixDispatched = false;
|
|
860
|
-
if (pr.reviewStatus !==
|
|
860
|
+
if (pr.reviewStatus !== REVIEW_STATUS.APPROVED) pr.reviewStatus = REVIEW_STATUS.PENDING;
|
|
861
861
|
log('info', `PR ${pr.id} reopened — reset transient state (reviewStatus=${pr.reviewStatus})`);
|
|
862
862
|
}
|
|
863
863
|
}
|
|
@@ -887,20 +887,20 @@ async function pollPrStatus(config) {
|
|
|
887
887
|
pr.reviewedBy = reviewedBy; updated = true;
|
|
888
888
|
}
|
|
889
889
|
|
|
890
|
-
let newReviewStatus = pr.reviewStatus ||
|
|
890
|
+
let newReviewStatus = pr.reviewStatus || REVIEW_STATUS.PENDING;
|
|
891
891
|
// Once approved, it stays approved permanently
|
|
892
|
-
if (pr.reviewStatus ===
|
|
893
|
-
newReviewStatus =
|
|
892
|
+
if (pr.reviewStatus === REVIEW_STATUS.APPROVED) {
|
|
893
|
+
newReviewStatus = REVIEW_STATUS.APPROVED;
|
|
894
894
|
} else if (states.some(s => s === 'CHANGES_REQUESTED')) {
|
|
895
|
-
if (pr.reviewStatus ===
|
|
896
|
-
newReviewStatus =
|
|
895
|
+
if (pr.reviewStatus === REVIEW_STATUS.WAITING && pr.minionsReview?.fixedAt && (!pr.lastPushedAt || pr.lastPushedAt <= pr.minionsReview.fixedAt)) {
|
|
896
|
+
newReviewStatus = REVIEW_STATUS.WAITING;
|
|
897
897
|
} else {
|
|
898
|
-
newReviewStatus =
|
|
898
|
+
newReviewStatus = REVIEW_STATUS.CHANGES_REQUESTED;
|
|
899
899
|
}
|
|
900
900
|
}
|
|
901
|
-
else if (states.some(s => s === 'APPROVED')) newReviewStatus =
|
|
902
|
-
else if (states.length > 0) newReviewStatus =
|
|
903
|
-
else if (states.length === 0 && reviews.length > 0 && newReviewStatus ===
|
|
901
|
+
else if (states.some(s => s === 'APPROVED')) newReviewStatus = REVIEW_STATUS.APPROVED;
|
|
902
|
+
else if (states.length > 0) newReviewStatus = REVIEW_STATUS.PENDING;
|
|
903
|
+
else if (states.length === 0 && reviews.length > 0 && newReviewStatus === REVIEW_STATUS.PENDING) newReviewStatus = REVIEW_STATUS.WAITING;
|
|
904
904
|
|
|
905
905
|
if (pr.reviewStatus !== newReviewStatus) {
|
|
906
906
|
log('info', `PR ${pr.id} reviewStatus: ${pr.reviewStatus} → ${newReviewStatus}`);
|
|
@@ -919,7 +919,7 @@ async function pollPrStatus(config) {
|
|
|
919
919
|
pr.lastBuildCheck = ts();
|
|
920
920
|
updated = true;
|
|
921
921
|
const runs = checksData.check_runs;
|
|
922
|
-
let buildStatus =
|
|
922
|
+
let buildStatus = BUILD_STATUS.NONE;
|
|
923
923
|
let buildFailReason = '';
|
|
924
924
|
let buildFailureSignature = '';
|
|
925
925
|
|
|
@@ -929,7 +929,7 @@ async function pollPrStatus(config) {
|
|
|
929
929
|
const allPassed = runs.every(r => r.conclusion === 'success' || r.conclusion === 'skipped' || r.conclusion === 'neutral');
|
|
930
930
|
|
|
931
931
|
if (hasFailed) {
|
|
932
|
-
buildStatus =
|
|
932
|
+
buildStatus = BUILD_STATUS.FAILING;
|
|
933
933
|
const failed = runs.find(r => r.conclusion === 'failure' || r.conclusion === 'timed_out');
|
|
934
934
|
buildFailReason = failed?.name || 'Check failed';
|
|
935
935
|
buildFailureSignature = shared.safeSlugComponent([
|
|
@@ -940,21 +940,21 @@ async function pollPrStatus(config) {
|
|
|
940
940
|
failed?.output?.text,
|
|
941
941
|
].filter(Boolean).join('\n') || buildFailReason, 80);
|
|
942
942
|
} else if (allDone && allPassed) {
|
|
943
|
-
buildStatus =
|
|
943
|
+
buildStatus = BUILD_STATUS.PASSING;
|
|
944
944
|
} else if (allDone) {
|
|
945
945
|
// Terminal-but-not-passing conclusions (cancelled, action_required,
|
|
946
946
|
// stale, etc.) — map to 'none' rather than 'failing'. These are not
|
|
947
947
|
// actionable build failures, so we deliberately do not block merge or
|
|
948
948
|
// dispatch build-fix agents. Previously fell through to 'running'
|
|
949
949
|
// forever (P-bf02-github-cancelled-stuck).
|
|
950
|
-
buildStatus =
|
|
950
|
+
buildStatus = BUILD_STATUS.NONE;
|
|
951
951
|
} else {
|
|
952
|
-
buildStatus =
|
|
952
|
+
buildStatus = BUILD_STATUS.RUNNING;
|
|
953
953
|
}
|
|
954
954
|
}
|
|
955
955
|
|
|
956
956
|
if (pr.buildStatus !== buildStatus) {
|
|
957
|
-
log('info', `PR ${pr.id} build: ${pr.buildStatus ||
|
|
957
|
+
log('info', `PR ${pr.id} build: ${pr.buildStatus || BUILD_STATUS.NONE} → ${buildStatus}${buildFailReason ? ' (' + buildFailReason + ')' : ''}`);
|
|
958
958
|
pr.buildStatus = buildStatus;
|
|
959
959
|
if (buildFailReason) pr.buildFailReason = buildFailReason;
|
|
960
960
|
else delete pr.buildFailReason;
|
|
@@ -962,14 +962,14 @@ async function pollPrStatus(config) {
|
|
|
962
962
|
else delete pr.buildFailureSignature;
|
|
963
963
|
// Build transitioned — clear grace period and auto-complete flag
|
|
964
964
|
delete pr._buildFixPushedAt;
|
|
965
|
-
if (buildStatus ===
|
|
966
|
-
if (buildStatus !==
|
|
965
|
+
if (buildStatus === BUILD_STATUS.FAILING) delete pr._autoCompleted; // allow re-merge after fix
|
|
966
|
+
if (buildStatus !== BUILD_STATUS.FAILING) {
|
|
967
967
|
delete pr._buildFailNotified;
|
|
968
968
|
// Preserve buildErrorLog + buildFixAttempts through transient 'none'/'running'
|
|
969
969
|
// transitions — only clear on confirmed 'passing' recovery. Issue #1232:
|
|
970
970
|
// clearing on every non-failing transition blinded the next fix agent
|
|
971
971
|
// while a queued build was still running.
|
|
972
|
-
if (buildStatus ===
|
|
972
|
+
if (buildStatus === BUILD_STATUS.PASSING) {
|
|
973
973
|
delete pr.buildErrorLog;
|
|
974
974
|
delete pr.buildFailureSignature;
|
|
975
975
|
// Reset build fix retry counter on recovery — allows fresh auto-fix cycles if build breaks again
|
|
@@ -978,7 +978,7 @@ async function pollPrStatus(config) {
|
|
|
978
978
|
}
|
|
979
979
|
updated = true;
|
|
980
980
|
}
|
|
981
|
-
if (buildStatus ===
|
|
981
|
+
if (buildStatus === BUILD_STATUS.FAILING) {
|
|
982
982
|
if (buildFailReason && pr.buildFailReason !== buildFailReason) {
|
|
983
983
|
pr.buildFailReason = buildFailReason;
|
|
984
984
|
updated = true;
|
|
@@ -1005,7 +1005,7 @@ async function pollPrStatus(config) {
|
|
|
1005
1005
|
}
|
|
1006
1006
|
|
|
1007
1007
|
// Auto-complete: merge PR when builds green + review approved
|
|
1008
|
-
if (pr.status === PR_STATUS.ACTIVE && pr.reviewStatus ===
|
|
1008
|
+
if (pr.status === PR_STATUS.ACTIVE && pr.reviewStatus === REVIEW_STATUS.APPROVED && pr.buildStatus === BUILD_STATUS.PASSING && !pr._autoCompleted) {
|
|
1009
1009
|
const autoComplete = config.engine?.autoCompletePrs === true; // opt-in
|
|
1010
1010
|
if (autoComplete) {
|
|
1011
1011
|
try {
|
|
@@ -1271,7 +1271,7 @@ async function reconcilePrs(config) {
|
|
|
1271
1271
|
title: (ghPr.title || `PR #${ghPr.number}`).slice(0, 120),
|
|
1272
1272
|
agent: (linkedItem?.dispatched_to || ghPr.user?.login || 'unknown').toLowerCase(),
|
|
1273
1273
|
branch,
|
|
1274
|
-
reviewStatus:
|
|
1274
|
+
reviewStatus: REVIEW_STATUS.PENDING,
|
|
1275
1275
|
status: 'active',
|
|
1276
1276
|
created: ghPr.created_at || ts(),
|
|
1277
1277
|
url: prUrl,
|
|
@@ -1297,7 +1297,7 @@ async function reconcilePrs(config) {
|
|
|
1297
1297
|
title: (ghPr.title || `PR #${ghPr.number}`).slice(0, 120),
|
|
1298
1298
|
agent: (linkedItem?.dispatched_to || ghPr.user?.login || 'unknown').toLowerCase(),
|
|
1299
1299
|
branch,
|
|
1300
|
-
reviewStatus:
|
|
1300
|
+
reviewStatus: REVIEW_STATUS.PENDING,
|
|
1301
1301
|
status: 'active',
|
|
1302
1302
|
created: ghPr.created_at || ts(),
|
|
1303
1303
|
url: prUrl,
|
|
@@ -1358,10 +1358,10 @@ async function checkLiveReviewStatus(pr, project) {
|
|
|
1358
1358
|
latestByUser.set(r.user?.login || '', r.state);
|
|
1359
1359
|
}
|
|
1360
1360
|
const states = [...latestByUser.values()];
|
|
1361
|
-
if (states.some(s => s === 'CHANGES_REQUESTED')) return
|
|
1362
|
-
if (states.some(s => s === 'APPROVED')) return
|
|
1363
|
-
if (states.length > 0) return
|
|
1364
|
-
return
|
|
1361
|
+
if (states.some(s => s === 'CHANGES_REQUESTED')) return REVIEW_STATUS.CHANGES_REQUESTED;
|
|
1362
|
+
if (states.some(s => s === 'APPROVED')) return REVIEW_STATUS.APPROVED;
|
|
1363
|
+
if (states.length > 0) return REVIEW_STATUS.PENDING;
|
|
1364
|
+
return REVIEW_STATUS.PENDING;
|
|
1365
1365
|
} catch (e) {
|
|
1366
1366
|
log('warn', `Live review check for ${pr.id}: ${e.message}`);
|
|
1367
1367
|
return null;
|
|
@@ -1527,14 +1527,14 @@ async function checkLiveBuildAndConflict(pr, project) {
|
|
|
1527
1527
|
if (checksData && Array.isArray(checksData.check_runs)) {
|
|
1528
1528
|
const runs = checksData.check_runs;
|
|
1529
1529
|
if (runs.length === 0) {
|
|
1530
|
-
buildStatus =
|
|
1530
|
+
buildStatus = BUILD_STATUS.NONE;
|
|
1531
1531
|
} else {
|
|
1532
1532
|
const hasFailed = runs.some(r => r.conclusion === 'failure' || r.conclusion === 'timed_out');
|
|
1533
1533
|
const allDone = runs.every(r => r.status === 'completed');
|
|
1534
1534
|
const allPassed = runs.every(r => r.conclusion === 'success' || r.conclusion === 'skipped' || r.conclusion === 'neutral');
|
|
1535
|
-
if (hasFailed) buildStatus =
|
|
1536
|
-
else if (allDone && allPassed) buildStatus =
|
|
1537
|
-
else buildStatus =
|
|
1535
|
+
if (hasFailed) buildStatus = BUILD_STATUS.FAILING;
|
|
1536
|
+
else if (allDone && allPassed) buildStatus = BUILD_STATUS.PASSING;
|
|
1537
|
+
else buildStatus = BUILD_STATUS.RUNNING;
|
|
1538
1538
|
}
|
|
1539
1539
|
}
|
|
1540
1540
|
} catch (e) {
|
package/engine/lifecycle.js
CHANGED
|
@@ -8,7 +8,7 @@ const path = require('path');
|
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const shared = require('./shared');
|
|
10
10
|
const { safeRead, safeJson, safeJsonArr, safeJsonNoRestore, safeWrite, mutateJsonFileLocked, mutateWorkItems, execAsync, projectPrPath, getPrLinks,
|
|
11
|
-
log, ts, dateStamp, WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PR_STATUS, DISPATCH_RESULT,
|
|
11
|
+
log, ts, dateStamp, WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PR_STATUS, REVIEW_STATUS, RETRY_DELAY_MS, DISPATCH_RESULT,
|
|
12
12
|
ENGINE_DEFAULTS, DEFAULT_AGENT_METRICS, FAILURE_CLASS } = shared;
|
|
13
13
|
const { trackEngineUsage } = require('./llm');
|
|
14
14
|
const { resolveRuntime } = require('./runtimes');
|
|
@@ -907,7 +907,7 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
907
907
|
title: (title || `PR created by ${agentName}`).slice(0, 120),
|
|
908
908
|
agent: agentName,
|
|
909
909
|
branch: meta?.branch || '',
|
|
910
|
-
reviewStatus:
|
|
910
|
+
reviewStatus: REVIEW_STATUS.PENDING,
|
|
911
911
|
status: PR_STATUS.ACTIVE,
|
|
912
912
|
// W-mpej044m00076d63: do NOT seed `created` with ts() (engine discovery time).
|
|
913
913
|
// The next GitHub/ADO poll backfills `pr.created` from the platform's real
|
|
@@ -1161,7 +1161,7 @@ async function findOpenPrForBranch(meta, config) {
|
|
|
1161
1161
|
if (!ghSlug) return null;
|
|
1162
1162
|
const maxAttempts = ENGINE_DEFAULTS.prAutoLinkRetries;
|
|
1163
1163
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1164
|
-
if (attempt > 0) await new Promise(r => setTimeout(r,
|
|
1164
|
+
if (attempt > 0) await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
|
|
1165
1165
|
let raw = '';
|
|
1166
1166
|
try {
|
|
1167
1167
|
raw = await runFileCapture('gh', ['pr', 'list', '--head', String(meta.branch), '--repo', ghSlug, '--json', 'number,url,state', '--limit', '1'], { timeout: 15000 });
|
|
@@ -1225,7 +1225,7 @@ function _attachFoundPrToWi(found, meta, agentId, resultSummary, config) {
|
|
|
1225
1225
|
title: meta.item?.title || `PR #${found.prNumber}`,
|
|
1226
1226
|
agent: agentId,
|
|
1227
1227
|
branch: meta.branch || '',
|
|
1228
|
-
reviewStatus:
|
|
1228
|
+
reviewStatus: REVIEW_STATUS.PENDING,
|
|
1229
1229
|
status: PR_STATUS.ACTIVE,
|
|
1230
1230
|
// W-mpej044m00076d63: omit `created` — the next poll backfills from the
|
|
1231
1231
|
// platform's real createdAt. `_attachedAt` is the discovery-time fallback.
|
|
@@ -1699,10 +1699,10 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1699
1699
|
// live platform status into local reviewStatus below (which can promote the
|
|
1700
1700
|
// PR to 'approved' based on platform-side votes).
|
|
1701
1701
|
const prevReviewStatus = reviewPr?.reviewStatus || '';
|
|
1702
|
-
const wasNegative = prevReviewStatus ===
|
|
1703
|
-
|| liveStatus ===
|
|
1702
|
+
const wasNegative = prevReviewStatus === REVIEW_STATUS.CHANGES_REQUESTED || prevReviewStatus === REVIEW_STATUS.WAITING
|
|
1703
|
+
|| liveStatus === REVIEW_STATUS.CHANGES_REQUESTED || liveStatus === REVIEW_STATUS.WAITING;
|
|
1704
1704
|
const autoApplyVote = config?.engine?.autoApplyReviewVote ?? ENGINE_DEFAULTS.autoApplyReviewVote;
|
|
1705
|
-
const canDismissOwnPriorNegative = verdictRaw ===
|
|
1705
|
+
const canDismissOwnPriorNegative = verdictRaw === REVIEW_STATUS.APPROVED && !isSelfReview && wasNegative && projectObjForChecks;
|
|
1706
1706
|
if (canDismissOwnPriorNegative) {
|
|
1707
1707
|
try {
|
|
1708
1708
|
const reconcileFn = hostForChecks === 'github'
|
|
@@ -1726,7 +1726,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1726
1726
|
}
|
|
1727
1727
|
}
|
|
1728
1728
|
|
|
1729
|
-
if (autoApplyVote && liveStatus && liveStatus !==
|
|
1729
|
+
if (autoApplyVote && liveStatus && liveStatus !== REVIEW_STATUS.PENDING) postReviewStatus = liveStatus;
|
|
1730
1730
|
|
|
1731
1731
|
// Fallback: if live check returned pending (e.g., GitHub self-approval blocked), use the agent's completion report.
|
|
1732
1732
|
if (!postReviewStatus) {
|
|
@@ -1740,7 +1740,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1740
1740
|
// the verdict and skips the no-verdict retry path, so _retryCount stays
|
|
1741
1741
|
// unchanged. REQUEST_CHANGES from a self-author is still accepted — a
|
|
1742
1742
|
// self-author flagging issues on their own PR is harmless and useful.
|
|
1743
|
-
if (verdict ===
|
|
1743
|
+
if (verdict === REVIEW_STATUS.APPROVED && isSelfReview) {
|
|
1744
1744
|
log('warn', `review verdict rejected: self-review (reviewer=${reviewerName}, author=${reviewPr.agent})`);
|
|
1745
1745
|
} else {
|
|
1746
1746
|
postReviewStatus = verdict;
|
|
@@ -1757,7 +1757,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1757
1757
|
if (!target) return prs;
|
|
1758
1758
|
// Once approved, stays approved — only changes-requested can override
|
|
1759
1759
|
if (postReviewStatus) {
|
|
1760
|
-
if (target.reviewStatus ===
|
|
1760
|
+
if (target.reviewStatus === REVIEW_STATUS.APPROVED && postReviewStatus !== REVIEW_STATUS.CHANGES_REQUESTED) {
|
|
1761
1761
|
// Keep approved — don't downgrade
|
|
1762
1762
|
} else {
|
|
1763
1763
|
target.reviewStatus = postReviewStatus;
|
|
@@ -1779,7 +1779,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1779
1779
|
...(reviewThreads.length > 0 ? { threads: reviewThreads } : {}),
|
|
1780
1780
|
// Preserve fixedAt across re-reviews so the poller guard knows a fix was pushed.
|
|
1781
1781
|
// Drop it when reviewer requests changes again — that starts a new fix cycle.
|
|
1782
|
-
...(target.minionsReview?.fixedAt && postReviewStatus !==
|
|
1782
|
+
...(target.minionsReview?.fixedAt && postReviewStatus !== REVIEW_STATUS.CHANGES_REQUESTED ? { fixedAt: target.minionsReview.fixedAt } : {}),
|
|
1783
1783
|
};
|
|
1784
1784
|
updatedTarget = { ...reviewPr, ...target };
|
|
1785
1785
|
return prs;
|
|
@@ -1796,7 +1796,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
1796
1796
|
});
|
|
1797
1797
|
}
|
|
1798
1798
|
|
|
1799
|
-
log('info', `Updated ${reviewPr.id} → minions review: ${postReviewStatus ||
|
|
1799
|
+
log('info', `Updated ${reviewPr.id} → minions review: ${postReviewStatus || REVIEW_STATUS.WAITING} by ${reviewerName}`);
|
|
1800
1800
|
if (updatedTarget) {
|
|
1801
1801
|
createReviewFeedbackForAuthor(agentId, updatedTarget, config, {
|
|
1802
1802
|
reviewContent: reviewNote,
|
|
@@ -2188,7 +2188,7 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
2188
2188
|
if (target.humanFeedback && clearPendingFix) target.humanFeedback.pendingFix = false;
|
|
2189
2189
|
if (explicitlyChangedBranch) {
|
|
2190
2190
|
// Never downgrade from approved — fix was dispatched but PR is already approved
|
|
2191
|
-
if (target.reviewStatus !==
|
|
2191
|
+
if (target.reviewStatus !== REVIEW_STATUS.APPROVED) target.reviewStatus = REVIEW_STATUS.WAITING;
|
|
2192
2192
|
target.minionsReview = { ...target.minionsReview, note: 'Fixed human feedback, awaiting re-review', fixedAt: ts() };
|
|
2193
2193
|
if (clearPendingFix) {
|
|
2194
2194
|
log('info', `Updated ${pr.id} → cleared humanFeedback.pendingFix, reset to waiting for re-review`);
|
|
@@ -2204,7 +2204,7 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
2204
2204
|
}
|
|
2205
2205
|
}
|
|
2206
2206
|
} else {
|
|
2207
|
-
if (target.reviewStatus !==
|
|
2207
|
+
if (target.reviewStatus !== REVIEW_STATUS.APPROVED) target.reviewStatus = REVIEW_STATUS.WAITING;
|
|
2208
2208
|
if (explicitlyChangedBranch) {
|
|
2209
2209
|
target.minionsReview = { ...target.minionsReview, note: 'Fixed, awaiting re-review', fixedAt: ts() };
|
|
2210
2210
|
log('info', `Updated ${pr.id} → reviewStatus: waiting (fix pushed)`);
|
|
@@ -3193,6 +3193,175 @@ function parseCompletionReportFile(dispatchItem, opts = {}) {
|
|
|
3193
3193
|
return report;
|
|
3194
3194
|
}
|
|
3195
3195
|
|
|
3196
|
+
function normalizeCompletionArtifacts(rawArtifacts) {
|
|
3197
|
+
let artifacts = rawArtifacts;
|
|
3198
|
+
if (typeof artifacts === 'string') {
|
|
3199
|
+
const trimmed = artifacts.trim();
|
|
3200
|
+
if (!trimmed) return [];
|
|
3201
|
+
try { artifacts = JSON.parse(trimmed); } catch { return []; }
|
|
3202
|
+
}
|
|
3203
|
+
if (!Array.isArray(artifacts)) {
|
|
3204
|
+
artifacts = shared.isPlainObject(artifacts) ? [artifacts] : [];
|
|
3205
|
+
}
|
|
3206
|
+
const out = [];
|
|
3207
|
+
for (const artifact of artifacts) {
|
|
3208
|
+
if (!shared.isPlainObject(artifact)) continue;
|
|
3209
|
+
const normalized = {};
|
|
3210
|
+
for (const key of ['type', 'path', 'title', 'id']) {
|
|
3211
|
+
if (artifact[key] == null) continue;
|
|
3212
|
+
const value = String(artifact[key]).trim();
|
|
3213
|
+
if (value) normalized[key] = value;
|
|
3214
|
+
}
|
|
3215
|
+
if (!normalized.path && !normalized.id && !normalized.title) continue;
|
|
3216
|
+
if (normalized.path) normalized.path = normalizeArtifactPath(normalized.path);
|
|
3217
|
+
out.push(normalized);
|
|
3218
|
+
}
|
|
3219
|
+
return out;
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
function normalizeArtifactPath(artifactPath) {
|
|
3223
|
+
const raw = String(artifactPath || '').trim();
|
|
3224
|
+
if (!raw) return '';
|
|
3225
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return raw;
|
|
3226
|
+
let normalized = raw.replace(/\\/g, '/');
|
|
3227
|
+
if (path.isAbsolute(raw)) {
|
|
3228
|
+
try {
|
|
3229
|
+
const rel = path.relative(MINIONS_DIR, raw).replace(/\\/g, '/');
|
|
3230
|
+
if (rel && !rel.startsWith('../') && rel !== '..') normalized = rel;
|
|
3231
|
+
} catch {}
|
|
3232
|
+
}
|
|
3233
|
+
return normalized.replace(/^\.\//, '');
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
function _artifactFilesystemPath(artifactPath) {
|
|
3237
|
+
if (!artifactPath || /^[a-z][a-z0-9+.-]*:\/\//i.test(artifactPath)) return null;
|
|
3238
|
+
if (path.isAbsolute(artifactPath)) return artifactPath;
|
|
3239
|
+
return path.join(MINIONS_DIR, artifactPath);
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3242
|
+
function completionArtifactToNoteEntry(artifact) {
|
|
3243
|
+
if (!shared.isPlainObject(artifact)) return null;
|
|
3244
|
+
const type = String(artifact.type || '').toLowerCase();
|
|
3245
|
+
const artifactPath = normalizeArtifactPath(artifact.path || '');
|
|
3246
|
+
const notePath = artifactPath.replace(/\\/g, '/');
|
|
3247
|
+
const isNote = type === 'note' || /(^|\/)notes\/(?:inbox|archive)\//.test(notePath);
|
|
3248
|
+
if (!isNote || !notePath || /^[a-z][a-z0-9+.-]*:\/\//i.test(notePath)) return null;
|
|
3249
|
+
|
|
3250
|
+
const base = path.basename(notePath);
|
|
3251
|
+
const file = notePath.includes('notes/archive/') ? `archive:${base}` : base;
|
|
3252
|
+
if (!file || file === '.' || file === '/') return null;
|
|
3253
|
+
const entry = { file };
|
|
3254
|
+
if (artifact.id) entry.id = String(artifact.id);
|
|
3255
|
+
if (artifact.title) entry.title = String(artifact.title);
|
|
3256
|
+
if (artifact.path) entry.path = artifactPath;
|
|
3257
|
+
if (!entry.id) {
|
|
3258
|
+
const filePath = _artifactFilesystemPath(artifactPath);
|
|
3259
|
+
if (filePath) {
|
|
3260
|
+
const noteId = shared.parseNoteId(safeRead(filePath));
|
|
3261
|
+
if (noteId) entry.id = noteId;
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
return entry;
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
function mergeCompletionArtifacts(existing, additions) {
|
|
3268
|
+
const merged = [];
|
|
3269
|
+
const seen = new Set();
|
|
3270
|
+
function keyFor(artifact) {
|
|
3271
|
+
if (!shared.isPlainObject(artifact)) return '';
|
|
3272
|
+
return [
|
|
3273
|
+
String(artifact.type || ''),
|
|
3274
|
+
String(artifact.path || ''),
|
|
3275
|
+
String(artifact.id || ''),
|
|
3276
|
+
String(artifact.title || ''),
|
|
3277
|
+
].join('\u0000');
|
|
3278
|
+
}
|
|
3279
|
+
for (const artifact of [...(Array.isArray(existing) ? existing : []), ...(Array.isArray(additions) ? additions : [])]) {
|
|
3280
|
+
if (!shared.isPlainObject(artifact)) continue;
|
|
3281
|
+
const normalized = normalizeCompletionArtifacts([artifact])[0];
|
|
3282
|
+
if (!normalized) continue;
|
|
3283
|
+
const key = keyFor(normalized);
|
|
3284
|
+
if (seen.has(key)) continue;
|
|
3285
|
+
seen.add(key);
|
|
3286
|
+
merged.push(normalized);
|
|
3287
|
+
}
|
|
3288
|
+
return merged;
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
function mergeArtifactNotes(existing, additions) {
|
|
3292
|
+
const merged = [];
|
|
3293
|
+
const seen = new Set();
|
|
3294
|
+
function normalize(note) {
|
|
3295
|
+
if (shared.isPlainObject(note)) {
|
|
3296
|
+
const file = String(note.file || '').trim();
|
|
3297
|
+
if (!file) return null;
|
|
3298
|
+
const out = { file };
|
|
3299
|
+
for (const key of ['id', 'title', 'path']) {
|
|
3300
|
+
if (note[key] == null) continue;
|
|
3301
|
+
const value = String(note[key]).trim();
|
|
3302
|
+
if (value) out[key] = value;
|
|
3303
|
+
}
|
|
3304
|
+
return out;
|
|
3305
|
+
}
|
|
3306
|
+
const file = String(note || '').trim();
|
|
3307
|
+
return file ? file : null;
|
|
3308
|
+
}
|
|
3309
|
+
function keyFor(note) {
|
|
3310
|
+
return shared.isPlainObject(note)
|
|
3311
|
+
? String(note.file || '')
|
|
3312
|
+
: String(note || '');
|
|
3313
|
+
}
|
|
3314
|
+
for (const note of [...(Array.isArray(existing) ? existing : []), ...(Array.isArray(additions) ? additions : [])]) {
|
|
3315
|
+
const normalized = normalize(note);
|
|
3316
|
+
if (!normalized) continue;
|
|
3317
|
+
const key = keyFor(normalized);
|
|
3318
|
+
if (seen.has(key)) continue;
|
|
3319
|
+
seen.add(key);
|
|
3320
|
+
merged.push(normalized);
|
|
3321
|
+
}
|
|
3322
|
+
return merged;
|
|
3323
|
+
}
|
|
3324
|
+
|
|
3325
|
+
function promoteCompletionArtifacts(meta, agentId, dispatchId, structuredCompletion, opts = {}) {
|
|
3326
|
+
const itemId = meta?.item?.id;
|
|
3327
|
+
if (!itemId) return { artifacts: [], notes: [] };
|
|
3328
|
+
const wiPath = resolveWorkItemPath(meta);
|
|
3329
|
+
if (!wiPath) return { artifacts: [], notes: [] };
|
|
3330
|
+
|
|
3331
|
+
const artifacts = normalizeCompletionArtifacts(structuredCompletion?.artifacts);
|
|
3332
|
+
const structuredNotes = artifacts.map(completionArtifactToNoteEntry).filter(Boolean);
|
|
3333
|
+
const additionalNotes = Array.isArray(opts.additionalNotes) ? opts.additionalNotes : [];
|
|
3334
|
+
const notes = mergeArtifactNotes(structuredNotes, additionalNotes);
|
|
3335
|
+
const outputLog = opts.outputLog ? String(opts.outputLog) : '';
|
|
3336
|
+
const branch = opts.branch || meta.branch || '';
|
|
3337
|
+
const resultSummary = opts.resultSummary ? String(opts.resultSummary).slice(0, 500) : '';
|
|
3338
|
+
|
|
3339
|
+
if (artifacts.length === 0 && notes.length === 0 && !outputLog && !branch && !resultSummary) {
|
|
3340
|
+
return { artifacts, notes };
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
mutateJsonFileLocked(wiPath, data => {
|
|
3344
|
+
if (!Array.isArray(data)) return data;
|
|
3345
|
+
const wi = data.find(i => i.id === itemId);
|
|
3346
|
+
if (!wi) return data;
|
|
3347
|
+
const arts = shared.isPlainObject(wi._artifacts) ? { ...wi._artifacts } : {};
|
|
3348
|
+
if (outputLog) arts.outputLog = outputLog;
|
|
3349
|
+
if (branch) arts.branch = branch;
|
|
3350
|
+
if (wi._pr) arts.pr = wi._pr;
|
|
3351
|
+
if (wi._prUrl) arts.prUrl = wi._prUrl;
|
|
3352
|
+
if (notes.length > 0) arts.notes = mergeArtifactNotes(arts.notes, notes);
|
|
3353
|
+
if (meta.item?.planFile) arts.plan = meta.item.planFile;
|
|
3354
|
+
if (meta.item?._prdFilename) arts.prd = meta.item._prdFilename;
|
|
3355
|
+
if (meta.item?.sourcePlan) arts.sourcePlan = meta.item.sourcePlan;
|
|
3356
|
+
if (artifacts.length > 0) wi.artifacts = mergeCompletionArtifacts(wi.artifacts, artifacts);
|
|
3357
|
+
if (resultSummary) wi.resultSummary = resultSummary;
|
|
3358
|
+
wi._artifacts = arts;
|
|
3359
|
+
return data;
|
|
3360
|
+
}, { defaultValue: [], skipWriteIfUnchanged: true });
|
|
3361
|
+
|
|
3362
|
+
return { artifacts, notes, agentId, dispatchId };
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3196
3365
|
function persistCompletionReport(dispatchItem, completion, source = 'fallback') {
|
|
3197
3366
|
if (!dispatchItem?.id || !completion || typeof completion !== 'object') return completion;
|
|
3198
3367
|
const reportPath = dispatchItem?.meta?.completionReportPath || shared.dispatchCompletionReportPath(dispatchItem.id);
|
|
@@ -3408,8 +3577,8 @@ function parseCompletionNoop(completion) {
|
|
|
3408
3577
|
|
|
3409
3578
|
function normalizeReviewVerdict(verdict) {
|
|
3410
3579
|
const value = String(verdict || '').trim().toLowerCase().replace(/[\s-]+/g, '_');
|
|
3411
|
-
if (value === 'approve' || value === 'approved') return
|
|
3412
|
-
if (value === 'request_changes' || value === 'changes_requested' || value === 'changes-requested') return
|
|
3580
|
+
if (value === 'approve' || value === 'approved') return REVIEW_STATUS.APPROVED;
|
|
3581
|
+
if (value === 'request_changes' || value === 'changes_requested' || value === 'changes-requested') return REVIEW_STATUS.CHANGES_REQUESTED;
|
|
3413
3582
|
return null;
|
|
3414
3583
|
}
|
|
3415
3584
|
|
|
@@ -3795,8 +3964,8 @@ function dispatchReReviewForFix(fixDispatchItem, meta, config) {
|
|
|
3795
3964
|
return null;
|
|
3796
3965
|
}
|
|
3797
3966
|
const reviewStatus = livePr.reviewStatus || '';
|
|
3798
|
-
if (reviewStatus !==
|
|
3799
|
-
log('info', `Re-review skipped for ${pr.id}: reviewStatus=${reviewStatus ||
|
|
3967
|
+
if (reviewStatus !== REVIEW_STATUS.CHANGES_REQUESTED && reviewStatus !== REVIEW_STATUS.WAITING) {
|
|
3968
|
+
log('info', `Re-review skipped for ${pr.id}: reviewStatus=${reviewStatus || REVIEW_STATUS.PENDING} (only changes-requested/waiting trigger closure-loop)`);
|
|
3800
3969
|
return null;
|
|
3801
3970
|
}
|
|
3802
3971
|
|
|
@@ -4305,6 +4474,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
4305
4474
|
meta._noopReason = noopRationale.slice(0, 500);
|
|
4306
4475
|
}
|
|
4307
4476
|
updateWorkItemStatus(meta, WI_STATUS.DONE, '');
|
|
4477
|
+
promoteCompletionArtifacts(meta, agentId, dispatchItem.id, structuredCompletion, { resultSummary });
|
|
4308
4478
|
}
|
|
4309
4479
|
// Failure retry is handled by completeDispatch in dispatch.js — not duplicated here.
|
|
4310
4480
|
// Only clear _decomposing flag on failure so decompose items don't get permanently stuck.
|
|
@@ -4895,6 +5065,10 @@ module.exports = {
|
|
|
4895
5065
|
enforcePrAttachmentContract,
|
|
4896
5066
|
markMissingPrAttachment,
|
|
4897
5067
|
parseCompletionReportFile,
|
|
5068
|
+
normalizeCompletionArtifacts,
|
|
5069
|
+
completionArtifactToNoteEntry,
|
|
5070
|
+
mergeArtifactNotes,
|
|
5071
|
+
promoteCompletionArtifacts,
|
|
4898
5072
|
persistCompletionReport,
|
|
4899
5073
|
runPostCompletionHooks,
|
|
4900
5074
|
syncPrdFromPrs,
|
package/engine/projects.js
CHANGED
|
@@ -137,6 +137,10 @@ function removeProject(target, options = {}) {
|
|
|
137
137
|
pipelineRefs: [],
|
|
138
138
|
archivedTo: null,
|
|
139
139
|
purgedDataDir: false,
|
|
140
|
+
// P-bfa2d-remove-project-cleanup — soft-delete tag counts for entries in
|
|
141
|
+
// the three central state files that survived the hard-cancel passes
|
|
142
|
+
// (steps 1, 2). See step 7.5 for the orphan-tag policy rationale.
|
|
143
|
+
orphanedRefs: { pullRequests: 0, workItems: 0, dispatches: 0 },
|
|
140
144
|
warnings: [],
|
|
141
145
|
};
|
|
142
146
|
|
|
@@ -300,6 +304,94 @@ function removeProject(target, options = {}) {
|
|
|
300
304
|
} catch (e) { writeError = e; }
|
|
301
305
|
if (writeError) return { ...summary, error: 'Failed to write config: ' + writeError.message };
|
|
302
306
|
|
|
307
|
+
// 7.5. P-bfa2d-remove-project-cleanup — Tag dangling refs in the three
|
|
308
|
+
// CENTRAL state files (`pull-requests.json`, `work-items.json`,
|
|
309
|
+
// `engine/dispatch.json`) so downstream consumers can filter or sweep
|
|
310
|
+
// them. Project-local files in `projects/<name>/` are handled by
|
|
311
|
+
// step 8 (archive or purge), so this step only touches central state.
|
|
312
|
+
//
|
|
313
|
+
// Policy is soft-delete (`_orphaned_at` + `_orphaned_project` tags)
|
|
314
|
+
// rather than hard-delete: preserves history for debugging, lets users
|
|
315
|
+
// see exactly which records were affected if a wrong project name was
|
|
316
|
+
// passed, and is reversible (strip the tags). Steps 1, 2 already
|
|
317
|
+
// hard-cancel/drain matching pending entries; this step only catches
|
|
318
|
+
// survivors (non-pending WIs, central PRs, defense-in-depth dispatch
|
|
319
|
+
// survivors).
|
|
320
|
+
//
|
|
321
|
+
// Lock acquisition is sequential and uses the mutate-* helpers from
|
|
322
|
+
// engine/dispatch.js and engine/shared.js so we never hold two file
|
|
323
|
+
// locks simultaneously. Order: dispatch.json → pull-requests.json →
|
|
324
|
+
// work-items.json (alphabetical by basename). Each helper acquires
|
|
325
|
+
// its own lock, runs the synchronous mutator, then releases before
|
|
326
|
+
// the next helper runs.
|
|
327
|
+
try {
|
|
328
|
+
const orphanedAt = new Date().toISOString();
|
|
329
|
+
const orphanedProject = project.name;
|
|
330
|
+
|
|
331
|
+
// (a) engine/dispatch.json — defense-in-depth. cleanDispatchEntries in
|
|
332
|
+
// step 2 should have already removed every matching entry; tag any
|
|
333
|
+
// survivor so the operator can see the residue.
|
|
334
|
+
try {
|
|
335
|
+
dispatch.mutateDispatch((state) => {
|
|
336
|
+
for (const queue of ['pending', 'active', 'completed']) {
|
|
337
|
+
const arr = Array.isArray(state?.[queue]) ? state[queue] : [];
|
|
338
|
+
for (const d of arr) {
|
|
339
|
+
if (!d || d._orphaned_at) continue;
|
|
340
|
+
if (!_dispatchMatchesProject(d, project, projects)) continue;
|
|
341
|
+
d._orphaned_at = orphanedAt;
|
|
342
|
+
d._orphaned_project = orphanedProject;
|
|
343
|
+
summary.orphanedRefs.dispatches++;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return state;
|
|
347
|
+
});
|
|
348
|
+
} catch (e) { summary.warnings.push('orphan-tag dispatch: ' + e.message); }
|
|
349
|
+
|
|
350
|
+
// (b) MINIONS_DIR/pull-requests.json — central PR registry. PRs in this
|
|
351
|
+
// file lack an explicit project field; match by canonical scope
|
|
352
|
+
// (e.g. `github:owner/repo`) against the removed project's PR scope.
|
|
353
|
+
const centralPrPath = shared.centralPullRequestsPath();
|
|
354
|
+
if (fs.existsSync(centralPrPath)) {
|
|
355
|
+
try {
|
|
356
|
+
const projectScope = shared.getProjectPrScope(project);
|
|
357
|
+
if (projectScope) {
|
|
358
|
+
shared.mutatePullRequests(centralPrPath, (prs) => {
|
|
359
|
+
for (const pr of prs) {
|
|
360
|
+
if (!pr || pr._orphaned_at) continue;
|
|
361
|
+
const canonical = shared.parseCanonicalPrId(pr.id);
|
|
362
|
+
if (canonical?.scope !== projectScope) continue;
|
|
363
|
+
pr._orphaned_at = orphanedAt;
|
|
364
|
+
pr._orphaned_project = orphanedProject;
|
|
365
|
+
summary.orphanedRefs.pullRequests++;
|
|
366
|
+
}
|
|
367
|
+
return prs;
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
} catch (e) { summary.warnings.push('orphan-tag pull-requests: ' + e.message); }
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// (c) MINIONS_DIR/work-items.json — central WI list. Pending/queued WIs
|
|
374
|
+
// were already CANCELLED by cancelPendingWorkItems in step 1; tag
|
|
375
|
+
// the remaining matchers (dispatched / done / failed / etc.) so
|
|
376
|
+
// downstream consumers can filter them.
|
|
377
|
+
const centralWiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
378
|
+
if (fs.existsSync(centralWiPath)) {
|
|
379
|
+
try {
|
|
380
|
+
shared.mutateWorkItems(centralWiPath, (items) => {
|
|
381
|
+
for (const w of items) {
|
|
382
|
+
if (!w || w._orphaned_at) continue;
|
|
383
|
+
if (w.status === shared.WI_STATUS.CANCELLED) continue;
|
|
384
|
+
if (!_workItemMatchesProject(w, project, projects)) continue;
|
|
385
|
+
w._orphaned_at = orphanedAt;
|
|
386
|
+
w._orphaned_project = orphanedProject;
|
|
387
|
+
summary.orphanedRefs.workItems++;
|
|
388
|
+
}
|
|
389
|
+
return items;
|
|
390
|
+
});
|
|
391
|
+
} catch (e) { summary.warnings.push('orphan-tag work-items: ' + e.message); }
|
|
392
|
+
}
|
|
393
|
+
} catch (e) { summary.warnings.push('orphan-sweep: ' + e.message); }
|
|
394
|
+
|
|
303
395
|
// 8. Move (or purge) projects/<name>/ — preserves PR/work-item history by
|
|
304
396
|
// default so a re-add can pick up where it left off.
|
|
305
397
|
const dataDir = shared.projectStateDir(project);
|