harnery 0.45.1 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/dist/commander.d.ts +13 -0
  2. package/dist/commander.d.ts.map +1 -1
  3. package/dist/commands/admission.d.ts.map +1 -1
  4. package/dist/commands/admission.js +214 -27
  5. package/dist/commands/agents.d.ts +4 -0
  6. package/dist/commands/agents.d.ts.map +1 -1
  7. package/dist/commands/agents.js +102 -25
  8. package/dist/commands/browse.d.ts +3 -0
  9. package/dist/commands/browse.d.ts.map +1 -1
  10. package/dist/commands/browse.js +172 -8
  11. package/dist/commands/fetch.js +1 -0
  12. package/dist/commands/ledger-v3.d.ts.map +1 -1
  13. package/dist/commands/ledger-v3.js +8 -2
  14. package/dist/commands/presence.d.ts.map +1 -1
  15. package/dist/commands/presence.js +40 -0
  16. package/dist/commands/qa-run.d.ts.map +1 -1
  17. package/dist/commands/qa-run.js +4 -3
  18. package/dist/commands/review-pack.d.ts.map +1 -1
  19. package/dist/commands/review-pack.js +33 -6
  20. package/dist/commands/workflow.d.ts.map +1 -1
  21. package/dist/commands/workflow.js +6 -45
  22. package/dist/core/agents/coord-client.d.ts.map +1 -1
  23. package/dist/core/agents/coord-client.js +0 -18
  24. package/dist/core/agents/index.d.ts +1 -0
  25. package/dist/core/agents/index.d.ts.map +1 -1
  26. package/dist/core/agents/index.js +1 -0
  27. package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
  28. package/dist/core/agents/live-authority-v3.js +11 -4
  29. package/dist/core/agents/render/session-context.d.ts +0 -6
  30. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  31. package/dist/core/agents/render/session-context.js +1 -6
  32. package/dist/core/agents/session-events.d.ts.map +1 -1
  33. package/dist/core/agents/session-events.js +12 -4
  34. package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
  35. package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
  36. package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
  37. package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
  38. package/dist/core/agents/state/live-coordination-view.js +6 -2
  39. package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
  40. package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
  41. package/dist/core/agents/state/parent-reference-contract.js +1 -0
  42. package/dist/core/agents/state/parent-reference.d.ts +18 -0
  43. package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
  44. package/dist/core/agents/state/parent-reference.js +119 -0
  45. package/dist/core/agents/state/pidmap.js +2 -2
  46. package/dist/core/agents/state/proc-start.d.ts +20 -31
  47. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  48. package/dist/core/agents/state/proc-start.js +37 -70
  49. package/dist/core/agents/state/service-process.d.ts +61 -0
  50. package/dist/core/agents/state/service-process.d.ts.map +1 -0
  51. package/dist/core/agents/state/service-process.js +143 -0
  52. package/dist/core/config.d.ts +39 -0
  53. package/dist/core/config.d.ts.map +1 -1
  54. package/dist/core/config.js +63 -0
  55. package/dist/core/context/index.d.ts +6 -4
  56. package/dist/core/context/index.d.ts.map +1 -1
  57. package/dist/core/context/index.js +194 -256
  58. package/dist/core/context/observation.d.ts +9 -9
  59. package/dist/core/context/observation.d.ts.map +1 -1
  60. package/dist/core/context/observation.js +37 -6
  61. package/dist/core/diagnostics/path-identity.d.ts +1 -1
  62. package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
  63. package/dist/core/diagnostics/path-identity.js +51 -6
  64. package/dist/core/events/v3/canonical.js +43 -14
  65. package/dist/core/events/v3/index.d.ts +1 -1
  66. package/dist/core/events/v3/index.d.ts.map +1 -1
  67. package/dist/core/events/v3/index.js +1 -1
  68. package/dist/core/events/v3/producers/recorder.d.ts +34 -0
  69. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  70. package/dist/core/events/v3/producers/recorder.js +117 -13
  71. package/dist/core/events/v3/reader.d.ts.map +1 -1
  72. package/dist/core/events/v3/reader.js +53 -13
  73. package/dist/core/events/v3/runtime-build.d.ts +14 -0
  74. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  75. package/dist/core/events/v3/runtime-build.js +44 -0
  76. package/dist/core/events/v3/validate.d.ts.map +1 -1
  77. package/dist/core/events/v3/validate.js +36 -1
  78. package/dist/core/governor/index.d.ts +2 -1
  79. package/dist/core/governor/index.d.ts.map +1 -1
  80. package/dist/core/governor/index.js +2 -1
  81. package/dist/core/governor/service-read.d.ts +3 -10
  82. package/dist/core/governor/service-read.d.ts.map +1 -1
  83. package/dist/core/governor/service-read.js +10 -94
  84. package/dist/core/governor/service.d.ts +3 -7
  85. package/dist/core/governor/service.d.ts.map +1 -1
  86. package/dist/core/governor/service.js +17 -182
  87. package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
  88. package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
  89. package/dist/core/hooks/adapter/native-parent.js +91 -0
  90. package/dist/core/hooks/cli.js +79 -4
  91. package/dist/core/hooks/resolve/owner.d.ts +0 -11
  92. package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
  93. package/dist/core/hooks/resolve/owner.js +2 -25
  94. package/dist/core/hooks/shell-admission.d.ts +84 -0
  95. package/dist/core/hooks/shell-admission.d.ts.map +1 -0
  96. package/dist/core/hooks/shell-admission.js +622 -0
  97. package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
  98. package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
  99. package/dist/core/hooks/shell-commit-tree.js +104 -0
  100. package/dist/core/inbox/contract.d.ts +12 -0
  101. package/dist/core/inbox/contract.d.ts.map +1 -1
  102. package/dist/core/inbox/contract.js +16 -0
  103. package/dist/core/inbox/peer-messages.d.ts +41 -16
  104. package/dist/core/inbox/peer-messages.d.ts.map +1 -1
  105. package/dist/core/inbox/peer-messages.js +372 -15
  106. package/dist/core/inbox/service.d.ts +8 -1
  107. package/dist/core/inbox/service.d.ts.map +1 -1
  108. package/dist/core/inbox/service.js +83 -3
  109. package/dist/core/inbox/targets.d.ts +17 -3
  110. package/dist/core/inbox/targets.d.ts.map +1 -1
  111. package/dist/core/inbox/targets.js +77 -2
  112. package/dist/core/presence/git.d.ts +18 -12
  113. package/dist/core/presence/git.d.ts.map +1 -1
  114. package/dist/core/presence/git.js +32 -38
  115. package/dist/core/presence/index.d.ts +2 -2
  116. package/dist/core/presence/index.d.ts.map +1 -1
  117. package/dist/core/presence/index.js +8 -6
  118. package/dist/core/presence/mail.d.ts +133 -0
  119. package/dist/core/presence/mail.d.ts.map +1 -0
  120. package/dist/core/presence/mail.js +464 -0
  121. package/dist/core/resources/sampler.js +2 -2
  122. package/dist/core/resources/service-status.d.ts +2 -14
  123. package/dist/core/resources/service-status.d.ts.map +1 -1
  124. package/dist/core/resources/service-status.js +3 -23
  125. package/dist/core/resources/service.d.ts +3 -7
  126. package/dist/core/resources/service.d.ts.map +1 -1
  127. package/dist/core/resources/service.js +12 -101
  128. package/dist/core/resources/status.d.ts.map +1 -1
  129. package/dist/core/resources/status.js +3 -2
  130. package/dist/core/semantic/contract.d.ts +5 -5
  131. package/dist/core/semantic/service.d.ts.map +1 -1
  132. package/dist/core/semantic/service.js +9 -2
  133. package/dist/core/servers/index.d.ts +0 -2
  134. package/dist/core/servers/index.d.ts.map +1 -1
  135. package/dist/core/servers/index.js +3 -18
  136. package/dist/core/storage/modes.d.ts +4 -0
  137. package/dist/core/storage/modes.d.ts.map +1 -1
  138. package/dist/core/storage/modes.js +4 -0
  139. package/dist/core/storage/segments.d.ts.map +1 -1
  140. package/dist/core/storage/segments.js +5 -4
  141. package/dist/core/supervisor/service.d.ts +4 -7
  142. package/dist/core/supervisor/service.d.ts.map +1 -1
  143. package/dist/core/supervisor/service.js +23 -122
  144. package/dist/core/supervisor/services.js +2 -2
  145. package/dist/core/supervisor/status.d.ts +3 -17
  146. package/dist/core/supervisor/status.d.ts.map +1 -1
  147. package/dist/core/supervisor/status.js +7 -70
  148. package/dist/core/workflow/engine.js +2 -1
  149. package/dist/core/workflow/index.d.ts +3 -4
  150. package/dist/core/workflow/index.d.ts.map +1 -1
  151. package/dist/core/workflow/index.js +2 -2
  152. package/dist/core/workflow/proof.d.ts.map +1 -1
  153. package/dist/core/workflow/proof.js +30 -74
  154. package/dist/core/workflow/resume-lock.d.ts +17 -0
  155. package/dist/core/workflow/resume-lock.d.ts.map +1 -0
  156. package/dist/core/workflow/resume-lock.js +158 -0
  157. package/dist/core/workflow/run-state.d.ts +0 -21
  158. package/dist/core/workflow/run-state.d.ts.map +1 -1
  159. package/dist/core/workflow/run-state.js +3 -174
  160. package/dist/core/workflow/workspaces/leases.d.ts +4 -0
  161. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  162. package/dist/core/workflow/workspaces/leases.js +1 -1
  163. package/dist/core/workflow/workspaces/local-git.js +91 -210
  164. package/dist/core/workflow/workspaces/ownership.d.ts +0 -7
  165. package/dist/core/workflow/workspaces/ownership.d.ts.map +1 -1
  166. package/dist/core/workflow/workspaces/ownership.js +3 -130
  167. package/dist/core/workflow/workspaces/reclaim.d.ts +4 -7
  168. package/dist/core/workflow/workspaces/reclaim.d.ts.map +1 -1
  169. package/dist/core/workflow/workspaces/reclaim.js +0 -4
  170. package/dist/core/workflow/workspaces/recovery.d.ts +1 -1
  171. package/dist/core/workflow/workspaces/recovery.d.ts.map +1 -1
  172. package/dist/core/workflow/workspaces/recovery.js +1 -1
  173. package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
  174. package/dist/core/workflow/workspaces/state.js +1 -4
  175. package/dist/lib/admission-history.d.ts +96 -0
  176. package/dist/lib/admission-history.d.ts.map +1 -0
  177. package/dist/lib/admission-history.js +196 -0
  178. package/dist/lib/admission.d.ts +56 -7
  179. package/dist/lib/admission.d.ts.map +1 -1
  180. package/dist/lib/admission.js +161 -19
  181. package/dist/lib/browser/client.d.ts +18 -0
  182. package/dist/lib/browser/client.d.ts.map +1 -1
  183. package/dist/lib/browser/client.js +100 -12
  184. package/dist/lib/browser/content-checks.d.ts +3 -1
  185. package/dist/lib/browser/content-checks.d.ts.map +1 -1
  186. package/dist/lib/browser/content-checks.js +10 -2
  187. package/dist/lib/browser/critique.d.ts +32 -0
  188. package/dist/lib/browser/critique.d.ts.map +1 -1
  189. package/dist/lib/browser/critique.js +72 -3
  190. package/dist/lib/browser/geometry.d.ts +2 -0
  191. package/dist/lib/browser/geometry.d.ts.map +1 -1
  192. package/dist/lib/browser/geometry.js +61 -15
  193. package/dist/lib/browser/index.d.ts +1 -1
  194. package/dist/lib/browser/index.d.ts.map +1 -1
  195. package/dist/lib/browser/index.js +1 -1
  196. package/dist/lib/browser/page-review-contracts.d.ts +6 -1
  197. package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
  198. package/dist/lib/browser/page-review-contracts.js +6 -1
  199. package/dist/lib/browser/page-review-judge.d.ts +3 -1
  200. package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
  201. package/dist/lib/browser/page-review-judge.js +41 -11
  202. package/dist/lib/browser/page-review-pack.d.ts +3 -0
  203. package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
  204. package/dist/lib/browser/page-review-pack.js +5 -2
  205. package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
  206. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
  207. package/dist/lib/browser/qa-run-contracts.js +11 -4
  208. package/dist/lib/browser/qa-run.d.ts.map +1 -1
  209. package/dist/lib/browser/qa-run.js +73 -16
  210. package/dist/lib/browser/runts.d.ts +4 -2
  211. package/dist/lib/browser/runts.d.ts.map +1 -1
  212. package/dist/lib/browser/runts.js +84 -33
  213. package/dist/lib/browser/windows-chrome.d.ts +14 -0
  214. package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
  215. package/dist/lib/browser/windows-chrome.js +42 -0
  216. package/dist/lib/browser/windows-login-sync.d.ts +110 -0
  217. package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
  218. package/dist/lib/browser/windows-login-sync.js +171 -0
  219. package/dist/lib/cookies/client.d.ts +30 -5
  220. package/dist/lib/cookies/client.d.ts.map +1 -1
  221. package/dist/lib/cookies/client.js +55 -10
  222. package/dist/lib/cookies/index.d.ts +1 -1
  223. package/dist/lib/cookies/index.d.ts.map +1 -1
  224. package/dist/lib/cookies/index.js +1 -1
  225. package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
  226. package/dist/lib/docs-metadata-sync.js +41 -2
  227. package/dist/lib/durable-job.d.ts +8 -0
  228. package/dist/lib/durable-job.d.ts.map +1 -1
  229. package/dist/lib/durable-job.js +10 -0
  230. package/dist/lib/http/client.d.ts +9 -8
  231. package/dist/lib/http/client.d.ts.map +1 -1
  232. package/dist/lib/http/client.js +44 -2
  233. package/dist/lib/instructions/templates.js +4 -4
  234. package/package.json +2 -2
  235. package/src/commander.ts +13 -0
  236. package/src/commands/admission.ts +272 -29
  237. package/src/commands/agents.ts +132 -27
  238. package/src/commands/browse.ts +208 -6
  239. package/src/commands/fetch.ts +1 -0
  240. package/src/commands/ledger-v3.ts +8 -2
  241. package/src/commands/presence.ts +49 -0
  242. package/src/commands/qa-run.ts +4 -3
  243. package/src/commands/review-pack.ts +41 -5
  244. package/src/commands/workflow.ts +8 -58
  245. package/src/core/agents/coord-client.ts +0 -20
  246. package/src/core/agents/index.ts +6 -0
  247. package/src/core/agents/live-authority-v3.ts +11 -4
  248. package/src/core/agents/render/session-context.ts +1 -7
  249. package/src/core/agents/session-events.ts +18 -3
  250. package/src/core/agents/state/heartbeat-reader.ts +3 -0
  251. package/src/core/agents/state/live-coordination-view.ts +13 -6
  252. package/src/core/agents/state/parent-reference-contract.ts +14 -0
  253. package/src/core/agents/state/parent-reference.ts +179 -0
  254. package/src/core/agents/state/pidmap.ts +2 -2
  255. package/src/core/agents/state/proc-start.ts +41 -69
  256. package/src/core/agents/state/service-process.ts +217 -0
  257. package/src/core/config.ts +85 -0
  258. package/src/core/context/index.ts +207 -272
  259. package/src/core/context/observation.ts +40 -9
  260. package/src/core/diagnostics/path-identity.ts +52 -6
  261. package/src/core/events/v3/canonical.ts +46 -13
  262. package/src/core/events/v3/index.ts +3 -0
  263. package/src/core/events/v3/producers/recorder.ts +151 -13
  264. package/src/core/events/v3/reader.ts +55 -11
  265. package/src/core/events/v3/runtime-build.ts +62 -0
  266. package/src/core/events/v3/validate.ts +42 -1
  267. package/src/core/governor/index.ts +1 -2
  268. package/src/core/governor/service-read.ts +15 -114
  269. package/src/core/governor/service.ts +36 -220
  270. package/src/core/hooks/adapter/native-parent.ts +109 -0
  271. package/src/core/hooks/cli.ts +83 -4
  272. package/src/core/hooks/resolve/owner.ts +2 -25
  273. package/src/core/hooks/shell-admission.ts +634 -0
  274. package/src/core/hooks/shell-commit-tree.ts +102 -0
  275. package/src/core/inbox/contract.ts +18 -0
  276. package/src/core/inbox/peer-messages.ts +501 -15
  277. package/src/core/inbox/service.ts +108 -3
  278. package/src/core/inbox/targets.ts +94 -3
  279. package/src/core/presence/git.ts +45 -37
  280. package/src/core/presence/index.ts +16 -8
  281. package/src/core/presence/mail.ts +620 -0
  282. package/src/core/resources/sampler.ts +3 -2
  283. package/src/core/resources/service-status.ts +6 -35
  284. package/src/core/resources/service.ts +23 -133
  285. package/src/core/resources/status.ts +3 -2
  286. package/src/core/semantic/service.ts +8 -2
  287. package/src/core/servers/index.ts +3 -23
  288. package/src/core/storage/modes.ts +6 -0
  289. package/src/core/storage/segments.ts +5 -4
  290. package/src/core/supervisor/service.ts +33 -155
  291. package/src/core/supervisor/services.ts +2 -2
  292. package/src/core/supervisor/status.ts +10 -85
  293. package/src/core/workflow/engine.ts +1 -1
  294. package/src/core/workflow/index.ts +2 -13
  295. package/src/core/workflow/proof.ts +35 -83
  296. package/src/core/workflow/resume-lock.ts +179 -0
  297. package/src/core/workflow/run-state.ts +3 -207
  298. package/src/core/workflow/workspaces/leases.ts +1 -1
  299. package/src/core/workflow/workspaces/local-git.ts +120 -239
  300. package/src/core/workflow/workspaces/ownership.ts +3 -158
  301. package/src/core/workflow/workspaces/reclaim.ts +3 -9
  302. package/src/core/workflow/workspaces/recovery.ts +1 -1
  303. package/src/core/workflow/workspaces/state.ts +1 -5
  304. package/src/lib/admission-history.ts +267 -0
  305. package/src/lib/admission.ts +217 -19
  306. package/src/lib/browser/client.ts +147 -35
  307. package/src/lib/browser/content-checks.ts +14 -3
  308. package/src/lib/browser/critique.ts +113 -3
  309. package/src/lib/browser/geometry.ts +63 -14
  310. package/src/lib/browser/index.ts +5 -0
  311. package/src/lib/browser/page-review-contracts.ts +6 -1
  312. package/src/lib/browser/page-review-judge.ts +58 -13
  313. package/src/lib/browser/page-review-pack.ts +13 -2
  314. package/src/lib/browser/qa-run-contracts.ts +30 -4
  315. package/src/lib/browser/qa-run.ts +78 -17
  316. package/src/lib/browser/runts.ts +85 -37
  317. package/src/lib/browser/windows-chrome.ts +50 -0
  318. package/src/lib/browser/windows-login-sync.ts +275 -0
  319. package/src/lib/cookies/client.ts +62 -10
  320. package/src/lib/cookies/index.ts +3 -0
  321. package/src/lib/docs-metadata-sync.ts +44 -2
  322. package/src/lib/durable-job.ts +15 -0
  323. package/src/lib/http/client.ts +63 -1
  324. package/src/lib/instructions/templates.ts +4 -4
  325. package/dist/core/agents/state/process-handle.d.ts +0 -26
  326. package/dist/core/agents/state/process-handle.d.ts.map +0 -1
  327. package/dist/core/agents/state/process-handle.js +0 -121
  328. package/dist/core/workflow/recovery-audit.d.ts +0 -30
  329. package/dist/core/workflow/recovery-audit.d.ts.map +0 -1
  330. package/dist/core/workflow/recovery-audit.js +0 -116
  331. package/src/core/agents/state/process-handle.ts +0 -165
  332. package/src/core/workflow/recovery-audit.ts +0 -181
