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,72 +1,72 @@
1
- #!/usr/bin/env bash
2
- # 3-arm offload eval for ONE indexed repo + ONE question, n reps each.
3
- # ARM offload : homegraph attached, managed offload ON (per-run AI usage log)
4
- # ARM raw : homegraph attached, HOMEGRAPH_OFFLOAD_DISABLE=1 (raw source)
5
- # ARM nocg : no homegraph (empty MCP config) -> Read/Grep baseline
6
- # All arms: claude -p sonnet --effort high. One JSON metrics line/run -> $RESULTS.
7
- #
8
- # Usage: offload-eval-3arm.sh <indexed-repo> <tier> <reps> "<question>"
9
- # Env: MODEL=sonnet EFFORT=high RESULTS=<file> AGENT_EVAL_OUT=<scratch dir>
10
- set -uo pipefail
11
- HERE="$(cd "$(dirname "$0")" && pwd)"
12
- ENGINE="$(cd "$HERE/../.." && pwd)"
13
- BIN="$ENGINE/dist/bin/homegraph.js"
14
- OUT="${AGENT_EVAL_OUT:-/tmp/cg-offload-eval}"
15
- TARGET="${1:?usage: offload-eval-3arm.sh <indexed-repo> <tier> <reps> \"<question>\"}"
16
- TIER="${2:?tier}"; REPS="${3:?reps}"; Q="${4:?question}"
17
- RUNS="$OUT/runs"
18
- EXTRACT="$HERE/offload-eval-metrics.mjs"
19
- RESULTS="${RESULTS:-$OUT/results.jsonl}"
20
- REPO=$(basename "$TARGET")
21
- mkdir -p "$RUNS"
22
- command -v claude >/dev/null || { echo "no claude on PATH"; exit 1; }
23
- [ -d "$TARGET/.homegraph" ] || { echo "not indexed: $TARGET (run offload-eval-setup.sh first)"; exit 1; }
24
- # Physical path so pkill matches the daemon's real cmdline (macOS /tmp->/private/tmp symlink
25
- # otherwise makes the kill miss the daemon, and the next arm connects to the SURVIVING daemon
26
- # — contaminating the raw arm with offload).
27
- TARGET=$(cd "$TARGET" && pwd -P)
28
-
29
- prewarm() { # path extra-env (e.g. "FOO=bar")
30
- pkill -9 -f "serve --mcp --path $1" 2>/dev/null; rm -f "$1/.homegraph/daemon.sock" 2>/dev/null; sleep 0.6
31
- env ${2:-} HOMEGRAPH_DAEMON_IDLE_TIMEOUT_MS=1800000 node "$BIN" serve --mcp --path "$1" </dev/null >/dev/null 2>&1 &
32
- node -e 'const fs=require("fs");let n=0;const t=setInterval(()=>{if(fs.existsSync(process.argv[1]+"/.homegraph/daemon.sock")){clearInterval(t);process.exit(0)}if(n++>150){clearInterval(t);process.exit(1)}},100)' "$1" \
33
- && echo " daemon warm" || echo " WARN daemon never bound"
34
- }
35
-
36
- run() { # arm rep mcp-config usage-log-or-dash
37
- local arm="$1" rep="$2" cfg="$3" usage="$4" tag="$REPO-$1-$2"
38
- [ "$usage" != "-" ] && : > "$usage"
39
- # DISALLOW (optional): block sub-agent delegation across all arms so the A/B
40
- # measures the retrieval mode, not whether Sonnet decides to spawn a homegraph-blind
41
- # Explore subagent (which thrashes regardless and adds huge variance).
42
- ( cd "$TARGET" && claude -p "$Q" \
43
- --output-format stream-json --verbose --permission-mode bypassPermissions \
44
- --model "${MODEL:-sonnet}" --effort "${EFFORT:-high}" --max-budget-usd 4 \
45
- ${DISALLOW:+--disallowedTools "$DISALLOW"} \
46
- --strict-mcp-config --mcp-config "$cfg" \
47
- </dev/null > "$RUNS/$tag.jsonl" 2>"$RUNS/$tag.err" )
48
- node "$EXTRACT" --run "$RUNS/$tag.jsonl" --usage "$usage" --arm "$arm" --rep "$rep" \
49
- --repo "$REPO" --tier "$TIER" --q "$Q" >> "$RESULTS"
50
- node -e 'const o=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8").trim().split("\n").pop());console.log(` [${o.arm} #${o.rep}] ${o.durationSec}s | main $${o.costUsdMain} ${o.tokBillable} tok | read=${o.read} grep=${o.grep} explore=${o.explore} offload=${o.offloadFired} | AI ${o.ai.calls}call/${o.ai.totalTokens}tok/$${o.ai.costUsd.toFixed(4)} | ok=${o.ok}`)' "$RESULTS"
51
- }
52
-
53
- CFG_OFF="$RUNS/mcp-offload-$REPO.json"; CFG_RAW="$RUNS/mcp-raw-$REPO.json"; CFG_NOCG="$RUNS/mcp-nocg.json"
54
- USAGE="$RUNS/$REPO-usage.jsonl"
55
- printf '{"mcpServers":{"homegraph":{"command":"env","args":["HOMEGRAPH_WASM_RELAUNCHED=1","HOMEGRAPH_OFFLOAD_USAGE_LOG=%s","node","%s","serve","--mcp","--path","%s"]}}}' "$USAGE" "$BIN" "$TARGET" > "$CFG_OFF"
56
- printf '{"mcpServers":{"homegraph":{"command":"env","args":["HOMEGRAPH_WASM_RELAUNCHED=1","HOMEGRAPH_OFFLOAD_DISABLE=1","node","%s","serve","--mcp","--path","%s"]}}}' "$BIN" "$TARGET" > "$CFG_RAW"
57
- printf '{"mcpServers":{}}' > "$CFG_NOCG"
58
-
59
- # REP_START lets a later batch ADD reps without clobbering earlier jsonls
60
- # (e.g. REP_START=4 REPS=3 -> reps 4,5,6; default starts at 1).
61
- START="${REP_START:-1}"; END=$((START + REPS - 1))
62
- echo "###### repo=$REPO tier=$TIER reps=$START..$END model=${MODEL:-sonnet}/${EFFORT:-high}"
63
- echo "###### Q=$Q"
64
- echo "== ARM offload =="; prewarm "$TARGET" "HOMEGRAPH_OFFLOAD_USAGE_LOG=$USAGE"
65
- for r in $(seq "$START" "$END"); do run offload "$r" "$CFG_OFF" "$USAGE"; done
66
- pkill -9 -f "serve --mcp --path $TARGET" 2>/dev/null; rm -f "$TARGET/.homegraph/daemon.sock" 2>/dev/null; sleep 1
67
- echo "== ARM raw =="; prewarm "$TARGET" "HOMEGRAPH_OFFLOAD_DISABLE=1"
68
- for r in $(seq "$START" "$END"); do run raw "$r" "$CFG_RAW" "-"; done
69
- pkill -9 -f "serve --mcp --path $TARGET" 2>/dev/null; rm -f "$TARGET/.homegraph/daemon.sock" 2>/dev/null; sleep 1
70
- echo "== ARM nocg =="
71
- for r in $(seq "$START" "$END"); do run nocg "$r" "$CFG_NOCG" "-"; done
72
- echo "###### DONE $REPO"
1
+ #!/usr/bin/env bash
2
+ # 3-arm offload eval for ONE indexed repo + ONE question, n reps each.
3
+ # ARM offload : homegraph attached, managed offload ON (per-run AI usage log)
4
+ # ARM raw : homegraph attached, HOMEGRAPH_OFFLOAD_DISABLE=1 (raw source)
5
+ # ARM nocg : no homegraph (empty MCP config) -> Read/Grep baseline
6
+ # All arms: claude -p sonnet --effort high. One JSON metrics line/run -> $RESULTS.
7
+ #
8
+ # Usage: offload-eval-3arm.sh <indexed-repo> <tier> <reps> "<question>"
9
+ # Env: MODEL=sonnet EFFORT=high RESULTS=<file> AGENT_EVAL_OUT=<scratch dir>
10
+ set -uo pipefail
11
+ HERE="$(cd "$(dirname "$0")" && pwd)"
12
+ ENGINE="$(cd "$HERE/../.." && pwd)"
13
+ BIN="$ENGINE/dist/bin/homegraph.js"
14
+ OUT="${AGENT_EVAL_OUT:-/tmp/cg-offload-eval}"
15
+ TARGET="${1:?usage: offload-eval-3arm.sh <indexed-repo> <tier> <reps> \"<question>\"}"
16
+ TIER="${2:?tier}"; REPS="${3:?reps}"; Q="${4:?question}"
17
+ RUNS="$OUT/runs"
18
+ EXTRACT="$HERE/offload-eval-metrics.mjs"
19
+ RESULTS="${RESULTS:-$OUT/results.jsonl}"
20
+ REPO=$(basename "$TARGET")
21
+ mkdir -p "$RUNS"
22
+ command -v claude >/dev/null || { echo "no claude on PATH"; exit 1; }
23
+ [ -d "$TARGET/.homegraph" ] || { echo "not indexed: $TARGET (run offload-eval-setup.sh first)"; exit 1; }
24
+ # Physical path so pkill matches the daemon's real cmdline (macOS /tmp->/private/tmp symlink
25
+ # otherwise makes the kill miss the daemon, and the next arm connects to the SURVIVING daemon
26
+ # — contaminating the raw arm with offload).
27
+ TARGET=$(cd "$TARGET" && pwd -P)
28
+
29
+ prewarm() { # path extra-env (e.g. "FOO=bar")
30
+ pkill -9 -f "serve --mcp --path $1" 2>/dev/null; rm -f "$1/.homegraph/daemon.sock" 2>/dev/null; sleep 0.6
31
+ env ${2:-} HOMEGRAPH_DAEMON_IDLE_TIMEOUT_MS=1800000 node "$BIN" serve --mcp --path "$1" </dev/null >/dev/null 2>&1 &
32
+ node -e 'const fs=require("fs");let n=0;const t=setInterval(()=>{if(fs.existsSync(process.argv[1]+"/.homegraph/daemon.sock")){clearInterval(t);process.exit(0)}if(n++>150){clearInterval(t);process.exit(1)}},100)' "$1" \
33
+ && echo " daemon warm" || echo " WARN daemon never bound"
34
+ }
35
+
36
+ run() { # arm rep mcp-config usage-log-or-dash
37
+ local arm="$1" rep="$2" cfg="$3" usage="$4" tag="$REPO-$1-$2"
38
+ [ "$usage" != "-" ] && : > "$usage"
39
+ # DISALLOW (optional): block sub-agent delegation across all arms so the A/B
40
+ # measures the retrieval mode, not whether Sonnet decides to spawn a homegraph-blind
41
+ # Explore subagent (which thrashes regardless and adds huge variance).
42
+ ( cd "$TARGET" && claude -p "$Q" \
43
+ --output-format stream-json --verbose --permission-mode bypassPermissions \
44
+ --model "${MODEL:-sonnet}" --effort "${EFFORT:-high}" --max-budget-usd 4 \
45
+ ${DISALLOW:+--disallowedTools "$DISALLOW"} \
46
+ --strict-mcp-config --mcp-config "$cfg" \
47
+ </dev/null > "$RUNS/$tag.jsonl" 2>"$RUNS/$tag.err" )
48
+ node "$EXTRACT" --run "$RUNS/$tag.jsonl" --usage "$usage" --arm "$arm" --rep "$rep" \
49
+ --repo "$REPO" --tier "$TIER" --q "$Q" >> "$RESULTS"
50
+ node -e 'const o=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8").trim().split("\n").pop());console.log(` [${o.arm} #${o.rep}] ${o.durationSec}s | main $${o.costUsdMain} ${o.tokBillable} tok | read=${o.read} grep=${o.grep} explore=${o.explore} offload=${o.offloadFired} | AI ${o.ai.calls}call/${o.ai.totalTokens}tok/$${o.ai.costUsd.toFixed(4)} | ok=${o.ok}`)' "$RESULTS"
51
+ }
52
+
53
+ CFG_OFF="$RUNS/mcp-offload-$REPO.json"; CFG_RAW="$RUNS/mcp-raw-$REPO.json"; CFG_NOCG="$RUNS/mcp-nocg.json"
54
+ USAGE="$RUNS/$REPO-usage.jsonl"
55
+ printf '{"mcpServers":{"homegraph":{"command":"env","args":["HOMEGRAPH_WASM_RELAUNCHED=1","HOMEGRAPH_OFFLOAD_USAGE_LOG=%s","node","%s","serve","--mcp","--path","%s"]}}}' "$USAGE" "$BIN" "$TARGET" > "$CFG_OFF"
56
+ printf '{"mcpServers":{"homegraph":{"command":"env","args":["HOMEGRAPH_WASM_RELAUNCHED=1","HOMEGRAPH_OFFLOAD_DISABLE=1","node","%s","serve","--mcp","--path","%s"]}}}' "$BIN" "$TARGET" > "$CFG_RAW"
57
+ printf '{"mcpServers":{}}' > "$CFG_NOCG"
58
+
59
+ # REP_START lets a later batch ADD reps without clobbering earlier jsonls
60
+ # (e.g. REP_START=4 REPS=3 -> reps 4,5,6; default starts at 1).
61
+ START="${REP_START:-1}"; END=$((START + REPS - 1))
62
+ echo "###### repo=$REPO tier=$TIER reps=$START..$END model=${MODEL:-sonnet}/${EFFORT:-high}"
63
+ echo "###### Q=$Q"
64
+ echo "== ARM offload =="; prewarm "$TARGET" "HOMEGRAPH_OFFLOAD_USAGE_LOG=$USAGE"
65
+ for r in $(seq "$START" "$END"); do run offload "$r" "$CFG_OFF" "$USAGE"; done
66
+ pkill -9 -f "serve --mcp --path $TARGET" 2>/dev/null; rm -f "$TARGET/.homegraph/daemon.sock" 2>/dev/null; sleep 1
67
+ echo "== ARM raw =="; prewarm "$TARGET" "HOMEGRAPH_OFFLOAD_DISABLE=1"
68
+ for r in $(seq "$START" "$END"); do run raw "$r" "$CFG_RAW" "-"; done
69
+ pkill -9 -f "serve --mcp --path $TARGET" 2>/dev/null; rm -f "$TARGET/.homegraph/daemon.sock" 2>/dev/null; sleep 1
70
+ echo "== ARM nocg =="
71
+ for r in $(seq "$START" "$END"); do run nocg "$r" "$CFG_NOCG" "-"; done
72
+ echo "###### DONE $REPO"
@@ -1,133 +1,133 @@
1
- #!/usr/bin/env node
2
- // Cost/token analysis for the 3-arm offload eval, with a MAIN-vs-SUBAGENT split.
3
- //
4
- // The explore-subagent question. With delegation ALLOWED, the nocg arm spawns a
5
- // Claude Code Explore subagent; the homegraph arms do all work in the main agent.
6
- // Two facts make naive accounting wrong:
7
- // 1. The Explore subagent runs on HAIKU 4.5; the main agent on SONNET 4.6.
8
- // So per-token cost differs ~3x between them — you cannot price both the same.
9
- // 2. The subagent's consumption is ~95% cache-reads. At Haiku's $0.10/MTok
10
- // cache-read rate, a huge TOKEN volume is a small DOLLAR cost.
11
- //
12
- // Rather than re-derive cost from raw token counts (and guess the cache TTL —
13
- // Claude Code uses 1-hour ephemeral cache here, 2x write, not 5-min), we read
14
- // Claude Code's OWN authoritative accounting from the `result` event:
15
- // result.modelUsage[model].costUSD — per-model cost CC itself billed
16
- // result.total_cost_usd — their sum (INCLUDES the Haiku subagent;
17
- // the handoff's "excludes subagent" was wrong)
18
- // The model split IS the agent split here: sonnet => main, haiku => Explore subagent
19
- // (only nocg spawns one, and only nocg shows haiku usage). Token volume is still
20
- // summed per-model from modelUsage for the separate "tokens" story.
21
- //
22
- // Usage: offload-eval-cost.mjs <runs-dir> <repo> [reps]
23
- // e.g. offload-eval-cost.mjs /tmp/cg-offload-eval/runs trezor 3
24
- import { readFileSync, existsSync } from 'fs';
25
-
26
- const MAIN_TIER = /sonnet/; // main agent
27
- const SUB_TIER = /haiku/; // Claude Code Explore subagent
28
-
29
- const [,, runsDir, repo, repsArg] = process.argv;
30
- if (!runsDir || !repo) { console.error('usage: offload-eval-cost.mjs <runs-dir> <repo> [reps] (env ARMS=nocg,raw,offload)'); process.exit(1); }
31
- const REPS = Number(repsArg || 3);
32
- // Arms to analyze (file stems `<repo>-<arm>-<rep>.jsonl`). Override for the style A/B:
33
- // ARMS=raw,refs,map,src. nocg's Haiku subagent is the only sub-tier; the rest are main-only.
34
- const ARMS = (process.env.ARMS || 'nocg,raw,offload').split(',').map((s) => s.trim()).filter(Boolean);
35
-
36
- const toks = (u) => (u.inputTokens||0)+(u.outputTokens||0)+(u.cacheReadInputTokens||0)+(u.cacheCreationInputTokens||0);
37
-
38
- function analyzeRun(file) {
39
- let result = null, agentCalls = 0;
40
- const tools = {}, subPids = new Set();
41
- for (const line of readFileSync(file, 'utf8').split('\n')) {
42
- if (!line) continue;
43
- let e; try { e = JSON.parse(line); } catch { continue; }
44
- if (e.parent_tool_use_id && e.message?.usage) subPids.add(e.parent_tool_use_id);
45
- if (e.type === 'assistant' && Array.isArray(e.message?.content))
46
- for (const b of e.message.content)
47
- if (b.type === 'tool_use') { tools[b.name] = (tools[b.name]||0)+1; if (b.name === 'Agent') agentCalls++; }
48
- if (e.type === 'result') result = e;
49
- }
50
- // Authoritative cost + tokens from Claude Code's per-model accounting.
51
- const mu = result?.modelUsage || {};
52
- const main = { cost: 0, tok: 0 }, sub = { cost: 0, tok: 0 };
53
- for (const [model, u] of Object.entries(mu)) {
54
- const bucket = SUB_TIER.test(model) ? sub : main; // sonnet/anything-else => main
55
- bucket.cost += u.costUSD || 0;
56
- bucket.tok += toks(u);
57
- }
58
- return {
59
- main, sub, subagents: subPids.size, agentCalls,
60
- ccTotal: result?.total_cost_usd ?? null,
61
- ok: result?.subtype === 'success',
62
- durationSec: result?.duration_ms ? +(result.duration_ms/1000).toFixed(1) : null,
63
- models: Object.keys(mu), tools,
64
- };
65
- }
66
-
67
- const k = (n) => (n/1000).toFixed(0).padStart(5) + 'K';
68
- const d = (n) => '$' + n.toFixed(3);
69
- const cost = (b) => b.cost;
70
- const tot = (b) => b.tok;
71
-
72
- const byArm = {};
73
- for (const arm of ARMS) {
74
- const runs = [];
75
- for (let r = 1; r <= REPS; r++) {
76
- const f = `${runsDir}/${repo}-${arm}-${r}.jsonl`;
77
- if (existsSync(f)) runs.push({ rep: r, ...analyzeRun(f) });
78
- }
79
- byArm[arm] = runs;
80
- }
81
-
82
- // Per-run detail. Cost is Claude Code's own modelUsage.costUSD (authoritative,
83
- // per-model pricing + correct cache TTL). MAIN=Sonnet, SUB=Haiku Explore subagent.
84
- // cc-check: main$+sub$ must equal result.total_cost_usd (delta should be ~0).
85
- console.log(`\n=== ${repo}: per-run main(Sonnet)/sub(Haiku) split — Claude Code's own cost accounting ===`);
86
- console.log('arm rep | subAg | MAIN(sonnet) tok / $ | SUB(haiku) tok / $ | TOTAL tok / $ | cc_total Δ | dur reads');
87
- for (const arm of ARMS) for (const r of byArm[arm]) {
88
- const mC = cost(r.main), sC = cost(r.sub), mT = tot(r.main), sT = tot(r.sub);
89
- const reads = r.tools['Read'] || 0, grep = (r.tools['Grep']||0)+(r.tools['Bash']||0)+(r.tools['Glob']||0);
90
- const explore = r.tools['mcp__homegraph__homegraph_explore'] || 0;
91
- const delta = (mC + sC) - (r.ccTotal || 0); // should be ~0
92
- console.log(
93
- `${arm.padEnd(8)} #${r.rep} | ${String(r.subagents).padStart(2)} | ${k(mT)} ${d(mC).padStart(7)} | ${k(sT)} ${d(sC).padStart(7)} | ${k(mT+sT)} ${d(mC+sC).padStart(7)} | ${d(r.ccTotal||0).padStart(7)} ${(delta>=0?'+':'')+delta.toFixed(4)} | ${String(r.durationSec).padStart(5)} r=${reads} g=${grep} x=${explore}`
94
- );
95
- }
96
-
97
- // Per-arm means
98
- const mean = (arr, f) => arr.length ? arr.reduce((s,x)=>s+f(x),0)/arr.length : 0;
99
- console.log(`\n=== ${repo}: per-arm MEANS (n per arm) ===`);
100
- console.log('arm n | main $ sub $ TOTAL $ | main tok sub tok TOTAL tok | %$ in sub | %tok in sub');
101
- for (const arm of ARMS) {
102
- const runs = byArm[arm]; if (!runs.length) continue;
103
- const mC = mean(runs, r=>cost(r.main)), sC = mean(runs, r=>cost(r.sub));
104
- const mT = mean(runs, r=>tot(r.main)), sT = mean(runs, r=>tot(r.sub));
105
- const pctSubC = (mC+sC) ? (100*sC/(mC+sC)) : 0;
106
- const pctSubT = (mT+sT) ? (100*sT/(mT+sT)) : 0;
107
- console.log(
108
- `${arm.padEnd(8)} ${runs.length} | ${d(mC).padStart(7)} ${d(sC).padStart(7)} ${d(mC+sC).padStart(7)} | ${k(mT)} ${k(sT)} ${k(mT+sT)} | ${pctSubC.toFixed(0).padStart(3)}% | ${pctSubT.toFixed(0).padStart(3)}%`
109
- );
110
- }
111
-
112
- // Headline ladders — cost, tokens, duration, all vs a baseline (nocg if present, else first arm).
113
- console.log(`\n=== Ladders (mean, incl. subagent) ===`);
114
- const totals = ARMS.map(a => ({ a, c: mean(byArm[a], r=>cost(r.main)+cost(r.sub)), t: mean(byArm[a], r=>tot(r.main)+tot(r.sub)) })).filter(x=>byArm[x.a].length);
115
- const base = totals.find(x=>x.a==='nocg') ?? totals[0];
116
- const bn = base?.a ?? '?';
117
- console.log(` COST (vs ${bn}):`);
118
- for (const x of totals) {
119
- const vs = base && base.c ? ` (${((x.c/base.c-1)*100>=0?'+':'')}${((x.c/base.c-1)*100).toFixed(0)}%)` : '';
120
- console.log(` ${x.a.padEnd(8)} ${d(x.c)}${vs}`);
121
- }
122
- console.log(` TOKENS (vs ${bn}):`);
123
- for (const x of totals) {
124
- const vs = base && base.t ? ` (${((x.t/base.t-1)*100>=0?'+':'')}${((x.t/base.t-1)*100).toFixed(0)}%)` : '';
125
- console.log(` ${x.a.padEnd(8)} ${k(x.t)}${vs}`);
126
- }
127
- console.log(` DURATION (wall-clock, vs ${bn}):`);
128
- const durs = ARMS.map(a => ({ a, s: mean(byArm[a].filter(r=>r.durationSec!=null), r=>r.durationSec) })).filter(x=>byArm[x.a].length);
129
- const dbase = durs.find(x=>x.a==='nocg') ?? durs[0];
130
- for (const x of durs) {
131
- const vs = dbase && dbase.s ? ` (${((x.s/dbase.s-1)*100>=0?'+':'')}${((x.s/dbase.s-1)*100).toFixed(0)}%)` : '';
132
- console.log(` ${x.a.padEnd(8)} ${x.s.toFixed(0)}s${vs}`);
133
- }
1
+ #!/usr/bin/env node
2
+ // Cost/token analysis for the 3-arm offload eval, with a MAIN-vs-SUBAGENT split.
3
+ //
4
+ // The explore-subagent question. With delegation ALLOWED, the nocg arm spawns a
5
+ // Claude Code Explore subagent; the homegraph arms do all work in the main agent.
6
+ // Two facts make naive accounting wrong:
7
+ // 1. The Explore subagent runs on HAIKU 4.5; the main agent on SONNET 4.6.
8
+ // So per-token cost differs ~3x between them — you cannot price both the same.
9
+ // 2. The subagent's consumption is ~95% cache-reads. At Haiku's $0.10/MTok
10
+ // cache-read rate, a huge TOKEN volume is a small DOLLAR cost.
11
+ //
12
+ // Rather than re-derive cost from raw token counts (and guess the cache TTL —
13
+ // Claude Code uses 1-hour ephemeral cache here, 2x write, not 5-min), we read
14
+ // Claude Code's OWN authoritative accounting from the `result` event:
15
+ // result.modelUsage[model].costUSD — per-model cost CC itself billed
16
+ // result.total_cost_usd — their sum (INCLUDES the Haiku subagent;
17
+ // the handoff's "excludes subagent" was wrong)
18
+ // The model split IS the agent split here: sonnet => main, haiku => Explore subagent
19
+ // (only nocg spawns one, and only nocg shows haiku usage). Token volume is still
20
+ // summed per-model from modelUsage for the separate "tokens" story.
21
+ //
22
+ // Usage: offload-eval-cost.mjs <runs-dir> <repo> [reps]
23
+ // e.g. offload-eval-cost.mjs /tmp/cg-offload-eval/runs trezor 3
24
+ import { readFileSync, existsSync } from 'fs';
25
+
26
+ const MAIN_TIER = /sonnet/; // main agent
27
+ const SUB_TIER = /haiku/; // Claude Code Explore subagent
28
+
29
+ const [,, runsDir, repo, repsArg] = process.argv;
30
+ if (!runsDir || !repo) { console.error('usage: offload-eval-cost.mjs <runs-dir> <repo> [reps] (env ARMS=nocg,raw,offload)'); process.exit(1); }
31
+ const REPS = Number(repsArg || 3);
32
+ // Arms to analyze (file stems `<repo>-<arm>-<rep>.jsonl`). Override for the style A/B:
33
+ // ARMS=raw,refs,map,src. nocg's Haiku subagent is the only sub-tier; the rest are main-only.
34
+ const ARMS = (process.env.ARMS || 'nocg,raw,offload').split(',').map((s) => s.trim()).filter(Boolean);
35
+
36
+ const toks = (u) => (u.inputTokens||0)+(u.outputTokens||0)+(u.cacheReadInputTokens||0)+(u.cacheCreationInputTokens||0);
37
+
38
+ function analyzeRun(file) {
39
+ let result = null, agentCalls = 0;
40
+ const tools = {}, subPids = new Set();
41
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
42
+ if (!line) continue;
43
+ let e; try { e = JSON.parse(line); } catch { continue; }
44
+ if (e.parent_tool_use_id && e.message?.usage) subPids.add(e.parent_tool_use_id);
45
+ if (e.type === 'assistant' && Array.isArray(e.message?.content))
46
+ for (const b of e.message.content)
47
+ if (b.type === 'tool_use') { tools[b.name] = (tools[b.name]||0)+1; if (b.name === 'Agent') agentCalls++; }
48
+ if (e.type === 'result') result = e;
49
+ }
50
+ // Authoritative cost + tokens from Claude Code's per-model accounting.
51
+ const mu = result?.modelUsage || {};
52
+ const main = { cost: 0, tok: 0 }, sub = { cost: 0, tok: 0 };
53
+ for (const [model, u] of Object.entries(mu)) {
54
+ const bucket = SUB_TIER.test(model) ? sub : main; // sonnet/anything-else => main
55
+ bucket.cost += u.costUSD || 0;
56
+ bucket.tok += toks(u);
57
+ }
58
+ return {
59
+ main, sub, subagents: subPids.size, agentCalls,
60
+ ccTotal: result?.total_cost_usd ?? null,
61
+ ok: result?.subtype === 'success',
62
+ durationSec: result?.duration_ms ? +(result.duration_ms/1000).toFixed(1) : null,
63
+ models: Object.keys(mu), tools,
64
+ };
65
+ }
66
+
67
+ const k = (n) => (n/1000).toFixed(0).padStart(5) + 'K';
68
+ const d = (n) => '$' + n.toFixed(3);
69
+ const cost = (b) => b.cost;
70
+ const tot = (b) => b.tok;
71
+
72
+ const byArm = {};
73
+ for (const arm of ARMS) {
74
+ const runs = [];
75
+ for (let r = 1; r <= REPS; r++) {
76
+ const f = `${runsDir}/${repo}-${arm}-${r}.jsonl`;
77
+ if (existsSync(f)) runs.push({ rep: r, ...analyzeRun(f) });
78
+ }
79
+ byArm[arm] = runs;
80
+ }
81
+
82
+ // Per-run detail. Cost is Claude Code's own modelUsage.costUSD (authoritative,
83
+ // per-model pricing + correct cache TTL). MAIN=Sonnet, SUB=Haiku Explore subagent.
84
+ // cc-check: main$+sub$ must equal result.total_cost_usd (delta should be ~0).
85
+ console.log(`\n=== ${repo}: per-run main(Sonnet)/sub(Haiku) split — Claude Code's own cost accounting ===`);
86
+ console.log('arm rep | subAg | MAIN(sonnet) tok / $ | SUB(haiku) tok / $ | TOTAL tok / $ | cc_total Δ | dur reads');
87
+ for (const arm of ARMS) for (const r of byArm[arm]) {
88
+ const mC = cost(r.main), sC = cost(r.sub), mT = tot(r.main), sT = tot(r.sub);
89
+ const reads = r.tools['Read'] || 0, grep = (r.tools['Grep']||0)+(r.tools['Bash']||0)+(r.tools['Glob']||0);
90
+ const explore = r.tools['mcp__homegraph__homegraph_explore'] || 0;
91
+ const delta = (mC + sC) - (r.ccTotal || 0); // should be ~0
92
+ console.log(
93
+ `${arm.padEnd(8)} #${r.rep} | ${String(r.subagents).padStart(2)} | ${k(mT)} ${d(mC).padStart(7)} | ${k(sT)} ${d(sC).padStart(7)} | ${k(mT+sT)} ${d(mC+sC).padStart(7)} | ${d(r.ccTotal||0).padStart(7)} ${(delta>=0?'+':'')+delta.toFixed(4)} | ${String(r.durationSec).padStart(5)} r=${reads} g=${grep} x=${explore}`
94
+ );
95
+ }
96
+
97
+ // Per-arm means
98
+ const mean = (arr, f) => arr.length ? arr.reduce((s,x)=>s+f(x),0)/arr.length : 0;
99
+ console.log(`\n=== ${repo}: per-arm MEANS (n per arm) ===`);
100
+ console.log('arm n | main $ sub $ TOTAL $ | main tok sub tok TOTAL tok | %$ in sub | %tok in sub');
101
+ for (const arm of ARMS) {
102
+ const runs = byArm[arm]; if (!runs.length) continue;
103
+ const mC = mean(runs, r=>cost(r.main)), sC = mean(runs, r=>cost(r.sub));
104
+ const mT = mean(runs, r=>tot(r.main)), sT = mean(runs, r=>tot(r.sub));
105
+ const pctSubC = (mC+sC) ? (100*sC/(mC+sC)) : 0;
106
+ const pctSubT = (mT+sT) ? (100*sT/(mT+sT)) : 0;
107
+ console.log(
108
+ `${arm.padEnd(8)} ${runs.length} | ${d(mC).padStart(7)} ${d(sC).padStart(7)} ${d(mC+sC).padStart(7)} | ${k(mT)} ${k(sT)} ${k(mT+sT)} | ${pctSubC.toFixed(0).padStart(3)}% | ${pctSubT.toFixed(0).padStart(3)}%`
109
+ );
110
+ }
111
+
112
+ // Headline ladders — cost, tokens, duration, all vs a baseline (nocg if present, else first arm).
113
+ console.log(`\n=== Ladders (mean, incl. subagent) ===`);
114
+ const totals = ARMS.map(a => ({ a, c: mean(byArm[a], r=>cost(r.main)+cost(r.sub)), t: mean(byArm[a], r=>tot(r.main)+tot(r.sub)) })).filter(x=>byArm[x.a].length);
115
+ const base = totals.find(x=>x.a==='nocg') ?? totals[0];
116
+ const bn = base?.a ?? '?';
117
+ console.log(` COST (vs ${bn}):`);
118
+ for (const x of totals) {
119
+ const vs = base && base.c ? ` (${((x.c/base.c-1)*100>=0?'+':'')}${((x.c/base.c-1)*100).toFixed(0)}%)` : '';
120
+ console.log(` ${x.a.padEnd(8)} ${d(x.c)}${vs}`);
121
+ }
122
+ console.log(` TOKENS (vs ${bn}):`);
123
+ for (const x of totals) {
124
+ const vs = base && base.t ? ` (${((x.t/base.t-1)*100>=0?'+':'')}${((x.t/base.t-1)*100).toFixed(0)}%)` : '';
125
+ console.log(` ${x.a.padEnd(8)} ${k(x.t)}${vs}`);
126
+ }
127
+ console.log(` DURATION (wall-clock, vs ${bn}):`);
128
+ const durs = ARMS.map(a => ({ a, s: mean(byArm[a].filter(r=>r.durationSec!=null), r=>r.durationSec) })).filter(x=>byArm[x.a].length);
129
+ const dbase = durs.find(x=>x.a==='nocg') ?? durs[0];
130
+ for (const x of durs) {
131
+ const vs = dbase && dbase.s ? ` (${((x.s/dbase.s-1)*100>=0?'+':'')}${((x.s/dbase.s-1)*100).toFixed(0)}%)` : '';
132
+ console.log(` ${x.a.padEnd(8)} ${x.s.toFixed(0)}s${vs}`);
133
+ }