forge-workflow 0.0.10 → 0.1.0-beta.3

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 (468) 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 +82 -5
  5. package/.forge/hooks/forge-native-hook.js +431 -0
  6. package/.forge/protected-paths.yaml +157 -0
  7. package/AGENTS.md +151 -61
  8. package/CHANGELOG.md +709 -0
  9. package/CLAUDE.md +9 -118
  10. package/QUICKSTART.md +175 -0
  11. package/README.md +275 -365
  12. package/bin/forge-cmd.js +120 -9
  13. package/bin/forge-preflight.js +26 -5
  14. package/bin/forge.js +532 -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 +121 -0
  29. package/docs/guides/SUPPORT.md +190 -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 +214 -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 +155 -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/activation/ensure-forge-home.js +135 -0
  67. package/lib/adapter-cli.js +307 -0
  68. package/lib/adapters/beads-issue-adapter.js +127 -0
  69. package/lib/adapters/beads-kernel-compat.js +1109 -0
  70. package/lib/adapters/greptile-review-adapter.js +141 -0
  71. package/lib/adapters/kernel-issue-adapter.js +101 -0
  72. package/lib/adapters/pr-state-adapter.js +484 -0
  73. package/lib/adoption-profiles.js +139 -0
  74. package/lib/agents/README.md +2 -6
  75. package/lib/agents/claude.plugin.json +3 -8
  76. package/lib/agents/codex.plugin.json +9 -1
  77. package/lib/agents/cursor.plugin.json +2 -6
  78. package/lib/agents/hermes.plugin.json +22 -0
  79. package/lib/agents-config.js +39 -1236
  80. package/lib/audit-evidence.js +282 -0
  81. package/lib/beads-detect.js +60 -0
  82. package/lib/beads-nudge.js +91 -0
  83. package/lib/beads-setup.js +121 -0
  84. package/lib/beads-sync-scaffold.js +25 -101
  85. package/lib/codex-skills.js +51 -1
  86. package/lib/commands/_aliases.js +248 -0
  87. package/lib/commands/_issue.js +780 -77
  88. package/lib/commands/_manifest.js +93 -0
  89. package/lib/commands/_registry.js +99 -34
  90. package/lib/commands/_resolve-command-opts.js +230 -0
  91. package/lib/commands/_serve-security.js +270 -0
  92. package/lib/commands/adapter.js +12 -0
  93. package/lib/commands/add.js +118 -0
  94. package/lib/commands/audit.js +70 -0
  95. package/lib/commands/blocked.js +5 -0
  96. package/lib/commands/board.js +64 -0
  97. package/lib/commands/claim.js +21 -2
  98. package/lib/commands/claims.js +7 -0
  99. package/lib/commands/clean.js +485 -75
  100. package/lib/commands/close.js +2 -2
  101. package/lib/commands/comment.js +5 -0
  102. package/lib/commands/control.js +148 -0
  103. package/lib/commands/create.js +2 -2
  104. package/lib/commands/dev.js +185 -7
  105. package/lib/commands/doc-gate.js +336 -0
  106. package/lib/commands/doctor.js +156 -0
  107. package/lib/commands/explain.js +15 -0
  108. package/lib/commands/export.js +237 -0
  109. package/lib/commands/gate.js +209 -0
  110. package/lib/commands/hooks.js +377 -0
  111. package/lib/commands/inbox.js +118 -0
  112. package/lib/commands/init.js +604 -0
  113. package/lib/commands/insights.js +79 -0
  114. package/lib/commands/issue.js +12 -1
  115. package/lib/commands/issues.js +17 -0
  116. package/lib/commands/lint.js +5 -0
  117. package/lib/commands/list.js +2 -2
  118. package/lib/commands/memory.js +81 -0
  119. package/lib/commands/merge.js +312 -0
  120. package/lib/commands/migrate.js +362 -0
  121. package/lib/commands/new.js +12 -0
  122. package/lib/commands/options.js +241 -0
  123. package/lib/commands/orient.js +13 -0
  124. package/lib/commands/orphans.js +5 -0
  125. package/lib/commands/patch.js +67 -0
  126. package/lib/commands/plan.js +481 -29
  127. package/lib/commands/pr.js +88 -0
  128. package/lib/commands/preflight.js +211 -0
  129. package/lib/commands/prime.js +13 -0
  130. package/lib/commands/push.js +135 -2
  131. package/lib/commands/ready.js +2 -2
  132. package/lib/commands/recall.js +171 -0
  133. package/lib/commands/recap.js +75 -0
  134. package/lib/commands/recommend.js +0 -1
  135. package/lib/commands/release.js +104 -0
  136. package/lib/commands/remember.js +140 -0
  137. package/lib/commands/role.js +99 -0
  138. package/lib/commands/serve.js +581 -0
  139. package/lib/commands/setup.js +900 -971
  140. package/lib/commands/shepherd.js +501 -0
  141. package/lib/commands/ship.js +59 -1
  142. package/lib/commands/show.js +2 -2
  143. package/lib/commands/stage.js +192 -0
  144. package/lib/commands/stale.js +5 -0
  145. package/lib/commands/status.js +158 -21
  146. package/lib/commands/sync.js +34 -46
  147. package/lib/commands/team.js +4 -1
  148. package/lib/commands/test.js +43 -27
  149. package/lib/commands/update.js +2 -2
  150. package/lib/commands/upgrade.js +47 -0
  151. package/lib/commands/validate.js +43 -18
  152. package/lib/commands/worktree.js +362 -99
  153. package/lib/config-writer.js +202 -0
  154. package/lib/control-plane.js +236 -0
  155. package/lib/core/runtime-graph.js +977 -0
  156. package/lib/dep-guard/keyword-ripple.js +2 -2
  157. package/lib/deprecated-sync-cleanup.js +362 -0
  158. package/lib/detect-agent.js +2 -28
  159. package/lib/detect-worktree.js +35 -9
  160. package/lib/doc-gate/declaration.js +177 -0
  161. package/lib/doc-gate/detect.js +289 -0
  162. package/lib/doc-gate/gate.js +375 -0
  163. package/lib/doc-gate/okf-config.js +128 -0
  164. package/lib/doc-gate/okf.js +429 -0
  165. package/lib/docs-command.js +1161 -6
  166. package/lib/forge-issues.js +382 -11
  167. package/lib/forge-lock.js +262 -0
  168. package/lib/gate-events.js +192 -0
  169. package/lib/global-flags.js +104 -0
  170. package/lib/greptile-match.js +7 -63
  171. package/lib/grounding/context-events.js +230 -0
  172. package/lib/grounding/read-first.js +112 -0
  173. package/lib/harness-capability-matrix.js +380 -0
  174. package/lib/hook-global-installer.js +347 -0
  175. package/lib/hook-renderer.js +541 -0
  176. package/lib/inbox.js +391 -0
  177. package/lib/insights.js +397 -0
  178. package/lib/issue-adapter.js +156 -0
  179. package/lib/issue-backend.js +145 -0
  180. package/lib/issue-render.js +220 -0
  181. package/lib/kernel/backing-issue.js +311 -0
  182. package/lib/kernel/broker.js +1218 -0
  183. package/lib/kernel/cli-broker-factory.js +130 -0
  184. package/lib/kernel/conflict-signal.js +82 -0
  185. package/lib/kernel/evaluators.js +195 -0
  186. package/lib/kernel/fs-class.js +495 -0
  187. package/lib/kernel/issue-command-contract.js +559 -0
  188. package/lib/kernel/issue-id-resolver.js +186 -0
  189. package/lib/kernel/lease-enforcer.js +158 -0
  190. package/lib/kernel/migrations.js +333 -0
  191. package/lib/kernel/owned-kernel.js +43 -0
  192. package/lib/kernel/planning-buckets-schema.js +109 -0
  193. package/lib/kernel/projection-jsonl-writer.js +450 -0
  194. package/lib/kernel/readiness-model.js +329 -0
  195. package/lib/kernel/schema.js +356 -0
  196. package/lib/kernel/sqlite-driver.js +2540 -0
  197. package/lib/kernel/taxonomy-validator.js +394 -0
  198. package/lib/lefthook-check.js +3 -2
  199. package/lib/lefthook-wiring.js +413 -0
  200. package/lib/mcp-config-renderer.js +288 -0
  201. package/lib/memory/graphiti-mcp.js +106 -0
  202. package/lib/memory/router.js +387 -0
  203. package/lib/memory/typed-api.js +102 -0
  204. package/lib/memory-digest.js +195 -0
  205. package/lib/merge-rules.js +395 -0
  206. package/lib/migrate-dry-run.js +466 -0
  207. package/lib/orientation.js +863 -0
  208. package/lib/package-manager-remediation.js +103 -0
  209. package/lib/package-root.js +381 -0
  210. package/lib/patch-intent.js +890 -0
  211. package/lib/plugin-catalog.js +3 -4
  212. package/lib/plugin-manager.js +0 -5
  213. package/lib/pr-bundle.js +186 -0
  214. package/lib/pr-monitor/auto-actions.js +175 -0
  215. package/lib/pr-monitor/differ.js +195 -0
  216. package/lib/pr-monitor/digest.js +206 -0
  217. package/lib/pr-monitor/events.js +0 -0
  218. package/lib/pr-monitor/gather.js +124 -0
  219. package/lib/pr-monitor/journal.js +299 -0
  220. package/lib/pr-monitor/monitor.js +146 -0
  221. package/lib/pr-monitor/render-sticky.js +192 -0
  222. package/lib/pr-monitor/upsert-sticky.js +169 -0
  223. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  224. package/lib/pr-monitor/watch.js +247 -0
  225. package/lib/pr-pull.js +1314 -0
  226. package/lib/pr-shepherd.js +494 -0
  227. package/lib/pr-state-validator.js +59 -0
  228. package/lib/preflight/gates.js +237 -0
  229. package/lib/preflight/runner.js +116 -0
  230. package/lib/project-discovery.js +0 -53
  231. package/lib/project-memory.js +99 -497
  232. package/lib/protected-path-manifest.js +281 -0
  233. package/lib/protected-state-surfaces.js +387 -0
  234. package/lib/release-readiness.js +2105 -0
  235. package/lib/reset.js +59 -45
  236. package/lib/review-adapter.js +68 -0
  237. package/lib/rules-sync.js +260 -0
  238. package/lib/runtime-health.js +241 -20
  239. package/lib/safety-config-renderer.js +268 -0
  240. package/lib/setup-action-log.js +1 -7
  241. package/lib/setup.js +27 -65
  242. package/lib/shell-utils.js +76 -6
  243. package/lib/skills-sync.js +330 -0
  244. package/lib/smart-status/scoring.js +17 -3
  245. package/lib/status/beads-snapshot.js +45 -2
  246. package/lib/status/presenter.js +169 -18
  247. package/lib/status/snapshot.js +186 -0
  248. package/lib/sync-backend.js +202 -0
  249. package/lib/untrusted-content.js +52 -0
  250. package/lib/upgrade-safety.js +251 -0
  251. package/lib/workflow/enforce-stage.js +351 -45
  252. package/lib/workflow/stage-transition.js +115 -0
  253. package/lib/workflow/stages.js +30 -6
  254. package/lib/workflow/state-manager.js +11 -22
  255. package/lib/workflow/state.js +23 -1
  256. package/lib/workflow-profiles.js +17 -5
  257. package/package.json +37 -35
  258. package/rules/documentation.md +19 -0
  259. package/rules/kernel-tracking.md +26 -0
  260. package/rules/security.md +22 -0
  261. package/rules/tdd.md +20 -0
  262. package/rules/workflow.md +27 -0
  263. package/scripts/auto-backing-issue.js +47 -0
  264. package/scripts/beads-context.sh +81 -57
  265. package/scripts/beads-upgrade-smoke.sh +24 -3
  266. package/scripts/bootstrap-windows-tools.sh +78 -0
  267. package/scripts/branch-protection.js +2 -3
  268. package/scripts/check-agents.js +34 -137
  269. package/scripts/commitlint.js +3 -1
  270. package/scripts/conflict-detect.sh +3 -0
  271. package/scripts/dep-guard.sh +22 -3
  272. package/scripts/file-index.sh +3 -0
  273. package/scripts/forge-team/lib/claim.sh +34 -18
  274. package/scripts/forge-team/lib/dashboard.sh +61 -86
  275. package/scripts/forge-team/lib/epic.sh +99 -263
  276. package/scripts/forge-team/lib/hooks.sh +26 -28
  277. package/scripts/forge-team/lib/identity.sh +4 -4
  278. package/scripts/forge-team/lib/sync-github.sh +49 -84
  279. package/scripts/forge-team/lib/verify.sh +93 -83
  280. package/scripts/forge-team/lib/workload.sh +41 -65
  281. package/scripts/forge-team/tests/claim.test.sh +25 -19
  282. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  283. package/scripts/forge-team/tests/epic.test.sh +52 -71
  284. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  285. package/scripts/forge-team/tests/identity.test.sh +3 -3
  286. package/scripts/forge-team/tests/integration.test.sh +44 -66
  287. package/scripts/forge-team/tests/sync-github.test.sh +50 -83
  288. package/scripts/forge-team/tests/verify.test.sh +37 -46
  289. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  290. package/scripts/forge-team/tests/workload.test.sh +32 -66
  291. package/scripts/gen-command-manifest.js +153 -0
  292. package/scripts/gen-embedded-assets.mjs +129 -0
  293. package/scripts/install.ps1 +139 -0
  294. package/scripts/install.sh +268 -0
  295. package/scripts/lib/release-asset.mjs +84 -0
  296. package/scripts/parity-check.mjs +145 -0
  297. package/scripts/parity-check.test.mjs +58 -0
  298. package/scripts/pin-agentic-workflow-images.js +112 -0
  299. package/scripts/pr-auto-actions.js +93 -0
  300. package/scripts/pr-coordinator.sh +3 -0
  301. package/scripts/pr-verdict-label.js +50 -0
  302. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  303. package/scripts/preflight.sh +21 -94
  304. package/scripts/protected-state-check.js +104 -0
  305. package/scripts/smart-status.sh +60 -57
  306. package/scripts/spikes/config-race-bench.js +111 -0
  307. package/scripts/spikes/harness-capability-matrix.js +13 -0
  308. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  309. package/scripts/spikes/protected-path-manifest.js +20 -0
  310. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  311. package/scripts/sync-agent-skills.js +62 -0
  312. package/scripts/sync-utils.sh +3 -0
  313. package/scripts/test-ci-shard.js +13 -6
  314. package/scripts/test.js +95 -12
  315. package/skills/claim-safety/SKILL.md +102 -0
  316. package/skills/claim-safety/evals/evals.json +46 -0
  317. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
  318. package/skills/dev/evals/evals.json +50 -0
  319. package/skills/hermes-forge/SKILL.md +185 -0
  320. package/skills/hermes-forge/evals/evals.json +46 -0
  321. package/skills/issue-basics/SKILL.md +111 -0
  322. package/skills/issue-basics/evals/evals.json +46 -0
  323. package/skills/kernel/SKILL.md +166 -0
  324. package/skills/kernel/evals/evals.json +50 -0
  325. package/skills/memory/SKILL.md +102 -0
  326. package/skills/parallel-deep-research/SKILL.md +14 -11
  327. package/skills/parallel-deep-research/evals/evals.json +11 -27
  328. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
  329. package/skills/plan/evals/evals.json +42 -0
  330. package/skills/research/SKILL.md +195 -0
  331. package/skills/research/evals/evals.json +42 -0
  332. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  333. package/skills/review/evals/evals.json +42 -0
  334. package/skills/rollback/SKILL.md +110 -0
  335. package/skills/rollback/evals/evals.json +46 -0
  336. package/skills/rollback/references/methods.md +204 -0
  337. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  338. package/skills/shepherd/SKILL.md +66 -0
  339. package/skills/shepherd/evals/evals.json +42 -0
  340. package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
  341. package/skills/ship/evals/evals.json +42 -0
  342. package/skills/smith/SKILL.md +142 -0
  343. package/skills/smith/evals/evals.json +46 -0
  344. package/skills/smith/references/autonomy-and-gates.md +94 -0
  345. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  346. package/skills/sonarcloud/evals/evals.json +46 -0
  347. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  348. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  349. package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
  350. package/skills/status/evals/evals.json +50 -0
  351. package/skills/triage-ready/SKILL.md +121 -0
  352. package/skills/triage-ready/evals/evals.json +42 -0
  353. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  354. package/skills/validate/evals/evals.json +42 -0
  355. package/skills/verify/SKILL.md +299 -0
  356. package/skills/verify/evals/evals.json +50 -0
  357. package/.claude/commands/dev.md +0 -345
  358. package/.claude/commands/plan.md +0 -566
  359. package/.claude/commands/premerge.md +0 -186
  360. package/.claude/commands/research.md +0 -42
  361. package/.claude/commands/review.md +0 -451
  362. package/.claude/commands/rollback.md +0 -721
  363. package/.claude/commands/ship.md +0 -213
  364. package/.claude/commands/sonarcloud.md +0 -152
  365. package/.claude/commands/status.md +0 -90
  366. package/.claude/commands/validate.md +0 -288
  367. package/.claude/commands/verify.md +0 -269
  368. package/.claude/rules/workflow.md +0 -121
  369. package/.cline/workflows/dev.md +0 -342
  370. package/.cline/workflows/plan.md +0 -563
  371. package/.cline/workflows/premerge.md +0 -183
  372. package/.cline/workflows/research.md +0 -39
  373. package/.cline/workflows/review.md +0 -448
  374. package/.cline/workflows/rollback.md +0 -718
  375. package/.cline/workflows/ship.md +0 -210
  376. package/.cline/workflows/sonarcloud.md +0 -146
  377. package/.cline/workflows/status.md +0 -87
  378. package/.cline/workflows/validate.md +0 -285
  379. package/.cline/workflows/verify.md +0 -266
  380. package/.codex/config.toml +0 -11
  381. package/.codex/skills/dev/SKILL.md +0 -345
  382. package/.codex/skills/plan/SKILL.md +0 -566
  383. package/.codex/skills/premerge/SKILL.md +0 -186
  384. package/.codex/skills/research/SKILL.md +0 -42
  385. package/.codex/skills/review/SKILL.md +0 -451
  386. package/.codex/skills/rollback/SKILL.md +0 -721
  387. package/.codex/skills/ship/SKILL.md +0 -213
  388. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  389. package/.codex/skills/status/SKILL.md +0 -90
  390. package/.codex/skills/validate/SKILL.md +0 -288
  391. package/.codex/skills/verify/SKILL.md +0 -269
  392. package/.cursor/commands/dev.md +0 -342
  393. package/.cursor/commands/plan.md +0 -563
  394. package/.cursor/commands/premerge.md +0 -183
  395. package/.cursor/commands/research.md +0 -39
  396. package/.cursor/commands/review.md +0 -448
  397. package/.cursor/commands/ship.md +0 -210
  398. package/.cursor/commands/sonarcloud.md +0 -146
  399. package/.cursor/commands/status.md +0 -87
  400. package/.cursor/commands/validate.md +0 -285
  401. package/.cursor/commands/verify.md +0 -266
  402. package/.cursorrules +0 -149
  403. package/.github/prompts/premerge.prompt.md +0 -188
  404. package/.github/prompts/research.prompt.md +0 -44
  405. package/.github/prompts/rollback.prompt.md +0 -723
  406. package/.github/prompts/verify.prompt.md +0 -271
  407. package/.github/workflows/beads-to-github.yml +0 -89
  408. package/.github/workflows/github-to-beads.yml +0 -100
  409. package/.kilocode/workflows/dev.md +0 -346
  410. package/.kilocode/workflows/plan.md +0 -567
  411. package/.kilocode/workflows/premerge.md +0 -187
  412. package/.kilocode/workflows/research.md +0 -43
  413. package/.kilocode/workflows/review.md +0 -452
  414. package/.kilocode/workflows/rollback.md +0 -722
  415. package/.kilocode/workflows/ship.md +0 -214
  416. package/.kilocode/workflows/sonarcloud.md +0 -150
  417. package/.kilocode/workflows/status.md +0 -91
  418. package/.kilocode/workflows/validate.md +0 -289
  419. package/.kilocode/workflows/verify.md +0 -270
  420. package/.opencode/commands/dev.md +0 -345
  421. package/.opencode/commands/plan.md +0 -566
  422. package/.opencode/commands/premerge.md +0 -186
  423. package/.opencode/commands/research.md +0 -42
  424. package/.opencode/commands/review.md +0 -451
  425. package/.opencode/commands/rollback.md +0 -721
  426. package/.opencode/commands/ship.md +0 -213
  427. package/.opencode/commands/sonarcloud.md +0 -149
  428. package/.opencode/commands/status.md +0 -90
  429. package/.opencode/commands/validate.md +0 -288
  430. package/.opencode/commands/verify.md +0 -269
  431. package/.roo/commands/dev.md +0 -346
  432. package/.roo/commands/plan.md +0 -567
  433. package/.roo/commands/premerge.md +0 -187
  434. package/.roo/commands/research.md +0 -43
  435. package/.roo/commands/review.md +0 -452
  436. package/.roo/commands/rollback.md +0 -722
  437. package/.roo/commands/ship.md +0 -214
  438. package/.roo/commands/sonarcloud.md +0 -150
  439. package/.roo/commands/status.md +0 -91
  440. package/.roo/commands/validate.md +0 -289
  441. package/.roo/commands/verify.md +0 -270
  442. package/docs/BEADS_GITHUB_SYNC.md +0 -281
  443. package/docs/GREPTILE_SETUP.md +0 -400
  444. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  445. package/docs/SETUP.md +0 -663
  446. package/docs/VALIDATION.md +0 -363
  447. package/lib/agents/cline.plugin.json +0 -29
  448. package/lib/agents/copilot.plugin.json +0 -24
  449. package/lib/agents/kilocode.plugin.json +0 -22
  450. package/lib/agents/opencode.plugin.json +0 -23
  451. package/lib/agents/roo.plugin.json +0 -30
  452. package/lib/beads-bootstrap.js +0 -225
  453. package/lib/beads-health-check.js +0 -188
  454. package/lib/commands/commands-reset.js +0 -147
  455. package/opencode.json +0 -67
  456. package/scripts/beads-context.test.js +0 -584
  457. package/scripts/github-beads-sync/comment.mjs +0 -64
  458. package/scripts/github-beads-sync/config.mjs +0 -148
  459. package/scripts/github-beads-sync/github-api.mjs +0 -131
  460. package/scripts/github-beads-sync/index.mjs +0 -356
  461. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  462. package/scripts/github-beads-sync/mapping.mjs +0 -132
  463. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  464. package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
  465. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  466. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  467. package/scripts/github-beads-sync.config.json +0 -26
  468. package/scripts/sync-commands.js +0 -600
