@yemi33/minions 0.1.1874 → 0.1.1875

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.1875 (2026-05-11)
4
+
5
+ ### Features
6
+ - real multi-project plan + PRD support
7
+
3
8
  ## 0.1.1874 (2026-05-11)
4
9
 
5
10
  ### Fixes
@@ -16,7 +16,7 @@ function openCreatePlanModal() {
16
16
  document.getElementById('modal-body').innerHTML =
17
17
  '<div style="display:flex;flex-direction:column;gap:10px">' +
18
18
  '<label style="color:var(--text);font-size:var(--text-md)">Title <input id="plan-new-title" style="' + inputStyle + '" placeholder="e.g. Add user authentication with JWT"></label>' +
19
- '<label style="color:var(--text);font-size:var(--text-md)">Project <select id="plan-new-project" style="' + inputStyle + '"><option value="">Auto</option>' + projOpts + '</select></label>' +
19
+ '<label style="color:var(--text);font-size:var(--text-md)">Project <select id="plan-new-project" style="' + inputStyle + '"><option value="">Multiple / cross-repo (agent routes per item)</option>' + projOpts + '</select><span style="display:block;font-size:10px;color:var(--muted);margin-top:2px">Pick a project to scope every item to one repo. Leave on cross-repo for plans that span multiple projects.</span></label>' +
20
20
  '<label style="color:var(--text);font-size:var(--text-md)">Plan Content <textarea id="plan-new-content" rows="12" style="' + inputStyle + ';resize:vertical;font-family:monospace;font-size:12px" placeholder="Write your plan in markdown...\n\nDescribe what needs to be built, the approach, requirements, and any constraints.\n\nThe squad will convert this into a PRD with structured work items."></textarea></label>' +
21
21
  '<div style="font-size:11px;color:var(--muted)">After creating, click Execute on the plan card to have an agent convert it into a PRD with work items.</div>' +
22
22
  '<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:4px">' +
package/dashboard.js CHANGED
@@ -516,22 +516,53 @@ function findWorkItemsTargetById(id, source, projects = PROJECTS) {
516
516
 
517
517
  function buildPlanWorkItem(body, projects = PROJECTS, options = {}) {
518
518
  if (!body?.title || !String(body.title).trim()) return { error: 'title is required' };
519
- const target = resolveWorkItemsCreateTarget(body.project, projects);
519
+
520
+ // Multi-project: body.project may be an array. Each entry must resolve to a
521
+ // known project; ≥2 valid entries trigger cross-repo mode (WI lands in central,
522
+ // _targetProjects records the list, description gains an explicit directive so
523
+ // the plan agent writes a header-less plan and plan-to-prd routes per item).
524
+ const rawProject = body.project;
525
+ let targetProjects = null;
526
+ let singleProjectInput = rawProject;
527
+ if (Array.isArray(rawProject)) {
528
+ const names = [];
529
+ for (const entry of rawProject) {
530
+ const t = resolveWorkItemsCreateTarget(entry, projects);
531
+ if (t.error) return { error: t.error };
532
+ if (t.project?.name && !names.includes(t.project.name)) names.push(t.project.name);
533
+ }
534
+ if (names.length > 1) {
535
+ targetProjects = names;
536
+ singleProjectInput = '';
537
+ } else {
538
+ singleProjectInput = names[0] || '';
539
+ }
540
+ }
541
+
542
+ const target = resolveWorkItemsCreateTarget(singleProjectInput, projects);
520
543
  if (target.error) return { error: target.error };
521
544
  let now = options.now ? new Date(options.now) : new Date();
522
545
  if (!Number.isFinite(now.getTime())) now = new Date();
546
+
547
+ let description = body.description || '';
548
+ if (targetProjects) {
549
+ const prefix = `**Target Projects:** ${targetProjects.join(', ')}\n\nThis is a cross-repo plan. In the plan markdown, omit the \`**Project:**\` header (or leave it blank) so the plan-to-prd step can route each PRD item to the correct project via per-item \`project\` fields.\n\n---\n\n`;
550
+ description = prefix + description;
551
+ }
552
+
523
553
  const item = {
524
554
  id: options.id || ('W-' + shared.uid()),
525
555
  title: body.title,
526
556
  type: 'plan',
527
557
  priority: body.priority || 'high',
528
- description: body.description || '',
558
+ description,
529
559
  status: WI_STATUS.PENDING,
530
560
  created: now.toISOString(),
531
561
  createdBy: 'dashboard',
532
562
  branchStrategy: body.branch_strategy || body.branchStrategy || 'parallel',
533
563
  };
534
564
  if (target.project) item.project = target.project.name;
565
+ if (targetProjects) item._targetProjects = targetProjects;
535
566
  if (body.agent) item.agent = body.agent;
536
567
  return { item, id: item.id };
537
568
  }
@@ -7082,7 +7113,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7082
7113
  { method: 'POST', path: '/api/notes-save', desc: 'Save edited notes.md content', params: 'content, file?', handler: handleNotesSave },
7083
7114
 
7084
7115
  // Plans
7085
- { method: 'POST', path: '/api/plan', desc: 'Create a plan work item that chains to PRD on completion', params: 'title, description?, priority?, project?, agent?, branch_strategy? or branchStrategy?', handler: handlePlanCreate },
7116
+ { method: 'POST', path: '/api/plan', desc: 'Create a plan work item that chains to PRD on completion', params: 'title, description?, priority?, project? (string OR array for cross-repo plans), agent?, branch_strategy? or branchStrategy?', handler: handlePlanCreate },
7086
7117
  { method: 'GET', path: '/api/plans', desc: 'List plan files (.md drafts + .json PRDs)', handler: handlePlansList },
7087
7118
  { method: 'POST', path: '/api/plans/trigger-verify', desc: 'Manually trigger verification for a completed plan', params: 'file', handler: handlePlansTriggerVerify },
7088
7119
  { method: 'POST', path: '/api/plans/approve', desc: 'Approve a plan for execution', params: 'file, approvedBy?', handler: handlePlansApprove },
@@ -157,11 +157,28 @@ function checkPlanCompletion(meta, config) {
157
157
  return data;
158
158
  });
159
159
 
160
- // Resolve the primary project for writing new work items (PR, verify)
160
+ // Resolve the primary project for writing new work items (PR, verify).
161
+ // Multi-project plans (no plan.project) derive primary from the done items —
162
+ // the project with the most completed items hosts the verify WI; its description
163
+ // already enumerates worktrees for every project so the agent verifies cross-repo.
161
164
  const projectName = plan.project;
162
- const primaryProject = projectName
165
+ let primaryProject = projectName
163
166
  ? shared.resolveProjectSource(projectName, projects, { allowCentral: false }).project
164
167
  : (projects.length === 1 ? projects[0] : null);
168
+ if (!primaryProject && doneItems.length > 0) {
169
+ const counts = new Map();
170
+ for (const wi of doneItems) {
171
+ if (!wi.project) continue;
172
+ counts.set(wi.project, (counts.get(wi.project) || 0) + 1);
173
+ }
174
+ if (counts.size > 0) {
175
+ const [topName] = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];
176
+ primaryProject = shared.resolveProjectSource(topName, projects, { allowCentral: false }).project;
177
+ if (primaryProject) {
178
+ log('info', `Plan ${planFile}: multi-project — verify WI routed to "${primaryProject.name}" (${counts.get(topName)}/${doneItems.length} done items)`);
179
+ }
180
+ }
181
+ }
165
182
  if (!primaryProject) {
166
183
  log('warn', `Plan ${planFile}: no primary project found — skipping PR/verify creation`);
167
184
  return;
@@ -190,32 +207,19 @@ function checkPlanCompletion(meta, config) {
190
207
  }
191
208
  }
