@yemi33/minions 0.1.2378 → 0.1.2380
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 +19 -9
- package/dashboard/js/refresh.js +2 -3
- package/dashboard/js/render-prd.js +1 -1
- package/dashboard/js/render-work-items.js +13 -6
- package/dashboard.js +393 -721
- package/docs/README.md +1 -0
- package/docs/architecture-review-2026-07-09.md +2 -4
- package/docs/auto-discovery.md +36 -49
- package/docs/blog-first-successful-dispatch.md +1 -1
- package/docs/branch-derivation.md +2 -2
- package/docs/command-center.md +1 -1
- package/docs/completion-reports.md +14 -4
- package/docs/constellation-bridge.md +59 -10
- package/docs/constellation-style-telemetry.md +6 -6
- package/docs/cooldown-merge-semantics.md +4 -0
- package/docs/copilot-cli-schema.md +3 -3
- package/docs/cross-repo-plans.md +17 -17
- package/docs/deprecated.json +2 -2
- package/docs/design-inbox-entries-schema.md +31 -39
- package/docs/design-state-storage.md +1 -1
- package/docs/documentation-audit-2026-07-09.md +2 -2
- package/docs/engine-restart.md +20 -8
- package/docs/harness-mode.md +1 -1
- package/docs/managed-spawn.md +4 -4
- package/docs/onboarding.md +1 -2
- package/docs/pr-comment-followup.md +3 -3
- package/docs/pr-review-fix-loop.md +1 -1
- package/docs/proposals/repo-pool-for-live-checkout.md +1 -2
- package/docs/qa-runbook-lifecycle.md +4 -4
- package/docs/qa-runbooks.md +2 -2
- package/docs/rfc-completion-json.md +4 -1
- package/docs/runtime-adapters.md +1 -1
- package/docs/self-improvement.md +4 -5
- package/docs/shared-lifecycle-module-map.md +3 -1
- package/docs/slim-ux/architecture-suggestions.md +5 -6
- package/docs/slim-ux/concepts.md +23 -25
- package/docs/watches.md +7 -7
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +1 -1
- package/engine/abandoned-pr-reconciliation.js +4 -5
- package/engine/acp-transport.js +49 -8
- package/engine/ado-status.js +5 -5
- package/engine/ado.js +20 -25
- package/engine/agent-worker-pool.js +124 -15
- package/engine/bridge.js +260 -5
- package/engine/cleanup.js +48 -131
- package/engine/cli.js +125 -83
- package/engine/cooldown.js +9 -16
- package/engine/db/index.js +22 -9
- package/engine/db/migrations/009-qa.js +1 -1
- package/engine/db/migrations/020-qa-session-scopes.js +23 -0
- package/engine/db/migrations/021-archived-work-items.js +72 -0
- package/engine/db/migrations/022-global-cc-session.js +31 -0
- package/engine/db/migrations/023-engine-state.js +30 -0
- package/engine/db/migrations/024-prd-ghost-collisions.js +53 -0
- package/engine/db/migrations/025-malformed-work-item-phantoms.js +33 -0
- package/engine/dispatch-store.js +2 -7
- package/engine/dispatch.js +34 -41
- package/engine/github.js +20 -27
- package/engine/lifecycle.js +221 -283
- package/engine/llm.js +1 -1
- package/engine/logs-store.js +2 -2
- package/engine/managed-spawn.js +2 -23
- package/engine/meeting.js +6 -6
- package/engine/metrics-store.js +2 -2
- package/engine/note-link-backfill.js +6 -11
- package/engine/pipeline.js +18 -36
- package/engine/playbook.js +1 -1
- package/engine/pooled-agent-process.js +46 -26
- package/engine/prd-store.js +73 -54
- package/engine/preflight.js +2 -5
- package/engine/projects.js +15 -62
- package/engine/pull-requests-store.js +0 -17
- package/engine/qa-runbooks.js +2 -2
- package/engine/qa-runs.js +1 -8
- package/engine/qa-sessions.js +41 -64
- package/engine/queries.js +120 -219
- package/engine/routing.js +3 -5
- package/engine/scheduler.js +0 -4
- package/engine/shared-branch-pr-reconcile.js +2 -3
- package/engine/shared.js +132 -637
- package/engine/small-state-store.js +89 -10
- package/engine/state-operations.js +16 -4
- package/engine/stdio-timestamps.js +1 -1
- package/engine/timeout.js +5 -12
- package/engine/watch-actions.js +20 -22
- package/engine/watches-store.js +1 -1
- package/engine/watches.js +6 -10
- package/engine/work-item-validation.js +52 -0
- package/engine/work-items-store.js +127 -29
- package/engine/worktree-gc.js +2 -2
- package/engine/worktree-pool.js +8 -18
- package/engine.js +167 -349
- package/minions.js +2 -2
- package/package.json +1 -1
- package/playbooks/plan-to-prd.md +2 -2
- package/playbooks/shared-rules.md +3 -3
- package/playbooks/templates/followup-dispatch.md +1 -1
- package/playbooks/verify.md +1 -1
- package/prompts/cc-system.md +2 -2
package/engine/lifecycle.js
CHANGED
|
@@ -7,29 +7,26 @@ const fs = require('fs');
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const os = require('os');
|
|
9
9
|
const shared = require('./shared');
|
|
10
|
-
const { safeRead, safeJson, safeJsonArr, safeJsonNoRestore, safeWrite, mutateJsonFileLocked, mutateWorkItems, execAsync,
|
|
10
|
+
const { safeRead, safeJson, safeJsonArr, safeJsonNoRestore, safeWrite, mutateJsonFileLocked, mutateWorkItems, execAsync, getPrLinks,
|
|
11
11
|
log, ts, dateStamp, WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, REVIEW_STATUS, RETRY_DELAY_MS, DISPATCH_RESULT,
|
|
12
12
|
ENGINE_DEFAULTS, DEFAULT_AGENT_METRICS, FAILURE_CLASS } = shared;
|
|
13
13
|
const { resolveRuntime } = require('./runtimes');
|
|
14
14
|
const adoGitAuth = require('./ado-git-auth');
|
|
15
15
|
const queries = require('./queries');
|
|
16
16
|
const harness = require('./harness');
|
|
17
|
+
const smallStateStore = require('./small-state-store');
|
|
18
|
+
const prdStore = require('./prd-store');
|
|
17
19
|
const { isBranchActive } = require('./cooldown');
|
|
18
20
|
const { worktreeMatchesBranch, getWorktreeBranch, cleanupMergedPrLocalBranch } = require('./cleanup');
|
|
19
21
|
const { getConfig, getInboxFiles, getNotes, getPrs, getDispatch,
|
|
20
|
-
MINIONS_DIR, ENGINE_DIR, PLANS_DIR,
|
|
22
|
+
MINIONS_DIR, ENGINE_DIR, PLANS_DIR, INBOX_DIR, AGENTS_DIR } = queries;
|
|
21
23
|
|
|
22
24
|
// ─── Plan Completion Detection ───────────────────────────────────────────────
|
|
23
25
|
function checkPlanCompletion(meta, config) {
|
|
24
26
|
|
|
25
27
|
const planFile = meta.item?.sourcePlan;
|
|
26
28
|
if (!planFile) return;
|
|
27
|
-
const
|
|
28
|
-
// Use safeJsonNoRestore — if the active PRD has been archived, the .backup
|
|
29
|
-
// sidecar may still be sitting in prd/. safeJson would auto-restore it and
|
|
30
|
-
// resurrect the archived PRD as active (W-mouptdh1000h9f39). PRDs are
|
|
31
|
-
// terminal artifacts; missing primary means "gone", not "needs recovery".
|
|
32
|
-
const plan = safeJsonNoRestore(planPath);
|
|
29
|
+
const plan = prdStore.readPrd(planFile);
|
|
33
30
|
if (!plan?.missing_features) return;
|
|
34
31
|
|
|
35
32
|
const projects = shared.getProjects(config);
|
|
@@ -54,7 +51,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
54
51
|
const reopenStaleCompletedPlan = (reason) => {
|
|
55
52
|
if (plan.status !== PLAN_STATUS.COMPLETED && !plan._completionNotified) return false;
|
|
56
53
|
log('warn', `Plan ${planFile}: re-opening stale completed PRD — ${reason}`);
|
|
57
|
-
|
|
54
|
+
prdStore.mutatePrd(planFile, (data) => {
|
|
58
55
|
if (data.status === PLAN_STATUS.COMPLETED) data.status = PLAN_STATUS.ACTIVE;
|
|
59
56
|
delete data.completedAt;
|
|
60
57
|
data._completionNotified = false;
|
|
@@ -180,8 +177,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
180
177
|
const prsCreated = [];
|
|
181
178
|
for (const p of projects) {
|
|
182
179
|
try {
|
|
183
|
-
const
|
|
184
|
-
const prs = safeJsonArr(prPath);
|
|
180
|
+
const prs = shared.readPullRequests(p);
|
|
185
181
|
const prLinks = getPrLinks();
|
|
186
182
|
for (const pr of prs) {
|
|
187
183
|
const linkedItemIds = prLinks[pr.id] || [];
|
|
@@ -223,8 +219,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
223
219
|
shared.writeToInbox('engine', summarySlug, summary);
|
|
224
220
|
log('info', `PRD completion summary written to notes/inbox/${summarySlug}`);
|
|
225
221
|
|
|
226
|
-
|
|
227
|
-
mutateJsonFileLocked(planPath, (data) => {
|
|
222
|
+
prdStore.mutatePrd(planFile, (data) => {
|
|
228
223
|
data.status = PLAN_STATUS.COMPLETED;
|
|
229
224
|
data.completedAt = plan.completedAt;
|
|
230
225
|
data._completionNotified = true;
|
|
@@ -258,17 +253,15 @@ function checkPlanCompletion(meta, config) {
|
|
|
258
253
|
log('warn', `Plan ${planFile}: no primary project found — skipping PR/verify creation`);
|
|
259
254
|
return;
|
|
260
255
|
}
|
|
261
|
-
const wiPath = shared.projectWorkItemsPath(primaryProject);
|
|
262
|
-
|
|
263
256
|
// 3. For shared-branch plans, create PR work item
|
|
264
|
-
if (plan.branch_strategy === 'shared-branch' && plan.feature_branch
|
|
257
|
+
if (plan.branch_strategy === 'shared-branch' && plan.feature_branch) {
|
|
265
258
|
const existingPrItem = allWorkItems.find(w => w.sourcePlan === planFile && w.itemType === 'pr');
|
|
266
259
|
if (!existingPrItem) {
|
|
267
260
|
const id = 'PL-' + shared.uid();
|
|
268
261
|
const featureBranch = plan.feature_branch;
|
|
269
262
|
const mainBranch = shared.resolveMainBranch(primaryProject.localPath, primaryProject.mainBranch);
|
|
270
263
|
const itemSummary = doneItems.map(w => '- ' + w.id + ': ' + (w.title || 'Untitled').replace('Implement: ', '')).join('\n');
|
|
271
|
-
mutateWorkItems(
|
|
264
|
+
mutateWorkItems(primaryProject, workItems => {
|
|
272
265
|
if (workItems.some(w => w.sourcePlan === planFile && w.itemType === 'pr')) return workItems;
|
|
273
266
|
workItems.push({
|
|
274
267
|
id, title: `Create PR for plan: ${plan.plan_summary || planFile}`,
|
|
@@ -295,7 +288,7 @@ function checkPlanCompletion(meta, config) {
|
|
|
295
288
|
const projectPrs = {};
|
|
296
289
|
const prLinks = getPrLinks();
|
|
297
290
|
for (const p of projects) {
|
|
298
|
-
const prs =
|
|
291
|
+
const prs = shared.readPullRequests(p)
|
|
299
292
|
.filter(pr => {
|
|
300
293
|
const linkedIds = prLinks[pr.id] || [];
|
|
301
294
|
return pr.status === PR_STATUS.ACTIVE && linkedIds.some(itemId => doneItems.find(w => w.id === itemId));
|
|
@@ -338,9 +331,8 @@ function checkPlanCompletion(meta, config) {
|
|
|
338
331
|
}
|
|
339
332
|
const verifyProject = existingVerify.project || projName;
|
|
340
333
|
const vProject = shared.resolveProjectSource(verifyProject, projects, { allowCentral: false }).project || p;
|
|
341
|
-
const vWiPath = shared.projectWorkItemsPath(vProject);
|
|
342
334
|
let reopened = false;
|
|
343
|
-
mutateWorkItems(
|
|
335
|
+
mutateWorkItems(vProject, items => {
|
|
344
336
|
const v = items.find(w => w.id === existingVerify.id);
|
|
345
337
|
if (isReopenableVerify(v)) {
|
|
346
338
|
shared.reopenWorkItem(v);
|
|
@@ -476,9 +468,8 @@ function checkPlanCompletion(meta, config) {
|
|
|
476
468
|
].join('\n');
|
|
477
469
|
|
|
478
470
|
const verifyId = 'PL-' + shared.uid();
|
|
479
|
-
const vWiPath = shared.projectWorkItemsPath(p);
|
|
480
471
|
let created = false;
|
|
481
|
-
mutateWorkItems(
|
|
472
|
+
mutateWorkItems(p, workItems => {
|
|
482
473
|
// Re-check under lock to prevent races
|
|
483
474
|
if (workItems.some(w => w.sourcePlan === planFile && w.itemType === WORK_TYPE.VERIFY && (w.project === projName || (!w.project && projName === primaryProject.name)))) {
|
|
484
475
|
return workItems;
|
|
@@ -517,18 +508,16 @@ function checkPlanCompletion(meta, config) {
|
|
|
517
508
|
|
|
518
509
|
// ─── Archive Plan ───────────────────────────────────────────────────────────
|
|
519
510
|
function archivePlan(planFile, plan, projects, config) {
|
|
520
|
-
const planPath = path.join(PRD_DIR, planFile);
|
|
521
|
-
|
|
522
511
|
// Archive in place by flagging the authoritative SQL PRD.
|
|
523
512
|
try {
|
|
524
|
-
if (
|
|
525
|
-
|
|
513
|
+
if (prdStore.readPrd(planFile)) {
|
|
514
|
+
prdStore.mutatePrd(planFile, (data) => {
|
|
526
515
|
if (!data || Array.isArray(data) || typeof data !== 'object') data = {};
|
|
527
516
|
data.status = 'archived';
|
|
528
517
|
data.archived = true;
|
|
529
518
|
data.archivedAt = new Date().toISOString();
|
|
530
519
|
return data;
|
|
531
|
-
}
|
|
520
|
+
});
|
|
532
521
|
log('info', `Archived PRD in place: ${planFile} (archived flag set)`);
|
|
533
522
|
}
|
|
534
523
|
} catch (err) {
|
|
@@ -585,7 +574,7 @@ function cleanupPlanWorktrees(planFile, plan, projects, config) {
|
|
|
585
574
|
|
|
586
575
|
for (const p of projects) {
|
|
587
576
|
try {
|
|
588
|
-
const prs =
|
|
577
|
+
const prs = shared.readPullRequests(p);
|
|
589
578
|
const prLinks = getPrLinks();
|
|
590
579
|
for (const pr of prs) {
|
|
591
580
|
const linkedIds = prLinks[pr.id] || [];
|
|
@@ -619,34 +608,29 @@ function cleanupPlanWorktrees(planFile, plan, projects, config) {
|
|
|
619
608
|
} catch (err) { log('warn', `Plan worktree cleanup: ${err.message}`); }
|
|
620
609
|
}
|
|
621
610
|
|
|
622
|
-
// ─── Work Item
|
|
611
|
+
// ─── Work Item Scope Resolution ──────────────────────────────────────────────
|
|
623
612
|
|
|
624
|
-
|
|
625
|
-
function resolveWorkItemPath(meta) {
|
|
613
|
+
function resolveWorkItemScope(meta) {
|
|
626
614
|
if (meta.source === 'central-work-item' || meta.source === 'central-work-item-fanout') {
|
|
627
|
-
return
|
|
615
|
+
return 'central';
|
|
628
616
|
}
|
|
629
617
|
if (meta.source === 'work-item' && meta.project?.name) {
|
|
630
|
-
return
|
|
618
|
+
return meta.project.name;
|
|
631
619
|
}
|
|
632
620
|
return null;
|
|
633
621
|
}
|
|
634
622
|
|
|
635
|
-
function
|
|
636
|
-
|
|
637
|
-
for (const p of shared.getProjects(config || {})) {
|
|
638
|
-
if (p && p.name) paths.push(shared.projectWorkItemsPath(p));
|
|
639
|
-
}
|
|
640
|
-
return paths;
|
|
623
|
+
function getWorkItemScopes(config) {
|
|
624
|
+
return ['central', ...shared.getProjects(config || {})];
|
|
641
625
|
}
|
|
642
626
|
|
|
643
627
|
function findWorkItemRecord(itemId, config) {
|
|
644
628
|
if (!itemId) return null;
|
|
645
|
-
for (const
|
|
629
|
+
for (const scope of getWorkItemScopes(config)) {
|
|
646
630
|
try {
|
|
647
|
-
const items =
|
|
631
|
+
const items = shared.readWorkItems(scope);
|
|
648
632
|
const item = items.find(i => i && i.id === itemId);
|
|
649
|
-
if (item) return { item,
|
|
633
|
+
if (item) return { item, scope };
|
|
650
634
|
} catch { /* best-effort lineage lookup */ }
|
|
651
635
|
}
|
|
652
636
|
return null;
|
|
@@ -707,11 +691,11 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
707
691
|
return;
|
|
708
692
|
}
|
|
709
693
|
|
|
710
|
-
const
|
|
711
|
-
if (!
|
|
694
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
695
|
+
if (!wiScope) return;
|
|
712
696
|
|
|
713
697
|
let completionGuarded = false;
|
|
714
|
-
|
|
698
|
+
mutateWorkItems(wiScope, (items) => {
|
|
715
699
|
if (!items || !Array.isArray(items)) return items;
|
|
716
700
|
const target = items.find(i => i.id === itemId);
|
|
717
701
|
if (!target) return items;
|
|
@@ -783,7 +767,7 @@ function updateWorkItemStatus(meta, status, reason) {
|
|
|
783
767
|
}
|
|
784
768
|
}
|
|
785
769
|
return items;
|
|
786
|
-
}
|
|
770
|
+
});
|
|
787
771
|
|
|
788
772
|
if (completionGuarded) {
|
|
789
773
|
log('info', `Work item ${itemId} already completed — ignoring ${status} status update`);
|
|
@@ -803,15 +787,15 @@ function storeAffectedFilesFromCompletion(meta, structuredCompletion) {
|
|
|
803
787
|
if (files.length === 0) return;
|
|
804
788
|
const itemId = meta?.item?.id;
|
|
805
789
|
if (!itemId) return;
|
|
806
|
-
const
|
|
807
|
-
if (!
|
|
808
|
-
|
|
790
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
791
|
+
if (!wiScope) return;
|
|
792
|
+
mutateWorkItems(wiScope, (items) => {
|
|
809
793
|
if (!Array.isArray(items)) return items;
|
|
810
794
|
const target = items.find(i => i.id === itemId);
|
|
811
795
|
if (!target) return items;
|
|
812
796
|
target.affected_files = files;
|
|
813
797
|
return items;
|
|
814
|
-
}
|
|
798
|
+
});
|
|
815
799
|
}
|
|
816
800
|
|
|
817
801
|
const _VALID_PRD_STATUSES = new Set([...Object.values(WI_STATUS), 'missing']);
|
|
@@ -832,16 +816,11 @@ function syncPrdItemStatus(itemId, status, sourcePlan) {
|
|
|
832
816
|
if (!itemId) return;
|
|
833
817
|
if (!_VALID_PRD_STATUSES.has(status)) return;
|
|
834
818
|
try {
|
|
835
|
-
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
836
819
|
const files = sourcePlan
|
|
837
820
|
? [sourcePlan]
|
|
838
|
-
:
|
|
821
|
+
: prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename);
|
|
839
822
|
for (const pf of files) {
|
|
840
|
-
const
|
|
841
|
-
// Lock-free peek: most PRDs won't contain the ID, so skip the lock cost.
|
|
842
|
-
// safeJsonNoRestore so an archived PRD's .backup sidecar can't resurrect
|
|
843
|
-
// the active PRD just because a done WI still references it.
|
|
844
|
-
const plan = safeJsonNoRestore(fpath);
|
|
823
|
+
const plan = prdStore.readPrd(pf);
|
|
845
824
|
// Frozen PRDs (paused/rejected/awaiting-approval/completed) must not have
|
|
846
825
|
// their item statuses mutated by a late-arriving WI status update — that
|
|
847
826
|
// would silently flip a paused/rejected PRD's items and feed the
|
|
@@ -850,7 +829,7 @@ function syncPrdItemStatus(itemId, status, sourcePlan) {
|
|
|
850
829
|
const feature = plan?.missing_features?.find(f => f.id === itemId);
|
|
851
830
|
if (!feature || feature.status === status) continue;
|
|
852
831
|
let updated = false;
|
|
853
|
-
|
|
832
|
+
prdStore.mutatePrd(pf, (fresh) => {
|
|
854
833
|
// Re-check under the lock — status may have frozen between peek and lock.
|
|
855
834
|
if (_PRD_FROZEN_STATUSES.has(fresh?.status)) return fresh;
|
|
856
835
|
const f = fresh?.missing_features?.find(x => x.id === itemId);
|
|
@@ -859,7 +838,7 @@ function syncPrdItemStatus(itemId, status, sourcePlan) {
|
|
|
859
838
|
updated = true;
|
|
860
839
|
}
|
|
861
840
|
return fresh;
|
|
862
|
-
}
|
|
841
|
+
});
|
|
863
842
|
if (updated) return;
|
|
864
843
|
}
|
|
865
844
|
} catch (err) { log('warn', `PRD status sync: ${err.message}`); }
|
|
@@ -872,30 +851,28 @@ function syncPrdItemStatus(itemId, status, sourcePlan) {
|
|
|
872
851
|
function stampPrdItemWorkItemId(itemId, sourcePlan) {
|
|
873
852
|
if (!itemId || !sourcePlan) return;
|
|
874
853
|
try {
|
|
875
|
-
const
|
|
876
|
-
const plan = safeJsonNoRestore(fpath);
|
|
854
|
+
const plan = prdStore.readPrd(sourcePlan);
|
|
877
855
|
const feature = plan?.missing_features?.find(f => f.id === itemId);
|
|
878
856
|
if (!feature || feature.workItemId === itemId) return;
|
|
879
|
-
|
|
857
|
+
prdStore.mutatePrd(sourcePlan, (fresh) => {
|
|
880
858
|
const f = fresh?.missing_features?.find(x => x.id === itemId);
|
|
881
859
|
if (f && f.workItemId !== itemId) f.workItemId = itemId;
|
|
882
860
|
return fresh;
|
|
883
|
-
}
|
|
861
|
+
});
|
|
884
862
|
} catch (err) { log('warn', `stampPrdItemWorkItemId: ${err.message}`); }
|
|
885
863
|
}
|
|
886
864
|
|
|
887
865
|
function stampPrdItemPrUrl(itemId, sourcePlan, prUrl) {
|
|
888
866
|
if (!itemId || !sourcePlan || !prUrl) return;
|
|
889
867
|
try {
|
|
890
|
-
const
|
|
891
|
-
const plan = safeJsonNoRestore(fpath);
|
|
868
|
+
const plan = prdStore.readPrd(sourcePlan);
|
|
892
869
|
const feature = plan?.missing_features?.find(f => f.id === itemId);
|
|
893
870
|
if (!feature || feature.pr_url === prUrl) return;
|
|
894
|
-
|
|
871
|
+
prdStore.mutatePrd(sourcePlan, (fresh) => {
|
|
895
872
|
const f = fresh?.missing_features?.find(x => x.id === itemId);
|
|
896
873
|
if (f && f.pr_url !== prUrl) f.pr_url = prUrl;
|
|
897
874
|
return fresh;
|
|
898
|
-
}
|
|
875
|
+
});
|
|
899
876
|
} catch (err) { log('warn', `stampPrdItemPrUrl: ${err?.message || err}`); }
|
|
900
877
|
}
|
|
901
878
|
|
|
@@ -907,7 +884,7 @@ function stampPrdItemPrUrl(itemId, sourcePlan, prUrl) {
|
|
|
907
884
|
// different ID than the original PRD feature, leaving the PRD status stale.
|
|
908
885
|
|
|
909
886
|
function reconcilePrdStatuses(config) {
|
|
910
|
-
const prdFiles =
|
|
887
|
+
const prdFiles = prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename);
|
|
911
888
|
if (prdFiles.length === 0) return;
|
|
912
889
|
|
|
913
890
|
const allWorkItems = queries.getWorkItems(config);
|
|
@@ -922,9 +899,8 @@ function reconcilePrdStatuses(config) {
|
|
|
922
899
|
|
|
923
900
|
for (const file of prdFiles) {
|
|
924
901
|
try {
|
|
925
|
-
const fpath = path.join(PRD_DIR, file);
|
|
926
902
|
const logMessages = [];
|
|
927
|
-
|
|
903
|
+
prdStore.mutatePrd(file, (plan) => {
|
|
928
904
|
if (!plan?.missing_features) return plan;
|
|
929
905
|
// Skip frozen PRDs — paused/rejected/awaiting-approval/completed require no reconciliation
|
|
930
906
|
if (_PRD_FROZEN_STATUSES.has(plan.status)) return plan;
|
|
@@ -943,7 +919,7 @@ function reconcilePrdStatuses(config) {
|
|
|
943
919
|
}
|
|
944
920
|
}
|
|
945
921
|
return plan;
|
|
946
|
-
}
|
|
922
|
+
});
|
|
947
923
|
for (const message of logMessages) log('info', message);
|
|
948
924
|
} catch (err) { log('warn', `PRD backward-scan for ${file}: ${err.message}`); }
|
|
949
925
|
}
|
|
@@ -1141,15 +1117,14 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
1141
1117
|
|
|
1142
1118
|
const agentName = config.agents?.[agentId]?.name || agentId;
|
|
1143
1119
|
let added = 0;
|
|
1144
|
-
const centralPrPath = path.join(MINIONS_DIR, 'pull-requests.json');
|
|
1145
1120
|
|
|
1146
|
-
|
|
1147
|
-
const newPrsByPath = new Map(); // prPath -> [{ prId, newEntry }]
|
|
1121
|
+
const newPrsByScope = new Map();
|
|
1148
1122
|
|
|
1149
1123
|
for (const prId of prEvidence.keys()) {
|
|
1150
1124
|
const targetProject = useCentral ? null : resolveProjectForPr(prId);
|
|
1151
1125
|
const targetName = targetProject ? targetProject.name : '_central';
|
|
1152
|
-
const
|
|
1126
|
+
const source = targetProject || 'central';
|
|
1127
|
+
const scope = shared.stateScope(source);
|
|
1153
1128
|
const prUrl = extractPrUrl(prId);
|
|
1154
1129
|
const fullId = shared.getCanonicalPrId(targetProject, prId, prUrl);
|
|
1155
1130
|
|
|
@@ -1160,7 +1135,7 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
1160
1135
|
title = meta?.item?.title || '';
|
|
1161
1136
|
}
|
|
1162
1137
|
|
|
1163
|
-
if (!
|
|
1138
|
+
if (!newPrsByScope.has(scope)) newPrsByScope.set(scope, { source, name: targetName, project: targetProject, entries: [] });
|
|
1164
1139
|
const entry = {
|
|
1165
1140
|
id: fullId,
|
|
1166
1141
|
prNumber: parseInt(prId, 10) || prId,
|
|
@@ -1186,15 +1161,15 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
1186
1161
|
// "review this PR" via CC) must stay reference-only. QA-session setup and
|
|
1187
1162
|
// other oneShot+skipPr PR-targeted work are operational context, not an
|
|
1188
1163
|
// observe toggle, so they must not demote a real tracked PR.
|
|
1189
|
-
|
|
1164
|
+
newPrsByScope.get(scope).entries.push({ prId, fullId, entry });
|
|
1190
1165
|
}
|
|
1191
1166
|
|
|
1192
1167
|
const entryBranch = meta?.branch || '';
|
|
1193
1168
|
|
|
1194
|
-
for (const
|
|
1169
|
+
for (const { source, name, project: targetProject, entries } of newPrsByScope.values()) {
|
|
1195
1170
|
for (const { prId, fullId, entry } of entries) {
|
|
1196
1171
|
let duplicateOnBranch = null;
|
|
1197
|
-
const result = shared.upsertPullRequestRecord(
|
|
1172
|
+
const result = shared.upsertPullRequestRecord(source, entry, {
|
|
1198
1173
|
project: targetProject,
|
|
1199
1174
|
itemId: meta?.item?.id || null,
|
|
1200
1175
|
beforeInsert: (prs, normalizedEntry) => {
|
|
@@ -1244,7 +1219,7 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
1244
1219
|
}
|
|
1245
1220
|
if (result.created || result.linked) added++;
|
|
1246
1221
|
}
|
|
1247
|
-
log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name}
|
|
1222
|
+
log('info', `Synced PR(s) from ${agentName}'s output to ${name === '_central' ? 'central' : name} state`);
|
|
1248
1223
|
}
|
|
1249
1224
|
return added;
|
|
1250
1225
|
}
|
|
@@ -1261,7 +1236,7 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
1261
1236
|
//
|
|
1262
1237
|
// `enrollPrFromCanonicalId` is the write-side fix: given a canonical PR id and
|
|
1263
1238
|
// the owning project, fetch live state from GitHub (or ADO, best-effort) and
|
|
1264
|
-
// insert a `contextOnly: true` row into the project's
|
|
1239
|
+
// insert a `contextOnly: true` row into the project's PR state.
|
|
1265
1240
|
// Idempotent — no-op if a record already exists. Always marks the record as
|
|
1266
1241
|
// context-only so the engine doesn't try to re-dispatch fix/review loops on
|
|
1267
1242
|
// an already-merged PR (matches the #1772 enrollment-flag semantics).
|
|
@@ -1367,21 +1342,18 @@ async function enrollPrFromCanonicalId(canonicalPrId, project, opts = {}) {
|
|
|
1367
1342
|
// pr-links stays consistent (#779 pattern).
|
|
1368
1343
|
if (opts.itemId && Array.isArray(existing.prdItems) && !existing.prdItems.includes(opts.itemId)) {
|
|
1369
1344
|
try {
|
|
1370
|
-
// W-mrduef3q000m2172 —
|
|
1345
|
+
// W-mrduef3q000m2172 — use the scope the store
|
|
1371
1346
|
// actually returned `existing` under (existing._scope), NOT from the
|
|
1372
1347
|
// current call's `project` argument. When the two differ (e.g. the
|
|
1373
1348
|
// record lives under 'central' but this enrollment call was made
|
|
1374
1349
|
// with a specific project), re-deriving from `project` sends the
|
|
1375
|
-
// upsert to the
|
|
1350
|
+
// upsert to the wrong scope. upsertPullRequestRecord's
|
|
1376
1351
|
// tombstone guard then finds no matching record in that (wrong)
|
|
1377
1352
|
// scope and falls through to insert-as-new, silently clearing any
|
|
1378
1353
|
// userDeleted tombstone on the real record. See issue writeup for
|
|
1379
1354
|
// the confirmed repro.
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
? store._filePathForScope(existing._scope)
|
|
1383
|
-
: (project ? shared.projectPrPath(project) : path.join(MINIONS_DIR, 'pull-requests.json'));
|
|
1384
|
-
shared.upsertPullRequestRecord(targetPath, {
|
|
1355
|
+
const targetScope = existing._scope || project || 'central';
|
|
1356
|
+
shared.upsertPullRequestRecord(targetScope, {
|
|
1385
1357
|
id: normalizedCanonicalId,
|
|
1386
1358
|
prNumber,
|
|
1387
1359
|
url: existing.url || '',
|
|
@@ -1439,8 +1411,7 @@ async function enrollPrFromCanonicalId(canonicalPrId, project, opts = {}) {
|
|
|
1439
1411
|
return { enrolled: false, reason: 'unsupported_scope' };
|
|
1440
1412
|
}
|
|
1441
1413
|
|
|
1442
|
-
const
|
|
1443
|
-
if (!prPath) return { enrolled: false, reason: 'no_pr_path' };
|
|
1414
|
+
const source = project || 'central';
|
|
1444
1415
|
|
|
1445
1416
|
const entry = {
|
|
1446
1417
|
id: normalizedCanonicalId,
|
|
@@ -1462,12 +1433,12 @@ async function enrollPrFromCanonicalId(canonicalPrId, project, opts = {}) {
|
|
|
1462
1433
|
if (liveState && liveState.closedAt && !liveState.mergedAt) entry.closedAt = liveState.closedAt;
|
|
1463
1434
|
|
|
1464
1435
|
try {
|
|
1465
|
-
const result = shared.upsertPullRequestRecord(
|
|
1436
|
+
const result = shared.upsertPullRequestRecord(source, entry, {
|
|
1466
1437
|
project,
|
|
1467
1438
|
itemId: opts.itemId || null,
|
|
1468
1439
|
});
|
|
1469
1440
|
if (result.created) {
|
|
1470
|
-
log('info', `enrollPrFromCanonicalId: enrolled ${normalizedCanonicalId} (status=${status}) into ${project?.name || 'central'}
|
|
1441
|
+
log('info', `enrollPrFromCanonicalId: enrolled ${normalizedCanonicalId} (status=${status}) into ${project?.name || 'central'} state`);
|
|
1471
1442
|
}
|
|
1472
1443
|
return {
|
|
1473
1444
|
enrolled: !!(result.created || result.linked),
|
|
@@ -1548,11 +1519,10 @@ function stampWiPrRef(meta, config) {
|
|
|
1548
1519
|
const prUrl = found.url;
|
|
1549
1520
|
const prNumber = found.prNumber ?? found.number ?? null;
|
|
1550
1521
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
if (wiPath) {
|
|
1522
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
1523
|
+
if (wiScope) {
|
|
1554
1524
|
try {
|
|
1555
|
-
mutateWorkItems(
|
|
1525
|
+
mutateWorkItems(wiScope, items => {
|
|
1556
1526
|
const wi = items.find(i => i.id === itemId);
|
|
1557
1527
|
if (wi && !wi._pr) {
|
|
1558
1528
|
if (prId) wi._pr = prId;
|
|
@@ -1622,10 +1592,10 @@ function hasCanonicalPrAttachment(itemId, config) {
|
|
|
1622
1592
|
if (Object.values(getPrLinks()).some(linkedIds => (linkedIds || []).includes(itemId))) return true;
|
|
1623
1593
|
const projects = shared.getProjects(config);
|
|
1624
1594
|
for (const p of projects) {
|
|
1625
|
-
const prs =
|
|
1595
|
+
const prs = shared.readPullRequests(p);
|
|
1626
1596
|
if (prs.some(pr => (pr.prdItems || []).includes(itemId))) return true;
|
|
1627
1597
|
}
|
|
1628
|
-
const centralPrs =
|
|
1598
|
+
const centralPrs = shared.readPullRequests('central');
|
|
1629
1599
|
return centralPrs.some(pr => (pr.prdItems || []).includes(itemId));
|
|
1630
1600
|
}
|
|
1631
1601
|
|
|
@@ -1826,7 +1796,7 @@ function _attachFoundPrToWi(found, meta, agentId, resultSummary, config) {
|
|
|
1826
1796
|
itemType: meta.item?.itemType || '',
|
|
1827
1797
|
};
|
|
1828
1798
|
applyScheduleContextToPrEntry(entry, meta?.item, config);
|
|
1829
|
-
shared.upsertPullRequestRecord(
|
|
1799
|
+
shared.upsertPullRequestRecord(found.project, entry, {
|
|
1830
1800
|
project: found.project,
|
|
1831
1801
|
itemId: meta.item.id,
|
|
1832
1802
|
});
|
|
@@ -1894,7 +1864,7 @@ function _outputHasRuntimeResultEvent(output) {
|
|
|
1894
1864
|
}
|
|
1895
1865
|
|
|
1896
1866
|
function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity, opts) {
|
|
1897
|
-
const
|
|
1867
|
+
const noPrWiScope = resolveWorkItemScope(meta);
|
|
1898
1868
|
const isHard = severity !== 'soft';
|
|
1899
1869
|
const isPhantom = !!(opts && opts.phantom);
|
|
1900
1870
|
let syncFailedToPrd = false;
|
|
@@ -1907,8 +1877,8 @@ function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity,
|
|
|
1907
1877
|
let phantomRetryExhausted = false;
|
|
1908
1878
|
let phantomRetryCount = 0;
|
|
1909
1879
|
let systematicPhantomFired = false;
|
|
1910
|
-
if (isHard && isPhantom &&
|
|
1911
|
-
|
|
1880
|
+
if (isHard && isPhantom && noPrWiScope) {
|
|
1881
|
+
mutateWorkItems(noPrWiScope, data => {
|
|
1912
1882
|
if (!Array.isArray(data)) return data;
|
|
1913
1883
|
const w = data.find(i => i.id === meta.item.id);
|
|
1914
1884
|
if (!w) return data;
|
|
@@ -1974,7 +1944,7 @@ function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity,
|
|
|
1974
1944
|
phantomRetryCount = phantomRetries;
|
|
1975
1945
|
}
|
|
1976
1946
|
return data;
|
|
1977
|
-
}
|
|
1947
|
+
});
|
|
1978
1948
|
if (phantomRetryDeferred) {
|
|
1979
1949
|
// Soft inbox note: the runtime crashed but we're retrying; surface the
|
|
1980
1950
|
// event without flagging the WI as silent failure.
|
|
@@ -2009,8 +1979,8 @@ function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity,
|
|
|
2009
1979
|
reason = `${reason} — phantom retries exhausted (${phantomRetryCount}/${ENGINE_DEFAULTS.maxPhantomRetries})`;
|
|
2010
1980
|
}
|
|
2011
1981
|
}
|
|
2012
|
-
if (
|
|
2013
|
-
|
|
1982
|
+
if (noPrWiScope) {
|
|
1983
|
+
mutateWorkItems(noPrWiScope, data => {
|
|
2014
1984
|
if (!Array.isArray(data)) return data;
|
|
2015
1985
|
const w = data.find(i => i.id === meta.item.id);
|
|
2016
1986
|
if (!w) return data;
|
|
@@ -2037,7 +2007,7 @@ function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity,
|
|
|
2037
2007
|
w._lastReviewReason = reason;
|
|
2038
2008
|
}
|
|
2039
2009
|
return data;
|
|
2040
|
-
}
|
|
2010
|
+
});
|
|
2041
2011
|
}
|
|
2042
2012
|
if (isHard && syncFailedToPrd) {
|
|
2043
2013
|
syncPrdItemStatus(meta.item.id, WI_STATUS.FAILED, meta.item.sourcePlan);
|
|
@@ -2071,10 +2041,10 @@ function markMissingPrAttachment(meta, agentId, reason, resultSummary, severity,
|
|
|
2071
2041
|
}
|
|
2072
2042
|
|
|
2073
2043
|
function markPrAttachmentVerificationError(meta, agentId, reason, resultSummary) {
|
|
2074
|
-
const
|
|
2044
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
2075
2045
|
let syncFailedToPrd = false;
|
|
2076
|
-
if (
|
|
2077
|
-
mutateWorkItems(
|
|
2046
|
+
if (wiScope) {
|
|
2047
|
+
mutateWorkItems(wiScope, data => {
|
|
2078
2048
|
if (!Array.isArray(data)) return data;
|
|
2079
2049
|
const w = data.find(i => i.id === meta.item.id);
|
|
2080
2050
|
if (!w) return data;
|
|
@@ -2088,7 +2058,7 @@ function markPrAttachmentVerificationError(meta, agentId, reason, resultSummary)
|
|
|
2088
2058
|
delete w._missingPrAttachment;
|
|
2089
2059
|
delete w._unverifiedPrAttachment;
|
|
2090
2060
|
return data;
|
|
2091
|
-
}
|
|
2061
|
+
});
|
|
2092
2062
|
}
|
|
2093
2063
|
if (syncFailedToPrd) {
|
|
2094
2064
|
syncPrdItemStatus(meta.item.id, WI_STATUS.FAILED, meta.item.sourcePlan);
|
|
@@ -2169,7 +2139,7 @@ async function enforcePrAttachmentContract(type, meta, agentId, config, resultSu
|
|
|
2169
2139
|
|
|
2170
2140
|
// W-mqsk1ip00006cbae — After a verify WI transitions to done, assert that a PR
|
|
2171
2141
|
// exists: either a direct reference on the item (_pr/_prUrl) or a matching
|
|
2172
|
-
//
|
|
2142
|
+
// tracked PR entry for the plan (by sourcePlan). If no PR is found,
|
|
2173
2143
|
// flip the WI to failed with failure_class: verify-missing-pr and write an
|
|
2174
2144
|
// inbox alert so operators can investigate.
|
|
2175
2145
|
//
|
|
@@ -2199,10 +2169,10 @@ function enforceVerifyPrContract(type, meta, agentId, config, resultSummary) {
|
|
|
2199
2169
|
try {
|
|
2200
2170
|
const projects = shared.getProjects(config);
|
|
2201
2171
|
for (const p of projects) {
|
|
2202
|
-
const prs =
|
|
2172
|
+
const prs = shared.readPullRequests(p);
|
|
2203
2173
|
if (prs.some(pr => pr.sourcePlan === item.sourcePlan)) return null;
|
|
2204
2174
|
}
|
|
2205
|
-
const centralPrs =
|
|
2175
|
+
const centralPrs = shared.readPullRequests('central');
|
|
2206
2176
|
if (centralPrs.some(pr => pr.sourcePlan === item.sourcePlan)) return null;
|
|
2207
2177
|
} catch (err) {
|
|
2208
2178
|
log('warn', `enforceVerifyPrContract: sourcePlan PR check failed for ${item.id}: ${err.message}`);
|
|
@@ -2212,13 +2182,13 @@ function enforceVerifyPrContract(type, meta, agentId, config, resultSummary) {
|
|
|
2212
2182
|
// No PR found — flip WI to failed and surface an alert.
|
|
2213
2183
|
const failureClass = FAILURE_CLASS.VERIFY_MISSING_PR;
|
|
2214
2184
|
const reason = `Verify WI ${item.id} completed done but no PR was found ` +
|
|
2215
|
-
`(no _prUrl/_pr on item, no canonical attachment, and no
|
|
2185
|
+
`(no _prUrl/_pr on item, no canonical attachment, and no tracked PR entry for plan ` +
|
|
2216
2186
|
`${item.sourcePlan || '(none)'}). Flipped to failed so the plan does not silently advance. ` +
|
|
2217
2187
|
`(Branch: ${meta.branch || '(none)'}, agent: ${agentId})`;
|
|
2218
2188
|
|
|
2219
|
-
const
|
|
2220
|
-
if (
|
|
2221
|
-
mutateWorkItems(
|
|
2189
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
2190
|
+
if (wiScope) {
|
|
2191
|
+
mutateWorkItems(wiScope, data => {
|
|
2222
2192
|
if (!Array.isArray(data)) return data;
|
|
2223
2193
|
const w = data.find(i => i.id === item.id);
|
|
2224
2194
|
if (!w) return data;
|
|
@@ -2228,7 +2198,7 @@ function enforceVerifyPrContract(type, meta, agentId, config, resultSummary) {
|
|
|
2228
2198
|
w.failedAt = ts();
|
|
2229
2199
|
delete w.completedAt;
|
|
2230
2200
|
return data;
|
|
2231
|
-
}
|
|
2201
|
+
});
|
|
2232
2202
|
}
|
|
2233
2203
|
|
|
2234
2204
|
if (item.sourcePlan) {
|
|
@@ -2312,10 +2282,6 @@ function reviewPrRefMatchesDispatchTarget(reportedPr, dispatchPr, project) {
|
|
|
2312
2282
|
return !(reportedScoped && dispatchScoped);
|
|
2313
2283
|
}
|
|
2314
2284
|
|
|
2315
|
-
function centralPrPath() {
|
|
2316
|
-
return path.join(MINIONS_DIR, 'pull-requests.json');
|
|
2317
|
-
}
|
|
2318
|
-
|
|
2319
2285
|
function resolveReviewPrContext(pr, project, config, structuredCompletion = null) {
|
|
2320
2286
|
const reportedPr = reviewPrRefFromCompletion(structuredCompletion);
|
|
2321
2287
|
if (reportedPr && pr && !reviewPrRefMatchesDispatchTarget(reportedPr, pr, project)) return null;
|
|
@@ -2330,28 +2296,26 @@ function resolveReviewPrContext(pr, project, config, structuredCompletion = null
|
|
|
2330
2296
|
}
|
|
2331
2297
|
|
|
2332
2298
|
for (const candidateProject of projectCandidates) {
|
|
2333
|
-
const
|
|
2334
|
-
const prs = safeJsonArr(prPath);
|
|
2299
|
+
const prs = shared.readPullRequests(candidateProject);
|
|
2335
2300
|
for (const ref of refs) {
|
|
2336
2301
|
const refUrl = typeof ref === 'object' ? ref.url || '' : String(ref || '');
|
|
2337
2302
|
if (!shared.isPrCompatibleWithProject(candidateProject, ref, refUrl)) continue;
|
|
2338
2303
|
const target = shared.findPrRecord(prs, ref, candidateProject);
|
|
2339
|
-
if (target) return { pr: { ...target }, project: candidateProject,
|
|
2304
|
+
if (target) return { pr: { ...target }, project: candidateProject, scope: candidateProject.name };
|
|
2340
2305
|
}
|
|
2341
2306
|
}
|
|
2342
2307
|
|
|
2343
|
-
const
|
|
2344
|
-
const centralPrs = safeJsonArr(centralPath);
|
|
2308
|
+
const centralPrs = shared.readPullRequests('central');
|
|
2345
2309
|
const centralRefs = reportedPr ? [reportedPr] : refs;
|
|
2346
2310
|
for (const ref of centralRefs) {
|
|
2347
2311
|
const target = shared.findPrRecord(centralPrs, ref, null);
|
|
2348
|
-
if (target) return { pr: { ...target }, project: null,
|
|
2312
|
+
if (target) return { pr: { ...target }, project: null, scope: 'central' };
|
|
2349
2313
|
}
|
|
2350
2314
|
|
|
2351
2315
|
if (reportedPr) return null;
|
|
2352
2316
|
|
|
2353
2317
|
return pr?.id
|
|
2354
|
-
? { pr, project: project || null,
|
|
2318
|
+
? { pr, project: project || null, scope: project?.name || 'central' }
|
|
2355
2319
|
: null;
|
|
2356
2320
|
}
|
|
2357
2321
|
|
|
@@ -2372,7 +2336,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
2372
2336
|
}
|
|
2373
2337
|
const reviewPr = reviewContext.pr;
|
|
2374
2338
|
const reviewProject = reviewContext.project;
|
|
2375
|
-
const
|
|
2339
|
+
const prScope = reviewContext.scope;
|
|
2376
2340
|
const reviewerName = config.agents?.[agentId]?.name || agentId;
|
|
2377
2341
|
|
|
2378
2342
|
// Check actual review status from the platform (agent may have approved or requested changes)
|
|
@@ -2383,7 +2347,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
2383
2347
|
// BUG-H14 (P-h14-repohost): resolve repoHost from the PR's canonical id
|
|
2384
2348
|
// FIRST. The previous code derived hostForChecks from the fallback project
|
|
2385
2349
|
// (`reviewProject || getProjects(config)[0]`) and silently defaulted to
|
|
2386
|
-
// 'ado' on miss — so a github:* PR found in
|
|
2350
|
+
// 'ado' on miss — so a github:* PR found in central state
|
|
2387
2351
|
// (reviewProject=null) with an ADO project as projects[0] was mis-routed to
|
|
2388
2352
|
// ado.checkLiveReviewStatus. The fallback project is still consulted when
|
|
2389
2353
|
// the id is unparseable, but there is no silent 'ado' default — an
|
|
@@ -2504,7 +2468,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
2504
2468
|
const maxMissingRetries = config?.engine?.maxReviewVerdictMissingRetries ?? ENGINE_DEFAULTS.maxReviewVerdictMissingRetries;
|
|
2505
2469
|
let missingCount = 0;
|
|
2506
2470
|
let retriesExhausted = false;
|
|
2507
|
-
shared.
|
|
2471
|
+
shared.mutatePullRequests(prScope, (prs) => {
|
|
2508
2472
|
if (!Array.isArray(prs)) return prs;
|
|
2509
2473
|
const target = shared.findPrRecord(prs, reviewPr, reviewProject);
|
|
2510
2474
|
if (!target) return prs;
|
|
@@ -2524,7 +2488,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
2524
2488
|
// normal needsReview gate re-dispatches a fresh review next tick.
|
|
2525
2489
|
if (retriesExhausted) target.lastReviewedAt = ts();
|
|
2526
2490
|
return prs;
|
|
2527
|
-
}
|
|
2491
|
+
});
|
|
2528
2492
|
|
|
2529
2493
|
const reason = `Review dispatch for ${reviewPr.id} completed without posting a VERDICT: comment ` +
|
|
2530
2494
|
`(reviewer: ${reviewerName}, dispatch: ${dispatchItem?.id || structuredCompletion?.dispatchId || '(unknown)'}). ` +
|
|
@@ -2545,7 +2509,7 @@ async function updatePrAfterReview(agentId, pr, project, config, resultSummary,
|
|
|
2545
2509
|
return;
|
|
2546
2510
|
}
|
|
2547
2511
|
|
|
2548
|
-
shared.
|
|
2512
|
+
shared.mutatePullRequests(prScope, (prs) => {
|
|
2549
2513
|
if (!Array.isArray(prs)) return prs;
|
|
2550
2514
|
const target = shared.findPrRecord(prs, reviewPr, reviewProject);
|
|
2551
2515
|
if (!target) return prs;
|
|
@@ -3171,7 +3135,7 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
3171
3135
|
options = { automationCauseKey: options, dispatchId: legacyDispatchId };
|
|
3172
3136
|
}
|
|
3173
3137
|
const explicitlyChangedBranch = options.branchChanged !== false;
|
|
3174
|
-
const
|
|
3138
|
+
const prScope = project || 'central';
|
|
3175
3139
|
const automationCauseKey = options.automationCauseKey || options.dispatchItem?.meta?.automationCauseKey || '';
|
|
3176
3140
|
const fixDispatchId = options.dispatchItem?.id || options.dispatchId || legacyDispatchId || '';
|
|
3177
3141
|
const cause = shared.getPrFixAutomationCause({
|
|
@@ -3180,7 +3144,7 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
3180
3144
|
task: options.dispatchItem?.task,
|
|
3181
3145
|
});
|
|
3182
3146
|
let result = null;
|
|
3183
|
-
shared.
|
|
3147
|
+
shared.mutatePullRequests(prScope, (prs) => {
|
|
3184
3148
|
if (!Array.isArray(prs)) return prs;
|
|
3185
3149
|
const target = shared.findPrRecord(prs, pr, project);
|
|
3186
3150
|
if (!target) return prs;
|
|
@@ -3384,7 +3348,7 @@ function updatePrAfterFix(pr, project, source, options = {}, legacyDispatchId =
|
|
|
3384
3348
|
function updatePrAfterFixError(pr, project, source, options = {}) {
|
|
3385
3349
|
if (!pr?.id) return null;
|
|
3386
3350
|
if (!options || typeof options !== 'object' || Array.isArray(options)) return null;
|
|
3387
|
-
const
|
|
3351
|
+
const prScope = project || 'central';
|
|
3388
3352
|
const dispatchItem = options.dispatchItem || null;
|
|
3389
3353
|
const errorMessage = String(options.errorMessage || '').slice(0, 200);
|
|
3390
3354
|
const errorClass = String(options.errorClass || 'agent-error');
|
|
@@ -3394,7 +3358,7 @@ function updatePrAfterFixError(pr, project, source, options = {}) {
|
|
|
3394
3358
|
task: dispatchItem?.task,
|
|
3395
3359
|
});
|
|
3396
3360
|
let result = null;
|
|
3397
|
-
shared.
|
|
3361
|
+
shared.mutatePullRequests(prScope, (prs) => {
|
|
3398
3362
|
if (!Array.isArray(prs)) return prs;
|
|
3399
3363
|
const target = shared.findPrRecord(prs, pr, project);
|
|
3400
3364
|
if (!target) return prs;
|
|
@@ -3478,7 +3442,7 @@ function findDependentActivePrs(mergedItemId, config) {
|
|
|
3478
3442
|
|
|
3479
3443
|
const projects = shared.getProjects(config);
|
|
3480
3444
|
for (const p of projects) {
|
|
3481
|
-
const prs =
|
|
3445
|
+
const prs = shared.readPullRequests(p);
|
|
3482
3446
|
for (const pr of prs) {
|
|
3483
3447
|
if (!pr.branch || pr.status !== PR_STATUS.ACTIVE) continue;
|
|
3484
3448
|
const linked = (pr.prdItems || []).some(id => dependentWis.some(wi => wi.id === id));
|
|
@@ -3560,10 +3524,8 @@ async function rebaseBranchOntoMain(pr, project, config) {
|
|
|
3560
3524
|
}
|
|
3561
3525
|
}
|
|
3562
3526
|
|
|
3563
|
-
const PENDING_REBASES_PATH = path.join(ENGINE_DIR, 'pending-rebases.json');
|
|
3564
|
-
|
|
3565
3527
|
function queuePendingRebase(pr, project, mergedItemId) {
|
|
3566
|
-
|
|
3528
|
+
smallStateStore.applyPendingRebasesMutation((pending) => {
|
|
3567
3529
|
const prDisplayId = shared.getPrDisplayId(pr);
|
|
3568
3530
|
if (pending.some(e => e.projectName === project.name && shared.getPrDisplayId(e.prId) === prDisplayId)) return pending; // already queued
|
|
3569
3531
|
pending.push({ prId: pr.id, branch: pr.branch, projectName: project.name, mergedItemId, queuedAt: ts(), attempts: 0 });
|
|
@@ -3575,10 +3537,10 @@ async function processPendingRebases(config) {
|
|
|
3575
3537
|
// Atomically drain the queue under lock so concurrent queuePendingRebase calls
|
|
3576
3538
|
// during processing don't lose entries (they append to the now-empty file).
|
|
3577
3539
|
let snapshot = [];
|
|
3578
|
-
|
|
3540
|
+
smallStateStore.applyPendingRebasesMutation((data) => {
|
|
3579
3541
|
snapshot = [...data];
|
|
3580
3542
|
return []; // drain file
|
|
3581
|
-
}
|
|
3543
|
+
});
|
|
3582
3544
|
if (snapshot.length === 0) return;
|
|
3583
3545
|
|
|
3584
3546
|
const remaining = [];
|
|
@@ -3606,9 +3568,9 @@ async function processPendingRebases(config) {
|
|
|
3606
3568
|
}
|
|
3607
3569
|
// Merge remaining items back under lock — entries queued during processing are preserved
|
|
3608
3570
|
if (remaining.length > 0) {
|
|
3609
|
-
|
|
3571
|
+
smallStateStore.applyPendingRebasesMutation((data) => {
|
|
3610
3572
|
data.push(...remaining);
|
|
3611
|
-
}
|
|
3573
|
+
});
|
|
3612
3574
|
}
|
|
3613
3575
|
}
|
|
3614
3576
|
|
|
@@ -3619,7 +3581,7 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
3619
3581
|
const prNum = shared.getPrNumber(pr);
|
|
3620
3582
|
|
|
3621
3583
|
// `project.localPath` guard: central/observed PRs (e.g. ADO PRs tracked in
|
|
3622
|
-
//
|
|
3584
|
+
// central PRs with no configured project are polled via a
|
|
3623
3585
|
// *synthesized* project that has no localPath (engine/ado.js + engine/github.js
|
|
3624
3586
|
// central-poll loops). There is no checkout to clean for those, and an
|
|
3625
3587
|
// unguarded path.resolve(undefined) here threw "paths[0] argument must be of
|
|
@@ -3664,13 +3626,11 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
3664
3626
|
if (mergedItemIds.length > 0) {
|
|
3665
3627
|
const mergedItemSet = new Set(mergedItemIds);
|
|
3666
3628
|
// Mark PRD feature as implemented
|
|
3667
|
-
const prdDir = path.join(MINIONS_DIR, 'prd');
|
|
3668
3629
|
try {
|
|
3669
|
-
const planFiles =
|
|
3630
|
+
const planFiles = prdStore.listPrdRows().filter(row => !row.archived).map(row => row.filename);
|
|
3670
3631
|
let updated = 0;
|
|
3671
3632
|
for (const pf of planFiles) {
|
|
3672
|
-
|
|
3673
|
-
mutateJsonFileLocked(planPath, (plan) => {
|
|
3633
|
+
prdStore.mutatePrd(pf, (plan) => {
|
|
3674
3634
|
if (!plan?.missing_features) return plan;
|
|
3675
3635
|
if (shared.isPrdArchived(plan)) return plan; // step 4: never mutate an archived (in-place) PRD's items
|
|
3676
3636
|
for (const feature of plan.missing_features) {
|
|
@@ -3680,18 +3640,17 @@ async function handlePostMerge(pr, project, config, newStatus) {
|
|
|
3680
3640
|
}
|
|
3681
3641
|
}
|
|
3682
3642
|
return plan;
|
|
3683
|
-
}
|
|
3643
|
+
});
|
|
3684
3644
|
}
|
|
3685
3645
|
if (updated > 0) log('info', `Post-merge: marked ${mergedItemIds.join(', ')} as done for ${pr.id}`);
|
|
3686
3646
|
} catch (err) { log('warn', `Post-merge PRD update: ${err.message}`); }
|
|
3687
3647
|
|
|
3688
3648
|
// Mark work item as done
|
|
3689
|
-
const
|
|
3690
|
-
for (const p of shared.getProjects(config)) wiPaths.push(shared.projectWorkItemsPath(p));
|
|
3649
|
+
const workItemScopes = ['central', ...shared.getProjects(config)];
|
|
3691
3650
|
const remainingMergedIds = new Set(mergedItemIds);
|
|
3692
|
-
for (const
|
|
3651
|
+
for (const scope of workItemScopes) {
|
|
3693
3652
|
try {
|
|
3694
|
-
mutateWorkItems(
|
|
3653
|
+
mutateWorkItems(scope, items => {
|
|
3695
3654
|
for (const item of items) {
|
|
3696
3655
|
if (!remainingMergedIds.has(item.id)) continue;
|
|
3697
3656
|
if (item.status !== WI_STATUS.DONE) {
|
|
@@ -3753,7 +3712,7 @@ function checkForLearnings(agentId, agentInfo, taskDesc) {
|
|
|
3753
3712
|
}
|
|
3754
3713
|
// Info, not warn: not writing learnings is a soft signal (the agent finished
|
|
3755
3714
|
// and the engine continues fine). Operators don't need to act on it; keeping
|
|
3756
|
-
// it at warn floods log
|
|
3715
|
+
// it at warn floods the engine log (~50 entries/day) with non-actionable noise.
|
|
3757
3716
|
log('info', `${agentInfo?.name || agentId} didn't write learnings — no follow-up queued`);
|
|
3758
3717
|
}
|
|
3759
3718
|
|
|
@@ -3846,9 +3805,8 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config, runtimeN
|
|
|
3846
3805
|
const projectSkillRoot = skillWriteTargets(effectiveRuntime, proj).project
|
|
3847
3806
|
|| path.resolve(proj.localPath, '.claude', 'skills');
|
|
3848
3807
|
const projectSkillPath = path.join(projectSkillRoot, skillDirName, 'SKILL.md');
|
|
3849
|
-
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
3850
3808
|
let skillId = null;
|
|
3851
|
-
|
|
3809
|
+
mutateWorkItems('central', data => {
|
|
3852
3810
|
data = data || [];
|
|
3853
3811
|
if (data.some(i => i.title === `Add skill: ${name}` && i.status !== WI_STATUS.FAILED)) return data;
|
|
3854
3812
|
skillId = `SK${String(data.filter(i => i.id?.startsWith('SK')).length + 1).padStart(3, '0')}`;
|
|
@@ -3856,7 +3814,7 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config, runtimeN
|
|
|
3856
3814
|
description: `Create project-level skill \`${skillDirName}/SKILL.md\` in ${project}.\n\nWrite this file to \`${projectSkillPath}\` via a PR.\n\n## Skill Content\n\n\`\`\`\n${enrichedBlock}\n\`\`\``,
|
|
3857
3815
|
priority: 'low', status: WI_STATUS.QUEUED, created: ts(), createdBy: `engine:skill-extraction:${agentName}` });
|
|
3858
3816
|
return data;
|
|
3859
|
-
}
|
|
3817
|
+
});
|
|
3860
3818
|
if (skillId) {
|
|
3861
3819
|
log('info', `Queued work item ${skillId} to PR project skill "${name}" into ${project}`);
|
|
3862
3820
|
}
|
|
@@ -4189,9 +4147,9 @@ function handleInjectionFlag(dispatchItem, agentId, structuredCompletion, config
|
|
|
4189
4147
|
}
|
|
4190
4148
|
|
|
4191
4149
|
try {
|
|
4192
|
-
const
|
|
4193
|
-
if (
|
|
4194
|
-
mutateWorkItems(
|
|
4150
|
+
const wiScope = dispatchItem?.meta ? resolveWorkItemScope(dispatchItem.meta) : null;
|
|
4151
|
+
if (wiScope && dispatchItem?.meta?.item?.id) {
|
|
4152
|
+
mutateWorkItems(wiScope, (items) => {
|
|
4195
4153
|
const wi = items.find((w) => w.id === dispatchItem.meta.item.id);
|
|
4196
4154
|
if (wi) {
|
|
4197
4155
|
wi._securityFlag = {
|
|
@@ -4367,8 +4325,8 @@ function mergeArtifactNotes(existing, additions) {
|
|
|
4367
4325
|
function promoteCompletionArtifacts(meta, agentId, dispatchId, structuredCompletion, opts = {}) {
|
|
4368
4326
|
const itemId = meta?.item?.id;
|
|
4369
4327
|
if (!itemId) return { artifacts: [], notes: [] };
|
|
4370
|
-
const
|
|
4371
|
-
if (!
|
|
4328
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
4329
|
+
if (!wiScope) return { artifacts: [], notes: [] };
|
|
4372
4330
|
|
|
4373
4331
|
const artifacts = normalizeCompletionArtifacts(structuredCompletion?.artifacts);
|
|
4374
4332
|
const structuredNotes = artifacts.map(completionArtifactToNoteEntry).filter(Boolean);
|
|
@@ -4381,7 +4339,7 @@ function promoteCompletionArtifacts(meta, agentId, dispatchId, structuredComplet
|
|
|
4381
4339
|
return { artifacts, notes };
|
|
4382
4340
|
}
|
|
4383
4341
|
|
|
4384
|
-
|
|
4342
|
+
mutateWorkItems(wiScope, data => {
|
|
4385
4343
|
if (!Array.isArray(data)) return data;
|
|
4386
4344
|
const wi = data.find(i => i.id === itemId);
|
|
4387
4345
|
if (!wi) return data;
|
|
@@ -4398,7 +4356,7 @@ function promoteCompletionArtifacts(meta, agentId, dispatchId, structuredComplet
|
|
|
4398
4356
|
if (resultSummary) wi.resultSummary = resultSummary;
|
|
4399
4357
|
wi._artifacts = arts;
|
|
4400
4358
|
return data;
|
|
4401
|
-
}
|
|
4359
|
+
});
|
|
4402
4360
|
|
|
4403
4361
|
return { artifacts, notes, agentId, dispatchId };
|
|
4404
4362
|
}
|
|
@@ -4487,13 +4445,13 @@ function _deferRetryWithCounter(meta, detection, counterField, maxCount, pending
|
|
|
4487
4445
|
const itemId = meta?.item?.id;
|
|
4488
4446
|
const reason = detection?.reason || 'Nonterminal completion summary';
|
|
4489
4447
|
if (!itemId) return reason;
|
|
4490
|
-
const
|
|
4491
|
-
if (!
|
|
4448
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
4449
|
+
if (!wiScope) return reason;
|
|
4492
4450
|
|
|
4493
4451
|
let finalStatus = WI_STATUS.PENDING;
|
|
4494
4452
|
let systematicPhantom = null;
|
|
4495
4453
|
try {
|
|
4496
|
-
|
|
4454
|
+
mutateWorkItems(wiScope, data => {
|
|
4497
4455
|
if (!Array.isArray(data)) return data;
|
|
4498
4456
|
const w = data.find(i => i.id === itemId);
|
|
4499
4457
|
if (!w) return data;
|
|
@@ -4578,7 +4536,7 @@ function _deferRetryWithCounter(meta, detection, counterField, maxCount, pending
|
|
|
4578
4536
|
log('warn', `Work item ${itemId} failed — repeated ${pendingReason} after ${maxCount} attempts`);
|
|
4579
4537
|
}
|
|
4580
4538
|
return data;
|
|
4581
|
-
}
|
|
4539
|
+
});
|
|
4582
4540
|
syncPrdItemStatus(itemId, finalStatus, meta.item?.sourcePlan);
|
|
4583
4541
|
} catch (err) {
|
|
4584
4542
|
log('warn', `${pendingReason} gate: ${err.message}`);
|
|
@@ -4840,7 +4798,7 @@ function extractDecompositionJson(stdout, runtimeName) {
|
|
|
4840
4798
|
* Parses the optional `followups` array on the agent's completion report,
|
|
4841
4799
|
* logs each entry at `info` so operators can trace fan-out, and warns when
|
|
4842
4800
|
* a claimed `wi_id` doesn't match any record in central or per-project
|
|
4843
|
-
* work-
|
|
4801
|
+
* work-item state. The mismatch warning catches dispatches that were reverted
|
|
4844
4802
|
* (HTTP 409 from /api/work-items dedup), fingerprinted away by the standard
|
|
4845
4803
|
* dedup window, or otherwise didn't actually land — situations where the
|
|
4846
4804
|
* agent claimed a follow-up exists but the dashboard/engine has no record.
|
|
@@ -4893,12 +4851,12 @@ function processCompletionFollowups(completion, agentId, dispatchItem, config) {
|
|
|
4893
4851
|
log('info', `Followup audit (${agentId || 'unknown'} wi=${wiId}): dispatched ${followupWiId}${title ? ` — ${title}` : ''}${parentCommentId ? ` (comment=${parentCommentId})` : ''}${reason ? ` — ${reason}` : ''}`);
|
|
4894
4852
|
const allIds = loadExistingIds();
|
|
4895
4853
|
if (allIds.size > 0 && !allIds.has(followupWiId)) {
|
|
4896
|
-
log('warn', `Followup audit (${agentId || 'unknown'} wi=${wiId}): claimed followup ${followupWiId} not found in
|
|
4854
|
+
log('warn', `Followup audit (${agentId || 'unknown'} wi=${wiId}): claimed followup ${followupWiId} not found in work-item state — dispatch may have been deduped, reverted, or never landed`);
|
|
4897
4855
|
}
|
|
4898
4856
|
if (parentItem && (parentSchedule.scheduleId || parentProject)) {
|
|
4899
4857
|
let stamped = false;
|
|
4900
|
-
for (const
|
|
4901
|
-
mutateWorkItems(
|
|
4858
|
+
for (const scope of getWorkItemScopes(config)) {
|
|
4859
|
+
mutateWorkItems(scope, items => {
|
|
4902
4860
|
const target = Array.isArray(items) ? items.find(i => i && i.id === followupWiId) : null;
|
|
4903
4861
|
if (!target) return items;
|
|
4904
4862
|
if (!target.parent_id && target.id !== wiId) target.parent_id = wiId;
|
|
@@ -4949,12 +4907,11 @@ function handleDecompositionResult(stdout, meta, config, runtimeName) {
|
|
|
4949
4907
|
|
|
4950
4908
|
// Find and update the parent work item
|
|
4951
4909
|
const projects = shared.getProjects(config);
|
|
4952
|
-
const
|
|
4953
|
-
for (const p of projects) allPaths.push(shared.projectWorkItemsPath(p));
|
|
4910
|
+
const allScopes = ['central', ...projects];
|
|
4954
4911
|
|
|
4955
|
-
for (const
|
|
4912
|
+
for (const scope of allScopes) {
|
|
4956
4913
|
let found = false;
|
|
4957
|
-
|
|
4914
|
+
mutateWorkItems(scope, data => {
|
|
4958
4915
|
if (!Array.isArray(data)) return data;
|
|
4959
4916
|
const p = data.find(i => i.id === parentId);
|
|
4960
4917
|
if (!p) return data;
|
|
@@ -5000,7 +4957,7 @@ function handleDecompositionResult(stdout, meta, config, runtimeName) {
|
|
|
5000
4957
|
data.push(childItem);
|
|
5001
4958
|
}
|
|
5002
4959
|
return data;
|
|
5003
|
-
}
|
|
4960
|
+
});
|
|
5004
4961
|
if (!found) continue;
|
|
5005
4962
|
log('info', `Decomposition: ${parentId} → ${subItems.length} sub-items: ${subItems.map(s => s.id).join(', ')}`);
|
|
5006
4963
|
return subItems.length;
|
|
@@ -5049,18 +5006,14 @@ function handleHarnessIterationResult(stdout, structuredCompletion, meta, config
|
|
|
5049
5006
|
}
|
|
5050
5007
|
if (!Array.isArray(nextItems) || nextItems.length === 0) return 0;
|
|
5051
5008
|
|
|
5052
|
-
//
|
|
5053
|
-
// and append into the file that owns the evaluator (the trio always lands in
|
|
5054
|
-
// the central file in practice — scheduler.discoverScheduledWork writes
|
|
5055
|
-
// directly to engine/work-items.json via engine.js — but iterate defensively).
|
|
5009
|
+
// Append into the scope that owns the evaluator.
|
|
5056
5010
|
const projects = shared.getProjects(config);
|
|
5057
|
-
const
|
|
5058
|
-
for (const p of projects) allPaths.push(shared.projectWorkItemsPath(p));
|
|
5011
|
+
const allScopes = ['central', ...projects];
|
|
5059
5012
|
|
|
5060
5013
|
let appendedTo = null;
|
|
5061
|
-
for (const
|
|
5014
|
+
for (const scope of allScopes) {
|
|
5062
5015
|
let found = false;
|
|
5063
|
-
|
|
5016
|
+
mutateWorkItems(scope, data => {
|
|
5064
5017
|
if (!Array.isArray(data)) return data;
|
|
5065
5018
|
const evaluator = data.find(i => i.id === evaluatorItem.id);
|
|
5066
5019
|
if (!evaluator) return data;
|
|
@@ -5072,12 +5025,12 @@ function handleHarnessIterationResult(stdout, structuredCompletion, meta, config
|
|
|
5072
5025
|
data.push(it);
|
|
5073
5026
|
}
|
|
5074
5027
|
return data;
|
|
5075
|
-
}
|
|
5076
|
-
if (found) { appendedTo =
|
|
5028
|
+
});
|
|
5029
|
+
if (found) { appendedTo = shared.stateScope(scope); break; }
|
|
5077
5030
|
}
|
|
5078
5031
|
|
|
5079
5032
|
if (!appendedTo) {
|
|
5080
|
-
log('warn', `Harness ${evaluatorItem._harness.missionId}: evaluator ${evaluatorItem.id} not found in
|
|
5033
|
+
log('warn', `Harness ${evaluatorItem._harness.missionId}: evaluator ${evaluatorItem.id} not found in work-item state — iteration skipped`);
|
|
5081
5034
|
return 0;
|
|
5082
5035
|
}
|
|
5083
5036
|
|
|
@@ -5112,7 +5065,7 @@ function handleHarnessIterationResult(stdout, structuredCompletion, meta, config
|
|
|
5112
5065
|
* ban. If even the deferred ANY_AGENT placeholder cannot be built, log and
|
|
5113
5066
|
* skip — the existing discovery-driven re-review path (engine.js:~4249)
|
|
5114
5067
|
* will re-evaluate on the next tick.
|
|
5115
|
-
* - All RMW
|
|
5068
|
+
* - All dispatch RMW goes through addToDispatch (mutateDispatch), per
|
|
5116
5069
|
* the concurrency rules in CLAUDE.md.
|
|
5117
5070
|
*/
|
|
5118
5071
|
function dispatchReReviewForFix(fixDispatchItem, meta, config, opts = {}) {
|
|
@@ -5134,12 +5087,9 @@ function dispatchReReviewForFix(fixDispatchItem, meta, config, opts = {}) {
|
|
|
5134
5087
|
const project = meta.project || null;
|
|
5135
5088
|
let livePr = pr;
|
|
5136
5089
|
try {
|
|
5137
|
-
const
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
const target = shared.findPrRecord(prs, pr, project);
|
|
5141
|
-
if (target) livePr = target;
|
|
5142
|
-
}
|
|
5090
|
+
const prs = shared.readPullRequests(project || 'central');
|
|
5091
|
+
const target = shared.findPrRecord(prs, pr, project);
|
|
5092
|
+
if (target) livePr = target;
|
|
5143
5093
|
} catch { /* fall through to dispatch meta copy */ }
|
|
5144
5094
|
if (livePr.status && livePr.status !== PR_STATUS.ACTIVE) {
|
|
5145
5095
|
log('info', `Re-review skipped for ${pr.id}: PR status is ${livePr.status}`);
|
|
@@ -5258,29 +5208,28 @@ function pickReReviewAgentHints(config, opts = {}) {
|
|
|
5258
5208
|
function applyGoalInvalidation(sourceWiId, invalidates, config) {
|
|
5259
5209
|
if (!Array.isArray(invalidates) || invalidates.length === 0) return;
|
|
5260
5210
|
|
|
5261
|
-
const
|
|
5211
|
+
const workItemScopes = getWorkItemScopes(config);
|
|
5262
5212
|
const cancellableStatuses = new Set([WI_STATUS.PENDING, WI_STATUS.QUEUED]);
|
|
5263
5213
|
const cancellationReason = `invalidated-by:${sourceWiId}`;
|
|
5264
5214
|
|
|
5265
5215
|
for (const targetId of invalidates) {
|
|
5266
5216
|
if (!targetId || typeof targetId !== 'string') continue;
|
|
5267
5217
|
|
|
5268
|
-
|
|
5269
|
-
let foundPath = null;
|
|
5218
|
+
let foundScope = null;
|
|
5270
5219
|
let foundStatus = null;
|
|
5271
|
-
for (const
|
|
5220
|
+
for (const scope of workItemScopes) {
|
|
5272
5221
|
try {
|
|
5273
|
-
const items =
|
|
5222
|
+
const items = shared.readWorkItems(scope);
|
|
5274
5223
|
const item = items.find(i => i && i.id === targetId);
|
|
5275
5224
|
if (item) {
|
|
5276
|
-
|
|
5225
|
+
foundScope = scope;
|
|
5277
5226
|
foundStatus = item.status;
|
|
5278
5227
|
break;
|
|
5279
5228
|
}
|
|
5280
5229
|
} catch { /* best-effort scan */ }
|
|
5281
5230
|
}
|
|
5282
5231
|
|
|
5283
|
-
if (!
|
|
5232
|
+
if (!foundScope) {
|
|
5284
5233
|
log('warn', `Goal invalidation: WI ${targetId} listed in invalidates[] by ${sourceWiId} not found — skipping`);
|
|
5285
5234
|
continue;
|
|
5286
5235
|
}
|
|
@@ -5292,7 +5241,7 @@ function applyGoalInvalidation(sourceWiId, invalidates, config) {
|
|
|
5292
5241
|
|
|
5293
5242
|
// Cancel the WI via mutateWorkItems so the SQL store + state event are updated.
|
|
5294
5243
|
try {
|
|
5295
|
-
mutateWorkItems(
|
|
5244
|
+
mutateWorkItems(foundScope, (items) => {
|
|
5296
5245
|
if (!Array.isArray(items)) return items;
|
|
5297
5246
|
const wi = items.find(i => i && i.id === targetId);
|
|
5298
5247
|
if (!wi) return items;
|
|
@@ -5463,7 +5412,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5463
5412
|
// PR follow-up audit (W-mpej3cox00099466) — when the agent declares
|
|
5464
5413
|
// `followups: [{wi_id, title, reason, parent_comment_id}]` in its completion
|
|
5465
5414
|
// report, log each entry and verify the claimed WI ids actually landed in
|
|
5466
|
-
// some project
|
|
5415
|
+
// some project scope. A mismatch (claimed wi_id with no matching
|
|
5467
5416
|
// record) is logged at warn so operators can spot dispatches that were
|
|
5468
5417
|
// reverted, fingerprinted-deduped, or otherwise didn't take.
|
|
5469
5418
|
if (structuredCompletion) {
|
|
@@ -5512,7 +5461,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5512
5461
|
// syncPrsFromOutput only enrolls PRs whose URL appeared in this dispatch's
|
|
5513
5462
|
// output. When meta.item._pr was stamped from an earlier dispatch (e.g. via
|
|
5514
5463
|
// reconcileItemsWithPrs against pr-links) but the canonical row never made
|
|
5515
|
-
// it into
|
|
5464
|
+
// it into tracked PR state, the dashboard's PRD-progress synthesis
|
|
5516
5465
|
// defaults to ACTIVE forever. This catches those cases at the end of every
|
|
5517
5466
|
// post-completion run.
|
|
5518
5467
|
try {
|
|
@@ -5579,10 +5528,10 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5579
5528
|
// the DONE update so the parent stays pending. The dispatch retry layer
|
|
5580
5529
|
// will requeue it through the standard cooldown/backoff path.
|
|
5581
5530
|
skipDoneStatus = true;
|
|
5582
|
-
const
|
|
5583
|
-
if (
|
|
5531
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
5532
|
+
if (wiScope) {
|
|
5584
5533
|
try {
|
|
5585
|
-
|
|
5534
|
+
mutateWorkItems(wiScope, data => {
|
|
5586
5535
|
if (!Array.isArray(data)) return data;
|
|
5587
5536
|
const wi = data.find(i => i.id === meta.item.id);
|
|
5588
5537
|
if (wi) {
|
|
@@ -5591,7 +5540,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5591
5540
|
wi._pendingReason = 'decompose_no_children';
|
|
5592
5541
|
}
|
|
5593
5542
|
return data;
|
|
5594
|
-
}
|
|
5543
|
+
});
|
|
5595
5544
|
} catch (err) { log('warn', `Decompose retry-prep cleanup: ${err.message}`); }
|
|
5596
5545
|
}
|
|
5597
5546
|
log('warn', `Decomposition for ${meta.item.id}: success report but no children created — parent left pending for retry`);
|
|
@@ -5600,7 +5549,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5600
5549
|
|
|
5601
5550
|
// Tri-agent harness iteration (W-mq07a9gf000jbc2b): if the evaluator just
|
|
5602
5551
|
// completed successfully and verdict says retry, append the next Gen+Eval
|
|
5603
|
-
// pair into the same work-
|
|
5552
|
+
// pair into the same work-item scope. Engine will dispatch them on the
|
|
5604
5553
|
// next tick. No interaction with skipDoneStatus — the evaluator itself
|
|
5605
5554
|
// still marks DONE; iteration is a sibling write, not a parent decomp.
|
|
5606
5555
|
if (effectiveSuccess && meta?.item?._harness?.role === harness.HARNESS_ROLE.EVALUATOR) {
|
|
@@ -5628,10 +5577,10 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5628
5577
|
log('info', `Review ${meta.item.id} bailed out (review already posted) — treating as DONE without retry`);
|
|
5629
5578
|
} else if (!verdict) {
|
|
5630
5579
|
skipDoneStatus = true;
|
|
5631
|
-
const
|
|
5632
|
-
if (
|
|
5580
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
5581
|
+
if (wiScope) {
|
|
5633
5582
|
try {
|
|
5634
|
-
|
|
5583
|
+
mutateWorkItems(wiScope, data => {
|
|
5635
5584
|
if (!Array.isArray(data)) return data;
|
|
5636
5585
|
const w = data.find(i => i.id === meta.item.id);
|
|
5637
5586
|
if (!w) return data;
|
|
@@ -5656,7 +5605,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5656
5605
|
log('warn', `Review ${meta.item.id} failed — no verdict after ${ENGINE_DEFAULTS.maxRetries} retries`);
|
|
5657
5606
|
}
|
|
5658
5607
|
return data;
|
|
5659
|
-
}
|
|
5608
|
+
});
|
|
5660
5609
|
} catch (err) { log('warn', `review verdict check: ${err.message}`); }
|
|
5661
5610
|
}
|
|
5662
5611
|
}
|
|
@@ -5683,7 +5632,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5683
5632
|
if (!prd || !shared.prdMatchesSourcePlan(prd.source_plan, meta.item.planFile)) {
|
|
5684
5633
|
throw new Error(`sidecar source_plan "${prd?.source_plan ?? '(unknown)'}" does not match "${meta.item.planFile}"`);
|
|
5685
5634
|
}
|
|
5686
|
-
prdStore.writePrd(
|
|
5635
|
+
prdStore.writePrd(expectedFile, prd);
|
|
5687
5636
|
fs.unlinkSync(resultPath);
|
|
5688
5637
|
} catch (e) {
|
|
5689
5638
|
log('warn', `plan-to-prd ${meta.item.id}: could not import result sidecar: ${e.message}`);
|
|
@@ -5707,14 +5656,14 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5707
5656
|
}
|
|
5708
5657
|
}
|
|
5709
5658
|
// Migrate _prdFilename when the source_plan scan found our PRD under a
|
|
5710
|
-
// different
|
|
5659
|
+
// different filename (e.g. agent bumped past a clobbered name, or
|
|
5711
5660
|
// a previous tick pinned a stale name). Future readers (artifacts, dashboard
|
|
5712
5661
|
// links) need to point at the actual file.
|
|
5713
5662
|
if (prdFound && resolvedPrdFilename && resolvedPrdFilename !== expectedFile) {
|
|
5714
|
-
const
|
|
5715
|
-
if (
|
|
5663
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
5664
|
+
if (wiScope) {
|
|
5716
5665
|
try {
|
|
5717
|
-
|
|
5666
|
+
mutateWorkItems(wiScope, data => {
|
|
5718
5667
|
if (!Array.isArray(data)) return data;
|
|
5719
5668
|
const w = data.find(i => i.id === meta.item.id);
|
|
5720
5669
|
if (!w) return data;
|
|
@@ -5723,7 +5672,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5723
5672
|
w._artifacts.prd = resolvedPrdFilename;
|
|
5724
5673
|
}
|
|
5725
5674
|
return data;
|
|
5726
|
-
}
|
|
5675
|
+
});
|
|
5727
5676
|
meta.item._prdFilename = resolvedPrdFilename;
|
|
5728
5677
|
log('info', `plan-to-prd ${meta.item.id}: migrated _prdFilename "${expectedFile}" → "${resolvedPrdFilename}" (matched by source_plan, likely collision)`);
|
|
5729
5678
|
} catch (err) { log('warn', `plan-to-prd _prdFilename migration: ${err.message}`); }
|
|
@@ -5731,10 +5680,10 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5731
5680
|
}
|
|
5732
5681
|
if (!prdFound) {
|
|
5733
5682
|
skipDoneStatus = true;
|
|
5734
|
-
const
|
|
5735
|
-
if (
|
|
5683
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
5684
|
+
if (wiScope) {
|
|
5736
5685
|
try {
|
|
5737
|
-
|
|
5686
|
+
mutateWorkItems(wiScope, data => {
|
|
5738
5687
|
if (!Array.isArray(data)) return data;
|
|
5739
5688
|
const w = data.find(i => i.id === meta.item.id);
|
|
5740
5689
|
if (!w) return data;
|
|
@@ -5764,7 +5713,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5764
5713
|
log('warn', `plan-to-prd ${meta.item.id} failed — no PRD result after ${ENGINE_DEFAULTS.maxRetries} retries`);
|
|
5765
5714
|
}
|
|
5766
5715
|
return data;
|
|
5767
|
-
}
|
|
5716
|
+
});
|
|
5768
5717
|
} catch (err) { log('warn', `plan-to-prd PRD check: ${err.message}`); }
|
|
5769
5718
|
}
|
|
5770
5719
|
}
|
|
@@ -5854,15 +5803,15 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5854
5803
|
// Failure retry is handled by completeDispatch in dispatch.js — not duplicated here.
|
|
5855
5804
|
// Only clear _decomposing flag on failure so decompose items don't get permanently stuck.
|
|
5856
5805
|
if (!effectiveSuccess && meta?.item?.id && type === WORK_TYPE.DECOMPOSE) {
|
|
5857
|
-
const
|
|
5858
|
-
if (
|
|
5806
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
5807
|
+
if (wiScope) {
|
|
5859
5808
|
try {
|
|
5860
|
-
|
|
5809
|
+
mutateWorkItems(wiScope, data => {
|
|
5861
5810
|
if (!Array.isArray(data)) return data;
|
|
5862
5811
|
const wi = data.find(i => i.id === meta.item.id);
|
|
5863
5812
|
if (wi) delete wi._decomposing;
|
|
5864
5813
|
return data;
|
|
5865
|
-
}
|
|
5814
|
+
});
|
|
5866
5815
|
} catch (err) { log('warn', `Decompose cleanup: ${err.message}`); }
|
|
5867
5816
|
}
|
|
5868
5817
|
}
|
|
@@ -5956,7 +5905,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5956
5905
|
if (qaSessionId && qaSessionPhase) {
|
|
5957
5906
|
try {
|
|
5958
5907
|
const qaSessions = require('./qa-sessions');
|
|
5959
|
-
const
|
|
5908
|
+
const scope = resolveWorkItemScope(meta);
|
|
5960
5909
|
const project = meta?.project?.name || meta?.item?.project || null;
|
|
5961
5910
|
const failureReason = typeof resultSummary === 'string' && resultSummary
|
|
5962
5911
|
? resultSummary
|
|
@@ -5964,7 +5913,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5964
5913
|
if (qaSessionPhase === 'setup') {
|
|
5965
5914
|
qaSessions.handleSetupComplete(qaSessionId, {
|
|
5966
5915
|
success: !!effectiveSuccess,
|
|
5967
|
-
|
|
5916
|
+
scope,
|
|
5968
5917
|
project,
|
|
5969
5918
|
failureClass: (structuredCompletion && structuredCompletion.failure_class) || null,
|
|
5970
5919
|
reason: failureReason,
|
|
@@ -6000,7 +5949,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6000
5949
|
qaSessions.handleDraftComplete(qaSessionId, {
|
|
6001
5950
|
success: true,
|
|
6002
5951
|
testFile,
|
|
6003
|
-
|
|
5952
|
+
scope,
|
|
6004
5953
|
project,
|
|
6005
5954
|
qaRunId,
|
|
6006
5955
|
reason: failureReason,
|
|
@@ -6078,13 +6027,12 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6078
6027
|
}
|
|
6079
6028
|
}
|
|
6080
6029
|
|
|
6081
|
-
// Scheduled task back-reference: update
|
|
6030
|
+
// Scheduled task back-reference: update SQL state and write a linked inbox note.
|
|
6082
6031
|
if (meta?.item?._scheduleId) {
|
|
6083
6032
|
try {
|
|
6084
6033
|
const scheduleId = meta.item._scheduleId;
|
|
6085
6034
|
const itemId = meta.item.id;
|
|
6086
|
-
|
|
6087
|
-
mutateJsonFileLocked(schedRunsPath, (runs) => {
|
|
6035
|
+
shared.mutateScheduleRuns((runs) => {
|
|
6088
6036
|
// P-c549d07e — preserve existing fields (esp. recentWorkItemIds ring
|
|
6089
6037
|
// populated at dispatch time by engine/scheduler.writeScheduleRunEntry).
|
|
6090
6038
|
// The literal-object replacement that lived here previously silently
|
|
@@ -6098,7 +6046,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6098
6046
|
lastCompletedAt: ts(),
|
|
6099
6047
|
};
|
|
6100
6048
|
return runs;
|
|
6101
|
-
}
|
|
6049
|
+
});
|
|
6102
6050
|
const status = effectiveSuccess ? 'succeeded' : 'failed';
|
|
6103
6051
|
if (effectiveSuccess) {
|
|
6104
6052
|
// Write a completion note to inbox with back-references only for successful scheduled runs.
|
|
@@ -6296,24 +6244,20 @@ function syncPrdFromPrs(config) {
|
|
|
6296
6244
|
const allProjects = shared.getProjects(config);
|
|
6297
6245
|
|
|
6298
6246
|
// Exact prdItems match only — no fuzzy matching
|
|
6299
|
-
const allPrs = allProjects.flatMap(p =>
|
|
6247
|
+
const allPrs = allProjects.flatMap(p => shared.readPullRequests(p));
|
|
6300
6248
|
|
|
6301
6249
|
let totalReconciled = 0;
|
|
6302
6250
|
for (const project of allProjects) {
|
|
6303
|
-
const
|
|
6304
|
-
const items = safeJsonArr(wiPath);
|
|
6251
|
+
const items = shared.readWorkItems(project);
|
|
6305
6252
|
const hasReconcilable = items.some(wi =>
|
|
6306
6253
|
(wi.status === WI_STATUS.PENDING && !wi._pr) || wi.status === WI_STATUS.FAILED);
|
|
6307
6254
|
if (!hasReconcilable) continue;
|
|
6308
6255
|
let reconciled = 0;
|
|
6309
|
-
|
|
6310
|
-
// ticks. Without the flag it'd unconditionally rewrite project work-items.json,
|
|
6311
|
-
// tripping the cli.js watcher into a tick storm even with zero reconciliation.
|
|
6312
|
-
const reconciledItems = mutateJsonFileLocked(wiPath, data => {
|
|
6256
|
+
const reconciledItems = mutateWorkItems(project, data => {
|
|
6313
6257
|
if (!Array.isArray(data)) return data;
|
|
6314
6258
|
reconciled = reconcileItemsWithPrs(data, allPrs);
|
|
6315
6259
|
return data;
|
|
6316
|
-
}
|
|
6260
|
+
});
|
|
6317
6261
|
if (reconciled > 0) {
|
|
6318
6262
|
// Sync done status to PRD JSON for each newly reconciled item
|
|
6319
6263
|
for (const wi of (reconciledItems || [])) {
|
|
@@ -6336,7 +6280,7 @@ function syncPrdFromPrs(config) {
|
|
|
6336
6280
|
let sourcePlan = pr.sourcePlan;
|
|
6337
6281
|
if (!sourcePlan) {
|
|
6338
6282
|
for (const project of allProjects) {
|
|
6339
|
-
const items =
|
|
6283
|
+
const items = shared.readWorkItems(project);
|
|
6340
6284
|
const wi = items.find(w => w.id === itemId);
|
|
6341
6285
|
if (wi?.sourcePlan) { sourcePlan = wi.sourcePlan; break; }
|
|
6342
6286
|
}
|
|
@@ -6377,15 +6321,14 @@ function advancePrdStatusOnAllItemsDone(config) {
|
|
|
6377
6321
|
// Collect all PRs by id across all projects for merged-status lookup
|
|
6378
6322
|
const prById = new Map();
|
|
6379
6323
|
for (const project of shared.getProjects(config)) {
|
|
6380
|
-
for (const pr of
|
|
6324
|
+
for (const pr of shared.readPullRequests(project)) {
|
|
6381
6325
|
if (pr?.id) prById.set(pr.id, pr);
|
|
6382
6326
|
}
|
|
6383
6327
|
}
|
|
6384
6328
|
|
|
6385
6329
|
for (const file of prdFiles) {
|
|
6386
6330
|
try {
|
|
6387
|
-
const
|
|
6388
|
-
const plan = safeJsonNoRestore(fpath);
|
|
6331
|
+
const plan = prdStore.readPrd(file);
|
|
6389
6332
|
if (!plan?.missing_features?.length) continue;
|
|
6390
6333
|
// Skip frozen PRDs — paused/rejected/awaiting-approval/completed/archived
|
|
6391
6334
|
if (_PRD_FROZEN_STATUSES.has(plan.status)) continue;
|
|
@@ -6405,13 +6348,13 @@ function advancePrdStatusOnAllItemsDone(config) {
|
|
|
6405
6348
|
|
|
6406
6349
|
if (!allDone) continue;
|
|
6407
6350
|
|
|
6408
|
-
|
|
6409
|
-
// Re-check
|
|
6351
|
+
prdStore.mutatePrd(file, (data) => {
|
|
6352
|
+
// Re-check transactionally — another writer may have frozen this PRD.
|
|
6410
6353
|
if (_PRD_FROZEN_STATUSES.has(data.status)) return data;
|
|
6411
6354
|
data.status = PLAN_STATUS.COMPLETED;
|
|
6412
6355
|
if (!data.completedAt) data.completedAt = ts();
|
|
6413
6356
|
return data;
|
|
6414
|
-
}
|
|
6357
|
+
});
|
|
6415
6358
|
log('info', `PRD auto-advance: ${file} → completed (all items done, all linked PRs merged)`);
|
|
6416
6359
|
} catch (err) {
|
|
6417
6360
|
log('warn', `advancePrdStatusOnAllItemsDone error for ${file}: ${err?.message || err}`);
|
|
@@ -6419,17 +6362,17 @@ function advancePrdStatusOnAllItemsDone(config) {
|
|
|
6419
6362
|
}
|
|
6420
6363
|
}
|
|
6421
6364
|
|
|
6422
|
-
// ─── Persist verify/E2E aggregate PRs onto the PRD
|
|
6365
|
+
// ─── Persist verify/E2E aggregate PRs onto the PRD (W-mqps9jlb) ───────────────
|
|
6423
6366
|
// The PRD view's "E2E Aggregate PRs" section (dashboard/js/render-prd.js#
|
|
6424
|
-
// renderE2eSection) sources its PRs from the
|
|
6367
|
+
// renderE2eSection) sources its PRs from the live tracker.
|
|
6425
6368
|
// A merged aggregate PR therefore VANISHES from the PRD page once its record is
|
|
6426
6369
|
// swept — by the daily-remove-merged-prs schedule, a manual delete, or a
|
|
6427
6370
|
// merge-back/reconcile flow. To make the PRD view survive removal of the live
|
|
6428
6371
|
// record we durably stamp a compact reference array (`prd.verifyPrs`) onto the
|
|
6429
|
-
// owning PRD
|
|
6372
|
+
// owning PRD.
|
|
6430
6373
|
//
|
|
6431
6374
|
// This runs per-tick (right after syncPrdFromPrs). It reads the NORMALIZED
|
|
6432
|
-
//
|
|
6375
|
+
// shared PR schema, so a single sweep covers both GitHub and ADO
|
|
6433
6376
|
// pollers at once (engine/github.js + engine/ado.js are intentional mirrors
|
|
6434
6377
|
// writing the same schema) — the durable single-source approach the task
|
|
6435
6378
|
// allows in lieu of hooking each poller's merged transition separately. Because
|
|
@@ -6488,7 +6431,7 @@ function persistVerifyPrsToPrd(config) {
|
|
|
6488
6431
|
const refsByPlan = new Map(); // planFile -> Map(id -> ref)
|
|
6489
6432
|
for (const project of projects) {
|
|
6490
6433
|
let prs;
|
|
6491
|
-
try { prs =
|
|
6434
|
+
try { prs = shared.readPullRequests(project); } catch { continue; }
|
|
6492
6435
|
for (const pr of (prs || [])) {
|
|
6493
6436
|
if (!pr || !pr.id) continue;
|
|
6494
6437
|
if (!_isVerifyAggregatePr(pr)) continue;
|
|
@@ -6503,20 +6446,19 @@ function persistVerifyPrsToPrd(config) {
|
|
|
6503
6446
|
if (refsByPlan.size === 0) return;
|
|
6504
6447
|
|
|
6505
6448
|
for (const [planFile, refMap] of refsByPlan) {
|
|
6506
|
-
|
|
6507
|
-
const candidates = [path.join(PRD_DIR, planFile), path.join(PRD_DIR, 'archive', planFile)];
|
|
6508
|
-
const fpath = candidates.find(p => safeJsonNoRestore(p));
|
|
6509
|
-
if (!fpath) continue;
|
|
6510
|
-
|
|
6511
|
-
// Lock-free peek (mirrors syncPrdItemStatus): only take the write lock
|
|
6512
|
-
// when the persisted refs would actually change. safeJsonNoRestore so an
|
|
6513
|
-
// archived PRD's .backup sidecar can't resurrect stale state.
|
|
6449
|
+
let archived = false;
|
|
6514
6450
|
let plan;
|
|
6515
|
-
try {
|
|
6451
|
+
try {
|
|
6452
|
+
plan = prdStore.readPrd(planFile);
|
|
6453
|
+
if (!plan) {
|
|
6454
|
+
archived = true;
|
|
6455
|
+
plan = prdStore.readPrd(planFile, { archived: true });
|
|
6456
|
+
}
|
|
6457
|
+
} catch { continue; }
|
|
6516
6458
|
if (!plan || typeof plan !== 'object') continue;
|
|
6517
6459
|
if (!_verifyPrsNeedUpdate(plan.verifyPrs, refMap)) continue;
|
|
6518
6460
|
|
|
6519
|
-
|
|
6461
|
+
prdStore.mutatePrd(planFile, (fresh) => {
|
|
6520
6462
|
if (!fresh || typeof fresh !== 'object') return fresh;
|
|
6521
6463
|
const byId = new Map();
|
|
6522
6464
|
for (const e of (Array.isArray(fresh.verifyPrs) ? fresh.verifyPrs : [])) {
|
|
@@ -6528,7 +6470,7 @@ function persistVerifyPrsToPrd(config) {
|
|
|
6528
6470
|
}
|
|
6529
6471
|
fresh.verifyPrs = [...byId.values()];
|
|
6530
6472
|
return fresh;
|
|
6531
|
-
}, {
|
|
6473
|
+
}, { archived });
|
|
6532
6474
|
}
|
|
6533
6475
|
} catch (err) {
|
|
6534
6476
|
try { log('warn', `persistVerifyPrsToPrd error: ${err?.message || err}`); } catch { /* engine not available */ }
|
|
@@ -6759,14 +6701,13 @@ function pruneScopeMismatchDuplicatePrs(config) {
|
|
|
6759
6701
|
for (const [targetProjectName, entries] of relocationsByTargetProject) {
|
|
6760
6702
|
const targetProject = projects.find(p => p.name === targetProjectName);
|
|
6761
6703
|
if (!targetProject) continue;
|
|
6762
|
-
const targetPrPath = projectPrPath(targetProject);
|
|
6763
6704
|
for (const { rec, owningScope } of entries) {
|
|
6764
6705
|
const entry = { ...rec };
|
|
6765
6706
|
delete entry._scope;
|
|
6766
6707
|
delete entry._invalidProjectScope;
|
|
6767
6708
|
delete entry.project; // let upsertPullRequestRecord backfill from targetProject
|
|
6768
6709
|
try {
|
|
6769
|
-
const result = shared.upsertPullRequestRecord(
|
|
6710
|
+
const result = shared.upsertPullRequestRecord(targetProject, entry, {
|
|
6770
6711
|
project: targetProject,
|
|
6771
6712
|
itemIds: Array.isArray(rec.prdItems) ? rec.prdItems : null,
|
|
6772
6713
|
});
|
|
@@ -6797,10 +6738,9 @@ function pruneScopeMismatchDuplicatePrs(config) {
|
|
|
6797
6738
|
for (const [projectName, idsToDelete] of deletesByProject) {
|
|
6798
6739
|
const project = projects.find(p => p.name === projectName);
|
|
6799
6740
|
if (!project) continue;
|
|
6800
|
-
const prPath = projectPrPath(project);
|
|
6801
6741
|
const pruneIds = pruneDeletesByProject.get(projectName) || new Set();
|
|
6802
6742
|
try {
|
|
6803
|
-
shared.mutatePullRequests(
|
|
6743
|
+
shared.mutatePullRequests(project, (prs) => {
|
|
6804
6744
|
const deletedIds = prs.filter(p => idsToDelete.has(p?.id)).map(p => p.id);
|
|
6805
6745
|
const next = prs.filter(p => !idsToDelete.has(p?.id));
|
|
6806
6746
|
for (const id of deletedIds) {
|
|
@@ -6831,9 +6771,8 @@ function collapseAllDuplicatePrRecords(config) {
|
|
|
6831
6771
|
|
|
6832
6772
|
for (const project of projects) {
|
|
6833
6773
|
if (!project || !project.name) continue;
|
|
6834
|
-
const prPath = projectPrPath(project);
|
|
6835
6774
|
try {
|
|
6836
|
-
const result = shared.collapseDuplicatePrRecords(
|
|
6775
|
+
const result = shared.collapseDuplicatePrRecords(project, { project });
|
|
6837
6776
|
collapsed += result.collapsed || 0;
|
|
6838
6777
|
scanned++;
|
|
6839
6778
|
} catch (err) {
|
|
@@ -6841,14 +6780,13 @@ function collapseAllDuplicatePrRecords(config) {
|
|
|
6841
6780
|
}
|
|
6842
6781
|
}
|
|
6843
6782
|
|
|
6844
|
-
// Central
|
|
6845
|
-
const centralPath = path.join(MINIONS_DIR, 'pull-requests.json');
|
|
6783
|
+
// Central scope has no project context; normalization uses URL-only scope.
|
|
6846
6784
|
try {
|
|
6847
|
-
const result = shared.collapseDuplicatePrRecords(
|
|
6785
|
+
const result = shared.collapseDuplicatePrRecords('central', {});
|
|
6848
6786
|
collapsed += result.collapsed || 0;
|
|
6849
6787
|
scanned++;
|
|
6850
6788
|
} catch (err) {
|
|
6851
|
-
log('warn', `collapseAllDuplicatePrRecords: failed for central
|
|
6789
|
+
log('warn', `collapseAllDuplicatePrRecords: failed for central scope: ${err?.message || err}`);
|
|
6852
6790
|
}
|
|
6853
6791
|
|
|
6854
6792
|
if (collapsed > 0) {
|
|
@@ -6981,11 +6919,11 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
6981
6919
|
if (!prRef) return;
|
|
6982
6920
|
|
|
6983
6921
|
const codingWiId = item.id;
|
|
6984
|
-
const
|
|
6985
|
-
if (!
|
|
6922
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
6923
|
+
if (!wiScope) return;
|
|
6986
6924
|
|
|
6987
6925
|
try {
|
|
6988
|
-
mutateWorkItems(
|
|
6926
|
+
mutateWorkItems(wiScope, items => {
|
|
6989
6927
|
if (!Array.isArray(items)) return items;
|
|
6990
6928
|
|
|
6991
6929
|
// Dedup: only one validation WI in flight per coding WI. Any non-terminal
|
|
@@ -7083,7 +7021,7 @@ module.exports = {
|
|
|
7083
7021
|
syncPrdFromPrs,
|
|
7084
7022
|
advancePrdStatusOnAllItemsDone,
|
|
7085
7023
|
persistVerifyPrsToPrd,
|
|
7086
|
-
|
|
7024
|
+
resolveWorkItemScope,
|
|
7087
7025
|
isItemCompleted,
|
|
7088
7026
|
classifyFailure,
|
|
7089
7027
|
diagnoseEmptyOutput,
|