@workerstack/repo 0.0.0-stage → 0.1.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 (438) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +0 -3
  4. package/agents/global-settings/antigravity.json +3 -0
  5. package/agents/global-settings/claude.json +6 -0
  6. package/agents/global-settings/codex.toml +2 -0
  7. package/agents/global-settings/cursor.json +3 -0
  8. package/agents/global-settings/grok.toml +36 -0
  9. package/agents/global-settings/manifest.json +36 -0
  10. package/agents/grok/hooks/orchestrate-parent.json +60 -0
  11. package/agents/grok/hooks/rtk.json +16 -0
  12. package/agents/grok/hooks/rulesync.json +3 -0
  13. package/agents/grok/roles/scout.toml +5 -0
  14. package/agents/hooks.jsonc +67 -0
  15. package/agents/mcp-policy.jsonc +16 -0
  16. package/agents/rules/overview.md +21 -0
  17. package/agents/settings/claude.json +5 -0
  18. package/agents/skills/ask/SKILL.md +13 -0
  19. package/agents/skills/auto/SKILL.md +13 -0
  20. package/agents/skills/away/SKILL.md +19 -0
  21. package/agents/skills/blast-radius/SKILL.md +54 -0
  22. package/agents/skills/bro/SKILL.md +9 -0
  23. package/agents/skills/browser/SKILL.md +16 -0
  24. package/agents/skills/clasp/SKILL.md +11 -0
  25. package/agents/skills/commit/SKILL.md +35 -0
  26. package/agents/skills/commit-and-push/SKILL.md +9 -0
  27. package/agents/skills/commit-push/SKILL.md +59 -0
  28. package/agents/skills/commit-push-deploy/SKILL.md +74 -0
  29. package/agents/skills/commit-push-release/SKILL.md +75 -0
  30. package/agents/skills/correct/SKILL.md +39 -0
  31. package/agents/skills/do/SKILL.md +21 -0
  32. package/agents/skills/explain/SKILL.md +13 -0
  33. package/agents/skills/google-workspace/SKILL.md +12 -0
  34. package/agents/skills/handoff/SKILL.md +81 -0
  35. package/agents/skills/implement-plan/SKILL.md +29 -0
  36. package/agents/skills/implement-roadmap/SKILL.md +27 -0
  37. package/agents/skills/jev/SKILL.md +17 -0
  38. package/agents/skills/listen/SKILL.md +15 -0
  39. package/agents/skills/pickup/SKILL.md +58 -0
  40. package/agents/skills/principle-attack-the-premise/SKILL.md +24 -0
  41. package/agents/skills/principle-boundary-discipline/SKILL.md +38 -0
  42. package/agents/skills/principle-build-the-lever/SKILL.md +28 -0
  43. package/agents/skills/principle-encode-lessons-in-structure/SKILL.md +15 -0
  44. package/agents/skills/principle-exhaust-the-design-space/SKILL.md +12 -0
  45. package/agents/skills/principle-experience-first/SKILL.md +22 -0
  46. package/agents/skills/principle-explain-the-number/SKILL.md +21 -0
  47. package/agents/skills/principle-fix-root-causes/SKILL.md +24 -0
  48. package/agents/skills/principle-foundational-thinking/SKILL.md +25 -0
  49. package/agents/skills/principle-guard-the-context-window/SKILL.md +16 -0
  50. package/agents/skills/principle-laziness-protocol/SKILL.md +13 -0
  51. package/agents/skills/principle-make-operations-idempotent/SKILL.md +25 -0
  52. package/agents/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +18 -0
  53. package/agents/skills/principle-minimize-reader-load/SKILL.md +22 -0
  54. package/agents/skills/principle-model-the-domain/SKILL.md +26 -0
  55. package/agents/skills/principle-never-block-on-the-human/SKILL.md +17 -0
  56. package/agents/skills/principle-outcome-oriented-execution/SKILL.md +25 -0
  57. package/agents/skills/principle-prove-it-works/SKILL.md +25 -0
  58. package/agents/skills/principle-redesign-from-first-principles/SKILL.md +18 -0
  59. package/agents/skills/principle-separate-before-serializing-shared-state/SKILL.md +20 -0
  60. package/agents/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
  61. package/agents/skills/principle-subtract-before-you-add/SKILL.md +21 -0
  62. package/agents/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
  63. package/agents/skills/principle-type-system-discipline/SKILL.md +17 -0
  64. package/agents/skills/review-plan/SKILL.md +28 -0
  65. package/agents/skills/review-roadmap/SKILL.md +28 -0
  66. package/agents/skills/show-me/SKILL.md +21 -0
  67. package/agents/skills/speak/SKILL.md +15 -0
  68. package/agents/skills/su/SKILL.md +11 -0
  69. package/agents/skills/teach/SKILL.md +25 -0
  70. package/agents/skills/tmp/SKILL.md +54 -0
  71. package/agents/skills/typescript-best-practices/SKILL.md +32 -0
  72. package/agents/skills/typescript-best-practices/references/patterns.md +328 -0
  73. package/agents/skills/unslop/SKILL.md +62 -0
  74. package/agents/skills/write-plan/SKILL.md +33 -0
  75. package/agents/skills/write-roadmap/SKILL.md +66 -0
  76. package/agents/subagents/implementer-deep.md +57 -0
  77. package/agents/subagents/implementer-fast.md +57 -0
  78. package/agents/subagents/orchestrator.md +43 -0
  79. package/agents/subagents/researcher.md +47 -0
  80. package/agents/subagents/scout.md +44 -0
  81. package/bin/repo.ts +4 -0
  82. package/cli/actionlint-default.test.ts +38 -0
  83. package/cli/actionlint-download.test.ts +162 -0
  84. package/cli/actionlint-real.test.ts +33 -0
  85. package/cli/actionlint.constants.ts +43 -0
  86. package/cli/actionlint.test.ts +108 -0
  87. package/cli/actionlint.ts +178 -0
  88. package/cli/bin.test.ts +54 -0
  89. package/cli/bin.ts +40 -0
  90. package/cli/check-changelog.test.ts +40 -0
  91. package/cli/check-changelog.ts +12 -0
  92. package/cli/check-comments.test.ts +157 -0
  93. package/cli/check-comments.ts +123 -0
  94. package/cli/check-env-modules.test.ts +85 -0
  95. package/cli/check-env-modules.ts +56 -0
  96. package/cli/check-magic.test.ts +139 -0
  97. package/cli/check-magic.ts +120 -0
  98. package/cli/check-manifests.constants.ts +4 -0
  99. package/cli/check-manifests.test.ts +210 -0
  100. package/cli/check-manifests.ts +71 -0
  101. package/cli/check-nightly.test.ts +103 -0
  102. package/cli/check-nightly.ts +37 -0
  103. package/cli/check-ratchets-missing.test.ts +46 -0
  104. package/cli/check-ratchets-mutation.test.ts +49 -0
  105. package/cli/check-ratchets.constants.ts +20 -0
  106. package/cli/check-ratchets.test.ts +190 -0
  107. package/cli/check-ratchets.ts +193 -0
  108. package/cli/check-rejections.test.ts +220 -0
  109. package/cli/check-rejections.ts +153 -0
  110. package/cli/check-reports.ts +37 -0
  111. package/cli/check-tsconfig.test.ts +507 -0
  112. package/cli/check-tsconfig.ts +205 -0
  113. package/cli/commands.test.ts +114 -0
  114. package/cli/commands.ts +218 -0
  115. package/cli/commit-changelog.ts +27 -0
  116. package/cli/commit-msg-changelog.test.ts +93 -0
  117. package/cli/commit-msg-range.test.ts +126 -0
  118. package/cli/commit-msg-red-proof.test.ts +147 -0
  119. package/cli/commit-msg-repo.test.ts +153 -0
  120. package/cli/commit-msg.test.ts +101 -0
  121. package/cli/commit-msg.ts +194 -0
  122. package/cli/commit-range.constants.ts +17 -0
  123. package/cli/commit-range.ts +57 -0
  124. package/cli/config-knip.test.ts +70 -0
  125. package/cli/config-nightly.test.ts +84 -0
  126. package/cli/config-tdd.test.ts +46 -0
  127. package/cli/config.test.ts +576 -0
  128. package/cli/config.ts +261 -0
  129. package/cli/conventional.ts +9 -0
  130. package/cli/coverage-config.ts +62 -0
  131. package/cli/coverage.constants.ts +6 -0
  132. package/cli/coverage.test.ts +199 -0
  133. package/cli/coverage.ts +127 -0
  134. package/cli/env.constants.ts +10 -0
  135. package/cli/env.test.ts +56 -0
  136. package/cli/env.ts +72 -0
  137. package/cli/fixtures/base-config.ts +13 -0
  138. package/cli/fixtures/gate-world.ts +48 -0
  139. package/cli/fixtures/knip-run.ts +38 -0
  140. package/cli/fixtures/mutate-fakes.ts +137 -0
  141. package/cli/fixtures/planted-repo.ts +50 -0
  142. package/cli/fixtures/publish-world.ts +71 -0
  143. package/cli/fixtures/red-proof-repo.ts +72 -0
  144. package/cli/fixtures/release-context.ts +15 -0
  145. package/cli/fixtures/release-world.ts +111 -0
  146. package/cli/fixtures/stub-bin.ts +40 -0
  147. package/cli/guard-diagnostics.test.ts +157 -0
  148. package/cli/guard-diagnostics.ts +195 -0
  149. package/cli/init.test.ts +487 -0
  150. package/cli/init.ts +275 -0
  151. package/cli/knip-check-ignores.test.ts +106 -0
  152. package/cli/knip-check-real.test.ts +65 -0
  153. package/cli/knip-check.constants.ts +46 -0
  154. package/cli/knip-check.test.ts +114 -0
  155. package/cli/knip-check.ts +169 -0
  156. package/cli/knip-schema.ts +67 -0
  157. package/cli/known-failures.constants.ts +22 -0
  158. package/cli/known-failures.test.ts +138 -0
  159. package/cli/known-failures.ts +131 -0
  160. package/cli/lint/as-needs-reason.ts +41 -0
  161. package/cli/lint/boundaries.ts +73 -0
  162. package/cli/lint/class-file.ts +39 -0
  163. package/cli/lint/config-file.ts +32 -0
  164. package/cli/lint/entry-file.ts +54 -0
  165. package/cli/lint/env-in-config.ts +66 -0
  166. package/cli/lint/filename-matches-export.ts +63 -0
  167. package/cli/lint/index-file.ts +22 -0
  168. package/cli/lint/index.ts +47 -0
  169. package/cli/lint/is-reason.ts +6 -0
  170. package/cli/lint/manifest.ts +46 -0
  171. package/cli/lint/names.ts +59 -0
  172. package/cli/lint/no-commented-out-code.ts +105 -0
  173. package/cli/lint/no-mock-of-subject.ts +64 -0
  174. package/cli/lint/no-module-side-effects.ts +94 -0
  175. package/cli/lint/no-module-state.ts +58 -0
  176. package/cli/lint/no-side-effect-import.ts +17 -0
  177. package/cli/lint/no-v8-ignore.ts +18 -0
  178. package/cli/lint/script-rules.ts +9 -0
  179. package/cli/lint/skip-on-platform.constants.ts +8 -0
  180. package/cli/lint/skip-on-platform.ts +118 -0
  181. package/cli/lint/todo-needs-id.ts +63 -0
  182. package/cli/lint/types-file.ts +28 -0
  183. package/cli/lint-schema.ts +32 -0
  184. package/cli/magic/fixtures/magic-repo.ts +64 -0
  185. package/cli/magic/magic-hit.ts +2 -0
  186. package/cli/magic/magic-nodes.ts +44 -0
  187. package/cli/magic/magic-numbers.test.ts +99 -0
  188. package/cli/magic/magic-numbers.ts +117 -0
  189. package/cli/magic/magic-strings.test.ts +38 -0
  190. package/cli/magic/magic-strings.ts +81 -0
  191. package/cli/main.test.ts +853 -0
  192. package/cli/main.ts +292 -0
  193. package/cli/mutate-args.ts +79 -0
  194. package/cli/mutate-check.test.ts +147 -0
  195. package/cli/mutate-deps.ts +42 -0
  196. package/cli/mutate-edges.test.ts +150 -0
  197. package/cli/mutate-files.test.ts +100 -0
  198. package/cli/mutate-files.ts +31 -0
  199. package/cli/mutate-floors.test.ts +91 -0
  200. package/cli/mutate-floors.ts +104 -0
  201. package/cli/mutate-real-deps.ts +57 -0
  202. package/cli/mutate-real.test.ts +96 -0
  203. package/cli/mutate-report.ts +96 -0
  204. package/cli/mutate-run.ts +128 -0
  205. package/cli/mutate-shards.ts +64 -0
  206. package/cli/mutate.constants.ts +67 -0
  207. package/cli/mutate.test.ts +182 -0
  208. package/cli/mutate.ts +64 -0
  209. package/cli/nested-vitest-flags.ts +8 -0
  210. package/cli/nightly-config.constants.ts +14 -0
  211. package/cli/nightly-config.ts +62 -0
  212. package/cli/prompt.test.ts +202 -0
  213. package/cli/prompt.ts +1 -0
  214. package/cli/red-proof-edges.test.ts +78 -0
  215. package/cli/red-proof-failures.test.ts +82 -0
  216. package/cli/red-proof-tree.test.ts +157 -0
  217. package/cli/red-proof-verdict.test.ts +44 -0
  218. package/cli/red-proof.constants.ts +14 -0
  219. package/cli/red-proof.ts +202 -0
  220. package/cli/release/changelog-parse.ts +83 -0
  221. package/cli/release/changelog.test.ts +141 -0
  222. package/cli/release/changelog.ts +106 -0
  223. package/cli/release/gate-refusals.test.ts +85 -0
  224. package/cli/release/gate.test.ts +63 -0
  225. package/cli/release/gate.ts +70 -0
  226. package/cli/release/git.ts +66 -0
  227. package/cli/release/github.test.ts +128 -0
  228. package/cli/release/github.ts +46 -0
  229. package/cli/release/list-dir.test.ts +11 -0
  230. package/cli/release/list-dir.ts +6 -0
  231. package/cli/release/main.test.ts +48 -0
  232. package/cli/release/main.ts +51 -0
  233. package/cli/release/manifest.test.ts +24 -0
  234. package/cli/release/manifest.ts +34 -0
  235. package/cli/release/pack.test.ts +107 -0
  236. package/cli/release/pack.ts +76 -0
  237. package/cli/release/plan.ts +127 -0
  238. package/cli/release/publish-run.test.ts +98 -0
  239. package/cli/release/publish.test.ts +127 -0
  240. package/cli/release/publish.ts +125 -0
  241. package/cli/release/refuse.ts +5 -0
  242. package/cli/release/release-messages.constants.ts +96 -0
  243. package/cli/release/release-refusals.test.ts +149 -0
  244. package/cli/release/release.constants.ts +128 -0
  245. package/cli/release/release.test.ts +199 -0
  246. package/cli/release/release.ts +145 -0
  247. package/cli/release/version.test.ts +118 -0
  248. package/cli/release/version.ts +69 -0
  249. package/cli/release/wait.test.ts +101 -0
  250. package/cli/release/wait.ts +95 -0
  251. package/cli/script-diagnostics.test.ts +76 -0
  252. package/cli/script-diagnostics.ts +62 -0
  253. package/cli/scripts/board.ts +519 -0
  254. package/cli/scripts/browser.ts +994 -0
  255. package/cli/scripts/change-set.ts +105 -0
  256. package/cli/scripts/check-globals.ts +199 -0
  257. package/cli/scripts/ci-skips.constants.ts +17 -0
  258. package/cli/scripts/ci-skips.ts +118 -0
  259. package/cli/scripts/ci-wait-run.ts +28 -0
  260. package/cli/scripts/ci-wait.constants.ts +44 -0
  261. package/cli/scripts/ci-wait.ts +175 -0
  262. package/cli/scripts/ci.constants.ts +15 -0
  263. package/cli/scripts/ci.ts +208 -0
  264. package/cli/scripts/clean.ts +363 -0
  265. package/cli/scripts/comments-step.ts +15 -0
  266. package/cli/scripts/doctor.ts +828 -0
  267. package/cli/scripts/emit-agent-shell-tools.ts +91 -0
  268. package/cli/scripts/emit-mcp-policy.ts +402 -0
  269. package/cli/scripts/folder-map.ts +353 -0
  270. package/cli/scripts/gate-fat-tools.ts +1095 -0
  271. package/cli/scripts/gitignore-guard.ts +338 -0
  272. package/cli/scripts/google-workspace.ts +779 -0
  273. package/cli/scripts/hook-command.ts +72 -0
  274. package/cli/scripts/hook-input.ts +178 -0
  275. package/cli/scripts/hook-io.constants.ts +5 -0
  276. package/cli/scripts/hook-io.ts +46 -0
  277. package/cli/scripts/hooks-union.ts +469 -0
  278. package/cli/scripts/interactive.ts +30 -0
  279. package/cli/scripts/jev.ts +420 -0
  280. package/cli/scripts/job.ts +934 -0
  281. package/cli/scripts/jsonc.ts +9 -0
  282. package/cli/scripts/lessons.ts +564 -0
  283. package/cli/scripts/lint-baseline.ts +233 -0
  284. package/cli/scripts/lint-main.ts +24 -0
  285. package/cli/scripts/lint-skips.ts +139 -0
  286. package/cli/scripts/lint.constants.ts +6 -0
  287. package/cli/scripts/lint.ts +142 -0
  288. package/cli/scripts/listen.ts +962 -0
  289. package/cli/scripts/managed-text.ts +47 -0
  290. package/cli/scripts/orchestrate-parent.ts +1155 -0
  291. package/cli/scripts/parse-need.ts +81 -0
  292. package/cli/scripts/persist-handoff.ts +228 -0
  293. package/cli/scripts/persist-implementation.ts +276 -0
  294. package/cli/scripts/persist-plan.ts +888 -0
  295. package/cli/scripts/persist-roadmap.ts +478 -0
  296. package/cli/scripts/persist-tmp.ts +177 -0
  297. package/cli/scripts/persist.ts +54 -0
  298. package/cli/scripts/phase-files.ts +121 -0
  299. package/cli/scripts/prompt.ts +38 -0
  300. package/cli/scripts/purge-global-mcps.ts +313 -0
  301. package/cli/scripts/rejections-step.ts +37 -0
  302. package/cli/scripts/rtk-grok.ts +102 -0
  303. package/cli/scripts/scrub-spawn-prompt.ts +103 -0
  304. package/cli/scripts/scrub-text-lib.ts +58 -0
  305. package/cli/scripts/session-map.ts +69 -0
  306. package/cli/scripts/setup.ts +862 -0
  307. package/cli/scripts/skill-conflicts.ts +82 -0
  308. package/cli/scripts/speak.ts +370 -0
  309. package/cli/scripts/strategy.ts +168 -0
  310. package/cli/scripts/sync-global-settings.ts +316 -0
  311. package/cli/scripts/turn-steps.ts +249 -0
  312. package/cli/scripts/turn.ts +157 -0
  313. package/cli/scripts/typecheck-step.ts +43 -0
  314. package/cli/scripts/vscode-tasks.ts +175 -0
  315. package/cli/scripts/workspace-root.ts +160 -0
  316. package/cli/scripts/worktree.ts +806 -0
  317. package/cli/spawn.test.ts +34 -0
  318. package/cli/spawn.ts +27 -0
  319. package/cli/staged-change.constants.ts +11 -0
  320. package/cli/staged-change.ts +79 -0
  321. package/cli/stryker-preload.test.ts +50 -0
  322. package/cli/stryker-preload.ts +9 -0
  323. package/cli/sync/agents/frontmatter.test.ts +197 -0
  324. package/cli/sync/agents/frontmatter.ts +140 -0
  325. package/cli/sync/agents/hooks.test.ts +517 -0
  326. package/cli/sync/agents/hooks.ts +401 -0
  327. package/cli/sync/agents/rules.test.ts +229 -0
  328. package/cli/sync/agents/rules.ts +84 -0
  329. package/cli/sync/agents/sources.test.ts +375 -0
  330. package/cli/sync/agents/sources.ts +244 -0
  331. package/cli/sync/agents/subagents.test.ts +149 -0
  332. package/cli/sync/agents/subagents.ts +158 -0
  333. package/cli/sync/agents-text.test.ts +31 -0
  334. package/cli/sync/agents-text.ts +36 -0
  335. package/cli/sync/fixtures/copied.README.md +35 -0
  336. package/cli/sync/fixtures/copied.tgz +0 -0
  337. package/cli/sync/fixtures/maps/golden/INDEX.md +22 -0
  338. package/cli/sync/fixtures/maps-ws.tgz +0 -0
  339. package/cli/sync/git-files.test.ts +340 -0
  340. package/cli/sync/git-files.ts +197 -0
  341. package/cli/sync/git-hooks.test.ts +94 -0
  342. package/cli/sync/golden.test.ts +15 -0
  343. package/cli/sync/hooks-mode.test.ts +99 -0
  344. package/cli/sync/hosts.ts +28 -0
  345. package/cli/sync/knip-config.constants.ts +4 -0
  346. package/cli/sync/knip-config.test.ts +147 -0
  347. package/cli/sync/knip-config.ts +108 -0
  348. package/cli/sync/manifest.test.ts +78 -0
  349. package/cli/sync/manifest.ts +60 -0
  350. package/cli/sync/maps.test.ts +304 -0
  351. package/cli/sync/maps.ts +297 -0
  352. package/cli/sync/migrate.test.ts +2000 -0
  353. package/cli/sync/migrate.ts +1104 -0
  354. package/cli/sync/nightly-jobs.test.ts +63 -0
  355. package/cli/sync/nightly.constants.ts +48 -0
  356. package/cli/sync/nightly.test.ts +100 -0
  357. package/cli/sync/nightly.ts +75 -0
  358. package/cli/sync/outputs.test.ts +351 -0
  359. package/cli/sync/outputs.ts +168 -0
  360. package/cli/sync/oxlint-config.test.ts +178 -0
  361. package/cli/sync/oxlint-config.ts +168 -0
  362. package/cli/sync/release-jobs.test.ts +29 -0
  363. package/cli/sync/release-reports.test.ts +31 -0
  364. package/cli/sync/release-workflow.constants.ts +61 -0
  365. package/cli/sync/release-workflow.test.ts +192 -0
  366. package/cli/sync/release-workflow.ts +86 -0
  367. package/cli/sync/release.constants.ts +93 -0
  368. package/cli/sync/release.test.ts +187 -0
  369. package/cli/sync/release.ts +112 -0
  370. package/cli/sync/repo-dir.test.ts +371 -0
  371. package/cli/sync/repo-dir.ts +205 -0
  372. package/cli/sync/standards.constants.ts +8 -0
  373. package/cli/sync/standards.test.ts +206 -0
  374. package/cli/sync/standards.ts +179 -0
  375. package/cli/sync/stock-history.json +2796 -0
  376. package/cli/sync/stock-history.test.ts +66 -0
  377. package/cli/sync/stock-history.ts +84 -0
  378. package/cli/sync/stock.test.ts +47 -0
  379. package/cli/sync/stock.ts +24 -0
  380. package/cli/sync/trees.test.ts +621 -0
  381. package/cli/sync/trees.ts +280 -0
  382. package/cli/sync/validate.test.ts +299 -0
  383. package/cli/sync/validate.ts +402 -0
  384. package/cli/sync/workspace.test.ts +180 -0
  385. package/cli/sync/workspace.ts +138 -0
  386. package/cli/sync/write.test.ts +341 -0
  387. package/cli/sync/write.ts +231 -0
  388. package/cli/sync.test.ts +1495 -0
  389. package/cli/sync.ts +360 -0
  390. package/cli/tdd-scope.test.ts +36 -0
  391. package/cli/tdd-scope.ts +41 -0
  392. package/cli/test-files.ts +16 -0
  393. package/cli/ts-program.ts +69 -0
  394. package/cli/tsconfig.constants.ts +8 -0
  395. package/cli/workerstack-config.types.ts +21 -0
  396. package/dist/cli/env.d.ts +10 -0
  397. package/dist/cli/env.js +64 -0
  398. package/dist/presets/temp-root-env.d.ts +10 -0
  399. package/dist/presets/temp-root-env.js +20 -0
  400. package/dist/presets/temp-root.constants.d.ts +6 -0
  401. package/dist/presets/temp-root.constants.js +6 -0
  402. package/dist/presets/temp-root.d.ts +2 -0
  403. package/dist/presets/temp-root.js +60 -0
  404. package/dist/presets/vitest.constants.d.ts +16 -0
  405. package/dist/presets/vitest.constants.js +16 -0
  406. package/dist/presets/vitest.d.ts +11 -0
  407. package/dist/presets/vitest.js +67 -0
  408. package/dist/presets/workers-plugin.d.ts +10 -0
  409. package/dist/presets/workers-plugin.js +54 -0
  410. package/package.json +141 -4
  411. package/references/INDEX.md +100 -0
  412. package/references/browser.md +66 -0
  413. package/references/cascade.md +15 -0
  414. package/references/clasp.md +21 -0
  415. package/references/cloudflare.md +36 -0
  416. package/references/google-workspace.md +79 -0
  417. package/references/hooks.md +103 -0
  418. package/references/implementation-format.md +119 -0
  419. package/references/jev.md +56 -0
  420. package/references/lessons.md +73 -0
  421. package/references/listen.md +80 -0
  422. package/references/map.md +107 -0
  423. package/references/output.md +47 -0
  424. package/references/plan-format.md +264 -0
  425. package/references/review-loop.md +48 -0
  426. package/references/roadmap-format.md +104 -0
  427. package/references/rtk.md +27 -0
  428. package/references/scope.md +76 -0
  429. package/references/setup.md +42 -0
  430. package/references/speak.md +59 -0
  431. package/tsconfig/base.json +28 -0
  432. package/tsconfig/bun.d.ts +1 -0
  433. package/tsconfig/bun.json +8 -0
  434. package/tsconfig/reset.d.ts +1 -0
  435. package/tsconfig/strict.json +12 -0
  436. package/tsconfig/web.json +7 -0
  437. package/tsconfig/worker.d.ts +1 -0
  438. package/tsconfig/worker.json +8 -0
