@coldtea/abide 0.0.1

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 (370) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -0
  3. package/dist/abide-hook.d.ts +2 -0
  4. package/dist/abide-hook.d.ts.map +1 -0
  5. package/dist/abide-hook.js +23 -0
  6. package/dist/abide-hook.js.map +1 -0
  7. package/dist/bin.d.ts +3 -0
  8. package/dist/bin.d.ts.map +1 -0
  9. package/dist/bin.js +64 -0
  10. package/dist/bin.js.map +1 -0
  11. package/dist/commands/audit.d.ts +3 -0
  12. package/dist/commands/audit.d.ts.map +1 -0
  13. package/dist/commands/audit.js +79 -0
  14. package/dist/commands/audit.js.map +1 -0
  15. package/dist/commands/bench.d.ts +3 -0
  16. package/dist/commands/bench.d.ts.map +1 -0
  17. package/dist/commands/bench.js +206 -0
  18. package/dist/commands/bench.js.map +1 -0
  19. package/dist/commands/calibrate.d.ts +3 -0
  20. package/dist/commands/calibrate.d.ts.map +1 -0
  21. package/dist/commands/calibrate.js +159 -0
  22. package/dist/commands/calibrate.js.map +1 -0
  23. package/dist/commands/check.d.ts +3 -0
  24. package/dist/commands/check.d.ts.map +1 -0
  25. package/dist/commands/check.js +106 -0
  26. package/dist/commands/check.js.map +1 -0
  27. package/dist/commands/compile.d.ts +3 -0
  28. package/dist/commands/compile.d.ts.map +1 -0
  29. package/dist/commands/compile.js +116 -0
  30. package/dist/commands/compile.js.map +1 -0
  31. package/dist/commands/init.d.ts +4 -0
  32. package/dist/commands/init.d.ts.map +1 -0
  33. package/dist/commands/init.js +93 -0
  34. package/dist/commands/init.js.map +1 -0
  35. package/dist/commands/login.d.ts +3 -0
  36. package/dist/commands/login.d.ts.map +1 -0
  37. package/dist/commands/login.js +58 -0
  38. package/dist/commands/login.js.map +1 -0
  39. package/dist/commands/replay.d.ts +4 -0
  40. package/dist/commands/replay.d.ts.map +1 -0
  41. package/dist/commands/replay.js +118 -0
  42. package/dist/commands/replay.js.map +1 -0
  43. package/dist/commands/report.d.ts +5 -0
  44. package/dist/commands/report.d.ts.map +1 -0
  45. package/dist/commands/report.js +78 -0
  46. package/dist/commands/report.js.map +1 -0
  47. package/dist/commands/rubric.d.ts +3 -0
  48. package/dist/commands/rubric.d.ts.map +1 -0
  49. package/dist/commands/rubric.js +40 -0
  50. package/dist/commands/rubric.js.map +1 -0
  51. package/dist/commands/uninstall.d.ts +2 -0
  52. package/dist/commands/uninstall.d.ts.map +1 -0
  53. package/dist/commands/uninstall.js +31 -0
  54. package/dist/commands/uninstall.js.map +1 -0
  55. package/dist/hooks/postToolUse.d.ts +3 -0
  56. package/dist/hooks/postToolUse.d.ts.map +1 -0
  57. package/dist/hooks/postToolUse.js +145 -0
  58. package/dist/hooks/postToolUse.js.map +1 -0
  59. package/dist/hooks/sessionStart.d.ts +11 -0
  60. package/dist/hooks/sessionStart.d.ts.map +1 -0
  61. package/dist/hooks/sessionStart.js +83 -0
  62. package/dist/hooks/sessionStart.js.map +1 -0
  63. package/dist/hooks/stop.d.ts +26 -0
  64. package/dist/hooks/stop.d.ts.map +1 -0
  65. package/dist/hooks/stop.js +227 -0
  66. package/dist/hooks/stop.js.map +1 -0
  67. package/dist/hooks/turnStart.d.ts +8 -0
  68. package/dist/hooks/turnStart.d.ts.map +1 -0
  69. package/dist/hooks/turnStart.js +40 -0
  70. package/dist/hooks/turnStart.js.map +1 -0
  71. package/dist/lib/applyPatch.d.ts +22 -0
  72. package/dist/lib/applyPatch.d.ts.map +1 -0
  73. package/dist/lib/applyPatch.js +63 -0
  74. package/dist/lib/applyPatch.js.map +1 -0
  75. package/dist/lib/audit.d.ts +40 -0
  76. package/dist/lib/audit.d.ts.map +1 -0
  77. package/dist/lib/audit.js +215 -0
  78. package/dist/lib/audit.js.map +1 -0
  79. package/dist/lib/band.d.ts +20 -0
  80. package/dist/lib/band.d.ts.map +1 -0
  81. package/dist/lib/band.js +40 -0
  82. package/dist/lib/band.js.map +1 -0
  83. package/dist/lib/calibration.d.ts +13 -0
  84. package/dist/lib/calibration.d.ts.map +1 -0
  85. package/dist/lib/calibration.js +25 -0
  86. package/dist/lib/calibration.js.map +1 -0
  87. package/dist/lib/checkRunner.d.ts +34 -0
  88. package/dist/lib/checkRunner.d.ts.map +1 -0
  89. package/dist/lib/checkRunner.js +76 -0
  90. package/dist/lib/checkRunner.js.map +1 -0
  91. package/dist/lib/claude.d.ts +4 -0
  92. package/dist/lib/claude.d.ts.map +1 -0
  93. package/dist/lib/claude.js +28 -0
  94. package/dist/lib/claude.js.map +1 -0
  95. package/dist/lib/compilePrompt.d.ts +22 -0
  96. package/dist/lib/compilePrompt.d.ts.map +1 -0
  97. package/dist/lib/compilePrompt.js +33 -0
  98. package/dist/lib/compilePrompt.js.map +1 -0
  99. package/dist/lib/constants.d.ts +33 -0
  100. package/dist/lib/constants.d.ts.map +1 -0
  101. package/dist/lib/constants.js +33 -0
  102. package/dist/lib/constants.js.map +1 -0
  103. package/dist/lib/coverage.d.ts +7 -0
  104. package/dist/lib/coverage.d.ts.map +1 -0
  105. package/dist/lib/coverage.js +17 -0
  106. package/dist/lib/coverage.js.map +1 -0
  107. package/dist/lib/credentials.d.ts +27 -0
  108. package/dist/lib/credentials.d.ts.map +1 -0
  109. package/dist/lib/credentials.js +89 -0
  110. package/dist/lib/credentials.js.map +1 -0
  111. package/dist/lib/diff.d.ts +30 -0
  112. package/dist/lib/diff.d.ts.map +1 -0
  113. package/dist/lib/diff.js +139 -0
  114. package/dist/lib/diff.js.map +1 -0
  115. package/dist/lib/events.d.ts +5 -0
  116. package/dist/lib/events.d.ts.map +1 -0
  117. package/dist/lib/events.js +38 -0
  118. package/dist/lib/events.js.map +1 -0
  119. package/dist/lib/git.d.ts +37 -0
  120. package/dist/lib/git.d.ts.map +1 -0
  121. package/dist/lib/git.js +163 -0
  122. package/dist/lib/git.js.map +1 -0
  123. package/dist/lib/hookRunner.d.ts +10 -0
  124. package/dist/lib/hookRunner.d.ts.map +1 -0
  125. package/dist/lib/hookRunner.js +52 -0
  126. package/dist/lib/hookRunner.js.map +1 -0
  127. package/dist/lib/hosts.d.ts +20 -0
  128. package/dist/lib/hosts.d.ts.map +1 -0
  129. package/dist/lib/hosts.js +101 -0
  130. package/dist/lib/hosts.js.map +1 -0
  131. package/dist/lib/jev.d.ts +24 -0
  132. package/dist/lib/jev.d.ts.map +1 -0
  133. package/dist/lib/jev.js +169 -0
  134. package/dist/lib/jev.js.map +1 -0
  135. package/dist/lib/lintConfig.d.ts +3 -0
  136. package/dist/lib/lintConfig.d.ts.map +1 -0
  137. package/dist/lib/lintConfig.js +55 -0
  138. package/dist/lib/lintConfig.js.map +1 -0
  139. package/dist/lib/loadRules.d.ts +12 -0
  140. package/dist/lib/loadRules.d.ts.map +1 -0
  141. package/dist/lib/loadRules.js +31 -0
  142. package/dist/lib/loadRules.js.map +1 -0
  143. package/dist/lib/opencodePlugin.d.ts +10 -0
  144. package/dist/lib/opencodePlugin.d.ts.map +1 -0
  145. package/dist/lib/opencodePlugin.js +36 -0
  146. package/dist/lib/opencodePlugin.js.map +1 -0
  147. package/dist/lib/output.d.ts +6 -0
  148. package/dist/lib/output.d.ts.map +1 -0
  149. package/dist/lib/output.js +35 -0
  150. package/dist/lib/output.js.map +1 -0
  151. package/dist/lib/packageRoot.d.ts +12 -0
  152. package/dist/lib/packageRoot.d.ts.map +1 -0
  153. package/dist/lib/packageRoot.js +50 -0
  154. package/dist/lib/packageRoot.js.map +1 -0
  155. package/dist/lib/paths.d.ts +24 -0
  156. package/dist/lib/paths.d.ts.map +1 -0
  157. package/dist/lib/paths.js +75 -0
  158. package/dist/lib/paths.js.map +1 -0
  159. package/dist/lib/reason.d.ts +11 -0
  160. package/dist/lib/reason.d.ts.map +1 -0
  161. package/dist/lib/reason.js +31 -0
  162. package/dist/lib/reason.js.map +1 -0
  163. package/dist/lib/regularFile.d.ts +26 -0
  164. package/dist/lib/regularFile.d.ts.map +1 -0
  165. package/dist/lib/regularFile.js +77 -0
  166. package/dist/lib/regularFile.js.map +1 -0
  167. package/dist/lib/replay.d.ts +74 -0
  168. package/dist/lib/replay.d.ts.map +1 -0
  169. package/dist/lib/replay.js +250 -0
  170. package/dist/lib/replay.js.map +1 -0
  171. package/dist/lib/replayCodex.d.ts +6 -0
  172. package/dist/lib/replayCodex.d.ts.map +1 -0
  173. package/dist/lib/replayCodex.js +132 -0
  174. package/dist/lib/replayCodex.js.map +1 -0
  175. package/dist/lib/replayOpencode.d.ts +15 -0
  176. package/dist/lib/replayOpencode.d.ts.map +1 -0
  177. package/dist/lib/replayOpencode.js +154 -0
  178. package/dist/lib/replayOpencode.js.map +1 -0
  179. package/dist/lib/rubricFile.d.ts +31 -0
  180. package/dist/lib/rubricFile.d.ts.map +1 -0
  181. package/dist/lib/rubricFile.js +75 -0
  182. package/dist/lib/rubricFile.js.map +1 -0
  183. package/dist/lib/scope.d.ts +4 -0
  184. package/dist/lib/scope.d.ts.map +1 -0
  185. package/dist/lib/scope.js +14 -0
  186. package/dist/lib/scope.js.map +1 -0
  187. package/dist/lib/session.d.ts +48 -0
  188. package/dist/lib/session.d.ts.map +1 -0
  189. package/dist/lib/session.js +182 -0
  190. package/dist/lib/session.js.map +1 -0
  191. package/dist/lib/settings.d.ts +26 -0
  192. package/dist/lib/settings.d.ts.map +1 -0
  193. package/dist/lib/settings.js +104 -0
  194. package/dist/lib/settings.js.map +1 -0
  195. package/dist/lib/sources.d.ts +28 -0
  196. package/dist/lib/sources.d.ts.map +1 -0
  197. package/dist/lib/sources.js +137 -0
  198. package/dist/lib/sources.js.map +1 -0
  199. package/dist/lib/stdin.d.ts +3 -0
  200. package/dist/lib/stdin.d.ts.map +1 -0
  201. package/dist/lib/stdin.js +24 -0
  202. package/dist/lib/stdin.js.map +1 -0
  203. package/dist/lib/transcript.d.ts +3 -0
  204. package/dist/lib/transcript.d.ts.map +1 -0
  205. package/dist/lib/transcript.js +57 -0
  206. package/dist/lib/transcript.js.map +1 -0
  207. package/dist/lib/ui.d.ts +8 -0
  208. package/dist/lib/ui.d.ts.map +1 -0
  209. package/dist/lib/ui.js +34 -0
  210. package/dist/lib/ui.js.map +1 -0
  211. package/dist/ui/components/Buckets.d.ts +6 -0
  212. package/dist/ui/components/Buckets.d.ts.map +1 -0
  213. package/dist/ui/components/Buckets.js +11 -0
  214. package/dist/ui/components/Buckets.js.map +1 -0
  215. package/dist/ui/components/Callout.d.ts +11 -0
  216. package/dist/ui/components/Callout.d.ts.map +1 -0
  217. package/dist/ui/components/Callout.js +10 -0
  218. package/dist/ui/components/Callout.js.map +1 -0
  219. package/dist/ui/components/Checklist.d.ts +10 -0
  220. package/dist/ui/components/Checklist.d.ts.map +1 -0
  221. package/dist/ui/components/Checklist.js +8 -0
  222. package/dist/ui/components/Checklist.js.map +1 -0
  223. package/dist/ui/components/Columns.d.ts +28 -0
  224. package/dist/ui/components/Columns.d.ts.map +1 -0
  225. package/dist/ui/components/Columns.js +20 -0
  226. package/dist/ui/components/Columns.js.map +1 -0
  227. package/dist/ui/components/Header.d.ts +9 -0
  228. package/dist/ui/components/Header.d.ts.map +1 -0
  229. package/dist/ui/components/Header.js +7 -0
  230. package/dist/ui/components/Header.js.map +1 -0
  231. package/dist/ui/components/Live.d.ts +16 -0
  232. package/dist/ui/components/Live.d.ts.map +1 -0
  233. package/dist/ui/components/Live.js +40 -0
  234. package/dist/ui/components/Live.js.map +1 -0
  235. package/dist/ui/components/RuleTable.d.ts +15 -0
  236. package/dist/ui/components/RuleTable.d.ts.map +1 -0
  237. package/dist/ui/components/RuleTable.js +81 -0
  238. package/dist/ui/components/RuleTable.js.map +1 -0
  239. package/dist/ui/components/Section.d.ts +10 -0
  240. package/dist/ui/components/Section.d.ts.map +1 -0
  241. package/dist/ui/components/Section.js +8 -0
  242. package/dist/ui/components/Section.js.map +1 -0
  243. package/dist/ui/components/Verdicts.d.ts +9 -0
  244. package/dist/ui/components/Verdicts.d.ts.map +1 -0
  245. package/dist/ui/components/Verdicts.js +12 -0
  246. package/dist/ui/components/Verdicts.js.map +1 -0
  247. package/dist/ui/render.d.ts +20 -0
  248. package/dist/ui/render.d.ts.map +1 -0
  249. package/dist/ui/render.js +32 -0
  250. package/dist/ui/render.js.map +1 -0
  251. package/dist/ui/theme.d.ts +37 -0
  252. package/dist/ui/theme.d.ts.map +1 -0
  253. package/dist/ui/theme.js +63 -0
  254. package/dist/ui/theme.js.map +1 -0
  255. package/dist/ui/views/AuditView.d.ts +18 -0
  256. package/dist/ui/views/AuditView.d.ts.map +1 -0
  257. package/dist/ui/views/AuditView.js +48 -0
  258. package/dist/ui/views/AuditView.js.map +1 -0
  259. package/dist/ui/views/BenchView.d.ts +33 -0
  260. package/dist/ui/views/BenchView.d.ts.map +1 -0
  261. package/dist/ui/views/BenchView.js +19 -0
  262. package/dist/ui/views/BenchView.js.map +1 -0
  263. package/dist/ui/views/CalibrateView.d.ts +26 -0
  264. package/dist/ui/views/CalibrateView.d.ts.map +1 -0
  265. package/dist/ui/views/CalibrateView.js +42 -0
  266. package/dist/ui/views/CalibrateView.js.map +1 -0
  267. package/dist/ui/views/CheckView.d.ts +19 -0
  268. package/dist/ui/views/CheckView.d.ts.map +1 -0
  269. package/dist/ui/views/CheckView.js +15 -0
  270. package/dist/ui/views/CheckView.js.map +1 -0
  271. package/dist/ui/views/ErrorView.d.ts +4 -0
  272. package/dist/ui/views/ErrorView.d.ts.map +1 -0
  273. package/dist/ui/views/ErrorView.js +26 -0
  274. package/dist/ui/views/ErrorView.js.map +1 -0
  275. package/dist/ui/views/HelpView.d.ts +2 -0
  276. package/dist/ui/views/HelpView.d.ts.map +1 -0
  277. package/dist/ui/views/HelpView.js +25 -0
  278. package/dist/ui/views/HelpView.js.map +1 -0
  279. package/dist/ui/views/InitView.d.ts +27 -0
  280. package/dist/ui/views/InitView.d.ts.map +1 -0
  281. package/dist/ui/views/InitView.js +24 -0
  282. package/dist/ui/views/InitView.js.map +1 -0
  283. package/dist/ui/views/ReplayView.d.ts +16 -0
  284. package/dist/ui/views/ReplayView.d.ts.map +1 -0
  285. package/dist/ui/views/ReplayView.js +44 -0
  286. package/dist/ui/views/ReplayView.js.map +1 -0
  287. package/dist/ui/views/ReportView.d.ts +14 -0
  288. package/dist/ui/views/ReportView.d.ts.map +1 -0
  289. package/dist/ui/views/ReportView.js +86 -0
  290. package/dist/ui/views/ReportView.js.map +1 -0
  291. package/dist/ui/views/RubricView.d.ts +18 -0
  292. package/dist/ui/views/RubricView.d.ts.map +1 -0
  293. package/dist/ui/views/RubricView.js +18 -0
  294. package/dist/ui/views/RubricView.js.map +1 -0
  295. package/opencode/abide.mjs +246 -0
  296. package/package.json +50 -0
  297. package/skills/abide-compile/SKILL.md +173 -0
  298. package/src/abide-hook.ts +23 -0
  299. package/src/bin.ts +64 -0
  300. package/src/commands/audit.ts +97 -0
  301. package/src/commands/bench.ts +226 -0
  302. package/src/commands/calibrate.ts +165 -0
  303. package/src/commands/check.ts +114 -0
  304. package/src/commands/compile.ts +127 -0
  305. package/src/commands/init.ts +109 -0
  306. package/src/commands/login.ts +61 -0
  307. package/src/commands/replay.ts +148 -0
  308. package/src/commands/report.ts +83 -0
  309. package/src/commands/rubric.ts +45 -0
  310. package/src/commands/uninstall.ts +33 -0
  311. package/src/hooks/postToolUse.ts +171 -0
  312. package/src/hooks/sessionStart.ts +100 -0
  313. package/src/hooks/stop.ts +275 -0
  314. package/src/hooks/turnStart.ts +36 -0
  315. package/src/lib/applyPatch.ts +75 -0
  316. package/src/lib/audit.ts +265 -0
  317. package/src/lib/band.ts +49 -0
  318. package/src/lib/calibration.ts +35 -0
  319. package/src/lib/checkRunner.ts +124 -0
  320. package/src/lib/claude.ts +35 -0
  321. package/src/lib/compilePrompt.ts +67 -0
  322. package/src/lib/constants.ts +39 -0
  323. package/src/lib/coverage.ts +21 -0
  324. package/src/lib/credentials.ts +104 -0
  325. package/src/lib/diff.ts +209 -0
  326. package/src/lib/events.ts +34 -0
  327. package/src/lib/git.ts +206 -0
  328. package/src/lib/hookRunner.ts +55 -0
  329. package/src/lib/hosts.ts +112 -0
  330. package/src/lib/jev.ts +223 -0
  331. package/src/lib/lintConfig.ts +53 -0
  332. package/src/lib/loadRules.ts +39 -0
  333. package/src/lib/opencodePlugin.ts +38 -0
  334. package/src/lib/output.ts +44 -0
  335. package/src/lib/packageRoot.ts +48 -0
  336. package/src/lib/paths.ts +96 -0
  337. package/src/lib/reason.ts +46 -0
  338. package/src/lib/regularFile.ts +96 -0
  339. package/src/lib/replay.ts +314 -0
  340. package/src/lib/replayCodex.ts +130 -0
  341. package/src/lib/replayOpencode.ts +186 -0
  342. package/src/lib/rubricFile.ts +99 -0
  343. package/src/lib/scope.ts +18 -0
  344. package/src/lib/session.ts +223 -0
  345. package/src/lib/settings.ts +112 -0
  346. package/src/lib/sources.ts +154 -0
  347. package/src/lib/stdin.ts +23 -0
  348. package/src/lib/transcript.ts +49 -0
  349. package/src/lib/ui.ts +38 -0
  350. package/src/ui/components/Buckets.tsx +30 -0
  351. package/src/ui/components/Callout.tsx +26 -0
  352. package/src/ui/components/Checklist.tsx +29 -0
  353. package/src/ui/components/Columns.tsx +62 -0
  354. package/src/ui/components/Header.tsx +35 -0
  355. package/src/ui/components/Live.tsx +73 -0
  356. package/src/ui/components/RuleTable.tsx +94 -0
  357. package/src/ui/components/Section.tsx +24 -0
  358. package/src/ui/components/Verdicts.tsx +40 -0
  359. package/src/ui/render.tsx +51 -0
  360. package/src/ui/theme.ts +65 -0
  361. package/src/ui/views/AuditView.tsx +140 -0
  362. package/src/ui/views/BenchView.tsx +87 -0
  363. package/src/ui/views/CalibrateView.tsx +108 -0
  364. package/src/ui/views/CheckView.tsx +56 -0
  365. package/src/ui/views/ErrorView.tsx +32 -0
  366. package/src/ui/views/HelpView.tsx +51 -0
  367. package/src/ui/views/InitView.tsx +103 -0
  368. package/src/ui/views/ReplayView.tsx +100 -0
  369. package/src/ui/views/ReportView.tsx +225 -0
  370. package/src/ui/views/RubricView.tsx +82 -0
