mixdog 0.9.37 → 0.9.39

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.
Files changed (127) hide show
  1. package/package.json +9 -4
  2. package/scripts/abort-recovery-test.mjs +43 -2
  3. package/scripts/agent-tag-reuse-smoke.mjs +146 -6
  4. package/scripts/agent-terminal-reap-test.mjs +127 -0
  5. package/scripts/agent-trace-io-test.mjs +69 -0
  6. package/scripts/code-graph-disk-hit-test.mjs +224 -0
  7. package/scripts/dispatch-persist-recovery-test.mjs +141 -0
  8. package/scripts/execution-completion-dedup-test.mjs +157 -0
  9. package/scripts/execution-pending-resume-kick-test.mjs +57 -2
  10. package/scripts/execution-resume-esc-integration-test.mjs +174 -0
  11. package/scripts/explore-bench.mjs +101 -8
  12. package/scripts/explore-prompt-policy-test.mjs +156 -11
  13. package/scripts/find-fuzzy-hidden-test.mjs +122 -0
  14. package/scripts/internal-comms-bench-test.mjs +226 -0
  15. package/scripts/internal-comms-bench.mjs +185 -58
  16. package/scripts/internal-comms-smoke.mjs +171 -23
  17. package/scripts/live-worker-smoke.mjs +38 -2
  18. package/scripts/memory-cycle-routing-test.mjs +111 -0
  19. package/scripts/memory-rule-contract-test.mjs +93 -0
  20. package/scripts/notify-completion-mirror-test.mjs +73 -0
  21. package/scripts/output-style-smoke.mjs +2 -2
  22. package/scripts/rg-runner-test.mjs +240 -0
  23. package/scripts/routing-corpus-test.mjs +349 -0
  24. package/scripts/routing-corpus.mjs +211 -32
  25. package/scripts/session-orphan-sweep-test.mjs +83 -0
  26. package/scripts/session-sweep.mjs +266 -0
  27. package/scripts/steering-drain-buckets-test.mjs +179 -0
  28. package/scripts/tool-smoke.mjs +21 -13
  29. package/scripts/tool-tui-presentation-test.mjs +202 -0
  30. package/src/agents/heavy-worker/AGENT.md +10 -7
  31. package/src/agents/reviewer/AGENT.md +6 -4
  32. package/src/agents/worker/AGENT.md +7 -5
  33. package/src/rules/agent/00-common.md +4 -4
  34. package/src/rules/agent/00-core.md +11 -14
  35. package/src/rules/agent/20-skip-protocol.md +3 -3
  36. package/src/rules/agent/30-explorer.md +56 -48
  37. package/src/rules/agent/40-cycle1-agent.md +15 -24
  38. package/src/rules/agent/41-cycle2-agent.md +33 -57
  39. package/src/rules/agent/42-cycle3-agent.md +28 -42
  40. package/src/rules/lead/01-general.md +7 -10
  41. package/src/rules/lead/lead-brief.md +11 -14
  42. package/src/rules/lead/lead-tool.md +6 -5
  43. package/src/rules/shared/01-tool.md +44 -41
  44. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +37 -1
  45. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
  46. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +31 -8
  47. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +13 -7
  48. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
  49. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +6 -6
  50. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
  51. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +5 -4
  52. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +119 -3
  53. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
  54. package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
  55. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
  56. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +18 -52
  57. package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
  58. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
  59. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +41 -2
  60. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  61. package/src/runtime/agent/orchestrator/session/store.mjs +339 -63
  62. package/src/runtime/agent/orchestrator/stall-policy.mjs +37 -0
  63. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
  65. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
  66. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
  67. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
  68. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
  69. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
  70. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
  72. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
  73. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
  74. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
  75. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
  76. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
  77. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
  78. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
  79. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
  80. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
  81. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
  82. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
  83. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
  84. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  85. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
  86. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
  87. package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
  88. package/src/runtime/shared/tool-primitives.mjs +4 -1
  89. package/src/runtime/shared/tool-status.mjs +27 -0
  90. package/src/runtime/shared/tool-surface.mjs +6 -3
  91. package/src/session-runtime/config-helpers.mjs +14 -0
  92. package/src/session-runtime/context-status.mjs +1 -0
  93. package/src/session-runtime/effort.mjs +6 -2
  94. package/src/session-runtime/model-recency.mjs +5 -2
  95. package/src/session-runtime/provider-models.mjs +3 -3
  96. package/src/session-runtime/runtime-core.mjs +78 -10
  97. package/src/session-runtime/tool-catalog.mjs +34 -0
  98. package/src/session-runtime/warmup-schedulers.mjs +7 -1
  99. package/src/standalone/agent-tool/notify.mjs +13 -0
  100. package/src/standalone/agent-tool.mjs +45 -69
  101. package/src/standalone/explore-tool.mjs +6 -7
  102. package/src/tui/App.jsx +31 -0
  103. package/src/tui/app/model-options.mjs +5 -3
  104. package/src/tui/app/model-picker.mjs +12 -24
  105. package/src/tui/app/transcript-window.mjs +1 -0
  106. package/src/tui/components/ToolExecution.jsx +11 -6
  107. package/src/tui/components/TranscriptItem.jsx +1 -1
  108. package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
  109. package/src/tui/components/tool-execution/text-format.mjs +10 -19
  110. package/src/tui/dist/index.mjs +533 -143
  111. package/src/tui/engine/agent-job-feed.mjs +153 -16
  112. package/src/tui/engine/agent-response-tail.mjs +68 -0
  113. package/src/tui/engine/notification-plan.mjs +16 -0
  114. package/src/tui/engine/queue-helpers.mjs +8 -0
  115. package/src/tui/engine/session-api.mjs +8 -2
  116. package/src/tui/engine/session-flow.mjs +34 -2
  117. package/src/tui/engine/tool-card-results.mjs +54 -32
  118. package/src/tui/engine/tool-result-status.mjs +75 -21
  119. package/src/tui/engine/turn.mjs +83 -43
  120. package/src/tui/engine.mjs +63 -2
  121. package/src/workflows/bench/WORKFLOW.md +25 -35
  122. package/src/workflows/default/WORKFLOW.md +38 -32
  123. package/src/workflows/solo/WORKFLOW.md +19 -22
  124. package/scripts/_jitter-fuzz.mjs +0 -44410
  125. package/scripts/_jitter-fuzz2.mjs +0 -44400
  126. package/scripts/_jitter-probe.mjs +0 -44397
  127. package/scripts/_jp2.mjs +0 -45614