@@ -0,0 +1,1095 @@
1
+ #!/usr/bin/env bun
2
+ /** PreToolUse: fat Read/Grep/Glob/Bash gate. Fail-open: parse/crash → empty stdout, exit 0. */
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import type { HookAgent, HookInput } from "./hook-input.ts";
6
+ import { processHookIo, readHookInput, type HookIo } from "./hook-io.ts";
7
+ import { patchIgnoreEdits, redundantIgnoreReason, shellIgnoreEdits, writeIgnoreEdits } from "./gitignore-guard.ts";
8
+ import { persistOwner } from "./persist-tmp.ts";
9
+ import {
10
+ ENFORCED_HARNESSES,
11
+ activeReview,
12
+ implementerOf,
13
+ parseStrategy,
14
+ stampPath,
15
+ writeStamp,
16
+ type ActiveReview,
17
+ } from "./strategy.ts";
18
+ import { canonicalPath, gitRevParse, resolveRoots, resolveTyped, type Roots } from "./workspace-root.ts";
19
+ import { gitAskedWords, mainCheckoutWriteReason, sharedGitReason, worktreeAddReason } from "./worktree.ts";
20
+
21
+ /** Which wire shape a hook payload came in; Grok sends Claude's. */
22
+ export type Harness = "claude" | "codex" | "antigravity" | "cursor";
23
+
24
+ /** What one tool of one harness is to the gate, and which input fields carry its command and cwd. */
25
+ export interface ToolSpec {
26
+ kind: "bash" | "write" | "read" | "grep" | "glob" | "patch";
27
+ command?: string;
28
+ cwd?: string;
29
+ persistShared?: boolean;
30
+ limits?: boolean;
31
+ }
32
+
33
+ /** A tool call's argument bag; the shape varies per harness and tool, so each field is checked where it is read. */
34
+ export type ToolArgs = Record<string, unknown>;
35
+
36
+ /** The gate's verdict on one call: a deny reason, a rewritten input, or null to let it through. */
37
+ export type GateResult = { type: "deny"; reason: string } | { type: "rewrite"; updatedInput: ToolArgs } | null;
38
+
39
+ /** The part of a hook payload the gate reads; a payload that failed parsing is null. */
40
+ type Payload = HookInput | null | undefined;
41
+
42
+ /** Session Grep head_limit when the model omits one. */
43
+ export const DEFAULT_GREP_HEAD_LIMIT = 50;
44
+ /** Session Read line limit when the model omits one. */
45
+ export const DEFAULT_READ_LIMIT = 200;
46
+ /** Unknown non-scout child Grep head_limit when omitted. */
47
+ export const CHILD_GREP_HEAD_LIMIT = 150;
48
+ /** Unknown non-scout child Read line limit when omitted. */
49
+ export const CHILD_READ_LIMIT = 400;
50
+ /** Deny if requested Grep head_limit or Read limit exceeds this. */
51
+ export const MAX_REQUEST_LIMIT = 500;
52
+ /** Deny Read when the file is larger than this, even with a line limit. */
53
+ export const DENY_FILE_BYTES: number = 200 * 1024;
54
+
55
+ export const DENY_PATH_SEGMENTS: Set<string> = new Set(["node_modules", "dist", ".next", "build", "coverage"]);
56
+ export const DENY_LOCKFILES: Set<string> = new Set(["bun.lock"]);
57
+
58
+ export const REASON_DENYLIST = "path denylist; use package API";
59
+ export const REASON_HEAD_LIMIT = "head_limit>500";
60
+ export const REASON_FILE_SIZE = "file >200KB";
61
+ export const REASON_REREAD = "already wrote this session; Read with offset+limit";
62
+ /** Cursor's Read takes no offset or limit, so its re-read reason names a command that does the slicing. */
63
+ export function rereadReasonFor(harness: Harness, absPath: string, checkout: string): string {
64
+ if (harness !== "cursor") return REASON_REREAD;
65
+ const slice = jobCommand(
66
+ checkout,
67
+ `--job exec --query ${quoteQuery(`sed -n 1,${DEFAULT_READ_LIMIT}p ${shellPath(absPath)}`)}`,
68
+ );
69
+ return `already wrote this session; Cursor Read takes no offset or limit, so slice it with ${slice}`;
70
+ }
71
+
72
+ const SHELL: ToolSpec = { kind: "bash", command: "command" };
73
+ // orchestrate-parent persists these itself, so the gate leaves their persist-owned redirects to it.
74
+ const SHARED_WRITE: ToolSpec = { kind: "write", persistShared: true };
75
+ const WRITE: ToolSpec = { kind: "write" };
76
+ // Cursor's Read and Grep take no offset, limit or head_limit, so the gate cannot inject or cap them.
77
+ const CURSOR_NO_LIMITS = { limits: false } as const;
78
+
79
+ /**
80
+ * The tools each harness sends, keyed by eventKey of the tool name. A harness only gets the kinds whose
81
+ * fields it uses; anything else gets no answer. Claude also covers Grok, which sends the same shape.
82
+ */
83
+ export const TOOLS: Record<Harness, Map<string, ToolSpec>> = {
84
+ claude: new Map<string, ToolSpec>([
85
+ ["write", SHARED_WRITE],
86
+ ["edit", SHARED_WRITE],
87
+ ["searchreplace", SHARED_WRITE],
88
+ ["bash", SHELL],
89
+ ["runterminalcommand", SHELL],
90
+ ["read", { kind: "read" }],
91
+ ["readfile", { kind: "read" }],
92
+ ["grep", { kind: "grep" }],
93
+ ["glob", { kind: "glob" }],
94
+ ["listdir", { kind: "glob" }],
95
+ ]),
96
+ codex: new Map<string, ToolSpec>([
97
+ ["applypatch", { kind: "patch" }],
98
+ ["bash", SHELL],
99
+ ]),
100
+ cursor: new Map<string, ToolSpec>([
101
+ ["shell", { kind: "bash", command: "command", cwd: "cwd" }],
102
+ ["read", { kind: "read", ...CURSOR_NO_LIMITS }],
103
+ ["grep", { kind: "grep", ...CURSOR_NO_LIMITS }],
104
+ ["write", SHARED_WRITE],
105
+ ]),
106
+ antigravity: new Map<string, ToolSpec>([
107
+ ["writefile", WRITE],
108
+ ["editfile", WRITE],
109
+ ["runcommand", { kind: "bash", command: "CommandLine", cwd: "Cwd" }],
110
+ ]),
111
+ };
112
+ export const WRITE_PATH_FIELDS: string[] = ["path", "file_path", "filePath", "target_file", "targetFile", "Path"];
113
+ const PATH_FIELDS = [
114
+ "path",
115
+ "file_path",
116
+ "filePath",
117
+ "target_file",
118
+ "targetFile",
119
+ "target_directory",
120
+ "targetDirectory",
121
+ "glob",
122
+ "glob_pattern",
123
+ "globPattern",
124
+ ];
125
+ const BASH_DUMP_CMDS = /\b(cat|tac|less|more|head|tail|nl|bat|hexdump|xxd|rg|grep|egrep|fgrep|find|fd)\b/;
126
+ const BASH_SEARCH = /\b(rg|grep|egrep|fgrep|find|fd)\b/;
127
+ const BASH_WEB = /\b(curl|wget)\b/;
128
+ export const SHELL_META: RegExp = /[|&;`$(){}<>\n\r]/;
129
+ const JOB_FLAGS = new Set(["--job", "--query", "--path", "--glob", "--cwd", "--stash", "--from", "--to"]);
130
+ const PRODUCT_ROOT_NAMES = new Set(["README.md", "CHANGELOG.md", "AGENTS.md", "package.json", "LICENSE"]);
131
+ const PRODUCT_ROOT_DIRS = new Set(["src", "scripts", "agents", ".grok", ".claude", ".agents", "cli"]);
132
+ const SCRATCH_ROOT_NAMES = new Set(["notes.md", "scratch.md", "output.txt"]);
133
+ const BASH_TREE = /\b(ls|tree)\b/;
134
+ const TEST_CMD =
135
+ /^\s*(?:bun (?:run )?test|bunx? (?:--bun )?vitest|npx vitest|vitest|npm (?:run )?test|pnpm (?:run )?test)\b/;
136
+
137
+ export function eventKey(s: unknown): string {
138
+ return String(s || "")
139
+ .toLowerCase()
140
+ .replace(/[^a-z0-9]/g, "");
141
+ }
142
+
143
+ const CHILD_TYPE_KEYS = [
144
+ "subagentType",
145
+ "subagent_type",
146
+ "agentType",
147
+ "agent_type",
148
+ "agentName",
149
+ "agent_name",
150
+ ] as const satisfies readonly (keyof HookInput)[];
151
+ const CHILD_ID_KEYS = ["agentId", "agent_id", "spawnedBy", "spawned_by"] as const satisfies readonly (keyof HookInput &
152
+ keyof HookAgent)[];
153
+ const AGENT_ID_KEYS = [...CHILD_ID_KEYS, "id"] as const satisfies readonly (keyof HookAgent)[];
154
+ const AGENT_TYPE_KEYS = [
155
+ "subagentType",
156
+ "subagent_type",
157
+ "type",
158
+ "name",
159
+ "agentType",
160
+ ] as const satisfies readonly (keyof HookAgent)[];
161
+
162
+ function nonEmptyChildId(raw: unknown): boolean {
163
+ if (raw == null) return false;
164
+ return String(raw).trim() !== "";
165
+ }
166
+
167
+ /** True when PreToolUse names a child session (UUID ok). Not a fat-gate type. */
168
+ export function hasChildAgentMarker(data: Payload): boolean {
169
+ if (!data || typeof data !== "object") return false;
170
+ for (const k of CHILD_ID_KEYS) {
171
+ if (nonEmptyChildId(data[k])) return true;
172
+ }
173
+ const agent = data.agent;
174
+ if (agent && typeof agent === "object" && !Array.isArray(agent)) {
175
+ for (const k of AGENT_ID_KEYS) {
176
+ if (nonEmptyChildId(agent[k])) return true;
177
+ }
178
+ }
179
+ return false;
180
+ }
181
+
182
+ function coerceChildType(raw: unknown): string {
183
+ if (raw == null) return "";
184
+ const s = String(raw).trim().toLowerCase();
185
+ if (!s) return "";
186
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s)) {
187
+ return "";
188
+ }
189
+ if (/^[0-9]+$/.test(s)) return "";
190
+ if (s.length > 64 || /[\/\\]/.test(s)) return "";
191
+ return s;
192
+ }
193
+
194
+ export function subagentTypeOf(data: Payload): string {
195
+ if (!data || typeof data !== "object") return "";
196
+ for (const k of [...CHILD_TYPE_KEYS, ...CHILD_ID_KEYS]) {
197
+ const t = coerceChildType(data[k]);
198
+ if (t) return t;
199
+ }
200
+ const agent = data.agent;
201
+ if (agent && typeof agent === "object" && !Array.isArray(agent)) {
202
+ for (const k of AGENT_TYPE_KEYS) {
203
+ const t = coerceChildType(agent[k]);
204
+ if (t) return t;
205
+ }
206
+ } else if (typeof agent === "string") {
207
+ const t = coerceChildType(agent);
208
+ if (t) return t;
209
+ }
210
+ return "";
211
+ }
212
+
213
+ export function isParentOrchestrator(data: Payload): boolean {
214
+ return !subagentTypeOf(data) && !hasChildAgentMarker(data);
215
+ }
216
+
217
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
218
+ return v != null && typeof v === "object" && !Array.isArray(v);
219
+ }
220
+
221
+ /** What the gate prints for a deny or a rewrite; each harness reads its own subset of these fields. */
222
+ export interface HookOutput {
223
+ decision?: string;
224
+ reason?: string;
225
+ permission?: string;
226
+ user_message?: string;
227
+ agent_message?: string;
228
+ overwrite?: ToolArgs;
229
+ hookSpecificOutput?: {
230
+ hookEventName: string;
231
+ permissionDecision?: string;
232
+ permissionDecisionReason?: string;
233
+ updatedInput?: ToolArgs;
234
+ };
235
+ }
236
+
237
+ /** A payload object as the gate reads it; anything else (null, an array, a scalar) is not a hook call. */
238
+ function isHookObject(v: Payload): v is HookInput {
239
+ return v != null && typeof v === "object" && !Array.isArray(v);
240
+ }
241
+
242
+ /** Which wire shape the hook payload came in. */
243
+ export function harnessOf(data: HookInput): Harness {
244
+ // Cursor also sends model and session_id, so it is told apart by its version before Codex's check.
245
+ if (typeof data.cursor_version === "string") return "cursor";
246
+ if (isPlainObject(data.toolCall)) return "antigravity";
247
+ // Codex sends both on every tool hook; Claude would read Codex's `allow` as skipping its permission prompt.
248
+ if (typeof data.turn_id === "string" && typeof data.model === "string") return "codex";
249
+ return "claude";
250
+ }
251
+
252
+ /** The call in one shape for every harness: its tool key and args (null when absent or not an object). */
253
+ export function toolCallOf(data: HookInput): { harness: Harness; key: string; args: ToolArgs | null } {
254
+ const harness = harnessOf(data);
255
+ const call = harness === "antigravity" ? data.toolCall : null;
256
+ const name = call ? call.name : data.toolName || data.tool_name;
257
+ let args: unknown = call ? call.args : data.toolInput;
258
+ if (!call && args == null) args = data.tool_input;
259
+ return { harness, key: eventKey(name), args: isPlainObject(args) ? args : null };
260
+ }
261
+
262
+ /** A package's shipped `references/` dir under any `node_modules/@workerstack/<pkg>/`; the one `node_modules` path agents may read. */
263
+ export const STOCK_REFERENCES: RegExp = /(?:^|\/)node_modules\/@workerstack\/[^/]+\/references(?:\/|$)/;
264
+
265
+ /** The typed path with backslashes swapped and `.`/`..` folded, so `references/../cli` is judged as `cli`. */
266
+ function typedPosix(p: unknown): string {
267
+ const swapped = String(p).replace(/\\/g, "/").trim();
268
+ return swapped ? path.posix.normalize(swapped) : "";
269
+ }
270
+
271
+ /** True when the typed path lies inside a stock references dir. */
272
+ function inStockReferences(p: unknown): boolean {
273
+ return STOCK_REFERENCES.test(typedPosix(p));
274
+ }
275
+
276
+ export function pathIsDenied(p: unknown): boolean {
277
+ if (p == null) return false;
278
+ const raw = String(p).replace(/\\/g, "/").trim();
279
+ if (!raw) return false;
280
+ if (DENY_LOCKFILES.has(path.posix.basename(raw)) || gitDirIn(raw)) return true;
281
+ // The match goes out as `/`, so what sat before and after it still reads as separate segments.
282
+ const n = typedPosix(raw).replace(STOCK_REFERENCES, "/");
283
+ if (DENY_LOCKFILES.has(path.posix.basename(n))) return true;
284
+ return n.split("/").some((seg) => DENY_PATH_SEGMENTS.has(seg));
285
+ }
286
+
287
+ /** Top-level "block" is the only legacy deny Claude/Codex validate; allow = empty stdout. */
288
+ function preToolUseDeny(reason: string): HookOutput {
289
+ return {
290
+ decision: "block",
291
+ reason,
292
+ hookSpecificOutput: {
293
+ hookEventName: "PreToolUse",
294
+ permissionDecision: "deny",
295
+ permissionDecisionReason: reason,
296
+ },
297
+ };
298
+ }
299
+
300
+ /**
301
+ * How each harness takes a deny and a rewrite. Codex drops `updatedInput` without `allow`, while Claude
302
+ * reads `allow` as approve-without-prompt, so only Codex gets it. Antigravity merges `overwrite` into its args.
303
+ * Cursor denies with its own top-level reply and takes Claude's nested `updatedInput` only while no top-level
304
+ * field is set, so its rewrite carries no `permission`.
305
+ */
306
+ /** How one harness takes a deny and a rewrite of `args` into `next`. */
307
+ interface Answers {
308
+ deny: (reason: string) => HookOutput;
309
+ rewrite: (args: ToolArgs | null, next: ToolArgs) => HookOutput;
310
+ }
311
+
312
+ const nestedRewrite = (_args: ToolArgs | null, next: ToolArgs): HookOutput => ({
313
+ hookSpecificOutput: { hookEventName: "PreToolUse", updatedInput: next },
314
+ });
315
+ const ANSWERS: Record<Harness, Answers> = {
316
+ claude: {
317
+ deny: preToolUseDeny,
318
+ rewrite: nestedRewrite,
319
+ },
320
+ cursor: {
321
+ deny: (reason: string) => ({ permission: "deny", user_message: reason, agent_message: reason }),
322
+ rewrite: nestedRewrite,
323
+ },
324
+ codex: {
325
+ deny: preToolUseDeny,
326
+ rewrite: (_args, next) => ({
327
+ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow", updatedInput: next },
328
+ }),
329
+ },
330
+ antigravity: {
331
+ deny: (reason: string) => ({ decision: "deny", reason }),
332
+ rewrite: (args, next) => ({
333
+ decision: "allow",
334
+ overwrite: Object.fromEntries(Object.entries(next).filter(([k, v]) => !args || args[k] !== v)),
335
+ }),
336
+ },
337
+ };
338
+
339
+ export function answerDeny(data: HookInput, reason: string): HookOutput {
340
+ return ANSWERS[harnessOf(data)].deny(reason);
341
+ }
342
+
343
+ export function answerRewrite(data: HookInput, next: ToolArgs): HookOutput {
344
+ return ANSWERS[harnessOf(data)].rewrite(toolCallOf(data).args, next);
345
+ }
346
+
347
+ /**
348
+ * A Write aimed at any `.tmp/repo/` other than the root's goes to `<root>/.tmp/repo/<rest>`.
349
+ * Relative paths resolve against the hook cwd. Null when it is already root scratch or not scratch.
350
+ */
351
+ export function rewriteScratchPath(
352
+ filePath: unknown,
353
+ roots: Roots | null | undefined,
354
+ p: typeof path = path,
355
+ ): string | null {
356
+ if (filePath == null || !roots) return null;
357
+ const raw = String(filePath);
358
+ if (!raw) return null;
359
+ const destRoot = p.resolve(roots.root, ".tmp", "repo");
360
+ const absIn = resolveTyped(roots, raw, p);
361
+ const inRoot = p.relative(destRoot, absIn);
362
+ if (!inRoot.startsWith("..") && !p.isAbsolute(inRoot)) return null;
363
+
364
+ // Inside the root, look below it, so a root that itself sits under some `.tmp/repo` is not matched.
365
+ const fromRoot = p.relative(roots.root, absIn);
366
+ const inside = !fromRoot.startsWith("..") && !p.isAbsolute(fromRoot);
367
+ const posix = (inside ? fromRoot : raw).replace(/\\/g, "/");
368
+ const marker = /(^|\/)\.tmp\/repo\//.exec(posix);
369
+ if (!marker) return null;
370
+
371
+ const rel = posix.slice(marker.index + marker[0].length);
372
+ if (!rel) return null;
373
+ if (p.posix.isAbsolute(rel) || rel.startsWith("/")) return null;
374
+ const relNorm = p.posix.normalize(rel);
375
+ if (
376
+ !relNorm ||
377
+ relNorm === "." ||
378
+ relNorm === ".." ||
379
+ rel.split("/").includes("..") ||
380
+ relNorm.split("/").includes("..")
381
+ ) {
382
+ return null;
383
+ }
384
+
385
+ const dest = p.resolve(destRoot, relNorm);
386
+ const check = p.relative(destRoot, dest);
387
+ if (!check || check.startsWith("..") || p.isAbsolute(check)) return null;
388
+ return dest;
389
+ }
390
+
391
+ // Antigravity sends its session id only as `conversationId` (phase 6 probe).
392
+ export function sessionIdOf(data: Payload): string {
393
+ const sid = String((data && (data.sessionId || data.session_id || data.conversationId)) || "").trim();
394
+ if (!sid || sid === "default") return "";
395
+ return sid;
396
+ }
397
+
398
+ export function wroteListPath(workspaceRoot: string | null | undefined, sid: string): string {
399
+ if (!workspaceRoot || !sid) return "";
400
+ return path.join(workspaceRoot, ".tmp", "repo", "sessions", sid, "wrote.txt");
401
+ }
402
+
403
+ /** Absolute posix key for a file this session wrote, or "" when it is outside the root and checkout. */
404
+ function wroteKey(roots: Roots, filePath: string): string {
405
+ const abs = resolveTyped(roots, filePath);
406
+ const inside = (base: string) => {
407
+ const rel = path.relative(base, abs);
408
+ return !rel.startsWith("..") && !path.isAbsolute(rel);
409
+ };
410
+ if (!inside(roots.root) && !inside(roots.checkout)) return "";
411
+ return abs.replace(/\\/g, "/");
412
+ }
413
+
414
+ export function recordWrote(roots: Roots | null | undefined, sid: string, filePath: string): void {
415
+ if (!roots || !sid || !filePath) return;
416
+ const key = wroteKey(roots, filePath);
417
+ if (!key) return;
418
+ const dest = wroteListPath(roots.root, sid);
419
+ try {
420
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
421
+ } catch {
422
+ return;
423
+ }
424
+ let cur = "";
425
+ try {
426
+ cur = fs.readFileSync(dest, "utf8");
427
+ } catch {
428
+ cur = "";
429
+ }
430
+ const lines = cur
431
+ .split(/\n/)
432
+ .map((l) => l.trim())
433
+ .filter(Boolean);
434
+ if (lines.includes(key)) return;
435
+ lines.push(key);
436
+ fs.writeFileSync(dest, lines.join("\n") + "\n");
437
+ }
438
+
439
+ export function wasWroteThisSession(roots: Roots | null | undefined, sid: string, filePath: string): boolean {
440
+ if (!roots || !sid || !filePath) return false;
441
+ let cur = "";
442
+ try {
443
+ cur = fs.readFileSync(wroteListPath(roots.root, sid), "utf8");
444
+ } catch {
445
+ return false;
446
+ }
447
+ const key = wroteKey(roots, filePath);
448
+ return Boolean(key) && cur.split(/\n/).some((l) => l.trim() === key);
449
+ }
450
+
451
+ const SCRATCH_ROOT_RE = /^(dump|scratch|notes|output)(\.|$)/i;
452
+
453
+ /** Stray scratch names and `tmp/` dirs at the checkout top go under the root's `.tmp/repo`. */
454
+ export function rewriteScratchWrite(
455
+ filePath: unknown,
456
+ roots: Roots | null | undefined,
457
+ p: typeof path = path,
458
+ ): string | null {
459
+ const existing = rewriteScratchPath(filePath, roots, p);
460
+ if (existing) return existing;
461
+ if (typeof filePath !== "string" || !roots) return null;
462
+ const wsRoot = p.resolve(roots.checkout);
463
+ const scratch = p.resolve(roots.root, ".tmp", "repo");
464
+ const abs = resolveTyped(roots, filePath, p);
465
+ const rel = p.relative(wsRoot, abs).replace(/\\/g, "/");
466
+ if (!rel || rel.startsWith("..") || p.isAbsolute(rel)) return null;
467
+ const segs = rel.split("/");
468
+ const base = path.posix.basename(rel);
469
+ if (PRODUCT_ROOT_NAMES.has(base) && segs.length === 1) return null;
470
+ if (PRODUCT_ROOT_DIRS.has(segs[0]!)) return null; // rel is non-empty, so segs has an element
471
+ if (rel.startsWith(".tmp/repo/")) return null;
472
+ if (segs[0] === "tmp" || (segs[0] === ".tmp" && segs[1] !== "repo")) {
473
+ const rest = segs[0] === "tmp" ? segs.slice(1) : segs.slice(2);
474
+ const dest = p.resolve(scratch, ...rest);
475
+ const check = p.relative(scratch, dest);
476
+ if (!check || check.startsWith("..") || p.isAbsolute(check)) return null;
477
+ return dest;
478
+ }
479
+ if (segs.length === 1 && (SCRATCH_ROOT_NAMES.has(base) || /\.tmp$/i.test(base) || SCRATCH_ROOT_RE.test(base))) {
480
+ return p.resolve(scratch, base);
481
+ }
482
+ return null;
483
+ }
484
+
485
+ const PATCH_PATH_RE = /^\*\*\* (Add File|Update File|Delete File|Move to): (.+)$/gm;
486
+ const PATCH_HEREDOC_RE = /^\s*apply_patch\s*<<\s*(['"]?)(\w+)\1[ \t]*\n([\s\S]*?)\n\2\s*$/;
487
+
488
+ /** The patch inside a Codex `apply_patch <<'EOF' … EOF` shell command, else null. */
489
+ export function patchOfCommand(command: unknown): string | null {
490
+ const m = PATCH_HEREDOC_RE.exec(String(command || ""));
491
+ return m ? m[3]! : null; // group 3 always participates
492
+ }
493
+
494
+ /**
495
+ * Add and Move headers whose path is scratch get the root scratch path, the same rule a Write gets.
496
+ * Update and Delete headers stay, since they act on a file that exists where it is.
497
+ * Returns the rewritten patch (null when nothing moved) and every final path.
498
+ */
499
+ export function rewritePatch(
500
+ patch: unknown,
501
+ roots: Roots,
502
+ p: typeof path = path,
503
+ ): { patch: string | null; paths: string[] } {
504
+ const paths: string[] = [];
505
+ let changed = false;
506
+ const next = String(patch).replace(PATCH_PATH_RE, (line: string, op: string, target: string) => {
507
+ const dest = op === "Add File" || op === "Move to" ? rewriteScratchWrite(target, roots, p) : null;
508
+ if (op !== "Delete File") paths.push(dest || resolveTyped(roots, target, p));
509
+ if (!dest) return line;
510
+ changed = true;
511
+ return `*** ${op}: ${dest}`;
512
+ });
513
+ return { patch: changed ? next : null, paths };
514
+ }
515
+
516
+ /** Every file a write or patch lands on, in the roots' spelling, after the scratch rewrite has moved what it moves. */
517
+ function writeTargets(tool: ToolSpec, input: ToolArgs | null, patch: string | null, roots: Roots): string[] {
518
+ if (tool.kind === "write") {
519
+ return WRITE_PATH_FIELDS.flatMap((k) => {
520
+ const v = input?.[k];
521
+ return typeof v === "string" && v ? [rewriteScratchWrite(v, roots) || resolveTyped(roots, v)] : [];
522
+ });
523
+ }
524
+ // Every caller that is not a write holds a patch, and both groups of PATCH_PATH_RE always participate in a match.
525
+ return [...String(patch).matchAll(PATCH_PATH_RE)].map(
526
+ ([, op, target]) =>
527
+ (op === "Add File" || op === "Move to" ? rewriteScratchWrite(target!, roots) : null) ||
528
+ resolveTyped(roots, target!),
529
+ );
530
+ }
531
+
532
+ /** The strategy a plan's frontmatter holds when it is a review one, else null. */
533
+ function reviewStrategyOf(text: string): "review-fast" | "review-deep" | null {
534
+ const front = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
535
+ const line = front && /^strategy:[ \t]*(\S+)/m.exec(front[1]!); // group 1 always participates
536
+ const strategy = line && parseStrategy(line[1]);
537
+ return strategy && strategy !== "default" ? strategy : null;
538
+ }
539
+
540
+ /** A parent's save of a plan that holds a review strategy points the session at it: the Write content, else the file on disk. */
541
+ function stampReviewPlan(tool: ToolSpec, input: ToolArgs, targets: string[], roots: Roots, sid: string): void {
542
+ const plans = canonicalPath(path.join(roots.root, ".tmp", "repo", "plans"));
543
+ const given = [input.content, input.Content, input.contents, input.file_text].find(
544
+ (v): v is string => typeof v === "string",
545
+ );
546
+ for (const target of targets) {
547
+ if (path.dirname(target) !== plans || path.extname(target) !== ".md") continue;
548
+ const text = tool.kind === "write" && given !== undefined ? given : fs.readFileSync(target, "utf8");
549
+ if (!reviewStrategyOf(text)) continue;
550
+ writeStamp(roots.root, sid, target);
551
+ return;
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Stamp the session for a full-content Write of a plan. orchestrate-parent persists that Write itself and answers
557
+ * before the gate runs, so it calls this first; every other save reaches the gate. Fails open.
558
+ */
559
+ export function stampPlanSave(data: Payload): void {
560
+ try {
561
+ if (!isHookObject(data)) return;
562
+ const sid = sessionIdOf(data);
563
+ if (!sid || !isParentOrchestrator(data)) return;
564
+ const { harness, key, args: input } = toolCallOf(data);
565
+ const tool = TOOLS[harness].get(key);
566
+ if (!tool || tool.kind !== "write" || !input) return;
567
+ const roots = resolveRoots(data);
568
+ stampReviewPlan(tool, input, writeTargets(tool, input, null, roots), roots, sid);
569
+ } catch {
570
+ // A stamp error never blocks a save.
571
+ }
572
+ }
573
+
574
+ /** The git grant words the user's latest message named, from the session's `git-asked.json`; empty when anything is missing or malformed. */
575
+ function gitAskedBy(root: string, sid: string): Set<string> {
576
+ try {
577
+ if (!sid) return new Set();
578
+ const parsed: unknown = JSON.parse(
579
+ fs.readFileSync(path.join(path.dirname(stampPath(root, sid)), "git-asked.json"), "utf8"),
580
+ );
581
+ // A file that is not an object throws in the destructure the gate always used, which lands in the catch below.
582
+ if (!isPlainObject(parsed)) return new Set();
583
+ const words = parsed.words;
584
+ return new Set(Array.isArray(words) ? words.filter((w): w is string => typeof w === "string") : []);
585
+ } catch {
586
+ return new Set();
587
+ }
588
+ }
589
+
590
+ /**
591
+ * The deny reason for a write or patch that lands in the main checkout, or a shell command that commits or destroys
592
+ * work there or hits shared refs. Ahead of every early return, so scouts, implementers and every harness are held to it.
593
+ * An error in here lets the call through, as the gate does everywhere else.
594
+ * Antigravity has no prompt event, so its git grant is the words in the call's own `lastUserInput` (empty in a
595
+ * subagent's calls); every other harness's grant is the session's `git-asked.json`.
596
+ */
597
+ function sharedCheckoutReason(
598
+ tool: ToolSpec,
599
+ input: ToolArgs,
600
+ patch: string | null,
601
+ text: string,
602
+ roots: Roots,
603
+ sid: string,
604
+ shellCwd: string,
605
+ harness: Harness,
606
+ lastUserInput: string | undefined,
607
+ ): string | null {
608
+ try {
609
+ if (tool.kind === "write" || patch) {
610
+ for (const target of writeTargets(tool, input, patch, roots)) {
611
+ const reason = mainCheckoutWriteReason(target, roots);
612
+ if (reason) return reason;
613
+ }
614
+ }
615
+ if (tool.kind === "bash" && /\bgit\b/.test(text)) {
616
+ const asked = harness === "antigravity" ? new Set(gitAskedWords(lastUserInput)) : gitAskedBy(roots.root, sid);
617
+ return sharedGitReason(text, roots, shellCwd, asked);
618
+ }
619
+ return null;
620
+ } catch {
621
+ return null;
622
+ }
623
+ }
624
+
625
+ export const REASON_IMPLEMENTER_WRITE =
626
+ "implementer writes product files and tests only; the reviewer writes the plan and the journal";
627
+ export const REASON_IMPLEMENTER_GIT =
628
+ "implementer never stages or commits; report the changed files and the reviewer stages them";
629
+ const GIT_STAGE = /(?:^|[\s;&|(])git(?:\s+(?:-[Cc]\s+\S+|--[\w-]+(?:=\S+)?|-\w))*\s+(?:add|commit)(?![\w-])/;
630
+
631
+ /** True for a child spawned from an implementer-* agent. */
632
+ export function isImplementer(sub: unknown): boolean {
633
+ return String(sub).startsWith("implementer-");
634
+ }
635
+
636
+ /** True when `target` is not inside `<root>/.tmp`; both in the roots' spelling. */
637
+ export function isOutsideRootTmp(target: string, root: string, p: typeof path = path): boolean {
638
+ return isOutside(p, canonicalPath(p.resolve(root, ".tmp"), p), target);
639
+ }
640
+
641
+ function isOutside(p: typeof path, dir: string, target: string): boolean {
642
+ const rel = p.relative(dir, target);
643
+ return rel === ".." || rel.startsWith(`..${p.sep}`) || p.isAbsolute(rel);
644
+ }
645
+
646
+ const reviewDenyReason = (review: ActiveReview): string =>
647
+ `plan ${review.serial} runs strategy ${review.strategy}: this session reviews. Spawn ${implementerOf(review.strategy, 1)} for product writes; the plan, the journal and .tmp/ stay writable.`;
648
+
649
+ /**
650
+ * What a review strategy asks of a write, and of an implementer's shell. Ahead of the write branch and the scout
651
+ * return, so no early return skips it. An error in here lets the call through, as the gate does everywhere else.
652
+ * implementer-* never writes the plan or the journal, never runs git add or git commit (every strategy)
653
+ * parent a save of a review plan stamps the session; while that plan runs, no write outside `<root>/.tmp`
654
+ */
655
+ function reviewGuard(
656
+ data: HookInput,
657
+ tool: ToolSpec,
658
+ input: ToolArgs,
659
+ patch: string | null,
660
+ roots: Roots,
661
+ sid: string,
662
+ harness: Harness,
663
+ command: string,
664
+ ): GateResult {
665
+ try {
666
+ const sub = subagentTypeOf(data);
667
+ const implementer = isImplementer(sub);
668
+ if (implementer && tool.kind === "bash" && GIT_STAGE.test(command))
669
+ return { type: "deny", reason: REASON_IMPLEMENTER_GIT };
670
+ if (tool.kind !== "write" && !patch) return null;
671
+ if (!implementer && !(sid && isParentOrchestrator(data))) return null;
672
+ const targets = writeTargets(tool, input, patch, roots);
673
+ if (implementer) {
674
+ const owned = ["plans", "implementations"].map((d) => canonicalPath(path.join(roots.root, ".tmp", "repo", d)));
675
+ const hit = targets.some((t) => owned.some((dir) => !isOutside(path, dir, t)));
676
+ return hit ? { type: "deny", reason: REASON_IMPLEMENTER_WRITE } : null;
677
+ }
678
+ stampReviewPlan(tool, input, targets, roots, sid);
679
+ const review = ENFORCED_HARNESSES.has(harness) ? activeReview(roots.root, sid) : null;
680
+ if (review && targets.some((t) => isOutsideRootTmp(t, roots.root)))
681
+ return { type: "deny", reason: reviewDenyReason(review) };
682
+ return null;
683
+ } catch {
684
+ return null;
685
+ }
686
+ }
687
+
688
+ /** POSIX single-quote so `, $, and \ stay literal. */
689
+ export function quoteQuery(q: unknown): string {
690
+ return `'${String(q).replace(/'/g, "'\\''")}'`;
691
+ }
692
+
693
+ export interface RepoBin {
694
+ bin: string;
695
+ /** True when the bin is the main checkout's because the worktree has no install of its own. */
696
+ fallback: boolean;
697
+ }
698
+
699
+ /** The first of `repo`, `repo.exe` that exists under `dir`'s `node_modules/.bin`. */
700
+ function installedBin(dir: string): string | undefined {
701
+ const plain = path.join(dir, "node_modules", ".bin", "repo");
702
+ return [plain, `${plain}.exe`].find((c) => fs.existsSync(c));
703
+ }
704
+
705
+ /**
706
+ * The installed `repo` bin a hook runs, in the order `hookCommand` probes: the checkout, its toplevel, then the main
707
+ * checkout of a linked worktree, each as `repo` then `repo.exe`. With no install anywhere it names the checkout's own
708
+ * path. Absolute, because the rewritten command runs from the agent's cwd.
709
+ */
710
+ export function resolveRepoBin(checkout: string, p: typeof path = path): RepoBin {
711
+ const own = p.join(checkout, "node_modules", ".bin", "repo");
712
+ // A foreign path flavor (a `path.win32` test on Linux) names no folder git or the disk can answer for.
713
+ if (p !== path) return { bin: own, fallback: false };
714
+ const facts = gitRevParse(checkout);
715
+ const found = installedBin(checkout) ?? (facts ? installedBin(facts.top) : undefined);
716
+ if (found) return { bin: found, fallback: false };
717
+ const main = facts ? installedBin(path.dirname(facts.commonDir)) : undefined;
718
+ return main ? { bin: main, fallback: true } : { bin: own, fallback: false };
719
+ }
720
+
721
+ /** The installed `repo` bin for `checkout`; see `resolveRepoBin`. */
722
+ export function repoBin(checkout: string, p: typeof path = path): string {
723
+ return resolveRepoBin(checkout, p).bin;
724
+ }
725
+
726
+ /** Bash eats unquoted backslashes, so a command an agent runs spells every path with forward slashes. */
727
+ function shellPath(p: string): string {
728
+ return p.split(path.sep).join("/");
729
+ }
730
+
731
+ export function jobCommand(workspaceRoot: string, args: string): string {
732
+ return `${shellPath(repoBin(workspaceRoot))} job ${args}`;
733
+ }
734
+
735
+ function asNumber(v: unknown): number | null {
736
+ if (v == null || v === "") return null;
737
+ const n = Number(v);
738
+ return Number.isFinite(n) ? n : null;
739
+ }
740
+
741
+ function requestedLimit(input: ToolArgs, snake: string, camel: string): number | null {
742
+ return asNumber(input[snake] ?? input[camel]);
743
+ }
744
+
745
+ function hasLimitField(input: ToolArgs, snake: string, camel: string): boolean {
746
+ return Object.prototype.hasOwnProperty.call(input, snake) || Object.prototype.hasOwnProperty.call(input, camel);
747
+ }
748
+
749
+ function usesCamelInput(input: ToolArgs): boolean {
750
+ return Object.keys(input).some(
751
+ (k) =>
752
+ k === "targetFile" ||
753
+ k === "filePath" ||
754
+ k === "headLimit" ||
755
+ k === "targetDirectory" ||
756
+ k === "globPattern" ||
757
+ k === "ignoreGlobs",
758
+ );
759
+ }
760
+
761
+ function setMissingLimit(next: ToolArgs, input: ToolArgs, snake: string, camel: string, value: number): boolean {
762
+ if (hasLimitField(input, snake, camel)) return false;
763
+ if (usesCamelInput(input) && snake !== camel) next[camel] = value;
764
+ else next[snake] = value;
765
+ return true;
766
+ }
767
+
768
+ export const GLOB_IGNORE: string[] = [...DENY_PATH_SEGMENTS, ".git"];
769
+
770
+ function setMissingIgnore(next: ToolArgs, input: ToolArgs): boolean {
771
+ if (hasLimitField(input, "ignore", "ignoreGlobs")) return false;
772
+ // A search rooted in the stock references would otherwise ignore its own base.
773
+ const list = collectPathValues(input).some(inStockReferences)
774
+ ? GLOB_IGNORE.filter((seg) => seg !== "node_modules")
775
+ : GLOB_IGNORE.slice();
776
+ if (usesCamelInput(input)) next.ignoreGlobs = list;
777
+ else next.ignore = list;
778
+ return true;
779
+ }
780
+
781
+ function collectPathValues(input: ToolArgs): string[] {
782
+ const out: string[] = [];
783
+ for (const key of PATH_FIELDS) {
784
+ const v = input[key];
785
+ if (typeof v === "string" && v) out.push(v);
786
+ }
787
+ return out;
788
+ }
789
+
790
+ function readFilePath(input: ToolArgs): unknown {
791
+ return input.target_file || input.file_path || input.targetFile || input.filePath || "";
792
+ }
793
+
794
+ export function resolveReadPath(filePath: unknown, roots: Roots): string | null {
795
+ if (!filePath || typeof filePath !== "string") return null;
796
+ return resolveTyped(roots, filePath);
797
+ }
798
+
799
+ export function fileSizeBytes(absPath: string | null | undefined): number | null {
800
+ if (!absPath) return null;
801
+ try {
802
+ const st = fs.statSync(absPath);
803
+ if (!st.isFile()) return null;
804
+ return st.size;
805
+ } catch {
806
+ return null;
807
+ }
808
+ }
809
+
810
+ export function bashTargetsDenylist(command: unknown): boolean {
811
+ const cmd = String(command || "");
812
+ if (!cmd.trim()) return false;
813
+ if (!BASH_DUMP_CMDS.test(cmd) && !DENY_LOCKFILES_IN_CMD(cmd)) return false;
814
+ if (DENY_LOCKFILES_IN_CMD(cmd) && BASH_DUMP_CMDS.test(cmd)) return true;
815
+ if (gitDirIn(cmd) && BASH_DUMP_CMDS.test(cmd)) return true;
816
+ if (!BASH_DUMP_CMDS.test(cmd)) return false;
817
+ return cmd.split(/[\s'"`|&;$(){}<>]+/).some((token) => pathIsDenied(token));
818
+ }
819
+
820
+ function gitDirIn(s: unknown): boolean {
821
+ return /(^|[\s/])\.git(\/|$)/.test(String(s).replace(/\\/g, "/"));
822
+ }
823
+
824
+ function DENY_LOCKFILES_IN_CMD(cmd: string): boolean {
825
+ return /\bbun\.lock\b/.test(cmd);
826
+ }
827
+
828
+ function escapeRe(s: string): string {
829
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
830
+ }
831
+
832
+ export function alreadyRtk(command: unknown): boolean {
833
+ const first =
834
+ String(command || "")
835
+ .trim()
836
+ .split(/\s+/, 1)[0] || "";
837
+ return first === "rtk" || first.endsWith("/rtk");
838
+ }
839
+
840
+ export function stripFlagValue(cmd: unknown, flag: string): string {
841
+ const f = escapeRe(flag);
842
+ const val = `(?:"[^"]*"|'[^']*'|\\S+)`;
843
+ return String(cmd || "")
844
+ .replace(new RegExp(`${f}=${val}`, "g"), `${flag}=_`)
845
+ .replace(new RegExp(`${f}\\s+${val}`, "g"), `${flag} _`);
846
+ }
847
+
848
+ /**
849
+ * Args after the job command, optionally behind `rtk`: `bun repo job` or `[bun] <checkout>/node_modules/.bin/repo job`.
850
+ * A relative path resolves against the hook cwd, and both sides compare in their canonical spelling.
851
+ */
852
+ function jobRest(command: unknown, roots: Roots | null | undefined, p: typeof path = path): string[] | null {
853
+ const cmd = String(command || "").trim();
854
+ if (!cmd || !roots) return null;
855
+ const parts = cmd.split(/\s+/).filter(Boolean);
856
+ let i = isBin(parts[0], "rtk") ? 1 : 0;
857
+ const viaBun = isBin(parts[i], "bun");
858
+ if (viaBun) i += 1;
859
+ const target = parts[i];
860
+ if (!target) return null;
861
+ if (viaBun && target === "repo") return parts[i + 1] === "job" ? parts.slice(i + 2) : null;
862
+ const abs = resolveTyped(roots, target, p);
863
+ const bin = canonicalPath(repoBin(roots.checkout, p), p).replace(/\.exe$/, "");
864
+ if (abs !== bin && abs !== `${bin}.exe`) return null;
865
+ return parts[i + 1] === "job" ? parts.slice(i + 2) : null;
866
+ }
867
+
868
+ /** True when `token` is the bare command `name` or a path ending in it, with POSIX or Windows separators. */
869
+ function isBin(token: string | undefined, name: string): boolean {
870
+ return token === name || new RegExp(`[\\\\/]${name}(?:\\.exe)?$`).test(token || "");
871
+ }
872
+
873
+ /** True when argv runs the workspace's job command (flags may be messy). */
874
+ export function isWorkspaceJobScript(
875
+ command: unknown,
876
+ roots: Roots | null | undefined,
877
+ p: typeof path = path,
878
+ ): boolean {
879
+ return jobRest(command, roots, p) != null;
880
+ }
881
+
882
+ export function parseJobCommand(
883
+ command: unknown,
884
+ roots: Roots | null | undefined,
885
+ ): { job: string; hasQuery: boolean; hasStash: boolean } | null {
886
+ const cmd = String(command || "").trim();
887
+ if (!cmd || !roots) return null;
888
+ if (SHELL_META.test(stripFlagValue(cmd, "--query"))) return null;
889
+ const rest = jobRest(cmd, roots);
890
+ if (!rest) return null;
891
+ let job = "";
892
+ let hasQuery = false;
893
+ let hasStash = false;
894
+ for (let j = 0; j < rest.length; j++) {
895
+ const a = rest[j]!; // j < rest.length
896
+ let flag = a;
897
+ let inline = false;
898
+ if (a.startsWith("--") && a.includes("=")) {
899
+ flag = a.slice(0, a.indexOf("="));
900
+ inline = true;
901
+ }
902
+ if (!JOB_FLAGS.has(flag)) return null;
903
+ if (!inline) j += 1;
904
+ if (flag === "--job") {
905
+ job = String(inline ? a.slice(a.indexOf("=") + 1) : rest[j] || "").toLowerCase();
906
+ } else if (flag === "--query") {
907
+ hasQuery = true;
908
+ } else if (flag === "--stash") {
909
+ hasStash = true;
910
+ }
911
+ }
912
+ return { job, hasQuery, hasStash };
913
+ }
914
+
915
+ export function processFatTools(data: Payload): GateResult {
916
+ if (!isHookObject(data)) return null;
917
+ const { harness, key, args: input } = toolCallOf(data);
918
+ const tool = TOOLS[harness].get(key);
919
+ if (!tool || !input) return null;
920
+ const roots = resolveRoots(data);
921
+ const sid = sessionIdOf(data);
922
+ // A Codex patch is a write, whether sent as the apply_patch tool or as a shell heredoc.
923
+ const field = tool.kind === "patch" ? "command" : tool.command;
924
+ const fieldValue = field ? input[field] : undefined;
925
+ const text = typeof fieldValue === "string" ? fieldValue : "";
926
+ const patch = tool.kind === "patch" ? text : tool.kind === "bash" ? patchOfCommand(text) : null;
927
+ const cwdValue = tool.cwd ? input[tool.cwd] : undefined;
928
+ const shellCwd = typeof cwdValue === "string" ? resolveTyped(roots, cwdValue) : roots.cwd;
929
+ // Ahead of every early return, so scout subagents are held to it too.
930
+ const redundant = redundantIgnoreReason(
931
+ tool.kind === "write"
932
+ ? writeIgnoreEdits(input, roots.cwd, WRITE_PATH_FIELDS)
933
+ : patch
934
+ ? patchIgnoreEdits(patch, shellCwd)
935
+ : tool.kind === "bash" && text.includes(".gitignore")
936
+ ? shellIgnoreEdits(text, shellCwd)
937
+ : [],
938
+ );
939
+ if (redundant) return { type: "deny", reason: redundant };
940
+ const stray = tool.kind === "bash" && text.includes("worktree") ? worktreeAddReason(text, roots, shellCwd) : null;
941
+ if (stray) return { type: "deny", reason: stray };
942
+ const shared = sharedCheckoutReason(tool, input, patch, text, roots, sid, shellCwd, harness, data.lastUserInput);
943
+ if (shared) return { type: "deny", reason: shared };
944
+ const guarded = reviewGuard(data, tool, input, patch, roots, sid, harness, text);
945
+ if (guarded) return guarded;
946
+ if (tool.kind === "write") {
947
+ const next = Object.assign({}, input);
948
+ const wrote: string[] = [];
949
+ let changed = false;
950
+ for (const key of WRITE_PATH_FIELDS) {
951
+ const current = next[key];
952
+ if (typeof current !== "string" || !current) continue;
953
+ const dest = rewriteScratchWrite(current, roots);
954
+ wrote.push(dest || current);
955
+ if (!dest || dest === current) continue;
956
+ // orchestrate-parent redirects the same `.tmp/repo` path, persists it, and answers the Write itself.
957
+ if (tool.persistShared && dest === rewriteScratchPath(current, roots) && persistOwner(dest, roots.root)) continue;
958
+ next[key] = dest;
959
+ changed = true;
960
+ }
961
+ if (sid) for (const p of wrote) recordWrote(roots, sid, p);
962
+ return changed ? { type: "rewrite", updatedInput: next } : null;
963
+ }
964
+ if (patch) {
965
+ const out = rewritePatch(patch, roots);
966
+ if (sid) for (const p of out.paths) recordWrote(roots, sid, p);
967
+ if (!out.patch) return null;
968
+ return { type: "rewrite", updatedInput: { ...input, [field!]: text.replace(patch, () => out.patch!) } }; // a patch only comes from tools that name a command field
969
+ }
970
+ if (tool.kind === "patch") return null;
971
+ const sub = subagentTypeOf(data);
972
+ if (sub === "scout" || sub === "researcher") return null;
973
+
974
+ const parent = isParentOrchestrator(data);
975
+ const isRead = tool.kind === "read";
976
+ const isGrep = tool.kind === "grep";
977
+ const isGlob = tool.kind === "glob";
978
+
979
+ if (isRead || isGrep || isGlob) {
980
+ for (const v of collectPathValues(input)) {
981
+ if (pathIsDenied(v)) return { type: "deny", reason: REASON_DENYLIST };
982
+ }
983
+ if (isGlob) {
984
+ const raw = input.target_directory || input.targetDirectory || input.path || "";
985
+ if (typeof raw === "string" && raw) {
986
+ const abs = path.isAbsolute(raw) ? raw : path.resolve(roots.cwd, raw);
987
+ if (pathIsDenied(abs) || pathIsDenied(path.basename(abs))) {
988
+ return { type: "deny", reason: REASON_DENYLIST };
989
+ }
990
+ }
991
+ }
992
+
993
+ if (isRead) {
994
+ const abs = resolveReadPath(readFilePath(input), roots);
995
+ const size = fileSizeBytes(abs);
996
+ if (size != null && size > DENY_FILE_BYTES) {
997
+ return { type: "deny", reason: REASON_FILE_SIZE };
998
+ }
999
+ const hasSlice = hasLimitField(input, "offset", "offset") || hasLimitField(input, "limit", "limit");
1000
+ if (sid && abs && !hasSlice && wasWroteThisSession(roots, sid, abs)) {
1001
+ return { type: "deny", reason: rereadReasonFor(harness, abs, roots.checkout) };
1002
+ }
1003
+ }
1004
+
1005
+ const grepDefault = parent ? DEFAULT_GREP_HEAD_LIMIT : CHILD_GREP_HEAD_LIMIT;
1006
+ const readDefault = parent ? DEFAULT_READ_LIMIT : CHILD_READ_LIMIT;
1007
+ const next = Object.assign({}, input);
1008
+ let changed = false;
1009
+ if (isGrep && tool.limits !== false) {
1010
+ const n = requestedLimit(input, "head_limit", "headLimit");
1011
+ if (n != null && n > MAX_REQUEST_LIMIT) {
1012
+ if (usesCamelInput(input)) next.headLimit = grepDefault;
1013
+ else next.head_limit = grepDefault;
1014
+ changed = true;
1015
+ } else {
1016
+ changed = setMissingLimit(next, input, "head_limit", "headLimit", grepDefault) || changed;
1017
+ }
1018
+ }
1019
+ if (isRead && tool.limits !== false) {
1020
+ const n = requestedLimit(input, "limit", "limit");
1021
+ if (n != null && n > MAX_REQUEST_LIMIT) {
1022
+ next.limit = readDefault;
1023
+ changed = true;
1024
+ } else {
1025
+ changed = setMissingLimit(next, input, "limit", "limit", readDefault) || changed;
1026
+ }
1027
+ }
1028
+ if (isGlob) {
1029
+ changed = setMissingIgnore(next, input) || changed;
1030
+ }
1031
+ return changed ? { type: "rewrite", updatedInput: next } : null;
1032
+ }
1033
+
1034
+ // Every other kind has returned above, so this is a shell call.
1035
+ const cmd = text;
1036
+ if (bashTargetsDenylist(cmd)) {
1037
+ return { type: "deny", reason: REASON_DENYLIST };
1038
+ }
1039
+ if (isWorkspaceJobScript(cmd, roots)) return null;
1040
+ const next = Object.assign({}, input);
1041
+ // A test run's full log goes to the stash and the caller sees the failing lines: for implementers, and for a
1042
+ // parent while its session reviews. job takes one plain command; compound shell passes through.
1043
+ if (
1044
+ TEST_CMD.test(cmd) &&
1045
+ !SHELL_META.test(cmd) &&
1046
+ (isImplementer(sub) || (parent && sid && activeReview(roots.root, sid)))
1047
+ ) {
1048
+ next[field!] = jobCommand(roots.checkout, `--job test --query ${quoteQuery(cmd.trim())}`);
1049
+ return { type: "rewrite", updatedInput: next };
1050
+ }
1051
+ if (BASH_WEB.test(cmd)) {
1052
+ const url = (cmd.match(/https?:\/\/\S+/) || [])[0] || "";
1053
+ if (url) {
1054
+ next[field!] = jobCommand(roots.checkout, `--job fetch --query ${quoteQuery(url)}`);
1055
+ return { type: "rewrite", updatedInput: next };
1056
+ }
1057
+ return { type: "deny", reason: REASON_DENYLIST };
1058
+ }
1059
+ // job and rtk take one plain command; compound shell passes through.
1060
+ const compound = SHELL_META.test(cmd);
1061
+ if (BASH_DUMP_CMDS.test(cmd) && !alreadyRtk(cmd) && !compound) {
1062
+ const job = BASH_SEARCH.test(cmd) ? "search" : "exec";
1063
+ const q = cmd.replace(/^\s*(rtk\s+)?/, "").trim();
1064
+ next[field!] = jobCommand(roots.checkout, `--job ${job} --query ${quoteQuery(q)}`);
1065
+ return { type: "rewrite", updatedInput: next };
1066
+ }
1067
+ if (BASH_TREE.test(cmd) && !alreadyRtk(cmd) && !compound) {
1068
+ next[field!] = `rtk ${cmd.trim()}`; // bash tools name their command field
1069
+ return { type: "rewrite", updatedInput: next };
1070
+ }
1071
+ return null;
1072
+ }
1073
+
1074
+ /** The hook answer for a gate result, in the shape the payload's harness takes. */
1075
+ export function fatHookOutput(data: HookInput, result: GateResult): HookOutput | null {
1076
+ if (!result) return null;
1077
+ return result.type === "deny" ? answerDeny(data, result.reason) : answerRewrite(data, result.updatedInput);
1078
+ }
1079
+
1080
+ export function processHookPayload(data: Payload): HookOutput | null {
1081
+ const result = processFatTools(data);
1082
+ return data && result ? fatHookOutput(data, result) : null;
1083
+ }
1084
+
1085
+ export function main(_argv: string[] = [], io: HookIo = processHookIo()): number {
1086
+ try {
1087
+ const data = readHookInput(io);
1088
+ const out = processHookPayload(data);
1089
+ if (!out) return 0;
1090
+ io.write(JSON.stringify(out));
1091
+ return 0;
1092
+ } catch {
1093
+ return 0;
1094
+ }
1095
+ }