@yemi33/minions 0.1.1835 → 0.1.1836
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/dashboard.js +84 -1978
- package/engine/copilot-models.json +1 -1
- package/package.json +1 -1
package/dashboard.js
CHANGED
|
@@ -1602,13 +1602,6 @@ function renderDocChatSystemPromptForTurn(turnId) {
|
|
|
1602
1602
|
.replace(/\{\{dashboard_port\}\}/g, String(PORT));
|
|
1603
1603
|
}
|
|
1604
1604
|
|
|
1605
|
-
// Doc-chat document rewrite delimiter constants. Kept for backwards-compat
|
|
1606
|
-
// while the system migrates to using Edit/Write tools directly (see
|
|
1607
|
-
// prompts/cc-system.md "Document edits" section). Once CC always uses
|
|
1608
|
-
// Edit/Write, these can be retired.
|
|
1609
|
-
const DOC_CHAT_DOCUMENT_DELIMITER = '---MINIONS-DOC-CHAT-DOCUMENT-v1-6f2f90e3---';
|
|
1610
|
-
const LEGACY_DOC_CHAT_DOCUMENT_DELIMITER = '---DOCUMENT---';
|
|
1611
|
-
|
|
1612
1605
|
// Per-CC-turn correlation. CC's tool calls (Bash curl to /api/...) attach an
|
|
1613
1606
|
// X-CC-Turn-Id header; the mutating endpoints record creations into this map
|
|
1614
1607
|
// keyed by the turn ID so the CC handler can surface them as confirmation
|
|
@@ -2073,135 +2066,25 @@ For all state files, look under \`${MINIONS_DIR}\`.${indexSection}`;
|
|
|
2073
2066
|
return result;
|
|
2074
2067
|
}
|
|
2075
2068
|
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
// Single helper that handles both the strict (well-formed) and loose forms of
|
|
2082
|
-
// the ===ACTIONS=== delimiter. `parseable` is true only for the strict form
|
|
2083
|
-
// that parseCCActions can JSON.parse; loose matches still split display text
|
|
2084
|
-
// but are not parsed (they shouldn't reach the user as actions).
|
|
2085
|
-
function findCCActionsHeader(text) {
|
|
2086
|
-
if (!text) return null;
|
|
2087
|
-
// Tier 1 — strict, parseable: the exact canonical delimiter on its own line.
|
|
2088
|
-
const strict = /(?:^|\r?\n)===ACTIONS===[ \t]*(?=\r?\n|$)/m.exec(text);
|
|
2089
|
-
if (strict) {
|
|
2090
|
-
const headerStart = strict.index + strict[0].indexOf('===ACTIONS');
|
|
2091
|
-
const headerMatch = text.slice(headerStart).match(/^===ACTIONS===[ \t]*/);
|
|
2092
|
-
return {
|
|
2093
|
-
index: headerStart,
|
|
2094
|
-
headerLength: headerMatch ? headerMatch[0].length : '===ACTIONS==='.length,
|
|
2095
|
-
parseable: true,
|
|
2096
|
-
};
|
|
2097
|
-
}
|
|
2098
|
-
// Tier 2 — loose: sentinel-looking malformed delimiters such as ===ACTIONS ->
|
|
2099
|
-
// should still be hidden, but prose like "===ACTIONS are documented" must render.
|
|
2100
|
-
const loose = /(?:^|\r?\n)===ACTIONS(?:[ \t]*(?:[-=]>?|={1,}|$)|[^A-Za-z0-9_\s\r\n][^\r\n]*)(?=\r?\n|$)/m.exec(text);
|
|
2101
|
-
if (loose) {
|
|
2102
|
-
const headerStart = loose.index + loose[0].indexOf('===ACTIONS');
|
|
2103
|
-
return { index: headerStart, headerLength: 0, parseable: false };
|
|
2104
|
-
}
|
|
2105
|
-
// Tier 3 — very loose: 2+ leading equals, optional whitespace, ACTIONS keyword
|
|
2106
|
-
// (case-insensitive), optional trailing equals. Catches the common model
|
|
2107
|
-
// fumbles: ====ACTIONS===, ===actions===, ===ACTIONS=====, ==ACTIONS==. Must
|
|
2108
|
-
// still be a complete line (preceded by line start, followed by EOL or EOS)
|
|
2109
|
-
// so prose like "the answer is 2 + 3 = 5" or "==Important==" doesn't match.
|
|
2110
|
-
const veryLoose = /(?:^|\r?\n)={2,}[ \t]*ACTIONS[ \t]*={0,}[ \t]*(?=\r?\n|$)/im.exec(text);
|
|
2111
|
-
if (veryLoose) {
|
|
2112
|
-
// Skip the leading newline (if any) so headerStart points to first '='.
|
|
2113
|
-
const offset = veryLoose[0].search(/=/);
|
|
2114
|
-
const headerStart = veryLoose.index + (offset >= 0 ? offset : 0);
|
|
2115
|
-
return { index: headerStart, headerLength: 0, parseable: false };
|
|
2116
|
-
}
|
|
2117
|
-
return null;
|
|
2118
|
-
}
|
|
2069
|
+
// The ===ACTIONS=== delimiter parser tiers (findCCActionsHeader,
|
|
2070
|
+
// findCCActionsPartialDelimiter, stripCCActionsForStream/Display) and the
|
|
2071
|
+
// _extractActionsJson Copilot fence-stripper were retired with the move to
|
|
2072
|
+
// direct /api/* tool calls. Action surfacing now flows entirely through the
|
|
2073
|
+
// X-CC-Turn-Id header → _ccTurnCreations → _buildSyntheticActionResultsForTurn.
|
|
2119
2074
|
|
|
2120
|
-
function
|
|
2121
|
-
if (!
|
|
2122
|
-
const
|
|
2123
|
-
const
|
|
2124
|
-
const
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
if (/^=+$/.test(trailingLine)) return lineStart;
|
|
2130
|
-
// Strict prefix of the canonical "===ACTIONS===" (case-insensitive so
|
|
2131
|
-
// "===act" and "===ACT" both strip).
|
|
2132
|
-
if (trailingLine.length >= 1 && trailingLine.length < delimiter.length
|
|
2133
|
-
&& delimiter.toLowerCase().startsWith(trailingLine.toLowerCase())) {
|
|
2134
|
-
return lineStart;
|
|
2135
|
-
}
|
|
2136
|
-
return -1;
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
function stripCCActionsForStream(text) {
|
|
2140
|
-
if (!text) return '';
|
|
2141
|
-
// Fast path: 95% of streamed chunks contain no '=' before any actions block
|
|
2142
|
-
// appears. Skip all regex work in that case.
|
|
2143
|
-
if (text.indexOf('===') < 0) return text;
|
|
2144
|
-
const header = findCCActionsHeader(text);
|
|
2145
|
-
if (header) return text.slice(0, header.index).trim();
|
|
2146
|
-
const partialIdx = findCCActionsPartialDelimiter(text);
|
|
2147
|
-
if (partialIdx >= 0) return text.slice(0, partialIdx).trimEnd();
|
|
2148
|
-
return text;
|
|
2149
|
-
}
|
|
2150
|
-
|
|
2151
|
-
function stripCCActionsForDisplay(text) {
|
|
2152
|
-
if (!text) return '';
|
|
2153
|
-
const header = findCCActionsHeader(text);
|
|
2154
|
-
if (header) return text.slice(0, header.index).trim();
|
|
2155
|
-
const partialIdx = findCCActionsPartialDelimiter(text);
|
|
2156
|
-
if (partialIdx >= 0) return text.slice(0, partialIdx).trimEnd();
|
|
2157
|
-
return text;
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
// Issue #1834: non-Claude runtimes (Copilot/GPT) routinely wrap the action JSON
|
|
2161
|
-
// in ```json fences or append trailing prose ("Let me know if that helps!").
|
|
2162
|
-
// JSON.parse on the raw segment fails silently → actions dropped, user sees
|
|
2163
|
-
// inert text. This extractor pulls out the balanced JSON value (array or
|
|
2164
|
-
// object) regardless of fences, leading whitespace, or trailing junk so the
|
|
2165
|
-
// downstream parse can succeed. Returns null if no plausible JSON value is
|
|
2166
|
-
// present (caller surfaces the failure via _actionParseError).
|
|
2167
|
-
function _extractActionsJson(segment) {
|
|
2168
|
-
if (!segment) return null;
|
|
2169
|
-
let body = segment.trim();
|
|
2170
|
-
// Strip ```json / ``` fences (open + close). The model sometimes only emits
|
|
2171
|
-
// an opening fence (truncation), so handle both halves independently.
|
|
2172
|
-
body = body.replace(/^```[a-zA-Z0-9_-]*\s*\r?\n?/, '').replace(/\r?\n?```\s*$/, '').trim();
|
|
2173
|
-
if (!body) return null;
|
|
2174
|
-
const first = body.indexOf('[');
|
|
2175
|
-
const firstObj = body.indexOf('{');
|
|
2176
|
-
let start = -1;
|
|
2177
|
-
let openCh = '';
|
|
2178
|
-
let closeCh = '';
|
|
2179
|
-
if (first >= 0 && (firstObj < 0 || first <= firstObj)) {
|
|
2180
|
-
start = first; openCh = '['; closeCh = ']';
|
|
2181
|
-
} else if (firstObj >= 0) {
|
|
2182
|
-
start = firstObj; openCh = '{'; closeCh = '}';
|
|
2183
|
-
}
|
|
2184
|
-
if (start < 0) return null;
|
|
2185
|
-
let depth = 0;
|
|
2186
|
-
let inString = false;
|
|
2187
|
-
let escape = false;
|
|
2188
|
-
for (let i = start; i < body.length; i++) {
|
|
2189
|
-
const ch = body[i];
|
|
2190
|
-
if (escape) { escape = false; continue; }
|
|
2191
|
-
if (ch === '\\') { escape = true; continue; }
|
|
2192
|
-
if (ch === '"') { inString = !inString; continue; }
|
|
2193
|
-
if (inString) continue;
|
|
2194
|
-
if (ch === openCh) depth++;
|
|
2195
|
-
else if (ch === closeCh) {
|
|
2196
|
-
depth--;
|
|
2197
|
-
if (depth === 0) return body.slice(start, i + 1);
|
|
2198
|
-
}
|
|
2075
|
+
function normalizeMeetingParticipants(participants) {
|
|
2076
|
+
if (!Array.isArray(participants)) return [];
|
|
2077
|
+
const seen = new Set();
|
|
2078
|
+
const normalized = [];
|
|
2079
|
+
for (const participant of participants) {
|
|
2080
|
+
const id = String(participant || '').trim();
|
|
2081
|
+
if (!id || seen.has(id)) continue;
|
|
2082
|
+
seen.add(id);
|
|
2083
|
+
normalized.push(id);
|
|
2199
2084
|
}
|
|
2200
|
-
return
|
|
2085
|
+
return normalized;
|
|
2201
2086
|
}
|
|
2202
2087
|
|
|
2203
|
-
const CC_DISPATCH_ACTION_ALIASES = new Set(['fix', 'explore', 'review', 'test', 'implement', 'implement:large', 'ask', 'verify']);
|
|
2204
|
-
const CC_ACTION_INTENT_WORK_TYPES = new Set(['fix', 'implement', 'implement:large', 'explore', 'review', 'test', 'ask', 'verify']);
|
|
2205
2088
|
|
|
2206
2089
|
function getWorkItemPrRef(input) {
|
|
2207
2090
|
if (!input || typeof input !== 'object') return null;
|
|
@@ -2229,1677 +2112,6 @@ function extractPrRefFromText(value) {
|
|
|
2229
2112
|
return numberMatch ? numberMatch[1] : null;
|
|
2230
2113
|
}
|
|
2231
2114
|
|
|
2232
|
-
function normalizeCCAction(action) {
|
|
2233
|
-
if (!action || typeof action !== 'object') return action;
|
|
2234
|
-
if (typeof action.type !== 'string') return action;
|
|
2235
|
-
const type = action.type.trim().toLowerCase();
|
|
2236
|
-
if (type === 'dispatch') {
|
|
2237
|
-
return action.type === 'dispatch' ? action : { ...action, type: 'dispatch' };
|
|
2238
|
-
}
|
|
2239
|
-
if (!CC_DISPATCH_ACTION_ALIASES.has(type)) return action;
|
|
2240
|
-
return { ...action, type: 'dispatch', workType: action.workType || type };
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
function _ccCleanIntentString(value, max = 500) {
|
|
2244
|
-
if (typeof value !== 'string') return '';
|
|
2245
|
-
let out = '';
|
|
2246
|
-
let lastWasSpace = true;
|
|
2247
|
-
for (const ch of value) {
|
|
2248
|
-
const isSpace = ch === '\0' || ch === ' ' || ch === '\n' || ch === '\r' || ch === '\t';
|
|
2249
|
-
if (isSpace) {
|
|
2250
|
-
if (!lastWasSpace) out += ' ';
|
|
2251
|
-
lastWasSpace = true;
|
|
2252
|
-
} else {
|
|
2253
|
-
out += ch;
|
|
2254
|
-
lastWasSpace = false;
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
return out.trim().slice(0, max);
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
function _ccNormalizeIntentMetadata(meta) {
|
|
2261
|
-
if (!meta || typeof meta !== 'object' || Array.isArray(meta)) return {};
|
|
2262
|
-
const out = {};
|
|
2263
|
-
for (const key of ['intent', 'type', 'title', 'description', 'priority', 'project', 'branchStrategy', 'branch_strategy', 'pr', 'targetPr', 'prId', 'prNumber', 'pullRequest', 'sourcePr', 'prUrl']) {
|
|
2264
|
-
const value = _ccCleanIntentString(meta[key], key === 'description' ? 2000 : 300);
|
|
2265
|
-
if (value) out[key] = value;
|
|
2266
|
-
}
|
|
2267
|
-
if (Array.isArray(meta.agents)) {
|
|
2268
|
-
out.agents = meta.agents.map(a => _ccCleanIntentString(a, 80)).filter(Boolean);
|
|
2269
|
-
}
|
|
2270
|
-
if (Array.isArray(meta.projects)) {
|
|
2271
|
-
out.projects = meta.projects.map(p => _ccCleanIntentString(p, 120)).filter(Boolean);
|
|
2272
|
-
}
|
|
2273
|
-
if (meta.fanout === true) out.fanout = true;
|
|
2274
|
-
return out;
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
function _messageExplicitlyRequestsMinionsOrchestration(message) {
|
|
2278
|
-
const normalized = _normalizeIntentText(message);
|
|
2279
|
-
if (!normalized.trim()) return false;
|
|
2280
|
-
if (_messageHasDelegationIntent(message)) return true;
|
|
2281
|
-
if (_intentHasAnyToken(normalized, ['minions']) &&
|
|
2282
|
-
(_messageHasMediumLargeWorkIntent(message) || _messageRequestsPlanIntent(message))) return true;
|
|
2283
|
-
if (_intentHasAnyToken(normalized, ['agent', 'agents']) && _messageHasMediumLargeWorkIntent(message)) return true;
|
|
2284
|
-
return _intentHasVerbObject(normalized, ['create', 'make', 'draft', 'write'], ['plan']) &&
|
|
2285
|
-
_intentHasAnyToken(normalized, ['minion', 'minions']);
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
function _ccInferMessageActionIntent(message, source = 'command-center') {
|
|
2289
|
-
const normalized = _normalizeIntentText(message);
|
|
2290
|
-
if (!normalized.trim()) return null;
|
|
2291
|
-
if (source === 'doc-chat' && !_messageExplicitlyRequestsMinionsOrchestration(message)) return null;
|
|
2292
|
-
if (_messageRequestsPlanIntent(message)) {
|
|
2293
|
-
return { kind: 'plan' };
|
|
2294
|
-
}
|
|
2295
|
-
if (!_messageHasDelegationIntent(message) && !_messageHasMediumLargeWorkIntent(message, { source })) return null;
|
|
2296
|
-
return { kind: 'work-item', workType: _inferDelegatedWorkType(message) };
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
function _ccInferMetadataActionIntent(meta) {
|
|
2300
|
-
if (meta.intent === 'plan') return { kind: 'plan' };
|
|
2301
|
-
if (meta.intent === 'work-item' && CC_ACTION_INTENT_WORK_TYPES.has(String(meta.type || '').toLowerCase())) {
|
|
2302
|
-
return { kind: 'work-item', workType: String(meta.type).toLowerCase() };
|
|
2303
|
-
}
|
|
2304
|
-
return null;
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
function _ccActionIntentTargetText(message, intent) {
|
|
2308
|
-
const tokens = _intentTokens(_normalizeIntentText(_ccCleanIntentString(message, 500)));
|
|
2309
|
-
let start = 0;
|
|
2310
|
-
const skip = (terms) => {
|
|
2311
|
-
if (terms.includes(tokens[start])) {
|
|
2312
|
-
start++;
|
|
2313
|
-
return true;
|
|
2314
|
-
}
|
|
2315
|
-
return false;
|
|
2316
|
-
};
|
|
2317
|
-
skip(['please']);
|
|
2318
|
-
if (tokens[start] && tokens[start].startsWith('/')) start++;
|
|
2319
|
-
if (['dispatch', 'delegate', 'assign', 'queue', 'enqueue'].includes(tokens[start])) {
|
|
2320
|
-
start++;
|
|
2321
|
-
while (['a', 'an', 'the', 'work', 'item', 'task', 'agent', 'to'].includes(tokens[start])) start++;
|
|
2322
|
-
} else if (['create', 'open', 'file'].includes(tokens[start]) && tokens[start + 1] && ['a', 'an', 'the', 'work', 'item', 'task'].includes(tokens[start + 1])) {
|
|
2323
|
-
start++;
|
|
2324
|
-
while (['a', 'an', 'the', 'work', 'item', 'task', 'to', 'for'].includes(tokens[start])) start++;
|
|
2325
|
-
} else if (['have', 'ask', 'tell'].includes(tokens[start])) {
|
|
2326
|
-
start++;
|
|
2327
|
-
while (tokens[start] && !['to', 'fix', 'implement', 'review', 'test', 'explore', 'investigate', 'audit'].includes(tokens[start])) start++;
|
|
2328
|
-
if (tokens[start] === 'to') start++;
|
|
2329
|
-
}
|
|
2330
|
-
if (intent?.kind === 'plan') {
|
|
2331
|
-
while (['make', 'create', 'draft', 'write', 'design', 'plan', 'out', 'a', 'an', 'the', 'for', 'to', 'about', 'how'].includes(tokens[start])) start++;
|
|
2332
|
-
} else if (intent?.workType) {
|
|
2333
|
-
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++;
|
|
2334
|
-
}
|
|
2335
|
-
while (['for', 'to', 'of', 'on', 'about', 'a', 'an', 'the'].includes(tokens[start])) start++;
|
|
2336
|
-
return tokens.slice(start).join(' ');
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
function _ccIntentHasConcreteTarget(message, metadataTitle, intent) {
|
|
2340
|
-
const raw = String(message || '');
|
|
2341
|
-
const normalized = _normalizeIntentText(raw);
|
|
2342
|
-
const tokens = _intentTokens(normalized);
|
|
2343
|
-
if (raw.includes('http://') || raw.includes('https://')) return true;
|
|
2344
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
2345
|
-
if (['pr', 'issue'].includes(tokens[i]) && tokens[i + 1] && _intentTokenIsNumeric(tokens[i + 1])) return true;
|
|
2346
|
-
if (tokens[i] === 'pull' && tokens[i + 1] === 'request' && tokens[i + 2] && _intentTokenIsNumeric(tokens[i + 2])) return true;
|
|
2347
|
-
if (tokens[i] === 'w' && tokens[i + 1] && tokens[i + 1].length >= 2) return true;
|
|
2348
|
-
}
|
|
2349
|
-
const target = _ccActionIntentTargetText(metadataTitle || raw, intent).trim().toLowerCase();
|
|
2350
|
-
if (!target) return false;
|
|
2351
|
-
const targetTokens = _intentTokens(_normalizeIntentText(target));
|
|
2352
|
-
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']);
|
|
2353
|
-
if (targetTokens.length > 0 && targetTokens.every(token => generic.has(token))) return false;
|
|
2354
|
-
return target.length >= 3;
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
function _ccIntentTitle(message, metadataTitle, intent) {
|
|
2358
|
-
const metaTitle = _ccCleanIntentString(metadataTitle, 300);
|
|
2359
|
-
if (metaTitle && _ccIntentHasConcreteTarget(message, metaTitle, intent)) return metaTitle;
|
|
2360
|
-
const cleaned = _ccCleanIntentString(message, 300);
|
|
2361
|
-
if (!cleaned) return '';
|
|
2362
|
-
const title = intent?.kind === 'plan'
|
|
2363
|
-
? cleaned
|
|
2364
|
-
: _delegatedWorkTitle(cleaned, intent?.workType || 'ask');
|
|
2365
|
-
return title.charAt(0).toUpperCase() + title.slice(1);
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
function _ccFallbackMissingTargetError(intent) {
|
|
2369
|
-
const label = intent?.kind === 'plan' ? 'plan' : (intent?.workType || 'dispatch');
|
|
2370
|
-
return `Missing target for ${label} request. Specify a concrete title, PR/work item, file, or feature to ${label}.`;
|
|
2371
|
-
}
|
|
2372
|
-
|
|
2373
|
-
function _actionsWithIntentFallback(actions, opts = {}) {
|
|
2374
|
-
const { message = '', intentMetadata = null, source = 'command-center', filePath = null, title: docTitle = null, answerText = '', toolUses = [] } = opts;
|
|
2375
|
-
const existing = (Array.isArray(actions) ? actions.map(normalizeCCAction) : [])
|
|
2376
|
-
.filter(action => !_ccShouldSuppressAnsweredAskDispatch(action, { message, source, answerText, toolUses }));
|
|
2377
|
-
if (_messageRequestsDirectHandling(message, { answerText, toolUses })) return existing.filter(a => normalizeCCAction(a)?.type !== 'dispatch');
|
|
2378
|
-
if (existing.some(a => normalizeCCAction(a)?.type === 'dispatch')) return existing;
|
|
2379
|
-
if (existing.length > 0) return existing;
|
|
2380
|
-
const meta = _ccNormalizeIntentMetadata(intentMetadata);
|
|
2381
|
-
const messageIntent = _ccInferMessageActionIntent(message, source);
|
|
2382
|
-
const metadataIntent = source === 'doc-chat' ? null : _ccInferMetadataActionIntent(meta);
|
|
2383
|
-
const intent = messageIntent || metadataIntent;
|
|
2384
|
-
if (!intent) return _ensureDelegationForIntent(existing, { message, source, filePath, title: docTitle, answerText, toolUses });
|
|
2385
|
-
|
|
2386
|
-
const title = _ccIntentTitle(message, meta.title, intent);
|
|
2387
|
-
const hasTarget = _ccIntentHasConcreteTarget(message, meta.title, intent);
|
|
2388
|
-
const description = meta.description || _ccCleanIntentString(message, 2000);
|
|
2389
|
-
const common = {
|
|
2390
|
-
...(title ? { title } : {}),
|
|
2391
|
-
...(description ? { description } : {}),
|
|
2392
|
-
...(meta.priority ? { priority: meta.priority } : {}),
|
|
2393
|
-
...(meta.project ? { project: meta.project } : {}),
|
|
2394
|
-
...(Array.isArray(meta.agents) && meta.agents.length ? { agents: meta.agents } : {}),
|
|
2395
|
-
...(meta.fanout ? { scope: 'fan-out' } : {}),
|
|
2396
|
-
};
|
|
2397
|
-
if (intent.kind === 'plan') {
|
|
2398
|
-
const action = {
|
|
2399
|
-
type: 'plan',
|
|
2400
|
-
...common,
|
|
2401
|
-
branchStrategy: meta.branchStrategy || meta.branch_strategy || 'parallel',
|
|
2402
|
-
};
|
|
2403
|
-
if (!hasTarget) action._intentFallbackError = _ccFallbackMissingTargetError(intent);
|
|
2404
|
-
return [action];
|
|
2405
|
-
}
|
|
2406
|
-
const action = {
|
|
2407
|
-
type: 'dispatch',
|
|
2408
|
-
workType: intent.workType,
|
|
2409
|
-
...common,
|
|
2410
|
-
};
|
|
2411
|
-
const prRef = getWorkItemPrRef(meta) || extractPrRefFromText([message, meta.title, meta.description].filter(Boolean).join('\n'));
|
|
2412
|
-
if (prRef && isPrTargetedWorkType(intent.workType)) action.pr = prRef;
|
|
2413
|
-
if (!hasTarget) action._intentFallbackError = _ccFallbackMissingTargetError(intent);
|
|
2414
|
-
if (_ccShouldSuppressAnsweredAskDispatch(action, { message, source, answerText, toolUses })) return existing;
|
|
2415
|
-
return [action];
|
|
2416
|
-
}
|
|
2417
|
-
|
|
2418
|
-
function parseCCActions(text) {
|
|
2419
|
-
let actions = [];
|
|
2420
|
-
let displayText = stripCCActionsForDisplay(text);
|
|
2421
|
-
let parseError = null;
|
|
2422
|
-
const header = findCCActionsHeader(text);
|
|
2423
|
-
let segment = '';
|
|
2424
|
-
if (header) {
|
|
2425
|
-
displayText = text.slice(0, header.index).trim();
|
|
2426
|
-
if (header.parseable) {
|
|
2427
|
-
segment = text.slice(header.index + header.headerLength);
|
|
2428
|
-
const jsonStr = _extractActionsJson(segment);
|
|
2429
|
-
if (jsonStr) {
|
|
2430
|
-
try {
|
|
2431
|
-
const parsed = JSON.parse(jsonStr);
|
|
2432
|
-
if (Array.isArray(parsed)) {
|
|
2433
|
-
actions = parsed;
|
|
2434
|
-
} else {
|
|
2435
|
-
parseError = 'actions JSON must be an array after ===ACTIONS=== delimiter';
|
|
2436
|
-
}
|
|
2437
|
-
} catch (e) {
|
|
2438
|
-
parseError = e.message || 'invalid JSON';
|
|
2439
|
-
}
|
|
2440
|
-
} else if (segment.trim()) {
|
|
2441
|
-
parseError = 'no JSON value found after ===ACTIONS=== delimiter';
|
|
2442
|
-
}
|
|
2443
|
-
} else {
|
|
2444
|
-
// Loose/very-loose match: delimiter present but malformed (e.g. extra
|
|
2445
|
-
// equals, lowercase, trailing punct). Surface so the client banner fires
|
|
2446
|
-
// instead of silently dropping actions — user resends with the strict
|
|
2447
|
-
// shape.
|
|
2448
|
-
parseError = 'Malformed ===ACTIONS=== delimiter (extra equals, lowercase, or trailing punctuation). Actions silently discarded — fix the model output.';
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
actions = actions.map(normalizeCCAction);
|
|
2452
|
-
const result = { text: displayText, actions };
|
|
2453
|
-
if (parseError && actions.length === 0) {
|
|
2454
|
-
result._actionParseError = parseError;
|
|
2455
|
-
// Visibility for the engine log — shared.log applies SEC-09 redaction before persistence.
|
|
2456
|
-
try {
|
|
2457
|
-
const snippet = (segment.trim() || '').slice(0, 200);
|
|
2458
|
-
if (typeof shared !== 'undefined' && shared && typeof shared.log === 'function') {
|
|
2459
|
-
shared.log('warn', `CC action JSON parse failed: ${parseError} — segment: ${snippet}`);
|
|
2460
|
-
}
|
|
2461
|
-
} catch { /* logging is best-effort */ }
|
|
2462
|
-
}
|
|
2463
|
-
return result;
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
const DELEGATION_ACTION_TERMS = ['dispatch', 'delegate', 'assign', 'queue', 'enqueue'];
|
|
2467
|
-
const DELEGATION_REQUEST_PREFIX_TERMS = ['please', 'can', 'could', 'would', 'will', 'should', 'you', 'cc', 'minions', 'minion'];
|
|
2468
|
-
const DELEGATION_PERSON_ACTION_TERMS = ['ask', 'tell', 'have'];
|
|
2469
|
-
// Subset of delegation action terms that double as common nouns in status
|
|
2470
|
-
// questions (e.g. "queue status", "dispatch contents"). When followed by a
|
|
2471
|
-
// status-noun follow-on these are noun usage, not imperative delegation.
|
|
2472
|
-
const DELEGATION_AMBIGUOUS_NOUN_TERMS = new Set(['dispatch', 'queue']);
|
|
2473
|
-
const DELEGATION_NON_VERB_FOLLOW_ONS = new Set([
|
|
2474
|
-
'status', 'contents', 'content', 'state', 'count', 'list', 'items', 'item',
|
|
2475
|
-
'size', 'length', 'depth', 'health', 'info', 'order', 'queue', 'history',
|
|
2476
|
-
'log', 'logs', 'summary', 'overview', 'report', 'is', 'was', 'are', 'were',
|
|
2477
|
-
'has', 'have', 'looks', 'shows',
|
|
2478
|
-
]);
|
|
2479
|
-
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'];
|
|
2480
|
-
const MEDIUM_INVESTIGATION_TERMS = ['audit', 'investigate', 'research', 'explore', 'analyze', 'analyse'];
|
|
2481
|
-
const MEDIUM_INVESTIGATION_PHRASES = ['deep dive', 'root cause'];
|
|
2482
|
-
const MEDIUM_IMPLEMENT_TERMS = ['implement', 'fix', 'debug', 'repair', 'refactor', 'migrate', 'harden', 'redesign'];
|
|
2483
|
-
const MEDIUM_BUILD_VERBS = ['build', 'add', 'create'];
|
|
2484
|
-
const MEDIUM_BUILD_OBJECTS = ['feature', 'support', 'endpoint', 'api', 'ui', 'page', 'component', 'flow', 'integration', 'automation', 'test', 'tests', 'tool', 'command', 'dashboard', 'service', 'module'];
|
|
2485
|
-
const MEDIUM_REVIEW_TERMS = ['review', 'test', 'verify', 'validate'];
|
|
2486
|
-
const MEDIUM_SIZE_TERMS = ['medium', 'large', 'larger'];
|
|
2487
|
-
const MEDIUM_SIZE_PHRASES = ['cross cutting', 'multi file', 'multi step', 'multi stage', 'multi module'];
|
|
2488
|
-
const PLAN_INTENT_TERMS = ['plan'];
|
|
2489
|
-
const PLAN_CREATE_TERMS = ['make', 'create', 'draft', 'write', 'design'];
|
|
2490
|
-
const PLAN_CREATE_PHRASES = ['come up with'];
|
|
2491
|
-
const DOC_CHAT_DIRECT_DOC_TERMS = ['summarize', 'summary', 'quote', 'extract', 'rewrite', 'reword', 'copyedit', 'proofread', 'format', 'typo'];
|
|
2492
|
-
const DOC_CHAT_DOC_ACTION_TERMS = ['edit', 'update', 'change', 'review', 'check'];
|
|
2493
|
-
const DOC_CHAT_DOC_OBJECTS = ['document', 'doc', 'paragraph', 'section', 'selection', 'text', 'wording'];
|
|
2494
|
-
const DIRECT_QUICK_TERMS = ['quick', 'simple', 'small'];
|
|
2495
|
-
const DIRECT_QUICK_OBJECTS = ['question', 'answer', 'lookup', 'check'];
|
|
2496
|
-
const DIRECT_REPLY_TERMS = ['answer', 'respond', 'reply'];
|
|
2497
|
-
const DIRECT_REPLY_TARGETS = ['directly', 'inline', 'here'];
|
|
2498
|
-
const DIRECT_SELF_TERMS = ['do', 'handle', 'answer', 'fix', 'edit', 'update', 'change', 'review', 'check'];
|
|
2499
|
-
const DIRECT_SELF_TARGETS = ['yourself', 'directly'];
|
|
2500
|
-
const DIRECT_HANDLING_PHRASES = [
|
|
2501
|
-
'you do it yourself', 'you handle it yourself', 'you handle this yourself',
|
|
2502
|
-
'cc do it yourself', 'doc chat do it yourself', 'doc chat handle it yourself',
|
|
2503
|
-
'do not dispatch', 'dont dispatch', 'do not delegate', 'dont delegate',
|
|
2504
|
-
'do not assign', 'dont assign', 'do not queue', 'dont queue',
|
|
2505
|
-
'do not enqueue', 'dont enqueue', 'do not create work item', 'dont create work item',
|
|
2506
|
-
'no dispatch', 'no delegate', 'no delegation', 'no work item',
|
|
2507
|
-
'without dispatching', 'without delegating', 'without creating work item',
|
|
2508
|
-
];
|
|
2509
|
-
const ANSWERED_ASK_MIN_CHARS = 80;
|
|
2510
|
-
const ANSWERED_ASK_INCOMPLETE_PHRASES = [
|
|
2511
|
-
'i will dispatch', 'ill dispatch', 'i will delegate', 'ill delegate',
|
|
2512
|
-
'i will ask', 'ill ask', 'need to dispatch', 'needs to dispatch',
|
|
2513
|
-
'need to delegate', 'needs to delegate', 'need an agent', 'needs an agent',
|
|
2514
|
-
'hand off', 'hand this off', 'deeper investigation', 'further investigation',
|
|
2515
|
-
'cannot answer', 'cant answer', 'could not answer', 'couldnt answer',
|
|
2516
|
-
'not enough information',
|
|
2517
|
-
];
|
|
2518
|
-
|
|
2519
|
-
function _isIntentWordChar(ch) {
|
|
2520
|
-
const code = ch.charCodeAt(0);
|
|
2521
|
-
return (code >= 48 && code <= 57) || (code >= 97 && code <= 122);
|
|
2522
|
-
}
|
|
2523
|
-
|
|
2524
|
-
function _normalizeIntentText(message) {
|
|
2525
|
-
const raw = String(message || '').toLowerCase();
|
|
2526
|
-
let out = ' ';
|
|
2527
|
-
let lastWasSpace = true;
|
|
2528
|
-
for (const ch of raw) {
|
|
2529
|
-
if (ch === '\'' || ch === '\u2019') continue;
|
|
2530
|
-
if (_isIntentWordChar(ch)) {
|
|
2531
|
-
out += ch;
|
|
2532
|
-
lastWasSpace = false;
|
|
2533
|
-
} else if (!lastWasSpace) {
|
|
2534
|
-
out += ' ';
|
|
2535
|
-
lastWasSpace = true;
|
|
2536
|
-
}
|
|
2537
|
-
}
|
|
2538
|
-
return lastWasSpace ? out : out + ' ';
|
|
2539
|
-
}
|
|
2540
|
-
|
|
2541
|
-
function _intentTokens(normalized) {
|
|
2542
|
-
const text = String(normalized || '').trim();
|
|
2543
|
-
return text ? text.split(' ') : [];
|
|
2544
|
-
}
|
|
2545
|
-
|
|
2546
|
-
function _intentHasAnyToken(normalized, terms) {
|
|
2547
|
-
const tokens = new Set(_intentTokens(normalized));
|
|
2548
|
-
return terms.some(term => tokens.has(term));
|
|
2549
|
-
}
|
|
2550
|
-
|
|
2551
|
-
function _intentHasPhrase(normalized, phrase) {
|
|
2552
|
-
return String(normalized || '').includes(` ${phrase} `);
|
|
2553
|
-
}
|
|
2554
|
-
|
|
2555
|
-
function _intentHasAnyPhrase(normalized, phrases) {
|
|
2556
|
-
return phrases.some(phrase => _intentHasPhrase(normalized, phrase));
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
function _intentTokenIsNumeric(token) {
|
|
2560
|
-
if (!token) return false;
|
|
2561
|
-
for (const ch of String(token)) {
|
|
2562
|
-
const code = ch.charCodeAt(0);
|
|
2563
|
-
if (code < 48 || code > 57) return false;
|
|
2564
|
-
}
|
|
2565
|
-
return true;
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
function _intentHasVerbObject(normalized, verbs, objects) {
|
|
2569
|
-
const verbSet = new Set(verbs);
|
|
2570
|
-
const objectSet = new Set(objects);
|
|
2571
|
-
const filler = new Set(['a', 'an', 'the', 'this', 'that', 'it', 'new', 'proper', 'some']);
|
|
2572
|
-
const tokens = _intentTokens(normalized);
|
|
2573
|
-
for (let i = 0; i < tokens.length; i++) {
|
|
2574
|
-
if (!verbSet.has(tokens[i])) continue;
|
|
2575
|
-
for (let j = i + 1; j < tokens.length && j <= i + 4; j++) {
|
|
2576
|
-
if (filler.has(tokens[j])) continue;
|
|
2577
|
-
if (objectSet.has(tokens[j])) return true;
|
|
2578
|
-
break;
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
return false;
|
|
2582
|
-
}
|
|
2583
|
-
|
|
2584
|
-
function _messageRequestsPlanIntent(message) {
|
|
2585
|
-
const normalized = _normalizeIntentText(message);
|
|
2586
|
-
const tokens = _intentTokens(normalized);
|
|
2587
|
-
if (!tokens.length) return false;
|
|
2588
|
-
if (tokens[0] === 'plan') return true;
|
|
2589
|
-
if (_intentHasAnyPhrase(normalized, PLAN_CREATE_PHRASES) && _intentHasAnyToken(normalized, PLAN_INTENT_TERMS)) return true;
|
|
2590
|
-
if (_intentHasVerbObject(normalized, PLAN_CREATE_TERMS, PLAN_INTENT_TERMS)) return true;
|
|
2591
|
-
return _intentHasAnyToken(normalized, PLAN_INTENT_TERMS) &&
|
|
2592
|
-
_intentHasAnyToken(normalized, ['this', 'that', 'it', 'for', 'how', 'out']);
|
|
2593
|
-
}
|
|
2594
|
-
|
|
2595
|
-
function _messageHasDelegationIntent(message) {
|
|
2596
|
-
const normalized = _normalizeIntentText(message);
|
|
2597
|
-
if (!normalized.trim()) return false;
|
|
2598
|
-
if (_intentHasAnyToken(normalized, DELEGATION_ACTION_TERMS)) return true;
|
|
2599
|
-
if (_intentHasPhrase(normalized, 'work item') && _intentHasAnyToken(normalized, ['create', 'open', 'add'])) return true;
|
|
2600
|
-
return _intentHasAnyPhrase(normalized, DELEGATION_MINIONS_PHRASES);
|
|
2601
|
-
}
|
|
2602
|
-
|
|
2603
|
-
function _delegationTokenIsImperativeVerb(tokens, idx) {
|
|
2604
|
-
const term = tokens[idx];
|
|
2605
|
-
if (!term) return false;
|
|
2606
|
-
if (!DELEGATION_AMBIGUOUS_NOUN_TERMS.has(term)) return true;
|
|
2607
|
-
const next = tokens[idx + 1];
|
|
2608
|
-
if (!next) return true;
|
|
2609
|
-
return !DELEGATION_NON_VERB_FOLLOW_ONS.has(next);
|
|
2610
|
-
}
|
|
2611
|
-
|
|
2612
|
-
function _messageExplicitlyRequestsDelegation(message) {
|
|
2613
|
-
const normalized = _normalizeIntentText(message);
|
|
2614
|
-
const tokens = _intentTokens(normalized);
|
|
2615
|
-
if (!tokens.length) return false;
|
|
2616
|
-
if (_intentHasAnyPhrase(normalized, DELEGATION_MINIONS_PHRASES)) return true;
|
|
2617
|
-
if (_intentHasPhrase(normalized, 'work item') && _intentHasAnyToken(normalized, ['create', 'open', 'add'])) return true;
|
|
2618
|
-
|
|
2619
|
-
let i = 0;
|
|
2620
|
-
while (DELEGATION_REQUEST_PREFIX_TERMS.includes(tokens[i])) i++;
|
|
2621
|
-
if (DELEGATION_ACTION_TERMS.includes(tokens[i]) && _delegationTokenIsImperativeVerb(tokens, i)) return true;
|
|
2622
|
-
if (DELEGATION_PERSON_ACTION_TERMS.includes(tokens[i])) {
|
|
2623
|
-
return tokens.slice(i + 1, i + 7).includes('to');
|
|
2624
|
-
}
|
|
2625
|
-
for (let j = 0; j < tokens.length && j <= 6; j++) {
|
|
2626
|
-
if (!DELEGATION_ACTION_TERMS.includes(tokens[j])) continue;
|
|
2627
|
-
if (!_delegationTokenIsImperativeVerb(tokens, j)) continue;
|
|
2628
|
-
const prefix = tokens.slice(0, j);
|
|
2629
|
-
if (prefix.includes('you') || prefix.includes('minions') || prefix.includes('minion')) return true;
|
|
2630
|
-
}
|
|
2631
|
-
return false;
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
function _messageRequestsDirectHandling(message, opts = {}) {
|
|
2635
|
-
const normalized = _normalizeIntentText(message);
|
|
2636
|
-
if (!normalized.trim()) return false;
|
|
2637
|
-
if (_messageRequestsInformationOnly(message) && !_unansweredInvestigationSignal(opts)) return true;
|
|
2638
|
-
if (_intentHasAnyPhrase(normalized, DIRECT_HANDLING_PHRASES)) return true;
|
|
2639
|
-
if (_intentHasAnyToken(normalized, DIRECT_QUICK_TERMS) && _intentHasAnyToken(normalized, DIRECT_QUICK_OBJECTS)) return true;
|
|
2640
|
-
if (_intentHasVerbObject(normalized, DIRECT_REPLY_TERMS, DIRECT_REPLY_TARGETS)) return true;
|
|
2641
|
-
return _intentHasVerbObject(normalized, DIRECT_SELF_TERMS, DIRECT_SELF_TARGETS);
|
|
2642
|
-
}
|
|
2643
|
-
|
|
2644
|
-
function _unansweredInvestigationSignal({ answerText = '', toolUses = [] } = {}) {
|
|
2645
|
-
const answerLen = String(answerText || '').trim().length;
|
|
2646
|
-
const toolCount = Array.isArray(toolUses) ? toolUses.length : 0;
|
|
2647
|
-
return toolCount >= 4 && answerLen < ANSWERED_ASK_MIN_CHARS;
|
|
2648
|
-
}
|
|
2649
|
-
|
|
2650
|
-
function _messageRequestsInformationOnly(message) {
|
|
2651
|
-
const normalized = _normalizeIntentText(message);
|
|
2652
|
-
const tokens = _intentTokens(normalized);
|
|
2653
|
-
if (!tokens.length) return false;
|
|
2654
|
-
if (['what', 'which', 'who', 'when', 'where', 'why', 'how'].includes(tokens[0])) return true;
|
|
2655
|
-
if (['show', 'list'].includes(tokens[0])) return true;
|
|
2656
|
-
if (tokens[0] === 'tell' && tokens[1] === 'me') return true;
|
|
2657
|
-
if (String(message || '').trim().endsWith('?') && !_intentHasAnyToken(normalized, DELEGATION_ACTION_TERMS)) return true;
|
|
2658
|
-
return false;
|
|
2659
|
-
}
|
|
2660
|
-
|
|
2661
|
-
function _messageIsSmallDocOnlyRequest(normalized) {
|
|
2662
|
-
if (_intentHasAnyToken(normalized, DOC_CHAT_DIRECT_DOC_TERMS)) return true;
|
|
2663
|
-
return _intentHasVerbObject(normalized, DOC_CHAT_DOC_ACTION_TERMS, DOC_CHAT_DOC_OBJECTS);
|
|
2664
|
-
}
|
|
2665
|
-
|
|
2666
|
-
function _messageHasMediumLargeWorkIntent(message, { source = 'command-center' } = {}) {
|
|
2667
|
-
const normalized = _normalizeIntentText(message);
|
|
2668
|
-
if (!normalized.trim()) return false;
|
|
2669
|
-
if (_messageRequestsDirectHandling(message)) return false;
|
|
2670
|
-
if (source === 'doc-chat' && _messageIsSmallDocOnlyRequest(normalized)) return false;
|
|
2671
|
-
return _intentHasAnyToken(normalized, MEDIUM_INVESTIGATION_TERMS) ||
|
|
2672
|
-
_intentHasAnyPhrase(normalized, MEDIUM_INVESTIGATION_PHRASES) ||
|
|
2673
|
-
_intentHasAnyToken(normalized, MEDIUM_IMPLEMENT_TERMS) ||
|
|
2674
|
-
_intentHasVerbObject(normalized, MEDIUM_BUILD_VERBS, MEDIUM_BUILD_OBJECTS) ||
|
|
2675
|
-
_intentHasAnyToken(normalized, MEDIUM_REVIEW_TERMS) ||
|
|
2676
|
-
_intentHasAnyToken(normalized, MEDIUM_SIZE_TERMS) ||
|
|
2677
|
-
_intentHasAnyPhrase(normalized, MEDIUM_SIZE_PHRASES);
|
|
2678
|
-
}
|
|
2679
|
-
|
|
2680
|
-
function _inferDelegatedWorkType(message) {
|
|
2681
|
-
const normalized = _normalizeIntentText(message);
|
|
2682
|
-
if (_intentHasAnyToken(normalized, ['test', 'verify', 'validate']) || _intentHasPhrase(normalized, 'build and test')) return 'test';
|
|
2683
|
-
if (_intentHasAnyToken(normalized, ['review']) || _intentHasPhrase(normalized, 'code review')) return 'review';
|
|
2684
|
-
if (_intentHasAnyToken(normalized, ['fix', 'debug', 'repair', 'bug', 'broken', 'failing', 'failure', 'regression']) || _intentHasPhrase(normalized, 'root cause')) return 'fix';
|
|
2685
|
-
if (_intentHasAnyToken(normalized, ['implement', 'refactor', 'migrate', 'harden', 'redesign']) ||
|
|
2686
|
-
_intentHasVerbObject(normalized, MEDIUM_BUILD_VERBS, MEDIUM_BUILD_OBJECTS)) return 'implement';
|
|
2687
|
-
if (_intentHasAnyToken(normalized, MEDIUM_INVESTIGATION_TERMS) || _intentHasAnyPhrase(normalized, MEDIUM_INVESTIGATION_PHRASES)) return 'explore';
|
|
2688
|
-
return 'ask';
|
|
2689
|
-
}
|
|
2690
|
-
|
|
2691
|
-
function _ccAnswerLooksCompleteForAsk(answerText, toolUses = []) {
|
|
2692
|
-
if (!Array.isArray(toolUses) || toolUses.length === 0) return false;
|
|
2693
|
-
const answer = _collapseWhitespace(stripCCActionSyntax(answerText || ''));
|
|
2694
|
-
if (answer.length < ANSWERED_ASK_MIN_CHARS) return false;
|
|
2695
|
-
const normalized = _normalizeIntentText(answer);
|
|
2696
|
-
if (_intentHasAnyPhrase(normalized, ANSWERED_ASK_INCOMPLETE_PHRASES)) return false;
|
|
2697
|
-
if (_intentHasVerbObject(normalized, ['need', 'needs', 'require', 'requires'], ['agent', 'delegation', 'investigation', 'research'])) return false;
|
|
2698
|
-
return true;
|
|
2699
|
-
}
|
|
2700
|
-
|
|
2701
|
-
function _ccShouldSuppressAnsweredAskDispatch(action, opts = {}) {
|
|
2702
|
-
const normalized = normalizeCCAction(action);
|
|
2703
|
-
if (normalized?.type !== 'dispatch') return false;
|
|
2704
|
-
if (String(normalized.workType || '').trim().toLowerCase() !== 'ask') return false;
|
|
2705
|
-
if (_messageExplicitlyRequestsDelegation(opts.message)) return false;
|
|
2706
|
-
return _ccAnswerLooksCompleteForAsk(opts.answerText, opts.toolUses);
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
function _collapseWhitespace(text) {
|
|
2710
|
-
let out = '';
|
|
2711
|
-
let lastWasSpace = true;
|
|
2712
|
-
for (const ch of String(text || '')) {
|
|
2713
|
-
const isSpace = ch === ' ' || ch === '\n' || ch === '\r' || ch === '\t';
|
|
2714
|
-
if (isSpace) {
|
|
2715
|
-
if (!lastWasSpace) out += ' ';
|
|
2716
|
-
lastWasSpace = true;
|
|
2717
|
-
} else {
|
|
2718
|
-
out += ch;
|
|
2719
|
-
lastWasSpace = false;
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
return out.trim();
|
|
2723
|
-
}
|
|
2724
|
-
|
|
2725
|
-
function _stripEdgeQuotes(text) {
|
|
2726
|
-
const quotes = new Set(['"', "'", '`']);
|
|
2727
|
-
let start = 0;
|
|
2728
|
-
let end = String(text || '').length;
|
|
2729
|
-
while (start < end && quotes.has(text[start])) start++;
|
|
2730
|
-
while (end > start && quotes.has(text[end - 1])) end--;
|
|
2731
|
-
return text.slice(start, end);
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
|
-
function _delegatedWorkTitle(message, workType) {
|
|
2735
|
-
const compact = _stripEdgeQuotes(_collapseWhitespace(String(message || '')));
|
|
2736
|
-
const trimmed = compact.length > 90 ? compact.slice(0, 87).trimEnd() + '...' : compact;
|
|
2737
|
-
const fallback = {
|
|
2738
|
-
fix: 'Fix requested issue',
|
|
2739
|
-
review: 'Review requested work',
|
|
2740
|
-
test: 'Test requested work',
|
|
2741
|
-
implement: 'Implement requested work',
|
|
2742
|
-
'implement:large': 'Implement requested work',
|
|
2743
|
-
explore: 'Investigate requested work',
|
|
2744
|
-
ask: 'Answer requested query',
|
|
2745
|
-
verify: 'Verify requested work',
|
|
2746
|
-
}[workType] || 'Handle requested work';
|
|
2747
|
-
if (!trimmed) return fallback;
|
|
2748
|
-
const prefix = {
|
|
2749
|
-
fix: 'Fix',
|
|
2750
|
-
review: 'Review',
|
|
2751
|
-
test: 'Test',
|
|
2752
|
-
implement: 'Implement',
|
|
2753
|
-
'implement:large': 'Implement',
|
|
2754
|
-
explore: 'Investigate',
|
|
2755
|
-
ask: 'Answer',
|
|
2756
|
-
verify: 'Verify',
|
|
2757
|
-
}[workType] || 'Handle';
|
|
2758
|
-
const lower = trimmed.toLowerCase();
|
|
2759
|
-
const prefixLower = prefix.toLowerCase();
|
|
2760
|
-
if (lower === prefixLower || lower.startsWith(prefixLower + ' ')) return trimmed;
|
|
2761
|
-
return `${prefix}: ${trimmed}`;
|
|
2762
|
-
}
|
|
2763
|
-
|
|
2764
|
-
function _priorityFromDelegationMessage(message) {
|
|
2765
|
-
const normalized = _normalizeIntentText(message);
|
|
2766
|
-
if (_intentHasAnyToken(normalized, ['urgent', 'asap', 'critical', 'blocker', 'p0', 'p1']) || _intentHasPhrase(normalized, 'high priority')) return 'high';
|
|
2767
|
-
if (_intentHasPhrase(normalized, 'low priority') || _intentHasPhrase(normalized, 'when you can') || _intentHasAnyToken(normalized, ['whenever'])) return 'low';
|
|
2768
|
-
return 'medium';
|
|
2769
|
-
}
|
|
2770
|
-
|
|
2771
|
-
function _inferDelegationActionFromMessage(message, { source = 'command-center', filePath = null, title = null, answerText = '', toolUses = [] } = {}) {
|
|
2772
|
-
if (_messageRequestsDirectHandling(message, { answerText, toolUses })) return null;
|
|
2773
|
-
const explicit = _messageHasDelegationIntent(message);
|
|
2774
|
-
const mediumLarge = _messageHasMediumLargeWorkIntent(message, { source });
|
|
2775
|
-
const toolCount = Array.isArray(toolUses) ? toolUses.length : 0;
|
|
2776
|
-
if (!explicit && !mediumLarge && toolCount < 4) return null;
|
|
2777
|
-
|
|
2778
|
-
const workType = _inferDelegatedWorkType(message);
|
|
2779
|
-
const inferredProject = source === 'doc-chat' && filePath && typeof _inferDocChatProject === 'function'
|
|
2780
|
-
? _inferDocChatProject(filePath)
|
|
2781
|
-
: null;
|
|
2782
|
-
const context = [];
|
|
2783
|
-
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.`);
|
|
2784
|
-
if (filePath) context.push(`Document: ${filePath}`);
|
|
2785
|
-
if (title && title !== filePath) context.push(`Title: ${title}`);
|
|
2786
|
-
if (answerText && String(answerText).trim()) context.push(`Assistant draft before delegation:\n${String(answerText).trim().slice(0, 1200)}`);
|
|
2787
|
-
context.push(`Original request:\n${String(message || '').trim()}`);
|
|
2788
|
-
|
|
2789
|
-
return {
|
|
2790
|
-
type: 'dispatch',
|
|
2791
|
-
title: _delegatedWorkTitle(message, workType),
|
|
2792
|
-
workType,
|
|
2793
|
-
priority: _priorityFromDelegationMessage(message),
|
|
2794
|
-
description: context.join('\n\n'),
|
|
2795
|
-
...(inferredProject ? { project: inferredProject } : {}),
|
|
2796
|
-
_autoDelegated: true,
|
|
2797
|
-
};
|
|
2798
|
-
}
|
|
2799
|
-
|
|
2800
|
-
function _ensureDelegationForIntent(actions, opts = {}) {
|
|
2801
|
-
const list = (Array.isArray(actions) ? actions.map(normalizeCCAction) : [])
|
|
2802
|
-
.filter(action => !_ccShouldSuppressAnsweredAskDispatch(action, opts));
|
|
2803
|
-
if (_messageRequestsDirectHandling(opts.message, opts)) return list.filter(a => normalizeCCAction(a)?.type !== 'dispatch');
|
|
2804
|
-
if (list.some(a => normalizeCCAction(a)?.type === 'dispatch')) return list;
|
|
2805
|
-
if (list.length > 0) return list;
|
|
2806
|
-
const inferred = _inferDelegationActionFromMessage(opts.message, opts);
|
|
2807
|
-
if (_ccShouldSuppressAnsweredAskDispatch(inferred, opts)) return list;
|
|
2808
|
-
return inferred ? [...list, inferred] : list;
|
|
2809
|
-
}
|
|
2810
|
-
|
|
2811
|
-
function stripCCActionSyntax(text) {
|
|
2812
|
-
if (!text) return '';
|
|
2813
|
-
let displayText = text;
|
|
2814
|
-
const header = findCCActionsHeader(text);
|
|
2815
|
-
if (header) {
|
|
2816
|
-
displayText = text.slice(0, header.index).trim();
|
|
2817
|
-
} else {
|
|
2818
|
-
// C-2: doc-chat streaming must also catch partial ===ACTIONS===
|
|
2819
|
-
// delimiters at the chunk tail — without this, a tail like `===ACT`
|
|
2820
|
-
// leaks raw to the modal until the next chunk completes the header.
|
|
2821
|
-
const partialIdx = findCCActionsPartialDelimiter(displayText);
|
|
2822
|
-
if (partialIdx >= 0) displayText = displayText.slice(0, partialIdx).trimEnd();
|
|
2823
|
-
}
|
|
2824
|
-
return displayText.replace(/`{3,}\s*action\s*\r?\n[\s\S]*?`{3,}\n?/g, '').trim();
|
|
2825
|
-
}
|
|
2826
|
-
|
|
2827
|
-
function _escapeRegExp(str) {
|
|
2828
|
-
return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
function findLineBoundedDelimiter(text, delimiter) {
|
|
2832
|
-
const re = new RegExp(`(?:^|\\r?\\n)${_escapeRegExp(delimiter)}[ \\t]*(?=\\r?\\n|$)`);
|
|
2833
|
-
const match = re.exec(text || '');
|
|
2834
|
-
if (!match) return null;
|
|
2835
|
-
return {
|
|
2836
|
-
index: match.index + match[0].indexOf(delimiter),
|
|
2837
|
-
length: delimiter.length,
|
|
2838
|
-
};
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
|
-
function findDocChatDocumentDelimiter(text) {
|
|
2842
|
-
return findLineBoundedDelimiter(text, DOC_CHAT_DOCUMENT_DELIMITER)
|
|
2843
|
-
|| findLineBoundedDelimiter(text, LEGACY_DOC_CHAT_DOCUMENT_DELIMITER);
|
|
2844
|
-
}
|
|
2845
|
-
|
|
2846
|
-
function markdownFenceFor(content) {
|
|
2847
|
-
const runs = String(content || '').match(/`+/g) || [];
|
|
2848
|
-
const maxRun = runs.reduce((max, run) => Math.max(max, run.length), 0);
|
|
2849
|
-
return '`'.repeat(Math.max(4, maxRun + 1));
|
|
2850
|
-
}
|
|
2851
|
-
|
|
2852
|
-
function fencedUntrustedBlock(label, content) {
|
|
2853
|
-
const value = String(content || '');
|
|
2854
|
-
const fence = markdownFenceFor(value);
|
|
2855
|
-
return `### ${label}\n${fence}text\n${value}\n${fence}`;
|
|
2856
|
-
}
|
|
2857
|
-
|
|
2858
|
-
function _messageExplicitlyRequestsMonitoring(message) {
|
|
2859
|
-
const text = String(message || '').toLowerCase();
|
|
2860
|
-
if (!text.trim()) return false;
|
|
2861
|
-
if (/\b(?:do\s+not|don't|dont|never|without|no\s+need\s+to)\s+(?:monitor(?:ing)?|watch(?:ing)?|poll(?:ing)?|check(?:ing)?|notify(?:ing)?|ping(?:ing)?|keep(?:ing)?\s+an\s+eye)\b/.test(text)) {
|
|
2862
|
-
return false;
|
|
2863
|
-
}
|
|
2864
|
-
return [
|
|
2865
|
-
/\bmonitor(?:ing)?\b/,
|
|
2866
|
-
/\bwatch(?:ing)?\b/,
|
|
2867
|
-
/\bkeep(?:ing)?\s+an\s+eye\s+on\b/,
|
|
2868
|
-
/\bcheck(?:ing)?\s+(?:it|this|that|[^.?!\n]+)\s+periodically\b/,
|
|
2869
|
-
/\bperiodically\s+check\b/,
|
|
2870
|
-
/\bcheck(?:ing)?\s+[^.?!\n]+\bevery\s+\d+\s*(?:s|sec|seconds?|m|min|minutes?|h|hr|hours?)\b/,
|
|
2871
|
-
/\bevery\s+\d+\s*(?:s|sec|seconds?|m|min|minutes?|h|hr|hours?)\b[^.?!\n]+\b(?:check|poll|monitor|watch)\b/,
|
|
2872
|
-
/\bping\s+(?:me\s+)?(?:on|when|after)\b/,
|
|
2873
|
-
/\bping\s+on\s+completion\b/,
|
|
2874
|
-
/\bnotify\s+(?:me\s+)?(?:on|when|after)\b/,
|
|
2875
|
-
/\blet\s+me\s+know\s+(?:when|once|if)\b/,
|
|
2876
|
-
/\bpoll(?:ing)?\b/,
|
|
2877
|
-
].some(pattern => pattern.test(text));
|
|
2878
|
-
}
|
|
2879
|
-
|
|
2880
|
-
function _isDispatchLikeCCAction(action) {
|
|
2881
|
-
const type = String(action?.type || '').trim().toLowerCase();
|
|
2882
|
-
return type === 'dispatch' || CC_DISPATCH_ACTION_ALIASES.has(type);
|
|
2883
|
-
}
|
|
2884
|
-
|
|
2885
|
-
function _filterImplicitPostDispatchActions(actions, humanMessage) {
|
|
2886
|
-
if (!Array.isArray(actions) || actions.length === 0) return [];
|
|
2887
|
-
if (_messageExplicitlyRequestsMonitoring(humanMessage)) return actions;
|
|
2888
|
-
let sawDispatch = false;
|
|
2889
|
-
const filtered = [];
|
|
2890
|
-
for (const action of actions) {
|
|
2891
|
-
if (_isDispatchLikeCCAction(action)) {
|
|
2892
|
-
sawDispatch = true;
|
|
2893
|
-
filtered.push(action);
|
|
2894
|
-
continue;
|
|
2895
|
-
}
|
|
2896
|
-
if (!sawDispatch) filtered.push(action);
|
|
2897
|
-
}
|
|
2898
|
-
return filtered;
|
|
2899
|
-
}
|
|
2900
|
-
|
|
2901
|
-
// ── /loop → create-watch safety net ──────────────────────────────────────────
|
|
2902
|
-
// CC sometimes invokes the /loop skill instead of emitting a create-watch action.
|
|
2903
|
-
// This pure function detects /loop invocation in CC response text and synthesizes
|
|
2904
|
-
// a create-watch action as a fallback. Returns null if no conversion needed.
|
|
2905
|
-
|
|
2906
|
-
function _detectLoopInvocation(text, actions, toolUses, humanMessage) {
|
|
2907
|
-
const observedToolUses = Array.isArray(toolUses) ? toolUses : [];
|
|
2908
|
-
if (!text && observedToolUses.length === 0) return null;
|
|
2909
|
-
// If a create-watch action was already emitted, no fallback needed
|
|
2910
|
-
if (actions && actions.some(a => a.type === 'create-watch')) return null;
|
|
2911
|
-
if (humanMessage !== undefined && !_messageExplicitlyRequestsMonitoring(humanMessage)) return null;
|
|
2912
|
-
|
|
2913
|
-
function _extractTargetFromValue(value, keyHint) {
|
|
2914
|
-
if (value == null) return null;
|
|
2915
|
-
const hint = String(keyHint || '').toLowerCase();
|
|
2916
|
-
if (Array.isArray(value)) {
|
|
2917
|
-
for (const item of value) {
|
|
2918
|
-
const nested = _extractTargetFromValue(item, hint);
|
|
2919
|
-
if (nested) return nested;
|
|
2920
|
-
}
|
|
2921
|
-
return null;
|
|
2922
|
-
}
|
|
2923
|
-
if (typeof value === 'object') {
|
|
2924
|
-
for (const [k, v] of Object.entries(value)) {
|
|
2925
|
-
const nested = _extractTargetFromValue(v, k);
|
|
2926
|
-
if (nested) return nested;
|
|
2927
|
-
}
|
|
2928
|
-
return null;
|
|
2929
|
-
}
|
|
2930
|
-
const str = String(value).trim();
|
|
2931
|
-
if (!str) return null;
|
|
2932
|
-
const prUrlMatch = str.match(/\/pull\/(\d+)\b/i) || str.match(/\/pullrequest\/(\d+)\b/i);
|
|
2933
|
-
if (prUrlMatch) return { target: prUrlMatch[1], targetType: 'pr' };
|
|
2934
|
-
const prMatch = str.match(/\bPR[- #:]?(\d+)\b/i) || str.match(/\bpull[- ]request[- #:]?(\d+)\b/i);
|
|
2935
|
-
if (prMatch) return { target: prMatch[1], targetType: 'pr' };
|
|
2936
|
-
const wiMatch = str.match(/\bW-([a-z0-9]+)\b/i);
|
|
2937
|
-
if (wiMatch) return { target: 'W-' + wiMatch[1], targetType: 'work-item' };
|
|
2938
|
-
if ((hint.includes('pr') || hint.includes('pull')) && /^\d+$/.test(str)) return { target: str, targetType: 'pr' };
|
|
2939
|
-
if ((hint.includes('work') || hint.includes('item') || hint === 'id') && /^W-[a-z0-9]+$/i.test(str)) return { target: str.toUpperCase().startsWith('W-') ? 'W-' + str.slice(2) : str, targetType: 'work-item' };
|
|
2940
|
-
if (hint.includes('target')) {
|
|
2941
|
-
if (/^\d+$/.test(str)) return { target: str, targetType: 'pr' };
|
|
2942
|
-
if (/^W-[a-z0-9]+$/i.test(str)) return { target: 'W-' + str.slice(2), targetType: 'work-item' };
|
|
2943
|
-
}
|
|
2944
|
-
return null;
|
|
2945
|
-
}
|
|
2946
|
-
|
|
2947
|
-
function _extractIntervalFromValue(value, keyHint) {
|
|
2948
|
-
if (value == null) return null;
|
|
2949
|
-
const hint = String(keyHint || '').toLowerCase();
|
|
2950
|
-
if (Array.isArray(value)) {
|
|
2951
|
-
for (const item of value) {
|
|
2952
|
-
const nested = _extractIntervalFromValue(item, hint);
|
|
2953
|
-
if (nested) return nested;
|
|
2954
|
-
}
|
|
2955
|
-
return null;
|
|
2956
|
-
}
|
|
2957
|
-
if (typeof value === 'object') {
|
|
2958
|
-
for (const [k, v] of Object.entries(value)) {
|
|
2959
|
-
const nested = _extractIntervalFromValue(v, k);
|
|
2960
|
-
if (nested) return nested;
|
|
2961
|
-
}
|
|
2962
|
-
return null;
|
|
2963
|
-
}
|
|
2964
|
-
if (!(hint.includes('interval') || hint.includes('every') || hint.includes('frequency'))) return null;
|
|
2965
|
-
const str = String(value).trim().toLowerCase();
|
|
2966
|
-
if (!str) return null;
|
|
2967
|
-
if (/^\d+$/.test(str)) return str;
|
|
2968
|
-
const match = str.match(/^(\d+(?:\.\d+)?)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?)$/i);
|
|
2969
|
-
if (!match) return null;
|
|
2970
|
-
return match[1] + match[2][0].toLowerCase();
|
|
2971
|
-
}
|
|
2972
|
-
|
|
2973
|
-
function _extractConditionFromValue(value, keyHint) {
|
|
2974
|
-
if (value == null) return null;
|
|
2975
|
-
const hint = String(keyHint || '').toLowerCase();
|
|
2976
|
-
if (Array.isArray(value)) {
|
|
2977
|
-
for (const item of value) {
|
|
2978
|
-
const nested = _extractConditionFromValue(item, hint);
|
|
2979
|
-
if (nested) return nested;
|
|
2980
|
-
}
|
|
2981
|
-
return null;
|
|
2982
|
-
}
|
|
2983
|
-
if (typeof value === 'object') {
|
|
2984
|
-
for (const [k, v] of Object.entries(value)) {
|
|
2985
|
-
const nested = _extractConditionFromValue(v, k);
|
|
2986
|
-
if (nested) return nested;
|
|
2987
|
-
}
|
|
2988
|
-
return null;
|
|
2989
|
-
}
|
|
2990
|
-
if (!(hint.includes('condition') || hint.includes('until') || hint.includes('goal') || hint.includes('status'))) return null;
|
|
2991
|
-
const str = String(value).trim().toLowerCase();
|
|
2992
|
-
if (['merged', 'build-pass', 'build-fail', 'completed', 'failed', 'status-change', 'any', 'new-comments', 'vote-change'].includes(str)) return str;
|
|
2993
|
-
if (/\b(?:pass(?:es|ing|ed)?|green|succeed(?:s|ed)?|success)\b/i.test(str)) return 'build-pass';
|
|
2994
|
-
if (/\b(?:fail(?:s|ing|ed)?|red|broken|broke)\b/i.test(str)) return 'build-fail';
|
|
2995
|
-
if (/\bmerge(?:d)?\b/i.test(str)) return 'merged';
|
|
2996
|
-
if (/\bcomplete(?:d)?\b/i.test(str)) return 'completed';
|
|
2997
|
-
if (/\bfail(?:ed)?\b/i.test(str)) return 'failed';
|
|
2998
|
-
if (/\bcomment/i.test(str)) return 'new-comments';
|
|
2999
|
-
if (/\bvote|review/i.test(str)) return 'vote-change';
|
|
3000
|
-
if (/\bstatus/i.test(str)) return 'any';
|
|
3001
|
-
return null;
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
|
-
const loopToolSeen = observedToolUses.some(t => /\bloop\b/i.test(String(t?.name || '')));
|
|
3005
|
-
const toolText = observedToolUses.map(t => {
|
|
3006
|
-
try { return [String(t?.name || ''), JSON.stringify(t?.input || {})].filter(Boolean).join(' '); }
|
|
3007
|
-
catch { return String(t?.name || ''); }
|
|
3008
|
-
}).join('\n');
|
|
3009
|
-
const combinedText = [text || '', toolText].filter(Boolean).join('\n');
|
|
3010
|
-
|
|
3011
|
-
// Check for /loop invocation patterns in CC response
|
|
3012
|
-
const loopPatterns = [
|
|
3013
|
-
/\/loop\b/i,
|
|
3014
|
-
/\bloop skill\b/i,
|
|
3015
|
-
/\bSkill.*\bloop\b/i,
|
|
3016
|
-
/\bstarted.*\bloop\b/i,
|
|
3017
|
-
/\bmonitoring.*\bloop\b/i,
|
|
3018
|
-
/\binvok(?:e|ed|ing).*\bloop\b/i,
|
|
3019
|
-
];
|
|
3020
|
-
if (!loopToolSeen && !loopPatterns.some(p => p.test(combinedText))) return null;
|
|
3021
|
-
|
|
3022
|
-
// Extract target — PR number or work item ID
|
|
3023
|
-
const directTarget = observedToolUses.map(t => _extractTargetFromValue(t && t.input, t && t.name)).find(Boolean);
|
|
3024
|
-
const prMatch = combinedText.match(/\/pull\/(\d+)\b/i) ||
|
|
3025
|
-
combinedText.match(/\/pullrequest\/(\d+)\b/i) ||
|
|
3026
|
-
combinedText.match(/\bPR[- #:]?(\d+)\b/i) ||
|
|
3027
|
-
combinedText.match(/\bpull[- ]request[- #:]?(\d+)/i);
|
|
3028
|
-
const wiMatch = combinedText.match(/\bW-([a-z0-9]+)\b/i);
|
|
3029
|
-
|
|
3030
|
-
let target = null, targetType = 'pr';
|
|
3031
|
-
if (directTarget) {
|
|
3032
|
-
target = directTarget.target;
|
|
3033
|
-
targetType = directTarget.targetType;
|
|
3034
|
-
} else if (prMatch) {
|
|
3035
|
-
target = prMatch[1];
|
|
3036
|
-
targetType = 'pr';
|
|
3037
|
-
} else if (wiMatch) {
|
|
3038
|
-
target = 'W-' + wiMatch[1];
|
|
3039
|
-
targetType = 'work-item';
|
|
3040
|
-
}
|
|
3041
|
-
if (!target) return null; // Can't synthesize without a target
|
|
3042
|
-
|
|
3043
|
-
// Extract interval (e.g. "every 15 minutes", "every 5m")
|
|
3044
|
-
const directInterval = observedToolUses.map(t => _extractIntervalFromValue(t && t.input, t && t.name)).find(Boolean);
|
|
3045
|
-
const intervalMatch = combinedText.match(/every\s+(\d+)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?)\b/i);
|
|
3046
|
-
let interval = '5m';
|
|
3047
|
-
if (directInterval) interval = directInterval;
|
|
3048
|
-
else if (intervalMatch) interval = intervalMatch[1] + intervalMatch[2][0];
|
|
3049
|
-
|
|
3050
|
-
// Infer condition from keywords
|
|
3051
|
-
let condition = observedToolUses.map(t => _extractConditionFromValue(t && t.input, t && t.name)).find(Boolean) || 'any';
|
|
3052
|
-
if (condition === 'any') {
|
|
3053
|
-
if (/\bbuild\b/i.test(combinedText) && /\b(?:pass(?:es|ing|ed)?|green|succeed(?:s|ed)?|success)\b/i.test(combinedText)) condition = 'build-pass';
|
|
3054
|
-
else if (/\bbuild\b/i.test(combinedText) && /\b(?:fail(?:s|ing|ed)?|red|broken|broke)\b/i.test(combinedText)) condition = 'build-fail';
|
|
3055
|
-
else if (/\bmerge[d]?\b/i.test(combinedText)) condition = 'merged';
|
|
3056
|
-
else if (/\bcomplete[d]?\b/i.test(combinedText)) condition = 'completed';
|
|
3057
|
-
}
|
|
3058
|
-
|
|
3059
|
-
return {
|
|
3060
|
-
type: 'create-watch',
|
|
3061
|
-
target,
|
|
3062
|
-
targetType,
|
|
3063
|
-
condition,
|
|
3064
|
-
interval,
|
|
3065
|
-
owner: 'human',
|
|
3066
|
-
description: 'Auto-converted from /loop invocation',
|
|
3067
|
-
stopAfter: condition === 'any' ? 0 : 1,
|
|
3068
|
-
};
|
|
3069
|
-
}
|
|
3070
|
-
|
|
3071
|
-
function _extractToolUsesFromRaw(raw) {
|
|
3072
|
-
const toolUses = [];
|
|
3073
|
-
if (!raw) return toolUses;
|
|
3074
|
-
for (const line of String(raw).split('\n')) {
|
|
3075
|
-
const trimmed = line.trim();
|
|
3076
|
-
if (!trimmed || !trimmed.startsWith('{')) continue;
|
|
3077
|
-
try {
|
|
3078
|
-
const obj = JSON.parse(trimmed);
|
|
3079
|
-
if (obj.type !== 'assistant' || !Array.isArray(obj.message?.content)) continue;
|
|
3080
|
-
for (const block of obj.message.content) {
|
|
3081
|
-
if (block?.type === 'tool_use' && block.name) toolUses.push({ name: block.name, input: block.input || {} });
|
|
3082
|
-
}
|
|
3083
|
-
} catch {}
|
|
3084
|
-
}
|
|
3085
|
-
return toolUses;
|
|
3086
|
-
}
|
|
3087
|
-
|
|
3088
|
-
// ── Server-side CC action execution ──────────────────────────────────────────
|
|
3089
|
-
// Actions are executed server-side so all clients (frontend, curl, Teams) get the same behavior.
|
|
3090
|
-
// The frontend still shows status toasts but no longer needs to fire the API calls.
|
|
3091
|
-
|
|
3092
|
-
// Parse interval from CC action — accepts ms number, "15m", "1h", "30s", or null (default 5m).
|
|
3093
|
-
function _parseWatchInterval(val) {
|
|
3094
|
-
if (!val) return 300000;
|
|
3095
|
-
if (typeof val === 'number') return Math.max(60000, val);
|
|
3096
|
-
const s = String(val).trim().toLowerCase();
|
|
3097
|
-
if (/^\d+$/.test(s)) { const n = parseInt(s, 10); return Math.max(60000, n >= 1000 ? n : n * 1000); }
|
|
3098
|
-
const m = s.match(/^(\d+(?:\.\d+)?)\s*(s|sec|m|min|h|hr|hours?)$/);
|
|
3099
|
-
if (!m) return 300000;
|
|
3100
|
-
const n = parseFloat(m[1]), u = m[2][0];
|
|
3101
|
-
return Math.max(60000, Math.round(u === 's' ? n * 1000 : u === 'm' ? n * 60000 : n * 3600000));
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3104
|
-
function normalizeMeetingParticipants(participants) {
|
|
3105
|
-
if (!Array.isArray(participants)) return [];
|
|
3106
|
-
const seen = new Set();
|
|
3107
|
-
const normalized = [];
|
|
3108
|
-
for (const participant of participants) {
|
|
3109
|
-
const id = String(participant || '').trim();
|
|
3110
|
-
if (!id || seen.has(id)) continue;
|
|
3111
|
-
seen.add(id);
|
|
3112
|
-
normalized.push(id);
|
|
3113
|
-
}
|
|
3114
|
-
return normalized;
|
|
3115
|
-
}
|
|
3116
|
-
|
|
3117
|
-
function meetingParticipantsFromAction(action) {
|
|
3118
|
-
return normalizeMeetingParticipants(
|
|
3119
|
-
Array.isArray(action?.participants) && action.participants.length > 0
|
|
3120
|
-
? action.participants
|
|
3121
|
-
: action?.agents
|
|
3122
|
-
);
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
|
-
function normalizePipelineForCompare(pipeline) {
|
|
3126
|
-
if (!pipeline || typeof pipeline !== 'object') return null;
|
|
3127
|
-
return {
|
|
3128
|
-
title: pipeline.title || '',
|
|
3129
|
-
stages: Array.isArray(pipeline.stages) ? pipeline.stages : [],
|
|
3130
|
-
trigger: pipeline.trigger && typeof pipeline.trigger === 'object' ? pipeline.trigger : {},
|
|
3131
|
-
enabled: pipeline.enabled !== false,
|
|
3132
|
-
project: pipeline.project !== undefined ? pipeline.project : null,
|
|
3133
|
-
projects: Array.isArray(pipeline.projects) ? pipeline.projects : [],
|
|
3134
|
-
stopWhen: pipeline.stopWhen || null,
|
|
3135
|
-
monitoredResources: Array.isArray(pipeline.monitoredResources) ? pipeline.monitoredResources : [],
|
|
3136
|
-
};
|
|
3137
|
-
}
|
|
3138
|
-
|
|
3139
|
-
function buildPipelineFromAction(action) {
|
|
3140
|
-
const pipeline = {
|
|
3141
|
-
id: String(action.id || '').trim(),
|
|
3142
|
-
title: String(action.title || '').trim(),
|
|
3143
|
-
stages: action.stages,
|
|
3144
|
-
trigger: action.trigger && typeof action.trigger === 'object' ? action.trigger : {},
|
|
3145
|
-
enabled: action.enabled !== false,
|
|
3146
|
-
};
|
|
3147
|
-
if (action.project !== undefined) pipeline.project = action.project;
|
|
3148
|
-
if (Array.isArray(action.projects)) pipeline.projects = action.projects;
|
|
3149
|
-
if (action.stopWhen) pipeline.stopWhen = action.stopWhen;
|
|
3150
|
-
if (Array.isArray(action.monitoredResources) && action.monitoredResources.length > 0) {
|
|
3151
|
-
pipeline.monitoredResources = action.monitoredResources;
|
|
3152
|
-
}
|
|
3153
|
-
return pipeline;
|
|
3154
|
-
}
|
|
3155
|
-
|
|
3156
|
-
function pipelineDefinitionsEqual(a, b) {
|
|
3157
|
-
return JSON.stringify(normalizePipelineForCompare(a)) === JSON.stringify(normalizePipelineForCompare(b));
|
|
3158
|
-
}
|
|
3159
|
-
|
|
3160
|
-
function createPipelineFromAction(action) {
|
|
3161
|
-
const { savePipeline, getPipeline } = require('./engine/pipeline');
|
|
3162
|
-
const pipeline = buildPipelineFromAction(action);
|
|
3163
|
-
const projectError = validatePipelineProjects(pipeline);
|
|
3164
|
-
if (projectError) {
|
|
3165
|
-
return { type: 'create-pipeline', id: pipeline.id, error: projectError };
|
|
3166
|
-
}
|
|
3167
|
-
const existing = getPipeline(pipeline.id);
|
|
3168
|
-
if (existing) {
|
|
3169
|
-
if (pipelineDefinitionsEqual(existing, pipeline)) {
|
|
3170
|
-
return {
|
|
3171
|
-
type: 'create-pipeline',
|
|
3172
|
-
id: pipeline.id,
|
|
3173
|
-
ok: true,
|
|
3174
|
-
duplicate: true,
|
|
3175
|
-
duplicateOf: pipeline.id,
|
|
3176
|
-
warning: `Pipeline "${pipeline.id}" already exists; no changes made.`,
|
|
3177
|
-
};
|
|
3178
|
-
}
|
|
3179
|
-
return {
|
|
3180
|
-
type: 'create-pipeline',
|
|
3181
|
-
id: pipeline.id,
|
|
3182
|
-
error: `Pipeline "${pipeline.id}" already exists with a different definition. Use edit-pipeline to update it.`,
|
|
3183
|
-
};
|
|
3184
|
-
}
|
|
3185
|
-
savePipeline(pipeline);
|
|
3186
|
-
const persisted = getPipeline(pipeline.id);
|
|
3187
|
-
if (!persisted) {
|
|
3188
|
-
return { type: 'create-pipeline', id: pipeline.id, error: `Pipeline "${pipeline.id}" was not persisted.` };
|
|
3189
|
-
}
|
|
3190
|
-
if (!pipelineDefinitionsEqual(persisted, pipeline)) {
|
|
3191
|
-
return { type: 'create-pipeline', id: pipeline.id, error: `Pipeline "${pipeline.id}" persisted with unexpected contents.` };
|
|
3192
|
-
}
|
|
3193
|
-
invalidateStatusCache();
|
|
3194
|
-
return { type: 'create-pipeline', id: pipeline.id, ok: true, created: true };
|
|
3195
|
-
}
|
|
3196
|
-
|
|
3197
|
-
// Required-field validator for CC actions. Returns null when valid, an error string when not.
|
|
3198
|
-
// Centralises field-required checks so the model can't quietly emit a malformed action and have
|
|
3199
|
-
// the server silently fall back to placeholder values (e.g. "Untitled"). The handler invokes this
|
|
3200
|
-
// before `try` to avoid filling `results` with cryptic per-handler error messages.
|
|
3201
|
-
function _ccValidateAction(action) {
|
|
3202
|
-
if (!action || typeof action !== 'object' || !action.type) return 'action is missing required field: type';
|
|
3203
|
-
const normalized = normalizeCCAction(action);
|
|
3204
|
-
switch (normalized.type) {
|
|
3205
|
-
case 'dispatch':
|
|
3206
|
-
if (!normalized.title || typeof normalized.title !== 'string' || !normalized.title.trim()) return `${action.type} action missing required field: title`;
|
|
3207
|
-
return null;
|
|
3208
|
-
case 'implement':
|
|
3209
|
-
return 'Unsupported action type "implement"; use type="dispatch" with workType="implement".';
|
|
3210
|
-
case 'build-and-test':
|
|
3211
|
-
if (!action.pr) return 'build-and-test action missing required field: pr';
|
|
3212
|
-
return null;
|
|
3213
|
-
case 'note':
|
|
3214
|
-
if (!action.title) return 'note action missing required field: title';
|
|
3215
|
-
if (!action.content && !action.description) return 'note action missing required field: content (or description)';
|
|
3216
|
-
return null;
|
|
3217
|
-
case 'knowledge':
|
|
3218
|
-
if (!action.title) return 'knowledge action missing required field: title';
|
|
3219
|
-
if (!action.content) return 'knowledge action missing required field: content';
|
|
3220
|
-
if (!action.category) return 'knowledge action missing required field: category';
|
|
3221
|
-
return null;
|
|
3222
|
-
case 'pin-to-pinned':
|
|
3223
|
-
if (!action.title || !action.content) return 'pin-to-pinned action missing title or content';
|
|
3224
|
-
return null;
|
|
3225
|
-
case 'plan':
|
|
3226
|
-
if (!action.title) return 'plan action missing required field: title';
|
|
3227
|
-
return null;
|
|
3228
|
-
case 'create-meeting': {
|
|
3229
|
-
if (!action.title || typeof action.title !== 'string' || !action.title.trim()) return 'create-meeting action missing required field: title';
|
|
3230
|
-
if (!action.agenda || typeof action.agenda !== 'string' || !action.agenda.trim()) return 'create-meeting action missing required field: agenda';
|
|
3231
|
-
if (meetingParticipantsFromAction(action).length < 2) return 'create-meeting action requires at least 2 participants';
|
|
3232
|
-
return null;
|
|
3233
|
-
}
|
|
3234
|
-
case 'create-pipeline':
|
|
3235
|
-
if (!action.id || typeof action.id !== 'string' || !action.id.trim()) return 'create-pipeline action missing required field: id';
|
|
3236
|
-
if (!action.title || typeof action.title !== 'string' || !action.title.trim()) return 'create-pipeline action missing required field: title';
|
|
3237
|
-
if (!Array.isArray(action.stages) || action.stages.length === 0) return 'create-pipeline action requires non-empty stages array';
|
|
3238
|
-
return null;
|
|
3239
|
-
default:
|
|
3240
|
-
return null; // unknown types fall through to existing handler / generic fallback
|
|
3241
|
-
}
|
|
3242
|
-
}
|
|
3243
|
-
|
|
3244
|
-
let _ccLocalApiInvokerForTest = null;
|
|
3245
|
-
|
|
3246
|
-
function _setCcLocalApiInvokerForTest(fn) {
|
|
3247
|
-
_ccLocalApiInvokerForTest = typeof fn === 'function' ? fn : null;
|
|
3248
|
-
}
|
|
3249
|
-
|
|
3250
|
-
function _ccRouteMethodsForPath(pathname) {
|
|
3251
|
-
if (!Array.isArray(_ccApiRoutesMeta) || _ccApiRoutesMeta.length === 0) return null;
|
|
3252
|
-
const methods = new Set();
|
|
3253
|
-
for (const route of _ccApiRoutesMeta) {
|
|
3254
|
-
if (route._pathRegex instanceof RegExp) {
|
|
3255
|
-
route._pathRegex.lastIndex = 0;
|
|
3256
|
-
if (route._pathRegex.test(pathname)) methods.add(String(route.method || '').toUpperCase());
|
|
3257
|
-
} else if (route.path === pathname) {
|
|
3258
|
-
methods.add(String(route.method || '').toUpperCase());
|
|
3259
|
-
}
|
|
3260
|
-
}
|
|
3261
|
-
return methods;
|
|
3262
|
-
}
|
|
3263
|
-
|
|
3264
|
-
function _ccValidateLocalApiFallback(endpoint, method) {
|
|
3265
|
-
if (typeof endpoint !== 'string' || !endpoint.trim()) return 'generic API fallback requires endpoint';
|
|
3266
|
-
const raw = endpoint.trim();
|
|
3267
|
-
if (!(raw === '/api' || raw.startsWith('/api/'))) return 'generic API fallback endpoint must be a local /api/ path';
|
|
3268
|
-
if (/[\0\r\n\\]/.test(raw) || raw.includes('..') || /%2e/i.test(raw) || /%5c/i.test(raw)) {
|
|
3269
|
-
return 'generic API fallback endpoint is unsafe';
|
|
3270
|
-
}
|
|
3271
|
-
let parsed;
|
|
3272
|
-
try {
|
|
3273
|
-
parsed = new URL(raw, 'http://127.0.0.1');
|
|
3274
|
-
} catch {
|
|
3275
|
-
return 'generic API fallback endpoint is invalid';
|
|
3276
|
-
}
|
|
3277
|
-
if (parsed.origin !== 'http://127.0.0.1' || !(parsed.pathname === '/api' || parsed.pathname.startsWith('/api/'))) {
|
|
3278
|
-
return 'generic API fallback endpoint must be a local /api/ path';
|
|
3279
|
-
}
|
|
3280
|
-
if (CC_API_FALLBACK_BLOCKED_PREFIXES.some(prefix => parsed.pathname === prefix || parsed.pathname.startsWith(prefix + '/'))) {
|
|
3281
|
-
return 'generic API fallback cannot call Command Center, doc-chat, or bot endpoints';
|
|
3282
|
-
}
|
|
3283
|
-
if (/stream/i.test(parsed.pathname) || parsed.pathname === '/api/hot-reload') {
|
|
3284
|
-
return 'generic API fallback cannot call streaming endpoints';
|
|
3285
|
-
}
|
|
3286
|
-
const normalizedMethod = String(method || 'POST').toUpperCase();
|
|
3287
|
-
if (!CC_API_FALLBACK_METHODS.has(normalizedMethod)) {
|
|
3288
|
-
return `generic API fallback method ${normalizedMethod} is not allowed`;
|
|
3289
|
-
}
|
|
3290
|
-
const routeMethods = _ccRouteMethodsForPath(parsed.pathname);
|
|
3291
|
-
if (routeMethods && routeMethods.size > 0 && !routeMethods.has(normalizedMethod)) {
|
|
3292
|
-
return `API endpoint ${parsed.pathname} does not allow ${normalizedMethod}; allowed methods: ${[...routeMethods].join(', ')}`;
|
|
3293
|
-
}
|
|
3294
|
-
if (routeMethods && routeMethods.size === 0) {
|
|
3295
|
-
return `API endpoint ${parsed.pathname} is not in the local API index`;
|
|
3296
|
-
}
|
|
3297
|
-
return null;
|
|
3298
|
-
}
|
|
3299
|
-
|
|
3300
|
-
function _ccBuildQueryString(params) {
|
|
3301
|
-
if (!params || typeof params !== 'object' || Array.isArray(params)) return '';
|
|
3302
|
-
const search = new URLSearchParams();
|
|
3303
|
-
for (const [key, value] of Object.entries(params)) {
|
|
3304
|
-
if (value === undefined || value === null) continue;
|
|
3305
|
-
if (Array.isArray(value)) {
|
|
3306
|
-
for (const item of value) search.append(key, String(item));
|
|
3307
|
-
} else if (typeof value === 'object') {
|
|
3308
|
-
search.append(key, JSON.stringify(value));
|
|
3309
|
-
} else {
|
|
3310
|
-
search.append(key, String(value));
|
|
3311
|
-
}
|
|
3312
|
-
}
|
|
3313
|
-
const text = search.toString();
|
|
3314
|
-
return text ? '?' + text : '';
|
|
3315
|
-
}
|
|
3316
|
-
|
|
3317
|
-
function _ccRequestPath(endpoint, method, params) {
|
|
3318
|
-
const parsed = new URL(endpoint, 'http://127.0.0.1');
|
|
3319
|
-
if (method === 'GET') {
|
|
3320
|
-
const extra = _ccBuildQueryString(params);
|
|
3321
|
-
if (extra) {
|
|
3322
|
-
const glue = parsed.search ? '&' : '?';
|
|
3323
|
-
return parsed.pathname + parsed.search + glue + extra.slice(1);
|
|
3324
|
-
}
|
|
3325
|
-
}
|
|
3326
|
-
return parsed.pathname + parsed.search;
|
|
3327
|
-
}
|
|
3328
|
-
|
|
3329
|
-
async function _ccInvokeLocalApi({ method, endpoint, params }) {
|
|
3330
|
-
if (_ccLocalApiInvokerForTest) return _ccLocalApiInvokerForTest({ method, endpoint, params });
|
|
3331
|
-
const requestPath = _ccRequestPath(endpoint, method, params);
|
|
3332
|
-
return new Promise((resolve, reject) => {
|
|
3333
|
-
const body = method === 'GET' ? null : JSON.stringify(params || {});
|
|
3334
|
-
const req = http.request({
|
|
3335
|
-
hostname: '127.0.0.1',
|
|
3336
|
-
port: PORT,
|
|
3337
|
-
method,
|
|
3338
|
-
path: requestPath,
|
|
3339
|
-
timeout: CC_API_FALLBACK_TIMEOUT_MS,
|
|
3340
|
-
headers: body ? {
|
|
3341
|
-
'Content-Type': 'application/json',
|
|
3342
|
-
'Content-Length': Buffer.byteLength(body),
|
|
3343
|
-
} : {},
|
|
3344
|
-
}, res => {
|
|
3345
|
-
let text = '';
|
|
3346
|
-
res.setEncoding('utf8');
|
|
3347
|
-
res.on('data', chunk => { text += chunk; });
|
|
3348
|
-
res.on('end', () => {
|
|
3349
|
-
let data = text;
|
|
3350
|
-
try { data = text ? JSON.parse(text) : {}; } catch { /* non-JSON API response */ }
|
|
3351
|
-
resolve({ status: res.statusCode || 0, data });
|
|
3352
|
-
});
|
|
3353
|
-
});
|
|
3354
|
-
req.on('timeout', () => {
|
|
3355
|
-
req.destroy(new Error(`local API fallback timed out after ${CC_API_FALLBACK_TIMEOUT_MS}ms`));
|
|
3356
|
-
});
|
|
3357
|
-
req.on('error', reject);
|
|
3358
|
-
if (body) req.write(body);
|
|
3359
|
-
req.end();
|
|
3360
|
-
});
|
|
3361
|
-
}
|
|
3362
|
-
|
|
3363
|
-
function _ccApiRequest(endpoint, params = {}, method = 'POST') {
|
|
3364
|
-
return { endpoint, params, method };
|
|
3365
|
-
}
|
|
3366
|
-
|
|
3367
|
-
function _ccMappedApiRequests(action) {
|
|
3368
|
-
switch (action.type) {
|
|
3369
|
-
case 'pin':
|
|
3370
|
-
case 'pin-to-pinned':
|
|
3371
|
-
return _ccApiRequest('/api/pinned', { title: action.title, content: action.content || action.description, level: action.level || '' });
|
|
3372
|
-
case 'plan': {
|
|
3373
|
-
const branchStrategy = action.branch_strategy || action.branchStrategy || 'parallel';
|
|
3374
|
-
return _ccApiRequest('/api/plan', {
|
|
3375
|
-
title: action.title, description: action.description || '', priority: action.priority,
|
|
3376
|
-
project: action.project, agent: action.agent, branch_strategy: branchStrategy,
|
|
3377
|
-
});
|
|
3378
|
-
}
|
|
3379
|
-
case 'cancel':
|
|
3380
|
-
return _ccApiRequest('/api/agents/cancel', {
|
|
3381
|
-
agent: action.agent || action.agentId,
|
|
3382
|
-
task: action.task || action.cancelTask,
|
|
3383
|
-
reason: action.reason || 'Cancelled via command center',
|
|
3384
|
-
});
|
|
3385
|
-
case 'retry':
|
|
3386
|
-
return (action.ids || []).map(id => _ccApiRequest('/api/work-items/retry', { id, source: action.source || '' }));
|
|
3387
|
-
case 'pause-plan':
|
|
3388
|
-
return _ccApiRequest('/api/plans/pause', { file: action.file });
|
|
3389
|
-
case 'approve-plan':
|
|
3390
|
-
return _ccApiRequest('/api/plans/approve', { file: action.file });
|
|
3391
|
-
case 'reject-plan':
|
|
3392
|
-
return _ccApiRequest('/api/plans/reject', { file: action.file, reason: action.reason || '' });
|
|
3393
|
-
case 'archive-plan':
|
|
3394
|
-
return _ccApiRequest('/api/plans/archive', { file: action.file });
|
|
3395
|
-
case 'unarchive-plan':
|
|
3396
|
-
return _ccApiRequest('/api/plans/unarchive', { file: action.file });
|
|
3397
|
-
case 'execute-plan':
|
|
3398
|
-
return _ccApiRequest('/api/plans/execute', { file: action.file, project: action.project || '' });
|
|
3399
|
-
case 'trigger-verify':
|
|
3400
|
-
return _ccApiRequest('/api/plans/trigger-verify', { file: action.file });
|
|
3401
|
-
case 'regenerate-plan':
|
|
3402
|
-
return _ccApiRequest('/api/plans/approve', { file: action.file, forceRegen: true });
|
|
3403
|
-
case 'revise-plan':
|
|
3404
|
-
return _ccApiRequest('/api/plans/revise', { file: action.file, feedback: action.feedback || action.description, requestedBy: 'command-center' });
|
|
3405
|
-
case 'edit-prd-item':
|
|
3406
|
-
return _ccApiRequest('/api/prd-items/update', {
|
|
3407
|
-
source: action.source, itemId: action.itemId, name: action.name, description: action.description,
|
|
3408
|
-
priority: action.priority, estimated_complexity: action.estimated_complexity || action.complexity,
|
|
3409
|
-
});
|
|
3410
|
-
case 'remove-prd-item':
|
|
3411
|
-
return _ccApiRequest('/api/prd-items/remove', { source: action.source, itemId: action.itemId });
|
|
3412
|
-
case 'reopen-prd-item':
|
|
3413
|
-
return _ccApiRequest('/api/prd-items/update', { source: action.file, itemId: action.id, status: 'updated' });
|
|
3414
|
-
case 'delete-work-item':
|
|
3415
|
-
return _ccApiRequest('/api/work-items/delete', { id: action.id, source: action.source || '' });
|
|
3416
|
-
case 'cancel-work-item':
|
|
3417
|
-
return _ccApiRequest('/api/work-items/cancel', { id: action.id, source: action.source || '', reason: action.reason || 'cc' });
|
|
3418
|
-
case 'archive-work-item':
|
|
3419
|
-
return _ccApiRequest('/api/work-items/archive', { id: action.id });
|
|
3420
|
-
case 'work-item-feedback':
|
|
3421
|
-
return _ccApiRequest('/api/work-items/feedback', { id: action.id, rating: action.rating || 'up', comment: action.comment || '' });
|
|
3422
|
-
case 'schedule':
|
|
3423
|
-
return _ccApiRequest(action._update ? '/api/schedules/update' : '/api/schedules', {
|
|
3424
|
-
id: action.id, title: action.title, cron: action.cron, type: action.workType || 'implement',
|
|
3425
|
-
project: action.project, agent: action.agent, description: action.description,
|
|
3426
|
-
priority: action.priority, enabled: action.enabled !== false,
|
|
3427
|
-
});
|
|
3428
|
-
case 'delete-schedule':
|
|
3429
|
-
return _ccApiRequest('/api/schedules/delete', { id: action.id });
|
|
3430
|
-
case 'edit-pipeline':
|
|
3431
|
-
return _ccApiRequest('/api/pipelines/update', {
|
|
3432
|
-
id: action.id, title: action.title, stages: action.stages,
|
|
3433
|
-
trigger: action.trigger, enabled: action.enabled, stopWhen: action.stopWhen,
|
|
3434
|
-
monitoredResources: action.monitoredResources,
|
|
3435
|
-
});
|
|
3436
|
-
case 'delete-pipeline':
|
|
3437
|
-
return _ccApiRequest('/api/pipelines/delete', { id: action.id });
|
|
3438
|
-
case 'trigger-pipeline':
|
|
3439
|
-
return _ccApiRequest('/api/pipelines/trigger', { id: action.id });
|
|
3440
|
-
case 'continue-pipeline':
|
|
3441
|
-
return _ccApiRequest('/api/pipelines/continue', { id: action.id, stageId: action.stageId });
|
|
3442
|
-
case 'abort-pipeline':
|
|
3443
|
-
return _ccApiRequest('/api/pipelines/abort', { id: action.id });
|
|
3444
|
-
case 'retrigger-pipeline':
|
|
3445
|
-
return _ccApiRequest('/api/pipelines/retrigger', { id: action.id });
|
|
3446
|
-
case 'add-meeting-note':
|
|
3447
|
-
return _ccApiRequest('/api/meetings/note', { id: action.id, note: action.note || action.content });
|
|
3448
|
-
case 'advance-meeting':
|
|
3449
|
-
return _ccApiRequest('/api/meetings/advance', { id: action.id });
|
|
3450
|
-
case 'end-meeting':
|
|
3451
|
-
return _ccApiRequest('/api/meetings/end', { id: action.id });
|
|
3452
|
-
case 'archive-meeting':
|
|
3453
|
-
return _ccApiRequest('/api/meetings/archive', { id: action.id });
|
|
3454
|
-
case 'unarchive-meeting':
|
|
3455
|
-
return _ccApiRequest('/api/meetings/unarchive', { id: action.id });
|
|
3456
|
-
case 'delete-meeting':
|
|
3457
|
-
return _ccApiRequest('/api/meetings/delete', { id: action.id });
|
|
3458
|
-
case 'set-config':
|
|
3459
|
-
return _ccApiRequest('/api/settings', { engine: { [action.setting]: action.value } });
|
|
3460
|
-
case 'update-routing':
|
|
3461
|
-
return _ccApiRequest('/api/settings/routing', { content: action.content });
|
|
3462
|
-
case 'steer-agent':
|
|
3463
|
-
return _ccApiRequest('/api/agents/steer', { agent: action.agent, message: action.message || action.content });
|
|
3464
|
-
case 'link-pr':
|
|
3465
|
-
return _ccApiRequest('/api/pull-requests/link', { url: action.url, title: action.title || '', project: action.project || '', autoObserve: action.autoObserve !== false });
|
|
3466
|
-
case 'delete-pr':
|
|
3467
|
-
return _ccApiRequest('/api/pull-requests/delete', { id: action.id, project: action.project || '' });
|
|
3468
|
-
case 'file-bug':
|
|
3469
|
-
return _ccApiRequest('/api/issues/create', { title: action.title, description: action.description, labels: action.labels });
|
|
3470
|
-
case 'promote-to-kb':
|
|
3471
|
-
return _ccApiRequest('/api/inbox/promote-kb', { name: action.file, category: action.category || 'project-notes' });
|
|
3472
|
-
case 'kb-sweep':
|
|
3473
|
-
return _ccApiRequest('/api/knowledge/sweep', {});
|
|
3474
|
-
case 'toggle-kb-pin':
|
|
3475
|
-
return _ccApiRequest('/api/kb-pins/toggle', { key: action.key });
|
|
3476
|
-
case 'unpin':
|
|
3477
|
-
return _ccApiRequest('/api/pinned' + '/remove', { title: action.title });
|
|
3478
|
-
case 'add-project':
|
|
3479
|
-
return _ccApiRequest('/api/projects/add', {
|
|
3480
|
-
path: action.path || action.localPath, name: action.name || '',
|
|
3481
|
-
repoHost: action.repoHost || 'github', allowNonRepo: action.allowNonRepo,
|
|
3482
|
-
confirmToken: action.confirmToken,
|
|
3483
|
-
});
|
|
3484
|
-
case 'restart-engine':
|
|
3485
|
-
return _ccApiRequest('/api/engine/restart', {});
|
|
3486
|
-
case 'reset-settings':
|
|
3487
|
-
return _ccApiRequest('/api/settings/reset', {});
|
|
3488
|
-
default:
|
|
3489
|
-
if (action.endpoint) return _ccApiRequest(action.endpoint, action.params || {}, action.method || 'POST');
|
|
3490
|
-
return null;
|
|
3491
|
-
}
|
|
3492
|
-
}
|
|
3493
|
-
|
|
3494
|
-
async function _ccExecuteLocalApiAction(action) {
|
|
3495
|
-
const mapped = _ccMappedApiRequests(action);
|
|
3496
|
-
if (!mapped) return null;
|
|
3497
|
-
const requests = Array.isArray(mapped) ? mapped : [mapped];
|
|
3498
|
-
if (requests.length === 0) throw new Error(`${action.type} action has no API requests to execute`);
|
|
3499
|
-
const apiResults = [];
|
|
3500
|
-
for (const request of requests) {
|
|
3501
|
-
const method = String(request.method || 'POST').toUpperCase();
|
|
3502
|
-
const endpoint = String(request.endpoint || '').trim();
|
|
3503
|
-
const params = request.params || {};
|
|
3504
|
-
const validationError = _ccValidateLocalApiFallback(endpoint, method);
|
|
3505
|
-
if (validationError) throw new Error(validationError);
|
|
3506
|
-
const response = await _ccInvokeLocalApi({ method, endpoint, params });
|
|
3507
|
-
const status = Number(response?.status) || 0;
|
|
3508
|
-
const data = response?.data === undefined ? {} : response.data;
|
|
3509
|
-
if (status < 200 || status >= 300) {
|
|
3510
|
-
const detail = data && typeof data === 'object' && data.error ? data.error : `HTTP ${status}`;
|
|
3511
|
-
throw new Error(`${method} ${endpoint} failed: ${detail}`);
|
|
3512
|
-
}
|
|
3513
|
-
if (data && typeof data === 'object' && data.error) throw new Error(`${method} ${endpoint} failed: ${data.error}`);
|
|
3514
|
-
apiResults.push({ status, data, endpoint, method });
|
|
3515
|
-
}
|
|
3516
|
-
const firstData = apiResults[0]?.data && typeof apiResults[0].data === 'object' ? apiResults[0].data : {};
|
|
3517
|
-
return {
|
|
3518
|
-
type: action.type,
|
|
3519
|
-
ok: true,
|
|
3520
|
-
endpoint: apiResults[0]?.endpoint,
|
|
3521
|
-
method: apiResults[0]?.method,
|
|
3522
|
-
status: apiResults[0]?.status,
|
|
3523
|
-
...(firstData.id ? { id: firstData.id } : {}),
|
|
3524
|
-
...(firstData.file ? { file: firstData.file } : {}),
|
|
3525
|
-
...(firstData.message ? { message: firstData.message } : {}),
|
|
3526
|
-
...(apiResults.length > 1 ? { count: apiResults.length, results: apiResults.map(r => r.data) } : { data: firstData }),
|
|
3527
|
-
};
|
|
3528
|
-
}
|
|
3529
|
-
|
|
3530
|
-
function _ccActionFailureLabel(action) {
|
|
3531
|
-
const type = String(action?.type || 'action').trim() || 'action';
|
|
3532
|
-
const title = String(action?.title || action?.id || action?.file || action?.target || '').replace(/\s+/g, ' ').trim();
|
|
3533
|
-
return title ? `${type} failed for '${title.slice(0, 120)}'` : `${type} failed`;
|
|
3534
|
-
}
|
|
3535
|
-
|
|
3536
|
-
function _ccMultiProjectRequiredError(action) {
|
|
3537
|
-
return `${_ccActionFailureLabel(action)}: project field is required when ${PROJECTS.length} projects are configured: ${PROJECTS.map(p => p.name).join(', ')}`;
|
|
3538
|
-
}
|
|
3539
|
-
|
|
3540
|
-
async function executeCCActions(actions, { source = 'command-center', inferredProject = null } = {}) {
|
|
3541
|
-
const results = [];
|
|
3542
|
-
const dispatchIdsCreatedInThisCall = new Map();
|
|
3543
|
-
for (let actionIndex = 0; actionIndex < actions.length; actionIndex++) {
|
|
3544
|
-
const rawAction = actions[actionIndex];
|
|
3545
|
-
const action = normalizeCCAction(rawAction);
|
|
3546
|
-
if (action?._intentFallbackError) {
|
|
3547
|
-
results.push({
|
|
3548
|
-
type: action.type || 'dispatch',
|
|
3549
|
-
...(action.workType ? { workType: action.workType } : {}),
|
|
3550
|
-
error: action._intentFallbackError,
|
|
3551
|
-
missingTarget: true,
|
|
3552
|
-
});
|
|
3553
|
-
continue;
|
|
3554
|
-
}
|
|
3555
|
-
const validationError = _ccValidateAction(action);
|
|
3556
|
-
if (validationError) {
|
|
3557
|
-
results.push({ type: action?.type || 'unknown', error: validationError });
|
|
3558
|
-
continue;
|
|
3559
|
-
}
|
|
3560
|
-
try {
|
|
3561
|
-
switch (action.type) {
|
|
3562
|
-
case 'dispatch': {
|
|
3563
|
-
const workType = routing.normalizeWorkType(action.workType || (action.type !== 'dispatch' ? action.type : WORK_TYPE.IMPLEMENT), WORK_TYPE.IMPLEMENT);
|
|
3564
|
-
const id = 'W-' + shared.uid();
|
|
3565
|
-
const project = action.project || '';
|
|
3566
|
-
const prTargetedWorkType = isPrTargetedWorkType(workType);
|
|
3567
|
-
let prRef = getWorkItemPrRef(action);
|
|
3568
|
-
let linkedPr = null;
|
|
3569
|
-
let allPrsForAction = null;
|
|
3570
|
-
if (!prRef && prTargetedWorkType) {
|
|
3571
|
-
allPrsForAction = getPullRequests().filter(p => !p._ghost);
|
|
3572
|
-
linkedPr = inferActionPrRecord(action, allPrsForAction, null);
|
|
3573
|
-
if (linkedPr) prRef = linkedPr.id || linkedPr.url || linkedPr.prNumber;
|
|
3574
|
-
}
|
|
3575
|
-
|
|
3576
|
-
// Strict project resolution. Silent fallback to PROJECTS[0] when the model named an unknown
|
|
3577
|
-
// project caused work items to land in the wrong repo. Now: unknown name → error; ambiguous
|
|
3578
|
-
// (multiple projects + no field) → error; single-project deployments fall through; zero
|
|
3579
|
-
// projects → root-level work-items.json (orchestration system standalone use).
|
|
3580
|
-
let targetProject = null;
|
|
3581
|
-
if (project) {
|
|
3582
|
-
const target = resolveProjectSourceTarget(project, PROJECTS, { allowCentral: false });
|
|
3583
|
-
targetProject = target.project;
|
|
3584
|
-
if (target.error) {
|
|
3585
|
-
results.push({ type: action.type, error: target.error });
|
|
3586
|
-
break;
|
|
3587
|
-
}
|
|
3588
|
-
} else if (prRef) {
|
|
3589
|
-
const allPrs = allPrsForAction || getPullRequests().filter(p => !p._ghost);
|
|
3590
|
-
if (!linkedPr) linkedPr = shared.findPrRecord(allPrs, prRef) || null;
|
|
3591
|
-
if (linkedPr?._project && linkedPr._project !== 'central') {
|
|
3592
|
-
targetProject = resolveProjectSourceTarget(linkedPr._project, PROJECTS, { allowCentral: false }).project || null;
|
|
3593
|
-
}
|
|
3594
|
-
} else if (inferredProject) {
|
|
3595
|
-
// Doc-chat fallback: filePath-derived project when the LLM omits the field. Validated against
|
|
3596
|
-
// PROJECTS upstream by _inferDocChatProject — a stale lookup would just yield null here.
|
|
3597
|
-
targetProject = resolveProjectSourceTarget(inferredProject, PROJECTS, { allowCentral: false }).project || null;
|
|
3598
|
-
}
|
|
3599
|
-
if (!targetProject) {
|
|
3600
|
-
if (PROJECTS.length > 1) {
|
|
3601
|
-
results.push({ type: action.type, error: _ccMultiProjectRequiredError(action) });
|
|
3602
|
-
break;
|
|
3603
|
-
}
|
|
3604
|
-
if (PROJECTS.length === 1) targetProject = PROJECTS[0];
|
|
3605
|
-
}
|
|
3606
|
-
// PROJECTS.length === 0 → targetProject stays null, falls back to root work-items.json (existing behavior).
|
|
3607
|
-
|
|
3608
|
-
if (targetProject && (!linkedPr || !prRef)) {
|
|
3609
|
-
const projectPrs = shared.safeJson(shared.projectPrPath(targetProject)) || [];
|
|
3610
|
-
shared.normalizePrRecords(projectPrs, targetProject);
|
|
3611
|
-
if (!prRef && prTargetedWorkType) {
|
|
3612
|
-
linkedPr = inferActionPrRecord(action, projectPrs, targetProject);
|
|
3613
|
-
if (linkedPr) prRef = linkedPr.id || linkedPr.url || linkedPr.prNumber;
|
|
3614
|
-
} else if (prRef && !linkedPr) {
|
|
3615
|
-
linkedPr = shared.findPrRecord(projectPrs, prRef, targetProject) || null;
|
|
3616
|
-
}
|
|
3617
|
-
}
|
|
3618
|
-
if (prRef && prTargetedWorkType && !linkedPr) {
|
|
3619
|
-
results.push({ type: action.type, error: `PR not found: ${prRef}` });
|
|
3620
|
-
break;
|
|
3621
|
-
}
|
|
3622
|
-
|
|
3623
|
-
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : shared.centralWorkItemsPath(MINIONS_DIR);
|
|
3624
|
-
|
|
3625
|
-
// Promote `agent` (singular) → `agents` (array). Models emit either shape and the prior code
|
|
3626
|
-
// only read `action.agents`, silently dropping `agent: "lambert"` style hints.
|
|
3627
|
-
const agentHints = (() => {
|
|
3628
|
-
if (Array.isArray(action.agents) && action.agents.length > 0) return action.agents.map(String).filter(Boolean);
|
|
3629
|
-
if (typeof action.agent === 'string' && action.agent) return [action.agent];
|
|
3630
|
-
return [];
|
|
3631
|
-
})();
|
|
3632
|
-
const knownAgents = Object.keys(CONFIG.agents || {});
|
|
3633
|
-
const unknownAgent = agentHints.find(a => !knownAgents.includes(a));
|
|
3634
|
-
if (unknownAgent) {
|
|
3635
|
-
results.push({ type: action.type, error: `Unknown agent "${unknownAgent}". Configured agents: ${knownAgents.join(', ') || '(none)'}` });
|
|
3636
|
-
break;
|
|
3637
|
-
}
|
|
3638
|
-
|
|
3639
|
-
// Issue #1772: CC review/explore/test are human-initiated one-offs.
|
|
3640
|
-
// Mark oneShot so any discovered PR is tagged _contextOnly (skips eval loop).
|
|
3641
|
-
const ccOneShotTypes = new Set([WORK_TYPE.REVIEW, WORK_TYPE.EXPLORE, WORK_TYPE.TEST, WORK_TYPE.ASK, WORK_TYPE.VERIFY]);
|
|
3642
|
-
const isOneShot = action.oneShot === true || (action.oneShot !== false && ccOneShotTypes.has(workType));
|
|
3643
|
-
const item = {
|
|
3644
|
-
id, title: action.title.trim(), type: workType,
|
|
3645
|
-
priority: action.priority || 'medium', description: action.description || '',
|
|
3646
|
-
status: WI_STATUS.PENDING, created: new Date().toISOString(),
|
|
3647
|
-
createdBy: source, project: targetProject?.name || project,
|
|
3648
|
-
...(action.scope ? { scope: action.scope } : {}),
|
|
3649
|
-
...(agentHints.length ? { preferred_agent: agentHints[0], agents: agentHints } : {}),
|
|
3650
|
-
...(isOneShot ? { oneShot: true } : {}),
|
|
3651
|
-
};
|
|
3652
|
-
copyWorkItemPrFields(item, action, linkedPr);
|
|
3653
|
-
const createResult = createWorkItemWithDedup(wiPath, item);
|
|
3654
|
-
if (!createResult.created) {
|
|
3655
|
-
const duplicateId = createResult.duplicateOf || createResult.item?.id;
|
|
3656
|
-
if (duplicateId && dispatchIdsCreatedInThisCall.has(duplicateId)) {
|
|
3657
|
-
results.push({
|
|
3658
|
-
type: action.type,
|
|
3659
|
-
id: duplicateId,
|
|
3660
|
-
ok: true,
|
|
3661
|
-
reusedFromAction: dispatchIdsCreatedInThisCall.get(duplicateId),
|
|
3662
|
-
});
|
|
3663
|
-
break;
|
|
3664
|
-
}
|
|
3665
|
-
results.push({ type: action.type, id: duplicateId, ok: true, duplicate: true, duplicateOf: duplicateId });
|
|
3666
|
-
break;
|
|
3667
|
-
}
|
|
3668
|
-
dispatchIdsCreatedInThisCall.set(id, actionIndex);
|
|
3669
|
-
results.push({ type: action.type, id, ok: true });
|
|
3670
|
-
|
|
3671
|
-
// Pre-flight routing check: warn the user if no agent is currently available so the new
|
|
3672
|
-
// item won't sit pending invisibly. Routing failure is non-fatal — the WI was created.
|
|
3673
|
-
try {
|
|
3674
|
-
const resolvedAgent = routing.resolveAgent(workType, CONFIG, { agentHints, dryRun: true });
|
|
3675
|
-
if (!resolvedAgent) {
|
|
3676
|
-
const lastResult = results[results.length - 1];
|
|
3677
|
-
lastResult.warning = `Created ${id} but no agent is currently available to dispatch (routing returned no match for workType=${workType}${agentHints.length ? ', hints=' + agentHints.join(',') : ''}). Item will sit pending until an agent becomes available.`;
|
|
3678
|
-
}
|
|
3679
|
-
} catch (e) {
|
|
3680
|
-
shared.log('warn', `CC dispatch routing pre-flight: ${e.message}`);
|
|
3681
|
-
}
|
|
3682
|
-
break;
|
|
3683
|
-
}
|
|
3684
|
-
case 'build-and-test': {
|
|
3685
|
-
// Resolve PR by number, ID, or URL — same lookup that drives the link-pr / PR-row paths.
|
|
3686
|
-
const allPrs = getPullRequests().filter(p => !p._ghost);
|
|
3687
|
-
const pr = shared.findPrRecord(allPrs, action.pr) || null;
|
|
3688
|
-
if (!pr) {
|
|
3689
|
-
results.push({ type: 'build-and-test', error: `PR not found: ${action.pr}` });
|
|
3690
|
-
break;
|
|
3691
|
-
}
|
|
3692
|
-
// Resolve project: explicit param wins, else PR's _project. No silent first-project fallback —
|
|
3693
|
-
// unresolved → error so build-and-test can't accidentally run against the wrong repo.
|
|
3694
|
-
const projectName = action.project || pr._project || null;
|
|
3695
|
-
const project = projectName
|
|
3696
|
-
? resolveProjectSourceTarget(projectName, PROJECTS, { allowCentral: false }).project
|
|
3697
|
-
: null;
|
|
3698
|
-
if (!project) {
|
|
3699
|
-
results.push({ type: 'build-and-test', error: `Project not found for PR ${pr.id}: ${projectName || '(none)'}` });
|
|
3700
|
-
break;
|
|
3701
|
-
}
|
|
3702
|
-
// Pick agent: explicit param wins; else routing for 'test' work type.
|
|
3703
|
-
let agentId = action.agent && CONFIG.agents?.[action.agent] ? action.agent : null;
|
|
3704
|
-
if (!agentId) {
|
|
3705
|
-
agentId = routing.resolveAgent('test', CONFIG, { authorAgent: pr.agent });
|
|
3706
|
-
}
|
|
3707
|
-
if (!agentId) {
|
|
3708
|
-
results.push({ type: 'build-and-test', error: 'No available agent for test routing' });
|
|
3709
|
-
break;
|
|
3710
|
-
}
|
|
3711
|
-
const prNumber = shared.getPrNumber(pr);
|
|
3712
|
-
const dispatchKey = `cc-bt-${project.name}-${pr.id}`;
|
|
3713
|
-
const item = playbook.buildPrDispatch(agentId, CONFIG, project, pr, 'test', {
|
|
3714
|
-
pr_id: pr.id, pr_number: prNumber, pr_title: pr.title || '', pr_branch: pr.branch || '',
|
|
3715
|
-
pr_author: pr.agent || '', pr_url: pr.url || '',
|
|
3716
|
-
project_path: project.localPath || '',
|
|
3717
|
-
task: `Build & test ${pr.id}: ${pr.title || ''}`,
|
|
3718
|
-
}, `Build & test ${pr.id}: ${pr.title || ''}`,
|
|
3719
|
-
{ dispatchKey, source: 'cc-build-and-test', pr, branch: pr.branch, project: { name: project.name, localPath: project.localPath } });
|
|
3720
|
-
if (!item) {
|
|
3721
|
-
if (agentId?.startsWith('temp-')) {
|
|
3722
|
-
routing.tempAgents.delete(agentId);
|
|
3723
|
-
routing._claimedAgents.delete(agentId);
|
|
3724
|
-
}
|
|
3725
|
-
results.push({ type: 'build-and-test', error: 'Failed to render build-and-test playbook' });
|
|
3726
|
-
break;
|
|
3727
|
-
}
|
|
3728
|
-
const id = dispatchMod.addToDispatch(item);
|
|
3729
|
-
if (agentId?.startsWith('temp-')) {
|
|
3730
|
-
routing.tempAgents.delete(agentId);
|
|
3731
|
-
routing._claimedAgents.delete(agentId);
|
|
3732
|
-
}
|
|
3733
|
-
results.push({ type: 'build-and-test', id, agent: agentId, pr: pr.id, ok: true });
|
|
3734
|
-
break;
|
|
3735
|
-
}
|
|
3736
|
-
case 'note': {
|
|
3737
|
-
shared.writeToInbox('command-center', shared.slugify(action.title || 'note'), `# ${action.title || 'Note'}\n\n${action.content || action.description || ''}`);
|
|
3738
|
-
results.push({ type: 'note', ok: true });
|
|
3739
|
-
break;
|
|
3740
|
-
}
|
|
3741
|
-
case 'knowledge': {
|
|
3742
|
-
const validCategories = ['architecture', 'conventions', 'project-notes', 'build-reports', 'reviews'];
|
|
3743
|
-
const category = action.category || 'project-notes';
|
|
3744
|
-
if (!validCategories.includes(category)) { results.push({ type: 'knowledge', error: 'Invalid category: ' + category }); break; }
|
|
3745
|
-
const slug = shared.slugify(action.title || 'entry');
|
|
3746
|
-
const kbDir = path.join(MINIONS_DIR, 'knowledge', category);
|
|
3747
|
-
if (!fs.existsSync(kbDir)) fs.mkdirSync(kbDir, { recursive: true });
|
|
3748
|
-
shared.safeWrite(path.join(kbDir, slug + '.md'), `# ${action.title}\n\n${action.content || action.description || ''}`);
|
|
3749
|
-
queries.invalidateKnowledgeBaseCache();
|
|
3750
|
-
results.push({ type: 'knowledge', ok: true });
|
|
3751
|
-
break;
|
|
3752
|
-
}
|
|
3753
|
-
case 'reopen-work-item': {
|
|
3754
|
-
const project = action.project || '';
|
|
3755
|
-
let targetProject = null;
|
|
3756
|
-
if (project) {
|
|
3757
|
-
const target = resolveProjectSourceTarget(project, PROJECTS, { allowCentral: false });
|
|
3758
|
-
targetProject = target.project;
|
|
3759
|
-
if (target.error) {
|
|
3760
|
-
results.push({ type: 'reopen-work-item', id: action.id, error: target.error });
|
|
3761
|
-
break;
|
|
3762
|
-
}
|
|
3763
|
-
} else if (inferredProject) {
|
|
3764
|
-
targetProject = resolveProjectSourceTarget(inferredProject, PROJECTS, { allowCentral: false }).project || null;
|
|
3765
|
-
}
|
|
3766
|
-
if (!targetProject) {
|
|
3767
|
-
if (PROJECTS.length > 1) {
|
|
3768
|
-
results.push({ type: 'reopen-work-item', id: action.id, error: _ccMultiProjectRequiredError({ ...action, type: 'reopen-work-item' }) });
|
|
3769
|
-
break;
|
|
3770
|
-
}
|
|
3771
|
-
if (PROJECTS.length === 1) targetProject = PROJECTS[0];
|
|
3772
|
-
}
|
|
3773
|
-
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : shared.centralWorkItemsPath(MINIONS_DIR);
|
|
3774
|
-
let reopenResult = null;
|
|
3775
|
-
mutateJsonFileLocked(wiPath, items => {
|
|
3776
|
-
if (!Array.isArray(items)) items = [];
|
|
3777
|
-
const item = items.find(i => i.id === action.id);
|
|
3778
|
-
if (!item) { reopenResult = { error: 'item not found' }; return items; }
|
|
3779
|
-
if (item.status !== WI_STATUS.DONE && item.status !== WI_STATUS.FAILED && !DONE_STATUSES.has(item.status)) {
|
|
3780
|
-
reopenResult = { error: 'can only reopen done or failed items' }; return items;
|
|
3781
|
-
}
|
|
3782
|
-
reopenWorkItem(item);
|
|
3783
|
-
if (action.description) item.description = action.description;
|
|
3784
|
-
reopenResult = { ok: true };
|
|
3785
|
-
return items;
|
|
3786
|
-
}, { defaultValue: [] });
|
|
3787
|
-
if (reopenResult?.ok) {
|
|
3788
|
-
// Clear dispatch history outside lock
|
|
3789
|
-
const sourcePrefix = targetProject ? `work-${targetProject.name}-` : 'central-work-';
|
|
3790
|
-
const dispatchKey = sourcePrefix + action.id;
|
|
3791
|
-
try {
|
|
3792
|
-
const dispatchPath = path.join(MINIONS_DIR, 'engine', 'dispatch.json');
|
|
3793
|
-
mutateJsonFileLocked(dispatchPath, dispatch => {
|
|
3794
|
-
dispatch.completed = Array.isArray(dispatch.completed) ? dispatch.completed : [];
|
|
3795
|
-
dispatch.completed = dispatch.completed.filter(d => d.meta?.dispatchKey !== dispatchKey);
|
|
3796
|
-
dispatch.completed = dispatch.completed.filter(d => !d.meta?.parentKey || d.meta.parentKey !== dispatchKey);
|
|
3797
|
-
return dispatch;
|
|
3798
|
-
}, { defaultValue: { pending: [], active: [], completed: [] } });
|
|
3799
|
-
} catch { /* best effort */ }
|
|
3800
|
-
invalidateStatusCache();
|
|
3801
|
-
}
|
|
3802
|
-
results.push({ type: 'reopen-work-item', id: action.id, ...(reopenResult || { error: 'unexpected' }) });
|
|
3803
|
-
break;
|
|
3804
|
-
}
|
|
3805
|
-
case 'create-watch': {
|
|
3806
|
-
const intervalMs = _parseWatchInterval(action.interval);
|
|
3807
|
-
const watch = watchesMod.createWatch({
|
|
3808
|
-
target: action.target,
|
|
3809
|
-
targetType: action.targetType || 'pr',
|
|
3810
|
-
condition: action.condition || 'build-pass',
|
|
3811
|
-
interval: intervalMs,
|
|
3812
|
-
owner: action.owner || 'human',
|
|
3813
|
-
description: action.description || null,
|
|
3814
|
-
project: action.project || null,
|
|
3815
|
-
notify: 'inbox',
|
|
3816
|
-
stopAfter: Number(action.stopAfter) || 0,
|
|
3817
|
-
onNotMet: action.onNotMet || null,
|
|
3818
|
-
});
|
|
3819
|
-
results.push({ type: 'create-watch', id: watch.id, ok: true });
|
|
3820
|
-
break;
|
|
3821
|
-
}
|
|
3822
|
-
case 'create-meeting': {
|
|
3823
|
-
const { createMeeting } = require('./engine/meeting');
|
|
3824
|
-
const meeting = createMeeting({
|
|
3825
|
-
title: action.title.trim(),
|
|
3826
|
-
agenda: action.agenda.trim(),
|
|
3827
|
-
participants: meetingParticipantsFromAction(action),
|
|
3828
|
-
});
|
|
3829
|
-
invalidateStatusCache();
|
|
3830
|
-
results.push({ type: 'create-meeting', id: meeting.id, ok: true });
|
|
3831
|
-
break;
|
|
3832
|
-
}
|
|
3833
|
-
case 'create-pipeline': {
|
|
3834
|
-
results.push(createPipelineFromAction(action));
|
|
3835
|
-
break;
|
|
3836
|
-
}
|
|
3837
|
-
case 'delete-watch': {
|
|
3838
|
-
const deleted = watchesMod.deleteWatch(action.id);
|
|
3839
|
-
if (deleted) invalidateStatusCache();
|
|
3840
|
-
results.push({ type: 'delete-watch', id: action.id, ok: deleted });
|
|
3841
|
-
break;
|
|
3842
|
-
}
|
|
3843
|
-
case 'pause-watch': {
|
|
3844
|
-
const paused = watchesMod.updateWatch(action.id, { status: shared.WATCH_STATUS.PAUSED });
|
|
3845
|
-
if (paused) invalidateStatusCache();
|
|
3846
|
-
results.push({ type: 'pause-watch', id: action.id, ok: !!paused });
|
|
3847
|
-
break;
|
|
3848
|
-
}
|
|
3849
|
-
case 'resume-watch': {
|
|
3850
|
-
const resumed = watchesMod.updateWatch(action.id, { status: shared.WATCH_STATUS.ACTIVE });
|
|
3851
|
-
if (resumed) invalidateStatusCache();
|
|
3852
|
-
results.push({ type: 'resume-watch', id: action.id, ok: !!resumed });
|
|
3853
|
-
break;
|
|
3854
|
-
}
|
|
3855
|
-
default: {
|
|
3856
|
-
const apiResult = await _ccExecuteLocalApiAction(action);
|
|
3857
|
-
if (apiResult) {
|
|
3858
|
-
results.push(apiResult);
|
|
3859
|
-
} else {
|
|
3860
|
-
// Server didn't handle — frontend must execute.
|
|
3861
|
-
results.push({ type: action.type });
|
|
3862
|
-
}
|
|
3863
|
-
break;
|
|
3864
|
-
}
|
|
3865
|
-
}
|
|
3866
|
-
} catch (e) {
|
|
3867
|
-
results.push({ type: action.type, error: e.message });
|
|
3868
|
-
}
|
|
3869
|
-
}
|
|
3870
|
-
return results;
|
|
3871
|
-
}
|
|
3872
|
-
|
|
3873
|
-
async function executeDocChatActions(actions, { filePath = null } = {}) {
|
|
3874
|
-
if (!Array.isArray(actions) || actions.length === 0) return undefined;
|
|
3875
|
-
return executeCCActions(actions, { source: 'doc-chat', inferredProject: _inferDocChatProject(filePath) });
|
|
3876
|
-
}
|
|
3877
|
-
|
|
3878
|
-
const DOC_CHAT_WORK_ITEM_ACTION_TYPES = new Set(['dispatch', 'fix', 'implement', 'implement:large', 'explore', 'review', 'test', 'ask', 'verify']);
|
|
3879
|
-
|
|
3880
|
-
function _buildDocChatActionFeedback(actions, actionResults) {
|
|
3881
|
-
if (!Array.isArray(actions) || !Array.isArray(actionResults)) return [];
|
|
3882
|
-
const feedback = [];
|
|
3883
|
-
for (let i = 0; i < actions.length && i < actionResults.length; i++) {
|
|
3884
|
-
const action = actions[i] || {};
|
|
3885
|
-
const result = actionResults[i] || {};
|
|
3886
|
-
const type = String(result.type || action.type || '').trim();
|
|
3887
|
-
if (!DOC_CHAT_WORK_ITEM_ACTION_TYPES.has(type)) continue;
|
|
3888
|
-
if (result.error) {
|
|
3889
|
-
feedback.push({ type, error: String(result.error) });
|
|
3890
|
-
continue;
|
|
3891
|
-
}
|
|
3892
|
-
if (result.reusedFromAction !== undefined) continue;
|
|
3893
|
-
const id = result.id || result.duplicateOf;
|
|
3894
|
-
if (!result.ok || !id) continue;
|
|
3895
|
-
const item = { type, id: String(id), ok: true };
|
|
3896
|
-
if (result.duplicate) item.duplicate = true;
|
|
3897
|
-
if (result.duplicateOf) item.duplicateOf = String(result.duplicateOf);
|
|
3898
|
-
if (result.warning) item.warning = String(result.warning);
|
|
3899
|
-
feedback.push(item);
|
|
3900
|
-
}
|
|
3901
|
-
return feedback;
|
|
3902
|
-
}
|
|
3903
2115
|
|
|
3904
2116
|
// ── Shared LLM call core — used by CC panel and doc modals ──────────────────
|
|
3905
2117
|
|
|
@@ -4318,33 +2530,11 @@ function contentFingerprint(str) {
|
|
|
4318
2530
|
return str.length + ':' + str.charCodeAt(0) + ':' + str.charCodeAt(mid) + ':' + str.charCodeAt(str.length - 1);
|
|
4319
2531
|
}
|
|
4320
2532
|
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
const answerPart = text.slice(0, docDelimiter.index).trim();
|
|
4325
|
-
const parsedActions = parseCCActions(answerPart);
|
|
4326
|
-
const { text: answer, actions } = parsedActions;
|
|
4327
|
-
let content = text.slice(docDelimiter.index + docDelimiter.length).trim();
|
|
4328
|
-
content = content.replace(/^```\w*\n?/, '').replace(/\n?```$/, '').trim();
|
|
4329
|
-
return {
|
|
4330
|
-
answer,
|
|
4331
|
-
content,
|
|
4332
|
-
actions,
|
|
4333
|
-
...(parsedActions._actionParseError ? { actionParseError: parsedActions._actionParseError } : {}),
|
|
4334
|
-
};
|
|
4335
|
-
}
|
|
4336
|
-
const parsedActions = parseCCActions(text);
|
|
4337
|
-
const { text: stripped, actions } = parsedActions;
|
|
4338
|
-
return {
|
|
4339
|
-
answer: stripped,
|
|
4340
|
-
content: null,
|
|
4341
|
-
actions,
|
|
4342
|
-
...(parsedActions._actionParseError ? { actionParseError: parsedActions._actionParseError } : {}),
|
|
4343
|
-
};
|
|
4344
|
-
}
|
|
4345
|
-
|
|
2533
|
+
// _parseDocChatResultText / _docChatDisplayText were retired together with
|
|
2534
|
+
// the ---DOCUMENT--- and ===ACTIONS=== delimiters. Doc-chat now returns the
|
|
2535
|
+
// raw text answer; file edits flow through the Edit/Write tools.
|
|
4346
2536
|
function _docChatDisplayText(text) {
|
|
4347
|
-
return
|
|
2537
|
+
return String(text || '').trim();
|
|
4348
2538
|
}
|
|
4349
2539
|
|
|
4350
2540
|
function _inferDocChatProject(filePath) {
|
|
@@ -4368,8 +2558,9 @@ function _formatDocChatContext({ document, title, filePath, selection, canEdit,
|
|
|
4368
2558
|
// the file path explicitly so the model knows which file Edit can target.
|
|
4369
2559
|
const editInstructions = canEdit
|
|
4370
2560
|
? `\n\nIf editing is requested:\n` +
|
|
4371
|
-
`-
|
|
4372
|
-
`-
|
|
2561
|
+
`- Use the runtime \`Edit\` tool against \`${filePath}\` for localized changes (typo fixes, single sections, ≲30% of the file).\n` +
|
|
2562
|
+
`- Use the runtime \`Write\` tool against \`${filePath}\` for wholesale rewrites or whenever an Edit would invalidate document structure (JSON, code).\n` +
|
|
2563
|
+
`- After your tool calls return, just describe what you changed in plain text — the server re-reads the file from disk and refreshes the editor view. Do NOT echo the document content back, do NOT emit any delimiter (\`---DOCUMENT---\` and similar are gone).\n` +
|
|
4373
2564
|
`- Never edit any file other than \`${filePath}\`.`
|
|
4374
2565
|
: '\n\nRead-only — answer questions only.';
|
|
4375
2566
|
let context = `## Document Context\n**${safeTitle}**${location}${isJson ? ' (JSON)' : ''}${projectHint}\n\n`;
|
|
@@ -4478,8 +2669,6 @@ function _docChatFailureResponse(label, filePath, result, sessionPreserved = fal
|
|
|
4478
2669
|
console.error(`[${label}] Failed: code=${result.code}, errorClass=${result.errorClass || 'null'}, sessionPreserved=${sessionPreserved}, empty=${!result.text}, tools=${toolUses.length}, filePath=${filePath}, stderr=${envelope.stderr.slice(0, 200)}`);
|
|
4479
2670
|
return {
|
|
4480
2671
|
answer: _docChatErrorMessage(result.errorClass, sessionPreserved, toolUses, result.errorMessage || null),
|
|
4481
|
-
content: null,
|
|
4482
|
-
actions: [],
|
|
4483
2672
|
toolUses,
|
|
4484
2673
|
error: envelope,
|
|
4485
2674
|
};
|
|
@@ -4493,18 +2682,11 @@ function _docChatFailureResponse(label, filePath, result, sessionPreserved = fal
|
|
|
4493
2682
|
// Returns null when there's nothing parseable; caller falls through to failure.
|
|
4494
2683
|
function _recoverPartialDocChatResponse(result, sessionKey) {
|
|
4495
2684
|
if (!result || !result.text || !result.text.trim()) return null;
|
|
4496
|
-
const parsed = _parseDocChatResultText(result.text);
|
|
4497
|
-
const hasActions = Array.isArray(parsed.actions) && parsed.actions.length > 0;
|
|
4498
|
-
const hasAnswer = typeof parsed.answer === 'string' && !!parsed.answer.trim();
|
|
4499
|
-
const hasContent = typeof parsed.content === 'string' && !!parsed.content.trim();
|
|
4500
|
-
if (!hasAnswer && !hasContent && !hasActions) return null;
|
|
4501
2685
|
return {
|
|
4502
|
-
|
|
2686
|
+
answer: result.text,
|
|
4503
2687
|
partial: true,
|
|
4504
2688
|
warning: _docChatPartialWarning(result.errorClass, result.errorMessage || result.stderr || null),
|
|
4505
2689
|
toolUses: Array.isArray(result.toolUses) ? result.toolUses : [],
|
|
4506
|
-
// Recovery path still attaches the raw runtime failure — the answer landed
|
|
4507
|
-
// despite a non-zero exit; users still benefit from seeing why.
|
|
4508
2690
|
error: _buildDocChatErrorEnvelope(result),
|
|
4509
2691
|
};
|
|
4510
2692
|
}
|
|
@@ -4617,40 +2799,18 @@ function _finalizeDocChatEdit({ filePath, fullPath, isJson, canEdit, originalCon
|
|
|
4617
2799
|
return { edited: true, content: diskContent };
|
|
4618
2800
|
}
|
|
4619
2801
|
|
|
4620
|
-
//
|
|
4621
|
-
//
|
|
4622
|
-
//
|
|
4623
|
-
//
|
|
4624
|
-
//
|
|
4625
|
-
//
|
|
4626
|
-
// Also dedups identical post-strip answers: the upstream accumulator dedups
|
|
4627
|
-
// against the raw growing text, but that text keeps changing as the document
|
|
4628
|
-
// body streams in even though the visible answer is locked. Without this
|
|
4629
|
-
// guard the SSE writer fires a duplicate `chunk` event for every doc-body
|
|
4630
|
-
// delta, which triggers a client DOM rerender and localStorage write each time.
|
|
2802
|
+
// Dedupes identical post-strip answers so the SSE writer doesn't fire a
|
|
2803
|
+
// duplicate `chunk` event for every doc-body delta when the model streams
|
|
2804
|
+
// `Edit`/`Write` tool input alongside narration. Since the ===ACTIONS=== /
|
|
2805
|
+
// ---DOCUMENT--- delimiters are gone, we just pass text through untouched
|
|
2806
|
+
// with a last-sent guard.
|
|
4631
2807
|
function _makeDocChatStreamStripper(onChunk) {
|
|
4632
2808
|
if (!onChunk) return undefined;
|
|
4633
|
-
let lockedAnswer = null;
|
|
4634
2809
|
let lastSent;
|
|
4635
2810
|
return (text) => {
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
} else if (text.indexOf('---') < 0) {
|
|
4640
|
-
// Fast path for typical Q&A: the doc delimiter starts with "---", so
|
|
4641
|
-
// when the chunk doesn't contain that substring it can't possibly
|
|
4642
|
-
// contain ---MINIONS-DOC-CHAT-DOCUMENT-…--- or ---DOCUMENT---. Skip the
|
|
4643
|
-
// regex-heavy delimiter scan; we still need the actions stripper to
|
|
4644
|
-
// hide partial ===ACTIONS=== while the model is mid-emission.
|
|
4645
|
-
answer = stripCCActionsForStream(text);
|
|
4646
|
-
} else {
|
|
4647
|
-
const parsed = _parseDocChatResultText(text);
|
|
4648
|
-
if (parsed.content !== null) lockedAnswer = parsed.answer;
|
|
4649
|
-
answer = parsed.answer;
|
|
4650
|
-
}
|
|
4651
|
-
if (answer === lastSent) return;
|
|
4652
|
-
lastSent = answer;
|
|
4653
|
-
onChunk(answer);
|
|
2811
|
+
if (text === lastSent) return;
|
|
2812
|
+
lastSent = text;
|
|
2813
|
+
onChunk(text);
|
|
4654
2814
|
};
|
|
4655
2815
|
}
|
|
4656
2816
|
|
|
@@ -4717,7 +2877,12 @@ async function ccDocCall({ message, document, title, filePath, selection, canEdi
|
|
|
4717
2877
|
return _docChatFailureResponse('doc-chat', filePath, result, sessionPreserved);
|
|
4718
2878
|
}
|
|
4719
2879
|
|
|
4720
|
-
|
|
2880
|
+
// No more ===ACTIONS=== / ---DOCUMENT--- parsing — CC's text answer is the
|
|
2881
|
+
// visible reply; file edits land via Edit/Write tools (validated in
|
|
2882
|
+
// _finalizeDocChatEdit by re-reading disk); state mutations land via direct
|
|
2883
|
+
// /api/* calls (correlated by X-CC-Turn-Id, surfaced as synthetic chips
|
|
2884
|
+
// server-side).
|
|
2885
|
+
return { answer: result.text, toolUses: Array.isArray(result.toolUses) ? result.toolUses : [] };
|
|
4721
2886
|
}
|
|
4722
2887
|
|
|
4723
2888
|
async function ccDocCallStreaming({ message, document, title, filePath, selection, canEdit, isJson, model, freshSession, transcript, onAbortReady, onChunk, onToolUse, onRetry }) {
|
|
@@ -4776,7 +2941,12 @@ async function ccDocCallStreaming({ message, document, title, filePath, selectio
|
|
|
4776
2941
|
return _docChatFailureResponse('doc-chat-stream', filePath, result, sessionPreserved);
|
|
4777
2942
|
}
|
|
4778
2943
|
|
|
4779
|
-
|
|
2944
|
+
// No more ===ACTIONS=== / ---DOCUMENT--- parsing — CC's text answer is the
|
|
2945
|
+
// visible reply; file edits land via Edit/Write tools (validated in
|
|
2946
|
+
// _finalizeDocChatEdit by re-reading disk); state mutations land via direct
|
|
2947
|
+
// /api/* calls (correlated by X-CC-Turn-Id, surfaced as synthetic chips
|
|
2948
|
+
// server-side).
|
|
2949
|
+
return { answer: result.text, toolUses: Array.isArray(result.toolUses) ? result.toolUses : [] };
|
|
4780
2950
|
}
|
|
4781
2951
|
|
|
4782
2952
|
// -- POST helpers --
|
|
@@ -6843,7 +5013,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6843
5013
|
}
|
|
6844
5014
|
}
|
|
6845
5015
|
|
|
6846
|
-
let { answer,
|
|
5016
|
+
let { answer, partial, warning, toolUses, error: ccError } = await ccDocCall({
|
|
6847
5017
|
message: body.message, document: currentContent, title: body.title,
|
|
6848
5018
|
filePath: body.filePath, selection: body.selection, canEdit, isJson,
|
|
6849
5019
|
model: body.model || undefined,
|
|
@@ -6851,17 +5021,16 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6851
5021
|
transcript: body.transcript,
|
|
6852
5022
|
onAbortReady: (abort) => { _docAbort = abort; },
|
|
6853
5023
|
});
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
const actionFeedback = _buildDocChatActionFeedback(delegatedActions, actionResults);
|
|
5024
|
+
// CC now mutates files via the Edit/Write tools and dispatches via direct
|
|
5025
|
+
// /api/* calls (correlated by X-CC-Turn-Id). No server-side parsing of
|
|
5026
|
+
// ===ACTIONS=== or ---DOCUMENT--- — _finalizeDocChatEdit just confirms
|
|
5027
|
+
// the file on disk changed during the turn.
|
|
6859
5028
|
const finalize = _finalizeDocChatEdit({
|
|
6860
5029
|
filePath: body.filePath, fullPath, isJson, canEdit,
|
|
6861
|
-
originalContent: currentContent, delimiterContent:
|
|
5030
|
+
originalContent: currentContent, delimiterContent: null,
|
|
6862
5031
|
});
|
|
6863
5032
|
const payload = _buildDocChatResponsePayload({
|
|
6864
|
-
answer, actions:
|
|
5033
|
+
answer, actions: [], actionResults: [], actionFeedback: null, actionParseError: null, ccError, partial, warning, toolUses, finalize,
|
|
6865
5034
|
});
|
|
6866
5035
|
_docDone = true;
|
|
6867
5036
|
return jsonReply(res, 200, payload);
|
|
@@ -6938,7 +5107,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6938
5107
|
|
|
6939
5108
|
try {
|
|
6940
5109
|
|
|
6941
|
-
let { answer,
|
|
5110
|
+
let { answer, partial, warning, toolUses, error: ccError } = await ccDocCallStreaming({
|
|
6942
5111
|
message: body.message, document: currentContent, title: body.title,
|
|
6943
5112
|
filePath: body.filePath, selection: body.selection, canEdit, isJson,
|
|
6944
5113
|
model: body.model || undefined,
|
|
@@ -6949,17 +5118,12 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6949
5118
|
onToolUse: (name, input) => { writeDocEvent({ type: 'tool', name, input: _lightToolInput(input) }); },
|
|
6950
5119
|
onRetry: (attempt) => { writeDocEvent({ type: 'progress', attempt }); },
|
|
6951
5120
|
});
|
|
6952
|
-
const delegatedActions = _ensureDelegationForIntent(actions, {
|
|
6953
|
-
message: body.message, source: 'doc-chat', filePath: body.filePath, title: body.title, answerText: answer, toolUses,
|
|
6954
|
-
});
|
|
6955
|
-
const actionResults = await executeDocChatActions(delegatedActions, { filePath: body.filePath });
|
|
6956
|
-
const actionFeedback = _buildDocChatActionFeedback(delegatedActions, actionResults);
|
|
6957
5121
|
const finalize = _finalizeDocChatEdit({
|
|
6958
5122
|
filePath: body.filePath, fullPath, isJson, canEdit,
|
|
6959
|
-
originalContent: currentContent, delimiterContent:
|
|
5123
|
+
originalContent: currentContent, delimiterContent: null,
|
|
6960
5124
|
});
|
|
6961
5125
|
const payload = _buildDocChatResponsePayload({
|
|
6962
|
-
answer, actions:
|
|
5126
|
+
answer, actions: [], actionResults: [], actionFeedback: null, actionParseError: null, ccError, partial, warning, toolUses, finalize,
|
|
6963
5127
|
});
|
|
6964
5128
|
const { answer: finalAnswer, ...donePayload } = payload;
|
|
6965
5129
|
writeDocEvent({
|
|
@@ -7486,45 +5650,24 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7486
5650
|
});
|
|
7487
5651
|
}
|
|
7488
5652
|
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
//
|
|
7492
|
-
const
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
)
|
|
7502
|
-
if (parsed.actions.length > 0) {
|
|
7503
|
-
parsed.actionResults = await executeCCActions(parsed.actions);
|
|
7504
|
-
parsed.actionResultsAt = new Date().toISOString();
|
|
7505
|
-
}
|
|
7506
|
-
// Merge synthetic action results from CC's direct /api/* tool calls
|
|
7507
|
-
// (correlated via the X-CC-Turn-Id header). Replaces the ===ACTIONS===
|
|
7508
|
-
// executor for the canonical "CC calls APIs directly" path while
|
|
7509
|
-
// leaving the legacy parser available as a fallback during transition.
|
|
7510
|
-
const _synthetic = _buildSyntheticActionResultsForTurn(ccTurnId, body.message, parsed.actionResultsAt || new Date().toISOString());
|
|
7511
|
-
if (_synthetic.actions.length > 0) {
|
|
7512
|
-
parsed.actions = (parsed.actions || []).concat(_synthetic.actions);
|
|
7513
|
-
parsed.actionResults = (parsed.actionResults || []).concat(_synthetic.results);
|
|
7514
|
-
parsed.actionResultsAt = parsed.actionResultsAt || new Date().toISOString();
|
|
7515
|
-
}
|
|
7516
|
-
// Mirror only user-facing text to Teams; never send the internal action block.
|
|
5653
|
+
// CC mutates state via direct /api/* calls (correlated via X-CC-Turn-Id);
|
|
5654
|
+
// any creations during this turn surface as synthetic action results
|
|
5655
|
+
// for the existing chip renderer.
|
|
5656
|
+
const _synthetic = _buildSyntheticActionResultsForTurn(ccTurnId, body.message, new Date().toISOString());
|
|
5657
|
+
const replyBody = {
|
|
5658
|
+
text: result.text,
|
|
5659
|
+
actions: _synthetic.actions,
|
|
5660
|
+
actionResults: _synthetic.results,
|
|
5661
|
+
...(_synthetic.actions.length > 0 ? { actionResultsAt: new Date().toISOString() } : {}),
|
|
5662
|
+
sessionId: ccSession.sessionId,
|
|
5663
|
+
newSession: !wasResume,
|
|
5664
|
+
};
|
|
5665
|
+
// Mirror user-facing text to Teams (skip Teams-originated turns).
|
|
7517
5666
|
if (!tabId.startsWith('teams-')) {
|
|
7518
|
-
teams.teamsPostCCResponse(body.message,
|
|
5667
|
+
teams.teamsPostCCResponse(body.message, result.text).catch(() => {});
|
|
7519
5668
|
}
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
// but the JSON couldn't be recovered.
|
|
7523
|
-
const { _actionParseError, ...parsedReply } = parsed;
|
|
7524
|
-
const reply = { ...parsedReply, sessionId: ccSession.sessionId, newSession: !wasResume };
|
|
7525
|
-
if (_actionParseError) reply.actionParseError = _actionParseError;
|
|
7526
|
-
if (sessionReset) reply.sessionReset = true;
|
|
7527
|
-
return jsonReply(res, 200, reply);
|
|
5669
|
+
if (sessionReset) replyBody.sessionReset = true;
|
|
5670
|
+
return jsonReply(res, 200, replyBody);
|
|
7528
5671
|
} finally {
|
|
7529
5672
|
_releaseCCTab(tabId);
|
|
7530
5673
|
}
|
|
@@ -7559,9 +5702,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7559
5702
|
engineConfig,
|
|
7560
5703
|
onChunk: (text) => {
|
|
7561
5704
|
_touchCcLiveStream(liveState);
|
|
7562
|
-
|
|
7563
|
-
liveState.text
|
|
7564
|
-
if (liveState.writer) liveState.writer({ type: 'chunk', text: display });
|
|
5705
|
+
liveState.text = text;
|
|
5706
|
+
if (liveState.writer) liveState.writer({ type: 'chunk', text });
|
|
7565
5707
|
},
|
|
7566
5708
|
onToolUse: (name, input) => {
|
|
7567
5709
|
_touchCcLiveStream(liveState);
|
|
@@ -7835,36 +5977,13 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
7835
5977
|
} catch { /* non-critical */ }
|
|
7836
5978
|
}
|
|
7837
5979
|
|
|
7838
|
-
//
|
|
7839
|
-
|
|
7840
|
-
|
|
7841
|
-
const
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
console.warn('[CC] /loop invocation detected — converted to create-watch');
|
|
7845
|
-
try { shared.log('warn', '/loop invocation detected in CC response — auto-converted to create-watch'); } catch {}
|
|
7846
|
-
}
|
|
7847
|
-
actions = _actionsWithIntentFallback(
|
|
7848
|
-
_filterImplicitPostDispatchActions(actions, body.message),
|
|
7849
|
-
{ message: body.message, intentMetadata: body.intentMetadata, source: 'command-center', answerText: displayText, toolUses }
|
|
7850
|
-
);
|
|
7851
|
-
let actionResults;
|
|
7852
|
-
let actionResultsAt;
|
|
7853
|
-
if (actions.length > 0) {
|
|
7854
|
-
actionResults = await executeCCActions(actions);
|
|
7855
|
-
actionResultsAt = new Date().toISOString();
|
|
7856
|
-
}
|
|
7857
|
-
// Merge synthetic action results from CC's direct /api/* tool calls.
|
|
7858
|
-
const _streamSynthetic = _buildSyntheticActionResultsForTurn(ccTurnId, body.message, actionResultsAt || new Date().toISOString());
|
|
7859
|
-
if (_streamSynthetic.actions.length > 0) {
|
|
7860
|
-
actions = (actions || []).concat(_streamSynthetic.actions);
|
|
7861
|
-
actionResults = (actionResults || []).concat(_streamSynthetic.results);
|
|
7862
|
-
actionResultsAt = actionResultsAt || new Date().toISOString();
|
|
7863
|
-
}
|
|
5980
|
+
// CC mutates state via direct /api/* tool calls; surface those as chips.
|
|
5981
|
+
const displayText = result.text;
|
|
5982
|
+
const _streamSynthetic = _buildSyntheticActionResultsForTurn(ccTurnId, body.message, new Date().toISOString());
|
|
5983
|
+
const actions = _streamSynthetic.actions;
|
|
5984
|
+
const actionResults = _streamSynthetic.results;
|
|
5985
|
+
const actionResultsAt = actions.length > 0 ? new Date().toISOString() : undefined;
|
|
7864
5986
|
const donePayload = { type: 'done', text: displayText, actions, actionResults, actionResultsAt, sessionId: responseSessionId, newSession: !wasResume };
|
|
7865
|
-
// Issue #1834: surface action JSON parse failures so the UI can warn
|
|
7866
|
-
// instead of silently dropping. Client renders this as a small notice.
|
|
7867
|
-
if (_actionParseError) donePayload.actionParseError = _actionParseError;
|
|
7868
5987
|
if (sessionReset) {
|
|
7869
5988
|
donePayload.sessionReset = true;
|
|
7870
5989
|
if (sessionResetReason) donePayload.sessionResetReason = sessionResetReason;
|
|
@@ -9492,12 +7611,8 @@ module.exports = {
|
|
|
9492
7611
|
readBody,
|
|
9493
7612
|
_filterCcTabSessions,
|
|
9494
7613
|
_getVersionCheckInterval,
|
|
9495
|
-
_parseWatchInterval,
|
|
9496
7614
|
_normalizeMeetingParticipants: normalizeMeetingParticipants,
|
|
9497
|
-
_meetingParticipantsFromAction: meetingParticipantsFromAction,
|
|
9498
7615
|
parsePinnedEntries,
|
|
9499
|
-
_parseDocChatResultText,
|
|
9500
|
-
_buildDocChatActionFeedback,
|
|
9501
7616
|
_formatDocChatContext,
|
|
9502
7617
|
_isCompletedMeetingJson,
|
|
9503
7618
|
_finalizeDocChatEdit,
|
|
@@ -9511,18 +7626,15 @@ module.exports = {
|
|
|
9511
7626
|
_shouldSuppressDocChatPostPatchError,
|
|
9512
7627
|
_buildDocChatResponsePayload,
|
|
9513
7628
|
_inferDocChatProject,
|
|
9514
|
-
_messageHasDelegationIntent,
|
|
9515
|
-
_messageRequestsDirectHandling,
|
|
9516
|
-
_messageHasMediumLargeWorkIntent,
|
|
9517
|
-
_inferDelegationActionFromMessage,
|
|
9518
|
-
_ensureDelegationForIntent,
|
|
9519
7629
|
_linkPullRequestForTracking: linkPullRequestForTracking,
|
|
9520
7630
|
_resolveSkillReadPath,
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
7631
|
+
// Per-CC-turn correlation surface (replaces ===ACTIONS=== protocol)
|
|
7632
|
+
_ccTurnCreations,
|
|
7633
|
+
_recordCcTurnCreation,
|
|
7634
|
+
_consumeCcTurnCreations,
|
|
7635
|
+
_readCcTurnIdHeader,
|
|
7636
|
+
_buildSyntheticActionResultsForTurn,
|
|
7637
|
+
CC_TURN_CREATION_TTL_MS,
|
|
9526
7638
|
_findDuplicateWorkItemCreate: findDuplicateWorkItemCreate,
|
|
9527
7639
|
_createWorkItemWithDedup: createWorkItemWithDedup,
|
|
9528
7640
|
_resolveWorkItemsCreateTarget: resolveWorkItemsCreateTarget,
|
|
@@ -9530,12 +7642,6 @@ module.exports = {
|
|
|
9530
7642
|
_buildManualPrdItemPlan: buildManualPrdItemPlan,
|
|
9531
7643
|
_resolveScheduleProjectValue: resolveScheduleProjectValue,
|
|
9532
7644
|
_collectArchivedWorkItems: collectArchivedWorkItems,
|
|
9533
|
-
_createPipelineFromAction: createPipelineFromAction,
|
|
9534
|
-
_setCcLocalApiInvokerForTest,
|
|
9535
|
-
_resetCcApiRoutesMetaForTest,
|
|
9536
|
-
_ccValidateLocalApiFallback,
|
|
9537
|
-
executeCCActions,
|
|
9538
|
-
executeDocChatActions,
|
|
9539
7645
|
buildCCStatePreamble,
|
|
9540
7646
|
_routesAsMeta,
|
|
9541
7647
|
_buildTranscriptCarryover,
|