@yemi33/minions 0.1.1809 → 0.1.1811
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 +12 -2
- package/dashboard.js +540 -224
- package/docs/pr-review-fix-loop.md +2 -0
- package/engine/cli.js +15 -9
- package/engine/copilot-models.json +1 -1
- package/engine/dispatch.js +4 -10
- package/engine/lifecycle.js +11 -11
- package/engine/pipeline.js +146 -43
- package/engine/projects.js +15 -16
- package/engine/queries.js +3 -3
- package/engine/shared.js +161 -3
- package/engine.js +63 -34
- package/package.json +1 -1
- package/playbooks/fix.md +17 -6
- package/prompts/cc-system.md +7 -2
- package/prompts/doc-chat-system.md +15 -7
- package/routing.md +1 -0
package/dashboard.js
CHANGED
|
@@ -130,8 +130,8 @@ function mergeSettingsConfigUpdate(current, candidate, body, patch = {}) {
|
|
|
130
130
|
if (body.projects && Array.isArray(body.projects)) {
|
|
131
131
|
if (!Array.isArray(current.projects)) current.projects = [];
|
|
132
132
|
for (const update of body.projects) {
|
|
133
|
-
const candidateProject = (candidate.projects || []
|
|
134
|
-
const currentProject = current.projects
|
|
133
|
+
const candidateProject = shared.findProjectByName(candidate.projects || [], update.name);
|
|
134
|
+
const currentProject = shared.findProjectByName(current.projects, update.name);
|
|
135
135
|
if (!candidateProject || !currentProject) continue;
|
|
136
136
|
currentProject.workSources = candidateProject.workSources;
|
|
137
137
|
}
|
|
@@ -259,12 +259,11 @@ function normalizeWorkItemDedupTitle(value) {
|
|
|
259
259
|
function resolveWorkItemDedupProject(item, wiPath = '') {
|
|
260
260
|
const projectName = normalizeWorkItemDedupText(item?.project || item?._project || item?._source);
|
|
261
261
|
if (projectName) {
|
|
262
|
-
const namedProject =
|
|
263
|
-
if (namedProject) return namedProject;
|
|
262
|
+
const namedProject = shared.resolveProjectSource(projectName, PROJECTS, { allowCentral: false });
|
|
263
|
+
if (namedProject.project) return namedProject.project;
|
|
264
264
|
}
|
|
265
265
|
if (!wiPath) return null;
|
|
266
|
-
|
|
267
|
-
return PROJECTS.find(p => path.resolve(shared.projectWorkItemsPath(p)) === resolvedWiPath) || null;
|
|
266
|
+
return shared.resolveProjectSource(wiPath, PROJECTS, { allowCentral: false }).project || null;
|
|
268
267
|
}
|
|
269
268
|
|
|
270
269
|
function getWorkItemPrRefCandidates(item) {
|
|
@@ -369,29 +368,86 @@ function createWorkItemWithDedup(wiPath, item, options = {}) {
|
|
|
369
368
|
}
|
|
370
369
|
|
|
371
370
|
function formatUnknownProjectError(projectName, projects = []) {
|
|
372
|
-
|
|
373
|
-
return `Project "${projectName}" not found. Known projects: ${known}`;
|
|
371
|
+
return shared.formatUnknownProjectError(projectName, projects);
|
|
374
372
|
}
|
|
375
373
|
|
|
376
374
|
function findProjectByName(projects, projectName) {
|
|
377
|
-
|
|
378
|
-
if (!name) return null;
|
|
379
|
-
return projects.find(p => p.name?.toLowerCase() === name) || null;
|
|
375
|
+
return shared.findProjectByName(projects, projectName);
|
|
380
376
|
}
|
|
381
377
|
|
|
382
378
|
function resolveWorkItemsCreateTarget(projectName, projects = PROJECTS) {
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
379
|
+
const target = shared.resolveProjectSource(projectName, projects, { defaultWhenSingle: true, minionsDir: MINIONS_DIR });
|
|
380
|
+
if (target.error) return { error: target.error };
|
|
381
|
+
return target;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function resolveProjectSourceTarget(source, projects = PROJECTS, options = {}) {
|
|
385
|
+
return shared.resolveProjectSource(source, projects, { minionsDir: MINIONS_DIR, ...options });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function dispatchPrefixForResolvedSource(target) {
|
|
389
|
+
return target?.project ? `work-${target.project.name}-` : 'central-work-';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function findWorkItemsTargetById(id, source, projects = PROJECTS) {
|
|
393
|
+
const explicitSource = source !== undefined && source !== null && String(source).trim() !== '';
|
|
394
|
+
if (explicitSource) {
|
|
395
|
+
const target = resolveProjectSourceTarget(source, projects);
|
|
396
|
+
if (target.error) return { error: target.error };
|
|
397
|
+
const items = shared.safeJson(target.wiPath) || [];
|
|
398
|
+
return { ...target, found: items.some(i => i.id === id) };
|
|
390
399
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
400
|
+
|
|
401
|
+
const central = resolveProjectSourceTarget('central', projects);
|
|
402
|
+
const centralItems = shared.safeJson(central.wiPath) || [];
|
|
403
|
+
if (centralItems.some(i => i.id === id)) return { ...central, found: true };
|
|
404
|
+
for (const project of projects) {
|
|
405
|
+
const target = resolveProjectSourceTarget(project.name, projects);
|
|
406
|
+
const items = shared.safeJson(target.wiPath) || [];
|
|
407
|
+
if (items.some(i => i.id === id)) return { ...target, found: true };
|
|
408
|
+
}
|
|
409
|
+
return { found: false };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function validatePipelineProjects(pipeline, projects = PROJECTS) {
|
|
413
|
+
const refs = [];
|
|
414
|
+
const collect = (value) => {
|
|
415
|
+
if (value === undefined || value === null || value === '') return;
|
|
416
|
+
if (Array.isArray(value)) { value.forEach(collect); return; }
|
|
417
|
+
if (typeof value === 'object') {
|
|
418
|
+
if (value.project !== undefined) collect(value.project);
|
|
419
|
+
else if (value._project !== undefined) collect(value._project);
|
|
420
|
+
else if (value.name !== undefined) collect(value.name);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
refs.push(String(value));
|
|
424
|
+
};
|
|
425
|
+
const collectResourceProjects = (resources) => {
|
|
426
|
+
for (const resource of Array.isArray(resources) ? resources : []) {
|
|
427
|
+
if (resource && typeof resource === 'object') {
|
|
428
|
+
if (resource.project !== undefined) collect(resource.project);
|
|
429
|
+
if (resource._project !== undefined) collect(resource._project);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
394
432
|
};
|
|
433
|
+
collect(pipeline.project);
|
|
434
|
+
collect(pipeline.projects);
|
|
435
|
+
collectResourceProjects(pipeline.monitoredResources);
|
|
436
|
+
for (const stage of pipeline.stages || []) {
|
|
437
|
+
collect(stage.project);
|
|
438
|
+
collect(stage.projects);
|
|
439
|
+
collectResourceProjects(stage.monitoredResources);
|
|
440
|
+
for (const item of stage.items || []) {
|
|
441
|
+
collect(item.project);
|
|
442
|
+
collect(item.projects);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
for (const ref of refs) {
|
|
446
|
+
if (ref.trim().toLowerCase() === 'central') continue;
|
|
447
|
+
const result = shared.resolveConfiguredProject(ref, projects);
|
|
448
|
+
if (result.error) return result.error;
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
395
451
|
}
|
|
396
452
|
|
|
397
453
|
/**
|
|
@@ -425,13 +481,21 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
|
|
|
425
481
|
}
|
|
426
482
|
const projects = shared.getProjects(config);
|
|
427
483
|
const explicitProjectName = String(projectName || '').trim();
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
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);
|
|
431
490
|
err.statusCode = 400;
|
|
432
491
|
throw err;
|
|
433
492
|
}
|
|
434
|
-
|
|
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
|
+
}
|
|
498
|
+
const prPath = targetProject ? shared.projectPrPath(targetProject) : shared.centralPullRequestsPath(MINIONS_DIR);
|
|
435
499
|
|
|
436
500
|
const prNumMatch = url.match(/\/pull\/(\d+)|pullrequest\/(\d+)/);
|
|
437
501
|
const prNum = prNumMatch ? (prNumMatch[1] || prNumMatch[2]) : Date.now().toString().slice(-6);
|
|
@@ -1814,8 +1878,8 @@ function _extractActionsJson(segment) {
|
|
|
1814
1878
|
return null;
|
|
1815
1879
|
}
|
|
1816
1880
|
|
|
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']);
|
|
1881
|
+
const CC_DISPATCH_ACTION_ALIASES = new Set(['fix', 'explore', 'review', 'test', 'implement', 'implement:large', 'ask', 'verify']);
|
|
1882
|
+
const CC_ACTION_INTENT_WORK_TYPES = new Set(['fix', 'implement', 'implement:large', 'explore', 'review', 'test', 'ask', 'verify']);
|
|
1819
1883
|
|
|
1820
1884
|
function normalizeCCAction(action) {
|
|
1821
1885
|
if (!action || typeof action !== 'object') return action;
|
|
@@ -1830,7 +1894,19 @@ function normalizeCCAction(action) {
|
|
|
1830
1894
|
|
|
1831
1895
|
function _ccCleanIntentString(value, max = 500) {
|
|
1832
1896
|
if (typeof value !== 'string') return '';
|
|
1833
|
-
|
|
1897
|
+
let out = '';
|
|
1898
|
+
let lastWasSpace = true;
|
|
1899
|
+
for (const ch of value) {
|
|
1900
|
+
const isSpace = ch === '\0' || ch === ' ' || ch === '\n' || ch === '\r' || ch === '\t';
|
|
1901
|
+
if (isSpace) {
|
|
1902
|
+
if (!lastWasSpace) out += ' ';
|
|
1903
|
+
lastWasSpace = true;
|
|
1904
|
+
} else {
|
|
1905
|
+
out += ch;
|
|
1906
|
+
lastWasSpace = false;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
return out.trim().slice(0, max);
|
|
1834
1910
|
}
|
|
1835
1911
|
|
|
1836
1912
|
function _ccNormalizeIntentMetadata(meta) {
|
|
@@ -1851,36 +1927,25 @@ function _ccNormalizeIntentMetadata(meta) {
|
|
|
1851
1927
|
}
|
|
1852
1928
|
|
|
1853
1929
|
function _messageExplicitlyRequestsMinionsOrchestration(message) {
|
|
1854
|
-
const
|
|
1855
|
-
if (!
|
|
1856
|
-
return
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
].some(pattern => pattern.test(text));
|
|
1930
|
+
const normalized = _normalizeIntentText(message);
|
|
1931
|
+
if (!normalized.trim()) return false;
|
|
1932
|
+
if (_messageHasDelegationIntent(message)) return true;
|
|
1933
|
+
if (_intentHasAnyToken(normalized, ['minions']) &&
|
|
1934
|
+
(_messageHasMediumLargeWorkIntent(message) || _messageRequestsPlanIntent(message))) return true;
|
|
1935
|
+
if (_intentHasAnyToken(normalized, ['agent', 'agents']) && _messageHasMediumLargeWorkIntent(message)) return true;
|
|
1936
|
+
return _intentHasVerbObject(normalized, ['create', 'make', 'draft', 'write'], ['plan']) &&
|
|
1937
|
+
_intentHasAnyToken(normalized, ['minion', 'minions']);
|
|
1863
1938
|
}
|
|
1864
1939
|
|
|
1865
1940
|
function _ccInferMessageActionIntent(message, source = 'command-center') {
|
|
1866
|
-
const
|
|
1867
|
-
|
|
1868
|
-
if (!
|
|
1869
|
-
if (
|
|
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)) {
|
|
1941
|
+
const normalized = _normalizeIntentText(message);
|
|
1942
|
+
if (!normalized.trim()) return null;
|
|
1943
|
+
if (source === 'doc-chat' && !_messageExplicitlyRequestsMinionsOrchestration(message)) return null;
|
|
1944
|
+
if (_messageRequestsPlanIntent(message)) {
|
|
1871
1945
|
return { kind: 'plan' };
|
|
1872
1946
|
}
|
|
1873
|
-
|
|
1874
|
-
|
|
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;
|
|
1947
|
+
if (!_messageHasDelegationIntent(message) && !_messageHasMediumLargeWorkIntent(message, { source })) return null;
|
|
1948
|
+
return { kind: 'work-item', workType: _inferDelegatedWorkType(message) };
|
|
1884
1949
|
}
|
|
1885
1950
|
|
|
1886
1951
|
function _ccInferMetadataActionIntent(meta) {
|
|
@@ -1892,52 +1957,64 @@ function _ccInferMetadataActionIntent(meta) {
|
|
|
1892
1957
|
}
|
|
1893
1958
|
|
|
1894
1959
|
function _ccActionIntentTargetText(message, intent) {
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
.
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1960
|
+
const tokens = _intentTokens(_normalizeIntentText(_ccCleanIntentString(message, 500)));
|
|
1961
|
+
let start = 0;
|
|
1962
|
+
const skip = (terms) => {
|
|
1963
|
+
if (terms.includes(tokens[start])) {
|
|
1964
|
+
start++;
|
|
1965
|
+
return true;
|
|
1966
|
+
}
|
|
1967
|
+
return false;
|
|
1968
|
+
};
|
|
1969
|
+
skip(['please']);
|
|
1970
|
+
if (tokens[start] && tokens[start].startsWith('/')) start++;
|
|
1971
|
+
if (['dispatch', 'delegate', 'assign', 'queue', 'enqueue'].includes(tokens[start])) {
|
|
1972
|
+
start++;
|
|
1973
|
+
while (['a', 'an', 'the', 'work', 'item', 'task', 'agent', 'to'].includes(tokens[start])) start++;
|
|
1974
|
+
} else if (['create', 'open', 'file'].includes(tokens[start]) && tokens[start + 1] && ['a', 'an', 'the', 'work', 'item', 'task'].includes(tokens[start + 1])) {
|
|
1975
|
+
start++;
|
|
1976
|
+
while (['a', 'an', 'the', 'work', 'item', 'task', 'to', 'for'].includes(tokens[start])) start++;
|
|
1977
|
+
} else if (['have', 'ask', 'tell'].includes(tokens[start])) {
|
|
1978
|
+
start++;
|
|
1979
|
+
while (tokens[start] && !['to', 'fix', 'implement', 'review', 'test', 'explore', 'investigate', 'audit'].includes(tokens[start])) start++;
|
|
1980
|
+
if (tokens[start] === 'to') start++;
|
|
1981
|
+
}
|
|
1903
1982
|
if (intent?.kind === 'plan') {
|
|
1904
|
-
|
|
1905
|
-
.replace(/^plan\s+(?:out\s+)?(?:for\s+|how\s+to\s+|this\s+|that\s+|it\s+)?/i, '');
|
|
1983
|
+
while (['make', 'create', 'draft', 'write', 'design', 'plan', 'out', 'a', 'an', 'the', 'for', 'to', 'about', 'how'].includes(tokens[start])) start++;
|
|
1906
1984
|
} else if (intent?.workType) {
|
|
1907
|
-
|
|
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, '');
|
|
1985
|
+
while (['fix', 'repair', 'resolve', 'patch', 'debug', 'implement', 'build', 'add', 'create', 'ship', 'review', 'inspect', 'audit', 'test', 'verify', 'validate', 'run', 'write', 'explore', 'investigate', 'research', 'analyze', 'analyse', 'understand', 'look', 'map', 'survey', 'a', 'an', 'the', 'for', 'to', 'of', 'on', 'about'].includes(tokens[start])) start++;
|
|
1917
1986
|
}
|
|
1918
|
-
|
|
1987
|
+
while (['for', 'to', 'of', 'on', 'about', 'a', 'an', 'the'].includes(tokens[start])) start++;
|
|
1988
|
+
return tokens.slice(start).join(' ');
|
|
1919
1989
|
}
|
|
1920
1990
|
|
|
1921
1991
|
function _ccIntentHasConcreteTarget(message, metadataTitle, intent) {
|
|
1922
1992
|
const raw = String(message || '');
|
|
1923
|
-
|
|
1924
|
-
const
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
.
|
|
1993
|
+
const normalized = _normalizeIntentText(raw);
|
|
1994
|
+
const tokens = _intentTokens(normalized);
|
|
1995
|
+
if (raw.includes('http://') || raw.includes('https://')) return true;
|
|
1996
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
1997
|
+
if (['pr', 'issue'].includes(tokens[i]) && tokens[i + 1] && _intentTokenIsNumeric(tokens[i + 1])) return true;
|
|
1998
|
+
if (tokens[i] === 'pull' && tokens[i + 1] === 'request' && tokens[i + 2] && _intentTokenIsNumeric(tokens[i + 2])) return true;
|
|
1999
|
+
if (tokens[i] === 'w' && tokens[i + 1] && tokens[i + 1].length >= 2) return true;
|
|
2000
|
+
}
|
|
2001
|
+
const target = _ccActionIntentTargetText(metadataTitle || raw, intent).trim().toLowerCase();
|
|
1928
2002
|
if (!target) return false;
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2003
|
+
const targetTokens = _intentTokens(_normalizeIntentText(target));
|
|
2004
|
+
const generic = new Set(['this', 'that', 'it', 'these', 'those', 'same', 'above', 'here', 'there', 'one', 'thing', 'issue', 'bug', 'task', 'request', 'change', 'pr', 'pull', 'code', 'test', 'plan', 'doc', 'document', 'file']);
|
|
2005
|
+
if (targetTokens.length > 0 && targetTokens.every(token => generic.has(token))) return false;
|
|
1932
2006
|
return target.length >= 3;
|
|
1933
2007
|
}
|
|
1934
2008
|
|
|
1935
2009
|
function _ccIntentTitle(message, metadataTitle, intent) {
|
|
1936
2010
|
const metaTitle = _ccCleanIntentString(metadataTitle, 300);
|
|
1937
2011
|
if (metaTitle && _ccIntentHasConcreteTarget(message, metaTitle, intent)) return metaTitle;
|
|
1938
|
-
const cleaned = _ccCleanIntentString(message, 300)
|
|
2012
|
+
const cleaned = _ccCleanIntentString(message, 300);
|
|
1939
2013
|
if (!cleaned) return '';
|
|
1940
|
-
|
|
2014
|
+
const title = intent?.kind === 'plan'
|
|
2015
|
+
? cleaned
|
|
2016
|
+
: _delegatedWorkTitle(cleaned, intent?.workType || 'ask');
|
|
2017
|
+
return title.charAt(0).toUpperCase() + title.slice(1);
|
|
1941
2018
|
}
|
|
1942
2019
|
|
|
1943
2020
|
function _ccFallbackMissingTargetError(intent) {
|
|
@@ -1945,17 +2022,20 @@ function _ccFallbackMissingTargetError(intent) {
|
|
|
1945
2022
|
return `Missing target for ${label} request. Specify a concrete title, PR/work item, file, or feature to ${label}.`;
|
|
1946
2023
|
}
|
|
1947
2024
|
|
|
1948
|
-
function _actionsWithIntentFallback(actions,
|
|
1949
|
-
const
|
|
2025
|
+
function _actionsWithIntentFallback(actions, opts = {}) {
|
|
2026
|
+
const { message = '', intentMetadata = null, source = 'command-center', filePath = null, title: docTitle = null, answerText = '', toolUses = [] } = opts;
|
|
2027
|
+
const existing = Array.isArray(actions) ? actions.map(normalizeCCAction) : [];
|
|
2028
|
+
if (_messageRequestsDirectHandling(message)) return existing.filter(a => normalizeCCAction(a)?.type !== 'dispatch');
|
|
2029
|
+
if (existing.some(a => normalizeCCAction(a)?.type === 'dispatch')) return existing;
|
|
1950
2030
|
if (existing.length > 0) return existing;
|
|
1951
2031
|
const meta = _ccNormalizeIntentMetadata(intentMetadata);
|
|
1952
2032
|
const messageIntent = _ccInferMessageActionIntent(message, source);
|
|
1953
2033
|
const metadataIntent = source === 'doc-chat' ? null : _ccInferMetadataActionIntent(meta);
|
|
1954
2034
|
const intent = messageIntent || metadataIntent;
|
|
1955
|
-
if (!intent) return existing;
|
|
2035
|
+
if (!intent) return _ensureDelegationForIntent(existing, { message, source, filePath, title: docTitle, answerText, toolUses });
|
|
1956
2036
|
|
|
1957
2037
|
const title = _ccIntentTitle(message, meta.title, intent);
|
|
1958
|
-
const hasTarget = _ccIntentHasConcreteTarget(message, meta.title
|
|
2038
|
+
const hasTarget = _ccIntentHasConcreteTarget(message, meta.title, intent);
|
|
1959
2039
|
const description = meta.description || _ccCleanIntentString(message, 2000);
|
|
1960
2040
|
const common = {
|
|
1961
2041
|
...(title ? { title } : {}),
|
|
@@ -2038,6 +2118,261 @@ function parseCCActions(text) {
|
|
|
2038
2118
|
return result;
|
|
2039
2119
|
}
|
|
2040
2120
|
|
|
2121
|
+
const DELEGATION_ACTION_TERMS = ['dispatch', 'delegate', 'assign', 'queue', 'enqueue'];
|
|
2122
|
+
const DELEGATION_MINIONS_PHRASES = ['have minions', 'ask minions', 'tell minions', 'hand off', 'hand it off', 'hand this off', 'send to agent', 'send it to agent', 'send this to agent'];
|
|
2123
|
+
const MEDIUM_INVESTIGATION_TERMS = ['audit', 'investigate', 'research', 'explore', 'analyze', 'analyse'];
|
|
2124
|
+
const MEDIUM_INVESTIGATION_PHRASES = ['deep dive', 'root cause'];
|
|
2125
|
+
const MEDIUM_IMPLEMENT_TERMS = ['implement', 'fix', 'debug', 'repair', 'refactor', 'migrate', 'harden', 'redesign'];
|
|
2126
|
+
const MEDIUM_BUILD_VERBS = ['build', 'add', 'create'];
|
|
2127
|
+
const MEDIUM_BUILD_OBJECTS = ['feature', 'support', 'endpoint', 'api', 'ui', 'page', 'component', 'flow', 'integration', 'automation', 'test', 'tests', 'tool', 'command', 'dashboard', 'service', 'module'];
|
|
2128
|
+
const MEDIUM_REVIEW_TERMS = ['review', 'test', 'verify', 'validate'];
|
|
2129
|
+
const MEDIUM_SIZE_TERMS = ['medium', 'large', 'larger'];
|
|
2130
|
+
const MEDIUM_SIZE_PHRASES = ['cross cutting', 'multi file', 'multi step', 'multi stage', 'multi module'];
|
|
2131
|
+
const PLAN_INTENT_TERMS = ['plan'];
|
|
2132
|
+
const PLAN_CREATE_TERMS = ['make', 'create', 'draft', 'write', 'design'];
|
|
2133
|
+
const PLAN_CREATE_PHRASES = ['come up with'];
|
|
2134
|
+
const DOC_CHAT_DIRECT_DOC_TERMS = ['summarize', 'summary', 'quote', 'extract', 'rewrite', 'reword', 'copyedit', 'proofread', 'format', 'typo'];
|
|
2135
|
+
const DOC_CHAT_DOC_ACTION_TERMS = ['edit', 'update', 'change', 'review', 'check'];
|
|
2136
|
+
const DOC_CHAT_DOC_OBJECTS = ['document', 'doc', 'paragraph', 'section', 'selection', 'text', 'wording'];
|
|
2137
|
+
const DIRECT_QUICK_TERMS = ['quick', 'simple', 'small'];
|
|
2138
|
+
const DIRECT_QUICK_OBJECTS = ['question', 'answer', 'lookup', 'check'];
|
|
2139
|
+
const DIRECT_REPLY_TERMS = ['answer', 'respond', 'reply'];
|
|
2140
|
+
const DIRECT_REPLY_TARGETS = ['directly', 'inline', 'here'];
|
|
2141
|
+
const DIRECT_SELF_TERMS = ['do', 'handle', 'answer', 'fix', 'edit', 'update', 'change', 'review', 'check'];
|
|
2142
|
+
const DIRECT_SELF_TARGETS = ['yourself', 'directly'];
|
|
2143
|
+
const DIRECT_HANDLING_PHRASES = [
|
|
2144
|
+
'you do it yourself', 'you handle it yourself', 'you handle this yourself',
|
|
2145
|
+
'cc do it yourself', 'doc chat do it yourself', 'doc chat handle it yourself',
|
|
2146
|
+
'do not dispatch', 'dont dispatch', 'do not delegate', 'dont delegate',
|
|
2147
|
+
'do not assign', 'dont assign', 'do not queue', 'dont queue',
|
|
2148
|
+
'do not enqueue', 'dont enqueue', 'do not create work item', 'dont create work item',
|
|
2149
|
+
'no dispatch', 'no delegate', 'no delegation', 'no work item',
|
|
2150
|
+
'without dispatching', 'without delegating', 'without creating work item',
|
|
2151
|
+
];
|
|
2152
|
+
|
|
2153
|
+
function _isIntentWordChar(ch) {
|
|
2154
|
+
const code = ch.charCodeAt(0);
|
|
2155
|
+
return (code >= 48 && code <= 57) || (code >= 97 && code <= 122);
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
function _normalizeIntentText(message) {
|
|
2159
|
+
const raw = String(message || '').toLowerCase();
|
|
2160
|
+
let out = ' ';
|
|
2161
|
+
let lastWasSpace = true;
|
|
2162
|
+
for (const ch of raw) {
|
|
2163
|
+
if (ch === '\'' || ch === '\u2019') continue;
|
|
2164
|
+
if (_isIntentWordChar(ch)) {
|
|
2165
|
+
out += ch;
|
|
2166
|
+
lastWasSpace = false;
|
|
2167
|
+
} else if (!lastWasSpace) {
|
|
2168
|
+
out += ' ';
|
|
2169
|
+
lastWasSpace = true;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
return lastWasSpace ? out : out + ' ';
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
function _intentTokens(normalized) {
|
|
2176
|
+
const text = String(normalized || '').trim();
|
|
2177
|
+
return text ? text.split(' ') : [];
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
function _intentHasAnyToken(normalized, terms) {
|
|
2181
|
+
const tokens = new Set(_intentTokens(normalized));
|
|
2182
|
+
return terms.some(term => tokens.has(term));
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
function _intentHasPhrase(normalized, phrase) {
|
|
2186
|
+
return String(normalized || '').includes(` ${phrase} `);
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
function _intentHasAnyPhrase(normalized, phrases) {
|
|
2190
|
+
return phrases.some(phrase => _intentHasPhrase(normalized, phrase));
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
function _intentTokenIsNumeric(token) {
|
|
2194
|
+
if (!token) return false;
|
|
2195
|
+
for (const ch of String(token)) {
|
|
2196
|
+
const code = ch.charCodeAt(0);
|
|
2197
|
+
if (code < 48 || code > 57) return false;
|
|
2198
|
+
}
|
|
2199
|
+
return true;
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
function _intentHasVerbObject(normalized, verbs, objects) {
|
|
2203
|
+
const verbSet = new Set(verbs);
|
|
2204
|
+
const objectSet = new Set(objects);
|
|
2205
|
+
const filler = new Set(['a', 'an', 'the', 'this', 'that', 'it', 'new', 'proper', 'some']);
|
|
2206
|
+
const tokens = _intentTokens(normalized);
|
|
2207
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2208
|
+
if (!verbSet.has(tokens[i])) continue;
|
|
2209
|
+
for (let j = i + 1; j < tokens.length && j <= i + 4; j++) {
|
|
2210
|
+
if (filler.has(tokens[j])) continue;
|
|
2211
|
+
if (objectSet.has(tokens[j])) return true;
|
|
2212
|
+
break;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
return false;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
function _messageRequestsPlanIntent(message) {
|
|
2219
|
+
const normalized = _normalizeIntentText(message);
|
|
2220
|
+
const tokens = _intentTokens(normalized);
|
|
2221
|
+
if (!tokens.length) return false;
|
|
2222
|
+
if (tokens[0] === 'plan') return true;
|
|
2223
|
+
if (_intentHasAnyPhrase(normalized, PLAN_CREATE_PHRASES) && _intentHasAnyToken(normalized, PLAN_INTENT_TERMS)) return true;
|
|
2224
|
+
if (_intentHasVerbObject(normalized, PLAN_CREATE_TERMS, PLAN_INTENT_TERMS)) return true;
|
|
2225
|
+
return _intentHasAnyToken(normalized, PLAN_INTENT_TERMS) &&
|
|
2226
|
+
_intentHasAnyToken(normalized, ['this', 'that', 'it', 'for', 'how', 'out']);
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
function _messageHasDelegationIntent(message) {
|
|
2230
|
+
const normalized = _normalizeIntentText(message);
|
|
2231
|
+
if (!normalized.trim()) return false;
|
|
2232
|
+
if (_intentHasAnyToken(normalized, DELEGATION_ACTION_TERMS)) return true;
|
|
2233
|
+
if (_intentHasPhrase(normalized, 'work item') && _intentHasAnyToken(normalized, ['create', 'open', 'add'])) return true;
|
|
2234
|
+
return _intentHasAnyPhrase(normalized, DELEGATION_MINIONS_PHRASES);
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
function _messageRequestsDirectHandling(message) {
|
|
2238
|
+
const normalized = _normalizeIntentText(message);
|
|
2239
|
+
if (!normalized.trim()) return false;
|
|
2240
|
+
if (_intentHasAnyPhrase(normalized, DIRECT_HANDLING_PHRASES)) return true;
|
|
2241
|
+
if (_intentHasAnyToken(normalized, DIRECT_QUICK_TERMS) && _intentHasAnyToken(normalized, DIRECT_QUICK_OBJECTS)) return true;
|
|
2242
|
+
if (_intentHasVerbObject(normalized, DIRECT_REPLY_TERMS, DIRECT_REPLY_TARGETS)) return true;
|
|
2243
|
+
return _intentHasVerbObject(normalized, DIRECT_SELF_TERMS, DIRECT_SELF_TARGETS);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
function _messageIsSmallDocOnlyRequest(normalized) {
|
|
2247
|
+
if (_intentHasAnyToken(normalized, DOC_CHAT_DIRECT_DOC_TERMS)) return true;
|
|
2248
|
+
return _intentHasVerbObject(normalized, DOC_CHAT_DOC_ACTION_TERMS, DOC_CHAT_DOC_OBJECTS);
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
function _messageHasMediumLargeWorkIntent(message, { source = 'command-center' } = {}) {
|
|
2252
|
+
const normalized = _normalizeIntentText(message);
|
|
2253
|
+
if (!normalized.trim()) return false;
|
|
2254
|
+
if (_messageRequestsDirectHandling(message)) return false;
|
|
2255
|
+
if (source === 'doc-chat' && _messageIsSmallDocOnlyRequest(normalized)) return false;
|
|
2256
|
+
return _intentHasAnyToken(normalized, MEDIUM_INVESTIGATION_TERMS) ||
|
|
2257
|
+
_intentHasAnyPhrase(normalized, MEDIUM_INVESTIGATION_PHRASES) ||
|
|
2258
|
+
_intentHasAnyToken(normalized, MEDIUM_IMPLEMENT_TERMS) ||
|
|
2259
|
+
_intentHasVerbObject(normalized, MEDIUM_BUILD_VERBS, MEDIUM_BUILD_OBJECTS) ||
|
|
2260
|
+
_intentHasAnyToken(normalized, MEDIUM_REVIEW_TERMS) ||
|
|
2261
|
+
_intentHasAnyToken(normalized, MEDIUM_SIZE_TERMS) ||
|
|
2262
|
+
_intentHasAnyPhrase(normalized, MEDIUM_SIZE_PHRASES);
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
function _inferDelegatedWorkType(message) {
|
|
2266
|
+
const normalized = _normalizeIntentText(message);
|
|
2267
|
+
if (_intentHasAnyToken(normalized, ['test', 'verify', 'validate']) || _intentHasPhrase(normalized, 'build and test')) return 'test';
|
|
2268
|
+
if (_intentHasAnyToken(normalized, ['review']) || _intentHasPhrase(normalized, 'code review')) return 'review';
|
|
2269
|
+
if (_intentHasAnyToken(normalized, ['fix', 'debug', 'repair', 'bug', 'broken', 'failing', 'failure', 'regression']) || _intentHasPhrase(normalized, 'root cause')) return 'fix';
|
|
2270
|
+
if (_intentHasAnyToken(normalized, ['implement', 'refactor', 'migrate', 'harden', 'redesign']) ||
|
|
2271
|
+
_intentHasVerbObject(normalized, MEDIUM_BUILD_VERBS, MEDIUM_BUILD_OBJECTS)) return 'implement';
|
|
2272
|
+
if (_intentHasAnyToken(normalized, MEDIUM_INVESTIGATION_TERMS) || _intentHasAnyPhrase(normalized, MEDIUM_INVESTIGATION_PHRASES)) return 'explore';
|
|
2273
|
+
return 'ask';
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
function _collapseWhitespace(text) {
|
|
2277
|
+
let out = '';
|
|
2278
|
+
let lastWasSpace = true;
|
|
2279
|
+
for (const ch of String(text || '')) {
|
|
2280
|
+
const isSpace = ch === ' ' || ch === '\n' || ch === '\r' || ch === '\t';
|
|
2281
|
+
if (isSpace) {
|
|
2282
|
+
if (!lastWasSpace) out += ' ';
|
|
2283
|
+
lastWasSpace = true;
|
|
2284
|
+
} else {
|
|
2285
|
+
out += ch;
|
|
2286
|
+
lastWasSpace = false;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
return out.trim();
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
function _stripEdgeQuotes(text) {
|
|
2293
|
+
const quotes = new Set(['"', "'", '`']);
|
|
2294
|
+
let start = 0;
|
|
2295
|
+
let end = String(text || '').length;
|
|
2296
|
+
while (start < end && quotes.has(text[start])) start++;
|
|
2297
|
+
while (end > start && quotes.has(text[end - 1])) end--;
|
|
2298
|
+
return text.slice(start, end);
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
function _delegatedWorkTitle(message, workType) {
|
|
2302
|
+
const compact = _stripEdgeQuotes(_collapseWhitespace(String(message || '')));
|
|
2303
|
+
const trimmed = compact.length > 90 ? compact.slice(0, 87).trimEnd() + '...' : compact;
|
|
2304
|
+
const fallback = {
|
|
2305
|
+
fix: 'Fix requested issue',
|
|
2306
|
+
review: 'Review requested work',
|
|
2307
|
+
test: 'Test requested work',
|
|
2308
|
+
implement: 'Implement requested work',
|
|
2309
|
+
'implement:large': 'Implement requested work',
|
|
2310
|
+
explore: 'Investigate requested work',
|
|
2311
|
+
ask: 'Answer requested query',
|
|
2312
|
+
verify: 'Verify requested work',
|
|
2313
|
+
}[workType] || 'Handle requested work';
|
|
2314
|
+
if (!trimmed) return fallback;
|
|
2315
|
+
const prefix = {
|
|
2316
|
+
fix: 'Fix',
|
|
2317
|
+
review: 'Review',
|
|
2318
|
+
test: 'Test',
|
|
2319
|
+
implement: 'Implement',
|
|
2320
|
+
'implement:large': 'Implement',
|
|
2321
|
+
explore: 'Investigate',
|
|
2322
|
+
ask: 'Answer',
|
|
2323
|
+
verify: 'Verify',
|
|
2324
|
+
}[workType] || 'Handle';
|
|
2325
|
+
const lower = trimmed.toLowerCase();
|
|
2326
|
+
const prefixLower = prefix.toLowerCase();
|
|
2327
|
+
if (lower === prefixLower || lower.startsWith(prefixLower + ' ')) return trimmed;
|
|
2328
|
+
return `${prefix}: ${trimmed}`;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
function _priorityFromDelegationMessage(message) {
|
|
2332
|
+
const normalized = _normalizeIntentText(message);
|
|
2333
|
+
if (_intentHasAnyToken(normalized, ['urgent', 'asap', 'critical', 'blocker', 'p0', 'p1']) || _intentHasPhrase(normalized, 'high priority')) return 'high';
|
|
2334
|
+
if (_intentHasPhrase(normalized, 'low priority') || _intentHasPhrase(normalized, 'when you can') || _intentHasAnyToken(normalized, ['whenever'])) return 'low';
|
|
2335
|
+
return 'medium';
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
function _inferDelegationActionFromMessage(message, { source = 'command-center', filePath = null, title = null, answerText = '', toolUses = [] } = {}) {
|
|
2339
|
+
if (_messageRequestsDirectHandling(message)) return null;
|
|
2340
|
+
const explicit = _messageHasDelegationIntent(message);
|
|
2341
|
+
const mediumLarge = _messageHasMediumLargeWorkIntent(message, { source });
|
|
2342
|
+
const toolCount = Array.isArray(toolUses) ? toolUses.length : 0;
|
|
2343
|
+
if (!explicit && !mediumLarge && toolCount < 4) return null;
|
|
2344
|
+
|
|
2345
|
+
const workType = _inferDelegatedWorkType(message);
|
|
2346
|
+
const inferredProject = source === 'doc-chat' && filePath && typeof _inferDocChatProject === 'function'
|
|
2347
|
+
? _inferDocChatProject(filePath)
|
|
2348
|
+
: null;
|
|
2349
|
+
const context = [];
|
|
2350
|
+
context.push(`${source === 'doc-chat' ? 'Doc-chat' : 'Command Center'} detected ${explicit ? 'an explicit dispatch/delegation request' : (toolCount >= 4 ? 'a medium-sized tool-using request' : 'a medium/large work request')} and routed it through the engine.`);
|
|
2351
|
+
if (filePath) context.push(`Document: ${filePath}`);
|
|
2352
|
+
if (title && title !== filePath) context.push(`Title: ${title}`);
|
|
2353
|
+
if (answerText && String(answerText).trim()) context.push(`Assistant draft before delegation:\n${String(answerText).trim().slice(0, 1200)}`);
|
|
2354
|
+
context.push(`Original request:\n${String(message || '').trim()}`);
|
|
2355
|
+
|
|
2356
|
+
return {
|
|
2357
|
+
type: 'dispatch',
|
|
2358
|
+
title: _delegatedWorkTitle(message, workType),
|
|
2359
|
+
workType,
|
|
2360
|
+
priority: _priorityFromDelegationMessage(message),
|
|
2361
|
+
description: context.join('\n\n'),
|
|
2362
|
+
...(inferredProject ? { project: inferredProject } : {}),
|
|
2363
|
+
_autoDelegated: true,
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
function _ensureDelegationForIntent(actions, opts = {}) {
|
|
2368
|
+
const list = Array.isArray(actions) ? actions.map(normalizeCCAction) : [];
|
|
2369
|
+
if (_messageRequestsDirectHandling(opts.message)) return list.filter(a => normalizeCCAction(a)?.type !== 'dispatch');
|
|
2370
|
+
if (list.some(a => normalizeCCAction(a)?.type === 'dispatch')) return list;
|
|
2371
|
+
if (list.length > 0) return list;
|
|
2372
|
+
const inferred = _inferDelegationActionFromMessage(opts.message, opts);
|
|
2373
|
+
return inferred ? [...list, inferred] : list;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2041
2376
|
function stripCCActionSyntax(text) {
|
|
2042
2377
|
if (!text) return '';
|
|
2043
2378
|
let displayText = text;
|
|
@@ -2414,9 +2749,10 @@ function createPipelineFromAction(action) {
|
|
|
2414
2749
|
// before `try` to avoid filling `results` with cryptic per-handler error messages.
|
|
2415
2750
|
function _ccValidateAction(action) {
|
|
2416
2751
|
if (!action || typeof action !== 'object' || !action.type) return 'action is missing required field: type';
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2752
|
+
const normalized = normalizeCCAction(action);
|
|
2753
|
+
switch (normalized.type) {
|
|
2754
|
+
case 'dispatch':
|
|
2755
|
+
if (!normalized.title || typeof normalized.title !== 'string' || !normalized.title.trim()) return `${action.type} action missing required field: title`;
|
|
2420
2756
|
return null;
|
|
2421
2757
|
case 'implement':
|
|
2422
2758
|
return 'Unsupported action type "implement"; use type="dispatch" with workType="implement".';
|
|
@@ -2760,7 +3096,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
2760
3096
|
}
|
|
2761
3097
|
try {
|
|
2762
3098
|
switch (action.type) {
|
|
2763
|
-
case 'dispatch':
|
|
3099
|
+
case 'dispatch': {
|
|
2764
3100
|
const workType = routing.normalizeWorkType(action.workType || (action.type !== 'dispatch' ? action.type : WORK_TYPE.IMPLEMENT), WORK_TYPE.IMPLEMENT);
|
|
2765
3101
|
const id = 'W-' + shared.uid();
|
|
2766
3102
|
const project = action.project || '';
|
|
@@ -2773,22 +3109,22 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
2773
3109
|
// projects → root-level work-items.json (orchestration system standalone use).
|
|
2774
3110
|
let targetProject = null;
|
|
2775
3111
|
if (project) {
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
results.push({ type: action.type, error:
|
|
3112
|
+
const target = resolveProjectSourceTarget(project, PROJECTS, { allowCentral: false });
|
|
3113
|
+
targetProject = target.project;
|
|
3114
|
+
if (target.error) {
|
|
3115
|
+
results.push({ type: action.type, error: target.error });
|
|
2780
3116
|
break;
|
|
2781
3117
|
}
|
|
2782
3118
|
} else if (prRef) {
|
|
2783
3119
|
const allPrs = getPullRequests().filter(p => !p._ghost);
|
|
2784
3120
|
linkedPr = shared.findPrRecord(allPrs, prRef) || null;
|
|
2785
3121
|
if (linkedPr?._project && linkedPr._project !== 'central') {
|
|
2786
|
-
targetProject =
|
|
3122
|
+
targetProject = resolveProjectSourceTarget(linkedPr._project, PROJECTS, { allowCentral: false }).project || null;
|
|
2787
3123
|
}
|
|
2788
3124
|
} else if (inferredProject) {
|
|
2789
3125
|
// Doc-chat fallback: filePath-derived project when the LLM omits the field. Validated against
|
|
2790
3126
|
// PROJECTS upstream by _inferDocChatProject — a stale lookup would just yield null here.
|
|
2791
|
-
targetProject =
|
|
3127
|
+
targetProject = resolveProjectSourceTarget(inferredProject, PROJECTS, { allowCentral: false }).project || null;
|
|
2792
3128
|
}
|
|
2793
3129
|
if (!targetProject) {
|
|
2794
3130
|
if (PROJECTS.length > 1) {
|
|
@@ -2809,7 +3145,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
2809
3145
|
break;
|
|
2810
3146
|
}
|
|
2811
3147
|
|
|
2812
|
-
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) :
|
|
3148
|
+
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : shared.centralWorkItemsPath(MINIONS_DIR);
|
|
2813
3149
|
|
|
2814
3150
|
// Promote `agent` (singular) → `agents` (array). Models emit either shape and the prior code
|
|
2815
3151
|
// only read `action.agents`, silently dropping `agent: "lambert"` style hints.
|
|
@@ -2827,7 +3163,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
2827
3163
|
|
|
2828
3164
|
// Issue #1772: CC review/explore/test are human-initiated one-offs.
|
|
2829
3165
|
// Mark oneShot so any discovered PR is tagged _contextOnly (skips eval loop).
|
|
2830
|
-
const ccOneShotTypes = new Set([
|
|
3166
|
+
const ccOneShotTypes = new Set([WORK_TYPE.REVIEW, WORK_TYPE.EXPLORE, WORK_TYPE.TEST, WORK_TYPE.ASK, WORK_TYPE.VERIFY]);
|
|
2831
3167
|
const isOneShot = action.oneShot === true || (action.oneShot !== false && ccOneShotTypes.has(workType));
|
|
2832
3168
|
const item = {
|
|
2833
3169
|
id, title: action.title.trim(), type: workType,
|
|
@@ -2872,7 +3208,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
2872
3208
|
// unresolved → error so build-and-test can't accidentally run against the wrong repo.
|
|
2873
3209
|
const projectName = action.project || pr._project || null;
|
|
2874
3210
|
const project = projectName
|
|
2875
|
-
?
|
|
3211
|
+
? resolveProjectSourceTarget(projectName, PROJECTS, { allowCentral: false }).project
|
|
2876
3212
|
: null;
|
|
2877
3213
|
if (!project) {
|
|
2878
3214
|
results.push({ type: 'build-and-test', error: `Project not found for PR ${pr.id}: ${projectName || '(none)'}` });
|
|
@@ -2933,14 +3269,14 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
2933
3269
|
const project = action.project || '';
|
|
2934
3270
|
let targetProject = null;
|
|
2935
3271
|
if (project) {
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
results.push({ type: 'reopen-work-item', id: action.id, error:
|
|
3272
|
+
const target = resolveProjectSourceTarget(project, PROJECTS, { allowCentral: false });
|
|
3273
|
+
targetProject = target.project;
|
|
3274
|
+
if (target.error) {
|
|
3275
|
+
results.push({ type: 'reopen-work-item', id: action.id, error: target.error });
|
|
2940
3276
|
break;
|
|
2941
3277
|
}
|
|
2942
3278
|
} else if (inferredProject) {
|
|
2943
|
-
targetProject =
|
|
3279
|
+
targetProject = resolveProjectSourceTarget(inferredProject, PROJECTS, { allowCentral: false }).project || null;
|
|
2944
3280
|
}
|
|
2945
3281
|
if (!targetProject) {
|
|
2946
3282
|
if (PROJECTS.length > 1) {
|
|
@@ -2949,7 +3285,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
|
|
|
2949
3285
|
}
|
|
2950
3286
|
if (PROJECTS.length === 1) targetProject = PROJECTS[0];
|
|
2951
3287
|
}
|
|
2952
|
-
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) :
|
|
3288
|
+
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : shared.centralWorkItemsPath(MINIONS_DIR);
|
|
2953
3289
|
let reopenResult = null;
|
|
2954
3290
|
mutateJsonFileLocked(wiPath, items => {
|
|
2955
3291
|
if (!Array.isArray(items)) items = [];
|
|
@@ -3054,7 +3390,7 @@ async function executeDocChatActions(actions, { filePath = null } = {}) {
|
|
|
3054
3390
|
return executeCCActions(actions, { source: 'doc-chat', inferredProject: _inferDocChatProject(filePath) });
|
|
3055
3391
|
}
|
|
3056
3392
|
|
|
3057
|
-
const DOC_CHAT_WORK_ITEM_ACTION_TYPES = new Set(['dispatch', 'fix', 'explore', 'review', 'test']);
|
|
3393
|
+
const DOC_CHAT_WORK_ITEM_ACTION_TYPES = new Set(['dispatch', 'fix', 'implement', 'implement:large', 'explore', 'review', 'test', 'ask', 'verify']);
|
|
3058
3394
|
|
|
3059
3395
|
function _buildDocChatActionFeedback(actions, actionResults) {
|
|
3060
3396
|
if (!Array.isArray(actions) || !Array.isArray(actionResults)) return [];
|
|
@@ -3529,7 +3865,7 @@ function _inferDocChatProject(filePath) {
|
|
|
3529
3865
|
if (!filePath) return null;
|
|
3530
3866
|
const m = String(filePath).replace(/\\/g, '/').match(/^projects\/([^/]+)\//);
|
|
3531
3867
|
if (!m) return null;
|
|
3532
|
-
const inferred =
|
|
3868
|
+
const inferred = resolveProjectSourceTarget(m[1], PROJECTS, { allowCentral: false }).project;
|
|
3533
3869
|
return inferred?.name || null;
|
|
3534
3870
|
}
|
|
3535
3871
|
|
|
@@ -3855,8 +4191,9 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
3855
4191
|
timeout: DOC_CHAT_TIMEOUT_MS,
|
|
3856
4192
|
// Match Command Center's full tool surface and turn budget so doc-chat
|
|
3857
4193
|
// can take action (read/write/edit/dispatch) instead of being limited
|
|
3858
|
-
// to Q&A. The doc-chat sysprompt
|
|
3859
|
-
//
|
|
4194
|
+
// to Q&A. The doc-chat sysprompt scopes orchestration to human message
|
|
4195
|
+
// intent (explicit dispatch or medium/larger work), and ---DOCUMENT---
|
|
4196
|
+
// remains the only whole-file document edit channel.
|
|
3860
4197
|
allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
|
|
3861
4198
|
systemPrompt: DOC_CHAT_SYSTEM_PROMPT,
|
|
3862
4199
|
transcript,
|
|
@@ -4215,14 +4552,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
4215
4552
|
}
|
|
4216
4553
|
|
|
4217
4554
|
const config = queries.getConfig();
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4555
|
+
// safeJsonNoRestore — never resurrect an archived PRD's .backup sidecar
|
|
4556
|
+
// during project resolution (W-mouptdh1000h9f39). The active-from-archive
|
|
4557
|
+
// write above (mutateJsonFileLocked) already created activePath when needed.
|
|
4558
|
+
const projectPlan = safeJsonNoRestore(activePath) || safeJsonNoRestore(prdPath);
|
|
4559
|
+
const projectTarget = projectPlan?.project
|
|
4560
|
+
? resolveProjectSourceTarget(projectPlan.project, PROJECTS, { allowCentral: false })
|
|
4561
|
+
: null;
|
|
4562
|
+
const project = projectTarget?.project || (PROJECTS.length === 1 ? PROJECTS[0] : null);
|
|
4226
4563
|
|
|
4227
4564
|
// Check for existing verify WI — reset to pending if already done (re-verify)
|
|
4228
4565
|
if (project) {
|
|
@@ -4276,25 +4613,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4276
4613
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4277
4614
|
|
|
4278
4615
|
// Find the right file — check source first, then search all project files
|
|
4279
|
-
let
|
|
4280
|
-
if (
|
|
4281
|
-
|
|
4282
|
-
if (proj) wiPath = shared.projectWorkItemsPath(proj);
|
|
4283
|
-
}
|
|
4284
|
-
if (!wiPath) {
|
|
4285
|
-
// Search central first, then all projects
|
|
4286
|
-
const centralPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4287
|
-
const centralItems = shared.safeJson(centralPath) || [];
|
|
4288
|
-
if (centralItems.some(i => i.id === id)) {
|
|
4289
|
-
wiPath = centralPath;
|
|
4290
|
-
} else {
|
|
4291
|
-
for (const proj of PROJECTS) {
|
|
4292
|
-
const projPath = shared.projectWorkItemsPath(proj);
|
|
4293
|
-
const projItems = shared.safeJson(projPath) || [];
|
|
4294
|
-
if (projItems.some(i => i.id === id)) { wiPath = projPath; break; }
|
|
4295
|
-
}
|
|
4296
|
-
}
|
|
4297
|
-
}
|
|
4616
|
+
let resolvedTarget = findWorkItemsTargetById(id, source, PROJECTS);
|
|
4617
|
+
if (resolvedTarget.error) return jsonReply(res, 404, { error: resolvedTarget.error });
|
|
4618
|
+
let wiPath = resolvedTarget.found ? resolvedTarget.wiPath : null;
|
|
4298
4619
|
// If no work item found, attempt to re-materialize from PRD item definition
|
|
4299
4620
|
if (!wiPath) {
|
|
4300
4621
|
const prdFile = body.prdFile;
|
|
@@ -4313,8 +4634,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4313
4634
|
|
|
4314
4635
|
// Determine target work-items file (project from PRD item or plan, fallback to central)
|
|
4315
4636
|
const projName = prdItem.project || plan.project || prdFile.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
|
|
4316
|
-
const
|
|
4317
|
-
const
|
|
4637
|
+
const prdTarget = resolveProjectSourceTarget(projName, PROJECTS, { allowCentral: false });
|
|
4638
|
+
const proj = prdTarget.project;
|
|
4639
|
+
const targetWiPath = proj ? shared.projectWorkItemsPath(proj) : shared.centralWorkItemsPath(MINIONS_DIR);
|
|
4318
4640
|
|
|
4319
4641
|
// Create new work item from PRD item definition (same logic as materializePlansAsWorkItems)
|
|
4320
4642
|
const complexity = prdItem.estimated_complexity || 'medium';
|
|
@@ -4346,8 +4668,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4346
4668
|
|
|
4347
4669
|
// Clear dispatch history and cooldowns for this item
|
|
4348
4670
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
4349
|
-
const
|
|
4350
|
-
const dispatchKey = sourcePrefix + id;
|
|
4671
|
+
const dispatchKey = (proj ? `work-${proj.name}-` : 'central-work-') + id;
|
|
4351
4672
|
try {
|
|
4352
4673
|
mutateJsonFileLocked(dispatchPath, (dispatch) => {
|
|
4353
4674
|
dispatch.completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
|
|
@@ -4392,8 +4713,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4392
4713
|
|
|
4393
4714
|
// Clear completed dispatch entries so the engine doesn't dedup this item
|
|
4394
4715
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
4395
|
-
const
|
|
4396
|
-
const dispatchKey = sourcePrefix + id;
|
|
4716
|
+
const dispatchKey = dispatchPrefixForResolvedSource(resolvedTarget) + id;
|
|
4397
4717
|
try {
|
|
4398
4718
|
mutateJsonFileLocked(dispatchPath, (dispatch) => {
|
|
4399
4719
|
dispatch.completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
|
|
@@ -4421,17 +4741,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4421
4741
|
const { id, source } = body;
|
|
4422
4742
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4423
4743
|
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4428
|
-
} else {
|
|
4429
|
-
const proj = PROJECTS.find(p => p.name === source);
|
|
4430
|
-
if (proj) {
|
|
4431
|
-
wiPath = shared.projectWorkItemsPath(proj);
|
|
4432
|
-
}
|
|
4433
|
-
}
|
|
4434
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
4744
|
+
const target = resolveProjectSourceTarget(source, PROJECTS);
|
|
4745
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4746
|
+
const wiPath = target.wiPath;
|
|
4435
4747
|
|
|
4436
4748
|
let item = null;
|
|
4437
4749
|
let found = false;
|
|
@@ -4481,15 +4793,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4481
4793
|
const { id, source, reason } = body;
|
|
4482
4794
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4483
4795
|
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4488
|
-
} else {
|
|
4489
|
-
const proj = PROJECTS.find(p => p.name === source);
|
|
4490
|
-
if (proj) wiPath = shared.projectWorkItemsPath(proj);
|
|
4491
|
-
}
|
|
4492
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
4796
|
+
const target = resolveProjectSourceTarget(source, PROJECTS);
|
|
4797
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4798
|
+
const wiPath = target.wiPath;
|
|
4493
4799
|
|
|
4494
4800
|
let result = null;
|
|
4495
4801
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
@@ -4537,16 +4843,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4537
4843
|
const { id, source } = body;
|
|
4538
4844
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4539
4845
|
|
|
4540
|
-
|
|
4541
|
-
if (
|
|
4542
|
-
|
|
4543
|
-
} else {
|
|
4544
|
-
const proj = PROJECTS.find(p => p.name === source);
|
|
4545
|
-
if (proj) {
|
|
4546
|
-
wiPath = shared.projectWorkItemsPath(proj);
|
|
4547
|
-
}
|
|
4548
|
-
}
|
|
4549
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
4846
|
+
const target = resolveProjectSourceTarget(source, PROJECTS);
|
|
4847
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4848
|
+
const wiPath = target.wiPath;
|
|
4550
4849
|
|
|
4551
4850
|
let archivedItem = null;
|
|
4552
4851
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
@@ -4568,7 +4867,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4568
4867
|
}, { defaultValue: [] });
|
|
4569
4868
|
|
|
4570
4869
|
// Clean dispatch entries for archived item
|
|
4571
|
-
const sourcePrefix = (
|
|
4870
|
+
const sourcePrefix = dispatchPrefixForResolvedSource(target);
|
|
4572
4871
|
cleanDispatchEntries(d =>
|
|
4573
4872
|
d.meta?.dispatchKey === sourcePrefix + id ||
|
|
4574
4873
|
d.meta?.item?.id === id
|
|
@@ -4594,15 +4893,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4594
4893
|
const project = body.project || body.source;
|
|
4595
4894
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4596
4895
|
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4601
|
-
} else {
|
|
4602
|
-
const proj = PROJECTS.find(p => p.name === project);
|
|
4603
|
-
if (proj) wiPath = shared.projectWorkItemsPath(proj);
|
|
4604
|
-
}
|
|
4605
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'project not found' });
|
|
4896
|
+
const target = resolveProjectSourceTarget(project, PROJECTS);
|
|
4897
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4898
|
+
const wiPath = target.wiPath;
|
|
4606
4899
|
|
|
4607
4900
|
let result = null;
|
|
4608
4901
|
mutateJsonFileLocked(wiPath, (items) => {
|
|
@@ -4622,8 +4915,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4622
4915
|
if (result.code !== 200) return jsonReply(res, result.code, result.body);
|
|
4623
4916
|
|
|
4624
4917
|
// Clear dispatch history and cooldowns outside lock
|
|
4625
|
-
const
|
|
4626
|
-
const dispatchKey = sourcePrefix + id;
|
|
4918
|
+
const dispatchKey = dispatchPrefixForResolvedSource(target) + id;
|
|
4627
4919
|
try {
|
|
4628
4920
|
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
4629
4921
|
mutateJsonFileLocked(dispatchPath, (dispatch) => {
|
|
@@ -4689,16 +4981,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
4689
4981
|
const { id, source, title, description, type, priority, agent } = body;
|
|
4690
4982
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
4691
4983
|
|
|
4692
|
-
|
|
4693
|
-
if (
|
|
4694
|
-
|
|
4695
|
-
} else {
|
|
4696
|
-
const proj = PROJECTS.find(p => p.name === source);
|
|
4697
|
-
if (proj) {
|
|
4698
|
-
wiPath = shared.projectWorkItemsPath(proj);
|
|
4699
|
-
}
|
|
4700
|
-
}
|
|
4701
|
-
if (!wiPath) return jsonReply(res, 404, { error: 'source not found' });
|
|
4984
|
+
const target = resolveProjectSourceTarget(source, PROJECTS);
|
|
4985
|
+
if (target.error) return jsonReply(res, 404, { error: target.error });
|
|
4986
|
+
const wiPath = target.wiPath;
|
|
4702
4987
|
|
|
4703
4988
|
let result = null;
|
|
4704
4989
|
let agentChanged = false;
|
|
@@ -4750,6 +5035,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
4750
5035
|
try {
|
|
4751
5036
|
const body = await readBody(req);
|
|
4752
5037
|
if (!body.title || !body.title.trim()) return jsonReply(res, 400, { error: 'title is required' });
|
|
5038
|
+
const target = resolveWorkItemsCreateTarget(body.project);
|
|
5039
|
+
if (target.error) return jsonReply(res, 400, { error: target.error });
|
|
4753
5040
|
// Write as a work item with type 'plan' — user must explicitly execute plan-to-prd after reviewing
|
|
4754
5041
|
const wiPath = path.join(MINIONS_DIR, 'work-items.json');
|
|
4755
5042
|
const id = 'W-' + shared.uid();
|
|
@@ -4759,7 +5046,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4759
5046
|
status: WI_STATUS.PENDING, created: new Date().toISOString(), createdBy: 'dashboard',
|
|
4760
5047
|
branchStrategy: body.branch_strategy || body.branchStrategy || 'parallel',
|
|
4761
5048
|
};
|
|
4762
|
-
if (
|
|
5049
|
+
if (target.project) item.project = target.project.name;
|
|
4763
5050
|
if (body.agent) item.agent = body.agent;
|
|
4764
5051
|
mutateWorkItems(wiPath, items => { items.push(item); });
|
|
4765
5052
|
return jsonReply(res, 200, { ok: true, id, agent: body.agent || '' });
|
|
@@ -4770,6 +5057,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
4770
5057
|
try {
|
|
4771
5058
|
const body = await readBody(req);
|
|
4772
5059
|
if (!body.name || !body.name.trim()) return jsonReply(res, 400, { error: 'name is required' });
|
|
5060
|
+
const target = resolveWorkItemsCreateTarget(body.project);
|
|
5061
|
+
if (target.error) return jsonReply(res, 400, { error: target.error });
|
|
4773
5062
|
|
|
4774
5063
|
if (!fs.existsSync(PRD_DIR)) fs.mkdirSync(PRD_DIR, { recursive: true });
|
|
4775
5064
|
|
|
@@ -4777,7 +5066,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
4777
5066
|
const planFile = 'manual-' + shared.uid() + '.json';
|
|
4778
5067
|
const plan = {
|
|
4779
5068
|
version: 'manual-' + new Date().toISOString().slice(0, 10),
|
|
4780
|
-
project:
|
|
5069
|
+
project: target.project?.name || 'Unknown',
|
|
4781
5070
|
generated_by: 'dashboard',
|
|
4782
5071
|
generated_at: new Date().toISOString().slice(0, 10),
|
|
4783
5072
|
plan_summary: body.name,
|
|
@@ -5524,7 +5813,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
5524
5813
|
}).join('\n');
|
|
5525
5814
|
|
|
5526
5815
|
const projectName = plan.project || body.file.replace(/-\d{4}-\d{2}-\d{2}\.json$/, '');
|
|
5527
|
-
const
|
|
5816
|
+
const projectTarget = resolveProjectSourceTarget(projectName, PROJECTS, { allowCentral: false });
|
|
5817
|
+
const targetProject = projectTarget.project || (PROJECTS.length === 1 ? PROJECTS[0] : null);
|
|
5528
5818
|
if (targetProject) {
|
|
5529
5819
|
diffAwareQueued = shared.queuePlanToPrd({
|
|
5530
5820
|
planFile: plan.source_plan, prdFile: body.file,
|
|
@@ -6088,15 +6378,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6088
6378
|
transcript: body.transcript,
|
|
6089
6379
|
onAbortReady: (abort) => { _docAbort = abort; },
|
|
6090
6380
|
});
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6381
|
+
const delegatedActions = _ensureDelegationForIntent(actions, {
|
|
6382
|
+
message: body.message, source: 'doc-chat', filePath: body.filePath, title: body.title, answerText: answer, toolUses,
|
|
6383
|
+
});
|
|
6384
|
+
const actionResults = await executeDocChatActions(delegatedActions, { filePath: body.filePath });
|
|
6385
|
+
const actionFeedback = _buildDocChatActionFeedback(delegatedActions, actionResults);
|
|
6094
6386
|
const finalize = _finalizeDocChatEdit({
|
|
6095
6387
|
filePath: body.filePath, fullPath, isJson, canEdit,
|
|
6096
6388
|
originalContent: currentContent, delimiterContent: content,
|
|
6097
6389
|
});
|
|
6098
6390
|
const payload = _buildDocChatResponsePayload({
|
|
6099
|
-
answer, actions, actionResults, actionFeedback, actionParseError, ccError, partial, warning, toolUses, finalize,
|
|
6391
|
+
answer, actions: delegatedActions, actionResults, actionFeedback, actionParseError, ccError, partial, warning, toolUses, finalize,
|
|
6100
6392
|
});
|
|
6101
6393
|
_docDone = true;
|
|
6102
6394
|
return jsonReply(res, 200, payload);
|
|
@@ -6184,15 +6476,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6184
6476
|
onToolUse: (name, input) => { writeDocEvent({ type: 'tool', name, input: _lightToolInput(input) }); },
|
|
6185
6477
|
onRetry: (attempt) => { writeDocEvent({ type: 'progress', attempt }); },
|
|
6186
6478
|
});
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6479
|
+
const delegatedActions = _ensureDelegationForIntent(actions, {
|
|
6480
|
+
message: body.message, source: 'doc-chat', filePath: body.filePath, title: body.title, answerText: answer, toolUses,
|
|
6481
|
+
});
|
|
6482
|
+
const actionResults = await executeDocChatActions(delegatedActions, { filePath: body.filePath });
|
|
6483
|
+
const actionFeedback = _buildDocChatActionFeedback(delegatedActions, actionResults);
|
|
6190
6484
|
const finalize = _finalizeDocChatEdit({
|
|
6191
6485
|
filePath: body.filePath, fullPath, isJson, canEdit,
|
|
6192
6486
|
originalContent: currentContent, delimiterContent: content,
|
|
6193
6487
|
});
|
|
6194
6488
|
const payload = _buildDocChatResponsePayload({
|
|
6195
|
-
answer, actions, actionResults, actionFeedback, actionParseError, ccError, partial, warning, toolUses, finalize,
|
|
6489
|
+
answer, actions: delegatedActions, actionResults, actionFeedback, actionParseError, ccError, partial, warning, toolUses, finalize,
|
|
6196
6490
|
});
|
|
6197
6491
|
const { answer: finalAnswer, ...donePayload } = payload;
|
|
6198
6492
|
writeDocEvent({
|
|
@@ -6484,7 +6778,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6484
6778
|
let alreadyLinked = false;
|
|
6485
6779
|
mutateDashboardConfig(config => {
|
|
6486
6780
|
if (!Array.isArray(config.projects)) config.projects = [];
|
|
6487
|
-
alreadyLinked = config.projects.some(p =>
|
|
6781
|
+
alreadyLinked = config.projects.some(p => shared.sameResolvedPath(p.localPath, target));
|
|
6488
6782
|
return config;
|
|
6489
6783
|
});
|
|
6490
6784
|
if (alreadyLinked) {
|
|
@@ -6529,7 +6823,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6529
6823
|
let duplicate = false;
|
|
6530
6824
|
mutateDashboardConfig(config => {
|
|
6531
6825
|
if (!Array.isArray(config.projects)) config.projects = [];
|
|
6532
|
-
if (config.projects.some(p =>
|
|
6826
|
+
if (config.projects.some(p => shared.sameResolvedPath(p.localPath, target))) {
|
|
6533
6827
|
duplicate = true;
|
|
6534
6828
|
return config;
|
|
6535
6829
|
}
|
|
@@ -6713,13 +7007,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6713
7007
|
});
|
|
6714
7008
|
}
|
|
6715
7009
|
|
|
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
|
-
});
|
|
6722
7010
|
const toolUses = Array.isArray(result.toolUses) ? result.toolUses : _extractToolUsesFromRaw(result.raw);
|
|
7011
|
+
const parsed = parseCCActions(result.text);
|
|
6723
7012
|
// Safety net: detect /loop invocation and convert to create-watch
|
|
6724
7013
|
const _loopWatch = _detectLoopInvocation(parsed.text, parsed.actions, toolUses, body.message);
|
|
6725
7014
|
if (_loopWatch) {
|
|
@@ -6727,7 +7016,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6727
7016
|
console.warn('[CC] /loop invocation detected — converted to create-watch');
|
|
6728
7017
|
try { shared.log('warn', '/loop invocation detected in CC response — auto-converted to create-watch'); } catch {}
|
|
6729
7018
|
}
|
|
6730
|
-
parsed.actions =
|
|
7019
|
+
parsed.actions = _actionsWithIntentFallback(
|
|
7020
|
+
_filterImplicitPostDispatchActions(parsed.actions, body.message),
|
|
7021
|
+
{ message: body.message, intentMetadata: body.intentMetadata, source: 'command-center', answerText: parsed.text, toolUses }
|
|
7022
|
+
);
|
|
6731
7023
|
if (parsed.actions.length > 0) {
|
|
6732
7024
|
parsed.actionResults = await executeCCActions(parsed.actions);
|
|
6733
7025
|
}
|
|
@@ -7048,11 +7340,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7048
7340
|
|
|
7049
7341
|
// Send final result with actions — execute server-side first
|
|
7050
7342
|
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
|
-
});
|
|
7056
7343
|
// Safety net: detect /loop invocation and convert to create-watch
|
|
7057
7344
|
const _loopWatch = _detectLoopInvocation(displayText, actions, toolUses, body.message);
|
|
7058
7345
|
if (_loopWatch) {
|
|
@@ -7060,7 +7347,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7060
7347
|
console.warn('[CC] /loop invocation detected — converted to create-watch');
|
|
7061
7348
|
try { shared.log('warn', '/loop invocation detected in CC response — auto-converted to create-watch'); } catch {}
|
|
7062
7349
|
}
|
|
7063
|
-
actions =
|
|
7350
|
+
actions = _actionsWithIntentFallback(
|
|
7351
|
+
_filterImplicitPostDispatchActions(actions, body.message),
|
|
7352
|
+
{ message: body.message, intentMetadata: body.intentMetadata, source: 'command-center', answerText: displayText, toolUses }
|
|
7353
|
+
);
|
|
7064
7354
|
let actionResults;
|
|
7065
7355
|
if (actions.length > 0) {
|
|
7066
7356
|
actionResults = await executeCCActions(actions);
|
|
@@ -7124,6 +7414,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7124
7414
|
const body = await readBody(req);
|
|
7125
7415
|
let { id, cron, title, type, project, agent, description, priority, enabled } = body;
|
|
7126
7416
|
if (!cron || !title) return jsonReply(res, 400, { error: 'cron and title are required' });
|
|
7417
|
+
const projectTarget = shared.resolveConfiguredProject(project, PROJECTS);
|
|
7418
|
+
if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
|
|
7419
|
+
project = projectTarget.project?.name || null;
|
|
7127
7420
|
|
|
7128
7421
|
// Auto-generate ID from title if not provided
|
|
7129
7422
|
if (!id) {
|
|
@@ -7156,8 +7449,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7156
7449
|
|
|
7157
7450
|
async function handleSchedulesUpdate(req, res) {
|
|
7158
7451
|
const body = await readBody(req);
|
|
7159
|
-
|
|
7452
|
+
let { id, cron, title, type, project, agent, description, priority, enabled } = body;
|
|
7160
7453
|
if (!id) return jsonReply(res, 400, { error: 'id required' });
|
|
7454
|
+
if (project !== undefined) {
|
|
7455
|
+
const projectTarget = shared.resolveConfiguredProject(project, PROJECTS);
|
|
7456
|
+
if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
|
|
7457
|
+
project = projectTarget.project?.name || null;
|
|
7458
|
+
}
|
|
7161
7459
|
|
|
7162
7460
|
let missingSchedules = false;
|
|
7163
7461
|
let sched = null;
|
|
@@ -7218,6 +7516,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7218
7516
|
reloadConfig();
|
|
7219
7517
|
const sched = (CONFIG.schedules || []).find(s => s.id === id);
|
|
7220
7518
|
if (!sched) return jsonReply(res, 404, { error: 'Schedule not found' });
|
|
7519
|
+
if (sched.project) {
|
|
7520
|
+
const projectTarget = shared.resolveConfiguredProject(sched.project, PROJECTS);
|
|
7521
|
+
if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
|
|
7522
|
+
sched.project = projectTarget.project.name;
|
|
7523
|
+
}
|
|
7221
7524
|
|
|
7222
7525
|
const schedulerMod = require('./engine/scheduler');
|
|
7223
7526
|
let item;
|
|
@@ -7675,7 +7978,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7675
7978
|
if (body.projects && Array.isArray(body.projects)) {
|
|
7676
7979
|
if (!config.projects) config.projects = [];
|
|
7677
7980
|
for (const update of body.projects) {
|
|
7678
|
-
const proj = config.projects
|
|
7981
|
+
const proj = shared.findProjectByName(config.projects, update.name);
|
|
7679
7982
|
if (!proj) continue;
|
|
7680
7983
|
if (!proj.workSources) proj.workSources = {};
|
|
7681
7984
|
if (update.workSources?.pullRequests !== undefined) {
|
|
@@ -8325,8 +8628,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
8325
8628
|
const { savePipeline, getPipeline } = require('./engine/pipeline');
|
|
8326
8629
|
if (getPipeline(body.id)) return jsonReply(res, 409, { error: 'Pipeline already exists' });
|
|
8327
8630
|
const pipeline = { id: body.id, title: body.title, stages: body.stages, trigger: body.trigger || {}, enabled: body.enabled !== false };
|
|
8631
|
+
if (body.project !== undefined) pipeline.project = body.project;
|
|
8632
|
+
if (Array.isArray(body.projects)) pipeline.projects = body.projects;
|
|
8328
8633
|
if (body.stopWhen) pipeline.stopWhen = body.stopWhen;
|
|
8329
8634
|
if (Array.isArray(body.monitoredResources) && body.monitoredResources.length > 0) pipeline.monitoredResources = body.monitoredResources;
|
|
8635
|
+
const projectError = validatePipelineProjects(pipeline);
|
|
8636
|
+
if (projectError) return jsonReply(res, 400, { error: projectError });
|
|
8330
8637
|
savePipeline(pipeline);
|
|
8331
8638
|
invalidateStatusCache();
|
|
8332
8639
|
return jsonReply(res, 200, { ok: true, id: pipeline.id });
|
|
@@ -8341,8 +8648,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
8341
8648
|
if (body.stages !== undefined) pipeline.stages = body.stages;
|
|
8342
8649
|
if (body.trigger !== undefined) pipeline.trigger = body.trigger;
|
|
8343
8650
|
if (body.enabled !== undefined) pipeline.enabled = body.enabled;
|
|
8651
|
+
if (body.project !== undefined) pipeline.project = body.project;
|
|
8652
|
+
if (body.projects !== undefined) pipeline.projects = body.projects;
|
|
8344
8653
|
if (body.monitoredResources !== undefined) pipeline.monitoredResources = body.monitoredResources;
|
|
8345
8654
|
if (body.stopWhen !== undefined) pipeline.stopWhen = body.stopWhen;
|
|
8655
|
+
const projectError = validatePipelineProjects(pipeline);
|
|
8656
|
+
if (projectError) return jsonReply(res, 400, { error: projectError });
|
|
8346
8657
|
savePipeline(pipeline);
|
|
8347
8658
|
invalidateStatusCache();
|
|
8348
8659
|
return jsonReply(res, 200, { ok: true });
|
|
@@ -8686,6 +8997,11 @@ module.exports = {
|
|
|
8686
8997
|
_shouldSuppressDocChatPostPatchError,
|
|
8687
8998
|
_buildDocChatResponsePayload,
|
|
8688
8999
|
_inferDocChatProject,
|
|
9000
|
+
_messageHasDelegationIntent,
|
|
9001
|
+
_messageRequestsDirectHandling,
|
|
9002
|
+
_messageHasMediumLargeWorkIntent,
|
|
9003
|
+
_inferDelegationActionFromMessage,
|
|
9004
|
+
_ensureDelegationForIntent,
|
|
8689
9005
|
_linkPullRequestForTracking: linkPullRequestForTracking,
|
|
8690
9006
|
_resolveSkillReadPath,
|
|
8691
9007
|
DOC_CHAT_DOCUMENT_DELIMITER,
|