@yemi33/minions 0.1.2382 → 0.1.2384
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 +1 -0
- package/dashboard/js/refresh.js +2 -1
- package/dashboard/js/settings.js +1 -1
- package/dashboard.js +37 -19
- package/docs/completion-reports.md +20 -1
- package/docs/keep-processes.md +7 -2
- package/docs/live-checkout-mode.md +7 -7
- package/docs/managed-spawn.md +14 -1
- package/docs/runtime-adapters.md +61 -26
- package/docs/skills.md +2 -0
- package/docs/workspace-manifests.md +1 -1
- package/docs/worktree-lifecycle.md +36 -0
- package/engine/acp-transport.js +273 -58
- package/engine/ado-comment.js +3 -1
- package/engine/agent-worker-pool.js +278 -54
- package/engine/cc-worker-pool.js +12 -3
- package/engine/cleanup.js +15 -11
- package/engine/cli.js +56 -28
- package/engine/comment-format.js +51 -14
- package/engine/create-pr-worktree.js +57 -79
- package/engine/dispatch.js +7 -2
- package/engine/execution-model.js +68 -0
- package/engine/gh-comment.js +6 -3
- package/engine/github.js +6 -7
- package/engine/keep-process-sweep.js +131 -9
- package/engine/lifecycle.js +126 -45
- package/engine/live-checkout.js +4 -2
- package/engine/llm.js +59 -83
- package/engine/managed-spawn.js +112 -5
- package/engine/memory-store.js +3 -1
- package/engine/playbook.js +4 -6
- package/engine/pooled-agent-process.js +512 -45
- package/engine/pr-action.js +6 -8
- package/engine/process-utils.js +154 -18
- package/engine/runtimes/claude.js +94 -25
- package/engine/runtimes/codex.js +1 -0
- package/engine/runtimes/contract.js +239 -0
- package/engine/runtimes/copilot.js +97 -9
- package/engine/runtimes/index.js +37 -9
- package/engine/shared.js +349 -82
- package/engine/spawn-agent.js +85 -116
- package/engine/spawn-phase-watchdog.js +8 -37
- package/engine/timeout.js +14 -10
- package/engine/worktree-gc.js +55 -46
- package/engine.js +418 -241
- package/package.json +1 -1
- package/playbooks/fix.md +20 -0
- package/playbooks/review.md +2 -0
- package/playbooks/shared-rules.md +2 -2
- package/prompts/cc-system.md +11 -11
- package/skills/check-self-authored-review-comment/SKILL.md +34 -0
package/engine/llm.js
CHANGED
|
@@ -18,7 +18,15 @@ const {
|
|
|
18
18
|
ENGINE_DEFAULTS,
|
|
19
19
|
resolveCcCli, resolveCcModel,
|
|
20
20
|
} = shared;
|
|
21
|
-
const {
|
|
21
|
+
const {
|
|
22
|
+
resolveRuntime,
|
|
23
|
+
resolveRuntimeByCapability,
|
|
24
|
+
listRuntimes,
|
|
25
|
+
RUNTIME_IMAGES_FILE_OPTION,
|
|
26
|
+
sanitizeInvocationOptions,
|
|
27
|
+
resolveInvocationOptions,
|
|
28
|
+
buildSpawnFlags,
|
|
29
|
+
} = require('./runtimes');
|
|
22
30
|
|
|
23
31
|
const MINIONS_DIR = shared.MINIONS_DIR;
|
|
24
32
|
const ENGINE_DIR = path.join(MINIONS_DIR, 'engine');
|
|
@@ -217,15 +225,17 @@ function _runtimeInstallHint(runtimeName, runtime) {
|
|
|
217
225
|
function _missingRuntimeMessage(runtimeName, runtime, reason) {
|
|
218
226
|
const selected = runtime?.name || runtimeName || 'configured';
|
|
219
227
|
if (!runtime) {
|
|
228
|
+
const installLines = listRuntimes().map((name) => {
|
|
229
|
+
const adapter = resolveRuntime(name);
|
|
230
|
+
return `- ${name}: ${_runtimeInstallHint(name, adapter)}`;
|
|
231
|
+
});
|
|
220
232
|
return [
|
|
221
233
|
`Minions can't run the configured runtime "${selected}".`,
|
|
222
234
|
'',
|
|
223
235
|
reason || 'The configured runtime is not registered.',
|
|
224
236
|
'',
|
|
225
237
|
'Choose a supported runtime in Settings -> Engine -> defaultCli/ccCli, then install its CLI:',
|
|
226
|
-
|
|
227
|
-
'- OpenAI Codex CLI: npm install -g @openai/codex or brew install --cask codex',
|
|
228
|
-
'- GitHub Copilot CLI: install via GitHub CLI/gh-copilot or download from https://github.com/github/copilot-cli/releases',
|
|
238
|
+
...installLines,
|
|
229
239
|
'',
|
|
230
240
|
'After installing, restart Minions so the dashboard and engine inherit the updated PATH.',
|
|
231
241
|
].join('\n');
|
|
@@ -239,10 +249,9 @@ function _missingRuntimeMessage(runtimeName, runtime, reason) {
|
|
|
239
249
|
'',
|
|
240
250
|
'After installing, restart Minions so the dashboard and engine inherit the updated PATH.',
|
|
241
251
|
];
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
lines.push('Or switch Settings -> Engine -> defaultCli/ccCli back to "claude" after installing Claude Code.');
|
|
252
|
+
const alternatives = listRuntimes().filter(name => name !== selected);
|
|
253
|
+
if (alternatives.length) {
|
|
254
|
+
lines.push(`Or switch Settings -> Engine -> defaultCli/ccCli to another installed runtime: ${alternatives.join(', ')}.`);
|
|
246
255
|
}
|
|
247
256
|
return lines.join('\n');
|
|
248
257
|
}
|
|
@@ -337,37 +346,12 @@ function _runtimeUnavailableResult(callOpts = {}) {
|
|
|
337
346
|
// ─── Spawn Helpers ───────────────────────────────────────────────────────────
|
|
338
347
|
|
|
339
348
|
/**
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
* the single source of truth and avoiding double-flag emission.
|
|
344
|
-
*
|
|
345
|
-
* Capability gating (matches engine.js _buildAgentSpawnFlags from P-2a6d9c4f):
|
|
346
|
-
* - effort/sessionId/maxBudget/bare/fallbackModel are dropped when the
|
|
347
|
-
* runtime's matching capability is false.
|
|
348
|
-
* - Copilot-specific opts (stream, disableBuiltinMcps,
|
|
349
|
-
* reasoningSummaries) are emitted unconditionally; the Claude adapter
|
|
350
|
-
* ignores them via the "tolerate unknown opts" rule.
|
|
349
|
+
* Encode the semantic invocation options through the stable runtime facade.
|
|
350
|
+
* Legacy named wrapper flags remain for compatibility; the opaque options bag
|
|
351
|
+
* is authoritative for adapter fields the wrapper does not know yet.
|
|
351
352
|
*/
|
|
352
353
|
function _buildSpawnAgentFlags(runtime, opts = {}) {
|
|
353
|
-
|
|
354
|
-
const flags = ['--runtime', String(runtime?.name || 'claude')];
|
|
355
|
-
|
|
356
|
-
if (opts.maxTurns != null) flags.push('--max-turns', String(opts.maxTurns));
|
|
357
|
-
if (opts.model) flags.push('--model', String(opts.model));
|
|
358
|
-
if (opts.allowedTools) flags.push('--allowedTools', String(opts.allowedTools));
|
|
359
|
-
|
|
360
|
-
if (caps.effortLevels && opts.effort) flags.push('--effort', String(opts.effort));
|
|
361
|
-
if (caps.sessionResume && opts.sessionId) flags.push('--resume', String(opts.sessionId));
|
|
362
|
-
if (caps.budgetCap && opts.maxBudget != null) flags.push('--max-budget-usd', String(opts.maxBudget));
|
|
363
|
-
if (caps.bareMode && opts.bare === true) flags.push('--bare');
|
|
364
|
-
if (caps.fallbackModel && opts.fallbackModel) flags.push('--fallback-model', String(opts.fallbackModel));
|
|
365
|
-
|
|
366
|
-
if (opts.stream === 'on' || opts.stream === 'off') flags.push('--stream', opts.stream);
|
|
367
|
-
if (opts.disableBuiltinMcps === true) flags.push('--disable-builtin-mcps');
|
|
368
|
-
if (opts.reasoningSummaries === true) flags.push('--enable-reasoning-summaries');
|
|
369
|
-
|
|
370
|
-
return flags;
|
|
354
|
+
return buildSpawnFlags(runtime, opts);
|
|
371
355
|
}
|
|
372
356
|
|
|
373
357
|
/**
|
|
@@ -379,15 +363,12 @@ function _buildSpawnAgentFlags(runtime, opts = {}) {
|
|
|
379
363
|
*
|
|
380
364
|
* Indirect path: uses engine/spawn-agent.js — mostly a fallback when the
|
|
381
365
|
* direct path can't resolve the binary cache. spawn-agent.js handles
|
|
382
|
-
* adapter resolution itself; we
|
|
383
|
-
*
|
|
366
|
+
* adapter resolution itself; we hand it the runtime plus an opaque options
|
|
367
|
+
* bag, so adding a harness flag does not require wrapper changes.
|
|
384
368
|
*/
|
|
385
369
|
function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
386
370
|
const {
|
|
387
|
-
direct, label, runtime,
|
|
388
|
-
maxBudget, bare, fallbackModel,
|
|
389
|
-
stream, disableBuiltinMcps, reasoningSummaries,
|
|
390
|
-
images,
|
|
371
|
+
direct, label, runtime, adapterOptions = {},
|
|
391
372
|
} = callOpts;
|
|
392
373
|
|
|
393
374
|
const id = uid();
|
|
@@ -399,24 +380,10 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
399
380
|
const cleanupFiles = [];
|
|
400
381
|
const cleanupDirs = [llmTmpDir];
|
|
401
382
|
const caps = (runtime && runtime.capabilities) || {};
|
|
402
|
-
const adapterOpts = {
|
|
403
|
-
|
|
404
|
-
maxBudget, bare, fallbackModel,
|
|
405
|
-
stream, disableBuiltinMcps, reasoningSummaries,
|
|
406
|
-
images,
|
|
383
|
+
const adapterOpts = sanitizeInvocationOptions(runtime, {
|
|
384
|
+
...adapterOptions,
|
|
407
385
|
tmpDir: llmTmpDir,
|
|
408
|
-
};
|
|
409
|
-
// Capability-gate per-flag opts before prompt construction so adapters can
|
|
410
|
-
// make resume-aware prompt decisions from the same opts used for argv.
|
|
411
|
-
if (!caps.effortLevels) adapterOpts.effort = undefined;
|
|
412
|
-
if (!caps.sessionResume) adapterOpts.sessionId = undefined;
|
|
413
|
-
if (!caps.budgetCap) adapterOpts.maxBudget = undefined;
|
|
414
|
-
if (!caps.bareMode) adapterOpts.bare = undefined;
|
|
415
|
-
if (!caps.fallbackModel) adapterOpts.fallbackModel = undefined;
|
|
416
|
-
// P-7b2e4d01 — defense in depth: never forward images to a runtime that
|
|
417
|
-
// can't read them (callLLM/callLLMStreaming already gate + surface a typed
|
|
418
|
-
// error upstream; this protects any other _spawnProcess caller).
|
|
419
|
-
if (!caps.imageInput) adapterOpts.images = undefined;
|
|
386
|
+
});
|
|
420
387
|
const finalPrompt = runtime.buildPrompt(promptText, sysPromptText, adapterOpts);
|
|
421
388
|
|
|
422
389
|
// ── Direct path ──
|
|
@@ -426,7 +393,7 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
426
393
|
// Only write a sys-prompt tmp file when the runtime actually consumes one
|
|
427
394
|
// via --system-prompt-file (Claude) AND we're not resuming (resumed sessions
|
|
428
395
|
// already have the sys prompt baked in).
|
|
429
|
-
if (!sessionId && sysPromptText && caps.systemPromptFile) {
|
|
396
|
+
if (!adapterOpts.sessionId && sysPromptText && caps.systemPromptFile) {
|
|
430
397
|
sysTmpPath = path.join(llmTmpDir, `direct-sys-${id}.md`);
|
|
431
398
|
fs.writeFileSync(sysTmpPath, sysPromptText);
|
|
432
399
|
cleanupFiles.push(sysTmpPath);
|
|
@@ -472,12 +439,16 @@ function _spawnProcess(promptText, sysPromptText, callOpts) {
|
|
|
472
439
|
const pidPath = promptPath.replace(/prompt-/, 'pid-').replace(/\.md$/, '.pid');
|
|
473
440
|
cleanupFiles.push(promptPath, sysPath, pidPath);
|
|
474
441
|
|
|
475
|
-
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
442
|
+
const spawnScript = runtime.spawnScript || path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
443
|
+
const indirectAdapterOpts = { ...adapterOpts };
|
|
444
|
+
if (Array.isArray(indirectAdapterOpts.images) && indirectAdapterOpts.images.length) {
|
|
445
|
+
const imagesPath = path.join(llmTmpDir, `runtime-images-${id}.json`);
|
|
446
|
+
safeWrite(imagesPath, JSON.stringify(indirectAdapterOpts.images));
|
|
447
|
+
cleanupFiles.push(imagesPath);
|
|
448
|
+
indirectAdapterOpts[RUNTIME_IMAGES_FILE_OPTION] = imagesPath;
|
|
449
|
+
delete indirectAdapterOpts.images;
|
|
450
|
+
}
|
|
451
|
+
const adapterFlags = _buildSpawnAgentFlags(runtime, indirectAdapterOpts);
|
|
481
452
|
const args = [spawnScript, promptPath, sysPath, ...adapterFlags];
|
|
482
453
|
|
|
483
454
|
const proc = runFile(process.execPath, args, {
|
|
@@ -692,11 +663,16 @@ function _resolveModelForRuntime(runtime, callOpts) {
|
|
|
692
663
|
function _resolveRuntimeFeatureOpts({
|
|
693
664
|
stream, disableBuiltinMcps, reasoningSummaries, engineConfig,
|
|
694
665
|
} = {}) {
|
|
695
|
-
const
|
|
666
|
+
const runtime = resolveRuntimeByCapability('acpWorkerPool');
|
|
667
|
+
const resolved = resolveInvocationOptions(runtime, {
|
|
668
|
+
stream,
|
|
669
|
+
disableBuiltinMcps,
|
|
670
|
+
reasoningSummaries,
|
|
671
|
+
}, { engineConfig: engineConfig || {} });
|
|
696
672
|
return {
|
|
697
|
-
stream: stream
|
|
698
|
-
disableBuiltinMcps: disableBuiltinMcps
|
|
699
|
-
reasoningSummaries: reasoningSummaries
|
|
673
|
+
stream: resolved.stream,
|
|
674
|
+
disableBuiltinMcps: resolved.disableBuiltinMcps,
|
|
675
|
+
reasoningSummaries: resolved.reasoningSummaries,
|
|
700
676
|
};
|
|
701
677
|
}
|
|
702
678
|
|
|
@@ -770,17 +746,17 @@ function callLLM(promptText, sysPromptText, opts = {}) {
|
|
|
770
746
|
const model = _resolveModelForRuntime(runtime, { model: modelOverride, engineConfig });
|
|
771
747
|
const imageGate = _resolveImageOpts(images, runtime && runtime.capabilities);
|
|
772
748
|
if (imageGate.error) return _resolvedCallResult(_imageUnsupportedResult(runtime, imageGate.error));
|
|
773
|
-
const
|
|
774
|
-
|
|
775
|
-
|
|
749
|
+
const runtimeOptions = resolveInvocationOptions(runtime, {
|
|
750
|
+
model, maxTurns, allowedTools, effort, sessionId,
|
|
751
|
+
maxBudget, bare, fallbackModel, images: imageGate.images,
|
|
752
|
+
stream, disableBuiltinMcps, reasoningSummaries,
|
|
753
|
+
}, { engineConfig: engineConfig || {} });
|
|
776
754
|
|
|
777
755
|
let _abort = null;
|
|
778
756
|
const promise = new Promise((resolve) => {
|
|
779
757
|
const _startMs = Date.now();
|
|
780
758
|
const { proc, cleanupFiles, cleanupDirs } = _spawnProcess(promptText, sysPromptText, {
|
|
781
|
-
direct, label, runtime,
|
|
782
|
-
maxBudget, bare, fallbackModel, images: imageGate.images,
|
|
783
|
-
...runtimeFeatureOpts,
|
|
759
|
+
direct, label, runtime, adapterOptions: runtimeOptions,
|
|
784
760
|
});
|
|
785
761
|
let resolved = false;
|
|
786
762
|
let exitFallbackTimer = null;
|
|
@@ -895,17 +871,17 @@ function callLLMStreaming(promptText, sysPromptText, opts = {}) {
|
|
|
895
871
|
const model = _resolveModelForRuntime(runtime, { model: modelOverride, engineConfig });
|
|
896
872
|
const imageGate = _resolveImageOpts(images, runtime && runtime.capabilities);
|
|
897
873
|
if (imageGate.error) return _resolvedCallResult(_imageUnsupportedResult(runtime, imageGate.error));
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
874
|
+
const runtimeOptions = resolveInvocationOptions(runtime, {
|
|
875
|
+
model, maxTurns, allowedTools, effort, sessionId,
|
|
876
|
+
maxBudget, bare, fallbackModel, images: imageGate.images,
|
|
877
|
+
stream, disableBuiltinMcps, reasoningSummaries,
|
|
878
|
+
}, { engineConfig: engineConfig || {} });
|
|
901
879
|
|
|
902
880
|
let _abort = null;
|
|
903
881
|
const promise = new Promise((resolve) => {
|
|
904
882
|
const _startMs = Date.now();
|
|
905
883
|
const { proc, cleanupFiles, cleanupDirs } = _spawnProcess(promptText, sysPromptText, {
|
|
906
|
-
direct, label, runtime,
|
|
907
|
-
maxBudget, bare, fallbackModel, images: imageGate.images,
|
|
908
|
-
...runtimeFeatureOpts,
|
|
884
|
+
direct, label, runtime, adapterOptions: runtimeOptions,
|
|
909
885
|
});
|
|
910
886
|
let resolved = false;
|
|
911
887
|
let exitFallbackTimer = null;
|
package/engine/managed-spawn.js
CHANGED
|
@@ -302,7 +302,22 @@ function _validateSpec(spec, index, limits, opts) {
|
|
|
302
302
|
if (typeof spec.cwd === 'string' && spec.cwd.length > 500) {
|
|
303
303
|
return { ok: false, reason: 'cwd-too-long' };
|
|
304
304
|
}
|
|
305
|
-
if (
|
|
305
|
+
if (opts.allowedCwdRoot) {
|
|
306
|
+
if (typeof spec.cwd !== 'string' || spec.cwd.length === 0) {
|
|
307
|
+
return { ok: false, reason: INVALID_WORKDIR_REASON_PREFIX + 'cwd-required-for-dispatch-confinement' };
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
const cwdReal = shared.realPathForComparison(spec.cwd);
|
|
311
|
+
const rootReal = shared.realPathForComparison(opts.allowedCwdRoot);
|
|
312
|
+
if (!shared.isPathInsideOrEqual(cwdReal, rootReal)) {
|
|
313
|
+
return { ok: false, reason: INVALID_WORKDIR_REASON_PREFIX + 'cwd-outside-dispatch-worktree' };
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
return { ok: false, reason: INVALID_WORKDIR_REASON_PREFIX + 'cwd-containment-check-failed (' + err.message + ')' };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
let normalizedCwd = typeof spec.cwd === 'string' ? spec.cwd : '';
|
|
320
|
+
if (_resolveRequireGitWorkdir(opts) && normalizedCwd.length > 0) {
|
|
306
321
|
const wt = shared.isValidGitWorktree(spec.cwd, { memo: opts._gitWorktreeMemo });
|
|
307
322
|
if (!wt.ok) {
|
|
308
323
|
// W-mpbpa01y000qcdc2 — enrich the reject reason so the dispatcher prompt
|
|
@@ -319,6 +334,12 @@ function _validateSpec(spec, index, limits, opts) {
|
|
|
319
334
|
return { ok: false, reason: INVALID_WORKDIR_REASON_PREFIX + detail };
|
|
320
335
|
}
|
|
321
336
|
}
|
|
337
|
+
if (normalizedCwd) {
|
|
338
|
+
try { normalizedCwd = shared.realPathForComparison(normalizedCwd); }
|
|
339
|
+
catch (err) {
|
|
340
|
+
return { ok: false, reason: INVALID_WORKDIR_REASON_PREFIX + 'cwd-canonicalization-failed (' + err.message + ')' };
|
|
341
|
+
}
|
|
342
|
+
}
|
|
322
343
|
|
|
323
344
|
// env (optional). Validation has three independent layers:
|
|
324
345
|
// 1. Shape guard: key matches /^[A-Za-z_][A-Za-z0-9_]*$/ (argv-smuggling
|
|
@@ -339,7 +360,7 @@ function _validateSpec(spec, index, limits, opts) {
|
|
|
339
360
|
if (envKeys.length > maxEnv) {
|
|
340
361
|
return { ok: false, reason: 'env-too-many (>' + maxEnv + ')' };
|
|
341
362
|
}
|
|
342
|
-
const projectForSpec = _resolveProjectForCwd(
|
|
363
|
+
const projectForSpec = opts.project || _resolveProjectForCwd(
|
|
343
364
|
typeof spec.cwd === 'string' ? spec.cwd : '',
|
|
344
365
|
opts && Array.isArray(opts.projects) ? opts.projects : null,
|
|
345
366
|
);
|
|
@@ -411,7 +432,7 @@ function _validateSpec(spec, index, limits, opts) {
|
|
|
411
432
|
name: spec.name,
|
|
412
433
|
cmd: spec.cmd,
|
|
413
434
|
args: args,
|
|
414
|
-
cwd:
|
|
435
|
+
cwd: normalizedCwd,
|
|
415
436
|
env: env,
|
|
416
437
|
ports: ports,
|
|
417
438
|
ttl_minutes: ttlMinutes,
|
|
@@ -585,7 +606,7 @@ function buildManagedSpawnHint(opts) {
|
|
|
585
606
|
' "name": "constellation-host",',
|
|
586
607
|
' "cmd": "bun",',
|
|
587
608
|
' "args": ["run", "dev"],',
|
|
588
|
-
' "cwd": "
|
|
609
|
+
' "cwd": "<absolute path at or below MINIONS_AGENT_CWD>",',
|
|
589
610
|
' "env": { "VITE_HOST": "127.0.0.1" },',
|
|
590
611
|
' "ports": [3001],',
|
|
591
612
|
' "ttl_minutes": ' + ttl + ',',
|
|
@@ -624,6 +645,7 @@ function buildManagedSpawnHint(opts) {
|
|
|
624
645
|
'- Ports: 1024–65535, ≤ 20 per spec',
|
|
625
646
|
'- TTL: ≤ ' + maxTtl + ' minutes (hard cap), defaults to ' + defaultTtl + ' if omitted',
|
|
626
647
|
'- `attrs` serialized: ≤ 2048 bytes (opaque blob the engine surfaces to downstream agents)',
|
|
648
|
+
'- `cwd`: must resolve at or below this dispatch\'s `MINIONS_AGENT_CWD`. A worktree-mode project\'s operator checkout is rejected even though it is a valid Git checkout.',
|
|
627
649
|
'',
|
|
628
650
|
'If your file is invalid the engine marks this dispatch ERROR with `failure_class: invalid-managed-spawn` (non-retryable) — fix the file shape, don\'t retry blindly.',
|
|
629
651
|
'',
|
|
@@ -875,6 +897,9 @@ function _toStateRecord(spec, runtime, ctx) {
|
|
|
875
897
|
owner_agent: (ctx && ctx.owner_agent) || '',
|
|
876
898
|
owner_wi: (ctx && ctx.owner_wi) || '',
|
|
877
899
|
owner_project: (ctx && ctx.owner_project) || '',
|
|
900
|
+
owner_work_type: (ctx && ctx.owner_work_type) || '',
|
|
901
|
+
dispatch_root: (ctx && ctx.dispatch_root) || '',
|
|
902
|
+
checkout_mode: (ctx && ctx.checkout_mode) || '',
|
|
878
903
|
cmd: spec.cmd,
|
|
879
904
|
args: Array.isArray(spec.args) ? spec.args.slice() : [],
|
|
880
905
|
cwd: typeof spec.cwd === 'string' ? spec.cwd : '',
|
|
@@ -1265,7 +1290,69 @@ function computeManagedSpecsEtag(opts) {
|
|
|
1265
1290
|
return crypto.createHash('sha1').update(json).digest('hex').slice(0, 16);
|
|
1266
1291
|
}
|
|
1267
1292
|
|
|
1268
|
-
function
|
|
1293
|
+
function validateManagedSpecExecutionContext(record, opts) {
|
|
1294
|
+
opts = opts || {};
|
|
1295
|
+
if (!record || typeof record.cwd !== 'string' || !record.cwd) {
|
|
1296
|
+
return { ok: false, reason: 'cwd-missing' };
|
|
1297
|
+
}
|
|
1298
|
+
let projects;
|
|
1299
|
+
try {
|
|
1300
|
+
projects = Array.isArray(opts.projects) ? opts.projects : shared.getProjects(opts.config);
|
|
1301
|
+
} catch {
|
|
1302
|
+
return { ok: false, reason: 'project-config-unavailable' };
|
|
1303
|
+
}
|
|
1304
|
+
const project = record.owner_project
|
|
1305
|
+
? shared.findProjectByName(projects, record.owner_project)
|
|
1306
|
+
: null;
|
|
1307
|
+
for (const candidate of projects) {
|
|
1308
|
+
if (!candidate?.localPath) continue;
|
|
1309
|
+
const candidateMode = shared.resolveCheckoutMode(
|
|
1310
|
+
candidate,
|
|
1311
|
+
record.owner_work_type || shared.WORK_TYPE.IMPLEMENT,
|
|
1312
|
+
);
|
|
1313
|
+
if (candidateMode !== shared.CHECKOUT_MODES.WORKTREE) continue;
|
|
1314
|
+
try {
|
|
1315
|
+
if (shared.pathsOverlap(record.cwd, candidate.localPath)) {
|
|
1316
|
+
return { ok: false, reason: 'cwd-overlaps-worktree-mode-operator-checkout' };
|
|
1317
|
+
}
|
|
1318
|
+
} catch {
|
|
1319
|
+
return { ok: false, reason: 'cwd-overlap-check-failed' };
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
const mode = project
|
|
1323
|
+
? shared.resolveCheckoutMode(project, record.owner_work_type || shared.WORK_TYPE.IMPLEMENT)
|
|
1324
|
+
: shared.CHECKOUT_MODES.WORKTREE;
|
|
1325
|
+
if (project && mode === shared.CHECKOUT_MODES.LIVE) {
|
|
1326
|
+
if (record.dispatch_root && !shared.sameResolvedPath(record.dispatch_root, project.localPath)) {
|
|
1327
|
+
return { ok: false, reason: 'live-dispatch-root-mismatch' };
|
|
1328
|
+
}
|
|
1329
|
+
try {
|
|
1330
|
+
if (!shared.isPathInsideOrEqual(record.cwd, project.localPath)) {
|
|
1331
|
+
return { ok: false, reason: 'live-cwd-outside-project' };
|
|
1332
|
+
}
|
|
1333
|
+
} catch {
|
|
1334
|
+
return { ok: false, reason: 'live-cwd-unavailable' };
|
|
1335
|
+
}
|
|
1336
|
+
return { ok: true, project, mode };
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
if (!record.dispatch_root) {
|
|
1340
|
+
return record.checkout_mode
|
|
1341
|
+
? { ok: false, reason: 'dispatch-root-missing' }
|
|
1342
|
+
: { ok: true, legacy: true, project, mode };
|
|
1343
|
+
}
|
|
1344
|
+
try {
|
|
1345
|
+
shared.assertWorktreeOutsideProjects(record.dispatch_root, projects);
|
|
1346
|
+
if (!shared.isPathInsideOrEqual(record.cwd, record.dispatch_root)) {
|
|
1347
|
+
return { ok: false, reason: 'cwd-outside-dispatch-root' };
|
|
1348
|
+
}
|
|
1349
|
+
} catch {
|
|
1350
|
+
return { ok: false, reason: 'dispatch-root-inside-operator-checkout' };
|
|
1351
|
+
}
|
|
1352
|
+
return { ok: true, project, mode };
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function restartManagedSpec(name, opts) {
|
|
1269
1356
|
if (typeof name !== 'string' || name.length === 0) {
|
|
1270
1357
|
throw new Error('restartManagedSpec: name required');
|
|
1271
1358
|
}
|
|
@@ -1273,6 +1360,11 @@ function restartManagedSpec(name) {
|
|
|
1273
1360
|
if (!existing) {
|
|
1274
1361
|
throw new Error('restartManagedSpec: spec not found: ' + name);
|
|
1275
1362
|
}
|
|
1363
|
+
const executionContext = validateManagedSpecExecutionContext(existing, opts);
|
|
1364
|
+
if (!executionContext.ok) {
|
|
1365
|
+
try { removeManagedSpec(name); } catch {}
|
|
1366
|
+
throw new Error('restartManagedSpec: unsafe persisted execution context (' + executionContext.reason + ')');
|
|
1367
|
+
}
|
|
1276
1368
|
// Reconstruct a sidecar-shaped spec from the persisted state row. The
|
|
1277
1369
|
// state shape keeps cmd/args/cwd/env/ports/attrs/healthcheck verbatim
|
|
1278
1370
|
// (see _toStateRecord), so this is a direct projection.
|
|
@@ -1299,6 +1391,9 @@ function restartManagedSpec(name) {
|
|
|
1299
1391
|
owner_agent: existing.owner_agent || '',
|
|
1300
1392
|
owner_wi: existing.owner_wi || '',
|
|
1301
1393
|
owner_project: existing.owner_project || '',
|
|
1394
|
+
owner_work_type: existing.owner_work_type || '',
|
|
1395
|
+
dispatch_root: existing.dispatch_root || '',
|
|
1396
|
+
checkout_mode: existing.checkout_mode || '',
|
|
1302
1397
|
};
|
|
1303
1398
|
const runtime = spawnManagedSpec(spec, ctx);
|
|
1304
1399
|
// recordManagedSpec replaces by name (item 2 idempotency contract) and
|
|
@@ -1380,6 +1475,12 @@ function _runManagedReconcile(opts) {
|
|
|
1380
1475
|
const killBatch = typeof opts.killBatch === 'function'
|
|
1381
1476
|
? opts.killBatch
|
|
1382
1477
|
: shared.killByPidsImmediate;
|
|
1478
|
+
let projects;
|
|
1479
|
+
try {
|
|
1480
|
+
projects = Array.isArray(opts.projects) ? opts.projects : shared.getProjects(opts.config);
|
|
1481
|
+
} catch {
|
|
1482
|
+
projects = [];
|
|
1483
|
+
}
|
|
1383
1484
|
const stats = {
|
|
1384
1485
|
scanned: 0,
|
|
1385
1486
|
ttlExpired: 0,
|
|
@@ -1413,6 +1514,11 @@ function _runManagedReconcile(opts) {
|
|
|
1413
1514
|
stats.deadDropped++;
|
|
1414
1515
|
continue; // dead + not expired → drop
|
|
1415
1516
|
}
|
|
1517
|
+
const executionContext = validateManagedSpecExecutionContext(rec, { projects });
|
|
1518
|
+
if (!executionContext.ok) {
|
|
1519
|
+
ttlPidsToKill.push(rec.pid);
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1416
1522
|
kept.push(rec);
|
|
1417
1523
|
survivors.push({ name: rec.name, log_path: rec.log_path || '', healthy: rec.healthy === true, last_health_at: rec.last_health_at || 0 });
|
|
1418
1524
|
}
|
|
@@ -1550,6 +1656,7 @@ module.exports = {
|
|
|
1550
1656
|
// Item 4 (P-4b8d2e57): discovery API (etag + state-driven respawn).
|
|
1551
1657
|
computeManagedSpecsEtag: computeManagedSpecsEtag,
|
|
1552
1658
|
restartManagedSpec: restartManagedSpec,
|
|
1659
|
+
validateManagedSpecExecutionContext: validateManagedSpecExecutionContext,
|
|
1553
1660
|
// Item 7 (P-8a4d6f29): TTL sweep + boot reconcile + project cleanup.
|
|
1554
1661
|
sweepManagedSpawn: sweepManagedSpawn,
|
|
1555
1662
|
bootReconcileManagedSpawn: bootReconcileManagedSpawn,
|
package/engine/memory-store.js
CHANGED
|
@@ -269,10 +269,12 @@ function searchMemoryRecords(query, options = {}) {
|
|
|
269
269
|
}
|
|
270
270
|
const limit = Math.max(1, Math.min(200, Number(options.limit) || 50));
|
|
271
271
|
args.push(limit);
|
|
272
|
+
// CROSS JOIN pins FTS as the outer loop; Node 22's SQLite otherwise prefers
|
|
273
|
+
// the scope index and evaluates MATCH once for every scoped record.
|
|
272
274
|
const sql = `
|
|
273
275
|
SELECT m.*, bm25(memory_records_fts, 5.0, 1.0, 2.0, 3.0) AS fts_rank
|
|
274
276
|
FROM memory_records_fts
|
|
275
|
-
JOIN memory_records m ON m.rowid=memory_records_fts.rowid
|
|
277
|
+
CROSS JOIN memory_records m ON m.rowid=memory_records_fts.rowid
|
|
276
278
|
WHERE ${where.join(' AND ')}
|
|
277
279
|
ORDER BY fts_rank
|
|
278
280
|
LIMIT ?
|
package/engine/playbook.js
CHANGED
|
@@ -1214,12 +1214,10 @@ function formatPrContextSuffix(pr) {
|
|
|
1214
1214
|
// ─── Work Discovery Helpers ──────────────────────────────────────────────────
|
|
1215
1215
|
|
|
1216
1216
|
function buildBaseVars(agentId, config, project) {
|
|
1217
|
-
//
|
|
1218
|
-
//
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1221
|
-
// field when neither agent.model nor engine.defaultModel resolves —
|
|
1222
|
-
// omitting would leave a dangling `· ` in the rendered sign-off.
|
|
1217
|
+
// This is the provisional requested model used in prompt examples. The
|
|
1218
|
+
// `minions pr comment` path replaces it with runtime/session evidence from
|
|
1219
|
+
// the active dispatch before posting; `unknown` survives only when neither
|
|
1220
|
+
// runtime nor requested-model evidence exists.
|
|
1223
1221
|
const resolvedModel = shared.resolveAgentModel(
|
|
1224
1222
|
config?.agents?.[agentId] || null,
|
|
1225
1223
|
config?.engine || null
|