@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/engine.js CHANGED
@@ -592,7 +592,7 @@ async function spawnAgent(dispatchItem, config) {
592
592
  // Resolve prompt — prefers sidecar file when dispatchItem._promptFile is set
593
593
  // (large prompts are written to engine/contexts/<id>.md to keep dispatch.json
594
594
  // small — see shared.sidecarDispatchPrompt / #1167).
595
- const taskPrompt = shared.resolveDispatchPrompt(dispatchItem);
595
+ let taskPrompt = shared.resolveDispatchPrompt(dispatchItem);
596
596
  const claudeConfig = config.claude || {};
597
597
  const engineConfig = config.engine || {};
598
598
  const startedAt = ts();
@@ -601,15 +601,16 @@ async function spawnAgent(dispatchItem, config) {
601
601
 
602
602
  // Resolve project context for this dispatch
603
603
  // meta.project has {name, localPath} — enrich with full config (mainBranch, repoHost, etc.)
604
- const metaProject = meta?.project || {};
604
+ const metaProject = meta?.project;
605
605
  const projects = getProjects(config);
606
- const fullProject = shared.findProjectByNameOrPath(projects, metaProject.name || metaProject.localPath);
607
- if ((metaProject.name || metaProject.localPath) && !fullProject) {
608
- const err = new Error(shared.formatUnknownProjectError(metaProject.name || metaProject.localPath, projects));
606
+ const projectResolution = shared.resolveConfiguredProject(metaProject, projects, { defaultWhenSingle: true });
607
+ if (projectResolution.error) {
608
+ const err = new Error(projectResolution.error);
609
609
  updateAgentStatus(id, AGENT_STATUS.FAILED, err.message);
610
610
  throw err;
611
611
  }
612
- const project = fullProject ? { ...fullProject, ...metaProject } : (projects.length === 1 && !(metaProject.name || metaProject.localPath) ? projects[0] : {});
612
+ const metaProjectFields = metaProject && typeof metaProject === 'object' ? metaProject : {};
613
+ const project = projectResolution.project ? { ...projectResolution.project, ...metaProjectFields } : {};
613
614
  const rootDir = project.localPath ? path.resolve(project.localPath) : path.resolve(MINIONS_DIR, '..');
614
615
 
615
616
  // Determine working directory
@@ -621,8 +622,8 @@ async function spawnAgent(dispatchItem, config) {
621
622
  const _gitOpts = { stdio: 'pipe', timeout: 30000, windowsHide: true, env: shared.gitEnv() };
622
623
  const _worktreeGitOpts = { ..._gitOpts, timeout: worktreeCreateTimeout };
623
624
 
624
- // Build prompt before worktree setup prompt doesn't depend on worktree path
625
- // and this avoids blocking 200ms of file reads behind 20-60s of git operations
625
+ // Build the initial prompt before worktree setup, then refresh shared-branch
626
+ // work-item prompts after setup because reused worktrees can live at arbitrary paths.
626
627
  const systemPrompt = buildSystemPrompt(agentId, config, project);
627
628
  const agentContext = buildAgentContext(agentId, config, project);
628
629
  const pendingSteering = steering.buildPendingSteeringPrompt(agentId);
@@ -642,15 +643,18 @@ async function spawnAgent(dispatchItem, config) {
642
643
  'This report is the primary completion signal; fenced completion blocks are only a fallback.',
643
644
  '',
644
645
  ].join('\n') : '';
645
- const taskPromptWithSteering = pendingSteering.prompt
646
- ? `${pendingSteering.prompt}\n\n---\n\n${taskPrompt}`
647
- : taskPrompt;
648
- const taskPromptWithReport = completionReportInstruction
649
- ? `${taskPromptWithSteering}\n\n---\n\n${completionReportInstruction}`
650
- : taskPromptWithSteering;
651
- const fullTaskPrompt = agentContext
652
- ? `## Agent Context\n\n${agentContext}\n---\n\n## Your Task\n\n${taskPromptWithReport}`
653
- : taskPromptWithReport;
646
+ const buildFullTaskPrompt = (promptBody) => {
647
+ const taskPromptWithSteering = pendingSteering.prompt
648
+ ? `${pendingSteering.prompt}\n\n---\n\n${promptBody}`
649
+ : promptBody;
650
+ const taskPromptWithReport = completionReportInstruction
651
+ ? `${taskPromptWithSteering}\n\n---\n\n${completionReportInstruction}`
652
+ : taskPromptWithSteering;
653
+ return agentContext
654
+ ? `## Agent Context\n\n${agentContext}\n---\n\n## Your Task\n\n${taskPromptWithReport}`
655
+ : taskPromptWithReport;
656
+ };
657
+ let fullTaskPrompt = buildFullTaskPrompt(taskPrompt);
654
658
  const tmpDir = path.join(ENGINE_DIR, 'tmp');
655
659
  if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
656
660
  const safeId = id.replace(/[:\\/*?"<>|]/g, '-');
@@ -1064,6 +1068,25 @@ async function spawnAgent(dispatchItem, config) {
1064
1068
 
1065
1069
  updateAgentStatus(id, AGENT_STATUS.READY, 'Worktree ready, preparing to spawn process');
1066
1070
 
1071
+ if (worktreePath && meta?.source === 'work-item' && meta?.item?.branchStrategy === 'shared-branch') {
1072
+ const refreshed = renderProjectWorkItemPromptForAgent(
1073
+ meta.item,
1074
+ routing.normalizeWorkType(type, WORK_TYPE.IMPLEMENT),
1075
+ agentId,
1076
+ config,
1077
+ project,
1078
+ rootDir,
1079
+ branchName,
1080
+ { worktreePath }
1081
+ );
1082
+ if (refreshed.prompt) {
1083
+ taskPrompt = refreshed.prompt;
1084
+ fullTaskPrompt = buildFullTaskPrompt(taskPrompt);
1085
+ safeWrite(promptPath, fullTaskPrompt);
1086
+ log('info', `Refreshed shared-branch prompt for ${id} with worktree ${worktreePath}`);
1087
+ }
1088
+ }
1089
+
1067
1090
  // Inject dirty file list when worktree has uncommitted changes (e.g., max_turns retry)
1068
1091
  // This signals to the respawned agent that prior work exists in the worktree (#960)
1069
1092
  if (worktreePath && fs.existsSync(worktreePath)) {
@@ -1714,7 +1737,7 @@ async function spawnAgent(dispatchItem, config) {
1714
1737
  }
1715
1738
  }, 5000);
1716
1739
 
1717
- // Move pending -> active under a lock to avoid cross-process lost updates (engine/dashboard)
1740
+ // Move pending -> active under lock to avoid lost updates.
1718
1741
  mutateDispatch((dispatch) => {
1719
1742
  const idx = dispatch.pending.findIndex(d => d.id === id);
1720
1743
  if (idx < 0) return dispatch;
@@ -1736,9 +1759,7 @@ async function spawnAgent(dispatchItem, config) {
1736
1759
  // reads dispatchItem.started_at for runtimeMs. (W-moux9nwn0008f923)
1737
1760
  dispatchItem.started_at = startedAt;
1738
1761
 
1739
- // Atomically stamp dispatched_to/dispatched_at on the originating work item (#402)
1740
- // The discover phase sets these via safeWrite which can race with concurrent writes;
1741
- // this locked write ensures the fields are persisted reliably.
1762
+ // Atomically stamp dispatched_to/dispatched_at on the originating work item (#402).
1742
1763
  if (meta?.item?.id) {
1743
1764
  try {
1744
1765
  let wiPath = null;
@@ -2248,7 +2269,7 @@ function materializePlansAsWorkItems(config) {
2248
2269
 
2249
2270
  const defaultProjectName = plan.project || file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
2250
2271
  const allProjects = getProjects(config);
2251
- const defaultProject = allProjects.find(p => p.name?.toLowerCase() === defaultProjectName.toLowerCase());
2272
+ const defaultProject = shared.resolveProjectSource(defaultProjectName, allProjects, { allowCentral: false }).project;
2252
2273
  // No project found — use central work-items.json (engine works without projects)
2253
2274
  const useCentral = !defaultProject;
2254
2275
 
@@ -2268,7 +2289,7 @@ function materializePlansAsWorkItems(config) {
2268
2289
  const itemsByProject = new Map(); // projectName -> { project, items: [] }
2269
2290
  for (const item of items) {
2270
2291
  if (item.project) {
2271
- const itemProject = allProjects.find(p => p.name?.toLowerCase() === String(item.project).toLowerCase());
2292
+ const itemProject = shared.resolveProjectSource(item.project, allProjects, { allowCentral: false }).project;
2272
2293
  if (!itemProject) {
2273
2294
  const error = shared.formatUnknownProjectError(item.project, allProjects);
2274
2295
  log('warn', `PRD ${file} item ${item.id || item.name}: ${error}`);
@@ -2288,7 +2309,7 @@ function materializePlansAsWorkItems(config) {
2288
2309
  itemsByProject.get('_central').items.push(item);
2289
2310
  } else {
2290
2311
  const itemProjectName = defaultProjectName;
2291
- const itemProject = allProjects.find(p => p.name?.toLowerCase() === itemProjectName.toLowerCase()) || defaultProject;
2312
+ const itemProject = shared.resolveProjectSource(itemProjectName, allProjects, { allowCentral: false }).project || defaultProject;
2292
2313
  if (!itemProject) continue;
2293
2314
  if (!itemsByProject.has(itemProject.name)) {
2294
2315
  itemsByProject.set(itemProject.name, { project: itemProject, items: [] });
@@ -2333,7 +2354,7 @@ function materializePlansAsWorkItems(config) {
2333
2354
  let alreadyExists = !!existingWi;
2334
2355
  if (!alreadyExists) {
2335
2356
  for (const p of allProjects) {
2336
- if (p.name === projName) continue;
2357
+ if (String(p.name || '').toLowerCase() === String(projName || '').toLowerCase()) continue;
2337
2358
  const otherItems = safeJson(projectWorkItemsPath(p)) || [];
2338
2359
  const otherWi = otherItems.find(w => w.id === item.id);
2339
2360
  if (otherWi) {
@@ -2399,7 +2420,7 @@ function materializePlansAsWorkItems(config) {
2399
2420
 
2400
2421
  // Process cross-project re-opens outside the lock (no nested locks)
2401
2422
  for (const { itemId, projectName: rProjName, item: rItem } of deferredReopens) {
2402
- const rProject = allProjects.find(p => p.name === rProjName);
2423
+ const rProject = shared.resolveProjectSource(rProjName, allProjects, { allowCentral: false }).project;
2403
2424
  if (!rProject) continue;
2404
2425
  const rPath = projectWorkItemsPath(rProject);
2405
2426
  mutateWorkItems(rPath, items => {
@@ -2631,7 +2652,7 @@ function isPrAutomationCausePending(project, pr, causeKey) {
2631
2652
  if (d.meta?.automationCauseKey !== causeKey) return false;
2632
2653
  if (!prCanonicalId) return true;
2633
2654
  const dispatchProject = d.meta?.project?.name
2634
- ? (getProjects(getConfig()).find(p => p.name === d.meta.project.name) || d.meta.project)
2655
+ ? (shared.resolveProjectSource(d.meta.project.name, getProjects(getConfig()), { allowCentral: false }).project || d.meta.project)
2635
2656
  : (d.meta?.project || null);
2636
2657
  const dispatchPrId = shared.getCanonicalPrId(dispatchProject, d.meta?.pr, d.meta?.pr?.url || '');
2637
2658
  return !dispatchPrId || dispatchPrId === prCanonicalId;
@@ -2678,8 +2699,6 @@ async function discoverFromPrs(config, project) {
2678
2699
  const newWork = [];
2679
2700
 
2680
2701
  const projMeta = { name: project?.name, localPath: project?.localPath };
2681
- const projectsByName = new Map(shared.getProjects(config).map(p => [p.name, p]));
2682
-
2683
2702
  // Resolve poll-enabled per project — stale reviewStatus is untrustworthy without poller
2684
2703
  const isAdoProject = project?.repoHost !== 'github';
2685
2704
  const pollEnabled = isAdoProject
@@ -2699,7 +2718,7 @@ async function discoverFromPrs(config, project) {
2699
2718
  .filter(d => d.meta?.pr?.id)
2700
2719
  .map(d => {
2701
2720
  const dispatchProject = d.meta?.project?.name
2702
- ? (projectsByName.get(d.meta.project.name) || d.meta.project)
2721
+ ? (shared.resolveProjectSource(d.meta.project.name, shared.getProjects(config), { allowCentral: false }).project || d.meta.project)
2703
2722
  : (d.meta?.project || null);
2704
2723
  return shared.getCanonicalPrId(dispatchProject, d.meta.pr, d.meta.pr?.url || '');
2705
2724
  })
@@ -3074,7 +3093,8 @@ async function discoverFromPrs(config, project) {
3074
3093
  /**
3075
3094
  * Scan work-items.json for manually queued tasks
3076
3095
  */
3077
- function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, project, root, branchName) {
3096
+ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, project, root, branchName, options = {}) {
3097
+ const worktreePath = options.worktreePath || path.resolve(root, config.engine?.worktreeRoot || '../worktrees', `${branchName}`);
3078
3098
  const vars = {
3079
3099
  ...buildBaseVars(agentId, config, project),
3080
3100
  item_id: item.id,
@@ -3091,7 +3111,7 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
3091
3111
  scope_section: `## Scope: Project — ${project?.name || 'default'}\n\nThis task is scoped to a single project.`,
3092
3112
  branch_name: branchName,
3093
3113
  project_path: root,
3094
- worktree_path: path.resolve(root, config.engine?.worktreeRoot || '../worktrees', `${branchName}`),
3114
+ worktree_path: worktreePath,
3095
3115
  commit_message: item.commitMessage || `feat: ${item.title || item.id}`,
3096
3116
  notes_content: '',
3097
3117
  pr_id: item.pr_id || item._pr || item.targetPr || item.sourcePr || item.pr || '',
@@ -3157,15 +3177,8 @@ function withWorkItemPrContext(item, pr) {
3157
3177
  function projectFromDispatchMeta(metaProject, config) {
3158
3178
  if (!metaProject) return null;
3159
3179
  const projects = getProjects(config);
3160
- if (metaProject.name) {
3161
- const byName = projects.find(p => p.name === metaProject.name);
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
- }
3180
+ const resolved = shared.resolveProjectSource(metaProject, projects, { allowCentral: false });
3181
+ if (resolved.project) return resolved.project;
3169
3182
  return metaProject;
3170
3183
  }
3171
3184
 
@@ -3707,7 +3720,6 @@ function discoverCentralWorkItems(config) {
3707
3720
  const items = safeJson(centralPath) || [];
3708
3721
  const projects = getProjects(config);
3709
3722
  const dispatchProjects = getCentralDispatchProjects(projects);
3710
- const projectsByName = new Map(dispatchProjects.map(p => [String(p.name || '').toLowerCase(), p]));
3711
3723
  const newWork = [];
3712
3724
  // Collect mutations to apply atomically inside lock callback (avoids TOCTOU)
3713
3725
  const mutations = new Map(); // item.id → { field: value, ... }
@@ -3750,9 +3762,9 @@ function discoverCentralWorkItems(config) {
3750
3762
 
3751
3763
  const workType = routing.normalizeWorkType(item.type, WORK_TYPE.IMPLEMENT);
3752
3764
  const isFanOut = item.scope === 'fan-out';
3753
- const explicitItemProject = typeof item.project === 'string' ? item.project : item.project?.name;
3754
- if (explicitItemProject && !projectsByName.get(String(explicitItemProject).toLowerCase())) {
3755
- const error = shared.formatUnknownProjectError(explicitItemProject, dispatchProjects);
3765
+ const itemProjectResolution = shared.resolveConfiguredProject(item.project, projects);
3766
+ if (itemProjectResolution.error) {
3767
+ const error = itemProjectResolution.error;
3756
3768
  mutations.set(item.id, { status: WI_STATUS.FAILED, failReason: error, failedAt: ts() });
3757
3769
  log('warn', `central work item ${item.id}: ${error}`);
3758
3770
  continue;
@@ -3856,10 +3868,11 @@ function discoverCentralWorkItems(config) {
3856
3868
  planReadError = e;
3857
3869
  }
3858
3870
  }
3859
- const requestedProjectName = declaredPlanProject || (typeof item.project === 'string' ? item.project : item.project?.name);
3860
- const requestedProject = requestedProjectName ? projectsByName.get(String(requestedProjectName).toLowerCase()) : null;
3861
- if (requestedProjectName && !requestedProject) {
3862
- const error = shared.formatUnknownProjectError(requestedProjectName, dispatchProjects);
3871
+ const requestedProjectResolution = declaredPlanProject
3872
+ ? shared.resolveConfiguredProject(declaredPlanProject, projects)
3873
+ : itemProjectResolution;
3874
+ if (requestedProjectResolution.error) {
3875
+ const error = requestedProjectResolution.error;
3863
3876
  mutations.set(item.id, {
3864
3877
  status: WI_STATUS.FAILED,
3865
3878
  failReason: error,
@@ -3869,7 +3882,7 @@ function discoverCentralWorkItems(config) {
3869
3882
  log('warn', `central work item ${item.id}: ${error}`);
3870
3883
  continue;
3871
3884
  }
3872
- const targetProject = requestedProject || (dispatchProjects.length === 1 ? dispatchProjects[0] : null);
3885
+ const targetProject = requestedProjectResolution.project || (projects.length === 1 ? projects[0] : null);
3873
3886
  if (declaredPlanProject) {
3874
3887
  const projectMutation = { project: targetProject.name, _declaredPlanProject: declaredPlanProject };
3875
3888
  mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, projectMutation));
@@ -4920,6 +4933,7 @@ module.exports = {
4920
4933
 
4921
4934
  // Playbooks
4922
4935
  renderPlaybook, validatePlaybookVars, PLAYBOOK_REQUIRED_VARS, buildWorkItemDispatchVars,
4936
+ renderProjectWorkItemPromptForAgent, // exported for testing
4923
4937
 
4924
4938
  // Timeout / Steering / Idle (re-exported from engine/timeout.js)
4925
4939
  checkTimeouts, checkSteering, checkIdleThreshold,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1810",
3
+ "version": "0.1.1812",
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"
@@ -18,6 +18,18 @@ Before answering, classify the human's chat message:
18
18
 
19
19
  Do not emit `===ACTIONS===` or fenced `action` JSON for normal small document questions, summaries, rewrites, extraction, or localized edits. If a small task becomes medium/large after inspection, stop and dispatch a work item rather than pushing through in doc-chat.
20
20
 
21
+ ## Explicit Dispatch/Delegation Hard Stop
22
+
23
+ Explicit dispatch/delegation intent hard stop: when the human's chat message asks to `dispatch`, `delegate`, `assign`, `have Minions...`, `open work items for...`, `queue fixes for...`, or otherwise asks another agent/minion/work item to do the work, you MUST emit `===ACTIONS===` with dispatch JSON when the required fields are available, and you MUST NOT use `Write`, `Edit`, or `Bash` against any file. If a required dispatch field is genuinely unknown, ask for that missing field in plain text; do not edit files or run Bash while waiting for clarification.
24
+
25
+ This rule takes precedence over all document-editing paths below. Do not "helpfully" start implementing, fixing, or testing inline after acknowledging a dispatch/delegation request. The document may mention source files, contain findings, or list exact edits to make; those references are inputs for the dispatched work item's description, not permission to edit those files in doc-chat.
26
+
27
+ Negative example A: if a findings or audit document is open and the human says "dispatch fixes for every issue", the correct response is one or more dispatch actions. Do not use `Edit`, `Write`, or `Bash` on source files referenced by the document, such as `engine.js`, `engine/pipeline.js`, or `test/unit.test.js`, even if the findings make the fix look obvious.
28
+
29
+ Negative example B: if the human says "go fix these items", "implement this list", "have minions tackle these", or similar delegation phrasing, the correct response is dispatch action JSON. Do not edit the referenced implementation files directly from doc-chat.
30
+
31
+ ANY `Edit` or `Write` call targeting a path other than the document-context `filePath` is forbidden, regardless of how compelling, urgent, or specific the human request sounds. If the requested work spans any file other than the current document filePath, dispatch it instead of editing it.
32
+
21
33
  ## Minions Orchestration Requests
22
34
 
23
35
  For explicit dispatch/delegation requests or medium/larger work without a direct-handling override, emit the same Command Center work-item action shape:
@@ -48,6 +60,6 @@ For wholesale rewrites, format conversions, or changes touching most of the file
48
60
 
49
61
  ### Rules for both paths
50
62
 
51
- - Never edit any file other than the one named in the document context.
63
+ - Never edit any file other than the one named in the document context. `Edit`/`Write` against any other path is forbidden; if the work needs other files, dispatch it.
52
64
  - If the user is asking a question rather than requesting an edit, do not edit. Answer in plain text.
53
65
  - If a JSON file's edit would invalidate it, prefer the whole-file rewrite path so the server can validate the result before persisting.