@@ -0,0 +1,226 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+ import test from 'node:test';
8
+
9
+ import {
10
+ CHILD_RESULT_END,
11
+ CHILD_RESULT_START,
12
+ DEFAULT_LEAD_APPROVAL_PROMPT,
13
+ buildLiveLeadDriver,
14
+ leadModeExitCode,
15
+ parseLiveLeadResult,
16
+ runLiveLeadDelegation,
17
+ validateLeadRun,
18
+ } from './internal-comms-bench.mjs';
19
+
20
+ function writeRuntime(root, managerSource) {
21
+ const runtimePath = join(root, 'runtime.mjs');
22
+ const tracePath = join(root, 'agent-trace-io.mjs');
23
+ writeFileSync(runtimePath, managerSource);
24
+ writeFileSync(tracePath, 'export async function drainAgentTrace() {}\n');
25
+ return {
26
+ runtimeUrl: pathToFileURL(runtimePath).href,
27
+ traceUrl: pathToFileURL(tracePath).href,
28
+ };
29
+ }
30
+
31
+ function writeSandboxRuntime(root, marker) {
32
+ const runtimePath = join(root, 'mixdog-session-runtime.mjs');
33
+ const tracePath = join(root, 'runtime', 'agent', 'orchestrator', 'agent-trace-io.mjs');
34
+ mkdirSync(join(root, 'runtime', 'agent', 'orchestrator'), { recursive: true });
35
+ writeFileSync(runtimePath, `
36
+ import { writeFileSync } from 'node:fs';
37
+ export async function createMixdogSessionRuntime() {
38
+ writeFileSync(process.env.RUNTIME_MARKER_PATH, ${JSON.stringify(marker)});
39
+ return {
40
+ id: 'sess_default_resolution', sessionId: 'sess_default_resolution',
41
+ async setFast() {},
42
+ toolsStatus() { return { activeTools: ['agent'] }; },
43
+ onNotification() {},
44
+ agentStatus() { return { agentJobs: [] }; },
45
+ async ask() { return { result: { text: 'done' } }; },
46
+ async close() {},
47
+ };
48
+ }
49
+ `);
50
+ writeFileSync(tracePath, 'export async function drainAgentTrace() {}\n');
51
+ }
52
+
53
+ test('lead delegation resolves default runtime and trace imports from the variant plugin root', () => {
54
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-internal-comms-bench-test-'));
55
+ const pluginA = join(root, 'plugin-A');
56
+ const pluginB = join(root, 'plugin-B');
57
+ const markerPath = join(root, 'runtime-marker');
58
+ const priorMarkerPath = process.env.RUNTIME_MARKER_PATH;
59
+ try {
60
+ process.env.RUNTIME_MARKER_PATH = markerPath;
61
+ writeSandboxRuntime(pluginA, 'A');
62
+ writeSandboxRuntime(pluginB, 'B');
63
+ for (const [pluginRoot, expected] of [[pluginA, 'A'], [pluginB, 'B']]) {
64
+ const run = runLiveLeadDelegation({
65
+ pluginRoot,
66
+ dataDir: root,
67
+ taskCwd: root,
68
+ prompt: 'Use the variant runtime.',
69
+ provider: 'test-provider',
70
+ model: 'test-model',
71
+ fast: false,
72
+ });
73
+ assert.equal(run.ok, true, run.stderr || run.stdout);
74
+ assert.equal(readFileSync(markerPath, 'utf8'), expected);
75
+ }
76
+ } finally {
77
+ if (priorMarkerPath === undefined) delete process.env.RUNTIME_MARKER_PATH;
78
+ else process.env.RUNTIME_MARKER_PATH = priorMarkerPath;
79
+ rmSync(root, { recursive: true, force: true });
80
+ }
81
+ });
82
+
83
+ test('lead driver plans before explicit approval, drains traces, and exits despite open handles', () => {
84
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-internal-comms-bench-test-'));
85
+ const promptsPath = join(root, 'prompts.jsonl');
86
+ const tracePath = join(root, 'trace-drained');
87
+ const activeToolsPath = join(root, 'active-tools.json');
88
+ try {
89
+ const runtimeUrls = writeRuntime(root, `
90
+ import { appendFileSync, writeFileSync } from 'node:fs';
91
+ let calls = 0;
92
+ let fastApplied = false;
93
+ setInterval(() => {}, 1000);
94
+ export async function createMixdogSessionRuntime() {
95
+ return {
96
+ id: 'sess_bench_test', sessionId: 'sess_bench_test',
97
+ async setFast(value) {
98
+ if (typeof value !== 'boolean') throw new Error('fast must be boolean');
99
+ fastApplied = true;
100
+ writeFileSync(process.env.FAST_PATH, JSON.stringify(value));
101
+ },
102
+ toolsStatus() {
103
+ writeFileSync(process.env.ACTIVE_TOOLS_PATH, JSON.stringify(['agent']));
104
+ return { activeTools: ['agent'] };
105
+ },
106
+ onNotification() {},
107
+ agentStatus() { return { agentJobs: [] }; },
108
+ async ask(prompt) {
109
+ if (!fastApplied) throw new Error('fast was not applied before prompting');
110
+ appendFileSync(process.env.PROMPTS_PATH, JSON.stringify(prompt) + '\\n');
111
+ calls += 1;
112
+ return { result: calls === 1 ? { text: 'plan' } : { text: 'completed {math.js:1}' } };
113
+ },
114
+ async close() { this.id = ''; this.sessionId = ''; },
115
+ };
116
+ }
117
+ `);
118
+ writeFileSync(join(root, 'agent-trace-io.mjs'), `
119
+ import { writeFileSync } from 'node:fs';
120
+ export async function drainAgentTrace() {
121
+ await new Promise((resolve) => setTimeout(resolve, 75));
122
+ writeFileSync(process.env.TRACE_PATH, 'drained');
123
+ }
124
+ `);
125
+ const driver = buildLiveLeadDriver({
126
+ ...runtimeUrls,
127
+ taskCwd: root,
128
+ prompt: 'Plan the benchmark task.',
129
+ provider: 'test-provider',
130
+ model: 'test-model',
131
+ fast: false,
132
+ });
133
+ const child = spawnSync(process.execPath, ['--input-type=module', '-e', driver], {
134
+ encoding: 'utf8',
135
+ env: { ...process.env, PROMPTS_PATH: promptsPath, ACTIVE_TOOLS_PATH: activeToolsPath, TRACE_PATH: tracePath, FAST_PATH: join(root, 'fast.json') },
136
+ timeout: 2000,
137
+ });
138
+
139
+ assert.equal(child.status, 0, child.stderr || child.stdout);
140
+ assert.deepEqual(
141
+ readFileSync(promptsPath, 'utf8').trim().split(/\r?\n/).map(JSON.parse),
142
+ ['Plan the benchmark task.', DEFAULT_LEAD_APPROVAL_PROMPT],
143
+ );
144
+ assert.deepEqual(JSON.parse(readFileSync(activeToolsPath, 'utf8')), ['agent']);
145
+ assert.equal(JSON.parse(readFileSync(join(root, 'fast.json'), 'utf8')), false);
146
+ assert.equal(readFileSync(tracePath, 'utf8'), 'drained');
147
+ assert.deepEqual(parseLiveLeadResult(child.stdout), {
148
+ text: 'completed {math.js:1}',
149
+ sessionId: 'sess_bench_test',
150
+ });
151
+ assert.match(child.stdout, new RegExp(`${CHILD_RESULT_START}.*${CHILD_RESULT_END}`));
152
+ } finally {
153
+ rmSync(root, { recursive: true, force: true });
154
+ }
155
+ });
156
+
157
+ test('lead validation rejects successful child output without worker and reviewer participation', () => {
158
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-internal-comms-bench-test-'));
159
+ try {
160
+ writeFileSync(join(root, 'math.js'), `export function mul(a, b) { return a * b; }\nexport function add(a, b) { return a + b; }\n`);
161
+ const split = {
162
+ byRole: {
163
+ lead: { usage_rows: 1 },
164
+ worker: { usage_rows: 0 },
165
+ reviewer: { usage_rows: 0 },
166
+ other: { usage_rows: 0 },
167
+ },
168
+ total: { usage_rows: 1 },
169
+ };
170
+ const validation = validateLeadRun({ taskCwd: root, run: { ok: true }, split });
171
+ assert.equal(validation.valid, false);
172
+ assert.deepEqual(validation.reasons, [
173
+ 'worker participation/usage is missing',
174
+ 'reviewer participation/usage is missing',
175
+ ]);
176
+ assert.equal(leadModeExitCode(
177
+ { A: [{ valid: false }], B: [{ valid: true }] },
178
+ { A: [], B: [split] },
179
+ ), 1);
180
+ } finally {
181
+ rmSync(root, { recursive: true, force: true });
182
+ }
183
+ });
184
+
185
+ test('child failures retain stdout and stderr diagnostics and cannot succeed', () => {
186
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-internal-comms-bench-test-'));
187
+ try {
188
+ const runtimeUrls = writeRuntime(root, `
189
+ export async function createMixdogSessionRuntime() {
190
+ return {
191
+ id: 'sess_bench_failure', sessionId: 'sess_bench_failure',
192
+ async setFast() {},
193
+ toolsStatus() { return { activeTools: ['agent'] }; },
194
+ onNotification() {},
195
+ agentStatus() { return { agentJobs: [] }; },
196
+ async ask() {
197
+ console.log('provider stdout {diagnostic}');
198
+ console.error('provider stderr {diagnostic}');
199
+ throw new Error('provider failure');
200
+ },
201
+ async close() {},
202
+ };
203
+ }
204
+ `);
205
+ const run = runLiveLeadDelegation({
206
+ pluginRoot: root,
207
+ dataDir: root,
208
+ taskCwd: root,
209
+ prompt: 'Plan the benchmark task.',
210
+ provider: 'test-provider',
211
+ model: 'test-model',
212
+ fast: false,
213
+ runtimeUrls,
214
+ });
215
+ assert.equal(run.ok, false);
216
+ assert.equal(run.sessionId, null);
217
+ assert.match(run.stdout, /provider stdout \{diagnostic\}/);
218
+ assert.match(run.stderr, /provider stderr \{diagnostic\}/);
219
+ assert.equal(leadModeExitCode(
220
+ { A: [{ valid: false }], B: [{ valid: false }] },
221
+ { A: [], B: [] },
222
+ ), 1);
223
+ } finally {
224
+ rmSync(root, { recursive: true, force: true });
225
+ }
226
+ });
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // node scripts/internal-comms-bench.mjs
9
9
  // node scripts/internal-comms-bench.mjs --run [--model grok] [--provider P] [--json]
