@yemi33/minions 0.1.2304 → 0.1.2306
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 +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 +13 -14
- package/dashboard/js/settings.js +3 -3
- package/dashboard/layout.html +13 -0
- package/dashboard/slim/js/modals-tiles.js +21 -130
- package/dashboard/slim/styles.css +9 -14
- package/dashboard/styles.css +79 -0
- package/dashboard.js +41 -9
- package/docs/completion-reports.md +25 -0
- package/docs/copilot-cli-schema.md +31 -1
- package/docs/design-state-storage.md +1 -1
- package/docs/live-checkout-mode.md +5 -3
- package/engine/ado.js +37 -0
- package/engine/consolidation.js +59 -2
- package/engine/lifecycle.js +149 -0
- package/engine/live-checkout.js +290 -79
- package/engine/playbook.js +2 -2
- package/engine/queries.js +11 -7
- package/engine/shared.js +6 -5
- package/engine/supervisor.js +131 -19
- package/engine.js +2 -2
- package/package.json +1 -1
package/engine/ado.js
CHANGED
|
@@ -42,6 +42,23 @@ function isGitHubProject(project) {
|
|
|
42
42
|
return String(project?.repoHost || '').toLowerCase() === 'github';
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// #3758 — True when at least one configured project is NOT a GitHub project
|
|
46
|
+
// (i.e., could plausibly need ADO). Tick-loop entry points
|
|
47
|
+
// (pollPrStatus/pollPrHumanComments/reconcilePrs) use this to stay fully
|
|
48
|
+
// inert on a GitHub-only install: without it, a config with adoPollEnabled
|
|
49
|
+
// left at its default attempts az/azureauth token acquisition every poll for
|
|
50
|
+
// no reason, producing a permanent, noisy — and, per the failure this
|
|
51
|
+
// closes, potentially fatal — failure loop.
|
|
52
|
+
//
|
|
53
|
+
// Deliberately does NOT require adoOrg/adoProject to already be populated:
|
|
54
|
+
// several legitimate ADO projects start with those fields blank and get them
|
|
55
|
+
// repaired in-place (from a tracked PR URL, canonical PR id, or `git remote`)
|
|
56
|
+
// by repairAdoProjectConfig() inside forEachActivePr/reconcilePrs itself —
|
|
57
|
+
// gating on adoOrg/adoProject here would skip that repair before it ever ran.
|
|
58
|
+
function hasAdoProjectConfigured(config) {
|
|
59
|
+
return shared.getProjects(config).some(p => !isGitHubProject(p));
|
|
60
|
+
}
|
|
61
|
+
|
|
45
62
|
function isAdoGuid(value) {
|
|
46
63
|
return ADO_GUID_RE.test(String(value || '').trim());
|
|
47
64
|
}
|
|
@@ -1165,6 +1182,13 @@ async function forEachActivePr(config, token, callback) {
|
|
|
1165
1182
|
if (currentPrs[idx].reviewStatus === REVIEW_STATUS.APPROVED && after.reviewStatus !== REVIEW_STATUS.APPROVED) {
|
|
1166
1183
|
after.reviewStatus = REVIEW_STATUS.APPROVED;
|
|
1167
1184
|
}
|
|
1185
|
+
// Never downgrade status from 'merged' — permanent terminal state. A
|
|
1186
|
+
// stale/out-of-order poll response could otherwise overwrite a concurrent
|
|
1187
|
+
// success/merge write-back (mirrors github.js:564-570 and the central
|
|
1188
|
+
// ADO write-back guard at ado.js:1327-1330).
|
|
1189
|
+
if (currentPrs[idx].status === PR_STATUS.MERGED && after.status !== PR_STATUS.MERGED) {
|
|
1190
|
+
after.status = PR_STATUS.MERGED;
|
|
1191
|
+
}
|
|
1168
1192
|
shared.applyPrFieldDelta(currentPrs[idx], before, after);
|
|
1169
1193
|
}
|
|
1170
1194
|
// Don't push if not found — it was deleted by another writer, respect that
|
|
@@ -1328,6 +1352,10 @@ async function forEachActivePr(config, token, callback) {
|
|
|
1328
1352
|
async function pollPrStatus(config) {
|
|
1329
1353
|
_adoPollHadAuthFailure = false; // reset before polling — set again if errors recur
|
|
1330
1354
|
|
|
1355
|
+
// #3758 — inert when no project actually uses ADO. Skip token acquisition
|
|
1356
|
+
// entirely rather than spawning az/azureauth for a GitHub-only install.
|
|
1357
|
+
if (!hasAdoProjectConfigured(config)) return;
|
|
1358
|
+
|
|
1331
1359
|
const token = await getAdoToken();
|
|
1332
1360
|
if (!token) {
|
|
1333
1361
|
const w = consumeNoAdoTokenWarning();
|
|
@@ -1882,6 +1910,9 @@ async function pollPrStatus(config) {
|
|
|
1882
1910
|
// ─── Poll Human Comments on PRs ──────────────────────────────────────────────
|
|
1883
1911
|
|
|
1884
1912
|
async function pollPrHumanComments(config) {
|
|
1913
|
+
// #3758 — inert when no project actually uses ADO.
|
|
1914
|
+
if (!hasAdoProjectConfigured(config)) return;
|
|
1915
|
+
|
|
1885
1916
|
const token = await getAdoToken();
|
|
1886
1917
|
if (!token) return;
|
|
1887
1918
|
|
|
@@ -2100,6 +2131,9 @@ async function pollPrHumanComments(config) {
|
|
|
2100
2131
|
* in pull-requests.json, and add them. Matches PRs to work items by branch name.
|
|
2101
2132
|
*/
|
|
2102
2133
|
async function reconcilePrs(config) {
|
|
2134
|
+
// #3758 — inert when no project actually uses ADO.
|
|
2135
|
+
if (!hasAdoProjectConfigured(config)) return;
|
|
2136
|
+
|
|
2103
2137
|
const token = await getAdoToken();
|
|
2104
2138
|
if (!token) {
|
|
2105
2139
|
const w = consumeNoAdoTokenWarning();
|
|
@@ -2984,4 +3018,7 @@ module.exports = {
|
|
|
2984
3018
|
isPrValidationBuild,
|
|
2985
3019
|
shouldRequeueStaleBuild,
|
|
2986
3020
|
PR_BUILD_REQUEUE_INTERVAL_MS,
|
|
3021
|
+
// #3758 — exported for unit tests of GitHub-only-config inertness.
|
|
3022
|
+
isGitHubProject,
|
|
3023
|
+
hasAdoProjectConfigured,
|
|
2987
3024
|
};
|
package/engine/consolidation.js
CHANGED
|
@@ -1136,6 +1136,52 @@ function consolidateWithRegex(items, files, config) {
|
|
|
1136
1136
|
|
|
1137
1137
|
// ─── Knowledge Base Classification ───────────────────────────────────────────
|
|
1138
1138
|
|
|
1139
|
+
// Matches the reusable-knowledge section headers agents conventionally use in
|
|
1140
|
+
// their inbox findings notes ("## Patterns / Conventions", "## Bugs & Gotchas",
|
|
1141
|
+
// "## Dependencies", etc. — see notes.md consolidation history). Presence
|
|
1142
|
+
// signals the note carries generalizable knowledge worth a full-body KB copy;
|
|
1143
|
+
// absence means the note is most likely a narrow, one-off deliverable (a
|
|
1144
|
+
// single PR review, a task status update, a one-time plan) that should get a
|
|
1145
|
+
// condensed stub instead of a full-body duplicate (issue #604).
|
|
1146
|
+
// The keyword doesn't have to open the heading — "#### Bugs & Gotchas" (the
|
|
1147
|
+
// exact header this module itself generates for regex-fallback consolidation
|
|
1148
|
+
// entries, see catLabels above) puts "Bugs" before "Gotchas". Allow up to a
|
|
1149
|
+
// few leading heading words (bounded to avoid ReDoS) before the keyword.
|
|
1150
|
+
const REUSABLE_SIGNAL_RE = /^#{1,4}\s*(?:[\w&/-]+\s+){0,4}(patterns?(\s*(&|and|\/)\s*conventions?)?|conventions?|gotchas?|dependencies|best practices?)\b/im;
|
|
1151
|
+
|
|
1152
|
+
function hasReusableSignal(content) {
|
|
1153
|
+
return REUSABLE_SIGNAL_RE.test(content || '');
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
/**
|
|
1157
|
+
* Build a condensed KB stub for narrowly-scoped, one-off notes instead of
|
|
1158
|
+
* duplicating the full body (issue #604 — classifyToKnowledgeBase was copying
|
|
1159
|
+
* every consolidated inbox note's ENTIRE body into knowledge/<category>/,
|
|
1160
|
+
* doubling disk usage and paying a recurring context/token cost on every
|
|
1161
|
+
* future dispatch that primes from knowledge/ before notes/inbox/).
|
|
1162
|
+
*
|
|
1163
|
+
* Deterministic and LLM-free: keeps the note's H1 title (if any) plus its
|
|
1164
|
+
* first substantive paragraph as a short abstract, then links back to the
|
|
1165
|
+
* archived note for anyone who needs the full detail. Existing full-body KB
|
|
1166
|
+
* files already on disk are untouched — this only changes how NEW entries
|
|
1167
|
+
* are written.
|
|
1168
|
+
*/
|
|
1169
|
+
function buildCondensedKbBody(content, titleLine, archiveRelPath) {
|
|
1170
|
+
const body = String(content || '').trim();
|
|
1171
|
+
const withoutTitle = titleLine ? body.replace(/^#\s+.+(\r?\n)+/, '') : body;
|
|
1172
|
+
const firstBlock = (withoutTitle.split(/\r?\n\s*\r?\n/).find(b => b.trim().length > 0) || '').trim();
|
|
1173
|
+
const MAX_LEN = 500;
|
|
1174
|
+
let abstract = firstBlock;
|
|
1175
|
+
if (abstract.length > MAX_LEN) {
|
|
1176
|
+
abstract = abstract.slice(0, MAX_LEN).replace(/\s+\S*$/, '').trim() + '\u2026';
|
|
1177
|
+
}
|
|
1178
|
+
const parts = [];
|
|
1179
|
+
if (titleLine) parts.push(titleLine.trim());
|
|
1180
|
+
parts.push(abstract || '_(no summary available)_');
|
|
1181
|
+
parts.push(`_Full note: \`${archiveRelPath}\`._`);
|
|
1182
|
+
return parts.join('\n\n') + '\n';
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1139
1185
|
async function classifyToKnowledgeBase(items, config) {
|
|
1140
1186
|
|
|
1141
1187
|
if (!fs.existsSync(KNOWLEDGE_DIR)) fs.mkdirSync(KNOWLEDGE_DIR, { recursive: true });
|
|
@@ -1174,9 +1220,18 @@ async function classifyToKnowledgeBase(items, config) {
|
|
|
1174
1220
|
const kbFilename = `${dateStamp()}-${agent}-${titleSlug}.md`;
|
|
1175
1221
|
const kbPath = shared.uniquePath(path.join(categoryDirs[category], kbFilename));
|
|
1176
1222
|
|
|
1177
|
-
|
|
1223
|
+
// Reserve full-body KB copies for content that's actually general/reusable
|
|
1224
|
+
// (patterns, conventions, gotchas); narrowly-scoped one-off notes get a
|
|
1225
|
+
// condensed stub + link back to the archived note instead (issue #604).
|
|
1226
|
+
const reusable = category === 'conventions' || hasReusableSignal(content);
|
|
1227
|
+
const archiveRelPath = `notes/archive/${dateStamp()}-${item.name}`;
|
|
1228
|
+
const kbBody = reusable
|
|
1229
|
+
? content
|
|
1230
|
+
: buildCondensedKbBody(content, titleMatch ? titleMatch[0] : null, archiveRelPath);
|
|
1231
|
+
|
|
1232
|
+
const frontmatter = `---\nsource: ${item.name}\nagent: ${agent}\ncategory: ${category}\ndate: ${dateStamp()}\nreusable: ${reusable}\n---\n\n`;
|
|
1178
1233
|
try {
|
|
1179
|
-
safeWrite(kbPath, frontmatter +
|
|
1234
|
+
safeWrite(kbPath, frontmatter + kbBody);
|
|
1180
1235
|
classified++;
|
|
1181
1236
|
} catch (err) {
|
|
1182
1237
|
log('warn', `Failed to classify ${item.name} to knowledge base: ${err.message}`);
|
|
@@ -1340,4 +1395,6 @@ module.exports = {
|
|
|
1340
1395
|
archiveInboxFiles,
|
|
1341
1396
|
_capNotesPreservingOverflow,
|
|
1342
1397
|
NOTES_MAX_BYTES,
|
|
1398
|
+
hasReusableSignal,
|
|
1399
|
+
buildCondensedKbBody,
|
|
1343
1400
|
};
|
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).
|
|
@@ -6400,7 +6547,9 @@ module.exports = {
|
|
|
6400
6547
|
mergeArtifactNotes,
|
|
6401
6548
|
promoteCompletionArtifacts,
|
|
6402
6549
|
runPostCompletionHooks,
|
|
6550
|
+
applyGoalInvalidation,
|
|
6403
6551
|
syncPrdFromPrs,
|
|
6552
|
+
advancePrdStatusOnAllItemsDone,
|
|
6404
6553
|
persistVerifyPrsToPrd,
|
|
6405
6554
|
resolveWorkItemPath,
|
|
6406
6555
|
isItemCompleted,
|