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
package/lib/commands/plan.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
const fs = require('node:fs');
|
|
12
12
|
const path = require('node:path');
|
|
13
13
|
const { execFileSync } = require('node:child_process');
|
|
14
|
+
const { resolveIssueBackend } = require('../issue-backend');
|
|
15
|
+
const { runIssueOperation } = require('../forge-issues');
|
|
14
16
|
|
|
15
17
|
// Constants for security
|
|
16
18
|
// Note: cwd is resolved at call-time via getExecOptions() to avoid stale require-time snapshots
|
|
@@ -22,6 +24,21 @@ function getExecOptions() {
|
|
|
22
24
|
const MAX_SLUG_LENGTH = 100;
|
|
23
25
|
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Human-readable label for the issue backend that created an issue.
|
|
29
|
+
* Keeps printed output backend-accurate so kernel-created issues are not
|
|
30
|
+
* mislabeled as "Beads".
|
|
31
|
+
*
|
|
32
|
+
* @param {string} [backend] - Resolved issue backend ('kernel' | 'beads').
|
|
33
|
+
* @returns {string} Display label ('Kernel', 'Beads', or a neutral 'Issue').
|
|
34
|
+
* @private
|
|
35
|
+
*/
|
|
36
|
+
function issueBackendLabel(backend) {
|
|
37
|
+
if (backend === 'beads') return 'Beads';
|
|
38
|
+
if (backend === 'kernel') return 'Kernel';
|
|
39
|
+
return 'Issue';
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
/**
|
|
26
43
|
* Validate feature slug format
|
|
27
44
|
* Ensures slug matches expected pattern and doesn't contain path traversal
|
|
@@ -58,6 +75,33 @@ function validateFeatureSlug(slug) {
|
|
|
58
75
|
return { valid: true };
|
|
59
76
|
}
|
|
60
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Build the issue description shared by both issue backends (kernel and beads).
|
|
80
|
+
* Strategic scope appends a design-doc pointer derived from a sanitized slug.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} featureName
|
|
83
|
+
* @param {string} researchPath
|
|
84
|
+
* @param {'tactical'|'strategic'} scope
|
|
85
|
+
* @returns {{description: string}|{error: string}}
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
function buildFeatureIssueDescription(featureName, researchPath, scope) {
|
|
89
|
+
let description = `Research: ${researchPath}`;
|
|
90
|
+
|
|
91
|
+
if (scope === 'strategic') {
|
|
92
|
+
// Sanitize derived slug: keep only safe characters (OWASP A03)
|
|
93
|
+
const featureSlug = featureName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/(?:^-+|-+$)/g, '').slice(0, MAX_SLUG_LENGTH); // NOSONAR S5852 - non-overlapping anchors, no backtracking
|
|
94
|
+
const slugValidation = validateFeatureSlug(featureSlug);
|
|
95
|
+
if (!slugValidation.valid) {
|
|
96
|
+
return { error: `Cannot generate valid slug from feature name: ${slugValidation.error}` };
|
|
97
|
+
}
|
|
98
|
+
const dateSlug = new Date().toISOString().slice(0, 10);
|
|
99
|
+
description += `\n\nDesign: docs/work/${dateSlug}-${featureSlug}/plan.md`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { description };
|
|
103
|
+
}
|
|
104
|
+
|
|
61
105
|
/**
|
|
62
106
|
* Read research document from file
|
|
63
107
|
*
|
|
@@ -235,17 +279,11 @@ function createBeadsIssue(featureName, researchPath, scope) {
|
|
|
235
279
|
}
|
|
236
280
|
|
|
237
281
|
try {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// Sanitize derived slug: keep only safe characters (OWASP A03)
|
|
242
|
-
const featureSlug = featureName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/(?:^-+|-+$)/g, '').slice(0, MAX_SLUG_LENGTH); // NOSONAR S5852 - non-overlapping anchors, no backtracking
|
|
243
|
-
const slugValidation = validateFeatureSlug(featureSlug);
|
|
244
|
-
if (!slugValidation.valid) {
|
|
245
|
-
return { success: false, error: `Cannot generate valid slug from feature name: ${slugValidation.error}` };
|
|
246
|
-
}
|
|
247
|
-
description += `\n\nDesign: docs/plans/${featureSlug}-design.md`;
|
|
282
|
+
const built = buildFeatureIssueDescription(featureName, researchPath, scope);
|
|
283
|
+
if (built.error) {
|
|
284
|
+
return { success: false, error: built.error };
|
|
248
285
|
}
|
|
286
|
+
const description = built.description;
|
|
249
287
|
|
|
250
288
|
// Execute bd create command using execFileSync for safety (OWASP A03)
|
|
251
289
|
const result = execFileSync( // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
@@ -294,6 +332,320 @@ function createBeadsIssue(featureName, researchPath, scope) {
|
|
|
294
332
|
}
|
|
295
333
|
}
|
|
296
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Create an issue via the Forge Kernel backend (bd-free).
|
|
337
|
+
*
|
|
338
|
+
* Mirrors `forge issue create` on the kernel: routes through runIssueOperation with
|
|
339
|
+
* the kernel broker instead of shelling out to `bd create`. Used by `forge plan` when
|
|
340
|
+
* the resolved issue backend is the kernel (the default), so planning needs no Beads
|
|
341
|
+
* install. The beads path (createBeadsIssue) is preserved for backend=beads.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} featureName
|
|
344
|
+
* @param {string} researchPath
|
|
345
|
+
* @param {'tactical'|'strategic'} scope
|
|
346
|
+
* @param {object} [options]
|
|
347
|
+
* @param {string} [options.projectRoot] - Repo root for the kernel store (defaults to cwd)
|
|
348
|
+
* @param {object} [options.kernelBroker] - Pre-built kernel broker (optional)
|
|
349
|
+
* @param {Function} [options.runIssueOperation] - Injectable runner (tests)
|
|
350
|
+
* @returns {Promise<{success: boolean, issueId?: string, description?: string, error?: string}>}
|
|
351
|
+
*/
|
|
352
|
+
const PLAN_ISSUE_UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Parse plan's own args: extract a `--issue <id>` / `--issue=<id>` selector and
|
|
356
|
+
* return the remaining positional feature name. The global flag parser only
|
|
357
|
+
* recognizes an allowlist, so — like `stage`/`worktree` — plan extracts its own.
|
|
358
|
+
*
|
|
359
|
+
* @param {string[]} args
|
|
360
|
+
* @returns {{ featureName: string|undefined, issueId: string|null }}
|
|
361
|
+
* @private
|
|
362
|
+
*/
|
|
363
|
+
function parsePlanArgs(args = []) {
|
|
364
|
+
let issueId = null;
|
|
365
|
+
let issueFlagSeen = false;
|
|
366
|
+
const positional = [];
|
|
367
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
368
|
+
const arg = args[i];
|
|
369
|
+
if (arg === '--issue') {
|
|
370
|
+
issueFlagSeen = true;
|
|
371
|
+
const next = i + 1 < args.length ? args[i + 1] : null;
|
|
372
|
+
// A following flag (or nothing) is NOT a value — treat as a missing value
|
|
373
|
+
// (F5) so we never silently fall back to CREATE a duplicate issue.
|
|
374
|
+
if (typeof next === 'string' && !next.startsWith('-')) {
|
|
375
|
+
issueId = next;
|
|
376
|
+
i += 1;
|
|
377
|
+
}
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (typeof arg === 'string' && arg.startsWith('--issue=')) {
|
|
381
|
+
issueFlagSeen = true;
|
|
382
|
+
issueId = arg.slice('--issue='.length) || null;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
positional.push(arg);
|
|
386
|
+
}
|
|
387
|
+
return { featureName: positional[0], issueId, issueFlagError: issueFlagSeen && !issueId };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Link an EXISTING issue instead of creating a new one (B4). Verifies the issue
|
|
392
|
+
* exists via `issue show` so `plan --issue <id>` links the claim-first flow's
|
|
393
|
+
* issue rather than forking a duplicate.
|
|
394
|
+
*
|
|
395
|
+
* @param {string} issueId
|
|
396
|
+
* @param {object} [options]
|
|
397
|
+
* @returns {Promise<{success: boolean, issueId?: string, error?: string}>}
|
|
398
|
+
* @private
|
|
399
|
+
*/
|
|
400
|
+
async function linkExistingIssue(issueId, options = {}) {
|
|
401
|
+
if (!issueId || typeof issueId !== 'string') {
|
|
402
|
+
return { success: false, error: 'An issue id is required to link an existing issue' };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const run = options.runIssueOperation || runIssueOperation;
|
|
406
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
407
|
+
const issueBackend = options.issueBackend || 'kernel';
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const result = await run(
|
|
411
|
+
'show',
|
|
412
|
+
[issueId],
|
|
413
|
+
projectRoot,
|
|
414
|
+
{
|
|
415
|
+
issueBackend,
|
|
416
|
+
useKernelBroker: issueBackend !== 'beads',
|
|
417
|
+
kernelBroker: options.kernelBroker,
|
|
418
|
+
},
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
if (!result || result.ok === false || result.success === false) {
|
|
422
|
+
const message = result?.error || 'issue not found';
|
|
423
|
+
return { success: false, error: `Issue ${issueId} not found: ${message}` };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const resolvedId = result?.data?.id ?? result?.issueId ?? result?.id ?? issueId;
|
|
427
|
+
return { success: true, issueId: resolvedId };
|
|
428
|
+
} catch (error) {
|
|
429
|
+
return { success: false, error: `Failed to look up issue ${issueId}: ${error.message}` };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Read the current git branch (empty string when detached / not a repo).
|
|
435
|
+
* @private
|
|
436
|
+
*/
|
|
437
|
+
function getCurrentBranch() {
|
|
438
|
+
try {
|
|
439
|
+
return execFileSync('git', ['branch', '--show-current'], { ...getExecOptions(), stdio: 'pipe' }).trim(); // NOSONAR S4036 - hardcoded CLI command, developer tool context
|
|
440
|
+
} catch {
|
|
441
|
+
return '';
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function isDefaultBranch(name) {
|
|
446
|
+
return name === '' || name === 'main' || name === 'master';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* When linking an existing issue, reuse the current feature branch instead of
|
|
451
|
+
* forking a second branch (B4). Only creates a fresh `feat/<slug>` branch when
|
|
452
|
+
* still sitting on a default branch.
|
|
453
|
+
*
|
|
454
|
+
* @param {string} featureSlug
|
|
455
|
+
* @returns {{success: boolean, branchName?: string, reused?: boolean, error?: string}}
|
|
456
|
+
* @private
|
|
457
|
+
*/
|
|
458
|
+
function reuseOrCreateFeatureBranch(featureSlug) {
|
|
459
|
+
const current = getCurrentBranch();
|
|
460
|
+
if (!isDefaultBranch(current)) {
|
|
461
|
+
return { success: true, branchName: current, reused: true };
|
|
462
|
+
}
|
|
463
|
+
return createFeatureBranch(featureSlug);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Read `git rev-parse --git-common-dir` (null on failure). @private */
|
|
467
|
+
function getGitCommonDir(cwd) {
|
|
468
|
+
try {
|
|
469
|
+
return execFileSync('git', ['rev-parse', '--git-common-dir'], { ...getExecOptions(), cwd, stdio: 'pipe' }).trim(); // NOSONAR S4036 - hardcoded CLI command, developer tool context
|
|
470
|
+
} catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** An already-resolved kernel driver (tests / commandOpts), or null. @private */
|
|
476
|
+
function injectedPlanDriver(options = {}) {
|
|
477
|
+
return options.kernelDriver || options.driver || null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Run `fn(driver)` with a kernel driver: an injected one (tests / commandOpts)
|
|
482
|
+
* when present, otherwise a throwaway migrated driver built from the project
|
|
483
|
+
* root and CLOSED afterwards, so it never leaves an open DB handle (which would
|
|
484
|
+
* lock the dir on Windows). `plan` is NOT an issue command, so
|
|
485
|
+
* `resolveCommandOpts` gives it no driver — building here is what lets the
|
|
486
|
+
* branch->issue registry be consulted in the real CLI (both the F4c conflict
|
|
487
|
+
* read and the F1 linkage write). `fn(null)` when the kernel is unavailable.
|
|
488
|
+
* @private
|
|
489
|
+
*/
|
|
490
|
+
async function withPlanDriver(options, fn) {
|
|
491
|
+
const injected = injectedPlanDriver(options);
|
|
492
|
+
if (injected) {
|
|
493
|
+
return fn(injected);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
let driver;
|
|
497
|
+
try {
|
|
498
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
499
|
+
const deps = await buildMigratedKernelIssueDeps({ projectRoot: options.projectRoot || process.cwd() });
|
|
500
|
+
driver = deps.kernelDriver || null;
|
|
501
|
+
} catch {
|
|
502
|
+
return fn(null);
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
return fn(driver);
|
|
506
|
+
} finally {
|
|
507
|
+
if (driver && typeof driver.close === 'function') driver.close();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Which issue the CURRENT branch is already bound to: the kernel worktree
|
|
513
|
+
* registry (authoritative) first, then a UUID encoded in the branch name.
|
|
514
|
+
* @private
|
|
515
|
+
*/
|
|
516
|
+
function currentBranchIssueFromDriver(driver, currentBranch) {
|
|
517
|
+
if (driver && typeof driver.listWorktrees === 'function') {
|
|
518
|
+
try {
|
|
519
|
+
const rows = driver.listWorktrees() || [];
|
|
520
|
+
const match = rows.find(row => row && row.branch === currentBranch && row.issue_id);
|
|
521
|
+
if (match) return match.issue_id;
|
|
522
|
+
} catch {
|
|
523
|
+
// fall through to branch-name parsing
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const encoded = PLAN_ISSUE_UUID_RE.exec(currentBranch || '');
|
|
527
|
+
return encoded ? encoded[0] : null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Refuse to link issue B onto a branch already bound to issue A (F4c): otherwise
|
|
532
|
+
* plan would link B while stage read/writes flow to A (split state). Consults the
|
|
533
|
+
* kernel worktree registry (via a built driver when the CLI supplies none) so a
|
|
534
|
+
* plan-created slug branch — whose name lacks a UUID — is still caught.
|
|
535
|
+
* @returns {Promise<string|null>} an error message when there is a conflict.
|
|
536
|
+
* @private
|
|
537
|
+
*/
|
|
538
|
+
async function detectBranchIssueConflict(options, explicitIssueId) {
|
|
539
|
+
const current = getCurrentBranch();
|
|
540
|
+
if (isDefaultBranch(current)) return null;
|
|
541
|
+
const boundIssue = await withPlanDriver(options, driver => currentBranchIssueFromDriver(driver, current));
|
|
542
|
+
if (boundIssue && boundIssue !== explicitIssueId) {
|
|
543
|
+
return `Current branch ${current} is already bound to issue ${boundIssue}; refusing to link ${explicitIssueId} (split state). Switch branches or link the matching issue.`;
|
|
544
|
+
}
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Persist the branch->issue linkage into the kernel worktree registry (F1) so a
|
|
550
|
+
* plan-created branch resolves to its issue for kernel-authoritative stage
|
|
551
|
+
* state. Best-effort, kernel-only. @private
|
|
552
|
+
*/
|
|
553
|
+
async function registerBranchIssueLinkage(options, branch, issueId) {
|
|
554
|
+
if (!branch || !issueId) return;
|
|
555
|
+
const cwd = options.projectRoot || process.cwd();
|
|
556
|
+
await withPlanDriver(options, driver => {
|
|
557
|
+
if (!driver || typeof driver.registerWorktree !== 'function') return;
|
|
558
|
+
try {
|
|
559
|
+
driver.registerWorktree({
|
|
560
|
+
git_common_dir: getGitCommonDir(cwd) || cwd,
|
|
561
|
+
path: cwd,
|
|
562
|
+
branch,
|
|
563
|
+
actor: null,
|
|
564
|
+
issue_id: issueId,
|
|
565
|
+
work_folder: null,
|
|
566
|
+
registered_at: new Date().toISOString(),
|
|
567
|
+
state: 'active',
|
|
568
|
+
});
|
|
569
|
+
} catch {
|
|
570
|
+
// best-effort: linkage failure must not fail plan
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Resolve the tracking issue for a plan: LINK an explicit issue, else CREATE via
|
|
577
|
+
* the active backend (the kernel default needs no Beads). Extracted to avoid a
|
|
578
|
+
* nested ternary in executePlan.
|
|
579
|
+
*
|
|
580
|
+
* @returns {Promise<{success: boolean, issueId?: string, error?: string}>}
|
|
581
|
+
* @private
|
|
582
|
+
*/
|
|
583
|
+
async function resolveTrackingIssue({ explicitIssueId, issueBackend, featureName, researchPath, scope, options }) {
|
|
584
|
+
if (explicitIssueId) {
|
|
585
|
+
return linkExistingIssue(explicitIssueId, { ...options, issueBackend });
|
|
586
|
+
}
|
|
587
|
+
if (issueBackend === 'kernel') {
|
|
588
|
+
return createKernelIssue(featureName, researchPath, scope, options);
|
|
589
|
+
}
|
|
590
|
+
return createBeadsIssue(featureName, researchPath, scope);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function createKernelIssue(featureName, researchPath, scope, options = {}) {
|
|
594
|
+
if (!featureName || !researchPath) {
|
|
595
|
+
return {
|
|
596
|
+
success: false,
|
|
597
|
+
error: 'Feature name and research path are required',
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (scope !== 'tactical' && scope !== 'strategic') {
|
|
602
|
+
return {
|
|
603
|
+
success: false,
|
|
604
|
+
error: `Invalid scope '${scope}'. Must be 'tactical' or 'strategic'`,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const built = buildFeatureIssueDescription(featureName, researchPath, scope);
|
|
609
|
+
if (built.error) {
|
|
610
|
+
return { success: false, error: built.error };
|
|
611
|
+
}
|
|
612
|
+
const description = built.description;
|
|
613
|
+
|
|
614
|
+
const run = options.runIssueOperation || runIssueOperation;
|
|
615
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
const result = await run(
|
|
619
|
+
'create',
|
|
620
|
+
[`--title=${featureName}`, `--description=${description}`, '--type=feature', '--priority=2'],
|
|
621
|
+
projectRoot,
|
|
622
|
+
{ issueBackend: 'kernel', useKernelBroker: true, kernelBroker: options.kernelBroker },
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
// The kernel returns the issue-command contract ({ ok, data: { id } }); a failure
|
|
626
|
+
// is { ok:false, error }. Guard both the contract shape and any { success:false }.
|
|
627
|
+
if (!result || result.ok === false || result.success === false) {
|
|
628
|
+
const message = result?.error || 'Kernel issue creation failed';
|
|
629
|
+
return { success: false, error: `Failed to create kernel issue: ${message}` };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const issueId = result?.data?.id ?? result?.issueId ?? result?.id;
|
|
633
|
+
if (!issueId) {
|
|
634
|
+
return {
|
|
635
|
+
success: false,
|
|
636
|
+
error: 'Failed to extract issue ID from kernel create result',
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return { success: true, issueId, description };
|
|
641
|
+
} catch (error) {
|
|
642
|
+
return {
|
|
643
|
+
success: false,
|
|
644
|
+
error: `Failed to create kernel issue: ${error.message}`,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
297
649
|
/**
|
|
298
650
|
* Create feature branch
|
|
299
651
|
* Creates and checks out a new git branch following feat/<slug> convention
|
|
@@ -596,13 +948,18 @@ function applyYAGNIFilter({ task, tasks, designDoc } = {}) {
|
|
|
596
948
|
* @returns {Promise<{
|
|
597
949
|
* success: boolean,
|
|
598
950
|
* scope?: 'tactical'|'strategic',
|
|
951
|
+
* issueBackend?: 'kernel'|'beads',
|
|
952
|
+
* issueId?: string,
|
|
599
953
|
* beadsIssueId?: string,
|
|
600
954
|
* branchName?: string,
|
|
601
955
|
*
|
|
602
956
|
* summary?: string,
|
|
603
957
|
* nextCommand?: string,
|
|
604
958
|
* error?: string
|
|
605
|
-
* }>} Execution result
|
|
959
|
+
* }>} Execution result. `issueId` is the created issue id under the active
|
|
960
|
+
* backend; `issueBackend` names that backend. `beadsIssueId` is a deprecated
|
|
961
|
+
* alias of `issueId` kept for backward compatibility (it does NOT imply the
|
|
962
|
+
* issue came from Beads — on the kernel path it holds the kernel issue id).
|
|
606
963
|
* @example
|
|
607
964
|
* const result = await executePlan('Payment Integration');
|
|
608
965
|
* if (result.success) {
|
|
@@ -610,7 +967,7 @@ function applyYAGNIFilter({ task, tasks, designDoc } = {}) {
|
|
|
610
967
|
* console.log('Next:', result.nextCommand);
|
|
611
968
|
* }
|
|
612
969
|
*/
|
|
613
|
-
async function executePlan(featureName) { // NOSONAR S3776
|
|
970
|
+
async function executePlan(featureName, options = {}) { // NOSONAR S3776
|
|
614
971
|
if (!featureName || typeof featureName !== 'string') {
|
|
615
972
|
return {
|
|
616
973
|
success: false,
|
|
@@ -618,6 +975,15 @@ async function executePlan(featureName) { // NOSONAR S3776
|
|
|
618
975
|
};
|
|
619
976
|
}
|
|
620
977
|
|
|
978
|
+
// Resolve the active issue backend (explicit opts > env > .forge/config.yaml >
|
|
979
|
+
// default 'kernel'). Kernel is bd-free, so planning works with no Beads install.
|
|
980
|
+
const issueBackend = resolveIssueBackend({
|
|
981
|
+
deps: options,
|
|
982
|
+
env: options.env || process.env,
|
|
983
|
+
projectRoot: options.projectRoot,
|
|
984
|
+
warn: () => {},
|
|
985
|
+
});
|
|
986
|
+
|
|
621
987
|
const featureSlug = featureName.toLowerCase()
|
|
622
988
|
.replaceAll(/[^a-z0-9-]/g, '-')
|
|
623
989
|
.split('-').filter(Boolean).join('-');
|
|
@@ -644,17 +1010,37 @@ async function executePlan(featureName) { // NOSONAR S3776
|
|
|
644
1010
|
// Step 2: Detect scope (tactical vs strategic)
|
|
645
1011
|
const scope = detectScope(research.content);
|
|
646
1012
|
|
|
647
|
-
// Step 3:
|
|
648
|
-
|
|
649
|
-
|
|
1013
|
+
// Step 3: Resolve the tracking issue. `--issue <id>` LINKS an existing issue
|
|
1014
|
+
// (claim-first flow) instead of creating a duplicate (B4). Otherwise create
|
|
1015
|
+
// via the active backend — kernel (default) routes through the kernel broker
|
|
1016
|
+
// (no bd); beads preserves the bd path.
|
|
1017
|
+
const explicitIssueId = options.issue || options.issueId || null;
|
|
1018
|
+
|
|
1019
|
+
// F4c: never link issue B onto a branch already bound to issue A.
|
|
1020
|
+
if (explicitIssueId) {
|
|
1021
|
+
const conflict = await detectBranchIssueConflict({ ...options, issueBackend }, explicitIssueId);
|
|
1022
|
+
if (conflict) {
|
|
1023
|
+
return { success: false, error: conflict };
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const issue = await resolveTrackingIssue({
|
|
1028
|
+
explicitIssueId, issueBackend, featureName, researchPath: research.path, scope: scope.type, options,
|
|
1029
|
+
});
|
|
1030
|
+
if (!issue.success) {
|
|
650
1031
|
return {
|
|
651
1032
|
success: false,
|
|
652
|
-
error:
|
|
1033
|
+
error: explicitIssueId
|
|
1034
|
+
? `Failed to link issue: ${issue.error}`
|
|
1035
|
+
: `Failed to create issue: ${issue.error}`,
|
|
653
1036
|
};
|
|
654
1037
|
}
|
|
655
1038
|
|
|
656
|
-
// Step 4:
|
|
657
|
-
|
|
1039
|
+
// Step 4: Resolve the feature branch. When linking, reuse the current
|
|
1040
|
+
// feature branch instead of forking a second branch (B4).
|
|
1041
|
+
const branch = explicitIssueId
|
|
1042
|
+
? reuseOrCreateFeatureBranch(featureSlug)
|
|
1043
|
+
: createFeatureBranch(featureSlug);
|
|
658
1044
|
if (!branch.success) {
|
|
659
1045
|
return {
|
|
660
1046
|
success: false,
|
|
@@ -662,13 +1048,27 @@ async function executePlan(featureName) { // NOSONAR S3776
|
|
|
662
1048
|
};
|
|
663
1049
|
}
|
|
664
1050
|
|
|
1051
|
+
// F1: persist the branch->issue linkage so a plan-created branch resolves
|
|
1052
|
+
// to its issue for kernel-authoritative stage state (dev/validate/ship).
|
|
1053
|
+
// Kernel-only, best-effort.
|
|
1054
|
+
if (issueBackend === 'kernel') {
|
|
1055
|
+
await registerBranchIssueLinkage({ ...options, issueBackend }, branch.branchName, issue.issueId);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
665
1058
|
// Build result summary
|
|
666
1059
|
const result = {
|
|
667
1060
|
success: true,
|
|
668
1061
|
scope: scope.type,
|
|
669
|
-
|
|
1062
|
+
issueBackend,
|
|
1063
|
+
issueId: issue.issueId,
|
|
1064
|
+
// Deprecated alias of issueId, retained for backward compatibility. It does
|
|
1065
|
+
// NOT imply the Beads backend — on the kernel path it holds the kernel id.
|
|
1066
|
+
beadsIssueId: issue.issueId,
|
|
670
1067
|
branchName: branch.branchName,
|
|
671
|
-
|
|
1068
|
+
linked: Boolean(explicitIssueId),
|
|
1069
|
+
summary: explicitIssueId
|
|
1070
|
+
? `Plan linked to existing issue ${issue.issueId} (${scope.type} scope)`
|
|
1071
|
+
: `Plan created for ${featureName} (${scope.type} scope)`,
|
|
672
1072
|
// Strategic path: /propose not yet implemented — falls through to /dev until it is
|
|
673
1073
|
nextCommand: '/dev',
|
|
674
1074
|
};
|
|
@@ -685,14 +1085,24 @@ async function executePlan(featureName) { // NOSONAR S3776
|
|
|
685
1085
|
module.exports = {
|
|
686
1086
|
name: 'plan',
|
|
687
1087
|
description: 'Create implementation plan from researched feature context',
|
|
688
|
-
handler: async (args) => {
|
|
689
|
-
const
|
|
1088
|
+
handler: async (args, _flags, projectRoot, opts = {}) => {
|
|
1089
|
+
const { featureName, issueId, issueFlagError } = parsePlanArgs(args);
|
|
1090
|
+
// F5: `--issue` with no value must ERROR, never silently create a duplicate.
|
|
1091
|
+
if (issueFlagError) {
|
|
1092
|
+
return { success: false, error: '--issue requires a value (an existing issue id to link).' };
|
|
1093
|
+
}
|
|
1094
|
+
const result = await executePlan(featureName, {
|
|
1095
|
+
...opts,
|
|
1096
|
+
projectRoot,
|
|
1097
|
+
issue: issueId ?? opts.issue ?? opts.issueId,
|
|
1098
|
+
});
|
|
690
1099
|
if (!result.success) {
|
|
691
1100
|
return result;
|
|
692
1101
|
}
|
|
693
1102
|
|
|
694
|
-
const
|
|
695
|
-
|
|
1103
|
+
const header = result.linked ? 'Plan linked' : 'Plan created';
|
|
1104
|
+
const lines = [`${header}: ${result.summary || result.branchName || featureName}`];
|
|
1105
|
+
if (result.issueId) lines.push(`${issueBackendLabel(result.issueBackend)}: ${result.issueId}`);
|
|
696
1106
|
if (result.branchName) lines.push(`Branch: ${result.branchName}`);
|
|
697
1107
|
if (result.nextCommand) lines.push(`Next: ${result.nextCommand}`);
|
|
698
1108
|
|
|
@@ -704,10 +1114,12 @@ module.exports = {
|
|
|
704
1114
|
readResearchDoc,
|
|
705
1115
|
detectScope,
|
|
706
1116
|
createBeadsIssue,
|
|
1117
|
+
createKernelIssue,
|
|
707
1118
|
createFeatureBranch,
|
|
708
1119
|
extractDesignDecisions,
|
|
709
1120
|
extractTasksFromResearch,
|
|
710
1121
|
detectDRYViolation,
|
|
711
1122
|
applyYAGNIFilter,
|
|
712
1123
|
executePlan,
|
|
1124
|
+
issueBackendLabel,
|
|
713
1125
|
};
|