@yemi33/minions 0.1.1811 → 0.1.1813
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 +11 -5
- package/dashboard/js/modal-qa.js +3 -2
- package/dashboard/js/render-prs.js +5 -3
- package/dashboard.js +214 -69
- package/engine/copilot-models.json +1 -1
- package/engine/issues.js +103 -11
- package/engine/pipeline.js +35 -13
- package/engine/shared.js +1 -1
- package/engine.js +55 -37
- package/package.json +1 -1
- package/prompts/doc-chat-system.md +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.1813 (2026-05-09)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- improve manual PR project inference (#2242)
|
|
7
|
+
|
|
8
|
+
## 0.1.1812 (2026-05-09)
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
- stop invalid project fallback (#2245)
|
|
12
|
+
- harden doc-chat dispatch prompt (#2249)
|
|
13
|
+
|
|
3
14
|
## 0.1.1811 (2026-05-09)
|
|
4
15
|
|
|
5
16
|
### Features
|
|
@@ -1022,6 +1022,7 @@ function _tagServerExecuted(actions, actionResults) {
|
|
|
1022
1022
|
if (r.id) actions[i]._serverId = r.id;
|
|
1023
1023
|
if (r.warning) actions[i]._serverWarning = r.warning;
|
|
1024
1024
|
if (r.duplicate) actions[i]._serverDuplicate = true;
|
|
1025
|
+
if (r.reusedFromAction !== undefined) actions[i]._serverHidden = true;
|
|
1025
1026
|
} else if (r && r.error) {
|
|
1026
1027
|
actions[i]._serverExecuted = true;
|
|
1027
1028
|
actions[i]._serverError = r.error;
|
|
@@ -1037,15 +1038,18 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1037
1038
|
|
|
1038
1039
|
// Server-executed actions: just show status, don't re-fire the API
|
|
1039
1040
|
if (action._serverExecuted) {
|
|
1041
|
+
if (action._serverHidden) return;
|
|
1040
1042
|
if (action._serverError) {
|
|
1041
1043
|
status.innerHTML = '✗ ' + escHtml(action.type) + ' failed: ' + escHtml(action._serverError);
|
|
1042
1044
|
status.style.color = 'var(--red)';
|
|
1043
1045
|
} else {
|
|
1044
1046
|
var label = action._serverId ? escHtml(action._serverId) : escHtml(action.title || action.type);
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
+
var serverActionType = action.type || '';
|
|
1048
|
+
var successLabel = serverActionType === 'dispatch' ? 'Dispatched' : serverActionType;
|
|
1049
|
+
status.innerHTML = '✓ ' + escHtml(successLabel) + ': <strong>' + label + '</strong>' +
|
|
1050
|
+
(action._serverDuplicate ? '<div style="font-size:10px;color:var(--orange);margin-top:2px">Already existed from a previous request; no duplicate work item was created.</div>' : '') +
|
|
1047
1051
|
(action._serverWarning ? '<div style="font-size:10px;color:var(--muted);margin-top:2px">' + escHtml(action._serverWarning) + '</div>' : '');
|
|
1048
|
-
status.style.color =
|
|
1052
|
+
status.style.color = 'var(--green)';
|
|
1049
1053
|
}
|
|
1050
1054
|
ccAddMessage('action', status.outerHTML, false, targetTabId);
|
|
1051
1055
|
if (['dispatch','fix','explore','review','test','create-meeting'].includes(action.type)) wakeEngine();
|
|
@@ -1336,8 +1340,10 @@ async function ccExecuteAction(action, targetTabId) {
|
|
|
1336
1340
|
break;
|
|
1337
1341
|
}
|
|
1338
1342
|
case 'link-pr': {
|
|
1339
|
-
await _ccFetch('/api/pull-requests/link', { url: action.url, title: action.title || '', project: action.project || '', autoObserve: action.autoObserve !== false });
|
|
1340
|
-
|
|
1343
|
+
var prLinkRes = await _ccFetch('/api/pull-requests/link', { url: action.url, title: action.title || '', project: action.project || '', autoObserve: action.autoObserve !== false });
|
|
1344
|
+
var prLinkData = await prLinkRes.json().catch(function() { return {}; });
|
|
1345
|
+
status.innerHTML = '✓ PR linked: <strong>' + escHtml(action.url) + '</strong>' +
|
|
1346
|
+
(prLinkData.message ? '<div style="font-size:11px;color:var(--muted);margin-top:4px">' + escHtml(prLinkData.message) + '</div>' : '');
|
|
1341
1347
|
status.style.color = 'var(--green)';
|
|
1342
1348
|
break;
|
|
1343
1349
|
}
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -367,9 +367,10 @@ function _qaBuildActionFeedbackHtml(actionFeedback) {
|
|
|
367
367
|
return '<div class="modal-qa-action-feedback" style="' + baseStyle + 'color:var(--red)">✗ ' + type + ' failed: ' + escHtml(item.error) + '</div>';
|
|
368
368
|
}
|
|
369
369
|
const label = escHtml(item.id || item.duplicateOf || '');
|
|
370
|
-
const
|
|
370
|
+
const verb = item.type === 'dispatch' ? 'Dispatched' : type;
|
|
371
|
+
const duplicate = item.duplicate ? '<div style="font-size:10px;color:var(--orange);margin-top:2px">Already existed from a previous request; no duplicate work item was created.</div>' : '';
|
|
371
372
|
const warning = item.warning ? '<div style="font-size:10px;color:var(--muted);margin-top:2px">' + escHtml(item.warning) + '</div>' : '';
|
|
372
|
-
return '<div class="modal-qa-action-feedback" style="' + baseStyle + 'color:
|
|
373
|
+
return '<div class="modal-qa-action-feedback" style="' + baseStyle + 'color:var(--green)">✓ ' + verb + ': <strong>' + label + '</strong>' + duplicate + warning + '</div>';
|
|
373
374
|
}).join('');
|
|
374
375
|
}
|
|
375
376
|
|
|
@@ -125,7 +125,7 @@ function openAddPrModal() {
|
|
|
125
125
|
'<div style="display:flex;flex-direction:column;gap:10px">' +
|
|
126
126
|
'<label style="color:var(--text);font-size:var(--text-md)">PR URL <input id="pr-link-url" style="' + inputStyle + '" placeholder="https://github.com/org/repo/pull/123"></label>' +
|
|
127
127
|
'<label style="color:var(--text);font-size:var(--text-md)">Title <input id="pr-link-title" style="' + inputStyle + '" placeholder="Short description (optional — auto-detected from URL)"></label>' +
|
|
128
|
-
'<label style="color:var(--text);font-size:var(--text-md)">Project <select id="pr-link-project" style="' + inputStyle + '"><option value="">Auto
|
|
128
|
+
'<label style="color:var(--text);font-size:var(--text-md)">Project <select id="pr-link-project" style="' + inputStyle + '"><option value="">Auto-detect from URL (central if no unique match)</option>' + projOpts + '</select></label>' +
|
|
129
129
|
'<label style="color:var(--text);font-size:var(--text-md)">Context <textarea id="pr-link-context" rows="3" style="' + inputStyle + ';resize:vertical" placeholder="Why are you linking this? What should agents know about it?"></textarea></label>' +
|
|
130
130
|
'<label style="display:flex;align-items:center;gap:8px;color:var(--text);font-size:var(--text-md);margin-top:4px;cursor:pointer">' +
|
|
131
131
|
'<input type="checkbox" id="pr-link-observe" style="width:16px;height:16px;accent-color:var(--blue)">' +
|
|
@@ -151,14 +151,16 @@ async function _submitLinkPr(e) {
|
|
|
151
151
|
const autoObserve = document.getElementById('pr-link-observe')?.checked || false;
|
|
152
152
|
|
|
153
153
|
try { closeModal(); } catch { /* expected */ }
|
|
154
|
-
showToast('cmd-toast', 'PR linked' + (autoObserve ? ' (auto-observe on)' : ''), true);
|
|
155
154
|
try {
|
|
156
155
|
const res = await fetch('/api/pull-requests/link', {
|
|
157
156
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
158
157
|
body: JSON.stringify({ url, title, project, context, autoObserve })
|
|
159
158
|
});
|
|
160
159
|
const data = await res.json();
|
|
161
|
-
if (res.ok) {
|
|
160
|
+
if (res.ok) {
|
|
161
|
+
showToast('cmd-toast', (data.message || 'PR linked') + (autoObserve ? ' (auto-observe on)' : ''), true, 6000);
|
|
162
|
+
refresh();
|
|
163
|
+
} else { alert('Failed: ' + (data.error || 'unknown')); openAddPrModal(); }
|
|
162
164
|
} catch (e) { alert('Error: ' + e.message); openAddPrModal(); }
|
|
163
165
|
}
|
|
164
166
|
|
package/dashboard.js
CHANGED
|
@@ -97,6 +97,13 @@ function reloadConfig() {
|
|
|
97
97
|
}
|
|
98
98
|
ensureConfiguredProjectStateFiles();
|
|
99
99
|
|
|
100
|
+
function resolveScheduleProjectValue(project, projects = PROJECTS) {
|
|
101
|
+
if (project === undefined) return { project: undefined };
|
|
102
|
+
const target = shared.resolveConfiguredProject(project, projects);
|
|
103
|
+
if (target.error) return { project: null, error: target.error };
|
|
104
|
+
return { project: target.project?.name || null };
|
|
105
|
+
}
|
|
106
|
+
|
|
100
107
|
function mutateDashboardConfig(mutator) {
|
|
101
108
|
return mutateJsonFileLocked(CONFIG_PATH, (config) => {
|
|
102
109
|
if (!config || typeof config !== 'object' || Array.isArray(config)) config = {};
|
|
@@ -375,6 +382,83 @@ function findProjectByName(projects, projectName) {
|
|
|
375
382
|
return shared.findProjectByName(projects, projectName);
|
|
376
383
|
}
|
|
377
384
|
|
|
385
|
+
function resolveManualPrLinkProject(url, projectName, projects = PROJECTS) {
|
|
386
|
+
const explicitProjectName = String(projectName || '').trim();
|
|
387
|
+
if (explicitProjectName) {
|
|
388
|
+
const explicitProject = shared.resolveProjectSource(explicitProjectName, projects, { allowCentral: false, minionsDir: MINIONS_DIR });
|
|
389
|
+
if (explicitProject.error) {
|
|
390
|
+
const err = new Error(explicitProject.error);
|
|
391
|
+
err.statusCode = 400;
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
394
|
+
const targetProject = explicitProject.project;
|
|
395
|
+
return {
|
|
396
|
+
project: targetProject,
|
|
397
|
+
resolution: {
|
|
398
|
+
reason: 'explicit',
|
|
399
|
+
project: targetProject.name || explicitProjectName,
|
|
400
|
+
storage: 'project',
|
|
401
|
+
message: `Linked to selected project "${targetProject.name || explicitProjectName}".`,
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const parsedPr = shared.parsePrUrl(url);
|
|
407
|
+
const prScope = parsedPr?.scope || '';
|
|
408
|
+
if (!prScope) {
|
|
409
|
+
return {
|
|
410
|
+
project: null,
|
|
411
|
+
resolution: {
|
|
412
|
+
reason: 'unrecognized-url',
|
|
413
|
+
project: 'central',
|
|
414
|
+
storage: 'central',
|
|
415
|
+
message: 'Could not infer a project from this PR URL; linked in central PR tracking.',
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const matches = projects.filter(p => shared.getProjectPrScope(p) === prScope);
|
|
421
|
+
if (matches.length === 1) {
|
|
422
|
+
const targetProject = matches[0];
|
|
423
|
+
return {
|
|
424
|
+
project: targetProject,
|
|
425
|
+
resolution: {
|
|
426
|
+
reason: 'inferred',
|
|
427
|
+
scope: prScope,
|
|
428
|
+
project: targetProject.name || '',
|
|
429
|
+
storage: 'project',
|
|
430
|
+
message: `Inferred project "${targetProject.name}" from PR scope ${prScope}.`,
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (matches.length > 1) {
|
|
436
|
+
const names = matches.map(p => p.name).filter(Boolean);
|
|
437
|
+
return {
|
|
438
|
+
project: null,
|
|
439
|
+
resolution: {
|
|
440
|
+
reason: 'ambiguous',
|
|
441
|
+
scope: prScope,
|
|
442
|
+
project: 'central',
|
|
443
|
+
storage: 'central',
|
|
444
|
+
matches: names,
|
|
445
|
+
message: `PR scope ${prScope} matches multiple configured projects (${names.join(', ')}); linked in central PR tracking. Select a project to attach it.`,
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
project: null,
|
|
452
|
+
resolution: {
|
|
453
|
+
reason: 'unmatched',
|
|
454
|
+
scope: prScope,
|
|
455
|
+
project: 'central',
|
|
456
|
+
storage: 'central',
|
|
457
|
+
message: `No configured project matches PR scope ${prScope}; linked in central PR tracking.`,
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
378
462
|
function resolveWorkItemsCreateTarget(projectName, projects = PROJECTS) {
|
|
379
463
|
const target = shared.resolveProjectSource(projectName, projects, { defaultWhenSingle: true, minionsDir: MINIONS_DIR });
|
|
380
464
|
if (target.error) return { error: target.error };
|
|
@@ -409,6 +493,73 @@ function findWorkItemsTargetById(id, source, projects = PROJECTS) {
|
|
|
409
493
|
return { found: false };
|
|
410
494
|
}
|
|
411
495
|
|
|
496
|
+
function buildPlanWorkItem(body, projects = PROJECTS, options = {}) {
|
|
497
|
+
if (!body?.title || !String(body.title).trim()) return { error: 'title is required' };
|
|
498
|
+
const target = resolveWorkItemsCreateTarget(body.project, projects);
|
|
499
|
+
if (target.error) return { error: target.error };
|
|
500
|
+
let now = options.now ? new Date(options.now) : new Date();
|
|
501
|
+
if (!Number.isFinite(now.getTime())) now = new Date();
|
|
502
|
+
const item = {
|
|
503
|
+
id: options.id || ('W-' + shared.uid()),
|
|
504
|
+
title: body.title,
|
|
505
|
+
type: 'plan',
|
|
506
|
+
priority: body.priority || 'high',
|
|
507
|
+
description: body.description || '',
|
|
508
|
+
status: WI_STATUS.PENDING,
|
|
509
|
+
created: now.toISOString(),
|
|
510
|
+
createdBy: 'dashboard',
|
|
511
|
+
branchStrategy: body.branch_strategy || body.branchStrategy || 'parallel',
|
|
512
|
+
};
|
|
513
|
+
if (target.project) item.project = target.project.name;
|
|
514
|
+
if (body.agent) item.agent = body.agent;
|
|
515
|
+
return { item, id: item.id };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function buildManualPrdItemPlan(body, projects = PROJECTS, options = {}) {
|
|
519
|
+
if (!body?.name || !String(body.name).trim()) return { error: 'name is required' };
|
|
520
|
+
const target = resolveWorkItemsCreateTarget(body.project, projects);
|
|
521
|
+
if (target.error) return { error: target.error };
|
|
522
|
+
|
|
523
|
+
const overrideProject = body.item_project ?? body.itemProject;
|
|
524
|
+
const itemProjectTarget = overrideProject !== undefined
|
|
525
|
+
? shared.resolveConfiguredProject(overrideProject, projects)
|
|
526
|
+
: null;
|
|
527
|
+
if (itemProjectTarget?.error) return { error: itemProjectTarget.error };
|
|
528
|
+
|
|
529
|
+
let now = options.now ? new Date(options.now) : new Date();
|
|
530
|
+
if (!Number.isFinite(now.getTime())) now = new Date();
|
|
531
|
+
const nowTime = now.getTime();
|
|
532
|
+
const projectName = target.project?.name || (projects.length > 0 ? projects[0].name : 'Unknown');
|
|
533
|
+
const itemProjectName = itemProjectTarget?.project?.name || target.project?.name || (projects.length === 1 ? projects[0].name : '');
|
|
534
|
+
const item = {
|
|
535
|
+
id: body.id || ('M' + String(nowTime).slice(-4)),
|
|
536
|
+
name: String(body.name).trim(),
|
|
537
|
+
description: body.description || '',
|
|
538
|
+
priority: body.priority || 'medium',
|
|
539
|
+
estimated_complexity: body.estimated_complexity || 'medium',
|
|
540
|
+
status: 'missing',
|
|
541
|
+
depends_on: [],
|
|
542
|
+
acceptance_criteria: [],
|
|
543
|
+
};
|
|
544
|
+
if (itemProjectName) item.project = itemProjectName;
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
plan: {
|
|
548
|
+
version: 'manual-' + now.toISOString().slice(0, 10),
|
|
549
|
+
project: projectName,
|
|
550
|
+
generated_by: 'dashboard',
|
|
551
|
+
generated_at: now.toISOString().slice(0, 10),
|
|
552
|
+
plan_summary: item.name,
|
|
553
|
+
status: 'approved',
|
|
554
|
+
requires_approval: false,
|
|
555
|
+
branch_strategy: 'parallel',
|
|
556
|
+
missing_features: [item],
|
|
557
|
+
open_questions: [],
|
|
558
|
+
},
|
|
559
|
+
id: item.id,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
412
563
|
function validatePipelineProjects(pipeline, projects = PROJECTS) {
|
|
413
564
|
const refs = [];
|
|
414
565
|
const collect = (value) => {
|
|
@@ -418,6 +569,7 @@ function validatePipelineProjects(pipeline, projects = PROJECTS) {
|
|
|
418
569
|
if (value.project !== undefined) collect(value.project);
|
|
419
570
|
else if (value._project !== undefined) collect(value._project);
|
|
420
571
|
else if (value.name !== undefined) collect(value.name);
|
|
572
|
+
else if (value.localPath !== undefined) collect(value.localPath);
|
|
421
573
|
return;
|
|
422
574
|
}
|
|
423
575
|
refs.push(String(value));
|
|
@@ -480,21 +632,7 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
|
|
|
480
632
|
throw err;
|
|
481
633
|
}
|
|
482
634
|
const projects = shared.getProjects(config);
|
|
483
|
-
const
|
|
484
|
-
const explicitProject = explicitProjectName
|
|
485
|
-
? shared.resolveProjectSource(explicitProjectName, projects, { allowCentral: false, minionsDir: MINIONS_DIR })
|
|
486
|
-
: null;
|
|
487
|
-
let targetProject = explicitProject?.project || null;
|
|
488
|
-
if (explicitProject?.error) {
|
|
489
|
-
const err = new Error(explicitProject.error);
|
|
490
|
-
err.statusCode = 400;
|
|
491
|
-
throw err;
|
|
492
|
-
}
|
|
493
|
-
if (!explicitProjectName) {
|
|
494
|
-
const prScope = shared.parsePrUrl(url)?.scope || '';
|
|
495
|
-
const matches = prScope ? projects.filter(p => shared.getProjectPrScope(p) === prScope) : [];
|
|
496
|
-
if (matches.length === 1) targetProject = matches[0];
|
|
497
|
-
}
|
|
635
|
+
const { project: targetProject, resolution: projectResolution } = resolveManualPrLinkProject(url, projectName, projects);
|
|
498
636
|
const prPath = targetProject ? shared.projectPrPath(targetProject) : shared.centralPullRequestsPath(MINIONS_DIR);
|
|
499
637
|
|
|
500
638
|
const prNumMatch = url.match(/\/pull\/(\d+)|pullrequest\/(\d+)/);
|
|
@@ -519,11 +657,12 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
|
|
|
519
657
|
_contextOnly: !autoObserve,
|
|
520
658
|
_autoObserve: !!autoObserve,
|
|
521
659
|
_context: contextText,
|
|
660
|
+
_projectResolution: projectResolution,
|
|
522
661
|
}, {
|
|
523
662
|
project: targetProject,
|
|
524
663
|
itemId: linkedWorkItemId,
|
|
525
664
|
});
|
|
526
|
-
return { ...result, prPath, targetProject, prNum };
|
|
665
|
+
return { ...result, prPath, targetProject, projectResolution, prNum };
|
|
527
666
|
}
|
|
528
667
|
|
|
529
668
|
function _normalizeSkillDirForCompare(dir) {
|
|
@@ -2686,6 +2825,8 @@ function normalizePipelineForCompare(pipeline) {
|
|
|
2686
2825
|
stages: Array.isArray(pipeline.stages) ? pipeline.stages : [],
|
|
2687
2826
|
trigger: pipeline.trigger && typeof pipeline.trigger === 'object' ? pipeline.trigger : {},
|
|
2688
2827
|
enabled: pipeline.enabled !== false,
|
|
2828
|
+
project: pipeline.project !== undefined ? pipeline.project : null,
|
|
2829
|
+
projects: Array.isArray(pipeline.projects) ? pipeline.projects : [],
|
|
2689
2830
|
stopWhen: pipeline.stopWhen || null,
|
|
2690
2831
|
monitoredResources: Array.isArray(pipeline.monitoredResources) ? pipeline.monitoredResources : [],
|
|
2691
2832
|
};
|
|
@@ -2699,6 +2840,8 @@ function buildPipelineFromAction(action) {
|
|
|
2699
2840
|
trigger: action.trigger && typeof action.trigger === 'object' ? action.trigger : {},
|
|
2700
2841
|
enabled: action.enabled !== false,
|
|
2701
2842
|
};
|
|
2843
|
+
if (action.project !== undefined) pipeline.project = action.project;
|
|
2844
|
+
if (Array.isArray(action.projects)) pipeline.projects = action.projects;
|
|
2702
2845
|
if (action.stopWhen) pipeline.stopWhen = action.stopWhen;
|
|
2703
2846
|
if (Array.isArray(action.monitoredResources) && action.monitoredResources.length > 0) {
|
|
2704
2847
|
pipeline.monitoredResources = action.monitoredResources;
|
|
@@ -2713,6 +2856,10 @@ function pipelineDefinitionsEqual(a, b) {
|
|
|
2713
2856
|
function createPipelineFromAction(action) {
|
|
2714
2857
|
const { savePipeline, getPipeline } = require('./engine/pipeline');
|
|
2715
2858
|
const pipeline = buildPipelineFromAction(action);
|
|
2859
|
+
const projectError = validatePipelineProjects(pipeline);
|
|
2860
|
+
if (projectError) {
|
|
2861
|
+
return { type: 'create-pipeline', id: pipeline.id, error: projectError };
|
|
2862
|
+
}
|
|
2716
2863
|
const existing = getPipeline(pipeline.id);
|
|
2717
2864
|
if (existing) {
|
|
2718
2865
|
if (pipelineDefinitionsEqual(existing, pipeline)) {
|
|
@@ -3078,7 +3225,9 @@ async function _ccExecuteLocalApiAction(action) {
|
|
|
3078
3225
|
|
|
3079
3226
|
async function executeCCActions(actions, { source = 'command-center', inferredProject = null } = {}) {
|
|
3080
3227
|
const results = [];
|
|
3081
|
-
|
|
3228
|
+
const dispatchIdsCreatedInThisCall = new Map();
|
|
3229
|
+
for (let actionIndex = 0; actionIndex < actions.length; actionIndex++) {
|
|
3230
|
+
const rawAction = actions[actionIndex];
|
|
3082
3231
|
const action = normalizeCCAction(rawAction);
|
|
3083
3232
|
if (action?._intentFallbackError) {
|
|
3084
3233
|
results.push({
|
|
@@ -3178,9 +3327,19 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
3178
3327
|
const createResult = createWorkItemWithDedup(wiPath, item);
|
|
3179
3328
|
if (!createResult.created) {
|
|
3180
3329
|
const duplicateId = createResult.duplicateOf || createResult.item?.id;
|
|
3330
|
+
if (duplicateId && dispatchIdsCreatedInThisCall.has(duplicateId)) {
|
|
3331
|
+
results.push({
|
|
3332
|
+
type: action.type,
|
|
3333
|
+
id: duplicateId,
|
|
3334
|
+
ok: true,
|
|
3335
|
+
reusedFromAction: dispatchIdsCreatedInThisCall.get(duplicateId),
|
|
3336
|
+
});
|
|
3337
|
+
break;
|
|
3338
|
+
}
|
|
3181
3339
|
results.push({ type: action.type, id: duplicateId, ok: true, duplicate: true, duplicateOf: duplicateId });
|
|
3182
3340
|
break;
|
|
3183
3341
|
}
|
|
3342
|
+
dispatchIdsCreatedInThisCall.set(id, actionIndex);
|
|
3184
3343
|
results.push({ type: action.type, id, ok: true });
|
|
3185
3344
|
|
|
3186
3345
|
// Pre-flight routing check: warn the user if no agent is currently available so the new
|
|
@@ -3404,6 +3563,7 @@ function _buildDocChatActionFeedback(actions, actionResults) {
|
|
|
3404
3563
|
feedback.push({ type, error: String(result.error) });
|
|
3405
3564
|
continue;
|
|
3406
3565
|
}
|
|
3566
|
+
if (result.reusedFromAction !== undefined) continue;
|
|
3407
3567
|
const id = result.id || result.duplicateOf;
|
|
3408
3568
|
if (!result.ok || !id) continue;
|
|
3409
3569
|
const item = { type, id: String(id), ok: true };
|
|
@@ -5035,21 +5195,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
5035
5195
|
try {
|
|
5036
5196
|
const body = await readBody(req);
|
|
5037
5197
|
if (!body.title || !body.title.trim()) return jsonReply(res, 400, { error: 'title is required' });
|
|
5038
|
-
const
|
|
5039
|
-
if (
|
|
5198
|
+
const planWorkItem = buildPlanWorkItem(body);
|
|
5199
|
+
if (planWorkItem.error) return jsonReply(res, 400, { error: planWorkItem.error });
|
|
5040
5200
|
// Write as a work item with type 'plan' — user must explicitly execute plan-to-prd after reviewing
|
|
5041
5201
|
const wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
id, title: body.title, type: 'plan',
|
|
5045
|
-
priority: body.priority || 'high', description: body.description || '',
|
|
5046
|
-
status: WI_STATUS.PENDING, created: new Date().toISOString(), createdBy: 'dashboard',
|
|
5047
|
-
branchStrategy: body.branch_strategy || body.branchStrategy || 'parallel',
|
|
5048
|
-
};
|
|
5049
|
-
if (target.project) item.project = target.project.name;
|
|
5050
|
-
if (body.agent) item.agent = body.agent;
|
|
5051
|
-
mutateWorkItems(wiPath, items => { items.push(item); });
|
|
5052
|
-
return jsonReply(res, 200, { ok: true, id, agent: body.agent || '' });
|
|
5202
|
+
mutateWorkItems(wiPath, items => { items.push(planWorkItem.item); });
|
|
5203
|
+
return jsonReply(res, 200, { ok: true, id: planWorkItem.id, agent: body.agent || '' });
|
|
5053
5204
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
5054
5205
|
}
|
|
5055
5206
|
|
|
@@ -5057,31 +5208,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
5057
5208
|
try {
|
|
5058
5209
|
const body = await readBody(req);
|
|
5059
5210
|
if (!body.name || !body.name.trim()) return jsonReply(res, 400, { error: 'name is required' });
|
|
5060
|
-
const
|
|
5061
|
-
if (
|
|
5211
|
+
const manualPrd = buildManualPrdItemPlan(body);
|
|
5212
|
+
if (manualPrd.error) return jsonReply(res, 400, { error: manualPrd.error });
|
|
5062
5213
|
|
|
5063
5214
|
if (!fs.existsSync(PRD_DIR)) fs.mkdirSync(PRD_DIR, { recursive: true });
|
|
5064
5215
|
|
|
5065
|
-
const id = body.id || ('M' + String(Date.now()).slice(-4));
|
|
5066
5216
|
const planFile = 'manual-' + shared.uid() + '.json';
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
project: target.project?.name || 'Unknown',
|
|
5070
|
-
generated_by: 'dashboard',
|
|
5071
|
-
generated_at: new Date().toISOString().slice(0, 10),
|
|
5072
|
-
plan_summary: body.name,
|
|
5073
|
-
status: 'approved',
|
|
5074
|
-
requires_approval: false,
|
|
5075
|
-
branch_strategy: 'parallel',
|
|
5076
|
-
missing_features: [{
|
|
5077
|
-
id, name: body.name, description: body.description || '',
|
|
5078
|
-
priority: body.priority || 'medium', estimated_complexity: body.estimated_complexity || 'medium',
|
|
5079
|
-
status: 'missing', depends_on: [], acceptance_criteria: [],
|
|
5080
|
-
}],
|
|
5081
|
-
open_questions: [],
|
|
5082
|
-
};
|
|
5083
|
-
safeWrite(path.join(PRD_DIR, planFile), plan);
|
|
5084
|
-
return jsonReply(res, 200, { ok: true, id, file: planFile });
|
|
5217
|
+
safeWrite(path.join(PRD_DIR, planFile), manualPrd.plan);
|
|
5218
|
+
return jsonReply(res, 200, { ok: true, id: manualPrd.id, file: planFile });
|
|
5085
5219
|
} catch (e) { return jsonReply(res, 400, { error: e.message }); }
|
|
5086
5220
|
}
|
|
5087
5221
|
|
|
@@ -7414,9 +7548,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7414
7548
|
const body = await readBody(req);
|
|
7415
7549
|
let { id, cron, title, type, project, agent, description, priority, enabled } = body;
|
|
7416
7550
|
if (!cron || !title) return jsonReply(res, 400, { error: 'cron and title are required' });
|
|
7417
|
-
|
|
7551
|
+
reloadConfig();
|
|
7552
|
+
const projectTarget = resolveScheduleProjectValue(project, PROJECTS);
|
|
7418
7553
|
if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
|
|
7419
|
-
project = projectTarget.project
|
|
7554
|
+
project = projectTarget.project || null;
|
|
7420
7555
|
|
|
7421
7556
|
// Auto-generate ID from title if not provided
|
|
7422
7557
|
if (!id) {
|
|
@@ -7451,10 +7586,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7451
7586
|
const body = await readBody(req);
|
|
7452
7587
|
let { id, cron, title, type, project, agent, description, priority, enabled } = body;
|
|
7453
7588
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
7589
|
+
reloadConfig();
|
|
7454
7590
|
if (project !== undefined) {
|
|
7455
|
-
const projectTarget =
|
|
7591
|
+
const projectTarget = resolveScheduleProjectValue(project, PROJECTS);
|
|
7456
7592
|
if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
|
|
7457
|
-
project = projectTarget.project
|
|
7593
|
+
project = projectTarget.project || null;
|
|
7458
7594
|
}
|
|
7459
7595
|
|
|
7460
7596
|
let missingSchedules = false;
|
|
@@ -7516,16 +7652,14 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7516
7652
|
reloadConfig();
|
|
7517
7653
|
const sched = (CONFIG.schedules || []).find(s => s.id === id);
|
|
7518
7654
|
if (!sched) return jsonReply(res, 404, { error: 'Schedule not found' });
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
sched.project = projectTarget.project.name;
|
|
7523
|
-
}
|
|
7655
|
+
const projectTarget = resolveScheduleProjectValue(sched.project, PROJECTS);
|
|
7656
|
+
if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
|
|
7657
|
+
const schedForRun = { ...sched, project: projectTarget.project || null };
|
|
7524
7658
|
|
|
7525
7659
|
const schedulerMod = require('./engine/scheduler');
|
|
7526
7660
|
let item;
|
|
7527
7661
|
try {
|
|
7528
|
-
item = schedulerMod.createScheduledWorkItem(
|
|
7662
|
+
item = schedulerMod.createScheduledWorkItem(schedForRun);
|
|
7529
7663
|
} catch (e) {
|
|
7530
7664
|
return jsonReply(res, 400, { error: e.message });
|
|
7531
7665
|
}
|
|
@@ -7536,7 +7670,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7536
7670
|
meeting = createMeeting({
|
|
7537
7671
|
title: item.title,
|
|
7538
7672
|
agenda: item.description,
|
|
7539
|
-
participants: Array.isArray(
|
|
7673
|
+
participants: Array.isArray(schedForRun.participants) ? schedForRun.participants : [],
|
|
7540
7674
|
});
|
|
7541
7675
|
} else {
|
|
7542
7676
|
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
@@ -7555,22 +7689,22 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7555
7689
|
return jsonReply(res, 409, {
|
|
7556
7690
|
error: 'Schedule already has an active work item',
|
|
7557
7691
|
id: duplicate.id,
|
|
7558
|
-
scheduleId:
|
|
7692
|
+
scheduleId: schedForRun.id,
|
|
7559
7693
|
});
|
|
7560
7694
|
}
|
|
7561
7695
|
}
|
|
7562
7696
|
|
|
7563
|
-
const runEntry = schedulerMod.recordScheduleRun(
|
|
7697
|
+
const runEntry = schedulerMod.recordScheduleRun(schedForRun.id, item.id);
|
|
7564
7698
|
try {
|
|
7565
7699
|
shared.mutateControl(control => ({ ...control, _wakeupAt: Date.now() }));
|
|
7566
7700
|
} catch (e) {
|
|
7567
|
-
shared.log('warn', `Schedule run-now wakeup failed for ${
|
|
7701
|
+
shared.log('warn', `Schedule run-now wakeup failed for ${schedForRun.id}: ${e.message}`);
|
|
7568
7702
|
}
|
|
7569
7703
|
invalidateStatusCache();
|
|
7570
7704
|
return jsonReply(res, 200, {
|
|
7571
7705
|
ok: true,
|
|
7572
7706
|
id: item.id,
|
|
7573
|
-
scheduleId:
|
|
7707
|
+
scheduleId: schedForRun.id,
|
|
7574
7708
|
lastRun: runEntry?.lastRun || null,
|
|
7575
7709
|
...(meeting ? { meetingId: meeting.id } : {}),
|
|
7576
7710
|
});
|
|
@@ -8356,7 +8490,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
8356
8490
|
{ method: 'GET', path: /^\/api\/plans\/([^?]+)$/, template: '/api/plans/:file', desc: 'Read a full plan (JSON from prd/ or markdown from plans/)', handler: handlePlansRead },
|
|
8357
8491
|
|
|
8358
8492
|
// PRD items
|
|
8359
|
-
{ method: 'POST', path: '/api/prd-items', desc: 'Create a PRD item as a plan file in prd/ (auto-approved)', params: 'name, description?, priority?, estimated_complexity?, project?, id?', handler: handlePrdItemsCreate },
|
|
8493
|
+
{ method: 'POST', path: '/api/prd-items', desc: 'Create a PRD item as a plan file in prd/ (auto-approved)', params: 'name, description?, priority?, estimated_complexity?, project?, item_project?, id?', handler: handlePrdItemsCreate },
|
|
8360
8494
|
{ method: 'POST', path: '/api/prd-items/update', desc: 'Edit a PRD item in its source plan JSON', params: 'source, itemId, name?, description?, priority?, estimated_complexity?, status?', handler: handlePrdItemsUpdate },
|
|
8361
8495
|
{ method: 'POST', path: '/api/prd-items/remove', desc: 'Remove a PRD item from plan + cancel materialized work item', params: 'source, itemId', handler: handlePrdItemsRemove },
|
|
8362
8496
|
// /api/prd/regenerate removed — use /api/plans/approve which does diff-aware update
|
|
@@ -8390,9 +8524,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
8390
8524
|
} catch (e) {
|
|
8391
8525
|
return jsonReply(res, e.statusCode || 400, { error: e.message }, req);
|
|
8392
8526
|
}
|
|
8393
|
-
const { id: prId, prPath, prNum, created, linked } = linkResult;
|
|
8527
|
+
const { id: prId, prPath, prNum, created, linked, targetProject, projectResolution } = linkResult;
|
|
8394
8528
|
invalidateStatusCache();
|
|
8395
|
-
jsonReply(res, 200, {
|
|
8529
|
+
jsonReply(res, 200, {
|
|
8530
|
+
ok: true,
|
|
8531
|
+
id: prId,
|
|
8532
|
+
created,
|
|
8533
|
+
linked,
|
|
8534
|
+
project: targetProject?.name || 'central',
|
|
8535
|
+
projectResolution,
|
|
8536
|
+
message: projectResolution?.message || 'PR linked.',
|
|
8537
|
+
});
|
|
8396
8538
|
|
|
8397
8539
|
// Async-enrich: fetch title, description, branch, author from GitHub/ADO API
|
|
8398
8540
|
(async () => {
|
|
@@ -9012,6 +9154,9 @@ module.exports = {
|
|
|
9012
9154
|
_findDuplicateWorkItemCreate: findDuplicateWorkItemCreate,
|
|
9013
9155
|
_createWorkItemWithDedup: createWorkItemWithDedup,
|
|
9014
9156
|
_resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
|
|
9157
|
+
_buildPlanWorkItem: buildPlanWorkItem,
|
|
9158
|
+
_buildManualPrdItemPlan: buildManualPrdItemPlan,
|
|
9159
|
+
_resolveScheduleProjectValue: resolveScheduleProjectValue,
|
|
9015
9160
|
_collectArchivedWorkItems: collectArchivedWorkItems,
|
|
9016
9161
|
_createPipelineFromAction: createPipelineFromAction,
|
|
9017
9162
|
_setCcLocalApiInvokerForTest,
|
package/engine/issues.js
CHANGED
|
@@ -9,6 +9,7 @@ const shared = require('./shared');
|
|
|
9
9
|
|
|
10
10
|
const DEFAULT_REPO = 'yemi33/minions';
|
|
11
11
|
const DEFAULT_LABELS = ['bug'];
|
|
12
|
+
const WRITABLE_REPO_PERMISSIONS = new Set(['WRITE', 'MAINTAIN', 'ADMIN']);
|
|
12
13
|
|
|
13
14
|
class GitHubIssueError extends Error {
|
|
14
15
|
constructor(message, statusCode = 500) {
|
|
@@ -67,16 +68,105 @@ function extractIssueUrl(output) {
|
|
|
67
68
|
return match ? match[0] : null;
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
function runGh(execFileSync, args, timeout) {
|
|
71
|
-
|
|
71
|
+
function runGh(execFileSync, args, timeout, { env } = {}) {
|
|
72
|
+
const opts = {
|
|
72
73
|
encoding: 'utf8',
|
|
73
74
|
timeout,
|
|
74
75
|
windowsHide: true,
|
|
75
|
-
}
|
|
76
|
+
};
|
|
77
|
+
if (env) opts.env = { ...process.env, ...env };
|
|
78
|
+
return execFileSync('gh', args, opts);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isWritableRepoPermission(permission) {
|
|
82
|
+
return WRITABLE_REPO_PERMISSIONS.has(String(permission || '').trim().toUpperCase());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function repoViewerPermission({ repo, execFileSync, ghEnv }) {
|
|
86
|
+
const output = runGh(execFileSync, ['repo', 'view', repo, '--json', 'viewerPermission'], 10000, { env: ghEnv });
|
|
87
|
+
const parsed = JSON.parse(output || '{}');
|
|
88
|
+
return String(parsed.viewerPermission || '').trim().toUpperCase();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseGhAuthUsers(output) {
|
|
92
|
+
const users = [];
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
for (const line of String(output || '').split(/\r?\n/)) {
|
|
95
|
+
const match = line.match(/\baccount\s+([^\s()]+)/i) || line.match(/\bas\s+([^\s()]+)/i);
|
|
96
|
+
if (!match) continue;
|
|
97
|
+
const user = match[1].trim();
|
|
98
|
+
const key = user.toLowerCase();
|
|
99
|
+
if (!user || seen.has(key)) continue;
|
|
100
|
+
seen.add(key);
|
|
101
|
+
users.push(user);
|
|
102
|
+
}
|
|
103
|
+
return users;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function listAuthenticatedGhUsers({ execFileSync }) {
|
|
107
|
+
const output = runGh(execFileSync, ['auth', 'status', '--hostname', 'github.com'], 10000);
|
|
108
|
+
return parseGhAuthUsers(output);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function ghTokenForUser({ user, execFileSync }) {
|
|
112
|
+
return String(runGh(execFileSync, ['auth', 'token', '--hostname', 'github.com', '--user', user], 10000) || '').trim();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveWritableGitHubEnv({ repo, execFileSync }) {
|
|
116
|
+
let activePermission = '';
|
|
117
|
+
let activeError = null;
|
|
118
|
+
try {
|
|
119
|
+
activePermission = repoViewerPermission({ repo, execFileSync });
|
|
120
|
+
if (isWritableRepoPermission(activePermission)) return undefined;
|
|
121
|
+
} catch (e) {
|
|
122
|
+
activeError = e;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let users = [];
|
|
126
|
+
let accountListError = null;
|
|
127
|
+
try {
|
|
128
|
+
users = listAuthenticatedGhUsers({ execFileSync });
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (activeError && isAuthError(activeError) && isAuthError(e)) {
|
|
131
|
+
throw new GitHubIssueError('GitHub auth required. Run: gh auth login', 401);
|
|
132
|
+
}
|
|
133
|
+
accountListError = e;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const candidateErrors = [];
|
|
137
|
+
for (const user of users) {
|
|
138
|
+
try {
|
|
139
|
+
const token = ghTokenForUser({ user, execFileSync });
|
|
140
|
+
if (!token) continue;
|
|
141
|
+
const ghEnv = { GH_TOKEN: token };
|
|
142
|
+
const permission = repoViewerPermission({ repo, execFileSync, ghEnv });
|
|
143
|
+
if (isWritableRepoPermission(permission)) return ghEnv;
|
|
144
|
+
} catch (e) {
|
|
145
|
+
candidateErrors.push(`${user}: ${conciseGhMessage(e)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const activeDetail = activePermission
|
|
150
|
+
? ` Active account permission is ${activePermission}.`
|
|
151
|
+
: activeError
|
|
152
|
+
? ` Active account permission could not be checked: ${conciseGhMessage(activeError)}.`
|
|
153
|
+
: '';
|
|
154
|
+
const authStatusDetail = accountListError
|
|
155
|
+
? ` Authenticated accounts could not be listed: ${conciseGhMessage(accountListError)}.`
|
|
156
|
+
: '';
|
|
157
|
+
const candidateDetail = candidateErrors.length
|
|
158
|
+
? ` Checked ${candidateErrors.length} authenticated account(s) without finding write access.`
|
|
159
|
+
: '';
|
|
160
|
+
throw new GitHubIssueError(
|
|
161
|
+
`No authenticated GitHub account with write access to ${repo}.${activeDetail} ` +
|
|
162
|
+
`${authStatusDetail}${candidateDetail} ` +
|
|
163
|
+
`Run: gh auth login --hostname github.com with a profile that can write to ${repo}, or set GH_TOKEN for that account.`,
|
|
164
|
+
403
|
|
165
|
+
);
|
|
76
166
|
}
|
|
77
167
|
|
|
78
|
-
function listRepoLabels({ repo, execFileSync }) {
|
|
79
|
-
const output = runGh(execFileSync, ['label', 'list', '--repo', repo, '--json', 'name', '--limit', '1000'], 15000);
|
|
168
|
+
function listRepoLabels({ repo, execFileSync, ghEnv }) {
|
|
169
|
+
const output = runGh(execFileSync, ['label', 'list', '--repo', repo, '--json', 'name', '--limit', '1000'], 15000, { env: ghEnv });
|
|
80
170
|
const parsed = JSON.parse(output || '[]');
|
|
81
171
|
if (!Array.isArray(parsed)) {
|
|
82
172
|
throw new GitHubIssueError('GitHub label list returned an unexpected response shape');
|
|
@@ -89,14 +179,14 @@ function listRepoLabels({ repo, execFileSync }) {
|
|
|
89
179
|
return labelsByLower;
|
|
90
180
|
}
|
|
91
181
|
|
|
92
|
-
function resolveLabels({ labels, repo, execFileSync }) {
|
|
182
|
+
function resolveLabels({ labels, repo, execFileSync, ghEnv }) {
|
|
93
183
|
const requested = normalizeLabels(labels);
|
|
94
184
|
if (requested.length === 0) {
|
|
95
185
|
return { requested, labelsToApply: [], labelsSkipped: [], validationUnavailable: false };
|
|
96
186
|
}
|
|
97
187
|
|
|
98
188
|
try {
|
|
99
|
-
const available = listRepoLabels({ repo, execFileSync });
|
|
189
|
+
const available = listRepoLabels({ repo, execFileSync, ghEnv });
|
|
100
190
|
const labelsToApply = [];
|
|
101
191
|
const labelsSkipped = [];
|
|
102
192
|
for (const label of requested) {
|
|
@@ -175,10 +265,10 @@ function _redactIssueContent(value, { repo, projects } = {}) {
|
|
|
175
265
|
});
|
|
176
266
|
}
|
|
177
267
|
|
|
178
|
-
function createIssueWithLabels({ title, bodyFile, repo, labels, execFileSync }) {
|
|
268
|
+
function createIssueWithLabels({ title, bodyFile, repo, labels, execFileSync, ghEnv }) {
|
|
179
269
|
const args = ['issue', 'create', '--repo', repo, '--title', title, '--body-file', bodyFile];
|
|
180
270
|
if (labels.length > 0) args.push('--label', labels.join(','));
|
|
181
|
-
const output = runGh(execFileSync, args, 30000);
|
|
271
|
+
const output = runGh(execFileSync, args, 30000, { env: ghEnv });
|
|
182
272
|
const url = extractIssueUrl(output);
|
|
183
273
|
if (!url) {
|
|
184
274
|
throw new GitHubIssueError(`Issue may not have been created: ${conciseGhMessage(output)}`);
|
|
@@ -202,6 +292,7 @@ function createGitHubIssue({
|
|
|
202
292
|
} catch (e) {
|
|
203
293
|
throw new GitHubIssueError('gh CLI not found. Install from https://cli.github.com/');
|
|
204
294
|
}
|
|
295
|
+
const ghEnv = resolveWritableGitHubEnv({ repo, execFileSync });
|
|
205
296
|
|
|
206
297
|
const redactionProjects = projects || shared.getProjects();
|
|
207
298
|
const safeTitle = _redactIssueContent(title, { repo, projects: redactionProjects });
|
|
@@ -214,13 +305,14 @@ function createGitHubIssue({
|
|
|
214
305
|
|
|
215
306
|
let resolved;
|
|
216
307
|
try {
|
|
217
|
-
resolved = resolveLabels({ labels, repo, execFileSync });
|
|
308
|
+
resolved = resolveLabels({ labels, repo, execFileSync, ghEnv });
|
|
218
309
|
const created = createIssueWithLabels({
|
|
219
310
|
title: safeTitle,
|
|
220
311
|
bodyFile,
|
|
221
312
|
repo,
|
|
222
313
|
labels: resolved.labelsToApply,
|
|
223
314
|
execFileSync,
|
|
315
|
+
ghEnv,
|
|
224
316
|
});
|
|
225
317
|
const filedWithoutLabels = resolved.requested.length > 0 && resolved.labelsToApply.length === 0;
|
|
226
318
|
return {
|
|
@@ -237,7 +329,7 @@ function createGitHubIssue({
|
|
|
237
329
|
if (isAuthError(e)) throw new GitHubIssueError('GitHub auth required. Run: gh auth login', 401);
|
|
238
330
|
if (resolved && resolved.labelsToApply.length > 0 && isLabelUnavailableError(e)) {
|
|
239
331
|
try {
|
|
240
|
-
const created = createIssueWithLabels({ title: safeTitle, bodyFile, repo, labels: [], execFileSync });
|
|
332
|
+
const created = createIssueWithLabels({ title: safeTitle, bodyFile, repo, labels: [], execFileSync, ghEnv });
|
|
241
333
|
const skipped = normalizeLabels([...resolved.labelsSkipped, ...resolved.labelsToApply], []);
|
|
242
334
|
return {
|
|
243
335
|
ok: true,
|
package/engine/pipeline.js
CHANGED
|
@@ -205,19 +205,26 @@ function _pipelineProjectValues(...values) {
|
|
|
205
205
|
for (const value of values) {
|
|
206
206
|
if (value === undefined) continue;
|
|
207
207
|
if (Array.isArray(value)) out.push(...value);
|
|
208
|
+
else if (value && typeof value === 'object' && value.project !== undefined) out.push(value.project);
|
|
209
|
+
else if (value && typeof value === 'object' && value._project !== undefined) out.push(value._project);
|
|
208
210
|
else out.push(value);
|
|
209
211
|
}
|
|
210
212
|
return out;
|
|
211
213
|
}
|
|
212
214
|
|
|
213
|
-
function
|
|
215
|
+
function _pipelineProjectValuePresent(value) {
|
|
216
|
+
if (value === undefined) return false;
|
|
217
|
+
if (value === null) return true;
|
|
218
|
+
if (typeof value === 'string') return value.trim() !== '';
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function _resolvePipelineProjectValues(rawValues, config) {
|
|
214
223
|
const projects = shared.getProjects(config);
|
|
215
|
-
const rawValues = _pipelineProjectValues(...values);
|
|
216
|
-
if (rawValues.length === 0) return { projects: [null] };
|
|
217
224
|
const resolved = [];
|
|
218
225
|
const seen = new Set();
|
|
219
226
|
for (const raw of rawValues) {
|
|
220
|
-
if (raw
|
|
227
|
+
if (!_pipelineProjectValuePresent(raw)) continue;
|
|
221
228
|
if (raw === null || String(raw).trim().toLowerCase() === 'central') {
|
|
222
229
|
if (!seen.has('central')) { seen.add('central'); resolved.push(null); }
|
|
223
230
|
continue;
|
|
@@ -230,6 +237,16 @@ function _resolvePipelineProjects(values, config) {
|
|
|
230
237
|
return { projects: resolved.length ? resolved : [null] };
|
|
231
238
|
}
|
|
232
239
|
|
|
240
|
+
function _resolvePipelineProjects(scopes, config) {
|
|
241
|
+
for (const scope of (Array.isArray(scopes) ? scopes : [scopes])) {
|
|
242
|
+
const values = _pipelineProjectValues(...(Array.isArray(scope) ? scope : [scope]))
|
|
243
|
+
.filter(_pipelineProjectValuePresent);
|
|
244
|
+
if (values.length === 0) continue;
|
|
245
|
+
return _resolvePipelineProjectValues(values, config);
|
|
246
|
+
}
|
|
247
|
+
return { projects: [null] };
|
|
248
|
+
}
|
|
249
|
+
|
|
233
250
|
function _pipelineProjectSlug(project) {
|
|
234
251
|
return project ? shared.safeSlugComponent(project.name || 'project', 32).toLowerCase() : 'central';
|
|
235
252
|
}
|
|
@@ -339,12 +356,9 @@ function executeTaskStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
339
356
|
for (let i = 0; i < count; i++) {
|
|
340
357
|
const item = items[i % items.length];
|
|
341
358
|
const projectResolution = _resolvePipelineProjects([
|
|
342
|
-
item.projects,
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
stage.project,
|
|
346
|
-
pipeline.projects,
|
|
347
|
-
pipeline.project,
|
|
359
|
+
[item.projects, item.project],
|
|
360
|
+
[stage.projects, stage.project],
|
|
361
|
+
[pipeline.projects, pipeline.project],
|
|
348
362
|
], config);
|
|
349
363
|
if (projectResolution.error) return { status: PIPELINE_STATUS.FAILED, error: projectResolution.error };
|
|
350
364
|
for (const project of projectResolution.projects) {
|
|
@@ -376,7 +390,11 @@ function executeTaskStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
376
390
|
touchedProjects.push(project?.name || null);
|
|
377
391
|
}
|
|
378
392
|
}
|
|
379
|
-
|
|
393
|
+
const projects = [...new Set(touchedProjects)];
|
|
394
|
+
return {
|
|
395
|
+
status: PIPELINE_STATUS.RUNNING,
|
|
396
|
+
artifacts: { workItems: [...new Set(createdIds)], projects },
|
|
397
|
+
};
|
|
380
398
|
}
|
|
381
399
|
|
|
382
400
|
function executeTaskStageLegacy(stage, stageState, run, config) {
|
|
@@ -488,7 +506,10 @@ async function executePlanStage(stage, stageState, run, config, pipeline = {}) {
|
|
|
488
506
|
if (!fs.existsSync(plansDir)) fs.mkdirSync(plansDir, { recursive: true });
|
|
489
507
|
|
|
490
508
|
const slug = slugify(stage.title || 'pipeline-plan');
|
|
491
|
-
const projectResolution = _resolvePipelineProjects([
|
|
509
|
+
const projectResolution = _resolvePipelineProjects([
|
|
510
|
+
[stage.projects, stage.project],
|
|
511
|
+
[pipeline.projects, pipeline.project],
|
|
512
|
+
], config);
|
|
492
513
|
if (projectResolution.error) return { status: PIPELINE_STATUS.FAILED, error: projectResolution.error };
|
|
493
514
|
const targetProjects = projectResolution.projects;
|
|
494
515
|
const wiIdForProject = (project) => `PL-${run.runId.slice(4, 12)}-${stage.id}-prd${targetProjects.length > 1 || project ? '-' + _pipelineProjectSlug(project) : ''}`;
|
|
@@ -1093,6 +1114,7 @@ module.exports = {
|
|
|
1093
1114
|
getPipelineRuns, getActiveRun, startRun, updateRunStage, completeRun,
|
|
1094
1115
|
discoverPipelineWork,
|
|
1095
1116
|
evaluateCondition, // exported for testing
|
|
1096
|
-
executeTaskStage, isStageComplete, resolveTemplate, // exported for testing
|
|
1117
|
+
executeTaskStage, executePlanStage, isStageComplete, resolveTemplate, // exported for testing
|
|
1118
|
+
_resolvePipelineProjects, // exported for testing
|
|
1097
1119
|
_findMeetingsInRun, _findExistingPlanForMeeting, _findExistingPrdForPlan, // exported for testing
|
|
1098
1120
|
};
|
package/engine/shared.js
CHANGED
|
@@ -2963,7 +2963,7 @@ function upsertPullRequestRecord(prPath, entry, { project = null, itemId = null,
|
|
|
2963
2963
|
target[key] = normalizedEntry[key];
|
|
2964
2964
|
}
|
|
2965
2965
|
}
|
|
2966
|
-
for (const key of ['_manual', '_autoObserve', '_context']) {
|
|
2966
|
+
for (const key of ['_manual', '_autoObserve', '_context', '_projectResolution']) {
|
|
2967
2967
|
if (normalizedEntry[key] != null) target[key] = normalizedEntry[key];
|
|
2968
2968
|
}
|
|
2969
2969
|
if (normalizedEntry._contextOnly != null) {
|
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;
|
|
@@ -3072,7 +3093,8 @@ async function discoverFromPrs(config, project) {
|
|
|
3072
3093
|
/**
|
|
3073
3094
|
* Scan work-items.json for manually queued tasks
|
|
3074
3095
|
*/
|
|
3075
|
-
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}`);
|
|
3076
3098
|
const vars = {
|
|
3077
3099
|
...buildBaseVars(agentId, config, project),
|
|
3078
3100
|
item_id: item.id,
|
|
@@ -3089,7 +3111,7 @@ function renderProjectWorkItemPromptForAgent(item, workType, agentId, config, pr
|
|
|
3089
3111
|
scope_section: `## Scope: Project — ${project?.name || 'default'}\n\nThis task is scoped to a single project.`,
|
|
3090
3112
|
branch_name: branchName,
|
|
3091
3113
|
project_path: root,
|
|
3092
|
-
worktree_path:
|
|
3114
|
+
worktree_path: worktreePath,
|
|
3093
3115
|
commit_message: item.commitMessage || `feat: ${item.title || item.id}`,
|
|
3094
3116
|
notes_content: '',
|
|
3095
3117
|
pr_id: item.pr_id || item._pr || item.targetPr || item.sourcePr || item.pr || '',
|
|
@@ -3740,12 +3762,9 @@ function discoverCentralWorkItems(config) {
|
|
|
3740
3762
|
|
|
3741
3763
|
const workType = routing.normalizeWorkType(item.type, WORK_TYPE.IMPLEMENT);
|
|
3742
3764
|
const isFanOut = item.scope === 'fan-out';
|
|
3743
|
-
const
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
: null;
|
|
3747
|
-
if (explicitProjectTarget?.error) {
|
|
3748
|
-
const error = explicitProjectTarget.error;
|
|
3765
|
+
const itemProjectResolution = shared.resolveConfiguredProject(item.project, projects);
|
|
3766
|
+
if (itemProjectResolution.error) {
|
|
3767
|
+
const error = itemProjectResolution.error;
|
|
3749
3768
|
mutations.set(item.id, { status: WI_STATUS.FAILED, failReason: error, failedAt: ts() });
|
|
3750
3769
|
log('warn', `central work item ${item.id}: ${error}`);
|
|
3751
3770
|
continue;
|
|
@@ -3849,13 +3868,11 @@ function discoverCentralWorkItems(config) {
|
|
|
3849
3868
|
planReadError = e;
|
|
3850
3869
|
}
|
|
3851
3870
|
}
|
|
3852
|
-
const
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
if (requestedTarget?.error) {
|
|
3858
|
-
const error = requestedTarget.error;
|
|
3871
|
+
const requestedProjectResolution = declaredPlanProject
|
|
3872
|
+
? shared.resolveConfiguredProject(declaredPlanProject, projects)
|
|
3873
|
+
: itemProjectResolution;
|
|
3874
|
+
if (requestedProjectResolution.error) {
|
|
3875
|
+
const error = requestedProjectResolution.error;
|
|
3859
3876
|
mutations.set(item.id, {
|
|
3860
3877
|
status: WI_STATUS.FAILED,
|
|
3861
3878
|
failReason: error,
|
|
@@ -3865,7 +3882,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3865
3882
|
log('warn', `central work item ${item.id}: ${error}`);
|
|
3866
3883
|
continue;
|
|
3867
3884
|
}
|
|
3868
|
-
const targetProject =
|
|
3885
|
+
const targetProject = requestedProjectResolution.project || (projects.length === 1 ? projects[0] : null);
|
|
3869
3886
|
if (declaredPlanProject) {
|
|
3870
3887
|
const projectMutation = { project: targetProject.name, _declaredPlanProject: declaredPlanProject };
|
|
3871
3888
|
mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, projectMutation));
|
|
@@ -4916,6 +4933,7 @@ module.exports = {
|
|
|
4916
4933
|
|
|
4917
4934
|
// Playbooks
|
|
4918
4935
|
renderPlaybook, validatePlaybookVars, PLAYBOOK_REQUIRED_VARS, buildWorkItemDispatchVars,
|
|
4936
|
+
renderProjectWorkItemPromptForAgent, // exported for testing
|
|
4919
4937
|
|
|
4920
4938
|
// Timeout / Steering / Idle (re-exported from engine/timeout.js)
|
|
4921
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.1813",
|
|
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.
|