@yemi33/minions 0.1.1812 → 0.1.1813

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1813 (2026-05-09)
4
+
5
+ ### Features
6
+ - improve manual PR project inference (#2242)
7
+
3
8
  ## 0.1.1812 (2026-05-09)
4
9
 
5
10
  ### Features
@@ -1340,8 +1340,10 @@ async function ccExecuteAction(action, targetTabId) {
1340
1340
  break;
1341
1341
  }
1342
1342
  case 'link-pr': {
1343
- await _ccFetch('/api/pull-requests/link', { url: action.url, title: action.title || '', project: action.project || '', autoObserve: action.autoObserve !== false });
1344
- status.innerHTML = '&#10003; PR linked: <strong>' + escHtml(action.url) + '</strong>';
1343
+ var prLinkRes = await _ccFetch('/api/pull-requests/link', { url: action.url, title: action.title || '', project: action.project || '', autoObserve: action.autoObserve !== false });
1344
+ var prLinkData = await prLinkRes.json().catch(function() { return {}; });
1345
+ status.innerHTML = '&#10003; PR linked: <strong>' + escHtml(action.url) + '</strong>' +
1346
+ (prLinkData.message ? '<div style="font-size:11px;color:var(--muted);margin-top:4px">' + escHtml(prLinkData.message) + '</div>' : '');
1345
1347
  status.style.color = 'var(--green)';
1346
1348
  break;
1347
1349
  }
@@ -125,7 +125,7 @@ function openAddPrModal() {
125
125
  '<div style="display:flex;flex-direction:column;gap:10px">' +
126
126
  '<label style="color:var(--text);font-size:var(--text-md)">PR URL <input id="pr-link-url" style="' + inputStyle + '" placeholder="https://github.com/org/repo/pull/123"></label>' +
127
127
  '<label style="color:var(--text);font-size:var(--text-md)">Title <input id="pr-link-title" style="' + inputStyle + '" placeholder="Short description (optional — auto-detected from URL)"></label>' +
128
- '<label style="color:var(--text);font-size:var(--text-md)">Project <select id="pr-link-project" style="' + inputStyle + '"><option value="">Auto / Central</option>' + projOpts + '</select></label>' +
128
+ '<label style="color:var(--text);font-size:var(--text-md)">Project <select id="pr-link-project" style="' + inputStyle + '"><option value="">Auto-detect from URL (central if no unique match)</option>' + projOpts + '</select></label>' +
129
129
  '<label style="color:var(--text);font-size:var(--text-md)">Context <textarea id="pr-link-context" rows="3" style="' + inputStyle + ';resize:vertical" placeholder="Why are you linking this? What should agents know about it?"></textarea></label>' +
130
130
  '<label style="display:flex;align-items:center;gap:8px;color:var(--text);font-size:var(--text-md);margin-top:4px;cursor:pointer">' +
131
131
  '<input type="checkbox" id="pr-link-observe" style="width:16px;height:16px;accent-color:var(--blue)">' +
@@ -151,14 +151,16 @@ async function _submitLinkPr(e) {
151
151
  const autoObserve = document.getElementById('pr-link-observe')?.checked || false;
152
152
 
153
153
  try { closeModal(); } catch { /* expected */ }
154
- showToast('cmd-toast', 'PR linked' + (autoObserve ? ' (auto-observe on)' : ''), true);
155
154
  try {
156
155
  const res = await fetch('/api/pull-requests/link', {
157
156
  method: 'POST', headers: { 'Content-Type': 'application/json' },
158
157
  body: JSON.stringify({ url, title, project, context, autoObserve })
159
158
  });
160
159
  const data = await res.json();
161
- if (res.ok) { refresh(); } else { alert('Failed: ' + (data.error || 'unknown')); openAddPrModal(); }
160
+ if (res.ok) {
161
+ showToast('cmd-toast', (data.message || 'PR linked') + (autoObserve ? ' (auto-observe on)' : ''), true, 6000);
162
+ refresh();
163
+ } else { alert('Failed: ' + (data.error || 'unknown')); openAddPrModal(); }
162
164
  } catch (e) { alert('Error: ' + e.message); openAddPrModal(); }
163
165
  }
164
166
 
package/dashboard.js CHANGED
@@ -382,6 +382,83 @@ function findProjectByName(projects, projectName) {
382
382
  return shared.findProjectByName(projects, projectName);
383
383
  }
384
384
 
385
+ function resolveManualPrLinkProject(url, projectName, projects = PROJECTS) {
386
+ const explicitProjectName = String(projectName || '').trim();
387
+ if (explicitProjectName) {
388
+ const explicitProject = shared.resolveProjectSource(explicitProjectName, projects, { allowCentral: false, minionsDir: MINIONS_DIR });
389
+ if (explicitProject.error) {
390
+ const err = new Error(explicitProject.error);
391
+ err.statusCode = 400;
392
+ throw err;
393
+ }
394
+ const targetProject = explicitProject.project;
395
+ return {
396
+ project: targetProject,
397
+ resolution: {
398
+ reason: 'explicit',
399
+ project: targetProject.name || explicitProjectName,
400
+ storage: 'project',
401
+ message: `Linked to selected project "${targetProject.name || explicitProjectName}".`,
402
+ },
403
+ };
404
+ }
405
+
406
+ const parsedPr = shared.parsePrUrl(url);
407
+ const prScope = parsedPr?.scope || '';
408
+ if (!prScope) {
409
+ return {
410
+ project: null,
411
+ resolution: {
412
+ reason: 'unrecognized-url',
413
+ project: 'central',
414
+ storage: 'central',
415
+ message: 'Could not infer a project from this PR URL; linked in central PR tracking.',
416
+ },
417
+ };
418
+ }
419
+
420
+ const matches = projects.filter(p => shared.getProjectPrScope(p) === prScope);
421
+ if (matches.length === 1) {
422
+ const targetProject = matches[0];
423
+ return {
424
+ project: targetProject,
425
+ resolution: {
426
+ reason: 'inferred',
427
+ scope: prScope,
428
+ project: targetProject.name || '',
429
+ storage: 'project',
430
+ message: `Inferred project "${targetProject.name}" from PR scope ${prScope}.`,
431
+ },
432
+ };
433
+ }
434
+
435
+ if (matches.length > 1) {
436
+ const names = matches.map(p => p.name).filter(Boolean);
437
+ return {
438
+ project: null,
439
+ resolution: {
440
+ reason: 'ambiguous',
441
+ scope: prScope,
442
+ project: 'central',
443
+ storage: 'central',
444
+ matches: names,
445
+ message: `PR scope ${prScope} matches multiple configured projects (${names.join(', ')}); linked in central PR tracking. Select a project to attach it.`,
446
+ },
447
+ };
448
+ }
449
+
450
+ return {
451
+ project: null,
452
+ resolution: {
453
+ reason: 'unmatched',
454
+ scope: prScope,
455
+ project: 'central',
456
+ storage: 'central',
457
+ message: `No configured project matches PR scope ${prScope}; linked in central PR tracking.`,
458
+ },
459
+ };
460
+ }
461
+
385
462
  function resolveWorkItemsCreateTarget(projectName, projects = PROJECTS) {
386
463
  const target = shared.resolveProjectSource(projectName, projects, { defaultWhenSingle: true, minionsDir: MINIONS_DIR });
387
464
  if (target.error) return { error: target.error };
@@ -416,6 +493,73 @@ function findWorkItemsTargetById(id, source, projects = PROJECTS) {
416
493
  return { found: false };
417
494
  }
418
495
 
496
+ function buildPlanWorkItem(body, projects = PROJECTS, options = {}) {
497
+ if (!body?.title || !String(body.title).trim()) return { error: 'title is required' };
498
+ const target = resolveWorkItemsCreateTarget(body.project, projects);
499
+ if (target.error) return { error: target.error };
500
+ let now = options.now ? new Date(options.now) : new Date();
501
+ if (!Number.isFinite(now.getTime())) now = new Date();
502
+ const item = {
503
+ id: options.id || ('W-' + shared.uid()),
504
+ title: body.title,
505
+ type: 'plan',
506
+ priority: body.priority || 'high',
507
+ description: body.description || '',
508
+ status: WI_STATUS.PENDING,
509
+ created: now.toISOString(),
510
+ createdBy: 'dashboard',
511
+ branchStrategy: body.branch_strategy || body.branchStrategy || 'parallel',
512
+ };
513
+ if (target.project) item.project = target.project.name;
514
+ if (body.agent) item.agent = body.agent;
515
+ return { item, id: item.id };
516
+ }
517
+
518
+ function buildManualPrdItemPlan(body, projects = PROJECTS, options = {}) {
519
+ if (!body?.name || !String(body.name).trim()) return { error: 'name is required' };
520
+ const target = resolveWorkItemsCreateTarget(body.project, projects);
521
+ if (target.error) return { error: target.error };
522
+
523
+ const overrideProject = body.item_project ?? body.itemProject;
524
+ const itemProjectTarget = overrideProject !== undefined
525
+ ? shared.resolveConfiguredProject(overrideProject, projects)
526
+ : null;
527
+ if (itemProjectTarget?.error) return { error: itemProjectTarget.error };
528
+
529
+ let now = options.now ? new Date(options.now) : new Date();
530
+ if (!Number.isFinite(now.getTime())) now = new Date();
531
+ const nowTime = now.getTime();
532
+ const projectName = target.project?.name || (projects.length > 0 ? projects[0].name : 'Unknown');
533
+ const itemProjectName = itemProjectTarget?.project?.name || target.project?.name || (projects.length === 1 ? projects[0].name : '');
534
+ const item = {
535
+ id: body.id || ('M' + String(nowTime).slice(-4)),
536
+ name: String(body.name).trim(),
537
+ description: body.description || '',
538
+ priority: body.priority || 'medium',
539
+ estimated_complexity: body.estimated_complexity || 'medium',
540
+ status: 'missing',
541
+ depends_on: [],
542
+ acceptance_criteria: [],
543
+ };
544
+ if (itemProjectName) item.project = itemProjectName;
545
+
546
+ return {
547
+ plan: {
548
+ version: 'manual-' + now.toISOString().slice(0, 10),
549
+ project: projectName,
550
+ generated_by: 'dashboard',
551
+ generated_at: now.toISOString().slice(0, 10),
552
+ plan_summary: item.name,
553
+ status: 'approved',
554
+ requires_approval: false,
555
+ branch_strategy: 'parallel',
556
+ missing_features: [item],
557
+ open_questions: [],
558
+ },
559
+ id: item.id,
560
+ };
561
+ }
562
+
419
563
  function validatePipelineProjects(pipeline, projects = PROJECTS) {
420
564
  const refs = [];
421
565
  const collect = (value) => {
@@ -488,21 +632,7 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
488
632
  throw err;
489
633
  }
490
634
  const projects = shared.getProjects(config);
491
- const explicitProjectName = String(projectName || '').trim();
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);
498
- err.statusCode = 400;
499
- throw err;
500
- }
501
- if (!explicitProjectName) {
502
- const prScope = shared.parsePrUrl(url)?.scope || '';
503
- const matches = prScope ? projects.filter(p => shared.getProjectPrScope(p) === prScope) : [];
504
- if (matches.length === 1) targetProject = matches[0];
505
- }
635
+ const { project: targetProject, resolution: projectResolution } = resolveManualPrLinkProject(url, projectName, projects);
506
636
  const prPath = targetProject ? shared.projectPrPath(targetProject) : shared.centralPullRequestsPath(MINIONS_DIR);
507
637
 
508
638
  const prNumMatch = url.match(/\/pull\/(\d+)|pullrequest\/(\d+)/);
@@ -527,11 +657,12 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
527
657
  _contextOnly: !autoObserve,
528
658
  _autoObserve: !!autoObserve,
529
659
  _context: contextText,
660
+ _projectResolution: projectResolution,
530
661
  }, {
531
662
  project: targetProject,
532
663
  itemId: linkedWorkItemId,
533
664
  });
