@yemi33/minions 0.1.1809 → 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.
@@ -43,6 +43,7 @@ When multiple problems coexist, earlier triggers get the first chance to enqueue
43
43
  - Coalesces multiple comments arriving during cooldown into single fix
44
44
  - Routes to author
45
45
  - Not gated by `_evalEscalated` — humans can always force more fixes via PR comments even after the minion review loop escalates.
46
+ - Fix agents must treat human comments as claims to verify, not commands. They inspect or reproduce each claimed issue, make the smallest correct fix only when the claim is valid, and otherwise reply with evidence-backed rationale.
46
47
 
47
48
  ### B. Review feedback (`changes-requested`)
48
49
 
@@ -51,6 +52,7 @@ When multiple problems coexist, earlier triggers get the first chance to enqueue
51
52
  - `review_note` = reviewer's feedback
52
53
  - Sets `fixDispatched = true` — prevents the later conflict fix from also firing this pass
53
54
  - **Review-loop escalation**: after `evalMaxIterations` review→fix cycles (default 3), `_evalEscalated` is set on the PR and *only this trigger plus minion review/re-review* stop. Triggers A (human comments), C (build failures), and D (merge conflicts) keep running. The dashboard PR row distinguishes the two states with separate badges (review badge `review-escalated` vs. build badge `build-escalated`).
55
+ - Fix agents validate each requested-change claim before editing. Invalid, stale, already-addressed, out-of-scope, or harmful feedback should get a respectful evidence-backed rebuttal rather than a success-shaped code change.
54
56
 
55
57
  ### C. Build failures (`buildStatus === 'failing'`)
56
58
 
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 targetProject = opts.project
1163
- ? projects.find(p => p.name?.toLowerCase() === opts.project?.toLowerCase()) || projects[0]
1164
- : projects[0];
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 targetProject = projectName
1208
- ? projects.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) || projects[0]
1209
- : projects[0];
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({ localPath: item.meta.project.localPath, name: item.meta.project.name, workSources: config.projects?.find(p => p.name === item.meta.project.name)?.workSources })
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 => {
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-08T20:58:39.806Z"
4
+ "cachedAt": "2026-05-09T08:23:02.798Z"
5
5
  }
