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,993 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * ashlr SessionStart hook (TypeScript).
4
+ *
5
+ * Replaces the legacy bash session-start.sh. Two responsibilities:
6
+ * 1. Run the baseline scanner (cache-hit budget ~2s) and inject the result
7
+ * as additionalContext so the agent sees a cheap project orientation.
8
+ * 2. Print the once-per-day activation notice (preserved from the bash
9
+ * script) on stderr so it lands in Claude Code's transcript.
10
+ *
11
+ * Hook contract (SessionStart):
12
+ * stdout → { hookSpecificOutput: { hookEventName: "SessionStart",
13
+ * additionalContext?: string } }
14
+ *
15
+ * Per Claude Code's hook docs, the `additionalContext` field for SessionStart
16
+ * is appended to the system prompt for the new session, so the baseline lands
17
+ * in the agent's visible context window automatically.
18
+ *
19
+ * Design rules:
20
+ * - Never throw — pass-through on any error.
21
+ * - 2-second budget: if scan blows the budget we still emit *something*
22
+ * (an empty additionalContext) rather than hang the session.
23
+ */
24
+
25
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from "fs";
26
+ import { homedir } from "os";
27
+ import { basename, dirname, join } from "path";
28
+ import { spawn } from "child_process";
29
+
30
+ import { formatBaseline, scan } from "../scripts/baseline-scan";
31
+ import { greet as sessionGreet } from "../scripts/session-greet";
32
+ import { initSessionBucket } from "../servers/_stats";
33
+ import { cwdLooksLikePluginRoot } from "../servers/_cwd-clamp";
34
+ import {
35
+ isFirstRun,
36
+ writeStamp,
37
+ stampPath,
38
+ readOnboardingState,
39
+ onboardingStatePath,
40
+ restartRequiredPath,
41
+ } from "../scripts/onboarding-wizard";
42
+ import { checkForUpdate } from "../scripts/auto-update";
43
+ import { runCloudPull } from "../scripts/genome-cloud-pull";
44
+ import { syncCloudDeltas, type Tier } from "../scripts/genome-cloud-sync";
45
+ import { isTelemetryEnabled, maybeTelemetryConsentNotice } from "../servers/_telemetry";
46
+ import { maybeCloudPull } from "../scripts/stats-cloud-pull.ts";
47
+
48
+ /**
49
+ * Template for the once-per-day activation notice. Placeholders are filled
50
+ * in at runtime by `buildActivationNotice()` so the version + skill count
51
+ * can never drift from the actual installed plugin (v1.17 had this wrong
52
+ * because the values were hardcoded).
53
+ */
54
+ export const ACTIVATION_NOTICE_TEMPLATE =
55
+ "ashlr-plugin v{version} active — Windows/macOS/Linux · {toolCount} skills · /ashlr-start for the onboarding wizard · /ashlr-upgrade to go Pro from the terminal.";
56
+
57
+ /**
58
+ * Default plugin root — resolves to this hook's parent directory when
59
+ * CLAUDE_PLUGIN_ROOT isn't set (e.g. during tests that import buildResponse
60
+ * directly).
61
+ */
62
+ function defaultPluginRoot(): string {
63
+ return process.env.CLAUDE_PLUGIN_ROOT ?? join(import.meta.dir, "..");
64
+ }
65
+
66
+ /**
67
+ * Read the plugin's current version from .claude-plugin/plugin.json. Returns
68
+ * a fallback of "unknown" if anything goes wrong — the banner is decoration,
69
+ * it must never throw.
70
+ */
71
+ export function readPluginVersion(pluginRoot: string = defaultPluginRoot()): string {
72
+ try {
73
+ const pj = JSON.parse(
74
+ readFileSync(join(pluginRoot, ".claude-plugin", "plugin.json"), "utf-8"),
75
+ ) as { version?: unknown };
76
+ if (typeof pj.version === "string" && pj.version.trim()) return pj.version.trim();
77
+ } catch {
78
+ /* ignore */
79
+ }
80
+ return "unknown";
81
+ }
82
+
83
+ /**
84
+ * Count the number of user-facing skills shipped by the plugin. Each slash
85
+ * command (e.g. /ashlr-start) is one `.md` file under commands/, so we count
86
+ * those rather than the MCP tool list (which targets the model, not the user).
87
+ */
88
+ export function readToolCount(pluginRoot: string = defaultPluginRoot()): number {
89
+ try {
90
+ const dir = join(pluginRoot, "commands");
91
+ if (!existsSync(dir)) return 0;
92
+ const st = statSync(dir);
93
+ if (!st.isDirectory()) return 0;
94
+ return readdirSync(dir).filter((n) => n.endsWith(".md")).length;
95
+ } catch {
96
+ return 0;
97
+ }
98
+ }
99
+
100
+ /** Render the activation notice with runtime-resolved version + tool count. */
101
+ export function buildActivationNotice(
102
+ pluginRoot: string = defaultPluginRoot(),
103
+ ): string {
104
+ const version = readPluginVersion(pluginRoot);
105
+ const toolCount = readToolCount(pluginRoot);
106
+ return ACTIVATION_NOTICE_TEMPLATE.replace("{version}", version).replace(
107
+ "{toolCount}",
108
+ String(toolCount),
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Back-compat shim: downstream tests/scripts import ACTIVATION_NOTICE as a
114
+ * bare string. We resolve it lazily at module load via the helpers above so
115
+ * the value is always in sync with plugin.json + the commands/ dir.
116
+ */
117
+ export const ACTIVATION_NOTICE = buildActivationNotice();
118
+ export const SCAN_BUDGET_MS = 2000;
119
+
120
+ /**
121
+ * Ensure the plugin's dependencies are installed.
122
+ * Claude Code clones the plugin but does not run `bun install`, so on first
123
+ * session we detect the missing node_modules and bootstrap them silently.
124
+ * Idempotent: no-op when deps are already present.
125
+ *
126
+ * Runs in the background so the SessionStart hook never blocks the agent.
127
+ */
128
+ const SEMVER_DIR_RE = /^[0-9]+\.[0-9]+\.[0-9]+$/;
129
+
130
+ /**
131
+ * Remove sibling versioned cache directories for the ashlr plugin so only the
132
+ * current active version remains. This prevents ~/.claude/plugins/cache from
133
+ * growing unboundedly across plugin upgrades.
134
+ *
135
+ * Safety guards:
136
+ * - Only acts when `${CLAUDE_PLUGIN_ROOT}` is set and its parent basename
137
+ * matches strict semver (`X.Y.Z`). If the shape looks unexpected we do
138
+ * nothing — never risk wiping user data.
139
+ * - Only removes siblings whose basename also matches strict semver; any
140
+ * other directory (e.g. `latest`, dotfiles, non-semver tags) is left
141
+ * untouched.
142
+ * - Wrapped in try/catch — never throws from inside a hook.
143
+ *
144
+ * Emits one stderr line when anything was removed.
145
+ */
146
+ export function cleanupStalePluginVersions(
147
+ pluginRoot: string | undefined = process.env.CLAUDE_PLUGIN_ROOT,
148
+ opts: { logger?: (msg: string) => void } = {},
149
+ ): { removed: string[]; reason?: string } {
150
+ const log = opts.logger ?? ((m: string) => process.stderr.write(m));
151
+ try {
152
+ if (!pluginRoot) return { removed: [], reason: "no-plugin-root" };
153
+ // basename/dirname handle trailing separators on both POSIX and Windows,
154
+ // so the old `.replace(/\/+$/, "")` was a no-op on Windows (only stripped
155
+ // forward slashes) and redundant on POSIX.
156
+ const currentVersion = basename(pluginRoot);
157
+ if (!SEMVER_DIR_RE.test(currentVersion)) {
158
+ return { removed: [], reason: "unexpected-shape" };
159
+ }
160
+ const parent = dirname(pluginRoot);
161
+ if (!existsSync(parent)) return { removed: [], reason: "no-parent" };
162
+ // Only sweep inside Claude Code's plugin cache tree. Guards against a
163
+ // stray CLAUDE_PLUGIN_ROOT pointing at e.g. ~/.nvm/versions/node/1.0.0
164
+ // which would otherwise make us rm semver-shaped siblings. Normalize
165
+ // separators so the check works on Windows (\plugins\cache\) too.
166
+ if (!parent.replace(/\\/g, "/").includes("/plugins/cache/")) {
167
+ return { removed: [], reason: "parent-not-in-plugin-cache" };
168
+ }
169
+
170
+ const entries = readdirSync(parent, { withFileTypes: true });
171
+ const removed: string[] = [];
172
+ for (const ent of entries) {
173
+ if (!ent.isDirectory()) continue;
174
+ if (ent.name === currentVersion) continue;
175
+ if (!SEMVER_DIR_RE.test(ent.name)) continue;
176
+ const target = join(parent, ent.name);
177
+ try {
178
+ rmSync(target, { recursive: true, force: true });
179
+ removed.push(ent.name);
180
+ } catch {
181
+ /* ignore per-directory errors */
182
+ }
183
+ }
184
+ if (removed.length > 0) {
185
+ log(`[ashlr] cleaned ${removed.length} stale cache version(s): ${removed.join(", ")}\n`);
186
+ }
187
+ return { removed };
188
+ } catch {
189
+ return { removed: [], reason: "error" };
190
+ }
191
+ }
192
+
193
+ export function ensureDepsInstalled(pluginRoot?: string): void {
194
+ const root = pluginRoot ?? (process.env.CLAUDE_PLUGIN_ROOT || join(import.meta.dir, ".."));
195
+ // Cheap check (existsSync is sub-ms): if deps are present, no-op.
196
+ if (existsSync(join(root, "node_modules", "@modelcontextprotocol", "sdk"))) return;
197
+
198
+ // Deps missing. We MUST NOT block session-start — a slow/hung `bun install`
199
+ // would freeze Claude Code on first run. Spawn detached + unref so the
200
+ // install runs in the background while session-start returns immediately.
201
+ // This session may have reduced functionality until the install completes;
202
+ // the next session will see deps present and skip this branch entirely.
203
+ try {
204
+ process.stderr.write(
205
+ "[ashlr] first-run: dependencies missing — installing in the background. " +
206
+ "This session may have reduced functionality until install completes; " +
207
+ "subsequent sessions will start instantly.\n",
208
+ );
209
+ const child = spawn("bun", ["install"], {
210
+ cwd: root,
211
+ stdio: "ignore",
212
+ env: { ...process.env, CI: "1" },
213
+ detached: true,
214
+ });
215
+ // unref so the parent (this hook) can exit even if the child is still
216
+ // running. We deliberately do not await or attach 'close' handlers — the
217
+ // hook's job is to kick the install, not wait for it.
218
+ child.on("error", (e) => {
219
+ process.stderr.write(
220
+ "[ashlr-session-start] background bun install failed to spawn: " +
221
+ (e instanceof Error ? e.message : String(e)) +
222
+ ` — run manually: cd "${root}" && bun install\n`,
223
+ );
224
+ });
225
+ child.unref();
226
+ } catch (e) {
227
+ process.stderr.write(
228
+ "[ashlr-session-start] background bun install spawn failed: " +
229
+ (e instanceof Error ? e.message : String(e)) +
230
+ ` — run manually: cd "${root}" && bun install\n`,
231
+ );
232
+ }
233
+ }
234
+
235
+ interface HookOutput {
236
+ hookSpecificOutput: {
237
+ hookEventName: "SessionStart";
238
+ additionalContext?: string;
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Path to `~/.ashlr/last-project.json` — a small hint file the MCP servers
244
+ * read to discover the user's real project directory.
245
+ *
246
+ * Why this file exists (v1.19.1 hotfix): Claude Code forwards
247
+ * `CLAUDE_PROJECT_DIR` to *hooks* but NOT to MCP subprocesses. Without this
248
+ * hint, the filesystem-touching MCP tools (ashlr__read, ashlr__grep, etc.)
249
+ * refuse any path outside their launch cwd — which is the plugin install
250
+ * dir under `.claude/plugins/cache/`, not the user's project. The redirect
251
+ * hook tells the model to call `ashlr__read` with an absolute project path,
252
+ * so without the hint the two halves dead-end each other. The session-start
253
+ * hook (which *does* see `CLAUDE_PROJECT_DIR`) writes this file so the MCP
254
+ * clamp can include the project dir in its allow-list.
255
+ */
256
+ /**
257
+ * v1.19.2: stable session-id fallback when CLAUDE_SESSION_ID isn't visible.
258
+ * Uses process.ppid + home + a per-session random seed persisted to the
259
+ * hint file. Since this runs in SessionStart exactly once per session,
260
+ * the random component changes per session even when ppid recycles — which
261
+ * is what makes it session-unique across Claude Code restarts.
262
+ */
263
+ function derivedSessionId(_home: string): string {
264
+ const seed = `h:${process.ppid ?? "?"}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
265
+ let h = 0;
266
+ for (let i = 0; i < seed.length; i++) h = ((h << 5) - h + seed.charCodeAt(i)) | 0;
267
+ return `h${(h >>> 0).toString(16)}`;
268
+ }
269
+
270
+ export function projectHintPath(home: string = homedir()): string {
271
+ return join(home, ".ashlr", "last-project.json");
272
+ }
273
+
274
+ /**
275
+ * Window after a wizard-written `~/.ashlr/restart-required` hint within which
276
+ * we still consider the hint "fresh" enough to compare pids. Anything older
277
+ * is treated as stale — Claude Code obviously restarted at some point — and
278
+ * silently cleared. 60s is generous: a real user typing /quit + reopening
279
+ * normally takes 2–8s; an automation pipeline running the wizard headlessly
280
+ * gets the same grace period.
281
+ */
282
+ export const RESTART_HINT_FRESHNESS_MS = 60_000;
283
+
284
+ export interface MissedRestartResult {
285
+ /** What we decided: "no-hint" | "stale" | "restarted" | "missed". */
286
+ outcome: "no-hint" | "stale" | "restarted" | "missed" | "error";
287
+ /** True iff we deleted the hint file as part of this run. */
288
+ cleared: boolean;
289
+ /** Warning text written to stderr — null when no warning was emitted. */
290
+ warning: string | null;
291
+ }
292
+
293
+ /**
294
+ * The exact stderr warning emitted when a user finishes the wizard but
295
+ * doesn't restart Claude Code. Exported so tests can assert on it without
296
+ * fragile string-matching across the source.
297
+ */
298
+ export const MISSED_RESTART_WARNING =
299
+ "[ashlr] WARNING: Wizard finished but Claude Code wasn't restarted. " +
300
+ "The next ashlr tool call will fall back to built-in Read/Edit/Grep. " +
301
+ "Run /quit then reopen Claude Code to register the plugin tools.\n";
302
+
303
+ /**
304
+ * Read the wizard's restart-required hint and decide whether we need to
305
+ * warn the user. Addresses UX-audit cliff #2: the wizard says "restart"
306
+ * but Claude Code's hook execution model means *this* SessionStart can
307
+ * tell whether a restart actually happened by comparing the hint's pid
308
+ * with our own. Same pid + recent timestamp = same shell process = user
309
+ * skipped the restart and will hit built-in Read/Edit/Grep on next call.
310
+ *
311
+ * Decisions:
312
+ * - No hint file → no-op.
313
+ * - Hint ≥60s old → real restart happened at some point; clear silently.
314
+ * - Hint <60s + different pid → new shell after restart; clear silently.
315
+ * - Hint <60s + SAME pid → warn on stderr, leave hint in place so the
316
+ * next *real* restart still detects + clears it.
317
+ *
318
+ * Wrapped end-to-end in try/catch — never errors the SessionStart hook.
319
+ * Filesystem-only, ~1ms — respects the v1.29 2s safety net by a wide margin.
320
+ */
321
+ export function checkMissedRestart(
322
+ opts: {
323
+ home?: string;
324
+ now?: number;
325
+ pid?: number;
326
+ /** Test injection: override stderr writer. */
327
+ stderr?: (msg: string) => void;
328
+ } = {},
329
+ ): MissedRestartResult {
330
+ try {
331
+ const home = opts.home ?? (process.env.ASHLR_HOME_OVERRIDE?.trim() || homedir());
332
+ const now = opts.now ?? Date.now();
333
+ const myPid = opts.pid ?? process.pid;
334
+ const writeStderr = opts.stderr ?? ((m: string) => process.stderr.write(m));
335
+
336
+ const hintPath = restartRequiredPath(home);
337
+ if (!existsSync(hintPath)) return { outcome: "no-hint", cleared: false, warning: null };
338
+
339
+ let parsed: { writtenAt?: unknown; pid?: unknown; wizardVersion?: unknown };
340
+ try {
341
+ parsed = JSON.parse(readFileSync(hintPath, "utf-8"));
342
+ } catch {
343
+ // Corrupt JSON: best-effort cleanup so we don't loop forever.
344
+ try { unlinkSync(hintPath); } catch { /* ignore */ }
345
+ return { outcome: "error", cleared: true, warning: null };
346
+ }
347
+
348
+ const writtenAtRaw = typeof parsed.writtenAt === "string" ? parsed.writtenAt : "";
349
+ const writtenAtMs = writtenAtRaw ? Date.parse(writtenAtRaw) : NaN;
350
+ const hintPid = typeof parsed.pid === "number" ? parsed.pid : -1;
351
+
352
+ if (!Number.isFinite(writtenAtMs)) {
353
+ // Malformed timestamp — clear and move on.
354
+ try { unlinkSync(hintPath); } catch { /* ignore */ }
355
+ return { outcome: "error", cleared: true, warning: null };
356
+ }
357
+
358
+ const ageMs = now - writtenAtMs;
359
+ const fresh = ageMs < RESTART_HINT_FRESHNESS_MS && ageMs >= 0;
360
+
361
+ if (!fresh) {
362
+ // Hint is stale (or written in the future via clock skew) — Claude Code
363
+ // clearly restarted at some point. Clear silently.
364
+ try { unlinkSync(hintPath); } catch { /* ignore */ }
365
+ return { outcome: "stale", cleared: true, warning: null };
366
+ }
367
+
368
+ if (hintPid !== myPid) {
369
+ // Fresh hint but different process — Claude Code did restart, just
370
+ // very recently. Clear silently.
371
+ try { unlinkSync(hintPath); } catch { /* ignore */ }
372
+ return { outcome: "restarted", cleared: true, warning: null };
373
+ }
374
+
375
+ // Fresh hint AND same pid: the wizard ran in this exact shell and
376
+ // Claude Code was NOT restarted. Warn — but leave the hint alone so
377
+ // the next genuine restart still detects + clears it.
378
+ writeStderr(MISSED_RESTART_WARNING);
379
+ return { outcome: "missed", cleared: false, warning: MISSED_RESTART_WARNING };
380
+ } catch {
381
+ return { outcome: "error", cleared: false, warning: null };
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Write the last-project hint. Picks up the project dir from
387
+ * `CLAUDE_PROJECT_DIR` (set by Claude Code in hook environments); no-ops
388
+ * silently when unset, when the dir doesn't exist, or on any write error —
389
+ * the hint is decoration, not a contract.
390
+ *
391
+ * Shape:
392
+ * { "projectDir": "/absolute/path", "updatedAt": "2026-...", "sessionId": "..." }
393
+ *
394
+ * The `sessionId` hint lets future MCP tooling detect cross-session staleness
395
+ * cheaply (hook ran in session X, MCP spawned in session Y → possibly stale).
396
+ * The MCP clamp today uses a 24h TTL on `updatedAt` and ignores `sessionId`.
397
+ */
398
+ export function writeProjectHint(
399
+ opts: {
400
+ home?: string;
401
+ projectDir?: string;
402
+ sessionId?: string;
403
+ now?: Date;
404
+ } = {},
405
+ ): { ok: boolean; reason?: string; path?: string } {
406
+ try {
407
+ const home = opts.home ?? (process.env.ASHLR_HOME_OVERRIDE?.trim() || homedir());
408
+ // Priority: explicit opt → CLAUDE_PROJECT_DIR env → process.cwd() (final
409
+ // fallback, guarded so we never write the plugin install dir into the
410
+ // hint). Claude Code does set the hook subprocess's cwd to the user's
411
+ // active project, but does not always forward CLAUDE_PROJECT_DIR — so
412
+ // without the cwd fallback every fresh session leaves the hint stale and
413
+ // the MCP cwd-clamp refuses every project-path call.
414
+ const envHint = (process.env.CLAUDE_PROJECT_DIR ?? "").trim();
415
+ const cwdFallback = (() => {
416
+ const c = process.cwd();
417
+ return cwdLooksLikePluginRoot(c) ? "" : c;
418
+ })();
419
+ const projectDir = opts.projectDir || envHint || cwdFallback;
420
+ if (!projectDir) return { ok: false, reason: "no-project-dir" };
421
+ // Only write when the path actually exists and is a directory — guards
422
+ // against a stale env or a race where the project was deleted mid-session.
423
+ try {
424
+ const st = statSync(projectDir);
425
+ if (!st.isDirectory()) return { ok: false, reason: "not-a-directory" };
426
+ } catch {
427
+ return { ok: false, reason: "stat-failed" };
428
+ }
429
+ // v1.19.2: sessionId is ALWAYS written. Priority: explicit opt → env
430
+ // (CLAUDE_SESSION_ID, ASHLR_SESSION_ID) → stable PPID-hash derived at
431
+ // session start. The deterministic fallback guarantees MCP + status-line
432
+ // readers converge on the SAME bucket even when neither sees the env.
433
+ const explicitSession =
434
+ opts.sessionId ??
435
+ process.env.CLAUDE_SESSION_ID ??
436
+ process.env.ASHLR_SESSION_ID;
437
+ const sessionId =
438
+ explicitSession && explicitSession.trim().length > 0
439
+ ? explicitSession.trim()
440
+ : derivedSessionId(home);
441
+ const payload: Record<string, string> = {
442
+ projectDir,
443
+ updatedAt: (opts.now ?? new Date()).toISOString(),
444
+ sessionId,
445
+ };
446
+ const path = projectHintPath(home);
447
+ mkdirSync(dirname(path), { recursive: true });
448
+ writeFileSync(path, JSON.stringify(payload, null, 2));
449
+ return { ok: true, path };
450
+ } catch (err) {
451
+ return { ok: false, reason: err instanceof Error ? err.message : "write-failed" };
452
+ }
453
+ }
454
+
455
+ export function announceStampPath(home: string = homedir()): string {
456
+ return join(home, ".ashlr", "last-announce");
457
+ }
458
+
459
+ /** Returns the activation notice if it hasn't fired today, else null. */
460
+ export function maybeActivationNotice(
461
+ home: string = homedir(),
462
+ today: string = new Date().toISOString().slice(0, 10),
463
+ ): string | null {
464
+ const stamp = announceStampPath(home);
465
+ let last = "";
466
+ try {
467
+ if (existsSync(stamp)) last = readFileSync(stamp, "utf-8").trim();
468
+ } catch {
469
+ /* ignore */
470
+ }
471
+ if (last === today) return null;
472
+ try {
473
+ mkdirSync(dirname(stamp), { recursive: true });
474
+ writeFileSync(stamp, today);
475
+ } catch {
476
+ /* ignore */
477
+ }
478
+ // Resolve the notice lazily so version + skill count reflect the plugin
479
+ // currently on disk — never a stale copy captured at module-load time.
480
+ return buildActivationNotice();
481
+ }
482
+
483
+ export interface BuildOpts {
484
+ dir?: string;
485
+ home?: string;
486
+ today?: string;
487
+ budgetMs?: number;
488
+ /** Override the scanner — used in tests. */
489
+ scanFn?: typeof scan;
490
+ formatFn?: typeof formatBaseline;
491
+ }
492
+
493
+ export interface BuildResult {
494
+ output: HookOutput;
495
+ notice: string | null;
496
+ }
497
+
498
+ /** Path to the first-run stamp file. Re-exported for tests. */
499
+ export { stampPath, isFirstRun, writeStamp } from "../scripts/onboarding-wizard";
500
+
501
+ // ---------------------------------------------------------------------------
502
+ // Onboarding state machine
503
+ // ---------------------------------------------------------------------------
504
+
505
+ /**
506
+ * The three banner states:
507
+ * "first" — no onboarding.json: show consent + /ashlr-start CTA
508
+ * "in-progress" — started but not completed: show "finish setup" hint
509
+ * "done" — completed or explicitly skipped: silent
510
+ */
511
+ export type OnboardingBannerState = "first" | "in-progress" | "done";
512
+
513
+ export function getOnboardingBannerState(home: string = homedir()): OnboardingBannerState {
514
+ // If the installed-at stamp doesn't exist it's truly first run.
515
+ if (isFirstRun(home)) return "first";
516
+
517
+ const state = readOnboardingState(home);
518
+ if (!state) {
519
+ // Stamp exists but no onboarding.json — treat as first run for the banner
520
+ // (wizard was never shown). This covers the case where the user was on an
521
+ // older version that wrote the stamp but not the onboarding.json.
522
+ return "first";
523
+ }
524
+ if (state.completed) return "done";
525
+ if (state.started) return "in-progress";
526
+ return "first";
527
+ }
528
+
529
+ /**
530
+ * Path to ~/.ashlr/config.json — used to persist permission consent decisions.
531
+ */
532
+ export function ashlrConfigPath(home: string = homedir()): string {
533
+ return join(home, ".ashlr", "config.json");
534
+ }
535
+
536
+ /** Read ~/.ashlr/config.json, returning {} on any error. */
537
+ export function readAshlrConfig(home: string = homedir()): Record<string, unknown> {
538
+ try {
539
+ const p = ashlrConfigPath(home);
540
+ if (!existsSync(p)) return {};
541
+ const raw = readFileSync(p, "utf-8");
542
+ const parsed = JSON.parse(raw);
543
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
544
+ return parsed as Record<string, unknown>;
545
+ }
546
+ } catch {
547
+ /* treat as empty */
548
+ }
549
+ return {};
550
+ }
551
+
552
+ /** Write a partial update into ~/.ashlr/config.json (merge, not replace). */
553
+ export function writeAshlrConfig(
554
+ patch: Record<string, unknown>,
555
+ home: string = homedir(),
556
+ ): void {
557
+ try {
558
+ mkdirSync(join(home, ".ashlr"), { recursive: true });
559
+ const existing = readAshlrConfig(home);
560
+ const merged = { ...existing, ...patch };
561
+ writeFileSync(ashlrConfigPath(home), JSON.stringify(merged, null, 2) + "\n");
562
+ } catch {
563
+ /* best-effort */
564
+ }
565
+ }
566
+
567
+ /**
568
+ * Check whether the canonical mcp__plugin_ashlr_* wildcard is already in the
569
+ * user's ~/.claude/settings.json allow list.
570
+ */
571
+ export function isAshlrWildcardPresent(home: string = homedir()): boolean {
572
+ try {
573
+ const settingsPath = join(home, ".claude", "settings.json");
574
+ if (!existsSync(settingsPath)) return false;
575
+ const raw = readFileSync(settingsPath, "utf-8");
576
+ const parsed = JSON.parse(raw) as { permissions?: { allow?: string[] } };
577
+ const allow = parsed?.permissions?.allow ?? [];
578
+ return allow.some(
579
+ (e) =>
580
+ e === "mcp__plugin_ashlr_*" ||
581
+ e === "mcp__ashlr-*" ||
582
+ /^mcp__plugin_ashlr_/.test(e) ||
583
+ /^mcp__ashlr(-|__)/.test(e),
584
+ );
585
+ } catch {
586
+ return false;
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Determine whether we should auto-grant permissions this session.
592
+ * Returns false when:
593
+ * - ASHLR_PERMISSIONS_CONSENT=skip env var is set
594
+ * - ~/.ashlr/config.json has permissionsConsent === "declined"
595
+ * - The wildcard is already in the allow list
596
+ */
597
+ export function shouldAutoGrantPermissions(home: string = homedir()): boolean {
598
+ if (process.env.ASHLR_PERMISSIONS_CONSENT === "skip") return false;
599
+ const config = readAshlrConfig(home);
600
+ if (config.permissionsConsent === "declined") return false;
601
+ if (isAshlrWildcardPresent(home)) return false;
602
+ return true;
603
+ }
604
+
605
+ /**
606
+ * Auto-grant mcp__plugin_ashlr_* permissions by calling installPermissions
607
+ * programmatically. Records the result in ~/.ashlr/config.json.
608
+ *
609
+ * Returns the consent banner text to prepend to additionalContext, or null
610
+ * if the grant was skipped (already present / env opt-out / declined).
611
+ */
612
+ export async function maybeAutoGrantPermissions(
613
+ home: string = homedir(),
614
+ ): Promise<string | null> {
615
+ if (!shouldAutoGrantPermissions(home)) return null;
616
+
617
+ try {
618
+ const settingsPath = join(home, ".claude", "settings.json");
619
+ const { installPermissions } = await import("../scripts/install-permissions.ts");
620
+ const result = await installPermissions({
621
+ settingsPath,
622
+ nonInteractive: true,
623
+ });
624
+
625
+ // Record that we granted so we don't re-prompt next session.
626
+ writeAshlrConfig(
627
+ {
628
+ permissionsConsent: "granted",
629
+ grantedAt: new Date().toISOString(),
630
+ },
631
+ home,
632
+ );
633
+
634
+ // Only emit the banner when we actually added entries (not already present).
635
+ if (result.added.length > 0 || result.alreadyPresent.length === 0) {
636
+ return (
637
+ "\nashlr: granting auto-allow for ashlr MCP tools so you don't see permission prompts.\n" +
638
+ "Run /ashlr-start to opt out, or set ASHLR_PERMISSIONS_CONSENT=skip to never auto-grant.\n"
639
+ );
640
+ }
641
+ return null;
642
+ } catch {
643
+ /* best-effort — never break the hook */
644
+ return null;
645
+ }
646
+ }
647
+
648
+ // ---------------------------------------------------------------------------
649
+ // Banner state machine
650
+ // ---------------------------------------------------------------------------
651
+
652
+ /**
653
+ * Returns the additionalContext string that fires the onboarding wizard on
654
+ * first run, or null when the stamp already exists.
655
+ *
656
+ * Side effect: writes the stamp on first run so subsequent sessions skip it.
657
+ */
658
+ export function maybeWizardTrigger(home: string = homedir()): string | null {
659
+ if (!isFirstRun(home)) return null;
660
+ writeStamp(home);
661
+ return (
662
+ "\n[ashlr] This is your first session with the ashlr-plugin. " +
663
+ "Please run /ashlr-start now to complete the 60-second onboarding wizard. " +
664
+ "It will check your setup, offer to approve tool permissions, show a live " +
665
+ "read demo, and optionally initialize a genome for this project.\n"
666
+ );
667
+ }
668
+
669
+ /**
670
+ * Returns an additionalContext banner based on the onboarding state machine:
671
+ * first → /ashlr-start CTA (0 tokens saved yet)
672
+ * in-progress → "finish setup" hint with last step
673
+ * done → null (silent)
674
+ */
675
+ export function buildOnboardingBanner(home: string = homedir()): string | null {
676
+ const bannerState = getOnboardingBannerState(home);
677
+ if (bannerState === "done") return null;
678
+
679
+ if (bannerState === "first") {
680
+ return (
681
+ "\n→ Run /ashlr-start for a 60-second guided setup. " +
682
+ "ashlr saved your last session 0 tokens (you haven't used it yet — let's fix that).\n"
683
+ );
684
+ }
685
+
686
+ // in-progress
687
+ const state = readOnboardingState(home);
688
+ const lastStep = state?.lastStep ?? 0;
689
+ return (
690
+ `\n→ Run /ashlr-start to finish setup (you stopped after step ${lastStep}).\n`
691
+ );
692
+ }
693
+
694
+ export function buildResponse(opts: BuildOpts = {}): BuildResult {
695
+ const home = opts.home ?? homedir();
696
+ const today = opts.today ?? new Date().toISOString().slice(0, 10);
697
+ const doScan = opts.scanFn ?? scan;
698
+ const doFormat = opts.formatFn ?? formatBaseline;
699
+
700
+ let baselineBlock = "";
701
+ try {
702
+ const b = doScan({ dir: opts.dir });
703
+ baselineBlock = doFormat(b);
704
+ } catch {
705
+ baselineBlock = "[ashlr baseline · unavailable]";
706
+ }
707
+
708
+ const notice = maybeActivationNotice(home, today);
709
+
710
+ // Banner state machine: first-run stamp write side-effect lives here.
711
+ // maybeWizardTrigger writes the stamp; buildOnboardingBanner reads state.
712
+ // We call maybeWizardTrigger first (for back-compat stamp semantics), then
713
+ // overlay the richer state-machine banner.
714
+ maybeWizardTrigger(home); // side-effect: write stamp on first run
715
+ const onboardingBanner = buildOnboardingBanner(home);
716
+
717
+ const additionalContext = onboardingBanner
718
+ ? baselineBlock + onboardingBanner
719
+ : baselineBlock;
720
+
721
+ return {
722
+ output: {
723
+ hookSpecificOutput: {
724
+ hookEventName: "SessionStart",
725
+ additionalContext,
726
+ },
727
+ },
728
+ notice,
729
+ };
730
+ }
731
+
732
+ /**
733
+ * Source ~/.ashlr/env if it exists, injecting KEY=VALUE lines into
734
+ * process.env. This makes ASHLR_PRO_TOKEN (and any other vars written by
735
+ * upgrade-flow.ts) available to all subsequent hook logic and sub-processes
736
+ * in this session — without requiring a shell restart.
737
+ */
738
+ function sourceAshlrEnv(): void {
739
+ try {
740
+ const envFile = join(homedir(), ".ashlr", "env");
741
+ if (!existsSync(envFile)) return;
742
+ const content = readFileSync(envFile, "utf8");
743
+ for (const line of content.split("\n")) {
744
+ const trimmed = line.trim();
745
+ // Match: export KEY=VALUE or KEY=VALUE (value may be quoted or bare)
746
+ const m = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
747
+ if (!m) continue;
748
+ const key = m[1]!;
749
+ let val = m[2]!;
750
+ // Strip surrounding single or double quotes
751
+ if ((val.startsWith('"') && val.endsWith('"')) ||
752
+ (val.startsWith("'") && val.endsWith("'"))) {
753
+ val = val.slice(1, -1);
754
+ }
755
+ if (!(key in process.env)) {
756
+ // Only set if not already present — never override explicit env vars
757
+ process.env[key] = val;
758
+ }
759
+ }
760
+ } catch {
761
+ /* env file is decoration — never break the hook */
762
+ }
763
+ }
764
+
765
+
766
+ /**
767
+ * Detect the active ashlr tier for Q2 cloud delta sync. Mirrors
768
+ * scripts/check-tier.ts so we can call syncCloudDeltas without spawning a
769
+ * subprocess. Always returns a definite tier — read failures = "free".
770
+ */
771
+ async function detectTierForSync(): Promise<Tier> {
772
+ try {
773
+ const home = process.env.ASHLR_HOME_OVERRIDE?.trim() || homedir();
774
+ const cachePath = join(home, ".ashlr", "pro-token-cache.json");
775
+ if (!existsSync(cachePath)) return "free";
776
+ const c = JSON.parse(readFileSync(cachePath, "utf-8")) as {
777
+ valid?: boolean;
778
+ validatedAt?: string;
779
+ plan?: string;
780
+ };
781
+ if (c.valid !== true || !c.validatedAt) return "free";
782
+ const age = Date.now() - new Date(c.validatedAt).getTime();
783
+ if (age >= 7 * 24 * 60 * 60 * 1000) return "free";
784
+ if (c.plan === "team") return "team";
785
+ if (c.plan === "pro") return "pro";
786
+ return "free";
787
+ } catch {
788
+ return "free";
789
+ }
790
+ }
791
+
792
+ async function main(): Promise<void> {
793
+ // Source ~/.ashlr/env so ASHLR_PRO_TOKEN from the upgrade flow is available
794
+ // without requiring a shell restart.
795
+ sourceAshlrEnv();
796
+
797
+ // First-run: bootstrap dependencies if missing. Silent no-op otherwise.
798
+ ensureDepsInstalled();
799
+
800
+ // Post-upgrade hygiene: drop sibling cache versions that aren't the active
801
+ // one. Never throws (see cleanupStalePluginVersions for safety guards).
802
+ cleanupStalePluginVersions();
803
+
804
+ // v1.19.1 hotfix: Claude Code forwards CLAUDE_PROJECT_DIR to hooks but
805
+ // NOT to MCP subprocesses. Write the project dir to ~/.ashlr/last-project.json
806
+ // so filesystem-touching MCP tools (ashlr__read, ashlr__grep, etc.) can
807
+ // extend their cwd-clamp allow-list to the user's real workspace. Without
808
+ // this, the PreToolUse redirect hook sends the model to ashlr__read with
809
+ // an absolute project path that the clamp then refuses — a dead-end loop.
810
+ try { writeProjectHint(); } catch { /* hint is decoration — never break the hook */ }
811
+
812
+ // UX-audit cliff #2: warn when wizard finished but Claude Code wasn't
813
+ // restarted. Filesystem-only + ~1ms; respects the v1.29 2s safety net.
814
+ try { checkMissedRestart(); } catch { /* check is best-effort */ }
815
+
816
+ // Opt-in: when ASHLR_STATS_BACKEND=sqlite, move the legacy ~/.ashlr/stats.json
817
+ // into ~/.ashlr/stats.db once at session start so MCP workers open an
818
+ // already-initialized db and don't race on first-time schema creation.
819
+ // Never throws — stats writes must never block the agent.
820
+ if (process.env.ASHLR_STATS_BACKEND === "sqlite") {
821
+ try {
822
+ const { migrateStatsIfNeeded } = await import("../scripts/migrate-stats-to-sqlite");
823
+ await migrateStatsIfNeeded();
824
+ } catch {
825
+ /* best-effort */
826
+ }
827
+ }
828
+
829
+ // Drain stdin (Claude Code passes hook input as JSON) but we don't need it.
830
+ try {
831
+ // Best-effort, non-blocking-ish: only attempt if stdin is a pipe.
832
+ if (!process.stdin.isTTY) {
833
+ // Read but don't wait forever
834
+ await Promise.race([
835
+ (async () => {
836
+ for await (const _ of process.stdin as AsyncIterable<unknown>) {
837
+ // discard
838
+ }
839
+ })(),
840
+ new Promise((r) => setTimeout(r, 50)),
841
+ ]);
842
+ }
843
+ } catch {
844
+ /* ignore */
845
+ }
846
+
847
+ let result: BuildResult;
848
+ try {
849
+ result = await Promise.race([
850
+ Promise.resolve(buildResponse()),
851
+ new Promise<BuildResult>((resolve) =>
852
+ setTimeout(
853
+ () =>
854
+ resolve({
855
+ output: {
856
+ hookSpecificOutput: {
857
+ hookEventName: "SessionStart",
858
+ additionalContext: "[ashlr baseline · timed out]",
859
+ },
860
+ },
861
+ notice: null,
862
+ }),
863
+ SCAN_BUDGET_MS,
864
+ )
865
+ ),
866
+ ]);
867
+ } catch {
868
+ result = {
869
+ output: { hookSpecificOutput: { hookEventName: "SessionStart" } },
870
+ notice: null,
871
+ };
872
+ }
873
+
874
+ if (result.notice) {
875
+ // stderr so it surfaces in the Claude Code transcript without polluting
876
+ // the JSON hook response on stdout.
877
+ process.stderr.write(result.notice + "\n");
878
+ }
879
+
880
+ // Auto-grant MCP permissions on first session (or when wildcard is absent).
881
+ // Runs after buildResponse so the consent banner is appended to the
882
+ // already-built additionalContext rather than racing the baseline scan.
883
+ try {
884
+ const consentBanner = await maybeAutoGrantPermissions();
885
+ if (consentBanner) {
886
+ const ctx = result.output.hookSpecificOutput.additionalContext ?? "";
887
+ result.output.hookSpecificOutput.additionalContext = ctx + consentBanner;
888
+ }
889
+ } catch {
890
+ /* best-effort — permissions grant never breaks the hook */
891
+ }
892
+
893
+ // Initialize the per-session bucket in ~/.ashlr/stats.json. This sets
894
+ // `startedAt` for the current CLAUDE_SESSION_ID so `/ashlr-savings` can
895
+ // report "session started Nm ago" accurately. Fire-and-forget — a stats
896
+ // write never blocks the hook response.
897
+ try { await initSessionBucket(); } catch { /* stats is decoration */ }
898
+
899
+ // Telemetry consent notice — shown once, only when user has opted in.
900
+ // Goes to stderr so it appears in the transcript but doesn't pollute stdout.
901
+ try {
902
+ const h = process.env.ASHLR_HOME_OVERRIDE?.trim() || homedir();
903
+ const telemetryNotice = maybeTelemetryConsentNotice(h);
904
+ if (telemetryNotice) process.stderr.write(telemetryNotice);
905
+ } catch {
906
+ /* decoration — never break the hook */
907
+ }
908
+
909
+ // Run the session-start greeting (first-run welcome / normal 1-liner /
910
+ // weekly digest). Writes to stderr; swallows its own errors. We run this
911
+ // AFTER the legacy notice so the greeting is the last thing the user sees
912
+ // in the transcript.
913
+ try {
914
+ sessionGreet();
915
+ } catch {
916
+ /* greeting is decoration — never break the hook */
917
+ }
918
+
919
+ // Fire-and-forget update check. Reads plugin.json for the current version,
920
+ // fetches the latest GitHub release (2s timeout), and prints a one-line
921
+ // notice to stderr at most once per day per upstream version.
922
+ //
923
+ // Defense-in-depth: checkForUpdate has its own internal 2s fetch timeout,
924
+ // but we wrap the call in an outer Promise.race so that *any* hang (DNS
925
+ // resolution, event-loop reference, unhandled await) can never keep the
926
+ // hook alive past 2 seconds. The promise is intentionally not awaited
927
+ // beyond this race so it never delays session-start.
928
+ try {
929
+ const pluginJsonPath = new URL("../.claude-plugin/plugin.json", import.meta.url).pathname;
930
+ const pluginJson = JSON.parse(readFileSync(pluginJsonPath, "utf-8")) as { version?: string };
931
+ const currentVersion = pluginJson.version ?? "";
932
+ // Race the update check against a hard 2s deadline. We swallow rejections
933
+ // here because checkForUpdate already does its own internal swallow; this
934
+ // race only protects against the fire-and-forget reference outliving the
935
+ // hook (e.g. a pending fetch that the AbortController didn't actually
936
+ // abort because the runtime was wedged).
937
+ void Promise.race([
938
+ checkForUpdate({ currentVersion }),
939
+ new Promise<void>((resolve) => setTimeout(resolve, 2000)),
940
+ ]).catch((e) => {
941
+ process.stderr.write(
942
+ "[ashlr-session-start] update check failed: " +
943
+ (e instanceof Error ? e.message : String(e)) +
944
+ "\n",
945
+ );
946
+ });
947
+ } catch (e) {
948
+ process.stderr.write(
949
+ "[ashlr-session-start] update check setup failed: " +
950
+ (e instanceof Error ? e.message : String(e)) +
951
+ "\n",
952
+ );
953
+ }
954
+
955
+ // Best-effort cloud genome pull. No-ops silently when: kill switch set,
956
+ // no pro-token, not a git repo, or genome not yet ready. Never blocks.
957
+ try {
958
+ await runCloudPull();
959
+ } catch (e) {
960
+ process.stderr.write(
961
+ "[ashlr-session-start] cloud genome pull failed: " +
962
+ (e instanceof Error ? e.message : String(e)) +
963
+ "\n",
964
+ );
965
+ }
966
+
967
+ // Q2 cloud delta sync — Pro/Team only. Tier check happens inside the script
968
+ // (free → instant return). We race against a 2s hard ceiling so a stalled
969
+ // fetch can never blow the SessionStart budget. Failures are swallowed.
970
+ try {
971
+ const tier = await detectTierForSync();
972
+ await Promise.race([
973
+ syncCloudDeltas({ tier }),
974
+ new Promise<void>((resolve) => setTimeout(resolve, 2000)),
975
+ ]);
976
+ } catch (e) {
977
+ process.stderr.write(
978
+ "[ashlr-session-start] cloud delta sync failed: " +
979
+ (e instanceof Error ? e.message : String(e)) +
980
+ "\n",
981
+ );
982
+ }
983
+
984
+ // Pro-gated: pull cross-machine aggregate stats (1h TTL cache). Best-effort
985
+ // fire-and-forget — the dashboard reads from cache, so this just warms it.
986
+ try { maybeCloudPull(); } catch { /* stats pull is decoration */ }
987
+
988
+ process.stdout.write(JSON.stringify(result.output));
989
+ }
990
+
991
+ if (import.meta.main) {
992
+ await main();
993
+ }