@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/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 || []).find(p => p.name === update.name);
134
- const currentProject = current.projects.find(p => p.name === update.name);
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 = PROJECTS.find(p => p?.name === projectName);
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
- const resolvedWiPath = path.resolve(wiPath);
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
- const known = projects.map(p => p.name).filter(Boolean).join(', ') || '(none configured)';
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
- const name = String(projectName || '').trim().toLowerCase();
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 project = String(projectName || '').trim();
384
- let targetProject = null;
385
- if (project) {
386
- targetProject = findProjectByName(projects, project);
387
- if (!targetProject) return { error: formatUnknownProjectError(project, projects) };
388
- } else if (projects.length === 1) {
389
- targetProject = projects[0];
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
- return {
392
- project: targetProject,
393
- wiPath: targetProject ? shared.projectWorkItemsPath(targetProject) : path.join(MINIONS_DIR, 'work-items.json'),
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 targetProject = explicitProjectName ? findProjectByName(projects, explicitProjectName) : (projects[0] || null);
429
- if (explicitProjectName && !targetProject) {
430
- const err = new Error(formatUnknownProjectError(explicitProjectName, projects));
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
- const prPath = targetProject ? shared.projectPrPath(targetProject) : path.join(MINIONS_DIR, 'pull-requests.json');
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
- return value.replace(/[\0\r\n]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, max);
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 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));
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 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)) {
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
- 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;
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
- 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();
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
- 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, '');
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
- 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, '');
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
- return text.replace(/^(?:for|to|of|on|about)\s+/i, '').trim();
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
- 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();
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
- 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
- }
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).replace(/^\/plan\s+/i, 'plan ');
2012
+ const cleaned = _ccCleanIntentString(message, 300);
1939
2013
  if (!cleaned) return '';
1940
- return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
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, { message = '', intentMetadata = null, source = 'command-center' } = {}) {
1949
- const existing = Array.isArray(actions) ? actions : [];
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 || title, intent);
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
- 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`;
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': case 'fix': case 'explore': case 'review': case 'test': {
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
- targetProject = PROJECTS.find(p => p.name?.toLowerCase() === project.toLowerCase());
2777
- if (!targetProject) {
2778
- const known = PROJECTS.map(p => p.name).join(', ') || '(none configured)';
2779
- results.push({ type: action.type, error: `Project "${project}" not found. Known projects: ${known}` });
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 = PROJECTS.find(p => p.name?.toLowerCase() === String(linkedPr._project).toLowerCase()) || null;
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 = PROJECTS.find(p => p.name?.toLowerCase() === inferredProject.toLowerCase()) || null;
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) : path.join(MINIONS_DIR, 'work-items.json');
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(['review', 'explore', 'test']);
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
- ? PROJECTS.find(p => p.name?.toLowerCase() === String(projectName).toLowerCase())
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
- targetProject = PROJECTS.find(p => p.name?.toLowerCase() === project.toLowerCase());
2937
- if (!targetProject) {
2938
- const known = PROJECTS.map(p => p.name).join(', ') || '(none configured)';
2939
- results.push({ type: 'reopen-work-item', id: action.id, error: `Project "${project}" not found. Known projects: ${known}` });
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 = PROJECTS.find(p => p.name?.toLowerCase() === inferredProject.toLowerCase()) || null;
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) : path.join(MINIONS_DIR, 'work-items.json');
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 = PROJECTS.find(p => p.name?.toLowerCase() === m[1].toLowerCase());
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 still scopes orchestration to explicit
3859
- // requests, and ---DOCUMENT--- remains the only document edit channel.
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
- const project = PROJECTS.find(p => {
4219
- // safeJsonNoRestore never resurrect an archived PRD's .backup
4220
- // sidecar during project resolution (W-mouptdh1000h9f39). The
4221
- // active-from-archive write above (mutateJsonFileLocked) already
4222
- // created activePath when needed.
4223
- const plan = safeJsonNoRestore(activePath) || safeJsonNoRestore(prdPath);
4224
- return plan && p.name?.toLowerCase() === (plan.project || '').toLowerCase();
4225
- }) || PROJECTS[0] || null;
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 wiPath;
4280
- if (source && source !== 'central') {
4281
- const proj = PROJECTS.find(p => p.name === source);
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 proj = PROJECTS.find(p => p.name?.toLowerCase() === projName.toLowerCase());
4317
- const targetWiPath = proj ? shared.projectWorkItemsPath(proj) : path.join(MINIONS_DIR, 'work-items.json');
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 sourcePrefix = proj ? `work-${proj.name}-` : 'central-work-';
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 sourcePrefix = (!source || source === 'central') ? 'central-work-' : `work-${source}-`;
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
- // Find the right work-items file
4425
- let wiPath;
4426
- if (!source || source === 'central') {
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
- // Find the right work-items file
4485
- let wiPath;
4486
- if (!source || source === 'central') {
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
- let wiPath;
4541
- if (!source || source === 'central') {
4542
- wiPath = path.join(MINIONS_DIR, 'work-items.json');
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 = (!source || source === 'central') ? 'central-work-' : `work-${source}-`;
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
- // Find the right work-items file
4598
- let wiPath;
4599
- if (!project || project === 'central') {
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 sourcePrefix = (!project || project === 'central') ? 'central-work-' : `work-${project}-`;
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
- let wiPath;
4693
- if (!source || source === 'central') {
4694
- wiPath = path.join(MINIONS_DIR, 'work-items.json');
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 (body.project) item.project = body.project;
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: body.project || (PROJECTS.length > 0 ? PROJECTS[0].name : 'Unknown'),
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 targetProject = PROJECTS.find(p => p.name?.toLowerCase() === projectName.toLowerCase()) || PROJECTS[0];
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
- actions = _actionsWithIntentFallback(actions, { message: body.message, source: 'doc-chat' });
6092
- const actionResults = await executeDocChatActions(actions, { filePath: body.filePath });
6093
- const actionFeedback = _buildDocChatActionFeedback(actions, actionResults);
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
- actions = _actionsWithIntentFallback(actions, { message: body.message, source: 'doc-chat' });
6188
- const actionResults = await executeDocChatActions(actions, { filePath: body.filePath });
6189
- const actionFeedback = _buildDocChatActionFeedback(actions, actionResults);
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 => path.resolve(p.localPath) === target);
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 => path.resolve(p.localPath) === target)) {
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 = _filterImplicitPostDispatchActions(parsed.actions, body.message);
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 = _filterImplicitPostDispatchActions(actions, body.message);
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
- const { id, cron, title, type, project, agent, description, priority, enabled } = body;
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.find(p => p.name === update.name);
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,