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,1042 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const BEADS_EXPORT_FILES = ['issues.jsonl', 'comments.jsonl', 'dependencies.jsonl'];
7
+ const FORGE_PROJECTION_METADATA_KEY = 'forge_projection';
8
+ // Issue fields with no dedicated Kernel column, dropped only after being surfaced as a gap.
9
+ // labels/acceptance_criteria (D18 TEXT columns), created_by/metadata (migration 006),
10
+ // assignee/design (migration 004), and owner (carried onto assignee/created_by/metadata) all
11
+ // moved OUT of this set: they are mapped, not dropped. The list stays as the extension point
12
+ // for any future beads field the Kernel cannot yet represent.
13
+ const UNSUPPORTED_ISSUE_FIELDS = Object.freeze([]);
14
+ const PRESERVED_FIELDS = Object.freeze([
15
+ 'issues.id',
16
+ 'issues.title',
17
+ 'issues.body',
18
+ 'issues.notes',
19
+ 'issues.type',
20
+ 'issues.status',
21
+ 'issues.priority',
22
+ 'issues.labels',
23
+ 'issues.acceptance_criteria',
24
+ 'issues.created_at',
25
+ 'issues.updated_at',
26
+ 'issues.design',
27
+ 'issues.assignee',
28
+ 'issues.owner',
29
+ 'issues.created_by',
30
+ 'issues.metadata',
31
+ 'dependencies.parent-child',
32
+ 'dependencies.blocks',
33
+ 'comments.body',
34
+ 'comments.actor',
35
+ 'comments.created_at',
36
+ 'events.close_reason',
37
+ 'events.closed_at',
38
+ // Legacy activity log: events.jsonl lifecycle events and interactions.jsonl agent
39
+ // interaction/memory records both land in kernel_events (origin=beads_import).
40
+ 'events.activity',
41
+ 'interactions.activity',
42
+ ]);
43
+
44
+ // D18 taxonomy collapse (see docs/reference/KERNEL_TAXONOMY_VALIDATION.md). Legacy Beads
45
+ // issue_types that are no longer Kernel types become labels on a `task`; the canonical
46
+ // types are epic/task/bug/decision, enforced by lib/kernel/taxonomy-validator. Normalizing
47
+ // here — on the adapter boundary — keeps legacy imports from being rejected by the D18
48
+ // validation layer downstream.
49
+ const LEGACY_TYPE_ALIASES = Object.freeze({
50
+ feature: 'task',
51
+ story: 'task',
52
+ chore: 'task',
53
+ spike: 'task',
54
+ });
55
+ // Legacy Beads `closed` collapses to the terminal `done`, unless the close reason signals an
56
+ // abandonment, in which case it maps to `cancelled`. Both are terminal Kernel statuses.
57
+ const CANCELLED_CLOSE_REASON = /\b(cancel|won'?t.?fix|wontfix|abandon|obsolet|duplicat|invalid|not.?planned|supersed|declin)/i;
58
+
59
+ function parseJsonl(content = '', file = 'jsonl') {
60
+ return String(content)
61
+ .split(/\r?\n/)
62
+ .map(line => line.trim())
63
+ .filter(Boolean)
64
+ .map((line, index) => {
65
+ try {
66
+ return JSON.parse(line);
67
+ } catch (error) {
68
+ throw new Error(`Invalid JSONL in ${file} at line ${index + 1}: ${error.message}`);
69
+ }
70
+ });
71
+ }
72
+
73
+ function stringifyJsonl(records = []) {
74
+ if (!Array.isArray(records) || records.length === 0) {
75
+ return '';
76
+ }
77
+
78
+ return `${records.map(record => JSON.stringify(record)).join('\n')}\n`;
79
+ }
80
+
81
+ // Read one beads sidecar file, tolerant of the SPLIT layout a real `.beads/` store uses: the
82
+ // backup export (issues/comments/dependencies/labels/events) lives under `.beads/backup/`, while
83
+ // the live interactions/memory log lives at `.beads/interactions.jsonl`. So neither `.beads/` nor
84
+ // `.beads/backup/` alone holds every file. We read from `beadsDir` first (it always wins), then
85
+ // fall back to a `backup/` subdir and the parent dir — so pointing `--source` at EITHER location
86
+ // picks up both events and interactions instead of silently half-reading (a missed memory sidecar
87
+ // is exactly the data-loss class this migrater exists to close).
88
+ function readBeadsSidecar(beadsDir, file) {
89
+ const candidates = [
90
+ path.join(beadsDir, file),
91
+ path.join(beadsDir, 'backup', file),
92
+ path.join(path.dirname(beadsDir), file),
93
+ ];
94
+ const found = candidates.find(candidate => fs.existsSync(candidate));
95
+ return found ? parseJsonl(fs.readFileSync(found, 'utf8'), path.basename(found)) : [];
96
+ }
97
+
98
+ function loadBeadsSnapshotFromDirectory(beadsDir) {
99
+ if (!beadsDir || typeof beadsDir !== 'string') {
100
+ throw new TypeError('beadsDir must be a directory path');
101
+ }
102
+
103
+ return {
104
+ beadsDir,
105
+ issues: readBeadsSidecar(beadsDir, 'issues.jsonl'),
106
+ comments: readBeadsSidecar(beadsDir, 'comments.jsonl'),
107
+ dependencies: readBeadsSidecar(beadsDir, 'dependencies.jsonl'),
108
+ events: readBeadsSidecar(beadsDir, 'events.jsonl'),
109
+ interactions: readBeadsSidecar(beadsDir, 'interactions.jsonl'),
110
+ labels: readBeadsSidecar(beadsDir, 'labels.jsonl'),
111
+ };
112
+ }
113
+
114
+ function normalizeStatus(status) {
115
+ const normalized = String(status || 'open').trim().toLowerCase().replace(/[-\s]+/g, '_');
116
+ return normalized || 'open';
117
+ }
118
+
119
+ // Map a Beads issue_type to a canonical Kernel type, returning the alias label to preserve
120
+ // when a legacy type (feature/story/chore/spike) collapses to `task`.
121
+ function normalizeKernelType(beadsType) {
122
+ const raw = String(beadsType || '').trim().toLowerCase();
123
+ if (Object.hasOwn(LEGACY_TYPE_ALIASES, raw)) {
124
+ return { type: LEGACY_TYPE_ALIASES[raw], aliasLabel: raw };
125
+ }
126
+ return { type: raw || 'task', aliasLabel: null };
127
+ }
128
+
129
+ // Collapse the legacy Beads `closed` status onto a terminal Kernel status before it reaches
130
+ // the D18 validation layer. Non-`closed` statuses pass through their normalized form.
131
+ function normalizeKernelStatus(issue) {
132
+ const status = normalizeStatus(issue.status);
133
+ if (status !== 'closed') {
134
+ return status;
135
+ }
136
+ return CANCELLED_CLOSE_REASON.test(String(issue.close_reason || '')) ? 'cancelled' : 'done';
137
+ }
138
+
139
+ // Project terminal Kernel statuses back onto the Beads `closed` vocabulary on export so a
140
+ // Beads -> Kernel -> Beads round-trip stays identity (Beads has no done/cancelled status).
141
+ function beadsStatusForKernelStatus(status) {
142
+ return status === 'done' || status === 'cancelled' ? 'closed' : status;
143
+ }
144
+
145
+ // Serialize a Beads labels array into the Kernel `labels` TEXT column as a JSON array string,
146
+ // deduping and dropping blanks. Returns null when there is nothing to store.
147
+ function serializeLabels(labels) {
148
+ if (!Array.isArray(labels) || labels.length === 0) {
149
+ return null;
150
+ }
151
+ const unique = Array.from(new Set(
152
+ labels.filter(label => label !== null && label !== undefined && String(label).trim() !== ''),
153
+ ));
154
+ return unique.length > 0 ? JSON.stringify(unique) : null;
155
+ }
156
+
157
+ // Inverse of serializeLabels for the export path. Tolerates already-array values, a null/empty
158
+ // column, and malformed JSON (never throws — JSON.parse(null) would).
159
+ function deserializeLabels(value) {
160
+ if (Array.isArray(value)) {
161
+ return value;
162
+ }
163
+ if (typeof value !== 'string' || value.trim() === '') {
164
+ return [];
165
+ }
166
+ try {
167
+ const parsed = JSON.parse(value);
168
+ return Array.isArray(parsed) ? parsed : [];
169
+ } catch (_error) { // NOSONAR S2486 — malformed labels column is non-fatal; fall back to no labels
170
+ return [];
171
+ }
172
+ }
173
+
174
+ // Serialize Beads acceptance_criteria into the Kernel TEXT column: strings are stored as-is,
175
+ // structured values are JSON-encoded. Returns null when absent.
176
+ function serializeAcceptanceCriteria(value) {
177
+ if (value === null || value === undefined || value === '') {
178
+ return null;
179
+ }
180
+ return typeof value === 'string' ? value : JSON.stringify(value);
181
+ }
182
+
183
+ // Inverse of serializeAcceptanceCriteria for the export path: a JSON-encoded array/object is
184
+ // parsed back to its structured shape so a round-trip restores the original value; plain prose
185
+ // strings (which never start with [ or {) pass through unchanged.
186
+ function deserializeAcceptanceCriteria(value) {
187
+ if (typeof value !== 'string') {
188
+ return value;
189
+ }
190
+ const trimmed = value.trim();
191
+ if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
192
+ try {
193
+ return JSON.parse(value);
194
+ } catch (_error) { // NOSONAR S2486 — bracket-prefixed but invalid JSON; treat as a plain string
195
+ return value;
196
+ }
197
+ }
198
+ return value;
199
+ }
200
+
201
+ function normalizePriority(priority) {
202
+ if (typeof priority === 'number' && Number.isFinite(priority)) {
203
+ return {
204
+ label: `P${priority}`,
205
+ rank: priority,
206
+ beads: priority,
207
+ };
208
+ }
209
+
210
+ const raw = String(priority ?? 'P2').trim().toUpperCase();
211
+ const match = raw.match(/^P?(\d+)$/);
212
+ if (!match) {
213
+ return {
214
+ label: raw || 'P2',
215
+ rank: 2,
216
+ beads: 2,
217
+ };
218
+ }
219
+
220
+ const rank = Number.parseInt(match[1], 10);
221
+ return {
222
+ label: `P${rank}`,
223
+ rank,
224
+ beads: rank,
225
+ };
226
+ }
227
+
228
+ function safeIdPart(value) {
229
+ const input = String(value || 'unknown').toLowerCase();
230
+ const parts = [];
231
+ let pendingSeparator = false;
232
+
233
+ for (const char of input) {
234
+ const code = char.charCodeAt(0);
235
+ const isAsciiLetter = code >= 97 && code <= 122;
236
+ const isDigit = code >= 48 && code <= 57;
237
+ if (isAsciiLetter || isDigit) {
238
+ if (pendingSeparator && parts.length > 0) {
239
+ parts.push('-');
240
+ }
241
+ parts.push(char);
242
+ pendingSeparator = false;
243
+ } else {
244
+ pendingSeparator = parts.length > 0;
245
+ }
246
+ }
247
+
248
+ return parts.join('') || 'unknown';
249
+ }
250
+
251
+ function encodedIdPart(value) {
252
+ return Buffer.from(String(value ?? ''), 'utf8').toString('hex') || '00';
253
+ }
254
+
255
+ function uniqueBy(records, keyFn) {
256
+ const seen = new Set();
257
+ const result = [];
258
+ for (const record of records) {
259
+ const key = keyFn(record);
260
+ if (!seen.has(key)) {
261
+ seen.add(key);
262
+ result.push(record);
263
+ }
264
+ }
265
+ return result;
266
+ }
267
+
268
+ function collectDependencies(snapshot) {
269
+ const dependencies = [...(snapshot.dependencies || [])];
270
+ for (const issue of snapshot.issues || []) {
271
+ if (Array.isArray(issue.dependencies)) {
272
+ dependencies.push(...issue.dependencies);
273
+ }
274
+ }
275
+
276
+ return uniqueBy(
277
+ dependencies.filter(dependency => dependency?.issue_id && dependency?.depends_on_id),
278
+ dependency => `${dependency.issue_id}\0${dependency.depends_on_id}\0${dependency.type || 'blocks'}`,
279
+ );
280
+ }
281
+
282
+ function getLabelsByIssue(snapshot) {
283
+ const byIssue = new Map();
284
+
285
+ for (const labelRecord of snapshot.labels || []) {
286
+ const issueId = labelRecord.issue_id || labelRecord.issueId;
287
+ const label = labelRecord.label || labelRecord.name;
288
+ if (!issueId || !label) continue;
289
+ if (!byIssue.has(issueId)) {
290
+ byIssue.set(issueId, []);
291
+ }
292
+ byIssue.get(issueId).push(label);
293
+ }
294
+
295
+ return byIssue;
296
+ }
297
+
298
+ function applyLabelSidecar(issues, snapshot) {
299
+ const labelsByIssue = getLabelsByIssue(snapshot);
300
+ if (labelsByIssue.size === 0) {
301
+ return issues;
302
+ }
303
+
304
+ return issues.map(issue => {
305
+ const sidecarLabels = labelsByIssue.get(issue.id) || [];
306
+ if (sidecarLabels.length === 0) {
307
+ return issue;
308
+ }
309
+ const labels = Array.from(new Set([...(Array.isArray(issue.labels) ? issue.labels : []), ...sidecarLabels]));
310
+ return {
311
+ ...issue,
312
+ labels,
313
+ };
314
+ });
315
+ }
316
+
317
+ function collectComments(snapshot) {
318
+ const comments = [...(snapshot.comments || [])];
319
+ for (const issue of snapshot.issues || []) {
320
+ if (Array.isArray(issue.comments)) {
321
+ for (const comment of issue.comments) {
322
+ comments.push({
323
+ ...comment,
324
+ issue_id: comment.issue_id || issue.id,
325
+ });
326
+ }
327
+ }
328
+ if (issue.notes && issue.notes !== issue.description && issue.notes !== issue.body) {
329
+ comments.push({
330
+ id: `beads-note-${encodedIdPart(issue.id)}`,
331
+ issue_id: issue.id,
332
+ body: issue.notes,
333
+ actor: issue.updated_by || issue.created_by || 'beads',
334
+ created_at: issue.updated_at || issue.created_at,
335
+ });
336
+ }
337
+ }
338
+
339
+ return uniqueBy(
340
+ comments.filter(comment => comment?.issue_id && (comment.body || comment.text)),
341
+ comment => `${comment.id || ''}\0${comment.issue_id}\0${comment.created_at || ''}\0${comment.body || comment.text}`,
342
+ );
343
+ }
344
+
345
+ function addGap(gaps, seen, field, reason) {
346
+ if (seen.has(field)) return;
347
+ seen.add(field);
348
+ gaps.push({ field, reason });
349
+ }
350
+
351
+ function hasBeadsFieldValue(value) {
352
+ if (value === null || value === undefined) return false;
353
+ if (typeof value === 'string') return value.trim() !== '' && value !== '{}';
354
+ if (Array.isArray(value)) return value.length > 0;
355
+ if (typeof value === 'object') return Object.keys(value).length > 0;
356
+ return true;
357
+ }
358
+
359
+ function parseMetadataObject(value) {
360
+ if (value === null || value === undefined || value === '') return {};
361
+ if (typeof value === 'object' && !Array.isArray(value)) return { ...value };
362
+ if (typeof value !== 'string') return {};
363
+ try {
364
+ const parsed = JSON.parse(value);
365
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
366
+ } catch (_error) {
367
+ return {};
368
+ }
369
+ }
370
+
371
+ // Carry a Beads issue's metadata onto the Kernel `metadata` TEXT column for full-fidelity import.
372
+ // The forge_projection key is an internal Forge -> Beads projection marker (written on export and
373
+ // consumed as close-event provenance via getForgeProjectionOrigin), NOT user data, so it is
374
+ // stripped here; the remaining object keys are preserved verbatim as a JSON string. Non-object
375
+ // metadata is kept as its raw value. Returns null when nothing user-authored remains.
376
+ function serializeBeadsMetadata(value) {
377
+ if (!hasBeadsFieldValue(value)) return null;
378
+ let metadata = null;
379
+ if (typeof value === 'object' && !Array.isArray(value)) {
380
+ metadata = { ...value };
381
+ } else if (typeof value === 'string') {
382
+ try {
383
+ const parsed = JSON.parse(value);
384
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
385
+ metadata = parsed;
386
+ }
387
+ } catch (_error) { // NOSONAR S2486 — non-JSON metadata is preserved verbatim below
388
+ metadata = null;
389
+ }
390
+ }
391
+ if (metadata === null) {
392
+ return typeof value === 'string' ? value : JSON.stringify(value);
393
+ }
394
+ delete metadata[FORGE_PROJECTION_METADATA_KEY];
395
+ return Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null;
396
+ }
397
+
398
+ // Beads issue fields with no dedicated Kernel column are folded into the metadata blob so nothing
399
+ // is silently dropped: the `owner` (only when not already captured on created_by/assignee — it
400
+ // primarily maps to the Kernel assignee), the `external_ref` (e.g. a linked GitHub issue/run),
401
+ // and the work-start timestamp `started_at`. A raw (non-object) metadata blob that must coexist
402
+ // is nested under metadata.beads_metadata rather than discarded. NOTE: beads-internal/derived
403
+ // fields (content_hash, agent_state, compaction_*, ephemeral, and other runtime plumbing) are
404
+ // intentionally NOT carried — they are not user data and would only pollute the Kernel row.
405
+ function serializeIssueMetadata(issue, resolved = {}) {
406
+ const base = serializeBeadsMetadata(issue.metadata);
407
+ let metadata = {};
408
+ let rawPreserved = null;
409
+ if (base !== null) {
410
+ try {
411
+ const parsed = JSON.parse(base);
412
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
413
+ metadata = parsed;
414
+ } else {
415
+ rawPreserved = base;
416
+ }
417
+ } catch (_error) { // NOSONAR S2486 — non-JSON metadata is preserved via beads_metadata below
418
+ rawPreserved = base;
419
+ }
420
+ }
421
+
422
+ // Compare trimmed on BOTH sides so a padded assignee/created_by does not double-store the owner.
423
+ const trim = value => (typeof value === 'string' ? value.trim() : value);
424
+ const owner = trim(issue.owner);
425
+ const ownerCaptured = owner && (owner === trim(resolved.createdBy) || owner === trim(resolved.assignee));
426
+ if (owner && !ownerCaptured) {
427
+ metadata.beads_owner = issue.owner;
428
+ }
429
+ if (hasBeadsFieldValue(issue.external_ref)) {
430
+ metadata.beads_external_ref = issue.external_ref;
431
+ }
432
+ if (hasBeadsFieldValue(issue.started_at)) {
433
+ metadata.beads_started_at = issue.started_at;
434
+ }
435
+
436
+ if (rawPreserved !== null) {
437
+ if (Object.keys(metadata).length === 0) return rawPreserved;
438
+ metadata.beads_metadata = rawPreserved;
439
+ }
440
+ return Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null;
441
+ }
442
+
443
+ function hasFiniteRevisionValue(value) {
444
+ if (typeof value === 'number') return Number.isFinite(value);
445
+ if (typeof value !== 'string') return false;
446
+ const trimmed = value.trim();
447
+ return trimmed !== '' && Number.isFinite(Number(trimmed));
448
+ }
449
+
450
+ function isValidForgeProjection(projection, issueId) {
451
+ if (!projection || typeof projection !== 'object' || Array.isArray(projection)) return false;
452
+ return projection.source === 'forge-kernel'
453
+ && projection.target === 'beads'
454
+ && projection.entity_type === 'issue'
455
+ && projection.entity_id === issueId
456
+ && hasFiniteRevisionValue(projection.entity_revision)
457
+ && typeof projection.payload_hash === 'string'
458
+ && projection.payload_hash !== '';
459
+ }
460
+
461
+ function getForgeProjectionOrigin(issue = {}, currentPayload = {}) {
462
+ const metadata = parseMetadataObject(issue.metadata);
463
+ const projection = metadata[FORGE_PROJECTION_METADATA_KEY];
464
+ if (!isValidForgeProjection(projection, issue.id)) return null;
465
+ const importedPayloadHash = JSON.stringify(currentPayload);
466
+ if (projection.payload_hash !== importedPayloadHash) return null;
467
+
468
+ return {
469
+ source: 'forge-kernel',
470
+ target: 'beads',
471
+ entity_type: 'issue',
472
+ entity_id: issue.id,
473
+ entity_revision: normalizeEntityRevision(projection.entity_revision),
474
+ payload_hash: projection.payload_hash,
475
+ imported_payload_hash: importedPayloadHash,
476
+ };
477
+ }
478
+
479
+ function buildCloseProjectionPayload(closeMetadata = {}) {
480
+ return {
481
+ closed_at: closeMetadata.closed_at || null,
482
+ close_reason: closeMetadata.close_reason || null,
483
+ };
484
+ }
485
+
486
+ function buildCloseProjectionFingerprint(issue = {}) {
487
+ return {
488
+ ...buildCloseProjectionPayload(issue),
489
+ actor: issue.closed_by || issue.created_by || 'beads',
490
+ };
491
+ }
492
+
493
+ function buildForgeProjectionMetadata(issue = {}, closeMetadata = {}) {
494
+ const metadata = parseMetadataObject(issue.metadata);
495
+ metadata[FORGE_PROJECTION_METADATA_KEY] = {
496
+ source: 'forge-kernel',
497
+ target: 'beads',
498
+ entity_type: 'issue',
499
+ entity_id: issue.id,
500
+ entity_revision: normalizeEntityRevision(issue.entity_revision),
501
+ payload_hash: JSON.stringify(buildCloseProjectionFingerprint({ ...issue, ...closeMetadata })),
502
+ };
503
+ return metadata;
504
+ }
505
+
506
+ function buildFidelityReport({ kernel, gaps, eventCount = 0, interactionCount = 0 }) {
507
+ return {
508
+ summary: {
509
+ issues: kernel.issues.length,
510
+ dependencies: kernel.dependencies.length,
511
+ comments: kernel.comments.length,
512
+ closeEvents: kernel.events.filter(event => event.event_type === 'beads.issue.closed').length,
513
+ // Legacy activity records that now land in kernel_events instead of being dropped.
514
+ events: eventCount,
515
+ interactions: interactionCount,
516
+ activityEvents: kernel.activityEvents ? kernel.activityEvents.length : 0,
517
+ unsupportedFields: gaps.length,
518
+ },
519
+ preservedFields: [...PRESERVED_FIELDS],
520
+ gaps,
521
+ };
522
+ }
523
+
524
+ function mapBeadsIssueToKernel(issue, importedAt, gaps, seenGaps) {
525
+ const priority = normalizePriority(issue.priority);
526
+ const { type, aliasLabel } = normalizeKernelType(issue.issue_type || issue.type);
527
+ const beadsLabels = Array.isArray(issue.labels) ? issue.labels : [];
528
+ const labels = aliasLabel ? [...beadsLabels, aliasLabel] : beadsLabels;
529
+ for (const [field, reason] of UNSUPPORTED_ISSUE_FIELDS) {
530
+ if (hasBeadsFieldValue(issue[field])) {
531
+ addGap(gaps, seenGaps, `issues.${field}`, reason);
532
+ }
533
+ }
534
+
535
+ // Full-fidelity import onto dedicated Kernel columns: the beads author lands on created_by
536
+ // (falling back to owner when no author is recorded); the responsible owner lands on the
537
+ // assignee column (falling back after an explicit beads assignee); design/notes-adjacent
538
+ // authored content lands on the design column. Any owner NOT captured by created_by/assignee
539
+ // is preserved in the metadata blob (serializeIssueMetadata) so nothing is dropped.
540
+ const createdBy = issue.created_by || issue.owner || null;
541
+ const assignee = issue.assignee || issue.owner || null;
542
+
543
+ return {
544
+ id: issue.id,
545
+ title: issue.title || issue.id,
546
+ body: issue.description ?? issue.body ?? issue.notes ?? '',
547
+ type,
548
+ status: normalizeKernelStatus(issue),
549
+ priority: priority.label,
550
+ priority_rank: priority.rank,
551
+ labels: serializeLabels(labels),
552
+ acceptance_criteria: serializeAcceptanceCriteria(issue.acceptance_criteria),
553
+ design: hasBeadsFieldValue(issue.design) ? issue.design : null,
554
+ assignee,
555
+ created_by: createdBy,
556
+ metadata: serializeIssueMetadata(issue, { createdBy, assignee }),
557
+ created_at: issue.created_at || importedAt,
558
+ updated_at: issue.updated_at || issue.created_at || importedAt,
559
+ entity_revision: 0,
560
+ };
561
+ }
562
+
563
+ function mapBeadsDependencyToKernel(dependency, importedAt, gaps, seenGaps) {
564
+ // An empty "{}" metadata blob carries no data, so it is not a loss — only surface a gap when
565
+ // the dependency actually holds metadata (the Kernel dependency row has no metadata column).
566
+ if (hasBeadsFieldValue(dependency.metadata)) {
567
+ addGap(gaps, seenGaps, 'dependencies.metadata', 'no Kernel dependency metadata column in schema v1');
568
+ }
569
+ // The dependency creator has no dedicated Kernel column; retained as a documented gap rather
570
+ // than silently dropped (resolving it would require an additive kernel_dependencies column).
571
+ if (dependency.created_by) {
572
+ addGap(gaps, seenGaps, 'dependencies.created_by', 'no Kernel dependency creator column in schema v1');
573
+ }
574
+
575
+ return {
576
+ id: [
577
+ 'beads-dependency',
578
+ encodedIdPart(dependency.issue_id),
579
+ encodedIdPart(dependency.depends_on_id),
580
+ encodedIdPart(dependency.type || 'blocks'),
581
+ ].join('-'),
582
+ issue_id: dependency.issue_id,
583
+ blocks_issue_id: dependency.depends_on_id,
584
+ dependency_type: dependency.type || 'blocks',
585
+ created_at: dependency.created_at || importedAt,
586
+ };
587
+ }
588
+
589
+ function mapBeadsCommentToKernel(comment, importedAt) {
590
+ return {
591
+ id: comment.id || [
592
+ 'beads-comment',
593
+ encodedIdPart(comment.issue_id),
594
+ encodedIdPart(comment.created_at || importedAt),
595
+ ].join('-'),
596
+ issue_id: comment.issue_id,
597
+ body: comment.body ?? comment.text ?? '',
598
+ actor: comment.actor || comment.author || 'beads',
599
+ visibility: comment.visibility || 'local',
600
+ created_at: comment.created_at || importedAt,
601
+ };
602
+ }
603
+
604
+ function buildPriorityEvent(issue, importedAt) {
605
+ const priority = normalizePriority(issue.priority);
606
+ return {
607
+ id: `beads-priority-${encodedIdPart(issue.id)}`,
608
+ issue_id: issue.id,
609
+ old_priority: null,
610
+ new_priority: priority.label,
611
+ priority_rank: priority.rank,
612
+ actor: issue.created_by || 'beads',
613
+ created_at: issue.updated_at || issue.created_at || importedAt,
614
+ };
615
+ }
616
+
617
+ function normalizeEntityRevision(value) {
618
+ const revision = Number(value);
619
+ return Number.isFinite(revision) ? revision : 0;
620
+ }
621
+
622
+ function buildCloseEvent(issue, importedAt) {
623
+ if (!issue.closed_at && !issue.close_reason) {
624
+ return null;
625
+ }
626
+
627
+ const createdAt = issue.closed_at || issue.updated_at || importedAt;
628
+ const payload = buildCloseProjectionPayload(issue);
629
+ const projectionOrigin = getForgeProjectionOrigin(issue, buildCloseProjectionFingerprint(issue));
630
+ if (projectionOrigin) {
631
+ payload.projection_origin = projectionOrigin;
632
+ }
633
+
634
+ return {
635
+ id: `beads-close-${encodedIdPart(issue.id)}`,
636
+ entity_type: 'issue',
637
+ entity_id: issue.id,
638
+ event_type: 'beads.issue.closed',
639
+ idempotency_key: `beads-close:${issue.id}:${createdAt}`,
640
+ expected_revision: normalizeEntityRevision(projectionOrigin?.entity_revision ?? issue.entity_revision),
641
+ actor: issue.closed_by || issue.created_by || 'beads',
642
+ origin: 'beads_import',
643
+ payload_json: JSON.stringify(payload),
644
+ created_at: createdAt,
645
+ };
646
+ }
647
+
648
+ // Map a legacy Beads activity event (events.jsonl: created/closed/status_changed/updated/
649
+ // label_added/claimed/…) onto a Kernel event row destined for kernel_events. The beads kind is
650
+ // namespaced as `beads.event.<kind>` and preserved verbatim in the payload alongside the
651
+ // old/new value and any inline comment, so no field is lost. id/idempotency_key derive from the
652
+ // original beads event id, making the write idempotent on re-migration.
653
+ function mapBeadsActivityEventToKernel(event = {}, importedAt) {
654
+ const kind = String(event.event_type || event.kind || 'event');
655
+ const createdAt = event.created_at || importedAt;
656
+ // For id-less events, derive the id/idempotency_key from STABLE record fields
657
+ // ONLY (never importedAt) so re-migrating the same legacy record maps to the
658
+ // same id and does not insert a duplicate kernel_events row.
659
+ const source = event.id != null && event.id !== ''
660
+ ? String(event.id)
661
+ : `${event.issue_id ?? ''}:${kind}:${event.created_at ?? ''}:${event.old_value ?? ''}:${event.new_value ?? ''}:${event.comment ?? ''}`;
662
+ return {
663
+ id: `beads-event-${encodedIdPart(source)}`,
664
+ entity_type: 'issue',
665
+ entity_id: event.issue_id != null ? String(event.issue_id) : '',
666
+ event_type: `beads.event.${kind}`,
667
+ idempotency_key: `beads-event:${source}`,
668
+ expected_revision: 0,
669
+ actor: event.actor || 'beads',
670
+ origin: 'beads_import',
671
+ payload_json: JSON.stringify({
672
+ kind,
673
+ old_value: event.old_value ?? null,
674
+ new_value: event.new_value ?? null,
675
+ comment: event.comment ?? null,
676
+ }),
677
+ created_at: createdAt,
678
+ };
679
+ }
680
+
681
+ // Map a legacy Beads interaction/memory record (interactions.jsonl: agent field_change/note/…
682
+ // audit entries) onto a Kernel event row destined for kernel_events. The interaction kind is
683
+ // namespaced as `beads.interaction.<kind>`; the structured `extra` payload is spread into the
684
+ // event payload (preserved verbatim) alongside the kind. id/idempotency_key derive from the
685
+ // original beads interaction id, making the write idempotent on re-migration.
686
+ function mapBeadsInteractionToKernel(interaction = {}, importedAt) {
687
+ const kind = String(interaction.kind || interaction.event_type || 'interaction');
688
+ const createdAt = interaction.created_at || importedAt;
689
+ // For id-less interactions, derive the id/idempotency_key from STABLE record
690
+ // fields ONLY (never importedAt) so re-migration is idempotent (no dup rows).
691
+ const source = interaction.id != null && interaction.id !== ''
692
+ ? String(interaction.id)
693
+ : `${interaction.issue_id ?? ''}:${kind}:${interaction.created_at ?? ''}:${JSON.stringify(interaction.extra ?? null)}`;
694
+ const extra = interaction.extra && typeof interaction.extra === 'object' && !Array.isArray(interaction.extra)
695
+ ? interaction.extra
696
+ : { extra: interaction.extra ?? null };
697
+ return {
698
+ id: `beads-interaction-${encodedIdPart(source)}`,
699
+ entity_type: 'issue',
700
+ entity_id: interaction.issue_id != null ? String(interaction.issue_id) : '',
701
+ event_type: `beads.interaction.${kind}`,
702
+ idempotency_key: `beads-interaction:${source}`,
703
+ expected_revision: 0,
704
+ actor: interaction.actor || 'beads',
705
+ origin: 'beads_import',
706
+ payload_json: JSON.stringify({ kind, ...extra }),
707
+ created_at: createdAt,
708
+ };
709
+ }
710
+
711
+ function importBeadsSnapshot(snapshot = {}, options = {}) {
712
+ const importedAt = options.importedAt || new Date().toISOString();
713
+ const gaps = [];
714
+ const seenGaps = new Set();
715
+ const rawIssues = Array.isArray(snapshot.issues) ? snapshot.issues : [];
716
+ const sourceIssues = applyLabelSidecar(rawIssues, snapshot);
717
+ const normalizedSnapshot = {
718
+ ...snapshot,
719
+ issues: sourceIssues,
720
+ };
721
+ // The legacy activity log (lifecycle events + agent interactions) is no longer dropped: both
722
+ // sidecars are mapped into a single kernel_events-bound bundle, preserving id/kind/actor/
723
+ // timestamp/payload. events first, then interactions, so the write order is deterministic.
724
+ const activityEvents = [
725
+ ...(Array.isArray(snapshot.events) ? snapshot.events : []).map(event => mapBeadsActivityEventToKernel(event, importedAt)),
726
+ ...(Array.isArray(snapshot.interactions) ? snapshot.interactions : []).map(interaction => mapBeadsInteractionToKernel(interaction, importedAt)),
727
+ ];
728
+ const kernel = {
729
+ issues: sourceIssues.map(issue => mapBeadsIssueToKernel(issue, importedAt, gaps, seenGaps)),
730
+ dependencies: collectDependencies(normalizedSnapshot)
731
+ .map(dependency => mapBeadsDependencyToKernel(dependency, importedAt, gaps, seenGaps)),
732
+ comments: collectComments(normalizedSnapshot).map(comment => mapBeadsCommentToKernel(comment, importedAt)),
733
+ priorityEvents: sourceIssues.map(issue => buildPriorityEvent(issue, importedAt)),
734
+ events: sourceIssues.map(issue => buildCloseEvent(issue, importedAt)).filter(Boolean),
735
+ activityEvents,
736
+ };
737
+
738
+ return {
739
+ source: 'beads',
740
+ authority: 'forge-kernel',
741
+ kernel,
742
+ report: buildFidelityReport({
743
+ kernel,
744
+ gaps,
745
+ eventCount: Array.isArray(snapshot.events) ? snapshot.events.length : 0,
746
+ interactionCount: Array.isArray(snapshot.interactions) ? snapshot.interactions.length : 0,
747
+ }),
748
+ rollback: {
749
+ available: true,
750
+ mode: 'import-only',
751
+ reason: 'Import did not mutate Beads files; discard imported Kernel records to roll back.',
752
+ },
753
+ };
754
+ }
755
+
756
+ function getCloseMetadataByIssue(kernel) {
757
+ const closeByIssue = new Map();
758
+ for (const event of kernel.events || []) {
759
+ if (event.event_type !== 'beads.issue.closed') continue;
760
+ let payload;
761
+ try {
762
+ payload = JSON.parse(event.payload_json || '{}');
763
+ } catch (_error) {
764
+ payload = {};
765
+ }
766
+ closeByIssue.set(event.entity_id, {
767
+ closed_at: payload.closed_at || event.created_at || null,
768
+ close_reason: payload.close_reason || null,
769
+ });
770
+ }
771
+ return closeByIssue;
772
+ }
773
+
774
+ function getDependenciesByIssue(kernel) {
775
+ const byIssue = new Map();
776
+ for (const dependency of kernel.dependencies || []) {
777
+ const beadsDependency = {
778
+ issue_id: dependency.issue_id,
779
+ depends_on_id: dependency.blocks_issue_id,
780
+ type: dependency.dependency_type || 'blocks',
781
+ created_at: dependency.created_at,
782
+ metadata: '{}',
783
+ };
784
+ if (dependency.created_by) {
785
+ beadsDependency.created_by = dependency.created_by;
786
+ }
787
+ if (!byIssue.has(dependency.issue_id)) {
788
+ byIssue.set(dependency.issue_id, []);
789
+ }
790
+ byIssue.get(dependency.issue_id).push(beadsDependency);
791
+ }
792
+ return byIssue;
793
+ }
794
+
795
+ function isBlockingDependencyType(type) {
796
+ return (type || 'blocks') === 'blocks';
797
+ }
798
+
799
+ function getBlockingDependentsByIssue(kernel) {
800
+ const byIssue = new Map();
801
+ for (const dependency of kernel.dependencies || []) {
802
+ if (!isBlockingDependencyType(dependency.dependency_type)) continue;
803
+ if (!dependency.blocks_issue_id) continue;
804
+ if (!byIssue.has(dependency.blocks_issue_id)) {
805
+ byIssue.set(dependency.blocks_issue_id, []);
806
+ }
807
+ byIssue.get(dependency.blocks_issue_id).push(dependency.issue_id);
808
+ }
809
+ return byIssue;
810
+ }
811
+
812
+ function mapKernelIssueToBeads(issue, dependenciesByIssue, dependentsByIssue, closeByIssue) {
813
+ const closeMetadata = closeByIssue.get(issue.id) || {};
814
+ const priority = normalizePriority(issue.priority);
815
+ const dependencies = dependenciesByIssue.get(issue.id) || [];
816
+ // Resolve the final Beads close payload — including the synthesized cancellation marker for a
817
+ // cancelled Kernel issue — BEFORE building projection metadata. forge_projection.payload_hash
818
+ // must be computed over the close_reason actually exported; otherwise re-import recomputes a
819
+ // different hash, drops projection_origin, and loses the entity_revision provenance.
820
+ const effectiveClose = { ...closeMetadata };
821
+ if (issue.status === 'cancelled' && !CANCELLED_CLOSE_REASON.test(String(effectiveClose.close_reason || ''))) {
822
+ effectiveClose.close_reason = effectiveClose.close_reason
823
+ ? `${effectiveClose.close_reason} (cancelled)`
824
+ : 'cancelled';
825
+ }
826
+ const exported = {
827
+ _type: 'issue',
828
+ id: issue.id,
829
+ title: issue.title,
830
+ description: issue.body || '',
831
+ status: beadsStatusForKernelStatus(issue.status),
832
+ priority: priority.beads,
833
+ issue_type: issue.type || 'task',
834
+ created_at: issue.created_at,
835
+ updated_at: issue.updated_at,
836
+ dependencies,
837
+ dependency_count: dependencies.filter(dependency => isBlockingDependencyType(dependency.type)).length,
838
+ dependent_count: (dependentsByIssue.get(issue.id) || []).length,
839
+ };
840
+ const labels = deserializeLabels(issue.labels);
841
+ if (labels.length > 0) {
842
+ exported.labels = labels;
843
+ }
844
+ const acceptanceCriteria = deserializeAcceptanceCriteria(issue.acceptance_criteria);
845
+ if (acceptanceCriteria !== null && acceptanceCriteria !== undefined && acceptanceCriteria !== '') {
846
+ exported.acceptance_criteria = acceptanceCriteria;
847
+ }
848
+ if (issue.created_by) {
849
+ exported.created_by = issue.created_by;
850
+ }
851
+ exported.metadata = JSON.stringify(buildForgeProjectionMetadata(issue, effectiveClose));
852
+
853
+ if (effectiveClose.closed_at) {
854
+ exported.closed_at = effectiveClose.closed_at;
855
+ }
856
+ if (effectiveClose.close_reason) {
857
+ exported.close_reason = effectiveClose.close_reason;
858
+ }
859
+
860
+ return exported;
861
+ }
862
+
863
+ function mapKernelCommentToBeads(comment) {
864
+ return {
865
+ id: comment.id,
866
+ issue_id: comment.issue_id,
867
+ author: comment.actor,
868
+ text: comment.body,
869
+ created_at: comment.created_at,
870
+ };
871
+ }
872
+
873
+ function mapKernelDependencyToBeads(dependency) {
874
+ const exported = {
875
+ issue_id: dependency.issue_id,
876
+ depends_on_id: dependency.blocks_issue_id,
877
+ type: dependency.dependency_type || 'blocks',
878
+ created_at: dependency.created_at,
879
+ metadata: '{}',
880
+ };
881
+ if (dependency.created_by) {
882
+ exported.created_by = dependency.created_by;
883
+ }
884
+ return exported;
885
+ }
886
+
887
+ function buildExportFiles(kernel) {
888
+ const dependenciesByIssue = getDependenciesByIssue(kernel);
889
+ const dependentsByIssue = getBlockingDependentsByIssue(kernel);
890
+ const closeByIssue = getCloseMetadataByIssue(kernel);
891
+ const issues = (kernel.issues || []).map(issue => (
892
+ mapKernelIssueToBeads(issue, dependenciesByIssue, dependentsByIssue, closeByIssue)
893
+ ));
894
+ const comments = (kernel.comments || []).map(mapKernelCommentToBeads);
895
+ const dependencies = (kernel.dependencies || []).map(mapKernelDependencyToBeads);
896
+
897
+ return {
898
+ records: {
899
+ issues,
900
+ comments,
901
+ dependencies,
902
+ },
903
+ files: {
904
+ 'issues.jsonl': stringifyJsonl(issues),
905
+ 'comments.jsonl': stringifyJsonl(comments),
906
+ 'dependencies.jsonl': stringifyJsonl(dependencies),
907
+ },
908
+ };
909
+ }
910
+
911
+ function buildExportReport(kernel, gaps = []) {
912
+ return buildFidelityReport({
913
+ kernel: {
914
+ issues: kernel.issues || [],
915
+ dependencies: kernel.dependencies || [],
916
+ comments: kernel.comments || [],
917
+ events: kernel.events || [],
918
+ },
919
+ gaps,
920
+ });
921
+ }
922
+
923
+ function captureRollbackSnapshot(beadsDir, files) {
924
+ const snapshot = {};
925
+ for (const file of files) {
926
+ const filePath = path.join(beadsDir, file);
927
+ snapshot[file] = fs.existsSync(filePath)
928
+ ? { existed: true, content: fs.readFileSync(filePath, 'utf8') }
929
+ : { existed: false, content: null };
930
+ }
931
+
932
+ return {
933
+ available: true,
934
+ beadsDir,
935
+ files: [...files],
936
+ snapshot,
937
+ };
938
+ }
939
+
940
+ function exportKernelToBeads(kernel = {}, options = {}) {
941
+ const dryRun = options.dryRun !== false;
942
+ const { files } = buildExportFiles(kernel);
943
+ const writes = BEADS_EXPORT_FILES.map(file => ({
944
+ file,
945
+ bytes: Buffer.byteLength(files[file], 'utf8'),
946
+ }));
947
+
948
+ if (dryRun) {
949
+ return {
950
+ source: 'forge-kernel',
951
+ target: 'beads',
952
+ dryRun: true,
953
+ files,
954
+ writes,
955
+ report: buildExportReport(kernel),
956
+ rollback: {
957
+ available: false,
958
+ reason: 'Dry-run did not write Beads files.',
959
+ },
960
+ };
961
+ }
962
+
963
+ if (!options.beadsDir || typeof options.beadsDir !== 'string') {
964
+ throw new TypeError('beadsDir is required when dryRun is false');
965
+ }
966
+
967
+ fs.mkdirSync(options.beadsDir, { recursive: true });
968
+ const rollback = captureRollbackSnapshot(options.beadsDir, BEADS_EXPORT_FILES);
969
+ try {
970
+ for (const file of BEADS_EXPORT_FILES) {
971
+ fs.writeFileSync(path.join(options.beadsDir, file), files[file]);
972
+ }
973
+ } catch (error) {
974
+ rollbackBeadsExport(rollback);
975
+ error.rollback = rollback;
976
+ throw error;
977
+ }
978
+
979
+ return {
980
+ source: 'forge-kernel',
981
+ target: 'beads',
982
+ dryRun: false,
983
+ files,
984
+ writes,
985
+ report: buildExportReport(kernel),
986
+ rollback,
987
+ };
988
+ }
989
+
990
+ function rollbackBeadsExport(rollback) {
991
+ if (!rollback?.available || !rollback.beadsDir || !rollback.snapshot) {
992
+ throw new Error('Rollback snapshot is not available');
993
+ }
994
+
995
+ for (const file of rollback.files || []) {
996
+ const filePath = path.join(rollback.beadsDir, file);
997
+ const entry = rollback.snapshot[file];
998
+ if (!entry) continue;
999
+ if (entry.existed) {
1000
+ fs.writeFileSync(filePath, entry.content);
1001
+ } else if (fs.existsSync(filePath)) {
1002
+ fs.unlinkSync(filePath);
1003
+ }
1004
+ }
1005
+
1006
+ return {
1007
+ rolledBack: true,
1008
+ files: [...(rollback.files || [])],
1009
+ };
1010
+ }
1011
+
1012
+ class BeadsKernelCompatibilityAdapter {
1013
+ constructor(options = {}) {
1014
+ this.id = options.id || 'beads-kernel-compat';
1015
+ this.kind = 'import-export';
1016
+ this.name = options.name || 'Beads Kernel Compatibility Adapter';
1017
+ this.version = options.version || '0.1.0';
1018
+ }
1019
+
1020
+ import(snapshot, options = {}) {
1021
+ return importBeadsSnapshot(snapshot, options);
1022
+ }
1023
+
1024
+ export(kernel, options = {}) {
1025
+ return exportKernelToBeads(kernel, options);
1026
+ }
1027
+ }
1028
+
1029
+ module.exports = {
1030
+ BEADS_EXPORT_FILES,
1031
+ PRESERVED_FIELDS,
1032
+ BeadsKernelCompatibilityAdapter,
1033
+ exportKernelToBeads,
1034
+ importBeadsSnapshot,
1035
+ mapBeadsActivityEventToKernel,
1036
+ mapBeadsInteractionToKernel,
1037
+ loadBeadsSnapshotFromDirectory,
1038
+ parseJsonl,
1039
+ rollbackBeadsExport,
1040
+ safeIdPart,
1041
+ stringifyJsonl,
1042
+ };