homegraph 1.1.2 → 1.1.3

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 (175) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +243 -243
  3. package/dist/bin/homegraph.js +371 -9
  4. package/dist/bin/homegraph.js.map +1 -1
  5. package/dist/db/migrations.js +18 -18
  6. package/dist/db/queries.js +140 -140
  7. package/dist/db/schema.sql +152 -152
  8. package/dist/directory.js +5 -5
  9. package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  10. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  11. package/dist/installer/instructions-template.js +9 -9
  12. package/dist/installer/targets/shared.d.ts +5 -6
  13. package/dist/installer/targets/shared.d.ts.map +1 -1
  14. package/dist/installer/targets/shared.js +5 -6
  15. package/dist/installer/targets/shared.js.map +1 -1
  16. package/dist/mcp/liveness-watchdog.js +16 -16
  17. package/dist/mcp/server-instructions.js +69 -69
  18. package/dist/mcp/tools.d.ts +17 -4
  19. package/dist/mcp/tools.d.ts.map +1 -1
  20. package/dist/mcp/tools.js +192 -22
  21. package/dist/mcp/tools.js.map +1 -1
  22. package/dist/reasoning/reasoner.js +32 -32
  23. package/dist/spec/config.d.ts +43 -0
  24. package/dist/spec/config.d.ts.map +1 -0
  25. package/dist/spec/config.js +275 -0
  26. package/dist/spec/config.js.map +1 -0
  27. package/dist/spec/db/commit-node.d.ts +23 -0
  28. package/dist/spec/db/commit-node.d.ts.map +1 -0
  29. package/dist/spec/db/commit-node.js +62 -0
  30. package/dist/spec/db/commit-node.js.map +1 -0
  31. package/dist/spec/db/fragment-node.d.ts +23 -0
  32. package/dist/spec/db/fragment-node.d.ts.map +1 -0
  33. package/dist/spec/db/fragment-node.js +120 -0
  34. package/dist/spec/db/fragment-node.js.map +1 -0
  35. package/dist/spec/db/fts.d.ts +60 -0
  36. package/dist/spec/db/fts.d.ts.map +1 -0
  37. package/dist/spec/db/fts.js +285 -0
  38. package/dist/spec/db/fts.js.map +1 -0
  39. package/dist/spec/db/index.d.ts +13 -0
  40. package/dist/spec/db/index.d.ts.map +1 -0
  41. package/dist/spec/db/index.js +50 -0
  42. package/dist/spec/db/index.js.map +1 -0
  43. package/dist/spec/db/relations.d.ts +55 -0
  44. package/dist/spec/db/relations.d.ts.map +1 -0
  45. package/dist/spec/db/relations.js +158 -0
  46. package/dist/spec/db/relations.js.map +1 -0
  47. package/dist/spec/db/schema.d.ts +30 -0
  48. package/dist/spec/db/schema.d.ts.map +1 -0
  49. package/dist/spec/db/schema.js +87 -0
  50. package/dist/spec/db/schema.js.map +1 -0
  51. package/dist/spec/db/schema.sql +107 -0
  52. package/dist/spec/db/spec-node.d.ts +41 -0
  53. package/dist/spec/db/spec-node.d.ts.map +1 -0
  54. package/dist/spec/db/spec-node.js +114 -0
  55. package/dist/spec/db/spec-node.js.map +1 -0
  56. package/dist/spec/evolve/impact-locator.d.ts +13 -0
  57. package/dist/spec/evolve/impact-locator.d.ts.map +1 -0
  58. package/dist/spec/evolve/impact-locator.js +25 -0
  59. package/dist/spec/evolve/impact-locator.js.map +1 -0
  60. package/dist/spec/evolve/llm-client.d.ts +50 -0
  61. package/dist/spec/evolve/llm-client.d.ts.map +1 -0
  62. package/dist/spec/evolve/llm-client.js +176 -0
  63. package/dist/spec/evolve/llm-client.js.map +1 -0
  64. package/dist/spec/evolve/logic-checker.d.ts +12 -0
  65. package/dist/spec/evolve/logic-checker.d.ts.map +1 -0
  66. package/dist/spec/evolve/logic-checker.js +48 -0
  67. package/dist/spec/evolve/logic-checker.js.map +1 -0
  68. package/dist/spec/evolve/pipeline.d.ts +40 -0
  69. package/dist/spec/evolve/pipeline.d.ts.map +1 -0
  70. package/dist/spec/evolve/pipeline.js +314 -0
  71. package/dist/spec/evolve/pipeline.js.map +1 -0
  72. package/dist/spec/evolve/spec-rewriter.d.ts +42 -0
  73. package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -0
  74. package/dist/spec/evolve/spec-rewriter.js +254 -0
  75. package/dist/spec/evolve/spec-rewriter.js.map +1 -0
  76. package/dist/spec/graph/queries.d.ts +58 -0
  77. package/dist/spec/graph/queries.d.ts.map +1 -0
  78. package/dist/spec/graph/queries.js +215 -0
  79. package/dist/spec/graph/queries.js.map +1 -0
  80. package/dist/spec/mining/diff-parser.d.ts +33 -0
  81. package/dist/spec/mining/diff-parser.d.ts.map +1 -0
  82. package/dist/spec/mining/diff-parser.js +166 -0
  83. package/dist/spec/mining/diff-parser.js.map +1 -0
  84. package/dist/spec/mining/git-scanner.d.ts +91 -0
  85. package/dist/spec/mining/git-scanner.d.ts.map +1 -0
  86. package/dist/spec/mining/git-scanner.js +266 -0
  87. package/dist/spec/mining/git-scanner.js.map +1 -0
  88. package/dist/spec/mining/pipeline.d.ts +53 -0
  89. package/dist/spec/mining/pipeline.d.ts.map +1 -0
  90. package/dist/spec/mining/pipeline.js +165 -0
  91. package/dist/spec/mining/pipeline.js.map +1 -0
  92. package/dist/spec/mining/scope-resolver.d.ts +45 -0
  93. package/dist/spec/mining/scope-resolver.d.ts.map +1 -0
  94. package/dist/spec/mining/scope-resolver.js +103 -0
  95. package/dist/spec/mining/scope-resolver.js.map +1 -0
  96. package/dist/spec/mining/spec-extractor.d.ts +69 -0
  97. package/dist/spec/mining/spec-extractor.d.ts.map +1 -0
  98. package/dist/spec/mining/spec-extractor.js +369 -0
  99. package/dist/spec/mining/spec-extractor.js.map +1 -0
  100. package/dist/spec/types.d.ts +149 -0
  101. package/dist/spec/types.d.ts.map +1 -0
  102. package/dist/spec/types.js +15 -0
  103. package/dist/spec/types.js.map +1 -0
  104. package/dist/spec/utils.d.ts +166 -0
  105. package/dist/spec/utils.d.ts.map +1 -0
  106. package/dist/spec/utils.js +461 -0
  107. package/dist/spec/utils.js.map +1 -0
  108. package/package.json +58 -57
  109. package/scripts/add-lang/bench.sh +60 -60
  110. package/scripts/add-lang/check-grammar.mjs +75 -75
  111. package/scripts/add-lang/dump-ast.mjs +103 -103
  112. package/scripts/add-lang/verify-extraction.mjs +70 -70
  113. package/scripts/agent-eval/ab-adoption.sh +91 -91
  114. package/scripts/agent-eval/ab-hook.sh +86 -86
  115. package/scripts/agent-eval/ab-impl.sh +78 -78
  116. package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -102
  117. package/scripts/agent-eval/ab-sufficiency.sh +78 -78
  118. package/scripts/agent-eval/arms-F.sh +21 -21
  119. package/scripts/agent-eval/arms-matrix.sh +37 -37
  120. package/scripts/agent-eval/audit.sh +68 -68
  121. package/scripts/agent-eval/bench-readme.sh +28 -28
  122. package/scripts/agent-eval/bench-why-repo.sh +22 -22
  123. package/scripts/agent-eval/block-read-hook.sh +19 -19
  124. package/scripts/agent-eval/hook-settings.json +15 -15
  125. package/scripts/agent-eval/itrun.sh +120 -120
  126. package/scripts/agent-eval/offload-eval-3arm.sh +72 -72
  127. package/scripts/agent-eval/offload-eval-cost.mjs +133 -133
  128. package/scripts/agent-eval/offload-eval-effort.mjs +108 -108
  129. package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -25
  130. package/scripts/agent-eval/offload-eval-frontload.sh +47 -47
  131. package/scripts/agent-eval/offload-eval-ground-truth.json +18 -18
  132. package/scripts/agent-eval/offload-eval-hook.mjs +84 -84
  133. package/scripts/agent-eval/offload-eval-judge.mjs +103 -103
  134. package/scripts/agent-eval/offload-eval-matrix.sh +20 -20
  135. package/scripts/agent-eval/offload-eval-metrics.mjs +94 -94
  136. package/scripts/agent-eval/offload-eval-refs1.sh +50 -50
  137. package/scripts/agent-eval/offload-eval-setup.sh +24 -24
  138. package/scripts/agent-eval/offload-eval-styles.sh +71 -71
  139. package/scripts/agent-eval/offload-eval-summarize.mjs +68 -68
  140. package/scripts/agent-eval/offload-eval.md +76 -76
  141. package/scripts/agent-eval/parse-arms.mjs +116 -116
  142. package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
  143. package/scripts/agent-eval/parse-run.mjs +45 -45
  144. package/scripts/agent-eval/parse-session.mjs +93 -93
  145. package/scripts/agent-eval/probe-context.mjs +21 -21
  146. package/scripts/agent-eval/probe-explore.mjs +40 -40
  147. package/scripts/agent-eval/probe-node.mjs +20 -20
  148. package/scripts/agent-eval/probe-sweep.mjs +119 -119
  149. package/scripts/agent-eval/probe-trace.mjs +20 -20
  150. package/scripts/agent-eval/redirect-read-hook.sh +38 -38
  151. package/scripts/agent-eval/run-agent.sh +34 -34
  152. package/scripts/agent-eval/run-all.sh +69 -69
  153. package/scripts/agent-eval/run-arms.sh +56 -56
  154. package/scripts/agent-eval/seq-matrix.mjs +137 -137
  155. package/scripts/build-bundle.sh +118 -118
  156. package/scripts/extract-release-notes.mjs +130 -130
  157. package/scripts/local-install.sh +41 -41
  158. package/scripts/npm-sdk.js +75 -75
  159. package/scripts/npm-shim.js +246 -246
  160. package/scripts/pack-npm.sh +119 -119
  161. package/scripts/prepare-release.mjs +270 -270
  162. package/scripts/qa_eval/README.md +407 -404
  163. package/scripts/qa_eval/_test_deveco_probe.py +41 -41
  164. package/scripts/qa_eval/agent_runner.py +526 -526
  165. package/scripts/qa_eval/data/.gitignore +4 -4
  166. package/scripts/qa_eval/data/test-set.jsonl +2 -22
  167. package/scripts/qa_eval/eval_metrics.py +274 -233
  168. package/scripts/qa_eval/external_agent.py +976 -671
  169. package/scripts/qa_eval/llm_config.py +92 -92
  170. package/scripts/qa_eval/memory_monitor.py +132 -132
  171. package/scripts/qa_eval/my_answer_accuracy.py +187 -187
  172. package/scripts/qa_eval/requirements.txt +2 -2
  173. package/scripts/qa_eval/run_pipeline.py +804 -711
  174. package/scripts/qa_eval/stats_efficiency.py +279 -279
  175. package/scripts/qa_eval/stats_scores.py +207 -207
