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
package/lib/insights.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const typedMemory = require('./memory/typed-api');
|
|
6
|
+
|
|
7
|
+
const ISSUE_REFS = ['forge-besw.12', 'forge-1gry', 'forge-5q7s'];
|
|
8
|
+
const DEFAULT_MIN_COUNT = 5;
|
|
9
|
+
const DEFAULT_LIMIT = 10;
|
|
10
|
+
const STOP_WORDS = new Set([
|
|
11
|
+
'after',
|
|
12
|
+
'against',
|
|
13
|
+
'and',
|
|
14
|
+
'are',
|
|
15
|
+
'beads',
|
|
16
|
+
'command',
|
|
17
|
+
'commands',
|
|
18
|
+
'forge',
|
|
19
|
+
'from',
|
|
20
|
+
'into',
|
|
21
|
+
'issue',
|
|
22
|
+
'stage',
|
|
23
|
+
'task',
|
|
24
|
+
'that',
|
|
25
|
+
'the',
|
|
26
|
+
'this',
|
|
27
|
+
'with',
|
|
28
|
+
'work',
|
|
29
|
+
'workflow',
|
|
30
|
+
'workflows',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function readJsonl(filePath) {
|
|
34
|
+
if (!fs.existsSync(filePath)) return [];
|
|
35
|
+
return fs.readFileSync(filePath, 'utf8')
|
|
36
|
+
.split(/\r?\n/)
|
|
37
|
+
.map(line => line.trim())
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map((line, index) => {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(line);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return {
|
|
44
|
+
_parseError: true,
|
|
45
|
+
line: index + 1,
|
|
46
|
+
error: error.message,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function asDate(value) {
|
|
53
|
+
const date = new Date(value);
|
|
54
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isSince(value, since) {
|
|
58
|
+
if (!since) return true;
|
|
59
|
+
const date = asDate(value);
|
|
60
|
+
return date ? date >= since : false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function slug(value) {
|
|
64
|
+
const chars = [];
|
|
65
|
+
let previousWasDash = true;
|
|
66
|
+
for (const char of String(value || 'pattern').toLowerCase()) {
|
|
67
|
+
const isAlphaNumeric = (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9');
|
|
68
|
+
if (isAlphaNumeric) {
|
|
69
|
+
chars.push(char);
|
|
70
|
+
previousWasDash = false;
|
|
71
|
+
} else if (!previousWasDash) {
|
|
72
|
+
chars.push('-');
|
|
73
|
+
previousWasDash = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (chars.at(-1) === '-') chars.pop();
|
|
77
|
+
const normalized = chars.join('');
|
|
78
|
+
return normalized || 'pattern';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function reasonFamily(reason = '') {
|
|
82
|
+
const lower = String(reason).toLowerCase();
|
|
83
|
+
if (lower.includes('merged') && lower.includes('verified')) return 'merged-and-verified';
|
|
84
|
+
if (lower.includes('superseded')) return 'superseded';
|
|
85
|
+
if (lower.includes('completed')) return 'completed';
|
|
86
|
+
if (lower.includes('review')) return 'review-outcome';
|
|
87
|
+
if (lower.includes('claim')) return 'claimed';
|
|
88
|
+
return 'unspecified';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function addPattern(map, key, patch) {
|
|
92
|
+
const existing = map.get(key) ?? {
|
|
93
|
+
key,
|
|
94
|
+
kind: patch.kind,
|
|
95
|
+
title: patch.title,
|
|
96
|
+
count: 0,
|
|
97
|
+
evidence: [],
|
|
98
|
+
sources: new Set(),
|
|
99
|
+
lastSeen: null,
|
|
100
|
+
};
|
|
101
|
+
existing.count += patch.count ?? 1;
|
|
102
|
+
if (patch.evidence) existing.evidence.push(patch.evidence);
|
|
103
|
+
if (patch.source) existing.sources.add(patch.source);
|
|
104
|
+
const seen = asDate(patch.lastSeen);
|
|
105
|
+
if (seen && (!existing.lastSeen || seen > existing.lastSeen)) {
|
|
106
|
+
existing.lastSeen = seen;
|
|
107
|
+
}
|
|
108
|
+
map.set(key, existing);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function interactionPatterns(projectRoot, options, map) {
|
|
112
|
+
const rows = readJsonl(path.join(projectRoot, '.beads', 'interactions.jsonl'));
|
|
113
|
+
for (const row of rows) {
|
|
114
|
+
if (!row || typeof row !== 'object' || row._parseError || !isSince(row.created_at, options.since)) continue;
|
|
115
|
+
const extra = row.extra && typeof row.extra === 'object' ? row.extra : {};
|
|
116
|
+
if (row.kind === 'field_change' && extra.field) {
|
|
117
|
+
const family = reasonFamily(extra.reason);
|
|
118
|
+
const key = `interaction:${extra.field}:${extra.new_value || 'changed'}:${family}`;
|
|
119
|
+
addPattern(map, key, {
|
|
120
|
+
kind: 'interaction',
|
|
121
|
+
title: `${extra.field} changed to ${extra.new_value || 'changed'} (${family})`,
|
|
122
|
+
evidence: row.issue_id || row.id,
|
|
123
|
+
source: '.beads/interactions.jsonl',
|
|
124
|
+
lastSeen: row.created_at,
|
|
125
|
+
});
|
|
126
|
+
} else if (row.kind) {
|
|
127
|
+
addPattern(map, `interaction:${row.kind}`, {
|
|
128
|
+
kind: 'interaction',
|
|
129
|
+
title: `Interaction event: ${row.kind}`,
|
|
130
|
+
evidence: row.issue_id || row.id,
|
|
131
|
+
source: '.beads/interactions.jsonl',
|
|
132
|
+
lastSeen: row.created_at,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return rows;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function words(value) {
|
|
140
|
+
const tokens = [];
|
|
141
|
+
let current = '';
|
|
142
|
+
const startsWithLetter = token => token.length > 0 && token[0] >= 'a' && token[0] <= 'z';
|
|
143
|
+
for (const char of String(value || '').toLowerCase()) {
|
|
144
|
+
const isAlphaNumeric = (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9');
|
|
145
|
+
if (isAlphaNumeric || (char === '-' && current.length > 0)) {
|
|
146
|
+
current += char;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (startsWithLetter(current) && current.length >= 4) tokens.push(current);
|
|
150
|
+
current = '';
|
|
151
|
+
}
|
|
152
|
+
if (startsWithLetter(current) && current.length >= 4) tokens.push(current);
|
|
153
|
+
return tokens;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function issuePatterns(projectRoot, options, map) {
|
|
157
|
+
const rows = readJsonl(path.join(projectRoot, '.beads', 'issues.jsonl'))
|
|
158
|
+
.filter(row => row && !row._parseError && row._type === 'issue');
|
|
159
|
+
const perWord = new Map();
|
|
160
|
+
for (const issue of rows) {
|
|
161
|
+
if (!isSince(issue.updated_at || issue.closed_at || issue.created_at, options.since)) continue;
|
|
162
|
+
const seen = new Set(words(`${issue.title || ''} ${issue.description || ''}`)
|
|
163
|
+
.filter(word => !STOP_WORDS.has(word)));
|
|
164
|
+
for (const word of seen) {
|
|
165
|
+
if (!perWord.has(word)) perWord.set(word, []);
|
|
166
|
+
perWord.get(word).push(issue);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const [word, issues] of perWord) {
|
|
170
|
+
if (issues.length < options.minCount) continue;
|
|
171
|
+
addPattern(map, `issue-theme:${word}`, {
|
|
172
|
+
kind: 'issue-theme',
|
|
173
|
+
title: `Recurring issue theme: ${word}`,
|
|
174
|
+
count: issues.length,
|
|
175
|
+
evidence: issues.slice(0, 5).map(issue => issue.id).join(', '),
|
|
176
|
+
source: '.beads/issues.jsonl',
|
|
177
|
+
lastSeen: issues.map(issue => asDate(issue.updated_at || issue.closed_at || issue.created_at))
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
.sort((a, b) => b - a)[0],
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return rows;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function auditPatterns(projectRoot, options, map) {
|
|
186
|
+
const sources = ['.forge/log.jsonl', '.forge/audit.log'];
|
|
187
|
+
const rows = sources.flatMap(source => readJsonl(path.join(projectRoot, source))
|
|
188
|
+
.map(row => ({ row, source })));
|
|
189
|
+
for (const { row, source } of rows) {
|
|
190
|
+
if (!row || typeof row !== 'object' || row._parseError || !isSince(row.timestamp || row.created_at, options.since)) continue;
|
|
191
|
+
const kind = row.kind || row.event || row.type;
|
|
192
|
+
if (!kind) continue;
|
|
193
|
+
addPattern(map, `audit:${kind}`, {
|
|
194
|
+
kind: 'audit',
|
|
195
|
+
title: `Audit event: ${kind}`,
|
|
196
|
+
evidence: row.issue_id || row.taskId || row.id || kind,
|
|
197
|
+
source,
|
|
198
|
+
lastSeen: row.timestamp || row.created_at,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return rows;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeOptions(options = {}) {
|
|
205
|
+
const minCount = Number.isFinite(Number(options.minCount)) ? Number(options.minCount) : DEFAULT_MIN_COUNT;
|
|
206
|
+
const limit = Number.isFinite(Number(options.limit)) ? Number(options.limit) : DEFAULT_LIMIT;
|
|
207
|
+
const since = options.since ? asDate(options.since) : null;
|
|
208
|
+
if (options.since && !since) {
|
|
209
|
+
throw new Error(`Invalid --since date: ${options.since}`);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
minCount: Math.max(1, minCount),
|
|
213
|
+
limit: Math.max(1, limit),
|
|
214
|
+
since,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function toPatternList(map, options) {
|
|
219
|
+
return [...map.values()]
|
|
220
|
+
.map(pattern => {
|
|
221
|
+
const weight = pattern.kind === 'issue-theme' ? 1 : 10;
|
|
222
|
+
return {
|
|
223
|
+
...pattern,
|
|
224
|
+
sources: [...pattern.sources],
|
|
225
|
+
evidence: [...new Set(pattern.evidence)].slice(0, 6),
|
|
226
|
+
lastSeen: pattern.lastSeen ? pattern.lastSeen.toISOString() : null,
|
|
227
|
+
score: pattern.count * weight + pattern.sources.size * 5 + Math.min(pattern.evidence.length, 5),
|
|
228
|
+
};
|
|
229
|
+
})
|
|
230
|
+
.filter(pattern => pattern.count >= options.minCount)
|
|
231
|
+
.sort((a, b) => b.score - a.score || b.count - a.count || a.key.localeCompare(b.key))
|
|
232
|
+
.slice(0, options.limit);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function candidateFromPattern(pattern) {
|
|
236
|
+
const id = `insight-${slug(pattern.key).slice(0, 52)}`;
|
|
237
|
+
return {
|
|
238
|
+
id,
|
|
239
|
+
title: pattern.title,
|
|
240
|
+
score: pattern.score,
|
|
241
|
+
patternKey: pattern.key,
|
|
242
|
+
nextStep: `Review ${pattern.kind} evidence and consider a local workflow skill only if the pattern is still useful.`,
|
|
243
|
+
evidence: pattern.evidence,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function analyzeInsights(projectRoot, options = {}) {
|
|
248
|
+
const normalized = normalizeOptions(options);
|
|
249
|
+
const map = new Map();
|
|
250
|
+
const interactions = interactionPatterns(projectRoot, normalized, map);
|
|
251
|
+
const issues = issuePatterns(projectRoot, normalized, map);
|
|
252
|
+
const audit = auditPatterns(projectRoot, normalized, map);
|
|
253
|
+
const patterns = toPatternList(map, normalized);
|
|
254
|
+
const candidates = patterns.map(candidateFromPattern);
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
generatedAt: new Date().toISOString(),
|
|
258
|
+
minCount: normalized.minCount,
|
|
259
|
+
limit: normalized.limit,
|
|
260
|
+
sources: {
|
|
261
|
+
interactions: interactions.length,
|
|
262
|
+
issues: issues.length,
|
|
263
|
+
audit: audit.length,
|
|
264
|
+
},
|
|
265
|
+
lowSignal: candidates.length === 0,
|
|
266
|
+
patterns,
|
|
267
|
+
candidates,
|
|
268
|
+
limitations: [
|
|
269
|
+
'Insights are local workflow signals, not proof of correctness.',
|
|
270
|
+
'Sparse Beads interactions or missing audit logs reduce confidence.',
|
|
271
|
+
'Accepting a suggestion records a decision; it does not install trusted executable code.',
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function formatInsightsText(result) {
|
|
277
|
+
const lines = [
|
|
278
|
+
'Forge insights',
|
|
279
|
+
`Sources: interactions=${result.sources.interactions}, issues=${result.sources.issues}, audit=${result.sources.audit}`,
|
|
280
|
+
];
|
|
281
|
+
if (result.lowSignal) {
|
|
282
|
+
lines.push(
|
|
283
|
+
'No strong recurring patterns found.',
|
|
284
|
+
`Threshold: min-count=${result.minCount}`,
|
|
285
|
+
);
|
|
286
|
+
} else {
|
|
287
|
+
lines.push('Ranked candidates:');
|
|
288
|
+
for (const candidate of result.candidates) {
|
|
289
|
+
lines.push(
|
|
290
|
+
`- ${candidate.id} (${candidate.score}): ${candidate.title}`,
|
|
291
|
+
` Next: ${candidate.nextStep}`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return `${[
|
|
296
|
+
...lines,
|
|
297
|
+
'Limitations:',
|
|
298
|
+
...result.limitations.map(limitation => `- ${limitation}`),
|
|
299
|
+
].join('\n')}\n`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function recordInsightDecision(projectRoot, candidateId, status, options = {}) {
|
|
303
|
+
if (!['accepted', 'rejected'].includes(status)) {
|
|
304
|
+
throw new Error('Insight decision status must be accepted or rejected');
|
|
305
|
+
}
|
|
306
|
+
if (!candidateId || typeof candidateId !== 'string') {
|
|
307
|
+
throw new Error('Insight candidate id is required');
|
|
308
|
+
}
|
|
309
|
+
return typedMemory.writeSkill(projectRoot, candidateId, {
|
|
310
|
+
candidateId,
|
|
311
|
+
status,
|
|
312
|
+
note: options.note || '',
|
|
313
|
+
decidedAt: new Date().toISOString(),
|
|
314
|
+
}, {
|
|
315
|
+
memory: options.memory,
|
|
316
|
+
tags: ['insights', status],
|
|
317
|
+
beadsRefs: ISSUE_REFS,
|
|
318
|
+
provenance: {
|
|
319
|
+
actor: 'forge insights',
|
|
320
|
+
reason: `Insight suggestion ${status}`,
|
|
321
|
+
source: 'forge insights',
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function issueSummary(issues) {
|
|
327
|
+
return issues.reduce((summary, issue) => {
|
|
328
|
+
summary.total += 1;
|
|
329
|
+
if (issue.status === 'closed') summary.closed += 1;
|
|
330
|
+
else summary.open += 1;
|
|
331
|
+
return summary;
|
|
332
|
+
}, { total: 0, open: 0, closed: 0 });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function buildRecap(projectRoot, options = {}) {
|
|
336
|
+
const normalized = normalizeOptions(options);
|
|
337
|
+
const insights = analyzeInsights(projectRoot, options);
|
|
338
|
+
const issues = readJsonl(path.join(projectRoot, '.beads', 'issues.jsonl'))
|
|
339
|
+
.filter(row => row && !row._parseError && row._type === 'issue')
|
|
340
|
+
.filter(issue => isSince(issue.updated_at || issue.closed_at || issue.created_at, normalized.since));
|
|
341
|
+
const interactions = readJsonl(path.join(projectRoot, '.beads', 'interactions.jsonl'))
|
|
342
|
+
.filter(row => row && typeof row === 'object' && !row._parseError)
|
|
343
|
+
.filter(row => isSince(row.created_at, normalized.since));
|
|
344
|
+
const reviewOutcomes = interactions.filter(row => {
|
|
345
|
+
const reason = row.extra?.reason || '';
|
|
346
|
+
return reasonFamily(reason) === 'merged-and-verified' || String(reason).toLowerCase().includes('review');
|
|
347
|
+
}).length;
|
|
348
|
+
const recentIssues = [...issues]
|
|
349
|
+
.sort((a, b) => String(b.updated_at || b.closed_at || b.created_at || '').localeCompare(String(a.updated_at || a.closed_at || a.created_at || '')))
|
|
350
|
+
.slice(0, normalized.limit)
|
|
351
|
+
.map(issue => ({
|
|
352
|
+
id: issue.id,
|
|
353
|
+
title: issue.title,
|
|
354
|
+
status: issue.status,
|
|
355
|
+
}));
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
generatedAt: new Date().toISOString(),
|
|
359
|
+
issueSummary: issueSummary(issues),
|
|
360
|
+
reviewOutcomes,
|
|
361
|
+
recentIssues,
|
|
362
|
+
insights,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function formatRecapText(recap) {
|
|
367
|
+
const lines = [
|
|
368
|
+
'Forge recap',
|
|
369
|
+
`Issues: ${recap.issueSummary.total} total, ${recap.issueSummary.open} open, ${recap.issueSummary.closed} closed`,
|
|
370
|
+
`Review outcomes found: ${recap.reviewOutcomes}`,
|
|
371
|
+
'Recent work:',
|
|
372
|
+
];
|
|
373
|
+
for (const issue of recap.recentIssues) {
|
|
374
|
+
lines.push(`- ${issue.id}: ${issue.title} [${issue.status || 'unknown'}]`);
|
|
375
|
+
}
|
|
376
|
+
lines.push('Insight candidates:');
|
|
377
|
+
if (recap.insights.candidates.length === 0) {
|
|
378
|
+
lines.push('- No strong recurring patterns found.');
|
|
379
|
+
} else {
|
|
380
|
+
for (const candidate of recap.insights.candidates) {
|
|
381
|
+
lines.push(`- ${candidate.id}: ${candidate.title}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
lines.push('Limitations:');
|
|
385
|
+
for (const limitation of recap.insights.limitations) lines.push(`- ${limitation}`);
|
|
386
|
+
return `${lines.join('\n')}\n`;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
module.exports = {
|
|
390
|
+
ISSUE_REFS,
|
|
391
|
+
analyzeInsights,
|
|
392
|
+
buildRecap,
|
|
393
|
+
formatInsightsText,
|
|
394
|
+
formatRecapText,
|
|
395
|
+
readJsonl,
|
|
396
|
+
recordInsightDecision,
|
|
397
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getFieldAuthority } = require('./issue-sync/authority.js');
|
|
4
|
+
|
|
5
|
+
const REQUIRED_ISSUE_ADAPTER_METHODS = [
|
|
6
|
+
'list',
|
|
7
|
+
'read',
|
|
8
|
+
'create',
|
|
9
|
+
'update',
|
|
10
|
+
'close',
|
|
11
|
+
'comment',
|
|
12
|
+
'mapStatus',
|
|
13
|
+
'decideAuthority',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const ABSTRACT_ISSUE_ADAPTER_METHODS = new Set([
|
|
17
|
+
'list',
|
|
18
|
+
'read',
|
|
19
|
+
'create',
|
|
20
|
+
'update',
|
|
21
|
+
'close',
|
|
22
|
+
'comment',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const GITHUB_OPEN_STATUSES = new Set(['open', 'in_progress', 'blocked', 'todo', 'ready']);
|
|
26
|
+
const GITHUB_CLOSED_STATUSES = new Set(['closed', 'done', 'resolved', 'complete', 'completed']);
|
|
27
|
+
|
|
28
|
+
class IssueAdapter {
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
this.id = options.id || 'issue-adapter';
|
|
31
|
+
this.kind = options.kind || 'issue';
|
|
32
|
+
this.name = options.name || this.id;
|
|
33
|
+
this.version = options.version || '0.1.0';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async list() {
|
|
37
|
+
throw new Error(`${this.id}.list is not implemented`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async read() {
|
|
41
|
+
throw new Error(`${this.id}.read is not implemented`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async create() {
|
|
45
|
+
throw new Error(`${this.id}.create is not implemented`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async update() {
|
|
49
|
+
throw new Error(`${this.id}.update is not implemented`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async close() {
|
|
53
|
+
throw new Error(`${this.id}.close is not implemented`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async comment() {
|
|
57
|
+
throw new Error(`${this.id}.comment is not implemented`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
mapStatus(status, context) {
|
|
61
|
+
return normalizeIssueStatus(status, context);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
decideAuthority(change, context) {
|
|
65
|
+
return decideIssueAuthority(change, context);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function validateIssueAdapter(adapter) {
|
|
70
|
+
const errors = [];
|
|
71
|
+
|
|
72
|
+
if (!adapter || typeof adapter !== 'object') {
|
|
73
|
+
return { valid: false, errors: ['adapter must be an object'] };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!adapter.id || typeof adapter.id !== 'string') {
|
|
77
|
+
errors.push('id must be a non-empty string');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (adapter.kind !== 'issue') {
|
|
81
|
+
errors.push('kind must be "issue"');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const method of REQUIRED_ISSUE_ADAPTER_METHODS) {
|
|
85
|
+
if (typeof adapter[method] !== 'function') {
|
|
86
|
+
errors.push(`${method} must be a function`);
|
|
87
|
+
} else if (
|
|
88
|
+
ABSTRACT_ISSUE_ADAPTER_METHODS.has(method)
|
|
89
|
+
&& adapter[method] === IssueAdapter.prototype[method]
|
|
90
|
+
) {
|
|
91
|
+
errors.push(`${method} must be implemented by the adapter`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { valid: errors.length === 0, errors };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function normalizeIssueStatus(status, options = {}) {
|
|
99
|
+
const normalized = typeof status === 'string'
|
|
100
|
+
? status.trim().toLowerCase().replace(/[-\s]+/g, '_')
|
|
101
|
+
: '';
|
|
102
|
+
|
|
103
|
+
if (options.target === 'github') {
|
|
104
|
+
if (GITHUB_CLOSED_STATUSES.has(normalized)) {
|
|
105
|
+
return 'closed';
|
|
106
|
+
}
|
|
107
|
+
if (GITHUB_OPEN_STATUSES.has(normalized)) {
|
|
108
|
+
return 'open';
|
|
109
|
+
}
|
|
110
|
+
return 'open';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return normalized || 'open';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function decideIssueAuthority(change = {}, _context = {}) {
|
|
117
|
+
const authority = getFieldAuthority(change.fieldPath);
|
|
118
|
+
|
|
119
|
+
if (authority === 'github') {
|
|
120
|
+
return {
|
|
121
|
+
authority,
|
|
122
|
+
action: change.direction === 'push' ? 'project-to-github' : 'apply-remote',
|
|
123
|
+
conflict: change.direction === 'push' ? 'remote-wins-on-pull' : 'record-drift',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (authority === 'forge') {
|
|
128
|
+
return {
|
|
129
|
+
authority,
|
|
130
|
+
action: change.direction === 'push' ? 'keep-local' : 'preserve-local',
|
|
131
|
+
conflict: change.direction === 'push' ? 'not-shared-to-github' : 'ignore-remote',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (authority === 'cache') {
|
|
136
|
+
return {
|
|
137
|
+
authority,
|
|
138
|
+
action: 'rebuild-cache',
|
|
139
|
+
conflict: 'derived',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
authority: null,
|
|
145
|
+
action: 'reject',
|
|
146
|
+
conflict: 'unknown-field',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
IssueAdapter,
|
|
152
|
+
REQUIRED_ISSUE_ADAPTER_METHODS,
|
|
153
|
+
decideIssueAuthority,
|
|
154
|
+
normalizeIssueStatus,
|
|
155
|
+
validateIssueAdapter,
|
|
156
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const VALID_BACKENDS = new Set(['kernel', 'beads']);
|
|
7
|
+
const DEFAULT_BACKEND = 'kernel';
|
|
8
|
+
const ENV_VAR = 'FORGE_ISSUE_BACKEND';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Read the `issueBackend` key from `<projectRoot>/.forge/config.yaml`, if the
|
|
12
|
+
* file exists and is parseable. Returns `null` when the file is missing, the
|
|
13
|
+
* key is absent, or the YAML cannot be parsed. Never throws.
|
|
14
|
+
*
|
|
15
|
+
* @param {string|undefined} projectRoot
|
|
16
|
+
* @returns {string|null}
|
|
17
|
+
*/
|
|
18
|
+
function readConfigBackend(projectRoot) {
|
|
19
|
+
if (!projectRoot) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const configPath = path.join(projectRoot, '.forge', 'config.yaml');
|
|
24
|
+
if (!fs.existsSync(configPath)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
// Lazy-require so the default (no-config) issue path imports no YAML parser at
|
|
31
|
+
// module load — only a project that actually ships .forge/config.yaml pays for it.
|
|
32
|
+
const YAML = require('yaml');
|
|
33
|
+
parsed = YAML.parse(fs.readFileSync(configPath, 'utf8'));
|
|
34
|
+
} catch {
|
|
35
|
+
// A malformed config file should not crash issue commands; treat as absent.
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const value = parsed.issueBackend;
|
|
44
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Gather a candidate backend value (without validation) following the documented
|
|
49
|
+
* precedence: explicit deps > env > config. Returns `{ value, source }` or
|
|
50
|
+
* `{ value: null, source: null }` when no signal exists.
|
|
51
|
+
*/
|
|
52
|
+
function collectBackendSignal({ deps = {}, env = process.env, projectRoot } = {}) {
|
|
53
|
+
if (typeof deps.issueBackend === 'string' && deps.issueBackend.trim()) {
|
|
54
|
+
return { value: deps.issueBackend.trim(), source: 'deps' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const envValue = env && env[ENV_VAR];
|
|
58
|
+
if (typeof envValue === 'string' && envValue.trim()) {
|
|
59
|
+
return { value: envValue.trim(), source: 'env' };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const configValue = readConfigBackend(projectRoot);
|
|
63
|
+
if (configValue) {
|
|
64
|
+
return { value: configValue, source: 'config' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { value: null, source: null };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the active issue backend by precedence:
|
|
72
|
+
* explicit deps.issueBackend > FORGE_ISSUE_BACKEND env > .forge/config.yaml > 'kernel'.
|
|
73
|
+
*
|
|
74
|
+
* An unknown value (from any source) falls back to the default backend and emits
|
|
75
|
+
* a warning via the injected `warn` callback (defaults to console.warn).
|
|
76
|
+
*
|
|
77
|
+
* @param {object} [options]
|
|
78
|
+
* @param {object} [options.deps]
|
|
79
|
+
* @param {object} [options.env]
|
|
80
|
+
* @param {string} [options.projectRoot]
|
|
81
|
+
* @param {function(string): void} [options.warn]
|
|
82
|
+
* @returns {'kernel'|'beads'}
|
|
83
|
+
*/
|
|
84
|
+
function resolveIssueBackend({
|
|
85
|
+
deps = {},
|
|
86
|
+
env = process.env,
|
|
87
|
+
projectRoot,
|
|
88
|
+
warn = console.warn,
|
|
89
|
+
} = {}) {
|
|
90
|
+
const { value, source } = collectBackendSignal({ deps, env, projectRoot });
|
|
91
|
+
|
|
92
|
+
if (!value) {
|
|
93
|
+
return DEFAULT_BACKEND;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const normalized = value.toLowerCase();
|
|
97
|
+
if (VALID_BACKENDS.has(normalized)) {
|
|
98
|
+
return normalized;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
warn(
|
|
102
|
+
`Unknown issue backend "${value}" from ${source}; `
|
|
103
|
+
+ `falling back to "${DEFAULT_BACKEND}". Valid backends: ${[...VALID_BACKENDS].join(', ')}.`,
|
|
104
|
+
);
|
|
105
|
+
return DEFAULT_BACKEND;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns true when an explicit backend signal exists in any source (deps, env,
|
|
110
|
+
* or config) — regardless of whether the value is valid. Used by the CLI to
|
|
111
|
+
* decide whether to inject a resolved backend into command opts, so the default
|
|
112
|
+
* (no-signal) path stays byte-identical and opt-in.
|
|
113
|
+
*
|
|
114
|
+
* @param {object} [options] — same shape as resolveIssueBackend options.
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
function hasExplicitBackendSignal(options = {}) {
|
|
118
|
+
return collectBackendSignal(options).value !== null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Returns true when the active selection routes to the Kernel broker. Centralized
|
|
123
|
+
* in the backend-authority module so the command layer (_issue.js) and the issue
|
|
124
|
+
* service (forge-issues.js) share ONE definition and cannot drift. Accepts either
|
|
125
|
+
* CLI command opts or service deps — both carry the same selection keys.
|
|
126
|
+
*
|
|
127
|
+
* Note: this is an exact `=== 'kernel'` check, so callers must pass an already
|
|
128
|
+
* normalized backend value (use resolveIssueBackend to normalize case/validate).
|
|
129
|
+
*
|
|
130
|
+
* @param {object} [opts]
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*/
|
|
133
|
+
function shouldUseKernelBroker(opts = {}) {
|
|
134
|
+
return Boolean(opts.useKernelBroker || opts.kernelBroker || opts.issueBackend === 'kernel');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
resolveIssueBackend,
|
|
139
|
+
hasExplicitBackendSignal,
|
|
140
|
+
shouldUseKernelBroker,
|
|
141
|
+
readConfigBackend,
|
|
142
|
+
VALID_BACKENDS,
|
|
143
|
+
DEFAULT_BACKEND,
|
|
144
|
+
ENV_VAR,
|
|
145
|
+
};
|