mixdog 0.9.38 → 0.9.40

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 (124) 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/execution-completion-dedup-test.mjs +157 -0
  8. package/scripts/execution-pending-resume-kick-test.mjs +57 -2
  9. package/scripts/execution-resume-esc-integration-test.mjs +176 -0
  10. package/scripts/explore-bench.mjs +38 -2
  11. package/scripts/explore-prompt-policy-test.mjs +152 -11
  12. package/scripts/find-fuzzy-hidden-test.mjs +122 -0
  13. package/scripts/internal-comms-bench-test.mjs +226 -0
  14. package/scripts/internal-comms-bench.mjs +185 -58
  15. package/scripts/internal-comms-smoke.mjs +171 -23
  16. package/scripts/live-worker-smoke.mjs +38 -2
  17. package/scripts/memory-cycle-routing-test.mjs +111 -0
  18. package/scripts/memory-rule-contract-test.mjs +93 -0
  19. package/scripts/output-style-smoke.mjs +2 -2
  20. package/scripts/rg-runner-test.mjs +240 -0
  21. package/scripts/routing-corpus-test.mjs +349 -0
  22. package/scripts/routing-corpus.mjs +211 -32
  23. package/scripts/session-orphan-sweep-test.mjs +83 -0
  24. package/scripts/steering-drain-buckets-test.mjs +316 -2
  25. package/scripts/tool-smoke.mjs +21 -13
  26. package/scripts/tool-tui-presentation-test.mjs +202 -0
  27. package/scripts/tui-transcript-perf-test.mjs +279 -0
  28. package/src/agents/heavy-worker/AGENT.md +10 -7
  29. package/src/agents/reviewer/AGENT.md +6 -4
  30. package/src/agents/worker/AGENT.md +7 -5
  31. package/src/rules/agent/00-common.md +4 -4
  32. package/src/rules/agent/00-core.md +11 -14
  33. package/src/rules/agent/20-skip-protocol.md +3 -3
  34. package/src/rules/agent/30-explorer.md +50 -60
  35. package/src/rules/agent/40-cycle1-agent.md +15 -24
  36. package/src/rules/agent/41-cycle2-agent.md +33 -57
  37. package/src/rules/agent/42-cycle3-agent.md +28 -42
  38. package/src/rules/lead/01-general.md +7 -10
  39. package/src/rules/lead/lead-brief.md +11 -14
  40. package/src/rules/lead/lead-tool.md +6 -5
  41. package/src/rules/shared/01-tool.md +44 -45
  42. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -1
  43. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
  44. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
  46. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
  47. package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
  48. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
  49. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -0
  50. package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
  51. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
  52. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +67 -2
  53. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/store.mjs +232 -43
  56. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +43 -1
  57. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
  58. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +27 -13
  59. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
  60. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
  61. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
  62. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
  63. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
  65. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
  66. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
  67. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
  68. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
  69. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
  70. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
  71. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
  72. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
  73. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
  74. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
  75. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
  76. package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
  77. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
  78. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
  79. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  80. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
  81. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
  82. package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
  83. package/src/runtime/shared/buffered-appender.mjs +13 -2
  84. package/src/runtime/shared/tool-primitives.mjs +4 -1
  85. package/src/runtime/shared/tool-status.mjs +27 -0
  86. package/src/runtime/shared/tool-surface.mjs +6 -3
  87. package/src/session-runtime/config-helpers.mjs +14 -0
  88. package/src/session-runtime/context-status.mjs +1 -0
  89. package/src/session-runtime/effort.mjs +6 -2
  90. package/src/session-runtime/lifecycle-api.mjs +4 -0
  91. package/src/session-runtime/model-recency.mjs +5 -2
  92. package/src/session-runtime/runtime-core.mjs +36 -2
  93. package/src/session-runtime/session-turn-api.mjs +12 -0
  94. package/src/session-runtime/tool-catalog.mjs +34 -0
  95. package/src/standalone/agent-tool/notify.mjs +13 -0
  96. package/src/standalone/agent-tool.mjs +53 -70
  97. package/src/standalone/explore-tool.mjs +6 -7
  98. package/src/tui/App.jsx +33 -1
  99. package/src/tui/app/model-options.mjs +5 -3
  100. package/src/tui/app/model-picker.mjs +12 -24
  101. package/src/tui/app/transcript-window.mjs +10 -10
  102. package/src/tui/app/use-transcript-window.mjs +38 -56
  103. package/src/tui/components/ToolExecution.jsx +11 -6
  104. package/src/tui/components/TranscriptItem.jsx +1 -1
  105. package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
  106. package/src/tui/components/tool-execution/text-format.mjs +10 -19
  107. package/src/tui/dist/index.mjs +866 -300
  108. package/src/tui/engine/agent-job-feed.mjs +216 -19
  109. package/src/tui/engine/agent-response-tail.mjs +68 -0
  110. package/src/tui/engine/notification-plan.mjs +16 -0
  111. package/src/tui/engine/session-api.mjs +14 -2
  112. package/src/tui/engine/session-flow.mjs +22 -2
  113. package/src/tui/engine/tool-card-results.mjs +64 -37
  114. package/src/tui/engine/tool-result-status.mjs +75 -21
  115. package/src/tui/engine/turn.mjs +172 -77
  116. package/src/tui/engine.mjs +199 -39
  117. package/src/tui/index.jsx +2 -2
  118. package/src/workflows/bench/WORKFLOW.md +25 -35
  119. package/src/workflows/default/WORKFLOW.md +38 -32
  120. package/src/workflows/solo/WORKFLOW.md +19 -22
  121. package/scripts/_jitter-fuzz.mjs +0 -44410
  122. package/scripts/_jitter-fuzz2.mjs +0 -44400
  123. package/scripts/_jitter-probe.mjs +0 -44397
  124. package/scripts/_jp2.mjs +0 -45614
