@yemi33/minions 0.1.1552 → 0.1.1554
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/CHANGELOG.md +10 -0
- package/dashboard/js/render-plans.js +16 -11
- package/dashboard.js +40 -17
- package/engine/pipeline.js +3 -2
- package/engine/queries.js +72 -68
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1554 (2026-04-27)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
- archive bugs — undefined resetBtn, .backup re-trigger, dispatch leak
|
|
7
|
+
|
|
8
|
+
## 0.1.1553 (2026-04-27)
|
|
9
|
+
|
|
10
|
+
### Other
|
|
11
|
+
- refactor(queries): cache PR list, fold ghost-project scan into single readdir pass
|
|
12
|
+
|
|
3
13
|
## 0.1.1552 (2026-04-27)
|
|
4
14
|
|
|
5
15
|
### Fixes
|
|
@@ -601,13 +601,12 @@ async function planArchive(file, btn) {
|
|
|
601
601
|
if (!confirm(confirmMsg)) return;
|
|
602
602
|
_stopPlanPoll();
|
|
603
603
|
markDeleted('plan:' + file);
|
|
604
|
-
// Also optimistically hide the linked source plan (server archives both)
|
|
605
604
|
if (isPrd) {
|
|
606
605
|
var linkedPlan = (window._lastPlans || []).find(function(p) { return p.file === file && p.sourcePlan; });
|
|
607
606
|
if (linkedPlan) markDeleted('plan:' + linkedPlan.sourcePlan);
|
|
608
607
|
}
|
|
609
608
|
try { closeModal(); } catch { /* may not be open */ }
|
|
610
|
-
showToast('cmd-toast', '
|
|
609
|
+
showToast('cmd-toast', 'Archived', true);
|
|
611
610
|
try {
|
|
612
611
|
const res = await fetch('/api/plans/archive', {
|
|
613
612
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
@@ -617,16 +616,18 @@ async function planArchive(file, btn) {
|
|
|
617
616
|
if (!ct.includes('json')) { refresh(); return; }
|
|
618
617
|
const d = await res.json().catch(() => ({}));
|
|
619
618
|
if (res.ok && d.ok) {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
619
|
+
if (d.archivedSource || d.cancelledItems) {
|
|
620
|
+
var msg = 'Archived';
|
|
621
|
+
if (d.archivedSource) msg += ' PRD + source plan';
|
|
622
|
+
if (d.cancelledItems) msg += ', cancelled ' + d.cancelledItems + ' pending item(s)';
|
|
623
|
+
showToast('cmd-toast', msg, true);
|
|
624
|
+
}
|
|
624
625
|
refresh();
|
|
625
626
|
} else {
|
|
626
|
-
|
|
627
|
-
|
|
627
|
+
showToast('cmd-toast', 'Archive failed: ' + (d.error || 'unknown'), false);
|
|
628
|
+
refresh();
|
|
628
629
|
}
|
|
629
|
-
} catch (e) {
|
|
630
|
+
} catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); refresh(); }
|
|
630
631
|
}
|
|
631
632
|
|
|
632
633
|
async function planPause(file, btn) {
|
|
@@ -793,8 +794,12 @@ async function planUnarchive(file, btn) {
|
|
|
793
794
|
body: JSON.stringify({ file })
|
|
794
795
|
});
|
|
795
796
|
if (res.ok) { refreshPlans(); refresh(); }
|
|
796
|
-
else {
|
|
797
|
-
|
|
797
|
+
else {
|
|
798
|
+
const d = await res.json().catch(() => ({}));
|
|
799
|
+
showToast('cmd-toast', 'Unarchive failed: ' + (d.error || 'unknown'), false);
|
|
800
|
+
refresh();
|
|
801
|
+
}
|
|
802
|
+
} catch (e) { showToast('cmd-toast', 'Error: ' + e.message, false); refresh(); }
|
|
798
803
|
}
|
|
799
804
|
|
|
800
805
|
window.MinionsPlans = { openCreatePlanModal, refreshPlans, derivePlanStatus, renderPlans, openArchivedPlansModal, planExecute, planSubmitRevise, planShowRevise, planHideRevise, planView, planApprove, planArchive, planUnarchive, planDelete, planPause, planReject, planDiscuss, planOpenInDocChat, planRegeneratePRD, openVerifyGuide, triggerVerify };
|
package/dashboard.js
CHANGED
|
@@ -3341,46 +3341,69 @@ If nothing to do: { "duplicates": [], "reclassify": [], "remove": [] }`;
|
|
|
3341
3341
|
try {
|
|
3342
3342
|
const body = await readBody(req);
|
|
3343
3343
|
if (!body.file) return jsonReply(res, 400, { error: 'file required' });
|
|
3344
|
-
|
|
3344
|
+
const isPrd = body.file.endsWith('.json');
|
|
3345
|
+
shared.sanitizePath(body.file, isPrd ? PRD_DIR : PLANS_DIR);
|
|
3345
3346
|
const planPath = resolvePlanPath(body.file);
|
|
3346
3347
|
if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan not found' });
|
|
3347
3348
|
|
|
3348
|
-
|
|
3349
|
-
const archiveDir = body.file.endsWith('.json') ? path.join(PRD_DIR, 'archive') : path.join(PLANS_DIR, 'archive');
|
|
3349
|
+
const archiveDir = isPrd ? path.join(PRD_DIR, 'archive') : path.join(PLANS_DIR, 'archive');
|
|
3350
3350
|
if (!fs.existsSync(archiveDir)) fs.mkdirSync(archiveDir, { recursive: true });
|
|
3351
3351
|
const archivePath = path.join(archiveDir, body.file);
|
|
3352
3352
|
fs.renameSync(planPath, archivePath);
|
|
3353
3353
|
|
|
3354
|
-
// Mark archived in JSON if PRD
|
|
3355
3354
|
let archivedSource = null;
|
|
3356
|
-
|
|
3355
|
+
let plan = {};
|
|
3356
|
+
if (isPrd) {
|
|
3357
3357
|
try {
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
safeWrite(archivePath,
|
|
3362
|
-
//
|
|
3363
|
-
|
|
3364
|
-
|
|
3358
|
+
plan = safeJsonObj(archivePath) || {};
|
|
3359
|
+
plan.status = 'archived';
|
|
3360
|
+
plan.archivedAt = new Date().toISOString();
|
|
3361
|
+
safeWrite(archivePath, plan);
|
|
3362
|
+
// Without removing the .backup sidecar, safeJson would auto-restore the
|
|
3363
|
+
// pre-completion snapshot on engine restart, re-triggering plan completion
|
|
3364
|
+
// and spawning duplicate verify tasks (regression of #f28162b0).
|
|
3365
|
+
const backupPath = planPath + '.backup';
|
|
3366
|
+
try { fs.unlinkSync(backupPath); } catch {
|
|
3367
|
+
try { fs.writeFileSync(backupPath, JSON.stringify({ status: 'archived' })); } catch { /* best-effort */ }
|
|
3368
|
+
}
|
|
3369
|
+
if (plan.source_plan) {
|
|
3370
|
+
const mdPath = path.join(PLANS_DIR, plan.source_plan);
|
|
3365
3371
|
if (fs.existsSync(mdPath)) {
|
|
3366
3372
|
const planArchive = path.join(PLANS_DIR, 'archive');
|
|
3367
3373
|
if (!fs.existsSync(planArchive)) fs.mkdirSync(planArchive, { recursive: true });
|
|
3368
|
-
fs.renameSync(mdPath, path.join(planArchive,
|
|
3369
|
-
archivedSource =
|
|
3374
|
+
fs.renameSync(mdPath, path.join(planArchive, plan.source_plan));
|
|
3375
|
+
archivedSource = plan.source_plan;
|
|
3370
3376
|
}
|
|
3371
3377
|
}
|
|
3372
3378
|
} catch { /* optional */ }
|
|
3373
3379
|
}
|
|
3374
3380
|
|
|
3375
|
-
//
|
|
3381
|
+
// Cancel pending work items linked to this plan so the engine stops
|
|
3382
|
+
// dispatching for an archived plan. Done items are preserved as history.
|
|
3383
|
+
let cancelledItems = 0;
|
|
3384
|
+
const wiPaths = [path.join(MINIONS_DIR, 'work-items.json'), ...PROJECTS.map(p => shared.projectWorkItemsPath(p))];
|
|
3385
|
+
for (const wiPath of wiPaths) {
|
|
3386
|
+
try {
|
|
3387
|
+
mutateWorkItems(wiPath, items => {
|
|
3388
|
+
for (const w of items) {
|
|
3389
|
+
if (w.sourcePlan !== body.file) continue;
|
|
3390
|
+
if (w.status === WI_STATUS.PENDING || w.status === WI_STATUS.QUEUED) {
|
|
3391
|
+
w.status = WI_STATUS.CANCELLED;
|
|
3392
|
+
w._cancelledBy = 'plan-archived';
|
|
3393
|
+
cancelledItems++;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
});
|
|
3397
|
+
} catch (e) { console.error('plan archive cancel:', e.message); }
|
|
3398
|
+
}
|
|
3399
|
+
|
|
3376
3400
|
try {
|
|
3377
|
-
const plan = body.file.endsWith('.json') ? (safeJsonObj(archivePath) || {}) : {};
|
|
3378
3401
|
const { cleanupPlanWorktrees } = require('./engine/lifecycle');
|
|
3379
3402
|
cleanupPlanWorktrees(body.file, plan, PROJECTS, getConfig());
|
|
3380
3403
|
} catch (e) { console.error('plan worktree cleanup:', e.message); }
|
|
3381
3404
|
|
|
3382
3405
|
invalidateStatusCache();
|
|
3383
|
-
return jsonReply(res, 200, { ok: true, archived: body.file, archivedSource });
|
|
3406
|
+
return jsonReply(res, 200, { ok: true, archived: body.file, archivedSource, cancelledItems });
|
|
3384
3407
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
3385
3408
|
}
|
|
3386
3409
|
|
package/engine/pipeline.js
CHANGED
|
@@ -231,8 +231,9 @@ function evaluateCondition(condition, ctx) {
|
|
|
231
231
|
return pipelineRuns.length >= threshold;
|
|
232
232
|
}
|
|
233
233
|
case 'allBuildsGreen': {
|
|
234
|
-
// True when all PRs in monitoredResources (pipeline-level or stage-level) or run artifacts have buildStatus 'passing'
|
|
235
|
-
|
|
234
|
+
// True when all PRs in monitoredResources (pipeline-level or stage-level) or run artifacts have buildStatus 'passing'.
|
|
235
|
+
// Exclude ghost PRs (records from project subdirs not in config) — their buildStatus is stale because no poller updates them.
|
|
236
|
+
const allPrs = queries.getPullRequests(config).filter(pr => !pr._ghost);
|
|
236
237
|
const prRefs = collectPipelinePrRefs(pipeline, run);
|
|
237
238
|
if (prRefs.length === 0) return false; // no PRs to check = can't confirm green
|
|
238
239
|
for (const prRef of prRefs) {
|
package/engine/queries.js
CHANGED
|
@@ -11,7 +11,7 @@ const shared = require('./shared');
|
|
|
11
11
|
|
|
12
12
|
const { safeRead, safeReadDir, safeJson, safeWrite, getProjects, mutateJsonFileLocked,
|
|
13
13
|
projectWorkItemsPath, projectPrPath, parseSkillFrontmatter, KB_CATEGORIES,
|
|
14
|
-
WI_STATUS, DONE_STATUSES, PRD_ITEM_STATUS, ENGINE_DEFAULTS } = shared;
|
|
14
|
+
WI_STATUS, DONE_STATUSES, PRD_ITEM_STATUS, PR_STATUS, ENGINE_DEFAULTS } = shared;
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Read the first `bytes` and last `bytes` of a file efficiently using byte offsets.
|
|
@@ -472,34 +472,57 @@ function getPrs(project) {
|
|
|
472
472
|
return all;
|
|
473
473
|
}
|
|
474
474
|
|
|
475
|
+
// Cache: getPullRequests is called 3-5x per /api/status (getMetrics, getWorkItems,
|
|
476
|
+
// getPrdInfo, dashboard.js status + count). 1s TTL eliminates redundant fs reads
|
|
477
|
+
// within a single request without masking real updates from polling.
|
|
478
|
+
let _prsCache = null;
|
|
479
|
+
let _prsCacheAt = 0;
|
|
480
|
+
|
|
475
481
|
function getPullRequests(config) {
|
|
482
|
+
const now = Date.now();
|
|
483
|
+
if (_prsCache && (now - _prsCacheAt) < 1000) return _prsCache;
|
|
476
484
|
config = config || getConfig();
|
|
477
485
|
const projects = getProjects(config);
|
|
486
|
+
const projectByName = new Map(projects.map(p => [p.name, p]));
|
|
478
487
|
const allPrs = [];
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
488
|
+
const seenIds = new Set();
|
|
489
|
+
// Single pass over projects/* — configured projects use their full config
|
|
490
|
+
// (prUrlBase fill-in, _project name); unconfigured subdirs are tagged _ghost
|
|
491
|
+
// so engine code can filter them out. Mirrors what shared.getPrLinks scans,
|
|
492
|
+
// so PRD links and PR records stay in sync after a project is removed.
|
|
493
|
+
let projectDirs = [];
|
|
494
|
+
try {
|
|
495
|
+
projectDirs = fs.readdirSync(path.join(MINIONS_DIR, 'projects'), { withFileTypes: true })
|
|
496
|
+
.filter(d => d.isDirectory()).map(d => d.name);
|
|
497
|
+
} catch { /* projects dir missing */ }
|
|
498
|
+
for (const dirName of projectDirs) {
|
|
499
|
+
const project = projectByName.get(dirName) || null;
|
|
500
|
+
const prPath = project ? projectPrPath(project) : path.join(MINIONS_DIR, 'projects', dirName, 'pull-requests.json');
|
|
501
|
+
const prs = safeJson(prPath);
|
|
502
|
+
if (!Array.isArray(prs)) continue;
|
|
482
503
|
shared.normalizePrRecords(prs, project);
|
|
483
|
-
const base = project
|
|
504
|
+
const base = project?.prUrlBase || '';
|
|
484
505
|
for (const pr of prs) {
|
|
485
|
-
if (!pr
|
|
506
|
+
if (!pr?.id || seenIds.has(pr.id)) continue;
|
|
507
|
+
if (project && !pr.url && base) {
|
|
486
508
|
const prNumber = shared.getPrNumber(pr);
|
|
487
509
|
if (prNumber != null) pr.url = base + prNumber;
|
|
488
510
|
}
|
|
489
|
-
pr._project = project.name || 'Project';
|
|
511
|
+
pr._project = project ? (project.name || 'Project') : dirName;
|
|
512
|
+
if (!project) pr._ghost = true;
|
|
490
513
|
allPrs.push(pr);
|
|
514
|
+
seenIds.add(pr.id);
|
|
491
515
|
}
|
|
492
516
|
}
|
|
493
|
-
//
|
|
494
|
-
const
|
|
495
|
-
const centralPrs = safeJson(centralPath);
|
|
517
|
+
// Central pull-requests.json — manually linked PRs without a project
|
|
518
|
+
const centralPrs = safeJson(path.join(MINIONS_DIR, 'pull-requests.json'));
|
|
496
519
|
if (centralPrs) {
|
|
497
520
|
shared.normalizePrRecords(centralPrs, null);
|
|
498
521
|
for (const pr of centralPrs) {
|
|
499
|
-
if (!
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
522
|
+
if (!pr?.id || seenIds.has(pr.id)) continue;
|
|
523
|
+
pr._project = 'central';
|
|
524
|
+
allPrs.push(pr);
|
|
525
|
+
seenIds.add(pr.id);
|
|
503
526
|
}
|
|
504
527
|
}
|
|
505
528
|
allPrs.sort((a, b) => {
|
|
@@ -513,9 +536,35 @@ function getPullRequests(config) {
|
|
|
513
536
|
const bNum = parseInt((b.id || '').replace(/\D/g, '')) || 0;
|
|
514
537
|
return bNum - aNum;
|
|
515
538
|
});
|
|
539
|
+
_prsCache = allPrs;
|
|
540
|
+
_prsCacheAt = now;
|
|
516
541
|
return allPrs;
|
|
517
542
|
}
|
|
518
543
|
|
|
544
|
+
// Resolve a PR URL by preferring the canonical PR ID's own scope (e.g.
|
|
545
|
+
// `github:owner/repo#N`) so a github PR never gets an ADO URL just because the
|
|
546
|
+
// only configured project happens to be ADO. Falls back to a name-matched
|
|
547
|
+
// project's prUrlBase only when the ID is legacy (`PR-N`) and a real project
|
|
548
|
+
// owns it. Never blindly uses projects[0].
|
|
549
|
+
function buildPrUrlFromId(prId, pr, projects) {
|
|
550
|
+
if (pr?.url) return pr.url;
|
|
551
|
+
const canonical = shared.parseCanonicalPrId(prId);
|
|
552
|
+
if (canonical) {
|
|
553
|
+
const [host, rest] = canonical.scope.split(':');
|
|
554
|
+
if (host === 'github') return `https://github.com/${rest}/pull/${canonical.prNumber}`;
|
|
555
|
+
if (host === 'ado') {
|
|
556
|
+
const [org, adoProject, repo] = rest.split('/');
|
|
557
|
+
if (org && adoProject && repo) {
|
|
558
|
+
return `https://dev.azure.com/${org}/${adoProject}/_git/${repo}/pullrequest/${canonical.prNumber}`;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const project = pr?._project ? projects.find(p => p.name === pr._project) : null;
|
|
563
|
+
const prNumber = shared.getPrNumber(pr || prId);
|
|
564
|
+
if (project?.prUrlBase && prNumber != null) return project.prUrlBase + prNumber;
|
|
565
|
+
return '';
|
|
566
|
+
}
|
|
567
|
+
|
|
519
568
|
// ── Skills ──────────────────────────────────────────────────────────────────
|
|
520
569
|
|
|
521
570
|
function collectSkillFiles(config) {
|
|
@@ -1017,56 +1066,12 @@ function getPrdInfo(config) {
|
|
|
1017
1066
|
for (const wi of centralWi) { if (!wi?.id) { console.warn('[queries] Skipping central work item without id:', JSON.stringify(wi).slice(0, 120)); continue; } if (wi.sourcePlan && !wiById[wi.id]) wiById[wi.id] = wi; }
|
|
1018
1067
|
} catch { /* optional */ }
|
|
1019
1068
|
|
|
1020
|
-
// PR-to-PRD linking — derived from PR.prdItems (single source of truth)
|
|
1069
|
+
// PR-to-PRD linking — derived from PR.prdItems (single source of truth).
|
|
1070
|
+
// getPullRequests includes records from unconfigured project subdirs so PRD
|
|
1071
|
+
// links can resolve to last-known status even after a project is removed.
|
|
1021
1072
|
const allPrs = getPullRequests(config);
|
|
1022
1073
|
const prById = {};
|
|
1023
1074
|
for (const pr of allPrs) prById[pr.id] = pr;
|
|
1024
|
-
// Also scan project subdirectories that aren't in config (e.g. removed projects
|
|
1025
|
-
// whose pull-requests.json still holds last-known status). getPrLinks already
|
|
1026
|
-
// reads all subdirs, so without this PRs from those dirs would have status/title
|
|
1027
|
-
// missing in the PRD view and fall back to literal 'active'/'' defaults.
|
|
1028
|
-
try {
|
|
1029
|
-
const projectsDir = path.join(MINIONS_DIR, 'projects');
|
|
1030
|
-
const projectsByName = new Map(projects.map(p => [p.name, p]));
|
|
1031
|
-
for (const d of fs.readdirSync(projectsDir, { withFileTypes: true })) {
|
|
1032
|
-
if (!d.isDirectory() || projectsByName.has(d.name)) continue;
|
|
1033
|
-
const ghostPath = path.join(projectsDir, d.name, 'pull-requests.json');
|
|
1034
|
-
const ghostPrs = safeJson(ghostPath);
|
|
1035
|
-
if (!Array.isArray(ghostPrs)) continue;
|
|
1036
|
-
shared.normalizePrRecords(ghostPrs, null);
|
|
1037
|
-
for (const pr of ghostPrs) {
|
|
1038
|
-
if (pr?.id && !prById[pr.id]) {
|
|
1039
|
-
pr._project = d.name;
|
|
1040
|
-
prById[pr.id] = pr;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
} catch { /* projects dir missing */ }
|
|
1045
|
-
|
|
1046
|
-
// Build URL for a PR when pr.url isn't set. Prefer the PR ID's own scope
|
|
1047
|
-
// (e.g. `github:owner/repo#123` → github.com URL) so a github PR never gets
|
|
1048
|
-
// an ADO URL just because the only configured project happens to be ADO.
|
|
1049
|
-
// Only use a project's prUrlBase when its host actually matches the PR.
|
|
1050
|
-
const _buildPrUrlFromId = (prId, pr) => {
|
|
1051
|
-
if (pr?.url) return pr.url;
|
|
1052
|
-
const canonical = shared.parseCanonicalPrId(prId);
|
|
1053
|
-
if (canonical) {
|
|
1054
|
-
const [host, rest] = canonical.scope.split(':');
|
|
1055
|
-
if (host === 'github') return `https://github.com/${rest}/pull/${canonical.prNumber}`;
|
|
1056
|
-
if (host === 'ado') {
|
|
1057
|
-
const [org, adoProject, repo] = rest.split('/');
|
|
1058
|
-
if (org && adoProject && repo) {
|
|
1059
|
-
return `https://dev.azure.com/${org}/${adoProject}/_git/${repo}/pullrequest/${canonical.prNumber}`;
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
// Legacy `PR-N` ID with no host scope: only use project's prUrlBase if a
|
|
1064
|
-
// matching project (by name) exists. Never blindly fall back to projects[0].
|
|
1065
|
-
const project = pr?._project ? projects.find(p => p.name === pr._project) : null;
|
|
1066
|
-
const prNumber = shared.getPrNumber(pr || prId);
|
|
1067
|
-
if (project?.prUrlBase && prNumber != null) return project.prUrlBase + prNumber;
|
|
1068
|
-
return '';
|
|
1069
|
-
};
|
|
1070
1075
|
|
|
1071
1076
|
const prdToPr = {};
|
|
1072
1077
|
const prLinks = shared.getPrLinks(); // { "PR-xxxx": ["P-xxxx", "P-yyyy"] }
|
|
@@ -1076,10 +1081,10 @@ function getPrdInfo(config) {
|
|
|
1076
1081
|
// (or are typed as verify) and would bleed through as duplicate entries on every
|
|
1077
1082
|
// constituent item. They are surfaced via renderE2eSection instead. (#1220)
|
|
1078
1083
|
if ((itemIds || []).length > 1 || pr?.itemType === 'verify' || pr?.title?.startsWith('[E2E]')) continue;
|
|
1079
|
-
const url =
|
|
1084
|
+
const url = buildPrUrlFromId(prId, pr, projects);
|
|
1080
1085
|
for (const itemId of (itemIds || [])) {
|
|
1081
1086
|
if (!prdToPr[itemId]) prdToPr[itemId] = [];
|
|
1082
|
-
prdToPr[itemId].push({ id: prId, url, title: pr?.title || '', status: pr?.status ||
|
|
1087
|
+
prdToPr[itemId].push({ id: prId, url, title: pr?.title || '', status: pr?.status || PR_STATUS.ACTIVE, _project: pr?._project || '' });
|
|
1083
1088
|
}
|
|
1084
1089
|
}
|
|
1085
1090
|
// Fallback: work item _pr field for anything still missing
|
|
@@ -1090,21 +1095,20 @@ function getPrdInfo(config) {
|
|
|
1090
1095
|
const exactPr = prById[canonicalPrId] || null;
|
|
1091
1096
|
const displayMatches = exactPr ? [] : Object.values(prById).filter(candidate => shared.getPrDisplayId(candidate) === shared.getPrDisplayId(wi._pr));
|
|
1092
1097
|
const pr = exactPr || (displayMatches.length === 1 ? displayMatches[0] : null);
|
|
1093
|
-
const url =
|
|
1094
|
-
prdToPr[wi.id] = [{ id: pr?.id || canonicalPrId || wi._pr, url, title: pr?.title || '', status: pr?.status ||
|
|
1098
|
+
const url = buildPrUrlFromId(canonicalPrId || wi._pr, pr, projects);
|
|
1099
|
+
prdToPr[wi.id] = [{ id: pr?.id || canonicalPrId || wi._pr, url, title: pr?.title || '', status: pr?.status || PR_STATUS.ACTIVE, _project: project?.name || '' }];
|
|
1095
1100
|
}
|
|
1096
1101
|
// Aggregate sub-task PRs to decomposed parent (sub-tasks aren't PRD items but their PRs should show)
|
|
1097
1102
|
for (const pr of allPrs) {
|
|
1098
1103
|
for (const itemId of (pr.prdItems || [])) {
|
|
1099
|
-
// Find if this is a sub-task with a parent
|
|
1100
1104
|
const allItems = Object.values(wiById);
|
|
1101
1105
|
const wi = allItems.find(w => w.id === itemId && w.parent_id);
|
|
1102
1106
|
if (!wi) continue;
|
|
1103
1107
|
const parentId = wi.parent_id;
|
|
1104
1108
|
if (!prdToPr[parentId]) prdToPr[parentId] = [];
|
|
1105
1109
|
if (!prdToPr[parentId].some(p => p.id === pr.id)) {
|
|
1106
|
-
const url =
|
|
1107
|
-
prdToPr[parentId].push({ id: pr.id, url, title: pr.title || '', status: pr.status ||
|
|
1110
|
+
const url = buildPrUrlFromId(pr.id, pr, projects);
|
|
1111
|
+
prdToPr[parentId].push({ id: pr.id, url, title: pr.title || '', status: pr.status || PR_STATUS.ACTIVE, _project: pr._project || '' });
|
|
1108
1112
|
}
|
|
1109
1113
|
}
|
|
1110
1114
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1554",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|