@@ -0,0 +1,1109 @@
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
+ // The *.jsonl sidecars this migrator knows how to map into the Kernel. Any other *.jsonl
99
+ // present in the store is unmapped data; the loader records it so the import can report it
100
+ // as an honest gap rather than dropping it silently.
101
+ const HANDLED_SIDECARS = new Set([
102
+ 'issues.jsonl',
103
+ 'comments.jsonl',
104
+ 'dependencies.jsonl',
105
+ 'events.jsonl',
106
+ 'interactions.jsonl',
107
+ 'labels.jsonl',
108
+ ]);
109
+
110
+ // Scan the same locations readBeadsSidecar reads (the dir itself, a `backup/` subdir, and the
111
+ // parent) for *.jsonl files this migrator does NOT map. Returns their sorted, de-duplicated
112
+ // basenames (`unmigrated`) so importBeadsSnapshot can surface each as a `sidecar.<name>`
113
+ // field-gap — closing the "an unknown/config sidecar was dropped without a word" data-loss
114
+ // class — plus any `scanErrors`: a candidate dir ABSENT from disk (ENOENT) is expected and
115
+ // swallowed, but a REAL scan failure (EACCES/ENOTDIR/…) could hide sidecars silently, so it is
116
+ // reported too rather than masquerading as "no sidecars".
117
+ function detectUnmigratedSidecars(beadsDir) {
118
+ const dirs = [beadsDir, path.join(beadsDir, 'backup'), path.dirname(beadsDir)];
119
+ const unmigrated = new Set();
120
+ const scanErrors = [];
121
+ for (const dir of dirs) {
122
+ let entries;
123
+ try {
124
+ entries = fs.readdirSync(dir);
125
+ } catch (err) {
126
+ // A missing candidate dir is the normal case (not every layout has a backup/ subdir);
127
+ // only ENOENT is safe to swallow. Any other error is a real scan failure that may hide
128
+ // sidecars — record it so importBeadsSnapshot can surface an honest gap.
129
+ if (err && err.code !== 'ENOENT') {
130
+ scanErrors.push({ dir, code: err.code || 'EUNKNOWN' });
131
+ }
132
+ continue;
133
+ }
134
+ for (const entry of entries) {
135
+ if (entry.endsWith('.jsonl') && !HANDLED_SIDECARS.has(entry)) {
136
+ unmigrated.add(entry);
137
+ }
138
+ }
139
+ }
140
+ return {
141
+ unmigrated: [...unmigrated].sort((a, b) => a.localeCompare(b)),
142
+ scanErrors,
143
+ };
144
+ }
145
+
146
+ function loadBeadsSnapshotFromDirectory(beadsDir) {
147
+ if (!beadsDir || typeof beadsDir !== 'string') {
148
+ throw new TypeError('beadsDir must be a directory path');
149
+ }
150
+
151
+ const { unmigrated, scanErrors } = detectUnmigratedSidecars(beadsDir);
152
+ return {
153
+ beadsDir,
154
+ issues: readBeadsSidecar(beadsDir, 'issues.jsonl'),
155
+ comments: readBeadsSidecar(beadsDir, 'comments.jsonl'),
156
+ dependencies: readBeadsSidecar(beadsDir, 'dependencies.jsonl'),
157
+ events: readBeadsSidecar(beadsDir, 'events.jsonl'),
158
+ interactions: readBeadsSidecar(beadsDir, 'interactions.jsonl'),
159
+ labels: readBeadsSidecar(beadsDir, 'labels.jsonl'),
160
+ unmigratedSidecars: unmigrated,
161
+ sidecarScanErrors: scanErrors,
162
+ };
163
+ }
164
+
165
+ function normalizeStatus(status) {
166
+ const normalized = String(status || 'open').trim().toLowerCase().replace(/[-\s]+/g, '_');
167
+ return normalized || 'open';
168
+ }
169
+
170
+ // Map a Beads issue_type to a canonical Kernel type, returning the alias label to preserve
171
+ // when a legacy type (feature/story/chore/spike) collapses to `task`.
172
+ function normalizeKernelType(beadsType) {
173
+ const raw = String(beadsType || '').trim().toLowerCase();
174
+ if (Object.hasOwn(LEGACY_TYPE_ALIASES, raw)) {
175
+ return { type: LEGACY_TYPE_ALIASES[raw], aliasLabel: raw };
176
+ }
177
+ return { type: raw || 'task', aliasLabel: null };
178
+ }
179
+
180
+ // Collapse the legacy Beads `closed` status onto a terminal Kernel status before it reaches
181
+ // the D18 validation layer. Non-`closed` statuses pass through their normalized form.
182
+ function normalizeKernelStatus(issue) {
183
+ const status = normalizeStatus(issue.status);
184
+ if (status !== 'closed') {
185
+ return status;
186
+ }
187
+ return CANCELLED_CLOSE_REASON.test(String(issue.close_reason || '')) ? 'cancelled' : 'done';
188
+ }
189
+
190
+ // Project terminal Kernel statuses back onto the Beads `closed` vocabulary on export so a
191
+ // Beads -> Kernel -> Beads round-trip stays identity (Beads has no done/cancelled status).
192
+ function beadsStatusForKernelStatus(status) {
193
+ return status === 'done' || status === 'cancelled' ? 'closed' : status;
194
+ }
195
+
196
+ // Serialize a Beads labels array into the Kernel `labels` TEXT column as a JSON array string,
197
+ // deduping and dropping blanks. Returns null when there is nothing to store.
198
+ function serializeLabels(labels) {
199
+ if (!Array.isArray(labels) || labels.length === 0) {
200
+ return null;
201
+ }
202
+ const unique = Array.from(new Set(
203
+ labels.filter(label => label !== null && label !== undefined && String(label).trim() !== ''),
204
+ ));
205
+ return unique.length > 0 ? JSON.stringify(unique) : null;
206
+ }
207
+
208
+ // Inverse of serializeLabels for the export path. Tolerates already-array values, a null/empty
209
+ // column, and malformed JSON (never throws — JSON.parse(null) would).
210
+ function deserializeLabels(value) {
211
+ if (Array.isArray(value)) {
212
+ return value;
213
+ }
214
+ if (typeof value !== 'string' || value.trim() === '') {
215
+ return [];
216
+ }
217
+ try {
218
+ const parsed = JSON.parse(value);
219
+ return Array.isArray(parsed) ? parsed : [];
220
+ } catch (_error) { // NOSONAR S2486 — malformed labels column is non-fatal; fall back to no labels
221
+ return [];
222
+ }
223
+ }
224
+
225
+ // Serialize Beads acceptance_criteria into the Kernel TEXT column: strings are stored as-is,
226
+ // structured values are JSON-encoded. Returns null when absent.
227
+ function serializeAcceptanceCriteria(value) {
228
+ if (value === null || value === undefined || value === '') {
229
+ return null;
230
+ }
231
+ return typeof value === 'string' ? value : JSON.stringify(value);
232
+ }
233
+
234
+ // Inverse of serializeAcceptanceCriteria for the export path: a JSON-encoded array/object is
235
+ // parsed back to its structured shape so a round-trip restores the original value; plain prose
236
+ // strings (which never start with [ or {) pass through unchanged.
237
+ function deserializeAcceptanceCriteria(value) {
238
+ if (typeof value !== 'string') {
239
+ return value;
240
+ }
241
+ const trimmed = value.trim();
242
+ if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
243
+ try {
244
+ return JSON.parse(value);
245
+ } catch (_error) { // NOSONAR S2486 — bracket-prefixed but invalid JSON; treat as a plain string
246
+ return value;
247
+ }
248
+ }
249
+ return value;
250
+ }
251
+
252
+ function normalizePriority(priority) {
253
+ if (typeof priority === 'number' && Number.isFinite(priority)) {
254
+ return {
255
+ label: `P${priority}`,
256
+ rank: priority,
257
+ beads: priority,
258
+ };
259
+ }
260
+
261
+ const raw = String(priority ?? 'P2').trim().toUpperCase();
262
+ const match = raw.match(/^P?(\d+)$/);
263
+ if (!match) {
264
+ return {
265
+ label: raw || 'P2',
266
+ rank: 2,
267
+ beads: 2,
268
+ };
269
+ }
270
+
271
+ const rank = Number.parseInt(match[1], 10);
272
+ return {
273
+ label: `P${rank}`,
274
+ rank,
275
+ beads: rank,
276
+ };
277
+ }
278
+
279
+ function safeIdPart(value) {
280
+ const input = String(value || 'unknown').toLowerCase();
281
+ const parts = [];
282
+ let pendingSeparator = false;
283
+
284
+ for (const char of input) {
285
+ const code = char.charCodeAt(0);
286
+ const isAsciiLetter = code >= 97 && code <= 122;
287
+ const isDigit = code >= 48 && code <= 57;
288
+ if (isAsciiLetter || isDigit) {
289
+ if (pendingSeparator && parts.length > 0) {
290
+ parts.push('-');
291
+ }
292
+ parts.push(char);
293
+ pendingSeparator = false;
294
+ } else {
295
+ pendingSeparator = parts.length > 0;
296
+ }
297
+ }
298
+
299
+ return parts.join('') || 'unknown';
300
+ }
301
+
302
+ function encodedIdPart(value) {
303
+ return Buffer.from(String(value ?? ''), 'utf8').toString('hex') || '00';
304
+ }
305
+
306
+ function uniqueBy(records, keyFn) {
307
+ const seen = new Set();
308
+ const result = [];
309
+ for (const record of records) {
310
+ const key = keyFn(record);
311
+ if (!seen.has(key)) {
312
+ seen.add(key);
313
+ result.push(record);
314
+ }
315
+ }
316
+ return result;
317
+ }
318
+
319
+ function collectDependencies(snapshot) {
320
+ const dependencies = [...(snapshot.dependencies || [])];
321
+ for (const issue of snapshot.issues || []) {
322
+ if (Array.isArray(issue.dependencies)) {
323
+ dependencies.push(...issue.dependencies);
324
+ }
325
+ }
326
+
327
+ return uniqueBy(
328
+ dependencies.filter(dependency => dependency?.issue_id && dependency?.depends_on_id),
329
+ dependency => `${dependency.issue_id}\0${dependency.depends_on_id}\0${dependency.type || 'blocks'}`,
330
+ );
331
+ }
332
+
333
+ function getLabelsByIssue(snapshot) {
334
+ const byIssue = new Map();
335
+
336
+ for (const labelRecord of snapshot.labels || []) {
337
+ const issueId = labelRecord.issue_id || labelRecord.issueId;
338
+ const label = labelRecord.label || labelRecord.name;
339
+ if (!issueId || !label) continue;
340
+ if (!byIssue.has(issueId)) {
341
+ byIssue.set(issueId, []);
342
+ }
343
+ byIssue.get(issueId).push(label);
344
+ }
345
+
346
+ return byIssue;
347
+ }
348
+
349
+ function applyLabelSidecar(issues, snapshot) {
350
+ const labelsByIssue = getLabelsByIssue(snapshot);
351
+ if (labelsByIssue.size === 0) {
352
+ return issues;
353
+ }
354
+
355
+ return issues.map(issue => {
356
+ const sidecarLabels = labelsByIssue.get(issue.id) || [];
357
+ if (sidecarLabels.length === 0) {
358
+ return issue;
359
+ }
360
+ const labels = Array.from(new Set([...(Array.isArray(issue.labels) ? issue.labels : []), ...sidecarLabels]));
361
+ return {
362
+ ...issue,
363
+ labels,
364
+ };
365
+ });
366
+ }
367
+
368
+ function collectComments(snapshot) {
369
+ const comments = [...(snapshot.comments || [])];
370
+ for (const issue of snapshot.issues || []) {
371
+ if (Array.isArray(issue.comments)) {
372
+ for (const comment of issue.comments) {
373
+ comments.push({
374
+ ...comment,
375
+ issue_id: comment.issue_id || issue.id,
376
+ });
377
+ }
378
+ }
379
+ if (issue.notes && issue.notes !== issue.description && issue.notes !== issue.body) {
380
+ comments.push({
381
+ id: `beads-note-${encodedIdPart(issue.id)}`,
382
+ issue_id: issue.id,
383
+ body: issue.notes,
384
+ actor: issue.updated_by || issue.created_by || 'beads',
385
+ created_at: issue.updated_at || issue.created_at,
386
+ });
387
+ }
388
+ }
389
+
390
+ return uniqueBy(
391
+ comments.filter(comment => comment?.issue_id && (comment.body || comment.text)),
392
+ comment => `${comment.id || ''}\0${comment.issue_id}\0${comment.created_at || ''}\0${comment.body || comment.text}`,
393
+ );
394
+ }
395
+
396
+ function addGap(gaps, seen, field, reason) {
397
+ if (seen.has(field)) return;
398
+ seen.add(field);
399
+ gaps.push({ field, reason });
400
+ }
401
+
402
+ function hasBeadsFieldValue(value) {
403
+ if (value === null || value === undefined) return false;
404
+ if (typeof value === 'string') return value.trim() !== '' && value !== '{}';
405
+ if (Array.isArray(value)) return value.length > 0;
406
+ if (typeof value === 'object') return Object.keys(value).length > 0;
407
+ return true;
408
+ }
409
+
410
+ function parseMetadataObject(value) {
411
+ if (value === null || value === undefined || value === '') return {};
412
+ if (typeof value === 'object' && !Array.isArray(value)) return { ...value };
413
+ if (typeof value !== 'string') return {};
414
+ try {
415
+ const parsed = JSON.parse(value);
416
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
417
+ } catch (_error) {
418
+ return {};
419
+ }
420
+ }
421
+
422
+ // Carry a Beads issue's metadata onto the Kernel `metadata` TEXT column for full-fidelity import.
423
+ // The forge_projection key is an internal Forge -> Beads projection marker (written on export and
424
+ // consumed as close-event provenance via getForgeProjectionOrigin), NOT user data, so it is
425
+ // stripped here; the remaining object keys are preserved verbatim as a JSON string. Non-object
426
+ // metadata is kept as its raw value. Returns null when nothing user-authored remains.
427
+ function serializeBeadsMetadata(value) {
428
+ if (!hasBeadsFieldValue(value)) return null;
429
+ let metadata = null;
430
+ if (typeof value === 'object' && !Array.isArray(value)) {
431
+ metadata = { ...value };
432
+ } else if (typeof value === 'string') {
433
+ try {
434
+ const parsed = JSON.parse(value);
435
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
436
+ metadata = parsed;
437
+ }
438
+ } catch (_error) { // NOSONAR S2486 — non-JSON metadata is preserved verbatim below
439
+ metadata = null;
440
+ }
441
+ }
442
+ if (metadata === null) {
443
+ return typeof value === 'string' ? value : JSON.stringify(value);
444
+ }
445
+ delete metadata[FORGE_PROJECTION_METADATA_KEY];
446
+ return Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null;
447
+ }
448
+
449
+ // Beads issue fields with no dedicated Kernel column are folded into the metadata blob so nothing
450
+ // is silently dropped: the `owner` (only when not already captured on created_by/assignee — it
451
+ // primarily maps to the Kernel assignee), the `external_ref` (e.g. a linked GitHub issue/run),
452
+ // and the work-start timestamp `started_at`. A raw (non-object) metadata blob that must coexist
453
+ // is nested under metadata.beads_metadata rather than discarded. NOTE: beads-internal/derived
454
+ // fields (content_hash, agent_state, compaction_*, ephemeral, and other runtime plumbing) are
455
+ // intentionally NOT carried — they are not user data and would only pollute the Kernel row.
456
+ function serializeIssueMetadata(issue, resolved = {}) {
457
+ const base = serializeBeadsMetadata(issue.metadata);
458
+ let metadata = {};
459
+ let rawPreserved = null;
460
+ if (base !== null) {
461
+ try {
462
+ const parsed = JSON.parse(base);
463
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
464
+ metadata = parsed;
465
+ } else {
466
+ rawPreserved = base;
467
+ }
468
+ } catch (_error) { // NOSONAR S2486 — non-JSON metadata is preserved via beads_metadata below
469
+ rawPreserved = base;
470
+ }
471
+ }
472
+
473
+ // Compare trimmed on BOTH sides so a padded assignee/created_by does not double-store the owner.
474
+ const trim = value => (typeof value === 'string' ? value.trim() : value);
475
+ const owner = trim(issue.owner);
476
+ const ownerCaptured = owner && (owner === trim(resolved.createdBy) || owner === trim(resolved.assignee));
477
+ if (owner && !ownerCaptured) {
478
+ metadata.beads_owner = issue.owner;
479
+ }
480
+ if (hasBeadsFieldValue(issue.external_ref)) {
481
+ metadata.beads_external_ref = issue.external_ref;
482
+ }
483
+ if (hasBeadsFieldValue(issue.started_at)) {
484
+ metadata.beads_started_at = issue.started_at;
485
+ }
486
+
487
+ if (rawPreserved !== null) {
488
+ if (Object.keys(metadata).length === 0) return rawPreserved;
489
+ metadata.beads_metadata = rawPreserved;
490
+ }
491
+ return Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null;
492
+ }
493
+
494
+ function hasFiniteRevisionValue(value) {
495
+ if (typeof value === 'number') return Number.isFinite(value);
496
+ if (typeof value !== 'string') return false;
497
+ const trimmed = value.trim();
498
+ return trimmed !== '' && Number.isFinite(Number(trimmed));
499
+ }
500
+
501
+ function isValidForgeProjection(projection, issueId) {
502
+ if (!projection || typeof projection !== 'object' || Array.isArray(projection)) return false;
503
+ return projection.source === 'forge-kernel'
504
+ && projection.target === 'beads'
505
+ && projection.entity_type === 'issue'
506
+ && projection.entity_id === issueId
507
+ && hasFiniteRevisionValue(projection.entity_revision)
508
+ && typeof projection.payload_hash === 'string'
509
+ && projection.payload_hash !== '';
510
+ }
511
+
512
+ function getForgeProjectionOrigin(issue = {}, currentPayload = {}) {
513
+ const metadata = parseMetadataObject(issue.metadata);
514
+ const projection = metadata[FORGE_PROJECTION_METADATA_KEY];
515
+ if (!isValidForgeProjection(projection, issue.id)) return null;
516
+ const importedPayloadHash = JSON.stringify(currentPayload);
517
+ if (projection.payload_hash !== importedPayloadHash) return null;
518
+
519
+ return {
520
+ source: 'forge-kernel',
521
+ target: 'beads',
522
+ entity_type: 'issue',
523
+ entity_id: issue.id,
524
+ entity_revision: normalizeEntityRevision(projection.entity_revision),
525
+ payload_hash: projection.payload_hash,
526
+ imported_payload_hash: importedPayloadHash,
527
+ };
528
+ }
529
+
530
+ function buildCloseProjectionPayload(closeMetadata = {}) {
531
+ return {
532
+ closed_at: closeMetadata.closed_at || null,
533
+ close_reason: closeMetadata.close_reason || null,
534
+ };
535
+ }
536
+
537
+ function buildCloseProjectionFingerprint(issue = {}) {
538
+ return {
539
+ ...buildCloseProjectionPayload(issue),
540
+ actor: issue.closed_by || issue.created_by || 'beads',
541
+ };
542
+ }
543
+
544
+ function buildForgeProjectionMetadata(issue = {}, closeMetadata = {}) {
545
+ const metadata = parseMetadataObject(issue.metadata);
546
+ metadata[FORGE_PROJECTION_METADATA_KEY] = {
547
+ source: 'forge-kernel',
548
+ target: 'beads',
549
+ entity_type: 'issue',
550
+ entity_id: issue.id,
551
+ entity_revision: normalizeEntityRevision(issue.entity_revision),
552
+ payload_hash: JSON.stringify(buildCloseProjectionFingerprint({ ...issue, ...closeMetadata })),
553
+ };
554
+ return metadata;
555
+ }
556
+
557
+ function buildFidelityReport({ kernel, gaps, eventCount = 0, interactionCount = 0 }) {
558
+ return {
559
+ summary: {
560
+ issues: kernel.issues.length,
561
+ dependencies: kernel.dependencies.length,
562
+ comments: kernel.comments.length,
563
+ closeEvents: kernel.events.filter(event => event.event_type === 'beads.issue.closed').length,
564
+ // Legacy activity records that now land in kernel_events instead of being dropped.
565
+ events: eventCount,
566
+ interactions: interactionCount,
567
+ activityEvents: kernel.activityEvents ? kernel.activityEvents.length : 0,
568
+ unsupportedFields: gaps.length,
569
+ },
570
+ preservedFields: [...PRESERVED_FIELDS],
571
+ gaps,
572
+ };
573
+ }
574
+
575
+ function mapBeadsIssueToKernel(issue, importedAt, gaps, seenGaps) {
576
+ const priority = normalizePriority(issue.priority);
577
+ const { type, aliasLabel } = normalizeKernelType(issue.issue_type || issue.type);
578
+ const beadsLabels = Array.isArray(issue.labels) ? issue.labels : [];
579
+ const labels = aliasLabel ? [...beadsLabels, aliasLabel] : beadsLabels;
580
+ for (const [field, reason] of UNSUPPORTED_ISSUE_FIELDS) {
581
+ if (hasBeadsFieldValue(issue[field])) {
582
+ addGap(gaps, seenGaps, `issues.${field}`, reason);
583
+ }
584
+ }
585
+
586
+ // Full-fidelity import onto dedicated Kernel columns: the beads author lands on created_by
587
+ // (falling back to owner when no author is recorded); the responsible owner lands on the
588
+ // assignee column (falling back after an explicit beads assignee); design/notes-adjacent
589
+ // authored content lands on the design column. Any owner NOT captured by created_by/assignee
590
+ // is preserved in the metadata blob (serializeIssueMetadata) so nothing is dropped.
591
+ const createdBy = issue.created_by || issue.owner || null;
592
+ const assignee = issue.assignee || issue.owner || null;
593
+
594
+ return {
595
+ id: issue.id,
596
+ title: issue.title || issue.id,
597
+ body: issue.description ?? issue.body ?? issue.notes ?? '',
598
+ type,
599
+ status: normalizeKernelStatus(issue),
600
+ priority: priority.label,
601
+ priority_rank: priority.rank,
602
+ labels: serializeLabels(labels),
603
+ acceptance_criteria: serializeAcceptanceCriteria(issue.acceptance_criteria),
604
+ design: hasBeadsFieldValue(issue.design) ? issue.design : null,
605
+ assignee,
606
+ created_by: createdBy,
607
+ metadata: serializeIssueMetadata(issue, { createdBy, assignee }),
608
+ created_at: issue.created_at || importedAt,
609
+ updated_at: issue.updated_at || issue.created_at || importedAt,
610
+ entity_revision: 0,
611
+ };
612
+ }
613
+
614
+ function mapBeadsDependencyToKernel(dependency, importedAt, gaps, seenGaps) {
615
+ // An empty "{}" metadata blob carries no data, so it is not a loss — only surface a gap when
616
+ // the dependency actually holds metadata (the Kernel dependency row has no metadata column).
617
+ if (hasBeadsFieldValue(dependency.metadata)) {
618
+ addGap(gaps, seenGaps, 'dependencies.metadata', 'no Kernel dependency metadata column in schema v1');
619
+ }
620
+ // The dependency creator has no dedicated Kernel column; retained as a documented gap rather
621
+ // than silently dropped (resolving it would require an additive kernel_dependencies column).
622
+ if (dependency.created_by) {
623
+ addGap(gaps, seenGaps, 'dependencies.created_by', 'no Kernel dependency creator column in schema v1');
624
+ }
625
+
626
+ return {
627
+ id: [
628
+ 'beads-dependency',
629
+ encodedIdPart(dependency.issue_id),
630
+ encodedIdPart(dependency.depends_on_id),
631
+ encodedIdPart(dependency.type || 'blocks'),
632
+ ].join('-'),
633
+ issue_id: dependency.issue_id,
634
+ blocks_issue_id: dependency.depends_on_id,
635
+ dependency_type: dependency.type || 'blocks',
636
+ created_at: dependency.created_at || importedAt,
637
+ };
638
+ }
639
+
640
+ function mapBeadsCommentToKernel(comment, importedAt) {
641
+ return {
642
+ id: comment.id || [
643
+ 'beads-comment',
644
+ encodedIdPart(comment.issue_id),
645
+ encodedIdPart(comment.created_at || importedAt),
646
+ ].join('-'),
647
+ issue_id: comment.issue_id,
648
+ body: comment.body ?? comment.text ?? '',
649
+ actor: comment.actor || comment.author || 'beads',
650
+ visibility: comment.visibility || 'local',
651
+ created_at: comment.created_at || importedAt,
652
+ };
653
+ }
654
+
655
+ function buildPriorityEvent(issue, importedAt) {
656
+ const priority = normalizePriority(issue.priority);
657
+ return {
658
+ id: `beads-priority-${encodedIdPart(issue.id)}`,
659
+ issue_id: issue.id,
660
+ old_priority: null,
661
+ new_priority: priority.label,
662
+ priority_rank: priority.rank,
663
+ actor: issue.created_by || 'beads',
664
+ created_at: issue.updated_at || issue.created_at || importedAt,
665
+ };
666
+ }
667
+
668
+ function normalizeEntityRevision(value) {
669
+ const revision = Number(value);
670
+ return Number.isFinite(revision) ? revision : 0;
671
+ }
672
+
673
+ function buildCloseEvent(issue, importedAt) {
674
+ if (!issue.closed_at && !issue.close_reason) {
675
+ return null;
676
+ }
677
+
678
+ const createdAt = issue.closed_at || issue.updated_at || importedAt;
679
+ const payload = buildCloseProjectionPayload(issue);
680
+ const projectionOrigin = getForgeProjectionOrigin(issue, buildCloseProjectionFingerprint(issue));
681
+ if (projectionOrigin) {
682
+ payload.projection_origin = projectionOrigin;
683
+ }
684
+
685
+ return {
686
+ id: `beads-close-${encodedIdPart(issue.id)}`,
687
+ entity_type: 'issue',
688
+ entity_id: issue.id,
689
+ event_type: 'beads.issue.closed',
690
+ idempotency_key: `beads-close:${issue.id}:${createdAt}`,
691
+ expected_revision: normalizeEntityRevision(projectionOrigin?.entity_revision ?? issue.entity_revision),
692
+ actor: issue.closed_by || issue.created_by || 'beads',
693
+ origin: 'beads_import',
694
+ payload_json: JSON.stringify(payload),
695
+ created_at: createdAt,
696
+ };
697
+ }
698
+
699
+ // Map a legacy Beads activity event (events.jsonl: created/closed/status_changed/updated/
700
+ // label_added/claimed/…) onto a Kernel event row destined for kernel_events. The beads kind is
701
+ // namespaced as `beads.event.<kind>` and preserved verbatim in the payload alongside the
702
+ // old/new value and any inline comment, so no field is lost. id/idempotency_key derive from the
703
+ // original beads event id, making the write idempotent on re-migration.
704
+ function mapBeadsActivityEventToKernel(event = {}, importedAt) {
705
+ const kind = String(event.event_type || event.kind || 'event');
706
+ const createdAt = event.created_at || importedAt;
707
+ // For id-less events, derive the id/idempotency_key from STABLE record fields
708
+ // ONLY (never importedAt) so re-migrating the same legacy record maps to the
709
+ // same id and does not insert a duplicate kernel_events row.
710
+ const source = event.id != null && event.id !== ''
711
+ ? String(event.id)
712
+ : `${event.issue_id ?? ''}:${kind}:${event.created_at ?? ''}:${event.old_value ?? ''}:${event.new_value ?? ''}:${event.comment ?? ''}`;
713
+ return {
714
+ id: `beads-event-${encodedIdPart(source)}`,
715
+ entity_type: 'issue',
716
+ entity_id: event.issue_id != null ? String(event.issue_id) : '',
717
+ event_type: `beads.event.${kind}`,
718
+ idempotency_key: `beads-event:${source}`,
719
+ expected_revision: 0,
720
+ actor: event.actor || 'beads',
721
+ origin: 'beads_import',
722
+ payload_json: JSON.stringify({
723
+ kind,
724
+ old_value: event.old_value ?? null,
725
+ new_value: event.new_value ?? null,
726
+ comment: event.comment ?? null,
727
+ }),
728
+ created_at: createdAt,
729
+ };
730
+ }
731
+
732
+ // Map a legacy Beads interaction/memory record (interactions.jsonl: agent field_change/note/…
733
+ // audit entries) onto a Kernel event row destined for kernel_events. The interaction kind is
734
+ // namespaced as `beads.interaction.<kind>`; the structured `extra` payload is spread into the
735
+ // event payload (preserved verbatim) alongside the kind. id/idempotency_key derive from the
736
+ // original beads interaction id, making the write idempotent on re-migration.
737
+ function mapBeadsInteractionToKernel(interaction = {}, importedAt) {
738
+ const kind = String(interaction.kind || interaction.event_type || 'interaction');
739
+ const createdAt = interaction.created_at || importedAt;
740
+ // For id-less interactions, derive the id/idempotency_key from STABLE record
741
+ // fields ONLY (never importedAt) so re-migration is idempotent (no dup rows).
742
+ const source = interaction.id != null && interaction.id !== ''
743
+ ? String(interaction.id)
744
+ : `${interaction.issue_id ?? ''}:${kind}:${interaction.created_at ?? ''}:${JSON.stringify(interaction.extra ?? null)}`;
745
+ const extra = interaction.extra && typeof interaction.extra === 'object' && !Array.isArray(interaction.extra)
746
+ ? interaction.extra
747
+ : { extra: interaction.extra ?? null };
748
+ return {
749
+ id: `beads-interaction-${encodedIdPart(source)}`,
750
+ entity_type: 'issue',
751
+ entity_id: interaction.issue_id != null ? String(interaction.issue_id) : '',
752
+ event_type: `beads.interaction.${kind}`,
753
+ idempotency_key: `beads-interaction:${source}`,
754
+ expected_revision: 0,
755
+ actor: interaction.actor || 'beads',
756
+ origin: 'beads_import',
757
+ payload_json: JSON.stringify({ kind, ...extra }),
758
+ created_at: createdAt,
759
+ };
760
+ }
761
+
762
+ function importBeadsSnapshot(snapshot = {}, options = {}) {
763
+ const importedAt = options.importedAt || new Date().toISOString();
764
+ const gaps = [];
765
+ const seenGaps = new Set();
766
+ const rawIssues = Array.isArray(snapshot.issues) ? snapshot.issues : [];
767
+ const sourceIssues = applyLabelSidecar(rawIssues, snapshot);
768
+ const normalizedSnapshot = {
769
+ ...snapshot,
770
+ issues: sourceIssues,
771
+ };
772
+ // The legacy activity log (lifecycle events + agent interactions) is no longer dropped: both
773
+ // sidecars are mapped into a single kernel_events-bound bundle, preserving id/kind/actor/
774
+ // timestamp/payload. events first, then interactions, so the write order is deterministic.
775
+ const activityEvents = [
776
+ ...(Array.isArray(snapshot.events) ? snapshot.events : []).map(event => mapBeadsActivityEventToKernel(event, importedAt)),
777
+ ...(Array.isArray(snapshot.interactions) ? snapshot.interactions : []).map(interaction => mapBeadsInteractionToKernel(interaction, importedAt)),
778
+ ];
779
+ // Honest field-gap report: any *.jsonl sidecar present in the store that this migrator does
780
+ // not map (e.g. config.jsonl, or a future/unknown sidecar) is surfaced here rather than
781
+ // silently dropped. The loader records these on `unmigratedSidecars`.
782
+ for (const sidecar of Array.isArray(snapshot.unmigratedSidecars) ? snapshot.unmigratedSidecars : []) {
783
+ addGap(gaps, seenGaps, `sidecar.${sidecar}`, 'present in the Beads store but has no Kernel target — not migrated');
784
+ }
785
+ // A directory the sidecar scan could not read (EACCES/ENOTDIR/…, but NOT a merely-absent dir)
786
+ // might hide sidecars; surface it so the loss is never silent.
787
+ for (const scanError of Array.isArray(snapshot.sidecarScanErrors) ? snapshot.sidecarScanErrors : []) {
788
+ addGap(
789
+ gaps,
790
+ seenGaps,
791
+ `sidecar-scan.${path.basename(scanError.dir)}`,
792
+ `could not scan ${scanError.dir} for unmapped sidecars (${scanError.code}) — any sidecars there were not migrated`,
793
+ );
794
+ }
795
+ const kernel = {
796
+ issues: sourceIssues.map(issue => mapBeadsIssueToKernel(issue, importedAt, gaps, seenGaps)),
797
+ dependencies: collectDependencies(normalizedSnapshot)
798
+ .map(dependency => mapBeadsDependencyToKernel(dependency, importedAt, gaps, seenGaps)),
799
+ comments: collectComments(normalizedSnapshot).map(comment => mapBeadsCommentToKernel(comment, importedAt)),
800
+ priorityEvents: sourceIssues.map(issue => buildPriorityEvent(issue, importedAt)),
801
+ events: sourceIssues.map(issue => buildCloseEvent(issue, importedAt)).filter(Boolean),
802
+ activityEvents,
803
+ };
804
+
805
+ return {
806
+ source: 'beads',
807
+ authority: 'forge-kernel',
808
+ kernel,
809
+ report: buildFidelityReport({
810
+ kernel,
811
+ gaps,
812
+ eventCount: Array.isArray(snapshot.events) ? snapshot.events.length : 0,
813
+ interactionCount: Array.isArray(snapshot.interactions) ? snapshot.interactions.length : 0,
814
+ }),
815
+ rollback: {
816
+ available: true,
817
+ mode: 'import-only',
818
+ reason: 'Import did not mutate Beads files; discard imported Kernel records to roll back.',
819
+ },
820
+ };
821
+ }
822
+
823
+ function getCloseMetadataByIssue(kernel) {
824
+ const closeByIssue = new Map();
825
+ for (const event of kernel.events || []) {
826
+ if (event.event_type !== 'beads.issue.closed') continue;
827
+ let payload;
828
+ try {
829
+ payload = JSON.parse(event.payload_json || '{}');
830
+ } catch (_error) {
831
+ payload = {};
832
+ }
833
+ closeByIssue.set(event.entity_id, {
834
+ closed_at: payload.closed_at || event.created_at || null,
835
+ close_reason: payload.close_reason || null,
836
+ });
837
+ }
838
+ return closeByIssue;
839
+ }
840
+
841
+ function getDependenciesByIssue(kernel) {
842
+ const byIssue = new Map();
843
+ for (const dependency of kernel.dependencies || []) {
844
+ const beadsDependency = {
845
+ issue_id: dependency.issue_id,
846
+ depends_on_id: dependency.blocks_issue_id,
847
+ type: dependency.dependency_type || 'blocks',
848
+ created_at: dependency.created_at,
849
+ metadata: '{}',
850
+ };
851
+ if (dependency.created_by) {
852
+ beadsDependency.created_by = dependency.created_by;
853
+ }
854
+ if (!byIssue.has(dependency.issue_id)) {
855
+ byIssue.set(dependency.issue_id, []);
856
+ }
857
+ byIssue.get(dependency.issue_id).push(beadsDependency);
858
+ }
859
+ return byIssue;
860
+ }
861
+
862
+ function isBlockingDependencyType(type) {
863
+ return (type || 'blocks') === 'blocks';
864
+ }
865
+
866
+ function getBlockingDependentsByIssue(kernel) {
867
+ const byIssue = new Map();
868
+ for (const dependency of kernel.dependencies || []) {
869
+ if (!isBlockingDependencyType(dependency.dependency_type)) continue;
870
+ if (!dependency.blocks_issue_id) continue;
871
+ if (!byIssue.has(dependency.blocks_issue_id)) {
872
+ byIssue.set(dependency.blocks_issue_id, []);
873
+ }
874
+ byIssue.get(dependency.blocks_issue_id).push(dependency.issue_id);
875
+ }
876
+ return byIssue;
877
+ }
878
+
879
+ function mapKernelIssueToBeads(issue, dependenciesByIssue, dependentsByIssue, closeByIssue) {
880
+ const closeMetadata = closeByIssue.get(issue.id) || {};
881
+ const priority = normalizePriority(issue.priority);
882
+ const dependencies = dependenciesByIssue.get(issue.id) || [];
883
+ // Resolve the final Beads close payload — including the synthesized cancellation marker for a
884
+ // cancelled Kernel issue — BEFORE building projection metadata. forge_projection.payload_hash
885
+ // must be computed over the close_reason actually exported; otherwise re-import recomputes a
886
+ // different hash, drops projection_origin, and loses the entity_revision provenance.
887
+ const effectiveClose = { ...closeMetadata };
888
+ if (issue.status === 'cancelled' && !CANCELLED_CLOSE_REASON.test(String(effectiveClose.close_reason || ''))) {
889
+ effectiveClose.close_reason = effectiveClose.close_reason
890
+ ? `${effectiveClose.close_reason} (cancelled)`
891
+ : 'cancelled';
892
+ }
893
+ const exported = {
894
+ _type: 'issue',
895
+ id: issue.id,
896
+ title: issue.title,
897
+ description: issue.body || '',
898
+ status: beadsStatusForKernelStatus(issue.status),
899
+ priority: priority.beads,
900
+ issue_type: issue.type || 'task',
901
+ created_at: issue.created_at,
902
+ updated_at: issue.updated_at,
903
+ dependencies,
904
+ dependency_count: dependencies.filter(dependency => isBlockingDependencyType(dependency.type)).length,
905
+ dependent_count: (dependentsByIssue.get(issue.id) || []).length,
906
+ };
907
+ const labels = deserializeLabels(issue.labels);
908
+ if (labels.length > 0) {
909
+ exported.labels = labels;
910
+ }
911
+ const acceptanceCriteria = deserializeAcceptanceCriteria(issue.acceptance_criteria);
912
+ if (acceptanceCriteria !== null && acceptanceCriteria !== undefined && acceptanceCriteria !== '') {
913
+ exported.acceptance_criteria = acceptanceCriteria;
914
+ }
915
+ if (issue.created_by) {
916
+ exported.created_by = issue.created_by;
917
+ }
918
+ exported.metadata = JSON.stringify(buildForgeProjectionMetadata(issue, effectiveClose));
919
+
920
+ if (effectiveClose.closed_at) {
921
+ exported.closed_at = effectiveClose.closed_at;
922
+ }
923
+ if (effectiveClose.close_reason) {
924
+ exported.close_reason = effectiveClose.close_reason;
925
+ }
926
+
927
+ return exported;
928
+ }
929
+
930
+ function mapKernelCommentToBeads(comment) {
931
+ return {
932
+ id: comment.id,
933
+ issue_id: comment.issue_id,
934
+ author: comment.actor,
935
+ text: comment.body,
936
+ created_at: comment.created_at,
937
+ };
938
+ }
939
+
940
+ function mapKernelDependencyToBeads(dependency) {
941
+ const exported = {
942
+ issue_id: dependency.issue_id,
943
+ depends_on_id: dependency.blocks_issue_id,
944
+ type: dependency.dependency_type || 'blocks',
945
+ created_at: dependency.created_at,
946
+ metadata: '{}',
947
+ };
948
+ if (dependency.created_by) {
949
+ exported.created_by = dependency.created_by;
950
+ }
951
+ return exported;
952
+ }
953
+
954
+ function buildExportFiles(kernel) {
955
+ const dependenciesByIssue = getDependenciesByIssue(kernel);
956
+ const dependentsByIssue = getBlockingDependentsByIssue(kernel);
957
+ const closeByIssue = getCloseMetadataByIssue(kernel);
958
+ const issues = (kernel.issues || []).map(issue => (
959
+ mapKernelIssueToBeads(issue, dependenciesByIssue, dependentsByIssue, closeByIssue)
960
+ ));
961
+ const comments = (kernel.comments || []).map(mapKernelCommentToBeads);
962
+ const dependencies = (kernel.dependencies || []).map(mapKernelDependencyToBeads);
963
+
964
+ return {
965
+ records: {
966
+ issues,
967
+ comments,
968
+ dependencies,
969
+ },
970
+ files: {
971
+ 'issues.jsonl': stringifyJsonl(issues),
972
+ 'comments.jsonl': stringifyJsonl(comments),
973
+ 'dependencies.jsonl': stringifyJsonl(dependencies),
974
+ },
975
+ };
976
+ }
977
+
978
+ function buildExportReport(kernel, gaps = []) {
979
+ return buildFidelityReport({
980
+ kernel: {
981
+ issues: kernel.issues || [],
982
+ dependencies: kernel.dependencies || [],
983
+ comments: kernel.comments || [],
984
+ events: kernel.events || [],
985
+ },
986
+ gaps,
987
+ });
988
+ }
989
+
990
+ function captureRollbackSnapshot(beadsDir, files) {
991
+ const snapshot = {};
992
+ for (const file of files) {
993
+ const filePath = path.join(beadsDir, file);
994
+ snapshot[file] = fs.existsSync(filePath)
995
+ ? { existed: true, content: fs.readFileSync(filePath, 'utf8') }
996
+ : { existed: false, content: null };
997
+ }
998
+
999
+ return {
1000
+ available: true,
1001
+ beadsDir,
1002
+ files: [...files],
1003
+ snapshot,
1004
+ };
1005
+ }
1006
+
1007
+ function exportKernelToBeads(kernel = {}, options = {}) {
1008
+ const dryRun = options.dryRun !== false;
1009
+ const { files } = buildExportFiles(kernel);
1010
+ const writes = BEADS_EXPORT_FILES.map(file => ({
1011
+ file,
1012
+ bytes: Buffer.byteLength(files[file], 'utf8'),
1013
+ }));
1014
+
1015
+ if (dryRun) {
1016
+ return {
1017
+ source: 'forge-kernel',
1018
+ target: 'beads',
1019
+ dryRun: true,
1020
+ files,
1021
+ writes,
1022
+ report: buildExportReport(kernel),
1023
+ rollback: {
1024
+ available: false,
1025
+ reason: 'Dry-run did not write Beads files.',
1026
+ },
1027
+ };
1028
+ }
1029
+
1030
+ if (!options.beadsDir || typeof options.beadsDir !== 'string') {
1031
+ throw new TypeError('beadsDir is required when dryRun is false');
1032
+ }
1033
+
1034
+ fs.mkdirSync(options.beadsDir, { recursive: true });
1035
+ const rollback = captureRollbackSnapshot(options.beadsDir, BEADS_EXPORT_FILES);
1036
+ try {
1037
+ for (const file of BEADS_EXPORT_FILES) {
1038
+ fs.writeFileSync(path.join(options.beadsDir, file), files[file]);
1039
+ }
1040
+ } catch (error) {
1041
+ rollbackBeadsExport(rollback);
1042
+ error.rollback = rollback;
1043
+ throw error;
1044
+ }
1045
+
1046
+ return {
1047
+ source: 'forge-kernel',
1048
+ target: 'beads',
1049
+ dryRun: false,
1050
+ files,
1051
+ writes,
1052
+ report: buildExportReport(kernel),
1053
+ rollback,
1054
+ };
1055
+ }
1056
+
1057
+ function rollbackBeadsExport(rollback) {
1058
+ if (!rollback?.available || !rollback.beadsDir || !rollback.snapshot) {
1059
+ throw new Error('Rollback snapshot is not available');
1060
+ }
1061
+
1062
+ for (const file of rollback.files || []) {
1063
+ const filePath = path.join(rollback.beadsDir, file);
1064
+ const entry = rollback.snapshot[file];
1065
+ if (!entry) continue;
1066
+ if (entry.existed) {
1067
+ fs.writeFileSync(filePath, entry.content);
1068
+ } else if (fs.existsSync(filePath)) {
1069
+ fs.unlinkSync(filePath);
1070
+ }
1071
+ }
1072
+
1073
+ return {
1074
+ rolledBack: true,
1075
+ files: [...(rollback.files || [])],
1076
+ };
1077
+ }
1078
+
1079
+ class BeadsKernelCompatibilityAdapter {
1080
+ constructor(options = {}) {
1081
+ this.id = options.id || 'beads-kernel-compat';
1082
+ this.kind = 'import-export';
1083
+ this.name = options.name || 'Beads Kernel Compatibility Adapter';
1084
+ this.version = options.version || '0.1.0';
1085
+ }
1086
+
1087
+ import(snapshot, options = {}) {
1088
+ return importBeadsSnapshot(snapshot, options);
1089
+ }
1090
+
1091
+ export(kernel, options = {}) {
1092
+ return exportKernelToBeads(kernel, options);
1093
+ }
1094
+ }
1095
+
1096
+ module.exports = {
1097
+ BEADS_EXPORT_FILES,
1098
+ PRESERVED_FIELDS,
1099
+ BeadsKernelCompatibilityAdapter,
1100
+ exportKernelToBeads,
1101
+ importBeadsSnapshot,
1102
+ mapBeadsActivityEventToKernel,
1103
+ mapBeadsInteractionToKernel,
1104
+ loadBeadsSnapshotFromDirectory,
1105
+ parseJsonl,
1106
+ rollbackBeadsExport,
1107
+ safeIdPart,
1108
+ stringifyJsonl,
1109
+ };