@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/CHANGELOG.md +46 -0
- package/README.md +60 -2
- package/bin/minions.js +51 -2
- package/config.template.json +1 -25
- package/dashboard.html +106 -3
- package/dashboard.js +101 -0
- package/docs/auto-discovery.md +2 -2
- package/engine/ado.js +1 -0
- package/engine/cli.js +120 -6
- package/engine/github.js +1 -0
- package/engine/lifecycle.js +86 -0
- package/engine/preflight.js +239 -0
- package/engine/scheduler.js +159 -0
- package/engine/shared.js +4 -1
- package/engine/spawn-agent.js +4 -2
- package/engine.js +167 -15
- package/package.json +1 -1
- package/playbooks/decompose.md +60 -0
- package/routing.md +1 -0
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:
|
|
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 = {
|
package/engine/spawn-agent.js
CHANGED
|
@@ -67,8 +67,10 @@ if (isResume) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
if (!claudeBin) {
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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),
|
|
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),
|
|
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)
|
|
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) {
|
|
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 ||
|
|
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
|
@@ -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