forge-workflow 0.0.10 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +150 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -118
- 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 +461 -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/forge/TOOLCHAIN.md +670 -0
- 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/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
- 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 +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +741 -77
- 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 +17 -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 +0 -1
- 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 +838 -972
- 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 +158 -21
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +4 -1
- package/lib/commands/test.js +43 -27
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +43 -18
- package/lib/commands/worktree.js +307 -100
- 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 +2 -2
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +35 -9
- 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 +382 -11
- 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/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 +3 -2
- 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 +99 -497
- 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 +241 -20
- 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/scoring.js +17 -3
- package/lib/status/beads-snapshot.js +45 -2
- package/lib/status/presenter.js +169 -18
- 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 +296 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +11 -22
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +37 -35
- 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 +81 -57
- package/scripts/beads-upgrade-smoke.sh +24 -3
- 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.sh +22 -3
- 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 +49 -84
- 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 +50 -83
- 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/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 +21 -94
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status.sh +60 -57
- 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-utils.sh +3 -0
- package/scripts/test-ci-shard.js +13 -6
- package/scripts/test.js +95 -12
- 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} +44 -50
- 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} +132 -157
- 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/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
- 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/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
- 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/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -89
- package/.github/workflows/github-to-beads.yml +0 -100
- 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 -281
- 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-bootstrap.js +0 -225
- package/lib/beads-health-check.js +0 -188
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -584
- 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 -356
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -132
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
- 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,329 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getTypeBehavior, isTerminalStatus, normalizeRank, toEpochMillis } = require('./taxonomy-validator');
|
|
4
|
+
|
|
5
|
+
// Readiness is a DERIVED read model (D18). `ready`/`blocked` are computed on demand
|
|
6
|
+
// from dependencies, claims, quarantine/conflicts, gates, defer windows, and policy —
|
|
7
|
+
// they are NEVER stored as issue status values.
|
|
8
|
+
const READINESS_REASONS = Object.freeze({
|
|
9
|
+
DEPENDENCY: 'dependency',
|
|
10
|
+
// A blocker that is itself PARKED (backlog): it will never complete on its own, so it
|
|
11
|
+
// blocks its dependents indefinitely. Surfaced as a distinct code (not plain
|
|
12
|
+
// `dependency`) so a parked blocker is not mistaken for ordinary in-flight work — a
|
|
13
|
+
// consumer can flag it for promotion instead of waiting on it forever.
|
|
14
|
+
DEPENDENCY_PARKED: 'dependency_parked',
|
|
15
|
+
QUARANTINE: 'quarantine',
|
|
16
|
+
CONFLICT: 'conflict',
|
|
17
|
+
GATE: 'gate',
|
|
18
|
+
CLAIM: 'claimed',
|
|
19
|
+
DEFERRED: 'deferred',
|
|
20
|
+
POLICY: 'policy_disabled',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const READINESS_STATES = Object.freeze([
|
|
24
|
+
'ready',
|
|
25
|
+
'blocked',
|
|
26
|
+
'gated',
|
|
27
|
+
'deferred',
|
|
28
|
+
'claimed',
|
|
29
|
+
'disabled',
|
|
30
|
+
'closed',
|
|
31
|
+
'backlog',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const WORKABLE_STATUSES = Object.freeze(['open', 'in_progress']);
|
|
35
|
+
|
|
36
|
+
/** @returns {boolean} true if `status` is a pickable working status (open/in_progress). */
|
|
37
|
+
function isWorkableStatus(status) {
|
|
38
|
+
return WORKABLE_STATUSES.includes(status);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Ready work must be claimable: epics (containers) and decisions are claimable:false, so they
|
|
42
|
+
// never belong in the ready queue even when unblocked. A known non-claimable type is excluded;
|
|
43
|
+
// an unknown/unspecified type is not penalised (callers may omit `type`).
|
|
44
|
+
function isReadyEligibleType(type) {
|
|
45
|
+
const behavior = getTypeBehavior(type);
|
|
46
|
+
return behavior ? behavior.claimable : true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Edge-only blocking test: keys off the dependency RELATIONSHIP (dependency_type),
|
|
50
|
+
// NOT the blocking issue's type. Distinct from taxonomy-validator's isBlockingDependency,
|
|
51
|
+
// which also falls back to `.type`; do not merge them (see readiness decision-dependency).
|
|
52
|
+
function isBlockingDependencyEdge(dependency) {
|
|
53
|
+
return (dependency.dependency_type || 'blocks') === 'blocks';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isDeferred(issue, now) {
|
|
57
|
+
if (!issue.defer_until) return false;
|
|
58
|
+
const deferMillis = toEpochMillis(issue.defer_until);
|
|
59
|
+
if (deferMillis === null) return false; // an unparseable defer window is not a defer
|
|
60
|
+
const nowMillis = toEpochMillis(now);
|
|
61
|
+
// Fail closed: without a usable clock we cannot confirm the window elapsed, so a deferred
|
|
62
|
+
// issue stays deferred rather than incorrectly surfacing as ready. Keeps the model pure
|
|
63
|
+
// (caller supplies `now`) instead of reaching for a wall clock.
|
|
64
|
+
if (nowMillis === null) return true;
|
|
65
|
+
return deferMillis > nowMillis;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function findConflictingClaim(issueId, claims, actor, now) {
|
|
69
|
+
const nowMillis = toEpochMillis(now);
|
|
70
|
+
for (const claim of claims || []) {
|
|
71
|
+
if (claim.issue_id !== issueId) continue;
|
|
72
|
+
if ((claim.state || 'active') !== 'active') continue;
|
|
73
|
+
if (actor && claim.actor === actor) continue; // the requesting actor's own claim does not block them
|
|
74
|
+
// An active row whose lease has elapsed is dead (validateClaim agrees) — it must not
|
|
75
|
+
// keep the issue out of the ready queue for other actors. If `now` is unknown we cannot
|
|
76
|
+
// confirm expiry, so the claim still blocks (conservative).
|
|
77
|
+
const expiresMillis = toEpochMillis(claim.expires_at);
|
|
78
|
+
if (expiresMillis !== null && nowMillis !== null && expiresMillis <= nowMillis) continue;
|
|
79
|
+
return claim;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function collectDependencyBlockers(context, reasons, blockedBy) {
|
|
85
|
+
for (const dependency of context.dependencyStatuses || []) {
|
|
86
|
+
if (!isBlockingDependencyEdge(dependency)) continue;
|
|
87
|
+
// A terminal (done OR cancelled) blocker will never complete again, so it no longer
|
|
88
|
+
// blocks — a cancelled dependency must not wedge the dependent as permanently blocked.
|
|
89
|
+
if (isTerminalStatus(dependency.status)) continue;
|
|
90
|
+
blockedBy.push(dependency.id);
|
|
91
|
+
// A parked (backlog) blocker earns a distinct code + flag: it is stalled by design,
|
|
92
|
+
// not being actively worked, so consumers can call it out for promotion.
|
|
93
|
+
const isParked = dependency.status === 'backlog';
|
|
94
|
+
const code = isParked ? READINESS_REASONS.DEPENDENCY_PARKED : READINESS_REASONS.DEPENDENCY;
|
|
95
|
+
const reason = { code, issue_id: dependency.id, status: dependency.status };
|
|
96
|
+
if (isParked) {
|
|
97
|
+
reason.parked = true;
|
|
98
|
+
}
|
|
99
|
+
if (dependency.type === 'decision') {
|
|
100
|
+
reason.decision = true;
|
|
101
|
+
}
|
|
102
|
+
reasons.push(reason);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function collectConflictBlockers(issue, context, reasons) {
|
|
107
|
+
let hasConflict = false;
|
|
108
|
+
for (const conflict of context.conflicts || []) {
|
|
109
|
+
// Conflicts are keyed by (entity_type, entity_id). Only issue-scoped conflicts block an
|
|
110
|
+
// issue; a quarantined dependency/release/sprint that happens to share the id string must
|
|
111
|
+
// not. Absent entity_type is treated as legacy issue-scoped.
|
|
112
|
+
if (conflict.entity_type && conflict.entity_type !== 'issue') continue;
|
|
113
|
+
if (conflict.entity_id && issue.id && conflict.entity_id !== issue.id) continue;
|
|
114
|
+
hasConflict = true;
|
|
115
|
+
const status = conflict.status || 'quarantined';
|
|
116
|
+
const code = status === 'quarantined' ? READINESS_REASONS.QUARANTINE : READINESS_REASONS.CONFLICT;
|
|
117
|
+
reasons.push({ code, status });
|
|
118
|
+
}
|
|
119
|
+
return hasConflict;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function collectGateBlockers(context, reasons) {
|
|
123
|
+
let hasGate = false;
|
|
124
|
+
for (const gate of context.gates || []) {
|
|
125
|
+
if (gate.satisfied) continue;
|
|
126
|
+
hasGate = true;
|
|
127
|
+
reasons.push({ code: READINESS_REASONS.GATE, gate: gate.name });
|
|
128
|
+
}
|
|
129
|
+
return hasGate;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Precedence high→low. `blocked` (dependencies/quarantine/conflict) always outranks the
|
|
133
|
+
// softer not-ready reasons; consumers picking next work should read the full reasons[]
|
|
134
|
+
// because a claim hidden behind a defer window is not reflected in this single summary.
|
|
135
|
+
function deriveState(flags) {
|
|
136
|
+
if (flags.blocked) return 'blocked';
|
|
137
|
+
if (flags.hasGate) return 'gated';
|
|
138
|
+
if (flags.deferred) return 'deferred';
|
|
139
|
+
if (flags.hasClaim) return 'claimed';
|
|
140
|
+
if (flags.policyDisabled) return 'disabled';
|
|
141
|
+
if (flags.ready) return 'ready';
|
|
142
|
+
return 'backlog';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Derive an issue's readiness (a read model, never stored) from its context.
|
|
147
|
+
* @param {object} issue stored issue fields (id, status, defer_until, ...).
|
|
148
|
+
* @param {object} context { now, actor, dependencyStatuses[], conflicts[], gates[], claims[], policyDisabled }.
|
|
149
|
+
* @returns {{id, status, ready: boolean, blocked: boolean, blocked_by: string[], reasons: object[], state: string}}
|
|
150
|
+
*/
|
|
151
|
+
function deriveReadiness(issue = {}, context = {}) {
|
|
152
|
+
const id = issue.id;
|
|
153
|
+
const status = issue.status;
|
|
154
|
+
|
|
155
|
+
if (isTerminalStatus(status)) {
|
|
156
|
+
return { id, status, ready: false, blocked: false, blocked_by: [], reasons: [], state: 'closed' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// A `backlog` (parked) issue is a first-class lifecycle state, not workable work: it
|
|
160
|
+
// reads as parked regardless of any dependencies/gates — never ready, never blocked —
|
|
161
|
+
// so parked ideas do not surface in ready/blocked queues or flag stale.
|
|
162
|
+
if (status === 'backlog') {
|
|
163
|
+
return { id, status, ready: false, blocked: false, blocked_by: [], reasons: [], state: 'backlog' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const reasons = [];
|
|
167
|
+
const blockedBy = [];
|
|
168
|
+
|
|
169
|
+
collectDependencyBlockers(context, reasons, blockedBy);
|
|
170
|
+
const hasConflict = collectConflictBlockers(issue, context, reasons);
|
|
171
|
+
const hasGate = collectGateBlockers(context, reasons);
|
|
172
|
+
|
|
173
|
+
const deferred = isDeferred(issue, context.now);
|
|
174
|
+
if (deferred) {
|
|
175
|
+
reasons.push({ code: READINESS_REASONS.DEFERRED, until: issue.defer_until });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const policyDisabled = Boolean(context.policyDisabled);
|
|
179
|
+
if (policyDisabled) {
|
|
180
|
+
reasons.push({ code: READINESS_REASONS.POLICY });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const conflictingClaim = findConflictingClaim(id, context.claims, context.actor, context.now);
|
|
184
|
+
if (conflictingClaim) {
|
|
185
|
+
reasons.push({ code: READINESS_REASONS.CLAIM, actor: conflictingClaim.actor });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const blocked = blockedBy.length > 0 || hasConflict;
|
|
189
|
+
const ready = !blocked
|
|
190
|
+
&& !hasGate
|
|
191
|
+
&& !deferred
|
|
192
|
+
&& !policyDisabled
|
|
193
|
+
&& !conflictingClaim
|
|
194
|
+
&& isWorkableStatus(status)
|
|
195
|
+
&& isReadyEligibleType(issue.type);
|
|
196
|
+
|
|
197
|
+
const state = deriveState({
|
|
198
|
+
blocked,
|
|
199
|
+
hasGate,
|
|
200
|
+
deferred,
|
|
201
|
+
hasClaim: Boolean(conflictingClaim),
|
|
202
|
+
policyDisabled,
|
|
203
|
+
ready,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return { id, status, ready, blocked, blocked_by: blockedBy, reasons, state };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// BETA stop-gap for claim thundering-herd (kernel 369c43d7): concurrent `forge ready`
|
|
210
|
+
// callers previously all saw the identical rank-0 top pick and raced to claim it (CAS
|
|
211
|
+
// lets exactly one win, but the rest burn a claim_conflict write + wasted pre-claim
|
|
212
|
+
// reasoning, then re-collide on rank-1, etc). Randomizing which of the top-K
|
|
213
|
+
// same-priority issues sorts first spreads concurrent agents across different ready
|
|
214
|
+
// issues. Priority tier ordering stays the PRIMARY, un-randomized sort key: only the
|
|
215
|
+
// contiguous run of issues sharing the single highest-ranked (lowest-number) priority
|
|
216
|
+
// tier is shuffled, and only its first `topK` members — a lower-priority issue can
|
|
217
|
+
// never be promoted ahead of a ready higher-priority one. Full fix is Phase-2 per-project
|
|
218
|
+
// lease dispatch; this is the cheap interim mitigation only.
|
|
219
|
+
const DEFAULT_TOP_K_READY_PICK = 5;
|
|
220
|
+
|
|
221
|
+
// Fisher-Yates, using an injectable rng (defaults to Math.random) so callers/tests can
|
|
222
|
+
// seed it for deterministic assertions. Mutates and returns `array`.
|
|
223
|
+
function shuffleInPlace(array, rng) {
|
|
224
|
+
for (let i = array.length - 1; i > 0; i--) {
|
|
225
|
+
const j = Math.floor(rng() * (i + 1));
|
|
226
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
227
|
+
}
|
|
228
|
+
return array;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Randomize only the top-K of the single highest-priority tier at the front of an
|
|
232
|
+
// already rank+id sorted `sortedIssues` array. Everything else (the tier's remainder
|
|
233
|
+
// beyond K, and every lower-priority tier) keeps its deterministic order untouched.
|
|
234
|
+
function applyTopKRandomizedPick(sortedIssues, { topK = DEFAULT_TOP_K_READY_PICK, rng = Math.random } = {}) {
|
|
235
|
+
if (sortedIssues.length < 2) return sortedIssues;
|
|
236
|
+
|
|
237
|
+
const topRank = normalizeRank(sortedIssues[0].priority_rank);
|
|
238
|
+
let tierEnd = 0;
|
|
239
|
+
while (tierEnd < sortedIssues.length && normalizeRank(sortedIssues[tierEnd].priority_rank) === topRank) {
|
|
240
|
+
tierEnd++;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const k = Math.max(0, Math.min(topK, tierEnd));
|
|
244
|
+
if (k < 2) return sortedIssues; // nothing to shuffle: <2 candidates in the randomized head
|
|
245
|
+
|
|
246
|
+
const head = shuffleInPlace(sortedIssues.slice(0, k), rng);
|
|
247
|
+
const rest = sortedIssues.slice(k);
|
|
248
|
+
return [...head, ...rest];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function groupBy(items, keyFn) {
|
|
252
|
+
const grouped = new Map();
|
|
253
|
+
for (const item of items || []) {
|
|
254
|
+
const key = keyFn(item);
|
|
255
|
+
if (!grouped.has(key)) grouped.set(key, []);
|
|
256
|
+
grouped.get(key).push(item);
|
|
257
|
+
}
|
|
258
|
+
return grouped;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Compute readiness for a whole board, resolving each dependency's status from the issue set.
|
|
263
|
+
* @param {object} input { issues[], dependencies[], conflicts[], claims[], gates[], policyDisabledIds[], now, actor,
|
|
264
|
+
* topK, rng }. `topK` (default {@link DEFAULT_TOP_K_READY_PICK}) and `rng` (default `Math.random`, injectable for
|
|
265
|
+
* deterministic tests) control the BETA top-K randomized ready pick (kernel 369c43d7): only the top-K of the
|
|
266
|
+
* single highest-priority ready tier is shuffled; priority ordering otherwise stays the primary sort.
|
|
267
|
+
* @returns {{readinessById: object, readyQueue: string[], blocked: string[]}} readyQueue is ranked by the single
|
|
268
|
+
* numeric rank, with the top-K of its leading priority tier randomized.
|
|
269
|
+
*/
|
|
270
|
+
function buildReadinessIndex(input = {}) {
|
|
271
|
+
const issues = input.issues || [];
|
|
272
|
+
const statusById = new Map(issues.map(issue => [issue.id, issue.status]));
|
|
273
|
+
const typeById = new Map(issues.map(issue => [issue.id, issue.type]));
|
|
274
|
+
|
|
275
|
+
const dependencyStatusesByIssue = new Map();
|
|
276
|
+
for (const dependency of input.dependencies || []) {
|
|
277
|
+
if (!dependencyStatusesByIssue.has(dependency.issue_id)) {
|
|
278
|
+
dependencyStatusesByIssue.set(dependency.issue_id, []);
|
|
279
|
+
}
|
|
280
|
+
dependencyStatusesByIssue.get(dependency.issue_id).push({
|
|
281
|
+
id: dependency.blocks_issue_id,
|
|
282
|
+
status: statusById.get(dependency.blocks_issue_id),
|
|
283
|
+
type: typeById.get(dependency.blocks_issue_id),
|
|
284
|
+
dependency_type: dependency.dependency_type,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const conflictsByIssue = groupBy(input.conflicts, conflict => conflict.entity_id);
|
|
289
|
+
const claimsByIssue = groupBy(input.claims, claim => claim.issue_id);
|
|
290
|
+
const gatesByIssue = groupBy(input.gates, gate => gate.issue_id);
|
|
291
|
+
const policyDisabledIds = new Set(input.policyDisabledIds || []);
|
|
292
|
+
|
|
293
|
+
// Null-prototype map: issue ids are unconstrained external strings, so a literal `{}`
|
|
294
|
+
// keyed by them would be a prototype-pollution vector (e.g. an id of `__proto__`).
|
|
295
|
+
const readinessById = Object.create(null);
|
|
296
|
+
for (const issue of issues) {
|
|
297
|
+
readinessById[issue.id] = deriveReadiness(issue, {
|
|
298
|
+
now: input.now,
|
|
299
|
+
actor: input.actor,
|
|
300
|
+
dependencyStatuses: dependencyStatusesByIssue.get(issue.id) || [],
|
|
301
|
+
conflicts: conflictsByIssue.get(issue.id) || [],
|
|
302
|
+
claims: claimsByIssue.get(issue.id) || [],
|
|
303
|
+
gates: gatesByIssue.get(issue.id) || [],
|
|
304
|
+
policyDisabled: policyDisabledIds.has(issue.id),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const readyIssues = issues.filter(issue => readinessById[issue.id].ready);
|
|
309
|
+
readyIssues.sort((left, right) => (
|
|
310
|
+
(normalizeRank(left.priority_rank) - normalizeRank(right.priority_rank))
|
|
311
|
+
|| String(left.id).localeCompare(String(right.id))
|
|
312
|
+
));
|
|
313
|
+
const pickedReadyIssues = applyTopKRandomizedPick(readyIssues, { topK: input.topK, rng: input.rng });
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
readinessById,
|
|
317
|
+
readyQueue: pickedReadyIssues.map(issue => issue.id),
|
|
318
|
+
blocked: issues.filter(issue => readinessById[issue.id].blocked).map(issue => issue.id),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = {
|
|
323
|
+
READINESS_REASONS,
|
|
324
|
+
READINESS_STATES,
|
|
325
|
+
DEFAULT_TOP_K_READY_PICK,
|
|
326
|
+
buildReadinessIndex,
|
|
327
|
+
deriveReadiness,
|
|
328
|
+
isWorkableStatus,
|
|
329
|
+
};
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
const KERNEL_STORAGE_CLASSES = Object.freeze([
|
|
2
|
+
'authority',
|
|
3
|
+
'read_model',
|
|
4
|
+
'projection',
|
|
5
|
+
'archive',
|
|
6
|
+
'configuration',
|
|
7
|
+
'cache',
|
|
8
|
+
'external_provider',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const KERNEL_FIELD_AUTHORITIES = Object.freeze([
|
|
12
|
+
'forge',
|
|
13
|
+
'provider',
|
|
14
|
+
'configured_provider',
|
|
15
|
+
'projection_only',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function deepFreeze(value) {
|
|
19
|
+
if (!value || typeof value !== 'object') return value;
|
|
20
|
+
Object.freeze(value);
|
|
21
|
+
for (const nestedValue of Object.values(value)) {
|
|
22
|
+
deepFreeze(nestedValue);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function field(name, type, options = {}) {
|
|
28
|
+
return {
|
|
29
|
+
name,
|
|
30
|
+
type,
|
|
31
|
+
storageClass: options.storageClass,
|
|
32
|
+
authority: options.authority || 'forge',
|
|
33
|
+
primaryKey: Boolean(options.primaryKey),
|
|
34
|
+
notNull: Boolean(options.notNull || options.primaryKey),
|
|
35
|
+
default: options.default,
|
|
36
|
+
references: options.references,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function index(name, columns, options = {}) {
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
columns,
|
|
44
|
+
unique: Boolean(options.unique),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function table(name, storageClass, fields, indexes = []) {
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
sqlName: `kernel_${name}`,
|
|
52
|
+
storageClass,
|
|
53
|
+
authority: 'forge',
|
|
54
|
+
fields: fields.map(fieldDefinition => ({
|
|
55
|
+
...fieldDefinition,
|
|
56
|
+
storageClass: fieldDefinition.storageClass || storageClass,
|
|
57
|
+
})),
|
|
58
|
+
indexes,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const TABLE_LIST = deepFreeze([
|
|
63
|
+
table('issues', 'authority', [
|
|
64
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
65
|
+
field('title', 'TEXT', { notNull: true }),
|
|
66
|
+
field('body', 'TEXT'),
|
|
67
|
+
field('type', 'TEXT', { notNull: true, default: "'task'" }),
|
|
68
|
+
field('status', 'TEXT', { notNull: true, default: "'open'" }),
|
|
69
|
+
field('priority', 'TEXT', { notNull: true, default: "'P2'" }),
|
|
70
|
+
field('priority_rank', 'INTEGER', { notNull: true, default: '0', storageClass: 'read_model' }),
|
|
71
|
+
field('created_at', 'TEXT', { notNull: true }),
|
|
72
|
+
field('updated_at', 'TEXT', { notNull: true }),
|
|
73
|
+
field('entity_revision', 'INTEGER', { notNull: true, default: '0' }),
|
|
74
|
+
// D18 taxonomy: hierarchy, planning buckets, and execution stage are separate
|
|
75
|
+
// axes from status. ready/blocked stay derived (readiness-model), never stored.
|
|
76
|
+
field('parent_id', 'TEXT', { references: 'issues.id' }),
|
|
77
|
+
field('sprint_id', 'TEXT'),
|
|
78
|
+
field('release_id', 'TEXT'),
|
|
79
|
+
field('stage_state', 'TEXT'),
|
|
80
|
+
field('labels', 'TEXT'),
|
|
81
|
+
field('acceptance_criteria', 'TEXT'),
|
|
82
|
+
field('estimate', 'TEXT'),
|
|
83
|
+
// KAP-10 (design/notes) + KAP-11 (assignee): authored content fields and a
|
|
84
|
+
// persistent assignee. Migration 004 backfills existing DBs; fresh DBs get them here.
|
|
85
|
+
field('design', 'TEXT'),
|
|
86
|
+
field('notes', 'TEXT'),
|
|
87
|
+
field('assignee', 'TEXT'),
|
|
88
|
+
// Beads full-fidelity import: the issue author, the close timestamp + raw close
|
|
89
|
+
// reason (distinct from the mapped terminal status), and a verbatim JSON blob for
|
|
90
|
+
// any other beads metadata. Migration 006 backfills existing DBs; fresh DBs get
|
|
91
|
+
// them here.
|
|
92
|
+
field('created_by', 'TEXT'),
|
|
93
|
+
field('closed_at', 'TEXT'),
|
|
94
|
+
field('close_reason', 'TEXT'),
|
|
95
|
+
field('metadata', 'TEXT'),
|
|
96
|
+
], [
|
|
97
|
+
index('idx_kernel_issues_status_priority', ['status', 'priority_rank']),
|
|
98
|
+
index('idx_kernel_issues_updated_at', ['updated_at']),
|
|
99
|
+
index('idx_kernel_issues_parent', ['parent_id']),
|
|
100
|
+
index('idx_kernel_issues_sprint', ['sprint_id']),
|
|
101
|
+
index('idx_kernel_issues_release', ['release_id']),
|
|
102
|
+
]),
|
|
103
|
+
table('dependencies', 'authority', [
|
|
104
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
105
|
+
field('issue_id', 'TEXT', { notNull: true, references: 'issues.id' }),
|
|
106
|
+
field('blocks_issue_id', 'TEXT', { notNull: true, references: 'issues.id' }),
|
|
107
|
+
field('dependency_type', 'TEXT', { notNull: true, default: "'blocks'" }),
|
|
108
|
+
field('created_at', 'TEXT', { notNull: true }),
|
|
109
|
+
], [
|
|
110
|
+
index('idx_kernel_dependencies_issue', ['issue_id']),
|
|
111
|
+
index('idx_kernel_dependencies_blocks', ['blocks_issue_id']),
|
|
112
|
+
]),
|
|
113
|
+
table('comments', 'authority', [
|
|
114
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
115
|
+
field('issue_id', 'TEXT', { notNull: true, references: 'issues.id' }),
|
|
116
|
+
field('body', 'TEXT', { notNull: true }),
|
|
117
|
+
field('actor', 'TEXT', { notNull: true }),
|
|
118
|
+
field('visibility', 'TEXT', { notNull: true, default: "'local'" }),
|
|
119
|
+
field('created_at', 'TEXT', { notNull: true }),
|
|
120
|
+
], [
|
|
121
|
+
index('idx_kernel_comments_issue_created', ['issue_id', 'created_at']),
|
|
122
|
+
]),
|
|
123
|
+
table('priority_events', 'authority', [
|
|
124
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
125
|
+
field('issue_id', 'TEXT', { notNull: true, references: 'issues.id' }),
|
|
126
|
+
field('old_priority', 'TEXT'),
|
|
127
|
+
field('new_priority', 'TEXT', { notNull: true }),
|
|
128
|
+
field('priority_rank', 'INTEGER', { notNull: true }),
|
|
129
|
+
field('actor', 'TEXT', { notNull: true }),
|
|
130
|
+
field('created_at', 'TEXT', { notNull: true }),
|
|
131
|
+
], [
|
|
132
|
+
index('idx_kernel_priority_events_issue_created', ['issue_id', 'created_at']),
|
|
133
|
+
]),
|
|
134
|
+
table('claims', 'authority', [
|
|
135
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
136
|
+
field('issue_id', 'TEXT', { notNull: true, references: 'issues.id' }),
|
|
137
|
+
field('actor', 'TEXT', { notNull: true }),
|
|
138
|
+
field('state', 'TEXT', { notNull: true, default: "'active'" }),
|
|
139
|
+
field('session_id', 'TEXT'),
|
|
140
|
+
field('worktree_id', 'TEXT'),
|
|
141
|
+
field('claimed_at', 'TEXT', { notNull: true }),
|
|
142
|
+
field('expires_at', 'TEXT'),
|
|
143
|
+
], [
|
|
144
|
+
index('idx_kernel_claims_issue_state', ['issue_id', 'state']),
|
|
145
|
+
index('idx_kernel_claims_actor_state', ['actor', 'state']),
|
|
146
|
+
]),
|
|
147
|
+
table('sessions', 'authority', [
|
|
148
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
149
|
+
field('actor', 'TEXT', { notNull: true }),
|
|
150
|
+
field('git_common_dir', 'TEXT', { notNull: true }),
|
|
151
|
+
field('worktree_id', 'TEXT'),
|
|
152
|
+
field('started_at', 'TEXT', { notNull: true }),
|
|
153
|
+
field('ended_at', 'TEXT'),
|
|
154
|
+
field('state', 'TEXT', { notNull: true, default: "'active'" }),
|
|
155
|
+
], [
|
|
156
|
+
index('idx_kernel_sessions_common_dir_state', ['git_common_dir', 'state']),
|
|
157
|
+
]),
|
|
158
|
+
table('worktrees', 'authority', [
|
|
159
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
160
|
+
field('git_common_dir', 'TEXT', { notNull: true }),
|
|
161
|
+
field('path', 'TEXT', { notNull: true, storageClass: 'configuration' }),
|
|
162
|
+
field('branch', 'TEXT', { notNull: true }),
|
|
163
|
+
field('actor', 'TEXT'),
|
|
164
|
+
// The kernel linkage backbone: a worktree row records which issue it serves
|
|
165
|
+
// (issue_id) and which work-folder that issue owns (work_folder, repo-relative).
|
|
166
|
+
// Both are nullable — a worktree can be registered before its issue/work-folder
|
|
167
|
+
// is known. Added by migration 007 (see migrations.js MIGRATION_ADDED_COLUMNS).
|
|
168
|
+
field('issue_id', 'TEXT'),
|
|
169
|
+
field('work_folder', 'TEXT', { storageClass: 'configuration' }),
|
|
170
|
+
field('registered_at', 'TEXT', { notNull: true }),
|
|
171
|
+
field('state', 'TEXT', { notNull: true, default: "'active'" }),
|
|
172
|
+
], [
|
|
173
|
+
index('idx_kernel_worktrees_common_dir_branch', ['git_common_dir', 'branch']),
|
|
174
|
+
]),
|
|
175
|
+
table('stage_runs', 'authority', [
|
|
176
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
177
|
+
field('issue_id', 'TEXT', { notNull: true, references: 'issues.id' }),
|
|
178
|
+
field('stage', 'TEXT', { notNull: true }),
|
|
179
|
+
field('substage', 'TEXT'),
|
|
180
|
+
field('status', 'TEXT', { notNull: true }),
|
|
181
|
+
field('started_at', 'TEXT', { notNull: true }),
|
|
182
|
+
field('completed_at', 'TEXT'),
|
|
183
|
+
field('evidence_id', 'TEXT'),
|
|
184
|
+
], [
|
|
185
|
+
index('idx_kernel_stage_runs_issue_stage', ['issue_id', 'stage']),
|
|
186
|
+
index('idx_kernel_stage_runs_status', ['status']),
|
|
187
|
+
]),
|
|
188
|
+
table('evidence', 'archive', [
|
|
189
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
190
|
+
field('issue_id', 'TEXT'),
|
|
191
|
+
field('run_id', 'TEXT'),
|
|
192
|
+
field('kind', 'TEXT', { notNull: true }),
|
|
193
|
+
field('uri', 'TEXT'),
|
|
194
|
+
field('summary', 'TEXT'),
|
|
195
|
+
field('created_at', 'TEXT', { notNull: true }),
|
|
196
|
+
], [
|
|
197
|
+
index('idx_kernel_evidence_issue_kind', ['issue_id', 'kind']),
|
|
198
|
+
]),
|
|
199
|
+
table('projections', 'projection', [
|
|
200
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
201
|
+
field('target', 'TEXT', { notNull: true, storageClass: 'external_provider', authority: 'configured_provider' }),
|
|
202
|
+
field('entity_type', 'TEXT', { notNull: true }),
|
|
203
|
+
field('entity_id', 'TEXT', { notNull: true }),
|
|
204
|
+
field('status', 'TEXT', { notNull: true }),
|
|
205
|
+
field('last_error', 'TEXT', { storageClass: 'cache', authority: 'projection_only' }),
|
|
206
|
+
field('updated_at', 'TEXT', { notNull: true }),
|
|
207
|
+
], [
|
|
208
|
+
index('idx_kernel_projections_target_status', ['target', 'status']),
|
|
209
|
+
index('idx_kernel_projections_entity', ['entity_type', 'entity_id']),
|
|
210
|
+
]),
|
|
211
|
+
table('conflicts', 'authority', [
|
|
212
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
213
|
+
field('entity_type', 'TEXT', { notNull: true }),
|
|
214
|
+
field('entity_id', 'TEXT', { notNull: true }),
|
|
215
|
+
field('expected_revision', 'INTEGER', { notNull: true }),
|
|
216
|
+
field('actual_revision', 'INTEGER', { notNull: true }),
|
|
217
|
+
field('status', 'TEXT', { notNull: true, default: "'quarantined'" }),
|
|
218
|
+
field('payload_json', 'TEXT', { notNull: true }),
|
|
219
|
+
field('created_at', 'TEXT', { notNull: true }),
|
|
220
|
+
], [
|
|
221
|
+
index('idx_kernel_conflicts_status', ['status']),
|
|
222
|
+
index('idx_kernel_conflicts_entity', ['entity_type', 'entity_id']),
|
|
223
|
+
]),
|
|
224
|
+
table('events', 'authority', [
|
|
225
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
226
|
+
field('entity_type', 'TEXT', { notNull: true }),
|
|
227
|
+
field('entity_id', 'TEXT', { notNull: true }),
|
|
228
|
+
field('event_type', 'TEXT', { notNull: true }),
|
|
229
|
+
field('idempotency_key', 'TEXT', { notNull: true }),
|
|
230
|
+
field('expected_revision', 'INTEGER', { notNull: true }),
|
|
231
|
+
field('actor', 'TEXT', { notNull: true }),
|
|
232
|
+
field('origin', 'TEXT', { notNull: true }),
|
|
233
|
+
field('payload_json', 'TEXT', { notNull: true }),
|
|
234
|
+
field('created_at', 'TEXT', { notNull: true }),
|
|
235
|
+
], [
|
|
236
|
+
index('idx_kernel_events_entity_created', ['entity_type', 'entity_id', 'created_at']),
|
|
237
|
+
index('idx_kernel_events_idempotency', ['idempotency_key'], { unique: true }),
|
|
238
|
+
]),
|
|
239
|
+
table('outbox', 'projection', [
|
|
240
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
241
|
+
field('event_id', 'TEXT', { notNull: true, references: 'events.id' }),
|
|
242
|
+
field('target', 'TEXT', { notNull: true }),
|
|
243
|
+
field('status', 'TEXT', { notNull: true, default: "'pending'" }),
|
|
244
|
+
field('attempts', 'INTEGER', { notNull: true, default: '0' }),
|
|
245
|
+
field('next_attempt_at', 'TEXT'),
|
|
246
|
+
field('created_at', 'TEXT', { notNull: true }),
|
|
247
|
+
], [
|
|
248
|
+
index('idx_kernel_outbox_target_status', ['target', 'status']),
|
|
249
|
+
]),
|
|
250
|
+
table('dead_letters', 'projection', [
|
|
251
|
+
field('id', 'TEXT', { primaryKey: true }),
|
|
252
|
+
field('outbox_id', 'TEXT'),
|
|
253
|
+
field('target', 'TEXT', { notNull: true }),
|
|
254
|
+
field('status', 'TEXT', { notNull: true, default: "'open'" }),
|
|
255
|
+
field('error', 'TEXT', { notNull: true }),
|
|
256
|
+
field('payload_json', 'TEXT', { notNull: true }),
|
|
257
|
+
field('created_at', 'TEXT', { notNull: true }),
|
|
258
|
+
], [
|
|
259
|
+
index('idx_kernel_dead_letters_status', ['status']),
|
|
260
|
+
]),
|
|
261
|
+
// Project memory is a Forge read model: durable key/value notes written DIRECTLY
|
|
262
|
+
// (NOT through the issue CAS/guarded-event path). `key` is the stable identity;
|
|
263
|
+
// value_json holds the serialized memory value, and the *_json columns carry the
|
|
264
|
+
// optional list fields. Created by migration 005 (excluded from the 001 initial
|
|
265
|
+
// schema), so a fresh DB creates it exactly once and an existing DB picks it up
|
|
266
|
+
// via the broker ledger.
|
|
267
|
+
table('memories', 'read_model', [
|
|
268
|
+
field('key', 'TEXT', { primaryKey: true }),
|
|
269
|
+
field('value_json', 'TEXT', { notNull: true }),
|
|
270
|
+
field('source_agent', 'TEXT', { notNull: true }),
|
|
271
|
+
field('scope', 'TEXT'),
|
|
272
|
+
field('confidence', 'REAL'),
|
|
273
|
+
field('tags_json', 'TEXT'),
|
|
274
|
+
field('supersedes_json', 'TEXT'),
|
|
275
|
+
field('beads_refs_json', 'TEXT'),
|
|
276
|
+
field('created_at', 'TEXT', { notNull: true }),
|
|
277
|
+
field('updated_at', 'TEXT', { notNull: true }),
|
|
278
|
+
], [
|
|
279
|
+
index('idx_kernel_memories_source_agent', ['source_agent']),
|
|
280
|
+
]),
|
|
281
|
+
]);
|
|
282
|
+
|
|
283
|
+
const KERNEL_TABLES = deepFreeze(Object.fromEntries(TABLE_LIST.map(candidate => [candidate.name, candidate])));
|
|
284
|
+
|
|
285
|
+
function classifyKernelStorage(tableName) {
|
|
286
|
+
const tableDefinition = KERNEL_TABLES[tableName];
|
|
287
|
+
if (!tableDefinition) {
|
|
288
|
+
throw new Error(`Unknown Kernel table: ${tableName}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
table: tableDefinition.name,
|
|
293
|
+
sqlName: tableDefinition.sqlName,
|
|
294
|
+
storageClass: tableDefinition.storageClass,
|
|
295
|
+
authority: tableDefinition.authority,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function validateKernelSchema(schema = getKernelSchema()) {
|
|
300
|
+
const tableNames = new Set();
|
|
301
|
+
for (const tableDefinition of schema.tables) {
|
|
302
|
+
if (tableNames.has(tableDefinition.name)) {
|
|
303
|
+
throw new Error(`Duplicate Kernel table: ${tableDefinition.name}`);
|
|
304
|
+
}
|
|
305
|
+
tableNames.add(tableDefinition.name);
|
|
306
|
+
if (!KERNEL_STORAGE_CLASSES.includes(tableDefinition.storageClass)) {
|
|
307
|
+
throw new Error(`Invalid storage class for ${tableDefinition.name}: ${tableDefinition.storageClass}`);
|
|
308
|
+
}
|
|
309
|
+
if (!KERNEL_FIELD_AUTHORITIES.includes(tableDefinition.authority)) {
|
|
310
|
+
throw new Error(`Invalid table authority for ${tableDefinition.name}: ${tableDefinition.authority}`);
|
|
311
|
+
}
|
|
312
|
+
for (const fieldDefinition of tableDefinition.fields) {
|
|
313
|
+
if (!KERNEL_STORAGE_CLASSES.includes(fieldDefinition.storageClass)) {
|
|
314
|
+
throw new Error(`Invalid storage class for ${tableDefinition.name}.${fieldDefinition.name}: ${fieldDefinition.storageClass}`);
|
|
315
|
+
}
|
|
316
|
+
if (!KERNEL_FIELD_AUTHORITIES.includes(fieldDefinition.authority)) {
|
|
317
|
+
throw new Error(`Invalid field authority for ${tableDefinition.name}.${fieldDefinition.name}: ${fieldDefinition.authority}`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Deep-clone a table list so callers receive isolated, mutation-safe schema definitions. */
|
|
326
|
+
function cloneTableList(tableList) {
|
|
327
|
+
return tableList.map(tableDefinition => ({
|
|
328
|
+
...tableDefinition,
|
|
329
|
+
fields: tableDefinition.fields.map(fieldDefinition => ({ ...fieldDefinition })),
|
|
330
|
+
indexes: tableDefinition.indexes.map(indexDefinition => ({
|
|
331
|
+
...indexDefinition,
|
|
332
|
+
columns: [...indexDefinition.columns],
|
|
333
|
+
})),
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getKernelSchema() {
|
|
338
|
+
return {
|
|
339
|
+
version: 1,
|
|
340
|
+
tables: cloneTableList(TABLE_LIST),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
module.exports = {
|
|
345
|
+
KERNEL_FIELD_AUTHORITIES,
|
|
346
|
+
KERNEL_STORAGE_CLASSES,
|
|
347
|
+
KERNEL_TABLES,
|
|
348
|
+
classifyKernelStorage,
|
|
349
|
+
cloneTableList,
|
|
350
|
+
deepFreeze,
|
|
351
|
+
field,
|
|
352
|
+
getKernelSchema,
|
|
353
|
+
index,
|
|
354
|
+
table,
|
|
355
|
+
validateKernelSchema,
|
|
356
|
+
};
|