192
209
 
193
- // 4. Create verification work item (build, test, start webapp, write testing guide)
194
- // Only one verify per PRD skip if pending/dispatched, re-open if done/failed (PRD was modified)
195
- const existingVerify = allWorkItems.find(w => w.sourcePlan === planFile && w.itemType === 'verify');
196
- if (isActiveVerify(existingVerify)) {
197
- log('info', `Plan ${planFile}: verify WI ${existingVerify.id} already ${existingVerify.status} skipping`);
198
- } else if (isReopenableVerify(existingVerify) && doneItems.length > 0) {
199
- const verifyProject = existingVerify.project || projectName;
200
- const vProject = shared.resolveProjectSource(verifyProject, projects, { allowCentral: false }).project || primaryProject;
201
- const vWiPath = shared.projectWorkItemsPath(vProject);
202
- let reopenedVerify = false;
203
- mutateWorkItems(vWiPath, items => {
204
- const v = items.find(w => w.id === existingVerify.id);
205
- if (isReopenableVerify(v)) {
206
- shared.reopenWorkItem(v);
207
- reopenedVerify = true;
208
- }
209
- });
210
- if (reopenedVerify) log('info', `Re-opened verification work item ${existingVerify.id} for modified plan ${planFile}`);
211
- } else if (!existingVerify && doneItems.length > 0) {
212
- const verifyId = 'PL-' + shared.uid();
210
+ // 4. Create verification work items one per touched project (project with
211
+ // active PRs linked to this plan's done items). Each project's verify agent
212
+ // works in its own worktree on its own build/test cycle, in parallel.
213
+ // Single-project plans behave as before (one verify WI). Cross-repo plans
214
+ // fan out so each repo gets its own dedicated verification run.
215
+ if (doneItems.length > 0) {
213
216
  const planSlug = planFile.replace('.json', '');
217
+ const isSharedBranch = plan.branch_strategy === 'shared-branch' && plan.feature_branch;
214
218
 
215
- // Group PRs by project — one worktree per project with all branches merged in
216
- const projectPrs = {}; // projectName -> { project, prs: [], mainBranch }
219
+ // Group active PRs by project — only projects with PRs from this plan get a verify WI
220
+ const projectPrs = {};
221
+ const prLinks = getPrLinks();
217
222
  for (const p of projects) {
218
- const prLinks = getPrLinks();
219
223
  const prs = (safeJson(shared.projectPrPath(p)) || [])
220
224
  .filter(pr => {
221
225
  const linkedIds = prLinks[pr.id] || [];
@@ -226,118 +230,160 @@ function checkPlanCompletion(meta, config) {
226
230
  }
227
231
  }
228
232
 
229
- // Shared-branch plans already have all changes on a single feature branch — no merge needed
230
- const isSharedBranch = plan.branch_strategy === 'shared-branch' && plan.feature_branch;
233
+ // Fallback: no PRs surfaced (e.g., items completed without PR records, or
234
+ // legacy state). Verify the primary project so the plan still gets verified.
235
+ const touchedProjects = Object.keys(projectPrs).length > 0
236
+ ? Object.entries(projectPrs)
237
+ : [[primaryProject.name, { project: primaryProject, prs: [], mainBranch: shared.resolveMainBranch(primaryProject.localPath, primaryProject.mainBranch) }]];
238
+
239
+ const otherProjectNames = (current) => touchedProjects.map(([n]) => n).filter(n => n !== current);
240
+ let createdAny = false;
241
+ let reopenedAny = false;
242
+
243
+ for (const [projName, { project: p, prs, mainBranch }] of touchedProjects) {
244
+ // Per-project existing verify lookup. Legacy single-WI plans may have
245
+ // existingVerify.project unset; match those against the primary project name.
246
+ const existingVerify = allWorkItems.find(w =>
247
+ w.sourcePlan === planFile && w.itemType === 'verify' &&
248
+ (w.project === projName || (!w.project && projName === primaryProject.name)));
249
+
250
+ if (isActiveVerify(existingVerify)) {
251
+ log('info', `Plan ${planFile}: verify WI ${existingVerify.id} for ${projName} already ${existingVerify.status} — skipping`);
252
+ continue;
253
+ }
254
+
255
+ if (isReopenableVerify(existingVerify)) {
256
+ const verifyProject = existingVerify.project || projName;
257
+ const vProject = shared.resolveProjectSource(verifyProject, projects, { allowCentral: false }).project || p;
258
+ const vWiPath = shared.projectWorkItemsPath(vProject);
259
+ let reopened = false;
260
+ mutateWorkItems(vWiPath, items => {
261
+ const v = items.find(w => w.id === existingVerify.id);
262
+ if (isReopenableVerify(v)) {
263
+ shared.reopenWorkItem(v);
264
+ reopened = true;
265
+ }
266
+ });
267
+ if (reopened) {
268
+ reopenedAny = true;
269
+ log('info', `Re-opened verification work item ${existingVerify.id} for ${projName} (modified plan ${planFile})`);
270
+ }
271
+ continue;
272
+ }
231
273
 
232
- // Build per-project checkout commands: one worktree, merge all PR branches into it
233
- const checkoutBlocks = Object.entries(projectPrs).map(([name, { project: p, prs, mainBranch }]) => {
274
+ // Build per-project setup block
275
+ let checkoutBlock;
276
+ let wtPath;
234
277
  if (isSharedBranch) {
278
+ wtPath = `${p.localPath}/../worktrees/verify-${projName}-${planSlug}`;
235
279
  const featureBranch = plan.feature_branch;
236
- const wtPath = `${p.localPath}/../worktrees/verify-${name}-${planSlug}`;
237
- const lines = [
238
- `# ${name} — shared-branch: use existing feature branch directly`,
280
+ checkoutBlock = [
281
+ `# ${projName} shared-branch: use existing feature branch directly`,
239
282
  `cd "${p.localPath.replace(/\\/g, '/')}"`,
240
283
  `git fetch origin "${featureBranch}"`,
241
284
  `git worktree add "${wtPath}" "origin/${featureBranch}" 2>/dev/null || (cd "${wtPath}" && git checkout "${featureBranch}" && git pull origin "${featureBranch}")`,
242
- ];
243
- return lines.join('\n');
285
+ ].join('\n');
286
+ } else {
287
+ wtPath = `${p.localPath}/../worktrees/verify-${projName}-${planSlug}-${shared.uid()}`;
288
+ const branches = prs.map(pr => pr.branch).filter(Boolean);
289
+ checkoutBlock = [
290
+ `# ${projName} — merge ${branches.length} PR branch(es) into one worktree`,
291
+ `cd "${p.localPath.replace(/\\/g, '/')}"`,
292
+ branches.length > 0
293
+ ? `git fetch origin ${branches.map(b => `"${b}"`).join(' ')} "${mainBranch}"`
294
+ : `git fetch origin "${mainBranch}"`,
295
+ `git worktree add "${wtPath}" "origin/${mainBranch}" 2>/dev/null || (cd "${wtPath}" && git checkout "${mainBranch}" && git pull origin "${mainBranch}")`,
296
+ `cd "${wtPath}"`,
297
+ ...branches.map(b => `git merge "origin/${b}" --no-edit # ${prs.find(pr => pr.branch === b)?.id || b}`),
298
+ ].join('\n');
244
299
  }
245
- const wtPath = `${p.localPath}/../worktrees/verify-${name}-${planSlug}-${shared.uid()}`;
246
- const branches = prs.map(pr => pr.branch).filter(Boolean);
247
- const lines = [
248
- `# ${name} — merge ${branches.length} PR branch(es) into one worktree`,
249
- `cd "${p.localPath.replace(/\\/g, '/')}"`,
250
- `git fetch origin ${branches.map(b => `"${b}"`).join(' ')} "${mainBranch}"`,
251
- `git worktree add "${wtPath}" "origin/${mainBranch}" 2>/dev/null || (cd "${wtPath}" && git checkout "${mainBranch}" && git pull origin "${mainBranch}")`,
252
- `cd "${wtPath}"`,
253
- ...branches.map(b => `git merge "origin/${b}" --no-edit # ${prs.find(pr => pr.branch === b)?.id || b}`),
254
- ];
255
- return lines.join('\n');
256
- }).join('\n\n');
257
-
258
- // Build completed items summary with acceptance criteria
259
- const itemsWithCriteria = doneItems.map(w => {
260
- const planItem = plan.missing_features?.find(f => f.id === w.id);
261
- const criteria = (planItem?.acceptance_criteria || []).map(c => ` - ${c}`).join('\n');
262
- return `### ${w.id}: ${(w.title || 'Untitled').replace('Implement: ', '')}\n${criteria ? '**Acceptance Criteria:**\n' + criteria : ''}`;
263
- }).join('\n\n');
264
-
265
- const prSummary = uniquePrs.map(pr =>
266
- `- ${pr.id}: ${pr.title || ''} (branch: \`${pr.branch || '?'}\`) ${pr.url || ''}`
267
- ).join('\n');
268
-
269
- // List projects and their worktree paths for the agent
270
- const projectWorktrees = Object.entries(projectPrs).map(([name, { project: p }]) =>
271
- `- **${name}**: \`${p.localPath}/../worktrees/verify-${planSlug}\``
272
- ).join('\n');
273
300
 
274
- const sharedBranchNote = isSharedBranch
275
- ? `\n**Shared-branch plan** — all changes are already on branch \`${plan.feature_branch}\`. Use this branch directly for the E2E PR instead of creating a new \`e2e/\` branch. Check if a PR already exists for this branch before creating one.\n`
276
- : '';
277
-
278
- const description = [
279
- `Verification task for completed plan \`${planFile}\`.`,
280
- sharedBranchNote,
281
- `## Projects & Worktrees`,
282
- ``,
283
- `Each project gets ONE worktree with all PR branches merged in:`,
284
- projectWorktrees,
285
- ``,
286
- `## Setup Commands`,
287
- ``,
288
- `\`\`\`bash`,
289
- checkoutBlocks,
290
- `\`\`\``,
291
- ``,
292
- `If any merge conflicts occur, resolve them (prefer the PR branch changes).`,
293
- `After setup, build and test from the worktree paths above.`,
294
- ``,
295
- `## Completed Items`,
296
- ``,
297
- itemsWithCriteria,
298
- ``,
299
- `## Pull Requests`,
300
- ``,
301
- prSummary,
302
- ].join('\n');
303
-
304
- let createdVerify = false;
305
- let reopenedVerifyId = null;
306
- mutateWorkItems(wiPath, workItems => {
307
- const v = workItems.find(w => w.sourcePlan === planFile && w.itemType === 'verify');
308
- if (v) {
309
- if (isReopenableVerify(v)) {
310
- shared.reopenWorkItem(v);
311
- reopenedVerifyId = v.id;
301
+ // Items scoped to this project (fall back to all done items if items lack project field — single-project plans)
302
+ const projItems = doneItems.filter(w => w.project === projName);
303
+ const itemsForProject = projItems.length > 0 ? projItems : doneItems;
304
+ const itemsWithCriteria = itemsForProject.map(w => {
305
+ const planItem = plan.missing_features?.find(f => f.id === w.id);
306
+ const criteria = (planItem?.acceptance_criteria || []).map(c => ` - ${c}`).join('\n');
307
+ return `### ${w.id}: ${(w.title || 'Untitled').replace('Implement: ', '')}\n${criteria ? '**Acceptance Criteria:**\n' + criteria : ''}`;
308
+ }).join('\n\n');
309
+
310
+ const prSummary = prs.map(pr =>
311
+ `- ${pr.id}: ${pr.title || ''} (branch: \`${pr.branch || '?'}\`) ${pr.url || ''}`
312
+ ).join('\n') || '_No PRs surfaced for this project — verify the worktree directly._';
313
+
314
+ const sharedBranchNote = isSharedBranch
315
+ ? `\n**Shared-branch plan** — all changes are already on branch \`${plan.feature_branch}\`. Use this branch directly for the E2E PR instead of creating a new \`e2e/\` branch. Check if a PR already exists for this branch before creating one.\n`
316
+ : '';
317
+
318
+ const siblings = otherProjectNames(projName);
319
+ const crossRepoNote = touchedProjects.length > 1
320
+ ? `\n**Cross-repo plan** — this verify task covers **${projName}** only. Sibling verify tasks run in parallel for: ${siblings.join(', ')}. Build/test this project's worktree independently.\n`
321
+ : '';
322
+
323
+ const description = [
324
+ `Verification task for completed plan \`${planFile}\` — project: **${projName}**.`,
325
+ crossRepoNote,
326
+ sharedBranchNote,
327
+ `## Worktree`,
328
+ ``,
329
+ `- **${projName}**: \`${wtPath}\``,
330
+ ``,
331
+ `## Setup Commands`,
332
+ ``,
333
+ `\`\`\`bash`,
334
+ checkoutBlock,
335
+ `\`\`\``,
336
+ ``,
337
+ `If any merge conflicts occur, resolve them (prefer the PR branch changes).`,
338
+ `After setup, build and test from the worktree path above.`,
339
+ ``,
340
+ `## Completed Items`,
341
+ ``,
342
+ itemsWithCriteria,
343
+ ``,
344
+ `## Pull Requests`,
345
+ ``,
346
+ prSummary,
347
+ ].join('\n');
348
+
349
+ const verifyId = 'PL-' + shared.uid();
350
+ const vWiPath = shared.projectWorkItemsPath(p);
351
+ let created = false;
352
+ mutateWorkItems(vWiPath, workItems => {
353
+ // Re-check under lock to prevent races
354
+ if (workItems.some(w => w.sourcePlan === planFile && w.itemType === 'verify' && (w.project === projName || (!w.project && projName === primaryProject.name)))) {
355
+ return workItems;
312
356
  }
313
- return workItems;
314
- }
315
- workItems.push({
316
- id: verifyId,
317
- title: `Verify plan: ${(plan.plan_summary || planFile).slice(0, 80)}`,
318
- type: 'verify',
319
- priority: 'high',
320
- description,
321
- status: WI_STATUS.PENDING,
322
- created: ts(),
323
- createdBy: 'engine:plan-verification',
324
- sourcePlan: planFile,
325
- itemType: 'verify',
326
- project: projectName,
357
+ workItems.push({
358
+ id: verifyId,
359
+ title: touchedProjects.length > 1
360
+ ? `Verify plan (${projName}): ${(plan.plan_summary || planFile).slice(0, 70)}`
361
+ : `Verify plan: ${(plan.plan_summary || planFile).slice(0, 80)}`,
362
+ type: 'verify',
363
+ priority: 'high',
364
+ description,
365
+ status: WI_STATUS.PENDING,
366
+ created: ts(),
367
+ createdBy: 'engine:plan-verification',
368
+ sourcePlan: planFile,
369
+ itemType: 'verify',
370
+ project: projName,
371
+ });
372
+ created = true;
327
373
  });
328
- createdVerify = true;
329
- });
330
- if (createdVerify) {
331
- log('info', `Created verification work item ${verifyId} for plan ${planFile}`);
374
+ if (created) {
375
+ createdAny = true;
376
+ log('info', `Created verification work item ${verifyId} for plan ${planFile} → ${projName}`);
377
+ }
378
+ }
332
379
 
333
- // Teams notification for verify creation — non-blocking
380
+ if (createdAny) {
334
381
  try {
335
382
  const teams = require('./teams');
336
383
  teams.teamsNotifyPlanEvent({ name: plan.plan_summary || planFile, file: planFile }, 'verify-created').catch(() => {});
337
384
  } catch {}
338
- } else if (reopenedVerifyId) {
339
- log('info', `Re-opened verification work item ${reopenedVerifyId} for modified plan ${planFile}`);
340
385
  }
386
+ if (reopenedAny) log('info', `Plan ${planFile}: re-opened verify WI(s) for modified plan`);
341
387
  }
342
388
 
343
389
  // Archive deferred until verify completes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.1874",
3
+ "version": "0.1.1875",
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"
@@ -88,6 +88,14 @@ Rules for items:
88
88
  - **`status` is `"missing"` for new items** — do not set `done`, `complete`, `implemented`, or any other value based on codebase observations. The only exception is when reusing an existing PRD (see below) — items already `"done"` in the existing PRD carry forward as `"done"`. Pre-setting any other status on new items causes them to be silently skipped by the engine.
89
89
  - **Do NOT include a "verify" or "test" or "integration test" item** — the engine automatically creates a verify task when all PRD items are done. Adding one manually creates a duplicate that blocks plan completion.
90
90
  - **`project` field is REQUIRED** — set it to the project name where the code changes go (e.g., `"OfficeAgent"`, `"office-bohemia"`). If the plan declares a single project, every item must use `{{project_name}}`; contextual mentions of another repo/product must not override it. Cross-repo plans must route each item to the correct project. The engine materializes items into that project's work queue.
91
+ - **Cross-repo plans:** when the plan body does NOT declare `**Project:**` (or declares it empty), `{{project_name}}` is unset. In that case, set each item's `project` to the actual repo where its code changes belong, and omit the top-level `"project"` field (or set it to `""`). Example:
92
+ ```json
93
+ "missing_features": [
94
+ { "id": "P-aaa1", "name": "API endpoint", "project": "backend-service", ... },
95
+ { "id": "P-bbb2", "name": "UI client", "project": "web-dashboard", ... }
96
+ ]
97
+ ```
98
+ The engine routes each item to that project's `work-items.json`. The verify task at plan completion picks the project with the most completed items and runs cross-repo verification from there.
91
99
  - `depends_on` lists IDs of items that must be done first
92
100
  - Keep descriptions actionable — name the files, functions, patterns, or integration points the implementing agent should touch whenever the plan makes them clear
93
101
  - Include `acceptance_criteria` so reviewers know when it's done