@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,244 @@
1
+ /**
2
+ * The inputs every agent generator reads: stock `agents/` and the consumer's `.repo/` overlay, merged.
3
+ * A `.repo/` rule or subagent replaces a stock one in place only when its frontmatter says `overrides: <name>`
4
+ * (the stock path without `.md`, case-folded); a same-name file without it, or an `overrides` naming nothing, is a
5
+ * problem. New files follow in path order. `.repo/hooks.jsonc` replaces the stock hooks whole.
6
+ *
7
+ * Frontmatter is user input, so it is checked here, once: a rule needs a `description` and may carry `paths` (a list
8
+ * of strings); `overrides` is a string. Every problem is collected, never thrown one by one.
9
+ */
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import { stripJsonc } from "../../scripts/jsonc.ts";
13
+ import { type ParsedDoc, parseDoc } from "./frontmatter.ts";
14
+ import { TARGET } from "./subagents.ts";
15
+
16
+ /** One rule or subagent source file. */
17
+ export interface Doc extends ParsedDoc {
18
+ /** POSIX path from its root, for messages: `agents/rules/overview.md`, `.repo/subagents/x/y.md`. */
19
+ rel: string;
20
+ /** POSIX path inside `rules/` or `subagents/`, which outputs are named after: `overview.md`, `x/y.md`. */
21
+ file: string;
22
+ /** `stock` for the installed package's `agents/`, `repo` for anything under a `.repo/`, root or package. */
23
+ origin: "stock" | "repo";
24
+ /** `""` for stock and the root `.repo/`; the package dir (`packages/web`) for a package's `.repo/`. */
25
+ scope: string;
26
+ }
27
+
28
+ interface HooksSource {
29
+ rel: string;
30
+ value: Record<string, unknown>;
31
+ }
32
+
33
+ export interface Sources {
34
+ rules: Doc[];
35
+ /** In merge order; subagents.ts re-sorts them shallowest-first as rulesync did. */
36
+ subagents: Doc[];
37
+ hooks: HooksSource;
38
+ }
39
+
40
+ /** Root-relative directory of each source root: stock first, the consumer overlay second. */
41
+ const STOCK_DIR = "agents";
42
+ const CONSUMER_DIR = ".repo";
43
+
44
+ /**
45
+ * The docs that loaded and every problem found, each starting with the file it is about. A doc with a problem is left
46
+ * out of `sources`, and so is a hooks file that failed (its `value` is then empty); a load with problems is not to be rendered.
47
+ */
48
+ export interface LoadResult {
49
+ sources: Sources;
50
+ problems: string[];
51
+ }
52
+
53
+ type Kind = "rules" | "subagents";
54
+
55
+ /**
56
+ * Read and merge the sources. Bad frontmatter, ownership of a stock name and bad hooks JSONC are problems, collected beside what loaded.
57
+ * `packages` are workspace package dirs (root-relative), read from `consumerRoot` (or `stockRoot` when there is none, as in
58
+ * this repo): each one's `.repo/rules` loads after the merged root rules, in the order given, never merged by name and never
59
+ * overriding; `overrides` there is a problem. Anything else in a package `.repo/` is for `repoDirProblems` to refuse.
60
+ */
61
+ export function loadSources(roots: {
62
+ stockRoot: string;
63
+ consumerRoot: string | null;
64
+ packages: readonly string[];
65
+ }): LoadResult {
66
+ const tops = [
67
+ { root: roots.stockRoot, dir: STOCK_DIR, origin: "stock" as const },
68
+ ...(roots.consumerRoot === null ? [] : [{ root: roots.consumerRoot, dir: CONSUMER_DIR, origin: "repo" as const }]),
69
+ ];
70
+ const problems: string[] = [];
71
+ const docs = (kind: Kind) =>
72
+ mergeDocs(
73
+ kind,
74
+ tops.map((top) => listDocs(top.root, top.dir, kind, top.origin, "")),
75
+ problems,
76
+ );
77
+ const packageRoot = roots.consumerRoot ?? roots.stockRoot;
78
+ const packageRules = roots.packages.flatMap((dir) =>
79
+ listDocs(packageRoot, `${dir}/${CONSUMER_DIR}`, "rules", "repo", dir).flatMap((file) => {
80
+ const doc = read("rules", file, problems);
81
+ if (doc?.data["overrides"] !== undefined) {
82
+ problems.push(`${file.rel}: overrides has no meaning in a package rule; remove it`);
83
+ return [];
84
+ }
85
+ return doc === null ? [] : [doc];
86
+ }),
87
+ );
88
+ const rules = [...docs("rules"), ...packageRules];
89
+ const subagents = docs("subagents");
90
+ const hooks = loadHooks(tops, problems);
91
+ return { sources: { rules, subagents, hooks }, problems };
92
+ }
93
+
94
+ interface DocFile {
95
+ abs: string;
96
+ rel: string;
97
+ file: string;
98
+ origin: Doc["origin"];
99
+ scope: string;
100
+ }
101
+
102
+ /** `<root>/<dir>/<kind>/**\/*.md` in plain string order of the POSIX path; dot entries skipped, symlinks followed. */
103
+ function listDocs(root: string, dir: string, kind: string, origin: Doc["origin"], scope: string): DocFile[] {
104
+ const base = path.join(root, dir, kind);
105
+ const found: DocFile[] = [];
106
+ const walk = (abs: string, file: string) => {
107
+ for (const name of fs.readdirSync(abs)) {
108
+ if (name.startsWith(".")) continue;
109
+ const child = path.join(abs, name);
110
+ const childFile = file === "" ? name : `${file}/${name}`;
111
+ if (fs.statSync(child).isDirectory()) walk(child, childFile);
112
+ else if (name.endsWith(".md"))
113
+ found.push({ abs: child, rel: `${dir}/${kind}/${childFile}`, file: childFile, origin, scope });
114
+ }
115
+ };
116
+ if (fs.existsSync(base)) walk(base, "");
117
+ // Paths in one root are unique, so the comparator never sees a tie.
118
+ return found.sort((a, b) => (a.file < b.file ? -1 : 1));
119
+ }
120
+
121
+ const nameOf = (file: string) => file.replace(/\.md$/, "").toLowerCase();
122
+
123
+ /**
124
+ * Stock files first, then each later root's: a file with `overrides: <name>` replaces the earlier file of that name
125
+ * in place; a file whose own name is taken, or whose `overrides` names nothing, is a problem. Only the files that
126
+ * survive are parsed.
127
+ */
128
+ function mergeDocs(kind: Kind, perRoot: DocFile[][], problems: string[]): Doc[] {
129
+ const singular = kind === "rules" ? "rule" : "subagent";
130
+ const merged: (Doc | null)[] = [];
131
+ const at = new Map<string, number>();
132
+ const stockFiles = perRoot[0]!;
133
+ const stockNames = new Set(stockFiles.map((file) => nameOf(file.file)));
134
+ const replaced = new Set<string>();
135
+
136
+ const parsed = perRoot
137
+ .slice(1)
138
+ .flat()
139
+ .map((file) => ({ file, doc: read(kind, file, problems) }));
140
+ for (const { file, doc } of parsed) {
141
+ const overrides = doc?.data["overrides"];
142
+ if (typeof overrides === "string" && stockNames.has(overrides.toLowerCase())) replaced.add(overrides.toLowerCase());
143
+ else if (typeof overrides === "string")
144
+ problems.push(`${file.rel}: overrides "${overrides}" but no stock ${singular} has that name`);
145
+ else if (doc !== null && stockNames.has(nameOf(file.file))) {
146
+ problems.push(
147
+ `${file.rel}: a stock ${singular} has this name; add \`overrides: ${file.file.replace(/\.md$/, "")}\` or rename it`,
148
+ );
149
+ }
150
+ }
151
+
152
+ const push = (doc: Doc, key: string) => {
153
+ const index = at.get(key);
154
+ if (index === undefined) at.set(key, merged.push(doc) - 1);
155
+ else merged[index] = doc;
156
+ };
157
+ for (const file of stockFiles) {
158
+ const key = nameOf(file.file);
159
+ if (replaced.has(key)) {
160
+ // The stock file is never read; its slot is held for the replacement.
161
+ at.set(key, merged.push(null) - 1);
162
+ } else {
163
+ const doc = read(kind, file, problems);
164
+ if (doc !== null) push(doc, key);
165
+ }
166
+ }
167
+ for (const { file, doc } of parsed) {
168
+ if (doc === null) continue;
169
+ const overrides = doc.data["overrides"];
170
+ push(
171
+ doc,
172
+ typeof overrides === "string" && replaced.has(overrides.toLowerCase())
173
+ ? overrides.toLowerCase()
174
+ : nameOf(file.file),
175
+ );
176
+ }
177
+ return merged.filter((doc): doc is Doc => doc !== null);
178
+ }
179
+
180
+ /** Parse and check one file; a problem is recorded and `null` returned. */
181
+ function read(kind: Kind, file: DocFile, problems: string[]): Doc | null {
182
+ let parsed: ParsedDoc;
183
+ try {
184
+ parsed = parseDoc(file.rel, fs.readFileSync(file.abs, "utf8"));
185
+ } catch (error) {
186
+ problems.push((error as Error).message);
187
+ return null;
188
+ }
189
+ const bad = frontmatterProblem(kind, parsed);
190
+ if (bad !== null) {
191
+ problems.push(`${file.rel}: ${bad}`);
192
+ return null;
193
+ }
194
+ return { rel: file.rel, file: file.file, origin: file.origin, scope: file.scope, ...parsed };
195
+ }
196
+
197
+ const RULE_KEYS = ["description", "paths", "overrides"];
198
+ const SUBAGENT_KEYS = ["name", "description", "targets", "overrides", "tier", "exec", ...Object.values(TARGET)];
199
+ /** The most a rule or subagent body may hold; longer detail belongs in a skill or a reference. */
200
+ const MAX_BODY_BYTES = 8 * 1024;
201
+
202
+ /** What is wrong with `parsed`, or null: unknown keys, bad value types, an empty or oversized body. */
203
+ function frontmatterProblem(kind: Kind, { data, body }: ParsedDoc): string | null {
204
+ const keys = kind === "rules" ? RULE_KEYS : SUBAGENT_KEYS;
205
+ const unknown = Object.keys(data).find((key) => !keys.includes(key));
206
+ if (unknown !== undefined) return `unknown frontmatter key "${unknown}"; use ${keys.join(", ")}`;
207
+ if (data["overrides"] !== undefined && typeof data["overrides"] !== "string")
208
+ return "invalid frontmatter: overrides must be a string";
209
+ if (kind === "rules") {
210
+ if (typeof data["description"] !== "string" || data["description"].trim() === "")
211
+ return "invalid frontmatter: description is required";
212
+ const paths = data["paths"];
213
+ if (paths !== undefined && !(Array.isArray(paths) && paths.every((path) => typeof path === "string"))) {
214
+ return "invalid frontmatter: paths must be a list of strings";
215
+ }
216
+ }
217
+ if (body.trim() === "") return "body is empty; write the instructions below the frontmatter";
218
+ const bytes = Buffer.byteLength(body);
219
+ if (bytes > MAX_BODY_BYTES)
220
+ return `body is ${bytes} bytes, over the 8 KiB (${MAX_BODY_BYTES} byte) limit; move detail into a skill or a reference`;
221
+ return null;
222
+ }
223
+
224
+ /**
225
+ * The last root with a `hooks.jsonc`; stock always counts, so a missing stock file fails the read.
226
+ * `stripJsonc` drops comments only: trailing commas are unsupported. A failure is recorded as a problem and the hooks left empty.
227
+ */
228
+ function loadHooks(tops: { root: string; dir: string }[], problems: string[]): HooksSource {
229
+ const top = tops.findLast((t, i) => i === 0 || fs.existsSync(path.join(t.root, t.dir, "hooks.jsonc")))!;
230
+ const rel = `${top.dir}/hooks.jsonc`;
231
+ const text = fs.readFileSync(path.join(top.root, rel), "utf8");
232
+ let value: unknown;
233
+ try {
234
+ value = JSON.parse(stripJsonc(text));
235
+ } catch (error) {
236
+ problems.push(`${rel}: invalid JSONC: ${(error as Error).message}`);
237
+ return { rel, value: {} };
238
+ }
239
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
240
+ problems.push(`${rel}: invalid JSONC: expected an object`);
241
+ return { rel, value: {} };
242
+ }
243
+ return { rel, value: value as Record<string, unknown> };
244
+ }
@@ -0,0 +1,149 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vitest";
5
+ import { CONSUMER_DIR, STOCK_DIR, readGoldens } from "../../../dev/capture-tree-goldens.ts";
6
+ import { HOSTS } from "../../config.ts";
7
+ import { loadSources } from "./sources.ts";
8
+ import { renderSubagents } from "./subagents.ts";
9
+
10
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
11
+ const AGENT_DIRS = [".claude/agents/", ".codex/agents/", ".agents/agents/", ".grok/agents/", ".cursor/agents/"];
12
+ const isAgentFile = (rel: string) => AGENT_DIRS.some((dir) => rel.startsWith(dir));
13
+
14
+ /** Frontmatter block (fences included) and the body after it. */
15
+ function split(text: string): { head: string; body: string } {
16
+ const close = text.indexOf("\n---\n", 4);
17
+ return { head: text.slice(0, close + 5), body: text.slice(close + 5) };
18
+ }
19
+
20
+ describe("renderSubagents", () => {
21
+ it("matches the consumer goldens byte for byte, path set included", () => {
22
+ const sources = loadSources({ stockRoot: STOCK_DIR, consumerRoot: CONSUMER_DIR, packages: [] }).sources;
23
+ const out = renderSubagents(sources, HOSTS);
24
+ const goldens = new Map([...readGoldens()].filter(([rel]) => isAgentFile(rel)));
25
+ expect([...out.keys()].sort()).toEqual([...goldens.keys()].sort());
26
+ for (const [rel, text] of goldens) {
27
+ // emitAgentShellTools later swaps the claude scout body for the stock one; ours is the pre-emit text:
28
+ // the overlay frontmatter (equal to the golden's) plus the overlay body.
29
+ if (rel === ".claude/agents/scout.md") continue;
30
+ expect(out.get(rel), rel).toBe(text);
31
+ }
32
+ const scout = split(out.get(".claude/agents/scout.md")!);
33
+ expect(scout.head).toBe(split(goldens.get(".claude/agents/scout.md")!).head);
34
+ const overlay = split(fs.readFileSync(path.join(CONSUMER_DIR, ".repo/subagents/scout.md"), "utf8"));
35
+ expect(scout.body).toBe(`${overlay.body.trim()}\n`);
36
+ });
37
+
38
+ it("matches this repo's committed agent files on stock alone", () => {
39
+ const out = renderSubagents(loadSources({ stockRoot: ROOT, consumerRoot: null, packages: [] }).sources, HOSTS);
40
+ const committed = AGENT_DIRS.flatMap((dir) => fs.readdirSync(path.join(ROOT, dir)).map((name) => `${dir}${name}`));
41
+ expect([...out.keys()].sort()).toEqual(committed.sort());
42
+ for (const rel of committed) expect(out.get(rel), rel).toBe(fs.readFileSync(path.join(ROOT, rel), "utf8"));
43
+ });
44
+
45
+ it("emits only the enabled hosts' files", () => {
46
+ const sources = loadSources({ stockRoot: STOCK_DIR, consumerRoot: CONSUMER_DIR, packages: [] }).sources;
47
+ expect(renderSubagents(sources, []).size).toBe(0);
48
+ const grok = [...renderSubagents(sources, ["grok"]).keys()];
49
+ expect(grok.length).toBeGreaterThan(0);
50
+ expect(grok.every((rel) => rel.startsWith(".grok/agents/"))).toBe(true);
51
+ // orchestrator targets grok and antigravity only.
52
+ expect(renderSubagents(sources, ["claude", "codex", "cursor"]).keys()).not.toContain(
53
+ ".claude/agents/orchestrator.md",
54
+ );
55
+ expect([...renderSubagents(sources, ["antigravity"]).keys()]).toContain(".agents/agents/orchestrator.md");
56
+ });
57
+
58
+ it("covers the edge shapes: nested paths, default descriptions, codex quirks", () => {
59
+ const doc = (rel: string, data: Record<string, unknown>, body: string) => ({
60
+ rel: `.repo/subagents/${rel}`,
61
+ file: rel,
62
+ origin: "repo" as const,
63
+ scope: "",
64
+ data,
65
+ body,
66
+ });
67
+ const sources = {
68
+ rules: [],
69
+ hooks: { rel: "agents/hooks.jsonc", value: {} },
70
+ subagents: [
71
+ doc("x/deep.md", { name: "deep" }, "deep body"),
72
+ doc("nobody.md", { name: "nobody", codexcli: { nested: { a: 1 } } }, "one\ntwo"),
73
+ doc(
74
+ "single.md",
75
+ {
76
+ name: "single",
77
+ description: "d",
78
+ "antigravity-ide": { model: "a", x: "1" },
79
+ "antigravity-cli": { model: "b" },
80
+ },
81
+ "one line",
82
+ ),
83
+ ],
84
+ };
85
+ const out = renderSubagents(sources, HOSTS);
86
+ expect(out.get(".claude/agents/x/deep.md")).toBe("---\nname: deep\n---\ndeep body\n");
87
+ expect([...out.keys()].filter((rel) => rel.includes("deep")).length).toBe(1);
88
+ expect(out.get(".cursor/agents/nobody.md")).toBe("---\nname: nobody\n---\none\ntwo\n");
89
+ expect(out.get(".agents/agents/nobody.md")).toBe(
90
+ "---\nname: nobody\ndescription: nobody subagent\n---\none\ntwo\n",
91
+ );
92
+ expect(out.get(".codex/agents/nobody.toml")).toBe(
93
+ "name = \"nobody\"\n\n[nested]\na = 1\ndeveloper_instructions = '''\none\ntwo\n'''\n",
94
+ );
95
+ expect(out.get(".codex/agents/single.toml")).toBe(
96
+ 'name = "single"\ndescription = "d"\ndeveloper_instructions = "one line"\n',
97
+ );
98
+ expect(out.get(".agents/agents/single.md")).toBe(
99
+ "---\nname: single\ndescription: d\nmodel: b\nx: '1'\n---\none line\n",
100
+ );
101
+ });
102
+
103
+ it("writes shallowest paths first, honours a scalar target and omits an empty codex body", () => {
104
+ const doc = (file: string, data: Record<string, unknown>) => ({
105
+ rel: `.repo/subagents/${file}`,
106
+ file,
107
+ origin: "repo" as const,
108
+ scope: "",
109
+ data,
110
+ body: "",
111
+ });
112
+ const sources = {
113
+ rules: [],
114
+ hooks: { rel: "agents/hooks.jsonc", value: {} },
115
+ subagents: [
116
+ doc("z.md", { name: "z" }),
117
+ doc("a/b.md", { name: "b" }),
118
+ doc("m.md", { name: "m", targets: "codexcli" }),
119
+ doc("c.md", { name: "c" }),
120
+ ],
121
+ };
122
+ expect([...renderSubagents(sources, ["claude"]).keys()]).toEqual([
123
+ ".claude/agents/c.md",
124
+ ".claude/agents/z.md",
125
+ ".claude/agents/a/b.md",
126
+ ]);
127
+ expect(renderSubagents(sources, ["codex"]).get(".codex/agents/m.toml")).toBe('name = "m"\n');
128
+ });
129
+
130
+ it("fails on a subagent without a string name", () => {
131
+ const sources = {
132
+ rules: [],
133
+ hooks: { rel: "agents/hooks.jsonc", value: {} },
134
+ subagents: [
135
+ {
136
+ rel: ".repo/subagents/bad.md",
137
+ file: "bad.md",
138
+ origin: "repo" as const,
139
+ scope: "",
140
+ data: { name: 3 },
141
+ body: "",
142
+ },
143
+ ],
144
+ };
145
+ expect(() => renderSubagents(sources, ["claude"])).toThrow(
146
+ ".repo/subagents/bad.md: invalid frontmatter: name must be a string",
147
+ );
148
+ });
149
+ });
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Subagent files for each enabled host, from one set of subagent sources. A source names its hosts with
3
+ * `targets` (default all) and carries one block per host (`claudecode:`, `codexcli:`, `antigravity-cli:`,
4
+ * `grokcli:`, `cursor:`); top-level keys the hosts do not read (`tier`, `exec`) are ignored.
5
+ */
6
+ import { stringify } from "smol-toml";
7
+ import type { Host } from "../../config.ts";
8
+ import { type YamlMap, type YamlMode, type YamlValue, emitYaml, finalize } from "./frontmatter.ts";
9
+ import type { Doc, Sources } from "./sources.ts";
10
+
11
+ /** The `targets` name and frontmatter block of each host. */
12
+ export const TARGET: Record<Host, string> = {
13
+ claude: "claudecode",
14
+ codex: "codexcli",
15
+ antigravity: "antigravity-cli",
16
+ grok: "grokcli",
17
+ cursor: "cursor",
18
+ };
19
+
20
+ /** Keys each host's schema knows, in the order they are written; other keys follow in source order. */
21
+ const CLAUDE_KEYS = [
22
+ "name",
23
+ "description",
24
+ "model",
25
+ "tools",
26
+ "disallowedTools",
27
+ "permissionMode",
28
+ "maxTurns",
29
+ "skills",
30
+ "color",
31
+ "memory",
32
+ "effort",
33
+ "isolation",
34
+ "background",
35
+ "initialPrompt",
36
+ "mcpServers",
37
+ "hooks",
38
+ ];
39
+ const ANTIGRAVITY_KEYS = [
40
+ "name",
41
+ "description",
42
+ "tools",
43
+ "mainAgent",
44
+ "subagent",
45
+ "model",
46
+ "commandExecutionPolicy",
47
+ "mcpServers",
48
+ "skills",
49
+ "plugins",
50
+ "hidden",
51
+ "inheritMcp",
52
+ ];
53
+
54
+ /** Path to text for every subagent file the enabled `hosts` get; nested sources (`x/y.md`) reach claude only. */
55
+ export function renderSubagents(sources: Sources, hosts: readonly Host[]): Map<string, string> {
56
+ const out = new Map<string, string>();
57
+ for (const doc of [...sources.subagents].sort(shallowestFirst)) {
58
+ const name = doc.data["name"];
59
+ if (typeof name !== "string") throw new Error(`${doc.rel}: invalid frontmatter: name must be a string`);
60
+ const nested = doc.file.includes("/");
61
+ for (const host of hosts) {
62
+ if (!targets(doc, TARGET[host]) || (nested && host !== "claude")) continue;
63
+ const [rel, text] = render(host, doc, name);
64
+ out.set(rel, finalize(text));
65
+ }
66
+ }
67
+ return out;
68
+ }
69
+
70
+ function render(host: Host, doc: Doc, name: string): [string, string] {
71
+ const { data, body } = doc;
72
+ const description = data["description"];
73
+ const own = (key: string) => without(block(data[key]), "name", "description");
74
+ switch (host) {
75
+ case "claude":
76
+ return [
77
+ `.claude/agents/${doc.file}`,
78
+ markdown(ordered({ name, description, ...own("claudecode") }, CLAUDE_KEYS), body, "claude"),
79
+ ];
80
+ case "antigravity": {
81
+ const merged = { ...block(data["antigravity-ide"]), ...block(data["antigravity-cli"]) };
82
+ const fm = { name, description: description ?? `${name} subagent`, ...without(merged, "name", "description") };
83
+ return [`.agents/agents/${doc.file}`, markdown(flatten(ordered(fm, ANTIGRAVITY_KEYS)), body, "default")];
84
+ }
85
+ case "grok":
86
+ case "cursor": {
87
+ const dir = host === "grok" ? ".grok" : ".cursor";
88
+ return [
89
+ `${dir}/agents/${doc.file}`,
90
+ markdown(flatten({ name, description, ...block(data[TARGET[host]]) }), body, "default"),
91
+ ];
92
+ }
93
+ case "codex": {
94
+ const rest: Record<string, unknown> = { name, ...(description ? { description } : {}) };
95
+ Object.assign(
96
+ rest,
97
+ without(block(data["codexcli"]), "name", "description", "developer_instructions", "short-description"),
98
+ );
99
+ return [`.codex/agents/${doc.file.replace(/\.md$/, ".toml")}`, codexToml(rest, body)];
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * smol-toml for the frontmatter, then `developer_instructions` appended last. A multi-line body without `'''`
106
+ * becomes a `'''` block. Appending after any `[table]` puts the key inside that table, as rulesync did.
107
+ */
108
+ function codexToml(rest: Record<string, unknown>, body: string): string {
109
+ const head = `${stringify(rest).trimEnd()}\n`;
110
+ if (body === "") return head;
111
+ if (body.includes("\n") && !body.includes("'''")) return `${head}developer_instructions = '''\n${body}\n'''\n`;
112
+ return `${head}${stringify({ developer_instructions: body })}`;
113
+ }
114
+
115
+ /** Gray-matter layout: fences around the YAML, then the body. */
116
+ function markdown(fm: Record<string, unknown>, body: string, mode: YamlMode): string {
117
+ return `---\n${emitYaml(Object.entries(fm) as [string, YamlValue][], mode)}---\n${body}\n`;
118
+ }
119
+
120
+ function targets(doc: Doc, target: string): boolean {
121
+ const list = doc.data["targets"];
122
+ if (list === undefined) return true;
123
+ const names = Array.isArray(list) ? list : [list];
124
+ return names.includes("*") || names.includes(target);
125
+ }
126
+
127
+ function block(value: unknown): Record<string, unknown> {
128
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
129
+ }
130
+
131
+ function without(map: Record<string, unknown>, ...keys: string[]): Record<string, unknown> {
132
+ return Object.fromEntries(Object.entries(map).filter(([key]) => !keys.includes(key)));
133
+ }
134
+
135
+ /** `known` keys first in that order, then the rest in source order; nullish values dropped. */
136
+ function ordered(map: Record<string, unknown>, known: readonly string[]): Record<string, unknown> {
137
+ const out: Record<string, unknown> = {};
138
+ for (const key of known) if (map[key] !== undefined && map[key] !== null) out[key] = map[key];
139
+ for (const [key, value] of Object.entries(map))
140
+ if (!(key in out) && value !== undefined && value !== null) out[key] = value;
141
+ return out;
142
+ }
143
+
144
+ /** Every string on one line, as the non-claude hosts' dumper wrote them: newline runs become a space, trimmed. */
145
+ function flatten<T>(value: T): T {
146
+ if (typeof value === "string") return value.replace(/\n+/g, " ").trim() as T;
147
+ if (Array.isArray(value)) return value.map(flatten) as T;
148
+ if (typeof value === "object" && value !== null) {
149
+ return Object.fromEntries(Object.entries(value as YamlMap).map(([key, item]) => [key, flatten(item)])) as T;
150
+ }
151
+ return value;
152
+ }
153
+
154
+ /** Fewer path segments first, then plain string order; merged paths are unique, so there is no tie. */
155
+ function shallowestFirst(a: Doc, b: Doc): number {
156
+ const depth = a.file.split("/").length - b.file.split("/").length;
157
+ return depth !== 0 ? depth : a.file < b.file ? -1 : 1;
158
+ }
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { extraH2Sections, extractUserMarkdown } from "./agents-text.ts";
3
+
4
+ describe("extractUserMarkdown", () => {
5
+ it("keeps the text outside the block and drops stray end markers", () => {
6
+ expect(extractUserMarkdown("<!-- repo:begin -->\nbody\n<!-- repo:end -->\n<!-- repo:end -->\n")).toBe("");
7
+ expect(extractUserMarkdown("<!-- repo:begin -->\nbody\n<!-- repo:end -->\nkeep\n<!-- repo:end -->\n")).toBe(
8
+ "keep\n",
9
+ );
10
+ expect(extractUserMarkdown("<!-- repo:begin -->\n\nbody\n\n<!-- repo:end -->\n\n# Mine\n")).toBe("\n# Mine\n");
11
+ expect(extractUserMarkdown("keep <!-- repo:end --> me")).toBe("keep me");
12
+ expect(extractUserMarkdown("keep\n<!-- repo:end -->\nme")).toBe("keep\nme");
13
+ expect(extractUserMarkdown("keep\n <!-- repo:end --> \nme")).toBe("keep\nme");
14
+ });
15
+
16
+ it("cuts an unclosed block to the end", () => {
17
+ expect(
18
+ extractUserMarkdown("<!-- repo:begin -->\nok\n<!-- repo:end -->\nuser bit\n<!-- repo:begin -->\nstale"),
19
+ ).toBe("user bit\n");
20
+ });
21
+ });
22
+
23
+ describe("extraH2Sections", () => {
24
+ it("keeps titles incoming does not have, including CRLF", () => {
25
+ const existing = "# Law\r\n\r\n## Skills naming\r\n\r\nstock\r\n\r\n## Autorun\r\n\r\ngo-live\r\n";
26
+ const incoming = "# Law\n\n## Skills naming\n\nstock plus su\n";
27
+ expect(extraH2Sections(existing, incoming)).toBe("## Autorun\n\ngo-live");
28
+ expect(extraH2Sections("# Law\n\nno headings\n", incoming)).toBe("");
29
+ expect(extraH2Sections("## \nempty title\n## Autorun\nkeep\n", incoming)).toBe("## Autorun\nkeep");
30
+ });
31
+ });
@@ -0,0 +1,36 @@
1
+ /** Reads of a copied AGENTS.md that the migration needs: the text outside the old sentinel block and the sections a stock text lacks. */
2
+
3
+ const SENTINEL_BEGIN = "<!-- repo:begin -->";
4
+ const MANAGED_REGION_RE =
5
+ /(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- repo:begin -->[ \t]*\r?\n?[\s\S]*?(?:^|(?<=\n)|(?<=\r\n))[ \t]*<!-- repo:end -->[ \t]*\r?\n?/g;
6
+ const ORPHAN_END_RE = /(?:^|\r?\n)[ \t]*<!-- repo:end -->[ \t]*(?=\r?\n|$)|<!-- repo:end -->/g;
7
+
8
+ const cloneRe = (re: RegExp): RegExp => new RegExp(re.source, re.flags);
9
+
10
+ /** `text` without its `repo:begin`..`repo:end` blocks, an unclosed block to the end, and stray end markers; "" when only blank lines remain. */
11
+ export function extractUserMarkdown(text: string): string {
12
+ let user = text.replace(cloneRe(MANAGED_REGION_RE), "");
13
+ const i = user.indexOf(SENTINEL_BEGIN);
14
+ if (i >= 0) user = user.slice(0, i);
15
+ user = user.replace(cloneRe(ORPHAN_END_RE), "");
16
+ return user.replace(/\r\n/g, "\n").replace(/\n/g, "") === "" ? "" : user;
17
+ }
18
+
19
+ /** `##` blocks in `existing` whose titles are not in `incoming`. */
20
+ export function extraH2Sections(existing: string, incoming: string): string {
21
+ // The pattern has one group and it always takes part in a match.
22
+ const incomingTitles = new Set([...incoming.matchAll(/^## (.+)$/gm)].map((m) => m[1]!.trim()));
23
+ const text = existing.replace(/\r\n/g, "\n");
24
+ const matches = [...text.matchAll(/^## /gm)];
25
+ if (!matches.length) return "";
26
+ const chunks: string[] = [];
27
+ for (let i = 0; i < matches.length; i++) {
28
+ // matchAll results always carry an index, and i is within matches.
29
+ const start = matches[i]!.index;
30
+ const end = i + 1 < matches.length ? matches[i + 1]!.index : text.length;
31
+ const block = text.slice(start, end).replace(/(?:\n)+$/, "");
32
+ const title = block.match(/^## (.+)$/m)?.[1]?.trim();
33
+ if (title && !incomingTitles.has(title)) chunks.push(block);
34
+ }
35
+ return chunks.join("\n\n");
36
+ }
@@ -0,0 +1,35 @@
1
+ # Copied-install fixture (`copied.tgz`)
2
+
3
+ A consumer repo as `repo init` of `@workerstack/repo` at commit `e7d680e` (the base of plan 12) left it, plus content
4
+ the consumer added. It is stored packed, because a live `.claude/skills` or `AGENTS.md` under the repo is discovered by agent harnesses as the
5
+ repo's own. `cli/sync/migrate.test.ts` extracts it into a temp dir with the `tar` package; nothing here is used at runtime.
6
+
7
+ ## How it was made
8
+
9
+ 1. `git archive e7d680e | tar -x -C <scratch>/base`, then `bun install` and `bun pm pack --destination <scratch>/pack` there.
10
+ 2. In an empty `git init` dir with `package.json` (`name`, `private`, `type: module`) and `HOME`, `XDG_CONFIG_HOME`,
11
+ `XDG_DATA_HOME`, `XDG_CACHE_HOME` pointed at scratch dirs (no real global settings are touched):
12
+ `bun add -d @workerstack/repo@file:<scratch>/pack/workerstack-repo-0.0.0.tgz`, then
13
+ `bun node_modules/@workerstack/repo/bin/repo.ts init --yes --packs none`.
14
+ 3. Added by hand, the consumer's own content:
15
+ - rule `.rulesync/rules/team.md`
16
+ - subagent `.rulesync/subagents/reviewer.md`
17
+ - skill `.agents/skills/deploy/SKILL.md` and its link `.claude/skills/deploy`
18
+ - `.claude/settings.json` key `permissions.allow: ["Bash(make lint:*)"]`
19
+ - an `AGENTS.md` line after the guarded block
20
+ - lesson `.rulesync/lessons/x.md` and `.rulesync/lessons/INDEX.md`
21
+ - `.rulesync/reference/deploy.md`, and `.rulesync/reference/hooks.md` (a copy of the stock file plus `## Our hooks`);
22
+ init at that commit does not create `.rulesync/reference/`
23
+ - a `postToolUse` entry in `.rulesync/hooks.jsonc`
24
+ - `allow` and `servers` in `.rulesync/mcp-policy.jsonc`
25
+ - `.rulesync/repo.features.jsonc` set to `{ "packs": ["listen"] }`
26
+ 4. Packed the tree, links included, with `tar --sort=name --mtime=@0 --owner=0 --group=0 --numeric-owner -cf - . | gzip -n -9 > copied.tgz`.
27
+
28
+ ## What was left out
29
+
30
+ - `node_modules/` and `.git/`: the tests create both (a link to this repo as the stock package, and `git init`).
31
+ - `bun.lock`: it names the scratch tarball path.
32
+ - `package.json` names `@workerstack/repo` as `^0.0.0` instead of the scratch tarball path.
33
+ - Nothing else: all 156 files (README excluded, it is `copied.README.md`) and 51 links stay (745 KB unpacked, 211 KB packed), because the stock copies in `.rulesync/scripts`,
34
+ `.rulesync/global-settings`, `.agents/skills`, `.claude`, `.codex`, `.cursor`, `.grok` are what the migration removes
35
+ and untracks, and the tests assert they are gone.
Binary file