forge-workflow 0.0.9 → 0.1.0-beta.2

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 (479) hide show
  1. package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
  2. package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
  3. package/.cursor/rules/permissions-guidance.mdc +2 -2
  4. package/.forge/hooks/check-tdd.js +3 -0
  5. package/.forge/hooks/forge-native-hook.js +245 -0
  6. package/.forge/protected-paths.yaml +157 -0
  7. package/AGENTS.md +151 -61
  8. package/CHANGELOG.md +681 -0
  9. package/CLAUDE.md +9 -106
  10. package/QUICKSTART.md +171 -0
  11. package/README.md +271 -363
  12. package/bin/forge-cmd.js +120 -9
  13. package/bin/forge-preflight.js +26 -5
  14. package/bin/forge.js +466 -489
  15. package/docs/INDEX.md +93 -0
  16. package/docs/PROJECT_DESIGN.md +685 -0
  17. package/docs/architecture/index.md +66 -0
  18. package/docs/architecture/notes/README.md +35 -0
  19. package/docs/architecture/subsystems/README.md +46 -0
  20. package/docs/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
  21. package/docs/forge/VALIDATION.md +82 -0
  22. package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
  23. package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
  24. package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
  25. package/docs/guides/GREPTILE_SETUP.md +46 -0
  26. package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
  27. package/docs/guides/MIGRATION.md +56 -0
  28. package/docs/guides/SETUP.md +118 -0
  29. package/docs/guides/SUPPORT.md +185 -0
  30. package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
  31. package/docs/guides/memory-backends.md +183 -0
  32. package/docs/reference/ADAPTERS.md +128 -0
  33. package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
  34. package/docs/reference/COMMANDS.md +205 -0
  35. package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
  36. package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
  37. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
  38. package/docs/reference/HERMES_INTEGRATION.md +118 -0
  39. package/docs/reference/INSIGHTS_RECAP.md +63 -0
  40. package/docs/reference/INSTALL.md +164 -0
  41. package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
  42. package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
  43. package/docs/reference/RELEASE.md +68 -0
  44. package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
  45. package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
  46. package/docs/reference/SKILLS.md +35 -0
  47. package/docs/reference/STATUS_BOARD.md +80 -0
  48. package/docs/reference/TEMPLATES.md +106 -0
  49. package/docs/reference/TOOLCHAIN.md +658 -0
  50. package/docs/reference/VALIDATION.md +82 -0
  51. package/docs/reference/agent-permissions.md +169 -0
  52. package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
  53. package/docs/reference/control-plane-guarantees.md +125 -0
  54. package/docs/reference/dependency-chain.md +331 -0
  55. package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
  56. package/docs/reference/forge-kernel-schema.md +72 -0
  57. package/docs/reference/kernel-conflict-evaluators.md +27 -0
  58. package/docs/reference/patch-md-format.md +77 -0
  59. package/docs/reference/protected-state-surfaces.md +59 -0
  60. package/docs/reference/shepherd.md +115 -0
  61. package/docs/reference/superpowers-analysis.md +320 -0
  62. package/docs/reference/superpowers-integration-options.md +404 -0
  63. package/docs/reference/test-environment.md +519 -0
  64. package/docs/reference/upgrade-safety.md +59 -0
  65. package/lefthook.yml +18 -0
  66. package/lib/adapter-cli.js +307 -0
  67. package/lib/adapters/beads-issue-adapter.js +127 -0
  68. package/lib/adapters/beads-kernel-compat.js +1042 -0
  69. package/lib/adapters/greptile-review-adapter.js +141 -0
  70. package/lib/adapters/kernel-issue-adapter.js +101 -0
  71. package/lib/adapters/pr-state-adapter.js +484 -0
  72. package/lib/adoption-profiles.js +126 -0
  73. package/lib/agents/README.md +2 -6
  74. package/lib/agents/claude.plugin.json +3 -8
  75. package/lib/agents/codex.plugin.json +9 -1
  76. package/lib/agents/cursor.plugin.json +2 -6
  77. package/lib/agents/hermes.plugin.json +22 -0
  78. package/lib/agents-config.js +39 -1236
  79. package/lib/audit-evidence.js +282 -0
  80. package/lib/beads-setup.js +225 -28
  81. package/lib/beads-sync-scaffold.js +36 -107
  82. package/lib/codex-skills.js +51 -1
  83. package/lib/commands/_issue.js +744 -70
  84. package/lib/commands/_manifest.js +91 -0
  85. package/lib/commands/_registry.js +85 -34
  86. package/lib/commands/_resolve-command-opts.js +261 -0
  87. package/lib/commands/_serve-security.js +270 -0
  88. package/lib/commands/adapter.js +12 -0
  89. package/lib/commands/add.js +118 -0
  90. package/lib/commands/audit.js +70 -0
  91. package/lib/commands/blocked.js +5 -0
  92. package/lib/commands/board.js +64 -0
  93. package/lib/commands/claim.js +21 -2
  94. package/lib/commands/claims.js +7 -0
  95. package/lib/commands/clean.js +485 -75
  96. package/lib/commands/close.js +2 -2
  97. package/lib/commands/comment.js +5 -0
  98. package/lib/commands/control.js +148 -0
  99. package/lib/commands/create.js +2 -2
  100. package/lib/commands/dev.js +185 -7
  101. package/lib/commands/doc-gate.js +336 -0
  102. package/lib/commands/doctor.js +156 -0
  103. package/lib/commands/explain.js +15 -0
  104. package/lib/commands/export.js +237 -0
  105. package/lib/commands/gate.js +192 -0
  106. package/lib/commands/hooks.js +242 -0
  107. package/lib/commands/inbox.js +118 -0
  108. package/lib/commands/init.js +598 -0
  109. package/lib/commands/insights.js +79 -0
  110. package/lib/commands/issue.js +12 -1
  111. package/lib/commands/issues.js +66 -0
  112. package/lib/commands/lint.js +5 -0
  113. package/lib/commands/list.js +2 -2
  114. package/lib/commands/merge.js +312 -0
  115. package/lib/commands/migrate.js +523 -0
  116. package/lib/commands/new.js +12 -0
  117. package/lib/commands/options.js +241 -0
  118. package/lib/commands/orient.js +13 -0
  119. package/lib/commands/orphans.js +5 -0
  120. package/lib/commands/patch.js +67 -0
  121. package/lib/commands/plan.js +436 -24
  122. package/lib/commands/preflight.js +211 -0
  123. package/lib/commands/prime.js +13 -0
  124. package/lib/commands/push.js +69 -2
  125. package/lib/commands/ready.js +2 -2
  126. package/lib/commands/recall.js +116 -0
  127. package/lib/commands/recap.js +61 -0
  128. package/lib/commands/recommend.js +22 -2
  129. package/lib/commands/release.js +91 -0
  130. package/lib/commands/remember.js +74 -0
  131. package/lib/commands/role.js +99 -0
  132. package/lib/commands/serve.js +581 -0
  133. package/lib/commands/setup.js +851 -979
  134. package/lib/commands/shepherd.js +436 -0
  135. package/lib/commands/ship.js +23 -1
  136. package/lib/commands/show.js +2 -2
  137. package/lib/commands/stage.js +192 -0
  138. package/lib/commands/stale.js +5 -0
  139. package/lib/commands/status.js +329 -11
  140. package/lib/commands/sync.js +34 -46
  141. package/lib/commands/team.js +15 -2
  142. package/lib/commands/test.js +58 -7
  143. package/lib/commands/update.js +2 -2
  144. package/lib/commands/upgrade.js +47 -0
  145. package/lib/commands/validate.js +56 -25
  146. package/lib/commands/worktree.js +308 -128
  147. package/lib/config-writer.js +202 -0
  148. package/lib/control-plane.js +236 -0
  149. package/lib/core/runtime-graph.js +946 -0
  150. package/lib/dep-guard/keyword-ripple.js +184 -0
  151. package/lib/deprecated-sync-cleanup.js +362 -0
  152. package/lib/detect-agent.js +2 -28
  153. package/lib/detect-worktree.js +42 -17
  154. package/lib/doc-gate/declaration.js +177 -0
  155. package/lib/doc-gate/detect.js +289 -0
  156. package/lib/doc-gate/gate.js +375 -0
  157. package/lib/doc-gate/okf-config.js +128 -0
  158. package/lib/doc-gate/okf.js +429 -0
  159. package/lib/docs-command.js +1161 -6
  160. package/lib/forge-issues.js +697 -0
  161. package/lib/forge-lock.js +262 -0
  162. package/lib/gate-events.js +193 -0
  163. package/lib/global-flags.js +74 -0
  164. package/lib/greptile-match.js +7 -63
  165. package/lib/harness-capability-matrix.js +380 -0
  166. package/lib/hook-global-installer.js +347 -0
  167. package/lib/hook-renderer.js +451 -0
  168. package/lib/inbox.js +391 -0
  169. package/lib/insights.js +397 -0
  170. package/lib/issue-adapter.js +156 -0
  171. package/lib/issue-backend.js +145 -0
  172. package/lib/issue-render.js +220 -0
  173. package/lib/issue-sync/authority.js +100 -0
  174. package/lib/issue-sync/github-pull.js +184 -0
  175. package/lib/issue-sync/import-primitives.js +98 -0
  176. package/lib/issue-sync/legacy-link-bridge.js +436 -0
  177. package/lib/issue-sync/link-store.js +292 -0
  178. package/lib/issue-sync/project-github.js +123 -0
  179. package/lib/issue-sync/reconcile.js +195 -0
  180. package/lib/issue-sync/schema.js +126 -0
  181. package/lib/kernel/backing-issue.js +305 -0
  182. package/lib/kernel/broker.js +1218 -0
  183. package/lib/kernel/cli-broker-factory.js +130 -0
  184. package/lib/kernel/conflict-signal.js +82 -0
  185. package/lib/kernel/evaluators.js +195 -0
  186. package/lib/kernel/fs-class.js +495 -0
  187. package/lib/kernel/issue-command-contract.js +559 -0
  188. package/lib/kernel/issue-id-resolver.js +186 -0
  189. package/lib/kernel/lease-enforcer.js +158 -0
  190. package/lib/kernel/migrations.js +333 -0
  191. package/lib/kernel/planning-buckets-schema.js +109 -0
  192. package/lib/kernel/projection-jsonl-writer.js +450 -0
  193. package/lib/kernel/readiness-model.js +329 -0
  194. package/lib/kernel/schema.js +356 -0
  195. package/lib/kernel/sqlite-driver.js +2504 -0
  196. package/lib/kernel/taxonomy-validator.js +394 -0
  197. package/lib/lefthook-check.js +8 -4
  198. package/lib/lefthook-wiring.js +413 -0
  199. package/lib/mcp-config-renderer.js +288 -0
  200. package/lib/memory/graphiti-mcp.js +106 -0
  201. package/lib/memory/router.js +387 -0
  202. package/lib/memory/typed-api.js +102 -0
  203. package/lib/memory-digest.js +195 -0
  204. package/lib/merge-rules.js +395 -0
  205. package/lib/migrate-dry-run.js +466 -0
  206. package/lib/orientation.js +863 -0
  207. package/lib/package-manager-remediation.js +103 -0
  208. package/lib/package-root.js +381 -0
  209. package/lib/patch-intent.js +890 -0
  210. package/lib/plugin-catalog.js +3 -4
  211. package/lib/plugin-manager.js +0 -5
  212. package/lib/pr-bundle.js +186 -0
  213. package/lib/pr-monitor/differ.js +195 -0
  214. package/lib/pr-monitor/events.js +0 -0
  215. package/lib/pr-monitor/gather.js +124 -0
  216. package/lib/pr-monitor/journal.js +299 -0
  217. package/lib/pr-monitor/monitor.js +146 -0
  218. package/lib/pr-monitor/render-sticky.js +157 -0
  219. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  220. package/lib/pr-monitor/watch.js +247 -0
  221. package/lib/pr-pull.js +1273 -0
  222. package/lib/pr-shepherd.js +494 -0
  223. package/lib/pr-state-validator.js +59 -0
  224. package/lib/preflight/gates.js +237 -0
  225. package/lib/preflight/runner.js +116 -0
  226. package/lib/project-discovery.js +0 -53
  227. package/lib/project-memory.js +166 -0
  228. package/lib/protected-path-manifest.js +281 -0
  229. package/lib/protected-state-surfaces.js +387 -0
  230. package/lib/release-readiness.js +2089 -0
  231. package/lib/reset.js +59 -45
  232. package/lib/review-adapter.js +68 -0
  233. package/lib/rules-sync.js +260 -0
  234. package/lib/runtime-health.js +332 -23
  235. package/lib/safety-config-renderer.js +268 -0
  236. package/lib/setup-action-log.js +1 -7
  237. package/lib/setup.js +27 -65
  238. package/lib/shell-utils.js +76 -6
  239. package/lib/skills-sync.js +330 -0
  240. package/lib/smart-status/conflicts.js +205 -0
  241. package/lib/smart-status/scoring.js +191 -0
  242. package/lib/status/beads-snapshot.js +145 -0
  243. package/lib/status/presenter.js +216 -0
  244. package/lib/status/snapshot.js +186 -0
  245. package/lib/sync-backend.js +202 -0
  246. package/lib/untrusted-content.js +52 -0
  247. package/lib/upgrade-safety.js +199 -0
  248. package/lib/workflow/enforce-stage.js +298 -47
  249. package/lib/workflow/stage-transition.js +115 -0
  250. package/lib/workflow/stages.js +30 -6
  251. package/lib/workflow/state-manager.js +159 -14
  252. package/lib/workflow/state.js +23 -1
  253. package/lib/workflow-profiles.js +17 -5
  254. package/package.json +46 -36
  255. package/rules/documentation.md +19 -0
  256. package/rules/kernel-tracking.md +26 -0
  257. package/rules/security.md +22 -0
  258. package/rules/tdd.md +20 -0
  259. package/rules/workflow.md +27 -0
  260. package/scripts/auto-backing-issue.js +47 -0
  261. package/scripts/beads-context.sh +165 -22
  262. package/scripts/beads-migrate-to-dolt.sh +7 -0
  263. package/scripts/beads-upgrade-smoke.sh +284 -0
  264. package/scripts/behavioral-judge.sh +115 -11
  265. package/scripts/benchmark.js +349 -63
  266. package/scripts/bootstrap-windows-tools.sh +78 -0
  267. package/scripts/branch-protection.js +2 -3
  268. package/scripts/check-agents.js +34 -137
  269. package/scripts/commitlint.js +3 -1
  270. package/scripts/conflict-detect.sh +3 -0
  271. package/scripts/dep-guard-analyze.js +52 -17
  272. package/scripts/dep-guard-keyword-ripple.js +29 -0
  273. package/scripts/dep-guard-render-review.js +86 -0
  274. package/scripts/dep-guard.sh +64 -232
  275. package/scripts/file-index.sh +3 -0
  276. package/scripts/forge-team/lib/claim.sh +34 -18
  277. package/scripts/forge-team/lib/dashboard.sh +61 -86
  278. package/scripts/forge-team/lib/epic.sh +99 -263
  279. package/scripts/forge-team/lib/hooks.sh +26 -28
  280. package/scripts/forge-team/lib/identity.sh +4 -4
  281. package/scripts/forge-team/lib/sync-github.sh +144 -47
  282. package/scripts/forge-team/lib/verify.sh +93 -83
  283. package/scripts/forge-team/lib/workload.sh +41 -65
  284. package/scripts/forge-team/tests/claim.test.sh +25 -19
  285. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  286. package/scripts/forge-team/tests/epic.test.sh +52 -71
  287. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  288. package/scripts/forge-team/tests/identity.test.sh +3 -3
  289. package/scripts/forge-team/tests/integration.test.sh +44 -66
  290. package/scripts/forge-team/tests/sync-github.test.sh +183 -79
  291. package/scripts/forge-team/tests/verify.test.sh +37 -46
  292. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  293. package/scripts/forge-team/tests/workload.test.sh +32 -66
  294. package/scripts/gen-command-manifest.js +153 -0
  295. package/scripts/gen-embedded-assets.mjs +129 -0
  296. package/scripts/install.ps1 +139 -0
  297. package/scripts/install.sh +268 -0
  298. package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
  299. package/scripts/lib/release-asset.mjs +84 -0
  300. package/scripts/parity-check.mjs +145 -0
  301. package/scripts/parity-check.test.mjs +58 -0
  302. package/scripts/pin-agentic-workflow-images.js +112 -0
  303. package/scripts/pr-coordinator.sh +3 -0
  304. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  305. package/scripts/preflight.sh +108 -0
  306. package/scripts/protected-state-check.js +104 -0
  307. package/scripts/smart-status-score.js +31 -0
  308. package/scripts/smart-status-sessions.js +51 -0
  309. package/scripts/smart-status.sh +117 -369
  310. package/scripts/spikes/config-race-bench.js +111 -0
  311. package/scripts/spikes/harness-capability-matrix.js +13 -0
  312. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  313. package/scripts/spikes/protected-path-manifest.js +20 -0
  314. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  315. package/scripts/sync-agent-skills.js +62 -0
  316. package/scripts/sync-agentic-workflow.js +48 -0
  317. package/scripts/sync-utils.sh +3 -0
  318. package/scripts/test-ci-shard.js +251 -0
  319. package/scripts/test-dashboard.js +188 -52
  320. package/scripts/test-full-suite.js +186 -0
  321. package/scripts/test-profile.js +278 -0
  322. package/scripts/test.js +302 -28
  323. package/scripts/validate.js +143 -0
  324. package/scripts/validate.sh +18 -1
  325. package/skills/claim-safety/SKILL.md +102 -0
  326. package/skills/claim-safety/evals/evals.json +46 -0
  327. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +46 -52
  328. package/skills/dev/evals/evals.json +50 -0
  329. package/skills/hermes-forge/SKILL.md +185 -0
  330. package/skills/hermes-forge/evals/evals.json +46 -0
  331. package/skills/issue-basics/SKILL.md +111 -0
  332. package/skills/issue-basics/evals/evals.json +46 -0
  333. package/skills/kernel/SKILL.md +166 -0
  334. package/skills/kernel/evals/evals.json +50 -0
  335. package/skills/memory/SKILL.md +102 -0
  336. package/skills/parallel-deep-research/SKILL.md +14 -11
  337. package/skills/parallel-deep-research/evals/evals.json +11 -27
  338. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +134 -159
  339. package/skills/plan/evals/evals.json +42 -0
  340. package/skills/research/SKILL.md +195 -0
  341. package/skills/research/evals/evals.json +42 -0
  342. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  343. package/skills/review/evals/evals.json +42 -0
  344. package/skills/rollback/SKILL.md +110 -0
  345. package/skills/rollback/evals/evals.json +46 -0
  346. package/skills/rollback/references/methods.md +204 -0
  347. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  348. package/skills/shepherd/SKILL.md +66 -0
  349. package/skills/shepherd/evals/evals.json +42 -0
  350. package/skills/ship/SKILL.md +251 -0
  351. package/skills/ship/evals/evals.json +42 -0
  352. package/skills/smith/SKILL.md +142 -0
  353. package/skills/smith/evals/evals.json +46 -0
  354. package/skills/smith/references/autonomy-and-gates.md +94 -0
  355. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  356. package/skills/sonarcloud/evals/evals.json +46 -0
  357. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  358. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  359. package/skills/status/SKILL.md +102 -0
  360. package/skills/status/evals/evals.json +50 -0
  361. package/skills/triage-ready/SKILL.md +121 -0
  362. package/skills/triage-ready/evals/evals.json +42 -0
  363. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  364. package/skills/validate/evals/evals.json +42 -0
  365. package/skills/verify/SKILL.md +299 -0
  366. package/skills/verify/evals/evals.json +50 -0
  367. package/.claude/commands/dev.md +0 -345
  368. package/.claude/commands/plan.md +0 -566
  369. package/.claude/commands/premerge.md +0 -186
  370. package/.claude/commands/research.md +0 -42
  371. package/.claude/commands/review.md +0 -451
  372. package/.claude/commands/rollback.md +0 -721
  373. package/.claude/commands/ship.md +0 -213
  374. package/.claude/commands/sonarcloud.md +0 -152
  375. package/.claude/commands/status.md +0 -90
  376. package/.claude/commands/validate.md +0 -288
  377. package/.claude/commands/verify.md +0 -269
  378. package/.claude/rules/workflow.md +0 -121
  379. package/.cline/workflows/dev.md +0 -342
  380. package/.cline/workflows/plan.md +0 -563
  381. package/.cline/workflows/premerge.md +0 -183
  382. package/.cline/workflows/research.md +0 -39
  383. package/.cline/workflows/review.md +0 -448
  384. package/.cline/workflows/rollback.md +0 -718
  385. package/.cline/workflows/ship.md +0 -210
  386. package/.cline/workflows/sonarcloud.md +0 -146
  387. package/.cline/workflows/status.md +0 -87
  388. package/.cline/workflows/validate.md +0 -285
  389. package/.cline/workflows/verify.md +0 -266
  390. package/.codex/config.toml +0 -11
  391. package/.codex/skills/dev/SKILL.md +0 -345
  392. package/.codex/skills/plan/SKILL.md +0 -566
  393. package/.codex/skills/premerge/SKILL.md +0 -186
  394. package/.codex/skills/research/SKILL.md +0 -42
  395. package/.codex/skills/review/SKILL.md +0 -451
  396. package/.codex/skills/rollback/SKILL.md +0 -721
  397. package/.codex/skills/ship/SKILL.md +0 -213
  398. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  399. package/.codex/skills/status/SKILL.md +0 -90
  400. package/.codex/skills/validate/SKILL.md +0 -288
  401. package/.codex/skills/verify/SKILL.md +0 -269
  402. package/.cursor/commands/dev.md +0 -342
  403. package/.cursor/commands/plan.md +0 -563
  404. package/.cursor/commands/premerge.md +0 -183
  405. package/.cursor/commands/research.md +0 -39
  406. package/.cursor/commands/review.md +0 -448
  407. package/.cursor/commands/ship.md +0 -210
  408. package/.cursor/commands/sonarcloud.md +0 -146
  409. package/.cursor/commands/status.md +0 -87
  410. package/.cursor/commands/validate.md +0 -285
  411. package/.cursor/commands/verify.md +0 -266
  412. package/.cursorrules +0 -149
  413. package/.github/prompts/premerge.prompt.md +0 -188
  414. package/.github/prompts/research.prompt.md +0 -44
  415. package/.github/prompts/rollback.prompt.md +0 -723
  416. package/.github/prompts/ship.prompt.md +0 -215
  417. package/.github/prompts/status.prompt.md +0 -92
  418. package/.github/prompts/verify.prompt.md +0 -271
  419. package/.github/workflows/beads-to-github.yml +0 -56
  420. package/.github/workflows/github-to-beads.yml +0 -97
  421. package/.kilocode/workflows/dev.md +0 -346
  422. package/.kilocode/workflows/plan.md +0 -567
  423. package/.kilocode/workflows/premerge.md +0 -187
  424. package/.kilocode/workflows/research.md +0 -43
  425. package/.kilocode/workflows/review.md +0 -452
  426. package/.kilocode/workflows/rollback.md +0 -722
  427. package/.kilocode/workflows/ship.md +0 -214
  428. package/.kilocode/workflows/sonarcloud.md +0 -150
  429. package/.kilocode/workflows/status.md +0 -91
  430. package/.kilocode/workflows/validate.md +0 -289
  431. package/.kilocode/workflows/verify.md +0 -270
  432. package/.opencode/commands/dev.md +0 -345
  433. package/.opencode/commands/plan.md +0 -566
  434. package/.opencode/commands/premerge.md +0 -186
  435. package/.opencode/commands/research.md +0 -42
  436. package/.opencode/commands/review.md +0 -451
  437. package/.opencode/commands/rollback.md +0 -721
  438. package/.opencode/commands/ship.md +0 -213
  439. package/.opencode/commands/sonarcloud.md +0 -149
  440. package/.opencode/commands/status.md +0 -90
  441. package/.opencode/commands/validate.md +0 -288
  442. package/.opencode/commands/verify.md +0 -269
  443. package/.roo/commands/dev.md +0 -346
  444. package/.roo/commands/plan.md +0 -567
  445. package/.roo/commands/premerge.md +0 -187
  446. package/.roo/commands/research.md +0 -43
  447. package/.roo/commands/review.md +0 -452
  448. package/.roo/commands/rollback.md +0 -722
  449. package/.roo/commands/ship.md +0 -214
  450. package/.roo/commands/sonarcloud.md +0 -150
  451. package/.roo/commands/status.md +0 -91
  452. package/.roo/commands/validate.md +0 -289
  453. package/.roo/commands/verify.md +0 -270
  454. package/docs/BEADS_GITHUB_SYNC.md +0 -255
  455. package/docs/GREPTILE_SETUP.md +0 -400
  456. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  457. package/docs/SETUP.md +0 -663
  458. package/docs/VALIDATION.md +0 -363
  459. package/lib/agents/cline.plugin.json +0 -29
  460. package/lib/agents/copilot.plugin.json +0 -24
  461. package/lib/agents/kilocode.plugin.json +0 -22
  462. package/lib/agents/opencode.plugin.json +0 -23
  463. package/lib/agents/roo.plugin.json +0 -30
  464. package/lib/beads-health-check.js +0 -143
  465. package/lib/commands/commands-reset.js +0 -147
  466. package/opencode.json +0 -67
  467. package/scripts/beads-context.test.js +0 -567
  468. package/scripts/github-beads-sync/comment.mjs +0 -64
  469. package/scripts/github-beads-sync/config.mjs +0 -148
  470. package/scripts/github-beads-sync/github-api.mjs +0 -131
  471. package/scripts/github-beads-sync/index.mjs +0 -332
  472. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  473. package/scripts/github-beads-sync/mapping.mjs +0 -78
  474. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  475. package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
  476. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  477. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  478. package/scripts/github-beads-sync.config.json +0 -26
  479. package/scripts/sync-commands.js +0 -600
