@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,621 @@
1
+ import { BASE_CONFIG } from "../fixtures/base-config.ts";
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { afterEach, describe, expect, it } from "vitest";
8
+ import { HOSTS, type Host, type SkillPack } from "../config.ts";
9
+ import type { Output, SyncContext } from "./outputs.ts";
10
+ import { runSync } from "../sync.ts";
11
+ import { renderTrees } from "./trees.ts";
12
+ import { BUN, linkText } from "../../dev/test-paths.ts";
13
+
14
+ const SELF_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
15
+ const STOCK_SKILL = "../../node_modules/@workerstack/repo/agents/skills";
16
+ // A render copies and merges on a disk stage, and one test renders up to three times.
17
+ const RENDER_TIMEOUT = 60_000;
18
+
19
+ const temps: string[] = [];
20
+
21
+ afterEach(() => {
22
+ for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
23
+ });
24
+
25
+ function tempDir(): string {
26
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "workerstack-trees-"));
27
+ temps.push(dir);
28
+ return dir;
29
+ }
30
+
31
+ /** The stage dirs a render left under `root`. */
32
+ function stagesIn(root: string): string[] {
33
+ const dir = path.join(root, ".repo", "tmp");
34
+ return fs.existsSync(dir) ? fs.readdirSync(dir).filter((name) => name.startsWith("stage-")) : [];
35
+ }
36
+
37
+ function write(root: string, rel: string, text: string): void {
38
+ const file = path.join(root, rel);
39
+ fs.mkdirSync(path.dirname(file), { recursive: true });
40
+ fs.writeFileSync(file, text);
41
+ }
42
+
43
+ /** A consumer: a config, a package.json and `node_modules/@workerstack/repo` linked to this repo's root. */
44
+ function consumer(files: Record<string, string> = {}): string {
45
+ const root = tempDir();
46
+ write(root, "workerstack.config.ts", "export default {}\n");
47
+ write(root, "package.json", JSON.stringify({ name: "fixture", scripts: { build: "tsc" } }));
48
+ fs.mkdirSync(path.join(root, "node_modules", "@workerstack"), { recursive: true });
49
+ fs.symlinkSync(SELF_ROOT, path.join(root, "node_modules", "@workerstack", "repo"), "dir");
50
+ for (const [rel, text] of Object.entries(files)) write(root, rel, text);
51
+ return root;
52
+ }
53
+
54
+ function context(
55
+ root: string,
56
+ hosts: readonly Host[],
57
+ skills: readonly SkillPack[] = [],
58
+ packages: readonly string[] = [],
59
+ ): SyncContext {
60
+ return {
61
+ root,
62
+ config: {
63
+ packs: ["agents"],
64
+ agents: { hosts, skills },
65
+ ...BASE_CONFIG,
66
+ },
67
+ workspace: { root, manifest: { name: "fixture" }, packages: packages.map((dir) => ({ dir, manifest: {} })) },
68
+ read: (rel) => {
69
+ try {
70
+ return fs.readFileSync(path.join(root, rel), "utf8");
71
+ } catch {
72
+ return null;
73
+ }
74
+ },
75
+ };
76
+ }
77
+
78
+ function render(
79
+ root: string,
80
+ hosts: readonly Host[],
81
+ skills: readonly SkillPack[] = [],
82
+ packages: readonly string[] = [],
83
+ ): Output[] {
84
+ const result = renderTrees(context(root, hosts, skills, packages));
85
+ if (!result.ok) throw new Error(result.errors.join("\n"));
86
+ return result.outputs;
87
+ }
88
+
89
+ function find(outputs: Output[], rel: string): Output | undefined {
90
+ return outputs.find((output) => output.path === rel);
91
+ }
92
+
93
+ function text(outputs: Output[], rel: string): string {
94
+ const output = find(outputs, rel);
95
+ if (output?.kind !== "file") throw new Error(`${rel} is not a file output`);
96
+ return output.content;
97
+ }
98
+
99
+ function linkTarget(outputs: Output[], rel: string): string | undefined {
100
+ const output = find(outputs, rel);
101
+ return output?.kind === "link" ? output.target : undefined;
102
+ }
103
+
104
+ function json(outputs: Output[], rel: string): Record<string, any> {
105
+ return JSON.parse(text(outputs, rel)) as Record<string, any>;
106
+ }
107
+
108
+ const paths = (outputs: Output[]) => outputs.map((output) => output.path);
109
+
110
+ describe("renderTrees with every host", { timeout: RENDER_TIMEOUT }, () => {
111
+ const root = consumer({
112
+ ".vscode/tasks.json": JSON.stringify({
113
+ version: "2.0.0",
114
+ tasks: [{ label: "mine", type: "shell", command: "make all", dependsOn: ["x"] }],
115
+ }),
116
+ });
117
+ const outputs = render(root, HOSTS);
118
+
119
+ it("writes AGENTS.md, .mcp.json and every host tree, untracked", () => {
120
+ const top = new Set(paths(outputs).map((rel) => (rel.includes("/") ? rel.slice(0, rel.indexOf("/")) : rel)));
121
+ expect([...top].sort()).toEqual(
122
+ [".agents", ".claude", ".codex", ".cursor", ".grok", ".mcp.json", ".vscode", "AGENTS.md"].sort(),
123
+ );
124
+ for (const output of outputs.filter((o) => o.path !== ".vscode/tasks.json")) expect(output.tracked).toBe(false);
125
+ });
126
+
127
+ it("generates the committed-shape files the generators and the emit steps own", () => {
128
+ for (const rel of [
129
+ ".claude/settings.json",
130
+ ".claude/agents/scout.md",
131
+ ".codex/config.toml",
132
+ ".codex/hooks.json",
133
+ ".agents/hooks.json",
134
+ ".agents/agents/scout.md",
135
+ ".cursor/hooks.json",
136
+ ".grok/config.toml",
137
+ ".grok/agents/scout.md",
138
+ ]) {
139
+ expect(find(outputs, rel)?.kind, rel).toBe("file");
140
+ }
141
+ expect(json(outputs, ".mcp.json")).toHaveProperty("mcpServers");
142
+ expect(text(outputs, ".claude/settings.json")).toContain("repo");
143
+ });
144
+
145
+ it("copies the static grok files and never a .grok/hooks script", () => {
146
+ expect(find(outputs, ".grok/roles/scout.toml")?.kind).toBe("file");
147
+ expect(find(outputs, ".grok/hooks/rulesync.json")?.kind).toBe("file");
148
+ expect(find(outputs, ".grok/hooks/orchestrate-parent.json")?.kind).toBe("file");
149
+ expect(paths(outputs).filter((rel) => rel.startsWith(".grok/hooks/") && /\.(ts|tsx)$/.test(rel))).toEqual([]);
150
+ expect(find(outputs, ".grok/hooks/package.json")).toBeUndefined();
151
+ });
152
+
153
+ it("keeps a consumer's own task in the tracked .vscode/tasks.json", () => {
154
+ const tasks = json(outputs, ".vscode/tasks.json");
155
+ expect(find(outputs, ".vscode/tasks.json")?.tracked).toBe(true);
156
+ expect(tasks.tasks.map((task: { label: string }) => task.label)).toEqual(["build", "mine"]);
157
+ });
158
+
159
+ it("removes the stage dir", () => {
160
+ expect(stagesIn(root)).toEqual([]);
161
+ });
162
+ });
163
+
164
+ describe("renderTrees by host", { timeout: RENDER_TIMEOUT }, () => {
165
+ it("claude alone writes AGENTS.md, .mcp.json and .claude, with skills linked straight to stock", () => {
166
+ const outputs = render(consumer(), ["claude"]);
167
+ const rest = paths(outputs).filter((rel) => rel !== ".vscode/tasks.json");
168
+ expect(rest.filter((rel) => !rel.startsWith(".claude/")).sort()).toEqual(["AGENTS.md", ".mcp.json"].sort());
169
+ expect(linkTarget(outputs, ".claude/skills/ask")).toBe(`${STOCK_SKILL}/ask`);
170
+ expect(find(outputs, ".claude/skills/ask")?.tracked).toBe(false);
171
+ expect(find(outputs, ".claude/skills/listen")).toBeUndefined();
172
+ });
173
+
174
+ it("codex alone links .agents/skills and writes no .agents/agents", () => {
175
+ const outputs = render(consumer(), ["codex"]);
176
+ expect(linkTarget(outputs, ".agents/skills/ask")).toBe(`${STOCK_SKILL}/ask`);
177
+ expect(paths(outputs).filter((rel) => rel.startsWith(".agents/") && !rel.startsWith(".agents/skills/"))).toEqual(
178
+ [],
179
+ );
180
+ expect(paths(outputs).some((rel) => rel.startsWith(".claude/"))).toBe(false);
181
+ expect(find(outputs, ".mcp.json")).toBeUndefined();
182
+ expect(find(outputs, ".codex/config.toml")?.kind).toBe("file");
183
+ expect(find(outputs, ".agents/mcp_config.json")).toBeUndefined();
184
+ });
185
+
186
+ it("antigravity alone writes .agents, and claude links through it when both are on", () => {
187
+ const agy = render(consumer(), ["antigravity"]);
188
+ expect(find(agy, ".agents/agents/scout.md")?.kind).toBe("file");
189
+ expect(linkTarget(agy, ".agents/skills/ask")).toBe(`${STOCK_SKILL}/ask`);
190
+ const both = render(consumer(), ["claude", "antigravity"]);
191
+ expect(linkTarget(both, ".claude/skills/ask")).toBe("../../.agents/skills/ask");
192
+ });
193
+
194
+ it("grok and cursor alone write only their dirs and AGENTS.md", () => {
195
+ const outputs = render(consumer(), ["grok", "cursor"]);
196
+ const top = new Set(paths(outputs).map((rel) => rel.split("/")[0]));
197
+ expect([...top].sort()).toEqual([".cursor", ".grok", ".vscode", "AGENTS.md"].sort());
198
+ });
199
+
200
+ it("no hosts writes nothing and runs nothing", () => {
201
+ const root = consumer();
202
+ expect(render(root, [])).toEqual([]);
203
+ expect(fs.existsSync(path.join(root, ".repo"))).toBe(false);
204
+ });
205
+ });
206
+
207
+ describe("renderTrees skills", { timeout: RENDER_TIMEOUT }, () => {
208
+ it("leaves out an optional pack unless agents.skills lists it", () => {
209
+ const off = render(consumer(), ["claude", "codex"]);
210
+ expect(find(off, ".agents/skills/listen")).toBeUndefined();
211
+ expect(find(off, ".claude/skills/listen")).toBeUndefined();
212
+ const on = render(consumer(), ["claude", "codex"], ["listen"]);
213
+ expect(linkTarget(on, ".agents/skills/listen")).toBe(`${STOCK_SKILL}/listen`);
214
+ expect(linkTarget(on, ".claude/skills/listen")).toBe("../../.agents/skills/listen");
215
+ });
216
+
217
+ it("lets a .repo/skills skill win over the stock one and add its own", () => {
218
+ const root = consumer({
219
+ ".repo/skills/ask/SKILL.md": "---\nname: ask\ndescription: mine\noverrides: ask\n---\n",
220
+ ".repo/skills/deploy/SKILL.md": "---\nname: deploy\ndescription: ship\n---\n",
221
+ });
222
+ const claude = render(root, ["claude"]);
223
+ expect(linkTarget(claude, ".claude/skills/ask")).toBe("../../.repo/skills/ask");
224
+ expect(linkTarget(claude, ".claude/skills/deploy")).toBe("../../.repo/skills/deploy");
225
+ const codex = render(root, ["codex"]);
226
+ expect(linkTarget(codex, ".agents/skills/ask")).toBe("../../.repo/skills/ask");
227
+ expect(linkTarget(codex, ".agents/skills/bro")).toBe(`${STOCK_SKILL}/bro`);
228
+ });
229
+ });
230
+
231
+ describe("a .repo/skills dir named like a stock skill", () => {
232
+ const failures = (files: Record<string, string>): string[] => {
233
+ const result = renderTrees(context(consumer(files), ["claude"]));
234
+ return result.ok ? [] : result.errors;
235
+ };
236
+
237
+ it("fails without overrides in its SKILL.md and names the fix", () => {
238
+ expect(failures({ ".repo/skills/ask/SKILL.md": "---\nname: ask\ndescription: mine\n---\n" })).toEqual([
239
+ ".repo/skills/ask: a stock skill has this name; add `overrides: ask` to its SKILL.md or rename it",
240
+ ]);
241
+ });
242
+
243
+ it("fails when SKILL.md has no frontmatter at all", () => {
244
+ expect(failures({ ".repo/skills/ask/SKILL.md": "no frontmatter\n" })).toHaveLength(1);
245
+ });
246
+
247
+ it("links the own skill when SKILL.md says overrides with its name", () => {
248
+ const root = consumer({ ".repo/skills/ask/SKILL.md": "---\nname: ask\ndescription: mine\noverrides: ask\n---\n" });
249
+ expect(linkTarget(render(root, ["claude"]), ".claude/skills/ask")).toBe("../../.repo/skills/ask");
250
+ });
251
+
252
+ it("fails an overrides that names no stock skill", () => {
253
+ expect(
254
+ failures({ ".repo/skills/mine/SKILL.md": "---\nname: mine\ndescription: mine\noverrides: nope\n---\n" }),
255
+ ).toEqual(['.repo/skills/mine/SKILL.md: overrides "nope" but no stock skill has that name']);
256
+ });
257
+
258
+ it("does not ask a new skill for anything", () => {
259
+ expect(failures({ ".repo/skills/deploy/SKILL.md": "---\nname: deploy\ndescription: ship\n---\n" })).toEqual([]);
260
+ });
261
+
262
+ it("reports skill and rule problems together", () => {
263
+ expect(
264
+ failures({
265
+ ".repo/skills/ask/SKILL.md": "---\nname: ask\ndescription: mine\n---\n",
266
+ ".repo/rules/a.md": "no fence\n",
267
+ }),
268
+ ).toEqual([
269
+ ".repo/skills/ask: a stock skill has this name; add `overrides: ask` to its SKILL.md or rename it",
270
+ ".repo/rules/a.md: missing frontmatter (start the file with a --- block)",
271
+ ]);
272
+ });
273
+ });
274
+
275
+ describe("renderTrees overlay", { timeout: RENDER_TIMEOUT }, () => {
276
+ it("puts a .repo/rules rule in AGENTS.md", () => {
277
+ const root = consumer({
278
+ ".repo/rules/house.md": '---\ndescription: house\npaths: ["**/*"]\n---\nAlways greet the cat.\n',
279
+ });
280
+ expect(text(render(root, ["claude"]), "AGENTS.md")).toContain("Always greet the cat.");
281
+ });
282
+
283
+ it("keeps the .repo/rules rule in AGENTS.md with every host on", () => {
284
+ const root = consumer({
285
+ ".repo/rules/house.md": '---\ndescription: house\npaths: ["**/*"]\n---\nAlways greet the cat.\n',
286
+ });
287
+ expect(text(render(root, HOSTS), "AGENTS.md")).toContain("Always greet the cat.");
288
+ });
289
+
290
+ describe("a rule with paths", () => {
291
+ const rule = "---\ndescription: Team rule\npaths:\n - src/**\n---\nTEAM RULE TEXT\n";
292
+ const reach = (hosts: readonly Host[]) => {
293
+ const outputs = render(consumer({ ".repo/rules/team.md": rule }), hosts);
294
+ return outputs.filter((o) => o.kind === "file" && o.content.includes("TEAM RULE TEXT")).map((o) => o.path);
295
+ };
296
+
297
+ it("lands in .claude/rules for claude and is only pointed to from AGENTS.md for codex", () => {
298
+ expect(reach(["claude"])).toEqual([".claude/rules/team.md"]);
299
+ expect(reach(["codex"])).toEqual([]);
300
+ const agents = text(render(consumer({ ".repo/rules/team.md": rule }), ["codex"]), "AGENTS.md");
301
+ expect(agents).toContain("- `src/**`: read `.repo/rules/team.md`");
302
+ });
303
+
304
+ it("reports every source problem in one failed render", () => {
305
+ const root = consumer({ ".repo/rules/a.md": "no fence\n", ".repo/rules/b.md": "---\npaths: []\n---\nB\n" });
306
+ const result = renderTrees(context(root, ["claude"]));
307
+ expect(result.ok).toBe(false);
308
+ if (!result.ok) {
309
+ expect(result.errors).toEqual([
310
+ ".repo/rules/a.md: missing frontmatter (start the file with a --- block)",
311
+ ".repo/rules/b.md: invalid frontmatter: description is required",
312
+ ]);
313
+ }
314
+ });
315
+ });
316
+
317
+ it("merges .repo/settings/claude.json into .claude/settings.json", () => {
318
+ const root = consumer({
319
+ ".repo/settings/claude.json": JSON.stringify({
320
+ env: { MINE: "1" },
321
+ enableAllProjectMcpServers: true,
322
+ permissions: { allow: ["Bash(ls:*)"], deny: ["Agent(Explore)", "Read(./secret)"] },
323
+ }),
324
+ });
325
+ const settings = json(render(root, ["claude"]), ".claude/settings.json");
326
+ expect(settings.env).toEqual({ MINE: "1" });
327
+ expect(settings.permissions.allow).toContain("Bash(ls:*)");
328
+ expect(settings.enableAllProjectMcpServers).toBe(true);
329
+ expect(settings.permissions.deny.filter((rule: string) => rule === "Agent(Explore)")).toHaveLength(1);
330
+ expect(settings.permissions.deny).toContain("Read(./secret)");
331
+ expect(Object.keys(settings.hooks).length).toBeGreaterThan(0);
332
+ });
333
+
334
+ it("appends .repo hooks after the generated ones and never repeats a stock hook", () => {
335
+ const plain = json(render(consumer(), ["claude"]), ".claude/settings.json");
336
+ const stockGroup = plain.hooks.PreToolUse[0];
337
+ const mine = { matcher: "Write", hooks: [{ type: "command", command: "make post-write", timeout: 5 }] };
338
+ const root = consumer({
339
+ ".repo/settings/claude.json": JSON.stringify({ hooks: { PreToolUse: [stockGroup], PostToolUse: [mine] } }),
340
+ });
341
+ const settings = json(render(root, ["claude"]), ".claude/settings.json");
342
+ expect(settings.hooks.PreToolUse).toEqual(plain.hooks.PreToolUse);
343
+ expect(settings.hooks.PostToolUse).toEqual([...(plain.hooks.PostToolUse ?? []), mine]);
344
+ expect(settings.hooks.PostToolUse.at(-1)).toEqual(mine);
345
+ });
346
+
347
+ it("reports an unreadable .repo/settings/claude.json", () => {
348
+ const root = consumer({ ".repo/settings/claude.json": "{" });
349
+ const result = renderTrees(context(root, ["claude"]));
350
+ expect(result).toEqual({ ok: false, errors: [expect.stringContaining(".repo/settings/claude.json")] });
351
+ expect(stagesIn(root)).toEqual([]);
352
+ });
353
+
354
+ it("reports a .repo/settings/claude.json that is not an object", () => {
355
+ const root = consumer({ ".repo/settings/claude.json": "[]" });
356
+ expect(renderTrees(context(root, ["claude"]))).toEqual({
357
+ ok: false,
358
+ errors: [expect.stringContaining("must be a JSON object")],
359
+ });
360
+ });
361
+
362
+ it("deep-merges .repo/mcp.json over the generated .mcp.json", () => {
363
+ const server = { command: "npx", args: ["-y", "mine"] };
364
+ const root = consumer({ ".repo/mcp.json": JSON.stringify({ mcpServers: { mine: server } }) });
365
+ const mcp = json(render(root, ["claude"]), ".mcp.json");
366
+ expect(mcp.mcpServers.mine).toEqual(server);
367
+ expect(json(render(consumer(), ["claude"]), ".mcp.json").mcpServers.mine).toBeUndefined();
368
+ });
369
+
370
+ it("reports an unreadable .repo/mcp.json, and one that is not an object", () => {
371
+ expect(renderTrees(context(consumer({ ".repo/mcp.json": "{" }), ["claude"]))).toEqual({
372
+ ok: false,
373
+ errors: [expect.stringContaining(".repo/mcp.json")],
374
+ });
375
+ expect(renderTrees(context(consumer({ ".repo/mcp.json": "[]" }), ["claude"]))).toEqual({
376
+ ok: false,
377
+ errors: [".repo/mcp.json: must be a JSON object"],
378
+ });
379
+ });
380
+
381
+ it("ignores .repo/mcp.json when claude is off", () => {
382
+ expect(renderTrees(context(consumer({ ".repo/mcp.json": "{" }), ["codex"])).ok).toBe(true);
383
+ });
384
+
385
+ it("reports an emit step that rejects the merged mcp policy", () => {
386
+ const root = consumer({ ".repo/mcp-policy.jsonc": '{ "allow": 5 }' });
387
+ expect(renderTrees(context(root, ["claude"]))).toEqual({
388
+ ok: false,
389
+ errors: ["mcp policy: allow must be an array"],
390
+ });
391
+ expect(stagesIn(root)).toEqual([]);
392
+ });
393
+
394
+ it("ignores .repo/settings/claude.json when claude is off", () => {
395
+ const root = consumer({ ".repo/settings/claude.json": "{" });
396
+ expect(renderTrees(context(root, ["codex"])).ok).toBe(true);
397
+ });
398
+
399
+ it("keeps a consumer scripts/ hook and a .repo/permissions.json deny in the hook files", () => {
400
+ const root = consumer({
401
+ "scripts/daily-pull-check.mjs": "// pull\n",
402
+ ".repo/permissions.json": JSON.stringify({ deny: ["Edit(./vendor/**)"] }),
403
+ });
404
+ const outputs = render(root, ["claude", "codex"]);
405
+ expect(text(outputs, ".claude/settings.json")).toContain("daily-pull-check.mjs");
406
+ expect(text(outputs, ".claude/settings.json")).toContain("Edit(./vendor/**)");
407
+ expect(text(outputs, ".codex/hooks.json")).toContain("daily-pull-check.mjs");
408
+ });
409
+
410
+ it("merges a .repo/mcp-policy.jsonc over the stock policy", () => {
411
+ const root = consumer({
412
+ ".repo/mcp-policy.jsonc": '{ "allow": ["mine"], "servers": { "mine": { "command": "x" } } }',
413
+ });
414
+ expect(json(render(root, ["claude"]), ".mcp.json").mcpServers).toHaveProperty("mine");
415
+ });
416
+ });
417
+
418
+ describe("renderTrees with workspace packages", { timeout: RENDER_TIMEOUT }, () => {
419
+ const pkgRule = "---\ndescription: Web rule\n---\nWEB PACKAGE RULE\n";
420
+
421
+ const skill = "---\nname: deploy\ndescription: Ships the package.\n---\nDeploy.\n";
422
+ const apiRule = "---\ndescription: Api\npaths:\n - src/api/**\n---\nAPI RULE\n";
423
+ const web = () =>
424
+ consumer({
425
+ "packages/web/.repo/rules/web.md": pkgRule,
426
+ "packages/web/.repo/rules/api.md": apiRule,
427
+ "packages/web/.repo/skills/deploy/SKILL.md": skill,
428
+ "packages/api/package.json": "{}",
429
+ ".repo/rules/house.md": "---\ndescription: h\n---\nHOUSE\n",
430
+ });
431
+ const renderWeb = (hosts: readonly Host[]) => render(web(), hosts, [], ["packages/web", "packages/api"]);
432
+ const linkTarget = (outputs: Output[], rel: string) => {
433
+ const found = find(outputs, rel);
434
+ return found?.kind === "link" ? found.target : undefined;
435
+ };
436
+
437
+ it("writes each package's AGENTS.md and CLAUDE.md, untracked, and keeps its rules out of the root AGENTS.md", () => {
438
+ const outputs = renderWeb(HOSTS);
439
+ expect(text(outputs, "AGENTS.md")).toContain("HOUSE");
440
+ expect(text(outputs, "AGENTS.md")).not.toMatch(/WEB PACKAGE RULE|API RULE/);
441
+ expect(text(outputs, "packages/web/AGENTS.md")).toBe(
442
+ "Start at references/INDEX.md in this package.\n\nWEB PACKAGE RULE\n\n## Rules by path\n- `packages/web/src/api/**`: read `packages/web/.repo/rules/api.md`\n",
443
+ );
444
+ expect(text(outputs, "packages/web/CLAUDE.md")).toBe("Start at references/INDEX.md in this package.\n");
445
+ expect(text(outputs, "packages/api/AGENTS.md")).toBe("Start at references/INDEX.md in this package.\n");
446
+ for (const rel of ["packages/web/AGENTS.md", "packages/web/CLAUDE.md"])
447
+ expect(find(outputs, rel)).toMatchObject({ tracked: false, managed: "whole" });
448
+ });
449
+
450
+ it("renders package rules under the host rule dirs and nests the package's skills per host", () => {
451
+ const outputs = renderWeb(HOSTS);
452
+ expect(text(outputs, ".claude/rules/packages/web/web.md")).toBe(
453
+ "---\npaths:\n - packages/web/**\n---\nWEB PACKAGE RULE\n",
454
+ );
455
+ expect(text(outputs, ".agents/rules/packages/web/api.md")).toContain("globs: packages/web/src/api/**");
456
+ expect(linkTarget(outputs, "packages/web/.claude/skills/deploy")).toBe("../../.repo/skills/deploy");
457
+ expect(linkTarget(outputs, "packages/web/.agents/skills/deploy")).toBe("../../.repo/skills/deploy");
458
+ expect(find(outputs, "packages/web/.claude/skills/deploy")).toMatchObject({ tracked: false, managed: "whole" });
459
+ expect(outputs.filter((o) => o.path.startsWith("packages/api/") && o.kind === "link")).toEqual([]);
460
+ });
461
+
462
+ it("links package skills only for the hosts that read them", () => {
463
+ const claude = renderWeb(["claude"]);
464
+ expect(find(claude, "packages/web/.claude/skills/deploy")).toBeDefined();
465
+ expect(find(claude, "packages/web/.agents/skills/deploy")).toBeUndefined();
466
+ expect(find(claude, "packages/web/CLAUDE.md")).toBeDefined();
467
+ const codex = renderWeb(["codex"]);
468
+ expect(find(codex, "packages/web/.agents/skills/deploy")).toBeDefined();
469
+ expect(find(codex, "packages/web/.claude/skills/deploy")).toBeUndefined();
470
+ expect(find(codex, "packages/web/CLAUDE.md")).toBeUndefined();
471
+ expect(find(codex, ".claude/rules/packages/web/web.md")).toBeUndefined();
472
+ });
473
+
474
+ it("refuses a root rule and a package rule that render one file, naming both sources", () => {
475
+ const root = consumer({
476
+ ".repo/rules/packages/web/web.md": "---\ndescription: r\npaths:\n - x/**\n---\nROOT\n",
477
+ "packages/web/.repo/rules/web.md": pkgRule,
478
+ "x/a.ts": "",
479
+ });
480
+ expect(renderTrees(context(root, ["claude"], [], ["packages/web"]))).toEqual({
481
+ ok: false,
482
+ errors: [
483
+ ".claude/rules/packages/web/web.md: planned by .repo/rules/packages/web/web.md and packages/web/.repo/rules/web.md; rename one",
484
+ ],
485
+ });
486
+ });
487
+
488
+ it("refuses a package .repo with a directory sync does not read there", () => {
489
+ const root = consumer({ "packages/web/.repo/lessons/a.md": "x\n" });
490
+ const result = renderTrees(context(root, ["claude"], [], ["packages/web"]));
491
+ expect(result).toEqual({
492
+ ok: false,
493
+ errors: [
494
+ "packages/web/.repo/lessons/: a package .repo holds only rules/ and skills/; lessons live in the root .repo",
495
+ ],
496
+ });
497
+ });
498
+ });
499
+
500
+ describe("renderTrees tasks", { timeout: RENDER_TIMEOUT }, () => {
501
+ it("fails on a tasks.json it cannot parse", () => {
502
+ const root = consumer({ ".vscode/tasks.json": "{" });
503
+ const result = renderTrees(context(root, ["claude"]));
504
+ expect(result).toEqual({ ok: false, errors: [expect.stringContaining(".vscode/tasks.json")] });
505
+ expect(stagesIn(root)).toEqual([]);
506
+ });
507
+ });
508
+
509
+ describe("renderTrees sources", { timeout: RENDER_TIMEOUT }, () => {
510
+ it("names a .repo rule without frontmatter and removes the stage", () => {
511
+ const root = consumer({ ".repo/rules/nofm.md": "no frontmatter here\n" });
512
+ expect(renderTrees(context(root, ["codex"]))).toEqual({
513
+ ok: false,
514
+ errors: [".repo/rules/nofm.md: missing frontmatter (start the file with a --- block)"],
515
+ });
516
+ expect(stagesIn(root)).toEqual([]);
517
+ });
518
+
519
+ it("reports a missing stock package", () => {
520
+ const bare = tempDir();
521
+ write(bare, "workerstack.config.ts", "export default {}\n");
522
+ expect(renderTrees(context(bare, ["claude"]))).toEqual({
523
+ ok: false,
524
+ errors: [expect.stringContaining("bun install")],
525
+ });
526
+ });
527
+ });
528
+
529
+ describe("renderTrees in this repo", { timeout: RENDER_TIMEOUT }, () => {
530
+ it("keeps every output tracked and links skills inside the repo", () => {
531
+ const result = renderTrees({
532
+ ...context(SELF_ROOT, HOSTS, ["google-workspace", "listen", "speak", "clasp"]),
533
+ workspace: { root: SELF_ROOT, manifest: { name: "@workerstack/repo" }, packages: [] },
534
+ });
535
+ if (!result.ok) throw new Error(result.errors.join("\n"));
536
+ expect(result.outputs.every((output) => output.tracked)).toBe(true);
537
+ expect(linkTarget(result.outputs, ".claude/skills/ask")).toBe("../../agents/skills/ask");
538
+ expect(linkTarget(result.outputs, ".agents/skills/ask")).toBe("../../agents/skills/ask");
539
+ });
540
+
541
+ it("renders the same files while other processes render the same root", async () => {
542
+ const trees = pathToFileURL(path.join(SELF_ROOT, "cli", "sync", "trees.ts")).href;
543
+ const script = `
544
+ const { renderTrees } = await import(${JSON.stringify(trees)});
545
+ const seen = new Set();
546
+ for (let i = 0; i < 6; i++) {
547
+ const result = renderTrees({
548
+ root: ${JSON.stringify(SELF_ROOT)},
549
+ config: { packs: ["agents"], agents: { hosts: ${JSON.stringify(HOSTS)}, skills: [] } },
550
+ workspace: { root: ${JSON.stringify(SELF_ROOT)}, manifest: { name: "@workerstack/repo" }, packages: [] },
551
+ read: () => null,
552
+ });
553
+ seen.add(result.ok ? result.outputs.map((output) => output.path).join(",") : result.errors.join("\\n"));
554
+ }
555
+ process.stdout.write(JSON.stringify([...seen]));`;
556
+ const child = () =>
557
+ new Promise<string>((resolve, reject) => {
558
+ const proc = spawn(BUN, ["-e", script], { cwd: SELF_ROOT, stdio: ["ignore", "pipe", "inherit"] });
559
+ let out = "";
560
+ proc.stdout.on("data", (chunk) => (out += String(chunk)));
561
+ proc.on("error", reject);
562
+ proc.on("close", () => resolve(out));
563
+ });
564
+ const results = await Promise.all([child(), child(), child(), child()]);
565
+ expect(new Set(results).size).toBe(1);
566
+ expect(JSON.parse(results[0] as string)).toHaveLength(1);
567
+ });
568
+
569
+ it("regenerates the committed trees byte for byte", async () => {
570
+ const out: string[] = [];
571
+ const code = await runSync(["--check"], { cwd: SELF_ROOT, stdout: (s) => out.push(s), stderr: (s) => out.push(s) });
572
+ expect(out.join("")).toBe("repo sync: up to date\n");
573
+ expect(code).toBe(0);
574
+ });
575
+ });
576
+
577
+ describe("repo sync with trees", { timeout: RENDER_TIMEOUT }, () => {
578
+ async function sync(root: string, ...args: string[]) {
579
+ const out: string[] = [];
580
+ const err: string[] = [];
581
+ const code = await runSync(args, { cwd: root, stdout: (s) => out.push(s), stderr: (s) => err.push(s) });
582
+ return { code, stdout: out.join(""), stderr: err.join("") };
583
+ }
584
+
585
+ it("replaces a dangling skill link left by a removed node_modules instead of following it", async () => {
586
+ const root = consumer();
587
+ write(root, "workerstack.config.ts", 'export default { agents: { hosts: ["claude"] } }\n');
588
+ fs.mkdirSync(path.join(root, ".claude", "skills"), { recursive: true });
589
+ fs.symlinkSync("../../gone/ask", path.join(root, ".claude", "skills", "ask"), "dir");
590
+
591
+ expect((await sync(root)).code).toBe(0);
592
+ const link = path.join(root, ".claude", "skills", "ask");
593
+ // A win32 junction reads back as the absolute path it resolved to, so it is the target that is compared there.
594
+ const stock = path.resolve(path.dirname(link), `${STOCK_SKILL}/ask`);
595
+ expect(fs.realpathSync(link)).toBe(fs.realpathSync(stock));
596
+ expect(process.platform === "win32" ? "" : linkText(link)).toBe(
597
+ process.platform === "win32" ? "" : `${STOCK_SKILL}/ask`,
598
+ );
599
+ expect(fs.existsSync(path.join(link, "SKILL.md"))).toBe(true);
600
+ expect(fs.existsSync(path.join(root, "gone"))).toBe(false);
601
+ expect(await sync(root, "--check")).toEqual({ code: 0, stdout: "repo sync: up to date\n", stderr: "" });
602
+ });
603
+
604
+ it("reports a failed render and leaves no stage behind", async () => {
605
+ const root = consumer({ ".repo/settings/claude.json": "{" });
606
+ write(root, "workerstack.config.ts", 'export default { agents: { hosts: ["claude"] } }\n');
607
+ const result = await sync(root);
608
+ expect(result.code).toBe(1);
609
+ expect(result.stderr).toContain(".repo/settings/claude.json");
610
+ expect(stagesIn(root)).toEqual([]);
611
+ expect(fs.existsSync(path.join(root, "AGENTS.md"))).toBe(false);
612
+ });
613
+
614
+ it("fails on a .repo subagent with broken frontmatter, naming the file", async () => {
615
+ const root = consumer({ ".repo/subagents/bad.md": "---\nname: [unclosed\n---\nbody\n" });
616
+ write(root, "workerstack.config.ts", 'export default { agents: { hosts: ["claude"] } }\n');
617
+ const result = await sync(root);
618
+ expect(result.code).toBe(1);
619
+ expect(result.stderr).toContain("sync: .repo/subagents/bad.md: invalid frontmatter: ");
620
+ });
621
+ });