@yemi33/minions 0.1.1807 → 0.1.1809
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 +8 -0
- package/dashboard/js/command-center.js +11 -8
- package/dashboard/js/command-input.js +2 -1
- package/dashboard/js/modal-qa.js +1 -1
- package/dashboard.js +186 -4
- package/engine/copilot-models.json +1 -1
- package/engine/playbook.js +21 -6
- package/engine/shared.js +20 -1
- package/engine.js +143 -10
- package/package.json +1 -1
- package/playbooks/plan-to-prd.md +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -567,7 +567,8 @@ function ccAddMessage(role, html, skipSave, targetTabId, meta) {
|
|
|
567
567
|
}
|
|
568
568
|
}
|
|
569
569
|
|
|
570
|
-
async function ccSend() {
|
|
570
|
+
async function ccSend(options) {
|
|
571
|
+
var intentMetadata = options && options.intentMetadata ? options.intentMetadata : null;
|
|
571
572
|
var input = document.getElementById('cc-input');
|
|
572
573
|
var message = input.value.trim();
|
|
573
574
|
if (!message) return;
|
|
@@ -580,11 +581,11 @@ async function ccSend() {
|
|
|
580
581
|
|
|
581
582
|
// If this tab is already processing, queue the message
|
|
582
583
|
if (tab._sending) {
|
|
583
|
-
tab._queue.push(message);
|
|
584
|
+
tab._queue.push({ message: message, intentMetadata: intentMetadata });
|
|
584
585
|
_renderQueueIndicator();
|
|
585
586
|
return;
|
|
586
587
|
}
|
|
587
|
-
var wasAborted = await _ccDoSend(message, false, originTabId);
|
|
588
|
+
var wasAborted = await _ccDoSend(message, false, originTabId, intentMetadata);
|
|
588
589
|
|
|
589
590
|
// Flush queued messages to the ORIGINAL tab, even if user switched tabs
|
|
590
591
|
while (tab._queue && tab._queue.length > 0) {
|
|
@@ -592,8 +593,10 @@ async function ccSend() {
|
|
|
592
593
|
await new Promise(function(r) { setTimeout(r, 1500); });
|
|
593
594
|
}
|
|
594
595
|
var next = tab._queue.shift();
|
|
596
|
+
var nextMessage = typeof next === 'string' ? next : next.message;
|
|
597
|
+
var nextIntentMetadata = typeof next === 'string' ? null : (next.intentMetadata || null);
|
|
595
598
|
_renderQueueIndicator();
|
|
596
|
-
wasAborted = await _ccDoSend(
|
|
599
|
+
wasAborted = await _ccDoSend(nextMessage, false, originTabId, nextIntentMetadata);
|
|
597
600
|
}
|
|
598
601
|
}
|
|
599
602
|
|
|
@@ -608,13 +611,13 @@ function _renderQueueIndicator() {
|
|
|
608
611
|
var el = document.createElement('div');
|
|
609
612
|
el.className = 'cc-queue-item';
|
|
610
613
|
el.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:12px;line-height:1.6;max-width:95%;align-self:flex-end;background:var(--blue);color:#fff;opacity:0.5;order:9999';
|
|
611
|
-
el.innerHTML = escHtml(m) + '<div style="font-size:9px;opacity:0.7;font-style:italic;margin-top:2px">queued</div>';
|
|
614
|
+
el.innerHTML = escHtml(typeof m === 'string' ? m : m.message) + '<div style="font-size:9px;opacity:0.7;font-style:italic;margin-top:2px">queued</div>';
|
|
612
615
|
msgs.appendChild(el);
|
|
613
616
|
});
|
|
614
617
|
if (msgs.scrollHeight - msgs.scrollTop - msgs.clientHeight < 150) msgs.scrollTop = msgs.scrollHeight;
|
|
615
618
|
}
|
|
616
619
|
|
|
617
|
-
async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
620
|
+
async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
|
|
618
621
|
// Client-side /pin and /unpin — no LLM round-trip needed
|
|
619
622
|
var pinMatch = message.match(/^\/(pin|unpin)\s+(.+)/i);
|
|
620
623
|
if (pinMatch) {
|
|
@@ -762,7 +765,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
762
765
|
if (!isReconnect && res.status === 429 && (!activeTab._429retries || activeTab._429retries < 3)) {
|
|
763
766
|
activeTab._429retries = (activeTab._429retries || 0) + 1;
|
|
764
767
|
await new Promise(function(r) { setTimeout(r, 1500); });
|
|
765
|
-
return await _ccConsumeStream({ message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null, transcript: _ccBuildTranscript(activeTab) }, false);
|
|
768
|
+
return await _ccConsumeStream({ message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null, transcript: _ccBuildTranscript(activeTab), intentMetadata: intentMetadata || null }, false);
|
|
766
769
|
}
|
|
767
770
|
activeTab._429retries = 0;
|
|
768
771
|
var errText = await res.text();
|
|
@@ -871,7 +874,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
871
874
|
while (true) {
|
|
872
875
|
var consume = await _ccConsumeStream(
|
|
873
876
|
reconnectAttempts === 0
|
|
874
|
-
? { message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null, transcript: _ccBuildTranscript(activeTab) }
|
|
877
|
+
? { message: message, tabId: activeTabId, sessionId: activeTab.sessionId || null, transcript: _ccBuildTranscript(activeTab), intentMetadata: intentMetadata || null }
|
|
875
878
|
: { tabId: activeTabId, sessionId: activeTab.sessionId || null, reconnect: true },
|
|
876
879
|
reconnectAttempts > 0
|
|
877
880
|
);
|
|
@@ -134,12 +134,13 @@ async function cmdSubmit() {
|
|
|
134
134
|
const input = document.getElementById('cmd-input');
|
|
135
135
|
const raw = input.value.trim();
|
|
136
136
|
if (!raw) return showToast('cmd-toast', 'Type something first', false);
|
|
137
|
+
const parsed = cmdParseInput(raw);
|
|
137
138
|
|
|
138
139
|
// Route to Command Center panel
|
|
139
140
|
input.value = '';
|
|
140
141
|
if (!_ccOpen) toggleCommandCenter();
|
|
141
142
|
document.getElementById('cc-input').value = raw;
|
|
142
|
-
ccSend();
|
|
143
|
+
ccSend({ intentMetadata: parsed });
|
|
143
144
|
cmdSaveHistory(raw, 'cc');
|
|
144
145
|
return;
|
|
145
146
|
}
|
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -344,7 +344,7 @@ function _qaBuildAssistantHtml(text, opts) {
|
|
|
344
344
|
'</div>';
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
const QA_WORK_ITEM_ACTION_TYPES = new Set(['dispatch', 'fix', '
|
|
347
|
+
const QA_WORK_ITEM_ACTION_TYPES = new Set(['dispatch', 'fix', 'explore', 'review', 'test']);
|
|
348
348
|
|
|
349
349
|
function _qaActionFeedbackHandled(action, actionResult) {
|
|
350
350
|
const type = String(actionResult?.type || action?.type || '');
|
package/dashboard.js
CHANGED
|
@@ -1815,6 +1815,7 @@ function _extractActionsJson(segment) {
|
|
|
1815
1815
|
}
|
|
1816
1816
|
|
|
1817
1817
|
const CC_DISPATCH_ACTION_ALIASES = new Set(['fix', 'explore', 'review', 'test']);
|
|
1818
|
+
const CC_ACTION_INTENT_WORK_TYPES = new Set(['fix', 'implement', 'explore', 'review', 'test']);
|
|
1818
1819
|
|
|
1819
1820
|
function normalizeCCAction(action) {
|
|
1820
1821
|
if (!action || typeof action !== 'object') return action;
|
|
@@ -1827,6 +1828,161 @@ function normalizeCCAction(action) {
|
|
|
1827
1828
|
return { ...action, type: 'dispatch', workType: action.workType || type };
|
|
1828
1829
|
}
|
|
1829
1830
|
|
|
1831
|
+
function _ccCleanIntentString(value, max = 500) {
|
|
1832
|
+
if (typeof value !== 'string') return '';
|
|
1833
|
+
return value.replace(/[\0\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, max);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
function _ccNormalizeIntentMetadata(meta) {
|
|
1837
|
+
if (!meta || typeof meta !== 'object' || Array.isArray(meta)) return {};
|
|
1838
|
+
const out = {};
|
|
1839
|
+
for (const key of ['intent', 'type', 'title', 'description', 'priority', 'project', 'branchStrategy', 'branch_strategy']) {
|
|
1840
|
+
const value = _ccCleanIntentString(meta[key], key === 'description' ? 2000 : 300);
|
|
1841
|
+
if (value) out[key] = value;
|
|
1842
|
+
}
|
|
1843
|
+
if (Array.isArray(meta.agents)) {
|
|
1844
|
+
out.agents = meta.agents.map(a => _ccCleanIntentString(a, 80)).filter(Boolean);
|
|
1845
|
+
}
|
|
1846
|
+
if (Array.isArray(meta.projects)) {
|
|
1847
|
+
out.projects = meta.projects.map(p => _ccCleanIntentString(p, 120)).filter(Boolean);
|
|
1848
|
+
}
|
|
1849
|
+
if (meta.fanout === true) out.fanout = true;
|
|
1850
|
+
return out;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function _messageExplicitlyRequestsMinionsOrchestration(message) {
|
|
1854
|
+
const text = String(message || '').toLowerCase();
|
|
1855
|
+
if (!text.trim()) return false;
|
|
1856
|
+
return [
|
|
1857
|
+
/\b(?:dispatch|delegate|assign|queue)\b/,
|
|
1858
|
+
/\b(?:create|open|file)\s+(?:a\s+)?(?:work\s*item|task)\b/,
|
|
1859
|
+
/\b(?:have|ask|tell)\s+(?:minions|an?\s+agent|[a-z][\w-]*)\s+(?:to\s+)?(?:fix|implement|review|test|explore|investigate)\b/,
|
|
1860
|
+
/\bminions\b[^.?!\n]*\b(?:fix|implement|review|test|explore|investigate|plan)\b/,
|
|
1861
|
+
/\b(?:create|make|draft|write)\s+(?:a\s+)?minions?\s+plan\b/,
|
|
1862
|
+
].some(pattern => pattern.test(text));
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function _ccInferMessageActionIntent(message, source = 'command-center') {
|
|
1866
|
+
const text = String(message || '').trim();
|
|
1867
|
+
const lower = text.toLowerCase();
|
|
1868
|
+
if (!lower) return null;
|
|
1869
|
+
if (source === 'doc-chat' && !_messageExplicitlyRequestsMinionsOrchestration(text)) return null;
|
|
1870
|
+
if (/^\/plan\b/.test(lower) || /\b(?:make|create|draft|write|design|come up with)\s+(?:a\s+)?plan\b/.test(lower) || /\bplan\s+(?:this|that|it|for|how|out)\b/.test(lower)) {
|
|
1871
|
+
return { kind: 'plan' };
|
|
1872
|
+
}
|
|
1873
|
+
const workPatterns = [
|
|
1874
|
+
['fix', /\b(?:fix|repair|resolve|patch|debug)\b|\b(?:bug|broken|crash|error|regression|failing|failure)\b/],
|
|
1875
|
+
['implement', /\b(?:implement|build|add|create|ship)\b/],
|
|
1876
|
+
['review', /\b(?:review|code review|inspect|audit)\b/],
|
|
1877
|
+
['test', /\b(?:test|verify|validate|run tests?|write tests?|add tests?|coverage|build)\b/],
|
|
1878
|
+
['explore', /\b(?:explore|investigate|research|analyze|understand|look into|map out|survey)\b/],
|
|
1879
|
+
];
|
|
1880
|
+
for (const [workType, pattern] of workPatterns) {
|
|
1881
|
+
if (pattern.test(lower)) return { kind: 'work-item', workType };
|
|
1882
|
+
}
|
|
1883
|
+
return null;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
function _ccInferMetadataActionIntent(meta) {
|
|
1887
|
+
if (meta.intent === 'plan') return { kind: 'plan' };
|
|
1888
|
+
if (meta.intent === 'work-item' && CC_ACTION_INTENT_WORK_TYPES.has(String(meta.type || '').toLowerCase())) {
|
|
1889
|
+
return { kind: 'work-item', workType: String(meta.type).toLowerCase() };
|
|
1890
|
+
}
|
|
1891
|
+
return null;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function _ccActionIntentTargetText(message, intent) {
|
|
1895
|
+
let text = _ccCleanIntentString(message, 500);
|
|
1896
|
+
text = text
|
|
1897
|
+
.replace(/^\/(?:plan|fix|implement|review|test|explore)\s+/i, '')
|
|
1898
|
+
.replace(/^(?:please\s+)?(?:dispatch|delegate|assign|queue)\s+(?:a\s+|an\s+)?(?:work\s*item|task|agent)?\s*(?:to\s+)?/i, '')
|
|
1899
|
+
.replace(/^(?:please\s+)?(?:create|open|file)\s+(?:a\s+)?(?:work\s*item|task)\s+(?:to\s+|for\s+)?/i, '')
|
|
1900
|
+
.replace(/^(?:please\s+)?(?:have|ask|tell)\s+(?:minions|an?\s+agent|[a-z][\w-]*)\s+(?:to\s+)?/i, '')
|
|
1901
|
+
.replace(/^(?:please\s+)?/, '')
|
|
1902
|
+
.trim();
|
|
1903
|
+
if (intent?.kind === 'plan') {
|
|
1904
|
+
text = text.replace(/^(?:make|create|draft|write|design|come up with)\s+(?:a\s+)?plan\s+(?:for\s+|to\s+|about\s+)?/i, '')
|
|
1905
|
+
.replace(/^plan\s+(?:out\s+)?(?:for\s+|how\s+to\s+|this\s+|that\s+|it\s+)?/i, '');
|
|
1906
|
+
} else if (intent?.workType) {
|
|
1907
|
+
const workType = intent.workType;
|
|
1908
|
+
const verbPattern = {
|
|
1909
|
+
fix: /^(?:fix|repair|resolve|patch|debug)\s+(?:the\s+|a\s+|an\s+)?/i,
|
|
1910
|
+
implement: /^(?:implement|build|add|create|ship)\s+(?:the\s+|a\s+|an\s+)?/i,
|
|
1911
|
+
review: /^(?:review|code review|inspect|audit)\s+(?:the\s+|a\s+|an\s+)?/i,
|
|
1912
|
+
test: /^(?:test|verify|validate|run tests?\s+(?:for\s+|on\s+)?|write tests?\s+(?:for\s+)?|add tests?\s+(?:for\s+)?)\s*(?:the\s+|a\s+|an\s+)?/i,
|
|
1913
|
+
explore: /^(?:explore|investigate|research|analyze|understand|look into|map out|survey)\s+(?:the\s+|a\s+|an\s+)?/i,
|
|
1914
|
+
}[workType];
|
|
1915
|
+
if (verbPattern) text = text.replace(verbPattern, '');
|
|
1916
|
+
text = text.replace(/^(?:a\s+)?(?:fix|review|test|implementation|exploration|investigation)\s+(?:for\s+|to\s+|of\s+)?/i, '');
|
|
1917
|
+
}
|
|
1918
|
+
return text.replace(/^(?:for|to|of|on|about)\s+/i, '').trim();
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
function _ccIntentHasConcreteTarget(message, metadataTitle, intent) {
|
|
1922
|
+
const raw = String(message || '');
|
|
1923
|
+
if (/\b(?:PR|pull request|issue)\s*#?\d+\b/i.test(raw) || /\bW-[a-z0-9][a-z0-9-]*\b/i.test(raw) || /https?:\/\//i.test(raw)) return true;
|
|
1924
|
+
const target = _ccActionIntentTargetText(metadataTitle || raw, intent)
|
|
1925
|
+
.replace(/[.!?]+$/g, '')
|
|
1926
|
+
.trim()
|
|
1927
|
+
.toLowerCase();
|
|
1928
|
+
if (!target) return false;
|
|
1929
|
+
if (/^(?:this|that|it|these|those|same|above|the above|here|there)(?:\s+(?:one|thing|issue|bug|task|request|change|pr|pull request|code|test|plan|doc|document|file))?$/.test(target)) {
|
|
1930
|
+
return false;
|
|
1931
|
+
}
|
|
1932
|
+
return target.length >= 3;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function _ccIntentTitle(message, metadataTitle, intent) {
|
|
1936
|
+
const metaTitle = _ccCleanIntentString(metadataTitle, 300);
|
|
1937
|
+
if (metaTitle && _ccIntentHasConcreteTarget(message, metaTitle, intent)) return metaTitle;
|
|
1938
|
+
const cleaned = _ccCleanIntentString(message, 300).replace(/^\/plan\s+/i, 'plan ');
|
|
1939
|
+
if (!cleaned) return '';
|
|
1940
|
+
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
function _ccFallbackMissingTargetError(intent) {
|
|
1944
|
+
const label = intent?.kind === 'plan' ? 'plan' : (intent?.workType || 'dispatch');
|
|
1945
|
+
return `Missing target for ${label} request. Specify a concrete title, PR/work item, file, or feature to ${label}.`;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
function _actionsWithIntentFallback(actions, { message = '', intentMetadata = null, source = 'command-center' } = {}) {
|
|
1949
|
+
const existing = Array.isArray(actions) ? actions : [];
|
|
1950
|
+
if (existing.length > 0) return existing;
|
|
1951
|
+
const meta = _ccNormalizeIntentMetadata(intentMetadata);
|
|
1952
|
+
const messageIntent = _ccInferMessageActionIntent(message, source);
|
|
1953
|
+
const metadataIntent = source === 'doc-chat' ? null : _ccInferMetadataActionIntent(meta);
|
|
1954
|
+
const intent = messageIntent || metadataIntent;
|
|
1955
|
+
if (!intent) return existing;
|
|
1956
|
+
|
|
1957
|
+
const title = _ccIntentTitle(message, meta.title, intent);
|
|
1958
|
+
const hasTarget = _ccIntentHasConcreteTarget(message, meta.title || title, intent);
|
|
1959
|
+
const description = meta.description || _ccCleanIntentString(message, 2000);
|
|
1960
|
+
const common = {
|
|
1961
|
+
...(title ? { title } : {}),
|
|
1962
|
+
...(description ? { description } : {}),
|
|
1963
|
+
...(meta.priority ? { priority: meta.priority } : {}),
|
|
1964
|
+
...(meta.project ? { project: meta.project } : {}),
|
|
1965
|
+
...(Array.isArray(meta.agents) && meta.agents.length ? { agents: meta.agents } : {}),
|
|
1966
|
+
...(meta.fanout ? { scope: 'fan-out' } : {}),
|
|
1967
|
+
};
|
|
1968
|
+
if (intent.kind === 'plan') {
|
|
1969
|
+
const action = {
|
|
1970
|
+
type: 'plan',
|
|
1971
|
+
...common,
|
|
1972
|
+
branchStrategy: meta.branchStrategy || meta.branch_strategy || 'parallel',
|
|
1973
|
+
};
|
|
1974
|
+
if (!hasTarget) action._intentFallbackError = _ccFallbackMissingTargetError(intent);
|
|
1975
|
+
return [action];
|
|
1976
|
+
}
|
|
1977
|
+
const action = {
|
|
1978
|
+
type: 'dispatch',
|
|
1979
|
+
workType: intent.workType,
|
|
1980
|
+
...common,
|
|
1981
|
+
};
|
|
1982
|
+
if (!hasTarget) action._intentFallbackError = _ccFallbackMissingTargetError(intent);
|
|
1983
|
+
return [action];
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1830
1986
|
function parseCCActions(text) {
|
|
1831
1987
|
let actions = [];
|
|
1832
1988
|
let displayText = stripCCActionsForDisplay(text);
|
|
@@ -2262,6 +2418,8 @@ function _ccValidateAction(action) {
|
|
|
2262
2418
|
case 'dispatch': case 'fix': case 'explore': case 'review': case 'test':
|
|
2263
2419
|
if (!action.title || typeof action.title !== 'string' || !action.title.trim()) return `${action.type} action missing required field: title`;
|
|
2264
2420
|
return null;
|
|
2421
|
+
case 'implement':
|
|
2422
|
+
return 'Unsupported action type "implement"; use type="dispatch" with workType="implement".';
|
|
2265
2423
|
case 'build-and-test':
|
|
2266
2424
|
if (!action.pr) return 'build-and-test action missing required field: pr';
|
|
2267
2425
|
return null;
|
|
@@ -2586,6 +2744,15 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
2586
2744
|
const results = [];
|
|
2587
2745
|
for (const rawAction of actions) {
|
|
2588
2746
|
const action = normalizeCCAction(rawAction);
|
|
2747
|
+
if (action?._intentFallbackError) {
|
|
2748
|
+
results.push({
|
|
2749
|
+
type: action.type || 'dispatch',
|
|
2750
|
+
...(action.workType ? { workType: action.workType } : {}),
|
|
2751
|
+
error: action._intentFallbackError,
|
|
2752
|
+
missingTarget: true,
|
|
2753
|
+
});
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2589
2756
|
const validationError = _ccValidateAction(action);
|
|
2590
2757
|
if (validationError) {
|
|
2591
2758
|
results.push({ type: action?.type || 'unknown', error: validationError });
|
|
@@ -2887,7 +3054,7 @@ async function executeDocChatActions(actions, { filePath = null } = {}) {
|
|
|
2887
3054
|
return executeCCActions(actions, { source: 'doc-chat', inferredProject: _inferDocChatProject(filePath) });
|
|
2888
3055
|
}
|
|
2889
3056
|
|
|
2890
|
-
const DOC_CHAT_WORK_ITEM_ACTION_TYPES = new Set(['dispatch', 'fix', '
|
|
3057
|
+
const DOC_CHAT_WORK_ITEM_ACTION_TYPES = new Set(['dispatch', 'fix', 'explore', 'review', 'test']);
|
|
2891
3058
|
|
|
2892
3059
|
function _buildDocChatActionFeedback(actions, actionResults) {
|
|
2893
3060
|
if (!Array.isArray(actions) || !Array.isArray(actionResults)) return [];
|
|
@@ -5506,12 +5673,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
5506
5673
|
shared.sanitizePath(body.file, PLANS_DIR);
|
|
5507
5674
|
const planPath = path.join(MINIONS_DIR, 'plans', body.file);
|
|
5508
5675
|
if (!fs.existsSync(planPath)) return jsonReply(res, 404, { error: 'plan file not found' });
|
|
5676
|
+
const planContent = fs.readFileSync(planPath, 'utf8');
|
|
5677
|
+
const declaredProject = shared.extractPlanDeclaredProject(planContent);
|
|
5509
5678
|
|
|
5510
5679
|
const queued = shared.queuePlanToPrd({
|
|
5511
5680
|
planFile: body.file,
|
|
5512
5681
|
title: 'Convert plan to PRD: ' + body.file.replace('.md', ''),
|
|
5513
5682
|
description: 'Plan file: plans/' + body.file,
|
|
5514
|
-
project: body.project || '', createdBy: 'dashboard:execute',
|
|
5683
|
+
project: declaredProject || body.project || '', createdBy: 'dashboard:execute',
|
|
5515
5684
|
});
|
|
5516
5685
|
if (!queued) return jsonReply(res, 200, { ok: true, alreadyQueued: true });
|
|
5517
5686
|
invalidateStatusCache();
|
|
@@ -5911,7 +6080,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5911
6080
|
}
|
|
5912
6081
|
}
|
|
5913
6082
|
|
|
5914
|
-
|
|
6083
|
+
let { answer, content, actions, actionParseError, partial, warning, toolUses, error: ccError } = await ccDocCall({
|
|
5915
6084
|
message: body.message, document: currentContent, title: body.title,
|
|
5916
6085
|
filePath: body.filePath, selection: body.selection, canEdit, isJson,
|
|
5917
6086
|
model: body.model || undefined,
|
|
@@ -5919,6 +6088,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5919
6088
|
transcript: body.transcript,
|
|
5920
6089
|
onAbortReady: (abort) => { _docAbort = abort; },
|
|
5921
6090
|
});
|
|
6091
|
+
actions = _actionsWithIntentFallback(actions, { message: body.message, source: 'doc-chat' });
|
|
5922
6092
|
const actionResults = await executeDocChatActions(actions, { filePath: body.filePath });
|
|
5923
6093
|
const actionFeedback = _buildDocChatActionFeedback(actions, actionResults);
|
|
5924
6094
|
const finalize = _finalizeDocChatEdit({
|
|
@@ -6003,7 +6173,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6003
6173
|
|
|
6004
6174
|
try {
|
|
6005
6175
|
|
|
6006
|
-
|
|
6176
|
+
let { answer, content, actions, actionParseError, partial, warning, toolUses, error: ccError } = await ccDocCallStreaming({
|
|
6007
6177
|
message: body.message, document: currentContent, title: body.title,
|
|
6008
6178
|
filePath: body.filePath, selection: body.selection, canEdit, isJson,
|
|
6009
6179
|
model: body.model || undefined,
|
|
@@ -6014,6 +6184,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6014
6184
|
onToolUse: (name, input) => { writeDocEvent({ type: 'tool', name, input: _lightToolInput(input) }); },
|
|
6015
6185
|
onRetry: (attempt) => { writeDocEvent({ type: 'progress', attempt }); },
|
|
6016
6186
|
});
|
|
6187
|
+
actions = _actionsWithIntentFallback(actions, { message: body.message, source: 'doc-chat' });
|
|
6017
6188
|
const actionResults = await executeDocChatActions(actions, { filePath: body.filePath });
|
|
6018
6189
|
const actionFeedback = _buildDocChatActionFeedback(actions, actionResults);
|
|
6019
6190
|
const finalize = _finalizeDocChatEdit({
|
|
@@ -6543,6 +6714,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6543
6714
|
}
|
|
6544
6715
|
|
|
6545
6716
|
const parsed = parseCCActions(result.text);
|
|
6717
|
+
parsed.actions = _actionsWithIntentFallback(parsed.actions, {
|
|
6718
|
+
message: body.message,
|
|
6719
|
+
intentMetadata: body.intentMetadata,
|
|
6720
|
+
source: 'command-center',
|
|
6721
|
+
});
|
|
6546
6722
|
const toolUses = Array.isArray(result.toolUses) ? result.toolUses : _extractToolUsesFromRaw(result.raw);
|
|
6547
6723
|
// Safety net: detect /loop invocation and convert to create-watch
|
|
6548
6724
|
const _loopWatch = _detectLoopInvocation(parsed.text, parsed.actions, toolUses, body.message);
|
|
@@ -6872,6 +7048,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6872
7048
|
|
|
6873
7049
|
// Send final result with actions — execute server-side first
|
|
6874
7050
|
let { text: displayText, actions, _actionParseError } = parseCCActions(result.text);
|
|
7051
|
+
actions = _actionsWithIntentFallback(actions, {
|
|
7052
|
+
message: body.message,
|
|
7053
|
+
intentMetadata: body.intentMetadata,
|
|
7054
|
+
source: 'command-center',
|
|
7055
|
+
});
|
|
6875
7056
|
// Safety net: detect /loop invocation and convert to create-watch
|
|
6876
7057
|
const _loopWatch = _detectLoopInvocation(displayText, actions, toolUses, body.message);
|
|
6877
7058
|
if (_loopWatch) {
|
|
@@ -8509,6 +8690,7 @@ module.exports = {
|
|
|
8509
8690
|
_resolveSkillReadPath,
|
|
8510
8691
|
DOC_CHAT_DOCUMENT_DELIMITER,
|
|
8511
8692
|
_ccValidateAction,
|
|
8693
|
+
_actionsWithIntentFallback,
|
|
8512
8694
|
_messageExplicitlyRequestsMonitoring,
|
|
8513
8695
|
_filterImplicitPostDispatchActions,
|
|
8514
8696
|
_findDuplicateWorkItemCreate: findDuplicateWorkItemCreate,
|
package/engine/playbook.js
CHANGED
|
@@ -332,14 +332,29 @@ function validatePlaybookVars(playbookName, vars) {
|
|
|
332
332
|
* @param {string} playbookType — playbook type name (e.g. 'implement', 'review')
|
|
333
333
|
* @returns {string} absolute path to the playbook file
|
|
334
334
|
*/
|
|
335
|
+
function isSafeProjectPlaybookName(projectName) {
|
|
336
|
+
const name = String(projectName || '').trim();
|
|
337
|
+
return !!name &&
|
|
338
|
+
!name.includes('\0') &&
|
|
339
|
+
!name.includes('..') &&
|
|
340
|
+
!/[\\/]/.test(name) &&
|
|
341
|
+
!path.isAbsolute(name) &&
|
|
342
|
+
!/^[a-zA-Z]:/.test(name);
|
|
343
|
+
}
|
|
344
|
+
|
|
335
345
|
function resolvePlaybookPath(projectName, playbookType) {
|
|
336
346
|
if (projectName) {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
347
|
+
const projectDirName = String(projectName).trim();
|
|
348
|
+
if (isSafeProjectPlaybookName(projectDirName)) {
|
|
349
|
+
const localPath = path.join(MINIONS_DIR, 'projects', projectDirName, 'playbooks', `${playbookType}.md`);
|
|
350
|
+
try {
|
|
351
|
+
fs.accessSync(localPath, fs.constants.R_OK);
|
|
352
|
+
log('info', `Using project-local playbook: projects/${projectDirName}/playbooks/${playbookType}.md`);
|
|
353
|
+
return localPath;
|
|
354
|
+
} catch { /* no local override — fall through to global */ }
|
|
355
|
+
} else {
|
|
356
|
+
log('warn', `Skipping project-local playbook lookup for unsafe project name: ${projectDirName}`);
|
|
357
|
+
}
|
|
343
358
|
}
|
|
344
359
|
return path.join(PLAYBOOKS_DIR, `${playbookType}.md`);
|
|
345
360
|
}
|
package/engine/shared.js
CHANGED
|
@@ -1579,6 +1579,24 @@ function queuePlanToPrd({ planFile, prdFile, title, description, project, create
|
|
|
1579
1579
|
}, { defaultValue: [] });
|
|
1580
1580
|
return queued;
|
|
1581
1581
|
}
|
|
1582
|
+
|
|
1583
|
+
function extractPlanDeclaredProject(planContent) {
|
|
1584
|
+
if (typeof planContent !== 'string' || !planContent.trim()) return '';
|
|
1585
|
+
const lines = planContent.split(/\r?\n/).slice(0, 80);
|
|
1586
|
+
for (const rawLine of lines) {
|
|
1587
|
+
const line = rawLine
|
|
1588
|
+
.replace(/^\s*[-*]\s+/, '')
|
|
1589
|
+
.replace(/\*\*/g, '')
|
|
1590
|
+
.trim();
|
|
1591
|
+
const match = line.match(/^Project\s*:\s*(.+)$/i);
|
|
1592
|
+
if (!match) continue;
|
|
1593
|
+
let value = match[1].trim();
|
|
1594
|
+
value = value.replace(/\s+#.*$/, '').trim();
|
|
1595
|
+
value = value.replace(/^["'`]+|["'`]+$/g, '').trim();
|
|
1596
|
+
return value;
|
|
1597
|
+
}
|
|
1598
|
+
return '';
|
|
1599
|
+
}
|
|
1582
1600
|
const DISPATCH_RESULT = { SUCCESS: 'success', ERROR: 'error', TIMEOUT: 'timeout' };
|
|
1583
1601
|
const PIPELINE_STATUS = {
|
|
1584
1602
|
PENDING: 'pending', RUNNING: 'running', COMPLETED: 'completed',
|
|
@@ -3257,7 +3275,7 @@ module.exports = {
|
|
|
3257
3275
|
runtimeConfigWarnings,
|
|
3258
3276
|
projectWorkSourceWarnings,
|
|
3259
3277
|
backfillProjectWorkSourceDefaults,
|
|
3260
|
-
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, DISPATCH_RESULT, trackReviewMetric, queuePlanToPrd,
|
|
3278
|
+
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, DISPATCH_RESULT, trackReviewMetric, queuePlanToPrd, extractPlanDeclaredProject,
|
|
3261
3279
|
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS,
|
|
3262
3280
|
PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
|
|
3263
3281
|
FAILURE_CLASS, ESCALATION_POLICY, COMPLETION_FIELDS,
|
|
@@ -3302,6 +3320,7 @@ module.exports = {
|
|
|
3302
3320
|
getAdoOrgBase,
|
|
3303
3321
|
sanitizePath,
|
|
3304
3322
|
sanitizeBranch,
|
|
3323
|
+
safeSlugComponent,
|
|
3305
3324
|
buildWorktreeDirName, // exported for testing
|
|
3306
3325
|
isPathInside,
|
|
3307
3326
|
isPathInsideOrEqual,
|
package/engine.js
CHANGED
|
@@ -1978,6 +1978,20 @@ function buildWiDescription(item, planFile) {
|
|
|
1978
1978
|
return `${item.description || ''}\n\n**Plan:** ${planFile}\n**Plan Item:** ${item.id}\n**Complexity:** ${complexity}${criteria ? '\n\n**Acceptance Criteria:**\n' + criteria : ''}`;
|
|
1979
1979
|
}
|
|
1980
1980
|
|
|
1981
|
+
function safePrdProjectSlug(projectName) {
|
|
1982
|
+
const slug = shared.safeSlugComponent(projectName || 'project', 80)
|
|
1983
|
+
.toLowerCase()
|
|
1984
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
1985
|
+
.replace(/^-+|-+$/g, '');
|
|
1986
|
+
return slug || 'project';
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
function safePrdFilenameForProject(projectName, suffix) {
|
|
1990
|
+
const fileName = `${safePrdProjectSlug(projectName)}-${suffix}.json`;
|
|
1991
|
+
shared.sanitizePath(fileName, PRD_DIR);
|
|
1992
|
+
return fileName;
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1981
1995
|
function materializePlansAsWorkItems(config) {
|
|
1982
1996
|
if (!fs.existsSync(PRD_DIR)) { try { fs.mkdirSync(PRD_DIR, { recursive: true }); } catch (e) { log('warn', 'create PRD directory: ' + e.message); } }
|
|
1983
1997
|
const writePrdLocked = (fileName, data) => {
|
|
@@ -1990,6 +2004,100 @@ function materializePlansAsWorkItems(config) {
|
|
|
1990
2004
|
return mutator(current) || current;
|
|
1991
2005
|
}, { defaultValue: fallback || {}, ...options });
|
|
1992
2006
|
};
|
|
2007
|
+
const declaredProjectPrdFilename = (fileName, projectName) => {
|
|
2008
|
+
const match = String(fileName || '').match(/-(\d{4}-\d{2}-\d{2}(?:-\d+)?)\.json$/);
|
|
2009
|
+
if (!match) return null;
|
|
2010
|
+
return safePrdFilenameForProject(projectName, match[1]);
|
|
2011
|
+
};
|
|
2012
|
+
const migratePrdFilenameReferences = (oldFileName, newFileName) => {
|
|
2013
|
+
if (!oldFileName || !newFileName || oldFileName === newFileName) return 0;
|
|
2014
|
+
let migrated = 0;
|
|
2015
|
+
const wiPaths = new Set([path.join(MINIONS_DIR, 'work-items.json')]);
|
|
2016
|
+
for (const project of getProjects(config)) wiPaths.add(projectWorkItemsPath(project));
|
|
2017
|
+
for (const wiPath of wiPaths) {
|
|
2018
|
+
if (!fs.existsSync(wiPath)) continue;
|
|
2019
|
+
mutateWorkItems(wiPath, (items) => {
|
|
2020
|
+
for (const wi of items) {
|
|
2021
|
+
if (!wi || typeof wi !== 'object') continue;
|
|
2022
|
+
if (wi.sourcePlan === oldFileName) {
|
|
2023
|
+
wi.sourcePlan = newFileName;
|
|
2024
|
+
migrated++;
|
|
2025
|
+
}
|
|
2026
|
+
if (wi._artifacts?.sourcePlan === oldFileName) {
|
|
2027
|
+
wi._artifacts.sourcePlan = newFileName;
|
|
2028
|
+
migrated++;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
return items;
|
|
2032
|
+
});
|
|
2033
|
+
}
|
|
2034
|
+
if (fs.existsSync(DISPATCH_PATH)) {
|
|
2035
|
+
mutateDispatch((dispatch) => {
|
|
2036
|
+
for (const queue of ['pending', 'active', 'completed']) {
|
|
2037
|
+
for (const entry of dispatch[queue] || []) {
|
|
2038
|
+
const metaItem = entry?.meta?.item;
|
|
2039
|
+
if (!metaItem || typeof metaItem !== 'object') continue;
|
|
2040
|
+
if (metaItem.sourcePlan === oldFileName) {
|
|
2041
|
+
metaItem.sourcePlan = newFileName;
|
|
2042
|
+
migrated++;
|
|
2043
|
+
}
|
|
2044
|
+
if (metaItem._prdFilename === oldFileName) {
|
|
2045
|
+
metaItem._prdFilename = newFileName;
|
|
2046
|
+
migrated++;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
return dispatch;
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
return migrated;
|
|
2054
|
+
};
|
|
2055
|
+
const enforceDeclaredPlanProject = (fileName, currentPlan) => {
|
|
2056
|
+
if (!currentPlan?.source_plan) return { fileName, plan: currentPlan };
|
|
2057
|
+
let declaredProject = '';
|
|
2058
|
+
try {
|
|
2059
|
+
declaredProject = shared.extractPlanDeclaredProject(safeRead(path.join(PLANS_DIR, currentPlan.source_plan)) || '');
|
|
2060
|
+
} catch (e) {
|
|
2061
|
+
log('warn', `Plan project enforcement: could not read source plan ${currentPlan.source_plan}: ${e.message}`);
|
|
2062
|
+
}
|
|
2063
|
+
if (!declaredProject) return { fileName, plan: currentPlan };
|
|
2064
|
+
|
|
2065
|
+
let changed = false;
|
|
2066
|
+
const normalizedPlan = mutatePrdLocked(fileName, currentPlan, (planData) => {
|
|
2067
|
+
if (planData.project !== declaredProject) {
|
|
2068
|
+
planData.project = declaredProject;
|
|
2069
|
+
changed = true;
|
|
2070
|
+
}
|
|
2071
|
+
if (Array.isArray(planData.missing_features)) {
|
|
2072
|
+
for (const feature of planData.missing_features) {
|
|
2073
|
+
if (feature && feature.project !== declaredProject) {
|
|
2074
|
+
feature.project = declaredProject;
|
|
2075
|
+
changed = true;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
return planData;
|
|
2080
|
+
}, { skipWriteIfUnchanged: true });
|
|
2081
|
+
|
|
2082
|
+
let nextFileName = fileName;
|
|
2083
|
+
const desiredFileName = declaredProjectPrdFilename(fileName, declaredProject);
|
|
2084
|
+
if (desiredFileName && desiredFileName.toLowerCase() !== String(fileName).toLowerCase()) {
|
|
2085
|
+
const fromPath = path.join(PRD_DIR, fileName);
|
|
2086
|
+
const desiredPath = shared.sanitizePath(desiredFileName, PRD_DIR);
|
|
2087
|
+
const toPath = shared.uniquePath(desiredPath);
|
|
2088
|
+
try {
|
|
2089
|
+
fs.renameSync(fromPath, toPath);
|
|
2090
|
+
nextFileName = path.basename(toPath);
|
|
2091
|
+
const migrated = migratePrdFilenameReferences(fileName, nextFileName);
|
|
2092
|
+
if (migrated > 0) log('info', `Plan project enforcement: migrated ${migrated} PRD reference(s) from ${fileName} to ${nextFileName}`);
|
|
2093
|
+
changed = true;
|
|
2094
|
+
} catch (e) {
|
|
2095
|
+
log('warn', `Plan project enforcement: could not rename ${fileName} to ${path.basename(toPath)}: ${e.message}`);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
if (changed) log('info', `Plan project enforcement: preserved declared project "${declaredProject}" for ${nextFileName}`);
|
|
2099
|
+
return { fileName: nextFileName, plan: normalizedPlan };
|
|
2100
|
+
};
|
|
1993
2101
|
|
|
1994
2102
|
// Enforce: PRDs must be .json — auto-rename .md files that contain valid PRD JSON
|
|
1995
2103
|
// Check both prd/ and plans/ (agents may still write JSON to plans/)
|
|
@@ -2036,12 +2144,13 @@ function materializePlansAsWorkItems(config) {
|
|
|
2036
2144
|
// Regex for detecting sequential PRD item IDs (P-001, P-002) — hoisted outside loop
|
|
2037
2145
|
const SEQUENTIAL_ID_RE = /^P-?\d+$/;
|
|
2038
2146
|
|
|
2039
|
-
for (
|
|
2147
|
+
for (let file of planFiles) {
|
|
2040
2148
|
// safeJsonNoRestore — if a PRD was archived between readdir and this
|
|
2041
2149
|
// read, do not auto-resurrect it from a stale .backup sidecar
|
|
2042
2150
|
// (W-mouptdh1000h9f39).
|
|
2043
2151
|
let plan = safeJsonNoRestore(path.join(PRD_DIR, file));
|
|
2044
2152
|
if (!plan?.missing_features) continue;
|
|
2153
|
+
({ fileName: file, plan } = enforceDeclaredPlanProject(file, plan));
|
|
2045
2154
|
|
|
2046
2155
|
// ID collision prevention: remap sequential IDs (P-001, P-002) to globally unique P-<uid> IDs.
|
|
2047
2156
|
// Agents are instructed to use P-<uuid> format but sometimes generate sequential IDs,
|
|
@@ -3576,7 +3685,7 @@ function discoverCentralWorkItems(config) {
|
|
|
3576
3685
|
const items = safeJson(centralPath) || [];
|
|
3577
3686
|
const projects = getProjects(config);
|
|
3578
3687
|
const dispatchProjects = getCentralDispatchProjects(projects);
|
|
3579
|
-
const projectsByName = new Map(dispatchProjects.map(p => [p.name, p]));
|
|
3688
|
+
const projectsByName = new Map(dispatchProjects.map(p => [String(p.name || '').toLowerCase(), p]));
|
|
3580
3689
|
const newWork = [];
|
|
3581
3690
|
// Collect mutations to apply atomically inside lock callback (avoids TOCTOU)
|
|
3582
3691
|
const mutations = new Map(); // item.id → { field: value, ... }
|
|
@@ -3706,9 +3815,32 @@ function discoverCentralWorkItems(config) {
|
|
|
3706
3815
|
|
|
3707
3816
|
const agentName = config.agents[agentId]?.name || agentId;
|
|
3708
3817
|
const agentRole = config.agents[agentId]?.role || 'Agent';
|
|
3818
|
+
let planFileContent = null;
|
|
3819
|
+
let planReadError = null;
|
|
3820
|
+
let declaredPlanProject = '';
|
|
3821
|
+
if (workType === WORK_TYPE.PLAN_TO_PRD && item.planFile) {
|
|
3822
|
+
const planPath = path.join(PLANS_DIR, item.planFile);
|
|
3823
|
+
try {
|
|
3824
|
+
planFileContent = fs.readFileSync(planPath, 'utf8');
|
|
3825
|
+
declaredPlanProject = shared.extractPlanDeclaredProject(planFileContent);
|
|
3826
|
+
} catch (e) {
|
|
3827
|
+
planReadError = e;
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3709
3830
|
const firstProject = dispatchProjects[0];
|
|
3710
|
-
const requestedProjectName = typeof item.project === 'string' ? item.project : item.project?.name;
|
|
3711
|
-
const
|
|
3831
|
+
const requestedProjectName = declaredPlanProject || (typeof item.project === 'string' ? item.project : item.project?.name);
|
|
3832
|
+
const requestedProject = requestedProjectName ? projectsByName.get(String(requestedProjectName).toLowerCase()) : null;
|
|
3833
|
+
const targetProject = requestedProject || (declaredPlanProject
|
|
3834
|
+
? { name: declaredPlanProject, localPath: '', repoName: declaredPlanProject, mainBranch: firstProject?.mainBranch || 'main' }
|
|
3835
|
+
: firstProject);
|
|
3836
|
+
if (declaredPlanProject) {
|
|
3837
|
+
const projectMutation = { project: targetProject.name, _declaredPlanProject: declaredPlanProject };
|
|
3838
|
+
if (!requestedProject) projectMutation._declaredPlanProjectMissing = true;
|
|
3839
|
+
mutations.set(item.id, Object.assign(mutations.get(item.id) || {}, projectMutation));
|
|
3840
|
+
if (!requestedProject) {
|
|
3841
|
+
log('warn', `plan-to-prd: plan ${item.planFile} declares project "${declaredPlanProject}" but no configured project matches; preserving the declared project name with no project_path`);
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3712
3844
|
|
|
3713
3845
|
// Branch mutex: skip if target branch is locked by an active dispatch
|
|
3714
3846
|
const centralBranch = item.branch || item.featureBranch || `work/${item.id}`;
|
|
@@ -3776,16 +3908,16 @@ function discoverCentralWorkItems(config) {
|
|
|
3776
3908
|
if (workType === WORK_TYPE.PLAN_TO_PRD && item.planFile) {
|
|
3777
3909
|
if (!fs.existsSync(PLANS_DIR)) fs.mkdirSync(PLANS_DIR, { recursive: true });
|
|
3778
3910
|
if (!fs.existsSync(PRD_DIR)) fs.mkdirSync(PRD_DIR, { recursive: true });
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
log('warn', `plan-to-prd: could not read plan file ${item.planFile} for ${item.id}: ${e.message}`);
|
|
3911
|
+
if (planFileContent !== null) {
|
|
3912
|
+
vars.plan_content = planFileContent;
|
|
3913
|
+
} else {
|
|
3914
|
+
if (planReadError) log('warn', `plan-to-prd: could not read plan file ${item.planFile} for ${item.id}: ${planReadError.message}`);
|
|
3784
3915
|
vars.plan_content = item.description || '';
|
|
3785
3916
|
}
|
|
3786
3917
|
vars.plan_summary = (item.title || item.planFile).substring(0, 80);
|
|
3787
3918
|
vars.plan_file = item.planFile || '';
|
|
3788
3919
|
vars.project_name_lower = (targetProject?.name || 'project').toLowerCase();
|
|
3920
|
+
vars.project_filename_slug = safePrdProjectSlug(targetProject?.name || 'project');
|
|
3789
3921
|
// Default empty string so the {{existing_prd_json}} token always resolves —
|
|
3790
3922
|
// playbook treats empty as "no existing PRD, fresh run". Without this default
|
|
3791
3923
|
// the renderPlaybook pass logs an "unresolved template variables" warning
|
|
@@ -3805,8 +3937,9 @@ function discoverCentralWorkItems(config) {
|
|
|
3805
3937
|
}
|
|
3806
3938
|
if (!prdFilename) {
|
|
3807
3939
|
// Generate unique PRD filename — check prd/ and prd/archive/ for collisions
|
|
3808
|
-
const prdBase = vars.
|
|
3940
|
+
const prdBase = vars.project_filename_slug + '-' + dateStamp();
|
|
3809
3941
|
prdFilename = prdBase + '.json';
|
|
3942
|
+
shared.sanitizePath(prdFilename, PRD_DIR);
|
|
3810
3943
|
const prdExisting = new Set([
|
|
3811
3944
|
...prdFiles,
|
|
3812
3945
|
...safeReadDir(path.join(PRD_DIR, 'archive')).filter(f => f.endsWith('.json')),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1809",
|
|
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"
|
package/playbooks/plan-to-prd.md
CHANGED
|
@@ -14,6 +14,7 @@ A user has provided a plan. Analyze it against the codebase and produce a struct
|
|
|
14
14
|
## Instructions
|
|
15
15
|
|
|
16
16
|
1. **Read the plan carefully** — understand the goals, scope, and requirements
|
|
17
|
+
- If the plan declares `Project: <name>` (including `**Project:** <name>`), the engine has resolved `{{project_name}}` from that declaration. Preserve `{{project_name}}` for the top-level `project`, default item `project`, filename, and implementation framing; do not let contextual mentions of another product or repository override it.
|
|
17
18
|
2. **Check for an existing PRD** — if the engine provides `existing_prd_json` below, a PRD already exists for this plan. See "Reusing an Existing PRD" section for how to preserve item IDs and done statuses. If no existing PRD is provided, this is a fresh run — all items start as `"missing"`.
|
|
18
19
|
3. **Explore the codebase** at `{{project_path}}` — understand the existing structure to write accurate descriptions and acceptance criteria. Do NOT use observations about existing PRs or partial work to set item statuses — status is determined only by existing PRD items (step 2), not codebase state
|
|
19
20
|
4. **Break the plan into discrete, implementable items** — each should be a single PR's worth of work, with enough detail for another agent to implement it directly
|
|
@@ -47,7 +48,7 @@ This file is NOT checked into the repo. The engine reads it on every tick and di
|
|
|
47
48
|
"id": "P-<uuid>",
|
|
48
49
|
"name": "Short feature name",
|
|
49
50
|
"description": "What needs to be built and why",
|
|
50
|
-
"project": "
|
|
51
|
+
"project": "{{project_name}}",
|
|
51
52
|
"status": "missing",
|
|
52
53
|
"estimated_complexity": "small|medium|large",
|
|
53
54
|
"priority": "high|medium|low",
|
|
@@ -86,7 +87,7 @@ Rules for items:
|
|
|
86
87
|
- IDs must be `P-<uuid>` format (e.g. `P-a3f9b2c1`) — globally unique, never sequential
|
|
87
88
|
- **`status` is `"missing"` for new items** — do not set `done`, `complete`, `implemented`, or any other value based on codebase observations. The only exception is when reusing an existing PRD (see below) — items already `"done"` in the existing PRD carry forward as `"done"`. Pre-setting any other status on new items causes them to be silently skipped by the engine.
|
|
88
89
|
- **Do NOT include a "verify" or "test" or "integration test" item** — the engine automatically creates a verify task when all PRD items are done. Adding one manually creates a duplicate that blocks plan completion.
|
|
89
|
-
- **`project` field is REQUIRED** — set it to the project name where the code changes go (e.g., `"OfficeAgent"`, `"office-bohemia"`). Cross-repo plans must route each item to the correct project. The engine materializes items into that project's work queue.
|
|
90
|
+
- **`project` field is REQUIRED** — set it to the project name where the code changes go (e.g., `"OfficeAgent"`, `"office-bohemia"`). If the plan declares a single project, every item must use `{{project_name}}`; contextual mentions of another repo/product must not override it. Cross-repo plans must route each item to the correct project. The engine materializes items into that project's work queue.
|
|
90
91
|
- `depends_on` lists IDs of items that must be done first
|
|
91
92
|
- Keep descriptions actionable — name the files, functions, patterns, or integration points the implementing agent should touch whenever the plan makes them clear
|
|
92
93
|
- Include `acceptance_criteria` so reviewers know when it's done
|