534
- return { ...result, prPath, targetProject, prNum };
665
+ return { ...result, prPath, targetProject, projectResolution, prNum };
535
666
  }
536
667
 
537
668
  function _normalizeSkillDirForCompare(dir) {
@@ -5064,21 +5195,12 @@ const server = http.createServer(async (req, res) => {
5064
5195
  try {
5065
5196
  const body = await readBody(req);
5066
5197
  if (!body.title || !body.title.trim()) return jsonReply(res, 400, { error: 'title is required' });
5067
- const target = resolveWorkItemsCreateTarget(body.project);
5068
- if (target.error) return jsonReply(res, 400, { error: target.error });
5198
+ const planWorkItem = buildPlanWorkItem(body);
5199
+ if (planWorkItem.error) return jsonReply(res, 400, { error: planWorkItem.error });
5069
5200
  // Write as a work item with type 'plan' — user must explicitly execute plan-to-prd after reviewing
5070
5201
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
5071
- const id = 'W-' + shared.uid();
5072
- const item = {
5073
- id, title: body.title, type: 'plan',
5074
- priority: body.priority || 'high', description: body.description || '',
5075
- status: WI_STATUS.PENDING, created: new Date().toISOString(), createdBy: 'dashboard',
5076
- branchStrategy: body.branch_strategy || body.branchStrategy || 'parallel',
5077
- };
5078
- if (target.project) item.project = target.project.name;
5079
- if (body.agent) item.agent = body.agent;
5080
- mutateWorkItems(wiPath, items => { items.push(item); });
5081
- return jsonReply(res, 200, { ok: true, id, agent: body.agent || '' });
5202
+ mutateWorkItems(wiPath, items => { items.push(planWorkItem.item); });
5203
+ return jsonReply(res, 200, { ok: true, id: planWorkItem.id, agent: body.agent || '' });
5082
5204
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
5083
5205
  }
5084
5206
 
@@ -5086,31 +5208,14 @@ const server = http.createServer(async (req, res) => {
5086
5208
  try {
5087
5209
  const body = await readBody(req);
5088
5210
  if (!body.name || !body.name.trim()) return jsonReply(res, 400, { error: 'name is required' });
5089
- const target = resolveWorkItemsCreateTarget(body.project);
5090
- if (target.error) return jsonReply(res, 400, { error: target.error });
5211
+ const manualPrd = buildManualPrdItemPlan(body);
5212
+ if (manualPrd.error) return jsonReply(res, 400, { error: manualPrd.error });
5091
5213
 
5092
5214
  if (!fs.existsSync(PRD_DIR)) fs.mkdirSync(PRD_DIR, { recursive: true });
5093
5215
 
5094
- const id = body.id || ('M' + String(Date.now()).slice(-4));
5095
5216
  const planFile = 'manual-' + shared.uid() + '.json';
5096
- const plan = {
5097
- version: 'manual-' + new Date().toISOString().slice(0, 10),
5098
- project: target.project?.name || 'Unknown',
5099
- generated_by: 'dashboard',
5100
- generated_at: new Date().toISOString().slice(0, 10),
5101
- plan_summary: body.name,
5102
- status: 'approved',
5103
- requires_approval: false,
5104
- branch_strategy: 'parallel',
5105
- missing_features: [{
5106
- id, name: body.name, description: body.description || '',
5107
- priority: body.priority || 'medium', estimated_complexity: body.estimated_complexity || 'medium',
5108
- status: 'missing', depends_on: [], acceptance_criteria: [],
5109
- }],
5110
- open_questions: [],
5111
- };
5112
- safeWrite(path.join(PRD_DIR, planFile), plan);
5113
- return jsonReply(res, 200, { ok: true, id, file: planFile });
5217
+ safeWrite(path.join(PRD_DIR, planFile), manualPrd.plan);
5218
+ return jsonReply(res, 200, { ok: true, id: manualPrd.id, file: planFile });
5114
5219
  } catch (e) { return jsonReply(res, 400, { error: e.message }); }
5115
5220
  }
5116
5221
 
@@ -8385,7 +8490,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
8385
8490
  { method: 'GET', path: /^\/api\/plans\/([^?]+)$/, template: '/api/plans/:file', desc: 'Read a full plan (JSON from prd/ or markdown from plans/)', handler: handlePlansRead },
8386
8491
 
8387
8492
  // PRD items
8388
- { method: 'POST', path: '/api/prd-items', desc: 'Create a PRD item as a plan file in prd/ (auto-approved)', params: 'name, description?, priority?, estimated_complexity?, project?, id?', handler: handlePrdItemsCreate },
8493
+ { method: 'POST', path: '/api/prd-items', desc: 'Create a PRD item as a plan file in prd/ (auto-approved)', params: 'name, description?, priority?, estimated_complexity?, project?, item_project?, id?', handler: handlePrdItemsCreate },
8389
8494
  { method: 'POST', path: '/api/prd-items/update', desc: 'Edit a PRD item in its source plan JSON', params: 'source, itemId, name?, description?, priority?, estimated_complexity?, status?', handler: handlePrdItemsUpdate },
8390
8495
  { method: 'POST', path: '/api/prd-items/remove', desc: 'Remove a PRD item from plan + cancel materialized work item', params: 'source, itemId', handler: handlePrdItemsRemove },
8391
8496
  // /api/prd/regenerate removed — use /api/plans/approve which does diff-aware update
@@ -8419,9 +8524,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
8419
8524
  } catch (e) {
8420
8525
  return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
8421
8526
  }
8422
- const { id: prId, prPath, prNum, created, linked } = linkResult;
8527
+ const { id: prId, prPath, prNum, created, linked, targetProject, projectResolution } = linkResult;
8423
8528
  invalidateStatusCache();
8424
- jsonReply(res, 200, { ok: true, id: prId, created, linked });
8529
+ jsonReply(res, 200, {
8530
+ ok: true,
8531
+ id: prId,
8532
+ created,
8533
+ linked,
8534
+ project: targetProject?.name || 'central',
8535
+ projectResolution,
8536
+ message: projectResolution?.message || 'PR linked.',
8537
+ });
8425
8538
 
8426
8539
  // Async-enrich: fetch title, description, branch, author from GitHub/ADO API
8427
8540
  (async () => {
@@ -9041,6 +9154,8 @@ module.exports = {
9041
9154
  _findDuplicateWorkItemCreate: findDuplicateWorkItemCreate,
9042
9155
  _createWorkItemWithDedup: createWorkItemWithDedup,
9043
9156
  _resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
9157
+ _buildPlanWorkItem: buildPlanWorkItem,
9158
+ _buildManualPrdItemPlan: buildManualPrdItemPlan,
9044
9159
  _resolveScheduleProjectValue: resolveScheduleProjectValue,
9045
9160
  _collectArchivedWorkItems: collectArchivedWorkItems,
9046
9161
  _createPipelineFromAction: createPipelineFromAction,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "runtime": "copilot",
3
3
  "models": null,
4
- "cachedAt": "2026-05-09T08:52:32.908Z"
4
+ "cachedAt": "2026-05-09T09:32:10.535Z"
5
5
  }
@@ -391,9 +391,10 @@ function executeTaskStage(stage, stageState, run, config, pipeline = {}) {
391
391
  }
392
392
  }
393
393
  const projects = [...new Set(touchedProjects)];
394
- const artifacts = { workItems: [...new Set(createdIds)] };
395
- if (projects.some(Boolean)) artifacts.projects = projects;
396
- return { status: PIPELINE_STATUS.RUNNING, artifacts };
394
+ return {
395
+ status: PIPELINE_STATUS.RUNNING,
396
+ artifacts: { workItems: [...new Set(createdIds)], projects },
397
+ };
397
398
  }
398
399
 
399
400
  function executeTaskStageLegacy(stage, stageState, run, config) {
package/engine/shared.js CHANGED
@@ -2963,7 +2963,7 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
2963
2963
  target[key] = normalizedEntry[key];
2964
2964
  }
2965
2965
  }
2966
- for (const key of ['_manual', '_autoObserve', '_context']) {
2966
+ for (const key of ['_manual', '_autoObserve', '_context', '_projectResolution']) {
2967
2967
  if (normalizedEntry[key] != null) target[key] = normalizedEntry[key];
2968
2968
  }
2969
2969
  if (normalizedEntry._contextOnly != null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1812",
3
+ "version": "0.1.1813",
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"