@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,888 @@
1
+ #!/usr/bin/env bun
2
+ /** Allocate/slugify/validate/write a local plan under .tmp/repo/plans/. */
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { canonicalPath, resolveRoots } from "./workspace-root.ts";
6
+ import { matchLessons, type Roots } from "./lessons.ts";
7
+ import { MODES, parseMode, parseStrategy, STRATEGIES } from "./strategy.ts";
8
+
9
+ export const PLAN_DIR = ".tmp/repo/plans";
10
+ /** Old unstamped names and new `{serial}-{slug}-{YYYYMMDDTHHMMSSZ}.md`. */
11
+ export const FILENAME_RE: RegExp = /^[0-9]+-[a-z0-9]+(-[a-z0-9]+)*(-\d{8}T\d{6}Z)?\.md$/;
12
+ export const SERIAL_LINE_RE: RegExp = /^serial: [1-9][0-9]*$/;
13
+ const STATUS_RE: RegExp = /^(ready|in-progress|done|blocked)$/;
14
+ export const DATETIME_RE: RegExp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
15
+ export const CREATED_RE: RegExp = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}Z)?$/;
16
+ const FILENAME_STAMP_RE: RegExp = /-\d{8}T\d{6}Z$/;
17
+ /** Conventional Commits subject. Scope optional. Lowercase, no trailing period. */
18
+ const COMMIT_SUBJECT_RE: RegExp =
19
+ /^(?:feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\([a-z0-9]+(?:-[a-z0-9]+)*\))?: [a-z0-9](?:[a-z0-9 .'/:_,-]*[a-z0-9])?$/;
20
+ const REQUIRED_H2: string[] = ["Goal", "Context", "Constraints", "Watch-outs", "Steps", "Verify"];
21
+ /** Last heading: this plan's own log of mistakes. Required on new plans, append-only on updates. */
22
+ export const MISTAKES_H2 = "Previous mistakes";
23
+ /** Entry kinds a plan logs. A roadmap adds its own. */
24
+ export const PLAN_MISTAKE_SOURCES: readonly string[] = ["feedback", "verify", "review", "premise", "decision"];
25
+ /** Frontmatter `key: value` lines, values as strings. */
26
+ export type FrontmatterMap = Record<string, string>;
27
+
28
+ /** `## ` heading order and section bodies of a document. */
29
+ export interface Sections {
30
+ order: string[];
31
+ bodies: Record<string, string>;
32
+ }
33
+
34
+ export interface MistakeEntry {
35
+ source: string;
36
+ text: string;
37
+ lesson: string | undefined;
38
+ line: string;
39
+ }
40
+
41
+ export type FrontmatterResult =
42
+ | { error: string; map?: undefined; body?: undefined }
43
+ | { error?: undefined; map: FrontmatterMap; body: string };
44
+
45
+ /**
46
+ * A persist outcome: `ok` with the written file's fields, or not ok with `error` (and `errors` when several).
47
+ * Each side lists the other's fields as `undefined`, so a reader can touch any field before narrowing on `ok`.
48
+ */
49
+ export type PersistResult<T> =
50
+ | ({ ok: true; error?: undefined; errors?: undefined } & T)
51
+ | ({ ok: false; error: string; errors?: string[] } & { [K in keyof T]?: undefined });
52
+
53
+ export type PlanResult = PersistResult<{
54
+ path: string;
55
+ serial: number;
56
+ slug: string;
57
+ content: string;
58
+ filename: string;
59
+ }>;
60
+
61
+ /** A capture group its pattern requires, so it is always set. */
62
+ export function captured(match: ArrayLike<string | undefined>, index: number): string {
63
+ return String(match[index]);
64
+ }
65
+
66
+ const BOX_RE = /^( {0,2})(\d+(?:\.\d+){0,2}) \[([ xX])\] (.+)$/;
67
+ const LEAF_ID_RE = /^\d+\.\d+(?:\.\d+)?$/;
68
+
69
+ export function slugify(name: unknown): string {
70
+ let s = String(name || "")
71
+ .toLowerCase()
72
+ .replace(/[^a-z0-9]+/g, "-")
73
+ .replace(/-+/g, "-")
74
+ .replace(/^-+|-+$/g, "");
75
+ if (s.length > 50) {
76
+ s = s.slice(0, 50).replace(/-+$/g, "");
77
+ }
78
+ if (!s) s = "unnamed";
79
+ return s;
80
+ }
81
+
82
+ export function utcDateTime(d: Date = new Date()): string {
83
+ return d.toISOString().replace(/\.\d{3}Z$/, "Z");
84
+ }
85
+
86
+ export function utcDate(d: Date = new Date()): string {
87
+ return utcDateTime(d).slice(0, 10);
88
+ }
89
+
90
+ export function compactCreatedStamp(created: unknown): string {
91
+ return String(created).replace(/[-:]/g, "");
92
+ }
93
+
94
+ function slugFromFilename(filename: unknown): string {
95
+ return String(filename)
96
+ .replace(/\.md$/, "")
97
+ .replace(/^[0-9]+-/, "")
98
+ .replace(FILENAME_STAMP_RE, "");
99
+ }
100
+
101
+ const PLAN_NAME_RE = /^\s*PLAN_NAME:\s*(\S.*?)\s*$/;
102
+
103
+ /** The `PLAN_NAME:` line outside a code fence and above the first `## `, with its index; null when absent. */
104
+ function planNameLine(lines: string[]) {
105
+ const { fenced } = fencedLineSet(lines);
106
+ for (const [at, line] of lines.entries()) {
107
+ if (fenced.has(at)) continue;
108
+ if (line.startsWith("## ")) return null;
109
+ const m = PLAN_NAME_RE.exec(line);
110
+ if (m) return { at, name: captured(m, 1) };
111
+ }
112
+ return null;
113
+ }
114
+
115
+ function extractPlanName(content: unknown): string {
116
+ return planNameLine(String(content).split("\n"))?.name ?? "";
117
+ }
118
+
119
+ /** `content` without its `PLAN_NAME:` line and the blank lines after it, wherever it sits above the first `## `. */
120
+ function stripPlanName(content: unknown): string {
121
+ const lines = String(content).split("\n");
122
+ const found = planNameLine(lines);
123
+ if (!found) return lines.join("\n");
124
+ const rest = lines.slice(found.at + 1);
125
+ const blanks = rest.findIndex((l) => l.trim() !== "");
126
+ lines.splice(found.at, 1 + (blanks === -1 ? rest.length : blanks));
127
+ return lines.join("\n");
128
+ }
129
+
130
+ export function extractH1(content: unknown): string {
131
+ const m = String(content).match(/^#\s+(.+?)\s*$/m);
132
+ return m ? captured(m, 1).trim() : "";
133
+ }
134
+
135
+ export function nextSerial(plansDir: string): number {
136
+ let next = 1;
137
+ if (!fs.existsSync(plansDir)) return next;
138
+ for (const b of fs.readdirSync(plansDir)) {
139
+ if (!b.endsWith(".md")) continue;
140
+ const m = b.match(/^([0-9]+)-/);
141
+ if (!m) continue;
142
+ next = Math.max(next, parseInt(captured(m, 1), 10) + 1);
143
+ }
144
+ return next;
145
+ }
146
+
147
+ function stripFrontmatter(text: unknown) {
148
+ const s = String(text).replace(/^\uFEFF/, "");
149
+ if (!s.startsWith("---\n")) return s;
150
+ const end = s.indexOf("\n---\n", 4);
151
+ if (end === -1) return s;
152
+ return s.slice(end + 5);
153
+ }
154
+
155
+ export function extractCommit(content: unknown): string | undefined {
156
+ const text = String(content).replace(/^\uFEFF/, "");
157
+ if (text.startsWith("---\n")) {
158
+ const fm = parseFrontmatter(text);
159
+ if (fm.error === undefined && Object.prototype.hasOwnProperty.call(fm.map, "commit")) {
160
+ return fm.map.commit;
161
+ }
162
+ }
163
+ const m = text.match(/^commit:\s*(.*?)\s*$/m);
164
+ return m ? m[1] : undefined;
165
+ }
166
+
167
+ function stripCommitLine(content: unknown) {
168
+ return String(content).replace(/^commit:\s*.*\n/m, "");
169
+ }
170
+
171
+ /** Index of the first `{key}:` line outside a code fence, else -1. */
172
+ function keyLineAt(lines: string[], key: string) {
173
+ const { fenced } = fencedLineSet(lines);
174
+ const re = new RegExp(`^${key}:`);
175
+ return lines.findIndex((l, i) => !fenced.has(i) && re.test(l));
176
+ }
177
+
178
+ /** The `{key}:` value a save sends, in its frontmatter or as a body line outside a code fence; undefined when absent. */
179
+ function extractKey(content: unknown, key: string) {
180
+ const text = String(content).replace(/^\uFEFF/, "");
181
+ if (text.startsWith("---\n")) {
182
+ const fm = parseFrontmatter(text);
183
+ if (fm.error === undefined && Object.prototype.hasOwnProperty.call(fm.map, key)) return fm.map[key];
184
+ }
185
+ const lines = text.split("\n");
186
+ const at = keyLineAt(lines, key);
187
+ return at === -1
188
+ ? undefined
189
+ : String(lines[at])
190
+ .replace(new RegExp(`^${key}:\\s*`), "")
191
+ .trim();
192
+ }
193
+
194
+ /** `content` without its first `{key}:` line outside a code fence. */
195
+ function stripKeyLine(content: unknown, key: string) {
196
+ const lines = String(content).split("\n");
197
+ const at = keyLineAt(lines, key);
198
+ if (at !== -1) lines.splice(at, 1);
199
+ return lines.join("\n");
200
+ }
201
+
202
+ /** The `strategy:` value a save sends, in its frontmatter or as a body line; undefined when absent. */
203
+ export function extractStrategy(content: unknown): string | undefined {
204
+ return extractKey(content, "strategy");
205
+ }
206
+
207
+ /** `content` without its first `strategy:` line outside a code fence. */
208
+ export function stripStrategyLine(content: unknown): string {
209
+ return stripKeyLine(content, "strategy");
210
+ }
211
+
212
+ /** The `mode:` value a save sends, in its frontmatter or as a body line; undefined when absent. */
213
+ export function extractMode(content: unknown): string | undefined {
214
+ return extractKey(content, "mode");
215
+ }
216
+
217
+ /** `content` without its first `mode:` line outside a code fence. */
218
+ export function stripModeLine(content: unknown): string {
219
+ return stripKeyLine(content, "mode");
220
+ }
221
+
222
+ /** `mode` is optional; when sent it must be one of the three values. */
223
+ const MODE_ERROR: string = `mode: ${MODES.join("|")}`;
224
+
225
+ /** Errors for a frontmatter's `mode`: none when absent, MODE_ERROR when present and not one of MODES. */
226
+ export function modeErrors(map: FrontmatterMap): string[] {
227
+ if (!Object.prototype.hasOwnProperty.call(map, "mode")) return [];
228
+ return parseMode(map.mode) ? [] : [MODE_ERROR];
229
+ }
230
+
231
+ /** `strategy` must be one of the three values. */
232
+ export const STRATEGY_ERROR: string = `strategy: ${STRATEGIES.join("|")}`;
233
+
234
+ export interface FrontmatterInput {
235
+ serial: number;
236
+ slug: string;
237
+ source?: unknown;
238
+ body: string;
239
+ created?: string;
240
+ commit?: string | undefined;
241
+ strategy?: string | undefined;
242
+ mode?: string | undefined;
243
+ }
244
+
245
+ export function injectFrontmatter({ serial, slug, source, body, created, commit, strategy, mode }: FrontmatterInput) {
246
+ const src = String(source || "").slice(0, 120);
247
+ let rest = stripFrontmatter(body).replace(/^\s+/, "");
248
+ if (!rest.startsWith("# ")) {
249
+ rest = `# ${slug}\n\n` + rest;
250
+ } else {
251
+ rest = rest.replace(/^# .+/, `# ${slug}`);
252
+ }
253
+ const commitLine = commit != null && commit !== "" ? `commit: ${commit}\n` : "";
254
+ const strategyLine = strategy != null && strategy !== "" ? `strategy: ${strategy}\n` : "";
255
+ // An empty `mode:` is still sent, so validation denies it rather than dropping it.
256
+ const modeLine = mode !== undefined ? `mode: ${mode}\n` : "";
257
+ return `---
258
+ serial: ${serial}
259
+ name: ${slug}
260
+ status: ready
261
+ reviews: 0
262
+ created: ${created || utcDateTime()}
263
+ source: ${JSON.stringify(src)}
264
+ ${commitLine}${strategyLine}${modeLine}---
265
+
266
+ ${rest}`;
267
+ }
268
+
269
+ const FENCE_OPEN_RE = /^ {0,3}(`{3,}|~{3,})/;
270
+ const FENCE_CLOSE_RE = /^ {0,3}(`{3,}|~{3,})\s*$/;
271
+
272
+ /**
273
+ * Lines inside CommonMark code fences, fence lines included. A fence closes only on the same
274
+ * character, at least as long, with nothing after it. `unclosed` is the 1-based line of a fence
275
+ * that never closes, else null.
276
+ */
277
+ export function fencedLineSet(lines: string[]): { fenced: Set<number>; unclosed: number | null } {
278
+ const fenced = new Set<number>();
279
+ let open: { char: string; length: number; line: number } | null = null;
280
+ for (const [i, line] of lines.entries()) {
281
+ if (open) {
282
+ fenced.add(i);
283
+ const m = FENCE_CLOSE_RE.exec(line);
284
+ const fence = m?.[1] ?? "";
285
+ if (m && fence[0] === open.char && fence.length >= open.length) open = null;
286
+ continue;
287
+ }
288
+ const m = FENCE_OPEN_RE.exec(line);
289
+ const fence = m?.[1] ?? "";
290
+ if (!m || (fence[0] === "`" && line.slice(line.indexOf(fence) + fence.length).includes("`"))) continue;
291
+ fenced.add(i);
292
+ open = { char: fence.charAt(0), length: fence.length, line: i + 1 };
293
+ }
294
+ return { fenced, unclosed: open ? open.line : null };
295
+ }
296
+
297
+ export function unclosedFenceErrors(text: unknown): string[] {
298
+ const { unclosed } = fencedLineSet(String(text).split("\n"));
299
+ return unclosed ? [`unclosed code fence at line ${unclosed}: close it with the same fence`] : [];
300
+ }
301
+
302
+ /** `## ` sections outside code fences, in order, with each body. */
303
+ export function sectionBodies(text: string): Sections {
304
+ const lines = text.split("\n");
305
+ const { fenced } = fencedLineSet(lines);
306
+ const h2: { title: string; index: number; end: number }[] = [];
307
+ let offset = 0;
308
+ lines.forEach((line, i) => {
309
+ const m = !fenced.has(i) && /^## (.+)$/.exec(line);
310
+ if (m) h2.push({ title: captured(m, 1).trim(), index: offset, end: offset + line.length + 1 });
311
+ offset += line.length + 1;
312
+ });
313
+ const bodies: Record<string, string> = {};
314
+ for (const [i, cur] of h2.entries()) {
315
+ const end = h2[i + 1]?.index ?? text.length;
316
+ bodies[cur.title] = text.slice(cur.end, end);
317
+ }
318
+ return { order: h2.map((h) => h.title), bodies };
319
+ }
320
+
321
+ export const USAGE_H2 = "Usage";
322
+
323
+ /**
324
+ * `## Usage` shows what a person or agent types and sees: required right after Goal on a fresh
325
+ * save, optional on an update to a file saved without it, and never prose alone.
326
+ */
327
+ export function usageErrors(order: string[], bodies: Record<string, string>, { fresh }: { fresh: boolean }): string[] {
328
+ const at = order.indexOf(USAGE_H2);
329
+ if (at === -1) {
330
+ return fresh
331
+ ? [
332
+ 'missing ## Usage after ## Goal: show what a person types and sees, before and after, in fenced blocks, or one "No visible change:" line',
333
+ ]
334
+ : [];
335
+ }
336
+ const errors: string[] = [];
337
+ if (order[at - 1] !== "Goal") errors.push("## Usage must come right after ## Goal");
338
+ const body = bodies[USAGE_H2] || "";
339
+ const lines = body.split("\n");
340
+ const hasFence = fencedLineSet(lines).fenced.size > 0;
341
+ if (!hasFence && !lines.some((l) => l.startsWith("No visible change:"))) {
342
+ errors.push('## Usage needs a fenced example or a "No visible change:" line');
343
+ }
344
+ return errors;
345
+ }
346
+
347
+ const CARD_LABELS = ["WHAT", "WHY", "RISK", "TOUCH", "DONE"] as const;
348
+ const CARD_WIDTH = 120;
349
+ const CARD_PAD = 7;
350
+ const CARD_LABEL_RE = new RegExp(`^(?:${CARD_LABELS.join("|")})\\b`);
351
+
352
+ /**
353
+ * The card a person reads first: the 5 lines after the `# ` title and one blank line, labels in
354
+ * order padded to 7 columns, each line at most 120 characters. One message per failing line.
355
+ */
356
+ export function cardErrors(body: string): string[] {
357
+ const lines = body.replace(/^\s+/, "").split("\n");
358
+ const block: string[] = [];
359
+ if (lines[1] === "") {
360
+ for (const line of lines.slice(2)) {
361
+ if (line.trim() === "" || line.startsWith("## ")) break;
362
+ block.push(line);
363
+ }
364
+ }
365
+ if (!block.some((l) => CARD_LABEL_RE.test(l))) {
366
+ return [`start with the 5-line card after the title: ${CARD_LABELS.join(", ")}`];
367
+ }
368
+ const errors: string[] = [];
369
+ CARD_LABELS.forEach((label, i) => {
370
+ const n = i + 1;
371
+ const line = block[i] ?? "";
372
+ const pad = label.padEnd(CARD_PAD);
373
+ if (!new RegExp(`^${label}\\b`).test(line)) errors.push(`card line ${n} must start with ${label}`);
374
+ else if (!line.startsWith(pad) || !/\S/.test(line.charAt(CARD_PAD))) {
375
+ errors.push(`card line ${n} (${label}) needs its text at column ${CARD_PAD + 1}: "${pad}<text>"`);
376
+ }
377
+ const length = [...line].length;
378
+ if (length > CARD_WIDTH)
379
+ errors.push(`card line ${n} (${label}) is ${length} characters; the limit is ${CARD_WIDTH}`);
380
+ });
381
+ if (block.length > CARD_LABELS.length) errors.push("the card is 5 lines; end it with a blank line after DONE");
382
+ return errors;
383
+ }
384
+
385
+ const PLAN_LINE_CAP = 100;
386
+ const PLAN_CAP_FIX = "split it into a roadmap (/write-roadmap)";
387
+
388
+ /**
389
+ * Lines above the first `## Previous mistakes` outside a fence, frontmatter included; the whole file
390
+ * when absent. Lines matching `skip` outside a fence are not counted.
391
+ */
392
+ function linesAboveMistakes(text: string, skip?: RegExp) {
393
+ const lines = text.replace(/\n$/, "").split("\n");
394
+ const { fenced } = fencedLineSet(lines);
395
+ const found = lines.findIndex((l, i) => !fenced.has(i) && l === `## ${MISTAKES_H2}`);
396
+ const above = found === -1 ? lines.length : found;
397
+ if (!skip) return above;
398
+ return lines.slice(0, above).filter((l, i) => fenced.has(i) || !skip.test(l)).length;
399
+ }
400
+
401
+ /**
402
+ * A file past `cap` lines above Previous mistakes is denied with `fix`; a plan's cap is 100. A fresh
403
+ * save is held to the cap; an update only when it grows past both the cap and the saved file, so
404
+ * legacy files stay editable.
405
+ */
406
+ export function capErrors(
407
+ text: string,
408
+ {
409
+ fresh,
410
+ previous,
411
+ cap = PLAN_LINE_CAP,
412
+ fix = PLAN_CAP_FIX,
413
+ skip,
414
+ }: { fresh: boolean; previous?: string | null | undefined; cap?: number; fix?: string; skip?: RegExp },
415
+ ): string[] {
416
+ const n = linesAboveMistakes(text, skip);
417
+ const ceiling = fresh || previous == null ? cap : Math.max(cap, linesAboveMistakes(previous, skip));
418
+ return n > ceiling ? [`${n} lines above ## ${MISTAKES_H2}, the cap is ${cap}; ${fix}`] : [];
419
+ }
420
+
421
+ export interface PlanBox {
422
+ id: string;
423
+ mark: string;
424
+ text: string;
425
+ }
426
+
427
+ export interface PlanPhase extends PlanBox {
428
+ leaves: PlanBox[];
429
+ }
430
+
431
+ /** Checkbox lines outside code fences, in file order. */
432
+ export function planBoxes(text: string): PlanBox[] {
433
+ const lines = text.split("\n");
434
+ const { fenced } = fencedLineSet(lines);
435
+ const boxes: PlanBox[] = [];
436
+ for (const [i, line] of lines.entries()) {
437
+ const m = !fenced.has(i) && BOX_RE.exec(line);
438
+ if (m) boxes.push({ id: captured(m, 2), mark: captured(m, 3), text: captured(m, 4) });
439
+ }
440
+ return boxes;
441
+ }
442
+
443
+ /** Phases (`N [ ]`) in order, each with the leaves under it. */
444
+ export function planPhases(boxes: PlanBox[]): PlanPhase[] {
445
+ return boxes
446
+ .filter((b) => /^\d+$/.test(b.id))
447
+ .map((p) => ({ ...p, leaves: boxes.filter((b) => LEAF_ID_RE.test(b.id) && b.id.startsWith(p.id + ".")) }));
448
+ }
449
+
450
+ const PRINCIPLE_READ_RE = /^read\s+(?:`principle-[\w-]+`(?:\s*(?:,|and)\s*)?)+\.?$/i;
451
+
452
+ function readsOnly(phase: PlanPhase) {
453
+ return phase.leaves.length > 0 && phase.leaves.every((l) => PRINCIPLE_READ_RE.test(l.text.trim()));
454
+ }
455
+
456
+ /**
457
+ * A phase that only reads principles gates nothing: each read belongs in the phase it gates. A
458
+ * fresh save is denied; an update only when the saved file had no such phase.
459
+ */
460
+ function readPhaseErrors(
461
+ phases: PlanPhase[],
462
+ { fresh, previous }: { fresh: boolean; previous?: string | null | undefined },
463
+ ): string[] {
464
+ const offending = phases.filter(readsOnly);
465
+ if (!offending.length) return [];
466
+ if (!fresh && previous != null && planPhases(planBoxes(previous)).some(readsOnly)) return [];
467
+ return offending.map((p) => `phase ${p.id} only reads principles; move each read into the phase it gates`);
468
+ }
469
+
470
+ const PARALLEL_RE = /\(parallel:([^)]*)\)\s*$/;
471
+
472
+ /** The files a `(parallel: a, b)` phase declares, `\` read as `/`, as written; null when not parallel. */
473
+ export function parallelFiles(phase: PlanBox): string[] | null {
474
+ const m = PARALLEL_RE.exec(phase.text);
475
+ if (!m) return null;
476
+ return captured(m, 1)
477
+ .split(/[,\s]+/)
478
+ .filter(Boolean)
479
+ .map((f) => f.replace(/\\/g, "/"));
480
+ }
481
+
482
+ /**
483
+ * Adjacent `parallel:` phases form a group that may run at once, so their declared files must be
484
+ * disjoint. A group needs two or more phases, and each phase must declare its files.
485
+ */
486
+ function parallelErrors(phases: PlanPhase[]): string[] {
487
+ const errors: string[] = [];
488
+ const groups: { id: string; files: string[] }[][] = [];
489
+ let run: { id: string; files: string[] }[] = [];
490
+ for (const p of [...phases, null]) {
491
+ const files = p && parallelFiles(p);
492
+ if (p && files) {
493
+ run.push({ id: p.id, files });
494
+ continue;
495
+ }
496
+ if (run.length) groups.push(run);
497
+ run = [];
498
+ }
499
+ for (const group of groups) {
500
+ for (const [i, a] of group.entries()) {
501
+ if (group.length === 1)
502
+ errors.push(`phase ${a.id} is parallel alone; parallel needs two or more adjacent phases`);
503
+ if (!a.files.length)
504
+ errors.push(`phase ${a.id} is parallel with no files; list the files it writes after parallel:`);
505
+ for (const b of group.slice(i + 1)) {
506
+ for (const f of new Set(a.files.filter((x) => b.files.includes(x)))) {
507
+ errors.push(`parallel phases ${a.id} and ${b.id} both list ${f}; parallel phases need disjoint files`);
508
+ }
509
+ }
510
+ }
511
+ }
512
+ return errors;
513
+ }
514
+
515
+ function parseFrontmatter(text: string): FrontmatterResult {
516
+ if (!text.startsWith("---\n")) return { error: "missing frontmatter" };
517
+ const end = text.indexOf("\n---\n", 4);
518
+ if (end === -1) return { error: "unterminated frontmatter" };
519
+ const raw = text.slice(4, end);
520
+ const map: FrontmatterMap = {};
521
+ for (const line of raw.split("\n")) {
522
+ const m = line.match(/^([a-z]+):(?:\s+(.*))?$/);
523
+ if (m?.[1] === undefined) continue;
524
+ let v = m[2] ?? "";
525
+ if (v.startsWith('"') && v.endsWith('"')) {
526
+ try {
527
+ v = String(JSON.parse(v));
528
+ } catch {
529
+ v = v.slice(1, -1);
530
+ }
531
+ }
532
+ map[m[1]] = v;
533
+ }
534
+ return { map, body: text.slice(end + 5) };
535
+ }
536
+
537
+ const REVIEWS_RE = /^(0|[1-9][0-9]*)$/;
538
+ /** Statuses a plan may enter only after that many settled `/review-plan` rounds. */
539
+ const REVIEWS_TO_ENTER: Record<string, number> = { "in-progress": 1 };
540
+
541
+ /**
542
+ * `reviews` counts settled review rounds: well-formed, never removed or lowered, and gating entry to
543
+ * a status. `gates` maps a status to the rounds it needs; `review` is the command the deny names.
544
+ */
545
+ export function reviewErrors(
546
+ map: FrontmatterMap,
547
+ prev: FrontmatterMap | null,
548
+ { gates, review }: { gates: Record<string, number>; review: string },
549
+ ): string[] {
550
+ const has = Object.prototype.hasOwnProperty.call(map, "reviews");
551
+ if (has && !REVIEWS_RE.test(String(map.reviews))) {
552
+ return ["reviews must be a whole number with no sign or leading zero, e.g. reviews: 1"];
553
+ }
554
+ const count = has ? Number(map.reviews) : 0;
555
+ const errors: string[] = [];
556
+ if (prev && Object.prototype.hasOwnProperty.call(prev, "reviews")) {
557
+ if (!has) errors.push(`keep reviews: ${prev.reviews} in the frontmatter; the review count cannot be removed`);
558
+ else if (count < Number(prev.reviews)) errors.push(`reviews cannot go down (saved ${prev.reviews}, got ${count})`);
559
+ }
560
+ const need = map.status === undefined ? undefined : gates[map.status];
561
+ if (need && (!prev || prev.status !== map.status) && count < need) {
562
+ errors.push(
563
+ `status ${map.status} needs reviews: ${need} or more; run ${review}, settle its findings, and bump reviews in that save`,
564
+ );
565
+ }
566
+ return errors;
567
+ }
568
+
569
+ /** `(none)` or one `- {source}: {what was wrong} → {what changed}` line per entry. */
570
+ export function parseMistakes(
571
+ sectionText: unknown,
572
+ sources: readonly string[],
573
+ ): { entries: MistakeEntry[]; errors: string[] } {
574
+ const re = new RegExp(`^- (${sources.join("|")}): (.+?) → (.+?)(?: \\(lesson: ([a-z0-9]+(?:-[a-z0-9]+)*)\\))?$`);
575
+ const lines = String(sectionText ?? "")
576
+ .split("\n")
577
+ .map((l) => l.trim())
578
+ .filter(Boolean);
579
+ if (lines.length === 1 && lines[0] === "(none)") return { entries: [], errors: [] };
580
+ if (!lines.length) return { entries: [], errors: ["## Previous mistakes is empty; write (none)"] };
581
+ const entries: MistakeEntry[] = [];
582
+ const errors: string[] = [];
583
+ for (const line of lines) {
584
+ const m = line.match(re);
585
+ if (!m) {
586
+ errors.push(
587
+ `Previous mistakes entry must be "- {${sources.join("|")}}: {what was wrong} → {what changed}": ${line}`,
588
+ );
589
+ continue;
590
+ }
591
+ entries.push({ source: captured(m, 1), text: `${m[2]} → ${m[3]}`, lesson: m[4], line });
592
+ }
593
+ return { entries, errors };
594
+ }
595
+
596
+ /**
597
+ * Errors in a `## Previous mistakes` log given a document's heading order and section bodies:
598
+ * last and once, required when `fresh`, well-formed, and append-only against `previous` (the saved
599
+ * document's order and bodies). `kind` names the document in the deny.
600
+ */
601
+ export function mistakeLogErrors(
602
+ order: string[],
603
+ bodies: Record<string, string>,
604
+ {
605
+ fresh,
606
+ previous,
607
+ sources,
608
+ kind,
609
+ }: { fresh: boolean; previous?: Sections | null | undefined; sources: readonly string[]; kind: string },
610
+ ): string[] {
611
+ const errors: string[] = [];
612
+ const at = order.indexOf(MISTAKES_H2);
613
+ if (at !== -1 && (at !== order.length - 1 || order.lastIndexOf(MISTAKES_H2) !== at)) {
614
+ errors.push("## Previous mistakes must be the last heading, once");
615
+ }
616
+ if (at === -1 && fresh) {
617
+ errors.push(`missing ## Previous mistakes; end a new ${kind} with it and (none)`);
618
+ }
619
+ const mistakes: { entries: MistakeEntry[]; errors: string[] } =
620
+ at === -1 ? { entries: [], errors: [] } : parseMistakes(bodies[MISTAKES_H2], sources);
621
+ errors.push(...mistakes.errors);
622
+ if (previous && previous.order.includes(MISTAKES_H2)) {
623
+ const current = mistakes.entries.map((e) => e.line);
624
+ parseMistakes(previous.bodies[MISTAKES_H2], sources).entries.forEach((e, i) => {
625
+ if (current[i] !== e.line) {
626
+ errors.push(`Previous mistakes is append-only; keep this entry unchanged: ${e.line}`);
627
+ }
628
+ });
629
+ }
630
+ return errors;
631
+ }
632
+
633
+ /**
634
+ * `fresh` is a new plan: no `[x]`, Previous mistakes required. `previous` is the saved text an
635
+ * update replaces: its Previous mistakes entries must survive unchanged and in order.
636
+ */
637
+ export function validatePlan(
638
+ filename: string,
639
+ text: string,
640
+ { fresh = true, previous }: { fresh?: boolean; previous?: string | null | undefined } = {},
641
+ ): string[] {
642
+ const errors: string[] = [];
643
+ if (!FILENAME_RE.test(filename)) errors.push("filename");
644
+ const fm = parseFrontmatter(text);
645
+ if (fm.error !== undefined) {
646
+ errors.push(fm.error);
647
+ return errors;
648
+ }
649
+ const { map, body } = fm;
650
+ const serialLine = text.split("\n").find((l) => l.startsWith("serial:"));
651
+ if (!serialLine || !SERIAL_LINE_RE.test(serialLine)) errors.push("serial line");
652
+ for (const k of ["serial", "name", "status", "created", "source"]) {
653
+ if (map[k] == null || map[k] === "") errors.push(`missing ${k}`);
654
+ }
655
+ if (map.status && !STATUS_RE.test(map.status)) errors.push("status");
656
+ if (map.created && !CREATED_RE.test(map.created)) errors.push("created");
657
+ if (typeof map.source === "string" && map.source.length > 120) {
658
+ errors.push("source length");
659
+ }
660
+ if (Object.prototype.hasOwnProperty.call(map, "commit") && !COMMIT_SUBJECT_RE.test(String(map.commit))) {
661
+ errors.push("commit");
662
+ }
663
+ if (!parseStrategy(map.strategy)) errors.push(STRATEGY_ERROR);
664
+ errors.push(...modeErrors(map));
665
+ const slugFromName = map.name;
666
+ if (slugFromName && slugFromFilename(filename) !== slugFromName) {
667
+ errors.push("name/filename slug");
668
+ }
669
+ if (!body.trim().startsWith(`# ${slugFromName}`)) errors.push("h1 slug");
670
+ const prevFm = previous == null ? null : parseFrontmatter(previous);
671
+ errors.push(
672
+ ...reviewErrors(map, prevFm?.map ?? null, {
673
+ gates: REVIEWS_TO_ENTER,
674
+ review: "/review-plan {n}",
675
+ }),
676
+ );
677
+ const fileSerial = filename.match(/^(\d+)-/);
678
+ if (fileSerial && map.serial && Number(map.serial) !== Number(fileSerial[1])) {
679
+ errors.push("serial/filename");
680
+ }
681
+ const fence = unclosedFenceErrors(text);
682
+ if (fence.length) return [...errors, ...fence];
683
+ const { order: allOrder, bodies } = sectionBodies(body);
684
+ errors.push(
685
+ ...mistakeLogErrors(allOrder, bodies, {
686
+ fresh,
687
+ previous: previous == null ? null : sectionBodies(stripFrontmatter(previous)),
688
+ sources: PLAN_MISTAKE_SOURCES,
689
+ kind: "plan",
690
+ }),
691
+ );
692
+ errors.push(...usageErrors(allOrder, bodies, { fresh }));
693
+ if (fresh) errors.push(...cardErrors(body));
694
+ errors.push(...capErrors(text, { fresh, previous }));
695
+ const order = allOrder.filter((h) => h !== MISTAKES_H2 && h !== USAGE_H2);
696
+ const firstSix = order.slice(0, REQUIRED_H2.length);
697
+ if (REQUIRED_H2.some((h, i) => firstSix[i] !== h)) errors.push("heading order");
698
+ const extra = order.filter((h) => !REQUIRED_H2.includes(h));
699
+ if (extra.length) errors.push("extra h2");
700
+ const goal = (bodies.Goal || "").trim();
701
+ const sentences = goal.split(/(?<=[.!?])(?:\s+|$)/).filter((s) => s.trim());
702
+ if (!goal || sentences.length < 1 || sentences.length > 3) errors.push("goal");
703
+ for (const h of ["Context", "Constraints", "Watch-outs"]) {
704
+ const b = (bodies[h] || "").trim();
705
+ if (!b) errors.push(`${h} empty`);
706
+ else if (!/^(?:[-*] |\(none\))/m.test(b)) errors.push(`${h} bullets`);
707
+ }
708
+ const boxes = planBoxes(text);
709
+ if (fresh && boxes.some((b) => b.mark !== " ")) errors.push("fresh [x]");
710
+ const leaves = boxes.filter((b) => LEAF_ID_RE.test(b.id));
711
+ if (leaves.length < 1) errors.push("no N.M leaf");
712
+ const phases = planPhases(boxes);
713
+ for (const p of phases) {
714
+ if (!boxes.some((b) => b.id.startsWith(p.id + "."))) errors.push(`phase ${p.id} leaf`);
715
+ }
716
+ errors.push(...readPhaseErrors(phases, { fresh, previous }));
717
+ errors.push(...parallelErrors(phases));
718
+ const last = phases[phases.length - 1];
719
+ if (last) {
720
+ if (!/^Verify\b/i.test(last.text) && last.id !== String(phases.length)) {
721
+ errors.push("verify last phase");
722
+ }
723
+ }
724
+ return [...new Set(errors)];
725
+ }
726
+
727
+ const ABSOLUTE_PATH = /(?<![\w.:\\/-])(?:[A-Za-z]:[\\/]|\/)[^\s`'"()<>,;]+/g;
728
+ /** A repo-relative path: two or more segments, the last with an extension, not the tail of an absolute path or a URL. */
729
+ const RELATIVE_PATH = /(?<![\w.:\\/@-])[\w.@-]+(?:[\\/][\w.@-]+)+/g;
730
+
731
+ /**
732
+ * Paths under `ws` (the root) that `text` names: absolute as written (either separator, any case on Windows, any
733
+ * spelling of `ws`: a symlink, an 8.3 name), then repo-relative ones resolved against `ws`. Paths under
734
+ * `<ws>/.worktrees/<name>/` count like any other. A sibling dir that shares the prefix does not.
735
+ */
736
+ export function workspacePaths(ws: string, text: unknown, p: path.PlatformPath = path): string[] {
737
+ const top = canonicalPath(p.resolve(ws), p);
738
+ const under = (abs: string) => {
739
+ const rel = p.relative(top, canonicalPath(abs, p));
740
+ return rel && !rel.startsWith("..") && !p.isAbsolute(rel);
741
+ };
742
+ const trim = (m: string) => m.replace(/[.:]+$/, "");
743
+ const source = String(text);
744
+ const absolute = (source.match(ABSOLUTE_PATH) || []).map(trim).filter(under);
745
+ const relative = (source.match(RELATIVE_PATH) || [])
746
+ .map(trim)
747
+ .filter((m) => /\.[A-Za-z0-9]+$/.test(m))
748
+ .map((m) => p.resolve(top, m))
749
+ .filter(under);
750
+ return [...new Set([...absolute, ...relative])];
751
+ }
752
+
753
+ /** Lessons matching files a new plan names, held by the checkout of each file, that Watch-outs does not cite. */
754
+ function uncitedLessons(roots: Roots, text: string) {
755
+ const files = [...new Set([roots.root, roots.checkout].flatMap((top) => workspacePaths(top, text)))];
756
+ if (!files.length) return [];
757
+ const watch = sectionBodies(stripFrontmatter(text)).bodies["Watch-outs"] || "";
758
+ return matchLessons({ roots, files }).filter((r) => !watch.includes(r.file));
759
+ }
760
+
761
+ /** `head` (frontmatter ending in `---\n`) with `key: value` set; unchanged when `value` is undefined. */
762
+ function setHeadKey(head: string, key: string, value: string | undefined) {
763
+ if (value === undefined) return head;
764
+ const line = new RegExp(`^${key}:.*$`, "m");
765
+ return line.test(head)
766
+ ? head.replace(line, () => `${key}: ${value}`)
767
+ : head.replace(/\n---\n$/, () => `\n${key}: ${value}\n---\n`);
768
+ }
769
+
770
+ /** Body-only revision (PLAN_NAME, no frontmatter): keep the saved frontmatter and slug. */
771
+ function withSavedFrontmatter(previous: string, raw: string) {
772
+ const fm = parseFrontmatter(previous);
773
+ // Nothing to keep: validation of the bare body names the missing frontmatter.
774
+ if (fm.error !== undefined) return raw;
775
+ const head = previous.slice(0, previous.length - fm.body.length);
776
+ const nextHead = setHeadKey(
777
+ setHeadKey(setHeadKey(head, "commit", extractCommit(raw)), "strategy", extractStrategy(raw)),
778
+ "mode",
779
+ extractMode(raw),
780
+ );
781
+ let rest = stripModeLine(stripStrategyLine(stripCommitLine(stripPlanName(raw)))).replace(/^\s+/, "");
782
+ rest = rest.startsWith("# ") ? rest.replace(/^# .+/, `# ${fm.map.name}`) : `# ${fm.map.name}\n\n${rest}`;
783
+ return `${nextHead}\n${rest}`;
784
+ }
785
+
786
+ /**
787
+ * Rewrite of a saved plan: same file and serial, `[x]` allowed, Previous mistakes append-only.
788
+ * Validates only. The host writes `content`: a write here first makes Claude Code refuse the
789
+ * Write as modified since read.
790
+ */
791
+ export function preparePlanUpdate({
792
+ workspaceRoot,
793
+ filePath,
794
+ content,
795
+ }: {
796
+ workspaceRoot?: string;
797
+ filePath: string;
798
+ content?: unknown;
799
+ }): PlanResult {
800
+ const ws = workspaceRoot || resolveRoots().root;
801
+ const abs = path.resolve(ws, filePath);
802
+ const filename = path.basename(abs);
803
+ const previous = fs.readFileSync(abs, "utf8");
804
+ const text = String(content || "").replace(/^/, "");
805
+ const fence = unclosedFenceErrors(text);
806
+ if (fence.length) return { ok: false, error: fence.join("; "), errors: fence };
807
+ const full = text.startsWith("---\n") ? text : withSavedFrontmatter(previous, text);
808
+ const errors = validatePlan(filename, full, { fresh: false, previous });
809
+ if (errors.length) {
810
+ return { ok: false, error: errors.join("; "), errors };
811
+ }
812
+ return {
813
+ ok: true,
814
+ path: path.relative(ws, abs).split(path.sep).join("/"),
815
+ serial: Number(filename.match(/^(\d+)-/)?.[1]), // validatePlan accepted the filename, so the serial prefix is there
816
+ slug: slugFromFilename(filename),
817
+ content: full,
818
+ filename,
819
+ };
820
+ }
821
+
822
+ export function persistPlan({
823
+ workspaceRoot,
824
+ checkout,
825
+ content,
826
+ source,
827
+ created,
828
+ }: {
829
+ workspaceRoot?: string;
830
+ checkout?: string;
831
+ content?: unknown;
832
+ source?: string;
833
+ created?: string;
834
+ } = {}): PlanResult {
835
+ const ws = workspaceRoot || resolveRoots().root;
836
+ // Lesson citations read tracked lessons, which live in the checkout that holds each file the plan names.
837
+ const lessonRoots = { root: ws, checkout: checkout || ws, cwd: ws };
838
+ const raw = String(content || "");
839
+ // Line numbers in this deny count from what the caller sent.
840
+ const fence = unclosedFenceErrors(raw);
841
+ if (fence.length) return { ok: false, error: fence.join("; "), errors: fence };
842
+ const planName = extractPlanName(raw) || extractH1(stripFrontmatter(stripPlanName(raw)));
843
+ const slug = slugify(planName);
844
+ const commit = extractCommit(raw);
845
+ const strategy = extractStrategy(raw);
846
+ const mode = extractMode(raw);
847
+ const body = stripModeLine(stripStrategyLine(stripCommitLine(stripPlanName(raw))));
848
+ const plansDir = path.join(ws, PLAN_DIR);
849
+ fs.mkdirSync(plansDir, { recursive: true });
850
+ const createdAt = created && DATETIME_RE.test(created) ? created : utcDateTime();
851
+ const stamp = compactCreatedStamp(createdAt);
852
+ const serial = nextSerial(plansDir);
853
+ const filename = `${serial}-${slug}-${stamp}.md`;
854
+ const dest = path.join(plansDir, filename);
855
+ if (fs.existsSync(dest)) {
856
+ return { ok: false, error: "serial collision" };
857
+ }
858
+ const goalMatch = stripFrontmatter(body).match(/## Goal\n([\s\S]*?)\n## /);
859
+ const sourceText = String(
860
+ source || planName || (goalMatch ? captured(goalMatch, 1).trim().replace(/\s+/g, " ") : slug),
861
+ ).slice(0, 120);
862
+ const full = injectFrontmatter({
863
+ serial,
864
+ slug,
865
+ source: sourceText,
866
+ body,
867
+ created: createdAt,
868
+ commit,
869
+ strategy,
870
+ mode,
871
+ });
872
+ const errors = validatePlan(filename, full);
873
+ for (const r of uncitedLessons(lessonRoots, full)) {
874
+ errors.push(`cite this matched lesson in Watch-outs: ${r.file} (${r.summary})`);
875
+ }
876
+ if (errors.length) {
877
+ return { ok: false, error: errors.join("; "), errors };
878
+ }
879
+ fs.writeFileSync(dest, full);
880
+ return {
881
+ ok: true,
882
+ path: `${PLAN_DIR}/${filename}`,
883
+ serial,
884
+ slug,
885
+ content: full,
886
+ filename,
887
+ };
888
+ }