@@ -0,0 +1,173 @@
1
+ ---
2
+ name: abide-compile
3
+ description: Compile a repository's instruction files (AGENTS.md, CLAUDE.md and friends) into an Abide rubric, then validate and calibrate it.
4
+ ---
5
+
6
+ # Compile an Abide rubric
7
+
8
+ You are turning the rules a person wrote for coding agents into a rubric that a checker outside your context window can enforce on every edit. The checker sees only one rule and one diff at a time. Nothing else. Every decision below follows from that.
9
+
10
+ Two facts before you start:
11
+
12
+ - Rules come only from the user's own files. Never add a rule you think they should have. If a file holds zero rules, the rubric holds zero rules and you say so.
13
+ - The rubric is a committed, hand-editable file. A person will read it, and a verdict must trace to a rule they can point at. Quote their wording.
14
+
15
+ ## Step 1. Read the sources
16
+
17
+ The hook that sent you here listed the instruction files it found and where the rubric goes. Read every listed file in full. Then:
18
+
19
+ - Follow pointer files. A file whose whole content is "Read instructions from X" or "Read ./AGENTS.md" is a pointer. Read X and treat X as a source in its own right: list it under `sources` with its own path so it gets its own hash.
20
+ - Files under a subdirectory apply only there. The hook gave each one a scope glob. Rules from that file carry that glob.
21
+ - Read `CONTRIBUTING.md` only when it was listed, and keep only sentences that give an instruction. Prose about how to open a pull request is not a rule.
22
+ - If a lint config was listed (`eslint.config.*`, `.eslintrc*`, `biome.json`, stylelint config), skim it for rule names. When a rule you extract is already enforced there, keep the rule and record the lint rule name in `check.overlaps`. It stays in the rubric because lint runs later than an edit and the convention still binds; the label keeps it from being reported as a new finding.
23
+
24
+ ## Step 2. Extract every statement that instructs
25
+
26
+ Go through each file top to bottom. A rule is any sentence that tells the agent to do or not do something about code. Headings, background, and rationale are not rules, but they often say what a rule is for and belong in the wording of its question.
27
+
28
+ For each rule capture:
29
+
30
+ - `text`: the user's own words, trimmed to one or two sentences. Do not paraphrase into something more general.
31
+ - `source.path` and `source.line`: where it came from. Line numbers matter; they are how the person finds it.
32
+ - Any code block next to it. A worked example or counterexample carries more signal than the prose, and belongs in the question's criteria (see step 4).
33
+
34
+ Do not merge two rules into one because they sit under one heading. Do not split a rule that is one idea.
35
+
36
+ ## Step 3. Classify each rule into exactly one bucket
37
+
38
+ Ask the questions in this order and stop at the first yes.
39
+
40
+ 1. Can a linter enforce it exactly? Then `check.type` is `lint`. Examples: `interface Foo` when the rule says use `type`; `as` casts; `fetch(`; `process.env.`; `Date.now()`; `console.log`; `npm install`. Put the linter rule that enforces it in `how` (`@typescript-eslint/consistent-type-definitions`, `no-restricted-syntax`, a stylelint rule) or, failing that, the AST or grep shape, and a `pattern` as a hint when one is obvious. Abide records these and reports them for the user's own linter; it never runs them and never sends them to the model. The judge is for what a linter cannot express.
41
+ 2. Does the rule need counting or measuring: line lengths, line counts, selector counts, nesting depth, alphabetical or length order (imports or props ordered by line length is the common one)? That is mechanical work, and the judge cannot count. If step 1 found no regex for it, `check.type` is `deferred` with reason "needs a script, not a judge". Do not turn it into a model question; it will score about 0.4 on everything.
42
+ 3. Can a judge answer it by looking at a change and nothing else? Then `check.type` is `model`. Most style, structure, comment, error handling, naming, and "do not do X" rules land here.
43
+ 4. Does answering need the rest of the repository? "Reuse existing error codes", "follow existing patterns", "any visual pattern in two places becomes a shared component", "check whether a helper already exists". Then `check.type` is `deferred` with a one line `reason`. These are real rules that this version cannot check on a diff, and the report says so.
44
+ 5. Is it about the conversation or the process rather than the code? "Ask when unsure", "state a plan", "run the tests before you finish", "clean up processes you started". Then `check.type` is `unenforceable` with a `reason`.
45
+
46
+ Every statement lands somewhere. Count them: the summary you give the user is the four bucket counts.
47
+
48
+ ## Step 4. Write the question for each model rule
49
+
50
+ The judge is a small, fast model that answers typed questions with a probability. It is good at narrow, concrete questions and bad at vague ones. A vague question scores about 0.4 on everything and never fires, and the user reads that silence as good news. Write every question so that a violating diff scores near 1 and a clean diff scores near 0.
51
+
52
+ - One idea per question. If the rule has two parts, make two rules.
53
+ - Ask about the diff: "Does this change add ...", "Does this change put ...". Name the concrete shape you want caught: the identifier, the call, the syntax, the position. "Does this change add a comment that restates what the line below it does?" beats "Are comments sparing?".
54
+ - Never put scope in the text. "For a file under apps/web/src/server" belongs in the `scope` field, not in the question. Scope is a list of globs relative to the repo root, for example `["apps/web/src/server/**/*.service.ts"]`. Omit it when the rule applies everywhere.
55
+ - Keep instructions under about 60 words. Every word is billed on every edit whether or not the rule fires.
56
+ - Ask for existence, not for a judgment of the whole: "Among the added lines, is there at least one comment whose content a reader could reconstruct from the code directly below it?" fires on the first offending line, while "Are the comments appropriate?" averages over the hunk and never leaves the middle.
57
+ - A rule about volume or restraint ("comment sparingly", "keep it short", "minimal code") needs two questions: an existence question for the concrete offence, and a `score` question for the amount, with levels the judge can point at ("no such comments", "one or two", "several, or a multi-line block", "most lines"). Do not collapse "sparingly" into a single narrow case and lose the volume.
58
+ - Use `criteria` for a worked example: `"criteria": { "true": "a `retry`loop written by hand when Radash`retry` is installed", "false": "calling an installed utility" }`. Put the user's own code block here when they gave one.
59
+ - Question types:
60
+ - `boolean` for almost everything. The answer is a probability that the rule is broken.
61
+ - `choice` when the rule names a closed set of shapes and only some are wrong: `criteria` maps each option to a description, `violating` lists the wrong ones. Example: a service must be a class with static methods, so options are `class-static`, `loose-functions`, `eager-init` and `violating` is the last two.
62
+ - `score` when the rule is a matter of degree: `criteria` is an ordered list of levels from compliant (index 0) to worst, and `violatingFrom` is the first level that counts as broken. Example for "minimum code that solves the problem": levels "as short as it can be", "somewhat longer than needed", "about twice as long", "several times longer", with `violatingFrom: 2`.
63
+
64
+ ## Step 5. Decide when each model rule runs
65
+
66
+ Every model rule carries `when`: `"edit"` or `"turn"`. Get this right; the wrong phase produces false violations and each one costs the agent a repair turn.
67
+
68
+ - `"edit"` runs after every edit, against that one hunk. Use it when the lines in front of the judge are enough: raw error text reaching a user, a narrating comment, a hand rolled utility, a type cast, an if/else chain over a discriminated union, an inline style, a hardcoded color.
69
+ - `"turn"` runs once when the agent finishes its turn, against the full diff of everything it changed. Use it for any question about the change as a whole: scope creep, changes outside what was asked, an abstraction with a single caller, overall length, whether a file grew past a cap, whether a new module was needed at all, whether a helper should have been extracted. After edit 1 of 12 these questions have no answer; a helper with one caller now may have three by the end.
70
+
71
+ The test: if a careful reviewer would want to see the whole change before answering, it is `"turn"`.
72
+
73
+ Lint rules always run per edit and do not need `when`.
74
+
75
+ ## Step 6. Write the rubric file
76
+
77
+ Write the file the hook named (`.abide/rubric.json` for the project, `~/.abide/global.json` for global instruction files). Global files get their own rubric because they apply in every repository.
78
+
79
+ ```json
80
+ {
81
+ "version": 1,
82
+ "compiledAt": "2026-09-17T10:00:00.000Z",
83
+ "compiledBy": "claude",
84
+ "sources": [
85
+ { "path": "AGENTS.md", "scope": "**/*" },
86
+ { "path": "apps/web/AGENTS.md", "scope": "apps/web/**/*" }
87
+ ],
88
+ "rules": [
89
+ {
90
+ "id": "no-interface",
91
+ "text": "Use `type`, never `interface`.",
92
+ "source": { "path": "AGENTS.md", "line": 41 },
93
+ "scope": ["**/*.ts", "**/*.tsx"],
94
+ "check": {
95
+ "type": "lint",
96
+ "pattern": "^\\s*(export\\s+)?(declare\\s+)?interface\\s+[A-Za-z_$]",
97
+ "how": "@typescript-eslint/consistent-type-definitions"
98
+ }
99
+ },
100
+ {
101
+ "id": "raw-error-to-user",
102
+ "text": "Never show a user a raw error. A message reaches a user only if the code that produced it authored it for a person.",
103
+ "source": { "path": "AGENTS.md", "line": 120 },
104
+ "scope": ["apps/web/src/**/*.ts", "apps/web/src/**/*.tsx"],
105
+ "when": "edit",
106
+ "check": {
107
+ "type": "model",
108
+ "overlaps": "coldtea/no-raw-error-in-response",
109
+ "question": {
110
+ "type": "boolean",
111
+ "instructions": "Does this change put raw exception text where a user will see it: error.message, String(error), .toString() or a template of them reaching a response body, rendered copy, or a stored column that is displayed?",
112
+ "criteria": {
113
+ "true": "res.status(500).json({ message: String(error) })",
114
+ "false": "log.error(error); res.status(500).json({ message: 'Could not load this run.' })"
115
+ }
116
+ }
117
+ }
118
+ },
119
+ {
120
+ "id": "scope-creep",
121
+ "text": "No features beyond what was asked. Every changed line should trace directly to the user's request.",
122
+ "source": { "path": "AGENTS.md", "line": 22 },
123
+ "when": "turn",
124
+ "check": {
125
+ "type": "model",
126
+ "question": {
127
+ "type": "boolean",
128
+ "instructions": "Does this change add functionality, files, options or abstractions that the task did not ask for?"
129
+ }
130
+ }
131
+ },
132
+ {
133
+ "id": "reuse-error-codes",
134
+ "text": "Reuse existing codes; don't invent near-duplicates.",
135
+ "source": { "path": "AGENTS.md", "line": 131 },
136
+ "check": { "type": "deferred", "reason": "needs the list of codes that already exist" }
137
+ },
138
+ {
139
+ "id": "ask-when-unsure",
140
+ "text": "State your assumptions explicitly. If uncertain, ask.",
141
+ "source": { "path": "AGENTS.md", "line": 12 },
142
+ "check": { "type": "unenforceable", "reason": "about the conversation, not the code" }
143
+ }
144
+ ]
145
+ }
146
+ ```
147
+
148
+ Rules of the file:
149
+
150
+ - `id` is a short kebab-case name that says what the rule catches. Ids must be unique within the file.
151
+ - Leave `sha` out of `sources`; the CLI computes it in the next step.
152
+ - Every rule's `source.path` must appear in `sources`.
153
+ - No `status` field on new rules; it defaults to `active`. Do not write `calibration`; the CLI does.
154
+ - Write it as plain JSON with your file-writing tool. Nothing else goes in the file.
155
+
156
+ ## Step 7. Validate, then calibrate
157
+
158
+ Run, using the CLI invocation the hook gave you:
159
+
160
+ ```
161
+ abide rubric validate (add --global for ~/.abide/global.json)
162
+ abide calibrate (same flag)
163
+ ```
164
+
165
+ `validate` parses the file, fills in the source hashes, and prints the bucket table. Fix every issue it prints and run it again until it is clean.
166
+
167
+ `calibrate` runs every model rule against about twenty real hunks from the repository's git history and marks rules whose scores never get near 0 or near 1 as `weak`, and rules that fire on most historic hunks as `noisy`. Both are switched off until rewritten. When it reports any, rewrite those questions once using step 4 (make them more concrete, split them, add a criteria example, or move them to the other phase), run `validate` and `calibrate` again, and stop there whatever the result. Do not loop more than once. A repository with no history skips calibration; that is fine.
168
+
169
+ If the hook said a rule is being tuned, it attached statistics. Rewrite only the rules it named. Keep every other rule byte for byte.
170
+
171
+ ## Step 8. Report and move on
172
+
173
+ Tell the user, in two or three plain sentences: how many rules, split across the four buckets; which rules are weak or noisy and switched off, if any; and that the rubric is at `.abide/rubric.json` for them to read and edit. Then continue with whatever they asked for. Do not paste the rubric into the conversation.
@@ -0,0 +1,23 @@
1
+ import { handlePostToolUse } from "./hooks/postToolUse.js";
2
+ import { handleSessionStart } from "./hooks/sessionStart.js";
3
+ import { handleStop } from "./hooks/stop.js";
4
+ import { handleTurnStart } from "./hooks/turnStart.js";
5
+ import { runHook } from "./lib/hookRunner.js";
6
+
7
+ const name = process.argv[2];
8
+ switch (name) {
9
+ case "session-start":
10
+ await runHook("session-start", handleSessionStart, 8_000);
11
+ break;
12
+ case "turn-start":
13
+ await runHook("turn-start", handleTurnStart, 8_000);
14
+ break;
15
+ case "post-tool-use":
16
+ await runHook("post-tool-use", handlePostToolUse, 18_000);
17
+ break;
18
+ case "stop":
19
+ await runHook("stop", handleStop, 28_000);
20
+ break;
21
+ default:
22
+ process.exit(0);
23
+ }
package/src/bin.ts ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import { runAudit } from "./commands/audit.js";
3
+ import { runBench } from "./commands/bench.js";
4
+ import { runCalibrate } from "./commands/calibrate.js";
5
+ import { runCheckCommand } from "./commands/check.js";
6
+ import { runCompile } from "./commands/compile.js";
7
+ import { runInit } from "./commands/init.js";
8
+ import { runLogin } from "./commands/login.js";
9
+ import { runReplay } from "./commands/replay.js";
10
+ import { runReport } from "./commands/report.js";
11
+ import { runRubric } from "./commands/rubric.js";
12
+ import { runUninstall } from "./commands/uninstall.js";
13
+ import { showError, showStatic } from "./ui/render.js";
14
+ import { HelpView } from "./ui/views/HelpView.js";
15
+
16
+ process.stdout.on("error", (error: NodeJS.ErrnoException) => {
17
+ if (error.code === "EPIPE") process.exit(0);
18
+ });
19
+
20
+ const main = async (): Promise<number> => {
21
+ const [command, ...rest] = process.argv.slice(2);
22
+ switch (command) {
23
+ case "login":
24
+ return runLogin(rest);
25
+ case "init":
26
+ return runInit(rest);
27
+ case "compile":
28
+ return runCompile(rest, false);
29
+ case "tune":
30
+ return runCompile(rest, true);
31
+ case "rubric":
32
+ return runRubric(rest);
33
+ case "calibrate":
34
+ return runCalibrate(rest);
35
+ case "check":
36
+ return runCheckCommand(rest);
37
+ case "audit":
38
+ return runAudit(rest);
39
+ case "report":
40
+ return runReport(rest);
41
+ case "replay":
42
+ return runReplay(rest);
43
+ case "bench":
44
+ return runBench(rest);
45
+ case "uninstall":
46
+ return runUninstall(rest);
47
+ case undefined:
48
+ case "help":
49
+ case "--help":
50
+ case "-h":
51
+ await showStatic(HelpView());
52
+ return 0;
53
+ default:
54
+ await showError(new Error(`Unknown command "${command}". Try: abide help`));
55
+ return 1;
56
+ }
57
+ };
58
+
59
+ try {
60
+ process.exitCode = await main();
61
+ } catch (error) {
62
+ await showError(error);
63
+ process.exitCode = 1;
64
+ }
@@ -0,0 +1,97 @@
1
+ import { parseArgs } from "node:util";
2
+ import { AbideError } from "@coldtea/abide-schema";
3
+ import { auditFiles, auditableFiles, listRepoFiles, tallyByRule } from "../lib/audit.js";
4
+ import { isGitRepo } from "../lib/git.js";
5
+ import { hasApiKey, NO_KEY_HINT } from "../lib/credentials.js";
6
+ import { loadRules } from "../lib/loadRules.js";
7
+ import { findRepoRoot } from "../lib/paths.js";
8
+ import { say, usd } from "../lib/ui.js";
9
+ import { Header } from "../ui/components/Header.js";
10
+ import { showLive } from "../ui/render.js";
11
+ import { AuditView, type AuditData } from "../ui/views/AuditView.js";
12
+
13
+ /** Judges every file in scope as if it had just been written, and reports what breaks which rule. */
14
+ export const runAudit = async (argv: string[]): Promise<number> => {
15
+ const { values, positionals } = parseArgs({
16
+ args: argv,
17
+ allowPositionals: true,
18
+ options: {
19
+ concurrency: { type: "string", default: "3" },
20
+ "max-files": { type: "string" },
21
+ all: { type: "boolean", default: false },
22
+ json: { type: "boolean", default: false },
23
+ },
24
+ });
25
+ const root = findRepoRoot(process.cwd());
26
+ if (!hasApiKey(root)) throw new AbideError("NO_API_KEY", NO_KEY_HINT);
27
+ if (!isGitRepo(root))
28
+ throw new AbideError(
29
+ "GIT_UNAVAILABLE",
30
+ "audit walks the files git knows about, and this is not a git repository",
31
+ );
32
+ const loaded = loadRules(root);
33
+ if (loaded.rules.length === 0)
34
+ throw new AbideError(
35
+ "RUBRIC_MISSING",
36
+ "no rubric here or in ~/.abide; run abide compile first",
37
+ );
38
+
39
+ const candidates = listRepoFiles(root, positionals);
40
+ const { files: inScope, skipped } = auditableFiles(root, candidates, loaded.rules);
41
+ const cap = values["max-files"] === undefined ? Infinity : Number(values["max-files"]);
42
+ const files = inScope.slice(0, cap);
43
+ const concurrency = Math.max(1, Number(values.concurrency));
44
+
45
+ const run = async (progress: (label: string) => void): Promise<AuditData> => {
46
+ const started = performance.now();
47
+ const results = await auditFiles(
48
+ root,
49
+ files,
50
+ loaded.rules,
51
+ loaded.thresholds,
52
+ concurrency,
53
+ (done, total, spend) => {
54
+ progress(`${done} of ${total} files ${"·"} about ${usd(spend)}`);
55
+ },
56
+ );
57
+ return {
58
+ root,
59
+ files: files.length,
60
+ skipped,
61
+ results,
62
+ tallies: tallyByRule(results),
63
+ spendUsd: results.reduce((s, r) => s + r.costUsd, 0),
64
+ elapsedMs: performance.now() - started,
65
+ all: values.all,
66
+ };
67
+ };
68
+
69
+ if (values.json) {
70
+ const data = await run(() => {});
71
+ say(
72
+ JSON.stringify({
73
+ root,
74
+ files: data.files,
75
+ skipped: data.skipped,
76
+ spendUsd: data.spendUsd,
77
+ elapsedMs: data.elapsedMs,
78
+ byRule: data.tallies,
79
+ byFile: data.results,
80
+ }),
81
+ );
82
+ return data.tallies.some((t) => t.broken.length > 0) ? 1 : 0;
83
+ }
84
+ return showLive<AuditData>({
85
+ header: Header({
86
+ command: "audit",
87
+ where: root,
88
+ note:
89
+ files.length === 0
90
+ ? "no files in scope of any edit-phase rule"
91
+ : `${files.length} files in scope, ${concurrency} at a time`,
92
+ }),
93
+ run,
94
+ done: (data) => AuditView({ data }),
95
+ code: (data) => (data.tallies.some((t) => t.broken.length > 0) ? 1 : 0),
96
+ });
97
+ };
@@ -0,0 +1,226 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { AbideError } from "@coldtea/abide-schema";
5
+ import { runCheck } from "../lib/checkRunner.js";
6
+ import { EDIT_CHECK_TIMEOUT_MS, TURN_CHECK_TIMEOUT_MS } from "../lib/constants.js";
7
+ import { readEvents } from "../lib/events.js";
8
+ import { hasApiKey, NO_KEY_HINT } from "../lib/credentials.js";
9
+ import { loadRules } from "../lib/loadRules.js";
10
+ import { hookScriptPath } from "../lib/packageRoot.js";
11
+ import { findRepoRoot } from "../lib/paths.js";
12
+ import { clearTurn, turnDir } from "../lib/session.js";
13
+ import { median, percentile, say } from "../lib/ui.js";
14
+ import { Header } from "../ui/components/Header.js";
15
+ import { showLive } from "../ui/render.js";
16
+ import { BenchView, type BenchData, type BenchRow } from "../ui/views/BenchView.js";
17
+
18
+ const smallDiff = `@@ -10,6 +10,12 @@ export const loadUser = async (id: string) => {
19
+ const row = await db.users.find(id);
20
+ + if (!row) {
21
+ + return null;
22
+ + }
23
+ + const name = row.name.trim();
24
+ + const email = row.email.toLowerCase();
25
+ + const createdAt = new Date(row.created_at);
26
+ + const plan = row.plan ?? "free";
27
+ + const seats = Number(row.seats ?? 1);
28
+ + return { id, name, email, createdAt, plan, seats };
29
+ - return row;
30
+ };`;
31
+
32
+ const largeDiff = [
33
+ "@@ -1,4 +1,54 @@",
34
+ '+import { z } from "zod";',
35
+ "+",
36
+ "+const inputSchema = z.object({ id: z.string(), limit: z.number().int().min(1).max(100) });",
37
+ "+",
38
+ ...Array.from(
39
+ { length: 46 },
40
+ (_, i) => `+ const step${i} = compute(step${Math.max(0, i - 1)}, ${i});`,
41
+ ),
42
+ "+export const handler = async (req: Request) => {",
43
+ "+ const input = inputSchema.parse(await req.json());",
44
+ "+ return Response.json({ ok: true, input });",
45
+ "+};",
46
+ ].join("\n");
47
+
48
+ const SAMPLE = "src/abide-bench-sample.ts";
49
+
50
+ const spawnHook = (script: string, name: string, payload: unknown): number => {
51
+ const started = performance.now();
52
+ spawnSync("node", [script, name], {
53
+ input: JSON.stringify(payload),
54
+ encoding: "utf8",
55
+ timeout: 30_000,
56
+ });
57
+ return performance.now() - started;
58
+ };
59
+
60
+ const times = async (n: number, fn: () => Promise<number>): Promise<number[]> => {
61
+ const out: number[] = [];
62
+ for (let i = 0; i < n; i += 1) out.push(await fn());
63
+ return out;
64
+ };
65
+
66
+ const row = (what: string, values: number[]): BenchRow => ({
67
+ what,
68
+ median: median(values) ?? 0,
69
+ p90: percentile(values, 90) ?? 0,
70
+ });
71
+
72
+ /** Measures the check path on this machine: process start, Jev latency, tokens, spend, and real sessions. */
73
+ export const runBench = async (argv: string[]): Promise<number> => {
74
+ const { values } = parseArgs({
75
+ args: argv,
76
+ options: { runs: { type: "string", default: "5" }, json: { type: "boolean", default: false } },
77
+ });
78
+ const runs = Math.max(1, Number(values.runs));
79
+ const root = findRepoRoot(process.cwd());
80
+ if (!hasApiKey(root)) throw new AbideError("NO_API_KEY", NO_KEY_HINT);
81
+ const loaded = loadRules(root);
82
+ if (loaded.rules.length === 0)
83
+ throw new AbideError("RUBRIC_MISSING", "no rubric to bench with; run abide compile first");
84
+ const script = hookScriptPath();
85
+ const sample = path.join(root, SAMPLE);
86
+ const activeModelRules = loaded.rules.filter(
87
+ (r) => r.status === "active" && r.check.type === "model",
88
+ ).length;
89
+
90
+ const run = async (progress: (label: string) => void): Promise<BenchData> => {
91
+ progress("hook process start");
92
+ const startup = await times(runs, async () =>
93
+ spawnHook(script, "session-start", { hook_event_name: "nope" }),
94
+ );
95
+
96
+ const edit = async (diff: string) => {
97
+ const out = await runCheck({
98
+ phase: "edit",
99
+ fileDiffs: [{ file: SAMPLE, text: diff }],
100
+ task: "Add a user loader",
101
+ rules: loaded.rules,
102
+ thresholds: loaded.thresholds,
103
+ timeoutMs: EDIT_CHECK_TIMEOUT_MS,
104
+ retries: 2,
105
+ });
106
+ return {
107
+ latency: out.modelLatencyMs,
108
+ tokens: out.usage.inputTokens ?? 0,
109
+ cost: out.usage.costUsd ?? 0,
110
+ rules: out.modelRules.length,
111
+ };
112
+ };
113
+ progress("warming the connection");
114
+ const warm = await edit(smallDiff);
115
+ let i = 0;
116
+ const small = await times(runs, async () => {
117
+ progress(`edit check, small diff ${(i += 1)}/${runs}`);
118
+ return (await edit(smallDiff)).latency;
119
+ });
120
+ i = 0;
121
+ const large = await times(runs, async () => {
122
+ progress(`edit check, large diff ${(i += 1)}/${runs}`);
123
+ return (await edit(largeDiff)).latency;
124
+ });
125
+ const largeOnce = await edit(largeDiff);
126
+
127
+ const turnCheck = () =>
128
+ runCheck({
129
+ phase: "turn",
130
+ fileDiffs: [{ file: SAMPLE, text: largeDiff }],
131
+ task: "Add a user loader",
132
+ rules: loaded.rules,
133
+ thresholds: loaded.thresholds,
134
+ timeoutMs: TURN_CHECK_TIMEOUT_MS,
135
+ retries: 2,
136
+ });
137
+ const turnOut = await turnCheck();
138
+ i = 0;
139
+ const turn =
140
+ turnOut.modelRules.length === 0
141
+ ? []
142
+ : await times(runs, async () => {
143
+ progress(`turn check ${(i += 1)}/${runs}`);
144
+ return (await turnCheck()).modelLatencyMs;
145
+ });
146
+
147
+ i = 0;
148
+ const fullHook = await times(runs, async () => {
149
+ progress(`whole PostToolUse hook ${(i += 1)}/${runs}`);
150
+ return spawnHook(script, "post-tool-use", {
151
+ session_id: "abide-bench",
152
+ cwd: root,
153
+ hook_event_name: "PostToolUse",
154
+ tool_name: "Write",
155
+ tool_input: {
156
+ file_path: sample,
157
+ content: largeDiff
158
+ .split("\n")
159
+ .slice(1)
160
+ .map((l) => l.slice(1))
161
+ .join("\n"),
162
+ },
163
+ tool_response: {
164
+ type: "create",
165
+ filePath: sample,
166
+ originalFile: null,
167
+ structuredPatch: [],
168
+ },
169
+ });
170
+ });
171
+ clearTurn(turnDir("abide-bench", undefined));
172
+
173
+ const sessions = new Map<string, { edits: number; latency: number; cost: number }>();
174
+ for (const e of readEvents(root)) {
175
+ if (e.kind !== "check" || e.sessionId === undefined || e.sessionId === "abide-bench")
176
+ continue;
177
+ const s = sessions.get(e.sessionId) ?? { edits: 0, latency: 0, cost: 0 };
178
+ s.edits += 1;
179
+ s.latency += e.latencyMs;
180
+ s.cost += e.usage?.costUsd ?? 0;
181
+ sessions.set(e.sessionId, s);
182
+ }
183
+ const all = [...sessions.values()];
184
+ return {
185
+ root,
186
+ runs,
187
+ activeModelRules,
188
+ rows: [
189
+ row("hook process start, no network", startup),
190
+ row(`edit check, small diff, ${warm.rules} rules`, small),
191
+ row(`edit check, large diff, ${largeOnce.rules} rules`, large),
192
+ ...(turn.length > 0 ? [row(`turn check, ${turnOut.modelRules.length} rules`, turn)] : []),
193
+ row("whole PostToolUse hook, large diff", fullHook),
194
+ ],
195
+ tokens: { small: warm.tokens, large: largeOnce.tokens },
196
+ cost: { small: warm.cost, large: largeOnce.cost },
197
+ perTurn: {
198
+ latencyMs: 15 * (median(fullHook) ?? 0) + (median(turn) ?? 0),
199
+ costUsd: 15 * largeOnce.cost + (turnOut.usage.costUsd ?? 0),
200
+ },
201
+ sessions:
202
+ all.length === 0
203
+ ? null
204
+ : {
205
+ count: all.length,
206
+ medianChecks: median(all.map((s) => s.edits)) ?? 0,
207
+ medianLatencyMs: median(all.map((s) => s.latency)) ?? 0,
208
+ medianCostUsd: median(all.map((s) => s.cost)) ?? 0,
209
+ },
210
+ };
211
+ };
212
+
213
+ if (values.json) {
214
+ say(JSON.stringify(await run(() => {})));
215
+ return 0;
216
+ }
217
+ return showLive<BenchData>({
218
+ header: Header({
219
+ command: "bench",
220
+ where: root,
221
+ note: `${activeModelRules} active model rules, ${runs} runs each`,
222
+ }),
223
+ run,
224
+ done: (data) => BenchView({ data }),
225
+ });
226
+ };