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,1096 @@
1
+ /**
2
+ * Registry of command-specific stdout summarizers extracted from bash-server.ts.
3
+ *
4
+ * Each entry maps a command key to a summarizer function. The key is either:
5
+ * - a single word (e.g. "ls", "find", "ps")
6
+ * - a two-word prefix joined by "-" (e.g. "git-status", "npm-ls", "npm-audit")
7
+ * - a three-word prefix for docker/kubectl variants
8
+ *
9
+ * `findSummarizer` parses a raw command string and returns the matching fn or null.
10
+ *
11
+ * Summarizers are purely synchronous and size-bounded — they do head/tail slicing,
12
+ * count aggregation, and regex extraction only. For LLM-grade summarization of
13
+ * large diff-like outputs (git diff, git show), callers should route through
14
+ * `summarizeIfLarge` with `PROMPTS.diff` instead; those commands are flagged by
15
+ * the `isLargeDiffCommand` helper below.
16
+ */
17
+
18
+ import { basename } from "path";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Individual summarizer functions (exact copies from bash-server.ts)
22
+ // ---------------------------------------------------------------------------
23
+
24
+ function summarizeGitStatus(stdout: string, branchHint?: string): string {
25
+ const lines = stdout.split("\n").filter((l) => l.length > 0);
26
+ const counts = new Map<string, number>();
27
+ for (const line of lines) {
28
+ const code = line.slice(0, 2).trim() || line.slice(0, 2);
29
+ const key = code === "" ? "?" : code;
30
+ counts.set(key, (counts.get(key) ?? 0) + 1);
31
+ }
32
+ const parts: string[] = [];
33
+ for (const [k, v] of [...counts.entries()].sort()) {
34
+ parts.push(`${k}: ${v}`);
35
+ }
36
+ const branch = branchHint ? ` · branch ${branchHint}` : "";
37
+ return parts.length === 0
38
+ ? `clean${branch}`
39
+ : `${parts.join(", ")}${branch}`;
40
+ }
41
+
42
+ function summarizeLs(stdout: string): string | null {
43
+ const lines = stdout.split("\n").filter((l) => l.length > 0);
44
+ if (lines.length <= 40) return null;
45
+ const head = lines.slice(0, 20).join("\n");
46
+ const tail = lines.slice(-10).join("\n");
47
+ return `${head}\n[... ${lines.length - 30} more entries elided ...]\n${tail}\n· ${lines.length} entries total`;
48
+ }
49
+
50
+ function summarizeFind(stdout: string): string | null {
51
+ const lines = stdout.split("\n").filter((l) => l.length > 0);
52
+ if (lines.length <= 100) return null;
53
+ const head = lines.slice(0, 50).join("\n");
54
+ return `${head}\n[... ${lines.length - 50} more matches elided ...]\n· ${lines.length} matches total`;
55
+ }
56
+
57
+ function summarizePs(stdout: string, cwd: string): string | null {
58
+ const lines = stdout.split("\n").filter((l) => l.length > 0);
59
+ if (lines.length <= 100) return null;
60
+ const cwdName = basename(cwd);
61
+ const header = lines[0]!;
62
+ const filtered = lines.slice(1).filter((l) => cwdName && l.includes(cwdName));
63
+ if (filtered.length > 0 && filtered.length < lines.length - 1) {
64
+ return `${header}\n${filtered.join("\n")}\n· filtered ${filtered.length} of ${lines.length - 1} processes by cwd name '${cwdName}'`;
65
+ }
66
+ return null;
67
+ }
68
+
69
+ function summarizeNpmLs(stdout: string): string | null {
70
+ const lines = stdout.split("\n");
71
+ if (lines.length < 50) return null;
72
+ const seenWarn = new Set<string>();
73
+ const kept: string[] = [];
74
+ for (const line of lines) {
75
+ const w = line.match(/(deduped|UNMET|invalid)/);
76
+ if (w) {
77
+ const sig = line.trim();
78
+ if (seenWarn.has(sig)) continue;
79
+ seenWarn.add(sig);
80
+ }
81
+ const depth = (line.match(/[│├└]\s/g) ?? []).length;
82
+ if (depth > 2) continue;
83
+ kept.push(line);
84
+ }
85
+ if (kept.length >= lines.length) return null;
86
+ return `${kept.join("\n")}\n· collapsed depth>2 and deduped warnings (${lines.length} → ${kept.length} lines)`;
87
+ }
88
+
89
+ function summarizeDockerPs(stdout: string): string | null {
90
+ const lines = stdout.split("\n").filter((l) => l.length > 0);
91
+ if (lines.length < 3) return null;
92
+
93
+ const header = lines[0]!;
94
+ const rows = lines.slice(1);
95
+
96
+ interface DockerRow { id: string; image: string; status: string; ports: string; raw: string }
97
+ const KNOWN = ["CONTAINER ID", "IMAGE", "COMMAND", "CREATED", "STATUS", "PORTS", "NAMES"] as const;
98
+ const starts: Array<{ name: string; start: number }> = [];
99
+ for (const name of KNOWN) {
100
+ const idx = header.indexOf(name);
101
+ if (idx >= 0) starts.push({ name, start: idx });
102
+ }
103
+ if (starts.length < 3) return null;
104
+
105
+ function sliceCol(row: string, name: typeof KNOWN[number]): string {
106
+ const i = starts.findIndex((s) => s.name === name);
107
+ if (i < 0) return "";
108
+ const start = starts[i]!.start;
109
+ const end = starts[i + 1]?.start ?? row.length;
110
+ return row.slice(start, end).trim();
111
+ }
112
+
113
+ const parsed: DockerRow[] = rows.map((raw) => ({
114
+ id: sliceCol(raw, "CONTAINER ID").slice(0, 12),
115
+ image: sliceCol(raw, "IMAGE"),
116
+ status: sliceCol(raw, "STATUS"),
117
+ ports: sliceCol(raw, "PORTS"),
118
+ raw,
119
+ }));
120
+
121
+ const groups = new Map<string, { rows: DockerRow[]; ports: Set<string> }>();
122
+ for (const row of parsed) {
123
+ const key = `${row.image}||${row.status}`;
124
+ const g = groups.get(key) ?? { rows: [], ports: new Set() };
125
+ g.rows.push(row);
126
+ if (row.ports) g.ports.add(row.ports);
127
+ groups.set(key, g);
128
+ }
129
+
130
+ const out: string[] = [header];
131
+ for (const g of groups.values()) {
132
+ if (g.rows.length === 1) {
133
+ out.push(g.rows[0]!.raw);
134
+ } else {
135
+ const r = g.rows[0]!;
136
+ const ports = g.ports.size > 0 ? ` ports: ${[...g.ports].join(", ")}` : "";
137
+ out.push(`${r.image} ×${g.rows.length} containers ${r.status}${ports}`);
138
+ }
139
+ }
140
+ if (out.length >= lines.length) return null;
141
+ out.push(`· ${rows.length} containers total (${groups.size} distinct image/status groups)`);
142
+ return out.join("\n");
143
+ }
144
+
145
+ function summarizeKubectlGet(stdout: string): string | null {
146
+ const lines = stdout.split("\n").filter((l) => l.length > 0);
147
+ if (lines.length < 4) return null;
148
+
149
+ const header = lines[0]!;
150
+ const cols = header.trim().split(/\s+/);
151
+ const nsIdx = cols.findIndex((c) => c.toUpperCase() === "NAMESPACE");
152
+ const statusIdx = cols.findIndex((c) => c.toUpperCase() === "STATUS" || c.toUpperCase() === "READY");
153
+ const ageIdx = cols.findIndex((c) => c.toUpperCase() === "AGE");
154
+ const nameIdx = cols.findIndex((c) => c.toUpperCase() === "NAME");
155
+
156
+ const rows = lines.slice(1);
157
+
158
+ const byNs = new Map<string, string[]>();
159
+ for (const row of rows) {
160
+ const parts = row.trim().split(/\s+/);
161
+ const ns = nsIdx >= 0 ? (parts[nsIdx] ?? "default") : "default";
162
+ const arr = byNs.get(ns) ?? [];
163
+ arr.push(row);
164
+ byNs.set(ns, arr);
165
+ }
166
+
167
+ if (byNs.size === 1) return null;
168
+
169
+ const out: string[] = [header];
170
+ for (const [ns, nsRows] of byNs) {
171
+ const statusCounts = new Map<string, number>();
172
+ for (const row of nsRows) {
173
+ const parts = row.trim().split(/\s+/);
174
+ const st = statusIdx >= 0 ? (parts[statusIdx] ?? "?") : "?";
175
+ statusCounts.set(st, (statusCounts.get(st) ?? 0) + 1);
176
+ }
177
+ const statusSummary = [...statusCounts.entries()]
178
+ .map(([st, n]) => `${st}: ${n}`)
179
+ .join(", ");
180
+
181
+ const ages: string[] = [];
182
+ if (ageIdx >= 0) {
183
+ const first = nsRows[0]!.trim().split(/\s+/)[ageIdx];
184
+ const last = nsRows[nsRows.length - 1]!.trim().split(/\s+/)[ageIdx];
185
+ if (first) ages.push(`oldest ${first}`);
186
+ if (last && last !== first) ages.push(`newest ${last}`);
187
+ }
188
+ const ageNote = ages.length ? ` age: ${ages.join("/")}` : "";
189
+ void nameIdx;
190
+ out.push(`namespace=${ns} ${nsRows.length} resources [${statusSummary}]${ageNote}`);
191
+ }
192
+ out.push(`· ${rows.length} resources total across ${byNs.size} namespace(s)`);
193
+ return out.join("\n");
194
+ }
195
+
196
+ function summarizeNpmAudit(stdout: string): string | null {
197
+ const lines = stdout.split("\n");
198
+ if (lines.length < 10) return null;
199
+
200
+ if (stdout.trimStart().startsWith("{")) {
201
+ try {
202
+ const j = JSON.parse(stdout) as {
203
+ metadata?: { vulnerabilities?: Record<string, number> };
204
+ vulnerabilities?: Record<string, { severity: string; via: Array<string | { url?: string; title?: string }> }>;
205
+ };
206
+ const vuln = j.metadata?.vulnerabilities ?? {};
207
+ const severitySummary = Object.entries(vuln)
208
+ .filter(([, n]) => n > 0)
209
+ .map(([sev, n]) => `${sev}: ${n}`)
210
+ .join(", ");
211
+ const advisories = Object.entries(j.vulnerabilities ?? {})
212
+ .slice(0, 10)
213
+ .map(([pkg, v]) => {
214
+ const via = v.via.find((x) => typeof x === "object") as { title?: string; url?: string } | undefined;
215
+ const title = via?.title ?? v.severity;
216
+ return ` ${pkg} (${v.severity}): ${title}`;
217
+ });
218
+ const lines2: string[] = [];
219
+ if (severitySummary) lines2.push(`npm audit: ${severitySummary}`);
220
+ lines2.push(...advisories);
221
+ if (advisories.length < Object.keys(j.vulnerabilities ?? {}).length) {
222
+ lines2.push(` … ${Object.keys(j.vulnerabilities ?? {}).length - advisories.length} more`);
223
+ }
224
+ return lines2.length > 1 ? lines2.join("\n") : null;
225
+ } catch {
226
+ // Fall through to text parsing.
227
+ }
228
+ }
229
+
230
+ const severityCounts: Record<string, number> = {};
231
+ const vulnLines: string[] = [];
232
+ let foundLine = "";
233
+
234
+ for (const line of lines) {
235
+ const sevMatch = line.match(/^\s*(critical|high|moderate|low|info)\s+(.+)/i);
236
+ if (sevMatch) {
237
+ const sev = sevMatch[1]!.toLowerCase();
238
+ severityCounts[sev] = (severityCounts[sev] ?? 0) + 1;
239
+ if (vulnLines.length < 10) vulnLines.push(` ${sev}: ${sevMatch[2]!.trim()}`);
240
+ }
241
+ if (/found \d+ vulnerabilit/i.test(line)) foundLine = line.trim();
242
+ }
243
+
244
+ if (Object.keys(severityCounts).length === 0) return null;
245
+
246
+ const summary = Object.entries(severityCounts)
247
+ .map(([sev, n]) => `${sev}: ${n}`)
248
+ .join(", ");
249
+ const out = [`npm audit: ${summary}`];
250
+ out.push(...vulnLines);
251
+ if (foundLine) out.push(foundLine);
252
+ return out.join("\n");
253
+ }
254
+
255
+ // ---------------------------------------------------------------------------
256
+ // v1.18 high-frequency additions
257
+ // ---------------------------------------------------------------------------
258
+
259
+ /**
260
+ * `git log` — keep last 10 commits (SHA + subject) plus a total-count footer
261
+ * when the log runs longer than 10 entries. Works against the common one-line
262
+ * formats (`--oneline`, default `commit <sha>` / `Author:` / blank / subject).
263
+ *
264
+ * Recognized shapes:
265
+ * "<sha> subject line" (--oneline / --format=%h %s)
266
+ * "commit <sha>\nAuthor: …\n\n subject" (default porcelain log)
267
+ */
268
+ function summarizeGitLog(stdout: string): string | null {
269
+ const text = stdout.replace(/\r\n/g, "\n");
270
+ if (text.length === 0) return null;
271
+
272
+ // Try parse default log shape: blocks separated by "commit <sha>" boundaries.
273
+ const commits: Array<{ sha: string; subject: string }> = [];
274
+ if (/^commit\s+[0-9a-f]{7,40}\b/m.test(text)) {
275
+ const blocks = text.split(/^commit\s+/m).slice(1);
276
+ for (const block of blocks) {
277
+ const shaMatch = block.match(/^([0-9a-f]{7,40})/);
278
+ if (!shaMatch) continue;
279
+ const sha = shaMatch[1]!.slice(0, 7);
280
+ // Subject is the first non-empty indented or trailing line.
281
+ const lines = block.split("\n").map((l) => l.trim());
282
+ // Skip headers (Author:, Date:, Merge:, etc.), pick first line after blank.
283
+ let subject = "";
284
+ let inBody = false;
285
+ for (const l of lines.slice(1)) {
286
+ if (!inBody) {
287
+ if (l === "") { inBody = true; continue; }
288
+ continue;
289
+ }
290
+ if (l.length > 0) { subject = l; break; }
291
+ }
292
+ commits.push({ sha, subject });
293
+ }
294
+ } else {
295
+ // Assume --oneline shape: "<sha> <subject>"
296
+ const lines = text.split("\n").filter((l) => l.length > 0);
297
+ for (const line of lines) {
298
+ const m = line.match(/^([0-9a-f]{7,40})\s+(.+)$/);
299
+ if (m) commits.push({ sha: m[1]!.slice(0, 7), subject: m[2]! });
300
+ }
301
+ }
302
+
303
+ if (commits.length <= 10) return null;
304
+ const keep = commits.slice(0, 10);
305
+ const rendered = keep.map((c) => `${c.sha} ${c.subject}`).join("\n");
306
+ return `${rendered}\n· ${commits.length} commits total (showing 10 most recent)`;
307
+ }
308
+
309
+ /**
310
+ * Test-runner output — unified summarizer matching jest / vitest / bun test /
311
+ * pytest / mocha / jasmine output shapes. Preserves: first ≤3 failing test
312
+ * descriptions (with file:line where available), final pass/fail line, total
313
+ * count. Drops verbose per-test "PASS" output.
314
+ *
315
+ * Detection is output-shape based, not command-arg based, so the same
316
+ * summarizer handles `jest`, `vitest`, `bun test`, `pytest`, `npm test`, etc.
317
+ */
318
+ function summarizeTestRunner(stdout: string): string | null {
319
+ if (stdout.length < 200) return null;
320
+ const lines = stdout.split("\n");
321
+
322
+ // Runner-specific final-line patterns.
323
+ const patterns: Array<{ re: RegExp; runner: string }> = [
324
+ { re: /^Tests?:\s+.*(?:passed|failed|skipped)/i, runner: "jest" },
325
+ { re: /^Test\s+Suites?:/i, runner: "jest" },
326
+ { re: /^Ran\s+\d+\s+tests?\s+in\s+[\d.]+s/, runner: "pytest" },
327
+ { re: /^=+\s+\d+\s+(?:passed|failed|error)/, runner: "pytest" },
328
+ { re: /^\s*\d+\s+pass(?:ed|ing)?\b/i, runner: "bun/mocha" },
329
+ { re: /^\s*\d+\s+fail(?:ed|ing)?\b/i, runner: "bun/mocha" },
330
+ { re: /^\s*✓\s+\d+\s+tests?\s+passed/i, runner: "vitest" },
331
+ ];
332
+ const isTestOutput = lines.some((l) => patterns.some((p) => p.re.test(l)));
333
+ if (!isTestOutput) return null;
334
+
335
+ // Collect failure indicators.
336
+ const failures: string[] = [];
337
+ const failRe = /^\s*(?:FAIL|✗|×|✘|✖|\[FAIL\])\s+(.+)$/;
338
+ const assertionRe = /^\s*(?:Error|AssertionError|Expected|FAIL):\s+(.+)$/i;
339
+ for (let i = 0; i < lines.length && failures.length < 3; i++) {
340
+ const line = lines[i]!;
341
+ let m = line.match(failRe);
342
+ if (!m) m = line.match(assertionRe);
343
+ if (m) {
344
+ const desc = m[1]!.trim();
345
+ // If a file:line marker is on an adjacent line, attach it.
346
+ const near = (lines[i + 1] ?? "") + " " + (lines[i - 1] ?? "");
347
+ const loc = near.match(/([^\s:]+\.[a-z]{1,5}):(\d+)/);
348
+ failures.push(loc ? `${desc} (${loc[1]}:${loc[2]})` : desc);
349
+ }
350
+ }
351
+
352
+ // Find the final summary line (last matching pattern line wins).
353
+ let finalLine = "";
354
+ for (let i = lines.length - 1; i >= 0; i--) {
355
+ const line = lines[i]!;
356
+ if (patterns.some((p) => p.re.test(line))) {
357
+ finalLine = line.trim();
358
+ break;
359
+ }
360
+ }
361
+
362
+ const out: string[] = [];
363
+ if (failures.length > 0) {
364
+ out.push(`failures (${failures.length}):`);
365
+ for (const f of failures) out.push(` ✗ ${f}`);
366
+ }
367
+ if (finalLine) out.push(finalLine);
368
+
369
+ // Only return a summary if we actually reduced something.
370
+ if (out.length === 0) return null;
371
+ const summary = out.join("\n");
372
+ if (summary.length >= stdout.length) return null;
373
+ return summary;
374
+ }
375
+
376
+ /**
377
+ * `tsc --noEmit` — return error count + first 3 errors with file:line.
378
+ * Matches the standard "path(line,col): error TSxxxx: message" shape.
379
+ */
380
+ function summarizeTsc(stdout: string): string | null {
381
+ const lines = stdout.split("\n");
382
+ const errorRe = /^(.+?)\((\d+),(\d+)\):\s+error\s+TS(\d+):\s+(.+)$/;
383
+ const errors: Array<{ file: string; line: string; code: string; msg: string }> = [];
384
+ for (const line of lines) {
385
+ const m = line.match(errorRe);
386
+ if (m) errors.push({ file: m[1]!, line: m[2]!, code: m[4]!, msg: m[5]! });
387
+ }
388
+ if (errors.length === 0) return null;
389
+ if (errors.length <= 3 && stdout.length < 2000) return null;
390
+
391
+ const keep = errors.slice(0, 3).map(
392
+ (e) => ` ${e.file}:${e.line}: TS${e.code}: ${e.msg}`,
393
+ );
394
+ const tail = errors.length > 3 ? ` … ${errors.length - 3} more errors` : "";
395
+ const out = [`tsc: ${errors.length} error${errors.length === 1 ? "" : "s"}`, ...keep];
396
+ if (tail) out.push(tail);
397
+ return out.join("\n");
398
+ }
399
+
400
+ /**
401
+ * `npm install` / `bun install` / `yarn install` / `pnpm install` — extract
402
+ * just the final "added N packages" / "Packages installed" line; drop all
403
+ * per-package progress chatter.
404
+ */
405
+ function summarizePackageInstall(stdout: string): string | null {
406
+ if (stdout.length < 200) return null;
407
+ const lines = stdout.split("\n");
408
+ const signalRe = new RegExp(
409
+ // npm: "added 42 packages, and audited 1234 packages in 3s"
410
+ // yarn: "Done in 4.12s." / "✨ Done in X"
411
+ // bun: "42 packages installed" / "Packages installed"
412
+ // pnpm: "Progress: resolved N, reused N, downloaded N"
413
+ "^(?:" +
414
+ "\\s*added\\s+\\d+\\s+packages?" +
415
+ "|\\s*changed\\s+\\d+\\s+packages?" +
416
+ "|\\s*removed\\s+\\d+\\s+packages?" +
417
+ "|\\s*up\\s+to\\s+date" +
418
+ "|\\s*\\d+\\s+packages?\\s+installed" +
419
+ "|\\s*Packages?\\s+installed" +
420
+ "|\\s*Done\\s+in\\s+[\\d.]+s" +
421
+ "|\\s*[✨🚀]\\s*Done\\s+in\\s+[\\d.]+s" +
422
+ "|\\s*Progress:\\s+resolved" +
423
+ ")",
424
+ "i",
425
+ );
426
+ const kept: string[] = [];
427
+ for (const line of lines) {
428
+ if (signalRe.test(line)) {
429
+ const trimmed = line.trim();
430
+ if (trimmed && !kept.includes(trimmed)) kept.push(trimmed);
431
+ }
432
+ }
433
+ if (kept.length === 0) return null;
434
+ if (kept.join("\n").length >= stdout.length) return null;
435
+ return kept.join("\n");
436
+ }
437
+
438
+ // ---------------------------------------------------------------------------
439
+ // v1.27 track-2 additions — 4 new lightweight pattern-match summarizers
440
+ // ---------------------------------------------------------------------------
441
+
442
+ /**
443
+ * `find` output — groups results by basename pattern, head 10 + tail 5 + total count.
444
+ * Replaces the existing stub (which only head-sliced) with richer grouping.
445
+ *
446
+ * NOTE: The registry entry for "find" already points to the old `summarizeFind`
447
+ * stub. These new summarizers are registered under more-specific two-word keys
448
+ * (e.g. "find-name") and the improved standalone entry replaces the old one.
449
+ */
450
+ function summarizeFindExpanded(stdout: string): string | null {
451
+ const lines = stdout.split("\n").filter((l) => l.length > 0);
452
+ if (lines.length <= 20) return null;
453
+
454
+ // Group by extension / basename pattern (last component after last dot, or no-ext).
455
+ const byExt = new Map<string, number>();
456
+ for (const line of lines) {
457
+ const base = line.split("/").pop() ?? line;
458
+ const dotIdx = base.lastIndexOf(".");
459
+ const ext = dotIdx > 0 ? base.slice(dotIdx) : "(no-ext)";
460
+ byExt.set(ext, (byExt.get(ext) ?? 0) + 1);
461
+ }
462
+
463
+ const topExts = [...byExt.entries()]
464
+ .sort((a, b) => b[1] - a[1])
465
+ .slice(0, 5)
466
+ .map(([ext, n]) => `${ext}: ${n}`)
467
+ .join(", ");
468
+
469
+ const head = lines.slice(0, 10).join("\n");
470
+ const tail = lines.slice(-5).join("\n");
471
+ const elided = lines.length - 15;
472
+ // Always emit the by-type breakdown — single-bucket (e.g. all (no-ext))
473
+ // is still useful signal and is what the v1.27.1 tests assert.
474
+ const extNote = byExt.size > 0 ? `\n· by type: ${topExts}` : "";
475
+
476
+ return (
477
+ `${head}\n[... ${elided} more matches elided ...]\n${tail}` +
478
+ `\n· ${lines.length} matches total${extNote}`
479
+ );
480
+ }
481
+
482
+ /**
483
+ * Raw `grep` / `rg` output — head 30 + tail 10 + total match count + unique-files count.
484
+ * Only activates when output exceeds 50 lines.
485
+ */
486
+ function summarizeGrepOutput(stdout: string): string | null {
487
+ const lines = stdout.split("\n").filter((l) => l.length > 0);
488
+ if (lines.length <= 50) return null;
489
+
490
+ // Count unique files (lines that contain ":" separator from grep -n / rg default).
491
+ const fileSet = new Set<string>();
492
+ for (const line of lines) {
493
+ const colonIdx = line.indexOf(":");
494
+ if (colonIdx > 0) {
495
+ const maybeFile = line.slice(0, colonIdx);
496
+ // Avoid treating line-number prefixes as filenames.
497
+ if (maybeFile.length > 0 && !/^\d+$/.test(maybeFile)) {
498
+ fileSet.add(maybeFile);
499
+ }
500
+ }
501
+ }
502
+
503
+ const head = lines.slice(0, 30).join("\n");
504
+ const tail = lines.slice(-10).join("\n");
505
+ const elided = lines.length - 40;
506
+ const fileNote = fileSet.size > 0 ? `, ${fileSet.size} file${fileSet.size === 1 ? "" : "s"}` : "";
507
+
508
+ return (
509
+ `${head}\n[... ${elided} more matches elided ...]\n${tail}` +
510
+ `\n· ${lines.length} matches total${fileNote}`
511
+ );
512
+ }
513
+
514
+ /**
515
+ * `bun install` / `npm install` — extract failures, count packages, head + tail.
516
+ * Supplements the existing `summarizePackageInstall` with richer error extraction.
517
+ */
518
+ function summarizeInstallExpanded(stdout: string): string | null {
519
+ if (stdout.length < 300) return null;
520
+ const lines = stdout.split("\n");
521
+
522
+ // Note: word-boundary after `ERR!` doesn't match because `!` is non-word
523
+ // and what follows is a space — so we accept ERR! without trailing \b.
524
+ const errorRe = /(\berror\b|ERR!|\bpanic\b|\bENOENT\b|\bEACCES\b|\bfailed\b)/i;
525
+ const errors: string[] = [];
526
+ for (const line of lines) {
527
+ if (errorRe.test(line) && errors.length < 5) {
528
+ errors.push(line.trim());
529
+ }
530
+ }
531
+
532
+ // Extract install count lines.
533
+ const countRe = /(\d+)\s+packages?(?:\s+(?:installed|added|removed|changed))?/i;
534
+ const counts: string[] = [];
535
+ for (const line of lines) {
536
+ if (countRe.test(line)) {
537
+ const trimmed = line.trim();
538
+ if (trimmed && !counts.includes(trimmed)) counts.push(trimmed);
539
+ }
540
+ }
541
+
542
+ if (errors.length === 0 && counts.length === 0) return null;
543
+ if (lines.length < 20 && errors.length === 0) return null;
544
+
545
+ const out: string[] = [];
546
+ if (errors.length > 0) {
547
+ out.push(`errors (${errors.length}):`);
548
+ for (const e of errors) out.push(` ! ${e}`);
549
+ }
550
+ if (counts.length > 0) {
551
+ out.push(...counts);
552
+ }
553
+
554
+ // Include head + tail context when output is long.
555
+ if (lines.length > 30) {
556
+ const head = lines.slice(0, 5).map((l) => l.trim()).filter(Boolean).join("\n");
557
+ const tail = lines.slice(-5).map((l) => l.trim()).filter(Boolean).join("\n");
558
+ if (head) out.unshift(head);
559
+ if (tail && tail !== head) out.push(tail);
560
+ }
561
+
562
+ out.push(`· ${lines.length} lines total`);
563
+ const summary = out.join("\n");
564
+ if (summary.length >= stdout.length) return null;
565
+ return summary;
566
+ }
567
+
568
+ /**
569
+ * CI matrix output — pivots by status (PASS/FAIL/SKIP/ok/not ok), samples failures.
570
+ * Recognises TAP, bun test, jest, pytest, and generic PASS/FAIL header lines.
571
+ */
572
+ function summarizeCiMatrix(stdout: string): string | null {
573
+ // 100-byte floor lets short CI/TAP runs (e.g. 8-10 status lines of "PASS suite-N")
574
+ // hit the summarizer; the >=3 status-line guard below remains the real gate.
575
+ if (stdout.length < 100) return null;
576
+ const lines = stdout.split("\n");
577
+
578
+ // Detect CI matrix shape: lines starting with PASS/FAIL/SKIP/ok/not ok.
579
+ const statusRe = /^(PASS|FAIL|SKIP|ok|not ok)\b/;
580
+ const statusLines = lines.filter((l) => statusRe.test(l.trim()));
581
+ if (statusLines.length < 3) return null;
582
+
583
+ // Pivot by status.
584
+ const counts = new Map<string, number>();
585
+ const failures: string[] = [];
586
+
587
+ for (const line of statusLines) {
588
+ const m = line.trim().match(/^(PASS|FAIL|SKIP|ok|not ok)\b\s*(.*)/);
589
+ if (!m) continue;
590
+ const status = m[1]!.toUpperCase().replace("NOT OK", "FAIL");
591
+ counts.set(status, (counts.get(status) ?? 0) + 1);
592
+ if ((status === "FAIL" || m[1] === "not ok") && failures.length < 5) {
593
+ const desc = m[2]!.trim().replace(/^-\s*/, "");
594
+ if (desc) failures.push(desc);
595
+ }
596
+ }
597
+
598
+ if (counts.size === 0) return null;
599
+
600
+ const pivot = [...counts.entries()]
601
+ .sort((a, b) => b[1] - a[1])
602
+ .map(([s, n]) => `${s}: ${n}`)
603
+ .join(", ");
604
+
605
+ const out: string[] = [`CI: ${pivot}`];
606
+ if (failures.length > 0) {
607
+ out.push(`failures:`);
608
+ for (const f of failures) out.push(` ✗ ${f}`);
609
+ }
610
+ out.push(`· ${statusLines.length} test entries, ${lines.length} lines total`);
611
+
612
+ const summary = out.join("\n");
613
+ if (summary.length >= stdout.length) return null;
614
+ return summary;
615
+ }
616
+
617
+ // ---------------------------------------------------------------------------
618
+ // v1.28 additions — language-toolchain summarizers
619
+ // ---------------------------------------------------------------------------
620
+
621
+ /**
622
+ * Cargo test runner — Rust's test output. Pivot pass/fail counts, surface
623
+ * up to 5 failure paths.
624
+ *
625
+ * Looks for the standard `test result: ok. N passed; M failed; ...` line.
626
+ * Returns null if the output doesn't match cargo test shape.
627
+ */
628
+ function summarizeCargoTest(stdout: string): string | null {
629
+ if (stdout.length < 100) return null;
630
+ const resultRe = /test result:\s+(\w+)\.\s+(\d+)\s+passed;\s+(\d+)\s+failed(?:;\s+(\d+)\s+ignored)?/;
631
+ const lines = stdout.split("\n");
632
+ const failures: string[] = [];
633
+ let passed = 0, failed = 0, ignored = 0, status = "";
634
+ for (const line of lines) {
635
+ const m = line.match(resultRe);
636
+ if (m) {
637
+ status = m[1]!;
638
+ passed += Number(m[2]);
639
+ failed += Number(m[3]);
640
+ ignored += Number(m[4] ?? 0);
641
+ }
642
+ const failMatch = line.match(/^test\s+(\S+)\s+\.\.\.\s+FAILED/);
643
+ if (failMatch && failures.length < 5) failures.push(failMatch[1]!);
644
+ }
645
+ if (status === "") return null;
646
+ const out: string[] = [`cargo test: ${status} · ${passed} passed, ${failed} failed${ignored ? `, ${ignored} ignored` : ""}`];
647
+ if (failures.length > 0) {
648
+ out.push("failures:");
649
+ for (const f of failures) out.push(` ✗ ${f}`);
650
+ }
651
+ out.push(`· ${lines.length} lines total`);
652
+ const summary = out.join("\n");
653
+ return summary.length < stdout.length ? summary : null;
654
+ }
655
+
656
+ /**
657
+ * Cargo check / cargo build — surfaces error/warning counts and up to 5
658
+ * unique error messages with file:line citations.
659
+ */
660
+ function summarizeCargoCheck(stdout: string): string | null {
661
+ if (stdout.length < 100) return null;
662
+ const lines = stdout.split("\n");
663
+ const errors: string[] = [];
664
+ const warnings: string[] = [];
665
+ for (const line of lines) {
666
+ const errMatch = line.match(/^error(?:\[[A-Z0-9]+\])?:\s+(.+)/);
667
+ const warnMatch = line.match(/^warning:\s+(.+)/);
668
+ if (errMatch && errors.length < 5) errors.push(errMatch[1]!);
669
+ else if (warnMatch && warnings.length < 5) warnings.push(warnMatch[1]!);
670
+ }
671
+ // Find the "Compiling" / "Finished" indicator
672
+ const finished = lines.find((l) => /^\s*(Finished|Compiling)\b/.test(l)) ?? "";
673
+ if (errors.length === 0 && warnings.length === 0 && !finished) return null;
674
+
675
+ const out: string[] = [];
676
+ if (finished) out.push(finished.trim());
677
+ out.push(`cargo: ${errors.length} errors, ${warnings.length} warnings`);
678
+ if (errors.length > 0) {
679
+ out.push("errors:");
680
+ for (const e of errors) out.push(` ✗ ${e}`);
681
+ }
682
+ if (warnings.length > 0) {
683
+ out.push("warnings:");
684
+ for (const w of warnings) out.push(` ⚠ ${w}`);
685
+ }
686
+ out.push(`· ${lines.length} lines total`);
687
+ const summary = out.join("\n");
688
+ return summary.length < stdout.length ? summary : null;
689
+ }
690
+
691
+ /**
692
+ * `go test` summarizer — pivots PASS/FAIL/SKIP per package, surfaces up to 5
693
+ * failing test names.
694
+ */
695
+ function summarizeGoTest(stdout: string): string | null {
696
+ if (stdout.length < 100) return null;
697
+ const lines = stdout.split("\n");
698
+ const counts = new Map<string, number>();
699
+ const failures: string[] = [];
700
+ for (const line of lines) {
701
+ const m = line.match(/^(ok|FAIL|---\s+(?:PASS|FAIL|SKIP):\s+(\S+))/);
702
+ if (!m) continue;
703
+ if (line.startsWith("---")) {
704
+ const status = line.match(/---\s+(\w+):/)?.[1] ?? "";
705
+ counts.set(status, (counts.get(status) ?? 0) + 1);
706
+ const failName = line.match(/---\s+FAIL:\s+(\S+)/)?.[1];
707
+ if (failName && failures.length < 5) failures.push(failName);
708
+ } else if (line.startsWith("ok")) counts.set("PASS", (counts.get("PASS") ?? 0) + 1);
709
+ else if (line.startsWith("FAIL")) counts.set("FAIL", (counts.get("FAIL") ?? 0) + 1);
710
+ }
711
+ if (counts.size === 0) return null;
712
+ const pivot = [...counts.entries()]
713
+ .sort((a, b) => b[1] - a[1])
714
+ .map(([s, n]) => `${s}: ${n}`)
715
+ .join(", ");
716
+ const out: string[] = [`go test: ${pivot}`];
717
+ if (failures.length > 0) {
718
+ out.push("failures:");
719
+ for (const f of failures) out.push(` ✗ ${f}`);
720
+ }
721
+ out.push(`· ${lines.length} lines total`);
722
+ const summary = out.join("\n");
723
+ return summary.length < stdout.length ? summary : null;
724
+ }
725
+
726
+ /**
727
+ * `terraform plan` summarizer — extracts the resource-action header
728
+ * (Plan: N to add, M to change, K to destroy) and lists up to 10 resource
729
+ * actions with their addresses.
730
+ */
731
+ function summarizeTerraformPlan(stdout: string): string | null {
732
+ if (stdout.length < 200) return null;
733
+ const lines = stdout.split("\n");
734
+ const planLine = lines.find((l) => /^Plan:\s+\d+\s+to add/.test(l.trim()));
735
+ if (!planLine) return null;
736
+
737
+ const actions: string[] = [];
738
+ for (const line of lines) {
739
+ // Match " # module.foo.aws_s3_bucket.bar will be created"
740
+ const m = line.match(/^\s*#\s+(\S+)\s+will be (created|updated|destroyed|read|replaced)/);
741
+ if (m && actions.length < 10) actions.push(` ${m[2]}: ${m[1]}`);
742
+ }
743
+
744
+ const out: string[] = [planLine.trim()];
745
+ if (actions.length > 0) {
746
+ out.push("actions (up to 10):");
747
+ out.push(...actions);
748
+ }
749
+ out.push(`· ${lines.length} lines total`);
750
+ const summary = out.join("\n");
751
+ return summary.length < stdout.length ? summary : null;
752
+ }
753
+
754
+ /**
755
+ * `kubectl logs` summarizer — long log streams. Returns head 50 + tail 30 +
756
+ * a "first N error lines" extraction. Only triggers above 80 lines.
757
+ */
758
+ function summarizeKubectlLogs(stdout: string): string | null {
759
+ const lines = stdout.split("\n");
760
+ if (lines.length <= 80) return null;
761
+ const head = lines.slice(0, 50).join("\n");
762
+ const tail = lines.slice(-30).join("\n");
763
+ const errorLines = lines.filter((l) => /\b(ERROR|WARN|FATAL|Exception|panic:)\b/.test(l)).slice(0, 5);
764
+ const errorBlock = errorLines.length > 0 ? `\nfirst ${errorLines.length} error/warn lines:\n${errorLines.map((l) => ` ${l.trim()}`).join("\n")}\n` : "";
765
+ return `${head}\n[... ${lines.length - 80} lines elided ...]${errorBlock}\n${tail}\n· ${lines.length} log lines total`;
766
+ }
767
+
768
+ /**
769
+ * `kubectl describe` summarizer — verbose resource description. Returns
770
+ * the top section (up to 20 lines) + the Events: section if present.
771
+ */
772
+ function summarizeKubectlDescribe(stdout: string): string | null {
773
+ const lines = stdout.split("\n");
774
+ if (lines.length <= 30) return null;
775
+ const eventsIdx = lines.findIndex((l) => /^Events:\s*$/.test(l));
776
+ const head = lines.slice(0, 20).join("\n");
777
+ let eventsBlock = "";
778
+ if (eventsIdx >= 0) {
779
+ const eventLines = lines.slice(eventsIdx).filter((l) => l.trim().length > 0);
780
+ eventsBlock = `\n${eventLines.slice(0, 15).join("\n")}\n`;
781
+ }
782
+ return `${head}\n[... ${lines.length - 20} description lines elided ...]${eventsBlock}\n· ${lines.length} lines total`;
783
+ }
784
+
785
+ /**
786
+ * `docker logs` — head 5 + tail 30 + extracted error/warn lines.
787
+ * Container logs are notoriously large; the recent tail + error lines
788
+ * answer "is it healthy and what's the latest?" in <1KB.
789
+ */
790
+ function summarizeDockerLogs(stdout: string): string | null {
791
+ const lines = stdout.split("\n");
792
+ if (lines.length <= 40) return null;
793
+
794
+ const head = lines.slice(0, 5);
795
+ const tail = lines.slice(-30);
796
+
797
+ const errorRe = /\b(ERROR|FATAL|PANIC|CRITICAL|EXCEPTION|TRACEBACK)\b/i;
798
+ const errorLines: string[] = [];
799
+ for (let i = 5; i < lines.length - 30 && errorLines.length < 10; i++) {
800
+ if (errorRe.test(lines[i]!)) errorLines.push(lines[i]!);
801
+ }
802
+
803
+ const parts = [head.join("\n")];
804
+ if (errorLines.length > 0) {
805
+ parts.push(`\n[... ${errorLines.length} error/fatal lines extracted ...]`);
806
+ parts.push(errorLines.join("\n"));
807
+ }
808
+ parts.push(`\n[... ${lines.length - 35} lines elided ...]`);
809
+ parts.push(tail.join("\n"));
810
+ parts.push(`· ${lines.length} log lines total`);
811
+ return parts.join("\n");
812
+ }
813
+
814
+ /**
815
+ * `docker stats --no-stream` — top 5 by CPU + top 5 by MEM, deduped.
816
+ * Output shape (header + rows of: CONTAINER CPU% MEM USAGE MEM% NET I/O …).
817
+ */
818
+ function summarizeDockerStats(stdout: string): string | null {
819
+ const lines = stdout.split("\n").filter((l) => l.trim().length > 0);
820
+ if (lines.length < 6) return null; // header + few rows isn't worth compressing
821
+ const header = lines[0]!;
822
+ const rows = lines.slice(1);
823
+
824
+ // Parse CPU% and MEM% (columns 1 and 3 typically) — tab/multi-space separated.
825
+ type Row = { line: string; cpu: number; mem: number };
826
+ const parsed: Row[] = rows.map((line) => {
827
+ const fields = line.split(/\s{2,}|\t/).map((s) => s.trim());
828
+ const cpu = parseFloat((fields[1] ?? "").replace("%", ""));
829
+ const mem = parseFloat((fields[3] ?? "").replace("%", ""));
830
+ return { line, cpu: isNaN(cpu) ? -1 : cpu, mem: isNaN(mem) ? -1 : mem };
831
+ });
832
+
833
+ const topCpu = [...parsed].sort((a, b) => b.cpu - a.cpu).slice(0, 5);
834
+ const topMem = [...parsed].sort((a, b) => b.mem - a.mem).slice(0, 5);
835
+ const seen = new Set<string>();
836
+ const merged: Row[] = [];
837
+ for (const r of [...topCpu, ...topMem]) {
838
+ if (!seen.has(r.line)) { seen.add(r.line); merged.push(r); }
839
+ }
840
+
841
+ if (merged.length >= rows.length) return null; // not worth compressing
842
+ return [
843
+ header,
844
+ ...merged.map((r) => r.line),
845
+ `· ${rows.length} containers total (top by CPU/MEM shown)`,
846
+ ].join("\n");
847
+ }
848
+
849
+ /**
850
+ * `gh pr list` (or `gh issue list`) — count by state + show first 10.
851
+ * GitHub CLI defaults to listing up to 30 PRs in a table; large repos can
852
+ * return huge tables. Pivot by state then head-10.
853
+ */
854
+ function summarizeGhPrList(stdout: string): string | null {
855
+ const lines = stdout.split("\n").filter((l) => l.trim().length > 0);
856
+ if (lines.length <= 12) return null;
857
+
858
+ // gh pr list output is tab-separated: NUMBER\tTITLE\tBRANCH\tSTATE...
859
+ // Detect the state column heuristically.
860
+ type Row = { line: string; state: string };
861
+ const parsed: Row[] = lines.map((line) => {
862
+ const fields = line.split("\t");
863
+ // The state field is one of: OPEN, CLOSED, MERGED, DRAFT (case-insensitive).
864
+ const stateField = fields.find((f) => /^(OPEN|CLOSED|MERGED|DRAFT)$/i.test(f.trim()));
865
+ return { line, state: (stateField ?? "OPEN").toUpperCase() };
866
+ });
867
+
868
+ const byState = new Map<string, number>();
869
+ for (const r of parsed) byState.set(r.state, (byState.get(r.state) ?? 0) + 1);
870
+
871
+ const counts = [...byState.entries()].sort((a, b) => b[1] - a[1]);
872
+ const summary = counts.map(([s, n]) => `${s}: ${n}`).join(" ");
873
+
874
+ return [
875
+ `gh list — ${lines.length} rows · ${summary}`,
876
+ ...lines.slice(0, 10),
877
+ `[... ${lines.length - 10} rows elided ...]`,
878
+ ].join("\n");
879
+ }
880
+
881
+ /**
882
+ * `gh pr view` — large PR descriptions + nested comments balloon fast.
883
+ * Strategy: keep the header block (everything before the first comment) +
884
+ * a count of comments + the latest 3 comments.
885
+ */
886
+ function summarizeGhPrView(stdout: string): string | null {
887
+ const lines = stdout.split("\n");
888
+ if (lines.length <= 60) return null;
889
+
890
+ // The header block ends where "-- " comment dividers begin in `gh pr view`,
891
+ // OR after the body section. Use a heuristic: first occurrence of a line
892
+ // that is just "--" or starts with "[Comment #" / "Comment by".
893
+ const commentMarker = /^(--+|\[Comment\b|Comment by\b|@\w+ commented\b)/;
894
+ let headerEnd = lines.findIndex((l) => commentMarker.test(l.trim()));
895
+ if (headerEnd === -1) headerEnd = Math.min(40, lines.length);
896
+
897
+ const header = lines.slice(0, headerEnd);
898
+ const commentSection = lines.slice(headerEnd);
899
+
900
+ // Count distinct comment markers in the comment section.
901
+ const commentCount = commentSection.filter((l) =>
902
+ /^(--+|\[Comment\b|Comment by\b|@\w+ commented\b)/.test(l.trim()),
903
+ ).length;
904
+
905
+ // Keep the last 3 comment-equivalent slices (after the last 2 markers each
906
+ // up to 10 lines).
907
+ const tailLines = commentSection.slice(-30);
908
+
909
+ return [
910
+ ...header,
911
+ `[... ${commentCount} comment${commentCount === 1 ? "" : "s"}, showing the latest ...]`,
912
+ ...tailLines,
913
+ `· ${lines.length} lines total`,
914
+ ].join("\n");
915
+ }
916
+
917
+ // ---------------------------------------------------------------------------
918
+ // Helper: flag commands whose output should flow through the LLM diff
919
+ // summarizer instead of a synchronous registry entry. bash-server can use
920
+ // this to short-circuit registry matching before invoking summarizeIfLarge.
921
+ // ---------------------------------------------------------------------------
922
+
923
+ /**
924
+ * True if `command` is a git diff-like command whose stdout should be piped
925
+ * through `summarizeIfLarge` with `PROMPTS.diff` when it exceeds 4 KB.
926
+ * (Pure check — no I/O; safe to call anywhere.)
927
+ */
928
+ export function isLargeDiffCommand(command: string): boolean {
929
+ const t = command.trim();
930
+ return /^git\s+(diff|show)\b/.test(t);
931
+ }
932
+
933
+ /** Byte threshold at which git diff / git show output should be LLM-summarized. */
934
+ export const DIFF_LLM_THRESHOLD_BYTES = 4 * 1024;
935
+
936
+ // ---------------------------------------------------------------------------
937
+ // Registry
938
+ // ---------------------------------------------------------------------------
939
+
940
+ /**
941
+ * Map of command key → summarizer.
942
+ * Keys use "-" to join multi-word prefixes (e.g. "git-status", "npm-ls").
943
+ *
944
+ * Note: summarizePs and summarizeGitStatus need extra context (cwd / branch).
945
+ * Their registry entries receive only stdout; callers that need the full
946
+ * behaviour (branch lookup, cwd filtering) use tryStructuredSummary directly.
947
+ * These entries are included so findSummarizer can report a match exists.
948
+ */
949
+ export const BASH_SUMMARIZERS: Map<string, (stdout: string) => string | null> = new Map([
950
+ ["git-status", (stdout) => summarizeGitStatus(stdout)],
951
+ ["ls", summarizeLs],
952
+ ["find", summarizeFind],
953
+ ["ps", (stdout) => summarizePs(stdout, process.cwd())],
954
+ ["npm-ls", summarizeNpmLs],
955
+ ["bun-pm-ls", summarizeNpmLs],
956
+ ["docker-ps", summarizeDockerPs],
957
+ ["docker-compose-ps", summarizeDockerPs],
958
+ ["docker-container-ls", summarizeDockerPs],
959
+ ["kubectl-get", summarizeKubectlGet],
960
+ ["npm-audit", summarizeNpmAudit],
961
+
962
+ // v1.18 high-frequency additions (token-compression-wins):
963
+ ["git-log", summarizeGitLog],
964
+ // Test runners — detect by output shape; the key matches the common
965
+ // first-word invocation so findSummarizer routes correctly.
966
+ ["jest", summarizeTestRunner],
967
+ ["vitest", summarizeTestRunner],
968
+ ["pytest", summarizeTestRunner],
969
+ ["mocha", summarizeTestRunner],
970
+ ["bun-test", summarizeTestRunner],
971
+ ["npm-test", summarizeTestRunner],
972
+ ["yarn-test", summarizeTestRunner],
973
+ ["pnpm-test", summarizeTestRunner],
974
+ // `tsc --noEmit` — match on bare `tsc` and `npx tsc`.
975
+ ["tsc", summarizeTsc],
976
+ ["npx-tsc", summarizeTsc],
977
+ ["bunx-tsc", summarizeTsc],
978
+ // Package installs — drop per-package chatter, keep the final line.
979
+ ["npm-install", summarizePackageInstall],
980
+ ["npm-i", summarizePackageInstall],
981
+ ["npm-ci", summarizePackageInstall],
982
+ ["bun-install", summarizePackageInstall],
983
+ ["bun-i", summarizePackageInstall],
984
+ ["yarn-install", summarizePackageInstall],
985
+ ["pnpm-install", summarizePackageInstall],
986
+ ["pnpm-i", summarizePackageInstall],
987
+
988
+ // v1.27 track-2 additions:
989
+ // find — richer grouping by extension + head/tail (overrides old stub).
990
+ ["find", summarizeFindExpanded],
991
+ // grep / rg — head 30 + tail 10 + unique-file count.
992
+ ["grep", summarizeGrepOutput],
993
+ ["rg", summarizeGrepOutput],
994
+ // install with error extraction (higher-specificity keys shadow the bare install key).
995
+ ["npm-install-expanded", summarizeInstallExpanded],
996
+ ["bun-add", summarizeInstallExpanded],
997
+ ["yarn-add", summarizeInstallExpanded],
998
+ ["pnpm-add", summarizeInstallExpanded],
999
+ // CI matrix — TAP / jest / bun test pivot by status.
1000
+ ["tap", summarizeCiMatrix],
1001
+ ["prove", summarizeCiMatrix],
1002
+
1003
+ // v1.28 additions — language toolchain summarizers.
1004
+ ["cargo-test", summarizeCargoTest],
1005
+ ["cargo-check", summarizeCargoCheck],
1006
+ ["cargo-build", summarizeCargoCheck],
1007
+ ["cargo-clippy", summarizeCargoCheck],
1008
+ ["go-test", summarizeGoTest],
1009
+ ["terraform-plan", summarizeTerraformPlan],
1010
+ ["tofu-plan", summarizeTerraformPlan], // OpenTofu fork uses identical plan format
1011
+ ["kubectl-logs", summarizeKubectlLogs],
1012
+ ["k-logs", summarizeKubectlLogs],
1013
+ ["kubectl-describe", summarizeKubectlDescribe],
1014
+ ["k-describe", summarizeKubectlDescribe],
1015
+
1016
+ // v1.30 additions — docker (logs/stats) and gh (pr list/view).
1017
+ ["docker-logs", summarizeDockerLogs],
1018
+ ["docker-stats", summarizeDockerStats],
1019
+ ["gh-pr-list", summarizeGhPrList],
1020
+ ["gh-issue-list", summarizeGhPrList],
1021
+ ["gh-pr-view", summarizeGhPrView],
1022
+ ["gh-issue-view", summarizeGhPrView],
1023
+ ]);
1024
+
1025
+ // ---------------------------------------------------------------------------
1026
+ // Helper
1027
+ // ---------------------------------------------------------------------------
1028
+
1029
+ /**
1030
+ * Parse a raw command string and return the matching summarizer, or null.
1031
+ *
1032
+ * Matching order (most-specific first):
1033
+ * 1. Three-word prefix: word0-word1-word2 (e.g. "docker container ls")
1034
+ * 2. Two-word prefix: word0-word1 (e.g. "git status", "npm audit")
1035
+ * 3. One-word prefix: word0 (e.g. "ls", "find")
1036
+ */
1037
+ export function findSummarizer(
1038
+ command: string,
1039
+ ): ((stdout: string) => string | null) | null {
1040
+ const words = command.trim().split(/\s+/);
1041
+ if (words.length === 0) return null;
1042
+
1043
+ const w0 = words[0] ?? "";
1044
+ const w1 = words[1] ?? "";
1045
+ const w2 = words[2] ?? "";
1046
+
1047
+ // Three-word key.
1048
+ if (w2) {
1049
+ const key3 = `${w0}-${w1}-${w2}`;
1050
+ const fn3 = BASH_SUMMARIZERS.get(key3);
1051
+ if (fn3) return fn3;
1052
+ }
1053
+
1054
+ // Two-word key.
1055
+ if (w1) {
1056
+ const key2 = `${w0}-${w1}`;
1057
+ const fn2 = BASH_SUMMARIZERS.get(key2);
1058
+ if (fn2) return fn2;
1059
+ }
1060
+
1061
+ // One-word key.
1062
+ return BASH_SUMMARIZERS.get(w0) ?? null;
1063
+ }
1064
+
1065
+ // Re-export internals for testing.
1066
+ export {
1067
+ summarizeGitStatus,
1068
+ summarizeLs,
1069
+ summarizeFind,
1070
+ summarizePs,
1071
+ summarizeNpmLs,
1072
+ summarizeDockerPs,
1073
+ summarizeKubectlGet,
1074
+ summarizeNpmAudit,
1075
+ summarizeGitLog,
1076
+ summarizeTestRunner,
1077
+ summarizeTsc,
1078
+ summarizePackageInstall,
1079
+ // v1.27 track-2 additions
1080
+ summarizeFindExpanded,
1081
+ summarizeGrepOutput,
1082
+ summarizeInstallExpanded,
1083
+ summarizeCiMatrix,
1084
+ // v1.28 additions — language toolchain summarizers
1085
+ summarizeCargoTest,
1086
+ summarizeCargoCheck,
1087
+ summarizeGoTest,
1088
+ summarizeTerraformPlan,
1089
+ summarizeKubectlLogs,
1090
+ summarizeKubectlDescribe,
1091
+ // v1.30 additions — docker (logs/stats) + gh (pr list/view)
1092
+ summarizeDockerLogs,
1093
+ summarizeDockerStats,
1094
+ summarizeGhPrList,
1095
+ summarizeGhPrView,
1096
+ };