@yemi33/minions 0.1.1808 → 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,7 +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']);
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']);
1818
1862
 
1819
1863
  function normalizeCCAction(action) {
1820
1864
  if (!action || typeof action !== 'object') return action;
@@ -1827,6 +1871,177 @@ function normalizeCCAction(action) {
1827
1871
  return { ...action, type: 'dispatch', workType: action.workType || type };
1828
1872
  }
1829
1873
 
1874
+ function _ccCleanIntentString(value, max = 500) {
1875
+ if (typeof value !== 'string') return '';
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);
1889
+ }
1890
+
1891
+ function _ccNormalizeIntentMetadata(meta) {
1892
+ if (!meta || typeof meta !== 'object' || Array.isArray(meta)) return {};
1893
+ const out = {};
1894
+ for (const key of ['intent', 'type', 'title', 'description', 'priority', 'project', 'branchStrategy', 'branch_strategy']) {
1895
+ const value = _ccCleanIntentString(meta[key], key === 'description' ? 2000 : 300);
1896
+ if (value) out[key] = value;
1897
+ }
1898
+ if (Array.isArray(meta.agents)) {
1899
+ out.agents = meta.agents.map(a => _ccCleanIntentString(a, 80)).filter(Boolean);
1900
+ }
1901
+ if (Array.isArray(meta.projects)) {
1902
+ out.projects = meta.projects.map(p => _ccCleanIntentString(p, 120)).filter(Boolean);
1903
+ }
1904
+ if (meta.fanout === true) out.fanout = true;
1905
+ return out;
1906
+ }
1907
+
1908
+ function _messageExplicitlyRequestsMinionsOrchestration(message) {
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']);
1917
+ }
1918
+
1919
+ function _ccInferMessageActionIntent(message, source = 'command-center') {
1920
+ const normalized = _normalizeIntentText(message);
1921
+ if (!normalized.trim()) return null;
1922
+ if (source === 'doc-chat' && !_messageExplicitlyRequestsMinionsOrchestration(message)) return null;
1923
+ if (_messageRequestsPlanIntent(message)) {
1924
+ return { kind: 'plan' };
1925
+ }
1926
+ if (!_messageHasDelegationIntent(message) && !_messageHasMediumLargeWorkIntent(message, { source })) return null;
1927
+ return { kind: 'work-item', workType: _inferDelegatedWorkType(message) };
1928
+ }
1929
+
1930
+ function _ccInferMetadataActionIntent(meta) {
1931
+ if (meta.intent === 'plan') return { kind: 'plan' };
1932
+ if (meta.intent === 'work-item' && CC_ACTION_INTENT_WORK_TYPES.has(String(meta.type || '').toLowerCase())) {
1933
+ return { kind: 'work-item', workType: String(meta.type).toLowerCase() };
1934
+ }
1935
+ return null;
1936
+ }
1937
+
1938
+ function _ccActionIntentTargetText(message, intent) {
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
+ }
1961
+ if (intent?.kind === 'plan') {
1962
+ while (['make', 'create', 'draft', 'write', 'design', 'plan', 'out', 'a', 'an', 'the', 'for', 'to', 'about', 'how'].includes(tokens[start])) start++;
1963
+ } else if (intent?.workType) {
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++;
1965
+ }
1966
+ while (['for', 'to', 'of', 'on', 'about', 'a', 'an', 'the'].includes(tokens[start])) start++;
1967
+ return tokens.slice(start).join(' ');
1968
+ }
1969
+
1970
+ function _ccIntentHasConcreteTarget(message, metadataTitle, intent) {
1971
+ const raw = String(message || '');
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();
1981
+ if (!target) return false;
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;
1985
+ return target.length >= 3;
1986
+ }
1987
+
1988
+ function _ccIntentTitle(message, metadataTitle, intent) {
1989
+ const metaTitle = _ccCleanIntentString(metadataTitle, 300);
1990
+ if (metaTitle && _ccIntentHasConcreteTarget(message, metaTitle, intent)) return metaTitle;
1991
+ const cleaned = _ccCleanIntentString(message, 300);
1992
+ if (!cleaned) return '';
1993
+ const title = intent?.kind === 'plan'
1994
+ ? cleaned
1995
+ : _delegatedWorkTitle(cleaned, intent?.workType || 'ask');
1996
+ return title.charAt(0).toUpperCase() + title.slice(1);
1997
+ }
1998
+
1999
+ function _ccFallbackMissingTargetError(intent) {
2000
+ const label = intent?.kind === 'plan' ? 'plan' : (intent?.workType || 'dispatch');
2001
+ return `Missing target for ${label} request. Specify a concrete title, PR/work item, file, or feature to ${label}.`;
2002
+ }
2003
+
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;
2009
+ if (existing.length > 0) return existing;
2010
+ const meta = _ccNormalizeIntentMetadata(intentMetadata);
2011
+ const messageIntent = _ccInferMessageActionIntent(message, source);
2012
+ const metadataIntent = source === 'doc-chat' ? null : _ccInferMetadataActionIntent(meta);
2013
+ const intent = messageIntent || metadataIntent;
2014
+ if (!intent) return _ensureDelegationForIntent(existing, { message, source, filePath, title: docTitle, answerText, toolUses });
2015
+
2016
+ const title = _ccIntentTitle(message, meta.title, intent);
2017
+ const hasTarget = _ccIntentHasConcreteTarget(message, meta.title, intent);
2018
+ const description = meta.description || _ccCleanIntentString(message, 2000);
2019
+ const common = {
2020
+ ...(title ? { title } : {}),
2021
+ ...(description ? { description } : {}),
2022
+ ...(meta.priority ? { priority: meta.priority } : {}),
2023
+ ...(meta.project ? { project: meta.project } : {}),
2024
+ ...(Array.isArray(meta.agents) && meta.agents.length ? { agents: meta.agents } : {}),
2025
+ ...(meta.fanout ? { scope: 'fan-out' } : {}),
2026
+ };
2027
+ if (intent.kind === 'plan') {
2028
+ const action = {
2029
+ type: 'plan',
2030
+ ...common,
2031
+ branchStrategy: meta.branchStrategy || meta.branch_strategy || 'parallel',
2032
+ };
2033
+ if (!hasTarget) action._intentFallbackError = _ccFallbackMissingTargetError(intent);
2034
+ return [action];
2035
+ }
2036
+ const action = {
2037
+ type: 'dispatch',
2038
+ workType: intent.workType,
2039
+ ...common,
2040
+ };
2041
+ if (!hasTarget) action._intentFallbackError = _ccFallbackMissingTargetError(intent);
2042
+ return [action];
2043
+ }
2044
+
1830
2045
  function parseCCActions(text) {
1831
2046
  let actions = [];
1832
2047
  let displayText = stripCCActionsForDisplay(text);
@@ -1882,6 +2097,261 @@ function parseCCActions(text) {
1882
2097
  return result;
1883
2098
  }
1884
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
+
1885
2355
  function stripCCActionSyntax(text) {
1886
2356
  if (!text) return '';
1887
2357
  let displayText = text;
@@ -2258,10 +2728,13 @@ function createPipelineFromAction(action) {
2258
2728
  // before `try` to avoid filling `results` with cryptic per-handler error messages.
2259
2729
  function _ccValidateAction(action) {
2260
2730
  if (!action || typeof action !== 'object' || !action.type) return 'action is missing required field: type';
2261
- switch (action.type) {
2262
- case 'dispatch': case 'fix': case 'explore': case 'review': case 'test':
2263
- 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`;
2264
2735
  return null;
2736
+ case 'implement':
2737
+ return 'Unsupported action type "implement"; use type="dispatch" with workType="implement".';
2265
2738
  case 'build-and-test':
2266
2739
  if (!action.pr) return 'build-and-test action missing required field: pr';
2267
2740
  return null;
@@ -2586,6 +3059,15 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
2586
3059
  const results = [];
2587
3060
  for (const rawAction of actions) {
2588
3061
  const action = normalizeCCAction(rawAction);
3062
+ if (action?._intentFallbackError) {
3063
+ results.push({
3064
+ type: action.type || 'dispatch',
3065
+ ...(action.workType ? { workType: action.workType } : {}),
3066
+ error: action._intentFallbackError,
3067
+ missingTarget: true,
3068
+ });
3069
+ continue;
3070
+ }
2589
3071
  const validationError = _ccValidateAction(action);
2590
3072
  if (validationError) {
2591
3073
  results.push({ type: action?.type || 'unknown', error: validationError });
@@ -2593,7 +3075,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
2593
3075
  }
2594
3076
  try {
2595
3077
  switch (action.type) {
2596
- case 'dispatch': case 'fix': case 'explore': case 'review': case 'test': {
3078
+ case 'dispatch': {
2597
3079
  const workType = routing.normalizeWorkType(action.workType || (action.type !== 'dispatch' ? action.type : WORK_TYPE.IMPLEMENT), WORK_TYPE.IMPLEMENT);
2598
3080
  const id = 'W-' + shared.uid();
2599
3081
  const project = action.project || '';
@@ -2660,7 +3142,7 @@ async function executeCCActions(actions, { source = 'command-center', inferredPr
2660
3142
 
2661
3143
  // Issue #1772: CC review/explore/test are human-initiated one-offs.
2662
3144
  // Mark oneShot so any discovered PR is tagged _contextOnly (skips eval loop).
2663
- 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]);
2664
3146
  const isOneShot = action.oneShot === true || (action.oneShot !== false && ccOneShotTypes.has(workType));
2665
3147
  const item = {
2666
3148
  id, title: action.title.trim(), type: workType,
@@ -2887,7 +3369,7 @@ async function executeDocChatActions(actions, { filePath = null } = {}) {
2887
3369
  return executeCCActions(actions, { source: 'doc-chat', inferredProject: _inferDocChatProject(filePath) });
2888
3370
  }
2889
3371
 
2890
- const DOC_CHAT_WORK_ITEM_ACTION_TYPES = new Set(['dispatch', 'fix', 'implement', 'explore', 'review', 'test']);
3372
+ const DOC_CHAT_WORK_ITEM_ACTION_TYPES = new Set(['dispatch', 'fix', 'implement', 'implement:large', 'explore', 'review', 'test', 'ask', 'verify']);
2891
3373
 
2892
3374
  function _buildDocChatActionFeedback(actions, actionResults) {
2893
3375
  if (!Array.isArray(actions) || !Array.isArray(actionResults)) return [];
@@ -3688,8 +4170,9 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
3688
4170
  timeout: DOC_CHAT_TIMEOUT_MS,
3689
4171
  // Match Command Center's full tool surface and turn budget so doc-chat
3690
4172
  // can take action (read/write/edit/dispatch) instead of being limited
3691
- // to Q&A. The doc-chat sysprompt still scopes orchestration to explicit
3692
- // 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.
3693
4176
  allowedTools: 'Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch',
3694
4177
  systemPrompt: DOC_CHAT_SYSTEM_PROMPT,
3695
4178
  transcript,
@@ -4583,6 +5066,8 @@ const server = http.createServer(async (req, res) => {
4583
5066
  try {
4584
5067
  const body = await readBody(req);
4585
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 });
4586
5071
  // Write as a work item with type 'plan' — user must explicitly execute plan-to-prd after reviewing
4587
5072
  const wiPath = path.join(MINIONS_DIR, 'work-items.json');
4588
5073
  const id = 'W-' + shared.uid();
@@ -4592,7 +5077,7 @@ const server = http.createServer(async (req, res) => {
4592
5077
  status: WI_STATUS.PENDING, created: new Date().toISOString(), createdBy: 'dashboard',
4593
5078
  branchStrategy: body.branch_strategy || body.branchStrategy || 'parallel',
4594
5079
  };
4595
- if (body.project) item.project = body.project;
5080
+ if (target.project) item.project = target.project.name;
4596
5081
  if (body.agent) item.agent = body.agent;
4597
5082
  mutateWorkItems(wiPath, items => { items.push(item); });
4598
5083
  return jsonReply(res, 200, { ok: true, id, agent: body.agent || '' });
@@ -4603,6 +5088,8 @@ const server = http.createServer(async (req, res) => {
4603
5088
  try {
4604
5089
  const body = await readBody(req);
4605
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 });
4606
5093
 
4607
5094
  if (!fs.existsSync(PRD_DIR)) fs.mkdirSync(PRD_DIR, { recursive: true });
4608
5095
 
@@ -4610,7 +5097,7 @@ const server = http.createServer(async (req, res) => {
4610
5097
  const planFile = 'manual-' + shared.uid() + '.json';
4611
5098
  const plan = {
4612
5099
  version: 'manual-' + new Date().toISOString().slice(0, 10),
4613
- project: body.project || (PROJECTS.length > 0 ? PROJECTS[0].name : 'Unknown'),
5100
+ project: target.project?.name || (PROJECTS.length > 0 ? PROJECTS[0].name : 'Unknown'),
4614
5101
  generated_by: 'dashboard',
4615
5102
  generated_at: new Date().toISOString().slice(0, 10),
4616
5103
  plan_summary: body.name,
@@ -5913,7 +6400,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5913
6400
  }
5914
6401
  }
5915
6402
 
5916
- const { answer, content, actions, actionParseError, partial, warning, toolUses, error: ccError } = await ccDocCall({
6403
+ let { answer, content, actions, actionParseError, partial, warning, toolUses, error: ccError } = await ccDocCall({
5917
6404
  message: body.message, document: currentContent, title: body.title,
5918
6405
  filePath: body.filePath, selection: body.selection, canEdit, isJson,
5919
6406
  model: body.model || undefined,
@@ -5921,14 +6408,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
5921
6408
  transcript: body.transcript,
5922
6409
  onAbortReady: (abort) => { _docAbort = abort; },
5923
6410
  });
5924
- const actionResults = await executeDocChatActions(actions, { filePath: body.filePath });
5925
- 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);
5926
6416
  const finalize = _finalizeDocChatEdit({
5927
6417
  filePath: body.filePath, fullPath, isJson, canEdit,
5928
6418
  originalContent: currentContent, delimiterContent: content,
5929
6419
  });
5930
6420
  const payload = _buildDocChatResponsePayload({
5931
- answer, actions, actionResults, actionFeedback, actionParseError, ccError, partial, warning, toolUses, finalize,
6421
+ answer, actions: delegatedActions, actionResults, actionFeedback, actionParseError, ccError, partial, warning, toolUses, finalize,
5932
6422
  });
5933
6423
  _docDone = true;
5934
6424
  return jsonReply(res, 200, payload);
@@ -6005,7 +6495,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6005
6495
 
6006
6496
  try {
6007
6497
 
6008
- const { answer, content, actions, actionParseError, partial, warning, toolUses, error: ccError } = await ccDocCallStreaming({
6498
+ let { answer, content, actions, actionParseError, partial, warning, toolUses, error: ccError } = await ccDocCallStreaming({
6009
6499
  message: body.message, document: currentContent, title: body.title,
6010
6500
  filePath: body.filePath, selection: body.selection, canEdit, isJson,
6011
6501
  model: body.model || undefined,
@@ -6016,14 +6506,17 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6016
6506
  onToolUse: (name, input) => { writeDocEvent({ type: 'tool', name, input: _lightToolInput(input) }); },
6017
6507
  onRetry: (attempt) => { writeDocEvent({ type: 'progress', attempt }); },
6018
6508
  });
6019
- const actionResults = await executeDocChatActions(actions, { filePath: body.filePath });
6020
- 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);
6021
6514
  const finalize = _finalizeDocChatEdit({
6022
6515
  filePath: body.filePath, fullPath, isJson, canEdit,
6023
6516
  originalContent: currentContent, delimiterContent: content,
6024
6517
  });
6025
6518
  const payload = _buildDocChatResponsePayload({
6026
- answer, actions, actionResults, actionFeedback, actionParseError, ccError, partial, warning, toolUses, finalize,
6519
+ answer, actions: delegatedActions, actionResults, actionFeedback, actionParseError, ccError, partial, warning, toolUses, finalize,
6027
6520
  });
6028
6521
  const { answer: finalAnswer, ...donePayload } = payload;
6029
6522
  writeDocEvent({
@@ -6544,8 +7037,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6544
7037
  });
6545
7038
  }
6546
7039
 
6547
- const parsed = parseCCActions(result.text);
6548
7040
  const toolUses = Array.isArray(result.toolUses) ? result.toolUses : _extractToolUsesFromRaw(result.raw);
7041
+ const parsed = parseCCActions(result.text);
6549
7042
  // Safety net: detect /loop invocation and convert to create-watch
6550
7043
  const _loopWatch = _detectLoopInvocation(parsed.text, parsed.actions, toolUses, body.message);
6551
7044
  if (_loopWatch) {
@@ -6553,7 +7046,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6553
7046
  console.warn('[CC] /loop invocation detected — converted to create-watch');
6554
7047
  try { shared.log('warn', '/loop invocation detected in CC response — auto-converted to create-watch'); } catch {}
6555
7048
  }
6556
- 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
+ );
6557
7053
  if (parsed.actions.length > 0) {
6558
7054
  parsed.actionResults = await executeCCActions(parsed.actions);
6559
7055
  }
@@ -6881,7 +7377,10 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6881
7377
  console.warn('[CC] /loop invocation detected — converted to create-watch');
6882
7378
  try { shared.log('warn', '/loop invocation detected in CC response — auto-converted to create-watch'); } catch {}
6883
7379
  }
6884
- 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
+ );
6885
7384
  let actionResults;
6886
7385
  if (actions.length > 0) {
6887
7386
  actionResults = await executeCCActions(actions);
@@ -6945,6 +7444,9 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6945
7444
  const body = await readBody(req);
6946
7445
  let { id, cron, title, type, project, agent, description, priority, enabled } = body;
6947
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;
6948
7450
 
6949
7451
  // Auto-generate ID from title if not provided
6950
7452
  if (!id) {
@@ -6977,8 +7479,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
6977
7479
 
6978
7480
  async function handleSchedulesUpdate(req, res) {
6979
7481
  const body = await readBody(req);
6980
- const { id, cron, title, type, project, agent, description, priority, enabled } = body;
7482
+ let { id, cron, title, type, project, agent, description, priority, enabled } = body;
6981
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
+ }
6982
7489
 
6983
7490
  let missingSchedules = false;
6984
7491
  let sched = null;
@@ -7039,6 +7546,11 @@ What would you like to discuss or change? When you're happy, say "approve" and I
7039
7546
  reloadConfig();
7040
7547
  const sched = (CONFIG.schedules || []).find(s => s.id === id);
7041
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
+ }
7042
7554
 
7043
7555
  const schedulerMod = require('./engine/scheduler');
7044
7556
  let item;
@@ -8146,8 +8658,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
8146
8658
  const { savePipeline, getPipeline } = require('./engine/pipeline');
8147
8659
  if (getPipeline(body.id)) return jsonReply(res, 409, { error: 'Pipeline already exists' });
8148
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;
8149
8663
  if (body.stopWhen) pipeline.stopWhen = body.stopWhen;
8150
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 });
8151
8667
  savePipeline(pipeline);
8152
8668
  invalidateStatusCache();
8153
8669
  return jsonReply(res, 200, { ok: true, id: pipeline.id });
@@ -8162,8 +8678,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
8162
8678
  if (body.stages !== undefined) pipeline.stages = body.stages;
8163
8679
  if (body.trigger !== undefined) pipeline.trigger = body.trigger;
8164
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;
8165
8683
  if (body.monitoredResources !== undefined) pipeline.monitoredResources = body.monitoredResources;
8166
8684
  if (body.stopWhen !== undefined) pipeline.stopWhen = body.stopWhen;
8685
+ const projectError = validatePipelineProjects(pipeline);
8686
+ if (projectError) return jsonReply(res, 400, { error: projectError });
8167
8687
  savePipeline(pipeline);
8168
8688
  invalidateStatusCache();
8169
8689
  return jsonReply(res, 200, { ok: true });
@@ -8507,10 +9027,16 @@ module.exports = {
8507
9027
  _shouldSuppressDocChatPostPatchError,
8508
9028
  _buildDocChatResponsePayload,
8509
9029
  _inferDocChatProject,
9030
+ _messageHasDelegationIntent,
9031
+ _messageRequestsDirectHandling,
9032
+ _messageHasMediumLargeWorkIntent,
9033
+ _inferDelegationActionFromMessage,
9034
+ _ensureDelegationForIntent,
8510
9035
  _linkPullRequestForTracking: linkPullRequestForTracking,
8511
9036
  _resolveSkillReadPath,
8512
9037
  DOC_CHAT_DOCUMENT_DELIMITER,
8513
9038
  _ccValidateAction,
9039
+ _actionsWithIntentFallback,
8514
9040
  _messageExplicitlyRequestsMonitoring,
8515
9041
  _filterImplicitPostDispatchActions,
8516
9042
  _findDuplicateWorkItemCreate: findDuplicateWorkItemCreate,