@@ -1,84 +1,84 @@
1
- #!/usr/bin/env node
2
- // UserPromptSubmit hook — APPROACH 1: additive context-injection.
3
- // Front-loads homegraph's structural answer for flow/impact/"how/where" prompts so the
4
- // agent's reflex grep/read has nothing left to find. Strictly additive (never blocks),
5
- // gated to structural prompts (no cost otherwise), and uses RAW explore (offload disabled)
6
- // so the injected context is accurate — never the (currently low-fidelity) synthesis.
7
- //
8
- // Reads {prompt, cwd} as JSON on stdin; prints the explore result to stdout (which Claude
9
- // Code injects into the agent's context). Any failure -> silent exit 0 (degradable).
10
- import { pathToFileURL, fileURLToPath } from 'node:url';
11
- import { resolve, join, dirname } from 'node:path';
12
- import { existsSync, readFileSync, appendFileSync } from 'node:fs';
13
-
14
- // Resolve the engine repo from this script's own location (scripts/agent-eval/ -> ../..),
15
- // overridable with CG_ENGINE. The hook ships inside the repo, so it finds its own dist.
16
- const HERE = dirname(fileURLToPath(import.meta.url));
17
- const ENGINE = process.env.CG_ENGINE || resolve(HERE, '..', '..');
18
- const BUDGET = Number(process.env.CG_FRONTLOAD_BUDGET || 16000);
19
-
20
- // Debug log only when CG_FRONTLOAD_DEBUG is set to a file path (the harness points it at a
21
- // log to count injections); off by default so the shipped hook writes nothing extra.
22
- const DBG = process.env.CG_FRONTLOAD_DEBUG;
23
- const dbg = (m) => { if (!DBG) return; try { appendFileSync(DBG, `[${new Date().toISOString()}] ${m}\n`); } catch { /* ignore */ } };
24
-
25
- let input = {};
26
- try { input = JSON.parse(readFileSync(0, 'utf8')); } catch (e) { dbg('stdin parse fail: ' + e.message); }
27
- const prompt = String(input.prompt || '');
28
- const cwd = String(input.cwd || process.cwd());
29
- dbg(`invoked: promptLen=${prompt.length} cwd=${cwd}`);
30
-
31
- // Gate: only structural / flow / impact / where-how questions. Cheap regex; silent no-op
32
- // otherwise so non-structural prompts ("fix this typo") cost nothing.
33
- const STRUCTURAL = /\b(how|where|trace|flow|path|reach(es|ed)?|call(s|ed|er|ers|ee)?|depend|impact|affect|wire[ds]?|connect|implement|architect|structure|breaks?|what calls|why does)\b/i;
34
- if (!prompt || !STRUCTURAL.test(prompt)) { dbg('gate: non-structural, no-op'); process.exit(0); }
35
- dbg('gate: structural PASS');
36
-
37
- // Find the index: cwd, then walk up a few levels.
38
- let root = cwd, found = null;
39
- for (let i = 0; i < 6 && root; i++) {
40
- if (existsSync(join(root, '.homegraph'))) { found = root; break; }
41
- const parent = resolve(root, '..'); if (parent === root) break; root = parent;
42
- }
43
- if (!found) { dbg(`no .homegraph found from cwd=${cwd}`); process.exit(0); }
44
- dbg(`found index at ${found}`);
45
-
46
- try {
47
- process.env.HOMEGRAPH_OFFLOAD_DISABLE = '1'; // raw, accurate — never the unfixed offload
48
- process.env.HOMEGRAPH_TELEMETRY = '0'; process.env.DO_NOT_TRACK = '1';
49
- const load = async (rel) => import(pathToFileURL(resolve(ENGINE, rel)).href);
50
- const idx = await load('dist/index.js');
51
- const tools = await load('dist/mcp/tools.js');
52
- const HomeGraph = idx.default?.default ?? idx.default ?? idx.HomeGraph;
53
- const ToolHandler = tools.ToolHandler ?? tools.default?.ToolHandler;
54
- if (typeof HomeGraph?.openSync !== 'function' || typeof ToolHandler !== 'function') process.exit(0);
55
-
56
- // Retry once on a transient busy/locked index (the hook's openSync can race a
57
- // freshly-warming daemon on the first prompt of a session).
58
- let text = '';
59
- for (let attempt = 1; attempt <= 2; attempt++) {
60
- try {
61
- const cg = HomeGraph.openSync(found);
62
- const h = new ToolHandler(cg);
63
- const res = await h.execute('homegraph_explore', { query: prompt });
64
- text = res?.content?.[0]?.text ?? '';
65
- try { cg.close?.(); } catch { /* ignore */ }
66
- dbg(`explore attempt ${attempt} returned ${text.length} chars`);
67
- break;
68
- } catch (e) {
69
- dbg(`explore attempt ${attempt} failed: ${e?.message || e}`);
70
- if (attempt === 2) throw e;
71
- await new Promise((r) => setTimeout(r, 800));
72
- }
73
- }
74
- if (!text.trim()) { dbg('empty explore result, no-op'); process.exit(0); }
75
- if (text.length > BUDGET) text = text.slice(0, BUDGET) + '\n…[front-load truncated to budget]';
76
-
77
- process.stdout.write(
78
- `## HomeGraph structural context (auto-retrieved for this question)\n` +
79
- `The code graph was queried for your question; the relevant symbols, source, and call flow are below. ` +
80
- `Treat the quoted source as already read. If you need more, call homegraph_explore with specific symbol names rather than grepping or reading files.\n\n` +
81
- text + '\n'
82
- );
83
- dbg(`INJECTED ${text.length} chars`);
84
- } catch (e) { dbg('ERROR: ' + (e?.stack || e?.message || e)); process.exit(0); } // degradable
1
+ #!/usr/bin/env node
2
+ // UserPromptSubmit hook — APPROACH 1: additive context-injection.
3
+ // Front-loads homegraph's structural answer for flow/impact/"how/where" prompts so the
4
+ // agent's reflex grep/read has nothing left to find. Strictly additive (never blocks),
5
+ // gated to structural prompts (no cost otherwise), and uses RAW explore (offload disabled)
6
+ // so the injected context is accurate — never the (currently low-fidelity) synthesis.
7
+ //
8
+ // Reads {prompt, cwd} as JSON on stdin; prints the explore result to stdout (which Claude
9
+ // Code injects into the agent's context). Any failure -> silent exit 0 (degradable).
10
+ import { pathToFileURL, fileURLToPath } from 'node:url';
11
+ import { resolve, join, dirname } from 'node:path';
12
+ import { existsSync, readFileSync, appendFileSync } from 'node:fs';
13
+
14
+ // Resolve the engine repo from this script's own location (scripts/agent-eval/ -> ../..),
15
+ // overridable with CG_ENGINE. The hook ships inside the repo, so it finds its own dist.
16
+ const HERE = dirname(fileURLToPath(import.meta.url));
17
+ const ENGINE = process.env.CG_ENGINE || resolve(HERE, '..', '..');
18
+ const BUDGET = Number(process.env.CG_FRONTLOAD_BUDGET || 16000);
19
+
20
+ // Debug log only when CG_FRONTLOAD_DEBUG is set to a file path (the harness points it at a
21
+ // log to count injections); off by default so the shipped hook writes nothing extra.
22
+ const DBG = process.env.CG_FRONTLOAD_DEBUG;
23
+ const dbg = (m) => { if (!DBG) return; try { appendFileSync(DBG, `[${new Date().toISOString()}] ${m}\n`); } catch { /* ignore */ } };
24
+
25
+ let input = {};
26
+ try { input = JSON.parse(readFileSync(0, 'utf8')); } catch (e) { dbg('stdin parse fail: ' + e.message); }
27
+ const prompt = String(input.prompt || '');
28
+ const cwd = String(input.cwd || process.cwd());
29
+ dbg(`invoked: promptLen=${prompt.length} cwd=${cwd}`);
30
+
31
+ // Gate: only structural / flow / impact / where-how questions. Cheap regex; silent no-op
32
+ // otherwise so non-structural prompts ("fix this typo") cost nothing.
33
+ const STRUCTURAL = /\b(how|where|trace|flow|path|reach(es|ed)?|call(s|ed|er|ers|ee)?|depend|impact|affect|wire[ds]?|connect|implement|architect|structure|breaks?|what calls|why does)\b/i;
34
+ if (!prompt || !STRUCTURAL.test(prompt)) { dbg('gate: non-structural, no-op'); process.exit(0); }
35
+ dbg('gate: structural PASS');
36
+
37
+ // Find the index: cwd, then walk up a few levels.
38
+ let root = cwd, found = null;
39
+ for (let i = 0; i < 6 && root; i++) {
40
+ if (existsSync(join(root, '.homegraph'))) { found = root; break; }
41
+ const parent = resolve(root, '..'); if (parent === root) break; root = parent;
42
+ }
43
+ if (!found) { dbg(`no .homegraph found from cwd=${cwd}`); process.exit(0); }
44
+ dbg(`found index at ${found}`);
45
+
46
+ try {
47
+ process.env.HOMEGRAPH_OFFLOAD_DISABLE = '1'; // raw, accurate — never the unfixed offload
48
+ process.env.HOMEGRAPH_TELEMETRY = '0'; process.env.DO_NOT_TRACK = '1';
49
+ const load = async (rel) => import(pathToFileURL(resolve(ENGINE, rel)).href);
50
+ const idx = await load('dist/index.js');
51
+ const tools = await load('dist/mcp/tools.js');
52
+ const HomeGraph = idx.default?.default ?? idx.default ?? idx.HomeGraph;
53
+ const ToolHandler = tools.ToolHandler ?? tools.default?.ToolHandler;
54
+ if (typeof HomeGraph?.openSync !== 'function' || typeof ToolHandler !== 'function') process.exit(0);
55
+
56
+ // Retry once on a transient busy/locked index (the hook's openSync can race a
57
+ // freshly-warming daemon on the first prompt of a session).
58
+ let text = '';
59
+ for (let attempt = 1; attempt <= 2; attempt++) {
60
+ try {
61
+ const cg = HomeGraph.openSync(found);
62
+ const h = new ToolHandler(cg);
63
+ const res = await h.execute('homegraph_explore', { query: prompt });
64
+ text = res?.content?.[0]?.text ?? '';
65
+ try { cg.close?.(); } catch { /* ignore */ }
66
+ dbg(`explore attempt ${attempt} returned ${text.length} chars`);
67
+ break;
68
+ } catch (e) {
69
+ dbg(`explore attempt ${attempt} failed: ${e?.message || e}`);
70
+ if (attempt === 2) throw e;
71
+ await new Promise((r) => setTimeout(r, 800));
72
+ }
73
+ }
74
+ if (!text.trim()) { dbg('empty explore result, no-op'); process.exit(0); }
75
+ if (text.length > BUDGET) text = text.slice(0, BUDGET) + '\n…[front-load truncated to budget]';
76
+
77
+ process.stdout.write(
78
+ `## HomeGraph structural context (auto-retrieved for this question)\n` +
79
+ `The code graph was queried for your question; the relevant symbols, source, and call flow are below. ` +
80
+ `Treat the quoted source as already read. If you need more, call homegraph_explore with specific symbol names rather than grepping or reading files.\n\n` +
81
+ text + '\n'
82
+ );
83
+ dbg(`INJECTED ${text.length} chars`);
84
+ } catch (e) { dbg('ERROR: ' + (e?.stack || e?.message || e)); process.exit(0); } // degradable
@@ -1,103 +1,103 @@
1
- #!/usr/bin/env node
2
- // Accuracy judge. For each run in results.jsonl:
3
- // - end-to-end: agent finalAnswer vs verified ground truth (all arms)
4
- // - fidelity: offload synthesized answer vs ground truth (offload arm only)
5
- // Judge = claude -p sonnet --effort high, no tools, run from a neutral cwd,
6
- // JSON-only verdicts. Writes judged.jsonl (one line per run, verdicts merged).
7
- //
8
- // Usage: judge.mjs --results <f> --truth <f> --out <f> [--concurrency 4]
9
- import { readFileSync, writeFileSync, existsSync } from 'fs';
10
- import { execFile } from 'child_process';
11
-
12
- const A = {};
13
- for (let i = 2; i < process.argv.length; i += 2) A[process.argv[i].replace(/^--/, '')] = process.argv[i + 1];
14
- const results = readFileSync(A.results, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l));
15
- const truth = JSON.parse(readFileSync(A.truth, 'utf8'));
16
- const OUT = A.out || '/tmp/cg-offload-eval/judged.jsonl';
17
- const CONC = Number(A.concurrency || 4);
18
-
19
- function askJudge(prompt) {
20
- return new Promise((resolve) => {
21
- execFile('claude', ['-p', prompt, '--model', 'sonnet', '--effort', 'high',
22
- '--max-budget-usd', '0.5', '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}'],
23
- // Run from a neutral dir with no repo files so the judge can't "cheat" by reading source.
24
- { cwd: process.env.AGENT_EVAL_OUT || '/tmp', maxBuffer: 1 << 24, timeout: 120000 },
25
- (err, stdout) => {
26
- const raw = (stdout || '').trim();
27
- const m = raw.match(/\{[\s\S]*\}/);
28
- if (!m) return resolve({ verdict: 'error', score: null, note: (err ? 'exec ' + err.message : 'no json').slice(0, 80) });
29
- try { resolve(JSON.parse(m[0])); } catch { resolve({ verdict: 'error', score: null, note: 'parse fail' }); }
30
- });
31
- });
32
- }
33
-
34
- const e2ePrompt = (gt, ans) => `You are scoring whether an AI coding agent correctly answered a code-flow question about a repository. Judge ONLY against the verified ground truth. Do NOT use any tools.
35
-
36
- QUESTION: ${gt.question}
37
-
38
- VERIFIED GROUND TRUTH (the actual call path + files):
39
- ${gt.truth}
40
-
41
- AGENT'S ANSWER:
42
- ${ans || '(empty)'}
43
-
44
- Score how correct the agent's answer is vs the ground truth. A "pass" means it identifies the core mechanism and the major hops with the right files/symbols and makes no materially wrong claim. "partial" = right area but misses major hops or has notable errors. "fail" = wrong layer, fabricated, or misses the mechanism.
45
- Output ONLY minified JSON, no prose, no code fences:
46
- {"verdict":"pass|partial|fail","score":<0-100>,"missedHops":["..."],"wrongClaims":["..."],"note":"<=20 words"}`;
47
-
48
- const fidPrompt = (gt, ans) => `You are scoring the FIDELITY of a machine-synthesized code-exploration answer against verified ground truth. The synthesized answer claims to trace a flow and cite file:line locations. Do NOT use any tools.
49
-
50
- QUESTION: ${gt.question}
51
-
52
- VERIFIED GROUND TRUTH (the actual call path + files):
53
- ${gt.truth}
54
-
55
- SYNTHESIZED ANSWER (to score):
56
- ${ans || '(empty)'}
57
-
58
- Judge: (1) is the traced call path correct vs ground truth? (2) are the cited files/symbols correct (not fabricated)? (3) if it gave a "Coverage:" verdict, was that verdict honest about what it actually covered? A confident WRONG trace is the worst outcome — penalize it harder than an honest "partial/not found".
59
- Output ONLY minified JSON, no prose, no code fences:
60
- {"verdict":"pass|partial|fail","score":<0-100>,"fabrication":<true|false>,"coverageHonest":<true|false>,"missedHops":["..."],"note":"<=20 words"}`;
61
-
62
- // Build the job list
63
- const jobs = [];
64
- for (const r of results) {
65
- const gt = truth[r.repo];
66
- if (!gt) { r._nojudge = true; continue; }
67
- jobs.push({ r, kind: 'e2e', prompt: e2ePrompt(gt, r.finalAnswer) });
68
- if (r.arm === 'offload' && Array.isArray(r.offloadAnswers))
69
- r.offloadAnswers.forEach((ans, i) => { if (ans && ans.trim()) jobs.push({ r, kind: 'fid', idx: i, prompt: fidPrompt(gt, ans) }); });
70
- }
71
- console.error(`judging ${jobs.length} verdicts across ${results.length} runs (concurrency ${CONC})...`);
72
-
73
- let done = 0;
74
- async function worker(queue) {
75
- while (queue.length) {
76
- const job = queue.shift();
77
- const v = await askJudge(job.prompt);
78
- if (job.kind === 'e2e') job.r.e2e = v; else (job.r._fid ??= []).push(v);
79
- console.error(` [${++done}/${jobs.length}] ${job.r.repo}/${job.r.arm}#${job.r.rep} ${job.kind}: ${v.verdict}${v.score != null ? ' ' + v.score : ''}`);
80
- }
81
- }
82
- const q = [...jobs];
83
- await Promise.all(Array.from({ length: CONC }, () => worker(q)));
84
-
85
- // Aggregate per-answer fidelity verdicts into one fidelity object per offload run.
86
- const medOf = (a) => { a = [...a].sort((x, y) => x - y); return a.length ? (a.length % 2 ? a[(a.length - 1) / 2] : (a[a.length / 2 - 1] + a[a.length / 2]) / 2) : null; };
87
- for (const r of results) {
88
- if (r._fid?.length) {
89
- const scores = r._fid.map(v => v.score).filter(x => x != null);
90
- r.fidelity = {
91
- n: r._fid.length, scores,
92
- max: scores.length ? Math.max(...scores) : null,
93
- min: scores.length ? Math.min(...scores) : null,
94
- median: medOf(scores),
95
- anyFabrication: r._fid.some(v => v.fabrication === true),
96
- allCoverageHonest: r._fid.every(v => v.coverageHonest !== false),
97
- verdicts: r._fid.map(v => v.verdict),
98
- };
99
- }
100
- delete r._fid;
101
- }
102
- writeFileSync(OUT, results.map(r => JSON.stringify(r)).join('\n') + '\n');
103
- console.error(`wrote ${OUT}`);
1
+ #!/usr/bin/env node
2
+ // Accuracy judge. For each run in results.jsonl:
3
+ // - end-to-end: agent finalAnswer vs verified ground truth (all arms)
4
+ // - fidelity: offload synthesized answer vs ground truth (offload arm only)
5
+ // Judge = claude -p sonnet --effort high, no tools, run from a neutral cwd,
6
+ // JSON-only verdicts. Writes judged.jsonl (one line per run, verdicts merged).
7
+ //
8
+ // Usage: judge.mjs --results <f> --truth <f> --out <f> [--concurrency 4]
9
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
10
+ import { execFile } from 'child_process';
11
+
12
+ const A = {};
13
+ for (let i = 2; i < process.argv.length; i += 2) A[process.argv[i].replace(/^--/, '')] = process.argv[i + 1];
14
+ const results = readFileSync(A.results, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l));
15
+ const truth = JSON.parse(readFileSync(A.truth, 'utf8'));
16
+ const OUT = A.out || '/tmp/cg-offload-eval/judged.jsonl';
17
+ const CONC = Number(A.concurrency || 4);
18
+
19
+ function askJudge(prompt) {
20
+ return new Promise((resolve) => {
21
+ execFile('claude', ['-p', prompt, '--model', 'sonnet', '--effort', 'high',
22
+ '--max-budget-usd', '0.5', '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}'],
23
+ // Run from a neutral dir with no repo files so the judge can't "cheat" by reading source.
24
+ { cwd: process.env.AGENT_EVAL_OUT || '/tmp', maxBuffer: 1 << 24, timeout: 120000 },
25
+ (err, stdout) => {
26
+ const raw = (stdout || '').trim();
27
+ const m = raw.match(/\{[\s\S]*\}/);
28
+ if (!m) return resolve({ verdict: 'error', score: null, note: (err ? 'exec ' + err.message : 'no json').slice(0, 80) });
29
+ try { resolve(JSON.parse(m[0])); } catch { resolve({ verdict: 'error', score: null, note: 'parse fail' }); }
30
+ });
31
+ });
32
+ }
33
+
34
+ const e2ePrompt = (gt, ans) => `You are scoring whether an AI coding agent correctly answered a code-flow question about a repository. Judge ONLY against the verified ground truth. Do NOT use any tools.
35
+
36
+ QUESTION: ${gt.question}
37
+
38
+ VERIFIED GROUND TRUTH (the actual call path + files):
39
+ ${gt.truth}
40
+
41
+ AGENT'S ANSWER:
42
+ ${ans || '(empty)'}
43
+
44
+ Score how correct the agent's answer is vs the ground truth. A "pass" means it identifies the core mechanism and the major hops with the right files/symbols and makes no materially wrong claim. "partial" = right area but misses major hops or has notable errors. "fail" = wrong layer, fabricated, or misses the mechanism.
45
+ Output ONLY minified JSON, no prose, no code fences:
46
+ {"verdict":"pass|partial|fail","score":<0-100>,"missedHops":["..."],"wrongClaims":["..."],"note":"<=20 words"}`;
47
+
48
+ const fidPrompt = (gt, ans) => `You are scoring the FIDELITY of a machine-synthesized code-exploration answer against verified ground truth. The synthesized answer claims to trace a flow and cite file:line locations. Do NOT use any tools.
49
+
50
+ QUESTION: ${gt.question}
51
+
52
+ VERIFIED GROUND TRUTH (the actual call path + files):
53
+ ${gt.truth}
54
+
55
+ SYNTHESIZED ANSWER (to score):
56
+ ${ans || '(empty)'}
57
+
58
+ Judge: (1) is the traced call path correct vs ground truth? (2) are the cited files/symbols correct (not fabricated)? (3) if it gave a "Coverage:" verdict, was that verdict honest about what it actually covered? A confident WRONG trace is the worst outcome — penalize it harder than an honest "partial/not found".
59
+ Output ONLY minified JSON, no prose, no code fences:
60
+ {"verdict":"pass|partial|fail","score":<0-100>,"fabrication":<true|false>,"coverageHonest":<true|false>,"missedHops":["..."],"note":"<=20 words"}`;
61
+
62
+ // Build the job list
63
+ const jobs = [];
64
+ for (const r of results) {
65
+ const gt = truth[r.repo];
66
+ if (!gt) { r._nojudge = true; continue; }
67
+ jobs.push({ r, kind: 'e2e', prompt: e2ePrompt(gt, r.finalAnswer) });
68
+ if (r.arm === 'offload' && Array.isArray(r.offloadAnswers))
69
+ r.offloadAnswers.forEach((ans, i) => { if (ans && ans.trim()) jobs.push({ r, kind: 'fid', idx: i, prompt: fidPrompt(gt, ans) }); });
70
+ }
71
+ console.error(`judging ${jobs.length} verdicts across ${results.length} runs (concurrency ${CONC})...`);
72
+
73
+ let done = 0;
74
+ async function worker(queue) {
75
+ while (queue.length) {
76
+ const job = queue.shift();
77
+ const v = await askJudge(job.prompt);
78
+ if (job.kind === 'e2e') job.r.e2e = v; else (job.r._fid ??= []).push(v);
79
+ console.error(` [${++done}/${jobs.length}] ${job.r.repo}/${job.r.arm}#${job.r.rep} ${job.kind}: ${v.verdict}${v.score != null ? ' ' + v.score : ''}`);
80
+ }
81
+ }
82
+ const q = [...jobs];
83
+ await Promise.all(Array.from({ length: CONC }, () => worker(q)));
84
+
85
+ // Aggregate per-answer fidelity verdicts into one fidelity object per offload run.
86
+ const medOf = (a) => { a = [...a].sort((x, y) => x - y); return a.length ? (a.length % 2 ? a[(a.length - 1) / 2] : (a[a.length / 2 - 1] + a[a.length / 2]) / 2) : null; };
87
+ for (const r of results) {
88
+ if (r._fid?.length) {
89
+ const scores = r._fid.map(v => v.score).filter(x => x != null);
90
+ r.fidelity = {
91
+ n: r._fid.length, scores,
92
+ max: scores.length ? Math.max(...scores) : null,
93
+ min: scores.length ? Math.min(...scores) : null,
94
+ median: medOf(scores),
95
+ anyFabrication: r._fid.some(v => v.fabrication === true),
96
+ allCoverageHonest: r._fid.every(v => v.coverageHonest !== false),
97
+ verdicts: r._fid.map(v => v.verdict),
98
+ };
99
+ }
100
+ delete r._fid;
101
+ }
102
+ writeFileSync(OUT, results.map(r => JSON.stringify(r)).join('\n') + '\n');
103
+ console.error(`wrote ${OUT}`);
@@ -1,20 +1,20 @@
1
- #!/usr/bin/env bash
2
- # Drive the 3-arm campaign (offload/raw/nocg) across all 4 tiers, n reps each, into one
3
- # results.jsonl. Reads the canonical question per repo from offload-eval-ground-truth.json.
4
- # Env: REPS (default 3) AGENT_EVAL_OUT=<scratch dir>
5
- set -uo pipefail
6
- HERE="$(cd "$(dirname "$0")" && pwd)"
7
- OUT="${AGENT_EVAL_OUT:-/tmp/cg-offload-eval}"
8
- GT="$HERE/offload-eval-ground-truth.json"
9
- REPS="${REPS:-3}"
10
- export RESULTS="$OUT/results.jsonl"
11
- : > "$RESULTS"
12
- for repo in mtkruto postybirb shapeshift trezor; do
13
- case "$repo" in mtkruto) tier=small;; postybirb) tier=medium;; shapeshift) tier=complex;; trezor) tier=large;; esac
14
- Q=$(node -e "console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))[process.argv[2]].question)" "$GT" "$repo")
15
- echo ""; echo "### $repo ($tier) $(date +%H:%M:%S)"
16
- bash "$HERE/offload-eval-3arm.sh" "$OUT/repos/$repo" "$tier" "$REPS" "$Q"
17
- done
18
- echo ""; echo "###### MATRIX DONE -> $RESULTS ($(wc -l < "$RESULTS") runs). Judge + summarize with:"
19
- echo " node $HERE/offload-eval-judge.mjs --results $RESULTS --truth $GT --out $OUT/judged.jsonl"
20
- echo " node $HERE/offload-eval-summarize.mjs $OUT/judged.jsonl"
1
+ #!/usr/bin/env bash
2
+ # Drive the 3-arm campaign (offload/raw/nocg) across all 4 tiers, n reps each, into one
3
+ # results.jsonl. Reads the canonical question per repo from offload-eval-ground-truth.json.
4
+ # Env: REPS (default 3) AGENT_EVAL_OUT=<scratch dir>
5
+ set -uo pipefail
6
+ HERE="$(cd "$(dirname "$0")" && pwd)"
7
+ OUT="${AGENT_EVAL_OUT:-/tmp/cg-offload-eval}"
8
+ GT="$HERE/offload-eval-ground-truth.json"
9
+ REPS="${REPS:-3}"
10
+ export RESULTS="$OUT/results.jsonl"
11
+ : > "$RESULTS"
12
+ for repo in mtkruto postybirb shapeshift trezor; do
13
+ case "$repo" in mtkruto) tier=small;; postybirb) tier=medium;; shapeshift) tier=complex;; trezor) tier=large;; esac
14
+ Q=$(node -e "console.log(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))[process.argv[2]].question)" "$GT" "$repo")
15
+ echo ""; echo "### $repo ($tier) $(date +%H:%M:%S)"
16
+ bash "$HERE/offload-eval-3arm.sh" "$OUT/repos/$repo" "$tier" "$REPS" "$Q"
17
+ done
18
+ echo ""; echo "###### MATRIX DONE -> $RESULTS ($(wc -l < "$RESULTS") runs). Judge + summarize with:"
19
+ echo " node $HERE/offload-eval-judge.mjs --results $RESULTS --truth $GT --out $OUT/judged.jsonl"
20
+ echo " node $HERE/offload-eval-summarize.mjs $OUT/judged.jsonl"
@@ -1,94 +1,94 @@
1
- #!/usr/bin/env node
2
- // Extract one eval run's metrics from its Claude stream-json transcript + the
3
- // offload usage sidecar log, emit ONE merged JSON line.
4
- //
5
- // Usage: extract-metrics.mjs --run <run.jsonl> --usage <usage.jsonl|-> \
6
- // --arm <a> --rep <n> --repo <r> --tier <t> --q <question>
7
- import { readFileSync, existsSync } from 'fs';
8
-
9
- const args = {};
10
- for (let i = 2; i < process.argv.length; i += 2) args[process.argv[i].replace(/^--/, '')] = process.argv[i + 1];
11
-
12
- const runFile = args.run;
13
- const lines = existsSync(runFile) ? readFileSync(runFile, 'utf8').split('\n').filter(Boolean) : [];
14
-
15
- const toolCounts = {};
16
- let result = null;
17
- const tok = { gen: 0, fresh: 0, cached: 0 };
18
- const offloadAnswers = [];
19
- let exploreResults = 0; // tool_results from explore (offload or raw)
20
- let lastAssistantText = '';
21
-
22
- for (const line of lines) {
23
- let ev; try { ev = JSON.parse(line); } catch { continue; }
24
-
25
- // per-turn token usage (authoritative token measure; result.usage is last-turn only)
26
- const u = ev.message?.usage;
27
- if (u) {
28
- tok.gen += u.output_tokens || 0;
29
- tok.fresh += (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0);
30
- tok.cached += u.cache_read_input_tokens || 0;
31
- }
32
-
33
- if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
34
- for (const b of ev.message.content) {
35
- if (b.type === 'tool_use') toolCounts[b.name] = (toolCounts[b.name] || 0) + 1;
36
- if (b.type === 'text' && b.text?.trim()) lastAssistantText = b.text.trim();
37
- }
38
- }
39
- // tool_results arrive in user messages
40
- if (ev.type === 'user' && Array.isArray(ev.message?.content)) {
41
- for (const b of ev.message.content) {
42
- if (b.type !== 'tool_result') continue;
43
- const text = Array.isArray(b.content)
44
- ? b.content.map(c => (typeof c === 'string' ? c : c.text || '')).join('')
45
- : (typeof b.content === 'string' ? b.content : '');
46
- // An offload answer is either the 'plain'/'report' synthesis (carries the
47
- // "Synthesized by HomeGraph" footer) or a 'refs' answer (carries the re-expanded
48
- // "### Referenced source — verbatim" appendix). A refs call that cited nothing
49
- // valid falls back to RAW source, which is correctly counted as a raw explore below.
50
- if (/Synthesized by HomeGraph|### Referenced source — verbatim/.test(text)) { offloadAnswers.push(text); exploreResults++; }
51
- else if (/Found \d+ symbols? across|\*\*Exploration:/.test(text)) exploreResults++;
52
- }
53
- }
54
- if (ev.type === 'result') result = ev;
55
- }
56
-
57
- // offload usage sidecar (HomeGraph AI tokens + cost) — one JSON line per offload call
58
- const ai = { calls: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, credits: 0, costUsd: 0, ms: 0 };
59
- if (args.usage && args.usage !== '-' && existsSync(args.usage)) {
60
- for (const line of readFileSync(args.usage, 'utf8').split('\n').filter(Boolean)) {
61
- let e; try { e = JSON.parse(line); } catch { continue; }
62
- ai.calls++;
63
- ai.promptTokens += e.promptTokens || 0;
64
- ai.completionTokens += e.completionTokens || 0;
65
- ai.totalTokens += e.totalTokens || 0;
66
- ai.credits += e.creditsCharged || 0;
67
- ai.costUsd += e.costUsd || 0;
68
- ai.ms += e.ms || 0;
69
- }
70
- }
71
-
72
- // front-load hook fired iff its injected header appears in the transcript
73
- const frontload = lines.some(l => l.includes('auto-retrieved for this question'));
74
- const get = (n) => toolCounts[n] || 0;
75
- const read = get('Read');
76
- const grep = get('Grep') + get('Bash') + get('Glob');
77
- const explore = get('mcp__homegraph__homegraph_explore');
78
- const cgAny = Object.keys(toolCounts).filter(k => /mcp__homegraph__/.test(k)).reduce((s, k) => s + toolCounts[k], 0);
79
-
80
- const out = {
81
- repo: args.repo, tier: args.tier, arm: args.arm, rep: Number(args.rep), question: args.q,
82
- ok: result?.subtype === 'success',
83
- durationSec: result ? +(result.duration_ms / 1000).toFixed(1) : null,
84
- numTurns: result?.num_turns ?? null,
85
- costUsdMain: result ? +(result.total_cost_usd || 0).toFixed(4) : null,
86
- tokGen: tok.gen, tokFresh: tok.fresh, tokCached: tok.cached, tokBillable: tok.gen + tok.fresh,
87
- read, grep, explore, cgAny, frontload,
88
- offloadFired: offloadAnswers.length,
89
- ai,
90
- // text payloads for the accuracy judge (kept separate; large)
91
- finalAnswer: (result?.result || lastAssistantText || '').slice(0, 8000),
92
- offloadAnswers: offloadAnswers.map(a => a.slice(0, 6000)),
93
- };
94
- process.stdout.write(JSON.stringify(out) + '\n');
1
+ #!/usr/bin/env node
2
+ // Extract one eval run's metrics from its Claude stream-json transcript + the
3
+ // offload usage sidecar log, emit ONE merged JSON line.
4
+ //
5
+ // Usage: extract-metrics.mjs --run <run.jsonl> --usage <usage.jsonl|-> \
6
+ // --arm <a> --rep <n> --repo <r> --tier <t> --q <question>
7
+ import { readFileSync, existsSync } from 'fs';
8
+
9
+ const args = {};
10
+ for (let i = 2; i < process.argv.length; i += 2) args[process.argv[i].replace(/^--/, '')] = process.argv[i + 1];
11
+
12
+ const runFile = args.run;
13
+ const lines = existsSync(runFile) ? readFileSync(runFile, 'utf8').split('\n').filter(Boolean) : [];
14
+
15
+ const toolCounts = {};
16
+ let result = null;
17
+ const tok = { gen: 0, fresh: 0, cached: 0 };
18
+ const offloadAnswers = [];
19
+ let exploreResults = 0; // tool_results from explore (offload or raw)
20
+ let lastAssistantText = '';
21
+
22
+ for (const line of lines) {
23
+ let ev; try { ev = JSON.parse(line); } catch { continue; }
24
+
25
+ // per-turn token usage (authoritative token measure; result.usage is last-turn only)
26
+ const u = ev.message?.usage;
27
+ if (u) {
28
+ tok.gen += u.output_tokens || 0;
29
+ tok.fresh += (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0);
30
+ tok.cached += u.cache_read_input_tokens || 0;
31
+ }
32
+
33
+ if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
34
+ for (const b of ev.message.content) {
35
+ if (b.type === 'tool_use') toolCounts[b.name] = (toolCounts[b.name] || 0) + 1;
36
+ if (b.type === 'text' && b.text?.trim()) lastAssistantText = b.text.trim();
37
+ }
38
+ }
39
+ // tool_results arrive in user messages
40
+ if (ev.type === 'user' && Array.isArray(ev.message?.content)) {
41
+ for (const b of ev.message.content) {
42
+ if (b.type !== 'tool_result') continue;
43
+ const text = Array.isArray(b.content)
44
+ ? b.content.map(c => (typeof c === 'string' ? c : c.text || '')).join('')
45
+ : (typeof b.content === 'string' ? b.content : '');
46
+ // An offload answer is either the 'plain'/'report' synthesis (carries the
47
+ // "Synthesized by HomeGraph" footer) or a 'refs' answer (carries the re-expanded
48
+ // "### Referenced source — verbatim" appendix). A refs call that cited nothing
49
+ // valid falls back to RAW source, which is correctly counted as a raw explore below.
50
+ if (/Synthesized by HomeGraph|### Referenced source — verbatim/.test(text)) { offloadAnswers.push(text); exploreResults++; }
51
+ else if (/Found \d+ symbols? across|\*\*Exploration:/.test(text)) exploreResults++;
52
+ }
53
+ }
54
+ if (ev.type === 'result') result = ev;
55
+ }
56
+
57
+ // offload usage sidecar (HomeGraph AI tokens + cost) — one JSON line per offload call
58
+ const ai = { calls: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0, credits: 0, costUsd: 0, ms: 0 };
59
+ if (args.usage && args.usage !== '-' && existsSync(args.usage)) {
60
+ for (const line of readFileSync(args.usage, 'utf8').split('\n').filter(Boolean)) {
61
+ let e; try { e = JSON.parse(line); } catch { continue; }
62
+ ai.calls++;
63
+ ai.promptTokens += e.promptTokens || 0;
64
+ ai.completionTokens += e.completionTokens || 0;
65
+ ai.totalTokens += e.totalTokens || 0;
66
+ ai.credits += e.creditsCharged || 0;
67
+ ai.costUsd += e.costUsd || 0;
68
+ ai.ms += e.ms || 0;
69
+ }
70
+ }
71
+
72
+ // front-load hook fired iff its injected header appears in the transcript
73
+ const frontload = lines.some(l => l.includes('auto-retrieved for this question'));
74
+ const get = (n) => toolCounts[n] || 0;
75
+ const read = get('Read');
76
+ const grep = get('Grep') + get('Bash') + get('Glob');
77
+ const explore = get('mcp__homegraph__homegraph_explore');
78
+ const cgAny = Object.keys(toolCounts).filter(k => /mcp__homegraph__/.test(k)).reduce((s, k) => s + toolCounts[k], 0);
79
+
80
+ const out = {
81
+ repo: args.repo, tier: args.tier, arm: args.arm, rep: Number(args.rep), question: args.q,
82
+ ok: result?.subtype === 'success',
83
+ durationSec: result ? +(result.duration_ms / 1000).toFixed(1) : null,
84
+ numTurns: result?.num_turns ?? null,
85
+ costUsdMain: result ? +(result.total_cost_usd || 0).toFixed(4) : null,
86
+ tokGen: tok.gen, tokFresh: tok.fresh, tokCached: tok.cached, tokBillable: tok.gen + tok.fresh,
87
+ read, grep, explore, cgAny, frontload,
88
+ offloadFired: offloadAnswers.length,
89
+ ai,
90
+ // text payloads for the accuracy judge (kept separate; large)
91
+ finalAnswer: (result?.result || lastAssistantText || '').slice(0, 8000),
92
+ offloadAnswers: offloadAnswers.map(a => a.slice(0, 6000)),
93
+ };
94
+ process.stdout.write(JSON.stringify(out) + '\n');