@yemi33/minions 0.1.2381 → 0.1.2383
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/bin/minions.js +42 -2
- package/dashboard/js/refresh.js +8 -2
- package/dashboard/js/render-other.js +38 -0
- package/dashboard/js/render-work-items.js +61 -20
- package/dashboard/js/settings.js +7 -8
- package/dashboard/pages/engine.html +6 -0
- package/dashboard.js +110 -27
- package/docs/README.md +1 -0
- package/docs/claude-md-propagation.md +118 -0
- package/docs/completion-reports.md +20 -1
- package/docs/engine-restart.md +10 -0
- package/docs/runtime-adapters.md +54 -22
- package/docs/skills.md +2 -0
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +23 -0
- package/engine/acp-transport.js +63 -35
- package/engine/ado-comment.js +3 -1
- package/engine/agent-worker-pool.js +11 -4
- package/engine/cc-worker-pool.js +4 -2
- package/engine/claude-md-context.js +195 -0
- package/engine/cli.js +9 -1
- package/engine/comment-format.js +51 -14
- package/engine/consolidation.js +47 -3
- package/engine/db/migrations/026-pre-dispatch-rejections.js +55 -0
- package/engine/dispatch.js +80 -23
- package/engine/execution-model.js +68 -0
- package/engine/gh-comment.js +6 -3
- package/engine/github.js +6 -7
- package/engine/lifecycle.js +195 -56
- package/engine/llm.js +59 -83
- package/engine/memory-store.js +3 -1
- package/engine/pipeline.js +147 -20
- package/engine/playbook.js +39 -6
- package/engine/pooled-agent-process.js +28 -26
- package/engine/prd-store.js +14 -6
- package/engine/quarantine-refs.js +33 -0
- package/engine/queries.js +8 -6
- package/engine/restart-health.js +69 -4
- package/engine/runtimes/claude.js +100 -25
- package/engine/runtimes/codex.js +19 -0
- package/engine/runtimes/contract.js +239 -0
- package/engine/runtimes/copilot.js +85 -8
- package/engine/runtimes/index.js +37 -9
- package/engine/shared.js +157 -64
- package/engine/spawn-agent.js +79 -113
- package/engine/spawn-phase-watchdog.js +8 -37
- package/engine/supervisor.js +72 -16
- package/engine/timeout.js +87 -16
- package/engine/untrusted-fence.js +2 -1
- package/engine.js +434 -125
- package/package.json +1 -1
- package/playbooks/fix.md +20 -0
- package/playbooks/review.md +2 -0
- package/skills/check-self-authored-review-comment/SKILL.md +34 -0
|
@@ -37,6 +37,14 @@ const { FAILURE_CLASS, safeWrite, ts, resolveEngineCacheDir } = require('../shar
|
|
|
37
37
|
const ENGINE_DIR = __dirname.replace(/[\\/]runtimes$/, '');
|
|
38
38
|
const _CACHE_DIR = resolveEngineCacheDir(ENGINE_DIR);
|
|
39
39
|
const isWin = process.platform === 'win32';
|
|
40
|
+
const RUNTIME_NAME = 'copilot';
|
|
41
|
+
const SPAWN_STARTUP_TYPES = new Set([
|
|
42
|
+
'session.mcp_server_status_changed',
|
|
43
|
+
'session.mcp_servers_loaded',
|
|
44
|
+
'session.skills_loaded',
|
|
45
|
+
'session.tools_updated',
|
|
46
|
+
'session.info',
|
|
47
|
+
]);
|
|
40
48
|
|
|
41
49
|
// ── Binary Resolution ───────────────────────────────────────────────────────
|
|
42
50
|
//
|
|
@@ -529,7 +537,7 @@ function buildArgs(opts = {}) {
|
|
|
529
537
|
}
|
|
530
538
|
|
|
531
539
|
function buildSpawnFlags(opts = {}) {
|
|
532
|
-
const flags = ['--runtime',
|
|
540
|
+
const flags = ['--runtime', RUNTIME_NAME];
|
|
533
541
|
if (opts.maxTurns != null) flags.push('--max-turns', String(opts.maxTurns));
|
|
534
542
|
if (opts.model) flags.push('--model', String(opts.model));
|
|
535
543
|
if (opts.allowedTools) flags.push('--allowedTools', String(opts.allowedTools));
|
|
@@ -552,12 +560,66 @@ function buildSpawnFlags(opts = {}) {
|
|
|
552
560
|
return flags;
|
|
553
561
|
}
|
|
554
562
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const
|
|
563
|
+
function resolveInvocationOptions({
|
|
564
|
+
options = {},
|
|
565
|
+
engineConfig = {},
|
|
566
|
+
failureClass = null,
|
|
567
|
+
} = {}) {
|
|
568
|
+
const resolved = { ...options };
|
|
569
|
+
if (resolved.stream == null) resolved.stream = engineConfig.copilotStreamMode;
|
|
570
|
+
if (resolved.disableBuiltinMcps == null) {
|
|
571
|
+
resolved.disableBuiltinMcps = engineConfig.copilotDisableBuiltinMcps;
|
|
572
|
+
}
|
|
573
|
+
if (resolved.reasoningSummaries == null) {
|
|
574
|
+
resolved.reasoningSummaries = engineConfig.copilotReasoningSummaries;
|
|
575
|
+
}
|
|
576
|
+
if (failureClass === FAILURE_CLASS.MODEL_UNAVAILABLE && engineConfig.copilotFallbackModel) {
|
|
577
|
+
resolved.model = engineConfig.copilotFallbackModel;
|
|
578
|
+
}
|
|
579
|
+
return resolved;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function isSpawnStartupEvent(event) {
|
|
583
|
+
return !!event && SPAWN_STARTUP_TYPES.has(event.type);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function shouldSuppressPostMutationError({ mutationApplied, error } = {}) {
|
|
587
|
+
if (mutationApplied !== true || !error) return false;
|
|
588
|
+
return error.errorClass === 'unknown-model' || error.errorClass === 'model-unavailable';
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function buildWorkerArgs() {
|
|
592
|
+
return ['--acp', '--allow-all', '--max-autopilot-continues', '1'];
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function encodePooledOutput(event = {}) {
|
|
596
|
+
if (event.kind === 'model') {
|
|
597
|
+
return JSON.stringify({
|
|
598
|
+
type: 'session.tools_updated',
|
|
599
|
+
data: { model: event.model || null },
|
|
600
|
+
}) + '\n';
|
|
601
|
+
}
|
|
602
|
+
if (event.kind === 'chunk') {
|
|
603
|
+
return JSON.stringify({
|
|
604
|
+
type: 'assistant.message_delta',
|
|
605
|
+
data: { deltaContent: event.text || '' },
|
|
606
|
+
}) + '\n';
|
|
607
|
+
}
|
|
608
|
+
if (event.kind === 'result') {
|
|
609
|
+
return JSON.stringify({
|
|
610
|
+
type: 'result',
|
|
611
|
+
sessionId: event.sessionId || null,
|
|
612
|
+
usage: {
|
|
613
|
+
totalApiDurationMs: event.durationMs,
|
|
614
|
+
stopReason: event.stopReason || null,
|
|
615
|
+
},
|
|
616
|
+
}) + '\n';
|
|
617
|
+
}
|
|
618
|
+
throw new Error(`Unsupported pooled output event kind: ${event.kind || '<missing>'}`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// RUNTIME_NAME is stamped into every session.json so the pre-spawn resume path
|
|
622
|
+
// can reject session IDs produced by another adapter before invoking the CLI.
|
|
561
623
|
|
|
562
624
|
function getResumeSessionId({ agentId, branchName, agentsDir, maxAgeMs = 2 * 60 * 60 * 1000, logger = console } = {}) {
|
|
563
625
|
if (!agentId || agentId.startsWith('temp-') || !agentsDir) return null;
|
|
@@ -1273,6 +1335,13 @@ const capabilities = {
|
|
|
1273
1335
|
budgetCap: false,
|
|
1274
1336
|
// No --bare equivalent.
|
|
1275
1337
|
bareMode: false,
|
|
1338
|
+
// Copilot has NO native CLAUDE.md auto-load — it only reads its own AGENTS.md
|
|
1339
|
+
// (native custom-instruction discovery owned by the CLI itself). FALSE
|
|
1340
|
+
// here opts this runtime INTO the engine's CLAUDE.md-propagation layer
|
|
1341
|
+
// (engine/claude-md-context.js) so repo-authored CLAUDE.md guidance is
|
|
1342
|
+
// surfaced in the prompt. This is orthogonal to AGENTS.md discovery —
|
|
1343
|
+
// CLAUDE.md carries no split-brain risk because Copilot never reads it itself.
|
|
1344
|
+
claudeMdNativeDiscovery: false,
|
|
1276
1345
|
// No --fallback-model
|
|
1277
1346
|
fallbackModel: false,
|
|
1278
1347
|
// Copilot manages session state internally in ~/.copilot/session-state/
|
|
@@ -1363,7 +1432,8 @@ function getMcpConfigPaths({ homeDir = os.homedir(), project = null } = {}) {
|
|
|
1363
1432
|
}
|
|
1364
1433
|
|
|
1365
1434
|
module.exports = {
|
|
1366
|
-
|
|
1435
|
+
apiVersion: 1,
|
|
1436
|
+
name: RUNTIME_NAME,
|
|
1367
1437
|
capabilities,
|
|
1368
1438
|
resolveBinary,
|
|
1369
1439
|
capsFile: CAPS_FILE,
|
|
@@ -1372,7 +1442,11 @@ module.exports = {
|
|
|
1372
1442
|
// Use the same wrapper as Claude — spawn-agent.js is runtime-agnostic per P-9c4f2d6a
|
|
1373
1443
|
spawnScript: path.join(ENGINE_DIR, 'spawn-agent.js'),
|
|
1374
1444
|
installHint: INSTALL_HINT,
|
|
1445
|
+
workerErrorHint: 'copilot --acp failed -- ensure copilot CLI >=1.0.46 and copilot login is complete',
|
|
1446
|
+
resolveInvocationOptions,
|
|
1375
1447
|
buildSpawnFlags,
|
|
1448
|
+
buildWorkerArgs,
|
|
1449
|
+
encodePooledOutput,
|
|
1376
1450
|
buildArgs,
|
|
1377
1451
|
buildPrompt,
|
|
1378
1452
|
getUserAssetDirs,
|
|
@@ -1392,6 +1466,8 @@ module.exports = {
|
|
|
1392
1466
|
parseStreamChunk,
|
|
1393
1467
|
parseError,
|
|
1394
1468
|
createStreamConsumer,
|
|
1469
|
+
isSpawnStartupEvent,
|
|
1470
|
+
shouldSuppressPostMutationError,
|
|
1395
1471
|
permissionBypassFlags: PERMISSION_BYPASS_FLAGS,
|
|
1396
1472
|
// Exposed for unit tests — engine code MUST go through resolveRuntime + the
|
|
1397
1473
|
// adapter contract; never reach into these helpers directly.
|
|
@@ -1412,4 +1488,5 @@ module.exports = {
|
|
|
1412
1488
|
_extractInvalidModelName,
|
|
1413
1489
|
CAPS_SCHEMA_VERSION,
|
|
1414
1490
|
KNOWN_EVENT_TYPES,
|
|
1491
|
+
SPAWN_STARTUP_TYPES,
|
|
1415
1492
|
};
|
package/engine/runtimes/index.js
CHANGED
|
@@ -7,18 +7,16 @@
|
|
|
7
7
|
* runtime name fails loudly with a clear list of registered options.
|
|
8
8
|
*
|
|
9
9
|
* Adding a new runtime:
|
|
10
|
-
* 1. Implement
|
|
11
|
-
* 2. `
|
|
10
|
+
* 1. Implement API version 1 from contract.js.
|
|
11
|
+
* 2. `registerRuntime('<name>', require('./<name>'), { strict: true });`.
|
|
12
12
|
* 3. Expose its capabilities via the `/api/runtimes` endpoint (free).
|
|
13
13
|
*
|
|
14
14
|
* Engine code MUST gate behavior on `runtime.capabilities.*` flags, not on
|
|
15
15
|
* `runtime.name === 'claude'` comparisons. The whole point of this layer.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
const contract = require('./contract');
|
|
18
19
|
const registry = new Map();
|
|
19
|
-
registry.set('claude', require('./claude'));
|
|
20
|
-
registry.set('codex', require('./codex'));
|
|
21
|
-
registry.set('copilot', require('./copilot'));
|
|
22
20
|
|
|
23
21
|
/**
|
|
24
22
|
* Look up a runtime adapter by name. Throws when the name is unknown so
|
|
@@ -47,16 +45,46 @@ function listRuntimes() {
|
|
|
47
45
|
* Register a runtime adapter. Exposed for tests and for downstream tooling
|
|
48
46
|
* that wants to register a custom runtime without editing this file.
|
|
49
47
|
*/
|
|
50
|
-
function registerRuntime(name, adapter) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
function registerRuntime(name, adapter, options) {
|
|
49
|
+
const normalized = contract.normalizeRuntimeAdapter(name, adapter, options);
|
|
50
|
+
registry.set(name, normalized);
|
|
51
|
+
return normalized;
|
|
54
52
|
}
|
|
55
53
|
|
|
54
|
+
function resolveRuntimeByCapability(capability) {
|
|
55
|
+
const matches = listRuntimes()
|
|
56
|
+
.map(name => registry.get(name))
|
|
57
|
+
.filter(adapter => adapter?.capabilities?.[capability] === true);
|
|
58
|
+
if (matches.length === 0) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`No registered runtime declares capability "${capability}"`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return matches[0];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
registerRuntime('claude', require('./claude'), { strict: true });
|
|
67
|
+
registerRuntime('codex', require('./codex'), { strict: true });
|
|
68
|
+
registerRuntime('copilot', require('./copilot'), { strict: true });
|
|
69
|
+
|
|
56
70
|
module.exports = {
|
|
71
|
+
ADAPTER_API_VERSION: contract.ADAPTER_API_VERSION,
|
|
72
|
+
RUNTIME_OPTIONS_FLAG: contract.RUNTIME_OPTIONS_FLAG,
|
|
73
|
+
RUNTIME_IMAGES_FILE_OPTION: contract.RUNTIME_IMAGES_FILE_OPTION,
|
|
57
74
|
resolveRuntime,
|
|
75
|
+
resolveRuntimeByCapability,
|
|
58
76
|
listRuntimes,
|
|
59
77
|
registerRuntime,
|
|
78
|
+
sanitizeInvocationOptions: contract.sanitizeInvocationOptions,
|
|
79
|
+
resolveInvocationOptions: contract.resolveInvocationOptions,
|
|
80
|
+
decodeRuntimeOptions: contract.decodeRuntimeOptions,
|
|
81
|
+
buildSpawnFlags: contract.buildSpawnFlags,
|
|
82
|
+
prepareWorkspace: contract.prepareWorkspace,
|
|
83
|
+
isSpawnStartupEvent: contract.isSpawnStartupEvent,
|
|
84
|
+
shouldSuppressPostMutationError: contract.shouldSuppressPostMutationError,
|
|
85
|
+
buildWorkerArgs: contract.buildWorkerArgs,
|
|
86
|
+
encodePooledOutput: contract.encodePooledOutput,
|
|
87
|
+
validateRuntimeAdapter: contract.normalizeRuntimeAdapter,
|
|
60
88
|
// Exposed for tests — engine code MUST go through resolveRuntime/listRuntimes
|
|
61
89
|
_registry: registry,
|
|
62
90
|
};
|
package/engine/shared.js
CHANGED
|
@@ -1037,8 +1037,16 @@ function validateDispatchTmpDir(dirPath) {
|
|
|
1037
1037
|
|
|
1038
1038
|
function removeDispatchTmpDir(dirPath) {
|
|
1039
1039
|
if (!validateDispatchTmpDir(dirPath)) return false;
|
|
1040
|
-
try {
|
|
1041
|
-
|
|
1040
|
+
try {
|
|
1041
|
+
_retryFsOp(
|
|
1042
|
+
() => fs.rmSync(dirPath, { recursive: true, force: true }),
|
|
1043
|
+
`removeDispatchTmpDir(${dirPath})`
|
|
1044
|
+
);
|
|
1045
|
+
return true;
|
|
1046
|
+
} catch (e) {
|
|
1047
|
+
log('warn', `removeDispatchTmpDir failed after retries: ${e.message}`, { path: dirPath, error: e.code });
|
|
1048
|
+
return false;
|
|
1049
|
+
}
|
|
1042
1050
|
}
|
|
1043
1051
|
|
|
1044
1052
|
// Read a file using O_NOFOLLOW where the platform supports it. On Windows
|
|
@@ -1439,48 +1447,37 @@ function withFileLock(lockPath, fn, {
|
|
|
1439
1447
|
const isLockRaceErr = err.code === 'EEXIST' ||
|
|
1440
1448
|
(process.platform === 'win32' && err.code === 'EPERM');
|
|
1441
1449
|
if (!isLockRaceErr) throw err;
|
|
1442
|
-
//
|
|
1443
|
-
//
|
|
1444
|
-
//
|
|
1445
|
-
// - dead PID → reap.
|
|
1446
|
-
// - alive PID, mtime <= 5× → don't reap (legitimate slow holder).
|
|
1447
|
-
// - alive PID, mtime > 5× → reap (last-resort guard against
|
|
1448
|
-
// stuck holders that never released).
|
|
1449
|
-
// 3. Legacy / empty / non-JSON lockfile → mtime-only path (reap).
|
|
1450
|
+
// PID-owned locks are safe to reap as soon as their owner is dead.
|
|
1451
|
+
// Alive owners retain the 5× stale window; legacy/partial lockfiles
|
|
1452
|
+
// without a readable PID retain the mtime-only stale window.
|
|
1450
1453
|
try {
|
|
1451
1454
|
const stat = fs.statSync(lockPath);
|
|
1452
1455
|
const mtimeAge = Date.now() - stat.mtimeMs;
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
holderPid = parsed.pid;
|
|
1460
|
-
}
|
|
1461
|
-
} catch { /* legacy/empty/corrupt lock → fall through to mtime-only */ }
|
|
1462
|
-
|
|
1463
|
-
let shouldReap;
|
|
1464
|
-
if (holderPid !== null) {
|
|
1465
|
-
shouldReap = !processUtils.isPidAlive(holderPid) || mtimeAge > LOCK_STALE_MS * 5;
|
|
1466
|
-
} else {
|
|
1467
|
-
// Legacy empty/non-JSON lockfile: trust mtime alone
|
|
1468
|
-
shouldReap = true;
|
|
1456
|
+
let holderPid = null;
|
|
1457
|
+
try {
|
|
1458
|
+
const raw = fs.readFileSync(lockPath, 'utf8');
|
|
1459
|
+
const parsed = JSON.parse(raw);
|
|
1460
|
+
if (parsed && Number.isFinite(parsed.pid) && parsed.pid > 0) {
|
|
1461
|
+
holderPid = parsed.pid;
|
|
1469
1462
|
}
|
|
1463
|
+
} catch { /* legacy/empty/corrupt lock → mtime-only fallback */ }
|
|
1470
1464
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1465
|
+
const shouldReap = holderPid !== null
|
|
1466
|
+
? !processUtils.isPidAlive(holderPid) || mtimeAge > LOCK_STALE_MS * 5
|
|
1467
|
+
: mtimeAge > LOCK_STALE_MS;
|
|
1468
|
+
|
|
1469
|
+
if (shouldReap) {
|
|
1470
|
+
try {
|
|
1471
|
+
fs.unlinkSync(lockPath);
|
|
1472
|
+
} catch (unlinkErr) {
|
|
1473
|
+
// ENOENT: another process deleted the lock between stat and unlink — safe to retry.
|
|
1474
|
+
// EPERM on Windows: file is in 'pending delete' state from a concurrent
|
|
1475
|
+
// reaper — the OS will finalize the delete shortly; retry will succeed.
|
|
1476
|
+
const tolerable = unlinkErr.code === 'ENOENT' ||
|
|
1477
|
+
(process.platform === 'win32' && unlinkErr.code === 'EPERM');
|
|
1478
|
+
if (!tolerable) throw unlinkErr;
|
|
1483
1479
|
}
|
|
1480
|
+
continue; // lock just removed — retry immediately
|
|
1484
1481
|
}
|
|
1485
1482
|
} catch (staleErr) {
|
|
1486
1483
|
// ENOENT from statSync: lock file disappeared between EEXIST and stat — retry will succeed.
|
|
@@ -3120,9 +3117,8 @@ const ENGINE_DEFAULTS = {
|
|
|
3120
3117
|
// description the validator will never accept (see stage-output
|
|
3121
3118
|
// substitution bug above) got re-evaluated forever, once per tick,
|
|
3122
3119
|
// invisible to the dashboard. Once the SAME (unchanged) description is
|
|
3123
|
-
// rejected this many consecutive times, the WI
|
|
3124
|
-
//
|
|
3125
|
-
// re-queued into review again.
|
|
3120
|
+
// rejected this many consecutive times, the WI stays pending but is marked
|
|
3121
|
+
// exhausted so discovery pauses until an operator edits it.
|
|
3126
3122
|
preDispatchEvalMaxRejections: 5,
|
|
3127
3123
|
completionNonceRequired: false, // P-d2a8f6c1 (agent trust boundary F8): when true, a missing `nonce` field in the completion JSON hard-fails the dispatch with failure_class:'completion-nonce-mismatch'. Default false for one release so older agents/runtime caches that haven't picked up the prompt change degrade with a warning instead of breaking. Mismatched nonces hard-fail regardless of this flag. See docs/completion-reports.md → "Trust boundary".
|
|
3128
3124
|
autoApplyReviewVote: false, // Master gate for platform vote mutations; local verdicts remain informational when false.
|
|
@@ -3255,6 +3251,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3255
3251
|
maxMeetingPromptBytes: 16 * 1024, // cap meeting findings/debate context injected into prompts
|
|
3256
3252
|
maxMeetingHumanNotesBytes: 2 * 1024, // cap human note bullet lists injected into meeting prompts
|
|
3257
3253
|
maxPipelineMeetingContextBytes: 16 * 1024, // cap aggregated meeting/dependency context for pipeline plan generation
|
|
3254
|
+
maxPipelineStageHandoffBytes: 32 * 1024, // cap current-run note content exposed through {{stages.<id>.handoff}}
|
|
3258
3255
|
notesArchiveMaxFiles: 2000, // keep notes/archive bounded during periodic cleanup
|
|
3259
3256
|
// ── Worktree pool (W-mp73ya3e000me6c5) ─────────────────────────────────────
|
|
3260
3257
|
// Per-project warm pool: completed worktrees are parked detached at
|
|
@@ -3324,7 +3321,7 @@ const ENGINE_DEFAULTS = {
|
|
|
3324
3321
|
'node', 'bun', 'npm', 'npx', 'pnpm', 'yarn',
|
|
3325
3322
|
'python', 'python3', 'pip', 'pip3',
|
|
3326
3323
|
'docker', 'podman',
|
|
3327
|
-
'adb', 'emulator',
|
|
3324
|
+
'adb', 'emulator', 'maestro',
|
|
3328
3325
|
'gradle', 'gradlew', 'mvn',
|
|
3329
3326
|
'pwsh', 'powershell', 'bash', 'sh',
|
|
3330
3327
|
'curl', 'wget',
|
|
@@ -3431,9 +3428,9 @@ function resolvePollFlag(engineCfg, granularKey, legacyMacroKey) {
|
|
|
3431
3428
|
|
|
3432
3429
|
// ─── Runtime Fleet Resolution (P-3b8e5f1d) ──────────────────────────────────
|
|
3433
3430
|
//
|
|
3434
|
-
//
|
|
3435
|
-
// + budget + bare-mode applies to this spawn?". Engine code MUST
|
|
3436
|
-
// these — never read `agent.cli`, `engine.defaultCli`, etc. directly. Future
|
|
3431
|
+
// Runtime helpers are the single source of truth for "which CLI runtime + model
|
|
3432
|
+
// + tool ceiling + budget + bare-mode applies to this spawn?". Engine code MUST
|
|
3433
|
+
// go through these — never read `agent.cli`, `engine.defaultCli`, etc. directly. Future
|
|
3437
3434
|
// agents adding new resolution rules should extend these helpers, not bypass
|
|
3438
3435
|
// them.
|
|
3439
3436
|
//
|
|
@@ -3479,28 +3476,27 @@ function resolveCcCli(engine) {
|
|
|
3479
3476
|
/**
|
|
3480
3477
|
* Resolve whether the Command Center / doc-chat path should route through the
|
|
3481
3478
|
* persistent ACP worker pool. Priority:
|
|
3482
|
-
* 1.
|
|
3483
|
-
*
|
|
3484
|
-
* switch
|
|
3485
|
-
* Refuse the pool when CC runtime is not copilot, even if explicit-true is
|
|
3486
|
-
* set. Future runtimes that don't implement ACP fall into the same bucket.
|
|
3479
|
+
* 1. Runtime capability guard. Refuse the pool unless the selected adapter
|
|
3480
|
+
* declares `acpWorkerPool`, even if explicit-true is set, so pooling can
|
|
3481
|
+
* never switch the configured harness.
|
|
3487
3482
|
* (W-mphlriic00095f69)
|
|
3488
3483
|
* 2. `engine.ccUseWorkerPool` explicit true/false — operator override wins
|
|
3489
|
-
*
|
|
3490
|
-
* 3. Runtime-aware default: ON
|
|
3491
|
-
* (Copilot's cold-spawn is 18-21s on Windows — pool turns subsequent turns
|
|
3492
|
-
* from 47-140s back into a few seconds).
|
|
3484
|
+
* within an ACP-capable runtime.
|
|
3485
|
+
* 3. Runtime-aware default: ON for any ACP-capable adapter.
|
|
3493
3486
|
* 4. ENGINE_DEFAULTS.ccUseWorkerPool — final fallback
|
|
3494
3487
|
*
|
|
3495
3488
|
* Strict boolean check on the override so a literal `false` opts out even on
|
|
3496
|
-
*
|
|
3489
|
+
* ACP runtimes, matching the "treat empty/null/undefined as unset" semantics used
|
|
3497
3490
|
* throughout the resolve* helpers for boolean flags.
|
|
3498
3491
|
*/
|
|
3499
3492
|
function resolveCcUseWorkerPool(engine) {
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3493
|
+
let runtime;
|
|
3494
|
+
try {
|
|
3495
|
+
runtime = require('./runtimes').resolveRuntime(resolveCcCli(engine));
|
|
3496
|
+
} catch {
|
|
3497
|
+
return false;
|
|
3498
|
+
}
|
|
3499
|
+
if (runtime?.capabilities?.acpWorkerPool !== true) return false;
|
|
3504
3500
|
if (engine && (engine.ccUseWorkerPool === true || engine.ccUseWorkerPool === false)) {
|
|
3505
3501
|
return engine.ccUseWorkerPool;
|
|
3506
3502
|
}
|
|
@@ -3514,7 +3510,7 @@ function resolveCcUseWorkerPool(engine) {
|
|
|
3514
3510
|
* adapter (as returned by `resolveRuntime(resolveAgentCli(agent, engine))`)
|
|
3515
3511
|
* rather than re-deriving it, since callers on the agent-spawn path already
|
|
3516
3512
|
* have it in hand. Priority:
|
|
3517
|
-
* 1. Capability guard — pool transport is ACP-only
|
|
3513
|
+
* 1. Capability guard — pool transport is ACP-only. Hard
|
|
3518
3514
|
* false whenever `runtime.capabilities.acpWorkerPool` isn't `true`,
|
|
3519
3515
|
* regardless of operator override, so a future non-ACP runtime can never
|
|
3520
3516
|
* be silently routed through the pool (same rationale as
|
|
@@ -3588,6 +3584,17 @@ function resolveCcModel(engine) {
|
|
|
3588
3584
|
return undefined;
|
|
3589
3585
|
}
|
|
3590
3586
|
|
|
3587
|
+
/**
|
|
3588
|
+
* Resolve the fleet tool ceiling for an agent spawn.
|
|
3589
|
+
*
|
|
3590
|
+
* `config.claude.allowedTools` predates runtime adapters but has always applied
|
|
3591
|
+
* to every selected runtime. Keep that cross-runtime contract centralized here
|
|
3592
|
+
* until the field is migrated to a runtime-neutral config namespace.
|
|
3593
|
+
*/
|
|
3594
|
+
function resolveAgentAllowedTools(config) {
|
|
3595
|
+
return config?.claude?.allowedTools;
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3591
3598
|
/**
|
|
3592
3599
|
* Resolve the per-spawn USD budget cap. Priority:
|
|
3593
3600
|
* 1. `agent.maxBudgetUsd` — per-agent override
|
|
@@ -3629,6 +3636,28 @@ function resolveAgentBareMode(agent, engine) {
|
|
|
3629
3636
|
return false;
|
|
3630
3637
|
}
|
|
3631
3638
|
|
|
3639
|
+
// W-mrdon0pe000l045a — resolve whether repo-authored CLAUDE.md instructions
|
|
3640
|
+
// should be propagated into the prompt for runtimes that don't auto-discover
|
|
3641
|
+
// CLAUDE.md natively (Copilot/Codex). Precedence mirrors
|
|
3642
|
+
// `resolveLiveCheckoutAutoReset`:
|
|
3643
|
+
// 1. per-project boolean `project.propagateClaudeMdForNonClaudeRuntimes`
|
|
3644
|
+
// 2. fleet-wide boolean `engine.propagateClaudeMdForNonClaudeRuntimes`
|
|
3645
|
+
// 3. hardcoded default TRUE (additive, on by default)
|
|
3646
|
+
// Only an explicit boolean counts as "set" at either level — undefined/null/
|
|
3647
|
+
// string are treated as unset so the next tier (or the TRUE default) decides.
|
|
3648
|
+
// This gate is orthogonal to each runtime's native AGENTS.md / repo-instruction
|
|
3649
|
+
// discovery by design (see ENGINE_DEFAULTS.propagateClaudeMdForNonClaudeRuntimes).
|
|
3650
|
+
// Pure, no I/O.
|
|
3651
|
+
function resolvePropagateClaudeMdForNonClaudeRuntimes(project, engine) {
|
|
3652
|
+
if (project && typeof project === 'object' && typeof project.propagateClaudeMdForNonClaudeRuntimes === 'boolean') {
|
|
3653
|
+
return project.propagateClaudeMdForNonClaudeRuntimes;
|
|
3654
|
+
}
|
|
3655
|
+
if (engine && typeof engine === 'object' && typeof engine.propagateClaudeMdForNonClaudeRuntimes === 'boolean') {
|
|
3656
|
+
return engine.propagateClaudeMdForNonClaudeRuntimes;
|
|
3657
|
+
}
|
|
3658
|
+
return true;
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3632
3661
|
// ─── Legacy ccModel → defaultModel Migration ─────────────────────────────────
|
|
3633
3662
|
//
|
|
3634
3663
|
// Pre-P-3b8e5f1d, `engine.ccModel` was the single fleet-wide model knob (it
|
|
@@ -4500,7 +4529,7 @@ const FAILURE_CLASS = {
|
|
|
4500
4529
|
OUTPUT_TRUNCATED: 'output-truncated', // P-8e4c2a17: the agent streamed more stdout than the engine's hard capture cap (engine.js AGENT_OUTPUT_CAP_BYTES, 1MB) BEFORE the terminal `result` event arrived. The result (session id, completion block, final text) lives at the END of the stream, so it fell outside the captured window and parseOutput found nothing — the dispatch would otherwise fail as an opaque UNKNOWN and retry to death. Surfaced loudly with an actionable message; non-retryable (mechanical retry just reproduces the overflow — the agent must reduce output volume or the task must be split).
|
|
4501
4530
|
VERIFY_MISSING_PR: 'verify-missing-pr', // Verify completed without an attached or tracked PR.
|
|
4502
4531
|
PROJECT_NOT_FOUND: 'project_not_found', // W-mqv2wsy30002e090: a work item's `project` field names a project that is not configured (resolveConfiguredProject / formatUnknownProjectError → `Project "<name>" not found. Known projects: …`). Stamped at discovery time so the dashboard PR-column renderer surfaces the red failReason snippet like other non-retryable failures instead of burying it in the Agent column. Non-retryable — operator must fix the WI's project field or configure the project.
|
|
4503
|
-
PRE_DISPATCH_EVAL_STUCK: 'pre-dispatch-eval-stuck', //
|
|
4532
|
+
PRE_DISPATCH_EVAL_STUCK: 'pre-dispatch-eval-stuck', // Legacy persisted classification normalized by migration 025. New exhausted evaluations remain pending with _preDispatchEval.exhausted so they are not counted as agent execution failures.
|
|
4504
4533
|
UNKNOWN: 'unknown', // Unclassified failure
|
|
4505
4534
|
};
|
|
4506
4535
|
|
|
@@ -6024,7 +6053,7 @@ function validateProjectPath(pathStr, options = {}) {
|
|
|
6024
6053
|
|
|
6025
6054
|
function parseSkillFrontmatter(content, filename) {
|
|
6026
6055
|
let name = filename.replace('.md', '');
|
|
6027
|
-
let trigger = '', description = '', project = 'any', author = '', created = '', allowedTools = '';
|
|
6056
|
+
let trigger = '', description = '', project = 'any', author = '', created = '', allowedTools = '', scope = 'minions';
|
|
6028
6057
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
6029
6058
|
if (fmMatch) {
|
|
6030
6059
|
const fm = fmMatch[1];
|
|
@@ -6036,8 +6065,52 @@ function parseSkillFrontmatter(content, filename) {
|
|
|
6036
6065
|
author = m('author');
|
|
6037
6066
|
created = m('created');
|
|
6038
6067
|
allowedTools = m('allowed-tools');
|
|
6068
|
+
scope = m('scope') || scope;
|
|
6069
|
+
}
|
|
6070
|
+
return { name, trigger, description, project, author, created, allowedTools, scope };
|
|
6071
|
+
}
|
|
6072
|
+
|
|
6073
|
+
function syncBundledPersonalSkills(skillsDir, opts = {}) {
|
|
6074
|
+
const result = { created: [], updated: [], unchanged: [] };
|
|
6075
|
+
if (!skillsDir || !fs.existsSync(skillsDir)) return result;
|
|
6076
|
+
|
|
6077
|
+
const homeDir = opts.homeDir || os.homedir();
|
|
6078
|
+
const { listRuntimes, resolveRuntime } = require('./runtimes');
|
|
6079
|
+
const personalRoots = new Set();
|
|
6080
|
+
for (const runtimeName of listRuntimes()) {
|
|
6081
|
+
const runtime = resolveRuntime(runtimeName);
|
|
6082
|
+
if (typeof runtime.getSkillWriteTargets !== 'function') continue;
|
|
6083
|
+
const personal = runtime.getSkillWriteTargets({ homeDir }).personal;
|
|
6084
|
+
if (typeof personal === 'string' && personal.trim()) {
|
|
6085
|
+
personalRoots.add(path.resolve(personal));
|
|
6086
|
+
}
|
|
6087
|
+
}
|
|
6088
|
+
|
|
6089
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
6090
|
+
.filter(entry => entry.isDirectory())
|
|
6091
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
6092
|
+
for (const entry of entries) {
|
|
6093
|
+
const sourcePath = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
6094
|
+
if (!fs.existsSync(sourcePath)) continue;
|
|
6095
|
+
const content = fs.readFileSync(sourcePath, 'utf8');
|
|
6096
|
+
const skill = parseSkillFrontmatter(content, `${entry.name}.md`);
|
|
6097
|
+
if (skill.scope !== 'minions') continue;
|
|
6098
|
+
const skillName = String(skill.name || entry.name).replace(/[^a-z0-9-]/g, '-');
|
|
6099
|
+
if (!skillName) throw new Error(`Bundled skill has an invalid name: ${sourcePath}`);
|
|
6100
|
+
|
|
6101
|
+
for (const personalRoot of personalRoots) {
|
|
6102
|
+
const targetPath = path.join(personalRoot, skillName, 'SKILL.md');
|
|
6103
|
+
const exists = fs.existsSync(targetPath);
|
|
6104
|
+
if (exists && fs.readFileSync(targetPath, 'utf8') === content) {
|
|
6105
|
+
result.unchanged.push(targetPath);
|
|
6106
|
+
continue;
|
|
6107
|
+
}
|
|
6108
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
6109
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
6110
|
+
result[exists ? 'updated' : 'created'].push(targetPath);
|
|
6111
|
+
}
|
|
6039
6112
|
}
|
|
6040
|
-
return
|
|
6113
|
+
return result;
|
|
6041
6114
|
}
|
|
6042
6115
|
|
|
6043
6116
|
// ── PR → PRD Links ────────────────────────────────────────────────────────────
|
|
@@ -7721,6 +7794,7 @@ function prFixEvidenceFingerprint(pr, cause = PR_FIX_CAUSE.UNKNOWN) {
|
|
|
7721
7794
|
evidence.lastReviewedAt = pr?.lastReviewedAt || '';
|
|
7722
7795
|
evidence.reviewedAt = review.reviewedAt || '';
|
|
7723
7796
|
evidence.reviewNote = review.note || pr?.reviewNote || '';
|
|
7797
|
+
evidence.findingId = prReviewFindingIdentity(pr);
|
|
7724
7798
|
// #2979 — same rationale as BUILD_FAILURE: review feedback fingerprints
|
|
7725
7799
|
// were sticky across force-push because reviewStatus / reviewedAt /
|
|
7726
7800
|
// reviewNote don't change when the author rebases. Adding the head SHA
|
|
@@ -7732,6 +7806,23 @@ function prFixEvidenceFingerprint(pr, cause = PR_FIX_CAUSE.UNKNOWN) {
|
|
|
7732
7806
|
return crypto.createHash('sha1').update(JSON.stringify(evidence)).digest('hex').slice(0, 16);
|
|
7733
7807
|
}
|
|
7734
7808
|
|
|
7809
|
+
function prReviewFindingIdentity(pr) {
|
|
7810
|
+
const review = pr?.minionsReview || {};
|
|
7811
|
+
const threads = Array.isArray(review.threads)
|
|
7812
|
+
? review.threads.map(value => String(value)).filter(Boolean).sort()
|
|
7813
|
+
: [];
|
|
7814
|
+
const evidence = {
|
|
7815
|
+
pr: pr?.id || pr?.url || '',
|
|
7816
|
+
sourceItem: review.sourceItem || '',
|
|
7817
|
+
dispatchId: review.dispatchId || '',
|
|
7818
|
+
reviewer: review.reviewer || '',
|
|
7819
|
+
reviewedAt: review.reviewedAt || pr?.lastReviewedAt || '',
|
|
7820
|
+
threads,
|
|
7821
|
+
note: review.note || pr?.reviewNote || '',
|
|
7822
|
+
};
|
|
7823
|
+
return `review-${crypto.createHash('sha1').update(JSON.stringify(evidence)).digest('hex').slice(0, 16)}`;
|
|
7824
|
+
}
|
|
7825
|
+
|
|
7735
7826
|
function getPrNoOpFixRecord(pr, cause) {
|
|
7736
7827
|
if (!pr || !cause || !pr._noOpFixes || typeof pr._noOpFixes !== 'object') return null;
|
|
7737
7828
|
const record = pr._noOpFixes[cause];
|
|
@@ -8491,7 +8582,7 @@ module.exports = {
|
|
|
8491
8582
|
ENGINE_DEFAULTS,
|
|
8492
8583
|
resolvePollFlag, // P-c4d8e1a3 — granular per-poller flag resolution
|
|
8493
8584
|
resolveAgentCli, resolveCcCli, resolveCcUseWorkerPool, resolveAgentUseWorkerPool, resolveAgentAcpPoolSize, resolveAgentModel, resolveCcModel,
|
|
8494
|
-
resolveAgentMaxBudget, resolveAgentBareMode,
|
|
8585
|
+
resolveAgentAllowedTools, resolveAgentMaxBudget, resolveAgentBareMode, resolvePropagateClaudeMdForNonClaudeRuntimes,
|
|
8495
8586
|
applyLegacyCcModelMigration, _resetLegacyCcModelMigrationFlag,
|
|
8496
8587
|
runtimeConfigWarnings,
|
|
8497
8588
|
projectWorkSourceWarnings,
|
|
@@ -8629,6 +8720,7 @@ module.exports = {
|
|
|
8629
8720
|
PR_FIX_CAUSE,
|
|
8630
8721
|
getPrFixAutomationCause,
|
|
8631
8722
|
prFixEvidenceFingerprint,
|
|
8723
|
+
prReviewFindingIdentity,
|
|
8632
8724
|
prMergeConflictGuardKey,
|
|
8633
8725
|
resetMergeConflictStateOnRetarget,
|
|
8634
8726
|
getPrNoOpFixRecord,
|
|
@@ -8636,6 +8728,7 @@ module.exports = {
|
|
|
8636
8728
|
getPrPausedCauses,
|
|
8637
8729
|
isBuildFixIneffectivePaused,
|
|
8638
8730
|
parseSkillFrontmatter,
|
|
8731
|
+
syncBundledPersonalSkills,
|
|
8639
8732
|
sleepMs,
|
|
8640
8733
|
clearWorktreeFailureCache,
|
|
8641
8734
|
removeWorktree,
|