@@ -0,0 +1,186 @@
1
+ 'use strict';
2
+
3
+ const { ISSUE_COMMAND_EXIT_CODES } = require('./issue-command-contract');
4
+
5
+ // Git-style short issue-id resolution (kernel 9556660b). The kernel mints full
6
+ // UUIDs, which are hostile to type by hand; this module resolves an UNAMBIGUOUS
7
+ // hex prefix (>= 6 chars, e.g. `forge show 9556660b`) to the stored full id.
8
+ // It runs ONCE at the broker boundary (runIssueOperation) so every issue
9
+ // subcommand — including the batch-close fan-out and the gate.issue_verify
10
+ // read-back — consumes the RESOLVED id. Resolution is deliberately conservative:
11
+ // * a full UUID never triggers a lookup (byte-identical fast path);
12
+ // * non-hex tokens (legacy `forge-*` / imported Beads ids) pass through untouched;
13
+ // * a prefix with ZERO matches passes through so the downstream not-found
14
+ // error is unchanged;
15
+ // * an EXACT stored id always wins over prefix expansion (a stored short
16
+ // hex id is never mis-expanded or rejected as "too short").
17
+
18
+ const MIN_ISSUE_ID_PREFIX_LENGTH = 6;
19
+ const MAX_AMBIGUOUS_CANDIDATES = 5;
20
+
21
+ const FULL_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
22
+ // A resolvable prefix: at least MIN hex chars, optionally continuing with hex
23
+ // and dashes (so a copied dashed UUID partial like `9556660b-a414` resolves too).
24
+ const HEX_PREFIX_PATTERN = /^[0-9a-f]{6}[0-9a-f-]*$/i;
25
+ // Pure hex but under the minimum length — candidate for the too-short guidance
26
+ // error (unless it exactly matches a stored id).
27
+ const SHORT_HEX_PATTERN = /^[0-9a-f]{1,5}$/i;
28
+
29
+ // The positional slots that carry issue ids, per kernel operation. Positions
30
+ // index the operation's positional tokens under the SAME rule every downstream
31
+ // consumer uses (firstPositionalArg / resolveDependencyEndpoints /
32
+ // buildCommentPayload): a positional is any token not starting with '-'. That
33
+ // keeps the resolved token exactly the one the downstream code reads — e.g.
34
+ // `claim --issue <id>` still resolves because `<id>` is positional 0 under this
35
+ // rule and flags.issue reads the same token. Operations absent from this map
36
+ // (list/ready/search/stats/create/...) take no issue id and pass through with
37
+ // no lookup — a search query is never mistaken for an id.
38
+ const OPERATION_ID_POSITIONS = Object.freeze({
39
+ update: Object.freeze([0]),
40
+ claim: Object.freeze([0]),
41
+ release: Object.freeze([0]),
42
+ comment: Object.freeze([0]),
43
+ close: Object.freeze([0]),
44
+ show: Object.freeze([0]),
45
+ owns: Object.freeze([0]),
46
+ children: Object.freeze([0]),
47
+ 'dep.add': Object.freeze([0, 1]),
48
+ 'dep.remove': Object.freeze([0, 1]),
49
+ });
50
+
51
+ // The `--issue=<id>` / `--blocks=<id>` =-joined flag forms carry ids inside a
52
+ // single token, invisible to the positional rule, so they are resolved by name.
53
+ const ID_FLAG_EQUALS_PATTERN = /^--(issue|blocks)=(.+)$/;
54
+
55
+ function tooShortError(token) {
56
+ return {
57
+ error: {
58
+ code: 'FORGE_ISSUE_ID_PREFIX_TOO_SHORT',
59
+ message: `Issue id prefix '${token}' is too short — use at least ${MIN_ISSUE_ID_PREFIX_LENGTH} hex characters `
60
+ + '(e.g. the first 8 of the id) or the full id.',
61
+ exitCode: ISSUE_COMMAND_EXIT_CODES.validation,
62
+ details: { prefix: token, min_length: MIN_ISSUE_ID_PREFIX_LENGTH },
63
+ },
64
+ };
65
+ }
66
+
67
+ function ambiguousError(token, candidates) {
68
+ const shown = candidates.slice(0, MAX_AMBIGUOUS_CANDIDATES);
69
+ const count = candidates.length > MAX_AMBIGUOUS_CANDIDATES
70
+ ? `${MAX_AMBIGUOUS_CANDIDATES}+`
71
+ : String(candidates.length);
72
+ const listing = shown
73
+ .map(candidate => `${candidate.id} (${candidate.title ?? 'untitled'})`)
74
+ .join('; ');
75
+ return {
76
+ error: {
77
+ code: 'FORGE_ISSUE_ID_AMBIGUOUS',
78
+ message: `Ambiguous issue id prefix '${token}' — matches ${count} issues: ${listing}. `
79
+ + 'Use a longer prefix or the full id.',
80
+ exitCode: ISSUE_COMMAND_EXIT_CODES.validation,
81
+ details: {
82
+ prefix: token,
83
+ candidates: shown.map(candidate => ({ id: candidate.id, title: candidate.title ?? null })),
84
+ },
85
+ },
86
+ };
87
+ }
88
+
89
+ // Resolve one id token. `lookup(prefix, limit)` returns candidate rows
90
+ // ({ id, title }) whose id starts with `prefix`, ordered by id ascending —
91
+ // which guarantees an exact match (shortest id sharing the prefix) sorts first
92
+ // and is never pushed out by the limit. Returns { id } on success (possibly the
93
+ // untouched input) or { error: { code, message, exitCode, details } }.
94
+ // Resolve a hex ref (prefix >= 6, or short hex < 6) against the store. Returns { id } —
95
+ // the resolved full id, or `ref` untouched when nothing matches — or an { error } for the
96
+ // too-short / ambiguous cases. `reportToken` is what user-facing errors name (the original
97
+ // input the caller typed, which may be a handle rather than the bare hex).
98
+ async function resolveHexToken(ref, lookup, reportToken = ref) {
99
+ const isPrefix = HEX_PREFIX_PATTERN.test(ref);
100
+ const isShortHex = !isPrefix && SHORT_HEX_PATTERN.test(ref);
101
+ if (!isPrefix && !isShortHex) return { id: ref };
102
+
103
+ const needle = ref.toLowerCase();
104
+ const candidates = (await lookup(needle, MAX_AMBIGUOUS_CANDIDATES + 1)) || [];
105
+ const exact = candidates.find(
106
+ candidate => candidate && typeof candidate.id === 'string' && candidate.id.toLowerCase() === needle,
107
+ );
108
+ if (exact) return { id: exact.id };
109
+ if (isShortHex) return tooShortError(reportToken);
110
+ if (candidates.length === 0) return { id: ref };
111
+ if (candidates.length === 1) return { id: candidates[0].id };
112
+ return ambiguousError(reportToken, candidates);
113
+ }
114
+
115
+ async function resolveIssueId(token, lookup) {
116
+ if (typeof token !== 'string' || token.length === 0) return { id: token };
117
+ if (FULL_UUID_PATTERN.test(token)) return { id: token };
118
+
119
+ // A display handle is `<title-slug>-<short-id>` (kernel 1db53c60); its short-id is the
120
+ // 8-char UUID prefix, so a handle ends in >= 8 trailing hex. Short legacy/Beads suffixes
121
+ // (`forge-2a3bc9`, 6 hex) are NOT handles and pass through untouched.
122
+ const handleSuffix = (/-([0-9a-f]{8,})$/i.exec(token) || [])[1] || null;
123
+
124
+ if (HEX_PREFIX_PATTERN.test(token) || SHORT_HEX_PATTERN.test(token)) {
125
+ const direct = await resolveHexToken(token, lookup);
126
+ // A handle whose slug is all hex letters (e.g. `facade-decade-fee-add-1a2b3c4d`) also
127
+ // matches the broad hex-prefix pattern; when the whole token matches nothing, retry
128
+ // with its trailing short-id so the handle still resolves (CodeRabbit, PR #335).
129
+ if (direct.id === token && handleSuffix && handleSuffix !== token) {
130
+ const viaHandle = await resolveHexToken(handleSuffix, lookup, token);
131
+ if (viaHandle.error || viaHandle.id !== handleSuffix) return viaHandle;
132
+ }
133
+ return direct;
134
+ }
135
+
136
+ // Non-hex token: a legacy/imported id (pass through so the store resolves it exactly),
137
+ // or a display handle. Prefer an exact whole-token match first so imported handle-shaped
138
+ // ids (`legacy-2a3bc9de`) still resolve; otherwise resolve by the handle's short-id.
139
+ if (!handleSuffix) return { id: token };
140
+ const whole = (await lookup(token.toLowerCase(), 2)) || [];
141
+ const wholeExact = whole.find(
142
+ candidate => candidate && typeof candidate.id === 'string' && candidate.id.toLowerCase() === token.toLowerCase(),
143
+ );
144
+ if (wholeExact) return { id: wholeExact.id };
145
+ return resolveHexToken(handleSuffix, lookup, token);
146
+ }
147
+
148
+ // Resolve every id-carrying token in `args` for `operation`. Returns
149
+ // { args: resolvedArgs } (a copy; the input is never mutated) or the first
150
+ // { error } encountered. Operations without id slots return their args
151
+ // unchanged and never invoke the lookup.
152
+ async function resolveIssueIdArgs(operation, args = [], lookup) {
153
+ const positions = OPERATION_ID_POSITIONS[operation];
154
+ if (!positions) return { args };
155
+
156
+ const resolved = [...args];
157
+ let positionalIndex = 0;
158
+ for (let index = 0; index < resolved.length; index += 1) {
159
+ const token = resolved[index];
160
+ if (typeof token !== 'string') continue;
161
+ if (token.startsWith('-')) {
162
+ const match = ID_FLAG_EQUALS_PATTERN.exec(token);
163
+ if (match) {
164
+ const result = await resolveIssueId(match[2], lookup);
165
+ if (result.error) return result;
166
+ resolved[index] = `--${match[1]}=${result.id}`;
167
+ }
168
+ continue;
169
+ }
170
+ if (positions.includes(positionalIndex)) {
171
+ const result = await resolveIssueId(token, lookup);
172
+ if (result.error) return result;
173
+ resolved[index] = result.id;
174
+ }
175
+ positionalIndex += 1;
176
+ }
177
+ return { args: resolved };
178
+ }
179
+
180
+ module.exports = {
181
+ MAX_AMBIGUOUS_CANDIDATES,
182
+ MIN_ISSUE_ID_PREFIX_LENGTH,
183
+ OPERATION_ID_POSITIONS,
184
+ resolveIssueId,
185
+ resolveIssueIdArgs,
186
+ };
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const { normalizePayload } = require('./evaluators');
4
+
5
+ // Default lease TTL (kernel d71a824b): how long a claim lease stays live before it
6
+ // is eligible for expiry-reclaim, when the caller does not pin an explicit
7
+ // expires_at (--expires). This is the ONE place the lease duration lives, reused by
8
+ // the CLI boundary (lib/forge-issues.js) to stamp a real expires_at so the dashboard's
9
+ // liveness layer has something to compute against. 8 hours: long enough to outlast a
10
+ // normal agent work session (no heartbeat/renewal exists yet, so a shorter window
11
+ // would risk expiring mid-work and letting another agent reclaim → silent double-work),
12
+ // short enough that a dead agent's lease frees within a working day. Callers that
13
+ // bypass the boundary (broker-direct, tests) carry no ttl and keep the historical
14
+ // null default (never expires).
15
+ const DEFAULT_LEASE_TTL_MS = 8 * 60 * 60 * 1000;
16
+
17
+ // The ECMAScript maximum representable Date, ±8.64e15 ms from the epoch. A time value
18
+ // outside this range makes `new Date(t)` an Invalid Date whose .toISOString() THROWS a
19
+ // RangeError. buildClaimRow calls computeLeaseExpiry synchronously on the claim path, so
20
+ // an uncaught throw here would fail the whole claim.
21
+ const MAX_DATE_MS = 8.64e15;
22
+
23
+ // Compute a lease expires_at from claimed_at (`now`) + a TTL in milliseconds. Returns
24
+ // null (never expires) for an absent/non-positive/non-finite ttl, an unparseable `now`,
25
+ // or a claimed_at+ttl that overflows the representable Date range (a fat-fingered
26
+ // FORGE_LEASE_TTL_MS) — so any of these degrades to the historical null default rather
27
+ // than throwing on the claim path. The result is always the canonical Date#toISOString
28
+ // form isValidExpiresAt requires (…mmmZ), so lexicographic comparison in isLeaseExpired
29
+ // stays chronological.
30
+ function computeLeaseExpiry(now, ttlMs) {
31
+ if (ttlMs === null || ttlMs === undefined) return null;
32
+ const ttl = Number(ttlMs);
33
+ if (!Number.isFinite(ttl) || ttl <= 0) return null;
34
+ const base = Date.parse(now);
35
+ if (Number.isNaN(base)) return null;
36
+ // Guard the sum BEFORE constructing the Date: an overflow (or a non-finite sum) would
37
+ // otherwise reach .toISOString() as an Invalid Date and throw RangeError.
38
+ const expiryMs = base + ttl;
39
+ if (!Number.isFinite(expiryMs) || Math.abs(expiryMs) > MAX_DATE_MS) return null;
40
+ return new Date(expiryMs).toISOString();
41
+ }
42
+
43
+ // Pure claim-lease enforcement helpers (task 9.5.10). No I/O — the broker
44
+ // performs all reads/writes and the DB partial UNIQUE index
45
+ // (idx_kernel_claims_active_lease) is the hard race-safe guarantee. These
46
+ // functions are the optimization + clean error path layered on top, mirroring
47
+ // the pure style of evaluators.js.
48
+ //
49
+ // Ownership model (Slice 1, deliberately conservative): a claim lease is a
50
+ // kernel_claims row with state='active'. A lease is "live" iff it is active and
51
+ // not expired. ANY claim.create against a live lease quarantines as
52
+ // 'claim_conflict' — there is NO silent same-owner renewal, because `actor` is
53
+ // not yet guaranteed distinct per concurrent agent and a renewal branch could
54
+ // let one agent silently steal another's live lease. Legitimate same-key
55
+ // retries never reach here: they are collapsed to duplicate replays by the
56
+ // idempotency-key path before claim planning runs.
57
+ //
58
+ // Claim scope/row/conflict all read from the SAME normalizePayload the evaluator
59
+ // uses to persist the event, so the lease can never describe a different issue
60
+ // than the accepted event/outbox.
61
+
62
+ // A claim lease's expires_at, if present, must be a UTC ISO-8601 timestamp with a
63
+ // trailing 'Z' AND a real calendar date — otherwise lexicographic comparison in
64
+ // isLeaseExpired is meaningless (e.g. 'zzz' sorts after any timestamp and would
65
+ // never expire; other junk sorts before and would look already expired). A
66
+ // null/absent value is valid and means "never expires". The broker quarantines
67
+ // any claim.create whose expires_at fails this check.
68
+ function isValidExpiresAt(value) {
69
+ if (value === null || value === undefined) return true;
70
+ if (typeof value !== 'string') return false;
71
+ const timestamp = Date.parse(value);
72
+ if (Number.isNaN(timestamp)) return false;
73
+ // Require the EXACT canonical Date#toISOString form (YYYY-MM-DDTHH:MM:SS.mmmZ):
74
+ // isLeaseExpired compares lexicographically, so a non-canonical spelling like
75
+ // '...00Z' would sort after the equal instant '...00.000Z' (Z > .) and a reclaim
76
+ // at the expiry instant would wrongly look live. The round-trip also rejects
77
+ // rollover dates Date.parse silently normalises (e.g. 2026-02-31 -> 2026-03-03).
78
+ return new Date(timestamp).toISOString() === value;
79
+ }
80
+
81
+ // A lease with no expires_at never expires. Timestamps are validated UTC ISO-8601
82
+ // with a trailing 'Z' (see isValidExpiresAt), so lexicographic comparison equals
83
+ // chronological comparison.
84
+ function isLeaseExpired(claim, now) {
85
+ if (!claim?.expires_at) return false;
86
+ return claim.expires_at <= now;
87
+ }
88
+
89
+ function buildClaimRow(event, now) {
90
+ const payload = normalizePayload(event);
91
+ return {
92
+ id: event.entity_id,
93
+ issue_id: payload.issue_id,
94
+ actor: event.actor,
95
+ state: 'active',
96
+ session_id: event.session_id ?? null,
97
+ worktree_id: event.worktree_id ?? null,
98
+ claimed_at: now,
99
+ // An explicit expires_at (from --expires) wins; otherwise derive from the event's
100
+ // lease_ttl_ms + claimed_at so the CLI boundary can stamp a real expiry. No ttl on
101
+ // the event → null (never expires), preserving the historical broker-direct default.
102
+ expires_at: payload.expires_at ?? computeLeaseExpiry(now, event.lease_ttl_ms),
103
+ };
104
+ }
105
+
106
+ // Decide how a claim.create event should be applied against the issue's current
107
+ // active claim (if any). Returns one of:
108
+ // { action: 'insert', claim } — no active claim
109
+ // { action: 'reclaim', supersede, claim } — active claim has expired
110
+ // { action: 'conflict' } — a live lease blocks the claim
111
+ function planClaimAcquisition({ event, activeClaim, now }) {
112
+ if (!activeClaim) {
113
+ return { action: 'insert', claim: buildClaimRow(event, now) };
114
+ }
115
+ if (isLeaseExpired(activeClaim, now)) {
116
+ return {
117
+ action: 'reclaim',
118
+ supersede: { claimId: activeClaim.id, toState: 'reclaimable' },
119
+ claim: buildClaimRow(event, now),
120
+ };
121
+ }
122
+ return { action: 'conflict' };
123
+ }
124
+
125
+ // Build a conflicts-table row for a quarantined claim conflict. Matches the
126
+ // shape produced by evaluators.buildConflict: claims are not revisioned, so the
127
+ // NOT NULL expected_revision / actual_revision columns default to 0. The
128
+ // human-meaningful detail (who owns the live lease, who attempted) lives in
129
+ // payload_json.
130
+ function buildClaimConflict(event, activeClaim, reason = 'claim_conflict') {
131
+ const payload = normalizePayload(event);
132
+ return {
133
+ entity_type: event.entity_type,
134
+ entity_id: event.entity_id,
135
+ expected_revision: 0,
136
+ actual_revision: 0,
137
+ status: 'quarantined',
138
+ reason,
139
+ payload_json: JSON.stringify({
140
+ reason,
141
+ issue_id: payload.issue_id ?? null,
142
+ attempted_by: event.actor,
143
+ current_owner: activeClaim ? activeClaim.actor : null,
144
+ current_claim_id: activeClaim ? activeClaim.id : null,
145
+ }),
146
+ created_at: event.created_at,
147
+ };
148
+ }
149
+
150
+ module.exports = {
151
+ DEFAULT_LEASE_TTL_MS,
152
+ buildClaimConflict,
153
+ buildClaimRow,
154
+ computeLeaseExpiry,
155
+ isLeaseExpired,
156
+ isValidExpiresAt,
157
+ planClaimAcquisition,
158
+ };
@@ -0,0 +1,333 @@
1
+ const { getKernelSchema, validateKernelSchema } = require('./schema');
2
+
3
+ function assertIdentifier(identifier, label) {
4
+ if (!/^[a-z][a-z0-9_]*$/.test(identifier)) {
5
+ throw new Error(`Invalid Kernel SQL ${label}: ${identifier}`);
6
+ }
7
+ }
8
+
9
+ function renderReference(reference) {
10
+ const parts = String(reference || '').split('.');
11
+ if (parts.length !== 2) {
12
+ throw new Error(`Invalid Kernel SQL reference: ${reference}`);
13
+ }
14
+ const [tableName, columnName] = parts;
15
+ assertIdentifier(tableName, 'reference table');
16
+ assertIdentifier(columnName, 'reference column');
17
+ return `REFERENCES kernel_${tableName}(${columnName})`;
18
+ }
19
+
20
+ function renderColumn(field) {
21
+ assertIdentifier(field.name, 'column');
22
+ const parts = [field.name, field.type];
23
+ if (field.notNull) parts.push('NOT NULL');
24
+ if (field.primaryKey) parts.push('PRIMARY KEY');
25
+ if (field.default !== undefined) parts.push(`DEFAULT ${field.default}`);
26
+ if (field.references) parts.push(renderReference(field.references));
27
+ return parts.join(' ');
28
+ }
29
+
30
+ function renderCreateTable(table) {
31
+ assertIdentifier(table.sqlName, 'table');
32
+ const columns = table.fields.map(field => ` ${renderColumn(field)}`).join(',\n');
33
+ return `CREATE TABLE IF NOT EXISTS ${table.sqlName} (\n${columns}\n);`;
34
+ }
35
+
36
+ function renderCreateIndex(table, index) {
37
+ assertIdentifier(index.name, 'index');
38
+ const unique = index.unique ? 'UNIQUE ' : '';
39
+ const columns = index.columns.map(column => {
40
+ assertIdentifier(column, 'index column');
41
+ return column;
42
+ }).join(', ');
43
+ return `CREATE ${unique}INDEX IF NOT EXISTS ${index.name} ON ${table.sqlName} (${columns});`;
44
+ }
45
+
46
+ function renderDropIndex(index) {
47
+ assertIdentifier(index.name, 'index');
48
+ return `DROP INDEX IF EXISTS ${index.name};`;
49
+ }
50
+
51
+ function renderDropTable(table) {
52
+ assertIdentifier(table.sqlName, 'table');
53
+ return `DROP TABLE IF EXISTS ${table.sqlName};`;
54
+ }
55
+
56
+ // Whole tables created by a LATER migration (not by the 001 initial schema). schema.js
57
+ // stays the full current schema; the named tables are filtered out of 001 so they are
58
+ // created exactly once by their dedicated migration (both on a fresh DB and, via the
59
+ // ledger, on an existing DB). KEEP IN SYNC with every new table-creating migration.
60
+ // memories → 005
61
+ const MIGRATION_ADDED_TABLES = ['memories'];
62
+
63
+ function getInitialKernelSchema() {
64
+ const schema = getKernelSchema();
65
+ // Columns added by a later migration MUST be excluded from the initial (001)
66
+ // CREATE TABLE, so a fresh DB doesn't create them before the ALTER … ADD COLUMN
67
+ // migration runs (else "duplicate column name"). schema.js stays the full current
68
+ // schema; this map mirrors which columns each later migration backfills.
69
+ // events.expected_revision → 002 ; issues.design/notes/assignee → 004 ;
70
+ // issues.created_by/closed_at/close_reason/metadata → 006 ;
71
+ // worktrees.issue_id/work_folder → 007
72
+ // KEEP IN SYNC: every new `ALTER TABLE … ADD COLUMN` migration MUST add its
73
+ // column(s) here, or a fresh DB will hit "duplicate column name" on setup.
74
+ const MIGRATION_ADDED_COLUMNS = {
75
+ events: ['expected_revision'],
76
+ issues: ['design', 'notes', 'assignee', 'created_by', 'closed_at', 'close_reason', 'metadata'],
77
+ worktrees: ['issue_id', 'work_folder'],
78
+ };
79
+ return {
80
+ ...schema,
81
+ tables: schema.tables
82
+ .filter(table => !MIGRATION_ADDED_TABLES.includes(table.name))
83
+ .map(table => {
84
+ const excluded = MIGRATION_ADDED_COLUMNS[table.name];
85
+ if (!excluded) return table;
86
+ return {
87
+ ...table,
88
+ fields: table.fields.filter(field => !excluded.includes(field.name)),
89
+ };
90
+ }),
91
+ };
92
+ }
93
+
94
+ function buildSchemaMigration(schema = getInitialKernelSchema()) {
95
+ validateKernelSchema(schema);
96
+
97
+ const apply = [];
98
+ for (const table of schema.tables) {
99
+ apply.push(renderCreateTable(table));
100
+ for (const tableIndex of table.indexes) {
101
+ apply.push(renderCreateIndex(table, tableIndex));
102
+ }
103
+ }
104
+
105
+ const rollback = [];
106
+ for (const table of [...schema.tables].reverse()) {
107
+ for (const tableIndex of [...table.indexes].reverse()) {
108
+ rollback.push(renderDropIndex(tableIndex));
109
+ }
110
+ rollback.push(renderDropTable(table));
111
+ }
112
+
113
+ return {
114
+ id: '001_kernel_schema',
115
+ apply,
116
+ rollback,
117
+ };
118
+ }
119
+
120
+ function buildEventExpectedRevisionMigration() {
121
+ return {
122
+ id: '002_kernel_events_expected_revision',
123
+ apply: ['ALTER TABLE kernel_events ADD COLUMN expected_revision INTEGER NOT NULL DEFAULT 0;'],
124
+ rollback: [
125
+ 'CREATE TABLE IF NOT EXISTS kernel_events_002_rollback (\n id TEXT NOT NULL PRIMARY KEY,\n entity_type TEXT NOT NULL,\n entity_id TEXT NOT NULL,\n event_type TEXT NOT NULL,\n idempotency_key TEXT NOT NULL,\n actor TEXT NOT NULL,\n origin TEXT NOT NULL,\n payload_json TEXT NOT NULL,\n created_at TEXT NOT NULL\n);',
126
+ 'INSERT INTO kernel_events_002_rollback (id, entity_type, entity_id, event_type, idempotency_key, actor, origin, payload_json, created_at) SELECT id, entity_type, entity_id, event_type, idempotency_key, actor, origin, payload_json, created_at FROM kernel_events;',
127
+ 'DROP TABLE kernel_events;',
128
+ 'ALTER TABLE kernel_events_002_rollback RENAME TO kernel_events;',
129
+ 'CREATE INDEX IF NOT EXISTS idx_kernel_events_entity_created ON kernel_events (entity_type, entity_id, created_at);',
130
+ 'CREATE UNIQUE INDEX IF NOT EXISTS idx_kernel_events_idempotency ON kernel_events (idempotency_key);',
131
+ ],
132
+ };
133
+ }
134
+
135
+ function buildClaimActiveLeaseMigration() {
136
+ // DB-enforced claim-lease invariant (task 9.5.10): at most one active claim
137
+ // per issue. A partial UNIQUE index is the only guarantee that survives
138
+ // multi-process races — a pre-read check cannot, because two writers can both
139
+ // read zero active claims before either commits. renderCreateIndex() has no
140
+ // partial-WHERE support, so this is a hand-written apply string (like 002).
141
+ return {
142
+ id: '003_kernel_claims_active_lease',
143
+ apply: [
144
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_kernel_claims_active_lease ON kernel_claims (issue_id) WHERE state = 'active';",
145
+ ],
146
+ rollback: [
147
+ 'DROP INDEX IF EXISTS idx_kernel_claims_active_lease;',
148
+ ],
149
+ };
150
+ }
151
+
152
+ // KAP-10 (acceptance/design/notes) + KAP-11 (assignee): three additive content
153
+ // columns on kernel_issues. acceptance_criteria/estimate already exist; these add
154
+ // the remaining authored fields plus a persistent assignee (distinct from the
155
+ // transient kernel_claims lease). Plain ADD COLUMNs — the 001 initial schema omits
156
+ // these columns (getInitialKernelSchema/MIGRATION_ADDED_COLUMNS) and the broker's
157
+ // per-migration ledger + guarded ADD COLUMN apply them exactly once.
158
+ function buildIssueContentFieldsMigration() {
159
+ return {
160
+ id: '004_kernel_issues_content_fields',
161
+ apply: [
162
+ 'ALTER TABLE kernel_issues ADD COLUMN design TEXT;',
163
+ 'ALTER TABLE kernel_issues ADD COLUMN notes TEXT;',
164
+ 'ALTER TABLE kernel_issues ADD COLUMN assignee TEXT;',
165
+ ],
166
+ rollback: [
167
+ 'ALTER TABLE kernel_issues DROP COLUMN assignee;',
168
+ 'ALTER TABLE kernel_issues DROP COLUMN notes;',
169
+ 'ALTER TABLE kernel_issues DROP COLUMN design;',
170
+ ],
171
+ };
172
+ }
173
+
174
+ // 005: the project-memory read-model table (kernel_memories). Rendered from the
175
+ // schema.js table definition so the DDL never drifts from the registry. Excluded from
176
+ // the 001 initial schema (MIGRATION_ADDED_TABLES), so a fresh DB creates it exactly
177
+ // once here and an existing DB picks it up through the broker's per-migration ledger.
178
+ // CREATE … IF NOT EXISTS keeps a re-run idempotent.
179
+ function buildMemoryProjectionMigration() {
180
+ const memories = getKernelSchema().tables.find(table => table.name === 'memories');
181
+ if (!memories) {
182
+ throw new Error('Kernel schema is missing the memories read-model table');
183
+ }
184
+ return {
185
+ id: '005_kernel_memories',
186
+ apply: [
187
+ renderCreateTable(memories),
188
+ ...memories.indexes.map(memoryIndex => renderCreateIndex(memories, memoryIndex)),
189
+ ],
190
+ rollback: [
191
+ ...[...memories.indexes].reverse().map(memoryIndex => renderDropIndex(memoryIndex)),
192
+ renderDropTable(memories),
193
+ ],
194
+ };
195
+ }
196
+
197
+ // 006: full-fidelity beads import. Four additive ADD COLUMNs on kernel_issues so no
198
+ // beads issue data is dropped — the author (created_by), the close timestamp
199
+ // (closed_at) and raw close reason (close_reason, distinct from the mapped terminal
200
+ // status), plus a verbatim JSON metadata blob (metadata). Plain ADD COLUMNs — the 001
201
+ // initial schema omits these columns (getInitialKernelSchema/MIGRATION_ADDED_COLUMNS)
202
+ // and the broker's per-migration ledger + guarded ADD COLUMN apply them exactly once.
203
+ function buildIssueFidelityColumnsMigration() {
204
+ return {
205
+ id: '006_kernel_issue_fidelity_columns',
206
+ apply: [
207
+ 'ALTER TABLE kernel_issues ADD COLUMN created_by TEXT;',
208
+ 'ALTER TABLE kernel_issues ADD COLUMN closed_at TEXT;',
209
+ 'ALTER TABLE kernel_issues ADD COLUMN close_reason TEXT;',
210
+ 'ALTER TABLE kernel_issues ADD COLUMN metadata TEXT;',
211
+ ],
212
+ rollback: [
213
+ 'ALTER TABLE kernel_issues DROP COLUMN metadata;',
214
+ 'ALTER TABLE kernel_issues DROP COLUMN close_reason;',
215
+ 'ALTER TABLE kernel_issues DROP COLUMN closed_at;',
216
+ 'ALTER TABLE kernel_issues DROP COLUMN created_by;',
217
+ ],
218
+ };
219
+ }
220
+
221
+ // 007: the kernel linkage backbone. Two additive ADD COLUMNs on kernel_worktrees so a
222
+ // worktree row records the issue it serves (issue_id) and the work-folder that issue
223
+ // owns (work_folder, repo-relative). This is what turns `kernel_worktrees` from a
224
+ // schema-only/never-written table into the queryable issue → worktree → work-folder
225
+ // chain, so orientation resolves the work-folder from the kernel instead of the
226
+ // "most-complete folder" filesystem heuristic. Plain ADD COLUMNs — the 001 initial
227
+ // schema omits these columns (getInitialKernelSchema/MIGRATION_ADDED_COLUMNS) and the
228
+ // broker's per-migration ledger + guarded ADD COLUMN apply them exactly once.
229
+ function buildWorktreeLinkageColumnsMigration() {
230
+ return {
231
+ id: '007_kernel_worktrees_linkage_columns',
232
+ apply: [
233
+ 'ALTER TABLE kernel_worktrees ADD COLUMN issue_id TEXT;',
234
+ 'ALTER TABLE kernel_worktrees ADD COLUMN work_folder TEXT;',
235
+ ],
236
+ rollback: [
237
+ 'ALTER TABLE kernel_worktrees DROP COLUMN work_folder;',
238
+ 'ALTER TABLE kernel_worktrees DROP COLUMN issue_id;',
239
+ ],
240
+ };
241
+ }
242
+
243
+ // 008: an FTS5 full-text index over kernel_memories, the shared token-efficient
244
+ // retrieval layer (Section B of the decision-store design). It is an EXTERNAL-CONTENT
245
+ // FTS5 table (content='kernel_memories', content_rowid='rowid'), so the index stores no
246
+ // second copy of the row — it reads the memory text back from kernel_memories by rowid.
247
+ // Three triggers keep the index synchronized with every INSERT/DELETE/UPDATE (the
248
+ // upsert-by-key path fires INSERT on first write and UPDATE on a key collision), and a
249
+ // one-time 'rebuild' backfills any rows written before the index existed (e.g. rows the
250
+ // insights engine wrote through recordMemory). The FTS virtual table lives OUTSIDE
251
+ // schema.js (it is not a kernel authority/read-model table), so it is created only here.
252
+ // CREATE … IF NOT EXISTS + CREATE TRIGGER IF NOT EXISTS keep a re-run idempotent, and the
253
+ // same DDL is reused by the driver's lazy ensureMemorySchema so a synchronous memory write
254
+ // stays indexed without a prior broker.initialize().
255
+ function memoryFtsDdl() {
256
+ return {
257
+ create: "CREATE VIRTUAL TABLE IF NOT EXISTS kernel_memories_fts USING fts5(\n key,\n value_json,\n tags_json,\n content='kernel_memories',\n content_rowid='rowid'\n);",
258
+ triggers: [
259
+ 'CREATE TRIGGER IF NOT EXISTS kernel_memories_ai AFTER INSERT ON kernel_memories BEGIN\n INSERT INTO kernel_memories_fts(rowid, key, value_json, tags_json)\n VALUES (new.rowid, new.key, new.value_json, new.tags_json);\nEND;',
260
+ "CREATE TRIGGER IF NOT EXISTS kernel_memories_ad AFTER DELETE ON kernel_memories BEGIN\n INSERT INTO kernel_memories_fts(kernel_memories_fts, rowid, key, value_json, tags_json)\n VALUES ('delete', old.rowid, old.key, old.value_json, old.tags_json);\nEND;",
261
+ "CREATE TRIGGER IF NOT EXISTS kernel_memories_au AFTER UPDATE ON kernel_memories BEGIN\n INSERT INTO kernel_memories_fts(kernel_memories_fts, rowid, key, value_json, tags_json)\n VALUES ('delete', old.rowid, old.key, old.value_json, old.tags_json);\n INSERT INTO kernel_memories_fts(rowid, key, value_json, tags_json)\n VALUES (new.rowid, new.key, new.value_json, new.tags_json);\nEND;",
262
+ ],
263
+ rebuild: "INSERT INTO kernel_memories_fts(kernel_memories_fts) VALUES('rebuild');",
264
+ };
265
+ }
266
+
267
+ function buildMemoryFtsMigration() {
268
+ const ddl = memoryFtsDdl();
269
+ return {
270
+ id: '008_kernel_memories_fts',
271
+ apply: [ddl.create, ...ddl.triggers, ddl.rebuild],
272
+ rollback: [
273
+ 'DROP TRIGGER IF EXISTS kernel_memories_au;',
274
+ 'DROP TRIGGER IF EXISTS kernel_memories_ad;',
275
+ 'DROP TRIGGER IF EXISTS kernel_memories_ai;',
276
+ 'DROP TABLE IF EXISTS kernel_memories_fts;',
277
+ ],
278
+ };
279
+ }
280
+
281
+ function validateKernelMigrations(migrations) {
282
+ const ids = new Set();
283
+ for (const migration of migrations) {
284
+ if (!migration || !migration.id) {
285
+ throw new Error('Kernel migration is missing an id');
286
+ }
287
+ if (ids.has(migration.id)) {
288
+ throw new Error(`Duplicate Kernel migration id: ${migration.id}`);
289
+ }
290
+ ids.add(migration.id);
291
+ if (!Array.isArray(migration.apply) || migration.apply.length === 0) {
292
+ throw new Error(`Kernel migration ${migration.id} has no apply statements`);
293
+ }
294
+ if (!Array.isArray(migration.rollback) || migration.rollback.length === 0) {
295
+ throw new Error(`Kernel migration ${migration.id} has no rollback statements`);
296
+ }
297
+ }
298
+
299
+ return true;
300
+ }
301
+
302
+ function buildKernelMigrationPlan(migrations = [
303
+ buildSchemaMigration(),
304
+ buildEventExpectedRevisionMigration(),
305
+ buildClaimActiveLeaseMigration(),
306
+ buildIssueContentFieldsMigration(),
307
+ buildMemoryProjectionMigration(),
308
+ buildIssueFidelityColumnsMigration(),
309
+ buildWorktreeLinkageColumnsMigration(),
310
+ buildMemoryFtsMigration(),
311
+ ]) {
312
+ validateKernelMigrations(migrations);
313
+
314
+ return {
315
+ migrations,
316
+ apply: migrations.flatMap(migration => migration.apply),
317
+ rollback: [...migrations].reverse().flatMap(migration => migration.rollback),
318
+ };
319
+ }
320
+
321
+ module.exports = {
322
+ buildClaimActiveLeaseMigration,
323
+ buildEventExpectedRevisionMigration,
324
+ buildIssueContentFieldsMigration,
325
+ buildIssueFidelityColumnsMigration,
326
+ buildKernelMigrationPlan,
327
+ buildMemoryFtsMigration,
328
+ buildMemoryProjectionMigration,
329
+ buildSchemaMigration,
330
+ buildWorktreeLinkageColumnsMigration,
331
+ memoryFtsDdl,
332
+ validateKernelMigrations,
333
+ };