@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,806 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Worktrees live at `<root>/.worktrees/<name>` on branch `wt/<name>` (roadmap D5, D6).
4
+ *
5
+ * bun repo worktree add <name> make or reuse one; prints its path
6
+ * bun repo worktree finish [<name>] rebase it onto the default branch, fast-forward that, remove worktree and branch
7
+ * bun repo worktree list every linked worktree with its state
8
+ * bun repo worktree prune remove clean ones whose own commits reached the default branch
9
+ * bun repo worktree hook Claude Code WorktreeCreate / WorktreeRemove on stdin
10
+ */
11
+ import { spawnSync } from "node:child_process";
12
+ import fs from "node:fs";
13
+ import path from "node:path";
14
+ import { parseHookInput, type HookInput } from "./hook-input.ts";
15
+ import { processHookIo, readAllText, type HookIo } from "./hook-io.ts";
16
+ import { canonicalPath, resolveRoots, type Roots } from "./workspace-root.ts";
17
+
18
+ export const WORKTREES_DIR = ".worktrees";
19
+ const BRANCH_PREFIX = "wt/";
20
+ const USAGE = "usage: bun repo worktree add <name> | finish [<name>] | list | prune | hook";
21
+ /** How people and agents run this script, in every message that names a next step. */
22
+ export const WORKTREE_CMD = "bun repo worktree";
23
+ const MAX_FINISH_TRIES = 3;
24
+ const LOCKFILES = ["bun.lock", "bun.lockb"];
25
+ const INSTALL_TIMEOUT_MS = 120_000;
26
+
27
+ /** A linked worktree as `git worktree list --porcelain` names it. */
28
+ interface WorktreeEntry {
29
+ path: string;
30
+ branch: string | null;
31
+ }
32
+
33
+ /** One `git worktree list` row with its state. */
34
+ export interface WorktreeInfo {
35
+ path: string;
36
+ branch: string | null;
37
+ state: "clean" | "dirty" | "missing";
38
+ merged: boolean;
39
+ managed: boolean;
40
+ }
41
+
42
+ /** What `finishWorktree` landed; `sha` is null when the branch had no commits of its own. */
43
+ export interface FinishResult {
44
+ branch: string;
45
+ path: string;
46
+ defaultBranch: string;
47
+ sha: string | null;
48
+ }
49
+
50
+ /** The hook outcome Claude Code reads: exit code, stdout and stderr. */
51
+ export interface HookResult {
52
+ code: number;
53
+ stdout: string;
54
+ stderr: string;
55
+ }
56
+
57
+ /** The roots a guard reads: `root` always, `checkout` and `cwd` when the caller has them. */
58
+ export type WorktreeRoots = Pick<Roots, "root"> & Partial<Pick<Roots, "checkout" | "cwd">>;
59
+
60
+ /** One shell word and whether it is free of `$`, backticks and a leading `~`. */
61
+ interface Word {
62
+ text: string;
63
+ literal: boolean;
64
+ }
65
+
66
+ /** A git command found in a shell line: the dir it runs in, its subcommand and the words after it. */
67
+ interface GitCommand {
68
+ at: string;
69
+ sub: string;
70
+ rest: Word[];
71
+ }
72
+
73
+ /** A parsed `git worktree add`. */
74
+ interface WorktreeAdd {
75
+ at: string;
76
+ target: Word | null;
77
+ branch: string | null;
78
+ commitish: string | null;
79
+ detach: boolean;
80
+ }
81
+
82
+ /** What a shared-git rule knows about where a command runs. */
83
+ interface GitContext {
84
+ p: typeof path;
85
+ root: string;
86
+ at: string;
87
+ own: string | null;
88
+ inMain: boolean;
89
+ defaultBranch: () => string;
90
+ }
91
+
92
+ type SharedRule = (g: GitCommand, c: GitContext) => string | null;
93
+
94
+ interface Heredoc {
95
+ word: string;
96
+ indented: boolean;
97
+ }
98
+
99
+ function errorMessage(err: unknown): string {
100
+ return err instanceof Error ? err.message : String(err);
101
+ }
102
+
103
+ /** The caller named no worktree and is not inside one: a usage error, exit 2. */
104
+ class UsageError extends Error {}
105
+
106
+ function git(cwd: string, args: string[]): { ok: boolean; out: string; err: string } {
107
+ const r = spawnSync("git", args, { cwd, encoding: "utf8" });
108
+ return { ok: r.status === 0, out: (r.stdout || "").trim(), err: (r.stderr || "").trim() };
109
+ }
110
+
111
+ function samePath(a: string, b: string): boolean {
112
+ return canonicalPath(path.resolve(a)) === canonicalPath(path.resolve(b));
113
+ }
114
+
115
+ /** A git-safe directory and branch name, or null when nothing usable is left. */
116
+ export function worktreeName(raw: unknown): string | null {
117
+ const name = String(raw ?? "")
118
+ .replace(/[^A-Za-z0-9._-]/g, "-")
119
+ .replace(/-+/g, "-")
120
+ .replace(/^[.-]+/, "")
121
+ .replace(/-+$/, "");
122
+ return name || null;
123
+ }
124
+
125
+ /** True when `file` is a direct child of `<root>/.worktrees`, however git spells the path. */
126
+ export function isManaged(file: string, root: string, p: typeof path = path): boolean {
127
+ const rel = p.relative(p.resolve(root, WORKTREES_DIR), p.resolve(file));
128
+ return !!rel && !rel.startsWith("..") && !p.isAbsolute(rel) && !rel.includes(p.sep);
129
+ }
130
+
131
+ /** `git worktree list --porcelain`, main checkout first. */
132
+ function worktreeEntries(root: string): WorktreeEntry[] {
133
+ const r = git(root, ["worktree", "list", "--porcelain"]);
134
+ if (!r.ok) return [];
135
+ return r.out.split(/\n\n+/).map((block): WorktreeEntry => {
136
+ // git opens every block with its `worktree <path>` line.
137
+ const wt = block.split("\n", 1).join("");
138
+ const branch = block.split("\n").find((l) => l.startsWith("branch "));
139
+ return {
140
+ path: canonicalPath(path.resolve(wt.slice("worktree ".length))),
141
+ branch: branch ? branch.slice("branch refs/heads/".length) : null,
142
+ };
143
+ });
144
+ }
145
+
146
+ function branchExists(root: string, branch: string): boolean {
147
+ return git(root, ["show-ref", "--verify", "-q", `refs/heads/${branch}`]).ok;
148
+ }
149
+
150
+ /**
151
+ * The shared default branch, never just whatever the main checkout has out: `origin/HEAD`'s branch, else `init.defaultBranch`,
152
+ * else `main`, else `master` (each only when it exists locally), else the main checkout's current branch, else "HEAD".
153
+ */
154
+ export function defaultBranch(root: string): string {
155
+ const remote = git(root, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]);
156
+ const configured = git(root, ["config", "--get", "init.defaultBranch"]);
157
+ const named = [
158
+ remote.ok ? remote.out.replace(/^origin\//, "") : "",
159
+ configured.ok ? configured.out : "",
160
+ "main",
161
+ "master",
162
+ ];
163
+ const found = named.find((b) => b && branchExists(root, b));
164
+ if (found) return found;
165
+ const head = git(root, ["symbolic-ref", "--short", "HEAD"]);
166
+ return head.ok ? head.out : "HEAD";
167
+ }
168
+
169
+ export function isMerged(root: string, branch: string): boolean {
170
+ return git(root, ["merge-base", "--is-ancestor", `refs/heads/${branch}`, defaultBranch(root)]).ok;
171
+ }
172
+
173
+ /** The branch moved after it was created; no reflog counts as no, so a fresh worktree is never pruned. */
174
+ export function hasOwnCommits(root: string, branch: string): boolean {
175
+ const r = git(root, ["reflog", "show", "--format=%gs", `refs/heads/${branch}`]);
176
+ return r.ok && r.out.split("\n").some((l) => l && !l.startsWith("branch: Created from"));
177
+ }
178
+
179
+ /** Copy each gitignored file `.worktreeinclude` matches that the worktree lacks; never overwrite. */
180
+ function copyIncluded(root: string, dest: string): void {
181
+ if (!fs.existsSync(path.join(root, ".worktreeinclude"))) return;
182
+ const listed = git(root, ["ls-files", "--others", "--ignored", "--exclude-from=.worktreeinclude"]);
183
+ const candidates = listed.out.split("\n").filter((rel) => rel && !rel.startsWith(`${WORKTREES_DIR}/`));
184
+ if (!candidates.length) return;
185
+ const ignored = spawnSync("git", ["check-ignore", "--stdin"], {
186
+ cwd: root,
187
+ input: candidates.join("\n"),
188
+ encoding: "utf8",
189
+ });
190
+ for (const rel of (ignored.stdout || "").split("\n").filter(Boolean)) {
191
+ const to = path.join(dest, rel);
192
+ if (fs.existsSync(to)) continue;
193
+ fs.mkdirSync(path.dirname(to), { recursive: true });
194
+ fs.copyFileSync(path.join(root, rel), to);
195
+ }
196
+ }
197
+
198
+ function requireName(raw: unknown): string {
199
+ const name = worktreeName(raw);
200
+ if (!name)
201
+ throw new Error(`worktree name "${raw ?? ""}" has no usable characters; use letters, digits, ".", "_" or "-"`);
202
+ return name;
203
+ }
204
+
205
+ /** Make or reuse `<root>/.worktrees/<name>` on `wt/<name>`; throws a one-line message when it cannot. */
206
+ export function addWorktree(root: string, raw: unknown): { path: string; branch: string; created: boolean } {
207
+ const name = requireName(raw);
208
+ const dest = path.join(root, WORKTREES_DIR, name);
209
+ const branch = BRANCH_PREFIX + name;
210
+ let known: WorktreeEntry | undefined = worktreeEntries(root)
211
+ .slice(1)
212
+ .find((e) => samePath(e.path, dest));
213
+ if (known && !fs.existsSync(dest)) {
214
+ git(root, ["worktree", "prune"]);
215
+ known = undefined;
216
+ }
217
+ if (!known) {
218
+ if (fs.existsSync(dest))
219
+ throw new Error(`${dest} exists and is not a worktree of this repo; move it or pick another name`);
220
+ const args = branchExists(root, branch)
221
+ ? ["worktree", "add", dest, branch]
222
+ : ["worktree", "add", "-b", branch, dest, "HEAD"];
223
+ // git's own output goes to stderr, so stdout carries only the path.
224
+ if (spawnSync("git", args, { cwd: root, stdio: ["ignore", 2, 2] }).status !== 0)
225
+ throw new Error(`git worktree add failed for ${dest}`);
226
+ }
227
+ copyIncluded(root, dest);
228
+ if (!known) installDependencies(root, dest);
229
+ return { path: canonicalPath(dest), branch, created: !known };
230
+ }
231
+
232
+ /**
233
+ * A new worktree has no `node_modules`, so tools that look up `<cwd>/node_modules/.bin` fail there.
234
+ * Install from the root's lockfile; the install's output goes to stderr and a failure only warns.
235
+ */
236
+ function installDependencies(root: string, dest: string): void {
237
+ if (!LOCKFILES.some((f) => fs.existsSync(path.join(root, f)))) return;
238
+ const r = spawnSync("bun", ["install", "--frozen-lockfile"], {
239
+ cwd: dest,
240
+ stdio: ["ignore", 2, 2],
241
+ timeout: INSTALL_TIMEOUT_MS,
242
+ });
243
+ if (r.status !== 0)
244
+ process.stderr.write(`could not install dependencies in ${dest}: run bun install --frozen-lockfile there\n`);
245
+ }
246
+
247
+ export function listWorktrees(root: string): WorktreeInfo[] {
248
+ return worktreeEntries(root)
249
+ .slice(1)
250
+ .map((e): WorktreeInfo => {
251
+ const state = !fs.existsSync(e.path) ? "missing" : git(e.path, ["status", "--porcelain"]).out ? "dirty" : "clean";
252
+ const merged = !!e.branch && hasOwnCommits(root, e.branch) && isMerged(root, e.branch);
253
+ return { path: e.path, branch: e.branch, state, merged, managed: isManaged(e.path, root) };
254
+ });
255
+ }
256
+
257
+ /** git's first message plus its indented detail lines, on one line. */
258
+ function gitReason(err: string): string {
259
+ const lines = err.split("\n");
260
+ const detail: string[] = [];
261
+ for (const line of lines.slice(1)) {
262
+ if (!/^\s/.test(line)) break;
263
+ detail.push(line.trim());
264
+ }
265
+ // split always yields a first line; a git that failed always said why.
266
+ return [lines[0], ...detail].join(" ");
267
+ }
268
+
269
+ function rebaseInProgress(dir: string): boolean {
270
+ const gitDir = git(dir, ["rev-parse", "--path-format=absolute", "--git-dir"]);
271
+ return gitDir.ok && ["rebase-merge", "rebase-apply"].some((d) => fs.existsSync(path.join(gitDir.out, d)));
272
+ }
273
+
274
+ function tipOf(root: string, branch: string): string {
275
+ return git(root, ["rev-parse", "--verify", "-q", `refs/heads/${branch}`]).out;
276
+ }
277
+
278
+ /** Move `target` to `branch`'s tip with a fast-forward only, in the root when it has `target` checked out, else through the ref. */
279
+ function fastForward(root: string, branch: string, target: string): ReturnType<typeof git> {
280
+ const source = `refs/heads/${branch}`;
281
+ const checkedOut = git(root, ["symbolic-ref", "--short", "HEAD"]).out === target;
282
+ return checkedOut
283
+ ? git(root, ["merge", "--ff-only", source])
284
+ : git(root, ["fetch", ".", `${source}:refs/heads/${target}`]);
285
+ }
286
+
287
+ /** Rebase the worktree onto `target`; on failure abort it and throw the conflict line or git's reason. */
288
+ function rebaseOnto(dest: string, branch: string, target: string): void {
289
+ const rebased = git(dest, ["rebase", target]);
290
+ if (rebased.ok) return;
291
+ const files = git(dest, ["diff", "--name-only", "--diff-filter=U"]).out.split("\n").filter(Boolean);
292
+ git(dest, ["rebase", "--abort"]);
293
+ if (!files.length) throw new Error(`rebasing ${branch} onto ${target} failed: ${gitReason(rebased.err)}`);
294
+ throw new Error(
295
+ `rebasing ${branch} onto ${target} conflicts in ${files.join(", ")}: run git -C ${dest} rebase ${target}, fix each file, git -C ${dest} add <file>, git -C ${dest} rebase --continue, rerun the checks, then run finish again`,
296
+ );
297
+ }
298
+
299
+ /**
300
+ * Remove the worktree (never forced), then delete the branch once the default branch holds it.
301
+ * A worktree dir that is already gone (another session's prune got there first) is only unregistered.
302
+ */
303
+ function retire(root: string, worktree: string | undefined, branch: string): void {
304
+ if (worktree) {
305
+ // win32 refuses to delete a dir that is any process's cwd, this one included when finish ran inside it.
306
+ const fromInside = path.relative(canonicalPath(worktree), canonicalPath(process.cwd()));
307
+ if (!fromInside.startsWith("..") && !path.isAbsolute(fromInside)) process.chdir(root);
308
+ if (!fs.existsSync(worktree)) git(root, ["worktree", "prune"]);
309
+ else {
310
+ const removed = git(root, ["worktree", "remove", worktree]);
311
+ if (!removed.ok)
312
+ throw new Error(`could not remove ${worktree}: ${gitReason(removed.err)}; fix that, then run finish again`);
313
+ }
314
+ }
315
+ if (isMerged(root, branch)) git(root, ["branch", "-D", branch]);
316
+ }
317
+
318
+ /**
319
+ * Land `wt/<name>` on the default branch and remove it: rebase inside the worktree, fast-forward the default branch,
320
+ * remove worktree and branch. A rerun converges from any crash point. Never pushes.
321
+ * `rootArg` is the main checkout; an undefined `raw` takes the name from the worktree `cwd` is in.
322
+ */
323
+ export function finishWorktree(rootArg: string, raw: string | undefined, cwd: string = rootArg): FinishResult {
324
+ const root = canonicalPath(rootArg);
325
+ const name = raw === undefined ? worktreeOf(canonicalPath(path.resolve(cwd)), root, path) : requireName(raw);
326
+ if (!name) throw new UsageError("run finish inside .worktrees/<name> or pass its name");
327
+ const branch = BRANCH_PREFIX + name;
328
+ const dest = path.join(root, WORKTREES_DIR, name);
329
+ let entry = worktreeEntries(root)
330
+ .slice(1)
331
+ .find((e) => samePath(e.path, dest));
332
+ if (entry && !fs.existsSync(dest)) {
333
+ git(root, ["worktree", "prune"]);
334
+ entry = undefined;
335
+ }
336
+ if (!entry && !branchExists(root, branch)) {
337
+ throw new Error(`no worktree or branch named ${name}: ${WORKTREE_CMD} list shows them`);
338
+ }
339
+ const target = defaultBranch(root);
340
+ if (target === "HEAD")
341
+ throw new Error(`${root} has no branch checked out and no origin/HEAD; check out the default branch there`);
342
+ if (entry) {
343
+ if (rebaseInProgress(dest)) {
344
+ throw new Error(
345
+ `${dest} is mid-rebase: finish it with git -C ${dest} rebase --continue (or --abort), then run finish again`,
346
+ );
347
+ }
348
+ if (entry.branch !== branch) {
349
+ throw new Error(
350
+ `${dest} has ${entry.branch ?? "a detached HEAD"} checked out, not ${branch}: run git -C ${dest} switch ${branch}, then run finish again`,
351
+ );
352
+ }
353
+ const dirty = git(dest, ["status", "--porcelain"]).out.split("\n").filter(Boolean).length;
354
+ if (dirty)
355
+ throw new Error(
356
+ `${dest} has uncommitted changes in ${dirty} ${dirty === 1 ? "file" : "files"}: commit or drop them, then run finish again`,
357
+ );
358
+ }
359
+ const ahead = git(root, ["rev-list", "--count", `${target}..${branch}`]);
360
+ if (!ahead.ok) throw new Error(`could not compare ${branch} with ${target}: ${gitReason(ahead.err)}`);
361
+ if (ahead.out === "0") {
362
+ const landed = hasOwnCommits(root, branch);
363
+ const sha = landed ? git(root, ["rev-parse", "--short", `refs/heads/${branch}`]).out : null;
364
+ retire(root, entry && dest, branch);
365
+ return { branch, path: dest, defaultBranch: target, sha };
366
+ }
367
+ if (!entry) {
368
+ throw new Error(
369
+ `${branch} has commits that ${target} lacks and no worktree: run ${WORKTREE_CMD} add ${name} to bring it back, then run finish again`,
370
+ );
371
+ }
372
+ for (let attempt = 1; ; attempt++) {
373
+ const before = tipOf(root, target);
374
+ rebaseOnto(dest, branch, target);
375
+ const forwarded = fastForward(root, branch, target);
376
+ if (forwarded.ok) break;
377
+ if (tipOf(root, target) === before)
378
+ throw new Error(`could not fast-forward ${target} in ${root}: ${gitReason(forwarded.err)}`);
379
+ if (attempt === MAX_FINISH_TRIES)
380
+ throw new Error(`${target} kept moving while finishing ${branch}; run finish again`);
381
+ }
382
+ const sha = git(root, ["rev-parse", "--short", `refs/heads/${branch}`]).out;
383
+ retire(root, dest, branch);
384
+ return { branch, path: dest, defaultBranch: target, sha };
385
+ }
386
+
387
+ /** Remove clean managed worktrees whose own commits reached the default branch, then merged `wt/*` branches left without one. */
388
+ export function pruneWorktrees(root: string): { path: string | null; branch: string }[] {
389
+ const removed: { path: string | null; branch: string }[] = [];
390
+ for (const w of listWorktrees(root)) {
391
+ if (!w.managed || w.state !== "clean" || !w.merged || !w.branch) continue;
392
+ if (!git(root, ["worktree", "remove", w.path]).ok) continue;
393
+ git(root, ["branch", "-D", w.branch]);
394
+ removed.push({ path: w.path, branch: w.branch });
395
+ }
396
+ const held = new Set(worktreeEntries(root).map((e) => e.branch));
397
+ const branches = git(root, ["for-each-ref", "--format=%(refname:short)", `refs/heads/${BRANCH_PREFIX}`]).out.split(
398
+ "\n",
399
+ );
400
+ for (const b of branches.filter(Boolean)) {
401
+ if (held.has(b) || !isMerged(root, b)) continue;
402
+ if (git(root, ["branch", "-D", b]).ok) removed.push({ path: null, branch: b });
403
+ }
404
+ return removed;
405
+ }
406
+
407
+ /** Claude Code's WorktreeCreate / WorktreeRemove; other events do nothing. */
408
+ export function worktreeHook(data: HookInput | null): HookResult {
409
+ const event = data && data.hook_event_name;
410
+ try {
411
+ if (event === "WorktreeCreate") {
412
+ return { code: 0, stdout: `${addWorktree(resolveRoots(data).root, data?.name).path}\n`, stderr: "" };
413
+ }
414
+ if (event === "WorktreeRemove") {
415
+ const target = String(data?.worktree_path || "");
416
+ if (!target) return { code: 0, stdout: "", stderr: "" };
417
+ const root = resolveRoots(data).root;
418
+ const entry = worktreeEntries(root)
419
+ .slice(1)
420
+ .find((e) => samePath(e.path, target));
421
+ if (entry) {
422
+ const r = git(root, ["worktree", "remove", "--force", entry.path]);
423
+ if (!r.ok && fs.existsSync(target)) return { code: 1, stdout: "", stderr: r.err };
424
+ }
425
+ const branch = entry ? entry.branch : BRANCH_PREFIX + path.basename(target);
426
+ if (branch && branch.startsWith(BRANCH_PREFIX) && branchExists(root, branch) && isMerged(root, branch)) {
427
+ git(root, ["branch", "-D", branch]);
428
+ }
429
+ }
430
+ return { code: 0, stdout: "", stderr: "" };
431
+ } catch (e) {
432
+ return { code: 1, stdout: "", stderr: errorMessage(e) };
433
+ }
434
+ }
435
+
436
+ const HEREDOC_OP = /<<(-?)[ \t]*(?:'([^'\n]*)'|"([^"\n]*)"|\\?([^\s;&|<>()'"\\]+))/y;
437
+
438
+ /** The heredoc delimiters opened on one line, in order: `{ word, indented }`; a quoted `<<` or a `<<<` here-string opens none. */
439
+ function heredocsOpenedBy(line: string): Heredoc[] {
440
+ const opened: Heredoc[] = [];
441
+ let quote = "";
442
+ for (let i = 0; i < line.length; i++) {
443
+ const ch = line[i]!; // i < line.length
444
+ if (quote) quote = ch === quote ? "" : quote;
445
+ else if (ch === "\\") i++;
446
+ else if (ch === "'" || ch === '"') quote = ch;
447
+ else if (line.startsWith("<<<", i)) i += 2;
448
+ else if (line.startsWith("<<", i)) {
449
+ HEREDOC_OP.lastIndex = i;
450
+ const m = HEREDOC_OP.exec(line);
451
+ if (!m) {
452
+ i++;
453
+ continue;
454
+ }
455
+ opened.push({ word: m[2] ?? m[3] ?? m[4]!, indented: m[1] === "-" });
456
+ i += m[0].length - 1;
457
+ }
458
+ }
459
+ return opened;
460
+ }
461
+
462
+ /** `command` without heredoc bodies and their terminator lines: they are data, not commands. */
463
+ function dropHeredocBodies(command: unknown): string {
464
+ const kept: string[] = [];
465
+ let pending: Heredoc[] = [];
466
+ for (const line of String(command).split("\n")) {
467
+ if (pending.length) {
468
+ const bare = line.replace(/\r$/, "");
469
+ const { word, indented } = pending[0]!; // pending is non-empty here
470
+ if ((indented ? bare.replace(/^\t+/, "") : bare) === word) pending.shift();
471
+ continue;
472
+ }
473
+ kept.push(line);
474
+ pending = heredocsOpenedBy(line);
475
+ }
476
+ return kept.join("\n");
477
+ }
478
+
479
+ /** Shell words per command segment; a word holding `$`, a backtick or a leading `~` is not literal. */
480
+ function shellSegments(command: unknown): Word[][] {
481
+ const segments: Word[][] = [[]];
482
+ // segments always holds at least the first, empty segment.
483
+ const current = (): Word[] => segments[segments.length - 1]!;
484
+ let word: Word | null = null;
485
+ const end = () => {
486
+ if (word) current().push(word);
487
+ word = null;
488
+ };
489
+ const add = (text: string, literal = true) => {
490
+ word = word || { text: "", literal: true };
491
+ word.text += text;
492
+ if (!literal) word.literal = false;
493
+ };
494
+ const s = dropHeredocBodies(command);
495
+ for (let i = 0; i < s.length; i++) {
496
+ const ch = s[i]!; // i < s.length
497
+ if (ch === " " || ch === "\t") end();
498
+ else if (";&|\n".includes(ch)) {
499
+ end();
500
+ if (current().length) segments.push([]);
501
+ } else if (ch === "'") {
502
+ const close = s.indexOf("'", i + 1);
503
+ add(close === -1 ? s.slice(i + 1) : s.slice(i + 1, close));
504
+ i = close === -1 ? s.length : close;
505
+ } else if (ch === '"') {
506
+ const close = s.indexOf('"', i + 1);
507
+ const inner = close === -1 ? s.slice(i + 1) : s.slice(i + 1, close);
508
+ add(inner, !/[$`\\]/.test(inner));
509
+ i = close === -1 ? s.length : close;
510
+ } else if (ch === "\\") {
511
+ add(s[i + 1] ?? "");
512
+ i++;
513
+ } else add(ch, !(ch === "$" || ch === "`" || (ch === "~" && !word)));
514
+ }
515
+ end();
516
+ return segments.filter((seg) => seg.length);
517
+ }
518
+
519
+ const ADD_VALUE_FLAGS = new Set(["-b", "-B", "--reason"]);
520
+
521
+ /** `{ at, sub, rest }` for a `[VAR=v] [rtk] [env VAR=v] git [-C dir] [-c k=v] <sub> …` segment; null for any other, or when a `-C` dir is not literal. */
522
+ function gitCommandOf(words: Word[], cwd: string, p: typeof path): GitCommand | null {
523
+ let i = 0;
524
+ // `FOO=1`, `env FOO=1` and the `rtk` proxy all stand before the real command.
525
+ for (; i < words.length; i++) {
526
+ // i < words.length in this loop and the next, so words[i] exists.
527
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(words[i]!.text)) continue;
528
+ if (!/^(?:.*\/)?(?:rtk|env)$/.test(words[i]!.text)) break;
529
+ }
530
+ const bin = words[i]?.text || "";
531
+ if (bin !== "git" && !bin.endsWith("/git")) return null;
532
+ let at = cwd;
533
+ for (i++; i < words.length && words[i]!.text.startsWith("-"); i++) {
534
+ if (words[i]!.text === "-C") {
535
+ const dir = words[++i];
536
+ if (!dir || !dir.literal) return null;
537
+ at = p.resolve(at, dir.text);
538
+ } else if (words[i]!.text === "-c") i++;
539
+ }
540
+ const sub = words[i];
541
+ return sub ? { at, sub: sub.text, rest: words.slice(i + 1) } : null;
542
+ }
543
+
544
+ /** Each git command of a shell line with the dir it runs in, following `cd <literal>` between segments. */
545
+ function* gitCommands(command: unknown, cwd: string, p: typeof path): Generator<GitCommand> {
546
+ let here = cwd;
547
+ for (const words of shellSegments(command)) {
548
+ // shellSegments drops empty segments, so words[0] exists; words[1] is checked by the length test.
549
+ if (words[0]!.text === "cd" && words.length === 2 && words[1]!.literal) {
550
+ here = p.resolve(here, words[1]!.text);
551
+ continue;
552
+ }
553
+ const g = gitCommandOf(words, here, p);
554
+ if (g) yield g;
555
+ }
556
+ }
557
+
558
+ /** `{ at, target, branch, commitish, detach }` for a `git … worktree add …` command, else null. */
559
+ function worktreeAddOf(g: GitCommand): WorktreeAdd | null {
560
+ if (g.sub !== "worktree" || g.rest[0]?.text !== "add") return null;
561
+ const out: WorktreeAdd = { at: g.at, target: null, branch: null, commitish: null, detach: false };
562
+ for (let i = 1; i < g.rest.length; i++) {
563
+ const w = g.rest[i]!; // i < g.rest.length
564
+ if (ADD_VALUE_FLAGS.has(w.text)) {
565
+ const value = g.rest[++i];
566
+ if (w.text !== "--reason") out.branch = value ? value.text : null;
567
+ } else if (w.text === "--detach" || w.text === "-d") out.detach = true;
568
+ else if (w.text.startsWith("-")) continue;
569
+ else if (!out.target) out.target = w;
570
+ else if (!out.commitish) out.commitish = w.text;
571
+ }
572
+ return out;
573
+ }
574
+
575
+ /** `abs` relative to `dir` (`""` for `dir` itself), or null when `abs` is outside it. */
576
+ function relIn(abs: string, dir: string, p: typeof path): string | null {
577
+ const rel = p.relative(dir, abs);
578
+ return rel === ".." || rel.startsWith(`..${p.sep}`) || p.isAbsolute(rel) ? null : rel;
579
+ }
580
+
581
+ /** The `<name>` of the managed worktree under `root` that holds `abs`, else null. */
582
+ function worktreeOf(abs: string, root: string, p: typeof path): string | null {
583
+ const rel = relIn(abs, p.join(root, WORKTREES_DIR), p);
584
+ return rel ? rel.split(p.sep, 1).join("") : null;
585
+ }
586
+
587
+ function hasCommit(root: string): boolean {
588
+ return git(root, ["rev-parse", "--verify", "-q", "HEAD"]).ok;
589
+ }
590
+
591
+ /** The deny reason for a `git worktree add` aimed anywhere but `<root>/.worktrees/<name>` on `wt/<name>`, else null. */
592
+ export function worktreeAddReason(
593
+ command: string,
594
+ roots: WorktreeRoots,
595
+ cwd: string,
596
+ p: typeof path = path,
597
+ ): string | null {
598
+ for (const g of gitCommands(command, cwd, p)) {
599
+ const add = worktreeAddOf(g);
600
+ if (!add || !add.target || !add.target.literal) continue;
601
+ const abs = canonicalPath(p.resolve(add.at, add.target.text), p);
602
+ const name = isManaged(abs, roots.root, p) ? p.basename(abs) : null;
603
+ const branch = add.branch ?? (add.detach ? null : add.commitish);
604
+ if (name && branch === BRANCH_PREFIX + name) continue;
605
+ const suggest = worktreeName(add.branch ? add.branch.replace(/^wt\//, "") : p.basename(abs)) || "<name>";
606
+ return `worktrees live in ${p.join(roots.root, WORKTREES_DIR, "<name>")} on branch wt/<name>: run ${WORKTREE_CMD} add ${suggest}`;
607
+ }
608
+ return null;
609
+ }
610
+
611
+ /**
612
+ * The deny reason for a write into the main checkout (inside the root, outside `.tmp` and `.worktrees`), else null.
613
+ * Null as well when the root is not a git repo or has no commit yet, since no worktree can be made there.
614
+ * `committed` is the git check, injectable for foreign-path tests.
615
+ */
616
+ export function mainCheckoutWriteReason(
617
+ target: string,
618
+ roots: WorktreeRoots,
619
+ p: typeof path = path,
620
+ committed: (root: string) => boolean = hasCommit,
621
+ ): string | null {
622
+ const root = canonicalPath(roots.root, p);
623
+ const abs = canonicalPath(p.resolve(roots.cwd || root, target), p);
624
+ if (relIn(abs, root, p) === null) return null;
625
+ if ([".tmp", WORKTREES_DIR].some((free) => relIn(abs, p.join(root, free), p) !== null)) return null;
626
+ if (!committed(root)) return null;
627
+ return `the main checkout is shared: run ${WORKTREE_CMD} add <name> and write under ${p.join(root, WORKTREES_DIR, "<name>")}; only ${p.join(root, ".tmp")}${p.sep} is writable here`;
628
+ }
629
+
630
+ /** The `.repo/lessons` dir of the root, or of the managed worktree, that holds `target`; null outside the root. */
631
+ export function managedLessonsDir(target: string, roots: WorktreeRoots, p: typeof path = path): string | null {
632
+ const root = canonicalPath(roots.root, p);
633
+ const abs = canonicalPath(p.resolve(roots.cwd || root, target), p);
634
+ if (relIn(abs, root, p) === null) return null;
635
+ const name = worktreeOf(abs, root, p);
636
+ return p.join(name ? p.join(root, WORKTREES_DIR, name) : root, ".repo", "lessons");
637
+ }
638
+
639
+ /** One grant per word the user's message can name; a bare word only where it has no everyday meaning here. */
640
+ export const GIT_ASKED: Record<string, RegExp> = {
641
+ stash: /\bstash\b/i,
642
+ commit: /\bcommit\b/i,
643
+ rebase: /\brebase\b/i,
644
+ "cherry-pick": /\bcherry-pick\b/i,
645
+ force: /\bforce[- ]push\b|\bpush\s+(?:-f|--force)\b/i,
646
+ ...Object.fromEntries(
647
+ ["checkout", "switch", "restore", "clean", "reset", "merge", "pull", "revert", "am", "branch", "update-ref"].map(
648
+ (sub) => [sub, new RegExp(`\\bgit\\s+${sub}\\b`, "i")],
649
+ ),
650
+ ),
651
+ };
652
+
653
+ /** The grant words in `prompt`, in `GIT_ASKED` order. */
654
+ export function gitAskedWords(prompt: unknown): string[] {
655
+ const text = String(prompt ?? "");
656
+ return Object.entries(GIT_ASKED)
657
+ .filter(([, re]) => re.test(text))
658
+ .map(([word]) => word);
659
+ }
660
+
661
+ function defaultBranchReason(branch: string, sub: string): string {
662
+ return `${branch} is the shared default branch: move it only with ${WORKTREE_CMD} finish <name>, or when the user's message asks for git ${sub}`;
663
+ }
664
+
665
+ const inMainCheckout =
666
+ (reason: SharedRule): SharedRule =>
667
+ (g, c) =>
668
+ c.inMain ? reason(g, c) : null;
669
+
670
+ function forcePushReason(g: GitCommand): string | null {
671
+ const forced = g.rest.some((w) => /^-[A-Za-z]*f[A-Za-z]*$|^--force/.test(w.text) || /^\+./.test(w.text));
672
+ return forced
673
+ ? "a force push rewrites shared history: run it only when the user's message asks for a force push"
674
+ : null;
675
+ }
676
+
677
+ function branchReason(g: GitCommand, c: GitContext): string | null {
678
+ const flags = g.rest.filter((w) => w.text.startsWith("-")).map((w) => w.text);
679
+ const moves = flags.some((f) => /^-[A-Za-z]*[dDmM][A-Za-z]*$|^--(?:delete|move)$/.test(f));
680
+ const forces = flags.some((f) => /^-[A-Za-z]*f[A-Za-z]*$|^--force$/.test(f));
681
+ if (!moves && !forces) return null;
682
+ const names = g.rest.filter((w) => !w.text.startsWith("-") && w.literal).map((w) => w.text);
683
+ const touched = moves ? names : names.slice(0, 1);
684
+ return touched.length && touched.includes(c.defaultBranch())
685
+ ? defaultBranchReason(c.defaultBranch(), "branch")
686
+ : null;
687
+ }
688
+
689
+ function updateRefReason(g: GitCommand, c: GitContext): string | null {
690
+ const refs: Word[] = [];
691
+ for (let i = 0; i < g.rest.length; i++) {
692
+ const w = g.rest[i]!; // i < g.rest.length
693
+ if (w.text === "-m") i++;
694
+ else if (!w.text.startsWith("-")) refs.push(w);
695
+ }
696
+ const ref = refs[0];
697
+ if (!ref || !ref.literal || !ref.text.startsWith("refs/heads/")) return null;
698
+ return ref.text === `refs/heads/${c.defaultBranch()}` ? defaultBranchReason(c.defaultBranch(), "update-ref") : null;
699
+ }
700
+
701
+ const SHARED_GIT = new Map<string, SharedRule>([
702
+ ...["commit", "merge", "cherry-pick", "revert", "am", "pull"].map((sub): [string, SharedRule] => [
703
+ sub,
704
+ inMainCheckout(
705
+ (_g, c) =>
706
+ `the main checkout is shared: commit inside ${c.p.join(c.root, WORKTREES_DIR, "<name>")}, then run ${WORKTREE_CMD} finish <name>`,
707
+ ),
708
+ ]),
709
+ ...["stash", "reset", "checkout", "switch", "restore", "clean", "rebase"].map((sub): [string, SharedRule] => [
710
+ sub,
711
+ inMainCheckout(
712
+ (g) =>
713
+ `git ${g.sub} in the main checkout can destroy other sessions' work: do it inside your worktree, or run it here only when the user's message asks for git ${g.sub}`,
714
+ ),
715
+ ]),
716
+ ["push", forcePushReason],
717
+ ["branch", branchReason],
718
+ ["update-ref", updateRefReason],
719
+ ]);
720
+
721
+ /**
722
+ * The deny reason for a git command that commits or destroys work in the main checkout, or that hits shared refs
723
+ * from anywhere in the repo, else null. Inside your own managed worktree every other command runs.
724
+ * `asked` holds the grant words (`GIT_ASKED`) the user's latest message named; a named subcommand passes.
725
+ * Null as well when the root is not a git repo or has no commit yet. `committed` is the git check, injectable for foreign-path tests.
726
+ */
727
+ export function sharedGitReason(
728
+ command: string,
729
+ roots: WorktreeRoots,
730
+ cwd: string,
731
+ asked: Set<string>,
732
+ p: typeof path = path,
733
+ committed: (root: string) => boolean = hasCommit,
734
+ ): string | null {
735
+ const root = canonicalPath(roots.root, p);
736
+ let defaultName: string | undefined;
737
+ for (const g of gitCommands(command, cwd, p)) {
738
+ const rule = SHARED_GIT.get(g.sub);
739
+ if (!rule || asked.has(g.sub === "push" ? "force" : g.sub)) continue;
740
+ const at = canonicalPath(g.at, p);
741
+ if (relIn(at, root, p) === null) continue;
742
+ const own = worktreeOf(at, root, p);
743
+ const c: GitContext = {
744
+ p,
745
+ root,
746
+ at,
747
+ own,
748
+ inMain: own === null,
749
+ defaultBranch: () => (defaultName ??= defaultBranch(root)),
750
+ };
751
+ const reason = rule(g, c);
752
+ if (reason && committed(root)) return reason;
753
+ }
754
+ return null;
755
+ }
756
+
757
+ /** The one-line outcome of `finish`. */
758
+ function finishLine(result: FinishResult, root: string): string {
759
+ if (!result.sha) return `${result.branch} has no commits of its own; removed ${result.path}`;
760
+ return `merged ${result.branch} into ${result.defaultBranch} at ${result.sha} and removed ${result.path}; your shell may still be in it: cd ${root}`;
761
+ }
762
+
763
+ export function main(argv: string[], io: HookIo = processHookIo()): number {
764
+ const [cmd, arg] = argv;
765
+ try {
766
+ if (cmd === "add" && arg !== undefined) {
767
+ process.stdout.write(`${addWorktree(resolveRoots({}).root, arg).path}\n`);
768
+ return 0;
769
+ }
770
+ if (cmd === "finish") {
771
+ const roots = resolveRoots({});
772
+ process.stdout.write(`${finishLine(finishWorktree(roots.root, arg, roots.cwd), roots.root)}\n`);
773
+ return 0;
774
+ }
775
+ if (cmd === "list") {
776
+ for (const w of listWorktrees(resolveRoots({}).root)) {
777
+ const cols = [w.path, w.branch || "(detached)", w.state, w.merged ? "merged" : "unmerged"];
778
+ if (!w.managed) cols.push(`outside ${WORKTREES_DIR}`);
779
+ process.stdout.write(`${cols.join("\t")}\n`);
780
+ }
781
+ return 0;
782
+ }
783
+ if (cmd === "prune") {
784
+ const removed = pruneWorktrees(resolveRoots({}).root);
785
+ for (const r of removed)
786
+ process.stdout.write(r.path ? `removed ${r.path} (${r.branch})\n` : `deleted branch ${r.branch}\n`);
787
+ if (!removed.length) process.stdout.write("nothing to prune\n");
788
+ return 0;
789
+ }
790
+ if (cmd === "hook") {
791
+ const raw = readAllText(io) || "{}";
792
+ const data = parseHookInput(raw);
793
+ // A payload that is not JSON still fails the hook with the parse error; any other non-object is a no-op.
794
+ if (!data) JSON.parse(raw);
795
+ const res = worktreeHook(data);
796
+ if (res.stdout) process.stdout.write(res.stdout);
797
+ if (res.stderr) process.stderr.write(`${res.stderr}\n`);
798
+ return res.code;
799
+ }
800
+ } catch (e) {
801
+ process.stderr.write(`${errorMessage(e)}\n`);
802
+ return e instanceof UsageError ? 2 : 1;
803
+ }
804
+ process.stderr.write(`${USAGE}\n`);
805
+ return 2;
806
+ }