@@ -20,6 +20,27 @@ const CWD = 'C:/Project/mixdog';
20
20
  function anchorLinesOf(text) {
21
21
  return text.split('\n').filter((l) => /:\d+/.test(l));
22
22
  }
23
+ // Variant-aware token matching for the fabricated-line guard. Split
24
+ // camelCase and snake_case/kebab joins into atomic sub-tokens, then compare
25
+ // on a light stem so plural/verb forms unify (importance/importanceScore/
26
+ // chunk_importance -> {importance,score,chunk}; scoring~score, memories~memory).
27
+ // Matching is exact stem-equality of sub-tokens (never substring), so it does
28
+ // not loosen into generic-word overlap.
29
+ function splitSubtokens(s) {
30
+ return String(s)
31
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // camelCase boundary
32
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') // ACRONYMWord boundary
33
+ .split(/[^A-Za-z0-9]+/) // snake_case / kebab / punctuation
34
+ .map((t) => t.toLowerCase())
35
+ .filter(Boolean);
36
+ }
37
+ function stemToken(t) {
38
+ if (t.length <= 4) return t;
39
+ return t
40
+ .replace(/ies$/, 'y')
41
+ .replace(/(ings|ing|edly|ed|es|ly|s)$/, '')
42
+ .replace(/e$/, '');
43
+ }
23
44
  function checkAnchorLine(line, ctxTokens = []) {
24
45
  const m = line.match(/([A-Za-z0-9_.\/\\-]+):(\d+)/);
25
46
  if (!m) return null; // no parseable path:line
@@ -34,8 +55,23 @@ function checkAnchorLine(line, ctxTokens = []) {
34
55
  // query/expected token, so a plausible-but-wrong line number fails.
35
56
  if (ctxTokens.length) {
36
57
  // ±2 lines around the 1-based cited line (indices ln-3 .. ln+1 inclusive).
37
- const win = fileLines.slice(Math.max(0, ln - 3), ln + 2).join('\n').toLowerCase();
38
- if (!ctxTokens.some((t) => win.includes(t))) return false;
58
+ const win = fileLines.slice(Math.max(0, ln - 3), ln + 2).join('\n');
59
+ const winStems = [...new Set(splitSubtokens(win).map(stemToken))];
60
+ const wantStems = ctxTokens.map((t) => stemToken(t.toLowerCase()));
61
+ // Exact stem-equality unifies plural/verb variants; a prefix relation
62
+ // additionally bridges derivational forms (compact/compaction) without
63
+ // matching generic filler. Guard the bridge so a short generic line
64
+ // token can't satisfy a longer wanted token (impl~implementation,
65
+ // clas~classifier): require the shorter stem >=5 chars AND a
66
+ // shorter/longer length ratio >=0.6.
67
+ const prefixBridge = (s, w) => {
68
+ if (!(s.startsWith(w) || w.startsWith(s))) return false;
69
+ const short = Math.min(s.length, w.length);
70
+ const long = Math.max(s.length, w.length);
71
+ return short >= 5 && short / long >= 0.6;
72
+ };
73
+ const hitStem = (w) => winStems.some((s) => s === w || prefixBridge(s, w));
74
+ if (!wantStems.some(hitStem)) return false;
39
75
  }
40
76
  return true;
41
77
  } catch { return false; }
@@ -3,29 +3,170 @@ import test from 'node:test';
3
3
  import assert from 'node:assert/strict';
4
4
  import { readFileSync } from 'node:fs';
5
5
  import { buildExplorerPrompt } from '../src/standalone/explore-tool.mjs';
6
+ import { EXPLORE_TOOL } from '../src/standalone/explore-tool.mjs';
6
7
  import { BUILTIN_TOOLS } from '../src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs';
8
+ import { CODE_GRAPH_TOOL_DEFS } from '../src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs';
9
+ import { isEagerDispatchable } from '../src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs';
7
10
 
8
- test('explore per-query prompt leaves path policy to tool schemas', () => {
11
+ test('explore per-query prompt contains only escaped query XML', () => {
9
12
  const prompt = buildExplorerPrompt('display model usage show usage model_usage provider_usage session cache usage state');
10
- assert.doesNotMatch(prompt, /Path policy:/i);
11
- assert.doesNotMatch(prompt, /run find for that fragment before any grep\/glob/i);
13
+ assert.equal(prompt, '<query>display model usage show usage model_usage provider_usage session cache usage state</query>');
14
+ assert.doesNotMatch(prompt, /Reminder:|BUDGET|TURN 1|STOP and answer|verdicts|ratings|recommendations/i);
15
+ assert.equal(buildExplorerPrompt('where is <agent> & status?'), '<query>where is &lt;agent&gt; &amp; status?</query>');
12
16
  });
13
17
 
14
- test('grep glob find tool schemas carry unverified path policy', () => {
18
+ test('builtin route descriptions carry the shortest verified-path policy', () => {
15
19
  const byName = Object.fromEntries(BUILTIN_TOOLS.map((tool) => [tool.name, tool]));
16
20
  // Conditional find-first: only genuinely guessed fragments route through
17
21
  // find, and in the SAME turn — project root itself is a verified scope.
18
22
  assert.match(byName.grep.description, /project root counts as verified/i);
19
- assert.match(byName.grep.description, /guessed path fragment → find first, same turn/i);
23
+ assert.match(byName.grep.description, /guessed path fragment → find first/i);
20
24
  assert.match(byName.grep.description, /no path "\." \+ guessed src\/\*\*/i);
21
25
  assert.match(byName.glob.description, /project root is verified/i);
22
- assert.match(byName.glob.description, /Guessed root\/name → find first, same turn/i);
23
- assert.match(byName.find.description, /verify roots before grep\/glob/i);
26
+ assert.match(byName.glob.description, /Guessed root\/name → find first/i);
27
+ assert.match(byName.find.description, /output paths are verified/i);
28
+ assert.match(byName.read.description, /guessed path\/name → find first/i);
29
+ assert.match(byName.read.description, /Batch paths\/regions as real arrays.*path\[\]/i);
30
+ assert.match(byName.shell.description, /Shell\/write calls are serial/i);
31
+ assert.doesNotMatch(byName.shell.description, /parallel calls/i);
32
+ assert.match(byName.find.description, /lookup only for unknown partial paths\/names/i);
33
+ assert.match(byName.find.description, /project root or already-verified roots/i);
34
+ assert.doesNotMatch(byName.grep.description, /find for that fragment before any grep\/glob/i);
35
+ assert.match(byName.grep.description, /nonzero content_with_context result resolves that search concept/i);
36
+ assert.match(byName.grep.description, /Only zero\/error results may change tokens or scope/i);
37
+ assert.match(`${byName.code_graph?.description || ''} ${CODE_GRAPH_TOOL_DEFS[0]?.inputSchema?.properties?.symbols?.description || ''}`, /multiple exact symbols use one symbols\[\] call/i);
38
+ assert.match(CODE_GRAPH_TOOL_DEFS[0]?.description || '', /verified source files only/i);
24
39
  });
25
40
 
26
- test('explorer turn-1 contract includes batched find for unknown broad targets', () => {
41
+ test('shared tool policy routes facets without duplicate content acquisition', () => {
42
+ const rule = readFileSync(new URL('../src/rules/shared/01-tool.md', import.meta.url), 'utf8');
43
+ const policy = rule.replace(/\s+/g, ' ');
44
+ assert.match(policy, /for each facet choose exactly one shortest locator route/i);
45
+ assert.match(policy, /broad\/uncertain→\s*`explore`.*partial path\/name→\s*`find`.*verified root\+wildcard→\s*`glob`.*quoted\/non-identifier literal or regex→\s*`grep`.*exact code identifier\/relation→\s*`code_graph` before grep/i);
46
+ assert.match(policy, /grep only for a requested literal occurrence or after graph zero\/error/i);
47
+ assert.match(policy, /batch compatible targets/i);
48
+ assert.match(policy, /parallelize distinct facets only, never alternative routes for one facet/i);
49
+ assert.match(policy, /put independent read-only calls in one turn; they may run concurrently regardless of tool/i);
50
+ assert.match(policy, /after locator results, collect all known candidate files\/regions before inspection/i);
51
+ assert.match(policy, /batch compatible reads.*same-file regions.*in one `path\[\]` call and graph targets in arrays/i);
52
+ assert.match(policy, /parallelize independent incompatible read-only inspections/i);
53
+ assert.match(policy, /do not start a singleton while a known compatible candidate remains/i);
54
+ assert.match(policy, /shell\/write calls are serial/i);
55
+ assert.match(policy, /later turns are only for targets dependent on prior results or unresolved facets/i);
56
+ assert.match(policy, /stop when evidence covers the deliverable/i);
57
+ assert.match(policy, /sufficient contextual grep means no overlapping `read`/i);
58
+ assert.match(policy, /files_with_matches.*count.*capped.*insufficient context.*inspect only missing content/i);
59
+ assert.match(policy, /known file\/span→`read` directly without `grep`/i);
60
+ assert.doesNotMatch(policy, /retrieval[^.]{0,120}\b(?:one|at most \d+)\s+(?:lookup|inspection|turn|call)/i);
61
+ assert.doesNotMatch(policy, /\bput independent calls in one turn\b/i);
62
+ assert.doesNotMatch(policy, /\b(?:all|every)\s+(?:independent\s+)?(?:calls|tool calls)\b[^.]{0,80}\bone turn\b/i);
63
+ assert.doesNotMatch(policy, /\bone lookup\b[^.]{0,80}\bat most\b[^.]{0,40}\binspection\b/i);
64
+ assert.doesNotMatch(policy, /all (?:independent )?(?:calls|tool calls|targets)[^.]{0,100}\bone (?:tool )?(?:message|call)/i);
65
+ });
66
+
67
+ test('explorer locator policy retains its compact behavioral contract', () => {
27
68
  const rule = readFileSync(new URL('../src/rules/agent/30-explorer.md', import.meta.url), 'utf8');
28
- assert.match(rule, /Turn 1 is the whole search in ONE message/i);
29
- assert.match(rule, /unknown\/broad\s+targets\)\s+find `query\[\]`/i);
30
- assert.match(rule, /path\/name fragments from multiple tokens/i);
69
+ const policy = rule.replace(/\s+/g, ' ');
70
+ const required = [
71
+ /Locator only:[\s\S]*WHERE \(`path:line`\), never WHY[\s\S]*You ARE `explore`; never[\s\S]*call it/i,
72
+ /ONLY grep\/find\/glob\/code_graph[\s\S]*`read`\/`list` are forbidden/i,
73
+ /Turn 1 \(`turn 1\/3`\)[\s\S]*gathers all known facets in ONE message[\s\S]*Broad\/uncertain is Explorer input[\s\S]*split it into concrete facets[\s\S]*apply the shared one-route\/batch contract[\s\S]*concept facets use grep[\s\S]*single-tool turn is valid[\s\S]*follow-up is only for unresolved pre-anchor\/\s*zero-hit facets/i,
74
+ /follow-up is only for unresolved pre-anchor\/\s*zero-hit facets under this budget/i,
75
+ /broad searches use `output_mode:"files_with_matches"`[\s\S]*`content_with_context` plus `head_limit` only on a path returned THIS session/i,
76
+ /Each pattern is one identifier or camel\/snake variant[\s\S]*spaces are only verbatim copied quoted\s*error\/log literals[\s\S]*Translate non-English queries to English identifiers first[\s\S]*non-ASCII only for quoted literals[\s\S]*Include concept synonyms[\s\S]*not prose phrases/i,
77
+ /Scope is session cwd; omit `path` freely[\s\S]*only from an exact find-returned path \(turn-2 recovery earliest\)[\s\S]*never guess or\s*invent directories[\s\S]*`path:"\."` with guessed `src\/\*\*`[\s\S]*After zero hits, change TOKENS\/scope, not wording\/guessed paths/i,
78
+ /anchor is any `path:line` with a query token\/synonym[\s\S]*code_graph hits[\s\S]*generic-only schema\/handler\/config\/resolver\/index\/error words are zero[\s\S]*Never re-locate an anchor[\s\S]*without `:line`[\s\S]*PRE-anchor/i,
79
+ /After every result[\s\S]*specific-token anchor means STOP and answer NOW[\s\S]*pre-anchors count as zero[\s\S]*Never re-confirm\/upgrade an anchor/i,
80
+ /sole legal follow-up[\s\S]*code-location query[\s\S]*only with pre-anchors[\s\S]*one scoped `content_with_context` grep with `head_limit` on those paths[\s\S]*If zero, remaining zero-anchor recovery turns are legal under the 3-turn budget but must change tokens\/scope[\s\S]*never a second minting hop, anchor upgrade, or fabricated\/estimated line/i,
81
+ /at most 3 turns \(expect 1\)[\s\S]*every tool message `turn N\/3`[\s\S]*normally two messages[\s\S]*third\/extra tool call[\s\S]*unless turn 1 has zero anchors[\s\S]*must change tokens\/scope/i,
82
+ /Single-hop exception[\s\S]*first matching entry\/definition anchors concept\/value\/\s*default[\s\S]*do not trace chains\/value-search[\s\S]*explicit flow or default-resolution query[\s\S]*entry anchor but not its resolved value[\s\S]*ONE resolving hop, then stop/i,
83
+ /Answer in ≤3 lines[\s\S]*`path:line — symbol — short reason`[\s\S]*Copy every cited `path:line` VERBATIM[\s\S]*THIS-session tool result[\s\S]*never estimate\/adjust\/recall/i,
84
+ /Code-location answers require `:line` on every line[\s\S]*`EXPLORATION_FAILED`[\s\S]*never a bare filename or vague prose/i,
85
+ /file\/dir-location queries[\s\S]*exact verified file\/dir path without `:line`[\s\S]*do not force a line\/failure/i,
86
+ /Emit `EXPLORATION_FAILED` only after the budget is spent with zero anchors[\s\S]*prefer a weak anchor to a false miss/i,
87
+ ];
88
+ for (const behavior of required) assert.match(policy, behavior);
89
+ assert.doesNotMatch(policy, /grep[^.]{0,120}\band\b[^.]{0,120}code_graph[^.]{0,120}\band\b[^.]{0,120}find/i);
90
+ });
91
+
92
+ test('canonical schemas advertise safe batching without changing tool shapes', () => {
93
+ const graph = readFileSync(new URL('../src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs', import.meta.url), 'utf8');
94
+ const patch = readFileSync(new URL('../src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs', import.meta.url), 'utf8');
95
+ assert.match(graph, /Batch symbols\[\]\/files\[\] by mode/i);
96
+ assert.match(patch, /one patch/i);
97
+ assert.match(patch, /one file block per target/i);
98
+ });
99
+
100
+ test('code graph and eager-dispatch boundaries preserve runtime shape', () => {
101
+ const schema = CODE_GRAPH_TOOL_DEFS[0]?.inputSchema?.properties || {};
102
+ const fileModes = ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols'];
103
+ const symbolModes = ['symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees'];
104
+ assert.deepEqual(new Set(schema.mode?.enum), new Set([...fileModes, ...symbolModes]));
105
+ const assertBatchShape = (field) => {
106
+ assert.equal(field?.anyOf?.length, 2);
107
+ assert.deepEqual(field.anyOf.map((entry) => entry.type), ['string', 'array']);
108
+ assert.equal(field.anyOf[1].items.type, 'string');
109
+ assert.equal(field.anyOf[1].minItems, 1);
110
+ };
111
+ assertBatchShape(schema.files);
112
+ assertBatchShape(schema.symbols);
113
+ assert.equal(schema.file, undefined);
114
+ assert.equal(schema.symbol, undefined);
115
+ assert.equal(schema.language, undefined);
116
+ const tools = [
117
+ ...BUILTIN_TOOLS,
118
+ { name: 'mcp_read', annotations: { readOnlyHint: true } },
119
+ { name: 'mcp_write', annotations: { readOnlyHint: false } },
120
+ ];
121
+ assert.equal(isEagerDispatchable('read', tools), true);
122
+ assert.equal(isEagerDispatchable('shell', tools), false);
123
+ assert.equal(isEagerDispatchable('mcp_read', tools), true);
124
+ assert.equal(isEagerDispatchable('mcp_write', tools), false);
125
+ });
126
+
127
+ test('code graph descriptions partition file and symbol targets', () => {
128
+ const description = CODE_GRAPH_TOOL_DEFS[0]?.description || '';
129
+ const mode = CODE_GRAPH_TOOL_DEFS[0]?.inputSchema?.properties?.mode?.description || '';
130
+ assert.match(mode, /file modes=\{overview,imports,dependents,related,impact\}.*symbols with files.*files\[\].*file outline/i);
131
+ assert.match(mode, /symbol modes=\{find_symbol,symbol_search,search,references,callers,callees\}.*fileless symbols.*symbol_search keywords/i);
132
+ assert.match(description, /exact identifiers.*find_symbol\/references\/callers\/callees.*keywords.*symbol_search\/search/i);
133
+ assert.match(description, /IDs→graph; literal\/zero→grep/i);
134
+ assert.match(description, /unsupported target arrays.*omitted.*never silently mixed/i);
135
+ assert.match(CODE_GRAPH_TOOL_DEFS[0]?.inputSchema?.properties?.files?.description || '', /supported targets only/i);
136
+ assert.match(CODE_GRAPH_TOOL_DEFS[0]?.inputSchema?.properties?.symbols?.description || '', /exact identifiers.*keywords/i);
137
+ const grep = Object.fromEntries(BUILTIN_TOOLS.map((tool) => [tool.name, tool])).grep;
138
+ assert.match(grep.description, /quoted\/non-identifier literal or regex→grep/i);
139
+ assert.doesNotMatch(grep.inputSchema.properties.pattern.description, /code_graph/i);
140
+ });
141
+
142
+ test('retrieval schemas require their primary arguments and preserve region paths', () => {
143
+ const byName = Object.fromEntries(BUILTIN_TOOLS.map((tool) => [tool.name, tool]));
144
+ const read = byName.read.inputSchema;
145
+ assert.deepEqual(read.required, ['path']);
146
+ const region = read.properties.path.anyOf[1].items.anyOf[1];
147
+ assert.deepEqual(region.required, ['path']);
148
+ assert.deepEqual(byName.grep.inputSchema.anyOf, [{ required: ['pattern'] }, { required: ['glob'] }]);
149
+ assert.deepEqual(byName.grep.inputSchema.properties.output_mode.enum, ['content_with_context', 'files_with_matches', 'count']);
150
+ const grepSchema = byName.grep.inputSchema;
151
+ const valid = (value) => grepSchema.anyOf.some((branch) => branch.required.every((key) => Object.hasOwn(value, key)));
152
+ assert.equal(valid({ pattern: 'x' }), true);
153
+ assert.equal(valid({ glob: '*.mjs' }), true);
154
+ assert.equal(valid({}), false);
155
+ });
156
+
157
+ test('grep scopes do not masquerade as read regions', () => {
158
+ const pattern = Object.fromEntries(BUILTIN_TOOLS.map((tool) => [tool.name, tool])).grep.inputSchema.properties.pattern.description;
159
+ assert.match(pattern, /path\[\] batches verified scopes only/i);
160
+ assert.match(pattern, /file\/span reads use read path\[\] regions/i);
161
+ assert.doesNotMatch(pattern, /known files\/spans use path\[\]/i);
162
+ });
163
+
164
+ test('explore freezes returned facets and avoids re-location', () => {
165
+ assert.match(EXPLORE_TOOL.description, /freezes the LOCATION only/i);
166
+ assert.match(EXPLORE_TOOL.description, /read\/code_graph detail inspection is valid when content was not returned/i);
167
+ assert.match(EXPLORE_TOOL.description, /never re-locate it/i);
168
+ assert.match(EXPLORE_TOOL.description, /search only unresolved facets/i);
169
+ const rule = readFileSync(new URL('../src/rules/shared/01-tool.md', import.meta.url), 'utf8');
170
+ assert.match(rule, /freezes the LOCATION only[\s\S]*read or[\s\S]*code_graph detail inspection is valid[\s\S]*never re-locate it/i);
171
+ assert.match(rule, /nonzero[\s\S]*content_with_context[\s\S]*result resolves[\s\S]*act directly/i);
31
172
  });
@@ -32,6 +32,93 @@ test('fuzzyRank floor drops a purely scattered subsequence but keeps a substring
32
32
  assert.deepEqual(ranked.map((r) => r.item.path), ['src/abcdef.txt']);
33
33
  });
34
34
 
35
+ // Frozen copy of the pre-optimization scorer: this must not call production
36
+ // fuzzy helpers, so rank parity also detects scoring regressions.
37
+ function legacyFuzzyScore(query, str) {
38
+ if (!query) return 0;
39
+ const normalizedQuery = String(query).replace(/[\/\\_.\-\s]+/g, '');
40
+ if (!normalizedQuery) return 0;
41
+ const q = normalizedQuery.toLowerCase();
42
+ const s = str.toLowerCase();
43
+ const qlen = q.length;
44
+ const slen = s.length;
45
+ if (qlen === 0) return 0;
46
+ if (qlen > slen) return null;
47
+
48
+ const lastSep = Math.max(str.lastIndexOf('/'), str.lastIndexOf('\\'));
49
+ let score = 0;
50
+ let si = 0;
51
+ let prevMatch = -2;
52
+ let firstMatchIdx = -1;
53
+ for (let qi = 0; qi < qlen; qi++) {
54
+ const qc = q[qi];
55
+ let found = -1;
56
+ for (let k = si; k < slen; k++) {
57
+ if (s[k] === qc) { found = k; break; }
58
+ }
59
+ if (found === -1) return null;
60
+ if (firstMatchIdx === -1) firstMatchIdx = found;
61
+ score += 1;
62
+ if (found === prevMatch + 1) score += 5;
63
+ const prevCh = found > 0 ? str[found - 1] : undefined;
64
+ if (prevCh === undefined
65
+ || prevCh === '/' || prevCh === '\\' || prevCh === '_' || prevCh === '-'
66
+ || prevCh === '.' || prevCh === ' '
67
+ || (/[a-z0-9]/.test(prevCh) && /[A-Z]/.test(str[found]))) {
68
+ score += 8;
69
+ }
70
+ if (str[found] === normalizedQuery[qi]) score += 1;
71
+ prevMatch = found;
72
+ si = found + 1;
73
+ }
74
+ if (firstMatchIdx > lastSep) score += 10;
75
+ score -= Math.floor(slen / 16);
76
+ score -= Math.floor(firstMatchIdx / 8);
77
+ return score;
78
+ }
79
+
80
+ function referenceFuzzyRank(query, items, limit = 0) {
81
+ const normQuery = String(query || '').toLowerCase().replace(/[\/\\_.\-\s]+/g, '');
82
+ const floor = normQuery.length * 4;
83
+ const scored = [];
84
+ for (const item of items) {
85
+ const p = String(item.path || '');
86
+ const pathScore = legacyFuzzyScore(query, p);
87
+ const base = p.split(/[\\/]/).pop() || '';
88
+ const baseScore = legacyFuzzyScore(query, base);
89
+ const score = Math.max(pathScore ?? -Infinity, baseScore === null ? -Infinity : baseScore + 40);
90
+ if (!Number.isFinite(score)) continue;
91
+ const strong = normQuery.length > 0
92
+ && (String(base || '').toLowerCase().replace(/[\/\\_.\-\s]+/g, '').includes(normQuery)
93
+ || String(p || '').toLowerCase().replace(/[\/\\_.\-\s]+/g, '').includes(normQuery));
94
+ if (!strong && score < floor) continue;
95
+ scored.push({ item, score });
96
+ }
97
+ scored.sort((a, b) => (b.score - a.score)
98
+ || (a.item.path < b.item.path ? -1 : a.item.path > b.item.path ? 1 : 0));
99
+ return limit > 0 ? scored.slice(0, limit) : scored;
100
+ }
101
+
102
+ test('fuzzyRank exactly matches the frozen legacy full-sort reference ranker', () => {
103
+ const fragments = ['alpha', 'Beta', 'tool-events', 'src', 'x_y', 'log', 'camelCase', 'archive'];
104
+ const items = Array.from({ length: 1200 }, (_, i) => ({
105
+ path: i % 97 === 0
106
+ ? 'duplicate/tool-events.log'
107
+ : `${fragments[i % fragments.length]}/${fragments[(i * 7) % fragments.length]}-${i}.mjs`,
108
+ }));
109
+ for (const query of ['', 'tool', 'ToolEvents', 'a_b', 'camel']) {
110
+ for (const limit of [-5, 0, 0.5, 1, 2, 25, 300, 2000]) {
111
+ const expected = referenceFuzzyRank(query, items, limit);
112
+ const actual = fuzzyRank(query, items, limit);
113
+ assert.deepEqual(
114
+ actual.map(({ item, score }) => ({ index: items.findIndex((candidate) => candidate === item), score })),
115
+ expected.map(({ item, score }) => ({ index: items.findIndex((candidate) => candidate === item), score })),
116
+ `query=${JSON.stringify(query)}, limit=${limit}`,
117
+ );
118
+ }
119
+ }
120
+ });
121
+
35
122
  // ── hidden-directory discovery via the find tool ─────────────────────────
36
123
 
37
124
  function makeRepo() {
@@ -69,6 +156,41 @@ test('find hidden:false skips dot-directories', async () => {
69
156
  }
70
157
  });
71
158
 
159
+ test('find respects .gitignore on the common path, then deterministically falls back for an exact ignored filename', async () => {
160
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-find-gitignore-'));
161
+ try {
162
+ writeFileSync(join(root, '.gitignore'), 'ignored-tree/\n');
163
+ mkdirSync(join(root, 'src'), { recursive: true });
164
+ mkdirSync(join(root, 'ignored-tree'), { recursive: true });
165
+ writeFileSync(join(root, 'src', 'common-visible.mjs'), 'x\n');
166
+ writeFileSync(join(root, 'ignored-tree', 'common-ignored.mjs'), 'x\n');
167
+ writeFileSync(join(root, 'src', 'needle-target.mjs.bak'), 'x\n');
168
+ writeFileSync(join(root, 'ignored-tree', 'needle-target.mjs'), 'x\n');
169
+ writeFileSync(join(root, 'ignored-tree', 'LICENSE'), 'x\n');
170
+ writeFileSync(join(root, 'ignored-tree', '[slug].tsx'), 'x\n');
171
+ writeFileSync(join(root, 'ignored-tree', 'my file.txt'), 'x\n');
172
+
173
+ const common = await executeFuzzyFindTool({ query: 'common-visible.mjs', head_limit: 1 }, root);
174
+ assert.ok(common.includes('src/common-visible.mjs'), `common path must keep visible files: ${common}`);
175
+ assert.ok(!common.includes('ignored-tree/common-ignored.mjs'), `common path must skip .gitignored trees: ${common}`);
176
+ assert.ok(common.includes('[gitignored trees not searched; retry with include_noise:true]'), `filled common path must disclose skipped trees: ${common}`);
177
+
178
+ const exact = await executeFuzzyFindTool({ query: 'needle-target.mjs' }, root);
179
+ assert.ok(exact.includes('src/needle-target.mjs.bak'), `visible fuzzy decoy must remain in pass one: ${exact}`);
180
+ assert.ok(exact.includes('ignored-tree/needle-target.mjs'), `exact ignored filename must trigger fallback: ${exact}`);
181
+ assert.ok(!exact.includes('[gitignored trees not searched'), `fallback result must not claim ignored trees were skipped: ${exact}`);
182
+
183
+ const extensionless = await executeFuzzyFindTool({ query: 'LICENSE' }, root);
184
+ assert.ok(extensionless.includes('ignored-tree/LICENSE'), `extensionless exact filename must trigger fallback: ${extensionless}`);
185
+ const literalGlob = await executeFuzzyFindTool({ query: '[slug].tsx' }, root);
186
+ assert.ok(literalGlob.includes('ignored-tree/[slug].tsx'), `literal-glob exact filename must trigger fallback: ${literalGlob}`);
187
+ const spaced = await executeFuzzyFindTool({ query: 'my file.txt' }, root);
188
+ assert.ok(spaced.includes('ignored-tree/my file.txt'), `space-containing exact filename must trigger fallback: ${spaced}`);
189
+ } finally {
190
+ rmSync(root, { recursive: true, force: true });
191
+ }
192
+ });
193
+
72
194
  // ── narrowed-pass merge / dedup backstop ─────────────────────────────────
73
195
 
74
196
  test('exact-name hit survives among many decoys and is not duplicated', async () => {
@@ -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
+ });