forge-workflow 0.0.9 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +3 -0
- package/.forge/hooks/forge-native-hook.js +245 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -106
- package/QUICKSTART.md +171 -0
- package/README.md +271 -363
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +466 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +118 -0
- package/docs/guides/SUPPORT.md +185 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +205 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/reference/TOOLCHAIN.md +658 -0
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +115 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1042 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +126 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-setup.js +225 -28
- package/lib/beads-sync-scaffold.js +36 -107
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +744 -70
- package/lib/commands/_manifest.js +91 -0
- package/lib/commands/_registry.js +85 -34
- package/lib/commands/_resolve-command-opts.js +261 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +192 -0
- package/lib/commands/hooks.js +242 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +598 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +66 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +523 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +436 -24
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +69 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +116 -0
- package/lib/commands/recap.js +61 -0
- package/lib/commands/recommend.js +22 -2
- package/lib/commands/release.js +91 -0
- package/lib/commands/remember.js +74 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +851 -979
- package/lib/commands/shepherd.js +436 -0
- package/lib/commands/ship.js +23 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +329 -11
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +15 -2
- package/lib/commands/test.js +58 -7
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +56 -25
- package/lib/commands/worktree.js +308 -128
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +946 -0
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +42 -17
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +697 -0
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +193 -0
- package/lib/global-flags.js +74 -0
- package/lib/greptile-match.js +7 -63
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +451 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/kernel/backing-issue.js +305 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2504 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +8 -4
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +157 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1273 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +166 -0
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2089 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +332 -23
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +191 -0
- package/lib/status/beads-snapshot.js +145 -0
- package/lib/status/presenter.js +216 -0
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +199 -0
- package/lib/workflow/enforce-stage.js +298 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +159 -14
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +46 -36
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +284 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +64 -232
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +144 -47
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +183 -79
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +108 -0
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +117 -369
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +251 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +302 -28
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +46 -52
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +134 -159
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/skills/ship/SKILL.md +251 -0
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/skills/status/SKILL.md +102 -0
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/ship.prompt.md +0 -215
- package/.github/prompts/status.prompt.md +0 -92
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -56
- package/.github/workflows/github-to-beads.yml +0 -97
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -255
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-health-check.js +0 -143
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -567
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -332
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -78
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- 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
|
+
};
|