ashlr-plugin 1.36.4

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 (499) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +39 -0
  3. package/.claude-plugin/plugin.json +36 -0
  4. package/.codex/agents/README.md +9 -0
  5. package/.codex/agents/ashlr-explorer.md +36 -0
  6. package/.codex/agents/ashlr-worker.md +38 -0
  7. package/.codex-plugin/plugin.json +45 -0
  8. package/.mcp.json +14 -0
  9. package/LICENSE +21 -0
  10. package/README.md +311 -0
  11. package/agents/ashlr-code.md +69 -0
  12. package/agents/ashlr-explore.md +44 -0
  13. package/agents/ashlr-plan.md +54 -0
  14. package/bench/fixtures/notebook-large.ipynb +660 -0
  15. package/bench/fixtures/notebook-small.ipynb +39 -0
  16. package/bench/fixtures/tasklist-closed.json +62 -0
  17. package/bench/fixtures/tasklist-large-mixed.json +27 -0
  18. package/bench/fixtures/tasklist-sprint-open.json +79 -0
  19. package/bench/fixtures/websearch-bun-runtime.json +32 -0
  20. package/bench/fixtures/websearch-react-hooks-patterns.json +32 -0
  21. package/bench/fixtures/websearch-typescript-generics.json +38 -0
  22. package/bench/refs/node-sdk/.refrev +1 -0
  23. package/bench/refs/node-sdk/README.md +17 -0
  24. package/bench/refs/node-sdk/src/agent/agent.ts +163 -0
  25. package/bench/refs/node-sdk/src/agent/create-agent-ui-stream.ts +83 -0
  26. package/bench/refs/node-sdk/src/agent/tool-loop-agent.ts +238 -0
  27. package/bench/refs/node-sdk/src/embed/embed-many.ts +409 -0
  28. package/bench/refs/node-sdk/src/embed/embed-result.ts +50 -0
  29. package/bench/refs/node-sdk/src/embed/embed.ts +257 -0
  30. package/bench/refs/node-sdk/src/error/invalid-argument-error.ts +34 -0
  31. package/bench/refs/node-sdk/src/error/invalid-tool-input-error.ts +33 -0
  32. package/bench/refs/node-sdk/src/error/no-object-generated-error.ts +70 -0
  33. package/bench/refs/node-sdk/src/generate-image/generate-image-result.ts +42 -0
  34. package/bench/refs/node-sdk/src/generate-image/generate-image.ts +362 -0
  35. package/bench/refs/node-sdk/src/generate-object/generate-object-result.ts +67 -0
  36. package/bench/refs/node-sdk/src/generate-object/generate-object.ts +501 -0
  37. package/bench/refs/node-sdk/src/generate-object/inject-json-instruction.ts +30 -0
  38. package/bench/refs/node-sdk/src/generate-object/output-strategy.ts +415 -0
  39. package/bench/refs/node-sdk/src/generate-object/repair-text.ts +12 -0
  40. package/bench/refs/node-sdk/src/generate-object/stream-object.ts +960 -0
  41. package/bench/refs/node-sdk/src/generate-speech/generate-speech.ts +191 -0
  42. package/bench/refs/node-sdk/src/generate-text/generate-text.ts +1484 -0
  43. package/bench/refs/node-sdk/src/generate-text/stream-text.ts +2708 -0
  44. package/bench/refs/node-sdk/src/generate-text/tool-call.ts +59 -0
  45. package/bench/refs/node-sdk/src/generate-text/tool-result.ts +36 -0
  46. package/bench/refs/node-sdk/src/telemetry/create-telemetry-dispatcher.ts +146 -0
  47. package/bench/refs/node-sdk/src/telemetry/telemetry-registry.ts +15 -0
  48. package/bench/refs/node-sdk/src/telemetry/telemetry.ts +223 -0
  49. package/bench/refs/node-sdk/src/util/async-iterable-stream.ts +94 -0
  50. package/bench/refs/node-sdk/src/util/consume-stream.ts +31 -0
  51. package/bench/refs/node-sdk/src/util/create-stitchable-stream.ts +112 -0
  52. package/bench/refs/node-sdk/src/util/download/download.ts +74 -0
  53. package/bench/refs/node-sdk/src/util/error-handler.ts +1 -0
  54. package/bench/refs/node-sdk/src/util/fix-json.ts +401 -0
  55. package/bench/refs/node-sdk/src/util/merge-objects.ts +79 -0
  56. package/bench/refs/python-lib/.refrev +1 -0
  57. package/bench/refs/python-lib/README.md +18 -0
  58. package/bench/refs/python-lib/pandas/core/_numba/executor.py +249 -0
  59. package/bench/refs/python-lib/pandas/core/_numba/extensions.py +586 -0
  60. package/bench/refs/python-lib/pandas/core/_numba/kernels/mean_.py +198 -0
  61. package/bench/refs/python-lib/pandas/core/_numba/kernels/min_max_.py +178 -0
  62. package/bench/refs/python-lib/pandas/core/_numba/kernels/sum_.py +255 -0
  63. package/bench/refs/python-lib/pandas/core/_numba/kernels/var_.py +251 -0
  64. package/bench/refs/python-lib/pandas/core/accessor.py +520 -0
  65. package/bench/refs/python-lib/pandas/core/api.py +138 -0
  66. package/bench/refs/python-lib/pandas/core/array_algos/masked_accumulations.py +97 -0
  67. package/bench/refs/python-lib/pandas/core/array_algos/masked_reductions.py +252 -0
  68. package/bench/refs/python-lib/pandas/core/array_algos/putmask.py +150 -0
  69. package/bench/refs/python-lib/pandas/core/array_algos/quantile.py +232 -0
  70. package/bench/refs/python-lib/pandas/core/array_algos/replace.py +159 -0
  71. package/bench/refs/python-lib/pandas/core/array_algos/take.py +609 -0
  72. package/bench/refs/python-lib/pandas/core/arraylike.py +534 -0
  73. package/bench/refs/python-lib/pandas/core/arrays/_arrow_string_mixins.py +434 -0
  74. package/bench/refs/python-lib/pandas/core/arrays/_mixins.py +630 -0
  75. package/bench/refs/python-lib/pandas/core/arrays/_ranges.py +276 -0
  76. package/bench/refs/python-lib/pandas/core/arrays/_utils.py +77 -0
  77. package/bench/refs/python-lib/pandas/core/arrays/arrow/accessors.py +521 -0
  78. package/bench/refs/python-lib/pandas/core/arrays/arrow/extension_types.py +174 -0
  79. package/bench/refs/python-lib/pandas/core/arrays/boolean.py +454 -0
  80. package/bench/refs/python-lib/pandas/core/arrays/floating.py +192 -0
  81. package/bench/refs/python-lib/pandas/core/arrays/integer.py +296 -0
  82. package/bench/refs/python-lib/pandas/core/arrays/masked.py +2140 -0
  83. package/bench/refs/python-lib/pandas/core/arrays/numeric.py +305 -0
  84. package/bench/refs/python-lib/pandas/core/arrays/numpy_.py +701 -0
  85. package/bench/refs/python-lib/pandas/core/arrays/period.py +1586 -0
  86. package/bench/refs/python-lib/pandas/core/arrays/sparse/accessor.py +505 -0
  87. package/bench/refs/python-lib/pandas/core/arrays/sparse/scipy_sparse.py +208 -0
  88. package/bench/refs/rust-project/.refrev +1 -0
  89. package/bench/refs/rust-project/README.md +17 -0
  90. package/bench/refs/rust-project/tokio/src/doc/os.rs +68 -0
  91. package/bench/refs/rust-project/tokio/src/fs/dir_builder.rs +129 -0
  92. package/bench/refs/rust-project/tokio/src/fs/file.rs +1113 -0
  93. package/bench/refs/rust-project/tokio/src/fs/mocks.rs +176 -0
  94. package/bench/refs/rust-project/tokio/src/fs/mod.rs +324 -0
  95. package/bench/refs/rust-project/tokio/src/fs/open_options.rs +856 -0
  96. package/bench/refs/rust-project/tokio/src/fs/read.rs +80 -0
  97. package/bench/refs/rust-project/tokio/src/fs/read_dir.rs +357 -0
  98. package/bench/refs/rust-project/tokio/src/fs/write.rs +99 -0
  99. package/bench/refs/rust-project/tokio/src/future/maybe_done.rs +123 -0
  100. package/bench/refs/rust-project/tokio/src/future/try_join.rs +82 -0
  101. package/bench/refs/rust-project/tokio/src/io/async_buf_read.rs +117 -0
  102. package/bench/refs/rust-project/tokio/src/io/async_read.rs +133 -0
  103. package/bench/refs/rust-project/tokio/src/io/async_seek.rs +96 -0
  104. package/bench/refs/rust-project/tokio/src/io/async_write.rs +402 -0
  105. package/bench/refs/rust-project/tokio/src/io/blocking.rs +339 -0
  106. package/bench/refs/rust-project/tokio/src/io/interest.rs +345 -0
  107. package/bench/refs/rust-project/tokio/src/io/join.rs +130 -0
  108. package/bench/refs/rust-project/tokio/src/io/mod.rs +308 -0
  109. package/bench/refs/rust-project/tokio/src/io/poll_evented.rs +319 -0
  110. package/bench/refs/rust-project/tokio/src/io/read_buf.rs +344 -0
  111. package/bench/refs/rust-project/tokio/src/io/ready.rs +322 -0
  112. package/bench/refs/rust-project/tokio/src/io/split.rs +166 -0
  113. package/bench/refs/rust-project/tokio/src/io/stderr.rs +148 -0
  114. package/bench/refs/rust-project/tokio/src/io/stdin.rs +98 -0
  115. package/bench/refs/rust-project/tokio/src/io/stdio_common.rs +222 -0
  116. package/bench/refs/rust-project/tokio/src/io/stdout.rs +197 -0
  117. package/bench/refs/rust-project/tokio/src/io/util/buf_reader.rs +311 -0
  118. package/bench/refs/rust-project/tokio/src/io/util/buf_stream.rs +207 -0
  119. package/bench/refs/rust-project/tokio/src/io/util/buf_writer.rs +310 -0
  120. package/bench/refs/rust-project/tokio/src/io/util/chain.rs +144 -0
  121. package/commands/_spawn-patterns.json +32 -0
  122. package/commands/ashlr-allow.md +21 -0
  123. package/commands/ashlr-benchmark.md +53 -0
  124. package/commands/ashlr-brief.md +97 -0
  125. package/commands/ashlr-budget.md +76 -0
  126. package/commands/ashlr-compact.md +54 -0
  127. package/commands/ashlr-dashboard.md +37 -0
  128. package/commands/ashlr-doctor.md +56 -0
  129. package/commands/ashlr-eco-mode.md +92 -0
  130. package/commands/ashlr-genome-init.md +24 -0
  131. package/commands/ashlr-genome-keygen.md +36 -0
  132. package/commands/ashlr-genome-loop.md +118 -0
  133. package/commands/ashlr-genome-push.md +36 -0
  134. package/commands/ashlr-genome-rewrap.md +48 -0
  135. package/commands/ashlr-genome-team-init.md +67 -0
  136. package/commands/ashlr-handoff.md +23 -0
  137. package/commands/ashlr-help.md +86 -0
  138. package/commands/ashlr-hook-timings.md +30 -0
  139. package/commands/ashlr-leaderboard.md +50 -0
  140. package/commands/ashlr-ollama-setup.md +39 -0
  141. package/commands/ashlr-orchestrate-status.md +72 -0
  142. package/commands/ashlr-orchestrate.md +83 -0
  143. package/commands/ashlr-parallelize.md +63 -0
  144. package/commands/ashlr-report-crash.md +33 -0
  145. package/commands/ashlr-resume.md +72 -0
  146. package/commands/ashlr-savings.md +24 -0
  147. package/commands/ashlr-settings.md +50 -0
  148. package/commands/ashlr-spawn.md +61 -0
  149. package/commands/ashlr-start.md +60 -0
  150. package/commands/ashlr-status.md +64 -0
  151. package/commands/ashlr-team-invite.md +47 -0
  152. package/commands/ashlr-tier.md +122 -0
  153. package/commands/ashlr-tour.md +60 -0
  154. package/commands/ashlr-update.md +155 -0
  155. package/commands/ashlr-upgrade.md +96 -0
  156. package/commands/goal.md +58 -0
  157. package/commands/loop.md +40 -0
  158. package/docs/CNAME +1 -0
  159. package/docs/PRO_TIER.md +104 -0
  160. package/docs/README.md +21 -0
  161. package/docs/architecture.md +415 -0
  162. package/docs/ashlr-pipe.md +161 -0
  163. package/docs/assets/logo.svg +19 -0
  164. package/docs/assets/og.png +0 -0
  165. package/docs/assets/og.svg +41 -0
  166. package/docs/benchmarks-v2.json +355 -0
  167. package/docs/benchmarks.json +102 -0
  168. package/docs/benchmarks.md +211 -0
  169. package/docs/billing.md +185 -0
  170. package/docs/cloud-genome.md +155 -0
  171. package/docs/competitive-wozcode.md +92 -0
  172. package/docs/deploy.md +325 -0
  173. package/docs/email-templates.md +113 -0
  174. package/docs/embed-tuning.md +103 -0
  175. package/docs/genome.md +120 -0
  176. package/docs/github-oauth-onboarding.md +127 -0
  177. package/docs/hero-video-script.md +129 -0
  178. package/docs/index.html +2262 -0
  179. package/docs/install-prompt.md +78 -0
  180. package/docs/install-windows.md +132 -0
  181. package/docs/install.ps1 +171 -0
  182. package/docs/install.sh +146 -0
  183. package/docs/launch/README.md +114 -0
  184. package/docs/launch/hn.md +49 -0
  185. package/docs/launch/reddit.md +45 -0
  186. package/docs/launch/x.md +65 -0
  187. package/docs/launch-post.md +112 -0
  188. package/docs/legal.md +78 -0
  189. package/docs/llm-providers.md +99 -0
  190. package/docs/multi-host-mcp.md +217 -0
  191. package/docs/multi-turn-architecture.md +108 -0
  192. package/docs/operations/deployment.md +108 -0
  193. package/docs/operations/error-monitoring.md +70 -0
  194. package/docs/operations/weekly-digest.md +70 -0
  195. package/docs/operations.md +494 -0
  196. package/docs/platform-support.md +133 -0
  197. package/docs/policy-packs.md +188 -0
  198. package/docs/press-kit/fact-sheet.md +78 -0
  199. package/docs/press-kit/founder-bio.md +3 -0
  200. package/docs/press-kit/logo.svg +33 -0
  201. package/docs/press-kit/screenshots.md +146 -0
  202. package/docs/pricing.md +251 -0
  203. package/docs/pro-backend-architecture.md +523 -0
  204. package/docs/producthunt.md +52 -0
  205. package/docs/router-migration.md +110 -0
  206. package/docs/session-log-schema.md +68 -0
  207. package/docs/social.md +95 -0
  208. package/docs/team-genome.md +528 -0
  209. package/docs/telemetry.md +194 -0
  210. package/docs/test-isolation.md +100 -0
  211. package/docs/upgrade.md +159 -0
  212. package/hooks/_hook-errors.ts +87 -0
  213. package/hooks/_hook-perf-emit.ts +113 -0
  214. package/hooks/_hook-timing-ledger.ts +1133 -0
  215. package/hooks/_nudge-throttle.ts +127 -0
  216. package/hooks/_recent-blocks.ts +127 -0
  217. package/hooks/audit-upload.ts +86 -0
  218. package/hooks/codex-hooks.json +171 -0
  219. package/hooks/commit-attribution.ts +179 -0
  220. package/hooks/edit-batching-nudge.ts +154 -0
  221. package/hooks/genome-scribe-hook.ts +219 -0
  222. package/hooks/hooks.json +294 -0
  223. package/hooks/orient-nudge-hook.ts +242 -0
  224. package/hooks/policy-enforce.ts +214 -0
  225. package/hooks/post-tool-use-embedding.ts +93 -0
  226. package/hooks/post-tool-use-genome.sh +37 -0
  227. package/hooks/post-tool-use-genome.ts +69 -0
  228. package/hooks/posttooluse-correlate.ts +122 -0
  229. package/hooks/posttooluse-dedupe-output.ts +241 -0
  230. package/hooks/posttooluse-genome-refresh.ts +223 -0
  231. package/hooks/posttooluse-native-nudge.ts +205 -0
  232. package/hooks/posttooluse-stale-result.ts +277 -0
  233. package/hooks/pretooluse-bash.ts +63 -0
  234. package/hooks/pretooluse-budget-guard.ts +318 -0
  235. package/hooks/pretooluse-common.ts +985 -0
  236. package/hooks/pretooluse-eco-router.ts +196 -0
  237. package/hooks/pretooluse-edit.sh +93 -0
  238. package/hooks/pretooluse-edit.ts +197 -0
  239. package/hooks/pretooluse-glob.ts +70 -0
  240. package/hooks/pretooluse-grep.sh +85 -0
  241. package/hooks/pretooluse-grep.ts +96 -0
  242. package/hooks/pretooluse-notebookedit.ts +121 -0
  243. package/hooks/pretooluse-read.sh +118 -0
  244. package/hooks/pretooluse-read.ts +118 -0
  245. package/hooks/pretooluse-task.ts +130 -0
  246. package/hooks/pretooluse-webfetch.ts +66 -0
  247. package/hooks/pretooluse-websearch.ts +85 -0
  248. package/hooks/pretooluse-write.ts +128 -0
  249. package/hooks/pulse-emit.ts +197 -0
  250. package/hooks/session-end-consolidate.sh +35 -0
  251. package/hooks/session-end-consolidate.ts +188 -0
  252. package/hooks/session-end-stats-gc.ts +80 -0
  253. package/hooks/session-log-append.sh +94 -0
  254. package/hooks/session-log-append.ts +116 -0
  255. package/hooks/session-start.ts +993 -0
  256. package/hooks/sessionend-hook-health-nudge.ts +451 -0
  257. package/hooks/sessionend-savings-nudge.ts +318 -0
  258. package/hooks/sessionend-status-line-nudge.ts +296 -0
  259. package/hooks/sessionstart-brief.ts +140 -0
  260. package/hooks/sessionstart-cost-refactor.ts +95 -0
  261. package/hooks/sessionstart-efficient.ts +110 -0
  262. package/hooks/sessionstart-genome-author.ts +93 -0
  263. package/hooks/sessionstart-lean-tools.ts +98 -0
  264. package/hooks/sessionstart-search.ts +97 -0
  265. package/hooks/stop-accounting.ts +152 -0
  266. package/hooks/subagent-stop-rollup.ts +162 -0
  267. package/hooks/userpromptsubmit-brief-trigger.ts +156 -0
  268. package/package.json +56 -0
  269. package/ports/README.md +108 -0
  270. package/ports/cursor/mcp.json +11 -0
  271. package/ports/goose/recipe.yaml +26 -0
  272. package/scripts/ashlr-mcp.ts +40 -0
  273. package/scripts/auto-update.ts +177 -0
  274. package/scripts/baseline-scan.ts +831 -0
  275. package/scripts/bench-orchestrate.ts +377 -0
  276. package/scripts/benchmark-refs.ts +292 -0
  277. package/scripts/bootstrap.mjs +100 -0
  278. package/scripts/brief-eval.ts +234 -0
  279. package/scripts/bump-version.ts +82 -0
  280. package/scripts/bun-resolve.d.mts +11 -0
  281. package/scripts/bun-resolve.mjs +59 -0
  282. package/scripts/calibrate-grep.ts +467 -0
  283. package/scripts/capture-grep-workload.ts +260 -0
  284. package/scripts/check-tier.ts +61 -0
  285. package/scripts/check-version-sync.ts +97 -0
  286. package/scripts/cli-orchestrate-status.ts +386 -0
  287. package/scripts/cli-orchestrate.ts +303 -0
  288. package/scripts/cli.ts +478 -0
  289. package/scripts/cloud-smoke-test.ts +313 -0
  290. package/scripts/coach-report.ts +373 -0
  291. package/scripts/context-db-migrate.ts +137 -0
  292. package/scripts/context-status.ts +43 -0
  293. package/scripts/demo-run.ts +255 -0
  294. package/scripts/deploy-smoke.sh +140 -0
  295. package/scripts/doctor.ts +992 -0
  296. package/scripts/embed-file-worker.ts +104 -0
  297. package/scripts/embed-tune.ts +308 -0
  298. package/scripts/errors-report.ts +428 -0
  299. package/scripts/find-test-leak.ts +69 -0
  300. package/scripts/generate-badge.ts +357 -0
  301. package/scripts/genome-auto-consolidate.ts +533 -0
  302. package/scripts/genome-auto-propose.ts +578 -0
  303. package/scripts/genome-cloud-pull.ts +363 -0
  304. package/scripts/genome-cloud-push.ts +382 -0
  305. package/scripts/genome-cloud-sync.ts +587 -0
  306. package/scripts/genome-commit-watcher.ts +295 -0
  307. package/scripts/genome-init.ts +986 -0
  308. package/scripts/genome-key.ts +233 -0
  309. package/scripts/genome-keygen.ts +168 -0
  310. package/scripts/genome-link.ts +59 -0
  311. package/scripts/genome-refresh-worker.ts +364 -0
  312. package/scripts/genome-rewrap.ts +90 -0
  313. package/scripts/genome-synthesizer.ts +679 -0
  314. package/scripts/genome-team-init.ts +312 -0
  315. package/scripts/handoff-pack.ts +438 -0
  316. package/scripts/hook-bootstrap.mjs +75 -0
  317. package/scripts/hook-timing-reader.ts +371 -0
  318. package/scripts/hook-timings-report.ts +554 -0
  319. package/scripts/install-genome-hooks.ts +196 -0
  320. package/scripts/install-onnx-model.ts +154 -0
  321. package/scripts/install-permissions.ts +276 -0
  322. package/scripts/install-status-line.ts +150 -0
  323. package/scripts/mcp-entrypoint.sh +78 -0
  324. package/scripts/mcp-entrypoint.ts +130 -0
  325. package/scripts/mdx-angle-bracket-lint.ts +152 -0
  326. package/scripts/measure-cold-start.ts +126 -0
  327. package/scripts/migrate-stats-to-sqlite.ts +86 -0
  328. package/scripts/ollama-setup.ts +498 -0
  329. package/scripts/onboarding-wizard.ts +1691 -0
  330. package/scripts/orchestrate-emit-telemetry.ts +177 -0
  331. package/scripts/orchestrate-expand.ts +517 -0
  332. package/scripts/orchestrate-run.ts +744 -0
  333. package/scripts/print-legend.ts +50 -0
  334. package/scripts/product-metadata.ts +38 -0
  335. package/scripts/prune-old-memories.ts +208 -0
  336. package/scripts/publish.sh +88 -0
  337. package/scripts/read-calibration.ts +122 -0
  338. package/scripts/report-crash.ts +226 -0
  339. package/scripts/run-benchmark.ts +750 -0
  340. package/scripts/run-root-tests.ts +28 -0
  341. package/scripts/savings-dashboard.ts +1258 -0
  342. package/scripts/savings-report-extras.ts +447 -0
  343. package/scripts/savings-status-line.ts +873 -0
  344. package/scripts/session-event-emit.ts +266 -0
  345. package/scripts/session-greet.ts +686 -0
  346. package/scripts/session-log-report.ts +551 -0
  347. package/scripts/session-resume.ts +614 -0
  348. package/scripts/set-hook-mode.ts +83 -0
  349. package/scripts/smoke-cross-platform.ts +117 -0
  350. package/scripts/smoke-realtime.ts +257 -0
  351. package/scripts/smoke-tools.ts +21 -0
  352. package/scripts/stats-cloud-pull.ts +213 -0
  353. package/scripts/stats-cloud-sync.ts +261 -0
  354. package/scripts/telemetry-flush.ts +160 -0
  355. package/scripts/telemetry-status.ts +266 -0
  356. package/scripts/timing-budget.ts +35 -0
  357. package/scripts/ui-animation.ts +120 -0
  358. package/scripts/ui.ts +431 -0
  359. package/scripts/upgrade-flow.ts +775 -0
  360. package/scripts/weekly-digest.ts +394 -0
  361. package/servers/_accounting.ts +78 -0
  362. package/servers/_ask-router.ts +84 -0
  363. package/servers/_ast-chunker.ts +434 -0
  364. package/servers/_ast-helpers.ts +183 -0
  365. package/servers/_ast-languages.ts +153 -0
  366. package/servers/_ast-refactor/_shared.ts +221 -0
  367. package/servers/_ast-refactor/cross-file-rename.ts +560 -0
  368. package/servers/_ast-refactor/extract-function.ts +357 -0
  369. package/servers/_ast-refactor/file-local-rename.ts +172 -0
  370. package/servers/_ast-refactor.ts +41 -0
  371. package/servers/_ast-skeleton.ts +107 -0
  372. package/servers/_bash-summarizers-registry.ts +1096 -0
  373. package/servers/_crash-dump.ts +210 -0
  374. package/servers/_cwd-clamp.ts +342 -0
  375. package/servers/_edit-log.ts +28 -0
  376. package/servers/_edit-match.ts +236 -0
  377. package/servers/_edit-validate.ts +196 -0
  378. package/servers/_embed-calibration.ts +123 -0
  379. package/servers/_embedding-cache.ts +401 -0
  380. package/servers/_embedding-model.ts +329 -0
  381. package/servers/_events.ts +113 -0
  382. package/servers/_genome-cache.ts +154 -0
  383. package/servers/_genome-commits.ts +172 -0
  384. package/servers/_genome-crypto-v2.ts +242 -0
  385. package/servers/_genome-crypto.ts +259 -0
  386. package/servers/_genome-discoveries.ts +140 -0
  387. package/servers/_genome-embed-populator.ts +216 -0
  388. package/servers/_genome-freshness.ts +134 -0
  389. package/servers/_genome-issues.ts +171 -0
  390. package/servers/_genome-live.ts +263 -0
  391. package/servers/_genome-prs.ts +188 -0
  392. package/servers/_genome-search.ts +479 -0
  393. package/servers/_genome-sync.ts +297 -0
  394. package/servers/_history-tracker.ts +279 -0
  395. package/servers/_http-helpers.ts +151 -0
  396. package/servers/_identity-hash.ts +151 -0
  397. package/servers/_llm-providers/anthropic.ts +132 -0
  398. package/servers/_llm-providers/cloud.ts +195 -0
  399. package/servers/_llm-providers/index.ts +350 -0
  400. package/servers/_llm-providers/local.ts +127 -0
  401. package/servers/_llm-providers/onnx.ts +393 -0
  402. package/servers/_llm-providers/types.ts +27 -0
  403. package/servers/_manifest-v2.ts +447 -0
  404. package/servers/_mcp-host.ts +93 -0
  405. package/servers/_nudge-events.ts +451 -0
  406. package/servers/_orchestrate-executor.ts +316 -0
  407. package/servers/_prefetch.ts +435 -0
  408. package/servers/_pricing.ts +256 -0
  409. package/servers/_pro.ts +381 -0
  410. package/servers/_read-cache.ts +44 -0
  411. package/servers/_router-handlers.ts +46 -0
  412. package/servers/_router.ts +55 -0
  413. package/servers/_run-with-timeout.ts +173 -0
  414. package/servers/_savings-render.ts +214 -0
  415. package/servers/_stats-sqlite.ts +640 -0
  416. package/servers/_stats.ts +1183 -0
  417. package/servers/_status-line-cells.ts +469 -0
  418. package/servers/_streaks.ts +162 -0
  419. package/servers/_summarize.ts +208 -0
  420. package/servers/_task-graph-render.ts +217 -0
  421. package/servers/_task-graph.ts +545 -0
  422. package/servers/_telemetry.ts +719 -0
  423. package/servers/_test-parsers.ts +530 -0
  424. package/servers/_test-watch.ts +626 -0
  425. package/servers/_text-helpers.ts +37 -0
  426. package/servers/_token-measure.ts +160 -0
  427. package/servers/_tool-base.ts +248 -0
  428. package/servers/_weekly-digest.ts +391 -0
  429. package/servers/ask-server-handlers.ts +49 -0
  430. package/servers/ask-server.ts +174 -0
  431. package/servers/bash-server-handlers.ts +138 -0
  432. package/servers/bash-server.ts +1136 -0
  433. package/servers/bench.ts +186 -0
  434. package/servers/diff-semantic-server-handlers.ts +39 -0
  435. package/servers/diff-semantic-server.ts +566 -0
  436. package/servers/diff-server-handlers.ts +317 -0
  437. package/servers/diff-server.ts +20 -0
  438. package/servers/edit-server-handlers.ts +31 -0
  439. package/servers/edit-server.ts +209 -0
  440. package/servers/edit-structural-server-handlers.ts +322 -0
  441. package/servers/efficiency-server-handlers.ts +161 -0
  442. package/servers/efficiency-server.ts +1135 -0
  443. package/servers/flush-server-handlers.ts +21 -0
  444. package/servers/flush-server.ts +19 -0
  445. package/servers/genome-server-handlers.ts +118 -0
  446. package/servers/genome-server.ts +397 -0
  447. package/servers/github-server-handlers.ts +187 -0
  448. package/servers/github-server.ts +856 -0
  449. package/servers/glob-server-handlers.ts +233 -0
  450. package/servers/glob-server.ts +20 -0
  451. package/servers/grep-server-handlers.ts +61 -0
  452. package/servers/grep-server.ts +564 -0
  453. package/servers/http-server-handlers.ts +36 -0
  454. package/servers/http-server.ts +149 -0
  455. package/servers/logs-server-handlers.ts +42 -0
  456. package/servers/logs-server.ts +359 -0
  457. package/servers/ls-server-handlers.ts +166 -0
  458. package/servers/ls-server.ts +20 -0
  459. package/servers/multi-edit-server-handlers.ts +51 -0
  460. package/servers/multi-edit-server.ts +422 -0
  461. package/servers/notebook-edit-server-handlers.ts +34 -0
  462. package/servers/notebook-edit-server.ts +181 -0
  463. package/servers/orient-server-handlers.ts +52 -0
  464. package/servers/orient-server.ts +436 -0
  465. package/servers/pipe-server-handlers.ts +68 -0
  466. package/servers/pipe-server.ts +300 -0
  467. package/servers/read-server-handlers.ts +42 -0
  468. package/servers/read-server.ts +206 -0
  469. package/servers/rename-file-server-handlers.ts +67 -0
  470. package/servers/rename-file-server.ts +699 -0
  471. package/servers/savings-server-handlers.ts +27 -0
  472. package/servers/savings-server.ts +54 -0
  473. package/servers/search-replace-regex-server-handlers.ts +96 -0
  474. package/servers/search-replace-regex-server.ts +751 -0
  475. package/servers/sql-server-handlers.ts +58 -0
  476. package/servers/sql-server.ts +505 -0
  477. package/servers/task-server-handlers.ts +358 -0
  478. package/servers/task-server.ts +24 -0
  479. package/servers/test-server-handlers.ts +351 -0
  480. package/servers/test-server.ts +20 -0
  481. package/servers/tree-server-handlers.ts +503 -0
  482. package/servers/tree-server.ts +20 -0
  483. package/servers/webfetch-server-handlers.ts +166 -0
  484. package/servers/webfetch-server.ts +20 -0
  485. package/servers/websearch-server-handlers.ts +215 -0
  486. package/servers/websearch-server.ts +19 -0
  487. package/servers/write-server-handlers.ts +27 -0
  488. package/servers/write-server.ts +97 -0
  489. package/skills/ashlr-brief/SKILL.md +80 -0
  490. package/skills/ashlr-codex-doctor/SKILL.md +21 -0
  491. package/skills/ashlr-codex-efficient-tools/SKILL.md +17 -0
  492. package/skills/ashlr-codex-genome-refresh/SKILL.md +20 -0
  493. package/skills/ashlr-codex-parallel-explore/SKILL.md +20 -0
  494. package/skills/ashlr-codex-savings/SKILL.md +17 -0
  495. package/skills/ashlr-cost-refactor/SKILL.md +50 -0
  496. package/skills/ashlr-efficient/SKILL.md +64 -0
  497. package/skills/ashlr-genome-author/SKILL.md +43 -0
  498. package/skills/ashlr-lean-tools/SKILL.md +43 -0
  499. package/skills/ashlr-search/SKILL.md +46 -0
