@yemi33/minions 0.1.1810 → 0.1.1812

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/dashboard.js CHANGED
@@ -97,6 +97,13 @@ function reloadConfig() {
97
97
  }
98
98
  ensureConfiguredProjectStateFiles();
99
99
 
100
+ function resolveScheduleProjectValue(project, projects = PROJECTS) {
101
+ if (project === undefined) return { project: undefined };
102
+ const target = shared.resolveConfiguredProject(project, projects);
103
+ if (target.error) return { project: null, error: target.error };
104
+ return { project: target.project?.name || null };
105
+ }
106
+
100
107
  function mutateDashboardConfig(mutator) {
101
108
  return mutateJsonFileLocked(CONFIG_PATH, (config) => {
102
109
  if (!config || typeof config !== 'object' || Array.isArray(config)) config = {};
@@ -130,8 +137,8 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
130
137
  if (body.projects && Array.isArray(body.projects)) {
131
138
  if (!Array.isArray(current.projects)) current.projects = [];
132
139
  for (const update of body.projects) {
133
- const candidateProject = (candidate.projects || []).find(p => p.name === update.name);
134
- const currentProject = current.projects.find(p => p.name === update.name);
140
+ const candidateProject = shared.findProjectByName(candidate.projects || [], update.name);
141
+ const currentProject = shared.findProjectByName(current.projects, update.name);
135
142
  if (!candidateProject || !currentProject) continue;
136
143
  currentProject.workSources = candidateProject.workSources;
137
144
  }
@@ -259,12 +266,11 @@ function normalizeWorkItemDedupTitle(value) {
259
266
  function resolveWorkItemDedupProject(item, wiPath = '') {
260
267
  const projectName = normalizeWorkItemDedupText(item?.project || item?._project || item?._source);
261
268
  if (projectName) {
262
- const namedProject = PROJECTS.find(p => p?.name === projectName);
263
- if (namedProject) return namedProject;
269
+ const namedProject = shared.resolveProjectSource(projectName, PROJECTS, { allowCentral: false });
270
+ if (namedProject.project) return namedProject.project;
264
271
  }
265
272
  if (!wiPath) return null;
266
- const resolvedWiPath = path.resolve(wiPath);
267
- return PROJECTS.find(p => path.resolve(shared.projectWorkItemsPath(p)) === resolvedWiPath) || null;
273
+ return shared.resolveProjectSource(wiPath, PROJECTS, { allowCentral: false }).project || null;
268
274
  }
269
275
 
270
276
  function getWorkItemPrRefCandidates(item) {
@@ -377,18 +383,37 @@ function findProjectByName(projects, projectName) {
377
383
  }
378
384
 
379
385
  function resolveWorkItemsCreateTarget(projectName, projects = PROJECTS) {
380
- const project = String(projectName || '').trim();
381
- let targetProject = null;
382
- if (project) {
383
- targetProject = findProjectByName(projects, project);
384
- if (!targetProject) return { error: formatUnknownProjectError(project, projects) };
385
- } else if (projects.length === 1) {
386
- targetProject = projects[0];
386
+ const target = shared.resolveProjectSource(projectName, projects, { defaultWhenSingle: true, minionsDir: MINIONS_DIR });
387
+ if (target.error) return { error: target.error };
388
+ return target;
389
+ }
390
+
391
+ function resolveProjectSourceTarget(source, projects = PROJECTS, options = {}) {
392
+ return shared.resolveProjectSource(source, projects, { minionsDir: MINIONS_DIR, ...options });
393
+ }
394
+
395
+ function dispatchPrefixForResolvedSource(target) {
396
+ return target?.project ? `work-${target.project.name}-` : 'central-work-';
397
+ }
398
+
399
+ function findWorkItemsTargetById(id, source, projects = PROJECTS) {
400
+ const explicitSource = source !== undefined && source !== null && String(source).trim() !== '';
401
+ if (explicitSource) {
402
+ const target = resolveProjectSourceTarget(source, projects);
403
+ if (target.error) return { error: target.error };
404
+ const items = shared.safeJson(target.wiPath) || [];
405
+ return { ...target, found: items.some(i => i.id === id) };
387
406
  }
388
- return {
389
- project: targetProject,
390
- wiPath: targetProject ? shared.projectWorkItemsPath(targetProject) : path.join(MINIONS_DIR, 'work-items.json'),
391
- };
407
+
408
+ const central = resolveProjectSourceTarget('central', projects);
409
+ const centralItems = shared.safeJson(central.wiPath) || [];
410
+ if (centralItems.some(i => i.id === id)) return { ...central, found: true };
411
+ for (const project of projects) {
412
+ const target = resolveProjectSourceTarget(project.name, projects);
413
+ const items = shared.safeJson(target.wiPath) || [];
414
+ if (items.some(i => i.id === id)) return { ...target, found: true };
415
+ }
416
+ return { found: false };
392
417
  }
393
418
 
394
419
  function validatePipelineProjects(pipeline, projects = PROJECTS) {
@@ -400,6 +425,7 @@ function validatePipelineProjects(pipeline, projects = PROJECTS) {
400
425
  if (value.project !== undefined) collect(value.project);
401
426
  else if (value._project !== undefined) collect(value._project);
402
427
  else if (value.name !== undefined) collect(value.name);
428
+ else if (value.localPath !== undefined) collect(value.localPath);
403
429
  return;
404
430
  }
405
431
  refs.push(String(value));
@@ -463,9 +489,12 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
463
489
  }
464
490
  const projects = shared.getProjects(config);
465
491
  const explicitProjectName = String(projectName || '').trim();
466
- let targetProject = explicitProjectName ? findProjectByName(projects, explicitProjectName) : null;
467
- if (explicitProjectName && !targetProject) {
468
- const err = new Error(formatUnknownProjectError(explicitProjectName, projects));
492
+ const explicitProject = explicitProjectName
493
+ ? shared.resolveProjectSource(explicitProjectName, projects, { allowCentral: false, minionsDir: MINIONS_DIR })
494
+ : null;
495
+ let targetProject = explicitProject?.project || null;
496
+ if (explicitProject?.error) {
497
+ const err = new Error(explicitProject.error);
469
498
  err.statusCode = 400;
470
499
  throw err;
471
500
  }
@@ -474,7 +503,7 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
474
503
  const matches = prScope ? projects.filter(p => shared.getProjectPrScope(p) === prScope) : [];
475
504
  if (matches.length === 1) targetProject = matches[0];
476
505
  }
477
- const prPath = targetProject ? shared.projectPrPath(targetProject) : path.join(MINIONS_DIR, 'pull-requests.json');
506
+ const prPath = targetProject ? shared.projectPrPath(targetProject) : shared.centralPullRequestsPath(MINIONS_DIR);
478
507
 
479
508
  const prNumMatch = url.match(/\/pull\/(\d+)|pullrequest\/(\d+)/);
480
509
  const prNum = prNumMatch ? (prNumMatch[1] || prNumMatch[2]) : Date.now().toString().slice(-6);
@@ -2665,6 +2694,8 @@ function normalizePipelineForCompare(pipeline) {
2665
2694
  stages: Array.isArray(pipeline.stages) ? pipeline.stages : [],
2666
2695
  trigger: pipeline.trigger && typeof pipeline.trigger === 'object' ? pipeline.trigger : {},
2667
2696
  enabled: pipeline.enabled !== false,
2697
+ project: pipeline.project !== undefined ? pipeline.project : null,
2698
+ projects: Array.isArray(pipeline.projects) ? pipeline.projects : [],
2668
2699
  stopWhen: pipeline.stopWhen || null,
2669
2700
  monitoredResources: Array.isArray(pipeline.monitoredResources) ? pipeline.monitoredResources : [],
2670
2701
  };
@@ -2678,6 +2709,8 @@ function buildPipelineFromAction(action) {
2678
2709
  trigger: action.trigger && typeof action.trigger === 'object' ? action.trigger : {},
2679
2710
  enabled: action.enabled !== false,
2680
2711
  };
2712
+ if (action.project !== undefined) pipeline.project = action.project;
2713
+ if (Array.isArray(action.projects)) pipeline.projects = action.projects;
2681
2714
  if (action.stopWhen) pipeline.stopWhen = action.stopWhen;
2682
2715
  if (Array.isArray(action.monitoredResources) && action.monitoredResources.length > 0) {
2683
2716
  pipeline.monitoredResources = action.monitoredResources;
@@ -2692,6 +2725,10 @@ function pipelineDefinitionsEqual(a, b) {
2692
2725
  function createPipelineFromAction(action) {
2693
2726
  const { savePipeline, getPipeline } = require('./engine/pipeline');
2694
2727
  const pipeline = buildPipelineFromAction(action);
2728
+ const projectError = validatePipelineProjects(pipeline);
2729
+ if (projectError) {
2730
+ return { type: 'create-pipeline', id: pipeline.id, error: projectError };
2731
+ }
2695
2732
  const existing = getPipeline(pipeline.id);
2696
2733
  if (existing) {
2697
2734
  if (pipelineDefinitionsEqual(existing, pipeline)) {
@@ -3057,7 +3094,9 @@ async function _ccExecuteLocalApiAction(action) {
3057
3094
 
3058
3095
  async function executeCCActions(actions, { source = 'command-center', inferredProject = null } = {}) {
3059
3096
  const results = [];
3060
- for (const rawAction of actions) {
3097
+ const dispatchIdsCreatedInThisCall = new Map();
3098
+ for (let actionIndex = 0; actionIndex < actions.length; actionIndex++) {
3099
+ const rawAction = actions[actionIndex];
3061
3100
  const action = normalizeCCAction(rawAction);
3062
3101
  if (action?._intentFallbackError) {
3063
3102
  results.push({
@@ -3088,22 +3127,22 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
3088
3127
  // projects → root-level work-items.json (orchestration system standalone use).
3089
3128
  let targetProject = null;
3090
3129
  if (project) {
3091
- targetProject = PROJECTS.find(p => p.name?.toLowerCase() === project.toLowerCase());
3092
- if (!targetProject) {
3093
- const known = PROJECTS.map(p => p.name).join(', ') || '(none configured)';
3094
- results.push({ type: action.type, error: `Project "${project}" not found. Known projects: ${known}` });
3130
+ const target = resolveProjectSourceTarget(project, PROJECTS, { allowCentral: false });
3131
+ targetProject = target.project;
3132
+ if (target.error) {
3133
+ results.push({ type: action.type, error: target.error });
3095
3134
  break;
3096
3135
  }
3097
3136
  } else if (prRef) {
3098
3137
  const allPrs = getPullRequests().filter(p => !p._ghost);
3099
3138
  linkedPr = shared.findPrRecord(allPrs, prRef) || null;
3100
3139
  if (linkedPr?._project && linkedPr._project !== 'central') {
3101
- targetProject = PROJECTS.find(p => p.name?.toLowerCase() === String(linkedPr._project).toLowerCase()) || null;
3140
+ targetProject = resolveProjectSourceTarget(linkedPr._project, PROJECTS, { allowCentral: false }).project || null;
3102
3141
  }
3103
3142
  } else if (inferredProject) {
3104
3143
  // Doc-chat fallback: filePath-derived project when the LLM omits the field. Validated against
3105
3144
  // PROJECTS upstream by _inferDocChatProject — a stale lookup would just yield null here.
3106
- targetProject = PROJECTS.find(p => p.name?.toLowerCase() === inferredProject.toLowerCase()) || null;
3145
+ targetProject = resolveProjectSourceTarget(inferredProject, PROJECTS, { allowCentral: false }).project || null;
3107
3146
  }
3108
3147
  if (!targetProject) {
3109
3148
  if (PROJECTS.length > 1) {
@@ -3124,7 +3163,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
3124
3163
  break;
3125
3164
  }
3126
3165
 
3127
- const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : path.join(MINIONS_DIR, 'work-items.json');
3166
+ const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : shared.centralWorkItemsPath(MINIONS_DIR);
3128
3167
 
3129
3168
  // Promote `agent` (singular) → `agents` (array). Models emit either shape and the prior code
3130
3169
  // only read `action.agents`, silently dropping `agent: "lambert"` style hints.
@@ -3157,9 +3196,19 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
3157
3196
  const createResult = createWorkItemWithDedup(wiPath, item);
3158
3197
  if (!createResult.created) {
3159
3198
  const duplicateId = createResult.duplicateOf || createResult.item?.id;
3199
+ if (duplicateId && dispatchIdsCreatedInThisCall.has(duplicateId)) {
3200
+ results.push({
3201
+ type: action.type,
3202
+ id: duplicateId,
3203
+ ok: true,
3204
+ reusedFromAction: dispatchIdsCreatedInThisCall.get(duplicateId),
3205
+ });
3206
+ break;
3207
+ }
3160
3208
  results.push({ type: action.type, id: duplicateId, ok: true, duplicate: true, duplicateOf: duplicateId });
3161
3209
  break;
3162
3210
  }
3211
+ dispatchIdsCreatedInThisCall.set(id, actionIndex);
3163
3212
  results.push({ type: action.type, id, ok: true });
3164
3213
 
3165
3214
  // Pre-flight routing check: warn the user if no agent is currently available so the new
@@ -3187,7 +3236,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
3187
3236
  // unresolved → error so build-and-test can't accidentally run against the wrong repo.
3188
3237
  const projectName = action.project || pr._project || null;
3189
3238
  const project = projectName
3190
- ? PROJECTS.find(p => p.name?.toLowerCase() === String(projectName).toLowerCase())
3239
+ ? resolveProjectSourceTarget(projectName, PROJECTS, { allowCentral: false }).project
3191
3240
  : null;
3192
3241
  if (!project) {
3193
3242
  results.push({ type: 'build-and-test', error: `Project not found for PR ${pr.id}: ${projectName || '(none)'}` });
@@ -3248,14 +3297,14 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
3248
3297
  const project = action.project || '';
3249
3298
  let targetProject = null;
3250
3299
  if (project) {
3251
- targetProject = PROJECTS.find(p => p.name?.toLowerCase() === project.toLowerCase());
3252
- if (!targetProject) {
3253
- const known = PROJECTS.map(p => p.name).join(', ') || '(none configured)';
3254
- results.push({ type: 'reopen-work-item', id: action.id, error: `Project "${project}" not found. Known projects: ${known}` });
3300
+ const target = resolveProjectSourceTarget(project, PROJECTS, { allowCentral: false });
3301
+ targetProject = target.project;
3302
+ if (target.error) {
3303
+ results.push({ type: 'reopen-work-item', id: action.id, error: target.error });
3255
3304
  break;
3256
3305
  }
3257
3306
  } else if (inferredProject) {
3258
- targetProject = PROJECTS.find(p => p.name?.toLowerCase() === inferredProject.toLowerCase()) || null;
3307
+ targetProject = resolveProjectSourceTarget(inferredProject, PROJECTS, { allowCentral: false }).project || null;
3259
3308
  }
3260
3309
  if (!targetProject) {
3261
3310
  if (PROJECTS.length > 1) {
@@ -3264,7 +3313,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
3264
3313
  }
3265
3314
  if (PROJECTS.length === 1) targetProject = PROJECTS[0];
3266
3315
  }
3267
- const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : path.join(MINIONS_DIR, 'work-items.json');
3316
+ const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : shared.centralWorkItemsPath(MINIONS_DIR);
3268
3317
  let reopenResult = null;
3269
3318
  mutateJsonFileLocked(wiPath, items => {
3270
3319
  if (!Array.isArray(items)) items = [];
@@ -3383,6 +3432,7 @@ function _buildDocChatActionFeedback(actions, actionResults) {
3383
3432
  feedback.push({ type, error: String(result.error) });
3384
3433
  continue;
3385
3434
  }
3435
+ if (result.reusedFromAction !== undefined) continue;
3386
3436
  const id = result.id || result.duplicateOf;
3387
3437
  if (!result.ok || !id) continue;
3388
3438
  const item = { type, id: String(id), ok: true };
@@ -3844,7 +3894,7 @@ function _inferDocChatProject(filePath) {
3844
3894
  if (!filePath) return null;
3845
3895
  const m = String(filePath).replace(/\\/g, '/').match(/^projects\/([^/]+)\//);
3846
3896
  if (!m) return null;
3847
- const inferred = PROJECTS.find(p => p.name?.toLowerCase() === m[1].toLowerCase());
3897
+ const inferred = resolveProjectSourceTarget(m[1], PROJECTS, { allowCentral: false }).project;
3848
3898
  return inferred?.name || null;
3849
3899
  }
3850
3900
 
@@ -4531,14 +4581,14 @@ const server = http.createServer(async (req, res) => {
4531
4581
  }
4532
4582
 
4533
4583
  const config = queries.getConfig();
4534
- const project = PROJECTS.find(p => {
4535
- // safeJsonNoRestore never resurrect an archived PRD's .backup
4536
- // sidecar during project resolution (W-mouptdh1000h9f39). The
4537
- // active-from-archive write above (mutateJsonFileLocked) already
4538
- // created activePath when needed.
4539
- const plan = safeJsonNoRestore(activePath) || safeJsonNoRestore(prdPath);
4540
- return plan && p.name?.toLowerCase() === (plan.project || '').toLowerCase();
4541
- }) || PROJECTS[0] || null;
4584
+ // safeJsonNoRestore never resurrect an archived PRD's .backup sidecar
4585
+ // during project resolution (W-mouptdh1000h9f39). The active-from-archive
4586
+ // write above (mutateJsonFileLocked) already created activePath when needed.
4587
+ const projectPlan = safeJsonNoRestore(activePath) || safeJsonNoRestore(prdPath);
4588
+ const projectTarget = projectPlan?.project
4589
+ ? resolveProjectSourceTarget(projectPlan.project, PROJECTS, { allowCentral: false })
4590
+ : null;
4591
+ const project = projectTarget?.project || (PROJECTS.length === 1 ? PROJECTS[0] : null);
4542
4592
 
4543
4593
  // Check for existing verify WI — reset to pending if already done (re-verify)
4544
4594
  if (project) {
@@ -4592,25 +4642,9 @@ const server = http.createServer(async (req, res) => {
4592
4642
  if (!id) return jsonReply(res, 400, { error: 'id required' });
4593
4643
 
4594
4644
  // Find the right file — check source first, then search all project files
4595
- let wiPath;
4596
- if (source && source !== 'central') {
4597
- const proj = PROJECTS.find(p => p.name === source);
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
- }
4645
+ let resolvedTarget = findWorkItemsTargetById(id, source, PROJECTS);
4646
+ if (resolvedTarget.error) return jsonReply(res, 404, { error: resolvedTarget.error });
4647
+ let wiPath = resolvedTarget.found ? resolvedTarget.wiPath : null;
4614
4648
  // If no work item found, attempt to re-materialize from PRD item definition
4615
4649
  if (!wiPath) {
4616
4650
  const prdFile = body.prdFile;
@@ -4629,8 +4663,9 @@ const server = http.createServer(async (req, res) => {
4629
4663
 
4630
4664
  // Determine target work-items file (project from PRD item or plan, fallback to central)
4631
4665
  const projName = prdItem.project || plan.project || prdFile.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
4632
- const proj = PROJECTS.find(p => p.name?.toLowerCase() === projName.toLowerCase());
4633
- const targetWiPath = proj ? shared.projectWorkItemsPath(proj) : path.join(MINIONS_DIR, 'work-items.json');
4666
+ const prdTarget = resolveProjectSourceTarget(projName, PROJECTS, { allowCentral: false });
4667
+ const proj = prdTarget.project;
4668
+ const targetWiPath = proj ? shared.projectWorkItemsPath(proj) : shared.centralWorkItemsPath(MINIONS_DIR);
4634
4669
 
4635
4670
  // Create new work item from PRD item definition (same logic as materializePlansAsWorkItems)
4636
4671
  const complexity = prdItem.estimated_complexity || 'medium';
@@ -4662,8 +4697,7 @@ const server = http.createServer(async (req, res) => {
4662
4697
 
4663
4698
  // Clear dispatch history and cooldowns for this item
4664
4699
  const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
4665
- const sourcePrefix = proj ? `work-${proj.name}-` : 'central-work-';
4666
- const dispatchKey = sourcePrefix + id;
4700
+ const dispatchKey = (proj ? `work-${proj.name}-` : 'central-work-') + id;
4667
4701
  try {
4668
4702
  mutateJsonFileLocked(dispatchPath, (dispatch) => {
4669
4703
  dispatch.completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
@@ -4708,8 +4742,7 @@ const server = http.createServer(async (req, res) => {
4708
4742
 
4709
4743
  // Clear completed dispatch entries so the engine doesn't dedup this item
4710
4744
  const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
4711
- const sourcePrefix = (!source || source === 'central') ? 'central-work-' : `work-${source}-`;
4712
- const dispatchKey = sourcePrefix + id;
4745
+ const dispatchKey = dispatchPrefixForResolvedSource(resolvedTarget) + id;
4713
4746
  try {
4714
4747
  mutateJsonFileLocked(dispatchPath, (dispatch) => {
4715
4748
  dispatch.completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
@@ -4737,17 +4770,9 @@ const server = http.createServer(async (req, res) => {
4737
4770
  const { id, source } = body;
4738
4771
  if (!id) return jsonReply(res, 400, { error: 'id required' });
4739
4772
 
4740
- // Find the right work-items file
4741
- let wiPath;
4742
- if (!source || source === 'central') {
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' });
4773
+ const target = resolveProjectSourceTarget(source, PROJECTS);
4774
+ if (target.error) return jsonReply(res, 404, { error: target.error });
4775
+ const wiPath = target.wiPath;
4751
4776
 
4752
4777
  let item = null;
4753
4778
  let found = false;
@@ -4797,15 +4822,9 @@ const server = http.createServer(async (req, res) => {
4797
4822
  const { id, source, reason } = body;
4798
4823
  if (!id) return jsonReply(res, 400, { error: 'id required' });
4799
4824
 
4800
- // Find the right work-items file
4801
- let wiPath;
4802
- if (!source || source === 'central') {
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' });
4825
+ const target = resolveProjectSourceTarget(source, PROJECTS);
4826
+ if (target.error) return jsonReply(res, 404, { error: target.error });
4827
+ const wiPath = target.wiPath;
4809
4828
 
4810
4829
  let result = null;
4811
4830
  mutateJsonFileLocked(wiPath, (items) => {
@@ -4853,16 +4872,9 @@ const server = http.createServer(async (req, res) => {
4853
4872
  const { id, source } = body;
4854
4873
  if (!id) return jsonReply(res, 400, { error: 'id required' });
4855
4874
 
4856
- let wiPath;
4857
- if (!source || source === 'central') {
4858
- wiPath = path.join(MINIONS_DIR, 'work-items.json');
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' });
4875
+ const target = resolveProjectSourceTarget(source, PROJECTS);
4876
+ if (target.error) return jsonReply(res, 404, { error: target.error });
4877
+ const wiPath = target.wiPath;
4866
4878
 
4867
4879
  let archivedItem = null;
4868
4880
  mutateJsonFileLocked(wiPath, (items) => {
@@ -4884,7 +4896,7 @@ const server = http.createServer(async (req, res) => {
4884
4896
  }, { defaultValue: [] });
4885
4897
 
4886
4898
  // Clean dispatch entries for archived item
4887
- const sourcePrefix = (!source || source === 'central') ? 'central-work-' : `work-${source}-`;
4899
+ const sourcePrefix = dispatchPrefixForResolvedSource(target);
4888
4900
  cleanDispatchEntries(d =>
4889
4901
  d.meta?.dispatchKey === sourcePrefix + id ||
4890
4902
  d.meta?.item?.id === id
@@ -4910,15 +4922,9 @@ const server = http.createServer(async (req, res) => {
4910
4922
  const project = body.project || body.source;
4911
4923
  if (!id) return jsonReply(res, 400, { error: 'id required' });
4912
4924
 
4913
- // Find the right work-items file
4914
- let wiPath;
4915
- if (!project || project === 'central') {
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' });
4925
+ const target = resolveProjectSourceTarget(project, PROJECTS);
4926
+ if (target.error) return jsonReply(res, 404, { error: target.error });
4927
+ const wiPath = target.wiPath;
4922
4928
 
4923
4929
  let result = null;
4924
4930
  mutateJsonFileLocked(wiPath, (items) => {
@@ -4938,8 +4944,7 @@ const server = http.createServer(async (req, res) => {
4938
4944
  if (result.code !== 200) return jsonReply(res, result.code, result.body);
4939
4945
 
4940
4946
  // Clear dispatch history and cooldowns outside lock
4941
- const sourcePrefix = (!project || project === 'central') ? 'central-work-' : `work-${project}-`;
4942
- const dispatchKey = sourcePrefix + id;
4947
+ const dispatchKey = dispatchPrefixForResolvedSource(target) + id;
4943
4948
  try {
4944
4949
  const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
4945
4950
  mutateJsonFileLocked(dispatchPath, (dispatch) => {
@@ -5005,16 +5010,9 @@ const server = http.createServer(async (req, res) => {
5005
5010
  const { id, source, title, description, type, priority, agent } = body;
5006
5011
  if (!id) return jsonReply(res, 400, { error: 'id required' });
5007
5012
 
5008
- let wiPath;
5009
- if (!source || source === 'central') {
5010
- wiPath = path.join(MINIONS_DIR, 'work-items.json');
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' });
5013
+ const target = resolveProjectSourceTarget(source, PROJECTS);
5014
+ if (target.error) return jsonReply(res, 404, { error: target.error });
5015
+ const wiPath = target.wiPath;
5018
5016
 
5019
5017
  let result = null;
5020
5018
  let agentChanged = false;
@@ -5097,7 +5095,7 @@ const server = http.createServer(async (req, res) => {
5097
5095
  const planFile = 'manual-' + shared.uid() + '.json';
5098
5096
  const plan = {
5099
5097
  version: 'manual-' + new Date().toISOString().slice(0, 10),
5100
- project: target.project?.name || (PROJECTS.length > 0 ? PROJECTS[0].name : 'Unknown'),
5098
+ project: target.project?.name || 'Unknown',
5101
5099
  generated_by: 'dashboard',
5102
5100
  generated_at: new Date().toISOString().slice(0, 10),
5103
5101
  plan_summary: body.name,
@@ -5844,7 +5842,8 @@ const server = http.createServer(async (req, res) => {
5844
5842
  }).join('\n');
5845
5843
 
5846
5844
  const projectName = plan.project || body.file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
5847
- const targetProject = PROJECTS.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) || PROJECTS[0];
5845
+ const projectTarget = resolveProjectSourceTarget(projectName, PROJECTS, { allowCentral: false });
5846
+ const targetProject = projectTarget.project || (PROJECTS.length === 1 ? PROJECTS[0] : null);
5848
5847
  if (targetProject) {
5849
5848
  diffAwareQueued = shared.queuePlanToPrd({
5850
5849
  planFile: plan.source_plan, prdFile: body.file,
@@ -6808,7 +6807,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6808
6807
  let alreadyLinked = false;
6809
6808
  mutateDashboardConfig(config => {
6810
6809
  if (!Array.isArray(config.projects)) config.projects = [];
6811
- alreadyLinked = config.projects.some(p => path.resolve(p.localPath) === target);
6810
+ alreadyLinked = config.projects.some(p => shared.sameResolvedPath(p.localPath, target));
6812
6811
  return config;
6813
6812
  });
6814
6813
  if (alreadyLinked) {
@@ -6853,7 +6852,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6853
6852
  let duplicate = false;
6854
6853
  mutateDashboardConfig(config => {
6855
6854
  if (!Array.isArray(config.projects)) config.projects = [];
6856
- if (config.projects.some(p => path.resolve(p.localPath) === target)) {
6855
+ if (config.projects.some(p => shared.sameResolvedPath(p.localPath, target))) {
6857
6856
  duplicate = true;
6858
6857
  return config;
6859
6858
  }
@@ -7444,9 +7443,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7444
7443
  const body = await readBody(req);
7445
7444
  let { id, cron, title, type, project, agent, description, priority, enabled } = body;
7446
7445
  if (!cron || !title) return jsonReply(res, 400, { error: 'cron and title are required' });
7447
- const projectTarget = shared.resolveConfiguredProject(project, PROJECTS);
7446
+ reloadConfig();
7447
+ const projectTarget = resolveScheduleProjectValue(project, PROJECTS);
7448
7448
  if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
7449
- project = projectTarget.project?.name || null;
7449
+ project = projectTarget.project || null;
7450
7450
 
7451
7451
  // Auto-generate ID from title if not provided
7452
7452
  if (!id) {
@@ -7481,10 +7481,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7481
7481
  const body = await readBody(req);
7482
7482
  let { id, cron, title, type, project, agent, description, priority, enabled } = body;
7483
7483
  if (!id) return jsonReply(res, 400, { error: 'id required' });
7484
+ reloadConfig();
7484
7485
  if (project !== undefined) {
7485
- const projectTarget = shared.resolveConfiguredProject(project, PROJECTS);
7486
+ const projectTarget = resolveScheduleProjectValue(project, PROJECTS);
7486
7487
  if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
7487
- project = projectTarget.project?.name || null;
7488
+ project = projectTarget.project || null;
7488
7489
  }
7489
7490
 
7490
7491
  let missingSchedules = false;
@@ -7546,16 +7547,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7546
7547
  reloadConfig();
7547
7548
  const sched = (CONFIG.schedules || []).find(s => s.id === id);
7548
7549
  if (!sched) return jsonReply(res, 404, { error: 'Schedule not found' });
7549
- if (sched.project) {
7550
- const projectTarget = shared.resolveConfiguredProject(sched.project, PROJECTS);
7551
- if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
7552
- sched.project = projectTarget.project.name;
7553
- }
7550
+ const projectTarget = resolveScheduleProjectValue(sched.project, PROJECTS);
7551
+ if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
7552
+ const schedForRun = { ...sched, project: projectTarget.project || null };
7554
7553
 
7555
7554
  const schedulerMod = require('./engine/scheduler');
7556
7555
  let item;
7557
7556
  try {
7558
- item = schedulerMod.createScheduledWorkItem(sched);
7557
+ item = schedulerMod.createScheduledWorkItem(schedForRun);
7559
7558
  } catch (e) {
7560
7559
  return jsonReply(res, 400, { error: e.message });
7561
7560
  }
@@ -7566,7 +7565,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7566
7565
  meeting = createMeeting({
7567
7566
  title: item.title,
7568
7567
  agenda: item.description,
7569
- participants: Array.isArray(sched.participants) ? sched.participants : [],
7568
+ participants: Array.isArray(schedForRun.participants) ? schedForRun.participants : [],
7570
7569
  });
7571
7570
  } else {
7572
7571
  const centralPath = path.join(MINIONS_DIR, 'work-items.json');
@@ -7585,22 +7584,22 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7585
7584
  return jsonReply(res, 409, {
7586
7585
  error: 'Schedule already has an active work item',
7587
7586
  id: duplicate.id,
7588
- scheduleId: sched.id,
7587
+ scheduleId: schedForRun.id,
7589
7588
  });
7590
7589
  }
7591
7590
  }
7592
7591
 
7593
- const runEntry = schedulerMod.recordScheduleRun(sched.id, item.id);
7592
+ const runEntry = schedulerMod.recordScheduleRun(schedForRun.id, item.id);
7594
7593
  try {
7595
7594
  shared.mutateControl(control => ({ ...control, _wakeupAt: Date.now() }));
7596
7595
  } catch (e) {
7597
- shared.log('warn', `Schedule run-now wakeup failed for ${sched.id}: ${e.message}`);
7596
+ shared.log('warn', `Schedule run-now wakeup failed for ${schedForRun.id}: ${e.message}`);
7598
7597
  }
7599
7598
  invalidateStatusCache();
7600
7599
  return jsonReply(res, 200, {
7601
7600
  ok: true,
7602
7601
  id: item.id,
7603
- scheduleId: sched.id,
7602
+ scheduleId: schedForRun.id,
7604
7603
  lastRun: runEntry?.lastRun || null,
7605
7604
  ...(meeting ? { meetingId: meeting.id } : {}),
7606
7605
  });
@@ -8008,7 +8007,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
8008
8007
  if (body.projects && Array.isArray(body.projects)) {
8009
8008
  if (!config.projects) config.projects = [];
8010
8009
  for (const update of body.projects) {
8011
- const proj = config.projects.find(p => p.name === update.name);
8010
+ const proj = shared.findProjectByName(config.projects, update.name);
8012
8011
  if (!proj) continue;
8013
8012
  if (!proj.workSources) proj.workSources = {};
8014
8013
  if (update.workSources?.pullRequests !== undefined) {
@@ -9042,6 +9041,7 @@ module.exports = {
9042
9041
  _findDuplicateWorkItemCreate: findDuplicateWorkItemCreate,
9043
9042
  _createWorkItemWithDedup: createWorkItemWithDedup,
9044
9043
  _resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
9044
+ _resolveScheduleProjectValue: resolveScheduleProjectValue,
9045
9045
  _collectArchivedWorkItems: collectArchivedWorkItems,
9046
9046
  _createPipelineFromAction: createPipelineFromAction,
9047
9047
  _setCcLocalApiInvokerForTest,