homegraph 1.1.2 → 1.2.0

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 (361) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +268 -243
  3. package/dist/arkts/ohos-api-index.d.ts +15 -0
  4. package/dist/arkts/ohos-api-index.d.ts.map +1 -0
  5. package/dist/arkts/ohos-api-index.js +190 -0
  6. package/dist/arkts/ohos-api-index.js.map +1 -0
  7. package/dist/arkts/ohos-sdk-input.d.ts +36 -0
  8. package/dist/arkts/ohos-sdk-input.d.ts.map +1 -0
  9. package/dist/arkts/ohos-sdk-input.js +214 -0
  10. package/dist/arkts/ohos-sdk-input.js.map +1 -0
  11. package/dist/bin/command-supervision.d.ts +12 -0
  12. package/dist/bin/command-supervision.d.ts.map +1 -0
  13. package/dist/bin/command-supervision.js +86 -0
  14. package/dist/bin/command-supervision.js.map +1 -0
  15. package/dist/bin/homegraph.d.ts +1 -1
  16. package/dist/bin/homegraph.js +971 -71
  17. package/dist/bin/homegraph.js.map +1 -1
  18. package/dist/db/index.d.ts +20 -0
  19. package/dist/db/index.d.ts.map +1 -1
  20. package/dist/db/index.js +39 -0
  21. package/dist/db/index.js.map +1 -1
  22. package/dist/db/migrations.d.ts +1 -1
  23. package/dist/db/migrations.d.ts.map +1 -1
  24. package/dist/db/migrations.js +58 -19
  25. package/dist/db/migrations.js.map +1 -1
  26. package/dist/db/queries.d.ts +14 -0
  27. package/dist/db/queries.d.ts.map +1 -1
  28. package/dist/db/queries.js +314 -143
  29. package/dist/db/queries.js.map +1 -1
  30. package/dist/db/schema.sql +172 -152
  31. package/dist/directory.d.ts +32 -0
  32. package/dist/directory.d.ts.map +1 -1
  33. package/dist/directory.js +88 -5
  34. package/dist/directory.js.map +1 -1
  35. package/dist/extraction/arkts-batch-worker.d.ts +2 -0
  36. package/dist/extraction/arkts-batch-worker.d.ts.map +1 -0
  37. package/dist/extraction/arkts-batch-worker.js +28 -0
  38. package/dist/extraction/arkts-batch-worker.js.map +1 -0
  39. package/dist/extraction/context.d.ts +11 -0
  40. package/dist/extraction/context.d.ts.map +1 -1
  41. package/dist/extraction/context.js +20 -0
  42. package/dist/extraction/context.js.map +1 -1
  43. package/dist/extraction/index.d.ts +16 -2
  44. package/dist/extraction/index.d.ts.map +1 -1
  45. package/dist/extraction/index.js +619 -380
  46. package/dist/extraction/index.js.map +1 -1
  47. package/dist/extraction/languages/arkts-state-decorators.d.ts +13 -0
  48. package/dist/extraction/languages/arkts-state-decorators.d.ts.map +1 -0
  49. package/dist/extraction/languages/arkts-state-decorators.js +26 -0
  50. package/dist/extraction/languages/arkts-state-decorators.js.map +1 -0
  51. package/dist/extraction/languages/arkts-viewtree.d.ts +4 -2
  52. package/dist/extraction/languages/arkts-viewtree.d.ts.map +1 -1
  53. package/dist/extraction/languages/arkts-viewtree.js +21 -6
  54. package/dist/extraction/languages/arkts-viewtree.js.map +1 -1
  55. package/dist/extraction/languages/arkts.d.ts +98 -2
  56. package/dist/extraction/languages/arkts.d.ts.map +1 -1
  57. package/dist/extraction/languages/arkts.js +1290 -61
  58. package/dist/extraction/languages/arkts.js.map +1 -1
  59. package/dist/extraction/languages/c-cpp.d.ts +56 -0
  60. package/dist/extraction/languages/c-cpp.d.ts.map +1 -1
  61. package/dist/extraction/languages/c-cpp.js +198 -1
  62. package/dist/extraction/languages/c-cpp.js.map +1 -1
  63. package/dist/extraction/languages/ohos-api-consumer.d.ts +34 -0
  64. package/dist/extraction/languages/ohos-api-consumer.d.ts.map +1 -0
  65. package/dist/extraction/languages/ohos-api-consumer.js +283 -0
  66. package/dist/extraction/languages/ohos-api-consumer.js.map +1 -0
  67. package/dist/extraction/parse-pool.d.ts +126 -0
  68. package/dist/extraction/parse-pool.d.ts.map +1 -0
  69. package/dist/extraction/parse-pool.js +319 -0
  70. package/dist/extraction/parse-pool.js.map +1 -0
  71. package/dist/extraction/tree-sitter-types.d.ts +17 -0
  72. package/dist/extraction/tree-sitter-types.d.ts.map +1 -1
  73. package/dist/extraction/tree-sitter.d.ts +21 -0
  74. package/dist/extraction/tree-sitter.d.ts.map +1 -1
  75. package/dist/extraction/tree-sitter.js +198 -27
  76. package/dist/extraction/tree-sitter.js.map +1 -1
  77. package/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
  78. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  79. package/dist/graph/traversal.d.ts.map +1 -1
  80. package/dist/graph/traversal.js +76 -17
  81. package/dist/graph/traversal.js.map +1 -1
  82. package/dist/index.d.ts +25 -0
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +78 -4
  85. package/dist/index.js.map +1 -1
  86. package/dist/installer/instructions-template.js +9 -9
  87. package/dist/installer/targets/shared.d.ts +5 -6
  88. package/dist/installer/targets/shared.d.ts.map +1 -1
  89. package/dist/installer/targets/shared.js +5 -6
  90. package/dist/installer/targets/shared.js.map +1 -1
  91. package/dist/mcp/daemon-paths.d.ts +30 -3
  92. package/dist/mcp/daemon-paths.d.ts.map +1 -1
  93. package/dist/mcp/daemon-paths.js +50 -10
  94. package/dist/mcp/daemon-paths.js.map +1 -1
  95. package/dist/mcp/daemon-registry.d.ts.map +1 -1
  96. package/dist/mcp/daemon-registry.js +7 -3
  97. package/dist/mcp/daemon-registry.js.map +1 -1
  98. package/dist/mcp/daemon.d.ts +48 -0
  99. package/dist/mcp/daemon.d.ts.map +1 -1
  100. package/dist/mcp/daemon.js +196 -32
  101. package/dist/mcp/daemon.js.map +1 -1
  102. package/dist/mcp/engine.d.ts +17 -0
  103. package/dist/mcp/engine.d.ts.map +1 -1
  104. package/dist/mcp/engine.js +73 -1
  105. package/dist/mcp/engine.js.map +1 -1
  106. package/dist/mcp/index.d.ts.map +1 -1
  107. package/dist/mcp/index.js +25 -43
  108. package/dist/mcp/index.js.map +1 -1
  109. package/dist/mcp/liveness-watchdog.js +16 -16
  110. package/dist/mcp/ppid-watchdog.d.ts +18 -0
  111. package/dist/mcp/ppid-watchdog.d.ts.map +1 -1
  112. package/dist/mcp/ppid-watchdog.js +37 -0
  113. package/dist/mcp/ppid-watchdog.js.map +1 -1
  114. package/dist/mcp/query-cache.d.ts +25 -0
  115. package/dist/mcp/query-cache.d.ts.map +1 -0
  116. package/dist/mcp/query-cache.js +191 -0
  117. package/dist/mcp/query-cache.js.map +1 -0
  118. package/dist/mcp/query-pool.d.ts +94 -0
  119. package/dist/mcp/query-pool.d.ts.map +1 -0
  120. package/dist/mcp/query-pool.js +297 -0
  121. package/dist/mcp/query-pool.js.map +1 -0
  122. package/dist/mcp/query-worker.d.ts +24 -0
  123. package/dist/mcp/query-worker.d.ts.map +1 -0
  124. package/dist/mcp/query-worker.js +87 -0
  125. package/dist/mcp/query-worker.js.map +1 -0
  126. package/dist/mcp/server-instructions.d.ts +5 -7
  127. package/dist/mcp/server-instructions.d.ts.map +1 -1
  128. package/dist/mcp/server-instructions.js +72 -74
  129. package/dist/mcp/server-instructions.js.map +1 -1
  130. package/dist/mcp/tools.d.ts +94 -4
  131. package/dist/mcp/tools.d.ts.map +1 -1
  132. package/dist/mcp/tools.js +877 -71
  133. package/dist/mcp/tools.js.map +1 -1
  134. package/dist/project-config.d.ts +20 -0
  135. package/dist/project-config.d.ts.map +1 -1
  136. package/dist/project-config.js +42 -2
  137. package/dist/project-config.js.map +1 -1
  138. package/dist/reasoning/login.js +1 -1
  139. package/dist/reasoning/login.js.map +1 -1
  140. package/dist/reasoning/reasoner.js +32 -32
  141. package/dist/resolution/c-fnptr-synthesizer.d.ts +0 -28
  142. package/dist/resolution/c-fnptr-synthesizer.d.ts.map +1 -1
  143. package/dist/resolution/c-fnptr-synthesizer.js +765 -79
  144. package/dist/resolution/c-fnptr-synthesizer.js.map +1 -1
  145. package/dist/resolution/callback-synthesizer.d.ts +1 -1
  146. package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
  147. package/dist/resolution/callback-synthesizer.js +72 -11
  148. package/dist/resolution/callback-synthesizer.js.map +1 -1
  149. package/dist/resolution/cooperative-yield.d.ts +32 -0
  150. package/dist/resolution/cooperative-yield.d.ts.map +1 -0
  151. package/dist/resolution/cooperative-yield.js +42 -0
  152. package/dist/resolution/cooperative-yield.js.map +1 -0
  153. package/dist/resolution/index.d.ts +11 -2
  154. package/dist/resolution/index.d.ts.map +1 -1
  155. package/dist/resolution/index.js +72 -4
  156. package/dist/resolution/index.js.map +1 -1
  157. package/dist/resolution/name-matcher.d.ts +22 -0
  158. package/dist/resolution/name-matcher.d.ts.map +1 -1
  159. package/dist/resolution/name-matcher.js +317 -20
  160. package/dist/resolution/name-matcher.js.map +1 -1
  161. package/dist/spec/config.d.ts +39 -0
  162. package/dist/spec/config.d.ts.map +1 -0
  163. package/dist/spec/config.js +304 -0
  164. package/dist/spec/config.js.map +1 -0
  165. package/dist/spec/db/commit-node.d.ts +23 -0
  166. package/dist/spec/db/commit-node.d.ts.map +1 -0
  167. package/dist/spec/db/commit-node.js +62 -0
  168. package/dist/spec/db/commit-node.js.map +1 -0
  169. package/dist/spec/db/fragment-node.d.ts +24 -0
  170. package/dist/spec/db/fragment-node.d.ts.map +1 -0
  171. package/dist/spec/db/fragment-node.js +128 -0
  172. package/dist/spec/db/fragment-node.js.map +1 -0
  173. package/dist/spec/db/fts.d.ts +74 -0
  174. package/dist/spec/db/fts.d.ts.map +1 -0
  175. package/dist/spec/db/fts.js +324 -0
  176. package/dist/spec/db/fts.js.map +1 -0
  177. package/dist/spec/db/index.d.ts +13 -0
  178. package/dist/spec/db/index.d.ts.map +1 -0
  179. package/dist/spec/db/index.js +50 -0
  180. package/dist/spec/db/index.js.map +1 -0
  181. package/dist/spec/db/relations.d.ts +55 -0
  182. package/dist/spec/db/relations.d.ts.map +1 -0
  183. package/dist/spec/db/relations.js +158 -0
  184. package/dist/spec/db/relations.js.map +1 -0
  185. package/dist/spec/db/schema.d.ts +33 -0
  186. package/dist/spec/db/schema.d.ts.map +1 -0
  187. package/dist/spec/db/schema.js +119 -0
  188. package/dist/spec/db/schema.js.map +1 -0
  189. package/dist/spec/db/schema.sql +117 -0
  190. package/dist/spec/db/spec-node.d.ts +41 -0
  191. package/dist/spec/db/spec-node.d.ts.map +1 -0
  192. package/dist/spec/db/spec-node.js +114 -0
  193. package/dist/spec/db/spec-node.js.map +1 -0
  194. package/dist/spec/evolve/impact-locator.d.ts +13 -0
  195. package/dist/spec/evolve/impact-locator.d.ts.map +1 -0
  196. package/dist/spec/evolve/impact-locator.js +25 -0
  197. package/dist/spec/evolve/impact-locator.js.map +1 -0
  198. package/dist/spec/evolve/llm-client.d.ts +50 -0
  199. package/dist/spec/evolve/llm-client.d.ts.map +1 -0
  200. package/dist/spec/evolve/llm-client.js +176 -0
  201. package/dist/spec/evolve/llm-client.js.map +1 -0
  202. package/dist/spec/evolve/logic-checker.d.ts +12 -0
  203. package/dist/spec/evolve/logic-checker.d.ts.map +1 -0
  204. package/dist/spec/evolve/logic-checker.js +24 -0
  205. package/dist/spec/evolve/logic-checker.js.map +1 -0
  206. package/dist/spec/evolve/pipeline.d.ts +42 -0
  207. package/dist/spec/evolve/pipeline.d.ts.map +1 -0
  208. package/dist/spec/evolve/pipeline.js +567 -0
  209. package/dist/spec/evolve/pipeline.js.map +1 -0
  210. package/dist/spec/evolve/spec-rewriter.d.ts +42 -0
  211. package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -0
  212. package/dist/spec/evolve/spec-rewriter.js +230 -0
  213. package/dist/spec/evolve/spec-rewriter.js.map +1 -0
  214. package/dist/spec/graph/queries.d.ts +155 -0
  215. package/dist/spec/graph/queries.d.ts.map +1 -0
  216. package/dist/spec/graph/queries.js +440 -0
  217. package/dist/spec/graph/queries.js.map +1 -0
  218. package/dist/spec/llm/client.d.ts +29 -0
  219. package/dist/spec/llm/client.d.ts.map +1 -0
  220. package/dist/spec/llm/client.js +123 -0
  221. package/dist/spec/llm/client.js.map +1 -0
  222. package/dist/spec/llm/index.d.ts +3 -0
  223. package/dist/spec/llm/index.d.ts.map +1 -0
  224. package/dist/spec/llm/index.js +11 -0
  225. package/dist/spec/llm/index.js.map +1 -0
  226. package/dist/spec/llm/prompts.d.ts +13 -0
  227. package/dist/spec/llm/prompts.d.ts.map +1 -0
  228. package/dist/spec/llm/prompts.js +75 -0
  229. package/dist/spec/llm/prompts.js.map +1 -0
  230. package/dist/spec/mining/diff-parser.d.ts +33 -0
  231. package/dist/spec/mining/diff-parser.d.ts.map +1 -0
  232. package/dist/spec/mining/diff-parser.js +166 -0
  233. package/dist/spec/mining/diff-parser.js.map +1 -0
  234. package/dist/spec/mining/git-scanner.d.ts +103 -0
  235. package/dist/spec/mining/git-scanner.d.ts.map +1 -0
  236. package/dist/spec/mining/git-scanner.js +307 -0
  237. package/dist/spec/mining/git-scanner.js.map +1 -0
  238. package/dist/spec/mining/pipeline.d.ts +53 -0
  239. package/dist/spec/mining/pipeline.d.ts.map +1 -0
  240. package/dist/spec/mining/pipeline.js +178 -0
  241. package/dist/spec/mining/pipeline.js.map +1 -0
  242. package/dist/spec/mining/scope-resolver.d.ts +45 -0
  243. package/dist/spec/mining/scope-resolver.d.ts.map +1 -0
  244. package/dist/spec/mining/scope-resolver.js +103 -0
  245. package/dist/spec/mining/scope-resolver.js.map +1 -0
  246. package/dist/spec/mining/spec-extractor.d.ts +69 -0
  247. package/dist/spec/mining/spec-extractor.d.ts.map +1 -0
  248. package/dist/spec/mining/spec-extractor.js +369 -0
  249. package/dist/spec/mining/spec-extractor.js.map +1 -0
  250. package/dist/spec/types.d.ts +149 -0
  251. package/dist/spec/types.d.ts.map +1 -0
  252. package/dist/spec/types.js +15 -0
  253. package/dist/spec/types.js.map +1 -0
  254. package/dist/spec/utils.d.ts +167 -0
  255. package/dist/spec/utils.d.ts.map +1 -0
  256. package/dist/spec/utils.js +463 -0
  257. package/dist/spec/utils.js.map +1 -0
  258. package/dist/sync/worktree.d.ts +9 -0
  259. package/dist/sync/worktree.d.ts.map +1 -1
  260. package/dist/sync/worktree.js +40 -0
  261. package/dist/sync/worktree.js.map +1 -1
  262. package/dist/types.d.ts +6 -1
  263. package/dist/types.d.ts.map +1 -1
  264. package/dist/ui/shimmer-progress.d.ts +2 -0
  265. package/dist/ui/shimmer-progress.d.ts.map +1 -1
  266. package/dist/ui/shimmer-progress.js +19 -2
  267. package/dist/ui/shimmer-progress.js.map +1 -1
  268. package/dist/upgrade/index.js +1 -1
  269. package/dist/upgrade/index.js.map +1 -1
  270. package/package.json +58 -57
  271. package/scripts/add-lang/bench.sh +60 -60
  272. package/scripts/add-lang/check-grammar.mjs +75 -75
  273. package/scripts/add-lang/dump-ast.mjs +103 -103
  274. package/scripts/add-lang/verify-extraction.mjs +70 -70
  275. package/scripts/agent-eval/ab-adoption.sh +91 -91
  276. package/scripts/agent-eval/ab-hook.sh +86 -86
  277. package/scripts/agent-eval/ab-impl.sh +78 -78
  278. package/scripts/agent-eval/ab-new-vs-baseline.sh +102 -102
  279. package/scripts/agent-eval/ab-sufficiency.sh +78 -78
  280. package/scripts/agent-eval/arms-F.sh +21 -21
  281. package/scripts/agent-eval/arms-matrix.sh +37 -37
  282. package/scripts/agent-eval/audit.sh +68 -68
  283. package/scripts/agent-eval/bench-readme.sh +28 -28
  284. package/scripts/agent-eval/bench-why-repo.sh +22 -22
  285. package/scripts/agent-eval/block-read-hook.sh +19 -19
  286. package/scripts/agent-eval/hook-settings.json +15 -15
  287. package/scripts/agent-eval/itrun.sh +120 -120
  288. package/scripts/agent-eval/offload-eval-3arm.sh +72 -72
  289. package/scripts/agent-eval/offload-eval-cost.mjs +133 -133
  290. package/scripts/agent-eval/offload-eval-effort.mjs +108 -108
  291. package/scripts/agent-eval/offload-eval-frontload-matrix.sh +25 -25
  292. package/scripts/agent-eval/offload-eval-frontload.sh +47 -47
  293. package/scripts/agent-eval/offload-eval-ground-truth.json +18 -18
  294. package/scripts/agent-eval/offload-eval-hook.mjs +84 -84
  295. package/scripts/agent-eval/offload-eval-judge.mjs +103 -103
  296. package/scripts/agent-eval/offload-eval-matrix.sh +20 -20
  297. package/scripts/agent-eval/offload-eval-metrics.mjs +94 -94
  298. package/scripts/agent-eval/offload-eval-refs1.sh +50 -50
  299. package/scripts/agent-eval/offload-eval-setup.sh +24 -24
  300. package/scripts/agent-eval/offload-eval-styles.sh +71 -71
  301. package/scripts/agent-eval/offload-eval-summarize.mjs +68 -68
  302. package/scripts/agent-eval/offload-eval.md +76 -76
  303. package/scripts/agent-eval/parse-arms.mjs +116 -116
  304. package/scripts/agent-eval/parse-bench-readme.mjs +84 -84
  305. package/scripts/agent-eval/parse-run.mjs +45 -45
  306. package/scripts/agent-eval/parse-session.mjs +93 -93
  307. package/scripts/agent-eval/probe-context.mjs +21 -21
  308. package/scripts/agent-eval/probe-explore.mjs +40 -40
  309. package/scripts/agent-eval/probe-node.mjs +20 -20
  310. package/scripts/agent-eval/probe-sweep.mjs +119 -119
  311. package/scripts/agent-eval/probe-trace.mjs +20 -20
  312. package/scripts/agent-eval/redirect-read-hook.sh +38 -38
  313. package/scripts/agent-eval/repro-concurrent-explore.mjs +119 -0
  314. package/scripts/agent-eval/repro-daemon-clients.mjs +125 -0
  315. package/scripts/agent-eval/run-agent.sh +34 -34
  316. package/scripts/agent-eval/run-all.sh +69 -69
  317. package/scripts/agent-eval/run-arms.sh +56 -56
  318. package/scripts/agent-eval/seq-matrix.mjs +137 -137
  319. package/scripts/build-bundle.sh +118 -118
  320. package/scripts/exp_boundary_eval/README.md +247 -0
  321. package/scripts/exp_boundary_eval/__pycache__/_utils.cpython-310.pyc +0 -0
  322. package/scripts/exp_boundary_eval/__pycache__/analyze.cpython-310.pyc +0 -0
  323. package/scripts/exp_boundary_eval/__pycache__/deveco_arm.cpython-310.pyc +0 -0
  324. package/scripts/exp_boundary_eval/__pycache__/run_one.cpython-310.pyc +0 -0
  325. package/scripts/exp_boundary_eval/__pycache__/run_session.cpython-310.pyc +0 -0
  326. package/scripts/exp_boundary_eval/__pycache__/setup.cpython-310.pyc +0 -0
  327. package/scripts/exp_boundary_eval/_test_mcp_chain.py +78 -0
  328. package/scripts/exp_boundary_eval/_test_stdin.py +8 -0
  329. package/scripts/exp_boundary_eval/_utils.py +1116 -0
  330. package/scripts/exp_boundary_eval/analyze.py +1313 -0
  331. package/scripts/exp_boundary_eval/data/agents.json +109 -0
  332. package/scripts/exp_boundary_eval/data/experiments.json +140 -0
  333. package/scripts/exp_boundary_eval/deveco_arm.py +519 -0
  334. package/scripts/exp_boundary_eval/run_all.py +378 -0
  335. package/scripts/exp_boundary_eval/run_one.py +165 -0
  336. package/scripts/exp_boundary_eval/run_session.py +158 -0
  337. package/scripts/exp_boundary_eval/setup.py +120 -0
  338. package/scripts/exp_boundary_eval/win_mcp_launcher.py +73 -0
  339. package/scripts/exp_boundary_eval/win_mcp_stdio_wrap.js +36 -0
  340. package/scripts/exp_boundary_eval/win_node_launcher.py +24 -0
  341. package/scripts/extract-release-notes.mjs +130 -130
  342. package/scripts/local-install.sh +41 -41
  343. package/scripts/npm-sdk.js +75 -75
  344. package/scripts/npm-shim.js +268 -246
  345. package/scripts/ohos-sdk-publish.mjs +133 -0
  346. package/scripts/pack-npm.sh +119 -119
  347. package/scripts/prepare-release.mjs +270 -270
  348. package/scripts/qa_eval/README.md +407 -404
  349. package/scripts/qa_eval/_test_deveco_probe.py +41 -41
  350. package/scripts/qa_eval/agent_runner.py +526 -526
  351. package/scripts/qa_eval/data/.gitignore +4 -4
  352. package/scripts/qa_eval/data/test-set.jsonl +2 -22
  353. package/scripts/qa_eval/eval_metrics.py +274 -233
  354. package/scripts/qa_eval/external_agent.py +976 -671
  355. package/scripts/qa_eval/llm_config.py +92 -92
  356. package/scripts/qa_eval/memory_monitor.py +132 -132
  357. package/scripts/qa_eval/my_answer_accuracy.py +187 -187
  358. package/scripts/qa_eval/requirements.txt +2 -2
  359. package/scripts/qa_eval/run_pipeline.py +804 -711
  360. package/scripts/qa_eval/stats_efficiency.py +279 -279
  361. package/scripts/qa_eval/stats_scores.py +207 -207
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env python3
2
+ """Run multi-turn session experiment (5). Supports multiple agents via --agent."""
3
+
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ import time
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import setup as setup_mod
12
+ from _utils import (OUTPUT_DIR, append_run_log, build_agent_cmd,
13
+ current_time_ms, find_experiment, get_agent, info, header,
14
+ log, parse_output, resolve_agent_model, run_monitored_subprocess,
15
+ subprocess_no_window_kwargs, write_json, warn)
16
+
17
+
18
+ def run_session(exp_id: str = "5", skip_permissions: bool = True,
19
+ agent_name: str = "",
20
+ results_dir: Optional[Path] = None,
21
+ state_dir: Optional[Path] = None) -> bool:
22
+ exp = find_experiment(exp_id)
23
+ if exp is None:
24
+ print(f"ERROR: Experiment {exp_id} not found")
25
+ return False
26
+
27
+ agent = get_agent(agent_name)
28
+ clone_path = os.environ.get("REPO_CLONE", "")
29
+ local_repo = os.environ.get("REPO_PATH", "")
30
+ if clone_path:
31
+ repo_root = Path(clone_path)
32
+ elif local_repo:
33
+ repo_root = Path(local_repo)
34
+ else:
35
+ print("ERROR: No repo. Use run_all.py to clone, or set REPO_PATH / REPO_CLONE.")
36
+ return False
37
+ max_turns = exp.get("max_turns_per_round", 25)
38
+
39
+ header("Experiment 5: Session Persistence & Context Decay")
40
+ info(f"Agent: {agent['name']} ({agent['binary']}) | Repo: {repo_root}")
41
+ info(f"Max turns/round: {max_turns} | Permissions: {'skip' if skip_permissions else 'ask'}")
42
+
43
+ session_id, _ = setup_mod.prepare_environment("5", "full", agent_name,
44
+ state_dir=state_dir)
45
+ log(f"Shared Session ID: {session_id}")
46
+
47
+ result_dir = results_dir / "5" if results_dir else OUTPUT_DIR / "5"
48
+ result_dir.mkdir(parents=True, exist_ok=True)
49
+
50
+ rounds_data = []
51
+ session_model = ""
52
+ deveco_session_id = ""
53
+ for round_num in range(1, 6):
54
+ ri = next((r for r in exp["rounds"] if r["round"] == round_num), None)
55
+ if not ri: continue
56
+
57
+ header(f"Round {round_num} of 5")
58
+ info(f"Title: {ri['title']}")
59
+
60
+ round_dir = result_dir / f"round_{round_num}"
61
+ round_dir.mkdir(parents=True, exist_ok=True)
62
+ (round_dir / "prompt.txt").write_text(ri["prompt"], encoding="utf-8")
63
+
64
+ start_ms = current_time_ms()
65
+ is_resume = round_num > 1
66
+
67
+ cmd = build_agent_cmd(agent, session_id, max_turns, ri["prompt"],
68
+ resume=is_resume, skip_permissions=skip_permissions,
69
+ repo_root=repo_root)
70
+
71
+ stream_path = round_dir / "stream_output.jsonl"
72
+ text_path = round_dir / "raw_output.txt"
73
+ track_hg = os.environ.get("EVAL_ARM", "") == "homegraph"
74
+
75
+ with open(stream_path, "w", encoding="utf-8") as stream_f:
76
+ result, mem = run_monitored_subprocess(
77
+ cmd, stdout=stream_f, stderr=subprocess.STDOUT, text=True,
78
+ track_homegraph=track_hg,
79
+ )
80
+ exit_code = result.returncode
81
+
82
+ end_ms = current_time_ms()
83
+ dur_ms = end_ms - start_ms
84
+ dur_s = f"{dur_ms / 1000:.1f}"
85
+
86
+ parser_name = agent.get("parser", "claude_stream_json")
87
+ stats = parse_output(stream_path, parser_name)
88
+ if not session_model:
89
+ session_model = resolve_agent_model(agent, stats)
90
+ if not deveco_session_id and stats.deveco_session_id:
91
+ deveco_session_id = stats.deveco_session_id
92
+
93
+ text_content = "\n".join(stats.text_lines)
94
+ text_path.write_text(text_content or "(no text output)", encoding="utf-8")
95
+
96
+ r2 = subprocess.run(
97
+ ["git", "diff", "--name-only"], capture_output=True, text=True, cwd=repo_root,
98
+ **subprocess_no_window_kwargs(),
99
+ )
100
+ mod_files = len([l for l in r2.stdout.strip().split("\n") if l])
101
+
102
+ rr = {"round": round_num, "title": ri["title"], "duration_ms": dur_ms,
103
+ "duration_s": dur_s, "exit_code": exit_code,
104
+ "tool_calls": stats.tool_calls, "tool_names": stats.tool_names,
105
+ "files_read": list(set(stats.files_read)),
106
+ "files_edited": list(set(stats.files_edited)),
107
+ "assistant_messages": stats.assistant_messages,
108
+ "max_turns_hit": stats.max_turns_hit,
109
+ "modified_files": mod_files, "max_turns": max_turns,
110
+ "memory": mem.to_dict()}
111
+ write_json(round_dir / "round_results.json", rr)
112
+ rounds_data.append(rr)
113
+
114
+ info(f"Round {round_num} done: {dur_s}s | Tool calls: {stats.tool_calls} | "
115
+ f"Files read: {len(set(stats.files_read))} | Edited: {mod_files}")
116
+ if stats.max_turns_hit:
117
+ warn(f" Max turns ({max_turns}) hit in round {round_num}")
118
+ if round_num < 5:
119
+ time.sleep(2)
120
+
121
+ summary = {
122
+ "experiment_id": "5", "session_id": session_id, "agent": agent["name"],
123
+ "model": session_model,
124
+ "deveco_session_id": deveco_session_id,
125
+ "total_rounds": len(rounds_data),
126
+ "total_duration_ms": sum(r["duration_ms"] for r in rounds_data),
127
+ "total_tool_calls": sum(int(r["tool_calls"]) for r in rounds_data),
128
+ "memory_retention_curve": [
129
+ {"round": r["round"], "tool_calls": int(r["tool_calls"]),
130
+ "files_read": r.get("files_read", [])} for r in rounds_data
131
+ ],
132
+ }
133
+ write_json(result_dir / "session_results.json", summary)
134
+
135
+ print("\nMemory Retention Curve:")
136
+ for r in rounds_data:
137
+ tc = int(r["tool_calls"])
138
+ fr = len(r.get("files_read", []))
139
+ print(f" Round {r['round']}: {'#' * (tc // 2)} ({tc} tool calls, {fr} files read)")
140
+
141
+ log(f"Results: {result_dir}/session_results.json")
142
+ return True
143
+
144
+
145
+ if __name__ == "__main__":
146
+ args = sys.argv[1:]
147
+ exp_id = args[0] if args else "5"
148
+ agent_name = ""
149
+ skip = True
150
+ i = 1
151
+ while i < len(args):
152
+ if args[i] == "--agent" and i + 1 < len(args):
153
+ agent_name = args[i + 1]; i += 2
154
+ elif args[i] == "--no-skip-permissions":
155
+ skip = False; i += 1
156
+ else:
157
+ i += 1
158
+ sys.exit(0 if run_session(exp_id, skip, agent_name) else 1)
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """Prepare a clean repo for an experiment run. Agent-aware."""
3
+
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Optional, Tuple
10
+
11
+ from _utils import (append_run_log, error, generate_session_id,
12
+ get_agent, get_agent_version, git_output, git_ok, log,
13
+ shell_ok, verify_agent_binary, write_json)
14
+ from deveco_arm import git_clean_repo, write_deveco_project_config
15
+
16
+
17
+ def prepare_environment(exp_id: str, clean_mode: str = "full",
18
+ agent_name: str = "",
19
+ state_dir: Optional[Path] = None) -> Tuple[str, str]:
20
+ """Prepare repo. Returns (session_id, agent_name)."""
21
+ clone_path = os.environ.get("REPO_CLONE", "")
22
+ is_clone = bool(clone_path and Path(clone_path).is_dir())
23
+ local_repo = os.environ.get("REPO_PATH", "")
24
+ if is_clone:
25
+ repo_root = Path(clone_path)
26
+ elif local_repo and Path(local_repo).is_dir():
27
+ repo_root = Path(local_repo)
28
+ else:
29
+ error("No repo available. Clone via run_all.py or set REPO_PATH / REPO_CLONE.")
30
+ sys.exit(1)
31
+
32
+ if state_dir is not None:
33
+ _state_dir = state_dir
34
+ else:
35
+ from _utils import LOG_DIR
36
+ _state_dir = LOG_DIR
37
+
38
+ if is_clone:
39
+ log(f"Using cloned repo: {repo_root}")
40
+ else:
41
+ log(f"Using local repo: {repo_root}")
42
+
43
+ os.chdir(repo_root)
44
+
45
+ # Agent config
46
+ agent = get_agent(agent_name)
47
+ binary = agent["binary"]
48
+
49
+ # Step 1: Clean repo
50
+ arm = os.environ.get("EVAL_ARM", "baseline")
51
+ if clean_mode == "full":
52
+ log("Restoring repo to clean state...")
53
+ branch = os.environ.get("BRANCH", "weekly_20260601")
54
+ shell_ok(f"git checkout {branch}")
55
+ git_ok(["checkout", "."], cwd=repo_root)
56
+ git_clean_repo(repo_root, arm)
57
+
58
+ current_branch = git_output(["branch", "--show-current"], cwd=repo_root)
59
+ commit = git_output(["rev-parse", "--short", "HEAD"], cwd=repo_root)
60
+ log(f"Branch: {current_branch} | Commit: {commit}")
61
+
62
+ # Step 1b: DevEco A/B project config (.deveco/deveco.jsonc)
63
+ if agent.get("binary") == "deveco":
64
+ write_deveco_project_config(repo_root, arm)
65
+
66
+ # Step 1c: Index rebuilt once at homegraph suite start (run_all / prebuild).
67
+ # Per-experiment git reset restores the same commit — no second index here.
68
+ # Step 2: Clear CLAUDE.md
69
+ claude_md = repo_root / "CLAUDE.md"
70
+ if claude_md.exists() and claude_md.stat().st_size > 0:
71
+ backup_dir = _state_dir / "claude_md_backups"
72
+ backup_dir.mkdir(parents=True, exist_ok=True)
73
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
74
+ backup_path = backup_dir / f"CLAUDE.md.{ts}"
75
+ backup_path.write_text(claude_md.read_text())
76
+ log(f"Backed up CLAUDE.md → {backup_path}")
77
+ with open(claude_md, "w") as f:
78
+ f.write("")
79
+ log("Cleared CLAUDE.md")
80
+
81
+ for f in [".cursorrules", ".cursor/rules"]:
82
+ p = repo_root / f
83
+ if p.exists():
84
+ p.unlink()
85
+
86
+ # Step 3: Session ID
87
+ session_id = generate_session_id()
88
+ (_state_dir / f"session_{exp_id}.txt").write_text(session_id)
89
+ log(f"Session ID: {session_id}")
90
+
91
+ # Step 4: Verify agent binary
92
+ ok, resolved, err_msg = verify_agent_binary(agent)
93
+ if not ok:
94
+ error(err_msg)
95
+ sys.exit(1)
96
+ binary = resolved
97
+ agent_ver = get_agent_version(agent)
98
+ log(f"Agent: {agent['name']} ({binary}) v{agent_ver}")
99
+
100
+ # Step 5: Record state
101
+ write_json(_state_dir / exp_id / "state.json", {
102
+ "experiment_id": exp_id, "session_id": session_id,
103
+ "started_at": datetime.now(timezone.utc).isoformat(),
104
+ "clean_mode": clean_mode, "agent": agent["name"],
105
+ "agent_version": agent_ver, "repo_root": str(repo_root),
106
+ "git_branch": git_output(["branch", "--show-current"], cwd=repo_root),
107
+ "git_commit": git_output(["rev-parse", "HEAD"], cwd=repo_root),
108
+ })
109
+
110
+ append_run_log(_state_dir, f"Setup complete for {exp_id}: session={session_id}, agent={agent['name']}")
111
+ log("Setup complete.")
112
+ return session_id, agent["name"]
113
+
114
+
115
+ if __name__ == "__main__":
116
+ exp_id = sys.argv[1] if len(sys.argv) > 1 else "all"
117
+ clean_mode = sys.argv[2] if len(sys.argv) > 2 else "full"
118
+ agent_name = sys.argv[3] if len(sys.argv) > 3 else ""
119
+ sid, _ = prepare_environment(exp_id, clean_mode, agent_name)
120
+ print(sid)
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env python3
2
+ """Windows MCP entry: pythonw (no window) → hidden node child; stdio inherited from DevEco."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+
11
+ _CREATE_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
12
+
13
+ # Applied here so MCP works even if DevEco ignores deveco.jsonc "environment".
14
+ _MCP_ENV_DEFAULTS = {
15
+ "HOMEGRAPH_NO_DAEMON": "1",
16
+ "HOMEGRAPH_NO_WATCHDOG": "1",
17
+ "HOMEGRAPH_WASM_RELAUNCHED": "1",
18
+ }
19
+
20
+
21
+ def _resolve_node() -> str:
22
+ env_node = os.environ.get("HOMEGRAPH_NODE", "").strip()
23
+ if env_node:
24
+ return env_node
25
+ found = shutil.which("node")
26
+ if not found:
27
+ print("node not found on PATH", file=sys.stderr)
28
+ sys.exit(127)
29
+ return found
30
+
31
+
32
+ def _node_argv(raw: list[str]) -> list[str]:
33
+ """Ensure --liftoff-only is on the node command line (before homegraph.js)."""
34
+ node = _resolve_node()
35
+ args = list(raw)
36
+ if "--liftoff-only" not in args:
37
+ args.insert(0, "--liftoff-only")
38
+ return [node, *args]
39
+
40
+
41
+ def _hidden_call(argv: list[str], env: dict) -> int:
42
+ si = subprocess.STARTUPINFO()
43
+ si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
44
+ si.wShowWindow = 0
45
+ return subprocess.call(
46
+ argv,
47
+ env=env,
48
+ stdin=sys.stdin,
49
+ stdout=sys.stdout,
50
+ stderr=sys.stderr,
51
+ creationflags=_CREATE_NO_WINDOW,
52
+ startupinfo=si,
53
+ )
54
+
55
+
56
+ def main() -> int:
57
+ if len(sys.argv) < 2:
58
+ print(
59
+ "usage: win_mcp_launcher.py [--liftoff-only] [--stack-size=N] homegraph.js …",
60
+ file=sys.stderr,
61
+ )
62
+ return 2
63
+ env = os.environ.copy()
64
+ for key, val in _MCP_ENV_DEFAULTS.items():
65
+ env.setdefault(key, val)
66
+ argv = _node_argv(sys.argv[1:])
67
+ if os.name == "nt":
68
+ return _hidden_call(argv, env)
69
+ return subprocess.call(argv, env=env)
70
+
71
+
72
+ if __name__ == "__main__":
73
+ raise SystemExit(main())
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Windows MCP launcher: spawn homegraph with windowsHide while keeping stdio
4
+ * connected to DevEco (pythonw + Python subprocess breaks JSON-RPC pipes).
5
+ *
6
+ * Usage: node win_mcp_stdio_wrap.js [--stack-size=N] homegraph.js serve --mcp --path <repo>
7
+ */
8
+ 'use strict';
9
+
10
+ const cp = require('child_process');
11
+
12
+ const node = process.env.HOMEGRAPH_NODE || process.execPath;
13
+ const args = process.argv.slice(2);
14
+ if (args.length === 0) {
15
+ process.stderr.write('usage: node win_mcp_stdio_wrap.js [--stack-size=N] homegraph.js …\n');
16
+ process.exit(2);
17
+ }
18
+
19
+ const child = cp.spawn(node, args, {
20
+ stdio: 'inherit',
21
+ windowsHide: true,
22
+ env: process.env,
23
+ });
24
+
25
+ child.on('error', (err) => {
26
+ process.stderr.write(String(err) + '\n');
27
+ process.exit(1);
28
+ });
29
+
30
+ child.on('exit', (code, signal) => {
31
+ if (signal) {
32
+ process.kill(process.pid, signal);
33
+ return;
34
+ }
35
+ process.exit(code ?? 1);
36
+ });
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env python3
2
+ """Windows: run node without a console window; inherit stdio (MCP / homegraph CLI)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import subprocess
8
+ import sys
9
+
10
+ _CREATE_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0x08000000)
11
+
12
+
13
+ def main() -> int:
14
+ if len(sys.argv) < 2:
15
+ print("usage: win_node_launcher.py [--stack-size=N] script.js …", file=sys.stderr)
16
+ return 2
17
+ node = os.environ.get("HOMEGRAPH_NODE", "node")
18
+ cmd = [node, *sys.argv[1:]]
19
+ flags = _CREATE_NO_WINDOW if os.name == "nt" else 0
20
+ return subprocess.call(cmd, creationflags=flags)
21
+
22
+
23
+ if __name__ == "__main__":
24
+ raise SystemExit(main())
@@ -1,130 +1,130 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Extract a release-notes block from CHANGELOG.md for a given version
4
- * (or unwrap text supplied on stdin), then join hard-wrapped paragraphs.
5
- *
6
- * Why: GitHub renders release-note Markdown with GFM hard breaks, so
7
- * every `\n` becomes `<br>`. The CHANGELOG is hard-wrapped at ~75
8
- * chars for readable diffs, which then renders as awkward visible
9
- * line breaks on the release page. This script joins indented
10
- * continuation lines into a single line per bullet so the GFM
11
- * renderer produces clean paragraphs.
12
- *
13
- * Repo-level CHANGELOG.md viewing is unaffected (CommonMark treats
14
- * newlines as spaces there).
15
- *
16
- * Usage:
17
- * extract-release-notes.mjs <version> # read CHANGELOG.md
18
- * extract-release-notes.mjs --stdin # read from stdin (any text)
19
- */
20
-
21
- import { readFileSync } from 'fs';
22
-
23
- const arg = process.argv[2];
24
- if (!arg) {
25
- console.error('usage: extract-release-notes.mjs <version> | --stdin');
26
- process.exit(1);
27
- }
28
-
29
- let block;
30
- if (arg === '--stdin') {
31
- block = readFileSync(0, 'utf8').replace(/\r\n?/g, '\n').split('\n');
32
- } else {
33
- const version = arg;
34
- const escaped = version.replace(/\./g, '\\.');
35
- const headerRe = new RegExp(`^## \\[${escaped}\\]`);
36
- const anyHeaderRe = /^## \[/;
37
- const lines = readFileSync('CHANGELOG.md', 'utf8').split('\n');
38
- const start = lines.findIndex((l) => headerRe.test(l));
39
- if (start === -1) {
40
- console.error(`no '## [${version}]' entry found in CHANGELOG.md`);
41
- process.exit(1);
42
- }
43
- const after = lines.findIndex((l, i) => i > start && anyHeaderRe.test(l));
44
- block = lines.slice(start, after === -1 ? lines.length : after);
45
- }
46
-
47
- // Track a stack of `{ indent: number }` frames so a continuation line
48
- // can attach to the right ancestor. Handles the post-nested-list
49
- // continuation pattern:
50
- //
51
- // - top-level
52
- // - nested
53
- // back to top-level <- 2-space indent, joins the top-level bullet
54
- const out = [];
55
- let buf = '';
56
- let stack = [];
57
-
58
- function flushBuf() {
59
- if (buf !== '') {
60
- out.push(buf);
61
- buf = '';
62
- }
63
- }
64
-
65
- function leadingSpaces(s) {
66
- const m = s.match(/^(\s*)/);
67
- return m ? m[1].length : 0;
68
- }
69
-
70
- // Bullets: `-`, `*`, `digit.` only. `+` is intentionally excluded — the
71
- // CHANGELOG uses literal `+` inline (`config + instructions`) and we
72
- // don't want to misread those as nested bullets.
73
- const listItemRe = /^(\s*)([-*]|\d+\.)\s+/;
74
- const fenceRe = /^\s*```/;
75
-
76
- let inFence = false;
77
-
78
- for (const line of block) {
79
- // Fenced code blocks: pass through verbatim, no joining.
80
- if (fenceRe.test(line)) {
81
- flushBuf();
82
- stack = [];
83
- out.push(line);
84
- inFence = !inFence;
85
- continue;
86
- }
87
- if (inFence) {
88
- out.push(line);
89
- continue;
90
- }
91
- if (/^\s*$/.test(line)) {
92
- flushBuf();
93
- out.push('');
94
- continue;
95
- }
96
- if (/^#/.test(line)) {
97
- flushBuf();
98
- stack = [];
99
- out.push(line);
100
- continue;
101
- }
102
- const itemMatch = line.match(listItemRe);
103
- if (itemMatch) {
104
- flushBuf();
105
- const indent = itemMatch[1].length;
106
- while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
107
- stack.pop();
108
- }
109
- stack.push({ indent });
110
- buf = line;
111
- continue;
112
- }
113
- if (/^\s/.test(line)) {
114
- const indent = leadingSpaces(line);
115
- while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
116
- flushBuf();
117
- stack.pop();
118
- }
119
- const trimmed = line.replace(/^\s+/, '');
120
- buf = buf === '' ? trimmed : `${buf} ${trimmed}`;
121
- continue;
122
- }
123
- flushBuf();
124
- stack = [];
125
- out.push(line);
126
- }
127
- flushBuf();
128
-
129
- process.stdout.write(out.join('\n'));
130
- if (!out[out.length - 1]?.endsWith('\n')) process.stdout.write('\n');
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Extract a release-notes block from CHANGELOG.md for a given version
4
+ * (or unwrap text supplied on stdin), then join hard-wrapped paragraphs.
5
+ *
6
+ * Why: GitHub renders release-note Markdown with GFM hard breaks, so
7
+ * every `\n` becomes `<br>`. The CHANGELOG is hard-wrapped at ~75
8
+ * chars for readable diffs, which then renders as awkward visible
9
+ * line breaks on the release page. This script joins indented
10
+ * continuation lines into a single line per bullet so the GFM
11
+ * renderer produces clean paragraphs.
12
+ *
13
+ * Repo-level CHANGELOG.md viewing is unaffected (CommonMark treats
14
+ * newlines as spaces there).
15
+ *
16
+ * Usage:
17
+ * extract-release-notes.mjs <version> # read CHANGELOG.md
18
+ * extract-release-notes.mjs --stdin # read from stdin (any text)
19
+ */
20
+
21
+ import { readFileSync } from 'fs';
22
+
23
+ const arg = process.argv[2];
24
+ if (!arg) {
25
+ console.error('usage: extract-release-notes.mjs <version> | --stdin');
26
+ process.exit(1);
27
+ }
28
+
29
+ let block;
30
+ if (arg === '--stdin') {
31
+ block = readFileSync(0, 'utf8').replace(/\r\n?/g, '\n').split('\n');
32
+ } else {
33
+ const version = arg;
34
+ const escaped = version.replace(/\./g, '\\.');
35
+ const headerRe = new RegExp(`^## \\[${escaped}\\]`);
36
+ const anyHeaderRe = /^## \[/;
37
+ const lines = readFileSync('CHANGELOG.md', 'utf8').split('\n');
38
+ const start = lines.findIndex((l) => headerRe.test(l));
39
+ if (start === -1) {
40
+ console.error(`no '## [${version}]' entry found in CHANGELOG.md`);
41
+ process.exit(1);
42
+ }
43
+ const after = lines.findIndex((l, i) => i > start && anyHeaderRe.test(l));
44
+ block = lines.slice(start, after === -1 ? lines.length : after);
45
+ }
46
+
47
+ // Track a stack of `{ indent: number }` frames so a continuation line
48
+ // can attach to the right ancestor. Handles the post-nested-list
49
+ // continuation pattern:
50
+ //
51
+ // - top-level
52
+ // - nested
53
+ // back to top-level <- 2-space indent, joins the top-level bullet
54
+ const out = [];
55
+ let buf = '';
56
+ let stack = [];
57
+
58
+ function flushBuf() {
59
+ if (buf !== '') {
60
+ out.push(buf);
61
+ buf = '';
62
+ }
63
+ }
64
+
65
+ function leadingSpaces(s) {
66
+ const m = s.match(/^(\s*)/);
67
+ return m ? m[1].length : 0;
68
+ }
69
+
70
+ // Bullets: `-`, `*`, `digit.` only. `+` is intentionally excluded — the
71
+ // CHANGELOG uses literal `+` inline (`config + instructions`) and we
72
+ // don't want to misread those as nested bullets.
73
+ const listItemRe = /^(\s*)([-*]|\d+\.)\s+/;
74
+ const fenceRe = /^\s*```/;
75
+
76
+ let inFence = false;
77
+
78
+ for (const line of block) {
79
+ // Fenced code blocks: pass through verbatim, no joining.
80
+ if (fenceRe.test(line)) {
81
+ flushBuf();
82
+ stack = [];
83
+ out.push(line);
84
+ inFence = !inFence;
85
+ continue;
86
+ }
87
+ if (inFence) {
88
+ out.push(line);
89
+ continue;
90
+ }
91
+ if (/^\s*$/.test(line)) {
92
+ flushBuf();
93
+ out.push('');
94
+ continue;
95
+ }
96
+ if (/^#/.test(line)) {
97
+ flushBuf();
98
+ stack = [];
99
+ out.push(line);
100
+ continue;
101
+ }
102
+ const itemMatch = line.match(listItemRe);
103
+ if (itemMatch) {
104
+ flushBuf();
105
+ const indent = itemMatch[1].length;
106
+ while (stack.length > 0 && stack[stack.length - 1].indent >= indent) {
107
+ stack.pop();
108
+ }
109
+ stack.push({ indent });
110
+ buf = line;
111
+ continue;
112
+ }
113
+ if (/^\s/.test(line)) {
114
+ const indent = leadingSpaces(line);
115
+ while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
116
+ flushBuf();
117
+ stack.pop();
118
+ }
119
+ const trimmed = line.replace(/^\s+/, '');
120
+ buf = buf === '' ? trimmed : `${buf} ${trimmed}`;
121
+ continue;
122
+ }
123
+ flushBuf();
124
+ stack = [];
125
+ out.push(line);
126
+ }
127
+ flushBuf();
128
+
129
+ process.stdout.write(out.join('\n'));
130
+ if (!out[out.length - 1]?.endsWith('\n')) process.stdout.write('\n');