@yemi33/minions 0.1.2178 → 0.1.2180
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/README.md +7 -5
- package/bin/minions.js +39 -17
- package/dashboard/js/command-parser.js +1 -1
- package/dashboard/js/memory-panel.js +324 -0
- package/dashboard/js/qa.js +2 -2
- package/dashboard/js/refresh.js +19 -1
- package/dashboard/js/render-other.js +143 -2
- package/dashboard/js/render-prs.js +2 -1
- package/dashboard/js/render-schedules.js +1 -1
- package/dashboard/js/render-watches.js +1 -1
- package/dashboard/js/render-work-items.js +18 -1
- package/dashboard/js/settings.js +23 -0
- package/dashboard/pages/engine-memory-panel.html +56 -0
- package/dashboard/pages/engine.html +1 -0
- package/dashboard/pages/tools.html +8 -0
- package/dashboard/slim/js/link-pr.js +5 -5
- package/dashboard/slim/js/modals-tiles.js +44 -3
- package/dashboard/slim/js/projects.js +8 -6
- package/dashboard/slim/styles.css +20 -0
- package/dashboard-build.js +17 -2
- package/dashboard.js +693 -19
- package/docs/branch-derivation.md +13 -1
- package/docs/diagnostics-memory.md +446 -0
- package/docs/harness-propagation.md +273 -0
- package/docs/human-vs-automated.md +1 -1
- package/docs/runtime-adapters.md +5 -0
- package/engine/cli.js +24 -5
- package/engine/diagnostics-memory.js +190 -0
- package/engine/lifecycle.js +111 -1
- package/engine/preflight.js +265 -0
- package/engine/queries.js +331 -19
- package/engine/runtimes/claude.js +36 -0
- package/engine/runtimes/codex.js +19 -0
- package/engine/runtimes/copilot.js +27 -36
- package/engine/shared.js +390 -15
- package/engine/spawn-agent.js +178 -12
- package/engine/watchdog.js +6 -0
- package/engine.js +277 -4
- package/package.json +2 -2
package/engine/spawn-agent.js
CHANGED
|
@@ -55,6 +55,15 @@ function parseSpawnArgs(argv) {
|
|
|
55
55
|
let runtimeName = 'claude';
|
|
56
56
|
const opts = {};
|
|
57
57
|
const passthrough = [];
|
|
58
|
+
// P-08b62d49 — accumulating array of project-local-on-main harness dirs that
|
|
59
|
+
// engine.js wants `computeAddDirs` to surface as additional `--add-dir`
|
|
60
|
+
// entries. Multiple `--project-harness-dir <path>` flags accumulate in
|
|
61
|
+
// declaration order; the dedup/exists filter happens inside `computeAddDirs`.
|
|
62
|
+
const projectHarnessDirs = [];
|
|
63
|
+
// P-49e1c8b7 — hermetic harness opt-out (boolean). Set by engine.js when
|
|
64
|
+
// shared.resolveAgentHermeticHarness(agent, engine) is true; propagates to
|
|
65
|
+
// computeAddDirs which then returns [minionsDir] only.
|
|
66
|
+
let hermetic = false;
|
|
58
67
|
|
|
59
68
|
for (let i = 2; i < args.length; i++) {
|
|
60
69
|
const a = args[i];
|
|
@@ -77,6 +86,17 @@ function parseSpawnArgs(argv) {
|
|
|
77
86
|
case '--disable-builtin-mcps': opts.disableBuiltinMcps = true; break;
|
|
78
87
|
case '--no-custom-instructions': opts.suppressAgentsMd = true; break;
|
|
79
88
|
case '--enable-reasoning-summaries': opts.reasoningSummaries = true; break;
|
|
89
|
+
case '--project-harness-dir': {
|
|
90
|
+
const v = peek();
|
|
91
|
+
if (typeof v === 'string' && v) projectHarnessDirs.push(v);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
// P-49e1c8b7 — hermetic harness opt-out. When set, computeAddDirs in
|
|
95
|
+
// main() returns [minionsDir] only (user-scope asset dirs from
|
|
96
|
+
// runtime.getUserAssetDirs AND any forwarded --project-harness-dir
|
|
97
|
+
// entries are dropped). Engine.js gates this flag via
|
|
98
|
+
// shared.resolveAgentHermeticHarness(agent, engine).
|
|
99
|
+
case '--hermetic-harness': hermetic = true; break;
|
|
80
100
|
// LEGACY: dropped — the runtime adapter emits its own permission flag.
|
|
81
101
|
// Pre-P-2a6d9c4f engine.js still passes `--permission-mode bypassPermissions`;
|
|
82
102
|
// letting it through would duplicate the permission flag for Claude.
|
|
@@ -85,7 +105,7 @@ function parseSpawnArgs(argv) {
|
|
|
85
105
|
}
|
|
86
106
|
}
|
|
87
107
|
|
|
88
|
-
return { promptFile, sysPromptFile, runtimeName, opts, passthrough };
|
|
108
|
+
return { promptFile, sysPromptFile, runtimeName, opts, passthrough, projectHarnessDirs, hermetic };
|
|
89
109
|
}
|
|
90
110
|
|
|
91
111
|
/**
|
|
@@ -299,18 +319,39 @@ function createParentPipeForwarder(stream, outputPath, prefix = '') {
|
|
|
299
319
|
|
|
300
320
|
/**
|
|
301
321
|
* Build the `--add-dir` list passed to the runtime CLI. Pure: takes
|
|
302
|
-
* `{ runtime, minionsDir, homeDir, exists }`
|
|
303
|
-
* array of dirs the agent should be able to
|
|
322
|
+
* `{ runtime, minionsDir, homeDir, projectHarnessDirs, hermetic, exists }`
|
|
323
|
+
* and returns an ordered, deduped array of dirs the agent should be able to
|
|
324
|
+
* read from outside its worktree.
|
|
325
|
+
*
|
|
326
|
+
* Order:
|
|
327
|
+
* 1. minionsDir (always first, so playbooks/system-prompt are reachable).
|
|
328
|
+
* 2. Every existing dir from `runtime.getUserAssetDirs({ homeDir })` —
|
|
329
|
+
* user-scope skill/command/MCP roots (`~/.claude`, `~/.copilot`,
|
|
330
|
+
* `~/.agents`, …) so worktree-bound agents inherit user-installed
|
|
331
|
+
* harnesses.
|
|
332
|
+
* 3. P-08b62d49 — Every existing dir in `projectHarnessDirs` (engine.js
|
|
333
|
+
* passes the union of `queries.getProjectHarnesses(project).{skills,
|
|
334
|
+
* commands}` dirs that live in `project.localPath` but NOT under the
|
|
335
|
+
* worktree, so uncommitted `<repo>/.claude/skills/…` etc. on the main
|
|
336
|
+
* checkout are reachable from worktree dispatches). See
|
|
337
|
+
* `docs/harness-propagation.md` → "The worktree-uncommitted footgun".
|
|
304
338
|
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
339
|
+
* Non-existent dirs are dropped at every layer — Claude CLI rejects unknown
|
|
340
|
+
* `--add-dir` entries. The dedup compares resolved paths so we never emit a
|
|
341
|
+
* dir twice (e.g. when a runtime asset dir IS the minions repo in unusual
|
|
342
|
+
* setups, or when a project-harness dir duplicates a user-asset dir).
|
|
343
|
+
*
|
|
344
|
+
* P-49e1c8b7 — When `hermetic === true`, the result is exactly `[minionsDir]`:
|
|
345
|
+
* both the runtime adapter's user-asset dirs AND `projectHarnessDirs` are
|
|
346
|
+
* dropped so the spawned agent runs with a known-empty harness surface.
|
|
347
|
+
* Engine.js sets this via `shared.resolveAgentHermeticHarness(agent, engine)`
|
|
348
|
+
* and forwards `--hermetic-harness` to spawn-agent so the flag survives the
|
|
349
|
+
* subprocess hop.
|
|
310
350
|
*
|
|
311
351
|
* `exists` is injectable for tests; defaults to `fs.existsSync`.
|
|
312
352
|
*/
|
|
313
|
-
function computeAddDirs({ runtime, minionsDir, homeDir, exists = fs.existsSync } = {}) {
|
|
353
|
+
function computeAddDirs({ runtime, minionsDir, homeDir, projectHarnessDirs, hermetic, exists = fs.existsSync } = {}) {
|
|
354
|
+
if (hermetic) return [minionsDir];
|
|
314
355
|
const out = [minionsDir];
|
|
315
356
|
const seen = new Set([path.resolve(minionsDir)]);
|
|
316
357
|
const assetDirs = typeof runtime?.getUserAssetDirs === 'function'
|
|
@@ -324,9 +365,121 @@ function computeAddDirs({ runtime, minionsDir, homeDir, exists = fs.existsSync }
|
|
|
324
365
|
out.push(d);
|
|
325
366
|
seen.add(resolved);
|
|
326
367
|
}
|
|
368
|
+
if (Array.isArray(projectHarnessDirs)) {
|
|
369
|
+
for (const d of projectHarnessDirs) {
|
|
370
|
+
if (!d) continue;
|
|
371
|
+
const resolved = path.resolve(d);
|
|
372
|
+
if (seen.has(resolved)) continue;
|
|
373
|
+
if (!exists(d)) continue;
|
|
374
|
+
out.push(d);
|
|
375
|
+
seen.add(resolved);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
327
378
|
return out;
|
|
328
379
|
}
|
|
329
380
|
|
|
381
|
+
/**
|
|
382
|
+
* P-7d31a06b — Pre-approve a worktree's `.mcp.json` servers in `~/.claude.json`
|
|
383
|
+
* so Claude Code's first invocation in a fresh worktree cwd doesn't show the
|
|
384
|
+
* project-scoped "trust this MCP server?" prompt (which `--dangerously-skip-permissions`
|
|
385
|
+
* silently suppresses → the agent runs without the workspace MCPs).
|
|
386
|
+
*
|
|
387
|
+
* Writes to `projects[worktreePath].enabledMcpjsonServers` (the field Claude
|
|
388
|
+
* Code actually reads — confirmed against the live `~/.claude.json` schema:
|
|
389
|
+
* `enabledMcpjsonServers` / `disabledMcpjsonServers` are siblings of
|
|
390
|
+
* `mcpServers`, indexed by absolute project path). Notes on the PRD wording
|
|
391
|
+
* "mcpServers.approved": that's the intent label; `enabledMcpjsonServers` is
|
|
392
|
+
* the on-disk reality.
|
|
393
|
+
*
|
|
394
|
+
* Pure-ish: all I/O (fs reads, mutator) is injected for tests. Best-effort —
|
|
395
|
+
* callers MUST swallow errors and never block dispatch on this.
|
|
396
|
+
*
|
|
397
|
+
* Returns `{ wrote: boolean, reason: string, servers: string[] }`:
|
|
398
|
+
* - reason ∈ { 'ok', 'flag-off', 'not-claude', 'no-worktree',
|
|
399
|
+
* 'no-workspace-mcp', 'invalid-workspace-mcp', 'no-servers' }
|
|
400
|
+
* - servers: keys discovered in the workspace `.mcp.json` (regardless of write)
|
|
401
|
+
*
|
|
402
|
+
* Args:
|
|
403
|
+
* runtimeName — `runtime.name` from `resolveRuntime(...)`. Helper no-ops if !== 'claude'.
|
|
404
|
+
* worktreePath — absolute path to the agent's cwd (the worktree). Used as both the file location
|
|
405
|
+
* (`<worktreePath>/.mcp.json`) and the `projects[...]` key in `~/.claude.json`.
|
|
406
|
+
* homeDir — `os.homedir()`. Injected for test isolation; helper does NOT call os.homedir directly.
|
|
407
|
+
* engineConfig — config.engine object; reads `claudePreApproveWorkspaceMcps` (default true via ENGINE_DEFAULTS).
|
|
408
|
+
* mutateJsonFileLocked — injected file-locked mutator (engine/shared.js#mutateJsonFileLocked).
|
|
409
|
+
* existsSync, readFileSync — injectable fs primitives (default to node:fs).
|
|
410
|
+
*/
|
|
411
|
+
function preApproveWorkspaceMcps({
|
|
412
|
+
runtimeName,
|
|
413
|
+
worktreePath,
|
|
414
|
+
homeDir,
|
|
415
|
+
engineConfig,
|
|
416
|
+
mutateJsonFileLocked,
|
|
417
|
+
existsSync = fs.existsSync,
|
|
418
|
+
readFileSync = fs.readFileSync,
|
|
419
|
+
} = {}) {
|
|
420
|
+
if (engineConfig && engineConfig.claudePreApproveWorkspaceMcps === false) {
|
|
421
|
+
return { wrote: false, reason: 'flag-off', servers: [] };
|
|
422
|
+
}
|
|
423
|
+
if (runtimeName !== 'claude') {
|
|
424
|
+
return { wrote: false, reason: 'not-claude', servers: [] };
|
|
425
|
+
}
|
|
426
|
+
if (!worktreePath || typeof worktreePath !== 'string') {
|
|
427
|
+
return { wrote: false, reason: 'no-worktree', servers: [] };
|
|
428
|
+
}
|
|
429
|
+
const workspaceMcpPath = path.join(worktreePath, '.mcp.json');
|
|
430
|
+
if (!existsSync(workspaceMcpPath)) {
|
|
431
|
+
return { wrote: false, reason: 'no-workspace-mcp', servers: [] };
|
|
432
|
+
}
|
|
433
|
+
let workspaceMcp;
|
|
434
|
+
try {
|
|
435
|
+
workspaceMcp = JSON.parse(readFileSync(workspaceMcpPath, 'utf8'));
|
|
436
|
+
} catch {
|
|
437
|
+
return { wrote: false, reason: 'invalid-workspace-mcp', servers: [] };
|
|
438
|
+
}
|
|
439
|
+
const serversObj = workspaceMcp && typeof workspaceMcp === 'object' ? workspaceMcp.mcpServers : null;
|
|
440
|
+
const servers = serversObj && typeof serversObj === 'object' && !Array.isArray(serversObj)
|
|
441
|
+
? Object.keys(serversObj).filter((k) => typeof k === 'string' && k.length > 0)
|
|
442
|
+
: [];
|
|
443
|
+
if (servers.length === 0) {
|
|
444
|
+
return { wrote: false, reason: 'no-servers', servers: [] };
|
|
445
|
+
}
|
|
446
|
+
if (!homeDir || typeof mutateJsonFileLocked !== 'function') {
|
|
447
|
+
return { wrote: false, reason: 'no-home-or-mutator', servers };
|
|
448
|
+
}
|
|
449
|
+
const claudeJsonPath = path.join(homeDir, '.claude.json');
|
|
450
|
+
let didWrite = false;
|
|
451
|
+
mutateJsonFileLocked(claudeJsonPath, (data) => {
|
|
452
|
+
const root = data && typeof data === 'object' ? data : {};
|
|
453
|
+
if (!root.projects || typeof root.projects !== 'object') root.projects = {};
|
|
454
|
+
const projectKey = worktreePath;
|
|
455
|
+
const proj = root.projects[projectKey] && typeof root.projects[projectKey] === 'object'
|
|
456
|
+
? root.projects[projectKey]
|
|
457
|
+
: {};
|
|
458
|
+
// enabledMcpjsonServers is Claude's actual on-disk approval field
|
|
459
|
+
const enabled = Array.isArray(proj.enabledMcpjsonServers) ? proj.enabledMcpjsonServers.slice() : [];
|
|
460
|
+
const disabled = Array.isArray(proj.disabledMcpjsonServers) ? proj.disabledMcpjsonServers.slice() : [];
|
|
461
|
+
const enabledSet = new Set(enabled.filter((s) => typeof s === 'string'));
|
|
462
|
+
let mutated = false;
|
|
463
|
+
for (const name of servers) {
|
|
464
|
+
if (!enabledSet.has(name)) {
|
|
465
|
+
enabledSet.add(name);
|
|
466
|
+
mutated = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// If a server was previously explicitly disabled, lift the disabled mark when we
|
|
470
|
+
// re-approve it — otherwise Claude treats the disabled entry as authoritative.
|
|
471
|
+
const nextDisabled = disabled.filter((s) => typeof s === 'string' && !servers.includes(s));
|
|
472
|
+
if (nextDisabled.length !== disabled.length) mutated = true;
|
|
473
|
+
if (!mutated) return data;
|
|
474
|
+
proj.enabledMcpjsonServers = [...enabledSet];
|
|
475
|
+
proj.disabledMcpjsonServers = nextDisabled;
|
|
476
|
+
root.projects[projectKey] = proj;
|
|
477
|
+
didWrite = true;
|
|
478
|
+
return root;
|
|
479
|
+
}, { defaultValue: {}, skipWriteIfUnchanged: true });
|
|
480
|
+
return { wrote: didWrite, reason: 'ok', servers };
|
|
481
|
+
}
|
|
482
|
+
|
|
330
483
|
// ─── Main script execution ──────────────────────────────────────────────────
|
|
331
484
|
|
|
332
485
|
function _installHint(name, runtime) {
|
|
@@ -345,7 +498,7 @@ function main() {
|
|
|
345
498
|
console.error('Usage: node spawn-agent.js <prompt-file> <sysprompt-file> [--runtime <name>] [args...]');
|
|
346
499
|
process.exit(1);
|
|
347
500
|
}
|
|
348
|
-
const { promptFile, sysPromptFile, runtimeName, opts, passthrough } = parsed;
|
|
501
|
+
const { promptFile, sysPromptFile, runtimeName, opts, passthrough, projectHarnessDirs, hermetic } = parsed;
|
|
349
502
|
|
|
350
503
|
const env = cleanChildEnv();
|
|
351
504
|
injectAdoTokenEnvForRepoHost(env);
|
|
@@ -382,8 +535,21 @@ function main() {
|
|
|
382
535
|
// Skill discovery dirs — agents run with CWD set to an external repo
|
|
383
536
|
// worktree, so runtime-native global assets would otherwise be invisible.
|
|
384
537
|
// The adapter owns both where those assets live and how to surface them.
|
|
538
|
+
// P-08b62d49: engine.js also forwards uncommitted project-local harness
|
|
539
|
+
// dirs from the operator's main checkout via `--project-harness-dir <path>`
|
|
540
|
+
// (one per dir, accumulated in `projectHarnessDirs`) so worktree-bound
|
|
541
|
+
// agents inherit them too. See docs/harness-propagation.md.
|
|
542
|
+
// P-49e1c8b7: when engine.js passes `--hermetic-harness`, computeAddDirs
|
|
543
|
+
// returns [minionsDir] only — both user-asset dirs and project harness
|
|
544
|
+
// dirs are dropped for a known-empty harness surface.
|
|
385
545
|
const minionsDir = path.resolve(__dirname, '..');
|
|
386
|
-
const addDirs = computeAddDirs({
|
|
546
|
+
const addDirs = computeAddDirs({
|
|
547
|
+
runtime,
|
|
548
|
+
minionsDir,
|
|
549
|
+
homeDir: os.homedir(),
|
|
550
|
+
projectHarnessDirs,
|
|
551
|
+
hermetic,
|
|
552
|
+
});
|
|
387
553
|
|
|
388
554
|
let resolved;
|
|
389
555
|
try { resolved = runtime.resolveBinary({ env }); }
|
|
@@ -639,6 +805,6 @@ function main() {
|
|
|
639
805
|
});
|
|
640
806
|
}
|
|
641
807
|
|
|
642
|
-
module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, shouldInjectAdoTokenEnv, injectAdoTokenEnv, injectAdoTokenEnvForRepoHost, writeProcessExitSentinel, computeAddDirs, createParentPipeForwarder, assertStaleHeadOk };
|
|
808
|
+
module.exports = { parseSpawnArgs, buildSpawnInvocation, normalizeRuntimeExit, shouldInjectAdoTokenEnv, injectAdoTokenEnv, injectAdoTokenEnvForRepoHost, writeProcessExitSentinel, computeAddDirs, preApproveWorkspaceMcps, createParentPipeForwarder, assertStaleHeadOk };
|
|
643
809
|
|
|
644
810
|
if (require.main === module) main();
|
package/engine/watchdog.js
CHANGED
|
@@ -146,6 +146,12 @@ async function tick(opts) {
|
|
|
146
146
|
detached: true,
|
|
147
147
|
stdio: 'ignore',
|
|
148
148
|
windowsHide: true,
|
|
149
|
+
// W-mqb9y83o — suppress auto-open in the respawned dashboard. The
|
|
150
|
+
// watchdog has no user-intent signal (it fires on health failure, not
|
|
151
|
+
// user action), so the post-restart hook MUST NOT pop a browser tab.
|
|
152
|
+
// The CLI's spawnFullStackAndVerify reads this env var as a hard
|
|
153
|
+
// kill-switch via MINIONS_NO_AUTO_OPEN=1.
|
|
154
|
+
env: { ...process.env, MINIONS_NO_AUTO_OPEN: '1' },
|
|
149
155
|
});
|
|
150
156
|
if (child && typeof child.unref === 'function') child.unref();
|
|
151
157
|
logLine(minionsHome, `spawned minions ${action} pid=${child && child.pid} (detached)`);
|
package/engine.js
CHANGED
|
@@ -168,7 +168,19 @@ const { runPostCompletionHooks, updateWorkItemStatus, syncPrdItemStatus, reconci
|
|
|
168
168
|
syncPrsFromOutput, updatePrAfterReview, updatePrAfterFix, checkForLearnings, extractSkillsFromOutput,
|
|
169
169
|
updateAgentHistory, updateMetrics, createReviewFeedbackForAuthor, parseAgentOutput, syncPrdFromPrs,
|
|
170
170
|
isItemCompleted, classifyFailure: classifyFailureFallback, diagnoseEmptyOutput, processPendingRebases, resolveWorkItemPath,
|
|
171
|
-
mergeArtifactNotes, promoteCompletionArtifacts } = require('./engine/lifecycle');
|
|
171
|
+
mergeArtifactNotes, promoteCompletionArtifacts, pruneScopeMismatchDuplicatePrs } = require('./engine/lifecycle');
|
|
172
|
+
|
|
173
|
+
// ─── Diagnostics: memory + event-loop + GC sampler (P-a1b2c3d4 / P-b2c3d4e5) ─
|
|
174
|
+
|
|
175
|
+
const diagnosticsMemory = require('./engine/diagnostics-memory');
|
|
176
|
+
const DIAGNOSTICS_MEMORY_PATH = path.join(ENGINE_DIR, 'diagnostics-memory.json');
|
|
177
|
+
|
|
178
|
+
// P-e5f6a7b8 — sentinel consumed each tick; dashboard.js writes it after
|
|
179
|
+
// validating the operator confirm token. We pick up the iso the dashboard
|
|
180
|
+
// chose so the engine snapshot file lands at a predictable path the
|
|
181
|
+
// dashboard can directly poll for.
|
|
182
|
+
const DIAGNOSTICS_DIR = path.join(ENGINE_DIR, 'diagnostics');
|
|
183
|
+
const HEAP_SNAPSHOT_REQUEST_PATH = path.join(DIAGNOSTICS_DIR, 'heap-snapshot-request.json');
|
|
172
184
|
|
|
173
185
|
// ─── Agent Spawner ──────────────────────────────────────────────────────────
|
|
174
186
|
|
|
@@ -1696,9 +1708,54 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1696
1708
|
// stages now short-circuit alongside any other read-only WI (see the gate at
|
|
1697
1709
|
// `if (branchName && READ_ONLY_ROOT_TASK_TYPES.has(type))` below).
|
|
1698
1710
|
const _preBranchName = meta?.branch ? sanitizeBranch(meta.branch) : null;
|
|
1711
|
+
|
|
1712
|
+
// ── Per-WI meta.workdir override (P-714ef144) ─────────────────────────
|
|
1713
|
+
// Optional relative POSIX subpath on the work item that lands the agent
|
|
1714
|
+
// at <base>/<workdir> instead of <base>. Used for monorepo subpackage
|
|
1715
|
+
// dispatches so the runtime CLI's cwd-rooted skill discovery surfaces
|
|
1716
|
+
// only the target package's `.claude/skills/<name>/SKILL.md` files.
|
|
1717
|
+
// Validation runs at dispatch time (the dashboard also validates on
|
|
1718
|
+
// create/update — this is defense-in-depth for ad-hoc dispatches and
|
|
1719
|
+
// upgrades that re-dispatch pre-validator WIs). Containment escapes are
|
|
1720
|
+
// non-retryable: the operator must fix the WI's meta.workdir or remove
|
|
1721
|
+
// it before a retry would succeed.
|
|
1722
|
+
const _rawWorkdir = meta?.item?.meta?.workdir;
|
|
1723
|
+
const _wdValidation = shared.validateWorkItemWorkdir(_rawWorkdir);
|
|
1724
|
+
if (!_wdValidation.valid) {
|
|
1725
|
+
const _wiId = meta?.item?.id || meta?.workItemId || id;
|
|
1726
|
+
log('warn', `spawnAgent: meta.workdir validation rejected dispatch ${id} (WI ${_wiId}): ${_wdValidation.error}`);
|
|
1727
|
+
try {
|
|
1728
|
+
writeInboxAlert(`invalid-workdir-${_wiId}`, [
|
|
1729
|
+
`# Invalid meta.workdir on ${_wiId}`,
|
|
1730
|
+
``,
|
|
1731
|
+
`Dispatch \`${id}\` for agent \`${agentId}\` was rejected before spawn.`,
|
|
1732
|
+
``,
|
|
1733
|
+
`**Reason:** ${_wdValidation.error}`,
|
|
1734
|
+
``,
|
|
1735
|
+
`**Submitted value:** \`${typeof _rawWorkdir === 'string' ? _rawWorkdir : JSON.stringify(_rawWorkdir)}\``,
|
|
1736
|
+
``,
|
|
1737
|
+
`**Project:** \`${project?.name || '(unknown)'}\` (localPath: \`${project?.localPath || '(none)'}\`)`,
|
|
1738
|
+
``,
|
|
1739
|
+
`meta.workdir must be a relative POSIX subpath under the project root (or worktree, for code-mutating types). Examples: \`packages/foo\`, \`apps/dashboard\`. Absolute paths, drive-letter prefixes, \`..\` segments, and null bytes are rejected.`,
|
|
1740
|
+
``,
|
|
1741
|
+
`Fix the WI's \`meta.workdir\` field (or remove it to dispatch at the project root) and re-dispatch.`,
|
|
1742
|
+
].join('\n'));
|
|
1743
|
+
} catch (e) { log('warn', `invalid-workdir inbox alert write failed: ${e.message}`); }
|
|
1744
|
+
completeDispatch(
|
|
1745
|
+
id,
|
|
1746
|
+
DISPATCH_RESULT.ERROR,
|
|
1747
|
+
_wdValidation.error.slice(0, 800),
|
|
1748
|
+
'meta.workdir validation rejected this dispatch — fix the WI subpath or remove it before re-dispatch.',
|
|
1749
|
+
{ failureClass: FAILURE_CLASS.INVALID_WORKDIR, agentRetryable: false },
|
|
1750
|
+
);
|
|
1751
|
+
cleanupTempAgent(agentId);
|
|
1752
|
+
return null;
|
|
1753
|
+
}
|
|
1754
|
+
const validatedWorkdir = _wdValidation.value; // null when unset / empty
|
|
1755
|
+
|
|
1699
1756
|
let cwd, worktreeRootDir, liveMode = false;
|
|
1700
1757
|
try {
|
|
1701
|
-
({ cwd, worktreeRootDir, liveMode = false } = shared.resolveSpawnPaths(project, type, MINIONS_DIR));
|
|
1758
|
+
({ cwd, worktreeRootDir, liveMode = false } = shared.resolveSpawnPaths(project, type, MINIONS_DIR, { workdir: validatedWorkdir }));
|
|
1702
1759
|
} catch (rootErr) {
|
|
1703
1760
|
if (rootErr?.code === 'WORKTREE_ROOTDIR_COLLAPSED_TO_DRIVE_ROOT' || rootErr?.code === 'WORKTREE_ROOTDIR_MISSING_BASE') {
|
|
1704
1761
|
log('error', `spawnAgent: project rootDir resolution failed for ${id}: ${rootErr.message}`);
|
|
@@ -1713,6 +1770,38 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
1713
1770
|
cleanupTempAgent(agentId);
|
|
1714
1771
|
return null;
|
|
1715
1772
|
}
|
|
1773
|
+
if (rootErr?.code === 'INVALID_WORKDIR') {
|
|
1774
|
+
// Post-resolve containment escape from live or read-only branch (the
|
|
1775
|
+
// pre-resolve validator above already caught the common cases; this
|
|
1776
|
+
// catches symlink-style attacks where the literal subpath looks fine
|
|
1777
|
+
// but path.resolve lands outside the base).
|
|
1778
|
+
const _wiId = meta?.item?.id || meta?.workItemId || id;
|
|
1779
|
+
log('warn', `spawnAgent: workdir post-resolve escape for ${id} (WI ${_wiId}): ${rootErr.message}`);
|
|
1780
|
+
try {
|
|
1781
|
+
writeInboxAlert(`invalid-workdir-${_wiId}`, [
|
|
1782
|
+
`# Invalid meta.workdir on ${_wiId} (post-resolve escape)`,
|
|
1783
|
+
``,
|
|
1784
|
+
`Dispatch \`${id}\` for agent \`${agentId}\` was rejected by the resolver containment guard.`,
|
|
1785
|
+
``,
|
|
1786
|
+
`**Reason:** ${rootErr.message}`,
|
|
1787
|
+
``,
|
|
1788
|
+
`**Submitted value:** \`${validatedWorkdir || ''}\``,
|
|
1789
|
+
``,
|
|
1790
|
+
`**Project:** \`${project?.name || '(unknown)'}\` (localPath: \`${project?.localPath || '(none)'}\`)`,
|
|
1791
|
+
``,
|
|
1792
|
+
`The literal subpath passed shape validation but path.resolve landed outside the base directory — usually a symlink in the project root pointing elsewhere. Fix the WI's meta.workdir or remove the offending symlink before re-dispatch.`,
|
|
1793
|
+
].join('\n'));
|
|
1794
|
+
} catch (e) { log('warn', `invalid-workdir inbox alert write failed: ${e.message}`); }
|
|
1795
|
+
completeDispatch(
|
|
1796
|
+
id,
|
|
1797
|
+
DISPATCH_RESULT.ERROR,
|
|
1798
|
+
rootErr.message.slice(0, 800),
|
|
1799
|
+
'meta.workdir resolved outside the project/worktree base — fix the subpath or remove it.',
|
|
1800
|
+
{ failureClass: FAILURE_CLASS.INVALID_WORKDIR, agentRetryable: false },
|
|
1801
|
+
);
|
|
1802
|
+
cleanupTempAgent(agentId);
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1716
1805
|
throw rootErr;
|
|
1717
1806
|
}
|
|
1718
1807
|
// Legacy local alias: downstream git ops (worktree add, prune, fetch) and
|
|
@@ -3081,6 +3170,15 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3081
3170
|
// Other runtimes ignore the opt (their buildSpawnFlags don't read it), so no
|
|
3082
3171
|
// runtime.name branch here.
|
|
3083
3172
|
const resolvedDisabledMcpServers = shared.resolveCopilotAgentDisabledMcpServers(agentConfig, engineConfig);
|
|
3173
|
+
// P-49e1c8b7 — hermetic harness opt-out (per-agent override allowed).
|
|
3174
|
+
// When true, this dispatch:
|
|
3175
|
+
// - skips Claude workspace .mcp.json pre-approval (preApproveWorkspaceMcps),
|
|
3176
|
+
// - skips project-local-on-main `--project-harness-dir` propagation,
|
|
3177
|
+
// - forwards `--hermetic-harness` to spawn-agent.js so computeAddDirs
|
|
3178
|
+
// returns [minionsDir] only (user-asset dirs stripped from --add-dir).
|
|
3179
|
+
// Independent of copilotDisableBuiltinMcps and copilotSuppressAgentsMd,
|
|
3180
|
+
// which retain their existing semantics.
|
|
3181
|
+
const resolvedHermetic = shared.resolveAgentHermeticHarness(agentConfig, engineConfig);
|
|
3084
3182
|
|
|
3085
3183
|
// W-mpg6isvy000xca4d — On retry after FAILURE_CLASS.MODEL_UNAVAILABLE, swap
|
|
3086
3184
|
// to the runtime-appropriate fallback model. Two paths gated on
|
|
@@ -3140,6 +3238,39 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3140
3238
|
|
|
3141
3239
|
// MCP servers: agents inherit from ~/.claude.json directly as Claude Code processes.
|
|
3142
3240
|
// No --mcp-config needed — avoids redundant config and ensures agents always have latest servers.
|
|
3241
|
+
//
|
|
3242
|
+
// P-7d31a06b — When the runtime is Claude AND the worktree has a `.mcp.json`,
|
|
3243
|
+
// pre-warm `~/.claude.json` projects.<worktreePath>.enabledMcpjsonServers so
|
|
3244
|
+
// Claude's first call doesn't show the project-MCP trust prompt (which
|
|
3245
|
+
// --dangerously-skip-permissions silently suppresses → agent runs without the
|
|
3246
|
+
// workspace MCPs connected). Helper internally no-ops for non-Claude runtimes
|
|
3247
|
+
// and when engine.claudePreApproveWorkspaceMcps is false (no runtime.name
|
|
3248
|
+
// check at this call site, per CLAUDE.md rule). Best-effort: NEVER block dispatch.
|
|
3249
|
+
//
|
|
3250
|
+
// P-49e1c8b7 — Skipped entirely when resolvedHermetic is true: a hermetic
|
|
3251
|
+
// dispatch wants a known-empty harness surface, so the workspace .mcp.json
|
|
3252
|
+
// approval must not be written.
|
|
3253
|
+
if (resolvedHermetic) {
|
|
3254
|
+
log('debug', `Hermetic harness: skipping workspace MCP pre-approval for ${id}`);
|
|
3255
|
+
} else {
|
|
3256
|
+
try {
|
|
3257
|
+
const result = preApproveWorkspaceMcps({
|
|
3258
|
+
runtimeName,
|
|
3259
|
+
worktreePath,
|
|
3260
|
+
homeDir: os.homedir(),
|
|
3261
|
+
engineConfig,
|
|
3262
|
+
mutateJsonFileLocked: shared.mutateJsonFileLocked,
|
|
3263
|
+
});
|
|
3264
|
+
if (result.wrote) {
|
|
3265
|
+
log('info', `Pre-approved ${result.servers.length} workspace MCP server(s) in ~/.claude.json for ${worktreePath}: ${result.servers.join(', ')}`);
|
|
3266
|
+
} else if (result.reason !== 'no-workspace-mcp' && result.reason !== 'not-claude' && result.reason !== 'no-worktree') {
|
|
3267
|
+
log('debug', `Skipped workspace MCP pre-approval for ${id} (reason=${result.reason})`);
|
|
3268
|
+
}
|
|
3269
|
+
} catch (err) {
|
|
3270
|
+
log('warn', `Workspace MCP pre-approval failed for ${id} (non-fatal): ${err.message}`);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3143
3274
|
_phaseT.afterRuntime = Date.now();
|
|
3144
3275
|
|
|
3145
3276
|
log('info', `Spawning agent: ${agentId} (${id}) in ${cwd}`);
|
|
@@ -3197,7 +3328,70 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3197
3328
|
// Spawn via wrapper script — node directly (no bash intermediary)
|
|
3198
3329
|
// spawn-agent.js handles CLAUDECODE env cleanup and claude binary resolution
|
|
3199
3330
|
const spawnScript = path.join(ENGINE_DIR, 'spawn-agent.js');
|
|
3200
|
-
|
|
3331
|
+
|
|
3332
|
+
// ── P-08b62d49 — project-local-on-main harness propagation ──────────────
|
|
3333
|
+
// The worktree-uncommitted footgun (docs/harness-propagation.md): when a
|
|
3334
|
+
// mutating dispatch runs in a fresh `git worktree add` (worktreePath !==
|
|
3335
|
+
// project.localPath), uncommitted `<repo>/.claude/skills/foo/SKILL.md` etc.
|
|
3336
|
+
// on the operator's main checkout are invisible to the runtime CLI because
|
|
3337
|
+
// its native discovery is rooted at cwd. Union the project-scope harness
|
|
3338
|
+
// dirs (skills + commands) that exist under `project.localPath` but NOT
|
|
3339
|
+
// under `worktreePath`, and forward them to spawn-agent as
|
|
3340
|
+
// `--project-harness-dir <dir>` so `computeAddDirs` surfaces them via
|
|
3341
|
+
// `--add-dir`. Gated by engine.harnessPropagateProjectLocal (default true).
|
|
3342
|
+
// No-op for live-checkout mode (worktreePath === null) and read-only types
|
|
3343
|
+
// (also worktreePath === null per resolveSpawnPaths).
|
|
3344
|
+
//
|
|
3345
|
+
// P-49e1c8b7 — Also skipped entirely when resolvedHermetic is true: hermetic
|
|
3346
|
+
// dispatches want a known-empty harness surface so neither user-asset dirs
|
|
3347
|
+
// (filtered inside computeAddDirs) nor project-local-on-main dirs (filtered
|
|
3348
|
+
// here) reach the agent. We skip the queries.getProjectHarnesses(...) call
|
|
3349
|
+
// too — saves the fs scan when the result will be discarded.
|
|
3350
|
+
//
|
|
3351
|
+
// P-714ef144 — when meta.workdir is set, the filter helper clips both the
|
|
3352
|
+
// project-side and worktree-side anchors to <base>/<workdir>, so a
|
|
3353
|
+
// `packages/foo` dispatch never sees harness dirs from sibling packages.
|
|
3354
|
+
// The clipping is purely additive on top of the existing project-vs-worktree
|
|
3355
|
+
// disjoint filter; when workdir is null the helper produces the same result
|
|
3356
|
+
// as the legacy inline loop.
|
|
3357
|
+
const projectHarnessArgs = [];
|
|
3358
|
+
if (!resolvedHermetic
|
|
3359
|
+
&& engineConfig.harnessPropagateProjectLocal !== false
|
|
3360
|
+
&& worktreePath
|
|
3361
|
+
&& project?.localPath
|
|
3362
|
+
&& path.resolve(worktreePath) !== path.resolve(project.localPath)) {
|
|
3363
|
+
try {
|
|
3364
|
+
const harnesses = queries.getProjectHarnesses(project);
|
|
3365
|
+
const candidateDirs = [
|
|
3366
|
+
...(harnesses.skills || []),
|
|
3367
|
+
...(harnesses.commands || []),
|
|
3368
|
+
]
|
|
3369
|
+
.map((entry) => entry?.dir)
|
|
3370
|
+
.filter((d) => typeof d === 'string' && d);
|
|
3371
|
+
const filteredDirs = shared.filterProjectHarnessDirsForWorkdir(candidateDirs, {
|
|
3372
|
+
projectLocalPath: project.localPath,
|
|
3373
|
+
worktreePath,
|
|
3374
|
+
workdir: validatedWorkdir,
|
|
3375
|
+
});
|
|
3376
|
+
for (const abs of filteredDirs) {
|
|
3377
|
+
if (!fs.existsSync(abs)) continue;
|
|
3378
|
+
projectHarnessArgs.push('--project-harness-dir', abs);
|
|
3379
|
+
}
|
|
3380
|
+
if (projectHarnessArgs.length) {
|
|
3381
|
+
log('debug', `Project-local harness propagation: ${projectHarnessArgs.length / 2} dir(s) for ${id} (${worktreePath}${validatedWorkdir ? ', workdir=' + validatedWorkdir : ''})`);
|
|
3382
|
+
}
|
|
3383
|
+
} catch (err) {
|
|
3384
|
+
log('warn', `Project-local harness propagation failed for ${id} (non-fatal): ${err.message}`);
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
|
|
3388
|
+
// P-49e1c8b7 — hermetic harness opt-out forwarded to spawn-agent.js so
|
|
3389
|
+
// computeAddDirs returns [minionsDir] only (user-asset dirs stripped).
|
|
3390
|
+
// Append BEFORE projectHarnessArgs (which will be empty when hermetic, but
|
|
3391
|
+
// ordering keeps the intent explicit in process listings).
|
|
3392
|
+
const hermeticArgs = resolvedHermetic ? ['--hermetic-harness'] : [];
|
|
3393
|
+
|
|
3394
|
+
const spawnArgs = [spawnScript, promptPath, sysPromptPath, ...args, ...hermeticArgs, ...projectHarnessArgs];
|
|
3201
3395
|
|
|
3202
3396
|
// Live output file — stamped BEFORE child process is spawned (#W-mo248lkjwgsu).
|
|
3203
3397
|
// Writing the stub pre-spawn lets the orphan detector distinguish three failure modes
|
|
@@ -3584,7 +3778,12 @@ async function spawnAgent(dispatchItem, config) {
|
|
|
3584
3778
|
let resumeProc;
|
|
3585
3779
|
try {
|
|
3586
3780
|
// detached so the resumed steering session also survives engine death (matches initial spawn)
|
|
3587
|
-
|
|
3781
|
+
// P-08b62d49 — also include projectHarnessArgs so the resumed runtime
|
|
3782
|
+
// can still reach uncommitted project-local harness dirs (the runtime
|
|
3783
|
+
// CLI re-indexes asset dirs on every spawn, including this resume).
|
|
3784
|
+
// P-49e1c8b7 — same for --hermetic-harness so the resumed dispatch
|
|
3785
|
+
// keeps the known-empty harness contract.
|
|
3786
|
+
resumeProc = runFile(process.execPath, [spawnScript, steerPromptPath, sysPromptPath, ...resumeArgs, ...hermeticArgs, ...projectHarnessArgs], {
|
|
3588
3787
|
cwd,
|
|
3589
3788
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
3590
3789
|
env: childEnv,
|
|
@@ -8670,6 +8869,20 @@ async function tickInner() {
|
|
|
8670
8869
|
}
|
|
8671
8870
|
if (reconcilePolls.length) await Promise.allSettled(reconcilePolls);
|
|
8672
8871
|
if (_isTickStale(myGeneration)) return;
|
|
8872
|
+
|
|
8873
|
+
// W-mqba5ulq000nd255 — Cross-project PR scope-mismatch sweep. Cleans up
|
|
8874
|
+
// stale records that were stamped with `_invalidProjectScope` and have a
|
|
8875
|
+
// sibling in the correctly-scoped project. Cheap (a single read of all
|
|
8876
|
+
// PRs + a per-affected-project mutation). Runs once per reconcile tick;
|
|
8877
|
+
// sibling-less stamps are preserved as tracking signals.
|
|
8878
|
+
try {
|
|
8879
|
+
const result = pruneScopeMismatchDuplicatePrs(config);
|
|
8880
|
+
if (result?.pruned > 0) {
|
|
8881
|
+
log('info', `[pull-requests] scope-mismatch sweep pruned ${result.pruned} duplicate record(s) across ${result.scanned} scanned`);
|
|
8882
|
+
}
|
|
8883
|
+
} catch (err) {
|
|
8884
|
+
log('warn', `[pull-requests] scope-mismatch sweep error: ${err?.message || err}`);
|
|
8885
|
+
}
|
|
8673
8886
|
}
|
|
8674
8887
|
|
|
8675
8888
|
// 2.9. Stalled dispatch detection — auto-retry failed items blocking the graph (~20 min — cadence in ENGINE_DEFAULTS.stalledDispatchSweepEvery)
|
|
@@ -9209,6 +9422,64 @@ async function tickInner() {
|
|
|
9209
9422
|
if (!discoveryOk) {
|
|
9210
9423
|
log('warn', 'Discovery failed after pending dispatch pass — new work may be stale until next tick');
|
|
9211
9424
|
}
|
|
9425
|
+
|
|
9426
|
+
// 6. Periodic memory baseline (P-b2c3d4e5).
|
|
9427
|
+
// Sample once every ENGINE_DEFAULTS.memoryBaselineEveryTicks ticks (default
|
|
9428
|
+
// 6 ≈ 60s at the default 10s tickInterval). When the cadence is <= 0,
|
|
9429
|
+
// sampling is disabled cleanly — no log emission, no sidecar write
|
|
9430
|
+
// (operator opt-out). The diagnostics-memory.json sidecar is a passive
|
|
9431
|
+
// cache (single-object latest sample) — gitignored and exempt from the
|
|
9432
|
+
// SQL-first state rule, same as engine/dashboard-port.json.
|
|
9433
|
+
safe('memoryBaseline', () => emitMemoryBaseline(tickCount));
|
|
9434
|
+
|
|
9435
|
+
// 7. Operator-driven heap snapshot capture (P-e5f6a7b8).
|
|
9436
|
+
// Dashboard's POST /api/diagnostics/heap-snapshot drops a sentinel in
|
|
9437
|
+
// engine/diagnostics/heap-snapshot-request.json after capturing its own
|
|
9438
|
+
// heap. We pick it up on the next tick, write the engine-side
|
|
9439
|
+
// .heapsnapshot, then remove the sentinel so the dashboard handler can
|
|
9440
|
+
// detect completion. The v8.writeHeapSnapshot call stalls THIS process
|
|
9441
|
+
// for several seconds — the dashboard polls with a 30s timeout.
|
|
9442
|
+
safe('heapSnapshotRequest', () => processHeapSnapshotRequest());
|
|
9443
|
+
}
|
|
9444
|
+
|
|
9445
|
+
function processHeapSnapshotRequest() {
|
|
9446
|
+
if (!fs.existsSync(HEAP_SNAPSHOT_REQUEST_PATH)) return;
|
|
9447
|
+
let req = null;
|
|
9448
|
+
try {
|
|
9449
|
+
req = JSON.parse(fs.readFileSync(HEAP_SNAPSHOT_REQUEST_PATH, 'utf8'));
|
|
9450
|
+
} catch (e) {
|
|
9451
|
+
log('warn', `heap-snapshot-request parse failed: ${e.message}`);
|
|
9452
|
+
try { safeUnlink(HEAP_SNAPSHOT_REQUEST_PATH); } catch { /* best effort */ }
|
|
9453
|
+
return;
|
|
9454
|
+
}
|
|
9455
|
+
const iso = (req && typeof req.requestedAt === 'string') ? req.requestedAt : new Date().toISOString();
|
|
9456
|
+
const safeIso = String(iso).replace(/[:.]/g, '-');
|
|
9457
|
+
const outPath = path.join(DIAGNOSTICS_DIR, `heap-engine-${safeIso}.heapsnapshot`);
|
|
9458
|
+
const v8 = require('v8');
|
|
9459
|
+
try {
|
|
9460
|
+
fs.mkdirSync(DIAGNOSTICS_DIR, { recursive: true });
|
|
9461
|
+
log('info', `HEAP_SNAPSHOT engine capturing -> ${outPath} (this stalls the engine for several seconds)`);
|
|
9462
|
+
v8.writeHeapSnapshot(outPath);
|
|
9463
|
+
log('info', `HEAP_SNAPSHOT engine wrote ${outPath}`);
|
|
9464
|
+
} catch (e) {
|
|
9465
|
+
log('warn', `heap-snapshot writeHeapSnapshot failed: ${e.message}`);
|
|
9466
|
+
} finally {
|
|
9467
|
+
try { safeUnlink(HEAP_SNAPSHOT_REQUEST_PATH); } catch { /* best effort */ }
|
|
9468
|
+
}
|
|
9469
|
+
}
|
|
9470
|
+
|
|
9471
|
+
function emitMemoryBaseline(tickN) {
|
|
9472
|
+
const every = Number(ENGINE_DEFAULTS.memoryBaselineEveryTicks);
|
|
9473
|
+
if (!Number.isFinite(every) || every <= 0) return;
|
|
9474
|
+
if ((tickN % every) !== 0) return;
|
|
9475
|
+
const sample = diagnosticsMemory.sampleSelf({ label: 'engine' });
|
|
9476
|
+
diagnosticsMemory.recordSample(sample);
|
|
9477
|
+
try { safeWrite(DIAGNOSTICS_MEMORY_PATH, sample); }
|
|
9478
|
+
catch (e) { log('warn', `memoryBaseline sidecar write: ${e.message}`); }
|
|
9479
|
+
log('info',
|
|
9480
|
+
`MEMORY_BASELINE engine rss=${sample.rss} heapUsed=${sample.heapUsed} ` +
|
|
9481
|
+
`eventLoopLagP99=${sample.eventLoopLagP99.toFixed(2)}ms ` +
|
|
9482
|
+
`gcPauses=${sample.gcCount}/${sample.gcPausesTotalMs.toFixed(2)}ms tickN=${tickN}`);
|
|
9212
9483
|
}
|
|
9213
9484
|
|
|
9214
9485
|
// ─── Exports (for engine/cli.js and other modules) ──────────────────────────
|
|
@@ -9287,6 +9558,8 @@ module.exports = {
|
|
|
9287
9558
|
resolvePreDispatchEvalConcurrency,
|
|
9288
9559
|
// P-c2e5a1d9-a — exported for testing the tick-generation force-release path
|
|
9289
9560
|
_isTickStale,
|
|
9561
|
+
// P-b2c3d4e5 — exported for testing the memory baseline emitter + sidecar path
|
|
9562
|
+
emitMemoryBaseline, DIAGNOSTICS_MEMORY_PATH,
|
|
9290
9563
|
get tickGeneration() { return tickGeneration; },
|
|
9291
9564
|
set tickGeneration(v) { tickGeneration = v; },
|
|
9292
9565
|
get tickRunning() { return tickRunning; },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2180",
|
|
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"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"test:e2e:report": "npx playwright show-report test/playwright/report",
|
|
26
26
|
"test:e2e:video": "npx playwright test --video=on --headed",
|
|
27
27
|
"test:all": "node test/run-parallel.js && node test/minions-tests.js && node test/integration/run.js",
|
|
28
|
-
"test:perf": "node test/perf/
|
|
28
|
+
"test:perf": "node test/perf/run.js",
|
|
29
29
|
"test:e2e:accept": "node test/playwright/accept-baseline.js",
|
|
30
30
|
"test:e2e:accept-force": "node test/playwright/accept-baseline.js --force",
|
|
31
31
|
"test:setup": "npx playwright install chromium",
|