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,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { cloneTableList, deepFreeze, field, index, table, validateKernelSchema } = require('./schema');
|
|
4
|
+
|
|
5
|
+
// Planning buckets are first-class entities, not string fields on issues (D5/D18,
|
|
6
|
+
// forge-2agy.9.2.7). Sprint/release/milestone each carry id, name, state, dates,
|
|
7
|
+
// owner/goal, ordering rank, completion rollups, and an entity revision.
|
|
8
|
+
const SPRINT_STATES = Object.freeze(['planned', 'active', 'completed', 'cancelled']);
|
|
9
|
+
const RELEASE_STATES = Object.freeze(['planned', 'in_progress', 'released', 'cancelled']);
|
|
10
|
+
const MILESTONE_STATES = Object.freeze(['planned', 'reached', 'missed', 'cancelled']);
|
|
11
|
+
|
|
12
|
+
// Board drag/drop and assignment operations emit Kernel events carrying
|
|
13
|
+
// expected_revision + idempotency_key (board rank / mutation event model,
|
|
14
|
+
// forge-2agy.9.2.6). See docs/reference/KERNEL_TAXONOMY_VALIDATION.md.
|
|
15
|
+
const BOARD_MUTATION_EVENT_TYPES = Object.freeze([
|
|
16
|
+
'issue.reordered',
|
|
17
|
+
'issue.status_changed',
|
|
18
|
+
'issue.sprint_assigned',
|
|
19
|
+
'issue.release_assigned',
|
|
20
|
+
'issue.blocked',
|
|
21
|
+
'issue.unblocked',
|
|
22
|
+
'issue.type_changed',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const ROLLUP = Object.freeze({ storageClass: 'read_model' });
|
|
26
|
+
|
|
27
|
+
// All three planning buckets share the same envelope (identity, state, ownership,
|
|
28
|
+
// ordering rank, completion rollups, and revision). `specificFields` carries the
|
|
29
|
+
// bucket-specific scheduling columns; this keeps the shared shape defined once.
|
|
30
|
+
function planningBucketTable(name, specificFields, indexes) {
|
|
31
|
+
return table(name, 'authority', [
|
|
32
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
33
|
+
field('name', 'TEXT', { notNull: true }),
|
|
34
|
+
field('state', 'TEXT', { notNull: true, default: "'planned'" }),
|
|
35
|
+
field('goal', 'TEXT'),
|
|
36
|
+
field('owner', 'TEXT'),
|
|
37
|
+
...specificFields,
|
|
38
|
+
field('rank', 'INTEGER', { notNull: true, default: '0' }),
|
|
39
|
+
field('total_count', 'INTEGER', { notNull: true, default: '0', ...ROLLUP }),
|
|
40
|
+
field('completed_count', 'INTEGER', { notNull: true, default: '0', ...ROLLUP }),
|
|
41
|
+
field('created_at', 'TEXT', { notNull: true }),
|
|
42
|
+
field('updated_at', 'TEXT', { notNull: true }),
|
|
43
|
+
field('entity_revision', 'INTEGER', { notNull: true, default: '0' }),
|
|
44
|
+
], indexes);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const PLANNING_BUCKET_TABLE_LIST = deepFreeze([
|
|
48
|
+
planningBucketTable('sprint', [
|
|
49
|
+
field('start_date', 'TEXT'),
|
|
50
|
+
field('end_date', 'TEXT'),
|
|
51
|
+
field('capacity', 'INTEGER'),
|
|
52
|
+
], [
|
|
53
|
+
index('idx_kernel_sprint_state', ['state']),
|
|
54
|
+
index('idx_kernel_sprint_rank', ['rank']),
|
|
55
|
+
]),
|
|
56
|
+
planningBucketTable('release', [
|
|
57
|
+
field('target_date', 'TEXT'),
|
|
58
|
+
field('released_at', 'TEXT'),
|
|
59
|
+
], [
|
|
60
|
+
index('idx_kernel_release_state', ['state']),
|
|
61
|
+
index('idx_kernel_release_rank', ['rank']),
|
|
62
|
+
]),
|
|
63
|
+
planningBucketTable('milestone', [
|
|
64
|
+
field('release_id', 'TEXT'),
|
|
65
|
+
field('target_date', 'TEXT'),
|
|
66
|
+
field('reached_at', 'TEXT'),
|
|
67
|
+
], [
|
|
68
|
+
index('idx_kernel_milestone_state', ['state']),
|
|
69
|
+
index('idx_kernel_milestone_release', ['release_id']),
|
|
70
|
+
]),
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
const PLANNING_BUCKET_TABLES = deepFreeze(
|
|
74
|
+
Object.fromEntries(PLANNING_BUCKET_TABLE_LIST.map(definition => [definition.name, definition])),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
/** @returns {{version: number, tables: object[]}} an isolated, deep-cloned copy of the planning-bucket schema. */
|
|
78
|
+
function getPlanningBucketsSchema() {
|
|
79
|
+
return {
|
|
80
|
+
version: 1,
|
|
81
|
+
tables: cloneTableList(PLANNING_BUCKET_TABLE_LIST),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validate the planning-bucket schema: enforce that the sprint/release/milestone tables are
|
|
87
|
+
* present, then run the shared Kernel structural validator.
|
|
88
|
+
* @throws {Error} if a required planning bucket table is missing.
|
|
89
|
+
* @returns {true} when membership, storage classes, and authorities are valid.
|
|
90
|
+
*/
|
|
91
|
+
function validatePlanningBucketsSchema(schema = getPlanningBucketsSchema()) {
|
|
92
|
+
const present = new Set((schema.tables || []).map(tableDefinition => tableDefinition.name));
|
|
93
|
+
for (const required of ['sprint', 'release', 'milestone']) {
|
|
94
|
+
if (!present.has(required)) {
|
|
95
|
+
throw new Error(`Missing planning bucket table: ${required}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return validateKernelSchema(schema);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
BOARD_MUTATION_EVENT_TYPES,
|
|
103
|
+
MILESTONE_STATES,
|
|
104
|
+
PLANNING_BUCKET_TABLES,
|
|
105
|
+
RELEASE_STATES,
|
|
106
|
+
SPRINT_STATES,
|
|
107
|
+
getPlanningBucketsSchema,
|
|
108
|
+
validatePlanningBucketsSchema,
|
|
109
|
+
};
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { parseJsonl, stringifyJsonl } = require('../adapters/beads-kernel-compat');
|
|
7
|
+
|
|
8
|
+
// Schema history (all additive; importProjection accepts any schema_version <= this):
|
|
9
|
+
// v1 — the original 11-key issue shape.
|
|
10
|
+
// v2 — added `created_by` (v1 lost the author on round-trip).
|
|
11
|
+
// v3 — carries the FULL kernel_issues column set (parent_id, sprint_id, release_id,
|
|
12
|
+
// stage_state, labels, acceptance_criteria, estimate, design, notes, assignee,
|
|
13
|
+
// closed_at, close_reason, metadata) so export → hydrate is lossless. v1/v2
|
|
14
|
+
// snapshots still import; their newer columns are simply absent → null.
|
|
15
|
+
const SCHEMA_VERSION = 3;
|
|
16
|
+
|
|
17
|
+
// Deterministic write/rename order. manifest.json is renamed last so a partial
|
|
18
|
+
// write never leaves a manifest pointing at incomplete JSONL (the manifest is
|
|
19
|
+
// effectively the commit marker for the snapshot).
|
|
20
|
+
const PROJECTION_FILE_ORDER = ['issues.jsonl', 'comments.jsonl', 'dependencies.jsonl', 'manifest.json'];
|
|
21
|
+
const DEFAULT_PROJECTION_DIR = path.join('.forge', 'kernel');
|
|
22
|
+
// Exclusive publish lock filename (transient — written and removed within a single
|
|
23
|
+
// writeProjection call) that serializes the rename phase across concurrent exports.
|
|
24
|
+
const PROJECTION_LOCK_FILE = '.export.lock';
|
|
25
|
+
|
|
26
|
+
// Fixed key insertion order (v3): 'kind' + the full kernel_issues column set, in the
|
|
27
|
+
// table's column order, so export → hydrate round-trips every column losslessly.
|
|
28
|
+
const ISSUE_KEYS = [
|
|
29
|
+
'kind',
|
|
30
|
+
'id', 'title', 'body', 'type', 'status', 'priority', 'priority_rank',
|
|
31
|
+
'created_at', 'updated_at', 'entity_revision',
|
|
32
|
+
'parent_id', 'sprint_id', 'release_id', 'stage_state', 'labels',
|
|
33
|
+
'acceptance_criteria', 'estimate', 'design', 'notes', 'assignee',
|
|
34
|
+
'created_by', 'closed_at', 'close_reason', 'metadata',
|
|
35
|
+
];
|
|
36
|
+
const COMMENT_KEYS = ['kind', 'id', 'issue_id', 'body', 'actor', 'visibility', 'created_at'];
|
|
37
|
+
const DEP_KEYS = ['kind', 'id', 'issue_id', 'blocks_issue_id', 'dependency_type', 'created_at'];
|
|
38
|
+
|
|
39
|
+
function pickKeys(record, keys) {
|
|
40
|
+
const out = {};
|
|
41
|
+
for (const key of keys) {
|
|
42
|
+
out[key] = Object.prototype.hasOwnProperty.call(record, key) ? record[key] : null;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeIssue(raw) {
|
|
48
|
+
return pickKeys({ ...raw, kind: 'issue', body: raw.body ?? null }, ISSUE_KEYS);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizeComment(raw) {
|
|
52
|
+
return pickKeys({ ...raw, kind: 'comment' }, COMMENT_KEYS);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeDependency(raw) {
|
|
56
|
+
return pickKeys({ ...raw, kind: 'dependency' }, DEP_KEYS);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeProjectionModel(model) {
|
|
60
|
+
return {
|
|
61
|
+
issues: (model.issues || []).map(normalizeIssue),
|
|
62
|
+
comments: (model.comments || []).map(normalizeComment),
|
|
63
|
+
dependencies: (model.dependencies || []).map(normalizeDependency),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function cmp(a, b) {
|
|
68
|
+
if (a < b) return -1;
|
|
69
|
+
if (a > b) return 1;
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function buildProjectionSnapshot(model) {
|
|
74
|
+
const issues = [...model.issues].sort((a, b) => cmp(a.id, b.id));
|
|
75
|
+
|
|
76
|
+
const comments = [...model.comments].sort((a, b) =>
|
|
77
|
+
cmp(a.issue_id, b.issue_id) || cmp(a.created_at, b.created_at) || cmp(a.id, b.id),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const dependencies = [...model.dependencies].sort((a, b) =>
|
|
81
|
+
cmp(a.issue_id, b.issue_id) ||
|
|
82
|
+
cmp(a.blocks_issue_id, b.blocks_issue_id) ||
|
|
83
|
+
cmp(a.dependency_type, b.dependency_type) ||
|
|
84
|
+
cmp(a.id, b.id),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return { issues, comments, dependencies };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function serializeProjection(model) {
|
|
91
|
+
// Normalize defensively: callers (including the outbox consumer) may pass raw
|
|
92
|
+
// driver rows that lack `kind`/canonical key order. normalizeProjectionModel is
|
|
93
|
+
// idempotent, so pre-normalized snapshots are unaffected.
|
|
94
|
+
const snap = buildProjectionSnapshot(normalizeProjectionModel(model));
|
|
95
|
+
|
|
96
|
+
const issuesJsonl = stringifyJsonl(snap.issues);
|
|
97
|
+
const commentsJsonl = stringifyJsonl(snap.comments);
|
|
98
|
+
const depsJsonl = stringifyJsonl(snap.dependencies);
|
|
99
|
+
|
|
100
|
+
const contentSha256 = crypto
|
|
101
|
+
.createHash('sha256')
|
|
102
|
+
.update(issuesJsonl)
|
|
103
|
+
.update(commentsJsonl)
|
|
104
|
+
.update(depsJsonl)
|
|
105
|
+
.digest('hex');
|
|
106
|
+
|
|
107
|
+
const manifest = {
|
|
108
|
+
schema_version: SCHEMA_VERSION,
|
|
109
|
+
source: 'kernel',
|
|
110
|
+
counts: {
|
|
111
|
+
issues: snap.issues.length,
|
|
112
|
+
comments: snap.comments.length,
|
|
113
|
+
dependencies: snap.dependencies.length,
|
|
114
|
+
},
|
|
115
|
+
content_sha256: contentSha256,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
files: {
|
|
120
|
+
'issues.jsonl': issuesJsonl,
|
|
121
|
+
'comments.jsonl': commentsJsonl,
|
|
122
|
+
'dependencies.jsonl': depsJsonl,
|
|
123
|
+
'manifest.json': JSON.stringify(manifest, null, 2) + '\n',
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function importProjection(files) {
|
|
129
|
+
if (!files['manifest.json']) {
|
|
130
|
+
throw new Error('Missing manifest.json in projection files');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const manifest = JSON.parse(files['manifest.json']);
|
|
134
|
+
|
|
135
|
+
// Versioned-manifest guard: refuse a snapshot written by a NEWER forge whose
|
|
136
|
+
// format we do not understand, rather than silently mis-importing it. Older
|
|
137
|
+
// versions (<= ours) remain importable — missing fields default on write.
|
|
138
|
+
const schemaVersion = manifest.schema_version;
|
|
139
|
+
if (!Number.isInteger(schemaVersion) || schemaVersion < 1) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Invalid projection manifest: schema_version must be a positive integer, got ${JSON.stringify(schemaVersion)}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (schemaVersion > SCHEMA_VERSION) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Unsupported projection schema_version ${schemaVersion}: this forge understands up to ${SCHEMA_VERSION}. `
|
|
147
|
+
+ 'Upgrade forge to import this snapshot.',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const issuesJsonl = files['issues.jsonl'] || '';
|
|
152
|
+
const commentsJsonl = files['comments.jsonl'] || '';
|
|
153
|
+
const depsJsonl = files['dependencies.jsonl'] || '';
|
|
154
|
+
|
|
155
|
+
const actualSha256 = crypto
|
|
156
|
+
.createHash('sha256')
|
|
157
|
+
.update(issuesJsonl)
|
|
158
|
+
.update(commentsJsonl)
|
|
159
|
+
.update(depsJsonl)
|
|
160
|
+
.digest('hex');
|
|
161
|
+
|
|
162
|
+
if (actualSha256 !== manifest.content_sha256) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`sha256 mismatch: manifest has ${manifest.content_sha256}, computed ${actualSha256}`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const issues = issuesJsonl ? parseJsonl(issuesJsonl, 'issues.jsonl') : [];
|
|
169
|
+
const comments = commentsJsonl ? parseJsonl(commentsJsonl, 'comments.jsonl') : [];
|
|
170
|
+
const dependencies = depsJsonl ? parseJsonl(depsJsonl, 'dependencies.jsonl') : [];
|
|
171
|
+
|
|
172
|
+
// The content hash binds only the concatenated byte stream, not file
|
|
173
|
+
// boundaries — moving a line between files leaves the stream unchanged. Bind
|
|
174
|
+
// records to their files (kind) and to the manifest (counts) so a relocated or
|
|
175
|
+
// dropped record is rejected even when the hash still matches.
|
|
176
|
+
assertRecordKind(issues, 'issue', 'issues.jsonl');
|
|
177
|
+
assertRecordKind(comments, 'comment', 'comments.jsonl');
|
|
178
|
+
assertRecordKind(dependencies, 'dependency', 'dependencies.jsonl');
|
|
179
|
+
assertManifestCounts(manifest.counts, { issues, comments, dependencies });
|
|
180
|
+
|
|
181
|
+
return { issues, comments, dependencies };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function assertRecordKind(records, expectedKind, file) {
|
|
185
|
+
for (let i = 0; i < records.length; i += 1) {
|
|
186
|
+
if (records[i].kind !== expectedKind) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Invalid record in ${file} at line ${i + 1}: kind "${records[i].kind}", expected "${expectedKind}"`,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function assertManifestCounts(counts, parsed) {
|
|
195
|
+
if (!counts) {
|
|
196
|
+
// A manifest with a valid hash but no counts would otherwise skip the
|
|
197
|
+
// per-file binding check entirely — treat the missing field as corruption.
|
|
198
|
+
throw new Error('Manifest is missing the required counts field');
|
|
199
|
+
}
|
|
200
|
+
for (const key of ['issues', 'comments', 'dependencies']) {
|
|
201
|
+
if (typeof counts[key] === 'number' && counts[key] !== parsed[key].length) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`Manifest count mismatch for ${key}: manifest says ${counts[key]}, found ${parsed[key].length}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resolveProjectionDir(projectionDir, projectRoot) {
|
|
210
|
+
const resolved = path.resolve(projectionDir || (projectRoot ? path.join(projectRoot, DEFAULT_PROJECTION_DIR) : DEFAULT_PROJECTION_DIR));
|
|
211
|
+
|
|
212
|
+
if (projectRoot) {
|
|
213
|
+
const root = path.resolve(projectRoot);
|
|
214
|
+
const relative = path.relative(root, resolved);
|
|
215
|
+
const escapes = relative.startsWith('..') || path.isAbsolute(relative);
|
|
216
|
+
if (escapes) {
|
|
217
|
+
throw new Error(`Projection directory escapes the project root: ${resolved}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return resolved;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Atomic-ish multi-file write: render every file to a sibling temp, snapshot the
|
|
225
|
+
// existing targets, then rename temps into place in a fixed order. If any rename
|
|
226
|
+
// throws, restore the snapshot (rewrite prior content or delete files that did
|
|
227
|
+
// not previously exist) and clean up temps before rethrowing. A projection write
|
|
228
|
+
// failure must never leave a half-applied snapshot on disk.
|
|
229
|
+
//
|
|
230
|
+
// The per-file renames are not individually atomic across files, so the publish
|
|
231
|
+
// phase is guarded by an exclusive directory lock: a concurrent export to the
|
|
232
|
+
// same dir fails fast instead of interleaving renames into a mixed snapshot whose
|
|
233
|
+
// manifest hash no longer matches. The lock is always released in `finally`, so
|
|
234
|
+
// only an abnormal crash mid-publish (a sub-millisecond window) could leave a
|
|
235
|
+
// stale lock — the error message tells the operator how to clear it.
|
|
236
|
+
function writeProjection({ model, projectionDir, projectRoot, fsImpl } = {}) {
|
|
237
|
+
const io = fsImpl || fs;
|
|
238
|
+
const dir = resolveProjectionDir(projectionDir, projectRoot);
|
|
239
|
+
const { files } = serializeProjection(model);
|
|
240
|
+
|
|
241
|
+
io.mkdirSync(dir, { recursive: true });
|
|
242
|
+
|
|
243
|
+
// Per-write unique tag so concurrent exports to the same dir never collide on
|
|
244
|
+
// each other's temp files (which would corrupt snapshots / rollback). pid +
|
|
245
|
+
// random bytes is unique without depending on wall-clock time.
|
|
246
|
+
const tempTag = `${process.pid}-${crypto.randomBytes(8).toString('hex')}`;
|
|
247
|
+
const tempPaths = [];
|
|
248
|
+
let bytes = 0;
|
|
249
|
+
for (const name of PROJECTION_FILE_ORDER) {
|
|
250
|
+
const content = files[name];
|
|
251
|
+
const tempPath = path.join(dir, `.tmp-${tempTag}-${name}`);
|
|
252
|
+
io.writeFileSync(tempPath, content);
|
|
253
|
+
tempPaths.push(tempPath);
|
|
254
|
+
bytes += Buffer.byteLength(content, 'utf8');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const lockPath = path.join(dir, PROJECTION_LOCK_FILE);
|
|
258
|
+
let lockHeld = false;
|
|
259
|
+
try {
|
|
260
|
+
acquireProjectionLock(io, lockPath);
|
|
261
|
+
lockHeld = true;
|
|
262
|
+
|
|
263
|
+
const snapshots = [];
|
|
264
|
+
const renamed = [];
|
|
265
|
+
try {
|
|
266
|
+
for (let i = 0; i < PROJECTION_FILE_ORDER.length; i += 1) {
|
|
267
|
+
const name = PROJECTION_FILE_ORDER[i];
|
|
268
|
+
const target = path.join(dir, name);
|
|
269
|
+
const existed = io.existsSync(target);
|
|
270
|
+
snapshots.push({ target, existed, content: existed ? io.readFileSync(target) : null });
|
|
271
|
+
io.renameSync(tempPaths[i], target);
|
|
272
|
+
renamed.push(target);
|
|
273
|
+
}
|
|
274
|
+
} catch (error) {
|
|
275
|
+
rollbackProjectionWrite(io, snapshots, renamed);
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
} finally {
|
|
279
|
+
// only release a lock we actually acquired (never delete a foreign lock)
|
|
280
|
+
if (lockHeld) removeIfPresent(io, lockPath);
|
|
281
|
+
cleanupTempFiles(io, tempPaths);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
dir,
|
|
286
|
+
writes: PROJECTION_FILE_ORDER.length,
|
|
287
|
+
bytes,
|
|
288
|
+
files: [...PROJECTION_FILE_ORDER],
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Acquire an exclusive publish lock via O_EXCL (flag 'wx'): fails if the lock
|
|
293
|
+
// already exists. Translates the raw EEXIST into an actionable message.
|
|
294
|
+
function acquireProjectionLock(io, lockPath) {
|
|
295
|
+
try {
|
|
296
|
+
io.writeFileSync(lockPath, String(process.pid), { flag: 'wx' });
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (error && error.code === 'EEXIST') {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Another forge export is already publishing to this directory (lock: ${lockPath}). `
|
|
301
|
+
+ 'Retry once it finishes, or remove the lock file if it is stale.',
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function rollbackProjectionWrite(io, snapshots, renamed) {
|
|
309
|
+
const renamedSet = new Set(renamed);
|
|
310
|
+
for (const snapshot of snapshots) {
|
|
311
|
+
if (!renamedSet.has(snapshot.target)) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (snapshot.existed) {
|
|
315
|
+
io.writeFileSync(snapshot.target, snapshot.content);
|
|
316
|
+
} else {
|
|
317
|
+
removeIfPresent(io, snapshot.target);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function cleanupTempFiles(io, tempPaths) {
|
|
323
|
+
for (const tempPath of tempPaths) {
|
|
324
|
+
removeIfPresent(io, tempPath);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function removeIfPresent(io, target) {
|
|
329
|
+
try {
|
|
330
|
+
if (io.existsSync(target)) {
|
|
331
|
+
io.rmSync(target, { force: true });
|
|
332
|
+
}
|
|
333
|
+
} catch { /* intentional: best-effort cleanup, never mask the original failure */ // NOSONAR S2486
|
|
334
|
+
// no-op
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Symmetric on-disk read of a projection snapshot. Returns null when no snapshot
|
|
339
|
+
// exists (no manifest), otherwise the imported model with its integrity verified
|
|
340
|
+
// by importProjection (throws on a tampered/corrupt manifest hash).
|
|
341
|
+
function readProjection({ projectionDir, projectRoot, fsImpl } = {}) {
|
|
342
|
+
const io = fsImpl || fs;
|
|
343
|
+
const dir = resolveProjectionDir(projectionDir, projectRoot);
|
|
344
|
+
const manifestPath = path.join(dir, 'manifest.json');
|
|
345
|
+
if (!io.existsSync(manifestPath)) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const files = {};
|
|
350
|
+
for (const name of PROJECTION_FILE_ORDER) {
|
|
351
|
+
const target = path.join(dir, name);
|
|
352
|
+
files[name] = io.existsSync(target) ? io.readFileSync(target, 'utf8') : '';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return { dir, model: importProjection(files) };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const DEFAULT_PROJECTION_TARGET = 'jsonl';
|
|
359
|
+
const DEFAULT_MAX_ATTEMPTS = 5;
|
|
360
|
+
const DEFAULT_BASE_BACKOFF_MS = 5000;
|
|
361
|
+
|
|
362
|
+
function computeBackoff(now, attempts, baseBackoffMs) {
|
|
363
|
+
const start = Date.parse(now);
|
|
364
|
+
if (Number.isNaN(start)) {
|
|
365
|
+
// Without this guard, new Date(NaN).toISOString() throws a bare
|
|
366
|
+
// "Invalid time value" RangeError inside the consumer's catch block,
|
|
367
|
+
// masking the original write error. Fail with an attributable message.
|
|
368
|
+
throw new RangeError(`computeBackoff: invalid 'now' timestamp: ${now}`);
|
|
369
|
+
}
|
|
370
|
+
const delay = baseBackoffMs * 2 ** Math.max(0, attempts - 1);
|
|
371
|
+
return new Date(start + delay).toISOString();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Outbox-driven JSONL consumer. Pending kernel_outbox rows for the projection
|
|
375
|
+
// target are dirty markers; draining them performs ONE full-snapshot write that
|
|
376
|
+
// covers all of them. On success every drained row is marked delivered. On write
|
|
377
|
+
// failure each row increments its attempt count: rows below maxAttempts return to
|
|
378
|
+
// pending with an exponential backoff; rows that reach maxAttempts are
|
|
379
|
+
// dead-lettered. A projection failure never mutates Kernel authority.
|
|
380
|
+
async function runJsonlProjectionConsumer({
|
|
381
|
+
broker,
|
|
382
|
+
projectionDir,
|
|
383
|
+
projectRoot,
|
|
384
|
+
now = new Date().toISOString(),
|
|
385
|
+
maxAttempts = DEFAULT_MAX_ATTEMPTS,
|
|
386
|
+
baseBackoffMs = DEFAULT_BASE_BACKOFF_MS,
|
|
387
|
+
target = DEFAULT_PROJECTION_TARGET,
|
|
388
|
+
writer = writeProjection,
|
|
389
|
+
} = {}) {
|
|
390
|
+
const pending = (await broker.listProjectionOutbox({ target, status: 'pending', now })) || [];
|
|
391
|
+
if (pending.length === 0) {
|
|
392
|
+
return { drained: 0, written: false, delivered: [], retried: [], dead: [] };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const model = await broker.loadProjectionModel();
|
|
396
|
+
|
|
397
|
+
let write;
|
|
398
|
+
try {
|
|
399
|
+
write = writer({ model, projectionDir, projectRoot });
|
|
400
|
+
} catch (error) {
|
|
401
|
+
const retried = [];
|
|
402
|
+
const dead = [];
|
|
403
|
+
for (const entry of pending) {
|
|
404
|
+
const attempts = (entry.attempts || 0) + 1;
|
|
405
|
+
if (attempts >= maxAttempts) {
|
|
406
|
+
await broker.deadLetterProjection({
|
|
407
|
+
outbox_id: entry.id,
|
|
408
|
+
target,
|
|
409
|
+
error: error.message,
|
|
410
|
+
payload_json: JSON.stringify({ event_id: entry.event_id, attempts }),
|
|
411
|
+
now,
|
|
412
|
+
});
|
|
413
|
+
dead.push(entry.id);
|
|
414
|
+
} else {
|
|
415
|
+
await broker.recordProjectionFailure({
|
|
416
|
+
id: entry.id,
|
|
417
|
+
attempts,
|
|
418
|
+
next_attempt_at: computeBackoff(now, attempts, baseBackoffMs),
|
|
419
|
+
error: error.message,
|
|
420
|
+
now,
|
|
421
|
+
});
|
|
422
|
+
retried.push(entry.id);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { drained: pending.length, written: false, error: error.message, delivered: [], retried, dead };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const delivered = pending.map(entry => entry.id);
|
|
429
|
+
await broker.markProjectionDelivered(delivered, { now });
|
|
430
|
+
|
|
431
|
+
return { drained: pending.length, written: true, write, delivered, retried: [], dead: [] };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
module.exports = {
|
|
435
|
+
SCHEMA_VERSION,
|
|
436
|
+
DEFAULT_PROJECTION_DIR,
|
|
437
|
+
DEFAULT_PROJECTION_TARGET,
|
|
438
|
+
DEFAULT_MAX_ATTEMPTS,
|
|
439
|
+
DEFAULT_BASE_BACKOFF_MS,
|
|
440
|
+
PROJECTION_FILE_ORDER,
|
|
441
|
+
normalizeProjectionModel,
|
|
442
|
+
buildProjectionSnapshot,
|
|
443
|
+
serializeProjection,
|
|
444
|
+
importProjection,
|
|
445
|
+
resolveProjectionDir,
|
|
446
|
+
writeProjection,
|
|
447
|
+
readProjection,
|
|
448
|
+
computeBackoff,
|
|
449
|
+
runJsonlProjectionConsumer,
|
|
450
|
+
};
|