@yemi33/minions 0.1.1810 → 0.1.1811
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 +5 -0
- package/dashboard.js +96 -126
- package/engine/cli.js +15 -9
- package/engine/copilot-models.json +1 -1
- package/engine/dispatch.js +4 -10
- package/engine/lifecycle.js +11 -11
- package/engine/projects.js +5 -5
- package/engine/queries.js +3 -3
- package/engine/shared.js +123 -4
- package/engine.js +22 -26
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/dashboard.js
CHANGED
|
@@ -130,8 +130,8 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
|
|
|
130
130
|
if (body.projects && Array.isArray(body.projects)) {
|
|
131
131
|
if (!Array.isArray(current.projects)) current.projects = [];
|
|
132
132
|
for (const update of body.projects) {
|
|
133
|
-
const candidateProject = (candidate.projects || []
|
|
134
|
-
const currentProject = current.projects
|
|
133
|
+
const candidateProject = shared.findProjectByName(candidate.projects || [], update.name);
|
|
134
|
+
const currentProject = shared.findProjectByName(current.projects, update.name);
|
|
135
135
|
if (!candidateProject || !currentProject) continue;
|
|
136
136
|
currentProject.workSources = candidateProject.workSources;
|
|
137
137
|
}
|
|
@@ -259,12 +259,11 @@ function normalizeWorkItemDedupTitle(value) {
|
|
|
259
259
|
function resolveWorkItemDedupProject(item, wiPath = '') {
|
|
260
260
|
const projectName = normalizeWorkItemDedupText(item?.project || item?._project || item?._source);
|
|
261
261
|
if (projectName) {
|
|
262
|
-
const namedProject =
|
|
263
|
-
if (namedProject) return namedProject;
|
|
262
|
+
const namedProject = shared.resolveProjectSource(projectName, PROJECTS, { allowCentral: false });
|
|
263
|
+
if (namedProject.project) return namedProject.project;
|
|
264
264
|
}
|
|
265
265
|
if (!wiPath) return null;
|
|
266
|
-
|
|
267
|
-
return PROJECTS.find(p => path.resolve(shared.projectWorkItemsPath(p)) === resolvedWiPath) || null;
|
|
266
|
+
return shared.resolveProjectSource(wiPath, PROJECTS, { allowCentral: false }).project || null;
|
|
268
267
|
}
|
|
269
268
|
|
|
270
269
|
function getWorkItemPrRefCandidates(item) {
|
|
@@ -377,18 +376,37 @@ function findProjectByName(projects, projectName) {
|
|
|
377
376
|
}
|
|
378
377
|
|
|
379
378
|
function resolveWorkItemsCreateTarget(projectName, projects = PROJECTS) {
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
379
|
+
const target = shared.resolveProjectSource(projectName, projects, { defaultWhenSingle: true, minionsDir: MINIONS_DIR });
|
|
380
|
+
if (target.error) return { error: target.error };
|
|
381
|
+
return target;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function resolveProjectSourceTarget(source, projects = PROJECTS, options = {}) {
|
|
385
|
+
return shared.resolveProjectSource(source, projects, { minionsDir: MINIONS_DIR, ...options });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function dispatchPrefixForResolvedSource(target) {
|
|
389
|
+
return target?.project ? `work-${target.project.name}-` : 'central-work-';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function findWorkItemsTargetById(id, source, projects = PROJECTS) {
|
|
393
|
+
const explicitSource = source !== undefined && source !== null && String(source).trim() !== '';
|
|
394
|
+
if (explicitSource) {
|
|
395
|
+
const target = resolveProjectSourceTarget(source, projects);
|
|
396
|
+
if (target.error) return { error: target.error };
|
|
397
|
+
const items = shared.safeJson(target.wiPath) || [];
|
|
398
|
+
return { ...target, found: items.some(i => i.id === id) };
|
|
387
399
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
};
|
|
400
|
+
|
|
401
|
+
const central = resolveProjectSourceTarget('central', projects);
|
|
402
|
+
const centralItems = shared.safeJson(central.wiPath) || [];
|
|
403
|
+
if (centralItems.some(i => i.id === id)) return { ...central, found: true };
|
|
404
|
+
for (const project of projects) {
|
|
405
|
+
const target = resolveProjectSourceTarget(project.name, projects);
|
|
406
|
+
const items = shared.safeJson(target.wiPath) || [];
|
|
407
|
+
if (items.some(i => i.id === id)) return { ...target, found: true };
|
|
408
|
+
}
|
|
409
|
+
return { found: false };
|
|
392
410
|
}
|
|
393
411
|
|
|
394
412
|
function validatePipelineProjects(pipeline, projects = PROJECTS) {
|
|
@@ -463,9 +481,12 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
|
|
|
463
481
|
}
|
|
464
482
|
const projects = shared.getProjects(config);
|
|
465
483
|
const explicitProjectName = String(projectName || '').trim();
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
484
|
+
const explicitProject = explicitProjectName
|
|
485
|
+
? shared.resolveProjectSource(explicitProjectName, projects, { allowCentral: false, minionsDir: MINIONS_DIR })
|
|
486
|
+
: null;
|
|
487
|
+
let targetProject = explicitProject?.project || null;
|
|
488
|
+
if (explicitProject?.error) {
|
|
489
|
+
const err = new Error(explicitProject.error);
|
|
469
490
|
err.statusCode = 400;
|
|
470
491
|
throw err;
|
|
471
492
|
}
|
|
@@ -474,7 +495,7 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
|
|
|
474
495
|
const matches = prScope ? projects.filter(p => shared.getProjectPrScope(p) === prScope) : [];
|
|
475
496
|
if (matches.length === 1) targetProject = matches[0];
|
|
476
497
|
}
|
|
477
|
-
const prPath = targetProject ? shared.projectPrPath(targetProject) :
|
|
498
|
+
const prPath = targetProject ? shared.projectPrPath(targetProject) : shared.centralPullRequestsPath(MINIONS_DIR);
|
|
478
499
|
|
|
479
500
|
const prNumMatch = url.match(/\/pull\/(\d+)|pullrequest\/(\d+)/);
|
|
480
501
|
const prNum = prNumMatch ? (prNumMatch[1] || prNumMatch[2]) : Date.now().toString().slice(-6);
|
|
@@ -3088,22 +3109,22 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3088
3109
|
// projects → root-level work-items.json (orchestration system standalone use).
|
|
3089
3110
|
let targetProject = null;
|
|
3090
3111
|
if (project) {
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
results.push({ type: action.type, error:
|
|
3112
|
+
const target = resolveProjectSourceTarget(project, PROJECTS, { allowCentral: false });
|
|
3113
|
+
targetProject = target.project;
|
|
3114
|
+
if (target.error) {
|
|
3115
|
+
results.push({ type: action.type, error: target.error });
|
|
3095
3116
|
break;
|
|
3096
3117
|
}
|
|
3097
3118
|
} else if (prRef) {
|
|
3098
3119
|
const allPrs = getPullRequests().filter(p => !p._ghost);
|
|
3099
3120
|
linkedPr = shared.findPrRecord(allPrs, prRef) || null;
|
|
3100
3121
|
if (linkedPr?._project && linkedPr._project !== 'central') {
|
|
3101
|
-
targetProject =
|
|
3122
|
+
targetProject = resolveProjectSourceTarget(linkedPr._project, PROJECTS, { allowCentral: false }).project || null;
|
|
3102
3123
|
}
|
|
3103
3124
|
} else if (inferredProject) {
|
|
3104
3125
|
// Doc-chat fallback: filePath-derived project when the LLM omits the field. Validated against
|
|
3105
3126
|
// PROJECTS upstream by _inferDocChatProject — a stale lookup would just yield null here.
|
|
3106
|
-
targetProject =
|
|
3127
|
+
targetProject = resolveProjectSourceTarget(inferredProject, PROJECTS, { allowCentral: false }).project || null;
|
|
3107
3128
|
}
|
|
3108
3129
|
if (!targetProject) {
|
|
3109
3130
|
if (PROJECTS.length > 1) {
|
|
@@ -3124,7 +3145,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3124
3145
|
break;
|
|
3125
3146
|
}
|
|
3126
3147
|
|
|
3127
|
-
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) :
|
|
3148
|
+
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : shared.centralWorkItemsPath(MINIONS_DIR);
|
|
3128
3149
|
|
|
3129
3150
|
// Promote `agent` (singular) → `agents` (array). Models emit either shape and the prior code
|
|
3130
3151
|
// only read `action.agents`, silently dropping `agent: "lambert"` style hints.
|
|
@@ -3187,7 +3208,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3187
3208
|
// unresolved → error so build-and-test can't accidentally run against the wrong repo.
|
|
3188
3209
|
const projectName = action.project || pr._project || null;
|
|
3189
3210
|
const project = projectName
|
|
3190
|
-
?
|
|
3211
|
+
? resolveProjectSourceTarget(projectName, PROJECTS, { allowCentral: false }).project
|
|
3191
3212
|
: null;
|
|
3192
3213
|
if (!project) {
|
|
3193
3214
|
results.push({ type: 'build-and-test', error: `Project not found for PR ${pr.id}: ${projectName || '(none)'}` });
|
|
@@ -3248,14 +3269,14 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3248
3269
|
const project = action.project || '';
|
|
3249
3270
|
let targetProject = null;
|
|
3250
3271
|
if (project) {
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
results.push({ type: 'reopen-work-item', id: action.id, error:
|
|
3272
|
+
const target = resolveProjectSourceTarget(project, PROJECTS, { allowCentral: false });
|
|
3273
|
+
targetProject = target.project;
|
|
3274
|
+
if (target.error) {
|
|
3275
|
+
results.push({ type: 'reopen-work-item', id: action.id, error: target.error });
|
|
3255
3276
|
break;
|
|
3256
3277
|
}
|
|
3257
3278
|
} else if (inferredProject) {
|
|
3258
|
-
targetProject =
|
|
3279
|
+
targetProject = resolveProjectSourceTarget(inferredProject, PROJECTS, { allowCentral: false }).project || null;
|
|
3259
3280
|
}
|
|
3260
3281
|
if (!targetProject) {
|
|
3261
3282
|
if (PROJECTS.length > 1) {
|
|
@@ -3264,7 +3285,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3264
3285
|
}
|
|
3265
3286
|
if (PROJECTS.length === 1) targetProject = PROJECTS[0];
|
|
3266
3287
|
}
|
|
3267
|
-
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) :
|
|
3288
|
+
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : shared.centralWorkItemsPath(MINIONS_DIR);
|
|
3268
3289
|
let reopenResult = null;
|
|
3269
3290
|
mutateJsonFileLocked(wiPath, items => {
|
|
3270
3291
|
if (!Array.isArray(items)) items = [];
|
|
@@ -3844,7 +3865,7 @@ function _inferDocChatProject(filePath) {
|
|
|
3844
3865
|
if (!filePath) return null;
|
|
3845
3866
|
const m = String(filePath).replace(/\\/g, '/').match(/^projects\/([^/]+)\//);
|
|
3846
3867
|
if (!m) return null;
|
|
3847
|
-
const inferred =
|
|
3868
|
+
const inferred = resolveProjectSourceTarget(m[1], PROJECTS, { allowCentral: false }).project;
|
|
3848
3869
|
return inferred?.name || null;
|
|
3849
3870
|
}
|
|
3850
3871
|
|
|
@@ -4531,14 +4552,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
4531
4552
|
}
|
|
4532
4553
|
|
|
4533
4554
|
const config = queries.getConfig();
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4555
|
+
// safeJsonNoRestore — never resurrect an archived PRD's .backup sidecar
|
|
4556
|
+
// during project resolution (W-mouptdh1000h9f39). The active-from-archive
|
|
4557
|
+
// write above (mutateJsonFileLocked) already created activePath when needed.
|
|
4558
|
+
const projectPlan = safeJsonNoRestore(activePath) || safeJsonNoRestore(prdPath);
|
|
4559
|
+
const projectTarget = projectPlan?.project
|
|
4560
|
+
? resolveProjectSourceTarget(projectPlan.project, PROJECTS, { allowCentral: false })
|
|
4561
|
+
: null;
|
|
4562
|
+
const project = projectTarget?.project || (PROJECTS.length === 1 ? PROJECTS[0] : null);
|
|
4542
4563
|
|
|
4543
4564
|
// Check for existing verify WI — reset to pending if already done (re-verify)
|
|
4544
4565
|
if (project) {
|
|
@@ -4592,25 +4613,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4592
4613
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4593
4614
|
|
|
4594
4615
|
// Find the right file — check source first, then search all project files
|
|
4595
|
-
let
|
|
4596
|
-
if (
|
|
4597
|
-
|
|
4598
|
-
if (proj) wiPath = shared.projectWorkItemsPath(proj);
|
|
4599
|
-
}
|
|
4600
|
-
if (!wiPath) {
|
|
4601
|
-
// Search central first, then all projects
|
|
4602
|
-
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4603
|
-
const centralItems = shared.safeJson(centralPath) || [];
|
|
4604
|
-
if (centralItems.some(i => i.id === id)) {
|
|
4605
|
-
wiPath = centralPath;
|
|
4606
|
-
} else {
|
|
4607
|
-
for (const proj of PROJECTS) {
|
|
4608
|
-
const projPath = shared.projectWorkItemsPath(proj);
|
|
4609
|
-
const projItems = shared.safeJson(projPath) || [];
|
|
4610
|
-
if (projItems.some(i => i.id === id)) { wiPath = projPath; break; }
|
|
4611
|
-
}
|
|
4612
|
-
}
|
|
4613
|
-
}
|
|
4616
|
+
let resolvedTarget = findWorkItemsTargetById(id, source, PROJECTS);
|
|
4617
|
+
if (resolvedTarget.error) return jsonReply(res, 404, { error: resolvedTarget.error });
|
|
4618
|
+
let wiPath = resolvedTarget.found ? resolvedTarget.wiPath : null;
|
|
4614
4619
|
// If no work item found, attempt to re-materialize from PRD item definition
|
|
4615
4620
|
if (!wiPath) {
|
|
4616
4621
|
const prdFile = body.prdFile;
|
|
@@ -4629,8 +4634,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4629
4634
|
|
|
4630
4635
|
// Determine target work-items file (project from PRD item or plan, fallback to central)
|
|
4631
4636
|
const projName = prdItem.project || plan.project || prdFile.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
|
|
4632
|
-
const
|
|
4633
|
-
const
|
|
4637
|
+
const prdTarget = resolveProjectSourceTarget(projName, PROJECTS, { allowCentral: false });
|
|
4638
|
+
const proj = prdTarget.project;
|
|
4639
|
+
const targetWiPath = proj ? shared.projectWorkItemsPath(proj) : shared.centralWorkItemsPath(MINIONS_DIR);
|
|
4634
4640
|
|
|
4635
4641
|
// Create new work item from PRD item definition (same logic as materializePlansAsWorkItems)
|
|
4636
4642
|
const complexity = prdItem.estimated_complexity || 'medium';
|
|
@@ -4662,8 +4668,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4662
4668
|
|
|
4663
4669
|
// Clear dispatch history and cooldowns for this item
|
|
4664
4670
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
4665
|
-
const
|
|
4666
|
-
const dispatchKey = sourcePrefix + id;
|
|
4671
|
+
const dispatchKey = (proj ? `work-${proj.name}-` : 'central-work-') + id;
|
|
4667
4672
|
try {
|
|
4668
4673
|
mutateJsonFileLocked(dispatchPath, (dispatch) => {
|
|
4669
4674
|
dispatch.completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
|
|
@@ -4708,8 +4713,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4708
4713
|
|
|
4709
4714
|
// Clear completed dispatch entries so the engine doesn't dedup this item
|
|
4710
4715
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
4711
|
-
const
|
|
4712
|
-
const dispatchKey = sourcePrefix + id;
|
|
4716
|
+
const dispatchKey = dispatchPrefixForResolvedSource(resolvedTarget) + id;
|
|
4713
4717
|
try {
|
|
4714
4718
|
mutateJsonFileLocked(dispatchPath, (dispatch) => {
|
|
4715
4719
|
dispatch.completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
|
|
@@ -4737,17 +4741,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4737
4741
|
const { id, source } = body;
|
|
4738
4742
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4739
4743
|
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4744
|
-
} else {
|
|
4745
|
-
const proj = PROJECTS.find(p => p.name === source);
|
|
4746
|
-
if (proj) {
|
|
4747
|
-
wiPath = shared.projectWorkItemsPath(proj);
|
|
4748
|
-
}
|
|
4749
|
-
}
|
|
4750
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
4744
|
+
const target = resolveProjectSourceTarget(source, PROJECTS);
|
|
4745
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4746
|
+
const wiPath = target.wiPath;
|
|
4751
4747
|
|
|
4752
4748
|
let item = null;
|
|
4753
4749
|
let found = false;
|
|
@@ -4797,15 +4793,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4797
4793
|
const { id, source, reason } = body;
|
|
4798
4794
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4799
4795
|
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4804
|
-
} else {
|
|
4805
|
-
const proj = PROJECTS.find(p => p.name === source);
|
|
4806
|
-
if (proj) wiPath = shared.projectWorkItemsPath(proj);
|
|
4807
|
-
}
|
|
4808
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
4796
|
+
const target = resolveProjectSourceTarget(source, PROJECTS);
|
|
4797
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4798
|
+
const wiPath = target.wiPath;
|
|
4809
4799
|
|
|
4810
4800
|
let result = null;
|
|
4811
4801
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
@@ -4853,16 +4843,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4853
4843
|
const { id, source } = body;
|
|
4854
4844
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4855
4845
|
|
|
4856
|
-
|
|
4857
|
-
if (
|
|
4858
|
-
|
|
4859
|
-
} else {
|
|
4860
|
-
const proj = PROJECTS.find(p => p.name === source);
|
|
4861
|
-
if (proj) {
|
|
4862
|
-
wiPath = shared.projectWorkItemsPath(proj);
|
|
4863
|
-
}
|
|
4864
|
-
}
|
|
4865
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
4846
|
+
const target = resolveProjectSourceTarget(source, PROJECTS);
|
|
4847
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4848
|
+
const wiPath = target.wiPath;
|
|
4866
4849
|
|
|
4867
4850
|
let archivedItem = null;
|
|
4868
4851
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
@@ -4884,7 +4867,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4884
4867
|
}, { defaultValue: [] });
|
|
4885
4868
|
|
|
4886
4869
|
// Clean dispatch entries for archived item
|
|
4887
|
-
const sourcePrefix = (
|
|
4870
|
+
const sourcePrefix = dispatchPrefixForResolvedSource(target);
|
|
4888
4871
|
cleanDispatchEntries(d =>
|
|
4889
4872
|
d.meta?.dispatchKey === sourcePrefix + id ||
|
|
4890
4873
|
d.meta?.item?.id === id
|
|
@@ -4910,15 +4893,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4910
4893
|
const project = body.project || body.source;
|
|
4911
4894
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4912
4895
|
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4917
|
-
} else {
|
|
4918
|
-
const proj = PROJECTS.find(p => p.name === project);
|
|
4919
|
-
if (proj) wiPath = shared.projectWorkItemsPath(proj);
|
|
4920
|
-
}
|
|
4921
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'project not found' });
|
|
4896
|
+
const target = resolveProjectSourceTarget(project, PROJECTS);
|
|
4897
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4898
|
+
const wiPath = target.wiPath;
|
|
4922
4899
|
|
|
4923
4900
|
let result = null;
|
|
4924
4901
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
@@ -4938,8 +4915,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4938
4915
|
if (result.code !== 200) return jsonReply(res, result.code, result.body);
|
|
4939
4916
|
|
|
4940
4917
|
// Clear dispatch history and cooldowns outside lock
|
|
4941
|
-
const
|
|
4942
|
-
const dispatchKey = sourcePrefix + id;
|
|
4918
|
+
const dispatchKey = dispatchPrefixForResolvedSource(target) + id;
|
|
4943
4919
|
try {
|
|
4944
4920
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
4945
4921
|
mutateJsonFileLocked(dispatchPath, (dispatch) => {
|
|
@@ -5005,16 +4981,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
5005
4981
|
const { id, source, title, description, type, priority, agent } = body;
|
|
5006
4982
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
5007
4983
|
|
|
5008
|
-
|
|
5009
|
-
if (
|
|
5010
|
-
|
|
5011
|
-
} else {
|
|
5012
|
-
const proj = PROJECTS.find(p => p.name === source);
|
|
5013
|
-
if (proj) {
|
|
5014
|
-
wiPath = shared.projectWorkItemsPath(proj);
|
|
5015
|
-
}
|
|
5016
|
-
}
|
|
5017
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
4984
|
+
const target = resolveProjectSourceTarget(source, PROJECTS);
|
|
4985
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4986
|
+
const wiPath = target.wiPath;
|
|
5018
4987
|
|
|
5019
4988
|
let result = null;
|
|
5020
4989
|
let agentChanged = false;
|
|
@@ -5097,7 +5066,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
5097
5066
|
const planFile = 'manual-' + shared.uid() + '.json';
|
|
5098
5067
|
const plan = {
|
|
5099
5068
|
version: 'manual-' + new Date().toISOString().slice(0, 10),
|
|
5100
|
-
project: target.project?.name ||
|
|
5069
|
+
project: target.project?.name || 'Unknown',
|
|
5101
5070
|
generated_by: 'dashboard',
|
|
5102
5071
|
generated_at: new Date().toISOString().slice(0, 10),
|
|
5103
5072
|
plan_summary: body.name,
|
|
@@ -5844,7 +5813,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
5844
5813
|
}).join('\n');
|
|
5845
5814
|
|
|
5846
5815
|
const projectName = plan.project || body.file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
|
|
5847
|
-
const
|
|
5816
|
+
const projectTarget = resolveProjectSourceTarget(projectName, PROJECTS, { allowCentral: false });
|
|
5817
|
+
const targetProject = projectTarget.project || (PROJECTS.length === 1 ? PROJECTS[0] : null);
|
|
5848
5818
|
if (targetProject) {
|
|
5849
5819
|
diffAwareQueued = shared.queuePlanToPrd({
|
|
5850
5820
|
planFile: plan.source_plan, prdFile: body.file,
|
|
@@ -6808,7 +6778,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6808
6778
|
let alreadyLinked = false;
|
|
6809
6779
|
mutateDashboardConfig(config => {
|
|
6810
6780
|
if (!Array.isArray(config.projects)) config.projects = [];
|
|
6811
|
-
alreadyLinked = config.projects.some(p =>
|
|
6781
|
+
alreadyLinked = config.projects.some(p => shared.sameResolvedPath(p.localPath, target));
|
|
6812
6782
|
return config;
|
|
6813
6783
|
});
|
|
6814
6784
|
if (alreadyLinked) {
|
|
@@ -6853,7 +6823,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6853
6823
|
let duplicate = false;
|
|
6854
6824
|
mutateDashboardConfig(config => {
|
|
6855
6825
|
if (!Array.isArray(config.projects)) config.projects = [];
|
|
6856
|
-
if (config.projects.some(p =>
|
|
6826
|
+
if (config.projects.some(p => shared.sameResolvedPath(p.localPath, target))) {
|
|
6857
6827
|
duplicate = true;
|
|
6858
6828
|
return config;
|
|
6859
6829
|
}
|
|
@@ -8008,7 +7978,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
8008
7978
|
if (body.projects && Array.isArray(body.projects)) {
|
|
8009
7979
|
if (!config.projects) config.projects = [];
|
|
8010
7980
|
for (const update of body.projects) {
|
|
8011
|
-
const proj = config.projects
|
|
7981
|
+
const proj = shared.findProjectByName(config.projects, update.name);
|
|
8012
7982
|
if (!proj) continue;
|
|
8013
7983
|
if (!proj.workSources) proj.workSources = {};
|
|
8014
7984
|
if (update.workSources?.pullRequests !== undefined) {
|
package/engine/cli.js
CHANGED
|
@@ -1157,11 +1157,14 @@ const commands = {
|
|
|
1157
1157
|
}
|
|
1158
1158
|
|
|
1159
1159
|
const config = getConfig();
|
|
1160
|
-
const { getProjects, projectWorkItemsPath } = require('./shared');
|
|
1160
|
+
const { getProjects, projectWorkItemsPath, resolveProjectSource } = require('./shared');
|
|
1161
1161
|
const projects = getProjects(config);
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1162
|
+
const target = opts.project ? resolveProjectSource(opts.project, projects, { allowCentral: false }) : null;
|
|
1163
|
+
if (target?.error) {
|
|
1164
|
+
console.log(target.error);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const targetProject = target?.project || projects[0];
|
|
1165
1168
|
const wiPath = projectWorkItemsPath(targetProject);
|
|
1166
1169
|
let item;
|
|
1167
1170
|
mutateWorkItems(wiPath, items => {
|
|
@@ -1202,11 +1205,14 @@ const commands = {
|
|
|
1202
1205
|
}
|
|
1203
1206
|
|
|
1204
1207
|
const config = getConfig();
|
|
1205
|
-
const { getProjects } = require('./shared');
|
|
1208
|
+
const { getProjects, resolveProjectSource } = require('./shared');
|
|
1206
1209
|
const projects = getProjects(config);
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
+
const target = projectName ? resolveProjectSource(projectName, projects, { allowCentral: false }) : null;
|
|
1211
|
+
if (target?.error) {
|
|
1212
|
+
console.log(target.error);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
const targetProject = target?.project || projects[0];
|
|
1210
1216
|
|
|
1211
1217
|
if (!targetProject) {
|
|
1212
1218
|
console.log('No projects configured. Run: minions add <dir>');
|
|
@@ -1395,7 +1401,7 @@ const commands = {
|
|
|
1395
1401
|
const wiPath = (item.meta.source === 'central-work-item' || item.meta.source === 'central-work-item-fanout')
|
|
1396
1402
|
? path.join(MINIONS_DIR, 'work-items.json')
|
|
1397
1403
|
: item.meta.project?.localPath
|
|
1398
|
-
? shared.projectWorkItemsPath(
|
|
1404
|
+
? shared.projectWorkItemsPath(shared.resolveProjectSource(item.meta.project, config.projects || [], { allowCentral: false }).project || item.meta.project)
|
|
1399
1405
|
: null;
|
|
1400
1406
|
if (wiPath) {
|
|
1401
1407
|
mutateWorkItems(wiPath, items => {
|
package/engine/dispatch.js
CHANGED
|
@@ -79,7 +79,8 @@ function mutateDispatch(mutator) {
|
|
|
79
79
|
|
|
80
80
|
function getDispatchProjectKey(project) {
|
|
81
81
|
if (!project) return '';
|
|
82
|
-
|
|
82
|
+
if (project.name) return String(project.name).toLowerCase();
|
|
83
|
+
return project.localPath ? path.resolve(project.localPath).toLowerCase() : '';
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
function getPrDispatchTargetKey(entry) {
|
|
@@ -176,15 +177,8 @@ function addToDispatch(item) {
|
|
|
176
177
|
function _resolveDispatchProject(projectRef, config) {
|
|
177
178
|
if (!projectRef) return null;
|
|
178
179
|
const projects = getProjects(config);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (byName) return byName;
|
|
182
|
-
}
|
|
183
|
-
if (projectRef.localPath) {
|
|
184
|
-
const refPath = path.resolve(projectRef.localPath);
|
|
185
|
-
const byPath = projects.find(p => p.localPath && path.resolve(p.localPath) === refPath);
|
|
186
|
-
if (byPath) return byPath;
|
|
187
|
-
}
|
|
180
|
+
const resolved = shared.resolveProjectSource(projectRef, projects, { allowCentral: false });
|
|
181
|
+
if (resolved.project) return resolved.project;
|
|
188
182
|
return projectRef;
|
|
189
183
|
}
|
|
190
184
|
|
package/engine/lifecycle.js
CHANGED
|
@@ -160,7 +160,8 @@ function checkPlanCompletion(meta, config) {
|
|
|
160
160
|
// Resolve the primary project for writing new work items (PR, verify)
|
|
161
161
|
const projectName = plan.project;
|
|
162
162
|
const primaryProject = projectName
|
|
163
|
-
?
|
|
163
|
+
? shared.resolveProjectSource(projectName, projects, { allowCentral: false }).project
|
|
164
|
+
: (projects.length === 1 ? projects[0] : null);
|
|
164
165
|
if (!primaryProject) {
|
|
165
166
|
log('warn', `Plan ${planFile}: no primary project found — skipping PR/verify creation`);
|
|
166
167
|
return;
|
|
@@ -196,9 +197,8 @@ function checkPlanCompletion(meta, config) {
|
|
|
196
197
|
log('info', `Plan ${planFile}: verify WI ${existingVerify.id} already ${existingVerify.status} — skipping`);
|
|
197
198
|
} else if (isReopenableVerify(existingVerify) && doneItems.length > 0) {
|
|
198
199
|
const verifyProject = existingVerify.project || projectName;
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
);
|
|
200
|
+
const vProject = shared.resolveProjectSource(verifyProject, projects, { allowCentral: false }).project || primaryProject;
|
|
201
|
+
const vWiPath = shared.projectWorkItemsPath(vProject);
|
|
202
202
|
let reopenedVerify = false;
|
|
203
203
|
mutateWorkItems(vWiPath, items => {
|
|
204
204
|
const v = items.find(w => w.id === existingVerify.id);
|
|
@@ -777,7 +777,8 @@ function syncPrsFromOutput(output, agentId, meta, config, opts = {}) {
|
|
|
777
777
|
|
|
778
778
|
const projects = shared.getProjects(config);
|
|
779
779
|
if (projects.length === 0 && !meta?.project?.name) return 0;
|
|
780
|
-
const defaultProject = (meta?.project?.name &&
|
|
780
|
+
const defaultProject = (meta?.project?.name && shared.resolveProjectSource(meta.project.name, projects, { allowCentral: false }).project) ||
|
|
781
|
+
(projects.length === 1 ? projects[0] : null);
|
|
781
782
|
const useCentral = !defaultProject;
|
|
782
783
|
|
|
783
784
|
// Match each PR to its correct project by finding which repo URL appears near the PR number in output
|
|
@@ -950,16 +951,15 @@ function hasCanonicalPrAttachment(itemId, config) {
|
|
|
950
951
|
function resolvePrFallbackProject(meta, config) {
|
|
951
952
|
const projects = shared.getProjects(config);
|
|
952
953
|
if (meta?.project?.name) {
|
|
953
|
-
const match =
|
|
954
|
+
const match = shared.resolveProjectSource(meta.project.name, projects, { allowCentral: false }).project;
|
|
954
955
|
if (match) return match;
|
|
955
956
|
}
|
|
956
957
|
if (meta?.project?.localPath) {
|
|
957
|
-
const
|
|
958
|
-
const match = projects.find(p => p.localPath && path.resolve(p.localPath) === metaPath);
|
|
958
|
+
const match = shared.resolveProjectSource(meta.project.localPath, projects, { allowCentral: false }).project;
|
|
959
959
|
if (match) return match;
|
|
960
960
|
}
|
|
961
961
|
if (meta?.item?.project) {
|
|
962
|
-
const match =
|
|
962
|
+
const match = shared.resolveProjectSource(meta.item.project, projects, { allowCentral: false }).project;
|
|
963
963
|
if (match) return match;
|
|
964
964
|
}
|
|
965
965
|
return projects.length === 1 ? projects[0] : null;
|
|
@@ -1813,7 +1813,7 @@ async function processPendingRebases(config) {
|
|
|
1813
1813
|
for (const entry of snapshot) {
|
|
1814
1814
|
if (isBranchActive(entry.branch)) { remaining.push(entry); continue; }
|
|
1815
1815
|
|
|
1816
|
-
const project = shared.getProjects(config)
|
|
1816
|
+
const project = shared.resolveProjectSource(entry.projectName, shared.getProjects(config), { allowCentral: false }).project;
|
|
1817
1817
|
if (!project) continue;
|
|
1818
1818
|
|
|
1819
1819
|
const prs = getPrs(project);
|
|
@@ -2037,7 +2037,7 @@ function extractSkillsFromOutput(output, agentId, dispatchItem, config, runtimeN
|
|
|
2037
2037
|
if (!m('created')) enrichedBlock = enrichedBlock.replace('---\n', `---\ncreated: ${dateStamp()}\n`);
|
|
2038
2038
|
const skillDirName = name.replace(/[^a-z0-9-]/g, '-');
|
|
2039
2039
|
if (scope === 'project' && project) {
|
|
2040
|
-
const proj = shared.getProjects(config)
|
|
2040
|
+
const proj = shared.resolveProjectSource(project, shared.getProjects(config), { allowCentral: false }).project;
|
|
2041
2041
|
if (proj) {
|
|
2042
2042
|
const projectSkillRoot = skillWriteTargets(effectiveRuntime, proj).project
|
|
2043
2043
|
|| path.resolve(proj.localPath, '.claude', 'skills');
|
package/engine/projects.js
CHANGED
|
@@ -66,7 +66,7 @@ function removeProject(target, options = {}) {
|
|
|
66
66
|
// 1. Cancel pending/queued work items linked to this project (project-local
|
|
67
67
|
// file + central). Done items are preserved as history.
|
|
68
68
|
summary.cancelledItems += dispatch.cancelPendingWorkItems(
|
|
69
|
-
|
|
69
|
+
shared.projectWorkItemsPath(project),
|
|
70
70
|
() => true,
|
|
71
71
|
'project-removed',
|
|
72
72
|
);
|
|
@@ -103,7 +103,7 @@ function removeProject(target, options = {}) {
|
|
|
103
103
|
// specifically. Don't touch schedules with project='any' or unset.
|
|
104
104
|
if (Array.isArray(config.schedules)) {
|
|
105
105
|
for (const s of config.schedules) {
|
|
106
|
-
if (s.project
|
|
106
|
+
if (shared.resolveProjectSource(s.project, [project], { allowCentral: false }).project && s.enabled !== false) {
|
|
107
107
|
s.enabled = false;
|
|
108
108
|
summary.disabledSchedules++;
|
|
109
109
|
}
|
|
@@ -164,20 +164,20 @@ function removeProject(target, options = {}) {
|
|
|
164
164
|
...(p.monitoredResources || []),
|
|
165
165
|
...((p.stages || []).flatMap(s => s.monitoredResources || [])),
|
|
166
166
|
];
|
|
167
|
-
if (refs.some(r => r && (r.project
|
|
167
|
+
if (refs.some(r => r && shared.resolveProjectSource(r.project || r._project, [project], { allowCentral: false }).project)) {
|
|
168
168
|
summary.pipelineRefs.push(p.id);
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
} catch { /* pipelines optional */ }
|
|
172
172
|
|
|
173
173
|
// 7. Remove from config.json (and persist any schedule disables)
|
|
174
|
-
config.projects = (config.projects || []).filter(p => p
|
|
174
|
+
config.projects = (config.projects || []).filter(p => !shared.resolveProjectSource(p, [project], { allowCentral: false }).project);
|
|
175
175
|
try { fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); }
|
|
176
176
|
catch (e) { return { ...summary, error: 'Failed to write config: ' + e.message }; }
|
|
177
177
|
|
|
178
178
|
// 8. Move (or purge) projects/<name>/ — preserves PR/work-item history by
|
|
179
179
|
// default so a re-add can pick up where it left off.
|
|
180
|
-
const dataDir =
|
|
180
|
+
const dataDir = shared.projectStateDir(project);
|
|
181
181
|
if (fs.existsSync(dataDir)) {
|
|
182
182
|
if (dataMode === 'purge') {
|
|
183
183
|
try { fs.rmSync(dataDir, { recursive: true, force: true }); summary.purgedDataDir = true; }
|
package/engine/queries.js
CHANGED
|
@@ -703,7 +703,7 @@ function buildPrUrlFromId(prId, pr, projects) {
|
|
|
703
703
|
}
|
|
704
704
|
}
|
|
705
705
|
}
|
|
706
|
-
const project = pr?._project ?
|
|
706
|
+
const project = pr?._project ? shared.resolveProjectSource(pr._project, projects, { allowCentral: false }).project : null;
|
|
707
707
|
const prNumber = shared.getPrNumber(pr || prId);
|
|
708
708
|
if (project?.prUrlBase && prNumber != null) return project.prUrlBase + prNumber;
|
|
709
709
|
return '';
|
|
@@ -1179,7 +1179,7 @@ function getWorkItems(config) {
|
|
|
1179
1179
|
const allPrs = getPullRequests(config);
|
|
1180
1180
|
for (const item of allItems) {
|
|
1181
1181
|
if (item._pr && !item._prUrl) {
|
|
1182
|
-
const project =
|
|
1182
|
+
const project = shared.resolveProjectSource(item.project || item._source, projects, { allowCentral: false }).project || null;
|
|
1183
1183
|
const canonicalPrId = shared.getCanonicalPrId(project, item._pr);
|
|
1184
1184
|
const displayPrId = shared.getPrDisplayId(item._pr);
|
|
1185
1185
|
const exactPr = allPrs.find(p => p.id === canonicalPrId);
|
|
@@ -1418,7 +1418,7 @@ function getPrdInfo(config) {
|
|
|
1418
1418
|
// Fallback: work item _pr field for anything still missing
|
|
1419
1419
|
for (const wi of Object.values(wiById)) {
|
|
1420
1420
|
if (!wi._pr || prdToPr[wi.id]?.length) continue;
|
|
1421
|
-
const project =
|
|
1421
|
+
const project = shared.resolveProjectSource(wi.project || wi._source, projects, { allowCentral: false }).project || null;
|
|
1422
1422
|
const canonicalPrId = shared.getCanonicalPrId(project, wi._pr);
|
|
1423
1423
|
const exactPr = prById[canonicalPrId] || null;
|
|
1424
1424
|
const displayMatches = exactPr ? [] : Object.values(prById).filter(candidate => shared.getPrDisplayId(candidate) === shared.getPrDisplayId(wi._pr));
|
package/engine/shared.js
CHANGED
|
@@ -1725,7 +1725,7 @@ function findProjectByName(projects, projectName) {
|
|
|
1725
1725
|
function findProjectByNameOrPath(projects, target) {
|
|
1726
1726
|
const value = String(target || '').trim();
|
|
1727
1727
|
if (!value) return null;
|
|
1728
|
-
return
|
|
1728
|
+
return resolveProjectSource(value, projects, { allowCentral: false }).project || null;
|
|
1729
1729
|
}
|
|
1730
1730
|
|
|
1731
1731
|
function resolveConfiguredProject(projectName, projectsOrConfig, options = {}) {
|
|
@@ -1746,6 +1746,117 @@ function resolveConfiguredProject(projectName, projectsOrConfig, options = {}) {
|
|
|
1746
1746
|
return { project: null, explicit: false, value: '' };
|
|
1747
1747
|
}
|
|
1748
1748
|
|
|
1749
|
+
function centralWorkItemsPath(minionsDir = MINIONS_DIR) {
|
|
1750
|
+
return path.join(minionsDir, 'work-items.json');
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function centralPullRequestsPath(minionsDir = MINIONS_DIR) {
|
|
1754
|
+
return path.join(minionsDir, 'pull-requests.json');
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function _projectSourceRawValue(source) {
|
|
1758
|
+
if (source && typeof source === 'object') {
|
|
1759
|
+
return source.name ?? source.project ?? source._project ?? source.source ?? source._source ??
|
|
1760
|
+
source.wiPath ?? source.workItemsPath ?? source.prPath ?? source.pullRequestsPath ??
|
|
1761
|
+
source.stateDir ?? source.path ?? source.localPath ?? '';
|
|
1762
|
+
}
|
|
1763
|
+
return source;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function _sameSourcePath(value, targetPath, minionsDir = MINIONS_DIR) {
|
|
1767
|
+
if (!value || !targetPath) return false;
|
|
1768
|
+
if (sameResolvedPath(value, targetPath)) return true;
|
|
1769
|
+
if (!path.isAbsolute(value) && sameResolvedPath(path.resolve(minionsDir, value), targetPath)) return true;
|
|
1770
|
+
return false;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
function _projectSourceDescriptor(project, value, explicit, minionsDir = MINIONS_DIR) {
|
|
1774
|
+
const wiPath = project ? projectWorkItemsPath(project) : centralWorkItemsPath(minionsDir);
|
|
1775
|
+
const prPath = project ? projectPrPath(project) : centralPullRequestsPath(minionsDir);
|
|
1776
|
+
const stateDir = project ? projectStateDir(project) : minionsDir;
|
|
1777
|
+
return {
|
|
1778
|
+
project: project || null,
|
|
1779
|
+
explicit: !!explicit,
|
|
1780
|
+
value: value || '',
|
|
1781
|
+
sourceName: project?.name || 'central',
|
|
1782
|
+
isCentral: !project,
|
|
1783
|
+
stateDir,
|
|
1784
|
+
wiPath,
|
|
1785
|
+
prPath,
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function resolveProjectSource(source, projectsOrConfig, options = {}) {
|
|
1790
|
+
const projects = Array.isArray(projectsOrConfig) ? projectsOrConfig : getProjects(projectsOrConfig);
|
|
1791
|
+
const minionsDir = options.minionsDir ? path.resolve(options.minionsDir) : MINIONS_DIR;
|
|
1792
|
+
const allowCentral = options.allowCentral !== false;
|
|
1793
|
+
const raw = _projectSourceRawValue(source);
|
|
1794
|
+
const value = String(raw || '').trim();
|
|
1795
|
+
const explicit = !!value;
|
|
1796
|
+
|
|
1797
|
+
if (!value) {
|
|
1798
|
+
if (options.defaultWhenSingle && projects.length === 1) {
|
|
1799
|
+
return _projectSourceDescriptor(projects[0], '', false, minionsDir);
|
|
1800
|
+
}
|
|
1801
|
+
if (allowCentral) return _projectSourceDescriptor(null, '', false, minionsDir);
|
|
1802
|
+
return { project: null, explicit: false, value: '', sourceName: '', isCentral: false, wiPath: null, prPath: null, stateDir: null };
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
const centralWorkPath = centralWorkItemsPath(minionsDir);
|
|
1806
|
+
const centralPrPath = centralPullRequestsPath(minionsDir);
|
|
1807
|
+
const centralNames = new Set(['central', 'root']);
|
|
1808
|
+
const lowerValue = value.toLowerCase();
|
|
1809
|
+
const isCentral = centralNames.has(lowerValue) ||
|
|
1810
|
+
_sameSourcePath(value, minionsDir, minionsDir) ||
|
|
1811
|
+
_sameSourcePath(value, centralWorkPath, minionsDir) ||
|
|
1812
|
+
_sameSourcePath(value, centralPrPath, minionsDir);
|
|
1813
|
+
if (isCentral) {
|
|
1814
|
+
if (allowCentral) return _projectSourceDescriptor(null, value, true, minionsDir);
|
|
1815
|
+
return {
|
|
1816
|
+
project: null,
|
|
1817
|
+
explicit: true,
|
|
1818
|
+
value,
|
|
1819
|
+
sourceName: '',
|
|
1820
|
+
isCentral: false,
|
|
1821
|
+
wiPath: null,
|
|
1822
|
+
prPath: null,
|
|
1823
|
+
stateDir: null,
|
|
1824
|
+
error: 'central source is not allowed here',
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
for (const project of projects || []) {
|
|
1829
|
+
if (!project) continue;
|
|
1830
|
+
if (String(project.name || '').toLowerCase() === lowerValue) {
|
|
1831
|
+
return _projectSourceDescriptor(project, value, explicit, minionsDir);
|
|
1832
|
+
}
|
|
1833
|
+
const candidates = [
|
|
1834
|
+
project.localPath,
|
|
1835
|
+
projectStateDir(project),
|
|
1836
|
+
projectWorkItemsPath(project),
|
|
1837
|
+
projectPrPath(project),
|
|
1838
|
+
legacyProjectStateDir(project),
|
|
1839
|
+
legacyProjectStatePath(project, 'work-items.json'),
|
|
1840
|
+
legacyProjectStatePath(project, 'pull-requests.json'),
|
|
1841
|
+
].filter(Boolean);
|
|
1842
|
+
if (candidates.some(candidate => _sameSourcePath(value, candidate, minionsDir))) {
|
|
1843
|
+
return _projectSourceDescriptor(project, value, explicit, minionsDir);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
return {
|
|
1848
|
+
project: null,
|
|
1849
|
+
explicit: true,
|
|
1850
|
+
value,
|
|
1851
|
+
sourceName: '',
|
|
1852
|
+
isCentral: false,
|
|
1853
|
+
wiPath: null,
|
|
1854
|
+
prPath: null,
|
|
1855
|
+
stateDir: null,
|
|
1856
|
+
error: formatUnknownProjectError(value, projects),
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1749
1860
|
function projectRoot(project) {
|
|
1750
1861
|
return path.resolve(project.localPath);
|
|
1751
1862
|
}
|
|
@@ -1812,9 +1923,13 @@ function mergeProjectStateArrays(current, legacy) {
|
|
|
1812
1923
|
|
|
1813
1924
|
function sameResolvedPath(a, b) {
|
|
1814
1925
|
if (!a || !b) return false;
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1926
|
+
try {
|
|
1927
|
+
const left = path.resolve(a);
|
|
1928
|
+
const right = path.resolve(b);
|
|
1929
|
+
return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
1930
|
+
} catch {
|
|
1931
|
+
return false;
|
|
1932
|
+
}
|
|
1818
1933
|
}
|
|
1819
1934
|
|
|
1820
1935
|
function removeLegacyProjectStateDir(project) {
|
|
@@ -3323,7 +3438,10 @@ module.exports = {
|
|
|
3323
3438
|
findProjectByName,
|
|
3324
3439
|
findProjectByNameOrPath,
|
|
3325
3440
|
resolveConfiguredProject,
|
|
3441
|
+
resolveProjectSource,
|
|
3326
3442
|
projectRoot,
|
|
3443
|
+
centralWorkItemsPath,
|
|
3444
|
+
centralPullRequestsPath,
|
|
3327
3445
|
projectStateDir,
|
|
3328
3446
|
projectStateDirEnsure,
|
|
3329
3447
|
projectWorkItemsPath,
|
|
@@ -3331,6 +3449,7 @@ module.exports = {
|
|
|
3331
3449
|
legacyProjectStateDir,
|
|
3332
3450
|
legacyProjectStatePath,
|
|
3333
3451
|
ensureProjectStateFiles,
|
|
3452
|
+
sameResolvedPath,
|
|
3334
3453
|
resolveProjectForPrPath, // exported for testing
|
|
3335
3454
|
getPrLinks,
|
|
3336
3455
|
addPrLink,
|
package/engine.js
CHANGED
|
@@ -2248,7 +2248,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
2248
2248
|
|
|
2249
2249
|
const defaultProjectName = plan.project || file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
|
|
2250
2250
|
const allProjects = getProjects(config);
|
|
2251
|
-
const defaultProject =
|
|
2251
|
+
const defaultProject = shared.resolveProjectSource(defaultProjectName, allProjects, { allowCentral: false }).project;
|
|
2252
2252
|
// No project found — use central work-items.json (engine works without projects)
|
|
2253
2253
|
const useCentral = !defaultProject;
|
|
2254
2254
|
|
|
@@ -2268,7 +2268,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
2268
2268
|
const itemsByProject = new Map(); // projectName -> { project, items: [] }
|
|
2269
2269
|
for (const item of items) {
|
|
2270
2270
|
if (item.project) {
|
|
2271
|
-
const itemProject =
|
|
2271
|
+
const itemProject = shared.resolveProjectSource(item.project, allProjects, { allowCentral: false }).project;
|
|
2272
2272
|
if (!itemProject) {
|
|
2273
2273
|
const error = shared.formatUnknownProjectError(item.project, allProjects);
|
|
2274
2274
|
log('warn', `PRD ${file} item ${item.id || item.name}: ${error}`);
|
|
@@ -2288,7 +2288,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
2288
2288
|
itemsByProject.get('_central').items.push(item);
|
|
2289
2289
|
} else {
|
|
2290
2290
|
const itemProjectName = defaultProjectName;
|
|
2291
|
-
const itemProject =
|
|
2291
|
+
const itemProject = shared.resolveProjectSource(itemProjectName, allProjects, { allowCentral: false }).project || defaultProject;
|
|
2292
2292
|
if (!itemProject) continue;
|
|
2293
2293
|
if (!itemsByProject.has(itemProject.name)) {
|
|
2294
2294
|
itemsByProject.set(itemProject.name, { project: itemProject, items: [] });
|
|
@@ -2333,7 +2333,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
2333
2333
|
let alreadyExists = !!existingWi;
|
|
2334
2334
|
if (!alreadyExists) {
|
|
2335
2335
|
for (const p of allProjects) {
|
|
2336
|
-
if (p.name === projName) continue;
|
|
2336
|
+
if (String(p.name || '').toLowerCase() === String(projName || '').toLowerCase()) continue;
|
|
2337
2337
|
const otherItems = safeJson(projectWorkItemsPath(p)) || [];
|
|
2338
2338
|
const otherWi = otherItems.find(w => w.id === item.id);
|
|
2339
2339
|
if (otherWi) {
|
|
@@ -2399,7 +2399,7 @@ function materializePlansAsWorkItems(config) {
|
|
|
2399
2399
|
|
|
2400
2400
|
// Process cross-project re-opens outside the lock (no nested locks)
|
|
2401
2401
|
for (const { itemId, projectName: rProjName, item: rItem } of deferredReopens) {
|
|
2402
|
-
const rProject =
|
|
2402
|
+
const rProject = shared.resolveProjectSource(rProjName, allProjects, { allowCentral: false }).project;
|
|
2403
2403
|
if (!rProject) continue;
|
|
2404
2404
|
const rPath = projectWorkItemsPath(rProject);
|
|
2405
2405
|
mutateWorkItems(rPath, items => {
|
|
@@ -2631,7 +2631,7 @@ function isPrAutomationCausePending(project, pr, causeKey) {
|
|
|
2631
2631
|
if (d.meta?.automationCauseKey !== causeKey) return false;
|
|
2632
2632
|
if (!prCanonicalId) return true;
|
|
2633
2633
|
const dispatchProject = d.meta?.project?.name
|
|
2634
|
-
? (getProjects(getConfig())
|
|
2634
|
+
? (shared.resolveProjectSource(d.meta.project.name, getProjects(getConfig()), { allowCentral: false }).project || d.meta.project)
|
|
2635
2635
|
: (d.meta?.project || null);
|
|
2636
2636
|
const dispatchPrId = shared.getCanonicalPrId(dispatchProject, d.meta?.pr, d.meta?.pr?.url || '');
|
|
2637
2637
|
return !dispatchPrId || dispatchPrId === prCanonicalId;
|
|
@@ -2678,8 +2678,6 @@ async function discoverFromPrs(config, project) {
|
|
|
2678
2678
|
const newWork = [];
|
|
2679
2679
|
|
|
2680
2680
|
const projMeta = { name: project?.name, localPath: project?.localPath };
|
|
2681
|
-
const projectsByName = new Map(shared.getProjects(config).map(p => [p.name, p]));
|
|
2682
|
-
|
|
2683
2681
|
// Resolve poll-enabled per project — stale reviewStatus is untrustworthy without poller
|
|
2684
2682
|
const isAdoProject = project?.repoHost !== 'github';
|
|
2685
2683
|
const pollEnabled = isAdoProject
|
|
@@ -2699,7 +2697,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2699
2697
|
.filter(d => d.meta?.pr?.id)
|
|
2700
2698
|
.map(d => {
|
|
2701
2699
|
const dispatchProject = d.meta?.project?.name
|
|
2702
|
-
? (
|
|
2700
|
+
? (shared.resolveProjectSource(d.meta.project.name, shared.getProjects(config), { allowCentral: false }).project || d.meta.project)
|
|
2703
2701
|
: (d.meta?.project || null);
|
|
2704
2702
|
return shared.getCanonicalPrId(dispatchProject, d.meta.pr, d.meta.pr?.url || '');
|
|
2705
2703
|
})
|
|
@@ -3157,15 +3155,8 @@ function withWorkItemPrContext(item, pr) {
|
|
|
3157
3155
|
function projectFromDispatchMeta(metaProject, config) {
|
|
3158
3156
|
if (!metaProject) return null;
|
|
3159
3157
|
const projects = getProjects(config);
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
if (byName) return byName;
|
|
3163
|
-
}
|
|
3164
|
-
if (metaProject.localPath) {
|
|
3165
|
-
const refPath = path.resolve(metaProject.localPath);
|
|
3166
|
-
const byPath = projects.find(p => p.localPath && path.resolve(p.localPath) === refPath);
|
|
3167
|
-
if (byPath) return byPath;
|
|
3168
|
-
}
|
|
3158
|
+
const resolved = shared.resolveProjectSource(metaProject, projects, { allowCentral: false });
|
|
3159
|
+
if (resolved.project) return resolved.project;
|
|
3169
3160
|
return metaProject;
|
|
3170
3161
|
}
|
|
3171
3162
|
|
|
@@ -3707,7 +3698,6 @@ function discoverCentralWorkItems(config) {
|
|
|
3707
3698
|
const items = safeJson(centralPath) || [];
|
|
3708
3699
|
const projects = getProjects(config);
|
|
3709
3700
|
const dispatchProjects = getCentralDispatchProjects(projects);
|
|
3710
|
-
const projectsByName = new Map(dispatchProjects.map(p => [String(p.name || '').toLowerCase(), p]));
|
|
3711
3701
|
const newWork = [];
|
|
3712
3702
|
// Collect mutations to apply atomically inside lock callback (avoids TOCTOU)
|
|
3713
3703
|
const mutations = new Map(); // item.id → { field: value, ... }
|
|
@@ -3750,9 +3740,12 @@ function discoverCentralWorkItems(config) {
|
|
|
3750
3740
|
|
|
3751
3741
|
const workType = routing.normalizeWorkType(item.type, WORK_TYPE.IMPLEMENT);
|
|
3752
3742
|
const isFanOut = item.scope === 'fan-out';
|
|
3753
|
-
const explicitItemProject = typeof item.project === 'string' ? item.project : item.project?.name;
|
|
3754
|
-
|
|
3755
|
-
|
|
3743
|
+
const explicitItemProject = typeof item.project === 'string' ? item.project : (item.project?.name || item.project?.localPath);
|
|
3744
|
+
const explicitProjectTarget = explicitItemProject
|
|
3745
|
+
? shared.resolveProjectSource(explicitItemProject, dispatchProjects, { allowCentral: false })
|
|
3746
|
+
: null;
|
|
3747
|
+
if (explicitProjectTarget?.error) {
|
|
3748
|
+
const error = explicitProjectTarget.error;
|
|
3756
3749
|
mutations.set(item.id, { status: WI_STATUS.FAILED, failReason: error, failedAt: ts() });
|
|
3757
3750
|
log('warn', `central work item ${item.id}: ${error}`);
|
|
3758
3751
|
continue;
|
|
@@ -3856,10 +3849,13 @@ function discoverCentralWorkItems(config) {
|
|
|
3856
3849
|
planReadError = e;
|
|
3857
3850
|
}
|
|
3858
3851
|
}
|
|
3859
|
-
const requestedProjectName = declaredPlanProject ||
|
|
3860
|
-
const
|
|
3861
|
-
|
|
3862
|
-
|
|
3852
|
+
const requestedProjectName = declaredPlanProject || explicitItemProject;
|
|
3853
|
+
const requestedTarget = requestedProjectName
|
|
3854
|
+
? shared.resolveProjectSource(requestedProjectName, dispatchProjects, { allowCentral: false })
|
|
3855
|
+
: null;
|
|
3856
|
+
const requestedProject = requestedTarget?.project || null;
|
|
3857
|
+
if (requestedTarget?.error) {
|
|
3858
|
+
const error = requestedTarget.error;
|
|
3863
3859
|
mutations.set(item.id, {
|
|
3864
3860
|
status: WI_STATUS.FAILED,
|
|
3865
3861
|
failReason: error,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1811",
|
|
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"
|