10
- import { execFileSync } from 'node:child_process';
10
+ import { execFileSync, spawnSync } from 'node:child_process';
11
11
  import {
12
12
  copyFileSync,
13
13
  cpSync,
@@ -31,11 +31,16 @@ const HEADLESS = pathToFileURL(resolve(__dir, '../src/headless-role.mjs')).href;
31
31
  const RULE_FILES = [
32
32
  'rules/agent/00-core.md',
33
33
  'rules/agent/00-common.md',
34
+ 'rules/agent/20-skip-protocol.md',
34
35
  'agents/worker/AGENT.md',
35
36
  'agents/heavy-worker/AGENT.md',
36
37
  'agents/reviewer/AGENT.md',
37
38
  'agents/debugger/AGENT.md',
38
39
  'workflows/default/WORKFLOW.md',
40
+ 'workflows/solo/WORKFLOW.md',
41
+ 'workflows/bench/WORKFLOW.md',
42
+ 'rules/lead/01-general.md',
43
+ 'rules/lead/02-channels.md',
39
44
  'rules/lead/lead-tool.md',
40
45
  'rules/lead/lead-brief.md',
41
46
  ];
@@ -48,11 +53,12 @@ const DEFAULT_WORKER_PROMPT =
48
53
  // Identical across variants A/B; only the on-disk rule files differ.
49
54
  const DEFAULT_LEAD_PROMPT = [
50
55
  'You are the Lead in an automation benchmark. A file named math.js already exists in your working directory.',
51
- 'Do NOT edit any file yourself and do NOT call apply_patch yourself. You MUST delegate every implementation step.',
52
- 'Step 1: call the agent tool with agent "worker" and give it a brief to add an exported function add(a, b) that returns a + b to math.js, keeping the existing mul export unchanged, using apply_patch only.',
53
- 'Step 2: after the worker hands off, call the agent tool with agent "reviewer" and give it a brief to verify that math.js exports both add and mul.',
54
- 'Step 3: stop and reply with a one-line outcome plus math.js:line. Do exactly these three steps and nothing else.',
56
+ 'This is the planning turn: give a concise plan only, then wait for a later explicit approval. Do not edit, mutate state, or delegate in this turn.',
57
+ 'After approval, do NOT edit any file yourself or call apply_patch yourself. Delegate every implementation step: first a worker adds exported add(a, b) returning a + b to math.js while preserving mul, then a reviewer verifies both exports. Finally reply with a one-line outcome plus math.js:line.',
55
58
  ].join(' ');
59
+ export const DEFAULT_LEAD_APPROVAL_PROMPT = 'Proceed with the latest plan.';
60
+ export const CHILD_RESULT_START = '__INTERNAL_COMMS_BENCH_RESULT_START__';
61
+ export const CHILD_RESULT_END = '__INTERNAL_COMMS_BENCH_RESULT_END__';
56
62
 
57
63
  const MODEL_ALIASES = {
58
64
  opus: { provider: 'anthropic-oauth', model: 'claude-opus-4-8' },
@@ -383,6 +389,18 @@ function emptyRoleSplit(dataDir) {
383
389
  };
384
390
  }
385
391
 
392
+ function mathTaskCompleted(taskCwd) {
393
+ let source = '';
394
+ try {
395
+ source = readFileSync(join(taskCwd, 'math.js'), 'utf8');
396
+ } catch {
397
+ return false;
398
+ }
399
+ return source !== INITIAL_MATH_JS
400
+ && /export\s+function\s+add\s*\(\s*a\s*,\s*b\s*\)\s*\{\s*return\s+a\s*\+\s*b\s*;?\s*\}/.test(source)
401
+ && /export\s+function\s+mul\s*\(\s*a\s*,\s*b\s*\)\s*\{\s*return\s+a\s*\*\s*b\s*;?\s*\}/.test(source);
402
+ }
403
+
386
404
  function splitTokensByRole(dataDir, rootSessionId) {
387
405
  const tracePath = join(dataDir, 'history', 'agent-trace.jsonl');
388
406
  const rows = readRows(tracePath);
@@ -420,6 +438,21 @@ function splitTokensByRole(dataDir, rootSessionId) {
420
438
  return { tracePath, session_ids: [...treeIds], byRole, total };
421
439
  }
422
440
 
441
+ export function validateLeadRun({ taskCwd, run, split }) {
442
+ const reasons = [];
443
+ if (!run.ok) reasons.push('child did not return a successful benchmark result');
444
+ if (!mathTaskCompleted(taskCwd)) reasons.push('math.js does not contain the approved add and preserved mul implementation');
445
+ if ((split?.byRole?.worker?.usage_rows || 0) < 1) reasons.push('worker participation/usage is missing');
446
+ if ((split?.byRole?.reviewer?.usage_rows || 0) < 1) reasons.push('reviewer participation/usage is missing');
447
+ return { valid: reasons.length === 0, reasons };
448
+ }
449
+
450
+ export function leadModeExitCode(runsMeta, perVariant) {
451
+ const allValid = ['A', 'B'].every((variant) => runsMeta[variant].length > 0 && runsMeta[variant].every((run) => run.valid));
452
+ const anyUsage = ['A', 'B'].some((variant) => perVariant[variant].some((split) => split.total.usage_rows > 0));
453
+ return allValid && anyUsage ? 0 : 1;
454
+ }
455
+
423
456
  function median(values) {
424
457
  const arr = values.filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
425
458
  if (!arr.length) return 0;
@@ -443,54 +476,120 @@ function aggregateVariant(splits) {
443
476
  return out;
444
477
  }
445
478
 
446
- // Drives a REAL Lead session in the variant sandbox (createSession/askSession/
447
- // closeSession from the runtime manager same pattern as output-style-bench).
448
- // The variant rule files take effect through MIXDOG_ROOT; the runtime modules
449
- // themselves are imported from the real PLUGIN_ROOT (identical across variants).
450
- function runLiveLeadDelegation({ pluginRoot, dataDir, taskCwd, prompt, provider, model, effort, fast }) {
451
- const cfgUrl = pathToFileURL(join(PLUGIN_ROOT, 'runtime/agent/orchestrator/config.mjs')).href;
452
- const regUrl = pathToFileURL(join(PLUGIN_ROOT, 'runtime/agent/orchestrator/providers/registry.mjs')).href;
453
- const mgrUrl = pathToFileURL(join(PLUGIN_ROOT, 'runtime/agent/orchestrator/session/manager.mjs')).href;
454
- const driver = [
455
- `import * as cfgMod from ${JSON.stringify(cfgUrl)};`,
456
- `import * as reg from ${JSON.stringify(regUrl)};`,
457
- `import { createSession, askSession, closeSession } from ${JSON.stringify(mgrUrl)};`,
458
- `const config = cfgMod.loadConfig({ secrets: true });`,
459
- `await reg.initProviders(config.providers || {});`,
460
- `const sessionOpts = { provider: ${JSON.stringify(provider)}, model: ${JSON.stringify(model)},`,
461
- ` owner: 'cli', agent: 'lead', lane: 'cli', sourceType: 'lead', sourceName: 'internal-comms-bench-lead',`,
462
- ` cwd: ${JSON.stringify(taskCwd)}, tools: 'full', fast: ${fast ? 'true' : 'false'} };`,
463
- effort ? `sessionOpts.effort = ${JSON.stringify(effort)};` : '',
464
- `const session = createSession(sessionOpts);`,
465
- `let result;`,
466
- `try { result = await askSession(session.id, ${JSON.stringify(prompt)}, null, null, ${JSON.stringify(taskCwd)}); }`,
467
- `finally { try { closeSession(session.id, 'internal-comms-bench-lead'); } catch {} }`,
468
- `process.stdout.write(JSON.stringify({ text: String(result?.text || result?.content || '').trim(), sessionId: session.id }));`,
479
+ // Drives a REAL Lead session in the variant sandbox through the public session
480
+ // runtime, including its deferred standalone tool surface.
481
+ // The variant rule files and public runtime modules take effect through the
482
+ // sandboxed MIXDOG_ROOT.
483
+ export function buildLiveLeadDriver({
484
+ runtimeUrl,
485
+ traceUrl = pathToFileURL(join(PLUGIN_ROOT, 'runtime/agent/orchestrator/agent-trace-io.mjs')).href,
486
+ taskCwd,
487
+ prompt,
488
+ approvalPrompt = DEFAULT_LEAD_APPROVAL_PROMPT,
489
+ provider,
490
+ model,
491
+ effort,
492
+ fast,
493
+ }) {
494
+ return [
495
+ `import { createMixdogSessionRuntime } from ${JSON.stringify(runtimeUrl)};`,
496
+ `import { drainAgentTrace } from ${JSON.stringify(traceUrl)};`,
497
+ `const rt = await createMixdogSessionRuntime({ provider: ${JSON.stringify(provider)}, model: ${JSON.stringify(model)},`,
498
+ ` cwd: ${JSON.stringify(taskCwd)}, toolMode: 'full' });`,
499
+ effort ? `await rt.setEffort(${JSON.stringify(effort)});` : '',
500
+ fast !== undefined ? `await rt.setFast(${JSON.stringify(fast)});` : '',
501
+ `const prePromptTools = rt.toolsStatus?.()?.activeTools || [];`,
502
+ `if (!prePromptTools.includes('agent')) throw new Error('Lead runtime agent tool is not active');`,
503
+ `let busy = false; let kickDeferred = false; let wake = null;`,
504
+ `rt.onNotification(() => { queueMicrotask(() => { kickDeferred = true; if (!busy && wake) wake(); }); return false; });`,
505
+ `const askOnce = async (msg) => { busy = true; try { const { result } = await rt.ask(msg); return result; } finally { busy = false; } };`,
506
+ `const agentsBusy = () => { const jobs = rt.agentStatus?.()?.agentJobs || []; return jobs.some((j) => /running|pending|spawn|queued/i.test(String(j?.status || j?.state || ''))); };`,
507
+ `let result; let sessionId = '';`,
508
+ `try {`,
509
+ ` await askOnce(${JSON.stringify(prompt)});`,
510
+ ` result = await askOnce(${JSON.stringify(approvalPrompt)});`,
511
+ ` const deadline = Date.now() + 30 * 60_000;`,
512
+ ` while (Date.now() < deadline) {`,
513
+ ` if (!kickDeferred) {`,
514
+ ` if (!agentsBusy()) break;`,
515
+ ` const kicked = await new Promise((resolve) => { wake = () => resolve(true); setTimeout(() => resolve(false), 10_000); });`,
516
+ ` wake = null;`,
517
+ ` if (!kicked && !kickDeferred && !agentsBusy()) break;`,
518
+ ` }`,
519
+ ` if (!kickDeferred && !agentsBusy()) break;`,
520
+ ` kickDeferred = false; result = (await askOnce('')) || result;`,
521
+ ` }`,
522
+ ` sessionId = rt.sessionId || rt.id || rt.session?.id || '';`,
523
+ `}`,
524
+ `finally { try { await rt.close('internal-comms-bench-lead'); } catch {} }`,
525
+ `await drainAgentTrace();`,
526
+ `const benchResult = JSON.stringify({ text: String(result?.text || result?.content || '').trim(), sessionId });`,
527
+ `process.stdout.write(${JSON.stringify(CHILD_RESULT_START)} + benchResult + ${JSON.stringify(CHILD_RESULT_END)} + '\\n', () => process.exit(0));`,
469
528
  ].filter(Boolean).join('\n');
470
- const started = Date.now();
471
- let raw = '';
472
- let ok = false;
473
- let sid = null;
529
+ }
530
+
531
+ export function parseLiveLeadResult(stdout) {
532
+ const source = String(stdout || '');
533
+ const start = source.lastIndexOf(CHILD_RESULT_START);
534
+ if (start < 0) return null;
535
+ const jsonStart = start + CHILD_RESULT_START.length;
536
+ const end = source.indexOf(CHILD_RESULT_END, jsonStart);
537
+ if (end < 0) return null;
474
538
  try {
475
- raw = execFileSync('node', ['--input-type=module', '-e', driver], {
476
- encoding: 'utf8',
477
- maxBuffer: 64 * 1024 * 1024,
478
- env: {
479
- ...process.env,
480
- MIXDOG_ROOT: pluginRoot,
481
- MIXDOG_DATA_DIR: dataDir,
482
- },
483
- });
484
- const j = raw.lastIndexOf('{');
485
- if (j >= 0) { try { sid = JSON.parse(raw.slice(j)).sessionId || null; } catch { /* tail */ } }
486
- ok = !!sid;
487
- } catch (e) {
488
- raw = String(e.stdout || '') + String(e.stderr || '');
489
- const j = raw.lastIndexOf('{');
490
- if (j >= 0) { try { sid = JSON.parse(raw.slice(j)).sessionId || null; } catch { /* tail */ } }
491
- ok = false;
539
+ const result = JSON.parse(source.slice(jsonStart, end));
540
+ return typeof result?.sessionId === 'string' && result.sessionId ? result : null;
541
+ } catch {
542
+ return null;
492
543
  }
493
- return { sessionId: sid || extractSessionId(raw), ms: Date.now() - started, ok, raw };
544
+ }
545
+
546
+ export function runLiveLeadDelegation({
547
+ pluginRoot,
548
+ dataDir,
549
+ taskCwd,
550
+ prompt,
551
+ provider,
552
+ model,
553
+ effort,
554
+ fast,
555
+ runtimeUrls = null,
556
+ }) {
557
+ const runtimeUrl = runtimeUrls?.runtimeUrl || pathToFileURL(join(pluginRoot, 'mixdog-session-runtime.mjs')).href;
558
+ const traceUrl = runtimeUrls?.traceUrl || pathToFileURL(join(pluginRoot, 'runtime/agent/orchestrator/agent-trace-io.mjs')).href;
559
+ const driver = buildLiveLeadDriver({
560
+ runtimeUrl,
561
+ traceUrl,
562
+ taskCwd,
563
+ prompt,
564
+ provider,
565
+ model,
566
+ effort,
567
+ fast,
568
+ });
569
+ const started = Date.now();
570
+ const child = spawnSync(process.execPath, ['--input-type=module', '-e', driver], {
571
+ encoding: 'utf8',
572
+ maxBuffer: 64 * 1024 * 1024,
573
+ env: {
574
+ ...process.env,
575
+ MIXDOG_ROOT: pluginRoot,
576
+ MIXDOG_DATA_DIR: dataDir,
577
+ },
578
+ });
579
+ const stdout = String(child.stdout || '');
580
+ const stderr = String(child.stderr || '');
581
+ const result = parseLiveLeadResult(stdout);
582
+ return {
583
+ sessionId: result?.sessionId || null,
584
+ ms: Date.now() - started,
585
+ ok: child.status === 0 && !child.error && !!result,
586
+ raw: stdout + stderr,
587
+ stdout,
588
+ stderr,
589
+ exitCode: child.status,
590
+ signal: child.signal || null,
591
+ error: child.error ? String(child.error.message || child.error) : null,
592
+ };
494
593
  }
495
594
 
496
595
  function fmtDeltaEntry(entry) {
@@ -505,12 +604,13 @@ function runLeadMode({ route, effort, fast, repeat, jsonMode, leadPrompt }) {
505
604
  const sandboxRoot = mkdtempSync(join(REPO_ROOT, '.tmp-internal-comms-bench-lead-'));
506
605
  const perVariant = { A: [], B: [] };
507
606
  const runsMeta = { A: [], B: [] };
607
+ let invalidRun = null;
508
608
  try {
509
609
  const pluginRoots = {
510
610
  A: materializePluginRoot('A', sandboxRoot),
511
611
  B: materializePluginRoot('B', sandboxRoot),
512
612
  };
513
- for (let i = 0; i < repeat; i += 1) {
613
+ runLoop: for (let i = 0; i < repeat; i += 1) {
514
614
  for (const variant of ['A', 'B']) {
515
615
  const taskCwd = prepareTaskCwd(sandboxRoot);
516
616
  const dataDir = prepareDataDir(sandboxRoot, `${variant}-r${i}`, realDataDir, userUnified, route.provider);
@@ -526,15 +626,44 @@ function runLeadMode({ route, effort, fast, repeat, jsonMode, leadPrompt }) {
526
626
  fast,
527
627
  });
528
628
  const split = run.sessionId ? splitTokensByRole(dataDir, run.sessionId) : emptyRoleSplit(dataDir);
529
- perVariant[variant].push(split);
530
- runsMeta[variant].push({ ok: run.ok, ms: run.ms, sessionId: run.sessionId, total: split.total.total_tokens });
531
- process.stderr.write(`[internal-comms-bench] -> ${run.ok ? 'ok' : 'FAIL'} ${Math.round(run.ms / 1000)}s session=${run.sessionId || '(none)'} total=${split.total.total_tokens} lead=${split.byRole.lead.total_tokens} worker=${split.byRole.worker.total_tokens} reviewer=${split.byRole.reviewer.total_tokens} other=${split.byRole.other.total_tokens}\n`);
629
+ const validation = validateLeadRun({ taskCwd, run, split });
630
+ if (validation.valid) perVariant[variant].push(split);
631
+ const meta = {
632
+ ok: run.ok,
633
+ valid: validation.valid,
634
+ reasons: validation.reasons,
635
+ ms: run.ms,
636
+ sessionId: run.sessionId,
637
+ total: split.total.total_tokens,
638
+ diagnostics: {
639
+ stdout: run.stdout,
640
+ stderr: run.stderr,
641
+ exitCode: run.exitCode,
642
+ signal: run.signal,
643
+ error: run.error,
644
+ },
645
+ };
646
+ runsMeta[variant].push(meta);
647
+ process.stderr.write(`[internal-comms-bench] -> ${validation.valid ? 'ok' : 'FAIL'} ${Math.round(run.ms / 1000)}s session=${run.sessionId || '(none)'} total=${split.total.total_tokens} lead=${split.byRole.lead.total_tokens} worker=${split.byRole.worker.total_tokens} reviewer=${split.byRole.reviewer.total_tokens} other=${split.byRole.other.total_tokens}${validation.valid ? '' : ` reasons=${validation.reasons.join('; ')}`}\n`);
648
+ if (!validation.valid && (run.stdout || run.stderr || run.error)) {
649
+ process.stderr.write(`[internal-comms-bench] child diagnostics variant=${variant} stdout=${JSON.stringify(run.stdout)} stderr=${JSON.stringify(run.stderr)} error=${JSON.stringify(run.error)}\n`);
650
+ }
651
+ if (!validation.valid) {
652
+ invalidRun = { variant, index: i, reasons: validation.reasons };
653
+ break runLoop;
654
+ }
532
655
  }
533
656
  }
534
657
  } finally {
535
658
  rmSync(sandboxRoot, { recursive: true, force: true });
536
659
  }
537
660
 
661
+ if (invalidRun) {
662
+ process.stderr.write(`[internal-comms-bench] aborting before aggregation: invalid variant=${invalidRun.variant} run=${invalidRun.index + 1} reasons=${invalidRun.reasons.join('; ')}\n`);
663
+ process.exit(1);
664
+ return;
665
+ }
666
+
538
667
  const aggA = aggregateVariant(perVariant.A);
539
668
  const aggB = aggregateVariant(perVariant.B);
540
669
  const deltaMedian = {};
@@ -574,9 +703,7 @@ function runLeadMode({ route, effort, fast, repeat, jsonMode, leadPrompt }) {
574
703
  console.log('delta B-vs-A (mean): ' + [...ROLES, 'total'].map((r) => `${r}=${fmtDeltaEntry(deltaMean[r])}`).join(' '));
575
704
  }
576
705
 
577
- const allOk = runsMeta.A.every((r) => r.ok) && runsMeta.B.every((r) => r.ok);
578
- const anyUsage = perVariant.A.some((s) => s.total.usage_rows > 0) || perVariant.B.some((s) => s.total.usage_rows > 0);
579
- process.exit(allOk && anyUsage ? 0 : 1);
706
+ process.exit(leadModeExitCode(runsMeta, perVariant));
580
707
  }
581
708
 
582
709
  function pctChange(b, a) {
@@ -724,4 +851,4 @@ function main() {
724
851
  process.exit(ok ? 0 : 1);
725
852
  }
726
853
 
727
- main();
854
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) main();