@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
package/cli/config.ts ADDED
@@ -0,0 +1,261 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { z } from "zod";
6
+ import { coverageOf, coverageSchema, type ResolvedCoverage } from "./coverage-config.ts";
7
+ import { knipSchema, type ResolvedKnip } from "./knip-schema.ts";
8
+ import { nightlyOf, nightlySchema, type ResolvedNightly } from "./nightly-config.ts";
9
+ import { lintSchema } from "./lint-schema.ts";
10
+ import type { WorkerstackConfig } from "./workerstack-config.types.ts";
11
+
12
+ /** Config packs a repo can turn on in workerstack.config.ts. */
13
+ export const PACKS = ["agents", "standards", "docs", "release"] as const;
14
+ export type Pack = (typeof PACKS)[number];
15
+
16
+ export const CONFIG_FILE = "workerstack.config.ts";
17
+
18
+ /** The package a config imports `defineConfig` from, and the import error that says nothing provides it. */
19
+ const SELF = "@workerstack/repo";
20
+ const MISSING_SELF = /Cannot find (?:package|module) '@workerstack\/repo'/;
21
+
22
+ /** Agent hosts repo sync can lay a tree out for. */
23
+ export const HOSTS = ["claude", "codex", "antigravity", "grok", "cursor"] as const;
24
+ export type Host = (typeof HOSTS)[number];
25
+
26
+ /** Optional skill packs a repo can ship to its agents. */
27
+ export const SKILL_PACKS = ["google-workspace", "listen", "speak", "clasp"] as const;
28
+ export type SkillPack = (typeof SKILL_PACKS)[number];
29
+
30
+ const listOf = (names: readonly string[]) => `${names.slice(0, -1).join(", ")} or ${names.at(-1)}`;
31
+
32
+ const packSchema = z.enum(PACKS, {
33
+ error: (issue) => `${JSON.stringify(issue.input)} is not a pack; use ${listOf(PACKS)}`,
34
+ });
35
+ const hostSchema = z.enum(HOSTS, {
36
+ error: (issue) => `${JSON.stringify(issue.input)} is not a host; use ${listOf(HOSTS)}`,
37
+ });
38
+ const skillSchema = z.enum(SKILL_PACKS, {
39
+ error: (issue) => `${JSON.stringify(issue.input)} is not a skill pack; use ${listOf(SKILL_PACKS)}`,
40
+ });
41
+
42
+ /** A list of unique names; a repeat is reported with its count. */
43
+ function uniqueList<S extends z.ZodType<string>>(item: S, error: string) {
44
+ return z
45
+ .array(item, { error })
46
+ .readonly()
47
+ .superRefine((names, ctx) => {
48
+ const counts = new Map<z.output<S>, number>();
49
+ for (const name of names) counts.set(name, (counts.get(name) ?? 0) + 1);
50
+ for (const [name, count] of counts) {
51
+ if (count === 1) continue;
52
+ const times = count === 2 ? "twice" : `${count} times`;
53
+ ctx.addIssue({ code: "custom", message: `${JSON.stringify(name)} is listed ${times}` });
54
+ }
55
+ });
56
+ }
57
+
58
+ const agentsSchema = z.strictObject(
59
+ {
60
+ hosts: uniqueList(hostSchema, "must be a list of host names")
61
+ .refine((hosts) => hosts.length > 0, { error: "list at least one host" })
62
+ .optional(),
63
+ skills: uniqueList(skillSchema, "must be a list of skill pack names").optional(),
64
+ },
65
+ { error: "must be an object like { hosts: [...], skills: [...] }" },
66
+ );
67
+
68
+ /** How many changed lines a commit may have before its message must say why. */
69
+ export const DEFAULT_MAX_CHANGED_LINES = 1000;
70
+
71
+ const commitSchema = z.strictObject(
72
+ {
73
+ maxChangedLines: z
74
+ .number({ error: "must be a whole number of lines above 0" })
75
+ .int({ error: "must be a whole number of lines above 0" })
76
+ .positive({ error: "must be a whole number of lines above 0" })
77
+ .optional(),
78
+ },
79
+ { error: `must be an object like { maxChangedLines: ${DEFAULT_MAX_CHANGED_LINES} }` },
80
+ );
81
+
82
+ /** A config section that only means something with its pack on. */
83
+ const needsPack = (key: "agents" | "commit" | "knip" | "lint" | "nightly" | "tdd", pack: Pack) => ({
84
+ path: [key],
85
+ error: `set only with the ${pack} pack; add "${pack}" to packs or delete ${key}`,
86
+ });
87
+
88
+ /** Whether the config turns `pack` on, by its `packs` or the default ones. */
89
+ const hasPack = (config: { packs?: readonly Pack[] | undefined }, pack: Pack) =>
90
+ (config.packs ?? defaultPacks()).includes(pack);
91
+
92
+ const configSchema = z
93
+ .strictObject(
94
+ {
95
+ packs: uniqueList(packSchema, "must be a list of pack names").optional(),
96
+ agents: agentsSchema.optional(),
97
+ commit: commitSchema.optional(),
98
+ lint: lintSchema().optional(),
99
+ knip: knipSchema().optional(),
100
+ coverage: coverageSchema.optional(),
101
+ nightly: nightlySchema.optional(),
102
+ tdd: z.boolean({ error: "must be true or false" }).optional(),
103
+ },
104
+ { error: "the default export must be an object; use `export default defineConfig({ ... })`" },
105
+ )
106
+ .refine((config) => config.agents === undefined || hasPack(config, "agents"), needsPack("agents", "agents"))
107
+ .refine((config) => config.commit === undefined || hasPack(config, "standards"), needsPack("commit", "standards"))
108
+ .refine((config) => config.lint === undefined || hasPack(config, "standards"), needsPack("lint", "standards"))
109
+ .refine((config) => config.knip === undefined || hasPack(config, "standards"), needsPack("knip", "standards"))
110
+ .refine((config) => config.nightly === undefined || hasPack(config, "release"), needsPack("nightly", "release"))
111
+ .refine(
112
+ (config) => config.tdd === undefined || hasPack(config, "standards"),
113
+ needsPack("tdd", "standards"),
114
+ ) satisfies z.ZodType<unknown, WorkerstackConfig>;
115
+
116
+ export type { WorkerstackConfig };
117
+ export type ResolvedAgents = { readonly hosts: readonly Host[]; readonly skills: readonly SkillPack[] };
118
+ /** What the commit-msg hook checks a commit against. */
119
+ export type ResolvedCommit = { readonly maxChangedLines: number };
120
+ /** One `lint.skip` entry: the rules (oxlint keys) it turns off, for the files its globs match, and why. */
121
+ export type LintSkip = { files: readonly string[]; rules: readonly string[]; reason: string };
122
+ /** What the generated oxlint config and `repo check` read from `lint`. */
123
+ export type ResolvedLint = { readonly skip: readonly LintSkip[] };
124
+ export type ResolvedConfig = {
125
+ readonly packs: readonly Pack[];
126
+ readonly agents: ResolvedAgents;
127
+ readonly commit: ResolvedCommit;
128
+ readonly lint: ResolvedLint;
129
+ readonly knip: ResolvedKnip;
130
+ readonly coverage: ResolvedCoverage;
131
+ /** The nightly workflow's shard count and secret names. */
132
+ readonly nightly: ResolvedNightly;
133
+ /** Whether a feat or fix in a private package must also fail its tests on HEAD's source; public packages always do. */
134
+ readonly tdd: boolean;
135
+ };
136
+ /** The commit rules a config gets when it omits `commit`. */
137
+ export const DEFAULT_COMMIT: ResolvedCommit = { maxChangedLines: DEFAULT_MAX_CHANGED_LINES };
138
+ export type LoadResult = { ok: true; config: ResolvedConfig } | { ok: false; errors: string[] };
139
+
140
+ /** Types a workerstack.config.ts default export. */
141
+ export function defineConfig(config: WorkerstackConfig): WorkerstackConfig {
142
+ return config;
143
+ }
144
+
145
+ /** The packs a repo gets when its config omits `packs`. */
146
+ export function defaultPacks(): Pack[] {
147
+ return ["agents", "standards", "docs"];
148
+ }
149
+
150
+ /** The nearest directory at or above `cwd` that holds workerstack.config.ts, or null. */
151
+ export function findRoot(cwd: string): string | null {
152
+ let dir = path.resolve(cwd);
153
+ for (;;) {
154
+ if (fs.existsSync(path.join(dir, CONFIG_FILE))) return dir;
155
+ const parent = path.dirname(dir);
156
+ if (parent === dir) return null;
157
+ dir = parent;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Imports and validates `<root>/workerstack.config.ts`, filling omitted packs from `defaultPacks`, omitted agents with
163
+ * every host and no skills, an omitted commit limit with the default, and omitted lint skips with none.
164
+ */
165
+ export async function loadConfig(root: string, importModule: ImportConfig = importConfig): Promise<LoadResult> {
166
+ const fail = (messages: string[]): LoadResult => ({
167
+ ok: false,
168
+ errors: messages.map((message) => `${CONFIG_FILE}: ${message}`),
169
+ });
170
+
171
+ let loaded: Record<string, unknown>;
172
+ try {
173
+ loaded = await importModule(root);
174
+ } catch (error) {
175
+ const message = messageOf(error);
176
+ // A linked worktree without its own install fails here; the fix is an install, not an edit to the config.
177
+ if (MISSING_SELF.test(message)) return fail([`${SELF} is not installed in ${root}; run \`bun install\` there`]);
178
+ return fail([`failed to load: ${message}`]);
179
+ }
180
+ if (!("default" in loaded)) return fail(["no default export; add `export default defineConfig({ ... })`"]);
181
+
182
+ const parsed = configSchema.safeParse(loaded.default);
183
+ if (!parsed.success) return fail(parsed.error.issues.flatMap(describeIssue));
184
+ return { ok: true, config: resolve(parsed.data) };
185
+ }
186
+
187
+ /** The agent hosts and skills a config turns on; every host and no skills when omitted. */
188
+ function agentsOf(agents: z.output<typeof agentsSchema> | undefined): ResolvedAgents {
189
+ return { hosts: agents?.hosts ?? HOSTS, skills: agents?.skills ?? [] };
190
+ }
191
+
192
+ /** A parsed config with every omitted value filled in. */
193
+ function resolve(d: z.output<typeof configSchema>): ResolvedConfig {
194
+ const agents = agentsOf(d.agents);
195
+ const commit = { maxChangedLines: d.commit?.maxChangedLines ?? DEFAULT_COMMIT.maxChangedLines };
196
+ return {
197
+ packs: d.packs ?? defaultPacks(),
198
+ agents,
199
+ commit,
200
+ lint: { skip: d.lint?.skip ?? [] },
201
+ knip: { ignore: d.knip?.ignore ?? [] },
202
+ tdd: d.tdd ?? false,
203
+ ...coverageOf(d),
204
+ ...nightlyOf(d),
205
+ };
206
+ }
207
+
208
+ /** Imports the config module of `root`; `loadConfig` takes another in tests, whose runner resolves packages its own way. */
209
+ export type ImportConfig = (root: string) => Promise<Record<string, unknown>>;
210
+
211
+ /** Each config file's last loaded text and module, so a load can tell whether the text on disk changed. */
212
+ const loadedConfigs = new Map<string, { text: string; module: Record<string, unknown> }>();
213
+
214
+ /**
215
+ * The config module. A module is cached by path for the life of the process (a query string does not bust that
216
+ * cache), so a file whose text changed since it was loaded is imported from a temporary copy beside it (same
217
+ * imports resolve, new path). A first load, or an unchanged file, writes nothing.
218
+ */
219
+ async function importConfig(root: string): Promise<Record<string, unknown>> {
220
+ const file = path.join(root, CONFIG_FILE);
221
+ const text = fs.readFileSync(file, "utf8");
222
+ const known = loadedConfigs.get(file);
223
+ if (known?.text === text) return known.module;
224
+ let module: Record<string, unknown>;
225
+ if (known === undefined) {
226
+ module = await import(/* @vite-ignore */ pathToFileURL(file).href);
227
+ } else {
228
+ const copy = path.join(root, `.workerstack.config.${randomBytes(6).toString("hex")}.ts`);
229
+ fs.copyFileSync(file, copy);
230
+ try {
231
+ module = await import(/* @vite-ignore */ pathToFileURL(copy).href);
232
+ } finally {
233
+ fs.rmSync(copy, { force: true });
234
+ }
235
+ }
236
+ loadedConfigs.set(file, { text, module });
237
+ return module;
238
+ }
239
+
240
+ /** The skill packs `agents.skills` enables for the repo at or above `cwd`; none when there is no config or it is invalid. */
241
+ export async function enabledSkills(cwd: string): Promise<readonly SkillPack[]> {
242
+ const root = findRoot(cwd);
243
+ if (root === null) return [];
244
+ const loaded = await loadConfig(root);
245
+ return loaded.ok ? loaded.config.agents.skills : [];
246
+ }
247
+
248
+ function describeIssue(issue: z.core.$ZodIssue): string[] {
249
+ // ["packs", 1] reads packs[1]; a config path always starts at a top-level key.
250
+ const at = issue.path
251
+ .map((key) => (typeof key === "number" ? `[${key}]` : `.${String(key)}`))
252
+ .join("")
253
+ .replace(/^\./, "");
254
+ const prefix = at ? `${at}: ` : "";
255
+ if (issue.code === "unrecognized_keys") return issue.keys.map((key) => `${prefix}unknown key ${JSON.stringify(key)}`);
256
+ return [`${prefix}${issue.message}`];
257
+ }
258
+
259
+ function messageOf(error: unknown): string {
260
+ return error instanceof Error ? error.message : String(error);
261
+ }
@@ -0,0 +1,9 @@
1
+ /** A Conventional Commits header: `type(scope)!: subject`. */
2
+ export const HEADER: RegExp = /^(?<type>[^\s():!]+)(?:\((?<scope>[^()]*)\))?(?<bang>!)?: (?<subject>.+)$/;
3
+
4
+ const FOOTER = /^BREAKING[ -]CHANGE: /;
5
+
6
+ /** Whether a commit is breaking: its header has `!`, or a line of its body is a `BREAKING CHANGE:` footer. */
7
+ export function isBreaking(header: string, body: readonly string[]): boolean {
8
+ return HEADER.exec(header)?.groups?.["bang"] === "!" || body.some((line) => FOOTER.test(line));
9
+ }
@@ -0,0 +1,62 @@
1
+ /** The `coverage` section of workerstack.config.ts: where the floors and the known-failures list live, what to mutate. */
2
+ import { z } from "zod";
3
+
4
+ /** Where `repo coverage`, `repo mutate` and `repo check` read and write their ratchet files, relative to the repo root. */
5
+ export type ResolvedCoverage = {
6
+ readonly floors: string;
7
+ readonly knownFailures: string;
8
+ /** The globs of the tracked files `repo mutate` mutates. */
9
+ readonly mutate: readonly string[];
10
+ readonly mutationFloors: string;
11
+ };
12
+
13
+ /** The files a config gets when it omits `coverage` or one of its keys. */
14
+ export const DEFAULT_COVERAGE: ResolvedCoverage = {
15
+ floors: "coverage-floors.json",
16
+ knownFailures: "known-failures.json",
17
+ mutate: ["src/**/*.ts"],
18
+ mutationFloors: "mutation-floors.json",
19
+ };
20
+
21
+ const PATH_ERROR = `must be a file path like ${DEFAULT_COVERAGE.floors}`;
22
+ const GLOBS_ERROR = 'must be a non-empty list of globs like ["src/**/*.ts"]';
23
+
24
+ type FileSchema = z.ZodOptional<z.ZodString>;
25
+ type CoverageSchema = z.ZodObject<
26
+ {
27
+ floors: FileSchema;
28
+ knownFailures: FileSchema;
29
+ mutate: z.ZodOptional<z.ZodArray<z.ZodString>>;
30
+ mutationFloors: FileSchema;
31
+ },
32
+ z.core.$strict
33
+ >;
34
+
35
+ function buildSchema(): CoverageSchema {
36
+ const file = z.string({ error: PATH_ERROR }).min(1, { error: PATH_ERROR }).optional();
37
+ const globs = z
38
+ .array(z.string({ error: GLOBS_ERROR }).min(1, { error: GLOBS_ERROR }), { error: GLOBS_ERROR })
39
+ .min(1, { error: GLOBS_ERROR })
40
+ .optional();
41
+ return z.strictObject(
42
+ { floors: file, knownFailures: file, mutate: globs, mutationFloors: file },
43
+ { error: `must be an object like { floors: "${DEFAULT_COVERAGE.floors}" }` },
44
+ );
45
+ }
46
+
47
+ export const coverageSchema: CoverageSchema = /* @__PURE__ */ buildSchema();
48
+
49
+ /** The parsed config's `coverage` section with every omitted key filled in. */
50
+ export function coverageOf(parsed: { coverage?: z.output<typeof coverageSchema> | undefined }): {
51
+ coverage: ResolvedCoverage;
52
+ } {
53
+ const { floors, knownFailures, mutate, mutationFloors } = parsed.coverage ?? {};
54
+ return {
55
+ coverage: {
56
+ floors: floors ?? DEFAULT_COVERAGE.floors,
57
+ knownFailures: knownFailures ?? DEFAULT_COVERAGE.knownFailures,
58
+ mutate: mutate ?? DEFAULT_COVERAGE.mutate,
59
+ mutationFloors: mutationFloors ?? DEFAULT_COVERAGE.mutationFloors,
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,6 @@
1
+ /** The names `repo coverage` reads from the coverage summary. */
2
+
3
+ /** The summary key holding the totals over every file, which is no file. */
4
+ export const TOTAL_KEY = "total";
5
+ /** The spaces the floors file is indented with. */
6
+ export const JSON_INDENT = 2;
@@ -0,0 +1,199 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
6
+ import { main } from "./coverage.ts";
7
+
8
+ const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9
+ const RUN_MS = 60_000;
10
+ const FLOORS = "coverage-floors.json";
11
+ const saved = process.cwd();
12
+ const temps: string[] = [];
13
+
14
+ const SOURCE = `export function sign(n: number): string {
15
+ if (n < 0) return "neg";
16
+ return "pos";
17
+ }
18
+
19
+ export function unused(): number {
20
+ return 1;
21
+ }
22
+ `;
23
+ const PASSING = `import { expect, it } from "vitest";
24
+ import { sign } from "./a.ts";
25
+
26
+ it("signs a positive number", () => {
27
+ expect(sign(1)).toBe("pos");
28
+ });
29
+ `;
30
+ const FAILING = PASSING.replace('"pos"', '"neg"');
31
+ const SECOND_SOURCE = "export const answer = (): number => 42;\n";
32
+ const SECOND_TEST = `import { expect, it } from "vitest";
33
+ import { answer } from "./b.ts";
34
+
35
+ it("answers", () => {
36
+ expect(answer()).toBe(42);
37
+ });
38
+ `;
39
+ /** What `SOURCE` measures with `PASSING`: lines are 2 of 3 (66.67), so the floor is 66, not 67. */
40
+ const MEASURED = { statements: 50, branches: 50, functions: 50, lines: 66 };
41
+
42
+ let out: string[] = [];
43
+ let err: string[] = [];
44
+
45
+ beforeEach(() => {
46
+ out = [];
47
+ err = [];
48
+ vi.spyOn(process.stdout, "write").mockImplementation((chunk) => out.push(String(chunk)) > 0);
49
+ vi.spyOn(process.stderr, "write").mockImplementation((chunk) => err.push(String(chunk)) > 0);
50
+ });
51
+
52
+ afterEach(() => {
53
+ vi.restoreAllMocks();
54
+ process.chdir(saved);
55
+ for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
56
+ });
57
+
58
+ /** A tiny vitest project in a temp dir, with the repo's installed packages linked in; `config` is its repo config. */
59
+ function project(config = "export default {};\n"): string {
60
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-coverage-")));
61
+ temps.push(root);
62
+ fs.symlinkSync(path.join(REPO_ROOT, "node_modules"), path.join(root, "node_modules"), "junction");
63
+ const preset = JSON.stringify(path.join(REPO_ROOT, "presets", "vitest.ts"));
64
+ const vitest = `import { defineConfig } from ${preset};
65
+ export default defineConfig({ test: { include: ["src/**/*.test.ts"], coverage: { include: ["src/**"] } } });
66
+ `;
67
+ const files = {
68
+ "workerstack.config.ts": config,
69
+ "vitest.config.ts": vitest,
70
+ "src/a.ts": SOURCE,
71
+ "src/a.test.ts": PASSING,
72
+ };
73
+ for (const [name, text] of Object.entries(files)) {
74
+ fs.mkdirSync(path.dirname(path.join(root, name)), { recursive: true });
75
+ fs.writeFileSync(path.join(root, name), text);
76
+ }
77
+ return root;
78
+ }
79
+
80
+ function floorsOf(root: string, file = FLOORS): unknown {
81
+ return JSON.parse(fs.readFileSync(path.join(root, file), "utf8"));
82
+ }
83
+
84
+ /** Runs `repo coverage` in `root`. */
85
+ function coverage(root: string): Promise<number> {
86
+ process.chdir(root);
87
+ return main([]);
88
+ }
89
+
90
+ it("repo coverage: writes a floor for every measured file on the first run", { timeout: RUN_MS }, async () => {
91
+ const root = project();
92
+ expect(await coverage(root)).toBe(0);
93
+ expect(floorsOf(root)).toEqual({ "src/a.ts": MEASURED });
94
+ const lines = ["statements new → 50", "branches new → 50", "functions new → 50", "lines new → 66"];
95
+ expect(out.join("")).toBe(`${lines.map((line) => `src/a.ts ${line}\n`).join("")}wrote ${FLOORS}\n`);
96
+ expect(fs.readdirSync(os.tmpdir()).filter((name) => name.startsWith("workerstack-cov-run"))).toEqual([]);
97
+ });
98
+
99
+ it("repo coverage: floors every file the suite measures, sorted by path", { timeout: RUN_MS }, async () => {
100
+ const root = project();
101
+ fs.writeFileSync(path.join(root, "src/b.ts"), SECOND_SOURCE);
102
+ fs.writeFileSync(path.join(root, "src/b.test.ts"), SECOND_TEST);
103
+ expect(await coverage(root)).toBe(0);
104
+ const written = floorsOf(root) /* the command wrote an object */ as Record<string, unknown>;
105
+ expect(Object.keys(written)).toEqual(["src/a.ts", "src/b.ts"]);
106
+ expect(written["src/b.ts"]).toEqual({ statements: 100, branches: 100, functions: 100, lines: 100 });
107
+ });
108
+
109
+ it(
110
+ "repo coverage: prints each raised floor with its old and new value and writes the file",
111
+ { timeout: RUN_MS },
112
+ async () => {
113
+ const root = project();
114
+ fs.writeFileSync(path.join(root, FLOORS), JSON.stringify({ "src/a.ts": { ...MEASURED, branches: 40, lines: 30 } }));
115
+ expect(await coverage(root)).toBe(0);
116
+ expect(out.join("")).toBe(`src/a.ts branches 40 → 50\nsrc/a.ts lines 30 → 66\nwrote ${FLOORS}\n`);
117
+ expect(floorsOf(root)).toEqual({ "src/a.ts": MEASURED });
118
+ },
119
+ );
120
+
121
+ it("repo coverage: prints only the wrote line when nothing rises", { timeout: RUN_MS }, async () => {
122
+ const root = project();
123
+ await coverage(root);
124
+ out = [];
125
+ expect(await coverage(root)).toBe(0);
126
+ expect(out.join("")).toBe(`wrote ${FLOORS}\n`);
127
+ });
128
+
129
+ // A project that links this checkout's packages shares their vitest module cache; a nested run that resets it
130
+ // (its lockfile differs) deletes entries the outer run is still reading.
131
+ it(
132
+ "repo coverage: leaves the module cache it shares through the linked packages alone",
133
+ { timeout: RUN_MS },
134
+ async () => {
135
+ const cache = path.join(REPO_ROOT, "node_modules", ".vitest-cache");
136
+ fs.mkdirSync(cache, { recursive: true });
137
+ const sentinel = path.join(cache, `sentinel-${process.pid}`);
138
+ fs.writeFileSync(sentinel, "");
139
+ try {
140
+ expect(await coverage(project())).toBe(0);
141
+ expect(fs.existsSync(sentinel)).toBe(true);
142
+ } finally {
143
+ fs.rmSync(sentinel, { force: true });
144
+ }
145
+ },
146
+ );
147
+
148
+ it("repo coverage: fails a drop with its line and writes nothing", { timeout: RUN_MS }, async () => {
149
+ const root = project();
150
+ const floors = JSON.stringify({ "src/a.ts": { ...MEASURED, branches: 100, lines: 75 } });
151
+ fs.writeFileSync(path.join(root, FLOORS), floors);
152
+ expect(await coverage(root)).toBe(1);
153
+ const keep = "restore the tests or keep the floor";
154
+ expect(err.join("")).toContain(`coverage: src/a.ts branches would drop 100 → 50; ${keep}\n`);
155
+ expect(err.join("")).toContain(`coverage: src/a.ts lines would drop 75 → 66; ${keep}\n`);
156
+ expect(out.join("")).toBe("");
157
+ expect(fs.readFileSync(path.join(root, FLOORS), "utf8")).toBe(floors);
158
+ });
159
+
160
+ it(
161
+ "repo coverage: fails a failing test as a test, not as a drop, and writes nothing",
162
+ { timeout: RUN_MS },
163
+ async () => {
164
+ const root = project();
165
+ fs.writeFileSync(path.join(root, "src/a.test.ts"), FAILING);
166
+ expect(await coverage(root)).toBe(1);
167
+ expect(err.join("")).toContain("coverage: the tests failed; fix them before raising floors");
168
+ expect(err.join("")).not.toContain("would drop");
169
+ expect(fs.existsSync(path.join(root, FLOORS))).toBe(false);
170
+ },
171
+ );
172
+
173
+ it("repo coverage: writes the file coverage.floors names in the config", { timeout: RUN_MS }, async () => {
174
+ const root = project('export default { coverage: { floors: "ratchets/floors.json" } };\n');
175
+ expect(await coverage(root)).toBe(0);
176
+ expect(floorsOf(root, "ratchets/floors.json")).toEqual({ "src/a.ts": MEASURED });
177
+ expect(out.join("")).toContain("wrote ratchets/floors.json\n");
178
+ expect(fs.existsSync(path.join(root, FLOORS))).toBe(false);
179
+ });
180
+
181
+ it("repo coverage: fails, saying so, when the floors file cannot be formatted", { timeout: RUN_MS }, async () => {
182
+ const root = project('export default { coverage: { floors: "floors.txt" } };\n');
183
+ expect(await coverage(root)).toBe(1);
184
+ expect(err.join("")).toContain("coverage: could not format floors.txt\n");
185
+ expect(out.join("")).toBe("");
186
+ });
187
+
188
+ it("repo coverage: fails before running anything when the repo config is invalid", async () => {
189
+ const root = project("export default { coverage: { floor: 'a.json' } };\n");
190
+ expect(await coverage(root)).toBe(1);
191
+ expect(err.join("")).toBe('repo coverage: workerstack.config.ts: coverage: unknown key "floor"\n');
192
+ });
193
+
194
+ it("repo coverage: names a missing repo config", async () => {
195
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-coverage-")));
196
+ temps.push(root);
197
+ expect(await coverage(root)).toBe(1);
198
+ expect(err.join("")).toContain("repo coverage: no workerstack.config.ts");
199
+ });
@@ -0,0 +1,127 @@
1
+ /**
2
+ * `repo coverage`: runs the suite with coverage and writes each measured file's statements, branches, functions and
3
+ * lines, floored to integers, to the `coverage.floors` file (`coverage-floors.json`). The preset applies them as
4
+ * per-file thresholds. A floor only rises: a measurement below one fails and writes nothing.
5
+ */
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+ import { CONFIG_FILE, findRoot, loadConfig } from "./config.ts";
10
+ import { JSON_INDENT, TOTAL_KEY } from "./coverage.constants.ts";
11
+ import { childEnv } from "./env.ts";
12
+ import { NESTED_VITEST_FLAGS } from "./nested-vitest-flags.ts";
13
+ import { captureBun, captureTool } from "./scripts/lint.ts";
14
+
15
+ const METRICS = ["statements", "branches", "functions", "lines"] as const;
16
+ type Metric = (typeof METRICS)[number];
17
+ type Floor = Record<Metric, number>;
18
+ type Floors = Record<string, Floor>;
19
+ type Summary = Record<string, Record<Metric, { pct: number }>>;
20
+
21
+ const SUMMARY_FILE = "coverage-summary.json";
22
+ const TEMP_PREFIX = "workerstack-cov-run-";
23
+ const FAILED = 1;
24
+ const PASSED = 0;
25
+
26
+ /** A comparison of the measurement with the old floors: what to write, what rose, what would drop. */
27
+ type Comparison = { next: Floors; raised: string[]; drops: string[] };
28
+
29
+ type Measured = { ok: true; summary: Summary } | { ok: false };
30
+
31
+ function write(stream: NodeJS.WriteStream, lines: readonly string[]): void {
32
+ stream.write(`${lines.join("\n")}\n`);
33
+ }
34
+
35
+ function readJson<T>(file: string): T | null {
36
+ try {
37
+ return JSON.parse(fs.readFileSync(file, "utf8")) /* the caller names the shape the file holds */ as T;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Runs the suite in `root` with thresholds off, so a floor miss cannot fail the run, and a `json-summary` report in
45
+ * a temp dir that is removed after. A failing test prints vitest's own output and its own line.
46
+ */
47
+ function measure(root: string): Measured {
48
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), TEMP_PREFIX));
49
+ try {
50
+ const flags = ["--coverage", "--coverage.thresholds=false", "--coverage.reporter=json-summary"];
51
+ const args = ["--bun", "vitest", "run", ...NESTED_VITEST_FLAGS, ...flags, `--coverage.reportsDirectory=${dir}`];
52
+ const ran = captureBun(args, root, childEnv());
53
+ if (ran.code === PASSED) {
54
+ const summary: unknown = JSON.parse(fs.readFileSync(path.join(dir, SUMMARY_FILE), "utf8"));
55
+ return { ok: true, summary: summary /* vitest writes the json-summary shape */ as Summary };
56
+ }
57
+ process.stderr.write(ran.stdout + ran.stderr);
58
+ write(process.stderr, ["coverage: the tests failed; fix them before raising floors"]);
59
+ return { ok: false };
60
+ } finally {
61
+ fs.rmSync(dir, { recursive: true, force: true });
62
+ }
63
+ }
64
+
65
+ /** Each measured file's floors against `old`: the new floors, the lines for a raise, the lines for a drop. */
66
+ function compare(root: string, summary: Summary, old: Floors): Comparison {
67
+ const result: Comparison = { next: {}, raised: [], drops: [] };
68
+ for (const [abs, metrics] of Object.entries(summary)) {
69
+ if (abs === TOTAL_KEY) continue;
70
+ const rel = path.relative(root, abs).split(path.sep).join("/");
71
+ const floor = {} /* filled for every metric below */ as Floor;
72
+ for (const key of METRICS) {
73
+ const now = Math.floor(metrics[key].pct);
74
+ const was = old[rel]?.[key];
75
+ if (was !== undefined && now < was) {
76
+ result.drops.push(`coverage: ${rel} ${key} would drop ${was} → ${now}; restore the tests or keep the floor`);
77
+ } else if (was !== now) {
78
+ result.raised.push(`${rel} ${key} ${was ?? "new"} → ${now}`);
79
+ }
80
+ floor[key] = now;
81
+ }
82
+ result.next[rel] = floor;
83
+ }
84
+ return result;
85
+ }
86
+
87
+ /** Writes `floors` sorted to `file`, then formats it with `repo fmt`'s tool; false, after printing why, if that fails. */
88
+ function writeFloors(root: string, file: string, floors: Floors): boolean {
89
+ const sorted = Object.fromEntries(Object.entries(floors).sort(([a], [b]) => a.localeCompare(b)));
90
+ const full = path.join(root, file);
91
+ fs.mkdirSync(path.dirname(full), { recursive: true });
92
+ fs.writeFileSync(full, `${JSON.stringify(sorted, null, JSON_INDENT)}\n`);
93
+ const formatted = captureTool("oxfmt", [file], root);
94
+ if (formatted.code === PASSED) return true;
95
+ process.stderr.write(formatted.stdout + formatted.stderr);
96
+ write(process.stderr, [`coverage: could not format ${file}`]);
97
+ return false;
98
+ }
99
+
100
+ /** The repo root and its floors path, or the lines that say why there is none. */
101
+ async function locate(cwd: string): Promise<{ root: string; floors: string } | string[]> {
102
+ const root = findRoot(cwd);
103
+ if (root === null) return [`repo coverage: no ${CONFIG_FILE} in ${cwd} or any parent; run \`bun repo init\``];
104
+ const loaded = await loadConfig(root);
105
+ if (!loaded.ok) return loaded.errors.map((line) => `repo coverage: ${line}`);
106
+ return { root, floors: loaded.config.coverage.floors };
107
+ }
108
+
109
+ export async function main(_argv: string[]): Promise<number> {
110
+ const located = await locate(process.cwd());
111
+ if (Array.isArray(located)) {
112
+ write(process.stderr, located);
113
+ return FAILED;
114
+ }
115
+ const { root, floors } = located;
116
+ const measured = measure(root);
117
+ if (!measured.ok) return FAILED;
118
+ const old = readJson<Floors>(path.join(root, floors)) ?? {};
119
+ const { next, raised, drops } = compare(fs.realpathSync(root), measured.summary, old);
120
+ if (drops.length > 0) {
121
+ write(process.stderr, drops);
122
+ return FAILED;
123
+ }
124
+ if (!writeFloors(root, floors, next)) return FAILED;
125
+ write(process.stdout, [...raised, `wrote ${floors}`]);
126
+ return PASSED;
127
+ }
@@ -0,0 +1,10 @@
1
+ /** The names `repo check` and the env-in-config rule compare against. */
2
+
3
+ /** The one module of a package that reads the process environment. */
4
+ export const ENV_FILE = "env.ts";
5
+ /** The file that marks a folder as a package. */
6
+ export const MANIFEST = "package.json";
7
+ /** The property that carries the environment on `process`, `Bun` and `import.meta`. */
8
+ export const ENV_PROPERTY = "env";
9
+ /** The keyword of `import.meta`. */
10
+ export const IMPORT_KEYWORD = "import";