@yemi33/minions 0.1.2379 → 0.1.2381
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 +3 -4
- package/dashboard/js/render-other.js +43 -23
- package/dashboard/js/render-prd.js +1 -1
- package/dashboard/js/render-work-items.js +13 -6
- package/dashboard/js/settings.js +40 -17
- package/dashboard.js +435 -768
- 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-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/live-checkout-mode.md +45 -26
- 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/ado-status.js +5 -5
- package/engine/ado.js +20 -25
- package/engine/agent-worker-pool.js +58 -1
- 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 +39 -44
- package/engine/github.js +20 -27
- package/engine/lifecycle.js +279 -354
- package/engine/live-checkout.js +193 -149
- 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 +13 -16
- 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 +4 -6
- package/engine/scheduler.js +0 -4
- package/engine/shared-branch-pr-reconcile.js +2 -3
- package/engine/shared.js +268 -699
- 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 +197 -358
- 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 +9 -9
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;
|
|
@@ -1589,7 +1559,7 @@ function isPrAttachmentRequired(type, item, meta = {}) {
|
|
|
1589
1559
|
// PR — the agent updates meta.pr in place. The meta.pr short-circuit beats
|
|
1590
1560
|
// the explicit-flag fallthrough so a legacy requiresPr:true fix doesn't
|
|
1591
1561
|
// trigger the contract when there's already a PR attached.
|
|
1592
|
-
if ((type
|
|
1562
|
+
if ((shared.isFixLikeWorkType(type) || type === WORK_TYPE.TEST) && meta?.pr) return false;
|
|
1593
1563
|
|
|
1594
1564
|
// Standalone test work is usually pure build/run/verify. It should only be
|
|
1595
1565
|
// PR-required when the caller explicitly marks it as file-changing work.
|
|
@@ -1598,7 +1568,7 @@ function isPrAttachmentRequired(type, item, meta = {}) {
|
|
|
1598
1568
|
return explicit
|
|
1599
1569
|
|| type === WORK_TYPE.IMPLEMENT
|
|
1600
1570
|
|| type === WORK_TYPE.IMPLEMENT_LARGE
|
|
1601
|
-
|| type
|
|
1571
|
+
|| shared.isFixLikeWorkType(type);
|
|
1602
1572
|
}
|
|
1603
1573
|
|
|
1604
1574
|
function readOptionalJsonStrict(filePath, label, validate) {
|
|
@@ -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 {
|
|
@@ -5545,7 +5494,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5545
5494
|
// Auto-recover: if a failed implement/fix/test agent created PRs, it likely succeeded before the failure surfaced.
|
|
5546
5495
|
// P-d2a8f6c1: skip auto-recovery for nonce-mismatched dispatches — a forged
|
|
5547
5496
|
// report shouldn't be able to ride PRs into a "done" status.
|
|
5548
|
-
const prCreatingType = type === WORK_TYPE.IMPLEMENT || type === WORK_TYPE.IMPLEMENT_LARGE || type
|
|
5497
|
+
const prCreatingType = type === WORK_TYPE.IMPLEMENT || type === WORK_TYPE.IMPLEMENT_LARGE || shared.isFixLikeWorkType(type) || type === WORK_TYPE.TEST;
|
|
5549
5498
|
const autoRecovered = !nonceMismatch && !agentReportedFailure && !isSuccess && prsCreatedCount > 0 && prCreatingType && !!meta?.item?.id;
|
|
5550
5499
|
if (autoRecovered) {
|
|
5551
5500
|
log('info', `Auto-recovery: agent failed but created ${prsCreatedCount} PR(s) — upgrading ${meta.item.id} to done`);
|
|
@@ -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
|
}
|
|
@@ -5843,26 +5792,28 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5843
5792
|
try { stampPrdItemWorkItemId(meta.item.id, meta.item.sourcePlan); } catch (err) { log('warn', `stampPrdItemWorkItemId: ${err.message}`); }
|
|
5844
5793
|
}
|
|
5845
5794
|
promoteCompletionArtifacts(meta, agentId, dispatchItem.id, structuredCompletion, { resultSummary });
|
|
5846
|
-
// M003 — auto-dispatch a live-validation WI when a coding WI completes.
|
|
5847
|
-
try { autoDispatchLiveValidationWi(meta, config); } catch (err) { log('warn', `autoDispatchLiveValidationWi: ${err.message}`); }
|
|
5848
5795
|
// P-mqyp0009y025z6a7 — goal invalidation: cancel any pending/queued WIs
|
|
5849
5796
|
// listed in the completion report's optional `invalidates[]` field.
|
|
5850
5797
|
if (structuredCompletion && Array.isArray(structuredCompletion.invalidates) && structuredCompletion.invalidates.length > 0) {
|
|
5851
5798
|
try { applyGoalInvalidation(meta.item.id, structuredCompletion.invalidates, config); } catch (err) { log('warn', `applyGoalInvalidation: ${err.message}`); }
|
|
5852
5799
|
}
|
|
5853
5800
|
}
|
|
5801
|
+
if (effectiveSuccess && !skipDoneStatus) {
|
|
5802
|
+
// Coding WIs and direct PR-sourced fix dispatches share this follow-up path.
|
|
5803
|
+
try { autoDispatchLiveValidationWi(meta, config, dispatchItem); } catch (err) { log('warn', `autoDispatchLiveValidationWi: ${err.message}`); }
|
|
5804
|
+
}
|
|
5854
5805
|
// Failure retry is handled by completeDispatch in dispatch.js — not duplicated here.
|
|
5855
5806
|
// Only clear _decomposing flag on failure so decompose items don't get permanently stuck.
|
|
5856
5807
|
if (!effectiveSuccess && meta?.item?.id && type === WORK_TYPE.DECOMPOSE) {
|
|
5857
|
-
const
|
|
5858
|
-
if (
|
|
5808
|
+
const wiScope = resolveWorkItemScope(meta);
|
|
5809
|
+
if (wiScope) {
|
|
5859
5810
|
try {
|
|
5860
|
-
|
|
5811
|
+
mutateWorkItems(wiScope, data => {
|
|
5861
5812
|
if (!Array.isArray(data)) return data;
|
|
5862
5813
|
const wi = data.find(i => i.id === meta.item.id);
|
|
5863
5814
|
if (wi) delete wi._decomposing;
|
|
5864
5815
|
return data;
|
|
5865
|
-
}
|
|
5816
|
+
});
|
|
5866
5817
|
} catch (err) { log('warn', `Decompose cleanup: ${err.message}`); }
|
|
5867
5818
|
}
|
|
5868
5819
|
}
|
|
@@ -5956,7 +5907,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5956
5907
|
if (qaSessionId && qaSessionPhase) {
|
|
5957
5908
|
try {
|
|
5958
5909
|
const qaSessions = require('./qa-sessions');
|
|
5959
|
-
const
|
|
5910
|
+
const scope = resolveWorkItemScope(meta);
|
|
5960
5911
|
const project = meta?.project?.name || meta?.item?.project || null;
|
|
5961
5912
|
const failureReason = typeof resultSummary === 'string' && resultSummary
|
|
5962
5913
|
? resultSummary
|
|
@@ -5964,7 +5915,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
5964
5915
|
if (qaSessionPhase === 'setup') {
|
|
5965
5916
|
qaSessions.handleSetupComplete(qaSessionId, {
|
|
5966
5917
|
success: !!effectiveSuccess,
|
|
5967
|
-
|
|
5918
|
+
scope,
|
|
5968
5919
|
project,
|
|
5969
5920
|
failureClass: (structuredCompletion && structuredCompletion.failure_class) || null,
|
|
5970
5921
|
reason: failureReason,
|
|
@@ -6000,7 +5951,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6000
5951
|
qaSessions.handleDraftComplete(qaSessionId, {
|
|
6001
5952
|
success: true,
|
|
6002
5953
|
testFile,
|
|
6003
|
-
|
|
5954
|
+
scope,
|
|
6004
5955
|
project,
|
|
6005
5956
|
qaRunId,
|
|
6006
5957
|
reason: failureReason,
|
|
@@ -6045,7 +5996,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6045
5996
|
|
|
6046
5997
|
let prFixBranchChange = null;
|
|
6047
5998
|
let prFixBuildStillFailing = null;
|
|
6048
|
-
if (type
|
|
5999
|
+
if (shared.isFixLikeWorkType(type) && effectiveSuccess && meta?.pr?.id) {
|
|
6049
6000
|
try {
|
|
6050
6001
|
prFixBranchChange = await detectPrFixBranchChange(meta, config);
|
|
6051
6002
|
} catch (err) {
|
|
@@ -6078,13 +6029,12 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6078
6029
|
}
|
|
6079
6030
|
}
|
|
6080
6031
|
|
|
6081
|
-
// Scheduled task back-reference: update
|
|
6032
|
+
// Scheduled task back-reference: update SQL state and write a linked inbox note.
|
|
6082
6033
|
if (meta?.item?._scheduleId) {
|
|
6083
6034
|
try {
|
|
6084
6035
|
const scheduleId = meta.item._scheduleId;
|
|
6085
6036
|
const itemId = meta.item.id;
|
|
6086
|
-
|
|
6087
|
-
mutateJsonFileLocked(schedRunsPath, (runs) => {
|
|
6037
|
+
shared.mutateScheduleRuns((runs) => {
|
|
6088
6038
|
// P-c549d07e — preserve existing fields (esp. recentWorkItemIds ring
|
|
6089
6039
|
// populated at dispatch time by engine/scheduler.writeScheduleRunEntry).
|
|
6090
6040
|
// The literal-object replacement that lived here previously silently
|
|
@@ -6098,7 +6048,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6098
6048
|
lastCompletedAt: ts(),
|
|
6099
6049
|
};
|
|
6100
6050
|
return runs;
|
|
6101
|
-
}
|
|
6051
|
+
});
|
|
6102
6052
|
const status = effectiveSuccess ? 'succeeded' : 'failed';
|
|
6103
6053
|
if (effectiveSuccess) {
|
|
6104
6054
|
// Write a completion note to inbox with back-references only for successful scheduled runs.
|
|
@@ -6188,7 +6138,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6188
6138
|
} else if (type === WORK_TYPE.REVIEW) {
|
|
6189
6139
|
log('warn', `Skipping PR review metadata update for ${meta?.pr?.id || meta?.pr?.url || '(unknown PR)'} because review dispatch ${dispatchItem.id} did not complete cleanly`);
|
|
6190
6140
|
}
|
|
6191
|
-
if (type
|
|
6141
|
+
if (shared.isFixLikeWorkType(type) && effectiveSuccess) {
|
|
6192
6142
|
updatePrAfterFix(meta?.pr, meta?.project, meta?.source, {
|
|
6193
6143
|
branchChanged: fixCompletionChangedBranch(structuredCompletion),
|
|
6194
6144
|
automationCauseKey: meta?.automationCauseKey,
|
|
@@ -6225,7 +6175,7 @@ async function runPostCompletionHooks(dispatchItem, agentId, code, stdout, confi
|
|
|
6225
6175
|
}
|
|
6226
6176
|
} catch (err) { log('warn', `PRD sync after fix: ${err.message}`); }
|
|
6227
6177
|
}
|
|
6228
|
-
} else if (type
|
|
6178
|
+
} else if (shared.isFixLikeWorkType(type) && meta?.pr?.id) {
|
|
6229
6179
|
// W-mph6br6a0006a2b9 (F9): the fix dispatch did not complete cleanly
|
|
6230
6180
|
// (agent crash, non-zero exit, hard contract failure, or nonce mismatch),
|
|
6231
6181
|
// so updatePrAfterFix above was skipped. Without a write here the PR's
|
|
@@ -6296,24 +6246,20 @@ function syncPrdFromPrs(config) {
|
|
|
6296
6246
|
const allProjects = shared.getProjects(config);
|
|
6297
6247
|
|
|
6298
6248
|
// Exact prdItems match only — no fuzzy matching
|
|
6299
|
-
const allPrs = allProjects.flatMap(p =>
|
|
6249
|
+
const allPrs = allProjects.flatMap(p => shared.readPullRequests(p));
|
|
6300
6250
|
|
|
6301
6251
|
let totalReconciled = 0;
|
|
6302
6252
|
for (const project of allProjects) {
|
|
6303
|
-
const
|
|
6304
|
-
const items = safeJsonArr(wiPath);
|
|
6253
|
+
const items = shared.readWorkItems(project);
|
|
6305
6254
|
const hasReconcilable = items.some(wi =>
|
|
6306
6255
|
(wi.status === WI_STATUS.PENDING && !wi._pr) || wi.status === WI_STATUS.FAILED);
|
|
6307
6256
|
if (!hasReconcilable) continue;
|
|
6308
6257
|
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 => {
|
|
6258
|
+
const reconciledItems = mutateWorkItems(project, data => {
|
|
6313
6259
|
if (!Array.isArray(data)) return data;
|
|
6314
6260
|
reconciled = reconcileItemsWithPrs(data, allPrs);
|
|
6315
6261
|
return data;
|
|
6316
|
-
}
|
|
6262
|
+
});
|
|
6317
6263
|
if (reconciled > 0) {
|
|
6318
6264
|
// Sync done status to PRD JSON for each newly reconciled item
|
|
6319
6265
|
for (const wi of (reconciledItems || [])) {
|
|
@@ -6336,7 +6282,7 @@ function syncPrdFromPrs(config) {
|
|
|
6336
6282
|
let sourcePlan = pr.sourcePlan;
|
|
6337
6283
|
if (!sourcePlan) {
|
|
6338
6284
|
for (const project of allProjects) {
|
|
6339
|
-
const items =
|
|
6285
|
+
const items = shared.readWorkItems(project);
|
|
6340
6286
|
const wi = items.find(w => w.id === itemId);
|
|
6341
6287
|
if (wi?.sourcePlan) { sourcePlan = wi.sourcePlan; break; }
|
|
6342
6288
|
}
|
|
@@ -6377,15 +6323,14 @@ function advancePrdStatusOnAllItemsDone(config) {
|
|
|
6377
6323
|
// Collect all PRs by id across all projects for merged-status lookup
|
|
6378
6324
|
const prById = new Map();
|
|
6379
6325
|
for (const project of shared.getProjects(config)) {
|
|
6380
|
-
for (const pr of
|
|
6326
|
+
for (const pr of shared.readPullRequests(project)) {
|
|
6381
6327
|
if (pr?.id) prById.set(pr.id, pr);
|
|
6382
6328
|
}
|
|
6383
6329
|
}
|
|
6384
6330
|
|
|
6385
6331
|
for (const file of prdFiles) {
|
|
6386
6332
|
try {
|
|
6387
|
-
const
|
|
6388
|
-
const plan = safeJsonNoRestore(fpath);
|
|
6333
|
+
const plan = prdStore.readPrd(file);
|
|
6389
6334
|
if (!plan?.missing_features?.length) continue;
|
|
6390
6335
|
// Skip frozen PRDs — paused/rejected/awaiting-approval/completed/archived
|
|
6391
6336
|
if (_PRD_FROZEN_STATUSES.has(plan.status)) continue;
|
|
@@ -6405,13 +6350,13 @@ function advancePrdStatusOnAllItemsDone(config) {
|
|
|
6405
6350
|
|
|
6406
6351
|
if (!allDone) continue;
|
|
6407
6352
|
|
|
6408
|
-
|
|
6409
|
-
// Re-check
|
|
6353
|
+
prdStore.mutatePrd(file, (data) => {
|
|
6354
|
+
// Re-check transactionally — another writer may have frozen this PRD.
|
|
6410
6355
|
if (_PRD_FROZEN_STATUSES.has(data.status)) return data;
|
|
6411
6356
|
data.status = PLAN_STATUS.COMPLETED;
|
|
6412
6357
|
if (!data.completedAt) data.completedAt = ts();
|
|
6413
6358
|
return data;
|
|
6414
|
-
}
|
|
6359
|
+
});
|
|
6415
6360
|
log('info', `PRD auto-advance: ${file} → completed (all items done, all linked PRs merged)`);
|
|
6416
6361
|
} catch (err) {
|
|
6417
6362
|
log('warn', `advancePrdStatusOnAllItemsDone error for ${file}: ${err?.message || err}`);
|
|
@@ -6419,17 +6364,17 @@ function advancePrdStatusOnAllItemsDone(config) {
|
|
|
6419
6364
|
}
|
|
6420
6365
|
}
|
|
6421
6366
|
|
|
6422
|
-
// ─── Persist verify/E2E aggregate PRs onto the PRD
|
|
6367
|
+
// ─── Persist verify/E2E aggregate PRs onto the PRD (W-mqps9jlb) ───────────────
|
|
6423
6368
|
// The PRD view's "E2E Aggregate PRs" section (dashboard/js/render-prd.js#
|
|
6424
|
-
// renderE2eSection) sources its PRs from the
|
|
6369
|
+
// renderE2eSection) sources its PRs from the live tracker.
|
|
6425
6370
|
// A merged aggregate PR therefore VANISHES from the PRD page once its record is
|
|
6426
6371
|
// swept — by the daily-remove-merged-prs schedule, a manual delete, or a
|
|
6427
6372
|
// merge-back/reconcile flow. To make the PRD view survive removal of the live
|
|
6428
6373
|
// record we durably stamp a compact reference array (`prd.verifyPrs`) onto the
|
|
6429
|
-
// owning PRD
|
|
6374
|
+
// owning PRD.
|
|
6430
6375
|
//
|
|
6431
6376
|
// This runs per-tick (right after syncPrdFromPrs). It reads the NORMALIZED
|
|
6432
|
-
//
|
|
6377
|
+
// shared PR schema, so a single sweep covers both GitHub and ADO
|
|
6433
6378
|
// pollers at once (engine/github.js + engine/ado.js are intentional mirrors
|
|
6434
6379
|
// writing the same schema) — the durable single-source approach the task
|
|
6435
6380
|
// allows in lieu of hooking each poller's merged transition separately. Because
|
|
@@ -6488,7 +6433,7 @@ function persistVerifyPrsToPrd(config) {
|
|
|
6488
6433
|
const refsByPlan = new Map(); // planFile -> Map(id -> ref)
|
|
6489
6434
|
for (const project of projects) {
|
|
6490
6435
|
let prs;
|
|
6491
|
-
try { prs =
|
|
6436
|
+
try { prs = shared.readPullRequests(project); } catch { continue; }
|
|
6492
6437
|
for (const pr of (prs || [])) {
|
|
6493
6438
|
if (!pr || !pr.id) continue;
|
|
6494
6439
|
if (!_isVerifyAggregatePr(pr)) continue;
|
|
@@ -6503,20 +6448,19 @@ function persistVerifyPrsToPrd(config) {
|
|
|
6503
6448
|
if (refsByPlan.size === 0) return;
|
|
6504
6449
|
|
|
6505
6450
|
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.
|
|
6451
|
+
let archived = false;
|
|
6514
6452
|
let plan;
|
|
6515
|
-
try {
|
|
6453
|
+
try {
|
|
6454
|
+
plan = prdStore.readPrd(planFile);
|
|
6455
|
+
if (!plan) {
|
|
6456
|
+
archived = true;
|
|
6457
|
+
plan = prdStore.readPrd(planFile, { archived: true });
|
|
6458
|
+
}
|
|
6459
|
+
} catch { continue; }
|
|
6516
6460
|
if (!plan || typeof plan !== 'object') continue;
|
|
6517
6461
|
if (!_verifyPrsNeedUpdate(plan.verifyPrs, refMap)) continue;
|
|
6518
6462
|
|
|
6519
|
-
|
|
6463
|
+
prdStore.mutatePrd(planFile, (fresh) => {
|
|
6520
6464
|
if (!fresh || typeof fresh !== 'object') return fresh;
|
|
6521
6465
|
const byId = new Map();
|
|
6522
6466
|
for (const e of (Array.isArray(fresh.verifyPrs) ? fresh.verifyPrs : [])) {
|
|
@@ -6528,7 +6472,7 @@ function persistVerifyPrsToPrd(config) {
|
|
|
6528
6472
|
}
|
|
6529
6473
|
fresh.verifyPrs = [...byId.values()];
|
|
6530
6474
|
return fresh;
|
|
6531
|
-
}, {
|
|
6475
|
+
}, { archived });
|
|
6532
6476
|
}
|
|
6533
6477
|
} catch (err) {
|
|
6534
6478
|
try { log('warn', `persistVerifyPrsToPrd error: ${err?.message || err}`); } catch { /* engine not available */ }
|
|
@@ -6759,14 +6703,13 @@ function pruneScopeMismatchDuplicatePrs(config) {
|
|
|
6759
6703
|
for (const [targetProjectName, entries] of relocationsByTargetProject) {
|
|
6760
6704
|
const targetProject = projects.find(p => p.name === targetProjectName);
|
|
6761
6705
|
if (!targetProject) continue;
|
|
6762
|
-
const targetPrPath = projectPrPath(targetProject);
|
|
6763
6706
|
for (const { rec, owningScope } of entries) {
|
|
6764
6707
|
const entry = { ...rec };
|
|
6765
6708
|
delete entry._scope;
|
|
6766
6709
|
delete entry._invalidProjectScope;
|
|
6767
6710
|
delete entry.project; // let upsertPullRequestRecord backfill from targetProject
|
|
6768
6711
|
try {
|
|
6769
|
-
const result = shared.upsertPullRequestRecord(
|
|
6712
|
+
const result = shared.upsertPullRequestRecord(targetProject, entry, {
|
|
6770
6713
|
project: targetProject,
|
|
6771
6714
|
itemIds: Array.isArray(rec.prdItems) ? rec.prdItems : null,
|
|
6772
6715
|
});
|
|
@@ -6797,10 +6740,9 @@ function pruneScopeMismatchDuplicatePrs(config) {
|
|
|
6797
6740
|
for (const [projectName, idsToDelete] of deletesByProject) {
|
|
6798
6741
|
const project = projects.find(p => p.name === projectName);
|
|
6799
6742
|
if (!project) continue;
|
|
6800
|
-
const prPath = projectPrPath(project);
|
|
6801
6743
|
const pruneIds = pruneDeletesByProject.get(projectName) || new Set();
|
|
6802
6744
|
try {
|
|
6803
|
-
shared.mutatePullRequests(
|
|
6745
|
+
shared.mutatePullRequests(project, (prs) => {
|
|
6804
6746
|
const deletedIds = prs.filter(p => idsToDelete.has(p?.id)).map(p => p.id);
|
|
6805
6747
|
const next = prs.filter(p => !idsToDelete.has(p?.id));
|
|
6806
6748
|
for (const id of deletedIds) {
|
|
@@ -6831,9 +6773,8 @@ function collapseAllDuplicatePrRecords(config) {
|
|
|
6831
6773
|
|
|
6832
6774
|
for (const project of projects) {
|
|
6833
6775
|
if (!project || !project.name) continue;
|
|
6834
|
-
const prPath = projectPrPath(project);
|
|
6835
6776
|
try {
|
|
6836
|
-
const result = shared.collapseDuplicatePrRecords(
|
|
6777
|
+
const result = shared.collapseDuplicatePrRecords(project, { project });
|
|
6837
6778
|
collapsed += result.collapsed || 0;
|
|
6838
6779
|
scanned++;
|
|
6839
6780
|
} catch (err) {
|
|
@@ -6841,14 +6782,13 @@ function collapseAllDuplicatePrRecords(config) {
|
|
|
6841
6782
|
}
|
|
6842
6783
|
}
|
|
6843
6784
|
|
|
6844
|
-
// Central
|
|
6845
|
-
const centralPath = path.join(MINIONS_DIR, 'pull-requests.json');
|
|
6785
|
+
// Central scope has no project context; normalization uses URL-only scope.
|
|
6846
6786
|
try {
|
|
6847
|
-
const result = shared.collapseDuplicatePrRecords(
|
|
6787
|
+
const result = shared.collapseDuplicatePrRecords('central', {});
|
|
6848
6788
|
collapsed += result.collapsed || 0;
|
|
6849
6789
|
scanned++;
|
|
6850
6790
|
} catch (err) {
|
|
6851
|
-
log('warn', `collapseAllDuplicatePrRecords: failed for central
|
|
6791
|
+
log('warn', `collapseAllDuplicatePrRecords: failed for central scope: ${err?.message || err}`);
|
|
6852
6792
|
}
|
|
6853
6793
|
|
|
6854
6794
|
if (collapsed > 0) {
|
|
@@ -6862,37 +6802,9 @@ function collapseAllDuplicatePrRecords(config) {
|
|
|
6862
6802
|
// a coding WI (not one of the validation types itself). Skips if the coding WI
|
|
6863
6803
|
// has no PR.
|
|
6864
6804
|
//
|
|
6865
|
-
//
|
|
6866
|
-
//
|
|
6867
|
-
//
|
|
6868
|
-
// explore, ask, docs, decompose, ...) is excluded even when it resolves a PR
|
|
6869
|
-
// ref and autoDispatch is on, because those completions don't author a code
|
|
6870
|
-
// diff worth re-validating — e.g. a `review` dispatch that merely comments on
|
|
6871
|
-
// someone else's PR previously spawned a spurious "Validate: Review opg PR
|
|
6872
|
-
// #792 ..." follow-up. See the ELIGIBLE_TYPES check below.
|
|
6873
|
-
//
|
|
6874
|
-
// Files exactly ONE validation WI per coding-WI completion (W-mrhybval0001).
|
|
6875
|
-
// `liveValidation.type` still governs CHECKOUT ROUTING — the set of work-item
|
|
6876
|
-
// types that run in-place on the live checkout (see resolveCheckoutMode's
|
|
6877
|
-
// membership check) — but auto-validation no longer fans that array out into
|
|
6878
|
-
// one validation WI per type. That earlier behavior (W-mr2m1ute000a9c01)
|
|
6879
|
-
// conflated "which types run live" with "how many validation tasks to file";
|
|
6880
|
-
// a multi-select routing config would silently spawn N validation WIs after
|
|
6881
|
-
// every coding completion. Validation is a single downstream step, so we
|
|
6882
|
-
// collapse to one WI.
|
|
6883
|
-
//
|
|
6884
|
-
// liveValidation.type may be a single string (legacy / back-compat) or an
|
|
6885
|
-
// array of strings (multi-select hybrid types). We choose ONE validation type
|
|
6886
|
-
// for the single WI. It MUST be a member of lvTypes, otherwise resolveCheckoutMode
|
|
6887
|
-
// would route the validation WI to a worktree instead of the live checkout
|
|
6888
|
-
// (defeating hybrid validation). Preference order: the canonical `test` type
|
|
6889
|
-
// when configured, else the first configured live type. The completing item's
|
|
6890
|
-
// own type is excluded so a validation-type WI can't recursively validate
|
|
6891
|
-
// itself (anti-loop).
|
|
6892
|
-
//
|
|
6893
|
-
// Only these item.type values ever trigger this hook (see ELIGIBLE_TYPES
|
|
6894
|
-
// below) — real code-authoring completions, not review/test/verify/setup/
|
|
6895
|
-
// explore/ask/docs/decompose completions.
|
|
6805
|
+
// Only real code-authoring completions trigger the hook. The follow-up is
|
|
6806
|
+
// always a canonical `test` WI with the PR-focused `build-and-test` playbook;
|
|
6807
|
+
// liveValidation.type controls routing, not the semantic type of the task.
|
|
6896
6808
|
//
|
|
6897
6809
|
// Deduplicates per codingWiId: any non-terminal WI with
|
|
6898
6810
|
// meta.liveValidationFor === codingWiId blocks a second dispatch, so only one
|
|
@@ -6914,22 +6826,30 @@ function collapseAllDuplicatePrRecords(config) {
|
|
|
6914
6826
|
// verify, setup, explore, ask, docs, decompose, ...) is excluded even when it
|
|
6915
6827
|
// resolves a PR ref, because those completions don't author a code diff
|
|
6916
6828
|
// worth re-validating.
|
|
6917
|
-
const ELIGIBLE_LIVE_VALIDATION_TYPES = new Set([
|
|
6829
|
+
const ELIGIBLE_LIVE_VALIDATION_TYPES = new Set([
|
|
6830
|
+
WORK_TYPE.IMPLEMENT,
|
|
6831
|
+
WORK_TYPE.IMPLEMENT_LARGE,
|
|
6832
|
+
WORK_TYPE.FIX,
|
|
6833
|
+
WORK_TYPE.BUILD_FIX_COMPLEX,
|
|
6834
|
+
]);
|
|
6918
6835
|
|
|
6919
|
-
function autoDispatchLiveValidationWi(meta, config) {
|
|
6920
|
-
const item = meta?.item;
|
|
6921
|
-
|
|
6836
|
+
function autoDispatchLiveValidationWi(meta, config, dispatchItem = null) {
|
|
6837
|
+
const item = meta?.item || null;
|
|
6838
|
+
const sourceType = item?.type || dispatchItem?.type || null;
|
|
6839
|
+
const sourceKey = item?.id || dispatchItem?.id || null;
|
|
6840
|
+
if (!sourceKey || !sourceType) return;
|
|
6841
|
+
if (!item && !meta?.pr) return;
|
|
6922
6842
|
|
|
6923
6843
|
// Never re-trigger off the completion of a WI that was itself auto-created
|
|
6924
6844
|
// as a live-validation follow-up — that's what causes the infinite
|
|
6925
6845
|
// "Validate: Validate: ..." cascade (W-mr9u39az000db5c2 / issue #708).
|
|
6926
|
-
if (item
|
|
6846
|
+
if (item?.meta && item.meta.liveValidationFor) return;
|
|
6927
6847
|
|
|
6928
|
-
//
|
|
6848
|
+
// Only implement/implement:large/fix/build-fix-complex completions ever
|
|
6929
6849
|
// author a real code diff worth validating. Review, test, verify, setup,
|
|
6930
6850
|
// explore, ask, docs, decompose, etc. completions must never spawn a
|
|
6931
6851
|
// "Validate: ..." follow-up, even when they resolve a PR reference.
|
|
6932
|
-
if (!ELIGIBLE_LIVE_VALIDATION_TYPES.has(
|
|
6852
|
+
if (!ELIGIBLE_LIVE_VALIDATION_TYPES.has(sourceType)) return;
|
|
6933
6853
|
|
|
6934
6854
|
// Issue #716 — QA Session infrastructure WIs (SETUP/DRAFT/EXECUTE, tagged
|
|
6935
6855
|
// via meta.sessionId and always dispatched with skipPr:true) never own a
|
|
@@ -6939,7 +6859,7 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
6939
6859
|
// for all three phases) spawns spurious "Validate: QA Session ..." WIs the
|
|
6940
6860
|
// moment any PR reference resolves against it (see the skipPr guard below
|
|
6941
6861
|
// resolveWorkItemPrRecord in engine.js for the matching pre-dispatch fix).
|
|
6942
|
-
if (item
|
|
6862
|
+
if (item?.skipPr || (item?.meta && item.meta.sessionId)) return;
|
|
6943
6863
|
|
|
6944
6864
|
const projectName = meta?.project?.name;
|
|
6945
6865
|
if (!projectName) return;
|
|
@@ -6949,23 +6869,8 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
6949
6869
|
const project = projects.find(p => p && p.name === projectName);
|
|
6950
6870
|
if (!project) return;
|
|
6951
6871
|
|
|
6952
|
-
const
|
|
6953
|
-
if (!
|
|
6954
|
-
|
|
6955
|
-
const lvTypes = Array.isArray(lv.type) ? lv.type : [lv.type];
|
|
6956
|
-
|
|
6957
|
-
// Candidate validation types = configured live types minus the completing
|
|
6958
|
-
// item's own type (avoid an infinite validate-the-validation loop).
|
|
6959
|
-
const candidates = lvTypes.filter(t => t && t !== item.type);
|
|
6960
|
-
if (candidates.length === 0) return;
|
|
6961
|
-
|
|
6962
|
-
// Collapse to a SINGLE validation type: prefer the canonical `test` type when
|
|
6963
|
-
// it is a configured live type, else the first configured live type. Either
|
|
6964
|
-
// way the chosen type is a member of lvTypes, so the validation WI routes to
|
|
6965
|
-
// the live checkout (not a worktree).
|
|
6966
|
-
const validationType = candidates.includes(WORK_TYPE.TEST)
|
|
6967
|
-
? WORK_TYPE.TEST
|
|
6968
|
-
: candidates[0];
|
|
6872
|
+
const validationType = shared.resolveLiveValidationAutoDispatchType(project);
|
|
6873
|
+
if (!validationType) return;
|
|
6969
6874
|
|
|
6970
6875
|
// Resolve PR reference: prefer the canonical stamped _pr field (set by
|
|
6971
6876
|
// stampWiPrRef which runs earlier in runPostCompletionHooks), then fall
|
|
@@ -6977,15 +6882,26 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
6977
6882
|
// (W-mq18ec6h000p7b87) and would otherwise let a non-fix WI (e.g.
|
|
6978
6883
|
// 'implement'/'docs') that merely *mentions* an unrelated PR number in
|
|
6979
6884
|
// prose trigger a live-validation dispatch against the wrong PR.
|
|
6980
|
-
const prRef = item
|
|
6885
|
+
const prRef = item
|
|
6886
|
+
? (item._pr || item._prUrl || shared.extractStructuredWorkItemPrRef(item) || null)
|
|
6887
|
+
: (meta?.pr || null);
|
|
6981
6888
|
if (!prRef) return;
|
|
6982
6889
|
|
|
6983
|
-
const
|
|
6984
|
-
|
|
6985
|
-
|
|
6890
|
+
const prUrl = item?._prUrl || (typeof prRef === 'object'
|
|
6891
|
+
? (prRef.url || prRef.webUrl || prRef.remoteUrl || null)
|
|
6892
|
+
: (/^https?:\/\//i.test(String(prRef)) ? String(prRef) : null));
|
|
6893
|
+
const prId = typeof prRef === 'object'
|
|
6894
|
+
? (prRef.id || prRef.pr_id || prRef.prId || prRef.pullRequestId || shared.getPrNumber(prRef))
|
|
6895
|
+
: String(prRef);
|
|
6896
|
+
const prTarget = prUrl || (prId != null ? String(prId) : null);
|
|
6897
|
+
if (!prTarget) return;
|
|
6898
|
+
|
|
6899
|
+
const codingWiId = sourceKey;
|
|
6900
|
+
const wiScope = item ? resolveWorkItemScope(meta) : projectName;
|
|
6901
|
+
if (!wiScope) return;
|
|
6986
6902
|
|
|
6987
6903
|
try {
|
|
6988
|
-
mutateWorkItems(
|
|
6904
|
+
mutateWorkItems(wiScope, items => {
|
|
6989
6905
|
if (!Array.isArray(items)) return items;
|
|
6990
6906
|
|
|
6991
6907
|
// Dedup: only one validation WI in flight per coding WI. Any non-terminal
|
|
@@ -7004,7 +6920,7 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
7004
6920
|
return items;
|
|
7005
6921
|
}
|
|
7006
6922
|
|
|
7007
|
-
const proposedTitle = 'Validate: ' + (item
|
|
6923
|
+
const proposedTitle = 'Validate: ' + (item?.title || meta?.pr?.title || codingWiId);
|
|
7008
6924
|
// Defense-in-depth (belt-and-suspenders alongside the meta.liveValidationFor
|
|
7009
6925
|
// guard above): never create a doubly-nested "Validate: Validate: ..."
|
|
7010
6926
|
// title even if some future caller manages to invoke this function
|
|
@@ -7019,11 +6935,20 @@ function autoDispatchLiveValidationWi(meta, config) {
|
|
|
7019
6935
|
title: proposedTitle,
|
|
7020
6936
|
type: validationType,
|
|
7021
6937
|
status: WI_STATUS.PENDING,
|
|
7022
|
-
depends_on: [codingWiId],
|
|
7023
|
-
|
|
7024
|
-
|
|
6938
|
+
depends_on: item?.id ? [codingWiId] : [],
|
|
6939
|
+
targetPr: prTarget,
|
|
6940
|
+
pr_id: prId != null ? String(prId) : prTarget,
|
|
6941
|
+
references: prUrl
|
|
6942
|
+
? [{ url: prUrl, label: 'Source PR' }]
|
|
6943
|
+
: [prTarget],
|
|
6944
|
+
meta: {
|
|
6945
|
+
liveValidationFor: codingWiId,
|
|
6946
|
+
liveValidationType: validationType,
|
|
6947
|
+
playbook: 'build-and-test',
|
|
6948
|
+
...(dispatchItem?.id ? { sourceDispatchId: dispatchItem.id } : {}),
|
|
6949
|
+
},
|
|
7025
6950
|
project: projectName,
|
|
7026
|
-
priority: item
|
|
6951
|
+
priority: item?.priority || 'medium',
|
|
7027
6952
|
created: ts(),
|
|
7028
6953
|
createdBy: 'lifecycle:live-validation-auto-dispatch',
|
|
7029
6954
|
};
|
|
@@ -7083,7 +7008,7 @@ module.exports = {
|
|
|
7083
7008
|
syncPrdFromPrs,
|
|
7084
7009
|
advancePrdStatusOnAllItemsDone,
|
|
7085
7010
|
persistVerifyPrsToPrd,
|
|
7086
|
-
|
|
7011
|
+
resolveWorkItemScope,
|
|
7087
7012
|
isItemCompleted,
|
|
7088
7013
|
classifyFailure,
|
|
7089
7014
|
diagnoseEmptyOutput,
|