@yemi33/minions 0.1.7 → 0.1.9

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/shared.js CHANGED
@@ -231,7 +231,7 @@ function classifyInboxItem(name, content) {
231
231
 
232
232
  const ENGINE_DEFAULTS = {
233
233
  tickInterval: 60000,
234
- maxConcurrent: 3,
234
+ maxConcurrent: 5,
235
235
  inboxConsolidateThreshold: 5,
236
236
  agentTimeout: 18000000, // 5h
237
237
  heartbeatTimeout: 300000, // 5min
@@ -242,6 +242,9 @@ const ENGINE_DEFAULTS = {
242
242
  idleAlertMinutes: 15,
243
243
  fanOutTimeout: null, // falls back to agentTimeout
244
244
  restartGracePeriod: 1200000, // 20min
245
+ shutdownTimeout: 300000, // 5min — max wait for active agents during graceful shutdown
246
+ allowTempAgents: false, // opt-in: spawn ephemeral agents when all permanent agents are busy
247
+ autoDecompose: true, // auto-decompose implement:large items into sub-tasks
245
248
  };
246
249
 
247
250
  const DEFAULT_AGENTS = {
@@ -67,8 +67,10 @@ if (isResume) {
67
67
  }
68
68
 
69
69
  if (!claudeBin) {
70
- fs.appendFileSync(debugPath, 'FATAL: Cannot find claude-code cli.js\n');
71
- process.exit(1);
70
+ const msg = 'FATAL: Cannot find claude-code cli.js — install with: npm install -g @anthropic-ai/claude-code';
71
+ fs.appendFileSync(debugPath, msg + '\n');
72
+ console.error(msg);
73
+ process.exit(78); // 78 = configuration error (distinct from runtime failures)
72
74
  }
73
75
 
74
76
  // Check if --system-prompt-file is supported (cached to avoid spawning claude --help every call)
package/engine.js CHANGED
@@ -207,6 +207,15 @@ function resolveAgent(workType, config, authorAgent = null) {
207
207
 
208
208
  if (idle[0]) { _claimedAgents.add(idle[0]); return idle[0]; }
209
209
 
210
+ // No idle configured agent — try temp agent if enabled
211
+ if (config.engine?.allowTempAgents) {
212
+ const tempId = `temp-${shared.uid()}`;
213
+ _claimedAgents.add(tempId);
214
+ tempAgents.set(tempId, { name: `Temp-${tempId.slice(5, 9)}`, role: 'Temporary Agent', createdAt: ts() });
215
+ log('info', `Spawning temp agent ${tempId} — all permanent agents busy`);
216
+ return tempId;
217
+ }
218
+
210
219
  // No idle agent available — return null, item stays pending until next tick
211
220
  return null;
212
221
  }
@@ -389,7 +398,15 @@ function getPrCreateInstructions(project) {
389
398
  const host = getRepoHost(project);
390
399
  const repoId = project?.repositoryId || '';
391
400
  if (host === 'github') {
392
- return `Use \`gh pr create\` or the GitHub MCP tools to create a pull request.`;
401
+ const org = project?.adoOrg || '';
402
+ const repo = project?.repoName || '';
403
+ const mainBranch = project?.mainBranch || 'main';
404
+ return `Use \`gh pr create\` to create a pull request:\n` +
405
+ `- \`gh pr create --base ${mainBranch} --head <your-branch> --title "PR title" --body "PR description" --repo ${org}/${repo}\`\n` +
406
+ `- Always set --base to \`${mainBranch}\` (the main branch)\n` +
407
+ `- Always set --repo to \`${org}/${repo}\` to target the correct repository\n` +
408
+ `- Use --head to specify your feature branch name\n` +
409
+ `- Include a meaningful --title and --body describing the changes`;
393
410
  }
394
411
  // Default: Azure DevOps
395
412
  return `Use \`mcp__azure-ado__repo_create_pull_request\`:\n- repositoryId: \`${repoId}\``;
@@ -399,7 +416,13 @@ function getPrCommentInstructions(project) {
399
416
  const host = getRepoHost(project);
400
417
  const repoId = project?.repositoryId || '';
401
418
  if (host === 'github') {
402
- return `Use \`gh pr comment\` or the GitHub MCP tools to post a comment on the PR.`;
419
+ const org = project?.adoOrg || '';
420
+ const repo = project?.repoName || '';
421
+ return `Use \`gh pr comment\` to post a comment on the PR:\n` +
422
+ `- \`gh pr comment <number> --body "Your comment text" --repo ${org}/${repo}\`\n` +
423
+ `- Replace <number> with the PR number\n` +
424
+ `- Always set --repo to \`${org}/${repo}\` to target the correct repository\n` +
425
+ `- Use --body to provide the comment text (supports Markdown)`;
403
426
  }
404
427
  return `Use \`mcp__azure-ado__repo_create_pull_request_thread\`:\n- repositoryId: \`${repoId}\``;
405
428
  }
@@ -407,7 +430,17 @@ function getPrCommentInstructions(project) {
407
430
  function getPrFetchInstructions(project) {
408
431
  const host = getRepoHost(project);
409
432
  if (host === 'github') {
410
- return `Use \`gh pr view\` or the GitHub MCP tools to fetch PR status.`;
433
+ const org = project?.adoOrg || '';
434
+ const repo = project?.repoName || '';
435
+ const mainBranch = project?.mainBranch || 'main';
436
+ return `Use \`gh pr view\` to fetch PR status:\n` +
437
+ `- \`gh pr view <number> --json number,title,state,mergeable,reviewDecision,headRefName,baseRefName,statusCheckRollup --repo ${org}/${repo}\`\n` +
438
+ `- This returns JSON with PR state, mergeability, review decision, and check statuses\n` +
439
+ `- To fetch the PR branch locally:\n` +
440
+ ` 1. \`git fetch origin <branch-name>\`\n` +
441
+ ` 2. \`git checkout <branch-name>\`\n` +
442
+ `- Or use \`gh pr checkout <number> --repo ${org}/${repo}\` to fetch and checkout in one step\n` +
443
+ `- The base branch is \`${mainBranch}\``;
411
444
  }
412
445
  return `Use \`mcp__azure-ado__repo_get_pull_request_by_id\` to fetch PR status.`;
413
446
  }
@@ -416,7 +449,15 @@ function getPrVoteInstructions(project) {
416
449
  const host = getRepoHost(project);
417
450
  const repoId = project?.repositoryId || '';
418
451
  if (host === 'github') {
419
- return `Use \`gh pr review\` to approve or request changes:\n- \`gh pr review <number> --approve\`\n- \`gh pr review <number> --request-changes\``;
452
+ const org = project?.adoOrg || '';
453
+ const repo = project?.repoName || '';
454
+ return `Use \`gh pr review\` to submit a review on the PR:\n` +
455
+ `- Approve: \`gh pr review <number> --approve --body "Approval comment" --repo ${org}/${repo}\`\n` +
456
+ `- Request changes: \`gh pr review <number> --request-changes --body "What needs to change" --repo ${org}/${repo}\`\n` +
457
+ `- Comment only: \`gh pr review <number> --comment --body "Review comment" --repo ${org}/${repo}\`\n` +
458
+ `- Replace <number> with the PR number\n` +
459
+ `- Always set --repo to \`${org}/${repo}\` to target the correct repository\n` +
460
+ `- Use --body to provide a review summary (supports Markdown)`;
420
461
  }
421
462
  return `Use \`mcp__azure-ado__repo_update_pull_request_reviewers\`:\n- repositoryId: \`${repoId}\`\n- Set your reviewer vote on the PR (10=approve, 5=approve-with-suggestions, -10=reject)`;
422
463
  }
@@ -437,8 +478,8 @@ function getRepoHostToolRule(project) {
437
478
 
438
479
  // Lean system prompt: agent identity + rules only (~2-4KB, never grows)
439
480
  function buildSystemPrompt(agentId, config, project) {
440
- const agent = config.agents[agentId];
441
- const charter = getAgentCharter(agentId);
481
+ const agent = config.agents[agentId] || tempAgents.get(agentId) || { name: agentId, role: 'Temporary Agent', skills: [] };
482
+ const charter = getAgentCharter(agentId); // returns '' for temp agents (no charter file)
442
483
  project = project || getProjects(config)[0] || {};
443
484
 
444
485
  let prompt = '';
@@ -567,6 +608,7 @@ const { runPostCompletionHooks, updateWorkItemStatus, syncPrdItemStatus, handleP
567
608
  // ─── Agent Spawner ──────────────────────────────────────────────────────────
568
609
 
569
610
  const activeProcesses = new Map(); // dispatchId → { proc, agentId, startedAt }
611
+ const tempAgents = new Map(); // tempAgentId → { name, role, createdAt }
570
612
  let engineRestartGraceUntil = 0; // timestamp — suppress orphan detection until this time
571
613
 
572
614
  // Resolve dependency plan item IDs to their PR branches
@@ -942,6 +984,17 @@ function spawnAgent(dispatchItem, config) {
942
984
  safeWrite(archivePath, outputContent);
943
985
  safeWrite(latestPath, outputContent); // overwrite latest for dashboard compat
944
986
 
987
+ // Detect configuration errors (e.g. Claude CLI not found) — fail immediately with clear message
988
+ if (code === 78) {
989
+ const errMsg = stderr.includes('claude-code') ? stderr.trim() : 'Configuration error — Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code';
990
+ log('error', `Agent ${agentId} (${id}) failed: ${errMsg}`);
991
+ completeDispatch(id, 'error', errMsg, '');
992
+ try { fs.unlinkSync(sysPromptPath); } catch {}
993
+ try { fs.unlinkSync(promptPath); } catch {}
994
+ try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch {}
995
+ return;
996
+ }
997
+
945
998
  // Parse output and run all post-completion hooks
946
999
  const { resultSummary } = runPostCompletionHooks(dispatchItem, agentId, code, stdout, config);
947
1000
 
@@ -954,6 +1007,17 @@ function spawnAgent(dispatchItem, config) {
954
1007
  try { fs.unlinkSync(promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid')); } catch {}
955
1008
 
956
1009
  log('info', `Agent ${agentId} completed. Output saved to ${archivePath}`);
1010
+
1011
+ // Clean up temp agent directory
1012
+ if (tempAgents.has(agentId)) {
1013
+ tempAgents.delete(agentId);
1014
+ try {
1015
+ const agentDir = path.join(AGENTS_DIR, agentId);
1016
+ // Keep output archive but remove temp agent directory (live-output.log etc.)
1017
+ fs.rmSync(agentDir, { recursive: true, force: true });
1018
+ log('info', `Temp agent ${agentId} cleaned up`);
1019
+ } catch {}
1020
+ }
957
1021
  });
958
1022
 
959
1023
  proc.on('error', (err) => {
@@ -1003,6 +1067,7 @@ function spawnAgent(dispatchItem, config) {
1003
1067
  if (idx < 0) return;
1004
1068
  const item = dispatch.pending.splice(idx, 1)[0];
1005
1069
  item.started_at = startedAt;
1070
+ delete item.skipReason;
1006
1071
  if (!dispatch.active.some(d => d.id === id)) {
1007
1072
  dispatch.active.push(item);
1008
1073
  }
@@ -1186,7 +1251,7 @@ function areDependenciesMet(item, config) {
1186
1251
  return false;
1187
1252
  }
1188
1253
  if (depItem.status === 'failed') return 'failed';
1189
- if (depItem.status !== 'done' && depItem.status !== 'in-pr') return false; // Pending, dispatched, or retrying — wait (in-pr accepted for backward compat)
1254
+ if (!PRD_MET_STATUSES.has(depItem.status)) return false; // Pending, dispatched, or retrying — wait (legacy aliases accepted)
1190
1255
  }
1191
1256
  return true;
1192
1257
  }
@@ -1893,7 +1958,7 @@ function saveCooldowns() {
1893
1958
  function isOnCooldown(key, cooldownMs) {
1894
1959
  const entry = dispatchCooldowns.get(key);
1895
1960
  if (!entry) return false;
1896
- const backoff = Math.min(Math.pow(2, entry.failures || 0), 2);
1961
+ const backoff = Math.min(Math.pow(2, entry.failures || 0), 8);
1897
1962
  return (Date.now() - entry.timestamp) < (cooldownMs * backoff);
1898
1963
  }
1899
1964
 
@@ -1908,7 +1973,7 @@ function setCooldownFailure(key) {
1908
1973
  const failures = (existing?.failures || 0) + 1;
1909
1974
  dispatchCooldowns.set(key, { timestamp: Date.now(), failures });
1910
1975
  if (failures >= 3) {
1911
- log('warn', `${key} has failed ${failures} times — cooldown is now ${Math.min(Math.pow(2, failures), 2)}x`);
1976
+ log('warn', `${key} has failed ${failures} times — cooldown is now ${Math.min(Math.pow(2, failures), 8)}x`);
1912
1977
  }
1913
1978
  saveCooldowns();
1914
1979
  }
@@ -2269,7 +2334,7 @@ function selectPlaybook(workType, item) {
2269
2334
  if (workType === 'review' && !item?._pr && !item?.pr_id) {
2270
2335
  return 'work-item';
2271
2336
  }
2272
- const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify'];
2337
+ const typeSpecificPlaybooks = ['explore', 'review', 'test', 'plan-to-prd', 'plan', 'ask', 'verify', 'decompose'];
2273
2338
  return typeSpecificPlaybooks.includes(workType) ? workType : 'work-item';
2274
2339
  }
2275
2340
 
@@ -2388,6 +2453,26 @@ function discoverFromPrs(config, project) {
2388
2453
  review_note: `Build is failing: ${pr.buildFailReason || 'Check CI pipeline for details'}. Fix the build errors and push.`,
2389
2454
  }, `Fix build failure on PR ${pr.id}`, { dispatchKey: key, source: 'pr', pr, branch: pr.branch, project: projMeta });
2390
2455
  if (item) { newWork.push(item); setCooldown(key); }
2456
+
2457
+ // Notify the author agent about the build failure
2458
+ if (pr.agent && !pr._buildFailNotified) {
2459
+ writeInboxAlert(`build-fail-${pr.agent}-${pr.id}`,
2460
+ `# Build Failure Notification\n\n` +
2461
+ `**Your PR ${pr.id}** on branch \`${pr.branch || 'unknown'}\` has a failing build.\n` +
2462
+ `**Reason:** ${pr.buildFailReason || 'Check CI pipeline for details'}\n\n` +
2463
+ `A fix agent has been dispatched to address this. Review the fix when complete.\n`
2464
+ );
2465
+ // Mark notified to prevent duplicate alerts
2466
+ try {
2467
+ const prPath = projectPrPath(project);
2468
+ const prs = safeJson(prPath) || [];
2469
+ const target = prs.find(p => p.id === pr.id);
2470
+ if (target) {
2471
+ target._buildFailNotified = true;
2472
+ safeWrite(prPath, prs);
2473
+ }
2474
+ } catch {}
2475
+ }
2391
2476
  }
2392
2477
 
2393
2478
  }
@@ -2431,10 +2516,15 @@ function discoverFromWorkItems(config, project) {
2431
2516
  if (depStatus === 'failed') {
2432
2517
  item.status = 'failed';
2433
2518
  item.failReason = 'Dependency failed — cannot proceed';
2519
+ delete item._pendingReason;
2434
2520
  log('warn', `Marking ${item.id} as failed: dependency failed (plan: ${item.sourcePlan})`);
2521
+ needsWrite = true;
2522
+ continue;
2523
+ }
2524
+ if (!depStatus) {
2525
+ if (item._pendingReason !== 'dependency_unmet') { item._pendingReason = 'dependency_unmet'; needsWrite = true; }
2435
2526
  continue;
2436
2527
  }
2437
- if (!depStatus) continue;
2438
2528
  }
2439
2529
 
2440
2530
  const key = `work-${project?.name || 'default'}-${item.id}`;
@@ -2454,14 +2544,30 @@ function discoverFromWorkItems(config, project) {
2454
2544
  delete item._resumedAt;
2455
2545
  safeWrite(projectWorkItemsPath(project), items);
2456
2546
  }
2457
- if (isAlreadyDispatched(key) || isOnCooldown(key, cooldownMs)) { skipped.gated++; continue; }
2547
+ if (isAlreadyDispatched(key)) {
2548
+ if (item._pendingReason !== 'already_dispatched') { item._pendingReason = 'already_dispatched'; needsWrite = true; }
2549
+ skipped.gated++; continue;
2550
+ }
2551
+ if (isOnCooldown(key, cooldownMs)) {
2552
+ if (item._pendingReason !== 'cooldown') { item._pendingReason = 'cooldown'; needsWrite = true; }
2553
+ skipped.gated++; continue;
2554
+ }
2458
2555
 
2459
2556
  let workType = item.type || 'implement';
2460
2557
  if (workType === 'implement' && (item.complexity === 'large' || item.estimated_complexity === 'large')) {
2461
2558
  workType = 'implement:large';
2462
2559
  }
2560
+ // Auto-decompose large items before implementation
2561
+ if (workType === 'implement:large' && !item._decomposed && !item._decomposing && config.engine?.autoDecompose !== false) {
2562
+ workType = 'decompose';
2563
+ item._decomposing = true;
2564
+ needsWrite = true;
2565
+ }
2463
2566
  const agentId = item.agent || resolveAgent(workType, config);
2464
- if (!agentId) { skipped.noAgent++; continue; }
2567
+ if (!agentId) {
2568
+ if (item._pendingReason !== 'no_agent') { item._pendingReason = 'no_agent'; needsWrite = true; }
2569
+ skipped.noAgent++; continue;
2570
+ }
2465
2571
 
2466
2572
  const isShared = item.branchStrategy === 'shared-branch' && item.featureBranch;
2467
2573
  const branchName = isShared ? item.featureBranch : (item.branch || `work/${item.id}`);
@@ -2514,6 +2620,7 @@ function discoverFromWorkItems(config, project) {
2514
2620
  item.status = 'dispatched';
2515
2621
  item.dispatched_at = ts();
2516
2622
  item.dispatched_to = agentId;
2623
+ delete item._pendingReason;
2517
2624
  prdSyncQueue.push({ id: item.id, sourcePlan: item.sourcePlan });
2518
2625
 
2519
2626
  newWork.push({
@@ -2953,6 +3060,23 @@ function discoverWork(config) {
2953
3060
  // Central work items (project-agnostic — agent decides where to work)
2954
3061
  const centralWork = discoverCentralWorkItems(config);
2955
3062
 
3063
+ // Scheduled tasks (cron-style recurring work)
3064
+ try {
3065
+ const { discoverScheduledWork } = require('./engine/scheduler');
3066
+ const scheduledWork = discoverScheduledWork(config);
3067
+ for (const item of scheduledWork) {
3068
+ // Write scheduled items to central work-items.json so they persist across ticks
3069
+ const centralPath = path.join(MINIONS_DIR, 'work-items.json');
3070
+ const items = safeJson(centralPath) || [];
3071
+ // Dedupe: don't re-create if same schedule already has a pending/dispatched item
3072
+ if (!items.some(i => i._scheduleId === item._scheduleId && i.status !== 'done' && i.status !== 'failed')) {
3073
+ items.push(item);
3074
+ safeWrite(centralPath, items);
3075
+ log('info', `Scheduled task fired: ${item._scheduleId} → ${item.title}`);
3076
+ }
3077
+ }
3078
+ } catch {}
3079
+
2956
3080
  // Gate reviews and fixes: do not dispatch until all implement items are complete
2957
3081
  const hasIncompleteImplements = projects.some(project => {
2958
3082
  const items = safeJson(projectWorkItemsPath(project)) || [];
@@ -3005,7 +3129,7 @@ async function tick() {
3005
3129
 
3006
3130
  async function tickInner() {
3007
3131
  const control = getControl();
3008
- if (control.state !== 'running') {
3132
+ if (control.state !== 'running' && control.state !== 'stopping') {
3009
3133
  log('info', `Engine state is "${control.state}" — exiting process`);
3010
3134
  process.exit(0);
3011
3135
  }
@@ -3020,6 +3144,12 @@ async function tickInner() {
3020
3144
  checkTimeouts(config);
3021
3145
  checkIdleThreshold(config);
3022
3146
 
3147
+ // In stopping state, only track agent completions — skip discovery and dispatch
3148
+ if (control.state === 'stopping') {
3149
+ log('info', `Engine stopping — ${activeProcesses.size} agent(s) still active, skipping discovery/dispatch`);
3150
+ return;
3151
+ }
3152
+
3023
3153
  // 2. Consolidate inbox
3024
3154
  consolidateInbox(config);
3025
3155
 
@@ -3158,7 +3288,7 @@ async function tickInner() {
3158
3288
  // 5. Process pending dispatches — auto-spawn agents
3159
3289
  const dispatch = getDispatch();
3160
3290
  const activeCount = (dispatch.active || []).length;
3161
- const maxConcurrent = config.engine?.maxConcurrent || 3;
3291
+ const maxConcurrent = config.engine?.maxConcurrent || 5;
3162
3292
 
3163
3293
  if (activeCount >= maxConcurrent) {
3164
3294
  log('info', `At max concurrency (${activeCount}/${maxConcurrent}) — skipping dispatch`);
@@ -3201,6 +3331,28 @@ async function tickInner() {
3201
3331
  dispatched.add(item.id);
3202
3332
  }
3203
3333
  }
3334
+
3335
+ // Annotate remaining pending items with skipReason so dashboard can show why they're waiting.
3336
+ // Re-read dispatch after spawns (spawnAgent moves items from pending→active).
3337
+ const postDispatch = getDispatch();
3338
+ const postBusyAgents = new Set((postDispatch.active || []).map(d => d.agent));
3339
+ const postActiveCount = (postDispatch.active || []).length;
3340
+ let skipReasonChanged = false;
3341
+ for (const item of (postDispatch.pending || [])) {
3342
+ let reason = null;
3343
+ if (postActiveCount >= maxConcurrent) {
3344
+ reason = 'max_concurrency';
3345
+ } else if (postBusyAgents.has(item.agent)) {
3346
+ reason = 'agent_busy';
3347
+ }
3348
+ if (item.skipReason !== reason) {
3349
+ item.skipReason = reason;
3350
+ skipReasonChanged = true;
3351
+ }
3352
+ }
3353
+ if (skipReasonChanged) {
3354
+ mutateDispatch((dp) => { dp.pending = postDispatch.pending; });
3355
+ }
3204
3356
  }
3205
3357
 
3206
3358
  // ─── Exports (for engine/cli.js and other modules) ──────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yemi33/minions",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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"
@@ -0,0 +1,60 @@
1
+ # Playbook: Task Decomposition
2
+
3
+ You are {{agent_name}}, the {{agent_role}} on the {{project_name}} project.
4
+ TEAM ROOT: {{team_root}}
5
+
6
+ ## Your Task
7
+
8
+ A work item has been flagged as too large for a single agent dispatch. Analyze the item and break it into 2-5 smaller, independently implementable sub-tasks.
9
+
10
+ ## Work Item
11
+
12
+ - **ID:** {{item_id}}
13
+ - **Title:** {{item_title}}
14
+ - **Description:** {{item_description}}
15
+ - **Complexity:** {{item_complexity}}
16
+ - **Project:** {{project_name}} (`{{project_path}}`)
17
+
18
+ {{#acceptance_criteria}}
19
+ ## Acceptance Criteria
20
+
21
+ {{acceptance_criteria}}
22
+ {{/acceptance_criteria}}
23
+
24
+ ## Instructions
25
+
26
+ 1. **Explore the codebase** at `{{project_path}}` — understand the existing structure, patterns, and dependencies
27
+ 2. **Analyze the work item** — identify distinct units of work that can be implemented as separate PRs
28
+ 3. **Break into 2-5 sub-tasks** — each should be:
29
+ - Small or medium complexity (not large)
30
+ - A single PR's worth of work
31
+ - Independently testable
32
+ - Clear enough for another agent to implement without ambiguity
33
+ 4. **Order by dependency** — if sub-task B needs sub-task A's code, declare `depends_on`
34
+ 5. **Generate unique IDs** — use format `{{item_id}}-a`, `{{item_id}}-b`, etc.
35
+
36
+ ## Output
37
+
38
+ Write the decomposition result as a JSON code block in your response:
39
+
40
+ ```json
41
+ {
42
+ "parent_id": "{{item_id}}",
43
+ "sub_items": [
44
+ {
45
+ "id": "{{item_id}}-a",
46
+ "name": "Short descriptive name",
47
+ "description": "What to build, where, and how. Be specific enough that an engineer can implement without further exploration.",
48
+ "estimated_complexity": "small|medium",
49
+ "depends_on": [],
50
+ "acceptance_criteria": ["Criterion 1", "Criterion 2"]
51
+ }
52
+ ]
53
+ }
54
+ ```
55
+
56
+ Keep the total number of sub-items between 2 and 5. If the task genuinely cannot be broken down further, output a single sub-item that matches the original.
57
+
58
+ {{pr_create_instructions}}
59
+
60
+ {{pr_comment_instructions}}
package/routing.md CHANGED
@@ -17,6 +17,7 @@ How the engine decides who handles what. Parsed by engine.js — keep the table
17
17
  | test | dallas | ralph |
18
18
  | ask | ripley | rebecca |
19
19
  | verify | dallas | ralph |
20
+ | decompose | ripley | rebecca |
20
21
 
21
22
  Notes:
22
23
  - `_author_` means route to the PR author