@@ -0,0 +1,634 @@
1
+ /**
2
+ * Admission guard for heavy shell checks. A whole-project typecheck, a full
3
+ * test suite, or a production build can each take gigabytes of memory, and
4
+ * several agents starting them at once on one machine can exhaust it. Asking
5
+ * agents in prose to wrap those commands in `admission run` does not work: the
6
+ * rule has to be read and remembered on every call. This guard rewrites the
7
+ * agent's shell command at PreToolUse instead, so each heavy check waits for a
8
+ * slot on a machine-wide admission resource.
9
+ *
10
+ * Enabled only when the project config names a resource
11
+ * (`admission.guard.resource`). It finds each simple command that is a heavy
12
+ * check and inserts `<bin> admission run --resource <name> --label 'agent:
13
+ * ...' --` in front of it, after any leading VAR=value assignments. The rest
14
+ * of the command (cd, pipes, redirects, && chains) is left byte for byte.
15
+ * Heavy means:
16
+ * - tsc, however launched (tsc, npx/bunx/bun x/pnpm exec/yarn tsc,
17
+ * node_modules/.bin/tsc, node .../typescript/bin/tsc)
18
+ * - next build
19
+ * - bun test with no file or directory named (a full suite)
20
+ * - the package scripts test / test:* (with no further positional
21
+ * argument), build, typecheck, type-check, typecheck:*, check-types, tsc,
22
+ * run through bun run, npm, pnpm, or yarn
23
+ * - bash/sh -c '<script>' whose script contains one of the above
24
+ * Watch modes, --help/--version, and named-file test runs are left alone.
25
+ *
26
+ * Passes through untouched: non-shell tools; a command that already runs
27
+ * `admission run` or `heavy-slot`; HARNERY_ADMISSION_SKIP=1 in the command or
28
+ * the hook's environment; HARNERY_ADMISSION_HELD in the hook's environment
29
+ * (the session already holds a slot); text inside quotes, heredocs, and
30
+ * comments. Any parse failure means no rewrite.
31
+ */
32
+
33
+ const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*(\[[^\]]*\])?\+?=/;
34
+ const RESERVED = new Set([
35
+ "if",
36
+ "then",
37
+ "else",
38
+ "elif",
39
+ "do",
40
+ "while",
41
+ "until",
42
+ "!",
43
+ "{",
44
+ "}",
45
+ "fi",
46
+ "done",
47
+ "esac",
48
+ ]);
49
+ const SEPARATORS = new Set([";", "&&", "||", "|", "|&", "&", "(", ")", ";;", ";&", ";;&"]);
50
+ const REDIRECTS = ["<<<", "<<-", "&>>", "<<", ">>", ">&", "<&", "&>", "<>", ">|", ">", "<"];
51
+ const OPERATORS = [...REDIRECTS, ";;&", ";;", ";&", "&&", "||", "|&"];
52
+ const HEAVY_SCRIPTS = /^(build|typecheck|type-check|typecheck:.*|check-types|tsc)$/;
53
+ const TEST_SCRIPTS = /^(test|test:.*)$/;
54
+ const SHELLS = new Set(["bash", "sh", "dash", "zsh"]);
55
+ const SHELL_C_FLAG = /^-[A-Za-z]*c[A-Za-z]*$/;
56
+ const WATCH_FLAGS = new Set(["-w", "--watch", "--watchAll"]);
57
+ const TSC_INFO_FLAGS = new Set([
58
+ "-v",
59
+ "--version",
60
+ "-h",
61
+ "--help",
62
+ "--init",
63
+ "--showConfig",
64
+ "--all",
65
+ "--listFilesOnly",
66
+ ]);
67
+ const BUN_BUILTINS = new Set([
68
+ "test",
69
+ "run",
70
+ "x",
71
+ "build",
72
+ "install",
73
+ "i",
74
+ "add",
75
+ "remove",
76
+ "rm",
77
+ "update",
78
+ "link",
79
+ "unlink",
80
+ "pm",
81
+ "init",
82
+ "create",
83
+ "upgrade",
84
+ "outdated",
85
+ "publish",
86
+ "audit",
87
+ "info",
88
+ "repl",
89
+ "exec",
90
+ "patch",
91
+ ]);
92
+ const BUN_RUN_VALUE_FLAGS = new Set([
93
+ "--cwd",
94
+ "--filter",
95
+ "-F",
96
+ "--elide-lines",
97
+ "--env-file",
98
+ "--config",
99
+ "-c",
100
+ "--preload",
101
+ "-r",
102
+ ]);
103
+ const BUN_TEST_VALUE_FLAGS = new Set([
104
+ "-t",
105
+ "--test-name-pattern",
106
+ "--timeout",
107
+ "--rerun-each",
108
+ "--preload",
109
+ "-r",
110
+ "--coverage-reporter",
111
+ "--coverage-dir",
112
+ "--reporter",
113
+ "--reporter-outfile",
114
+ "--max-concurrency",
115
+ "--seed",
116
+ "--cwd",
117
+ "--env-file",
118
+ "--config",
119
+ "-c",
120
+ "--path-ignore-patterns",
121
+ ]);
122
+ const NPM_VALUE_FLAGS = new Set([
123
+ "-w",
124
+ "--workspace",
125
+ "--prefix",
126
+ "-C",
127
+ "--dir",
128
+ "--filter",
129
+ "-F",
130
+ "--cwd",
131
+ ]);
132
+ const RUNNER_VALUE_FLAGS = new Set(["-p", "--package", "-c", "--call"]);
133
+
134
+ export const ADMISSION_SKIP_PREFIX = "HARNERY_ADMISSION_SKIP=1";
135
+ /** Claude Code's maximum Bash timeout; a queued wait counts against it. */
136
+ export const BASH_TIMEOUT_MAX_MS = 600_000;
137
+ const LABEL_MAX = 70;
138
+
139
+ interface Word {
140
+ value: string;
141
+ start: number;
142
+ end: number;
143
+ quoted: boolean;
144
+ }
145
+
146
+ type Lexeme = { kind: "word"; word: Word } | { kind: "op"; op: string; start: number };
147
+
148
+ function baseName(value: string): string {
149
+ return value.split("/").at(-1) ?? value;
150
+ }
151
+
152
+ /**
153
+ * Tokens with source offsets. Quotes stay opaque, heredoc bodies and comments
154
+ * are skipped, and `$(` opens a nested command list.
155
+ */
156
+ function lex(src: string): Lexeme[] {
157
+ const out: Lexeme[] = [];
158
+ const n = src.length;
159
+ let heredocs: { end: string; tabs: boolean }[] = [];
160
+ let pendingHeredoc: string | null = null;
161
+ let i = 0;
162
+ while (i < n) {
163
+ const ch = src[i] as string;
164
+ if (ch === "\\" && src[i + 1] === "\n") {
165
+ i += 2;
166
+ continue;
167
+ }
168
+ if (ch === "\n") {
169
+ out.push({ kind: "op", op: ";", start: i });
170
+ i += 1;
171
+ for (const doc of heredocs) {
172
+ while (i < n) {
173
+ const found = src.indexOf("\n", i);
174
+ const stop = found === -1 ? n : found;
175
+ const line = src.slice(i, stop);
176
+ i = stop + 1;
177
+ if ((doc.tabs ? line.replace(/^\t+/, "") : line) === doc.end) break;
178
+ }
179
+ }
180
+ heredocs = [];
181
+ continue;
182
+ }
183
+ if (ch === " " || ch === "\t" || ch === "\r") {
184
+ i += 1;
185
+ continue;
186
+ }
187
+ if (ch === "#") {
188
+ const found = src.indexOf("\n", i);
189
+ i = found === -1 ? n : found;
190
+ continue;
191
+ }
192
+ if (ch === "$" && src.startsWith("$(", i) && !src.startsWith("$((", i)) {
193
+ out.push({ kind: "op", op: "(", start: i });
194
+ i += 2;
195
+ continue;
196
+ }
197
+ if (";|&()<>".includes(ch)) {
198
+ const op = OPERATORS.find((candidate) => src.startsWith(candidate, i)) ?? ch;
199
+ out.push({ kind: "op", op, start: i });
200
+ if (op === "<<" || op === "<<-") pendingHeredoc = op;
201
+ i += op.length;
202
+ continue;
203
+ }
204
+ const start = i;
205
+ let value = "";
206
+ let quoted = false;
207
+ while (i < n && !" \t\r\n;|&()<>".includes(src[i] as string)) {
208
+ const c = src[i] as string;
209
+ if (c === "'" || c === '"' || c === "`") {
210
+ quoted = true;
211
+ let close: number;
212
+ if (c === "'") {
213
+ close = src.indexOf(c, i + 1);
214
+ } else {
215
+ let j = i + 1;
216
+ while (j < n && src[j] !== c) j += src[j] === "\\" ? 2 : 1;
217
+ close = j < n ? j : -1;
218
+ }
219
+ if (close === -1) close = n;
220
+ value += src.slice(i + 1, close);
221
+ i = close + 1;
222
+ } else if (c === "$" && src.startsWith("$(", i)) {
223
+ // A substitution inside a word keeps the word opaque to its end.
224
+ let depth = 1;
225
+ i += 2;
226
+ while (i < n && depth) {
227
+ if (src[i] === "(") depth += 1;
228
+ else if (src[i] === ")") depth -= 1;
229
+ i += 1;
230
+ }
231
+ quoted = true;
232
+ } else if (c === "\\" && i + 1 < n) {
233
+ value += src[i + 1];
234
+ i += 2;
235
+ } else {
236
+ value += c;
237
+ i += 1;
238
+ }
239
+ }
240
+ const word: Word = { value, start, end: Math.min(i, n), quoted };
241
+ if (pendingHeredoc) {
242
+ heredocs.push({ end: word.value, tabs: pendingHeredoc === "<<-" });
243
+ pendingHeredoc = null;
244
+ }
245
+ out.push({ kind: "word", word });
246
+ }
247
+ return out;
248
+ }
249
+
250
+ /**
251
+ * Each simple command as its argument words: leading assignments, redirect
252
+ * targets, and reserved words removed.
253
+ */
254
+ function simpleCommands(tokens: Lexeme[]): Word[][] {
255
+ const commands: Word[][] = [];
256
+ let words: Word[] = [];
257
+ let prefixDone = false;
258
+ let skipNext = false;
259
+ for (let k = 0; k < tokens.length; k++) {
260
+ const token = tokens[k] as Lexeme;
261
+ const next = tokens[k + 1];
262
+ if (
263
+ token.kind === "word" &&
264
+ /^\d+$/.test(token.word.value) &&
265
+ !token.word.quoted &&
266
+ next?.kind === "op" &&
267
+ REDIRECTS.includes(next.op) &&
268
+ next.start === token.word.end
269
+ ) {
270
+ continue; // the 2 in 2>&1 is a file descriptor, not an argument
271
+ }
272
+ if (token.kind === "op") {
273
+ if (SEPARATORS.has(token.op)) {
274
+ if (words.length) commands.push(words);
275
+ words = [];
276
+ prefixDone = false;
277
+ skipNext = false;
278
+ } else {
279
+ skipNext = true;
280
+ }
281
+ continue;
282
+ }
283
+ const word = token.word;
284
+ if (skipNext) {
285
+ skipNext = false;
286
+ continue;
287
+ }
288
+ if (!words.length && !word.quoted) {
289
+ if (RESERVED.has(word.value)) continue;
290
+ if (!prefixDone && ASSIGNMENT.test(word.value)) continue;
291
+ }
292
+ prefixDone = true;
293
+ words.push(word);
294
+ }
295
+ if (words.length) commands.push(words);
296
+ return commands;
297
+ }
298
+
299
+ /** Arguments that are not flags or flag values. */
300
+ function positional(args: string[], valueFlags: Set<string> = new Set()): string[] {
301
+ const out: string[] = [];
302
+ let skip = false;
303
+ for (const arg of args) {
304
+ if (skip) {
305
+ skip = false;
306
+ continue;
307
+ }
308
+ if (arg === "--") continue;
309
+ if (arg.startsWith("-")) {
310
+ if (valueFlags.has(arg)) skip = true;
311
+ continue;
312
+ }
313
+ out.push(arg);
314
+ }
315
+ return out;
316
+ }
317
+
318
+ function intersects(args: string[], set: Set<string>): boolean {
319
+ return args.some((arg) => set.has(arg));
320
+ }
321
+
322
+ function tscHeavy(args: string[]): boolean {
323
+ return !intersects(args, TSC_INFO_FLAGS) && !intersects(args, WATCH_FLAGS);
324
+ }
325
+
326
+ function scriptHeavy(script: string, rest: string[]): boolean {
327
+ if (intersects(rest, WATCH_FLAGS)) return false;
328
+ if (HEAVY_SCRIPTS.test(script)) return true;
329
+ return TEST_SCRIPTS.test(script) && positional(rest).length === 0;
330
+ }
331
+
332
+ /** A package binary launched by npx/bunx/yarn/pnpm exec/node_modules/.bin. */
333
+ function runBin(name: string, args: string[]): boolean {
334
+ if (name === "tsc" || name === "typescript" || name.startsWith("typescript@")) {
335
+ return tscHeavy(args);
336
+ }
337
+ if (name === "next" || name.startsWith("next@")) {
338
+ return args[0] === "build" && !args.includes("-h") && !args.includes("--help");
339
+ }
340
+ return false;
341
+ }
342
+
343
+ /** Skip npx/bunx options to the binary name, then judge it. */
344
+ function runnerBin(args: string[], valueFlags: Set<string>): boolean {
345
+ let i = 0;
346
+ while (i < args.length && (args[i] as string).startsWith("-")) {
347
+ i += valueFlags.has(args[i] as string) ? 2 : 1;
348
+ }
349
+ return i < args.length && runBin(args[i] as string, args.slice(i + 1));
350
+ }
351
+
352
+ function skipFlags(args: string[], valueFlags: Set<string>): number {
353
+ let i = 0;
354
+ while (i < args.length && (args[i] as string).startsWith("-")) {
355
+ i += valueFlags.has(args[i] as string) ? 2 : 1;
356
+ }
357
+ return i;
358
+ }
359
+
360
+ /** True when this simple command is a heavy check. */
361
+ function isHeavy(words: Word[], depth = 0): boolean {
362
+ let args = words.map((word) => word.value);
363
+ // Wrappers that are binaries (timeout, nice, nohup, env, stdbuf) or keywords.
364
+ while (args.length) {
365
+ const cmd = baseName(args[0] as string);
366
+ if (cmd === "timeout") {
367
+ let j = 1;
368
+ while (j < args.length && (args[j] as string).startsWith("-")) {
369
+ j += ["-k", "--kill-after", "-s", "--signal"].includes(args[j] as string) ? 2 : 1;
370
+ }
371
+ args = args.slice(j + 1);
372
+ } else if (cmd === "nice") {
373
+ if (args[1] === "-n") args = args.slice(3);
374
+ else if (args[1] !== undefined && /^-\d+$/.test(args[1])) args = args.slice(2);
375
+ else args = args.slice(1);
376
+ } else if (["nohup", "time", "command", "exec", "stdbuf", "env"].includes(cmd)) {
377
+ if (cmd === "command" && (args[1] === "-v" || args[1] === "-V")) return false;
378
+ let j = 1;
379
+ while (
380
+ j < args.length &&
381
+ ((args[j] as string).startsWith("-") ||
382
+ (cmd === "env" && ASSIGNMENT.test(args[j] as string)))
383
+ ) {
384
+ j += 1;
385
+ }
386
+ args = args.slice(j);
387
+ } else {
388
+ break;
389
+ }
390
+ }
391
+ if (!args.length) return false;
392
+ const cmd = baseName(args[0] as string);
393
+ const rest = args.slice(1);
394
+ if (SHELLS.has(cmd) && depth < 2) {
395
+ for (let k = 0; k < rest.length; k++) {
396
+ if (SHELL_C_FLAG.test(rest[k] as string) && k + 1 < rest.length) {
397
+ return simpleCommands(lex(rest[k + 1] as string)).some((inner) =>
398
+ isHeavy(inner, depth + 1),
399
+ );
400
+ }
401
+ }
402
+ return false;
403
+ }
404
+ if (cmd === "tsc") return tscHeavy(rest);
405
+ if (cmd === "next") return runBin("next", rest);
406
+ if (cmd === "node") {
407
+ const script = rest.find((arg) => !arg.startsWith("-")) ?? "";
408
+ const after = rest.slice(rest.indexOf(script) + 1);
409
+ if (script.endsWith("/tsc") || script.endsWith("typescript/bin/tsc")) return tscHeavy(after);
410
+ if (script.endsWith("/next") || script.endsWith("next/dist/bin/next")) {
411
+ return runBin("next", after);
412
+ }
413
+ return false;
414
+ }
415
+ if (cmd === "npx" || cmd === "bunx" || cmd === "pnpx") return runnerBin(rest, RUNNER_VALUE_FLAGS);
416
+ if (cmd === "bun") {
417
+ const i = skipFlags(rest, BUN_RUN_VALUE_FLAGS);
418
+ if (i >= rest.length) return false;
419
+ const sub = rest[i] as string;
420
+ const tail = rest.slice(i + 1);
421
+ if (sub === "x") return runnerBin(tail, new Set(["-p", "--package"]));
422
+ if (sub === "test") {
423
+ return !intersects(tail, WATCH_FLAGS) && positional(tail, BUN_TEST_VALUE_FLAGS).length === 0;
424
+ }
425
+ if (sub === "run") {
426
+ const j = skipFlags(tail, BUN_RUN_VALUE_FLAGS);
427
+ if (j >= tail.length) return false;
428
+ const script = tail[j] as string;
429
+ return scriptHeavy(script, tail.slice(j + 1)) || runBin(script, tail.slice(j + 1));
430
+ }
431
+ if (BUN_BUILTINS.has(sub) || sub.includes("/") || /\.(ts|js|tsx|mjs|cjs)$/.test(sub)) {
432
+ return false;
433
+ }
434
+ return scriptHeavy(sub, tail) || runBin(sub, tail);
435
+ }
436
+ if (cmd === "npm" || cmd === "pnpm" || cmd === "yarn") {
437
+ const i = skipFlags(rest, NPM_VALUE_FLAGS);
438
+ if (i >= rest.length) return false;
439
+ const sub = rest[i] as string;
440
+ const tail = rest.slice(i + 1);
441
+ if (sub === "test" || sub === "t" || sub === "tst") return scriptHeavy("test", tail);
442
+ if (["run", "run-script", "rum", "urn"].includes(sub)) {
443
+ const j = skipFlags(tail, NPM_VALUE_FLAGS);
444
+ return j < tail.length && scriptHeavy(tail[j] as string, tail.slice(j + 1));
445
+ }
446
+ if (sub === "exec" || sub === "dlx") return runnerBin(tail, RUNNER_VALUE_FLAGS);
447
+ // pnpm <script> and yarn <script|bin> run package scripts and bins directly.
448
+ if (cmd !== "npm") return scriptHeavy(sub, tail) || runBin(sub, tail);
449
+ return false;
450
+ }
451
+ return false;
452
+ }
453
+
454
+ /**
455
+ * Offset to insert the wrapper: before binary wrappers, after the shell
456
+ * keywords and builtins that `admission run` could not exec.
457
+ */
458
+ function insertionPoint(words: Word[]): number {
459
+ for (let k = 0; k < words.length; k++) {
460
+ const word = words[k] as Word;
461
+ const keyword =
462
+ !word.quoted &&
463
+ (word.value === "time" ||
464
+ word.value === "command" ||
465
+ word.value === "exec" ||
466
+ (word.value === "-p" && k > 0 && words[k - 1]?.value === "time"));
467
+ if (!keyword) return word.start;
468
+ }
469
+ return (words[0] as Word).start;
470
+ }
471
+
472
+ /** POSIX single-quote a word when it needs quoting (shlex.quote semantics). */
473
+ export function shellQuote(value: string): string {
474
+ if (value === "") return "''";
475
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
476
+ return `'${value.replace(/'/g, `'"'"'`)}'`;
477
+ }
478
+
479
+ /** True when a simple command already queues through admission on purpose. */
480
+ function alreadyAdmitted(words: Word[]): boolean {
481
+ const first = baseName(words[0]?.value ?? "");
482
+ if (first === "heavy-slot") return true;
483
+ const values = words.map((word) => word.value);
484
+ const at = values.indexOf("admission");
485
+ return at !== -1 && values[at + 1] === "run";
486
+ }
487
+
488
+ /**
489
+ * Return the command with each heavy simple command wrapped in `wrapper`
490
+ * (a shell-ready prefix such as `harn admission run --resource build`), or
491
+ * null when nothing changes.
492
+ */
493
+ export function rewriteHeavyCommand(command: string, wrapper: string): string | null {
494
+ if (command.includes(ADMISSION_SKIP_PREFIX)) return null;
495
+ const commands = simpleCommands(lex(command));
496
+ if (commands.some(alreadyAdmitted)) return null;
497
+ const inserts: { at: number; text: string }[] = [];
498
+ for (const words of commands) {
499
+ if (!isHeavy(words)) continue;
500
+ const at = insertionPoint(words);
501
+ let label = command
502
+ .slice((words[0] as Word).start, (words.at(-1) as Word).end)
503
+ .split(/\s+/)
504
+ .filter(Boolean)
505
+ .join(" ");
506
+ if (label.length > LABEL_MAX) label = `${label.slice(0, LABEL_MAX - 1)}~`;
507
+ inserts.push({ at, text: `${wrapper} --label ${shellQuote(`agent: ${label}`)} -- ` });
508
+ }
509
+ if (!inserts.length) return null;
510
+ let out = command;
511
+ for (const { at, text } of inserts.sort((a, b) => b.at - a.at)) {
512
+ out = out.slice(0, at) + text + out.slice(at);
513
+ }
514
+ return out;
515
+ }
516
+
517
+ const SHELL_TOOLS = new Set(["bash", "shell", "shell_command", "exec_command", "local_shell"]);
518
+
519
+ /** Normalized shell-tool name, or null for any other tool. */
520
+ export function shellToolName(toolName: unknown): string | null {
521
+ if (typeof toolName !== "string") return null;
522
+ const name = toolName.toLowerCase().replace(/^functions\./, "");
523
+ return SHELL_TOOLS.has(name) ? name : null;
524
+ }
525
+
526
+ export interface AdmissionRewrite {
527
+ updatedInput: Record<string, unknown>;
528
+ }
529
+
530
+ /**
531
+ * Rewrite a shell tool's input so its heavy checks queue for admission.
532
+ * Handles `command` or `cmd` as a string, or as an argv such as
533
+ * ["bash", "-lc", "<script>"]. Returns null when nothing changes.
534
+ */
535
+ export function admissionRewriteForTool(
536
+ toolName: unknown,
537
+ toolInput: unknown,
538
+ wrapper: string,
539
+ ): AdmissionRewrite | null {
540
+ if (shellToolName(toolName) === null) return null;
541
+ let input = toolInput;
542
+ if (typeof input === "string") {
543
+ try {
544
+ input = JSON.parse(input);
545
+ } catch {
546
+ return null;
547
+ }
548
+ }
549
+ if (!input || typeof input !== "object" || Array.isArray(input)) return null;
550
+ const record = input as Record<string, unknown>;
551
+ for (const key of ["command", "cmd"]) {
552
+ const value = record[key];
553
+ if (typeof value === "string") {
554
+ const next = rewriteHeavyCommand(value, wrapper);
555
+ return next === null ? null : { updatedInput: { ...record, [key]: next } };
556
+ }
557
+ if (Array.isArray(value) && value.length && value.every((v) => typeof v === "string")) {
558
+ const argv = value as string[];
559
+ if (
560
+ argv.length >= 3 &&
561
+ SHELLS.has(baseName(argv[0] as string)) &&
562
+ SHELL_C_FLAG.test(argv[argv.length - 2] as string)
563
+ ) {
564
+ const next = rewriteHeavyCommand(argv[argv.length - 1] as string, wrapper);
565
+ return next === null
566
+ ? null
567
+ : { updatedInput: { ...record, [key]: [...argv.slice(0, -1), next] } };
568
+ }
569
+ const next = rewriteHeavyCommand(argv.map(shellQuote).join(" "), wrapper);
570
+ return next === null ? null : { updatedInput: { ...record, [key]: ["bash", "-c", next] } };
571
+ }
572
+ }
573
+ return null;
574
+ }
575
+
576
+ export interface AdmissionHookInput {
577
+ adapter: string;
578
+ /** Admission resource the project's guard queues on. */
579
+ resource: string;
580
+ /** Host bin name used in the wrapper and the agent-facing note. */
581
+ bin: string;
582
+ /**
583
+ * Project whose admission config `admission run` should read. A nested
584
+ * checkout with its own `.harnery/` would otherwise shadow the host's
585
+ * resource settings for a shell started inside it.
586
+ */
587
+ projectRoot?: string;
588
+ toolName: unknown;
589
+ toolInput: unknown;
590
+ cwd?: unknown;
591
+ /** True when HARNERY_ADMISSION_SKIP=1 or HARNERY_ADMISSION_HELD is set for the hook. */
592
+ skip?: boolean;
593
+ platform?: string;
594
+ }
595
+
596
+ /**
597
+ * The PreToolUse output that queues a shell command's heavy checks, or null
598
+ * to let the command run as written. Claude Code applies `updatedInput`
599
+ * under the session's own permission rules, so no decision is sent, and its
600
+ * Bash timeout is raised to the maximum because a queued wait counts against
601
+ * it. Codex applies a rewrite only beside `permissionDecision: "allow"`.
602
+ * Cursor and OpenCode have no rewrite channel. Windows-native shells (a
603
+ * drive-letter or UNC workspace) are outside the machine whose queue this
604
+ * guards.
605
+ */
606
+ export function admissionHookOutput(input: AdmissionHookInput): Record<string, unknown> | null {
607
+ if (input.skip) return null;
608
+ if (input.adapter !== "claude-code" && input.adapter !== "codex") return null;
609
+ const cwd = typeof input.cwd === "string" ? input.cwd : "";
610
+ if (input.platform === "win32" || /^(?:\\\\|[A-Za-z]:[\\/])/.test(cwd)) return null;
611
+ const wrapper =
612
+ `${shellQuote(input.bin)} admission run --resource ${shellQuote(input.resource)}` +
613
+ (input.projectRoot ? ` --project-root ${shellQuote(input.projectRoot)}` : "");
614
+ const rewrite = admissionRewriteForTool(input.toolName, input.toolInput, wrapper);
615
+ if (!rewrite) return null;
616
+ const updatedInput = rewrite.updatedInput;
617
+ const output: Record<string, unknown> = { hookEventName: "PreToolUse", updatedInput };
618
+ if (input.adapter === "codex") {
619
+ output.permissionDecision = "allow";
620
+ } else {
621
+ if (shellToolName(input.toolName) === "bash" && !updatedInput.run_in_background) {
622
+ const current = updatedInput.timeout;
623
+ if (typeof current !== "number" || current < BASH_TIMEOUT_MAX_MS) {
624
+ updatedInput.timeout = BASH_TIMEOUT_MAX_MS;
625
+ }
626
+ }
627
+ output.additionalContext =
628
+ `Harnery queued this command's heavy check for a machine-wide \`${input.resource}\` ` +
629
+ `admission slot, so it may wait first; \`${input.bin} admission status --resource ` +
630
+ `${input.resource}\` shows the queue. Prefix ${ADMISSION_SKIP_PREFIX} to run a check ` +
631
+ `ungated on purpose.`;
632
+ }
633
+ return { hookSpecificOutput: output };
634
+ }