forge-workflow 0.0.10 → 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 (454) 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 +150 -61
  8. package/CHANGELOG.md +681 -0
  9. package/CLAUDE.md +9 -118
  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 +461 -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/forge/TOOLCHAIN.md +670 -0
  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/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
  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 +121 -0
  81. package/lib/beads-sync-scaffold.js +25 -101
  82. package/lib/codex-skills.js +51 -1
  83. package/lib/commands/_issue.js +741 -77
  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 +17 -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 +0 -1
  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 +838 -972
  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 +158 -21
  140. package/lib/commands/sync.js +34 -46
  141. package/lib/commands/team.js +4 -1
  142. package/lib/commands/test.js +43 -27
  143. package/lib/commands/update.js +2 -2
  144. package/lib/commands/upgrade.js +47 -0
  145. package/lib/commands/validate.js +43 -18
  146. package/lib/commands/worktree.js +307 -100
  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 +2 -2
  151. package/lib/deprecated-sync-cleanup.js +362 -0
  152. package/lib/detect-agent.js +2 -28
  153. package/lib/detect-worktree.js +35 -9
  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 +382 -11
  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/kernel/backing-issue.js +305 -0
  174. package/lib/kernel/broker.js +1218 -0
  175. package/lib/kernel/cli-broker-factory.js +130 -0
  176. package/lib/kernel/conflict-signal.js +82 -0
  177. package/lib/kernel/evaluators.js +195 -0
  178. package/lib/kernel/fs-class.js +495 -0
  179. package/lib/kernel/issue-command-contract.js +559 -0
  180. package/lib/kernel/issue-id-resolver.js +186 -0
  181. package/lib/kernel/lease-enforcer.js +158 -0
  182. package/lib/kernel/migrations.js +333 -0
  183. package/lib/kernel/planning-buckets-schema.js +109 -0
  184. package/lib/kernel/projection-jsonl-writer.js +450 -0
  185. package/lib/kernel/readiness-model.js +329 -0
  186. package/lib/kernel/schema.js +356 -0
  187. package/lib/kernel/sqlite-driver.js +2504 -0
  188. package/lib/kernel/taxonomy-validator.js +394 -0
  189. package/lib/lefthook-check.js +3 -2
  190. package/lib/lefthook-wiring.js +413 -0
  191. package/lib/mcp-config-renderer.js +288 -0
  192. package/lib/memory/graphiti-mcp.js +106 -0
  193. package/lib/memory/router.js +387 -0
  194. package/lib/memory/typed-api.js +102 -0
  195. package/lib/memory-digest.js +195 -0
  196. package/lib/merge-rules.js +395 -0
  197. package/lib/migrate-dry-run.js +466 -0
  198. package/lib/orientation.js +863 -0
  199. package/lib/package-manager-remediation.js +103 -0
  200. package/lib/package-root.js +381 -0
  201. package/lib/patch-intent.js +890 -0
  202. package/lib/plugin-catalog.js +3 -4
  203. package/lib/plugin-manager.js +0 -5
  204. package/lib/pr-bundle.js +186 -0
  205. package/lib/pr-monitor/differ.js +195 -0
  206. package/lib/pr-monitor/events.js +0 -0
  207. package/lib/pr-monitor/gather.js +124 -0
  208. package/lib/pr-monitor/journal.js +299 -0
  209. package/lib/pr-monitor/monitor.js +146 -0
  210. package/lib/pr-monitor/render-sticky.js +157 -0
  211. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  212. package/lib/pr-monitor/watch.js +247 -0
  213. package/lib/pr-pull.js +1273 -0
  214. package/lib/pr-shepherd.js +494 -0
  215. package/lib/pr-state-validator.js +59 -0
  216. package/lib/preflight/gates.js +237 -0
  217. package/lib/preflight/runner.js +116 -0
  218. package/lib/project-discovery.js +0 -53
  219. package/lib/project-memory.js +99 -497
  220. package/lib/protected-path-manifest.js +281 -0
  221. package/lib/protected-state-surfaces.js +387 -0
  222. package/lib/release-readiness.js +2089 -0
  223. package/lib/reset.js +59 -45
  224. package/lib/review-adapter.js +68 -0
  225. package/lib/rules-sync.js +260 -0
  226. package/lib/runtime-health.js +241 -20
  227. package/lib/safety-config-renderer.js +268 -0
  228. package/lib/setup-action-log.js +1 -7
  229. package/lib/setup.js +27 -65
  230. package/lib/shell-utils.js +76 -6
  231. package/lib/skills-sync.js +330 -0
  232. package/lib/smart-status/scoring.js +17 -3
  233. package/lib/status/beads-snapshot.js +45 -2
  234. package/lib/status/presenter.js +169 -18
  235. package/lib/status/snapshot.js +186 -0
  236. package/lib/sync-backend.js +202 -0
  237. package/lib/untrusted-content.js +52 -0
  238. package/lib/upgrade-safety.js +199 -0
  239. package/lib/workflow/enforce-stage.js +296 -47
  240. package/lib/workflow/stage-transition.js +115 -0
  241. package/lib/workflow/stages.js +30 -6
  242. package/lib/workflow/state-manager.js +11 -22
  243. package/lib/workflow/state.js +23 -1
  244. package/lib/workflow-profiles.js +17 -5
  245. package/package.json +37 -35
  246. package/rules/documentation.md +19 -0
  247. package/rules/kernel-tracking.md +26 -0
  248. package/rules/security.md +22 -0
  249. package/rules/tdd.md +20 -0
  250. package/rules/workflow.md +27 -0
  251. package/scripts/auto-backing-issue.js +47 -0
  252. package/scripts/beads-context.sh +81 -57
  253. package/scripts/beads-upgrade-smoke.sh +24 -3
  254. package/scripts/bootstrap-windows-tools.sh +78 -0
  255. package/scripts/branch-protection.js +2 -3
  256. package/scripts/check-agents.js +34 -137
  257. package/scripts/commitlint.js +3 -1
  258. package/scripts/conflict-detect.sh +3 -0
  259. package/scripts/dep-guard.sh +22 -3
  260. package/scripts/file-index.sh +3 -0
  261. package/scripts/forge-team/lib/claim.sh +34 -18
  262. package/scripts/forge-team/lib/dashboard.sh +61 -86
  263. package/scripts/forge-team/lib/epic.sh +99 -263
  264. package/scripts/forge-team/lib/hooks.sh +26 -28
  265. package/scripts/forge-team/lib/identity.sh +4 -4
  266. package/scripts/forge-team/lib/sync-github.sh +49 -84
  267. package/scripts/forge-team/lib/verify.sh +93 -83
  268. package/scripts/forge-team/lib/workload.sh +41 -65
  269. package/scripts/forge-team/tests/claim.test.sh +25 -19
  270. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  271. package/scripts/forge-team/tests/epic.test.sh +52 -71
  272. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  273. package/scripts/forge-team/tests/identity.test.sh +3 -3
  274. package/scripts/forge-team/tests/integration.test.sh +44 -66
  275. package/scripts/forge-team/tests/sync-github.test.sh +50 -83
  276. package/scripts/forge-team/tests/verify.test.sh +37 -46
  277. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  278. package/scripts/forge-team/tests/workload.test.sh +32 -66
  279. package/scripts/gen-command-manifest.js +153 -0
  280. package/scripts/gen-embedded-assets.mjs +129 -0
  281. package/scripts/install.ps1 +139 -0
  282. package/scripts/install.sh +268 -0
  283. package/scripts/lib/release-asset.mjs +84 -0
  284. package/scripts/parity-check.mjs +145 -0
  285. package/scripts/parity-check.test.mjs +58 -0
  286. package/scripts/pin-agentic-workflow-images.js +112 -0
  287. package/scripts/pr-coordinator.sh +3 -0
  288. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  289. package/scripts/preflight.sh +21 -94
  290. package/scripts/protected-state-check.js +104 -0
  291. package/scripts/smart-status.sh +60 -57
  292. package/scripts/spikes/config-race-bench.js +111 -0
  293. package/scripts/spikes/harness-capability-matrix.js +13 -0
  294. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  295. package/scripts/spikes/protected-path-manifest.js +20 -0
  296. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  297. package/scripts/sync-agent-skills.js +62 -0
  298. package/scripts/sync-utils.sh +3 -0
  299. package/scripts/test-ci-shard.js +13 -6
  300. package/scripts/test.js +95 -12
  301. package/skills/claim-safety/SKILL.md +102 -0
  302. package/skills/claim-safety/evals/evals.json +46 -0
  303. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
  304. package/skills/dev/evals/evals.json +50 -0
  305. package/skills/hermes-forge/SKILL.md +185 -0
  306. package/skills/hermes-forge/evals/evals.json +46 -0
  307. package/skills/issue-basics/SKILL.md +111 -0
  308. package/skills/issue-basics/evals/evals.json +46 -0
  309. package/skills/kernel/SKILL.md +166 -0
  310. package/skills/kernel/evals/evals.json +50 -0
  311. package/skills/memory/SKILL.md +102 -0
  312. package/skills/parallel-deep-research/SKILL.md +14 -11
  313. package/skills/parallel-deep-research/evals/evals.json +11 -27
  314. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
  315. package/skills/plan/evals/evals.json +42 -0
  316. package/skills/research/SKILL.md +195 -0
  317. package/skills/research/evals/evals.json +42 -0
  318. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  319. package/skills/review/evals/evals.json +42 -0
  320. package/skills/rollback/SKILL.md +110 -0
  321. package/skills/rollback/evals/evals.json +46 -0
  322. package/skills/rollback/references/methods.md +204 -0
  323. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  324. package/skills/shepherd/SKILL.md +66 -0
  325. package/skills/shepherd/evals/evals.json +42 -0
  326. package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
  327. package/skills/ship/evals/evals.json +42 -0
  328. package/skills/smith/SKILL.md +142 -0
  329. package/skills/smith/evals/evals.json +46 -0
  330. package/skills/smith/references/autonomy-and-gates.md +94 -0
  331. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  332. package/skills/sonarcloud/evals/evals.json +46 -0
  333. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  334. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  335. package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
  336. package/skills/status/evals/evals.json +50 -0
  337. package/skills/triage-ready/SKILL.md +121 -0
  338. package/skills/triage-ready/evals/evals.json +42 -0
  339. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  340. package/skills/validate/evals/evals.json +42 -0
  341. package/skills/verify/SKILL.md +299 -0
  342. package/skills/verify/evals/evals.json +50 -0
  343. package/.claude/commands/dev.md +0 -345
  344. package/.claude/commands/plan.md +0 -566
  345. package/.claude/commands/premerge.md +0 -186
  346. package/.claude/commands/research.md +0 -42
  347. package/.claude/commands/review.md +0 -451
  348. package/.claude/commands/rollback.md +0 -721
  349. package/.claude/commands/ship.md +0 -213
  350. package/.claude/commands/sonarcloud.md +0 -152
  351. package/.claude/commands/status.md +0 -90
  352. package/.claude/commands/validate.md +0 -288
  353. package/.claude/commands/verify.md +0 -269
  354. package/.claude/rules/workflow.md +0 -121
  355. package/.cline/workflows/dev.md +0 -342
  356. package/.cline/workflows/plan.md +0 -563
  357. package/.cline/workflows/premerge.md +0 -183
  358. package/.cline/workflows/research.md +0 -39
  359. package/.cline/workflows/review.md +0 -448
  360. package/.cline/workflows/rollback.md +0 -718
  361. package/.cline/workflows/ship.md +0 -210
  362. package/.cline/workflows/sonarcloud.md +0 -146
  363. package/.cline/workflows/status.md +0 -87
  364. package/.cline/workflows/validate.md +0 -285
  365. package/.cline/workflows/verify.md +0 -266
  366. package/.codex/config.toml +0 -11
  367. package/.codex/skills/dev/SKILL.md +0 -345
  368. package/.codex/skills/plan/SKILL.md +0 -566
  369. package/.codex/skills/premerge/SKILL.md +0 -186
  370. package/.codex/skills/research/SKILL.md +0 -42
  371. package/.codex/skills/review/SKILL.md +0 -451
  372. package/.codex/skills/rollback/SKILL.md +0 -721
  373. package/.codex/skills/ship/SKILL.md +0 -213
  374. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  375. package/.codex/skills/status/SKILL.md +0 -90
  376. package/.codex/skills/validate/SKILL.md +0 -288
  377. package/.codex/skills/verify/SKILL.md +0 -269
  378. package/.cursor/commands/dev.md +0 -342
  379. package/.cursor/commands/plan.md +0 -563
  380. package/.cursor/commands/premerge.md +0 -183
  381. package/.cursor/commands/research.md +0 -39
  382. package/.cursor/commands/review.md +0 -448
  383. package/.cursor/commands/ship.md +0 -210
  384. package/.cursor/commands/sonarcloud.md +0 -146
  385. package/.cursor/commands/status.md +0 -87
  386. package/.cursor/commands/validate.md +0 -285
  387. package/.cursor/commands/verify.md +0 -266
  388. package/.cursorrules +0 -149
  389. package/.github/prompts/premerge.prompt.md +0 -188
  390. package/.github/prompts/research.prompt.md +0 -44
  391. package/.github/prompts/rollback.prompt.md +0 -723
  392. package/.github/prompts/verify.prompt.md +0 -271
  393. package/.github/workflows/beads-to-github.yml +0 -89
  394. package/.github/workflows/github-to-beads.yml +0 -100
  395. package/.kilocode/workflows/dev.md +0 -346
  396. package/.kilocode/workflows/plan.md +0 -567
  397. package/.kilocode/workflows/premerge.md +0 -187
  398. package/.kilocode/workflows/research.md +0 -43
  399. package/.kilocode/workflows/review.md +0 -452
  400. package/.kilocode/workflows/rollback.md +0 -722
  401. package/.kilocode/workflows/ship.md +0 -214
  402. package/.kilocode/workflows/sonarcloud.md +0 -150
  403. package/.kilocode/workflows/status.md +0 -91
  404. package/.kilocode/workflows/validate.md +0 -289
  405. package/.kilocode/workflows/verify.md +0 -270
  406. package/.opencode/commands/dev.md +0 -345
  407. package/.opencode/commands/plan.md +0 -566
  408. package/.opencode/commands/premerge.md +0 -186
  409. package/.opencode/commands/research.md +0 -42
  410. package/.opencode/commands/review.md +0 -451
  411. package/.opencode/commands/rollback.md +0 -721
  412. package/.opencode/commands/ship.md +0 -213
  413. package/.opencode/commands/sonarcloud.md +0 -149
  414. package/.opencode/commands/status.md +0 -90
  415. package/.opencode/commands/validate.md +0 -288
  416. package/.opencode/commands/verify.md +0 -269
  417. package/.roo/commands/dev.md +0 -346
  418. package/.roo/commands/plan.md +0 -567
  419. package/.roo/commands/premerge.md +0 -187
  420. package/.roo/commands/research.md +0 -43
  421. package/.roo/commands/review.md +0 -452
  422. package/.roo/commands/rollback.md +0 -722
  423. package/.roo/commands/ship.md +0 -214
  424. package/.roo/commands/sonarcloud.md +0 -150
  425. package/.roo/commands/status.md +0 -91
  426. package/.roo/commands/validate.md +0 -289
  427. package/.roo/commands/verify.md +0 -270
  428. package/docs/BEADS_GITHUB_SYNC.md +0 -281
  429. package/docs/GREPTILE_SETUP.md +0 -400
  430. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  431. package/docs/SETUP.md +0 -663
  432. package/docs/VALIDATION.md +0 -363
  433. package/lib/agents/cline.plugin.json +0 -29
  434. package/lib/agents/copilot.plugin.json +0 -24
  435. package/lib/agents/kilocode.plugin.json +0 -22
  436. package/lib/agents/opencode.plugin.json +0 -23
  437. package/lib/agents/roo.plugin.json +0 -30
  438. package/lib/beads-bootstrap.js +0 -225
  439. package/lib/beads-health-check.js +0 -188
  440. package/lib/commands/commands-reset.js +0 -147
  441. package/opencode.json +0 -67
  442. package/scripts/beads-context.test.js +0 -584
  443. package/scripts/github-beads-sync/comment.mjs +0 -64
  444. package/scripts/github-beads-sync/config.mjs +0 -148
  445. package/scripts/github-beads-sync/github-api.mjs +0 -131
  446. package/scripts/github-beads-sync/index.mjs +0 -356
  447. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  448. package/scripts/github-beads-sync/mapping.mjs +0 -132
  449. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  450. package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
  451. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  452. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  453. package/scripts/github-beads-sync.config.json +0 -26
  454. 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
+ };