@yemi33/minions 0.1.1642 → 0.1.1644
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/command-center.js +19 -0
- package/dashboard.js +138 -11
- package/engine/ado.js +3 -2
- package/engine/copilot-models.json +1 -1
- package/engine/github.js +5 -4
- package/engine/lifecycle.js +57 -45
- package/engine/queries.js +13 -1
- package/engine/shared.js +56 -4
- package/engine/steering.js +187 -0
- package/engine/timeout.js +12 -15
- package/engine.js +43 -6
- package/package.json +1 -1
- package/prompts/cc-system.md +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -686,11 +686,30 @@ async function _ccDoSend(message, skipUserMsg, forceTabId) {
|
|
|
686
686
|
if (evt.actions && evt.actions.length > 0) {
|
|
687
687
|
_tagServerExecuted(evt.actions, evt.actionResults);
|
|
688
688
|
for (var ai = 0; ai < evt.actions.length; ai++) { await ccExecuteAction(evt.actions[ai], activeTabId); }
|
|
689
|
+
// Surface per-action errors/warnings inline alongside the prose so the user can see
|
|
690
|
+
// exactly which actions failed or completed with caveats. Previously these only
|
|
691
|
+
// appeared as a small "executed" pill which gave no detail.
|
|
692
|
+
if (evt.actionResults && Array.isArray(evt.actionResults)) {
|
|
693
|
+
var failures = evt.actionResults.filter(function(r) { return r && r.error; });
|
|
694
|
+
var warnings = evt.actionResults.filter(function(r) { return r && r.warning; });
|
|
695
|
+
if (failures.length > 0) {
|
|
696
|
+
var failHtml = failures.map(function(r) { return '<li>' + escHtml(r.type || 'action') + ': ' + escHtml(r.error) + '</li>'; }).join('');
|
|
697
|
+
addMsg('system', '<div style="padding:6px 12px;font-size:11px;color:var(--red);background:var(--surface2);border-radius:6px;margin:4px 0">⚠️ ' + failures.length + ' action' + (failures.length > 1 ? 's' : '') + ' failed:<ul style="margin:4px 0 0 16px;padding:0">' + failHtml + '</ul></div>', false, activeTabId);
|
|
698
|
+
}
|
|
699
|
+
if (warnings.length > 0) {
|
|
700
|
+
var warnHtml = warnings.map(function(r) { return '<li>' + escHtml(r.type || 'action') + ': ' + escHtml(r.warning) + '</li>'; }).join('');
|
|
701
|
+
addMsg('system', '<div style="padding:6px 12px;font-size:11px;color:var(--orange);background:var(--surface2);border-radius:6px;margin:4px 0">ℹ️ ' + warnings.length + ' action' + (warnings.length > 1 ? '' : '') + ' completed with warnings:<ul style="margin:4px 0 0 16px;padding:0">' + warnHtml + '</ul></div>', false, activeTabId);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
689
704
|
} else if (evt.actionParseError) {
|
|
690
705
|
// Issue #1834: server saw ===ACTIONS=== but couldn't parse the JSON.
|
|
691
706
|
// Surface as an inline warning so the user knows actions were dropped
|
|
692
707
|
// (was previously silent — appeared as "actions failed" with no signal).
|
|
693
708
|
addMsg('system', '<div style="padding:6px 12px;font-size:11px;color:var(--red);background:var(--surface2);border-radius:6px;margin:4px 0">⚠️ Actions block emitted but JSON could not be parsed — no actions were executed. Resend or rephrase. (' + escHtml(String(evt.actionParseError).slice(0, 200)) + ')</div>', false, activeTabId);
|
|
709
|
+
} else if (evt.hallucinationWarning) {
|
|
710
|
+
// CC said it dispatched/queued/assigned something but emitted no actions block —
|
|
711
|
+
// surface the false-claim guard so the user knows nothing actually ran.
|
|
712
|
+
addMsg('system', '<div style="padding:6px 12px;font-size:11px;color:var(--orange);background:var(--surface2);border-radius:6px;margin:4px 0">⚠️ ' + escHtml(evt.hallucinationWarning) + '</div>', false, activeTabId);
|
|
694
713
|
}
|
|
695
714
|
} else if (evt.type === 'error') {
|
|
696
715
|
terminalEventSeen = true;
|
package/dashboard.js
CHANGED
|
@@ -28,6 +28,7 @@ const watchesMod = require('./engine/watches');
|
|
|
28
28
|
const routing = require('./engine/routing');
|
|
29
29
|
const playbook = require('./engine/playbook');
|
|
30
30
|
const dispatchMod = require('./engine/dispatch');
|
|
31
|
+
const steering = require('./engine/steering');
|
|
31
32
|
const os = require('os');
|
|
32
33
|
|
|
33
34
|
const { safeRead, safeReadDir, safeWrite, safeJson, safeJsonObj, safeJsonArr, safeUnlink, mutateJsonFileLocked, mutateWorkItems, getProjects: _getProjects, DONE_STATUSES, WI_STATUS, reopenWorkItem } = shared;
|
|
@@ -148,6 +149,21 @@ function _resolveSkillReadPath({ file, dir, source, config, skillFiles } = {}) {
|
|
|
148
149
|
return null;
|
|
149
150
|
}
|
|
150
151
|
|
|
152
|
+
function _agentSessionIsDraining(agentId) {
|
|
153
|
+
const activeForAgent = (getDispatchQueue().active || []).some(d => d.agent === agentId);
|
|
154
|
+
if (!activeForAgent) return false;
|
|
155
|
+
const liveLogPath = path.join(AGENTS_DIR, agentId, 'live-output.log');
|
|
156
|
+
const tail = (safeRead(liveLogPath) || '').slice(-65536);
|
|
157
|
+
if (!tail) return false;
|
|
158
|
+
const lastSteer = tail.lastIndexOf('[human-steering]');
|
|
159
|
+
const terminalIdx = Math.max(
|
|
160
|
+
tail.lastIndexOf('[process-exit]'),
|
|
161
|
+
tail.lastIndexOf('"type":"session.task_complete"'),
|
|
162
|
+
tail.lastIndexOf('"type":"result"')
|
|
163
|
+
);
|
|
164
|
+
return terminalIdx >= 0 && terminalIdx > lastSteer;
|
|
165
|
+
}
|
|
166
|
+
|
|
151
167
|
const PLANS_DIR = path.join(MINIONS_DIR, 'plans');
|
|
152
168
|
const TEAMS_INBOX_PATH = path.join(ENGINE_DIR, 'teams-inbox.json');
|
|
153
169
|
|
|
@@ -1296,17 +1312,100 @@ function _parseWatchInterval(val) {
|
|
|
1296
1312
|
return Math.max(60000, Math.round(u === 's' ? n * 1000 : u === 'm' ? n * 60000 : n * 3600000));
|
|
1297
1313
|
}
|
|
1298
1314
|
|
|
1315
|
+
// Required-field validator for CC actions. Returns null when valid, an error string when not.
|
|
1316
|
+
// Centralises field-required checks so the model can't quietly emit a malformed action and have
|
|
1317
|
+
// the server silently fall back to placeholder values (e.g. "Untitled"). The handler invokes this
|
|
1318
|
+
// before `try` to avoid filling `results` with cryptic per-handler error messages.
|
|
1319
|
+
function _ccValidateAction(action) {
|
|
1320
|
+
if (!action || typeof action !== 'object' || !action.type) return 'action is missing required field: type';
|
|
1321
|
+
switch (action.type) {
|
|
1322
|
+
case 'dispatch': case 'fix': case 'implement': case 'explore': case 'review': case 'test':
|
|
1323
|
+
if (!action.title || typeof action.title !== 'string' || !action.title.trim()) return `${action.type} action missing required field: title`;
|
|
1324
|
+
return null;
|
|
1325
|
+
case 'build-and-test':
|
|
1326
|
+
if (!action.pr) return 'build-and-test action missing required field: pr';
|
|
1327
|
+
return null;
|
|
1328
|
+
case 'note':
|
|
1329
|
+
if (!action.title) return 'note action missing required field: title';
|
|
1330
|
+
if (!action.content && !action.description) return 'note action missing required field: content (or description)';
|
|
1331
|
+
return null;
|
|
1332
|
+
case 'knowledge':
|
|
1333
|
+
if (!action.title) return 'knowledge action missing required field: title';
|
|
1334
|
+
if (!action.content) return 'knowledge action missing required field: content';
|
|
1335
|
+
if (!action.category) return 'knowledge action missing required field: category';
|
|
1336
|
+
return null;
|
|
1337
|
+
case 'pin-to-pinned':
|
|
1338
|
+
if (!action.title || !action.content) return 'pin-to-pinned action missing title or content';
|
|
1339
|
+
return null;
|
|
1340
|
+
case 'plan':
|
|
1341
|
+
if (!action.title) return 'plan action missing required field: title';
|
|
1342
|
+
return null;
|
|
1343
|
+
default:
|
|
1344
|
+
return null; // unknown types fall through to existing handler / generic fallback
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Hallucination guard: detect prose like "I dispatched ..." when no ===ACTIONS=== block was emitted.
|
|
1349
|
+
// The regex is intentionally narrow — we only want affirmative claims about completed work, not
|
|
1350
|
+
// hypotheticals like "I would dispatch this" or "consider dispatching X".
|
|
1351
|
+
function _detectClaimedActionWithoutBlock(displayText, actions) {
|
|
1352
|
+
if (Array.isArray(actions) && actions.length > 0) return null; // there are actions, no false claim
|
|
1353
|
+
const triggers = /\b(dispatched|enqueued|queued|created (?:a |the )?work item|assigned (?:this |it )?(?:to|for)|spun up|kicked off|i'?ll dispatch|i (?:have )?(?:just )?dispatched)\b/i;
|
|
1354
|
+
if (!triggers.test(displayText || '')) return null;
|
|
1355
|
+
return 'CC described an action ("dispatched", "assigned", etc.) but no ===ACTIONS=== block was emitted. No work was actually queued. Resend or rephrase the request.';
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1299
1358
|
async function executeCCActions(actions) {
|
|
1300
1359
|
const results = [];
|
|
1301
1360
|
for (const action of actions) {
|
|
1361
|
+
const validationError = _ccValidateAction(action);
|
|
1362
|
+
if (validationError) {
|
|
1363
|
+
results.push({ type: action?.type || 'unknown', error: validationError });
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1302
1366
|
try {
|
|
1303
1367
|
switch (action.type) {
|
|
1304
1368
|
case 'dispatch': case 'fix': case 'implement': case 'explore': case 'review': case 'test': {
|
|
1305
1369
|
const workType = action.workType || (action.type !== 'dispatch' ? action.type : 'implement');
|
|
1306
1370
|
const id = 'W-' + shared.uid();
|
|
1307
1371
|
const project = action.project || '';
|
|
1308
|
-
|
|
1372
|
+
|
|
1373
|
+
// Strict project resolution. Silent fallback to PROJECTS[0] when the model named an unknown
|
|
1374
|
+
// project caused work items to land in the wrong repo. Now: unknown name → error; ambiguous
|
|
1375
|
+
// (multiple projects + no field) → error; single-project deployments fall through; zero
|
|
1376
|
+
// projects → root-level work-items.json (orchestration system standalone use).
|
|
1377
|
+
let targetProject = null;
|
|
1378
|
+
if (project) {
|
|
1379
|
+
targetProject = PROJECTS.find(p => p.name?.toLowerCase() === project.toLowerCase());
|
|
1380
|
+
if (!targetProject) {
|
|
1381
|
+
const known = PROJECTS.map(p => p.name).join(', ') || '(none configured)';
|
|
1382
|
+
results.push({ type: action.type, error: `Project "${project}" not found. Known projects: ${known}` });
|
|
1383
|
+
break;
|
|
1384
|
+
}
|
|
1385
|
+
} else if (PROJECTS.length > 1) {
|
|
1386
|
+
results.push({ type: action.type, error: `project field is required when ${PROJECTS.length} projects are configured: ${PROJECTS.map(p => p.name).join(', ')}` });
|
|
1387
|
+
break;
|
|
1388
|
+
} else if (PROJECTS.length === 1) {
|
|
1389
|
+
targetProject = PROJECTS[0];
|
|
1390
|
+
}
|
|
1391
|
+
// PROJECTS.length === 0 → targetProject stays null, falls back to root work-items.json (existing behavior).
|
|
1392
|
+
|
|
1309
1393
|
const wiPath = targetProject ? shared.projectWorkItemsPath(targetProject) : path.join(MINIONS_DIR, 'work-items.json');
|
|
1394
|
+
|
|
1395
|
+
// Promote `agent` (singular) → `agents` (array). Models emit either shape and the prior code
|
|
1396
|
+
// only read `action.agents`, silently dropping `agent: "lambert"` style hints.
|
|
1397
|
+
const agentHints = (() => {
|
|
1398
|
+
if (Array.isArray(action.agents) && action.agents.length > 0) return action.agents.map(String).filter(Boolean);
|
|
1399
|
+
if (typeof action.agent === 'string' && action.agent) return [action.agent];
|
|
1400
|
+
return [];
|
|
1401
|
+
})();
|
|
1402
|
+
const knownAgents = Object.keys(CONFIG.agents || {});
|
|
1403
|
+
const unknownAgent = agentHints.find(a => !knownAgents.includes(a));
|
|
1404
|
+
if (unknownAgent) {
|
|
1405
|
+
results.push({ type: action.type, error: `Unknown agent "${unknownAgent}". Configured agents: ${knownAgents.join(', ') || '(none)'}` });
|
|
1406
|
+
break;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1310
1409
|
// Issue #1772: CC review/explore/test are human-initiated one-offs.
|
|
1311
1410
|
// Mark oneShot so any discovered PR is tagged _contextOnly (skips eval loop).
|
|
1312
1411
|
const ccOneShotTypes = new Set(['review', 'explore', 'test']);
|
|
@@ -1314,16 +1413,28 @@ async function executeCCActions(actions) {
|
|
|
1314
1413
|
shared.mutateJsonFileLocked(wiPath, items => {
|
|
1315
1414
|
if (!Array.isArray(items)) items = [];
|
|
1316
1415
|
items.push({
|
|
1317
|
-
id, title: action.title
|
|
1416
|
+
id, title: action.title, type: workType,
|
|
1318
1417
|
priority: action.priority || 'medium', description: action.description || '',
|
|
1319
1418
|
status: WI_STATUS.PENDING, created: new Date().toISOString(),
|
|
1320
1419
|
createdBy: 'command-center', project,
|
|
1321
|
-
...(
|
|
1420
|
+
...(agentHints.length ? { preferred_agent: agentHints[0], agents: agentHints } : {}),
|
|
1322
1421
|
...(isOneShot ? { oneShot: true } : {}),
|
|
1323
1422
|
});
|
|
1324
1423
|
return items;
|
|
1325
1424
|
}, { defaultValue: [] });
|
|
1326
1425
|
results.push({ type: action.type, id, ok: true });
|
|
1426
|
+
|
|
1427
|
+
// Pre-flight routing check: warn the user if no agent is currently available so the new
|
|
1428
|
+
// item won't sit pending invisibly. Routing failure is non-fatal — the WI was created.
|
|
1429
|
+
try {
|
|
1430
|
+
const resolvedAgent = routing.resolveAgent(workType, CONFIG, { agentHints });
|
|
1431
|
+
if (!resolvedAgent) {
|
|
1432
|
+
const lastResult = results[results.length - 1];
|
|
1433
|
+
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.`;
|
|
1434
|
+
}
|
|
1435
|
+
} catch (e) {
|
|
1436
|
+
shared.log('warn', `CC dispatch routing pre-flight: ${e.message}`);
|
|
1437
|
+
}
|
|
1327
1438
|
break;
|
|
1328
1439
|
}
|
|
1329
1440
|
case 'build-and-test': {
|
|
@@ -4753,6 +4864,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
4753
4864
|
const { _actionParseError, ...parsedReply } = parsed;
|
|
4754
4865
|
const reply = { ...parsedReply, sessionId: ccSession.sessionId, newSession: !wasResume };
|
|
4755
4866
|
if (_actionParseError) reply.actionParseError = _actionParseError;
|
|
4867
|
+
const hallucinationWarning = _detectClaimedActionWithoutBlock(parsed.text, parsed.actions);
|
|
4868
|
+
if (hallucinationWarning) reply.hallucinationWarning = hallucinationWarning;
|
|
4756
4869
|
if (sessionReset) reply.sessionReset = true;
|
|
4757
4870
|
return jsonReply(res, 200, reply);
|
|
4758
4871
|
} finally {
|
|
@@ -5036,6 +5149,8 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5036
5149
|
// Issue #1834: surface action JSON parse failures so the UI can warn
|
|
5037
5150
|
// instead of silently dropping. Client renders this as a small notice.
|
|
5038
5151
|
if (_actionParseError) donePayload.actionParseError = _actionParseError;
|
|
5152
|
+
const hallucinationWarning = _detectClaimedActionWithoutBlock(displayText, actions);
|
|
5153
|
+
if (hallucinationWarning) donePayload.hallucinationWarning = hallucinationWarning;
|
|
5039
5154
|
if (sessionReset) donePayload.sessionReset = true;
|
|
5040
5155
|
liveState.donePayload = donePayload;
|
|
5041
5156
|
if (liveState.writer) liveState.writer(donePayload);
|
|
@@ -5924,7 +6039,7 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
5924
6039
|
if (!pr.branch && prData.branch) {
|
|
5925
6040
|
pr.branch = prData.branch;
|
|
5926
6041
|
if (pr._branchResolutionError) delete pr._branchResolutionError;
|
|
5927
|
-
if (pr._pendingReason ===
|
|
6042
|
+
if (pr._pendingReason === shared.PR_PENDING_REASON.MISSING_BRANCH) delete pr._pendingReason;
|
|
5928
6043
|
}
|
|
5929
6044
|
if (pr.agent === 'human' && prData.author) pr.agent = prData.author;
|
|
5930
6045
|
return prs;
|
|
@@ -6002,21 +6117,30 @@ What would you like to discuss or change? When you're happy, say "approve" and I
|
|
|
6002
6117
|
}},
|
|
6003
6118
|
{ method: 'POST', path: '/api/agents/steer', desc: 'Inject steering message into a running agent', params: 'agent, message', handler: async (req, res) => {
|
|
6004
6119
|
const body = await readBody(req);
|
|
6005
|
-
const { agent
|
|
6006
|
-
if (!
|
|
6120
|
+
const { agent, message } = body;
|
|
6121
|
+
if (!agent || !message) return jsonReply(res, 400, { error: 'agent and message required' });
|
|
6122
|
+
const agentId = String(agent).replace(/[^a-zA-Z0-9_-]/g, '');
|
|
6123
|
+
const text = String(message).trim();
|
|
6124
|
+
if (!agentId || !text) return jsonReply(res, 400, { error: 'agent and message required' });
|
|
6007
6125
|
|
|
6008
|
-
const steerPath = path.join(MINIONS_DIR, 'agents', agentId, 'steer.md');
|
|
6009
6126
|
const agentDir = path.join(MINIONS_DIR, 'agents', agentId);
|
|
6010
6127
|
if (!fs.existsSync(agentDir)) return jsonReply(res, 404, { error: 'Agent not found' });
|
|
6128
|
+
if (_agentSessionIsDraining(agentId)) {
|
|
6129
|
+
return jsonReply(res, 409, { error: 'Agent session is finishing; retry when the next session starts' });
|
|
6130
|
+
}
|
|
6011
6131
|
|
|
6012
|
-
|
|
6013
|
-
safeWrite(steerPath, message);
|
|
6132
|
+
const entry = steering.writeSteeringMessage(agentId, text);
|
|
6014
6133
|
|
|
6015
6134
|
// Also append to live-output.log so it shows in the chat view
|
|
6016
6135
|
const liveLogPath = path.join(agentDir, 'live-output.log');
|
|
6017
|
-
try { fs.appendFileSync(liveLogPath, '\n[human-steering] ' +
|
|
6136
|
+
try { fs.appendFileSync(liveLogPath, '\n[human-steering] ' + text + '\n'); } catch { /* optional */ }
|
|
6018
6137
|
|
|
6019
|
-
return jsonReply(res, 200, {
|
|
6138
|
+
return jsonReply(res, 200, {
|
|
6139
|
+
ok: true,
|
|
6140
|
+
message: 'Steering message queued',
|
|
6141
|
+
file: entry?.file || null,
|
|
6142
|
+
inboxCount: steering.listUnreadSteeringMessages(agentId).length,
|
|
6143
|
+
});
|
|
6020
6144
|
}},
|
|
6021
6145
|
{ method: 'POST', path: '/api/agents/cancel', desc: 'Cancel an active agent by ID or task substring', params: 'agent?, task?', handler: handleAgentsCancel },
|
|
6022
6146
|
{ method: 'POST', path: /^\/api\/agent\/([\w-]+)\/kill$/, desc: 'Kill a running agent: stop process, clear dispatch, reset work items to pending', handler: handleAgentKill },
|
|
@@ -6425,6 +6549,9 @@ module.exports = {
|
|
|
6425
6549
|
_linkPullRequestForTracking: linkPullRequestForTracking,
|
|
6426
6550
|
_resolveSkillReadPath,
|
|
6427
6551
|
DOC_CHAT_DOCUMENT_DELIMITER,
|
|
6552
|
+
_ccValidateAction,
|
|
6553
|
+
_detectClaimedActionWithoutBlock,
|
|
6554
|
+
executeCCActions,
|
|
6428
6555
|
};
|
|
6429
6556
|
|
|
6430
6557
|
// Start the HTTP server only when run directly (node dashboard.js).
|
package/engine/ado.js
CHANGED
|
@@ -268,7 +268,8 @@ async function forEachActivePr(config, token, callback) {
|
|
|
268
268
|
if (!project.adoOrg || !project.adoProject) continue;
|
|
269
269
|
|
|
270
270
|
const prs = getPrs(project);
|
|
271
|
-
const activePrs = prs.filter(pr => shared.PR_POLLABLE_STATUSES.has(pr.status)
|
|
271
|
+
const activePrs = prs.filter(pr => shared.PR_POLLABLE_STATUSES.has(pr.status)
|
|
272
|
+
&& shared.isPrCompatibleWithProject(project, pr, pr.url || ''));
|
|
272
273
|
if (activePrs.length === 0) continue;
|
|
273
274
|
|
|
274
275
|
const adoRepositoryId = getAdoRepositoryId(project);
|
|
@@ -798,7 +799,7 @@ async function reconcilePrs(config) {
|
|
|
798
799
|
if (existing && !existing.branch && branch) {
|
|
799
800
|
existing.branch = branch;
|
|
800
801
|
if (existing._branchResolutionError) delete existing._branchResolutionError;
|
|
801
|
-
if (existing._pendingReason ===
|
|
802
|
+
if (existing._pendingReason === shared.PR_PENDING_REASON.MISSING_BRANCH) delete existing._pendingReason;
|
|
802
803
|
metadataUpdated++;
|
|
803
804
|
}
|
|
804
805
|
// PR already tracked — write link to pr-links.json if we can extract an ID
|
package/engine/github.js
CHANGED
|
@@ -206,7 +206,8 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
206
206
|
if (isSlugInBackoff(slug)) continue;
|
|
207
207
|
|
|
208
208
|
const prs = getPrs(project);
|
|
209
|
-
const activePrs = prs.filter(pr => PR_POLLABLE_STATUSES.has(pr.status)
|
|
209
|
+
const activePrs = prs.filter(pr => PR_POLLABLE_STATUSES.has(pr.status)
|
|
210
|
+
&& shared.isPrCompatibleWithProject(project, pr, pr.url || ''));
|
|
210
211
|
if (activePrs.length === 0) continue;
|
|
211
212
|
|
|
212
213
|
// Probe repo accessibility before iterating PRs — avoids N warnings per inaccessible repo
|
|
@@ -285,7 +286,7 @@ async function forEachActiveGhPr(config, callback) {
|
|
|
285
286
|
if (!pr.branch && prData.head?.ref) {
|
|
286
287
|
pr.branch = prData.head.ref;
|
|
287
288
|
if (pr._branchResolutionError) delete pr._branchResolutionError;
|
|
288
|
-
if (pr._pendingReason ===
|
|
289
|
+
if (pr._pendingReason === shared.PR_PENDING_REASON.MISSING_BRANCH) delete pr._pendingReason;
|
|
289
290
|
}
|
|
290
291
|
}
|
|
291
292
|
}
|
|
@@ -332,7 +333,7 @@ async function pollPrStatus(config) {
|
|
|
332
333
|
if (headBranch && pr.branch !== headBranch) {
|
|
333
334
|
pr.branch = headBranch;
|
|
334
335
|
if (pr._branchResolutionError) delete pr._branchResolutionError;
|
|
335
|
-
if (pr._pendingReason ===
|
|
336
|
+
if (pr._pendingReason === shared.PR_PENDING_REASON.MISSING_BRANCH) delete pr._pendingReason;
|
|
336
337
|
updated = true;
|
|
337
338
|
}
|
|
338
339
|
|
|
@@ -713,7 +714,7 @@ async function reconcilePrs(config) {
|
|
|
713
714
|
if (existing && !existing.branch && branch) {
|
|
714
715
|
existing.branch = branch;
|
|
715
716
|
if (existing._branchResolutionError) delete existing._branchResolutionError;
|
|
716
|
-
if (existing._pendingReason ===
|
|
717
|
+
if (existing._pendingReason === shared.PR_PENDING_REASON.MISSING_BRANCH) delete existing._pendingReason;
|
|
717
718
|
metadataUpdated++;
|
|
718
719
|
}
|
|
719
720
|
if (confirmedItemId) {
|
package/engine/lifecycle.js
CHANGED
|
@@ -707,11 +707,46 @@ function reconcilePrdStatuses(config) {
|
|
|
707
707
|
|
|
708
708
|
function syncPrsFromOutput(output, agentId, meta, config) {
|
|
709
709
|
|
|
710
|
-
const
|
|
711
|
-
const
|
|
712
|
-
const
|
|
710
|
+
const prEvidence = new Map();
|
|
711
|
+
const trustedPrCreateToolIds = new Set();
|
|
712
|
+
const prUrlPattern = /(https?:\/\/github\.com\/[^\s"'\\)\]]+\/[^\s"'\\)\]]+\/pull\/(\d+)(?:[^\s"'\\)\]]*)?|https?:\/\/(?:dev\.azure\.com|[^/\s"'\\)\]]+\.visualstudio\.com)[^\s"'\\)\]]*?pullrequest\/(\d+)(?:[^\s"'\\)\]]*)?)/gi;
|
|
713
713
|
let match;
|
|
714
714
|
|
|
715
|
+
function cleanPrUrl(url) {
|
|
716
|
+
return String(url || '').replace(/[.,;:]+$/, '');
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function addPrUrlEvidence(text) {
|
|
720
|
+
if (!text) return;
|
|
721
|
+
prUrlPattern.lastIndex = 0;
|
|
722
|
+
while ((match = prUrlPattern.exec(String(text))) !== null) {
|
|
723
|
+
const prId = match[2] || match[3];
|
|
724
|
+
if (prId && !prEvidence.has(prId)) prEvidence.set(prId, cleanPrUrl(match[1]));
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function addExplicitPrCreatedEvidence(text) {
|
|
729
|
+
if (!text) return;
|
|
730
|
+
const explicitPrCreatedPattern = /(?:^|\n)\s*\*{0,2}(?:PR|Pull\s+Request|E2E\s+PR)\s+(?:created|opened|submitted)\*{0,2}\s*[:\-]\s*([^\n]+)/gi;
|
|
731
|
+
let createdMatch;
|
|
732
|
+
while ((createdMatch = explicitPrCreatedPattern.exec(String(text))) !== null) {
|
|
733
|
+
addPrUrlEvidence(createdMatch[1]);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function isTrustedPrCreateToolUse(block) {
|
|
738
|
+
const name = String(block?.name || '');
|
|
739
|
+
if (/(?:create|open|submit)[_-]?(?:pull[_-]?request|pr)|(?:pull[_-]?request|pr)[_-]?(?:create|open|submit)/i.test(name)) {
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
const inputText = typeof block?.input === 'string' ? block.input : JSON.stringify(block?.input || {});
|
|
743
|
+
if (/\bgh(?:\.exe)?\s+pr\s+create\b/i.test(inputText)) return true;
|
|
744
|
+
if (/\baz(?:\.cmd|\.exe)?\s+repos\s+pr\s+create\b/i.test(inputText)) return true;
|
|
745
|
+
const callsAdoCreateApi = /_apis\/git\/repositories\/[^\s"'\\]+\/pullrequests\b/i.test(inputText);
|
|
746
|
+
const usesPost = /\bPOST\b|-X\s*POST|-Method\s+POST|method["']?\s*:\s*["']?POST/i.test(inputText);
|
|
747
|
+
return callsAdoCreateApi && usesPost;
|
|
748
|
+
}
|
|
749
|
+
|
|
715
750
|
try {
|
|
716
751
|
const lines = output.split('\n');
|
|
717
752
|
for (const line of lines) {
|
|
@@ -720,60 +755,43 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
720
755
|
const parsed = JSON.parse(line);
|
|
721
756
|
const content = parsed.message?.content || [];
|
|
722
757
|
for (const block of content) {
|
|
723
|
-
|
|
758
|
+
if (block.type === 'tool_use' && block.id && isTrustedPrCreateToolUse(block)) {
|
|
759
|
+
trustedPrCreateToolIds.add(block.id);
|
|
760
|
+
}
|
|
761
|
+
// Tool output is trusted only when tied to a known PR-create command/API call.
|
|
724
762
|
if (block.type === 'tool_result' && block.content) {
|
|
725
|
-
|
|
726
|
-
|
|
763
|
+
if (trustedPrCreateToolIds.has(block.tool_use_id)) {
|
|
764
|
+
const text = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
765
|
+
addPrUrlEvidence(text);
|
|
766
|
+
}
|
|
727
767
|
}
|
|
728
|
-
//
|
|
768
|
+
// Assistant text must use the explicit Minions PR-created protocol line.
|
|
729
769
|
if (block.type === 'text' && block.text) {
|
|
730
|
-
|
|
731
|
-
textCreatedPattern.lastIndex = 0;
|
|
732
|
-
let m2;
|
|
733
|
-
while ((m2 = textCreatedPattern.exec(block.text)) !== null) prMatches.add(m2[1]);
|
|
770
|
+
addExplicitPrCreatedEvidence(block.text);
|
|
734
771
|
}
|
|
735
772
|
}
|
|
736
773
|
if (parsed.type === 'result' && parsed.result) {
|
|
737
|
-
const resultText = parsed.result;
|
|
738
|
-
|
|
739
|
-
while ((match = createdPattern.exec(resultText)) !== null) prMatches.add(match[1] || match[2]);
|
|
740
|
-
const createdIdPattern = /(?:created|opened|submitted|new)\s+PR[# -]*(\d{1,})/gi;
|
|
741
|
-
while ((match = createdIdPattern.exec(resultText)) !== null) prMatches.add(match[1]);
|
|
774
|
+
const resultText = typeof parsed.result === 'string' ? parsed.result : JSON.stringify(parsed.result);
|
|
775
|
+
addExplicitPrCreatedEvidence(resultText);
|
|
742
776
|
}
|
|
743
777
|
} catch {}
|
|
744
778
|
}
|
|
745
779
|
} catch {}
|
|
746
780
|
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
// the URL (the W-moljyu60wuzr / #1902 case — gh pr create ran in a sibling
|
|
750
|
-
// dispatch and only the inbox note carries the link).
|
|
751
|
-
const inboxUrls = new Map();
|
|
781
|
+
// Accept inbox fallback only when the agent wrote the explicit PR-created
|
|
782
|
+
// protocol line; generic PR mentions in findings/review notes are not evidence.
|
|
752
783
|
const today = dateStamp();
|
|
753
784
|
const inboxFiles = getInboxFiles().filter(f => f.includes(agentId) && f.includes(today));
|
|
754
785
|
for (const f of inboxFiles) {
|
|
755
786
|
const content = safeRead(path.join(INBOX_DIR, f));
|
|
756
787
|
if (!content) continue;
|
|
757
|
-
|
|
758
|
-
// optional "Pull Request" spelling, line-anchored so "see PR https://..."
|
|
759
|
-
// mid-paragraph mentions don't trigger a false-positive. The protocol
|
|
760
|
-
// and host prefix is optional so "PR: https://github.com/..." ,
|
|
761
|
-
// "**PR:** github.com/...", etc. all match.
|
|
762
|
-
const prHeaderPattern = /(?:^|\n)\s*\*{0,2}(?:PR|Pull\s+Request)[:\*]*\*?\s*[#-]*\s*(?:https?:\/\/)?[^\s"]*?(?:(?:visualstudio\.com|dev\.azure\.com)[^\s"]*?pullrequest\/(\d+)|github\.com\/[^\s"]*?\/pull\/(\d+))/gi;
|
|
788
|
+
const prHeaderPattern = /(?:^|\n)\s*\*{0,2}(?:PR|Pull\s+Request|E2E\s+PR)\s+(?:created|opened|submitted)\*{0,2}\s*[:\-]\s*([^\n]+)/gi;
|
|
763
789
|
while ((match = prHeaderPattern.exec(content)) !== null) {
|
|
764
|
-
|
|
765
|
-
prMatches.add(prId);
|
|
766
|
-
// Pull the URL substring out of the matched chunk so we can hand it to
|
|
767
|
-
// extractPrUrl as a fallback. Prefer the first inbox URL we see for a
|
|
768
|
-
// given prId — later notes don't override the canonical record.
|
|
769
|
-
if (!inboxUrls.has(prId)) {
|
|
770
|
-
const urlMatch = match[0].match(/https?:\/\/[^\s"\\)]+/);
|
|
771
|
-
if (urlMatch) inboxUrls.set(prId, urlMatch[0].replace(/[.,;:]+$/, ''));
|
|
772
|
-
}
|
|
790
|
+
addPrUrlEvidence(match[1]);
|
|
773
791
|
}
|
|
774
792
|
}
|
|
775
793
|
|
|
776
|
-
if (
|
|
794
|
+
if (prEvidence.size === 0) return 0;
|
|
777
795
|
|
|
778
796
|
const projects = shared.getProjects(config);
|
|
779
797
|
if (projects.length === 0 && !meta?.project?.name) return 0;
|
|
@@ -798,13 +816,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
798
816
|
// doesn't contain the link (gh pr create may have run in a sibling dispatch
|
|
799
817
|
// whose stdout was rotated; the inbox note is the durable artifact).
|
|
800
818
|
function extractPrUrl(prId) {
|
|
801
|
-
|
|
802
|
-
// backslash-n), so without this the regex would capture e.g. "pull/1804\n/usr/bin/bash".
|
|
803
|
-
const ghMatch = output.match(new RegExp(`https?://github\\.com/[^\\s"'\\)\\]\\\\]*?/pull/${prId}(?:[^\\s"'\\)\\]\\\\]*)`, 'i'));
|
|
804
|
-
if (ghMatch) return ghMatch[0].replace(/[.,;:]+$/, '');
|
|
805
|
-
const adoMatch = output.match(new RegExp(`https?://(?:dev\\.azure\\.com|[^/]+\\.visualstudio\\.com)[^\\s"'\\)\\]\\\\]*?pullrequest/${prId}(?:[^\\s"'\\)\\]\\\\]*)`, 'i'));
|
|
806
|
-
if (adoMatch) return adoMatch[0].replace(/[.,;:]+$/, '');
|
|
807
|
-
return inboxUrls.get(prId) || '';
|
|
819
|
+
return prEvidence.get(prId) || '';
|
|
808
820
|
}
|
|
809
821
|
|
|
810
822
|
const agentName = config.agents?.[agentId]?.name || agentId;
|
|
@@ -814,7 +826,7 @@ function syncPrsFromOutput(output, agentId, meta, config) {
|
|
|
814
826
|
// Group new PRs by target file path
|
|
815
827
|
const newPrsByPath = new Map(); // prPath -> [{ prId, newEntry }]
|
|
816
828
|
|
|
817
|
-
for (const prId of
|
|
829
|
+
for (const prId of prEvidence.keys()) {
|
|
818
830
|
const targetProject = useCentral ? null : resolveProjectForPr(prId);
|
|
819
831
|
const targetName = targetProject ? targetProject.name : '_central';
|
|
820
832
|
const prPath = targetProject ? shared.projectPrPath(targetProject) : centralPrPath;
|
package/engine/queries.js
CHANGED
|
@@ -8,6 +8,7 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
const shared = require('./shared');
|
|
11
|
+
const steering = require('./steering');
|
|
11
12
|
|
|
12
13
|
const { safeRead, safeReadDir, safeJson, safeWrite, getProjects, mutateJsonFileLocked,
|
|
13
14
|
projectWorkItemsPath, projectPrPath, parseSkillFrontmatter, KB_CATEGORIES,
|
|
@@ -418,12 +419,18 @@ function getAgents(config) {
|
|
|
418
419
|
// runtime tag next to the agent name.
|
|
419
420
|
const runtime = shared.resolveAgentCli(a, config.engine || {});
|
|
420
421
|
const inboxFiles = allInboxFiles.filter(f => f.includes(a.id));
|
|
422
|
+
let steeringInboxFiles = [];
|
|
423
|
+
try { steeringInboxFiles = steering.listUnreadSteeringMessages(a.id); } catch { steeringInboxFiles = []; }
|
|
421
424
|
const s = getAgentStatus(a.id); // derives from dispatch.json
|
|
422
425
|
|
|
423
426
|
let lastAction = 'Waiting for assignment';
|
|
424
427
|
if (s.status === 'working') lastAction = s._runningToolDescription ? `Running: ${s._runningToolDescription}` : `Working: ${s.task}`;
|
|
425
428
|
else if (s.status === 'done') lastAction = `Done: ${s.task}`;
|
|
426
429
|
else if (s.status === 'error') lastAction = `Error: ${s.task}`;
|
|
430
|
+
else if (steeringInboxFiles.length > 0) {
|
|
431
|
+
const lastSteer = steeringInboxFiles[steeringInboxFiles.length - 1];
|
|
432
|
+
lastAction = `Pending steering: ${lastSteer.file} (${timeSince(lastSteer.createdAtMs)})`;
|
|
433
|
+
}
|
|
427
434
|
else if (inboxFiles.length > 0) {
|
|
428
435
|
const lastOutput = path.join(INBOX_DIR, inboxFiles[inboxFiles.length - 1]);
|
|
429
436
|
try { lastAction = `Output: ${path.basename(lastOutput)} (${timeSince(fs.statSync(lastOutput).mtimeMs)})`; } catch { /* optional */ }
|
|
@@ -440,7 +447,7 @@ function getAgents(config) {
|
|
|
440
447
|
_blockingToolCall: s._blockingToolCall || null,
|
|
441
448
|
_warning: s._warning || null,
|
|
442
449
|
_permissionMode: s._permissionMode || null,
|
|
443
|
-
chartered, inboxCount: inboxFiles.length
|
|
450
|
+
chartered, inboxCount: inboxFiles.length + steeringInboxFiles.length
|
|
444
451
|
};
|
|
445
452
|
});
|
|
446
453
|
}
|
|
@@ -458,6 +465,11 @@ function getAgentDetail(id) {
|
|
|
458
465
|
const inboxContents = safeReadDir(INBOX_DIR)
|
|
459
466
|
.filter(f => f.includes(id))
|
|
460
467
|
.map(f => ({ name: f, content: safeRead(path.join(INBOX_DIR, f)) || '' }));
|
|
468
|
+
try {
|
|
469
|
+
for (const entry of steering.listUnreadSteeringMessages(id)) {
|
|
470
|
+
inboxContents.push({ name: entry.file, content: entry.raw || '', type: 'steering' });
|
|
471
|
+
}
|
|
472
|
+
} catch { /* optional */ }
|
|
461
473
|
|
|
462
474
|
let recentDispatches = [];
|
|
463
475
|
try {
|
package/engine/shared.js
CHANGED
|
@@ -1073,6 +1073,9 @@ const PRD_MATERIALIZABLE = new Set([PRD_ITEM_STATUS.MISSING, PRD_ITEM_STATUS.UPD
|
|
|
1073
1073
|
const PR_STATUS = { ACTIVE: 'active', MERGED: 'merged', ABANDONED: 'abandoned', CLOSED: 'closed', LINKED: 'linked' };
|
|
1074
1074
|
// PRs eligible for polling (status/build/comment checks) — excludes terminal statuses
|
|
1075
1075
|
const PR_POLLABLE_STATUSES = new Set([PR_STATUS.ACTIVE, PR_STATUS.LINKED]);
|
|
1076
|
+
const PR_PENDING_REASON = {
|
|
1077
|
+
MISSING_BRANCH: 'missing_pr_branch',
|
|
1078
|
+
};
|
|
1076
1079
|
|
|
1077
1080
|
// Watch statuses — engine-level persistent watches that survive restarts
|
|
1078
1081
|
const WATCH_STATUS = { ACTIVE: 'active', PAUSED: 'paused', TRIGGERED: 'triggered', EXPIRED: 'expired' };
|
|
@@ -1661,6 +1664,10 @@ function parseAdoPrUrl(url) {
|
|
|
1661
1664
|
};
|
|
1662
1665
|
}
|
|
1663
1666
|
|
|
1667
|
+
function parsePrUrl(url) {
|
|
1668
|
+
return parseGitHubPrUrl(url) || parseAdoPrUrl(url);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1664
1671
|
function getProjectPrScope(project) {
|
|
1665
1672
|
if (!project) return '';
|
|
1666
1673
|
const host = String(project.repoHost || '').toLowerCase();
|
|
@@ -1705,16 +1712,47 @@ function getPrDisplayId(value, fallbackPrNumber = null) {
|
|
|
1705
1712
|
return typeof value === 'object' ? String(value?.id || '') : String(value || '');
|
|
1706
1713
|
}
|
|
1707
1714
|
|
|
1715
|
+
function getPrScopeInfo(prRef, url = '') {
|
|
1716
|
+
const isObjectRef = !!prRef && typeof prRef === 'object';
|
|
1717
|
+
const rawUrl = url || (isObjectRef ? prRef.url || '' : String(prRef || ''));
|
|
1718
|
+
const parsedUrl = parsePrUrl(rawUrl);
|
|
1719
|
+
if (parsedUrl) return { ...parsedUrl, source: 'url' };
|
|
1720
|
+
const rawId = isObjectRef ? (prRef.id || '') : String(prRef || '');
|
|
1721
|
+
const canonical = parseCanonicalPrId(rawId);
|
|
1722
|
+
return canonical ? { ...canonical, source: 'id' } : null;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
function getPrProjectScopeMismatch(project, prRef, url = '') {
|
|
1726
|
+
const projectScope = getProjectPrScope(project);
|
|
1727
|
+
if (!projectScope) return null;
|
|
1728
|
+
const refScope = getPrScopeInfo(prRef, url)?.scope || '';
|
|
1729
|
+
if (!refScope) return null;
|
|
1730
|
+
if (refScope === projectScope) return null;
|
|
1731
|
+
const [projectHost, projectRest = ''] = projectScope.split(':');
|
|
1732
|
+
const [refHost, refRest = ''] = refScope.split(':');
|
|
1733
|
+
if (projectHost === refHost && projectHost === 'ado' && !project.prUrlBase) {
|
|
1734
|
+
const projectParts = projectRest.split('/');
|
|
1735
|
+
const refParts = refRest.split('/');
|
|
1736
|
+
if (projectParts[0] === refParts[0] && projectParts[1] === refParts[1]) return null;
|
|
1737
|
+
}
|
|
1738
|
+
return { reason: 'pr_scope_mismatch', projectScope, prScope: refScope };
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
function isPrCompatibleWithProject(project, prRef, url = '') {
|
|
1742
|
+
return !getPrProjectScopeMismatch(project, prRef, url);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1708
1745
|
function getCanonicalPrId(project, prRef, url = '') {
|
|
1709
1746
|
const isObjectRef = !!prRef && typeof prRef === 'object';
|
|
1710
1747
|
const rawId = isObjectRef ? (prRef.id || '') : String(prRef || '');
|
|
1748
|
+
const rawUrl = url || (isObjectRef ? prRef.url || '' : String(prRef || ''));
|
|
1749
|
+
const parsedUrl = parsePrUrl(rawUrl);
|
|
1750
|
+
if (parsedUrl) return `${parsedUrl.scope}#${parsedUrl.prNumber}`;
|
|
1711
1751
|
const canonical = parseCanonicalPrId(rawId);
|
|
1712
1752
|
if (canonical) return `${canonical.scope}#${canonical.prNumber}`;
|
|
1713
|
-
const parsedUrl = parseGitHubPrUrl(url || (isObjectRef ? prRef.url || '' : ''))
|
|
1714
|
-
|| parseAdoPrUrl(url || (isObjectRef ? prRef.url || '' : ''));
|
|
1715
1753
|
const prNumber = getPrNumber(isObjectRef ? (prRef.prNumber ?? prRef.id ?? prRef.url) : prRef);
|
|
1716
1754
|
if (prNumber == null) return rawId;
|
|
1717
|
-
const scope = getProjectPrScope(project) ||
|
|
1755
|
+
const scope = getProjectPrScope(project) || '';
|
|
1718
1756
|
return scope ? `${scope}#${prNumber}` : `PR-${prNumber}`;
|
|
1719
1757
|
}
|
|
1720
1758
|
|
|
@@ -1755,6 +1793,17 @@ function normalizePrRecord(pr, project = null) {
|
|
|
1755
1793
|
pr.id = canonicalId;
|
|
1756
1794
|
changed = true;
|
|
1757
1795
|
}
|
|
1796
|
+
const mismatch = getPrProjectScopeMismatch(project, pr, pr.url || '');
|
|
1797
|
+
if (mismatch) {
|
|
1798
|
+
const current = pr._invalidProjectScope || {};
|
|
1799
|
+
if (current.reason !== mismatch.reason || current.projectScope !== mismatch.projectScope || current.prScope !== mismatch.prScope) {
|
|
1800
|
+
pr._invalidProjectScope = mismatch;
|
|
1801
|
+
changed = true;
|
|
1802
|
+
}
|
|
1803
|
+
} else if (Object.prototype.hasOwnProperty.call(pr, '_invalidProjectScope')) {
|
|
1804
|
+
delete pr._invalidProjectScope;
|
|
1805
|
+
changed = true;
|
|
1806
|
+
}
|
|
1758
1807
|
return changed;
|
|
1759
1808
|
}
|
|
1760
1809
|
|
|
@@ -2255,7 +2304,7 @@ module.exports = {
|
|
|
2255
2304
|
resolveAgentMaxBudget, resolveAgentBareMode,
|
|
2256
2305
|
applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
|
|
2257
2306
|
runtimeConfigWarnings,
|
|
2258
|
-
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, DISPATCH_RESULT, trackReviewMetric, queuePlanToPrd,
|
|
2307
|
+
WI_STATUS, DONE_STATUSES, PLAN_TERMINAL_STATUSES, WORK_TYPE, PLAN_STATUS, PRD_ITEM_STATUS, PRD_MATERIALIZABLE, PR_STATUS, PR_POLLABLE_STATUSES, PR_PENDING_REASON, DISPATCH_RESULT, trackReviewMetric, queuePlanToPrd,
|
|
2259
2308
|
WATCH_STATUS, WATCH_TARGET_TYPE, WATCH_CONDITION, WATCH_ABSOLUTE_CONDITIONS,
|
|
2260
2309
|
PIPELINE_STATUS, STAGE_TYPE, MEETING_STATUS, AGENT_STATUS,
|
|
2261
2310
|
FAILURE_CLASS, ESCALATION_POLICY, COMPLETION_FIELDS,
|
|
@@ -2273,6 +2322,9 @@ module.exports = {
|
|
|
2273
2322
|
getProjectPrScope,
|
|
2274
2323
|
getPrNumber,
|
|
2275
2324
|
getPrDisplayId,
|
|
2325
|
+
getPrScopeInfo,
|
|
2326
|
+
getPrProjectScopeMismatch,
|
|
2327
|
+
isPrCompatibleWithProject,
|
|
2276
2328
|
getCanonicalPrId,
|
|
2277
2329
|
findPrRecord,
|
|
2278
2330
|
normalizePrRecord,
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine/steering.js — Durable agent-scoped steering inbox helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const shared = require('./shared');
|
|
8
|
+
|
|
9
|
+
const AGENTS_DIR = path.join(shared.MINIONS_DIR, 'agents');
|
|
10
|
+
|
|
11
|
+
function agentInboxDir(agentId) {
|
|
12
|
+
return path.join(AGENTS_DIR, agentId, 'inbox');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function _createdAtFromPath(filePath, stat) {
|
|
16
|
+
const base = path.basename(filePath);
|
|
17
|
+
const m = base.match(/^steering-(\d+)/);
|
|
18
|
+
if (m) {
|
|
19
|
+
const n = Number(m[1]);
|
|
20
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
21
|
+
}
|
|
22
|
+
return stat?.mtimeMs || Date.now();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function _stripFrontmatter(raw) {
|
|
26
|
+
const text = String(raw || '');
|
|
27
|
+
if (!text.startsWith('---\n')) return text;
|
|
28
|
+
const end = text.indexOf('\n---\n', 4);
|
|
29
|
+
return end >= 0 ? text.slice(end + 5) : text;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _frontmatterValue(raw, key) {
|
|
33
|
+
const text = String(raw || '');
|
|
34
|
+
if (!text.startsWith('---\n')) return null;
|
|
35
|
+
const end = text.indexOf('\n---\n', 4);
|
|
36
|
+
if (end < 0) return null;
|
|
37
|
+
const fm = text.slice(4, end).split(/\r?\n/);
|
|
38
|
+
const prefix = key + ':';
|
|
39
|
+
for (const line of fm) {
|
|
40
|
+
if (line.startsWith(prefix)) return line.slice(prefix.length).trim();
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function _messageFromRaw(raw) {
|
|
46
|
+
let body = _stripFrontmatter(raw).trim();
|
|
47
|
+
const forwarded = body.match(/Original steering from human:\s*([\s\S]*)$/i);
|
|
48
|
+
if (forwarded) body = forwarded[1].trim();
|
|
49
|
+
return body;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _readEntry(filePath, legacy = false) {
|
|
53
|
+
let stat;
|
|
54
|
+
try { stat = fs.statSync(filePath); } catch { return null; }
|
|
55
|
+
const raw = shared.safeRead(filePath);
|
|
56
|
+
const fmCreatedAtMs = Number(_frontmatterValue(raw, 'createdAtMs'));
|
|
57
|
+
const createdAtMs = Number.isFinite(fmCreatedAtMs) && fmCreatedAtMs > 0
|
|
58
|
+
? fmCreatedAtMs
|
|
59
|
+
: _createdAtFromPath(filePath, stat);
|
|
60
|
+
return {
|
|
61
|
+
path: filePath,
|
|
62
|
+
file: path.basename(filePath),
|
|
63
|
+
createdAtMs,
|
|
64
|
+
createdAt: new Date(createdAtMs).toISOString(),
|
|
65
|
+
raw,
|
|
66
|
+
message: _messageFromRaw(raw),
|
|
67
|
+
legacy,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function _uniqueSteeringPath(inboxDir, createdAtMs) {
|
|
72
|
+
let filePath = path.join(inboxDir, `steering-${createdAtMs}.md`);
|
|
73
|
+
for (let i = 1; fs.existsSync(filePath); i++) {
|
|
74
|
+
filePath = path.join(inboxDir, `steering-${createdAtMs}-${i}.md`);
|
|
75
|
+
}
|
|
76
|
+
return filePath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writeSteeringMessage(agentId, message, opts = {}) {
|
|
80
|
+
const createdAtMs = Number(opts.createdAtMs) || Date.now();
|
|
81
|
+
const createdAt = new Date(createdAtMs).toISOString();
|
|
82
|
+
const inboxDir = agentInboxDir(agentId);
|
|
83
|
+
fs.mkdirSync(inboxDir, { recursive: true });
|
|
84
|
+
const filePath = _uniqueSteeringPath(inboxDir, createdAtMs);
|
|
85
|
+
const body = [
|
|
86
|
+
'---',
|
|
87
|
+
`createdAt: ${createdAt}`,
|
|
88
|
+
`createdAtMs: ${createdAtMs}`,
|
|
89
|
+
`source: ${opts.source || 'human'}`,
|
|
90
|
+
'---',
|
|
91
|
+
'',
|
|
92
|
+
String(message || '').trim(),
|
|
93
|
+
'',
|
|
94
|
+
].join('\n');
|
|
95
|
+
shared.safeWrite(filePath, body);
|
|
96
|
+
return _readEntry(filePath);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function listUnreadSteeringMessages(agentId, opts = {}) {
|
|
100
|
+
const includeLegacy = opts.includeLegacy !== false;
|
|
101
|
+
const entries = [];
|
|
102
|
+
const inboxDir = agentInboxDir(agentId);
|
|
103
|
+
for (const file of shared.safeReadDir(inboxDir)) {
|
|
104
|
+
if (!/^steering-.*\.md$/i.test(file)) continue;
|
|
105
|
+
const entry = _readEntry(path.join(inboxDir, file), false);
|
|
106
|
+
if (entry) entries.push(entry);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (includeLegacy) {
|
|
110
|
+
const legacyPath = path.join(AGENTS_DIR, agentId, 'steer.md');
|
|
111
|
+
const legacy = _readEntry(legacyPath, true);
|
|
112
|
+
if (legacy) entries.push(legacy);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
entries.sort((a, b) => (a.createdAtMs - b.createdAtMs) || a.file.localeCompare(b.file));
|
|
116
|
+
return entries;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildPendingSteeringPrompt(agentId) {
|
|
120
|
+
const entries = listUnreadSteeringMessages(agentId).filter(entry => entry.message.trim());
|
|
121
|
+
if (entries.length === 0) return { entries, prompt: '' };
|
|
122
|
+
|
|
123
|
+
const sections = [
|
|
124
|
+
'## Pending instructions from prior session',
|
|
125
|
+
'',
|
|
126
|
+
'These human steering messages were not confirmed processed before the previous session ended. Address them before continuing with the task.',
|
|
127
|
+
];
|
|
128
|
+
entries.forEach((entry, idx) => {
|
|
129
|
+
sections.push('', `### Message ${idx + 1} — ${entry.createdAt}`, '', entry.message.trim());
|
|
130
|
+
});
|
|
131
|
+
return { entries, prompt: sections.join('\n') };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function _eventTimestampMs(obj, observedAtMs) {
|
|
135
|
+
const value = obj?.timestamp || obj?.createdAt || obj?.created_at || obj?.time || obj?.data?.timestamp;
|
|
136
|
+
const parsed = value ? Date.parse(value) : NaN;
|
|
137
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
138
|
+
return Number(observedAtMs) || Date.now();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function _isProcessEvidenceEvent(obj) {
|
|
142
|
+
if (!obj || typeof obj !== 'object') return false;
|
|
143
|
+
const type = String(obj.type || '');
|
|
144
|
+
if (type === 'assistant' || type === 'tool_use') return true;
|
|
145
|
+
if (type.startsWith('assistant.') || type.startsWith('tool.')) return true;
|
|
146
|
+
if (Array.isArray(obj.message?.content)) {
|
|
147
|
+
return obj.message.content.some(block => block?.type === 'text' || block?.type === 'tool_use');
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _processEvidenceTimes(rawOutput, observedAtMs) {
|
|
153
|
+
const times = [];
|
|
154
|
+
for (const line of String(rawOutput || '').split(/\r?\n/)) {
|
|
155
|
+
const trimmed = line.trim();
|
|
156
|
+
if (!trimmed.startsWith('{')) continue;
|
|
157
|
+
try {
|
|
158
|
+
const obj = JSON.parse(trimmed);
|
|
159
|
+
if (_isProcessEvidenceEvent(obj)) times.push(_eventTimestampMs(obj, observedAtMs));
|
|
160
|
+
} catch { /* ignore non-JSON output */ }
|
|
161
|
+
}
|
|
162
|
+
return times;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function ackProcessedSteeringMessages(agentId, pendingEntries, rawOutput, opts = {}) {
|
|
166
|
+
const entries = Array.isArray(pendingEntries) ? pendingEntries : [];
|
|
167
|
+
if (entries.length === 0) return [];
|
|
168
|
+
const times = _processEvidenceTimes(rawOutput, opts.observedAtMs);
|
|
169
|
+
if (times.length === 0) return [];
|
|
170
|
+
|
|
171
|
+
const acked = [];
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
if (!entry?.path) continue;
|
|
174
|
+
if (!times.some(t => t > entry.createdAtMs)) continue;
|
|
175
|
+
shared.safeUnlink(entry.path);
|
|
176
|
+
acked.push(entry);
|
|
177
|
+
}
|
|
178
|
+
return acked;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
agentInboxDir,
|
|
183
|
+
writeSteeringMessage,
|
|
184
|
+
listUnreadSteeringMessages,
|
|
185
|
+
buildPendingSteeringPrompt,
|
|
186
|
+
ackProcessedSteeringMessages,
|
|
187
|
+
};
|
package/engine/timeout.js
CHANGED
|
@@ -6,6 +6,7 @@ const fs = require('fs');
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const shared = require('./shared');
|
|
8
8
|
const queries = require('./queries');
|
|
9
|
+
const steering = require('./steering');
|
|
9
10
|
|
|
10
11
|
const { safeRead, safeWrite, safeJson, mutateJsonFileLocked, getProjects, projectWorkItemsPath, log, ts,
|
|
11
12
|
ENGINE_DEFAULTS, WI_STATUS, WORK_TYPE, DISPATCH_RESULT, AGENT_STATUS } = shared;
|
|
@@ -78,25 +79,20 @@ function checkSteering(config) {
|
|
|
78
79
|
// Skip if already being steered (prevents double-kill race)
|
|
79
80
|
if (info._steeringMessage || info._steeringAt) continue;
|
|
80
81
|
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
const alreadyPending = new Set((info._pendingSteeringFiles || []).map(entry => entry.path || entry));
|
|
83
|
+
const unread = steering.listUnreadSteeringMessages(info.agentId);
|
|
84
|
+
for (const empty of unread.filter(entry => !entry.message.trim())) {
|
|
85
|
+
shared.safeUnlink(empty.path);
|
|
86
|
+
}
|
|
87
|
+
const steerEntry = unread.find(entry => entry.message.trim() && !alreadyPending.has(entry.path));
|
|
88
|
+
if (!steerEntry) continue; // ENOENT/no agents/<id>/inbox/steering-*.md message
|
|
89
|
+
const message = steerEntry.message.trim();
|
|
89
90
|
|
|
90
91
|
const sessionId = info.sessionId;
|
|
91
92
|
if (!sessionId) {
|
|
92
|
-
// No session to resume — kill agent and
|
|
93
|
+
// No session to resume — kill agent and leave message unread in inbox for retry.
|
|
93
94
|
// Previously this silently skipped for up to 5m then deleted the message (#627).
|
|
94
|
-
log('info', `Steering: no sessionId for ${info.agentId} (${id}) — killing and
|
|
95
|
-
|
|
96
|
-
// Write steering message to agent inbox so it survives the retry
|
|
97
|
-
const inboxDir = path.join(AGENTS_DIR, info.agentId, 'inbox');
|
|
98
|
-
try { fs.mkdirSync(inboxDir, { recursive: true }); } catch {}
|
|
99
|
-
safeWrite(path.join(inboxDir, `steering-${Date.now()}.md`), `# Steering Message (Forwarded)\n\nOriginal steering from human:\n\n${message}\n`);
|
|
95
|
+
log('info', `Steering: no sessionId for ${info.agentId} (${id}) — killing and keeping unread message in inbox`);
|
|
100
96
|
|
|
101
97
|
// Append to live output so user sees confirmation in the dashboard
|
|
102
98
|
try {
|
|
@@ -115,6 +111,7 @@ function checkSteering(config) {
|
|
|
115
111
|
// Set steering state BEFORE kill — close event may fire synchronously on some platforms
|
|
116
112
|
info._steeringMessage = message;
|
|
117
113
|
info._steeringSessionId = sessionId;
|
|
114
|
+
info._steeringEntry = steerEntry;
|
|
118
115
|
info._steeringAt = Date.now();
|
|
119
116
|
|
|
120
117
|
shared.killImmediate(info.proc);
|
package/engine.js
CHANGED
|
@@ -107,6 +107,7 @@ const { mutateDispatch, addToDispatch, isRetryableFailureReason, completeDispatc
|
|
|
107
107
|
// ─── Timeout / Steering / Idle (extracted to engine/timeout.js) ──────────────
|
|
108
108
|
|
|
109
109
|
const { checkTimeouts, checkSteering, checkIdleThreshold } = require('./engine/timeout');
|
|
110
|
+
const steering = require('./engine/steering');
|
|
110
111
|
|
|
111
112
|
// ─── Cleanup (extracted to engine/cleanup.js) ────────────────────────────────
|
|
112
113
|
|
|
@@ -295,6 +296,17 @@ function _buildAgentSpawnFlags(runtime, opts = {}) {
|
|
|
295
296
|
return flags;
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
function ackPendingSteeringFiles(agentId, procInfo, rawOutput, observedAtMs = Date.now()) {
|
|
300
|
+
if (!procInfo?._pendingSteeringFiles?.length || !rawOutput) return;
|
|
301
|
+
const acked = steering.ackProcessedSteeringMessages(agentId, procInfo._pendingSteeringFiles, rawOutput, { observedAtMs });
|
|
302
|
+
if (acked.length === 0) return;
|
|
303
|
+
|
|
304
|
+
const ackedPaths = new Set(acked.map(entry => entry.path));
|
|
305
|
+
procInfo._pendingSteeringFiles = procInfo._pendingSteeringFiles.filter(entry => !ackedPaths.has(entry.path));
|
|
306
|
+
if (procInfo._pendingSteeringFiles.length === 0) delete procInfo._pendingSteeringFiles;
|
|
307
|
+
log('info', `Steering: ACKed ${acked.length} processed message(s) for ${agentId}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
298
310
|
// Resolve dependency plan item IDs to their PR branches
|
|
299
311
|
function resolveDependencyBranches(depIds, sourcePlan, project, config) {
|
|
300
312
|
const results = []; // [{ branch, prId }]
|
|
@@ -436,9 +448,13 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
436
448
|
// and this avoids blocking 200ms of file reads behind 20-60s of git operations
|
|
437
449
|
const systemPrompt = buildSystemPrompt(agentId, config, project);
|
|
438
450
|
const agentContext = buildAgentContext(agentId, config, project);
|
|
439
|
-
const
|
|
440
|
-
|
|
451
|
+
const pendingSteering = steering.buildPendingSteeringPrompt(agentId);
|
|
452
|
+
const taskPromptWithSteering = pendingSteering.prompt
|
|
453
|
+
? `${pendingSteering.prompt}\n\n---\n\n${taskPrompt}`
|
|
441
454
|
: taskPrompt;
|
|
455
|
+
const fullTaskPrompt = agentContext
|
|
456
|
+
? `## Agent Context\n\n${agentContext}\n---\n\n## Your Task\n\n${taskPromptWithSteering}`
|
|
457
|
+
: taskPromptWithSteering;
|
|
442
458
|
const tmpDir = path.join(ENGINE_DIR, 'tmp');
|
|
443
459
|
if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
|
|
444
460
|
const safeId = id.replace(/[:\\/*?"<>|]/g, '-');
|
|
@@ -1036,6 +1052,8 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1036
1052
|
}
|
|
1037
1053
|
} catch { /* JSON parse — output may not be valid JSON */ }
|
|
1038
1054
|
}
|
|
1055
|
+
|
|
1056
|
+
ackPendingSteeringFiles(agentId, procInfo, chunk);
|
|
1039
1057
|
});
|
|
1040
1058
|
|
|
1041
1059
|
proc.stderr.on('data', (data) => {
|
|
@@ -1058,13 +1076,17 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1058
1076
|
try { shared.safeUnlink(path.join(AGENTS_DIR, agentId, 'session.json')); } catch {}
|
|
1059
1077
|
}
|
|
1060
1078
|
|
|
1061
|
-
// Check if this was a steering kill — re-spawn with resume
|
|
1062
1079
|
const procInfo = activeProcesses.get(id);
|
|
1080
|
+
ackPendingSteeringFiles(agentId, procInfo, stdout);
|
|
1081
|
+
|
|
1082
|
+
// Check if this was a steering kill — re-spawn with resume
|
|
1063
1083
|
if (procInfo?._steeringMessage) {
|
|
1064
1084
|
const steerMsg = procInfo._steeringMessage;
|
|
1065
1085
|
const steerSessionId = procInfo._steeringSessionId;
|
|
1086
|
+
const steerEntry = procInfo._steeringEntry;
|
|
1066
1087
|
delete procInfo._steeringMessage;
|
|
1067
1088
|
delete procInfo._steeringSessionId;
|
|
1089
|
+
delete procInfo._steeringEntry;
|
|
1068
1090
|
|
|
1069
1091
|
// Guard: can't resume without a session
|
|
1070
1092
|
if (!steerSessionId) {
|
|
@@ -1156,7 +1178,14 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1156
1178
|
// into the resumed process, it kills the resumed session. The kill watcher only exists
|
|
1157
1179
|
// to handle cases where the original kill didn't take effect — once the process has
|
|
1158
1180
|
// exited and the resume is spawned, _steeringAt must not be present.
|
|
1159
|
-
activeProcesses.set(id, {
|
|
1181
|
+
activeProcesses.set(id, {
|
|
1182
|
+
proc: resumeProc,
|
|
1183
|
+
agentId,
|
|
1184
|
+
startedAt: procInfo.startedAt,
|
|
1185
|
+
sessionId: steerSessionId,
|
|
1186
|
+
lastRealOutputAt: Date.now(),
|
|
1187
|
+
_pendingSteeringFiles: steerEntry ? [steerEntry] : (procInfo._pendingSteeringFiles || []),
|
|
1188
|
+
});
|
|
1160
1189
|
|
|
1161
1190
|
// Reset output buffers so post-completion parsing only sees the resumed session
|
|
1162
1191
|
stdout = '';
|
|
@@ -1167,6 +1196,7 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1167
1196
|
realActivityMap.set(id, Date.now());
|
|
1168
1197
|
if (stdout.length < MAX_OUTPUT) stdout += chunk.slice(0, MAX_OUTPUT - stdout.length);
|
|
1169
1198
|
try { fs.appendFileSync(liveOutputPath, chunk); } catch { /* optional */ }
|
|
1199
|
+
ackPendingSteeringFiles(agentId, activeProcesses.get(id), chunk);
|
|
1170
1200
|
});
|
|
1171
1201
|
resumeProc.stderr.on('data', (data) => {
|
|
1172
1202
|
const chunk = data.toString();
|
|
@@ -1370,7 +1400,13 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1370
1400
|
// realActivityMap was already seeded immediately after runFile() returned (#W-mo25loq8kjer);
|
|
1371
1401
|
// don't re-seed here — the stdout/stderr handlers above can already have updated it with
|
|
1372
1402
|
// a fresher timestamp, and overwriting would clobber the real "last activity" signal.
|
|
1373
|
-
activeProcesses.set(id, {
|
|
1403
|
+
activeProcesses.set(id, {
|
|
1404
|
+
proc,
|
|
1405
|
+
agentId,
|
|
1406
|
+
startedAt,
|
|
1407
|
+
sessionId: cachedSessionId,
|
|
1408
|
+
_pendingSteeringFiles: pendingSteering.entries,
|
|
1409
|
+
});
|
|
1374
1410
|
|
|
1375
1411
|
updateAgentStatus(id, AGENT_STATUS.RUNNING, `Process spawned for ${agentId}`);
|
|
1376
1412
|
|
|
@@ -1984,7 +2020,7 @@ function clearPendingHumanFeedbackFlag(projectMeta, prId) {
|
|
|
1984
2020
|
} catch (e) { log('warn', 'clear pending human feedback flag: ' + e.message); }
|
|
1985
2021
|
}
|
|
1986
2022
|
|
|
1987
|
-
const PR_PENDING_MISSING_BRANCH =
|
|
2023
|
+
const PR_PENDING_MISSING_BRANCH = shared.PR_PENDING_REASON.MISSING_BRANCH;
|
|
1988
2024
|
|
|
1989
2025
|
function normalizePrBranch(value) {
|
|
1990
2026
|
const raw = value == null ? '' : String(value).trim();
|
|
@@ -2117,6 +2153,7 @@ async function discoverFromPrs(config, project) {
|
|
|
2117
2153
|
const knownAgents = new Set(Object.keys(config.agents || {}));
|
|
2118
2154
|
for (const pr of prs) {
|
|
2119
2155
|
if (pr.status !== PR_STATUS.ACTIVE || pr._contextOnly) continue;
|
|
2156
|
+
if (!shared.isPrCompatibleWithProject(project, pr, pr.url || '')) continue;
|
|
2120
2157
|
const prDisplayId = shared.getPrDisplayId(pr);
|
|
2121
2158
|
const prCanonicalId = shared.getCanonicalPrId(project, pr, pr.url || '');
|
|
2122
2159
|
if (activePrIds.has(prCanonicalId)) continue; // Skip PRs with active dispatch (prevent race)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1644",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|
package/prompts/cc-system.md
CHANGED
|
@@ -72,8 +72,17 @@ I'll dispatch dallas to fix that bug.
|
|
|
72
72
|
|
|
73
73
|
**Generic fallback:** For any action not listed below, include `"endpoint": "/api/..."` and `"params": {...}` to call the API directly. Example: `{"type": "custom-op", "endpoint": "/api/some/endpoint", "params": {"key": "value"}}`.
|
|
74
74
|
|
|
75
|
+
**Required fields per action type — server rejects with an error if missing:**
|
|
76
|
+
|
|
77
|
+
- `dispatch` (and aliases: `fix`, `implement`, `explore`, `review`, `test`): `title` is REQUIRED. `description` recommended. `project` REQUIRED when multiple projects are configured (server returns the list of known names if you guess wrong). For agent hints emit either `agents: ["dallas"]` (array, preferred) or `agent: "dallas"` (string — auto-promoted server-side). Unknown agent names error.
|
|
78
|
+
- `build-and-test`: `pr` REQUIRED (number, ID, or URL).
|
|
79
|
+
- `note`: `title` and `content` (or `description`) REQUIRED.
|
|
80
|
+
- `knowledge`: `title`, `content`, and `category` REQUIRED. Valid categories: architecture, conventions, project-notes, build-reports, reviews.
|
|
81
|
+
|
|
82
|
+
If you describe an action in prose ("I'll dispatch this..."), you MUST emit a matching `===ACTIONS===` block. The server detects prose claims without action blocks and surfaces a warning to the user — i.e., your false claim becomes visible. Either dispatch or don't promise to.
|
|
83
|
+
|
|
75
84
|
Core action types:
|
|
76
|
-
- **dispatch**: title, workType, priority (low/medium/high), agents[] (optional), project, description
|
|
85
|
+
- **dispatch**: title (REQUIRED), workType, priority (low/medium/high), agents[] or agent (optional — both shapes accepted), project (REQUIRED when multi-project), description
|
|
77
86
|
workTypes: `explore` (research/report only, NO PR), `ask` (answer/report, NO PR), `implement` (new code, PR REQUIRED), `fix` (bug fix, PR REQUIRED), `review` (code review, NO PR), `test` (tests, PR if new), `verify` (merge/build/maintenance, NO PR)
|
|
78
87
|
If the user wants a design/architecture artifact committed through a PR, dispatch `implement` or `docs` rather than `explore`.
|
|
79
88
|
When the user names a specific agent ("assign this to lambert"), put exactly that one name in `agents` (e.g. `"agents": ["lambert"]`). A single-agent assignment is hard-pinned by the server — it will queue for that agent only and skip the routing table. Use multi-agent arrays only when the user names multiple agents or asks for fan-out.
|