@@ -79,7 +79,8 @@ function mutateDispatch(mutator) {
79
79
 
80
80
  function getDispatchProjectKey(project) {
81
81
  if (!project) return '';
82
- return project.name || (project.localPath ? path.resolve(project.localPath).toLowerCase() : '');
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
- if (projectRef.name) {
180
- const byName = projects.find(p => p.name === projectRef.name);
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
 
@@ -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
- ? projects.find(p => p.name?.toLowerCase() === projectName?.toLowerCase()) : projects[0];
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 vWiPath = shared.projectWorkItemsPath(
200
- projects.find(p => p.name?.toLowerCase() === verifyProject?.toLowerCase()) || primaryProject
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 && projects.find(p => p.name === meta.project.name)) || projects[0];
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 = projects.find(p => p.name === meta.project.name);
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 metaPath = path.resolve(meta.project.localPath);
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 = projects.find(p => p.name === meta.item.project);
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).find(p => p.name === entry.projectName);
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).find(p => p.name === project);
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');
@@ -200,6 +200,44 @@ function collectPipelinePrRefs(pipeline, run) {
200
200
  return refs;
201
201
  }
202
202
 
203
+ function _pipelineProjectValues(...values) {
204
+ const out = [];
205
+ for (const value of values) {
206
+ if (value === undefined) continue;
207
+ if (Array.isArray(value)) out.push(...value);
208
+ else out.push(value);
209
+ }
210
+ return out;
211
+ }
212
+
213
+ function _resolvePipelineProjects(values, config) {
214
+ const projects = shared.getProjects(config);
215
+ const rawValues = _pipelineProjectValues(...values);
216
+ if (rawValues.length === 0) return { projects: [null] };
217
+ const resolved = [];
218
+ const seen = new Set();
219
+ for (const raw of rawValues) {
220
+ if (raw === undefined) continue;
221
+ if (raw === null || String(raw).trim().toLowerCase() === 'central') {
222
+ if (!seen.has('central')) { seen.add('central'); resolved.push(null); }
223
+ continue;
224
+ }
225
+ const result = shared.resolveConfiguredProject(raw, projects);
226
+ if (result.error) return { error: result.error };
227
+ const key = result.project?.name || 'central';
228
+ if (!seen.has(key)) { seen.add(key); resolved.push(result.project || null); }
229
+ }
230
+ return { projects: resolved.length ? resolved : [null] };
231
+ }
232
+
233
+ function _pipelineProjectSlug(project) {
234
+ return project ? shared.safeSlugComponent(project.name || 'project', 32).toLowerCase() : 'central';
235
+ }
236
+
237
+ function _pipelineWorkItemsPath(project) {
238
+ return project ? shared.projectWorkItemsPath(project) : CENTRAL_WI_PATH;
239
+ }
240
+
203
241
  // ── Condition Evaluation ─────────────────────────────────────────────────────
204
242
 
205
243
  /**
@@ -267,11 +305,11 @@ async function executeStage(stage, run, pipeline, config) {
267
305
 
268
306
  switch (resolved.type) {
269
307
  case STAGE_TYPE.TASK:
270
- return executeTaskStage(resolved, stageState, run, config);
308
+ return executeTaskStage(resolved, stageState, run, config, pipeline);
271
309
  case STAGE_TYPE.MEETING:
272
310
  return executeMeetingStage(resolved, stageState, run, config);
273
311
  case STAGE_TYPE.PLAN:
274
- return executePlanStage(resolved, stageState, run, config);
312
+ return executePlanStage(resolved, stageState, run, config, pipeline);
275
313
  case STAGE_TYPE.API:
276
314
  return executeApiStage(resolved, stageState, run);
277
315
  case STAGE_TYPE.MERGE_PRS:
@@ -291,8 +329,57 @@ async function executeStage(stage, run, pipeline, config) {
291
329
  }
292
330
  }
293
331
 
294
- function executeTaskStage(stage, stageState, run, config) {
332
+ function executeTaskStage(stage, stageState, run, config, pipeline = {}) {
295
333
  // Create work item(s) for the task
334
+ const items = stage.items || [{ title: stage.title, description: stage.description || '', type: stage.taskType || 'explore', agent: stage.agent }];
335
+ const count = stage.count || items.length;
336
+ const createdIds = [];
337
+ const touchedProjects = [];
338
+
339
+ for (let i = 0; i < count; i++) {
340
+ const item = items[i % items.length];
341
+ const projectResolution = _resolvePipelineProjects([
342
+ item.projects,
343
+ item.project,
344
+ stage.projects,
345
+ stage.project,
346
+ pipeline.projects,
347
+ pipeline.project,
348
+ ], config);
349
+ if (projectResolution.error) return { status: PIPELINE_STATUS.FAILED, error: projectResolution.error };
350
+ for (const project of projectResolution.projects) {
351
+ const projectSlug = _pipelineProjectSlug(project);
352
+ const id = `PL-${run.runId.slice(4, 12)}-${stage.id}-${i}${projectResolution.projects.length > 1 || project ? '-' + projectSlug : ''}`;
353
+ const wiPath = _pipelineWorkItemsPath(project);
354
+ mutateWorkItems(wiPath, workItems => {
355
+ if (workItems.some(w => w.id === id)) { createdIds.push(id); return workItems; }
356
+ workItems.push({
357
+ id,
358
+ title: item.title || stage.title,
359
+ description: item.description || stage.description || '',
360
+ type: routing.normalizeWorkType(item.type || stage.taskType, WORK_TYPE.EXPLORE),
361
+ priority: item.priority || stage.priority || 'medium',
362
+ // Agent is a soft routing hint unless agentLock/hardAgent is set.
363
+ ...(item.agent || stage.agent ? { agent: item.agent || stage.agent } : {}),
364
+ ...(item.agentLock === true || stage.agentLock === true || item.hardAgent === true || stage.hardAgent === true ? { agentLock: true } : {}),
365
+ ...(project ? { project: project.name } : {}),
366
+ status: WI_STATUS.PENDING,
367
+ created: ts(),
368
+ createdBy: 'pipeline:' + run.pipelineId,
369
+ branch: `pipeline/${run.pipelineId}/${stage.id}`,
370
+ _pipelineRun: run.runId,
371
+ _pipelineStage: stage.id,
372
+ });
373
+ return workItems;
374
+ });
375
+ createdIds.push(id);
376
+ touchedProjects.push(project?.name || null);
377
+ }
378
+ }
379
+ return { status: PIPELINE_STATUS.RUNNING, artifacts: { workItems: [...new Set(createdIds)], projects: [...new Set(touchedProjects)] } };
380
+ }
381
+
382
+ function executeTaskStageLegacy(stage, stageState, run, config) {
296
383
  const items = stage.items || [{ title: stage.title, description: stage.description || '', type: stage.taskType || 'explore', agent: stage.agent }];
297
384
  const count = stage.count || items.length;
298
385
  const wiPath = CENTRAL_WI_PATH;
@@ -396,13 +483,15 @@ function _findExistingPrdForPlan(planFile, prdDir) {
396
483
  return null;
397
484
  }
398
485
 
399
- async function executePlanStage(stage, stageState, run, config) {
486
+ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
400
487
  const plansDir = PLANS_DIR;
401
488
  if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
402
489
 
403
490
  const slug = slugify(stage.title || 'pipeline-plan');
404
- const wiPath = CENTRAL_WI_PATH;
405
- const wiId = `PL-${run.runId.slice(4, 12)}-${stage.id}-prd`;
491
+ const projectResolution = _resolvePipelineProjects([stage.projects, stage.project, pipeline.projects, pipeline.project], config);
492
+ if (projectResolution.error) return { status: PIPELINE_STATUS.FAILED, error: projectResolution.error };
493
+ const targetProjects = projectResolution.projects;
494
+ const wiIdForProject = (project) => `PL-${run.runId.slice(4, 12)}-${stage.id}-prd${targetProjects.length > 1 || project ? '-' + _pipelineProjectSlug(project) : ''}`;
406
495
 
407
496
  // ── Reconciliation: check if a plan already exists for a meeting in this run ──
408
497
  const meetingIds = _findMeetingsInRun(run);
@@ -422,26 +511,33 @@ async function executePlanStage(stage, stageState, run, config) {
422
511
  };
423
512
  }
424
513
 
425
- // Adopt or create plan-to-prd WI atomically under lock
426
- let adoptedWiId = wiId;
427
- mutateWorkItems(wiPath, workItems => {
428
- const existing = workItems.find(w => w.type === WORK_TYPE.PLAN_TO_PRD && w.planFile === existingPlanFile);
429
- if (existing) {
430
- existing._pipelineRun = run.runId;
431
- existing._pipelineStage = stage.id;
432
- adoptedWiId = existing.id;
433
- } else if (!workItems.some(w => w.id === wiId)) {
434
- workItems.push({
435
- id: wiId, title: `Convert plan to PRD: ${existingPlanFile}`,
436
- type: WORK_TYPE.PLAN_TO_PRD, priority: 'high', status: WI_STATUS.PENDING,
437
- planFile: existingPlanFile, created: ts(), createdBy: 'pipeline:' + run.pipelineId,
438
- branch: `pipeline/${run.pipelineId}/${stage.id}`, _pipelineRun: run.runId, _pipelineStage: stage.id,
439
- });
440
- }
441
- });
514
+ // Adopt or create plan-to-prd WIs atomically under lock for each target project.
515
+ const adoptedWiIds = [];
516
+ for (const project of targetProjects) {
517
+ const wiPath = _pipelineWorkItemsPath(project);
518
+ const wiId = wiIdForProject(project);
519
+ let adoptedWiId = wiId;
520
+ mutateWorkItems(wiPath, workItems => {
521
+ const existing = workItems.find(w => w.type === WORK_TYPE.PLAN_TO_PRD && w.planFile === existingPlanFile);
522
+ if (existing) {
523
+ existing._pipelineRun = run.runId;
524
+ existing._pipelineStage = stage.id;
525
+ adoptedWiId = existing.id;
526
+ } else if (!workItems.some(w => w.id === wiId)) {
527
+ workItems.push({
528
+ id: wiId, title: `Convert plan to PRD: ${existingPlanFile}`,
529
+ type: WORK_TYPE.PLAN_TO_PRD, priority: 'high', status: WI_STATUS.PENDING,
530
+ planFile: existingPlanFile, created: ts(), createdBy: 'pipeline:' + run.pipelineId,
531
+ branch: `pipeline/${run.pipelineId}/${stage.id}`, _pipelineRun: run.runId, _pipelineStage: stage.id,
532
+ ...(project ? { project: project.name } : {}),
533
+ });
534
+ }
535
+ });
536
+ adoptedWiIds.push(adoptedWiId);
537
+ }
442
538
  return {
443
539
  status: PIPELINE_STATUS.RUNNING,
444
- artifacts: { plans: [existingPlanFile], workItems: [adoptedWiId], prds: [], prs: [] },
540
+ artifacts: { plans: [existingPlanFile], workItems: adoptedWiIds, prds: [], prs: [], projects: targetProjects.map(p => p?.name || null) },
445
541
  };
446
542
  }
447
543
  }
@@ -511,32 +607,39 @@ async function executePlanStage(stage, stageState, run, config) {
511
607
  const filePath = shared.uniquePath(path.join(plansDir, filename));
512
608
  safeWrite(filePath, content);
513
609
 
514
- // Create plan-to-prd work item — atomic write to prevent race with dispatch status updates
515
- mutateWorkItems(wiPath, workItems => {
516
- if (!workItems.some(w => w.id === wiId)) {
517
- workItems.push({
518
- id: wiId,
519
- title: `Convert plan to PRD: ${path.basename(filePath)}`,
520
- type: WORK_TYPE.PLAN_TO_PRD,
521
- priority: 'high',
522
- status: WI_STATUS.PENDING,
523
- planFile: path.basename(filePath),
524
- created: ts(),
525
- createdBy: 'pipeline:' + run.pipelineId,
526
- branch: `pipeline/${run.pipelineId}/${stage.id}`,
527
- _pipelineRun: run.runId,
528
- _pipelineStage: stage.id,
529
- });
530
- }
531
- });
610
+ const createdWiIds = [];
611
+ for (const project of targetProjects) {
612
+ const wiPath = _pipelineWorkItemsPath(project);
613
+ const wiId = wiIdForProject(project);
614
+ mutateWorkItems(wiPath, workItems => {
615
+ if (!workItems.some(w => w.id === wiId)) {
616
+ workItems.push({
617
+ id: wiId,
618
+ title: `Convert plan to PRD: ${path.basename(filePath)}`,
619
+ type: WORK_TYPE.PLAN_TO_PRD,
620
+ priority: 'high',
621
+ status: WI_STATUS.PENDING,
622
+ planFile: path.basename(filePath),
623
+ created: ts(),
624
+ createdBy: 'pipeline:' + run.pipelineId,
625
+ branch: `pipeline/${run.pipelineId}/${stage.id}`,
626
+ _pipelineRun: run.runId,
627
+ _pipelineStage: stage.id,
628
+ ...(project ? { project: project.name } : {}),
629
+ });
630
+ }
631
+ });
632
+ createdWiIds.push(wiId);
633
+ }
532
634
 
533
635
  return {
534
636
  status: PIPELINE_STATUS.RUNNING,
535
637
  artifacts: {
536
638
  plans: [path.basename(filePath)],
537
- workItems: [wiId],
639
+ workItems: createdWiIds,
538
640
  prds: [], // discovered later when PRD materializes
539
641
  prs: [], // discovered later when agents create PRs
642
+ projects: targetProjects.map(p => p?.name || null),
540
643
  }
541
644
  };
542
645
  }
@@ -56,8 +56,7 @@ function removeProject(target, options = {}) {
56
56
  try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); }
57
57
  catch (e) { return { ...summary, error: 'Failed to read config: ' + e.message }; }
58
58
 
59
- const project = (config.projects || []).find(p =>
60
- p.name === target || path.resolve(p.localPath || '') === path.resolve(target));
59
+ const project = shared.findProjectByNameOrPath(config.projects || [], target);
61
60
  if (!project) {
62
61
  const available = (config.projects || []).map(p => p.name).join(', ') || '(none)';
63
62
  return { ...summary, error: `No project linked matching: ${target}. Available: ${available}` };
@@ -66,21 +65,21 @@ function removeProject(target, options = {}) {
66
65
 
67
66
  // 1. Cancel pending/queued work items linked to this project (project-local
68
67
  // file + central). Done items are preserved as history.
69
- for (const wiPath of [
70
- path.join(MINIONS_DIR, 'projects', project.name, 'work-items.json'),
68
+ summary.cancelledItems += dispatch.cancelPendingWorkItems(
69
+ shared.projectWorkItemsPath(project),
70
+ () => true,
71
+ 'project-removed',
72
+ );
73
+ summary.cancelledItems += dispatch.cancelPendingWorkItems(
71
74
  path.join(MINIONS_DIR, 'work-items.json'),
72
- ]) {
73
- summary.cancelledItems += dispatch.cancelPendingWorkItems(
74
- wiPath,
75
- w => !w.project || w.project === project.name,
76
- 'project-removed',
77
- );
78
- }
75
+ w => String(w.project || '').toLowerCase() === String(project.name || '').toLowerCase(),
76
+ 'project-removed',
77
+ );
79
78
 
80
79
  // 2. Drain dispatch — also kills active agent processes and unlinks pid +
81
80
  // prompt sidecars in engine/tmp/, matching what plan delete does.
82
81
  summary.drainedDispatches = dispatch.cleanDispatchEntries(
83
- d => _dispatchProjectName(d) === project.name,
82
+ d => String(_dispatchProjectName(d) || '').toLowerCase() === String(project.name || '').toLowerCase(),
84
83
  );
85
84
 
86
85
  // 3. Clean up worktrees under this project's worktree root, honoring
@@ -104,7 +103,7 @@ function removeProject(target, options = {}) {
104
103
  // specifically. Don't touch schedules with project='any' or unset.
105
104
  if (Array.isArray(config.schedules)) {
106
105
  for (const s of config.schedules) {
107
- if (s.project === project.name && s.enabled !== false) {
106
+ if (shared.resolveProjectSource(s.project, [project], { allowCentral: false }).project && s.enabled !== false) {
108
107
  s.enabled = false;
109
108
  summary.disabledSchedules++;
110
109
  }
@@ -165,20 +164,20 @@ function removeProject(target, options = {}) {
165
164
  ...(p.monitoredResources || []),
166
165
  ...((p.stages || []).flatMap(s => s.monitoredResources || [])),
167
166
  ];
168
- if (refs.some(r => r && (r.project === project.name || r._project === project.name))) {
167
+ if (refs.some(r => r && shared.resolveProjectSource(r.project || r._project, [project], { allowCentral: false }).project)) {
169
168
  summary.pipelineRefs.push(p.id);
170
169
  }
171
170
  }
172
171
  } catch { /* pipelines optional */ }
173
172
 
174
173
  // 7. Remove from config.json (and persist any schedule disables)
175
- config.projects = (config.projects || []).filter(p => p.name !== project.name);
174
+ config.projects = (config.projects || []).filter(p => !shared.resolveProjectSource(p, [project], { allowCentral: false }).project);
176
175
  try { fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); }
177
176
  catch (e) { return { ...summary, error: 'Failed to write config: ' + e.message }; }
178
177
 
179
178
  // 8. Move (or purge) projects/<name>/ — preserves PR/work-item history by
180
179
  // default so a re-add can pick up where it left off.
181
- const dataDir = path.join(MINIONS_DIR, 'projects', project.name);
180
+ const dataDir = shared.projectStateDir(project);
182
181
  if (fs.existsSync(dataDir)) {
183
182
  if (dataMode === 'purge') {
184
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 ? projects.find(p => p.name === pr._project) : null;
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 = projects.find(p => p.name === item.project || p.name === item._source) || null;
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 = projects.find(p => p.name === wi.project || p.name === wi._source) || null;
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));