@yemi33/minions 0.1.2383 → 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/dashboard/js/refresh.js +2 -1
- package/dashboard/js/settings.js +1 -1
- package/dashboard.js +11 -7
- 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 +7 -4
- package/docs/worktree-lifecycle.md +22 -0
- package/engine/acp-transport.js +216 -29
- package/engine/agent-worker-pool.js +268 -51
- package/engine/cc-worker-pool.js +8 -1
- package/engine/cleanup.js +15 -11
- package/engine/cli.js +47 -27
- package/engine/create-pr-worktree.js +57 -79
- package/engine/keep-process-sweep.js +131 -9
- package/engine/lifecycle.js +1 -1
- package/engine/live-checkout.js +4 -2
- package/engine/managed-spawn.js +112 -5
- package/engine/pooled-agent-process.js +486 -21
- package/engine/pr-action.js +6 -8
- package/engine/process-utils.js +154 -18
- package/engine/runtimes/copilot.js +22 -4
- package/engine/shared.js +254 -61
- package/engine/spawn-agent.js +6 -3
- package/engine/timeout.js +14 -10
- package/engine/worktree-gc.js +55 -46
- package/engine.js +237 -134
- package/package.json +1 -1
- package/playbooks/shared-rules.md +2 -2
- package/prompts/cc-system.md +11 -11
package/engine/process-utils.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
|
-
const { execSync: _execSync } = require('child_process');
|
|
17
|
+
const { execSync: _execSync, execFile: _execFile } = require('child_process');
|
|
18
18
|
|
|
19
19
|
// Shared.js-local PID liveness check, moved here unchanged. Avoids a circular
|
|
20
20
|
// require on engine/cli.js (which has its own isPidAlive) and engine/timeout.js
|
|
@@ -24,7 +24,12 @@ const { execSync: _execSync } = require('child_process');
|
|
|
24
24
|
function isPidAlive(pid) {
|
|
25
25
|
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
26
26
|
try { process.kill(pid, 0); return true; }
|
|
27
|
-
catch {
|
|
27
|
+
catch (err) {
|
|
28
|
+
// Signal 0 returning EPERM means the process exists but this caller cannot
|
|
29
|
+
// inspect it. Treating that as dead lets lock recovery delete a live
|
|
30
|
+
// holder's lock and breaks cross-process mutual exclusion.
|
|
31
|
+
return err && err.code === 'EPERM';
|
|
32
|
+
}
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
// ─── Cross-Platform Process Kill Helpers ─────────────────────────────────────
|
|
@@ -206,6 +211,7 @@ function _parseWmicCsv(text) {
|
|
|
206
211
|
const ppidIdx = idx('ParentProcessId');
|
|
207
212
|
const pidIdx = idx('ProcessId');
|
|
208
213
|
const cmdIdx = idx('CommandLine');
|
|
214
|
+
const creationIdx = idx('CreationDate');
|
|
209
215
|
if (nameIdx < 0 || ppidIdx < 0 || pidIdx < 0) return [];
|
|
210
216
|
const out = [];
|
|
211
217
|
for (let i = 1; i < lines.length; i++) {
|
|
@@ -218,28 +224,31 @@ function _parseWmicCsv(text) {
|
|
|
218
224
|
const ppid = parseInt(cols[ppidIdx], 10);
|
|
219
225
|
const name = cols[nameIdx] || '';
|
|
220
226
|
const cmd = cmdIdx >= 0 ? cols.slice(cmdIdx).join(',') : '';
|
|
221
|
-
|
|
227
|
+
const created = creationIdx >= 0
|
|
228
|
+
? String(cols[creationIdx] || '').match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d{3})\d{3}([+-]\d{3})$/)
|
|
229
|
+
: null;
|
|
230
|
+
const startedAt = created
|
|
231
|
+
? Date.UTC(
|
|
232
|
+
Number(created[1]), Number(created[2]) - 1, Number(created[3]),
|
|
233
|
+
Number(created[4]), Number(created[5]), Number(created[6]), Number(created[7])
|
|
234
|
+
) - Number(created[8]) * 60000
|
|
235
|
+
: 0;
|
|
236
|
+
out.push({ pid, ppid: Number.isInteger(ppid) ? ppid : 0, name, cmd, startedAt });
|
|
222
237
|
}
|
|
223
238
|
return out;
|
|
224
239
|
}
|
|
225
240
|
|
|
241
|
+
const PROCESS_LIST_PS_SCRIPT = "Get-CimInstance Win32_Process | ForEach-Object { [PSCustomObject]@{ Name = $_.Name; ParentProcessId = $_.ParentProcessId; ProcessId = $_.ProcessId; CommandLine = $_.CommandLine; StartedAt = if ($_.CreationDate) { [int64](([datetimeoffset]$_.CreationDate).ToUnixTimeMilliseconds()) } else { 0 } } } | ConvertTo-Json -Compress -Depth 2";
|
|
242
|
+
|
|
226
243
|
// PowerShell Get-CimInstance is the modern path (wmic is removed on Win11
|
|
227
244
|
// 24H2+). We try it first and fall back to wmic for older Windows hosts.
|
|
228
245
|
function _psListProcesses() {
|
|
229
|
-
const script = "Get-CimInstance Win32_Process | Select-Object Name,ParentProcessId,ProcessId,CommandLine | ConvertTo-Json -Compress -Depth 2";
|
|
230
246
|
try {
|
|
231
247
|
const out = _execSync(
|
|
232
|
-
`powershell -NoProfile -NonInteractive -Command "${
|
|
248
|
+
`powershell -NoProfile -NonInteractive -Command "${PROCESS_LIST_PS_SCRIPT}"`,
|
|
233
249
|
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 4000, windowsHide: true, maxBuffer: 4 * 1024 * 1024 }
|
|
234
250
|
);
|
|
235
|
-
|
|
236
|
-
const arr = Array.isArray(parsed) ? parsed : [parsed];
|
|
237
|
-
return arr.map(p => ({
|
|
238
|
-
pid: Number(p.ProcessId),
|
|
239
|
-
ppid: Number(p.ParentProcessId) || 0,
|
|
240
|
-
name: p.Name || '',
|
|
241
|
-
cmd: p.CommandLine || '',
|
|
242
|
-
})).filter(p => Number.isInteger(p.pid) && p.pid > 0);
|
|
251
|
+
return _parsePowerShellProcesses(out);
|
|
243
252
|
} catch { return null; }
|
|
244
253
|
}
|
|
245
254
|
|
|
@@ -261,11 +270,7 @@ function _unixListProcesses() {
|
|
|
261
270
|
'ps -A -o pid=,ppid=,comm=',
|
|
262
271
|
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }
|
|
263
272
|
);
|
|
264
|
-
return out
|
|
265
|
-
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
266
|
-
if (!m) return null;
|
|
267
|
-
return { pid: parseInt(m[1], 10), ppid: parseInt(m[2], 10), name: m[3], cmd: '' };
|
|
268
|
-
}).filter(Boolean);
|
|
273
|
+
return _parseUnixProcesses(out);
|
|
269
274
|
} catch { return []; }
|
|
270
275
|
}
|
|
271
276
|
|
|
@@ -273,6 +278,103 @@ function listAllProcesses() {
|
|
|
273
278
|
return process.platform === 'win32' ? _winListProcesses() : _unixListProcesses();
|
|
274
279
|
}
|
|
275
280
|
|
|
281
|
+
function _parsePowerShellProcesses(out) {
|
|
282
|
+
const parsed = JSON.parse(out);
|
|
283
|
+
const arr = Array.isArray(parsed) ? parsed : [parsed];
|
|
284
|
+
return arr.map(p => ({
|
|
285
|
+
pid: Number(p.ProcessId),
|
|
286
|
+
ppid: Number(p.ParentProcessId) || 0,
|
|
287
|
+
name: p.Name || '',
|
|
288
|
+
cmd: p.CommandLine || '',
|
|
289
|
+
startedAt: Number(p.StartedAt) || 0,
|
|
290
|
+
})).filter(p => Number.isInteger(p.pid) && p.pid > 0);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function _parseUnixProcesses(out) {
|
|
294
|
+
return out.split(/\r?\n/).map(line => {
|
|
295
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(.*)$/);
|
|
296
|
+
if (!m) return null;
|
|
297
|
+
return { pid: parseInt(m[1], 10), ppid: parseInt(m[2], 10), name: m[3], cmd: '' };
|
|
298
|
+
}).filter(Boolean);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function _parseUnixProcessesWithStart(out) {
|
|
302
|
+
return out.split(/\r?\n/).map(line => {
|
|
303
|
+
const m = line.trim().match(/^(\d+)\s+(\d+)\s+(.{24})\s+(.*)$/);
|
|
304
|
+
if (!m) return null;
|
|
305
|
+
const startedAt = Date.parse(m[3].trim());
|
|
306
|
+
return {
|
|
307
|
+
pid: parseInt(m[1], 10),
|
|
308
|
+
ppid: parseInt(m[2], 10),
|
|
309
|
+
name: m[4],
|
|
310
|
+
cmd: '',
|
|
311
|
+
startedAt: Number.isNaN(startedAt) ? 0 : startedAt,
|
|
312
|
+
};
|
|
313
|
+
}).filter(Boolean);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function _applyLinuxStartTicks(processes) {
|
|
317
|
+
if (process.platform !== 'linux') return processes;
|
|
318
|
+
await Promise.all(processes.map(async (entry) => {
|
|
319
|
+
try {
|
|
320
|
+
const stat = await fs.promises.readFile(`/proc/${entry.pid}/stat`, 'utf8');
|
|
321
|
+
const fields = stat.slice(stat.lastIndexOf(') ') + 2).trim().split(/\s+/);
|
|
322
|
+
const startTicks = Number(fields[19]);
|
|
323
|
+
entry.startedAt = Number.isFinite(startTicks) && startTicks > 0 ? startTicks : 0;
|
|
324
|
+
} catch {
|
|
325
|
+
entry.startedAt = 0;
|
|
326
|
+
}
|
|
327
|
+
}));
|
|
328
|
+
return processes;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function _execFileText(file, args, opts) {
|
|
332
|
+
return new Promise((resolve, reject) => {
|
|
333
|
+
_execFile(file, args, opts, (err, stdout) => {
|
|
334
|
+
if (err) reject(err);
|
|
335
|
+
else resolve(stdout);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function listAllProcessesAsync() {
|
|
341
|
+
if (process.platform === 'win32') {
|
|
342
|
+
try {
|
|
343
|
+
const out = await _execFileText(
|
|
344
|
+
'powershell',
|
|
345
|
+
['-NoProfile', '-NonInteractive', '-Command', PROCESS_LIST_PS_SCRIPT],
|
|
346
|
+
{ encoding: 'utf8', windowsHide: true, timeout: 4000, maxBuffer: 4 * 1024 * 1024 }
|
|
347
|
+
);
|
|
348
|
+
const parsed = _parsePowerShellProcesses(out);
|
|
349
|
+
if (parsed.length > 0) return parsed;
|
|
350
|
+
} catch { /* fall through to wmic */ }
|
|
351
|
+
try {
|
|
352
|
+
const out = await _execFileText(
|
|
353
|
+
'wmic',
|
|
354
|
+
['process', 'get', 'CreationDate,Name,ParentProcessId,ProcessId', '/format:csv'],
|
|
355
|
+
{ encoding: 'utf8', windowsHide: true, timeout: 4000, maxBuffer: 4 * 1024 * 1024 }
|
|
356
|
+
);
|
|
357
|
+
return _parseWmicCsv(out);
|
|
358
|
+
} catch {
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
const out = await _execFileText(
|
|
364
|
+
'ps',
|
|
365
|
+
['-A', '-o', 'pid=,ppid=,lstart=,comm='],
|
|
366
|
+
{
|
|
367
|
+
encoding: 'utf8',
|
|
368
|
+
timeout: 5000,
|
|
369
|
+
env: { ...process.env, LC_ALL: 'C', LANG: 'C' },
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
return await _applyLinuxStartTicks(_parseUnixProcessesWithStart(out));
|
|
373
|
+
} catch {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
276
378
|
// W-mq6f2fe0000557fa — orphan-worktree GC: identify OS processes whose cwd
|
|
277
379
|
// is at or inside `dir`. Cross-platform with a fail-open contract: on any
|
|
278
380
|
// error or timeout we return [] (the caller continues with its existing
|
|
@@ -315,6 +417,7 @@ function findProcessesWithCwdInside(dir, opts = {}) {
|
|
|
315
417
|
if (process.platform === 'win32') {
|
|
316
418
|
return _findWindowsProcessesWithCwdInside(resolved, timeoutMs, now);
|
|
317
419
|
}
|
|
420
|
+
|
|
318
421
|
if (process.platform === 'linux') {
|
|
319
422
|
return _findLinuxProcessesWithCwdInside(resolved, timeoutMs, now);
|
|
320
423
|
}
|
|
@@ -322,6 +425,37 @@ function findProcessesWithCwdInside(dir, opts = {}) {
|
|
|
322
425
|
} catch { return []; }
|
|
323
426
|
}
|
|
324
427
|
|
|
428
|
+
async function findProcessesWithCwdInsideAsync(dir, opts = {}) {
|
|
429
|
+
const timeoutMs = Number(opts.timeoutMs) > 0 ? Number(opts.timeoutMs) : 5000;
|
|
430
|
+
const script = [
|
|
431
|
+
'const utils = require(process.argv[1]);',
|
|
432
|
+
'(async () => {',
|
|
433
|
+
'const timeoutMs = Number(process.argv[3]);',
|
|
434
|
+
"const rows = process.platform === 'win32'",
|
|
435
|
+
'? utils.findProcessCwdHolders(process.argv[2], { timeoutMs })',
|
|
436
|
+
': utils.findProcessesWithCwdInside(process.argv[2], { timeoutMs });',
|
|
437
|
+
'const processes = await utils.listAllProcessesAsync();',
|
|
438
|
+
'const byPid = new Map(processes.map((entry) => [Number(entry.pid), entry]));',
|
|
439
|
+
'const enriched = rows.map((entry) => ({',
|
|
440
|
+
'...entry, startedAt: Number(byPid.get(Number(entry.pid))?.startedAt) || 0,',
|
|
441
|
+
'})).filter((entry) => entry.startedAt > 0);',
|
|
442
|
+
'process.stdout.write(JSON.stringify(enriched));',
|
|
443
|
+
'})().catch((err) => { console.error(err.message); process.exitCode = 1; });',
|
|
444
|
+
].join('');
|
|
445
|
+
const out = await _execFileText(
|
|
446
|
+
process.execPath,
|
|
447
|
+
['-e', script, __filename, String(dir || ''), String(timeoutMs)],
|
|
448
|
+
{
|
|
449
|
+
encoding: 'utf8',
|
|
450
|
+
timeout: timeoutMs + 2000,
|
|
451
|
+
windowsHide: true,
|
|
452
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
const parsed = JSON.parse(out || '[]');
|
|
456
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
457
|
+
}
|
|
458
|
+
|
|
325
459
|
function _findWindowsProcessesWithCwdInside(resolved, timeoutMs, now) {
|
|
326
460
|
// Win32_Process exposes CommandLine but not the cwd. Match by basename of
|
|
327
461
|
// the worktree dir (the dispatch id like W-mq1k8z6o003acd89) appearing in
|
|
@@ -695,7 +829,9 @@ module.exports = {
|
|
|
695
829
|
_winListProcesses,
|
|
696
830
|
_unixListProcesses,
|
|
697
831
|
listAllProcesses,
|
|
832
|
+
listAllProcessesAsync,
|
|
698
833
|
findProcessesWithCwdInside,
|
|
834
|
+
findProcessesWithCwdInsideAsync,
|
|
699
835
|
_findWindowsProcessesWithCwdInside,
|
|
700
836
|
_findLinuxProcessesWithCwdInside,
|
|
701
837
|
_findMacProcessesWithCwdInside,
|
|
@@ -32,7 +32,7 @@ const https = require('https');
|
|
|
32
32
|
const os = require('os');
|
|
33
33
|
const path = require('path');
|
|
34
34
|
const { execSync } = require('child_process');
|
|
35
|
-
const { FAILURE_CLASS, safeWrite, ts, resolveEngineCacheDir } = require('../shared');
|
|
35
|
+
const { ENGINE_DEFAULTS, FAILURE_CLASS, safeWrite, ts, resolveEngineCacheDir } = require('../shared');
|
|
36
36
|
|
|
37
37
|
const ENGINE_DIR = __dirname.replace(/[\\/]runtimes$/, '');
|
|
38
38
|
const _CACHE_DIR = resolveEngineCacheDir(ENGINE_DIR);
|
|
@@ -568,7 +568,8 @@ function resolveInvocationOptions({
|
|
|
568
568
|
const resolved = { ...options };
|
|
569
569
|
if (resolved.stream == null) resolved.stream = engineConfig.copilotStreamMode;
|
|
570
570
|
if (resolved.disableBuiltinMcps == null) {
|
|
571
|
-
resolved.disableBuiltinMcps = engineConfig.copilotDisableBuiltinMcps
|
|
571
|
+
resolved.disableBuiltinMcps = engineConfig.copilotDisableBuiltinMcps
|
|
572
|
+
?? ENGINE_DEFAULTS.copilotDisableBuiltinMcps;
|
|
572
573
|
}
|
|
573
574
|
if (resolved.reasoningSummaries == null) {
|
|
574
575
|
resolved.reasoningSummaries = engineConfig.copilotReasoningSummaries;
|
|
@@ -576,6 +577,11 @@ function resolveInvocationOptions({
|
|
|
576
577
|
if (failureClass === FAILURE_CLASS.MODEL_UNAVAILABLE && engineConfig.copilotFallbackModel) {
|
|
577
578
|
resolved.model = engineConfig.copilotFallbackModel;
|
|
578
579
|
}
|
|
580
|
+
if (Array.isArray(resolved.disabledMcpServers)) {
|
|
581
|
+
resolved.disabledMcpServers = [...new Set(
|
|
582
|
+
resolved.disabledMcpServers.filter(Boolean).map(String)
|
|
583
|
+
)].sort();
|
|
584
|
+
}
|
|
579
585
|
return resolved;
|
|
580
586
|
}
|
|
581
587
|
|
|
@@ -588,8 +594,20 @@ function shouldSuppressPostMutationError({ mutationApplied, error } = {}) {
|
|
|
588
594
|
return error.errorClass === 'unknown-model' || error.errorClass === 'model-unavailable';
|
|
589
595
|
}
|
|
590
596
|
|
|
591
|
-
function buildWorkerArgs() {
|
|
592
|
-
|
|
597
|
+
function buildWorkerArgs(options = {}) {
|
|
598
|
+
const maxTurns = Number(options.maxTurns);
|
|
599
|
+
const args = [
|
|
600
|
+
'--acp',
|
|
601
|
+
'--allow-all',
|
|
602
|
+
'--max-autopilot-continues',
|
|
603
|
+
String(Number.isFinite(maxTurns) && maxTurns > 0 ? Math.floor(maxTurns) : 1),
|
|
604
|
+
];
|
|
605
|
+
if (options.disableBuiltinMcps === true) args.push('--disable-builtin-mcps');
|
|
606
|
+
if (options.reasoningSummaries === true) args.push('--enable-reasoning-summaries');
|
|
607
|
+
for (const name of options.disabledMcpServers || []) {
|
|
608
|
+
if (name) args.push('--disable-mcp-server', String(name));
|
|
609
|
+
}
|
|
610
|
+
return args;
|
|
593
611
|
}
|
|
594
612
|
|
|
595
613
|
function encodePooledOutput(event = {}) {
|