@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/CHANGELOG.md +11 -0
- package/dashboard/js/command-center.js +7 -3
- package/dashboard/js/modal-qa.js +3 -2
- package/dashboard.js +142 -142
- package/engine/cli.js +15 -9
- package/engine/copilot-models.json +1 -1
- package/engine/dispatch.js +4 -10
- package/engine/issues.js +103 -11
- package/engine/lifecycle.js +11 -11
- package/engine/pipeline.js +34 -13
- package/engine/projects.js +5 -5
- package/engine/queries.js +3 -3
- package/engine/shared.js +123 -4
- package/engine.js +64 -50
- package/package.json +1 -1
- package/prompts/doc-chat-system.md +13 -1
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
|
-
|
|
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
|
|
607
|
-
if (
|
|
608
|
-
const err = new Error(
|
|
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
|
|
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
|
|
625
|
-
//
|
|
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
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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())
|
|
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
|
-
? (
|
|
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:
|
|
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
|
-
|
|
3161
|
-
|
|
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
|
|
3754
|
-
if (
|
|
3755
|
-
const error =
|
|
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
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
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 =
|
|
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.
|
|
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.
|