@yemi33/minions 0.1.1809 → 0.1.1810

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/dashboard.js CHANGED
@@ -369,14 +369,11 @@ function createWorkItemWithDedup(wiPath, item, options = {}) {
369
369
  }
370
370
 
371
371
  function formatUnknownProjectError(projectName, projects = []) {
372
- const known = projects.map(p => p.name).filter(Boolean).join(', ') || '(none configured)';
373
- return `Project "${projectName}" not found. Known projects: ${known}`;
372
+ return shared.formatUnknownProjectError(projectName, projects);
374
373
  }
375
374
 
376
375
  function findProjectByName(projects, projectName) {
377
- const name = String(projectName || '').trim().toLowerCase();
378
- if (!name) return null;
379
- return projects.find(p => p.name?.toLowerCase() === name) || null;
376
+ return shared.findProjectByName(projects, projectName);
380
377
  }
381
378
 
382
379
  function resolveWorkItemsCreateTarget(projectName, projects = PROJECTS) {
@@ -394,6 +391,47 @@ function resolveWorkItemsCreateTarget(projectName, projects = PROJECTS) {
394
391
  };
395
392
  }
396
393
 
394
+ function validatePipelineProjects(pipeline, projects = PROJECTS) {
395
+ const refs = [];
396
+ const collect = (value) => {
397
+ if (value === undefined || value === null || value === '') return;
398
+ if (Array.isArray(value)) { value.forEach(collect); return; }
399
+ if (typeof value === 'object') {
400
+ if (value.project !== undefined) collect(value.project);
401
+ else if (value._project !== undefined) collect(value._project);
402
+ else if (value.name !== undefined) collect(value.name);
403
+ return;
404
+ }
405
+ refs.push(String(value));
406
+ };
407
+ const collectResourceProjects = (resources) => {
408
+ for (const resource of Array.isArray(resources) ? resources : []) {
409
+ if (resource && typeof resource === 'object') {
410
+ if (resource.project !== undefined) collect(resource.project);
411
+ if (resource._project !== undefined) collect(resource._project);
412
+ }
413
+ }
414
+ };
415
+ collect(pipeline.project);
416
+ collect(pipeline.projects);
417
+ collectResourceProjects(pipeline.monitoredResources);
418
+ for (const stage of pipeline.stages || []) {
419
+ collect(stage.project);
420
+ collect(stage.projects);
421
+ collectResourceProjects(stage.monitoredResources);
422
+ for (const item of stage.items || []) {
423
+ collect(item.project);
424
+ collect(item.projects);
425
+ }
426
+ }
427
+ for (const ref of refs) {
428
+ if (ref.trim().toLowerCase() === 'central') continue;
429
+ const result = shared.resolveConfiguredProject(ref, projects);
430
+ if (result.error) return result.error;
431
+ }
432
+ return null;
433
+ }
434
+
397
435
  /**
398
436
  * Aggregate archived work items from the central archive plus every project
399
437
  * archive. Each item is tagged with `_source` (`'central'` or the project name)
@@ -425,12 +463,17 @@ function linkPullRequestForTracking({ url, title, project: projectName, autoObse
425
463
  }
426
464
  const projects = shared.getProjects(config);
427
465
  const explicitProjectName = String(projectName || '').trim();
428
- const targetProject = explicitProjectName ? findProjectByName(projects, explicitProjectName) : (projects[0] || null);
466
+ let targetProject = explicitProjectName ? findProjectByName(projects, explicitProjectName) : null;
429
467
  if (explicitProjectName && !targetProject) {
430
468
  const err = new Error(formatUnknownProjectError(explicitProjectName, projects));
431
469
  err.statusCode = 400;
432
470
  throw err;
433
471
  }
472
+ if (!explicitProjectName) {
473
+ const prScope = shared.parsePrUrl(url)?.scope || '';
474
+ const matches = prScope ? projects.filter(p => shared.getProjectPrScope(p) === prScope) : [];
475
+ if (matches.length === 1) targetProject = matches[0];
476
+ }
434
477
  const prPath = targetProject ? shared.projectPrPath(targetProject) : path.join(MINIONS_DIR, 'pull-requests.json');
435
478
 
436
479
  const prNumMatch = url.match(/\/pull\/(\d+)|pullrequest\/(\d+)/);
@@ -1814,8 +1857,8 @@ function _extractActionsJson(segment) {
1814
1857
  return null;
1815
1858
  }
1816
1859
 
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']);
1860
+ const CC_DISPATCH_ACTION_ALIASES = new Set(['fix', 'explore', 'review', 'test', 'implement', 'implement:large', 'ask', 'verify']);
1861
+ const CC_ACTION_INTENT_WORK_TYPES = new Set(['fix', 'implement', 'implement:large', 'explore', 'review', 'test', 'ask', 'verify']);
1819
1862
 
1820
1863
  function normalizeCCAction(action) {
1821
1864
  if (!action || typeof action !== 'object') return action;
@@ -1830,7 +1873,19 @@ function normalizeCCAction(action) {
1830
1873
 
1831
1874
  function _ccCleanIntentString(value, max = 500) {
1832
1875
  if (typeof value !== 'string') return '';
1833
- return value.replace(/[\0\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, max);
1876
+ let out = '';
1877
+ let lastWasSpace = true;
1878
+ for (const ch of value) {
1879
+ const isSpace = ch === '\0' || ch === ' ' || ch === '\n' || ch === '\r' || ch === '\t';
1880
+ if (isSpace) {
1881
+ if (!lastWasSpace) out += ' ';
1882
+ lastWasSpace = true;
1883
+ } else {
1884
+ out += ch;
1885
+ lastWasSpace = false;
1886
+ }
1887
+ }
1888
+ return out.trim().slice(0, max);
1834
1889
  }
1835
1890
 
1836
1891
  function _ccNormalizeIntentMetadata(meta) {
@@ -1851,36 +1906,25 @@ function _ccNormalizeIntentMetadata(meta) {
1851
1906
  }
1852
1907
 
1853
1908
  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));
1909
+ const normalized = _normalizeIntentText(message);
1910
+ if (!normalized.trim()) return false;
1911
+ if (_messageHasDelegationIntent(message)) return true;
1912
+ if (_intentHasAnyToken(normalized, ['minions']) &&
1913
+ (_messageHasMediumLargeWorkIntent(message) || _messageRequestsPlanIntent(message))) return true;
1914
+ if (_intentHasAnyToken(normalized, ['agent', 'agents']) && _messageHasMediumLargeWorkIntent(message)) return true;
1915
+ return _intentHasVerbObject(normalized, ['create', 'make', 'draft', 'write'], ['plan']) &&
1916
+ _intentHasAnyToken(normalized, ['minion', 'minions']);
1863
1917
  }
1864
1918
 
1865
1919
  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)) {
1920
+ const normalized = _normalizeIntentText(message);
1921
+ if (!normalized.trim()) return null;
1922
+ if (source === 'doc-chat' && !_messageExplicitlyRequestsMinionsOrchestration(message)) return null;
1923
+ if (_messageRequestsPlanIntent(message)) {
1871
1924
  return { kind: 'plan' };
1872
1925
  }
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;
1926
+ if (!_messageHasDelegationIntent(message) && !_messageHasMediumLargeWorkIntent(message, { source })) return null;
1927
+ return { kind: 'work-item', workType: _inferDelegatedWorkType(message) };
1884
1928
  }
1885
1929
 
1886
1930
  function _ccInferMetadataActionIntent(meta) {
@@ -1892,52 +1936,64 @@ function _ccInferMetadataActionIntent(meta) {
1892
1936
  }
1893
1937
 
1894
1938
  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();
1939
+ const tokens = _intentTokens(_normalizeIntentText(_ccCleanIntentString(message, 500)));
1940
+ let start = 0;
1941
+ const skip = (terms) => {
1942
+ if (terms.includes(tokens[start])) {
1943
+ start++;
1944
+ return true;
1945
+ }
1946
+ return false;
1947
+ };
1948
+ skip(['please']);
1949
+ if (tokens[start] && tokens[start].startsWith('/')) start++;
1950
+ if (['dispatch', 'delegate', 'assign', 'queue', 'enqueue'].includes(tokens[start])) {
1951
+ start++;
1952
+ while (['a', 'an', 'the', 'work', 'item', 'task', 'agent', 'to'].includes(tokens[start])) start++;
1953
+ } else if (['create', 'open', 'file'].includes(tokens[start]) && tokens[start + 1] && ['a', 'an', 'the', 'work', 'item', 'task'].includes(tokens[start + 1])) {
1954
+ start++;
1955
+ while (['a', 'an', 'the', 'work', 'item', 'task', 'to', 'for'].includes(tokens[start])) start++;
1956
+ } else if (['have', 'ask', 'tell'].includes(tokens[start])) {
1957
+ start++;
1958
+ while (tokens[start] && !['to', 'fix', 'implement', 'review', 'test', 'explore', 'investigate', 'audit'].includes(tokens[start])) start++;
1959
+ if (tokens[start] === 'to') start++;
1960
+ }
1903
1961
  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, '');
1962
+ while (['make', 'create', 'draft', 'write', 'design', 'plan', 'out', 'a', 'an', 'the', 'for', 'to', 'about', 'how'].includes(tokens[start])) start++;
1906
1963
  } 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, '');
1964
+ 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
1965
  }
1918
- return text.replace(/^(?:for|to|of|on|about)\s+/i, '').trim();
1966
+ while (['for', 'to', 'of', 'on', 'about', 'a', 'an', 'the'].includes(tokens[start])) start++;
1967
+ return tokens.slice(start).join(' ');
1919
1968
  }
1920
1969
 
1921
1970
  function _ccIntentHasConcreteTarget(message, metadataTitle, intent) {
1922
1971
  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();
1972
+ const normalized = _normalizeIntentText(raw);
1973
+ const tokens = _intentTokens(normalized);
1974
+ if (raw.includes('http://') || raw.includes('https://')) return true;
1975
+ for (let i = 0; i < tokens.length; i++) {
1976
+ if (['pr', 'issue'].includes(tokens[i]) && tokens[i + 1] && _intentTokenIsNumeric(tokens[i + 1])) return true;
1977
+ if (tokens[i] === 'pull' && tokens[i + 1] === 'request' && tokens[i + 2] && _intentTokenIsNumeric(tokens[i + 2])) return true;
1978
+ if (tokens[i] === 'w' && tokens[i + 1] && tokens[i + 1].length >= 2) return true;
1979
+ }
1980
+ const target = _ccActionIntentTargetText(metadataTitle || raw, intent).trim().toLowerCase();
1928
1981
  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
- }
1982
+ const targetTokens = _intentTokens(_normalizeIntentText(target));
1983
+ 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']);
1984
+ if (targetTokens.length > 0 && targetTokens.every(token => generic.has(token))) return false;
1932
1985
  return target.length >= 3;
1933
1986
  }
1934
1987
 
1935
1988
  function _ccIntentTitle(message, metadataTitle, intent) {
1936
1989
  const metaTitle = _ccCleanIntentString(metadataTitle, 300);
1937
1990
  if (metaTitle && _ccIntentHasConcreteTarget(message, metaTitle, intent)) return metaTitle;
1938
- const cleaned = _ccCleanIntentString(message, 300).replace(/^\/plan\s+/i, 'plan ');
1991
+ const cleaned = _ccCleanIntentString(message, 300);
1939
1992
  if (!cleaned) return '';
1940
- return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
1993
+ const title = intent?.kind === 'plan'
1994
+ ? cleaned
1995
+ : _delegatedWorkTitle(cleaned, intent?.workType || 'ask');
1996
+ return title.charAt(0).toUpperCase() + title.slice(1);
1941
1997
  }
1942
1998
 
1943
1999
  function _ccFallbackMissingTargetError(intent) {
@@ -1945,17 +2001,20 @@ function _ccFallbackMissingTargetError(intent) {
1945
2001
  return `Missing target for ${label} request. Specify a concrete title, PR/work item, file, or feature to ${label}.`;
1946
2002
  }
1947
2003
 
1948
- function _actionsWithIntentFallback(actions, { message = '', intentMetadata = null, source = 'command-center' } = {}) {
1949
- const existing = Array.isArray(actions) ? actions : [];
2004
+ function _actionsWithIntentFallback(actions, opts = {}) {
2005
+ const { message = '', intentMetadata = null, source = 'command-center', filePath = null, title: docTitle = null, answerText = '', toolUses = [] } = opts;
2006
+ const existing = Array.isArray(actions) ? actions.map(normalizeCCAction) : [];
2007
+ if (_messageRequestsDirectHandling(message)) return existing.filter(a => normalizeCCAction(a)?.type !== 'dispatch');
2008
+ if (existing.some(a => normalizeCCAction(a)?.type === 'dispatch')) return existing;
1950
2009
  if (existing.length > 0) return existing;
1951
2010
  const meta = _ccNormalizeIntentMetadata(intentMetadata);
1952
2011
  const messageIntent = _ccInferMessageActionIntent(message, source);
1953
2012
  const metadataIntent = source === 'doc-chat' ? null : _ccInferMetadataActionIntent(meta);
1954
2013
  const intent = messageIntent || metadataIntent;
1955
- if (!intent) return existing;
2014
+ if (!intent) return _ensureDelegationForIntent(existing, { message, source, filePath, title: docTitle, answerText, toolUses });
1956
2015
 
1957
2016
  const title = _ccIntentTitle(message, meta.title, intent);
1958
- const hasTarget = _ccIntentHasConcreteTarget(message, meta.title || title, intent);
2017
+ const hasTarget = _ccIntentHasConcreteTarget(message, meta.title, intent);
1959
2018
  const description = meta.description || _ccCleanIntentString(message, 2000);
1960
2019
  const common = {
1961
2020
  ...(title ? { title } : {}),
@@ -2038,6 +2097,261 @@ function parseCCActions(text) {
2038
2097
  return result;
2039
2098
  }
2040
2099
 
2100
+ const DELEGATION_ACTION_TERMS = ['dispatch', 'delegate', 'assign', 'queue', 'enqueue'];
2101
+ 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'];
2102
+ const MEDIUM_INVESTIGATION_TERMS = ['audit', 'investigate', 'research', 'explore', 'analyze', 'analyse'];
2103
+ const MEDIUM_INVESTIGATION_PHRASES = ['deep dive', 'root cause'];
2104
+ const MEDIUM_IMPLEMENT_TERMS = ['implement', 'fix', 'debug', 'repair', 'refactor', 'migrate', 'harden', 'redesign'];
2105
+ const MEDIUM_BUILD_VERBS = ['build', 'add', 'create'];
2106
+ const MEDIUM_BUILD_OBJECTS = ['feature', 'support', 'endpoint', 'api', 'ui', 'page', 'component', 'flow', 'integration', 'automation', 'test', 'tests', 'tool', 'command', 'dashboard', 'service', 'module'];
2107
+ const MEDIUM_REVIEW_TERMS = ['review', 'test', 'verify', 'validate'];
2108
+ const MEDIUM_SIZE_TERMS = ['medium', 'large', 'larger'];
2109
+ const MEDIUM_SIZE_PHRASES = ['cross cutting', 'multi file', 'multi step', 'multi stage', 'multi module'];
2110
+ const PLAN_INTENT_TERMS = ['plan'];
2111
+ const PLAN_CREATE_TERMS = ['make', 'create', 'draft', 'write', 'design'];
2112
+ const PLAN_CREATE_PHRASES = ['come up with'];
2113
+ const DOC_CHAT_DIRECT_DOC_TERMS = ['summarize', 'summary', 'quote', 'extract', 'rewrite', 'reword', 'copyedit', 'proofread', 'format', 'typo'];
2114
+ const DOC_CHAT_DOC_ACTION_TERMS = ['edit', 'update', 'change', 'review', 'check'];
2115
+ const DOC_CHAT_DOC_OBJECTS = ['document', 'doc', 'paragraph', 'section', 'selection', 'text', 'wording'];
2116
+ const DIRECT_QUICK_TERMS = ['quick', 'simple', 'small'];
2117
+ const DIRECT_QUICK_OBJECTS = ['question', 'answer', 'lookup', 'check'];
2118
+ const DIRECT_REPLY_TERMS = ['answer', 'respond', 'reply'];
2119
+ const DIRECT_REPLY_TARGETS = ['directly', 'inline', 'here'];
2120
+ const DIRECT_SELF_TERMS = ['do', 'handle', 'answer', 'fix', 'edit', 'update', 'change', 'review', 'check'];
2121
+ const DIRECT_SELF_TARGETS = ['yourself', 'directly'];
2122
+ const DIRECT_HANDLING_PHRASES = [
2123
+ 'you do it yourself', 'you handle it yourself', 'you handle this yourself',
2124
+ 'cc do it yourself', 'doc chat do it yourself', 'doc chat handle it yourself',
2125
+ 'do not dispatch', 'dont dispatch', 'do not delegate', 'dont delegate',
2126
+ 'do not assign', 'dont assign', 'do not queue', 'dont queue',
2127
+ 'do not enqueue', 'dont enqueue', 'do not create work item', 'dont create work item',
2128
+ 'no dispatch', 'no delegate', 'no delegation', 'no work item',
2129
+ 'without dispatching', 'without delegating', 'without creating work item',
2130
+ ];
2131
+
2132
+ function _isIntentWordChar(ch) {
2133
+ const code = ch.charCodeAt(0);
2134
+ return (code >= 48 && code <= 57) || (code >= 97 && code <= 122);
2135
+ }
2136
+
2137
+ function _normalizeIntentText(message) {
2138
+ const raw = String(message || '').toLowerCase();
2139
+ let out = ' ';
2140
+ let lastWasSpace = true;
2141
+ for (const ch of raw) {
2142
+ if (ch === '\'' || ch === '\u2019') continue;
2143
+ if (_isIntentWordChar(ch)) {
2144
+ out += ch;
2145
+ lastWasSpace = false;
2146
+ } else if (!lastWasSpace) {
2147
+ out += ' ';
2148
+ lastWasSpace = true;
2149
+ }
2150
+ }
2151
+ return lastWasSpace ? out : out + ' ';
2152
+ }
2153
+
2154
+ function _intentTokens(normalized) {
2155
+ const text = String(normalized || '').trim();
2156
+ return text ? text.split(' ') : [];
2157
+ }
2158
+
2159
+ function _intentHasAnyToken(normalized, terms) {
2160
+ const tokens = new Set(_intentTokens(normalized));
2161
+ return terms.some(term => tokens.has(term));
2162
+ }
2163
+
2164
+ function _intentHasPhrase(normalized, phrase) {
2165
+ return String(normalized || '').includes(` ${phrase} `);
2166
+ }
2167
+
2168
+ function _intentHasAnyPhrase(normalized, phrases) {
2169
+ return phrases.some(phrase => _intentHasPhrase(normalized, phrase));
2170
+ }
2171
+
2172
+ function _intentTokenIsNumeric(token) {
2173
+ if (!token) return false;
2174
+ for (const ch of String(token)) {
2175
+ const code = ch.charCodeAt(0);
2176
+ if (code < 48 || code > 57) return false;
2177
+ }
2178
+ return true;
2179
+ }
2180
+
2181
+ function _intentHasVerbObject(normalized, verbs, objects) {
2182
+ const verbSet = new Set(verbs);
2183
+ const objectSet = new Set(objects);
2184
+ const filler = new Set(['a', 'an', 'the', 'this', 'that', 'it', 'new', 'proper', 'some']);
2185
+ const tokens = _intentTokens(normalized);
2186
+ for (let i = 0; i < tokens.length; i++) {
2187
+ if (!verbSet.has(tokens[i])) continue;
2188
+ for (let j = i + 1; j < tokens.length && j <= i + 4; j++) {
2189
+ if (filler.has(tokens[j])) continue;
2190
+ if (objectSet.has(tokens[j])) return true;
2191
+ break;
2192
+ }
2193
+ }
2194
+ return false;
2195
+ }
2196
+
2197
+ function _messageRequestsPlanIntent(message) {
2198
+ const normalized = _normalizeIntentText(message);
2199
+ const tokens = _intentTokens(normalized);
2200
+ if (!tokens.length) return false;
2201
+ if (tokens[0] === 'plan') return true;
2202
+ if (_intentHasAnyPhrase(normalized, PLAN_CREATE_PHRASES) && _intentHasAnyToken(normalized, PLAN_INTENT_TERMS)) return true;
2203
+ if (_intentHasVerbObject(normalized, PLAN_CREATE_TERMS, PLAN_INTENT_TERMS)) return true;
2204
+ return _intentHasAnyToken(normalized, PLAN_INTENT_TERMS) &&
2205
+ _intentHasAnyToken(normalized, ['this', 'that', 'it', 'for', 'how', 'out']);
2206
+ }
2207
+
2208
+ function _messageHasDelegationIntent(message) {
2209
+ const normalized = _normalizeIntentText(message);
2210
+ if (!normalized.trim()) return false;
2211
+ if (_intentHasAnyToken(normalized, DELEGATION_ACTION_TERMS)) return true;
2212
+ if (_intentHasPhrase(normalized, 'work item') && _intentHasAnyToken(normalized, ['create', 'open', 'add'])) return true;
2213
+ return _intentHasAnyPhrase(normalized, DELEGATION_MINIONS_PHRASES);
2214
+ }
2215
+
2216
+ function _messageRequestsDirectHandling(message) {
2217
+ const normalized = _normalizeIntentText(message);
2218
+ if (!normalized.trim()) return false;
2219
+ if (_intentHasAnyPhrase(normalized, DIRECT_HANDLING_PHRASES)) return true;
2220
+ if (_intentHasAnyToken(normalized, DIRECT_QUICK_TERMS) && _intentHasAnyToken(normalized, DIRECT_QUICK_OBJECTS)) return true;
2221
+ if (_intentHasVerbObject(normalized, DIRECT_REPLY_TERMS, DIRECT_REPLY_TARGETS)) return true;
2222
+ return _intentHasVerbObject(normalized, DIRECT_SELF_TERMS, DIRECT_SELF_TARGETS);
2223
+ }
2224
+
2225
+ function _messageIsSmallDocOnlyRequest(normalized) {
2226
+ if (_intentHasAnyToken(normalized, DOC_CHAT_DIRECT_DOC_TERMS)) return true;
2227
+ return _intentHasVerbObject(normalized, DOC_CHAT_DOC_ACTION_TERMS, DOC_CHAT_DOC_OBJECTS);
2228
+ }
2229
+
2230
+ function _messageHasMediumLargeWorkIntent(message, { source = 'command-center' } = {}) {
2231
+ const normalized = _normalizeIntentText(message);
2232
+ if (!normalized.trim()) return false;
2233
+ if (_messageRequestsDirectHandling(message)) return false;
2234
+ if (source === 'doc-chat' && _messageIsSmallDocOnlyRequest(normalized)) return false;
2235
+ return _intentHasAnyToken(normalized, MEDIUM_INVESTIGATION_TERMS) ||
2236
+ _intentHasAnyPhrase(normalized, MEDIUM_INVESTIGATION_PHRASES) ||
2237
+ _intentHasAnyToken(normalized, MEDIUM_IMPLEMENT_TERMS) ||
2238
+ _intentHasVerbObject(normalized, MEDIUM_BUILD_VERBS, MEDIUM_BUILD_OBJECTS) ||
2239
+ _intentHasAnyToken(normalized, MEDIUM_REVIEW_TERMS) ||
2240
+ _intentHasAnyToken(normalized, MEDIUM_SIZE_TERMS) ||
2241
+ _intentHasAnyPhrase(normalized, MEDIUM_SIZE_PHRASES);
2242
+ }
2243
+
2244
+ function _inferDelegatedWorkType(message) {
2245
+ const normalized = _normalizeIntentText(message);
2246
+ if (_intentHasAnyToken(normalized, ['test', 'verify', 'validate']) || _intentHasPhrase(normalized, 'build and test')) return 'test';
2247
+ if (_intentHasAnyToken(normalized, ['review']) || _intentHasPhrase(normalized, 'code review')) return 'review';
2248
+ if (_intentHasAnyToken(normalized, ['fix', 'debug', 'repair', 'bug', 'broken', 'failing', 'failure', 'regression']) || _intentHasPhrase(normalized, 'root cause')) return 'fix';
2249
+ if (_intentHasAnyToken(normalized, ['implement', 'refactor', 'migrate', 'harden', 'redesign']) ||
2250
+ _intentHasVerbObject(normalized, MEDIUM_BUILD_VERBS, MEDIUM_BUILD_OBJECTS)) return 'implement';
2251
+ if (_intentHasAnyToken(normalized, MEDIUM_INVESTIGATION_TERMS) || _intentHasAnyPhrase(normalized, MEDIUM_INVESTIGATION_PHRASES)) return 'explore';
2252
+ return 'ask';
2253
+ }
2254
+
2255
+ function _collapseWhitespace(text) {
2256
+ let out = '';
2257
+ let lastWasSpace = true;
2258
+ for (const ch of String(text || '')) {
2259
+ const isSpace = ch === ' ' || ch === '\n' || ch === '\r' || ch === '\t';
2260
+ if (isSpace) {
2261
+ if (!lastWasSpace) out += ' ';
2262
+ lastWasSpace = true;
2263
+ } else {
2264
+ out += ch;
2265
+ lastWasSpace = false;
2266
+ }
2267
+ }
2268
+ return out.trim();
2269
+ }
2270
+
2271
+ function _stripEdgeQuotes(text) {
2272
+ const quotes = new Set(['"', "'", '`']);
2273
+ let start = 0;
2274
+ let end = String(text || '').length;
2275
+ while (start < end && quotes.has(text[start])) start++;
2276
+ while (end > start && quotes.has(text[end - 1])) end--;
2277
+ return text.slice(start, end);
2278
+ }
2279
+
2280
+ function _delegatedWorkTitle(message, workType) {
2281
+ const compact = _stripEdgeQuotes(_collapseWhitespace(String(message || '')));
2282
+ const trimmed = compact.length > 90 ? compact.slice(0, 87).trimEnd() + '...' : compact;
2283
+ const fallback = {
2284
+ fix: 'Fix requested issue',
2285
+ review: 'Review requested work',
2286
+ test: 'Test requested work',
2287
+ implement: 'Implement requested work',
2288
+ 'implement:large': 'Implement requested work',
2289
+ explore: 'Investigate requested work',
2290
+ ask: 'Answer requested query',
2291
+ verify: 'Verify requested work',
2292
+ }[workType] || 'Handle requested work';
2293
+ if (!trimmed) return fallback;
2294
+ const prefix = {
2295
+ fix: 'Fix',
2296
+ review: 'Review',
2297
+ test: 'Test',
2298
+ implement: 'Implement',
2299
+ 'implement:large': 'Implement',
2300
+ explore: 'Investigate',
2301
+ ask: 'Answer',
2302
+ verify: 'Verify',
2303
+ }[workType] || 'Handle';
2304
+ const lower = trimmed.toLowerCase();
2305
+ const prefixLower = prefix.toLowerCase();
2306
+ if (lower === prefixLower || lower.startsWith(prefixLower + ' ')) return trimmed;
2307
+ return `${prefix}: ${trimmed}`;
2308
+ }
2309
+
2310
+ function _priorityFromDelegationMessage(message) {
2311
+ const normalized = _normalizeIntentText(message);
2312
+ if (_intentHasAnyToken(normalized, ['urgent', 'asap', 'critical', 'blocker', 'p0', 'p1']) || _intentHasPhrase(normalized, 'high priority')) return 'high';
2313
+ if (_intentHasPhrase(normalized, 'low priority') || _intentHasPhrase(normalized, 'when you can') || _intentHasAnyToken(normalized, ['whenever'])) return 'low';
2314
+ return 'medium';
2315
+ }
2316
+
2317
+ function _inferDelegationActionFromMessage(message, { source = 'command-center', filePath = null, title = null, answerText = '', toolUses = [] } = {}) {
2318
+ if (_messageRequestsDirectHandling(message)) return null;
2319
+ const explicit = _messageHasDelegationIntent(message);
2320
+ const mediumLarge = _messageHasMediumLargeWorkIntent(message, { source });
2321
+ const toolCount = Array.isArray(toolUses) ? toolUses.length : 0;
2322
+ if (!explicit && !mediumLarge && toolCount < 4) return null;
2323
+
2324
+ const workType = _inferDelegatedWorkType(message);
2325
+ const inferredProject = source === 'doc-chat' && filePath && typeof _inferDocChatProject === 'function'
2326
+ ? _inferDocChatProject(filePath)
2327
+ : null;
2328
+ const context = [];
2329
+ 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.`);
2330
+ if (filePath) context.push(`Document: ${filePath}`);
2331
+ if (title && title !== filePath) context.push(`Title: ${title}`);
2332
+ if (answerText && String(answerText).trim()) context.push(`Assistant draft before delegation:\n${String(answerText).trim().slice(0, 1200)}`);
2333
+ context.push(`Original request:\n${String(message || '').trim()}`);
2334
+
2335
+ return {
2336
+ type: 'dispatch',
2337
+ title: _delegatedWorkTitle(message, workType),
2338
+ workType,
2339
+ priority: _priorityFromDelegationMessage(message),
2340
+ description: context.join('\n\n'),
2341
+ ...(inferredProject ? { project: inferredProject } : {}),
2342
+ _autoDelegated: true,
2343
+ };
2344
+ }
2345
+
2346
+ function _ensureDelegationForIntent(actions, opts = {}) {
2347
+ const list = Array.isArray(actions) ? actions.map(normalizeCCAction) : [];
2348
+ if (_messageRequestsDirectHandling(opts.message)) return list.filter(a => normalizeCCAction(a)?.type !== 'dispatch');
2349
+ if (list.some(a => normalizeCCAction(a)?.type === 'dispatch')) return list;
2350
+ if (list.length > 0) return list;
2351
+ const inferred = _inferDelegationActionFromMessage(opts.message, opts);
2352
+ return inferred ? [...list, inferred] : list;
2353
+ }
2354
+
2041
2355
  function stripCCActionSyntax(text) {
2042
2356
  if (!text) return '';
2043
2357
  let displayText = text;
@@ -2414,9 +2728,10 @@ function createPipelineFromAction(action) {
2414
2728
  // before `try` to avoid filling `results` with cryptic per-handler error messages.
2415
2729
  function _ccValidateAction(action) {
2416
2730
  if (!action || typeof action !== 'object' || !action.type) return 'action is missing required field: type';
2417
- switch (action.type) {
2418
- case 'dispatch': case 'fix': case 'explore': case 'review': case 'test':
2419
- if (!action.title || typeof action.title !== 'string' || !action.title.trim()) return `${action.type} action missing required field: title`;
2731
+ const normalized = normalizeCCAction(action);
2732
+ switch (normalized.type) {
2733
+ case 'dispatch':
2734
+ if (!normalized.title || typeof normalized.title !== 'string' || !normalized.title.trim()) return `${action.type} action missing required field: title`;
2420
2735
  return null;
2421
2736
  case 'implement':
2422
2737
  return 'Unsupported action type "implement"; use type="dispatch" with workType="implement".';
@@ -2760,7 +3075,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
2760
3075
  }
2761
3076
  try {
2762
3077
  switch (action.type) {
2763
- case 'dispatch': case 'fix': case 'explore': case 'review': case 'test': {
3078
+ case 'dispatch': {
2764
3079
  const workType = routing.normalizeWorkType(action.workType || (action.type !== 'dispatch' ? action.type : WORK_TYPE.IMPLEMENT), WORK_TYPE.IMPLEMENT);
2765
3080
  const id = 'W-' + shared.uid();
2766
3081
  const project = action.project || '';
@@ -2827,7 +3142,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
2827
3142
 
2828
3143
  // Issue #1772: CC review/explore/test are human-initiated one-offs.
2829
3144
  // Mark oneShot so any discovered PR is tagged _contextOnly (skips eval loop).
2830
- const ccOneShotTypes = new Set(['review', 'explore', 'test']);
3145
+ const ccOneShotTypes = new Set([WORK_TYPE.REVIEW, WORK_TYPE.EXPLORE, WORK_TYPE.TEST, WORK_TYPE.ASK, WORK_TYPE.VERIFY]);
2831
3146
  const isOneShot = action.oneShot === true || (action.oneShot !== false && ccOneShotTypes.has(workType));
2832
3147
  const item = {
2833
3148
  id, title: action.title.trim(), type: workType,
@@ -3054,7 +3369,7 @@ async function executeDocChatActions(actions, { filePath = null } = {}) {
3054
3369
  return executeCCActions(actions, { source: 'doc-chat', inferredProject: _inferDocChatProject(filePath) });
3055
3370
  }
3056
3371
 
3057
- const DOC_CHAT_WORK_ITEM_ACTION_TYPES = new Set(['dispatch', 'fix', 'explore', 'review', 'test']);
3372
+ const DOC_CHAT_WORK_ITEM_ACTION_TYPES = new Set(['dispatch', 'fix', 'implement', 'implement:large', 'explore', 'review', 'test', 'ask', 'verify']);
3058
3373
 
3059
3374
  function _buildDocChatActionFeedback(actions, actionResults) {
3060
3375
  if (!Array.isArray(actions) || !Array.isArray(actionResults)) return [];
@@ -3855,8 +4170,9 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
3855
4170
  timeout: DOC_CHAT_TIMEOUT_MS,
3856
4171
  // Match Command Center's full tool surface and turn budget so doc-chat
3857
4172
  // can take action (read/write/edit/dispatch) instead of being limited
3858
- // to Q&A. The doc-chat sysprompt still scopes orchestration to explicit
3859
- // requests, and ---DOCUMENT--- remains the only document edit channel.
4173
+ // to Q&A. The doc-chat sysprompt scopes orchestration to human message
4174
+ // intent (explicit dispatch or medium/larger work), and ---DOCUMENT---
4175
+ // remains the only whole-file document edit channel.
3860
4176
  allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
3861
4177
  systemPrompt: DOC_CHAT_SYSTEM_PROMPT,
3862
4178
  transcript,
@@ -4750,6 +5066,8 @@ const server = http.createServer(async (req, res) => {
4750
5066
  try {
4751
5067
  const body = await readBody(req);
4752
5068
  if (!body.title || !body.title.trim()) return jsonReply(res, 400, { error: 'title is required' });
5069
+ const target = resolveWorkItemsCreateTarget(body.project);
5070
+ if (target.error) return jsonReply(res, 400, { error: target.error });
4753
5071
  // Write as a work item with type 'plan' — user must explicitly execute plan-to-prd after reviewing
4754
5072
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
4755
5073
  const id = 'W-' + shared.uid();
@@ -4759,7 +5077,7 @@ const server = http.createServer(async (req, res) => {
4759
5077
  status: WI_STATUS.PENDING, created: new Date().toISOString(), createdBy: 'dashboard',
4760
5078
  branchStrategy: body.branch_strategy || body.branchStrategy || 'parallel',
4761
5079
  };
4762
- if (body.project) item.project = body.project;
5080
+ if (target.project) item.project = target.project.name;
4763
5081
  if (body.agent) item.agent = body.agent;
4764
5082
  mutateWorkItems(wiPath, items => { items.push(item); });
4765
5083
  return jsonReply(res, 200, { ok: true, id, agent: body.agent || '' });
@@ -4770,6 +5088,8 @@ const server = http.createServer(async (req, res) => {
4770
5088
  try {
4771
5089
  const body = await readBody(req);
4772
5090
  if (!body.name || !body.name.trim()) return jsonReply(res, 400, { error: 'name is required' });
5091
+ const target = resolveWorkItemsCreateTarget(body.project);
5092
+ if (target.error) return jsonReply(res, 400, { error: target.error });
4773
5093
 
4774
5094
  if (!fs.existsSync(PRD_DIR)) fs.mkdirSync(PRD_DIR, { recursive: true });
4775
5095
 
@@ -4777,7 +5097,7 @@ const server = http.createServer(async (req, res) => {
4777
5097
  const planFile = 'manual-' + shared.uid() + '.json';
4778
5098
  const plan = {
4779
5099
  version: 'manual-' + new Date().toISOString().slice(0, 10),
4780
- project: body.project || (PROJECTS.length > 0 ? PROJECTS[0].name : 'Unknown'),
5100
+ project: target.project?.name || (PROJECTS.length > 0 ? PROJECTS[0].name : 'Unknown'),
4781
5101
  generated_by: 'dashboard',
4782
5102
  generated_at: new Date().toISOString().slice(0, 10),
4783
5103
  plan_summary: body.name,
@@ -6088,15 +6408,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6088
6408
  transcript: body.transcript,
6089
6409
  onAbortReady: (abort) => { _docAbort = abort; },
6090
6410
  });
6091
- actions = _actionsWithIntentFallback(actions, { message: body.message, source: 'doc-chat' });
6092
- const actionResults = await executeDocChatActions(actions, { filePath: body.filePath });
6093
- const actionFeedback = _buildDocChatActionFeedback(actions, actionResults);
6411
+ const delegatedActions = _ensureDelegationForIntent(actions, {
6412
+ message: body.message, source: 'doc-chat', filePath: body.filePath, title: body.title, answerText: answer, toolUses,
6413
+ });
6414
+ const actionResults = await executeDocChatActions(delegatedActions, { filePath: body.filePath });
6415
+ const actionFeedback = _buildDocChatActionFeedback(delegatedActions, actionResults);
6094
6416
  const finalize = _finalizeDocChatEdit({
6095
6417
  filePath: body.filePath, fullPath, isJson, canEdit,
6096
6418
  originalContent: currentContent, delimiterContent: content,
6097
6419
  });
6098
6420
  const payload = _buildDocChatResponsePayload({
6099
- answer, actions, actionResults, actionFeedback, actionParseError, ccError, partial, warning, toolUses, finalize,
6421
+ answer, actions: delegatedActions, actionResults, actionFeedback, actionParseError, ccError, partial, warning, toolUses, finalize,
6100
6422
  });
6101
6423
  _docDone = true;
6102
6424
  return jsonReply(res, 200, payload);
@@ -6184,15 +6506,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6184
6506
  onToolUse: (name, input) => { writeDocEvent({ type: 'tool', name, input: _lightToolInput(input) }); },
6185
6507
  onRetry: (attempt) => { writeDocEvent({ type: 'progress', attempt }); },
6186
6508
  });
6187
- actions = _actionsWithIntentFallback(actions, { message: body.message, source: 'doc-chat' });
6188
- const actionResults = await executeDocChatActions(actions, { filePath: body.filePath });
6189
- const actionFeedback = _buildDocChatActionFeedback(actions, actionResults);
6509
+ const delegatedActions = _ensureDelegationForIntent(actions, {
6510
+ message: body.message, source: 'doc-chat', filePath: body.filePath, title: body.title, answerText: answer, toolUses,
6511
+ });
6512
+ const actionResults = await executeDocChatActions(delegatedActions, { filePath: body.filePath });
6513
+ const actionFeedback = _buildDocChatActionFeedback(delegatedActions, actionResults);
6190
6514
  const finalize = _finalizeDocChatEdit({
6191
6515
  filePath: body.filePath, fullPath, isJson, canEdit,
6192
6516
  originalContent: currentContent, delimiterContent: content,
6193
6517
  });
6194
6518
  const payload = _buildDocChatResponsePayload({
6195
- answer, actions, actionResults, actionFeedback, actionParseError, ccError, partial, warning, toolUses, finalize,
6519
+ answer, actions: delegatedActions, actionResults, actionFeedback, actionParseError, ccError, partial, warning, toolUses, finalize,
6196
6520
  });
6197
6521
  const { answer: finalAnswer, ...donePayload } = payload;
6198
6522
  writeDocEvent({
@@ -6713,13 +7037,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6713
7037
  });
6714
7038
  }
6715
7039
 
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
7040
  const toolUses = Array.isArray(result.toolUses) ? result.toolUses : _extractToolUsesFromRaw(result.raw);
7041
+ const parsed = parseCCActions(result.text);
6723
7042
  // Safety net: detect /loop invocation and convert to create-watch
6724
7043
  const _loopWatch = _detectLoopInvocation(parsed.text, parsed.actions, toolUses, body.message);
6725
7044
  if (_loopWatch) {
@@ -6727,7 +7046,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6727
7046
  console.warn('[CC] /loop invocation detected — converted to create-watch');
6728
7047
  try { shared.log('warn', '/loop invocation detected in CC response — auto-converted to create-watch'); } catch {}
6729
7048
  }
6730
- parsed.actions = _filterImplicitPostDispatchActions(parsed.actions, body.message);
7049
+ parsed.actions = _actionsWithIntentFallback(
7050
+ _filterImplicitPostDispatchActions(parsed.actions, body.message),
7051
+ { message: body.message, intentMetadata: body.intentMetadata, source: 'command-center', answerText: parsed.text, toolUses }
7052
+ );
6731
7053
  if (parsed.actions.length > 0) {
6732
7054
  parsed.actionResults = await executeCCActions(parsed.actions);
6733
7055
  }
@@ -7048,11 +7370,6 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7048
7370
 
7049
7371
  // Send final result with actions — execute server-side first
7050
7372
  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
7373
  // Safety net: detect /loop invocation and convert to create-watch
7057
7374
  const _loopWatch = _detectLoopInvocation(displayText, actions, toolUses, body.message);
7058
7375
  if (_loopWatch) {
@@ -7060,7 +7377,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7060
7377
  console.warn('[CC] /loop invocation detected — converted to create-watch');
7061
7378
  try { shared.log('warn', '/loop invocation detected in CC response — auto-converted to create-watch'); } catch {}
7062
7379
  }
7063
- actions = _filterImplicitPostDispatchActions(actions, body.message);
7380
+ actions = _actionsWithIntentFallback(
7381
+ _filterImplicitPostDispatchActions(actions, body.message),
7382
+ { message: body.message, intentMetadata: body.intentMetadata, source: 'command-center', answerText: displayText, toolUses }
7383
+ );
7064
7384
  let actionResults;
7065
7385
  if (actions.length > 0) {
7066
7386
  actionResults = await executeCCActions(actions);
@@ -7124,6 +7444,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7124
7444
  const body = await readBody(req);
7125
7445
  let { id, cron, title, type, project, agent, description, priority, enabled } = body;
7126
7446
  if (!cron || !title) return jsonReply(res, 400, { error: 'cron and title are required' });
7447
+ const projectTarget = shared.resolveConfiguredProject(project, PROJECTS);
7448
+ if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
7449
+ project = projectTarget.project?.name || null;
7127
7450
 
7128
7451
  // Auto-generate ID from title if not provided
7129
7452
  if (!id) {
@@ -7156,8 +7479,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7156
7479
 
7157
7480
  async function handleSchedulesUpdate(req, res) {
7158
7481
  const body = await readBody(req);
7159
- const { id, cron, title, type, project, agent, description, priority, enabled } = body;
7482
+ let { id, cron, title, type, project, agent, description, priority, enabled } = body;
7160
7483
  if (!id) return jsonReply(res, 400, { error: 'id required' });
7484
+ if (project !== undefined) {
7485
+ const projectTarget = shared.resolveConfiguredProject(project, PROJECTS);
7486
+ if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
7487
+ project = projectTarget.project?.name || null;
7488
+ }
7161
7489
 
7162
7490
  let missingSchedules = false;
7163
7491
  let sched = null;
@@ -7218,6 +7546,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7218
7546
  reloadConfig();
7219
7547
  const sched = (CONFIG.schedules || []).find(s => s.id === id);
7220
7548
  if (!sched) return jsonReply(res, 404, { error: 'Schedule not found' });
7549
+ if (sched.project) {
7550
+ const projectTarget = shared.resolveConfiguredProject(sched.project, PROJECTS);
7551
+ if (projectTarget.error) return jsonReply(res, 400, { error: projectTarget.error });
7552
+ sched.project = projectTarget.project.name;
7553
+ }
7221
7554
 
7222
7555
  const schedulerMod = require('./engine/scheduler');
7223
7556
  let item;
@@ -8325,8 +8658,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
8325
8658
  const { savePipeline, getPipeline } = require('./engine/pipeline');
8326
8659
  if (getPipeline(body.id)) return jsonReply(res, 409, { error: 'Pipeline already exists' });
8327
8660
  const pipeline = { id: body.id, title: body.title, stages: body.stages, trigger: body.trigger || {}, enabled: body.enabled !== false };
8661
+ if (body.project !== undefined) pipeline.project = body.project;
8662
+ if (Array.isArray(body.projects)) pipeline.projects = body.projects;
8328
8663
  if (body.stopWhen) pipeline.stopWhen = body.stopWhen;
8329
8664
  if (Array.isArray(body.monitoredResources) && body.monitoredResources.length > 0) pipeline.monitoredResources = body.monitoredResources;
8665
+ const projectError = validatePipelineProjects(pipeline);
8666
+ if (projectError) return jsonReply(res, 400, { error: projectError });
8330
8667
  savePipeline(pipeline);
8331
8668
  invalidateStatusCache();
8332
8669
  return jsonReply(res, 200, { ok: true, id: pipeline.id });
@@ -8341,8 +8678,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
8341
8678
  if (body.stages !== undefined) pipeline.stages = body.stages;
8342
8679
  if (body.trigger !== undefined) pipeline.trigger = body.trigger;
8343
8680
  if (body.enabled !== undefined) pipeline.enabled = body.enabled;
8681
+ if (body.project !== undefined) pipeline.project = body.project;
8682
+ if (body.projects !== undefined) pipeline.projects = body.projects;
8344
8683
  if (body.monitoredResources !== undefined) pipeline.monitoredResources = body.monitoredResources;
8345
8684
  if (body.stopWhen !== undefined) pipeline.stopWhen = body.stopWhen;
8685
+ const projectError = validatePipelineProjects(pipeline);
8686
+ if (projectError) return jsonReply(res, 400, { error: projectError });
8346
8687
  savePipeline(pipeline);
8347
8688
  invalidateStatusCache();
8348
8689
  return jsonReply(res, 200, { ok: true });
@@ -8686,6 +9027,11 @@ module.exports = {
8686
9027
  _shouldSuppressDocChatPostPatchError,
8687
9028
  _buildDocChatResponsePayload,
8688
9029
  _inferDocChatProject,
9030
+ _messageHasDelegationIntent,
9031
+ _messageRequestsDirectHandling,
9032
+ _messageHasMediumLargeWorkIntent,
9033
+ _inferDelegationActionFromMessage,
9034
+ _ensureDelegationForIntent,
8689
9035
  _linkPullRequestForTracking: linkPullRequestForTracking,
8690
9036
  _resolveSkillReadPath,
8691
9037
  DOC_CHAT_DOCUMENT_DELIMITER,