@@ -0,0 +1,626 @@
1
+ /**
2
+ * _test-watch — background test-watch session manager for `ashlr__test`.
3
+ *
4
+ * Companion to the `ashlr__bash_start`/`bash_tail`/`bash_stop`/`bash_list`
5
+ * primitives. A test-watch session:
6
+ *
7
+ * 1. Spawns the detected runner (bun/vitest/jest/pytest/go) once on entry.
8
+ * 2. Watches the session cwd for .ts/.tsx/.js/.jsx/.mjs/.cjs/.py/.go file
9
+ * changes (via `fs.watch` recursive, with a poll fallback when the
10
+ * platform rejects `{ recursive: true }`).
11
+ * 3. Debounces rapid change bursts (200ms) so a multi-file save doesn't
12
+ * thrash the runner.
13
+ * 4. Reruns the same command+args on each settled change.
14
+ * 5. Lives until explicitly stopped (via `stopTestWatch` / `ashlr__bash_stop`)
15
+ * or until the parent MCP server exits (`process.on('exit')`).
16
+ *
17
+ * Sessions are registered with the bash-server via `registerExternalSessions`,
18
+ * so `ashlr__bash_list` enumerates both "bash" and "test-watch" sessions from
19
+ * one place. Per-session metadata is persisted to
20
+ * `~/.ashlr/test-watch-sessions/<id>.json` so a doctor can prune stale entries
21
+ * after a crash.
22
+ *
23
+ * Safety caps:
24
+ * - max 8 concurrent watch sessions per user
25
+ * - 50 MB stdout cap per session (mirrors bash-server)
26
+ * - child is spawned with its own process group on POSIX so SIGKILL reaches
27
+ * grandchildren (vitest workers, bun subprocesses)
28
+ */
29
+
30
+ import { spawn, type ChildProcess } from "child_process";
31
+ import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, watch, writeFileSync, type FSWatcher } from "fs";
32
+ import { writeFile, readFile, mkdir, readdir, unlink } from "fs/promises";
33
+ import { homedir } from "os";
34
+ import { join, sep } from "path";
35
+ import { randomBytes } from "crypto";
36
+ import { registerExternalSessions, type ExternalSession } from "./bash-server";
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Constants
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const MAX_WATCH_SESSIONS = 8;
43
+ const DEBOUNCE_MS = 200;
44
+ const MAX_CUMULATIVE_BYTES = 50 * 1024 * 1024;
45
+ const POLL_INTERVAL_MS = 1000;
46
+ const WATCH_EXTENSIONS = new Set([
47
+ ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go",
48
+ ]);
49
+ const IGNORE_DIRS = new Set([
50
+ "node_modules", ".git", "dist", "build", "coverage",
51
+ ".next", ".nuxt", ".svelte-kit", "target", "__pycache__",
52
+ ".pytest_cache", ".venv", "venv",
53
+ ]);
54
+
55
+ // Prefer $HOME when explicitly set (matches the rest of the ashlr codebase).
56
+ const ASHLR_HOME = process.env.HOME ?? homedir();
57
+ const WATCH_DIR = join(ASHLR_HOME, ".ashlr", "test-watch-sessions");
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Types
61
+ // ---------------------------------------------------------------------------
62
+
63
+ export interface TestWatchArgs {
64
+ /** Working directory to watch and to spawn tests in. Must be clamped. */
65
+ cwd: string;
66
+ /** Command + args array to run on each change (already resolved). */
67
+ command: string[];
68
+ /** Human-readable label for bash_list ("bun test src/foo.test.ts"). */
69
+ label: string;
70
+ /** Optional grep filter appended to args for re-runs (informational). */
71
+ grep?: string;
72
+ }
73
+
74
+ interface WatchSession {
75
+ id: string;
76
+ pid: number; // pid of the CURRENT running child (-1 if idle)
77
+ command: string[];
78
+ label: string;
79
+ cwd: string;
80
+ startedAt: number;
81
+ child: ChildProcess | null;
82
+ stdout: string;
83
+ stderr: string;
84
+ offset: number;
85
+ stderrOffset: number;
86
+ cumulativeBytes: number;
87
+ exitCode: number | null;
88
+ signal: NodeJS.Signals | null;
89
+ lastActivity: number;
90
+ lastRunAt: number;
91
+ runCount: number;
92
+ watcher: FSWatcher | null;
93
+ pollTimer: ReturnType<typeof setInterval> | null;
94
+ pollSnapshot: Map<string, number>; // path -> mtimeMs
95
+ debounceTimer: ReturnType<typeof setTimeout> | null;
96
+ stopped: boolean;
97
+ dataEmitter: Set<() => void>;
98
+ }
99
+
100
+ interface PersistedWatchSession {
101
+ id: string;
102
+ pid: number;
103
+ command: string[];
104
+ label: string;
105
+ cwd: string;
106
+ startedAt: number;
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // In-memory registry
111
+ // ---------------------------------------------------------------------------
112
+
113
+ const SESSIONS = new Map<string, WatchSession>();
114
+
115
+ function newId(): string {
116
+ return randomBytes(4).toString("hex");
117
+ }
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // Persistence
121
+ // ---------------------------------------------------------------------------
122
+
123
+ function sessionFile(id: string): string {
124
+ return join(WATCH_DIR, `${id}.json`);
125
+ }
126
+
127
+ async function persistSession(s: WatchSession): Promise<void> {
128
+ try {
129
+ await mkdir(WATCH_DIR, { recursive: true });
130
+ const payload: PersistedWatchSession = {
131
+ id: s.id,
132
+ pid: s.pid,
133
+ command: s.command,
134
+ label: s.label,
135
+ cwd: s.cwd,
136
+ startedAt: s.startedAt,
137
+ };
138
+ await writeFile(sessionFile(s.id), JSON.stringify(payload, null, 2));
139
+ } catch {
140
+ /* best-effort — disk full / read-only home shouldn't wedge the runner */
141
+ }
142
+ }
143
+
144
+ async function unpersistSession(id: string): Promise<void> {
145
+ try {
146
+ await unlink(sessionFile(id));
147
+ } catch {
148
+ /* already gone */
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Prune stale JSON files whose pids are no longer alive. Called once at
154
+ * module load; dead files don't count against MAX_WATCH_SESSIONS.
155
+ */
156
+ export async function reloadWatchSessions(): Promise<void> {
157
+ if (!existsSync(WATCH_DIR)) return;
158
+ let entries: string[];
159
+ try {
160
+ entries = await readdir(WATCH_DIR);
161
+ } catch {
162
+ return;
163
+ }
164
+ for (const name of entries) {
165
+ if (!name.endsWith(".json")) continue;
166
+ const path = join(WATCH_DIR, name);
167
+ try {
168
+ const raw = await readFile(path, "utf-8");
169
+ const p = JSON.parse(raw) as PersistedWatchSession;
170
+ if (!pidAlive(p.pid)) {
171
+ try { await unlink(path); } catch { /* ignore */ }
172
+ }
173
+ // We can't reattach to a process we didn't spawn, so even live pids
174
+ // from a prior run are orphan zombies; drop their files too. They
175
+ // were cleaned up by process.on('exit') in the previous run when the
176
+ // parent died.
177
+ } catch {
178
+ try { await unlink(path); } catch { /* ignore */ }
179
+ }
180
+ }
181
+ }
182
+
183
+ function pidAlive(pid: number): boolean {
184
+ if (!pid || pid <= 0) return false;
185
+ try {
186
+ process.kill(pid, 0);
187
+ return true;
188
+ } catch {
189
+ return false;
190
+ }
191
+ }
192
+
193
+ // ---------------------------------------------------------------------------
194
+ // File watching
195
+ // ---------------------------------------------------------------------------
196
+
197
+ function isWatchedFile(path: string): boolean {
198
+ const parts = path.split(sep);
199
+ for (const p of parts) {
200
+ if (IGNORE_DIRS.has(p)) return false;
201
+ }
202
+ const dot = path.lastIndexOf(".");
203
+ if (dot < 0) return false;
204
+ const ext = path.slice(dot);
205
+ return WATCH_EXTENSIONS.has(ext);
206
+ }
207
+
208
+ function snapshotDir(root: string): Map<string, number> {
209
+ // Walk the tree breadth-first and stat each watched file. Used by the
210
+ // poll-fallback path when fs.watch(recursive) is unavailable.
211
+ const out = new Map<string, number>();
212
+ const queue: string[] = [root];
213
+ while (queue.length > 0) {
214
+ const dir = queue.shift()!;
215
+ let names: string[];
216
+ try { names = readdirSync(dir); } catch { continue; }
217
+ for (const name of names) {
218
+ if (IGNORE_DIRS.has(name)) continue;
219
+ const full = join(dir, name);
220
+ let st: ReturnType<typeof statSync>;
221
+ try { st = statSync(full); } catch { continue; }
222
+ if (st.isDirectory()) {
223
+ queue.push(full);
224
+ } else if (st.isFile() && isWatchedFile(full)) {
225
+ out.set(full, st.mtimeMs);
226
+ }
227
+ }
228
+ }
229
+ return out;
230
+ }
231
+
232
+ function diffSnapshots(prev: Map<string, number>, next: Map<string, number>): boolean {
233
+ if (prev.size !== next.size) return true;
234
+ for (const [k, v] of next) {
235
+ const pv = prev.get(k);
236
+ if (pv === undefined || pv !== v) return true;
237
+ }
238
+ return false;
239
+ }
240
+
241
+ /**
242
+ * Install a watcher on `cwd`. Prefers `fs.watch(cwd, { recursive: true })`
243
+ * (supported on macOS/Windows; Linux requires kernel >= 5.1 for inotify
244
+ * recursive, otherwise throws ERR_FEATURE_UNAVAILABLE_ON_PLATFORM). On
245
+ * failure, falls back to a 1-second mtime poll.
246
+ *
247
+ * Returns { watcher, pollTimer }; exactly one is non-null.
248
+ */
249
+ function installWatcher(
250
+ session: WatchSession,
251
+ onChange: (path: string) => void,
252
+ ): { watcher: FSWatcher | null; pollTimer: ReturnType<typeof setInterval> | null; pollSnapshot: Map<string, number> } {
253
+ // Try native recursive watch first.
254
+ try {
255
+ const w = watch(session.cwd, { recursive: true }, (_event, filename) => {
256
+ if (!filename) return;
257
+ const fname = typeof filename === "string" ? filename : String(filename);
258
+ const full = join(session.cwd, fname);
259
+ if (!isWatchedFile(full)) return;
260
+ onChange(full);
261
+ });
262
+ // fs.watch emits 'error' on some platforms instead of throwing.
263
+ w.on("error", () => { /* swallow — poll will take over if we respawn */ });
264
+ return { watcher: w, pollTimer: null, pollSnapshot: new Map() };
265
+ } catch {
266
+ // Fallback: poll at 1Hz comparing mtime snapshots.
267
+ const snap = snapshotDir(session.cwd);
268
+ const timer = setInterval(() => {
269
+ const next = snapshotDir(session.cwd);
270
+ if (diffSnapshots(session.pollSnapshot, next)) {
271
+ session.pollSnapshot = next;
272
+ // We don't have a single "changed" path; pass the cwd as a sentinel.
273
+ onChange(session.cwd);
274
+ }
275
+ }, POLL_INTERVAL_MS);
276
+ return { watcher: null, pollTimer: timer, pollSnapshot: snap };
277
+ }
278
+ }
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // Child process lifecycle
282
+ // ---------------------------------------------------------------------------
283
+
284
+ function spawnTestRun(session: WatchSession): void {
285
+ if (session.stopped) return;
286
+ // If a prior run is still going, kill it so we don't stack runners.
287
+ if (session.child && session.exitCode === null) {
288
+ killChild(session.child);
289
+ }
290
+
291
+ const isWin = process.platform === "win32";
292
+ const [bin, ...args] = session.command;
293
+ const child = spawn(bin, args, {
294
+ cwd: session.cwd,
295
+ env: { ...process.env },
296
+ stdio: ["ignore", "pipe", "pipe"],
297
+ detached: !isWin,
298
+ shell: isWin,
299
+ });
300
+
301
+ session.child = child;
302
+ session.pid = child.pid ?? -1;
303
+ session.exitCode = null;
304
+ session.signal = null;
305
+ session.lastRunAt = Date.now();
306
+ session.runCount += 1;
307
+ session.lastActivity = Date.now();
308
+
309
+ const runHeader = `\n[run #${session.runCount} at ${new Date().toISOString()}] $ ${session.label}\n`;
310
+ session.stdout += runHeader;
311
+ session.cumulativeBytes += runHeader.length;
312
+
313
+ child.stdout?.on("data", (b: Buffer) => {
314
+ const s = b.toString("utf-8");
315
+ session.stdout += s;
316
+ session.cumulativeBytes += b.length;
317
+ session.lastActivity = Date.now();
318
+ if (session.cumulativeBytes > MAX_CUMULATIVE_BYTES) {
319
+ session.stderr += `\n[ashlr: test-watch session exceeded ${MAX_CUMULATIVE_BYTES} bytes — killed]\n`;
320
+ stopTestWatchInternal(session);
321
+ }
322
+ for (const fn of [...session.dataEmitter]) fn();
323
+ });
324
+ child.stderr?.on("data", (b: Buffer) => {
325
+ session.stderr += b.toString("utf-8");
326
+ session.lastActivity = Date.now();
327
+ for (const fn of [...session.dataEmitter]) fn();
328
+ });
329
+ child.on("close", (code, signal) => {
330
+ if (session.child === child) {
331
+ session.exitCode = code;
332
+ session.signal = signal;
333
+ session.pid = -1;
334
+ session.child = null;
335
+ session.lastActivity = Date.now();
336
+ const footer = `[run #${session.runCount} → exit ${code ?? "?"}${signal ? ` · ${signal}` : ""}]\n`;
337
+ session.stdout += footer;
338
+ session.cumulativeBytes += footer.length;
339
+ for (const fn of [...session.dataEmitter]) fn();
340
+ }
341
+ });
342
+ child.on("error", (err) => {
343
+ session.stderr += `\n[spawn error: ${err.message}]\n`;
344
+ for (const fn of [...session.dataEmitter]) fn();
345
+ });
346
+
347
+ // Update persisted pid so a doctor can reap cleanly.
348
+ void persistSession(session);
349
+ }
350
+
351
+ function killChild(child: ChildProcess): void {
352
+ const isWin = process.platform === "win32";
353
+ if (isWin) {
354
+ try { child.kill("SIGKILL"); } catch { /* already dead */ }
355
+ } else if (child.pid != null) {
356
+ try { process.kill(-child.pid, "SIGKILL"); } catch {
357
+ try { child.kill("SIGKILL"); } catch { /* already dead */ }
358
+ }
359
+ } else {
360
+ try { child.kill("SIGKILL"); } catch { /* already dead */ }
361
+ }
362
+ }
363
+
364
+ // ---------------------------------------------------------------------------
365
+ // Public API
366
+ // ---------------------------------------------------------------------------
367
+
368
+ export interface StartResult {
369
+ ok: true;
370
+ id: string;
371
+ pid: number;
372
+ message: string;
373
+ }
374
+
375
+ export interface StartError {
376
+ ok: false;
377
+ message: string;
378
+ }
379
+
380
+ export function startTestWatch(args: TestWatchArgs): StartResult | StartError {
381
+ // Count only still-alive sessions.
382
+ for (const s of [...SESSIONS.values()]) {
383
+ if (s.stopped) SESSIONS.delete(s.id);
384
+ }
385
+ if (SESSIONS.size >= MAX_WATCH_SESSIONS) {
386
+ return {
387
+ ok: false,
388
+ message:
389
+ `ashlr__test watch: max ${MAX_WATCH_SESSIONS} concurrent watch sessions reached — ` +
390
+ `stop one via ashlr__bash_stop first.`,
391
+ };
392
+ }
393
+
394
+ // Ensure the persistence dir exists synchronously so the caller sees
395
+ // the file immediately (tests rely on this).
396
+ try { mkdirSync(WATCH_DIR, { recursive: true }); } catch { /* ignore */ }
397
+
398
+ const id = newId();
399
+ const session: WatchSession = {
400
+ id,
401
+ pid: -1,
402
+ command: args.command,
403
+ label: args.label,
404
+ cwd: args.cwd,
405
+ startedAt: Date.now(),
406
+ child: null,
407
+ stdout: "",
408
+ stderr: "",
409
+ offset: 0,
410
+ stderrOffset: 0,
411
+ cumulativeBytes: 0,
412
+ exitCode: null,
413
+ signal: null,
414
+ lastActivity: Date.now(),
415
+ lastRunAt: 0,
416
+ runCount: 0,
417
+ watcher: null,
418
+ pollTimer: null,
419
+ pollSnapshot: new Map(),
420
+ debounceTimer: null,
421
+ stopped: false,
422
+ dataEmitter: new Set(),
423
+ };
424
+
425
+ // Install watcher with debounce.
426
+ const onChange = (_path: string) => {
427
+ if (session.stopped) return;
428
+ if (session.debounceTimer) clearTimeout(session.debounceTimer);
429
+ session.debounceTimer = setTimeout(() => {
430
+ session.debounceTimer = null;
431
+ spawnTestRun(session);
432
+ }, DEBOUNCE_MS);
433
+ };
434
+ const { watcher, pollTimer, pollSnapshot } = installWatcher(session, onChange);
435
+ session.watcher = watcher;
436
+ session.pollTimer = pollTimer;
437
+ session.pollSnapshot = pollSnapshot;
438
+
439
+ SESSIONS.set(id, session);
440
+
441
+ // Persist synchronously-ish (fire-and-forget; file exists shortly after).
442
+ void persistSession(session);
443
+
444
+ // Kick off the initial run.
445
+ spawnTestRun(session);
446
+
447
+ return {
448
+ ok: true,
449
+ id,
450
+ pid: session.pid,
451
+ message:
452
+ `[ashlr__test watch] started · id=${id} · watching ${args.cwd} · ` +
453
+ `use ashlr__bash_tail id=${id} to stream output, ashlr__bash_stop id=${id} to stop.`,
454
+ };
455
+ }
456
+
457
+ function stopTestWatchInternal(session: WatchSession): void {
458
+ if (session.stopped) return;
459
+ session.stopped = true;
460
+ if (session.debounceTimer) {
461
+ clearTimeout(session.debounceTimer);
462
+ session.debounceTimer = null;
463
+ }
464
+ if (session.watcher) {
465
+ try { session.watcher.close(); } catch { /* ignore */ }
466
+ session.watcher = null;
467
+ }
468
+ if (session.pollTimer) {
469
+ clearInterval(session.pollTimer);
470
+ session.pollTimer = null;
471
+ }
472
+ if (session.child && session.exitCode === null) {
473
+ killChild(session.child);
474
+ }
475
+ SESSIONS.delete(session.id);
476
+ void unpersistSession(session.id);
477
+ for (const fn of [...session.dataEmitter]) fn();
478
+ }
479
+
480
+ export function stopTestWatch(id: string, _signal?: NodeJS.Signals): string | null {
481
+ const s = SESSIONS.get(id);
482
+ if (!s) return null;
483
+ const label = s.label;
484
+ stopTestWatchInternal(s);
485
+ return `[${id}] test-watch stopped · ${label}`;
486
+ }
487
+
488
+ export function getTestWatchSession(id: string): WatchSession | undefined {
489
+ return SESSIONS.get(id);
490
+ }
491
+
492
+ // ---------------------------------------------------------------------------
493
+ // Tail adapter — mirrors ashlr__bash_tail's semantics so the same tool works
494
+ // ---------------------------------------------------------------------------
495
+
496
+ export async function tailTestWatch(
497
+ id: string,
498
+ waitMs: number,
499
+ _maxBytes: number,
500
+ ): Promise<string | null> {
501
+ const s = SESSIONS.get(id);
502
+ if (!s) return null;
503
+
504
+ const hasNew = (): boolean =>
505
+ s.stdout.length > s.offset ||
506
+ s.stderr.length > s.stderrOffset ||
507
+ s.stopped;
508
+
509
+ if (!hasNew() && waitMs > 0) {
510
+ await new Promise<void>((resolve) => {
511
+ let done = false;
512
+ const finish = () => {
513
+ if (done) return;
514
+ done = true;
515
+ s.dataEmitter.delete(notify);
516
+ clearTimeout(t);
517
+ resolve();
518
+ };
519
+ const notify = () => finish();
520
+ const t = setTimeout(finish, waitMs);
521
+ s.dataEmitter.add(notify);
522
+ if (hasNew()) finish();
523
+ });
524
+ }
525
+
526
+ const newStdout = s.stdout.slice(s.offset);
527
+ const newStderr = s.stderr.slice(s.stderrOffset);
528
+ const newBytes = newStdout.length;
529
+ s.offset = s.stdout.length;
530
+ s.stderrOffset = s.stderr.length;
531
+ s.lastActivity = Date.now();
532
+
533
+ const statusLabel = s.stopped
534
+ ? "stopped"
535
+ : s.child
536
+ ? `running (run #${s.runCount})`
537
+ : `idle (last run #${s.runCount} exit ${s.exitCode ?? "?"})`;
538
+
539
+ const header = `[${id} · ${statusLabel}] +${newBytes} bytes since last poll`;
540
+ const stderrBlock = newStderr.length > 0 ? `\n--- stderr ---\n${newStderr}` : "";
541
+ const body = newStdout;
542
+
543
+ return `${header}\n${body}${body.endsWith("\n") || body.length === 0 ? "" : "\n"}${stderrBlock}`;
544
+ }
545
+
546
+ // ---------------------------------------------------------------------------
547
+ // bash-server integration — expose test-watch sessions to ashlr__bash_list
548
+ // and to ashlr__bash_stop / ashlr__bash_tail (so one tool surface manages
549
+ // both kinds of background work).
550
+ // ---------------------------------------------------------------------------
551
+
552
+ function toExternalSession(s: WatchSession): ExternalSession {
553
+ return {
554
+ id: s.id,
555
+ pid: s.pid > 0 ? s.pid : -1,
556
+ command: `[test-watch] ${s.label}`,
557
+ cwd: s.cwd,
558
+ startedAt: s.startedAt,
559
+ cumulativeBytes: s.cumulativeBytes,
560
+ kind: "test-watch",
561
+ };
562
+ }
563
+
564
+ registerExternalSessions({
565
+ kind: "test-watch",
566
+ list(): ExternalSession[] {
567
+ return [...SESSIONS.values()].map(toExternalSession);
568
+ },
569
+ tail(id, waitMs, maxBytes): Promise<string | null> {
570
+ return tailTestWatch(id, waitMs, maxBytes);
571
+ },
572
+ stop(id, signal): string | null {
573
+ return stopTestWatch(id, signal);
574
+ },
575
+ has(id): boolean {
576
+ return SESSIONS.has(id);
577
+ },
578
+ });
579
+
580
+ // ---------------------------------------------------------------------------
581
+ // Parent-exit cleanup — kill every watcher child and clean up JSON files so
582
+ // a future run sees a clean slate.
583
+ // ---------------------------------------------------------------------------
584
+
585
+ let exitHandlerInstalled = false;
586
+ function installExitHandler(): void {
587
+ if (exitHandlerInstalled) return;
588
+ exitHandlerInstalled = true;
589
+ const cleanup = () => {
590
+ for (const s of [...SESSIONS.values()]) {
591
+ try { stopTestWatchInternal(s); } catch { /* ignore */ }
592
+ try {
593
+ // Synchronous unlink on exit — async won't complete.
594
+ unlinkSync(sessionFile(s.id));
595
+ } catch { /* already gone */ }
596
+ // Also synchronously write a best-effort tombstone so cwd-clamp
597
+ // tests can observe final state.
598
+ try {
599
+ writeFileSync(
600
+ sessionFile(s.id) + ".tombstone",
601
+ JSON.stringify({ id: s.id, exitedAt: Date.now() }),
602
+ );
603
+ unlinkSync(sessionFile(s.id) + ".tombstone");
604
+ } catch { /* ignore */ }
605
+ }
606
+ };
607
+ process.on("exit", cleanup);
608
+ process.on("SIGINT", () => { cleanup(); process.exit(130); });
609
+ process.on("SIGTERM", () => { cleanup(); process.exit(143); });
610
+ }
611
+ installExitHandler();
612
+
613
+ // Opportunistic prune at module load (best-effort).
614
+ void reloadWatchSessions();
615
+
616
+ // Exposed for tests.
617
+ export function __activeWatchSessionsForTests(): number {
618
+ return SESSIONS.size;
619
+ }
620
+
621
+ export function __clearWatchSessionsForTests(): void {
622
+ for (const s of [...SESSIONS.values()]) {
623
+ stopTestWatchInternal(s);
624
+ }
625
+ SESSIONS.clear();
626
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * _text-helpers — pure string utilities shared across servers.
3
+ *
4
+ * Kept dependency-free so other `_*`-prefixed modules (especially the
5
+ * router-bound `_ask-router.ts`) can import them without pulling a whole
6
+ * tool-server module.
7
+ */
8
+
9
+ // Minimal English stopword list — enough to filter "how does auth work" → ["auth"].
10
+ export const STOPWORDS: ReadonlySet<string> = new Set([
11
+ "the","and","for","are","but","not","you","all","can","her","was","one",
12
+ "our","out","his","who","its","how","what","where","when","why","does",
13
+ "did","done","this","that","those","these","here","there","with","from",
14
+ "into","onto","your","yours","have","has","had","been","being","work",
15
+ "works","working","about","show","tell","explain","please","over","some",
16
+ "such","than","then","them","they","thing","things","stuff","use","used",
17
+ "using","code","file","files","find","look","see","want","need","get",
18
+ ]);
19
+
20
+ /**
21
+ * Tokenize a natural-language query into content keywords.
22
+ *
23
+ * - Lowercases, splits on non-alnum/underscore boundaries.
24
+ * - Drops tokens ≤3 chars (avoids pronouns + tool noise).
25
+ * - Drops {@link STOPWORDS}.
26
+ * - Deduplicates, preserving first-occurrence order.
27
+ */
28
+ export function extractKeywords(query: string): string[] {
29
+ const tokens = query
30
+ .toLowerCase()
31
+ .split(/[^a-z0-9_]+/)
32
+ .filter((t) => t.length > 3 && !STOPWORDS.has(t));
33
+ const seen = new Set<string>();
34
+ const out: string[] = [];
35
+ for (const t of tokens) if (!seen.has(t)) { seen.add(t); out.push(t); }
36
+ return out;
37
+ }