forge-workflow 0.0.10 → 0.1.0-beta.3
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 +82 -5
- package/.forge/hooks/forge-native-hook.js +431 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +709 -0
- package/CLAUDE.md +9 -118
- package/QUICKSTART.md +175 -0
- package/README.md +275 -365
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +532 -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 +121 -0
- package/docs/guides/SUPPORT.md +190 -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 +214 -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 +155 -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/activation/ensure-forge-home.js +135 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1109 -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 +139 -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-detect.js +60 -0
- package/lib/beads-nudge.js +91 -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/_aliases.js +248 -0
- package/lib/commands/_issue.js +780 -77
- package/lib/commands/_manifest.js +93 -0
- package/lib/commands/_registry.js +99 -34
- package/lib/commands/_resolve-command-opts.js +230 -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 +209 -0
- package/lib/commands/hooks.js +377 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +604 -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/memory.js +81 -0
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +362 -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 +481 -29
- package/lib/commands/pr.js +88 -0
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +135 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +171 -0
- package/lib/commands/recap.js +75 -0
- package/lib/commands/recommend.js +0 -1
- package/lib/commands/release.js +104 -0
- package/lib/commands/remember.js +140 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +900 -971
- package/lib/commands/shepherd.js +501 -0
- package/lib/commands/ship.js +59 -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 +362 -99
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +977 -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 +192 -0
- package/lib/global-flags.js +104 -0
- package/lib/greptile-match.js +7 -63
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +541 -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 +311 -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/owned-kernel.js +43 -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 +2540 -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/auto-actions.js +175 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/digest.js +206 -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 +192 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1314 -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 +2105 -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 +251 -0
- package/lib/workflow/enforce-stage.js +351 -45
- 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-auto-actions.js +93 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/pr-verdict-label.js +50 -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,9 +332,337 @@ 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
|
+
// Only an ACTIVE (live) linkage row binds the branch: a superseded/stale
|
|
521
|
+
// registration for a reused branch name must not trigger a false split-state
|
|
522
|
+
// conflict against the OLD issue (R4/be18881c). Tolerate a null state for
|
|
523
|
+
// rows written before the state column was populated.
|
|
524
|
+
const match = rows.find(row => row && row.branch === currentBranch && row.issue_id
|
|
525
|
+
&& (row.state === 'active' || row.state == null));
|
|
526
|
+
if (match) return match.issue_id;
|
|
527
|
+
} catch {
|
|
528
|
+
// fall through to branch-name parsing
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const encoded = PLAN_ISSUE_UUID_RE.exec(currentBranch || '');
|
|
532
|
+
return encoded ? encoded[0] : null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Refuse to link issue B onto a branch already bound to issue A (F4c): otherwise
|
|
537
|
+
* plan would link B while stage read/writes flow to A (split state). Consults the
|
|
538
|
+
* kernel worktree registry (via a built driver when the CLI supplies none) so a
|
|
539
|
+
* plan-created slug branch — whose name lacks a UUID — is still caught.
|
|
540
|
+
* @returns {Promise<string|null>} an error message when there is a conflict.
|
|
541
|
+
* @private
|
|
542
|
+
*/
|
|
543
|
+
async function detectBranchIssueConflict(options, explicitIssueId) {
|
|
544
|
+
const current = getCurrentBranch();
|
|
545
|
+
if (isDefaultBranch(current)) return null;
|
|
546
|
+
const boundIssue = await withPlanDriver(options, driver => currentBranchIssueFromDriver(driver, current));
|
|
547
|
+
if (boundIssue && boundIssue !== explicitIssueId) {
|
|
548
|
+
return `Current branch ${current} is already bound to issue ${boundIssue}; refusing to link ${explicitIssueId} (split state). Switch branches or link the matching issue.`;
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Persist the branch->issue linkage into the kernel worktree registry (F1) so a
|
|
555
|
+
* plan-created branch resolves to its issue for kernel-authoritative stage
|
|
556
|
+
* state. Best-effort, kernel-only. @private
|
|
557
|
+
*/
|
|
558
|
+
async function registerBranchIssueLinkage(options, branch, issueId) {
|
|
559
|
+
if (!branch || !issueId) return;
|
|
560
|
+
const cwd = options.projectRoot || process.cwd();
|
|
561
|
+
// F6 defaultStageWarn pattern: write to stderr so a dropped linkage never
|
|
562
|
+
// pollutes machine-readable stdout, yet leaves a trace even under FORGE_JSON=1.
|
|
563
|
+
const warn = options.warn || (message => process.stderr.write(`${message}\n`));
|
|
564
|
+
await withPlanDriver(options, driver => {
|
|
565
|
+
if (!driver || typeof driver.registerWorktree !== 'function') return;
|
|
566
|
+
try {
|
|
567
|
+
driver.registerWorktree({
|
|
568
|
+
git_common_dir: getGitCommonDir(cwd) || cwd,
|
|
569
|
+
path: cwd,
|
|
570
|
+
branch,
|
|
571
|
+
actor: null,
|
|
572
|
+
issue_id: issueId,
|
|
573
|
+
work_folder: null,
|
|
574
|
+
registered_at: new Date().toISOString(),
|
|
575
|
+
state: 'active',
|
|
576
|
+
});
|
|
577
|
+
} catch (error) {
|
|
578
|
+
// Best-effort: linkage failure must not fail plan, but it must NOT be
|
|
579
|
+
// silent (R3) — otherwise ship later fail-closes with no signal at plan
|
|
580
|
+
// time about the dropped branch->issue linkage.
|
|
581
|
+
warn(`[forge] could not register branch->issue linkage for ${branch} -> ${issueId}: ${error.message}`);
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Resolve the tracking issue for a plan: LINK an explicit issue, else CREATE via
|
|
588
|
+
* the active backend (the kernel default needs no Beads). Extracted to avoid a
|
|
589
|
+
* nested ternary in executePlan.
|
|
590
|
+
*
|
|
591
|
+
* @returns {Promise<{success: boolean, issueId?: string, error?: string}>}
|
|
592
|
+
* @private
|
|
593
|
+
*/
|
|
594
|
+
async function resolveTrackingIssue({ explicitIssueId, issueBackend, featureName, researchPath, scope, options }) {
|
|
595
|
+
if (explicitIssueId) {
|
|
596
|
+
return linkExistingIssue(explicitIssueId, { ...options, issueBackend });
|
|
597
|
+
}
|
|
598
|
+
if (issueBackend === 'kernel') {
|
|
599
|
+
return createKernelIssue(featureName, researchPath, scope, options);
|
|
600
|
+
}
|
|
601
|
+
return createBeadsIssue(featureName, researchPath, scope);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function createKernelIssue(featureName, researchPath, scope, options = {}) {
|
|
605
|
+
if (!featureName || !researchPath) {
|
|
606
|
+
return {
|
|
607
|
+
success: false,
|
|
608
|
+
error: 'Feature name and research path are required',
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (scope !== 'tactical' && scope !== 'strategic') {
|
|
613
|
+
return {
|
|
614
|
+
success: false,
|
|
615
|
+
error: `Invalid scope '${scope}'. Must be 'tactical' or 'strategic'`,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const built = buildFeatureIssueDescription(featureName, researchPath, scope);
|
|
620
|
+
if (built.error) {
|
|
621
|
+
return { success: false, error: built.error };
|
|
622
|
+
}
|
|
623
|
+
const description = built.description;
|
|
624
|
+
|
|
625
|
+
const run = options.runIssueOperation || runIssueOperation;
|
|
626
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
const result = await run(
|
|
630
|
+
'create',
|
|
631
|
+
[`--title=${featureName}`, `--description=${description}`, '--type=feature', '--priority=2'],
|
|
632
|
+
projectRoot,
|
|
633
|
+
{ issueBackend: 'kernel', useKernelBroker: true, kernelBroker: options.kernelBroker },
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
// The kernel returns the issue-command contract ({ ok, data: { id } }); a failure
|
|
637
|
+
// is { ok:false, error }. Guard both the contract shape and any { success:false }.
|
|
638
|
+
if (!result || result.ok === false || result.success === false) {
|
|
639
|
+
const message = result?.error || 'Kernel issue creation failed';
|
|
640
|
+
return { success: false, error: `Failed to create kernel issue: ${message}` };
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const issueId = result?.data?.id ?? result?.issueId ?? result?.id;
|
|
644
|
+
if (!issueId) {
|
|
645
|
+
return {
|
|
646
|
+
success: false,
|
|
647
|
+
error: 'Failed to extract issue ID from kernel create result',
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return { success: true, issueId, description };
|
|
652
|
+
} catch (error) {
|
|
653
|
+
return {
|
|
654
|
+
success: false,
|
|
655
|
+
error: `Failed to create kernel issue: ${error.message}`,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
297
660
|
/**
|
|
298
661
|
* Create feature branch
|
|
299
|
-
* Creates
|
|
662
|
+
* Creates a new git branch following feat/<slug> convention WITHOUT switching
|
|
663
|
+
* the shared checkout's HEAD (uses `git branch`, not `git checkout -b`).
|
|
664
|
+
* Switching HEAD in the shared working tree corrupts concurrent agents
|
|
665
|
+
* (kernel issue aa14966c); isolated work happens in a dedicated worktree.
|
|
300
666
|
*
|
|
301
667
|
* Security: Uses execFileSync with array args to prevent command injection
|
|
302
668
|
*
|
|
@@ -326,14 +692,19 @@ function createFeatureBranch(featureSlug) {
|
|
|
326
692
|
execFileSync('git', ['rev-parse', '--verify', branchName], { ...getExecOptions(), stdio: 'pipe' }); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
327
693
|
return {
|
|
328
694
|
success: false,
|
|
329
|
-
error: `Branch ${branchName} already exists\n\
|
|
695
|
+
error: `Branch ${branchName} already exists\n\nWork on it in an isolated checkout: forge worktree create ${featureSlug}\n(or, working solo: git switch ${branchName})`,
|
|
330
696
|
};
|
|
331
697
|
} catch {
|
|
332
698
|
// Branch doesn't exist, continue (expected case)
|
|
333
699
|
}
|
|
334
700
|
|
|
335
|
-
// Create
|
|
336
|
-
|
|
701
|
+
// Create the branch WITHOUT switching the shared checkout's HEAD.
|
|
702
|
+
// Historically this used `git checkout -b`, which flipped the shared
|
|
703
|
+
// working tree onto the new branch and corrupted concurrent agents
|
|
704
|
+
// (kernel issue aa14966c). `git branch` creates the ref at the current
|
|
705
|
+
// HEAD without touching the working tree; isolated work happens in a
|
|
706
|
+
// dedicated worktree (`forge worktree create`), never the shared tree.
|
|
707
|
+
execFileSync('git', ['branch', branchName], getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
337
708
|
|
|
338
709
|
return {
|
|
339
710
|
success: true,
|
|
@@ -596,13 +967,18 @@ function applyYAGNIFilter({ task, tasks, designDoc } = {}) {
|
|
|
596
967
|
* @returns {Promise<{
|
|
597
968
|
* success: boolean,
|
|
598
969
|
* scope?: 'tactical'|'strategic',
|
|
970
|
+
* issueBackend?: 'kernel'|'beads',
|
|
971
|
+
* issueId?: string,
|
|
599
972
|
* beadsIssueId?: string,
|
|
600
973
|
* branchName?: string,
|
|
601
974
|
*
|
|
602
975
|
* summary?: string,
|
|
603
976
|
* nextCommand?: string,
|
|
604
977
|
* error?: string
|
|
605
|
-
* }>} Execution result
|
|
978
|
+
* }>} Execution result. `issueId` is the created issue id under the active
|
|
979
|
+
* backend; `issueBackend` names that backend. `beadsIssueId` is a deprecated
|
|
980
|
+
* alias of `issueId` kept for backward compatibility (it does NOT imply the
|
|
981
|
+
* issue came from Beads — on the kernel path it holds the kernel issue id).
|
|
606
982
|
* @example
|
|
607
983
|
* const result = await executePlan('Payment Integration');
|
|
608
984
|
* if (result.success) {
|
|
@@ -610,7 +986,7 @@ function applyYAGNIFilter({ task, tasks, designDoc } = {}) {
|
|
|
610
986
|
* console.log('Next:', result.nextCommand);
|
|
611
987
|
* }
|
|
612
988
|
*/
|
|
613
|
-
async function executePlan(featureName) { // NOSONAR S3776
|
|
989
|
+
async function executePlan(featureName, options = {}) { // NOSONAR S3776
|
|
614
990
|
if (!featureName || typeof featureName !== 'string') {
|
|
615
991
|
return {
|
|
616
992
|
success: false,
|
|
@@ -618,6 +994,15 @@ async function executePlan(featureName) { // NOSONAR S3776
|
|
|
618
994
|
};
|
|
619
995
|
}
|
|
620
996
|
|
|
997
|
+
// Resolve the active issue backend (explicit opts > env > .forge/config.yaml >
|
|
998
|
+
// default 'kernel'). Kernel is bd-free, so planning works with no Beads install.
|
|
999
|
+
const issueBackend = resolveIssueBackend({
|
|
1000
|
+
deps: options,
|
|
1001
|
+
env: options.env || process.env,
|
|
1002
|
+
projectRoot: options.projectRoot,
|
|
1003
|
+
warn: () => {},
|
|
1004
|
+
});
|
|
1005
|
+
|
|
621
1006
|
const featureSlug = featureName.toLowerCase()
|
|
622
1007
|
.replaceAll(/[^a-z0-9-]/g, '-')
|
|
623
1008
|
.split('-').filter(Boolean).join('-');
|
|
@@ -644,17 +1029,37 @@ async function executePlan(featureName) { // NOSONAR S3776
|
|
|
644
1029
|
// Step 2: Detect scope (tactical vs strategic)
|
|
645
1030
|
const scope = detectScope(research.content);
|
|
646
1031
|
|
|
647
|
-
// Step 3:
|
|
648
|
-
|
|
649
|
-
|
|
1032
|
+
// Step 3: Resolve the tracking issue. `--issue <id>` LINKS an existing issue
|
|
1033
|
+
// (claim-first flow) instead of creating a duplicate (B4). Otherwise create
|
|
1034
|
+
// via the active backend — kernel (default) routes through the kernel broker
|
|
1035
|
+
// (no bd); beads preserves the bd path.
|
|
1036
|
+
const explicitIssueId = options.issue || options.issueId || null;
|
|
1037
|
+
|
|
1038
|
+
// F4c: never link issue B onto a branch already bound to issue A.
|
|
1039
|
+
if (explicitIssueId) {
|
|
1040
|
+
const conflict = await detectBranchIssueConflict({ ...options, issueBackend }, explicitIssueId);
|
|
1041
|
+
if (conflict) {
|
|
1042
|
+
return { success: false, error: conflict };
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const issue = await resolveTrackingIssue({
|
|
1047
|
+
explicitIssueId, issueBackend, featureName, researchPath: research.path, scope: scope.type, options,
|
|
1048
|
+
});
|
|
1049
|
+
if (!issue.success) {
|
|
650
1050
|
return {
|
|
651
1051
|
success: false,
|
|
652
|
-
error:
|
|
1052
|
+
error: explicitIssueId
|
|
1053
|
+
? `Failed to link issue: ${issue.error}`
|
|
1054
|
+
: `Failed to create issue: ${issue.error}`,
|
|
653
1055
|
};
|
|
654
1056
|
}
|
|
655
1057
|
|
|
656
|
-
// Step 4:
|
|
657
|
-
|
|
1058
|
+
// Step 4: Resolve the feature branch. When linking, reuse the current
|
|
1059
|
+
// feature branch instead of forking a second branch (B4).
|
|
1060
|
+
const branch = explicitIssueId
|
|
1061
|
+
? reuseOrCreateFeatureBranch(featureSlug)
|
|
1062
|
+
: createFeatureBranch(featureSlug);
|
|
658
1063
|
if (!branch.success) {
|
|
659
1064
|
return {
|
|
660
1065
|
success: false,
|
|
@@ -662,13 +1067,33 @@ async function executePlan(featureName) { // NOSONAR S3776
|
|
|
662
1067
|
};
|
|
663
1068
|
}
|
|
664
1069
|
|
|
1070
|
+
// F1: persist the branch->issue linkage so a plan-created branch resolves
|
|
1071
|
+
// to its issue for kernel-authoritative stage state (dev/validate/ship).
|
|
1072
|
+
// Kernel-only, best-effort.
|
|
1073
|
+
if (issueBackend === 'kernel') {
|
|
1074
|
+
await registerBranchIssueLinkage({ ...options, issueBackend }, branch.branchName, issue.issueId);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
665
1077
|
// Build result summary
|
|
666
1078
|
const result = {
|
|
667
1079
|
success: true,
|
|
668
1080
|
scope: scope.type,
|
|
669
|
-
|
|
1081
|
+
issueBackend,
|
|
1082
|
+
issueId: issue.issueId,
|
|
1083
|
+
// Deprecated alias of issueId, retained for backward compatibility. It does
|
|
1084
|
+
// NOT imply the Beads backend — on the kernel path it holds the kernel id.
|
|
1085
|
+
beadsIssueId: issue.issueId,
|
|
670
1086
|
branchName: branch.branchName,
|
|
671
|
-
|
|
1087
|
+
linked: Boolean(explicitIssueId),
|
|
1088
|
+
// A FRESH branch was created (HEAD did NOT move — aa14966c). Stage
|
|
1089
|
+
// commands resolve the CHECKED-OUT branch, so the user must enter an
|
|
1090
|
+
// isolated checkout on this branch before /dev, or stage state resolves
|
|
1091
|
+
// against the default branch (no linkage → ship dead-ends). When the
|
|
1092
|
+
// branch was reused, HEAD is already on it and /dev works directly.
|
|
1093
|
+
branchCreated: !branch.reused,
|
|
1094
|
+
summary: explicitIssueId
|
|
1095
|
+
? `Plan linked to existing issue ${issue.issueId} (${scope.type} scope)`
|
|
1096
|
+
: `Plan created for ${featureName} (${scope.type} scope)`,
|
|
672
1097
|
// Strategic path: /propose not yet implemented — falls through to /dev until it is
|
|
673
1098
|
nextCommand: '/dev',
|
|
674
1099
|
};
|
|
@@ -685,16 +1110,39 @@ async function executePlan(featureName) { // NOSONAR S3776
|
|
|
685
1110
|
module.exports = {
|
|
686
1111
|
name: 'plan',
|
|
687
1112
|
description: 'Create implementation plan from researched feature context',
|
|
688
|
-
handler: async (args) => {
|
|
689
|
-
const
|
|
1113
|
+
handler: async (args, _flags, projectRoot, opts = {}) => {
|
|
1114
|
+
const { featureName, issueId, issueFlagError } = parsePlanArgs(args);
|
|
1115
|
+
// F5: `--issue` with no value must ERROR, never silently create a duplicate.
|
|
1116
|
+
if (issueFlagError) {
|
|
1117
|
+
return { success: false, error: '--issue requires a value (an existing issue id to link).' };
|
|
1118
|
+
}
|
|
1119
|
+
const result = await executePlan(featureName, {
|
|
1120
|
+
...opts,
|
|
1121
|
+
projectRoot,
|
|
1122
|
+
issue: issueId ?? opts.issue ?? opts.issueId,
|
|
1123
|
+
});
|
|
690
1124
|
if (!result.success) {
|
|
691
1125
|
return result;
|
|
692
1126
|
}
|
|
693
1127
|
|
|
694
|
-
const
|
|
695
|
-
|
|
1128
|
+
const header = result.linked ? 'Plan linked' : 'Plan created';
|
|
1129
|
+
const lines = [`${header}: ${result.summary || result.branchName || featureName}`];
|
|
1130
|
+
if (result.issueId) lines.push(`${issueBackendLabel(result.issueBackend)}: ${result.issueId}`);
|
|
696
1131
|
if (result.branchName) lines.push(`Branch: ${result.branchName}`);
|
|
697
|
-
if (result.
|
|
1132
|
+
if (result.branchCreated) {
|
|
1133
|
+
// A fresh branch was created but HEAD was NOT switched (aa14966c). Stage
|
|
1134
|
+
// commands (/dev, /validate, /ship) resolve the CHECKED-OUT branch — from
|
|
1135
|
+
// the shared tree that is still the default branch, which has no
|
|
1136
|
+
// branch->issue linkage, so ship would dead-end. Direct the user into an
|
|
1137
|
+
// isolated checkout on the new branch first.
|
|
1138
|
+
const slug = String(result.branchName).replace(/^feat\//, '');
|
|
1139
|
+
lines.push('Next: work on this branch in an isolated checkout (HEAD stays put in the shared tree):');
|
|
1140
|
+
lines.push(` forge worktree create ${slug} # concurrent-safe; checks out the existing ${result.branchName}`);
|
|
1141
|
+
lines.push(` # or, working solo: git switch ${result.branchName}`);
|
|
1142
|
+
lines.push(`Then run ${result.nextCommand || '/dev'} from that checkout.`);
|
|
1143
|
+
} else if (result.nextCommand) {
|
|
1144
|
+
lines.push(`Next: ${result.nextCommand}`);
|
|
1145
|
+
}
|
|
698
1146
|
|
|
699
1147
|
return {
|
|
700
1148
|
...result,
|
|
@@ -704,10 +1152,14 @@ module.exports = {
|
|
|
704
1152
|
readResearchDoc,
|
|
705
1153
|
detectScope,
|
|
706
1154
|
createBeadsIssue,
|
|
1155
|
+
createKernelIssue,
|
|
707
1156
|
createFeatureBranch,
|
|
708
1157
|
extractDesignDecisions,
|
|
709
1158
|
extractTasksFromResearch,
|
|
710
1159
|
detectDRYViolation,
|
|
711
1160
|
applyYAGNIFilter,
|
|
712
1161
|
executePlan,
|
|
1162
|
+
issueBackendLabel,
|
|
1163
|
+
registerBranchIssueLinkage,
|
|
1164
|
+
currentBranchIssueFromDriver,
|
|
713
1165
|
};
|