@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,478 @@
1
+ #!/usr/bin/env bun
2
+ /** Allocate/validate/write a new roadmap under .tmp/repo/roadmaps/; validate an update of a saved one. */
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { resolveRoots } from "./workspace-root.ts";
6
+ import { parseStrategy } from "./strategy.ts";
7
+ import {
8
+ type FrontmatterMap,
9
+ type PersistResult,
10
+ capErrors,
11
+ cardErrors,
12
+ compactCreatedStamp,
13
+ FILENAME_RE,
14
+ captured,
15
+ MISTAKES_H2,
16
+ mistakeLogErrors,
17
+ PLAN_DIR,
18
+ PLAN_MISTAKE_SOURCES,
19
+ reviewErrors,
20
+ sectionBodies,
21
+ slugify,
22
+ STRATEGY_ERROR,
23
+ extractMode,
24
+ extractStrategy,
25
+ modeErrors,
26
+ stripModeLine,
27
+ stripStrategyLine,
28
+ unclosedFenceErrors,
29
+ USAGE_H2,
30
+ usageErrors,
31
+ utcDateTime,
32
+ } from "./persist-plan.ts";
33
+
34
+ export const ROADMAP_DIR = ".tmp/repo/roadmaps";
35
+ const ROADMAP_FILENAME_RE: RegExp = /^[0-9]+-[a-z0-9]+(-[a-z0-9]+)*-\d{8}T\d{6}Z\.md$/;
36
+ const STATUS_RE = /^(draft|locked|in-progress|done|blocked)$/;
37
+ const DATETIME_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
38
+ const REQUIRED_H2 = ["Goal", "Context", "Decisions", "Order", "Watch-outs", "Verify"];
39
+ /** A roadmap also logs `slice`: what a child plan found that changed the program. */
40
+ const ROADMAP_MISTAKE_SOURCES: readonly string[] = [...PLAN_MISTAKE_SOURCES, "slice"];
41
+ /** Statuses a roadmap may enter only after that many settled `/review-roadmap` rounds. */
42
+ const REVIEWS_TO_ENTER: Record<string, number> = { locked: 1 };
43
+ const SLICE_RE = /^(?: {0,2})(\d+) \[([ xX])\] Title: (.+)$/;
44
+ const KEY_RE = /^ {2,8}(Goal|Why|Location): (.*)$/;
45
+ /** A roadmap with no `## Goal` is a pointer roadmap: the card, one line per slice, Verify. */
46
+ const POINTER_H2 = ["Order", "Verify"];
47
+ const POINTER_LINE_CAP = 60;
48
+ const POINTER_CAP_FIX = "move detail into the slice plans";
49
+ const POINTER_SLICE_RE = /^(?: {0,2})(\d+) \[[ xX]\] (.*)$/;
50
+ const POINTER_TEXT_RE = /^(.+?): (.+)$/;
51
+ const LOCATION_LINE_RE = /^\s+Location: (.*)$/;
52
+ const DRIVE_RE = /^[A-Za-z]:[\\/]/;
53
+ /** A saved roadmap's frontmatter and the text after it. */
54
+ interface Split {
55
+ body: string;
56
+ map: FrontmatterMap;
57
+ }
58
+
59
+ /** One `Title:` slice of the Order section. */
60
+ interface Slice {
61
+ title: string;
62
+ goal: string;
63
+ why: string;
64
+ location: string;
65
+ hasGoal: boolean;
66
+ hasWhy: boolean;
67
+ hasLocation: boolean;
68
+ }
69
+
70
+ /** One line of a pointer roadmap's Order: `N [ ] {title}: {one sentence}`, then an optional Location. */
71
+ interface PointerSlice {
72
+ n: string;
73
+ ok: boolean;
74
+ location: string | null;
75
+ }
76
+
77
+ interface Preamble {
78
+ body: string;
79
+ status: string;
80
+ reviews: string | undefined;
81
+ strategy: string | undefined;
82
+ mode: string | undefined;
83
+ fullFrontmatter: boolean;
84
+ name: string;
85
+ }
86
+
87
+ export type RoadmapResult = PersistResult<{ path: string; serial: number; filename: string; content: string }>;
88
+
89
+ const BOX_RE = /^(?: {0,2})(\d+(?:\.\d+){0,2}) \[([ xX])\] (.+)$/;
90
+
91
+ /**
92
+ * A slice's Location: any absolute path, or a plan file under `.tmp/repo/plans/`. A drive-letter
93
+ * path is absolute under every platform's rules, so a roadmap written on Windows checks the same on posix.
94
+ */
95
+ export function locationOk(loc: string, p: path.PlatformPath = path): boolean {
96
+ const norm = String(loc || "").replace(/\\/g, "/");
97
+ if (p.isAbsolute(loc) || DRIVE_RE.test(loc)) return true;
98
+ const prefix = `${PLAN_DIR}/`;
99
+ if (!norm.startsWith(prefix)) return false;
100
+ const base = norm.slice(prefix.length);
101
+ return !base.includes("/") && FILENAME_RE.test(base);
102
+ }
103
+
104
+ function sentences(text: unknown) {
105
+ return String(text)
106
+ .trim()
107
+ .split(/(?<=[.!?])(?:\s+|$)/)
108
+ .filter((s) => s.trim());
109
+ }
110
+
111
+ function stripFrontmatter(text: unknown): Split {
112
+ const s = String(text).replace(/^\uFEFF/, "");
113
+ if (!s.startsWith("---\n")) return { body: s, map: {} };
114
+ const end = s.indexOf("\n---\n", 4);
115
+ if (end === -1) return { body: s, map: {} };
116
+ const map: FrontmatterMap = {};
117
+ for (const line of s.slice(4, end).split("\n")) {
118
+ const m = line.match(/^([a-z]+):\s*(.*)$/);
119
+ if (m?.[1] === undefined) continue;
120
+ let v = captured(m, 2);
121
+ if (v.startsWith('"') && v.endsWith('"')) {
122
+ try {
123
+ v = String(JSON.parse(v));
124
+ } catch {
125
+ v = v.slice(1, -1);
126
+ }
127
+ }
128
+ map[m[1]] = v;
129
+ }
130
+ return { body: s.slice(end + 5), map };
131
+ }
132
+
133
+ function preamble(raw: string): Preamble {
134
+ const stripped = stripFrontmatter(raw);
135
+ let text = stripped.body.replace(/^\s+/, "");
136
+ let status = stripped.map.status || "";
137
+ let reviews: string | undefined = stripped.map.reviews;
138
+ const nameMatch = text.match(/^ROADMAP_NAME:\s*(.+)\s*$/m);
139
+ text = text.replace(/^ROADMAP_NAME:\s*.+\n/m, "");
140
+ const statusLine = text.match(/^status:\s*(\S+)\s*$/m);
141
+ if (statusLine) {
142
+ if (!status) status = captured(statusLine, 1);
143
+ text = text.replace(/^status:\s*\S+\s*\n/m, "");
144
+ }
145
+ const reviewsLine = text.match(/^reviews:\s*(\S+)\s*$/m);
146
+ if (reviewsLine) {
147
+ if (reviews == null) reviews = captured(reviewsLine, 1);
148
+ text = text.replace(/^reviews:\s*\S+\s*\n/m, "");
149
+ }
150
+ const strategy = stripped.map.strategy ?? extractStrategy(text);
151
+ text = stripStrategyLine(text);
152
+ const mode = stripped.map.mode ?? extractMode(text);
153
+ text = stripModeLine(text);
154
+ const h1 = text.match(/^#\s+(.+)\s*$/m);
155
+ return {
156
+ body: text,
157
+ status,
158
+ reviews,
159
+ strategy,
160
+ mode,
161
+ // A sent frontmatter is the whole frontmatter: a missing key there is dropped, not kept.
162
+ fullFrontmatter: stripped.body !== String(raw).replace(/^\uFEFF/, ""),
163
+ name: nameMatch ? captured(nameMatch, 1).trim() : h1 ? captured(h1, 1).trim() : "",
164
+ };
165
+ }
166
+
167
+ function parseSlices(orderBody: unknown): Slice[] {
168
+ const slices: Slice[] = [];
169
+ let cur: Slice | null = null;
170
+ for (const line of String(orderBody).split("\n")) {
171
+ const head = line.match(SLICE_RE);
172
+ if (head) {
173
+ if (cur) slices.push(cur);
174
+ cur = {
175
+ title: captured(head, 3).trim(),
176
+ goal: "",
177
+ why: "",
178
+ location: "",
179
+ hasGoal: false,
180
+ hasWhy: false,
181
+ hasLocation: false,
182
+ };
183
+ continue;
184
+ }
185
+ if (!cur) continue;
186
+ const key = line.match(KEY_RE);
187
+ if (!key) continue;
188
+ if (key[1] === "Goal") {
189
+ cur.goal = captured(key, 2).trim();
190
+ cur.hasGoal = true;
191
+ } else if (key[1] === "Why") {
192
+ cur.why = captured(key, 2).trim();
193
+ cur.hasWhy = true;
194
+ } else {
195
+ cur.location = captured(key, 2).trim();
196
+ cur.hasLocation = true;
197
+ }
198
+ }
199
+ if (cur) slices.push(cur);
200
+ return slices;
201
+ }
202
+
203
+ function parsePointerSlices(orderBody: string): PointerSlice[] {
204
+ const slices: PointerSlice[] = [];
205
+ for (const line of orderBody.split("\n")) {
206
+ const head = line.match(POINTER_SLICE_RE);
207
+ if (head) {
208
+ const text = captured(head, 2).trim().match(POINTER_TEXT_RE);
209
+ const words = text ? captured(text, 1).split(/\s+/).filter(Boolean).length : 0;
210
+ const ok = !!text && words >= 2 && words <= 6 && sentences(text[2]).length === 1;
211
+ slices.push({ n: captured(head, 1), ok, location: null });
212
+ continue;
213
+ }
214
+ const cur = slices[slices.length - 1];
215
+ if (!cur || !line.trim()) continue;
216
+ const loc = line.match(LOCATION_LINE_RE);
217
+ // A slice is one line; only a Location line may follow it.
218
+ if (loc && cur.location === null) cur.location = captured(loc, 1).trim();
219
+ else cur.ok = false;
220
+ }
221
+ return slices;
222
+ }
223
+
224
+ /** A pointer roadmap: no Usage, only Order and Verify, one-line slices, the 60-line cap. */
225
+ function pointerErrors(
226
+ text: string,
227
+ body: string,
228
+ allOrder: string[],
229
+ bodies: Record<string, string>,
230
+ { fresh, previous }: { fresh: boolean; previous?: string | null | undefined },
231
+ ) {
232
+ const errors: string[] = [];
233
+ if (fresh) errors.push(...cardErrors(body));
234
+ const order = allOrder.filter((h) => h !== MISTAKES_H2);
235
+ for (const h of order) {
236
+ if (!POINTER_H2.includes(h)) {
237
+ errors.push(
238
+ `a new roadmap has only ## Order, ## Verify and ## ${MISTAKES_H2}; move ## ${h} into the slice plans`,
239
+ );
240
+ }
241
+ }
242
+ const kept = order.filter((h) => POINTER_H2.includes(h));
243
+ if (kept.length !== POINTER_H2.length || POINTER_H2.some((h, i) => kept[i] !== h)) errors.push("heading order");
244
+ const slices = parsePointerSlices(bodies.Order || "");
245
+ if (slices.length < 2) errors.push("slices");
246
+ for (const slice of slices) {
247
+ if (!slice.ok) errors.push(`slice ${slice.n} needs "N [ ] {title, 2–6 words}: {one sentence}"`);
248
+ if (slice.location !== null && !locationOk(slice.location)) errors.push("location");
249
+ }
250
+ errors.push(
251
+ ...capErrors(text, { fresh, previous, cap: POINTER_LINE_CAP, fix: POINTER_CAP_FIX, skip: LOCATION_LINE_RE }),
252
+ );
253
+ return errors;
254
+ }
255
+
256
+ /** The old shape roadmaps saved before pointers keep: Goal, Usage, Context, Decisions, Title/Goal/Why slices, Watch-outs. */
257
+ function legacyErrors(allOrder: string[], bodies: Record<string, string>, { fresh }: { fresh: boolean }) {
258
+ const errors: string[] = [];
259
+ errors.push(...usageErrors(allOrder, bodies, { fresh }));
260
+ const order = allOrder.filter((h) => h !== MISTAKES_H2 && h !== USAGE_H2);
261
+ if (REQUIRED_H2.some((h, i) => order[i] !== h)) errors.push("heading order");
262
+ if (order.some((h) => !REQUIRED_H2.includes(h))) errors.push("extra h2");
263
+ const goalN = sentences(bodies.Goal || "");
264
+ if (goalN.length < 1 || goalN.length > 3) errors.push("goal");
265
+ for (const h of ["Context", "Decisions", "Watch-outs"]) {
266
+ const b = (bodies[h] || "").trim();
267
+ if (!b || !/^(?:[-*] |\(none\))/m.test(b)) errors.push(`${h.toLowerCase()} bullets`);
268
+ }
269
+ const slices = parseSlices(bodies.Order || "");
270
+ if (slices.length < 2) errors.push("slices");
271
+ for (const slice of slices) {
272
+ const words = slice.title.split(/\s+/).filter(Boolean);
273
+ if (words.length < 2 || words.length > 6) errors.push("title");
274
+ if (!slice.hasGoal || sentences(slice.goal).length !== 1) errors.push("slice goal");
275
+ if (!slice.hasWhy || sentences(slice.why).length < 1 || sentences(slice.why).length > 2) {
276
+ errors.push("why");
277
+ }
278
+ if (slice.hasLocation && !locationOk(slice.location)) errors.push("location");
279
+ }
280
+ return errors;
281
+ }
282
+
283
+ export function validateRoadmap(
284
+ filename: string,
285
+ text: string,
286
+ { fresh = false, previous }: { fresh?: boolean; previous?: string | null | undefined } = {},
287
+ ): string[] {
288
+ const errors: string[] = [];
289
+ if (!ROADMAP_FILENAME_RE.test(filename)) errors.push("filename");
290
+ const { body, map } = stripFrontmatter(text);
291
+ if (!map.serial || !map.name || !map.status || !map.created || !map.source) {
292
+ errors.push("frontmatter");
293
+ }
294
+ if (map.status && !STATUS_RE.test(map.status)) errors.push("status");
295
+ if (!parseStrategy(map.strategy)) errors.push(STRATEGY_ERROR);
296
+ errors.push(...modeErrors(map));
297
+ const prev = previous == null ? null : stripFrontmatter(previous);
298
+ // A new roadmap is always a pointer roadmap. An update keeps the saved file's shape, so deleting
299
+ // ## Goal cannot move an old roadmap under other rules; with no saved file the text decides.
300
+ const pointer = fresh || !sectionBodies((prev ?? { body }).body).order.includes("Goal");
301
+ errors.push(...reviewErrors(map, prev ? prev.map : null, { gates: REVIEWS_TO_ENTER, review: "/review-roadmap {n}" }));
302
+ const slug = String(filename)
303
+ .replace(/\.md$/, "")
304
+ .replace(/^[0-9]+-/, "")
305
+ .replace(/-\d{8}T\d{6}Z$/, "");
306
+ if (map.name && map.name !== slug) errors.push("name");
307
+ if (!body.trim().startsWith(`# ${slug}`)) errors.push("h1");
308
+ const fence = unclosedFenceErrors(text);
309
+ if (fence.length) return [...new Set([...errors, ...fence])];
310
+ const { order: allOrder, bodies } = sectionBodies(body);
311
+ errors.push(
312
+ ...mistakeLogErrors(allOrder, bodies, {
313
+ fresh,
314
+ previous: prev && sectionBodies(prev.body),
315
+ sources: ROADMAP_MISTAKE_SOURCES,
316
+ kind: "roadmap",
317
+ }),
318
+ );
319
+ errors.push(
320
+ ...(pointer
321
+ ? pointerErrors(text, body, allOrder, bodies, { fresh, previous })
322
+ : legacyErrors(allOrder, bodies, { fresh })),
323
+ );
324
+ if (fresh && map.status && map.status !== "draft") errors.push("fresh status");
325
+ if (fresh) {
326
+ for (const line of String(bodies.Order || "").split("\n")) {
327
+ const m = line.match(BOX_RE);
328
+ if (m && m[2] !== " ") errors.push("fresh [x]");
329
+ }
330
+ }
331
+ const boxes: string[] = [];
332
+ for (const line of String(bodies.Verify || "").split("\n")) {
333
+ const m = line.match(BOX_RE);
334
+ if (!m) continue;
335
+ if (fresh && m[2] !== " ") errors.push("fresh [x]");
336
+ boxes.push(captured(m, 1));
337
+ }
338
+ const leaves = boxes.filter((id) => /^\d+\.\d+/.test(id));
339
+ if (leaves.length < 1 || leaves.length > 5) errors.push("verify");
340
+ return [...new Set(errors)];
341
+ }
342
+
343
+ function render(
344
+ serial: number,
345
+ slug: string,
346
+ status: string,
347
+ reviews: string | number | undefined,
348
+ created: string,
349
+ source: unknown,
350
+ strategy: string | undefined,
351
+ mode: string | undefined,
352
+ body: string,
353
+ ) {
354
+ let rest = body.replace(/^\s+/, "");
355
+ if (!rest.startsWith("# ")) rest = `# ${slug}\n\n${rest}`;
356
+ else rest = rest.replace(/^# .+/, `# ${slug}`);
357
+ return `---
358
+ serial: ${serial}
359
+ name: ${slug}
360
+ status: ${status}
361
+ ${reviews == null ? "" : `reviews: ${reviews}\n`}created: ${created}
362
+ source: ${JSON.stringify(String(source).slice(0, 120))}
363
+ ${strategy ? `strategy: ${strategy}\n` : ""}${mode === undefined ? "" : `mode: ${mode}\n`}---
364
+
365
+ ${rest}`;
366
+ }
367
+
368
+ function sourceOf(pre: Preamble, slug: string) {
369
+ const goal = pre.body.match(/## Goal\n([\s\S]*?)\n## /);
370
+ return String(pre.name || (goal ? captured(goal, 1).trim().replace(/\s+/g, " ") : slug)).slice(0, 120);
371
+ }
372
+
373
+ /**
374
+ * Rewrite of a saved roadmap: same file and serial, saved frontmatter kept unless sent.
375
+ * Validates only. The host writes `content`: a write here first makes Claude Code refuse the
376
+ * Write as modified since read.
377
+ */
378
+ export function prepareRoadmapUpdate({
379
+ workspaceRoot,
380
+ filePath,
381
+ content,
382
+ }: {
383
+ workspaceRoot?: string;
384
+ filePath: string;
385
+ content?: unknown;
386
+ }): RoadmapResult {
387
+ const ws = workspaceRoot || resolveRoots().root;
388
+ const abs = path.resolve(ws, filePath);
389
+ const filename = path.basename(abs);
390
+ const previous = fs.readFileSync(abs, "utf8");
391
+ const fence = unclosedFenceErrors(String(content || ""));
392
+ if (fence.length) return { ok: false, error: fence.join("; "), errors: fence };
393
+ const pre = preamble(String(content || ""));
394
+ const prev = stripFrontmatter(previous).map;
395
+ const serial = parseInt(String(filename.split("-")[0]), 10);
396
+ const slug = slugFromFile(filename);
397
+ const createdAt = prev.created && DATETIME_RE.test(prev.created) ? prev.created : utcDateTime();
398
+ const reviews = pre.reviews ?? (pre.fullFrontmatter ? undefined : prev.reviews);
399
+ const strategy = pre.strategy ?? (pre.fullFrontmatter ? undefined : prev.strategy);
400
+ const mode = pre.mode ?? (pre.fullFrontmatter ? undefined : prev.mode);
401
+ const full = render(
402
+ serial,
403
+ slug,
404
+ pre.status || prev.status || "draft",
405
+ reviews,
406
+ createdAt,
407
+ prev.source || sourceOf(pre, slug),
408
+ strategy,
409
+ mode,
410
+ pre.body,
411
+ );
412
+ const errors = validateRoadmap(filename, full, { fresh: false, previous });
413
+ if (errors.length) return { ok: false, error: errors.join("; "), errors };
414
+ return {
415
+ ok: true,
416
+ path: path.relative(ws, abs).split(path.sep).join("/"),
417
+ serial,
418
+ filename,
419
+ content: full,
420
+ };
421
+ }
422
+
423
+ export function persistRoadmap({
424
+ workspaceRoot,
425
+ content,
426
+ created,
427
+ }: { workspaceRoot?: string; content?: unknown; created?: string } = {}): RoadmapResult {
428
+ const ws = workspaceRoot || resolveRoots().root;
429
+ const dir = path.join(ws, ROADMAP_DIR);
430
+ fs.mkdirSync(dir, { recursive: true });
431
+ const fence = unclosedFenceErrors(String(content || ""));
432
+ if (fence.length) return { ok: false, error: fence.join("; "), errors: fence };
433
+ const pre = preamble(String(content || ""));
434
+ const slug = slugify(pre.name);
435
+ const createdAt = created && DATETIME_RE.test(created) ? created : utcDateTime();
436
+ const stamp = compactCreatedStamp(createdAt);
437
+ const serial = nextSerial(dir);
438
+ const filename = `${serial}-${slug}-${stamp}.md`;
439
+ const dest = path.join(dir, filename);
440
+ const full = render(
441
+ serial,
442
+ slug,
443
+ pre.status || "draft",
444
+ 0,
445
+ createdAt,
446
+ sourceOf(pre, slug),
447
+ pre.strategy,
448
+ pre.mode,
449
+ pre.body,
450
+ );
451
+ const errors = validateRoadmap(filename, full, { fresh: true });
452
+ if (errors.length) return { ok: false, error: errors.join("; "), errors };
453
+ fs.writeFileSync(dest, full);
454
+ return {
455
+ ok: true,
456
+ path: `${ROADMAP_DIR}/${filename}`,
457
+ serial,
458
+ filename,
459
+ content: full,
460
+ };
461
+ }
462
+
463
+ function slugFromFile(filename: unknown) {
464
+ return String(filename)
465
+ .replace(/\.md$/, "")
466
+ .replace(/^[0-9]+-/, "")
467
+ .replace(/-\d{8}T\d{6}Z$/, "");
468
+ }
469
+
470
+ function nextSerial(dir: string) {
471
+ let next = 1;
472
+ for (const b of fs.readdirSync(dir)) {
473
+ const m = b.match(/^([0-9]+)-/);
474
+ if (!m) continue;
475
+ next = Math.max(next, parseInt(captured(m, 1), 10) + 1);
476
+ }
477
+ return next;
478
+ }
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env bun
2
+ /** Allocate/slugify/write a one-off convo artifact under .tmp/repo/. */
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { resolveRoots } from "./workspace-root.ts";
6
+ import { DATETIME_RE, type PersistResult, compactCreatedStamp, slugify, utcDateTime } from "./persist-plan.ts";
7
+
8
+ export const TMP_DIR = ".tmp/repo";
9
+ /** Top-level names under `.tmp/repo/` that repo owns; anything else at that level is a `/tmp` artifact or scratch. */
10
+ export const TMP_RESERVED_DIRS: Set<string> = new Set([
11
+ "plans",
12
+ "roadmaps",
13
+ "handoffs",
14
+ "implementations",
15
+ "browser",
16
+ "orchestrator-logs",
17
+ "stash",
18
+ "sessions",
19
+ "listen",
20
+ "speak",
21
+ ]);
22
+ export function isUnderDir(
23
+ filePath: string | null | undefined,
24
+ workspaceRoot: string | null | undefined,
25
+ segments: string[],
26
+ ): boolean {
27
+ if (!filePath || !workspaceRoot) return false;
28
+ const abs = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(workspaceRoot, filePath);
29
+ const dir = path.resolve(workspaceRoot, ...segments);
30
+ const rel = path.relative(dir, abs);
31
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
32
+ }
33
+
34
+ export function isUnderPlans(filePath: string | null | undefined, workspaceRoot: string | null | undefined): boolean {
35
+ return isUnderDir(filePath, workspaceRoot, [".tmp", "repo", "plans"]);
36
+ }
37
+
38
+ export function isUnderRoadmaps(
39
+ filePath: string | null | undefined,
40
+ workspaceRoot: string | null | undefined,
41
+ ): boolean {
42
+ return isUnderDir(filePath, workspaceRoot, [".tmp", "repo", "roadmaps"]);
43
+ }
44
+
45
+ export function isUnderHandoffs(
46
+ filePath: string | null | undefined,
47
+ workspaceRoot: string | null | undefined,
48
+ ): boolean {
49
+ return isUnderDir(filePath, workspaceRoot, [".tmp", "repo", "handoffs"]);
50
+ }
51
+
52
+ export function isUnderImplementations(
53
+ filePath: string | null | undefined,
54
+ workspaceRoot: string | null | undefined,
55
+ ): boolean {
56
+ return isUnderDir(filePath, workspaceRoot, [".tmp", "repo", "implementations"]);
57
+ }
58
+
59
+ export function isUnderTmp(filePath: string | null | undefined, workspaceRoot: string | null | undefined): boolean {
60
+ if (!filePath || !workspaceRoot) return false;
61
+ const abs = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(workspaceRoot, filePath);
62
+ const root = path.resolve(workspaceRoot, ".tmp", "repo");
63
+ if (path.dirname(abs) !== root) return false;
64
+ return !TMP_RESERVED_DIRS.has(path.basename(abs));
65
+ }
66
+
67
+ /** Which persist owns a Write to `absPath`, given the scratch root; null for none. */
68
+ export function persistOwner(
69
+ absPath: string,
70
+ root: string,
71
+ ): "plans" | "roadmaps" | "handoffs" | "implementations" | "tmp" | null {
72
+ if (isUnderPlans(absPath, root)) return "plans";
73
+ if (isUnderRoadmaps(absPath, root)) return "roadmaps";
74
+ if (isUnderHandoffs(absPath, root)) return "handoffs";
75
+ if (isUnderImplementations(absPath, root)) return "implementations";
76
+ if (isUnderTmp(absPath, root)) return "tmp";
77
+ return null;
78
+ }
79
+
80
+ /** `{serial}-{slug}-{YYYYMMDDTHHMMSSZ}.{ext}`; stamp required. */
81
+ export const FILENAME_RE: RegExp = /^[1-9][0-9]*-[a-z0-9]+(-[a-z0-9]+)*-\d{8}T\d{6}Z\.[a-z0-9]+$/;
82
+ const META_LINE_RE = /^\s*TMP_(NAME|EXT):\s*.+\s*$/;
83
+ const NAME_RE = /^\s*TMP_NAME:\s*(.+?)\s*$/m;
84
+ const EXT_RE = /^\s*TMP_EXT:\s*(.+?)\s*$/m;
85
+
86
+ export function extractTmpName(content: unknown): string {
87
+ const m = String(content).match(NAME_RE);
88
+ return m ? String(m[1]).trim() : "";
89
+ }
90
+
91
+ export function extractTmpExt(content: unknown): string {
92
+ const m = String(content).match(EXT_RE);
93
+ return m ? String(m[1]).trim() : "";
94
+ }
95
+
96
+ export function stripTmpMeta(content: unknown): string {
97
+ return String(content)
98
+ .replace(/^\uFEFF/, "")
99
+ .split(/\r?\n/)
100
+ .filter((line) => !META_LINE_RE.test(line))
101
+ .join("\n");
102
+ }
103
+
104
+ export function normalizeTmpExt(raw: unknown): string {
105
+ let e = String(raw || "")
106
+ .trim()
107
+ .toLowerCase();
108
+ if (e.startsWith(".")) e = e.slice(1);
109
+ if (!/^[a-z0-9]+$/.test(e) || e.length > 16) return "";
110
+ return e;
111
+ }
112
+
113
+ function nextTmpSerial(dir: string) {
114
+ let next = 1;
115
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
116
+ if (!ent.isFile()) continue;
117
+ const m = ent.name.match(/^([0-9]+)-/);
118
+ if (!m) continue;
119
+ const n = parseInt(String(m[1]), 10);
120
+ next = Math.max(next, n + 1);
121
+ }
122
+ return next;
123
+ }
124
+
125
+ export interface PersistTmpOptions {
126
+ workspaceRoot?: string;
127
+ content?: unknown;
128
+ created?: string;
129
+ }
130
+
131
+ export type PersistTmpResult = PersistResult<{
132
+ path: string;
133
+ serial: number;
134
+ slug: string;
135
+ ext: string;
136
+ content: string;
137
+ filename: string;
138
+ }>;
139
+
140
+ export function persistTmp({ workspaceRoot, content, created }: PersistTmpOptions = {}): PersistTmpResult {
141
+ const ws = workspaceRoot || resolveRoots().root;
142
+ const raw = String(content || "");
143
+ if (raw.includes("\0")) return { ok: false, error: "binary" };
144
+ const name = extractTmpName(raw);
145
+ if (!name) return { ok: false, error: "missing TMP_NAME" };
146
+ const extRaw = extractTmpExt(raw);
147
+ const ext = extRaw ? normalizeTmpExt(extRaw) : "md";
148
+ if (!ext) return { ok: false, error: "ext" };
149
+ const slug = slugify(name);
150
+ const body = stripTmpMeta(raw);
151
+ const dir = path.join(ws, TMP_DIR);
152
+ fs.mkdirSync(dir, { recursive: true });
153
+ const createdAt = created && DATETIME_RE.test(created) ? created : utcDateTime();
154
+ const stamp = compactCreatedStamp(createdAt);
155
+ let serial = nextTmpSerial(dir);
156
+ let filename = `${serial}-${slug}-${stamp}.${ext}`;
157
+ let dest = path.join(dir, filename);
158
+ let tries = 0;
159
+ while (fs.existsSync(dest) && tries < 5) {
160
+ serial += 1;
161
+ filename = `${serial}-${slug}-${stamp}.${ext}`;
162
+ dest = path.join(dir, filename);
163
+ tries += 1;
164
+ }
165
+ if (fs.existsSync(dest)) return { ok: false, error: "serial collision" };
166
+ if (!FILENAME_RE.test(filename)) return { ok: false, error: "filename" };
167
+ fs.writeFileSync(dest, body);
168
+ return {
169
+ ok: true,
170
+ path: `${TMP_DIR}/${filename}`,
171
+ serial,
172
+ slug,
173
+ ext,
174
+ content: body,
175
+ filename,
176
+ };
177
+ }