@yemi33/minions 0.1.2305 → 0.1.2307
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 +1 -0
- package/dashboard/js/refresh.js +17 -13
- package/dashboard/js/render-other.js +251 -3
- package/dashboard/js/render-plans.js +1 -1
- package/dashboard/js/render-prs.js +4 -1
- package/dashboard/js/render-utils.js +13 -0
- package/dashboard/js/render-work-items.js +11 -13
- package/dashboard/js/settings.js +30 -3
- package/dashboard/slim/body.html +6 -22
- package/dashboard/slim/js/modals-tiles.js +32 -22
- package/dashboard/slim/js/plans.js +24 -516
- package/dashboard/slim/styles.css +10 -7
- package/dashboard/styles.css +58 -0
- package/dashboard.js +66 -9
- package/docs/completion-reports.md +27 -2
- package/docs/copilot-cli-schema.md +31 -1
- package/docs/design-state-storage.md +1 -1
- package/docs/harness-transparency.md +18 -0
- package/docs/live-checkout-mode.md +57 -8
- package/engine/ado-comment.js +5 -2
- package/engine/ado.js +37 -0
- package/engine/cli.js +58 -3
- package/engine/comment-format.js +82 -2
- package/engine/consolidation.js +59 -2
- package/engine/discover-project-skills.js +4 -0
- package/engine/dispatch.js +36 -1
- package/engine/gh-comment.js +8 -3
- package/engine/lifecycle.js +241 -25
- package/engine/live-checkout.js +335 -7
- package/engine/playbook.js +2 -2
- package/engine/pre-dispatch-eval.js +54 -5
- package/engine/queries.js +11 -7
- package/engine/shared.js +96 -6
- package/engine/supervisor.js +144 -19
- package/engine/watchdog.js +10 -0
- package/engine.js +210 -5
- package/package.json +1 -1
- package/playbooks/review.md +2 -0
- package/playbooks/shared-rules.md +9 -0
package/engine/lifecycle.js
CHANGED
|
@@ -4981,6 +4981,77 @@ function pickReReviewAgentHints(config, opts = {}) {
|
|
|
4981
4981
|
return out;
|
|
4982
4982
|
}
|
|
4983
4983
|
|
|
4984
|
+
/**
|
|
4985
|
+
* P-mqyp0009y025z6a7 — Goal invalidation.
|
|
4986
|
+
*
|
|
4987
|
+
* When a completing WI's report carries a non-empty `invalidates: [wi-id, …]`
|
|
4988
|
+
* array, cancel each listed WI that is currently pending or queued.
|
|
4989
|
+
*
|
|
4990
|
+
* Rules:
|
|
4991
|
+
* - Only cancels WIs in status 'pending' or 'queued'.
|
|
4992
|
+
* - WIs in any other status (dispatched, done, failed, cancelled, …) are
|
|
4993
|
+
* skipped with a warning log — they are not cancelled.
|
|
4994
|
+
* - Missing WI IDs produce a warning but do not throw.
|
|
4995
|
+
* - Uses mutateWorkItems (SQL-canonical) so the dashboard reflects the change.
|
|
4996
|
+
*/
|
|
4997
|
+
function applyGoalInvalidation(sourceWiId, invalidates, config) {
|
|
4998
|
+
if (!Array.isArray(invalidates) || invalidates.length === 0) return;
|
|
4999
|
+
|
|
5000
|
+
const wiPaths = getWorkItemPaths(config);
|
|
5001
|
+
const cancellableStatuses = new Set([WI_STATUS.PENDING, WI_STATUS.QUEUED]);
|
|
5002
|
+
const cancellationReason = `invalidated-by:${sourceWiId}`;
|
|
5003
|
+
|
|
5004
|
+
for (const targetId of invalidates) {
|
|
5005
|
+
if (!targetId || typeof targetId !== 'string') continue;
|
|
5006
|
+
|
|
5007
|
+
// Find which file contains the WI.
|
|
5008
|
+
let foundPath = null;
|
|
5009
|
+
let foundStatus = null;
|
|
5010
|
+
for (const wiPath of wiPaths) {
|
|
5011
|
+
try {
|
|
5012
|
+
const items = readOptionalJsonStrict(wiPath, 'work-items', Array.isArray) || [];
|
|
5013
|
+
const item = items.find(i => i && i.id === targetId);
|
|
5014
|
+
if (item) {
|
|
5015
|
+
foundPath = wiPath;
|
|
5016
|
+
foundStatus = item.status;
|
|
5017
|
+
break;
|
|
5018
|
+
}
|
|
5019
|
+
} catch { /* best-effort scan */ }
|
|
5020
|
+
}
|
|
5021
|
+
|
|
5022
|
+
if (!foundPath) {
|
|
5023
|
+
log('warn', `Goal invalidation: WI ${targetId} listed in invalidates[] by ${sourceWiId} not found — skipping`);
|
|
5024
|
+
continue;
|
|
5025
|
+
}
|
|
5026
|
+
|
|
5027
|
+
if (!cancellableStatuses.has(foundStatus)) {
|
|
5028
|
+
log('warn', `Goal invalidation: WI ${targetId} is in status '${foundStatus}' — only pending/queued WIs are cancelled; skipping`);
|
|
5029
|
+
continue;
|
|
5030
|
+
}
|
|
5031
|
+
|
|
5032
|
+
// Cancel the WI via mutateWorkItems so the SQL store + state event are updated.
|
|
5033
|
+
try {
|
|
5034
|
+
mutateWorkItems(foundPath, (items) => {
|
|
5035
|
+
if (!Array.isArray(items)) return items;
|
|
5036
|
+
const wi = items.find(i => i && i.id === targetId);
|
|
5037
|
+
if (!wi) return items;
|
|
5038
|
+
// Re-check status inside the lock to avoid TOCTOU races.
|
|
5039
|
+
if (!cancellableStatuses.has(wi.status)) {
|
|
5040
|
+
log('warn', `Goal invalidation: WI ${targetId} changed to '${wi.status}' before cancel lock acquired — skipping`);
|
|
5041
|
+
return items;
|
|
5042
|
+
}
|
|
5043
|
+
wi.status = WI_STATUS.CANCELLED;
|
|
5044
|
+
wi.cancellationReason = cancellationReason;
|
|
5045
|
+
wi.cancelledAt = ts();
|
|
5046
|
+
log('info', `Goal invalidation: cancelled WI ${targetId} (${cancellationReason})`);
|
|
5047
|
+
return items;
|
|
5048
|
+
});
|
|
5049
|
+
} catch (err) {
|
|
5050
|
+
log('warn', `Goal invalidation: failed to cancel WI ${targetId}: ${err.message}`);
|
|
5051
|
+
}
|
|
5052
|
+
}
|
|
5053
|
+
}
|
|
5054
|
+
|
|
4984
5055
|
async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, config, opts) {
|
|
4985
5056
|
|
|
4986
5057
|
const detectPhantom = !!(opts && opts.detectPhantom);
|
|
@@ -5459,6 +5530,11 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5459
5530
|
promoteCompletionArtifacts(meta, agentId, dispatchItem.id, structuredCompletion, { resultSummary });
|
|
5460
5531
|
// M003 — auto-dispatch a live-validation WI when a coding WI completes.
|
|
5461
5532
|
try { autoDispatchLiveValidationWi(meta, config); } catch (err) { log('warn', `autoDispatchLiveValidationWi: ${err.message}`); }
|
|
5533
|
+
// P-mqyp0009y025z6a7 — goal invalidation: cancel any pending/queued WIs
|
|
5534
|
+
// listed in the completion report's optional `invalidates[]` field.
|
|
5535
|
+
if (structuredCompletion && Array.isArray(structuredCompletion.invalidates) && structuredCompletion.invalidates.length > 0) {
|
|
5536
|
+
try { applyGoalInvalidation(meta.item.id, structuredCompletion.invalidates, config); } catch (err) { log('warn', `applyGoalInvalidation: ${err.message}`); }
|
|
5537
|
+
}
|
|
5462
5538
|
}
|
|
5463
5539
|
// Failure retry is handled by completeDispatch in dispatch.js — not duplicated here.
|
|
5464
5540
|
// Only clear _decomposing flag on failure so decompose items don't get permanently stuck.
|
|
@@ -5927,12 +6003,83 @@ function syncPrdFromPrs(config) {
|
|
|
5927
6003
|
if (sourcePlan) stampPrdItemPrUrl(itemId, sourcePlan, pr.url);
|
|
5928
6004
|
}
|
|
5929
6005
|
}
|
|
6006
|
+
// Advance PRD top-level status when all items are done and all linked PRs merged
|
|
6007
|
+
advancePrdStatusOnAllItemsDone(config);
|
|
5930
6008
|
} catch (err) {
|
|
5931
6009
|
// Non-fatal — log and continue
|
|
5932
6010
|
try { log('warn', `syncPrdFromPrs error: ${err?.message || err}`); } catch { /* engine not available */ }
|
|
5933
6011
|
}
|
|
5934
6012
|
}
|
|
5935
6013
|
|
|
6014
|
+
// ─── PRD Top-Level Status Auto-Completion (P-d7e8f9a1) ───────────────────────
|
|
6015
|
+
// Runs at the end of syncPrdFromPrs (same PR-poll cadence). Advances PRD
|
|
6016
|
+
// status to 'completed' when every missing_features item satisfies both:
|
|
6017
|
+
// 1. A work item with that id exists in done status (DONE_STATUSES)
|
|
6018
|
+
// 2. The linked PR (WI._pr) is merged — or the item has no linked PR
|
|
6019
|
+
// (non-code items that never raised a PR)
|
|
6020
|
+
// Paused, rejected, awaiting-approval, completed, and archived PRDs are
|
|
6021
|
+
// skipped via _PRD_FROZEN_STATUSES. The locked write is idempotent.
|
|
6022
|
+
function advancePrdStatusOnAllItemsDone(config) {
|
|
6023
|
+
if (!fs.existsSync(PRD_DIR)) return;
|
|
6024
|
+
let prdFiles;
|
|
6025
|
+
try { prdFiles = fs.readdirSync(PRD_DIR).filter(f => f.endsWith('.json')); } catch { return; }
|
|
6026
|
+
if (!prdFiles.length) return;
|
|
6027
|
+
|
|
6028
|
+
const allWorkItems = queries.getWorkItems(config);
|
|
6029
|
+
if (!allWorkItems.length) return;
|
|
6030
|
+
|
|
6031
|
+
// Index done WIs by id for O(1) lookup
|
|
6032
|
+
const doneWiById = new Map();
|
|
6033
|
+
for (const wi of allWorkItems) {
|
|
6034
|
+
if (wi.id && DONE_STATUSES.has(wi.status)) doneWiById.set(wi.id, wi);
|
|
6035
|
+
}
|
|
6036
|
+
if (!doneWiById.size) return;
|
|
6037
|
+
|
|
6038
|
+
// Collect all PRs by id across all projects for merged-status lookup
|
|
6039
|
+
const prById = new Map();
|
|
6040
|
+
for (const project of shared.getProjects(config)) {
|
|
6041
|
+
for (const pr of safeJsonArr(shared.projectPrPath(project))) {
|
|
6042
|
+
if (pr?.id) prById.set(pr.id, pr);
|
|
6043
|
+
}
|
|
6044
|
+
}
|
|
6045
|
+
|
|
6046
|
+
for (const file of prdFiles) {
|
|
6047
|
+
try {
|
|
6048
|
+
const fpath = path.join(PRD_DIR, file);
|
|
6049
|
+
const plan = safeJsonNoRestore(fpath);
|
|
6050
|
+
if (!plan?.missing_features?.length) continue;
|
|
6051
|
+
// Skip frozen PRDs — paused/rejected/awaiting-approval/completed/archived
|
|
6052
|
+
if (_PRD_FROZEN_STATUSES.has(plan.status)) continue;
|
|
6053
|
+
|
|
6054
|
+
const allDone = plan.missing_features.every(feature => {
|
|
6055
|
+
const wi = doneWiById.get(feature.id);
|
|
6056
|
+
if (!wi) return false; // no done WI for this feature
|
|
6057
|
+
// If the WI has a linked PR, it must be merged
|
|
6058
|
+
if (wi._pr) {
|
|
6059
|
+
const pr = prById.get(wi._pr);
|
|
6060
|
+
if (!pr) return true; // PR no longer in tracker (swept after merge) — allow
|
|
6061
|
+
return pr.status === PR_STATUS.MERGED;
|
|
6062
|
+
}
|
|
6063
|
+
// No linked PR — non-code item or PR not tracked; done WI is sufficient
|
|
6064
|
+
return true;
|
|
6065
|
+
});
|
|
6066
|
+
|
|
6067
|
+
if (!allDone) continue;
|
|
6068
|
+
|
|
6069
|
+
mutateJsonFileLocked(fpath, (data) => {
|
|
6070
|
+
// Re-check under lock — another writer may have frozen this PRD
|
|
6071
|
+
if (_PRD_FROZEN_STATUSES.has(data.status)) return data;
|
|
6072
|
+
data.status = PLAN_STATUS.COMPLETED;
|
|
6073
|
+
if (!data.completedAt) data.completedAt = ts();
|
|
6074
|
+
return data;
|
|
6075
|
+
}, { skipWriteIfUnchanged: true });
|
|
6076
|
+
log('info', `PRD auto-advance: ${file} → completed (all items done, all linked PRs merged)`);
|
|
6077
|
+
} catch (err) {
|
|
6078
|
+
log('warn', `advancePrdStatusOnAllItemsDone error for ${file}: ${err?.message || err}`);
|
|
6079
|
+
}
|
|
6080
|
+
}
|
|
6081
|
+
}
|
|
6082
|
+
|
|
5936
6083
|
// ─── Persist verify/E2E aggregate PRs onto the PRD JSON (W-mqps9jlb) ──────────
|
|
5937
6084
|
// The PRD view's "E2E Aggregate PRs" section (dashboard/js/render-prd.js#
|
|
5938
6085
|
// renderE2eSection) sources its PRs from the LIVE tracker (pull-requests.json).
|
|
@@ -6161,18 +6308,34 @@ function diagnoseEmptyOutput(failureClass, code, elapsedMs) {
|
|
|
6161
6308
|
return `[empty-output: process exited in ${elapsedMs}ms \u2014 possible causes: machine sleep, network unavailability, auth failure]`;
|
|
6162
6309
|
}
|
|
6163
6310
|
|
|
6164
|
-
// W-mqba5ulq000nd255 — Reconciliation sweep
|
|
6165
|
-
// `_invalidProjectScope: { reason: "pr_scope_mismatch" }`
|
|
6166
|
-
//
|
|
6167
|
-
//
|
|
6168
|
-
// record
|
|
6169
|
-
//
|
|
6311
|
+
// W-mqba5ulq000nd255 / W-mr2cqq12000b04f8 — Reconciliation sweep with two
|
|
6312
|
+
// defenses against mis-scoped `_invalidProjectScope: { reason: "pr_scope_mismatch" }`
|
|
6313
|
+
// records:
|
|
6314
|
+
//
|
|
6315
|
+
// 1. PRUNE — a sibling record for the same canonical pr.id already exists in
|
|
6316
|
+
// the correctly-scoped project (e.g. a fresh dispatch there re-synced it).
|
|
6317
|
+
// The mis-scoped duplicate is deleted; the sibling is the surviving record.
|
|
6170
6318
|
//
|
|
6171
|
-
//
|
|
6319
|
+
// 2. RELOCATE — no sibling exists yet, but a project IS configured whose
|
|
6320
|
+
// scope matches the PR's real repo (e.g. work dispatched under project
|
|
6321
|
+
// "minions-opg" opened a PR against `yemi33/minions`, and a "minions"
|
|
6322
|
+
// project mapping to that repo is configured). Without this, the record
|
|
6323
|
+
// would sit mis-scoped forever — the correct project's tracker (read by
|
|
6324
|
+
// repo-scoped schedules such as daily-merge-approved-minions-prs) would
|
|
6325
|
+
// never see it. The record is moved: upserted into the correct project's
|
|
6326
|
+
// file (which clears `_invalidProjectScope` since it's now correctly
|
|
6327
|
+
// scoped) and removed from the mis-scoped project's file.
|
|
6328
|
+
//
|
|
6329
|
+
// When no configured project matches the PR's real scope at all, the record
|
|
6330
|
+
// is left in place with its `_invalidProjectScope` stamp so operators can
|
|
6331
|
+
// still find it (still surfaced via GET /api/pull-requests aggregation).
|
|
6332
|
+
// Runs once per tick from engine.js after the ADO/GitHub reconcile polls finish.
|
|
6333
|
+
//
|
|
6334
|
+
// Returns { pruned, relocated, scanned } so the engine tick can log a summary line.
|
|
6172
6335
|
function pruneScopeMismatchDuplicatePrs(config) {
|
|
6173
6336
|
config = config || getConfig();
|
|
6174
6337
|
const projects = shared.getProjects(config) || [];
|
|
6175
|
-
if (projects.length === 0) return { pruned: 0, scanned: 0 };
|
|
6338
|
+
if (projects.length === 0) return { pruned: 0, relocated: 0, scanned: 0 };
|
|
6176
6339
|
|
|
6177
6340
|
// Map project name -> canonical scope so we can find which project IS the
|
|
6178
6341
|
// correctly-scoped owner for a given mismatch record.
|
|
@@ -6197,13 +6360,17 @@ function pruneScopeMismatchDuplicatePrs(config) {
|
|
|
6197
6360
|
}
|
|
6198
6361
|
|
|
6199
6362
|
// Build per-project delete sets keyed by id, so we batch one mutation per
|
|
6200
|
-
// affected project file.
|
|
6201
|
-
|
|
6363
|
+
// affected project file. Track prune-deletes and relocate-deletes in
|
|
6364
|
+
// separate id sets (per project) so `pruned` vs `relocated` counts stay
|
|
6365
|
+
// accurate even though both end up removing rows from the same file.
|
|
6366
|
+
const pruneDeletesByProject = new Map(); // projectName -> Set(prId)
|
|
6367
|
+
const relocateDeletesByProject = new Map(); // projectName -> Set(prId)
|
|
6368
|
+
// Records to insert into their correctly-scoped project's file.
|
|
6369
|
+
const relocationsByTargetProject = new Map(); // projectName -> [{ rec, owningScope }]
|
|
6202
6370
|
let scanned = 0;
|
|
6203
6371
|
let pruned = 0;
|
|
6204
6372
|
|
|
6205
6373
|
for (const records of byId.values()) {
|
|
6206
|
-
if (records.length < 2) continue;
|
|
6207
6374
|
for (const rec of records) {
|
|
6208
6375
|
scanned++;
|
|
6209
6376
|
if (!rec._invalidProjectScope || rec._invalidProjectScope.reason !== 'pr_scope_mismatch') continue;
|
|
@@ -6211,32 +6378,79 @@ function pruneScopeMismatchDuplicatePrs(config) {
|
|
|
6211
6378
|
if (!correctScope) continue;
|
|
6212
6379
|
const correctProject = projectByScope.get(correctScope);
|
|
6213
6380
|
if (!correctProject) continue; // no configured project owns the URL — keep as tracking
|
|
6214
|
-
// Sibling check: is there a record under the correct scope for the same id?
|
|
6215
|
-
const sibling = records.find(r => r !== rec && r._scope === correctProject.name);
|
|
6216
|
-
if (!sibling) continue;
|
|
6217
|
-
// Safe to prune.
|
|
6218
6381
|
const owningScope = rec._scope;
|
|
6219
6382
|
if (!owningScope || owningScope === 'central') continue;
|
|
6220
|
-
if (
|
|
6221
|
-
|
|
6383
|
+
if (owningScope === correctProject.name) continue; // already correctly scoped
|
|
6384
|
+
// Sibling check: is there a record under the correct scope for the same id?
|
|
6385
|
+
const sibling = records.find(r => r !== rec && r._scope === correctProject.name);
|
|
6386
|
+
if (sibling) {
|
|
6387
|
+
// Safe to prune — the sibling already owns the canonical record.
|
|
6388
|
+
if (!pruneDeletesByProject.has(owningScope)) pruneDeletesByProject.set(owningScope, new Set());
|
|
6389
|
+
pruneDeletesByProject.get(owningScope).add(rec.id);
|
|
6390
|
+
} else {
|
|
6391
|
+
// No sibling anywhere: relocate the record into the correctly-scoped
|
|
6392
|
+
// project instead of leaving it stuck mis-scoped indefinitely.
|
|
6393
|
+
if (!relocationsByTargetProject.has(correctProject.name)) relocationsByTargetProject.set(correctProject.name, []);
|
|
6394
|
+
relocationsByTargetProject.get(correctProject.name).push({ rec, owningScope });
|
|
6395
|
+
}
|
|
6222
6396
|
}
|
|
6223
6397
|
}
|
|
6224
6398
|
|
|
6225
|
-
|
|
6399
|
+
// Relocate first (insert into target), only marking the source record for
|
|
6400
|
+
// deletion once the insert into the correct project succeeds — this avoids
|
|
6401
|
+
// losing a record if the insert throws mid-way.
|
|
6402
|
+
let relocated = 0;
|
|
6403
|
+
for (const [targetProjectName, entries] of relocationsByTargetProject) {
|
|
6404
|
+
const targetProject = projects.find(p => p.name === targetProjectName);
|
|
6405
|
+
if (!targetProject) continue;
|
|
6406
|
+
const targetPrPath = projectPrPath(targetProject);
|
|
6407
|
+
for (const { rec, owningScope } of entries) {
|
|
6408
|
+
const entry = { ...rec };
|
|
6409
|
+
delete entry._scope;
|
|
6410
|
+
delete entry._invalidProjectScope;
|
|
6411
|
+
delete entry.project; // let upsertPullRequestRecord backfill from targetProject
|
|
6412
|
+
try {
|
|
6413
|
+
const result = shared.upsertPullRequestRecord(targetPrPath, entry, {
|
|
6414
|
+
project: targetProject,
|
|
6415
|
+
itemIds: Array.isArray(rec.prdItems) ? rec.prdItems : null,
|
|
6416
|
+
});
|
|
6417
|
+
if (result.created || result.linked) {
|
|
6418
|
+
relocated++;
|
|
6419
|
+
if (!relocateDeletesByProject.has(owningScope)) relocateDeletesByProject.set(owningScope, new Set());
|
|
6420
|
+
relocateDeletesByProject.get(owningScope).add(rec.id);
|
|
6421
|
+
log('info', `[pull-requests] relocated mis-scoped PR ${rec.id} from project=${owningScope} to correctly-scoped project=${targetProjectName}`);
|
|
6422
|
+
}
|
|
6423
|
+
} catch (err) {
|
|
6424
|
+
log('warn', `pruneScopeMismatchDuplicatePrs: failed to relocate ${rec.id} to ${targetProjectName}: ${err?.message || err}`);
|
|
6425
|
+
}
|
|
6426
|
+
}
|
|
6427
|
+
}
|
|
6428
|
+
|
|
6429
|
+
// Merge prune + relocate delete sets per project so each file is mutated once.
|
|
6430
|
+
const deletesByProject = new Map();
|
|
6431
|
+
for (const [projectName, ids] of pruneDeletesByProject) {
|
|
6432
|
+
deletesByProject.set(projectName, new Set(ids));
|
|
6433
|
+
}
|
|
6434
|
+
for (const [projectName, ids] of relocateDeletesByProject) {
|
|
6435
|
+
if (!deletesByProject.has(projectName)) deletesByProject.set(projectName, new Set());
|
|
6436
|
+
for (const id of ids) deletesByProject.get(projectName).add(id);
|
|
6437
|
+
}
|
|
6438
|
+
|
|
6439
|
+
if (deletesByProject.size === 0) return { pruned: 0, relocated, scanned };
|
|
6226
6440
|
|
|
6227
6441
|
for (const [projectName, idsToDelete] of deletesByProject) {
|
|
6228
6442
|
const project = projects.find(p => p.name === projectName);
|
|
6229
6443
|
if (!project) continue;
|
|
6230
6444
|
const prPath = projectPrPath(project);
|
|
6445
|
+
const pruneIds = pruneDeletesByProject.get(projectName) || new Set();
|
|
6231
6446
|
try {
|
|
6232
6447
|
shared.mutatePullRequests(prPath, (prs) => {
|
|
6233
|
-
const
|
|
6448
|
+
const deletedIds = prs.filter(p => idsToDelete.has(p?.id)).map(p => p.id);
|
|
6234
6449
|
const next = prs.filter(p => !idsToDelete.has(p?.id));
|
|
6235
|
-
const
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
log('info', `[pull-requests] pruned scope-mismatch duplicate ${id} from project=${projectName} (sibling exists in correctly-scoped project)`);
|
|
6450
|
+
for (const id of deletedIds) {
|
|
6451
|
+
if (pruneIds.has(id)) {
|
|
6452
|
+
pruned++;
|
|
6453
|
+
log('info', `[pull-requests] removed mis-scoped duplicate ${id} from project=${projectName}`);
|
|
6240
6454
|
}
|
|
6241
6455
|
}
|
|
6242
6456
|
return next;
|
|
@@ -6246,7 +6460,7 @@ function pruneScopeMismatchDuplicatePrs(config) {
|
|
|
6246
6460
|
}
|
|
6247
6461
|
}
|
|
6248
6462
|
|
|
6249
|
-
return { pruned, scanned };
|
|
6463
|
+
return { pruned, relocated, scanned };
|
|
6250
6464
|
}
|
|
6251
6465
|
|
|
6252
6466
|
// Repair helper: collapse prNumber duplicates across all project-scoped and
|
|
@@ -6400,7 +6614,9 @@ module.exports = {
|
|
|
6400
6614
|
mergeArtifactNotes,
|
|
6401
6615
|
promoteCompletionArtifacts,
|
|
6402
6616
|
runPostCompletionHooks,
|
|
6617
|
+
applyGoalInvalidation,
|
|
6403
6618
|
syncPrdFromPrs,
|
|
6619
|
+
advancePrdStatusOnAllItemsDone,
|
|
6404
6620
|
persistVerifyPrsToPrd,
|
|
6405
6621
|
resolveWorkItemPath,
|
|
6406
6622
|
isItemCompleted,
|