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/_issue.js
CHANGED
|
@@ -1,70 +1,364 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
4
|
-
const {
|
|
3
|
+
const { runIssueOperation: defaultRunIssueOperation } = require('../forge-issues');
|
|
4
|
+
const { resolveIssueBackend, hasExplicitBackendSignal, shouldUseKernelBroker } = require('../issue-backend');
|
|
5
|
+
const {
|
|
6
|
+
ISSUE_COMMAND_SCHEMA_VERSION,
|
|
7
|
+
ISSUE_COMMAND_ERROR_SCHEMA_VERSION,
|
|
8
|
+
ISSUE_COMMAND_EXIT_CODES,
|
|
9
|
+
resolveNextCommands,
|
|
10
|
+
normalizePriority,
|
|
11
|
+
} = require('../kernel/issue-command-contract');
|
|
12
|
+
const { getResolvedRuntimeGraph } = require('../core/runtime-graph');
|
|
13
|
+
const { renderIssueEnvelope } = require('../issue-render');
|
|
14
|
+
const { recordStageTransition } = require('../workflow/stage-transition');
|
|
5
15
|
|
|
16
|
+
// The Forge issue command surface. Each subcommand routes through the shared
|
|
17
|
+
// runIssueOperation, which selects the active backend (Kernel by --kernel /
|
|
18
|
+
// --issue-backend kernel / FORGE_ISSUE_BACKEND=kernel; Beads otherwise) and performs
|
|
19
|
+
// any backend-specific argument translation. This module therefore carries NO direct
|
|
20
|
+
// issue-tracker invocation or argv translation — those live in the backend
|
|
21
|
+
// abstraction (lib/forge-issues.js + the issue adapters).
|
|
6
22
|
const SUBCOMMANDS = {
|
|
7
23
|
create: {
|
|
8
|
-
description: 'Create
|
|
9
|
-
usage: 'forge create [title] [
|
|
10
|
-
helpCommand: 'create',
|
|
11
|
-
buildBdArgs: (args) => ['create', ...args],
|
|
24
|
+
description: 'Create an issue via Forge',
|
|
25
|
+
usage: 'forge create [title] [flags]',
|
|
12
26
|
},
|
|
13
27
|
update: {
|
|
14
|
-
description: 'Update
|
|
15
|
-
usage: 'forge update <id...> [
|
|
16
|
-
helpCommand: 'update',
|
|
17
|
-
buildBdArgs: (args) => ['update', ...args],
|
|
28
|
+
description: 'Update an issue via Forge',
|
|
29
|
+
usage: 'forge update <id...> [flags]',
|
|
18
30
|
},
|
|
19
31
|
claim: {
|
|
20
|
-
description: 'Claim
|
|
21
|
-
usage: 'forge claim <id> [
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
description: 'Claim an issue via Forge',
|
|
33
|
+
usage: 'forge claim <id> [flags]',
|
|
34
|
+
},
|
|
35
|
+
release: {
|
|
36
|
+
description: 'Release a Kernel issue claim via Forge',
|
|
37
|
+
usage: 'forge release <id>',
|
|
38
|
+
},
|
|
39
|
+
comment: {
|
|
40
|
+
description: 'Add an issue comment via Forge',
|
|
41
|
+
usage: 'forge comment <id> <body...>',
|
|
30
42
|
},
|
|
31
43
|
close: {
|
|
32
|
-
description: 'Close
|
|
33
|
-
usage: 'forge close <id...> [
|
|
34
|
-
helpCommand: 'close',
|
|
35
|
-
buildBdArgs: (args) => ['close', ...args],
|
|
44
|
+
description: 'Close an issue via Forge',
|
|
45
|
+
usage: 'forge close <id...> [flags]',
|
|
36
46
|
},
|
|
37
47
|
show: {
|
|
38
|
-
description: 'Show
|
|
39
|
-
usage: 'forge show <id> [
|
|
40
|
-
helpCommand: 'show',
|
|
41
|
-
buildBdArgs: (args) => ['show', ...args],
|
|
48
|
+
description: 'Show an issue via Forge',
|
|
49
|
+
usage: 'forge show <id> [flags]',
|
|
42
50
|
},
|
|
43
51
|
list: {
|
|
44
|
-
description: 'List
|
|
45
|
-
usage: 'forge list [
|
|
46
|
-
helpCommand: 'list',
|
|
47
|
-
buildBdArgs: (args) => ['list', ...args],
|
|
52
|
+
description: 'List issues via Forge',
|
|
53
|
+
usage: 'forge list [flags]',
|
|
48
54
|
},
|
|
49
55
|
ready: {
|
|
50
|
-
description: 'Show ready
|
|
51
|
-
usage: 'forge ready [
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
description: 'Show ready issues via Forge',
|
|
57
|
+
usage: 'forge ready [flags]',
|
|
58
|
+
},
|
|
59
|
+
search: {
|
|
60
|
+
description: 'Search issues via Forge',
|
|
61
|
+
usage: 'forge issue search <query> [flags]',
|
|
62
|
+
},
|
|
63
|
+
stats: {
|
|
64
|
+
description: 'Show issue statistics via Forge',
|
|
65
|
+
usage: 'forge issue stats [flags]',
|
|
66
|
+
},
|
|
67
|
+
// KAP-7: derived read queries. The backend abstraction maps each to its tracker
|
|
68
|
+
// equivalent (the Kernel passes the operation name through unchanged; the Beads
|
|
69
|
+
// backend maps each to its passthrough subcommand). They are READS, so they are
|
|
70
|
+
// intentionally NOT in WRITE_SUBCOMMANDS.
|
|
71
|
+
blocked: {
|
|
72
|
+
description: 'Show blocked issues via Forge',
|
|
73
|
+
usage: 'forge issue blocked [flags]',
|
|
74
|
+
},
|
|
75
|
+
stale: {
|
|
76
|
+
description: 'Show stale issues via Forge',
|
|
77
|
+
usage: 'forge issue stale [--days <n>]',
|
|
78
|
+
},
|
|
79
|
+
orphans: {
|
|
80
|
+
description: 'Show issues with dangling dependency edges via Forge',
|
|
81
|
+
usage: 'forge issue orphans',
|
|
82
|
+
},
|
|
83
|
+
// KAP-12: read-only content lint — issues missing required content
|
|
84
|
+
// (task/bug with no acceptance_criteria). A READ, so NOT in WRITE_SUBCOMMANDS.
|
|
85
|
+
lint: {
|
|
86
|
+
description: 'Show issues missing required content via Forge',
|
|
87
|
+
usage: 'forge issue lint',
|
|
88
|
+
},
|
|
89
|
+
// Epic support: an epic's DIRECT children + kernel-computed rollup
|
|
90
|
+
// (done-only percentage, per-status counts, blocked list). A READ, so
|
|
91
|
+
// intentionally NOT in WRITE_SUBCOMMANDS.
|
|
92
|
+
children: {
|
|
93
|
+
description: 'Show an epic\'s child issues + rollup via Forge',
|
|
94
|
+
usage: 'forge issue children <epic-id> [--json]',
|
|
95
|
+
},
|
|
96
|
+
// Lease-ownership verification (kernel d71a824b). A READ whose EXIT CODE encodes the
|
|
97
|
+
// verdict: exit 0 iff the resolving actor holds the live lease, non-zero otherwise.
|
|
98
|
+
// A claim returning ok:true does not prove ownership (a duplicate replay also returns
|
|
99
|
+
// ok:true), so a worker calls `owns` to CONFIRM before mutating a claimed issue. A
|
|
100
|
+
// READ, so intentionally NOT in WRITE_SUBCOMMANDS.
|
|
101
|
+
owns: {
|
|
102
|
+
description: 'Verify the current actor holds the live lease on an issue via Forge',
|
|
103
|
+
usage: 'forge issue owns <id> [--json]',
|
|
104
|
+
},
|
|
105
|
+
// Active-lease listing (kernel issue 7dc229d4). A bare passthrough to the Kernel
|
|
106
|
+
// lease table (kernel_claims); Beads has no lease table to enumerate, so
|
|
107
|
+
// forge-issues.js rejects the Beads path explicitly. A READ, so intentionally NOT
|
|
108
|
+
// in WRITE_SUBCOMMANDS.
|
|
109
|
+
claims: {
|
|
110
|
+
description: 'Show active issue leases (claims) via Forge',
|
|
111
|
+
usage: 'forge issue claims [--json]',
|
|
112
|
+
},
|
|
113
|
+
dep: {
|
|
114
|
+
description: 'Manage issue dependencies via Forge',
|
|
115
|
+
usage: 'forge issue dep <add|remove> <issue-id> <blocks-issue-id>',
|
|
116
|
+
// The dep subcommand fans out to two actions. Declaring them here keeps the
|
|
117
|
+
// supported action set discoverable from the command spec (and lets the
|
|
118
|
+
// release-readiness gate confirm dep add/remove are present).
|
|
119
|
+
actions: {
|
|
120
|
+
add: { description: 'Add a dependency edge (issue-id blocked by blocks-issue-id)' },
|
|
121
|
+
remove: { description: 'Remove a dependency edge' },
|
|
122
|
+
},
|
|
54
123
|
},
|
|
55
124
|
};
|
|
56
125
|
|
|
57
|
-
|
|
58
|
-
|
|
126
|
+
const WRITE_SUBCOMMANDS = new Set(['create', 'update', 'claim', 'release', 'comment', 'close', 'dep']);
|
|
127
|
+
|
|
128
|
+
// Human-first reads (kernel issue a9bbd065, 0.1.0 critical path): these
|
|
129
|
+
// subcommands default to a compact text rendering (lib/issue-render.js) instead
|
|
130
|
+
// of the raw forge.issue.v1 envelope. --json (or FORGE_JSON=1) restores the
|
|
131
|
+
// byte-identical contract output for scripts. DELIBERATE breaking change to the
|
|
132
|
+
// DEFAULT output only, approved before the 0.1.0 API freeze — the kernel
|
|
133
|
+
// contract itself is untouched.
|
|
134
|
+
const HUMAN_RENDERED_SUBCOMMANDS = new Set(['ready', 'list', 'show']);
|
|
135
|
+
// Writes render a human confirmation ONLY at an interactive TTY; piped/CI callers keep
|
|
136
|
+
// the machine-parseable JSON envelope by default so scripts work without --json (842a8be7).
|
|
137
|
+
const HUMAN_RENDERED_WRITE_SUBCOMMANDS = new Set(['create', 'update', 'claim', 'release', 'comment', 'close']);
|
|
138
|
+
const DEP_ACTIONS = Object.keys(SUBCOMMANDS.dep.actions);
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Check-after-write verification (gate.issue_verify, kernel issue 5f928cd0).
|
|
142
|
+
//
|
|
143
|
+
// The kernel has two PROVEN cases where a mutation's ok:true lied about the
|
|
144
|
+
// stored outcome: 145d9ad1 (close --reason/closed_at dropped by the
|
|
145
|
+
// kernel_issues projection) and d71a824b (idempotent claim replay telling a
|
|
146
|
+
// losing agent it won). So after a successful kernel mutation, this boundary
|
|
147
|
+
// re-reads the issue THROUGH THE SAME runner/broker and asserts the intended
|
|
148
|
+
// delta actually landed, emitting `verified: true|false` + `mismatches: []` in
|
|
149
|
+
// the envelope. WARN MODE ONLY: a mismatch prints a warning and the write's
|
|
150
|
+
// success/exit code are never touched; a verification READ error yields
|
|
151
|
+
// `verified: null` (unknown) rather than failing the write. Governed by the
|
|
152
|
+
// default-ON, unlocked `gate.issue_verify` (runtime-graph) — disable via
|
|
153
|
+
// `forge gate disable gate.issue_verify`, which skips the read-back entirely.
|
|
154
|
+
const ISSUE_VERIFY_GATE_ID = 'gate.issue_verify';
|
|
155
|
+
const VERIFIED_SUBCOMMANDS = new Set(['create', 'update', 'close', 'claim', 'comment']);
|
|
156
|
+
|
|
157
|
+
// Resolve whether gate.issue_verify is enabled for this project. An injected
|
|
158
|
+
// opts.resolveRuntimeGraph wins (tests); an unresolvable config (lint errors)
|
|
159
|
+
// SKIPS verification rather than breaking the write path.
|
|
160
|
+
function isIssueVerifyEnabled(projectRoot, opts = {}) {
|
|
161
|
+
const resolveGraph = opts.resolveRuntimeGraph || getResolvedRuntimeGraph;
|
|
162
|
+
try {
|
|
163
|
+
const graph = resolveGraph({ projectRoot });
|
|
164
|
+
const gate = (graph.gates || []).find(candidate => candidate.id === ISSUE_VERIFY_GATE_ID);
|
|
165
|
+
return gate ? gate.enabled !== false : true;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Minimal --flag parser mirroring the broker's parseFlagPairs semantics
|
|
172
|
+
// (--key value and --key=value; last value wins) so the expected delta is
|
|
173
|
+
// derived from the SAME tokens the broker consumed.
|
|
174
|
+
function parseVerifyFlags(args = []) {
|
|
175
|
+
const flags = {};
|
|
176
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
177
|
+
const token = args[index];
|
|
178
|
+
if (typeof token !== 'string' || !token.startsWith('--')) continue;
|
|
179
|
+
const body = token.slice(2);
|
|
180
|
+
const eq = body.indexOf('=');
|
|
181
|
+
if (eq !== -1) {
|
|
182
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const next = args[index + 1];
|
|
186
|
+
if (typeof next === 'string' && !next.startsWith('--')) {
|
|
187
|
+
flags[body] = next;
|
|
188
|
+
index += 1;
|
|
189
|
+
} else {
|
|
190
|
+
flags[body] = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return flags;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// The exact-match fields a mutation promised, derived from its flags. Only the
|
|
197
|
+
// REQUESTED fields are asserted (no coupling to backend defaults). Close always
|
|
198
|
+
// asserts the terminal status the broker resolves (explicit --status wins, else
|
|
199
|
+
// 'done' — mirrors the driver's resolveMutationStatus).
|
|
200
|
+
function expectedIssueFields(subcommand, flags) {
|
|
201
|
+
const expected = {};
|
|
202
|
+
if (subcommand === 'create' || subcommand === 'update') {
|
|
203
|
+
if (typeof flags.title === 'string') expected.title = flags.title;
|
|
204
|
+
}
|
|
205
|
+
if (subcommand === 'create' && typeof flags.type === 'string') expected.type = flags.type;
|
|
206
|
+
if (subcommand === 'update') {
|
|
207
|
+
if (typeof flags.status === 'string') expected.status = flags.status;
|
|
208
|
+
if (typeof flags.assignee === 'string') expected.assignee = flags.assignee;
|
|
209
|
+
if (typeof flags.priority === 'string') expected.priority = normalizePriority(flags.priority);
|
|
210
|
+
}
|
|
211
|
+
if (subcommand === 'close') {
|
|
212
|
+
expected.status = typeof flags.status === 'string' ? flags.status : 'done';
|
|
213
|
+
}
|
|
214
|
+
return expected;
|
|
59
215
|
}
|
|
60
216
|
|
|
61
|
-
function
|
|
217
|
+
function collectFieldMismatches(expected, data) {
|
|
218
|
+
const mismatches = [];
|
|
219
|
+
for (const [field, value] of Object.entries(expected)) {
|
|
220
|
+
const stored = data ? data[field] : undefined;
|
|
221
|
+
if (stored !== value) {
|
|
222
|
+
mismatches.push(`${field}: expected ${JSON.stringify(value)}, read back ${JSON.stringify(stored ?? null)}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return mismatches;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Close-specific presence checks — the regression trap for 145d9ad1, where the
|
|
229
|
+
// projection silently dropped closed_at and the --reason text.
|
|
230
|
+
function collectCloseMismatches(flags, data, mismatches) {
|
|
231
|
+
if (data.closed_at === null || data.closed_at === undefined) {
|
|
232
|
+
mismatches.push('closed_at: expected a close timestamp, read back null (145d9ad1-class projection drop)');
|
|
233
|
+
}
|
|
234
|
+
const reasonRequested = typeof flags.reason === 'string' || typeof flags['close-reason'] === 'string';
|
|
235
|
+
if (reasonRequested && (data.close_reason === null || data.close_reason === undefined)) {
|
|
236
|
+
mismatches.push('close_reason: expected the requested reason to persist, read back null (145d9ad1-class projection drop)');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Claim verification uses the claim-safety primitive directly: `owns` encodes
|
|
241
|
+
// "does the RESOLVING ACTOR hold the live lease" — exactly the check that
|
|
242
|
+
// catches a d71a824b phantom-claim replay (ok:true for a lease someone else holds).
|
|
243
|
+
async function verifyClaimDelta(runner, issueId, projectRoot, opts) {
|
|
244
|
+
const readBack = await runner('owns', [issueId, '--json'], projectRoot, opts);
|
|
245
|
+
if (!readBack || readBack.ok !== true || !readBack.data || typeof readBack.data !== 'object') {
|
|
246
|
+
return { verified: null };
|
|
247
|
+
}
|
|
248
|
+
if (readBack.data.owned === true) {
|
|
249
|
+
return { verified: true, mismatches: [] };
|
|
250
|
+
}
|
|
251
|
+
const heldBy = JSON.stringify(readBack.data.claimed_by ?? null);
|
|
252
|
+
return {
|
|
253
|
+
verified: false,
|
|
254
|
+
mismatches: [
|
|
255
|
+
`claimed_by: expected the resolved actor to hold the live lease, but owns reports held by ${heldBy} `
|
|
256
|
+
+ '(d71a824b-class phantom-claim replay)',
|
|
257
|
+
],
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Comment verification matches the MINTED comment_id in the re-read comments
|
|
262
|
+
// array (show returns comments with ids), sidestepping the count-increment
|
|
263
|
+
// approach that would need a pre-write read.
|
|
264
|
+
function verifyCommentDelta(result, data) {
|
|
265
|
+
const commentId = result.data && typeof result.data.comment_id === 'string' ? result.data.comment_id : null;
|
|
266
|
+
if (!commentId) return { verified: null };
|
|
267
|
+
const comments = Array.isArray(data.comments) ? data.comments : [];
|
|
268
|
+
if (comments.some(comment => comment && comment.id === commentId)) {
|
|
269
|
+
return { verified: true, mismatches: [] };
|
|
270
|
+
}
|
|
62
271
|
return {
|
|
63
|
-
|
|
64
|
-
|
|
272
|
+
verified: false,
|
|
273
|
+
mismatches: [`comment: expected comment ${commentId} on the issue, not found in the read-back`],
|
|
65
274
|
};
|
|
66
275
|
}
|
|
67
276
|
|
|
277
|
+
// Re-read the mutated issue through the SAME runner and assert the intended
|
|
278
|
+
// delta. Returns { verified: true|false|null, mismatches? }. NEVER throws — a
|
|
279
|
+
// verification failure must never make a successful write report failure.
|
|
280
|
+
async function verifyIssueMutation(subcommand, operationArgs, result, runner, projectRoot, opts) {
|
|
281
|
+
try {
|
|
282
|
+
const flags = parseVerifyFlags(operationArgs);
|
|
283
|
+
const issueId = (result.data && typeof result.data.id === 'string' && result.data.id)
|
|
284
|
+
? result.data.id
|
|
285
|
+
: operationArgs.find(arg => typeof arg === 'string' && !arg.startsWith('-'));
|
|
286
|
+
if (!issueId) return { verified: null };
|
|
287
|
+
if (subcommand === 'claim') {
|
|
288
|
+
return await verifyClaimDelta(runner, issueId, projectRoot, opts);
|
|
289
|
+
}
|
|
290
|
+
const readBack = await runner('show', [issueId, '--json'], projectRoot, opts);
|
|
291
|
+
if (!readBack || readBack.ok !== true || !readBack.data || typeof readBack.data !== 'object') {
|
|
292
|
+
return { verified: null };
|
|
293
|
+
}
|
|
294
|
+
if (subcommand === 'comment') {
|
|
295
|
+
return verifyCommentDelta(result, readBack.data);
|
|
296
|
+
}
|
|
297
|
+
const mismatches = collectFieldMismatches(expectedIssueFields(subcommand, flags), readBack.data);
|
|
298
|
+
if (subcommand === 'close') {
|
|
299
|
+
collectCloseMismatches(flags, readBack.data, mismatches);
|
|
300
|
+
}
|
|
301
|
+
return { verified: mismatches.length === 0, mismatches };
|
|
302
|
+
} catch {
|
|
303
|
+
return { verified: null };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Warn-mode reporting: verified:false lists the mismatches; verified:null says
|
|
308
|
+
// the read-back could not confirm. Either way the write stays successful.
|
|
309
|
+
function warnVerifyOutcome(subcommand, issueRef, verification) {
|
|
310
|
+
if (verification.verified === true) return;
|
|
311
|
+
const lines = verification.verified === false
|
|
312
|
+
? [
|
|
313
|
+
`[forge verify] WARNING: '${subcommand}' reported ok but the read-back of ${issueRef} does not match the requested change:`,
|
|
314
|
+
...verification.mismatches.map(entry => ` - ${entry}`),
|
|
315
|
+
]
|
|
316
|
+
: [`[forge verify] WARNING: '${subcommand}' succeeded but the read-back of ${issueRef} could not confirm the change (verification read failed).`];
|
|
317
|
+
lines.push('The write itself succeeded (exit 0). Check-after-write is governed by gate.issue_verify (warn mode) — disable via: forge gate disable gate.issue_verify');
|
|
318
|
+
console.warn(lines.join('\n'));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Run verification for one successful mutation result and attach the outcome
|
|
322
|
+
// (verified / mismatches) to it, warning on anything but verified:true.
|
|
323
|
+
async function applyIssueVerification(subcommand, operationArgs, result, runner, projectRoot, opts) {
|
|
324
|
+
const verification = await verifyIssueMutation(subcommand, operationArgs, result, runner, projectRoot, opts);
|
|
325
|
+
result.verified = verification.verified;
|
|
326
|
+
if (Array.isArray(verification.mismatches)) {
|
|
327
|
+
result.mismatches = verification.mismatches;
|
|
328
|
+
}
|
|
329
|
+
const issueRef = (result.data && result.data.id) || operationArgs.find(arg => typeof arg === 'string' && !arg.startsWith('-')) || '<issue>';
|
|
330
|
+
warnVerifyOutcome(subcommand, issueRef, verification);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function normalizeArgs(args = []) {
|
|
334
|
+
return args.filter(arg => arg !== '--');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 5a5ba3a6: auto-wire stage_runs. When a kernel `comment` carries the Descriptive
|
|
338
|
+
// Context Convention's `stage: <from> -> <to>` line, mirror it into stage_runs
|
|
339
|
+
// (complete the from-stage, start the to-stage) so `current_stage` becomes real
|
|
340
|
+
// without a manual `forge stage` verb call. Best-effort and non-blocking:
|
|
341
|
+
// recordStageTransition never throws, so a stage_run write can never break the
|
|
342
|
+
// comment that already succeeded. Kernel path only — reuses the driver already
|
|
343
|
+
// assembled on opts (no second DB handle).
|
|
344
|
+
function recordStageTransitionFromComment(subcommand, operationArgs, result, opts) {
|
|
345
|
+
if (subcommand !== 'comment' || !shouldUseKernelBroker(opts)) return;
|
|
346
|
+
if (!result || result.ok !== true || !result.data) return;
|
|
347
|
+
const issueId = typeof result.data.id === 'string' ? result.data.id : null;
|
|
348
|
+
if (!issueId) return;
|
|
349
|
+
// Drop the leading issue-ref positional; the rest is the comment body.
|
|
350
|
+
const body = normalizeArgs(operationArgs)
|
|
351
|
+
.filter(arg => typeof arg === 'string' && !arg.startsWith('-'))
|
|
352
|
+
.slice(1)
|
|
353
|
+
.join(' ');
|
|
354
|
+
recordStageTransition({
|
|
355
|
+
driver: opts.kernelDriver,
|
|
356
|
+
issueId,
|
|
357
|
+
body,
|
|
358
|
+
config: opts.kernelDatabasePath ? { databasePath: opts.kernelDatabasePath } : {},
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
68
362
|
function formatIssueHelp() {
|
|
69
363
|
const lines = [
|
|
70
364
|
'Usage: forge issue <subcommand> [...]',
|
|
@@ -82,64 +376,428 @@ function formatIssueHelp() {
|
|
|
82
376
|
lines.push(' forge claim forge-abc');
|
|
83
377
|
lines.push(' forge update forge-abc --priority 1');
|
|
84
378
|
lines.push(' forge close forge-abc --reason "Done"');
|
|
379
|
+
lines.push(' forge comment forge-abc "Handoff note"');
|
|
85
380
|
lines.push(' forge issue show forge-abc --json');
|
|
381
|
+
lines.push(' forge issue search "kernel contract" --json');
|
|
382
|
+
lines.push(' forge issue stats --json');
|
|
383
|
+
lines.push(' forge issue dep add forge-work forge-blocker');
|
|
86
384
|
|
|
87
385
|
return lines.join('\n');
|
|
88
386
|
}
|
|
89
387
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
388
|
+
// Map a CLI subcommand to the backend operation name. `dep` fans out to
|
|
389
|
+
// `dep.<action>`; every other subcommand uses its own name (the backend performs
|
|
390
|
+
// any tracker-specific translation, e.g. Beads claim -> `update <id> --claim`).
|
|
391
|
+
function resolveIssueOperation(subcommand, args) {
|
|
392
|
+
if (subcommand === 'dep') {
|
|
393
|
+
return `dep.${normalizeArgs(args)[0]}`;
|
|
93
394
|
}
|
|
395
|
+
return subcommand;
|
|
396
|
+
}
|
|
94
397
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
398
|
+
// The Kernel create payload (buildCreatePayload) reads only the --title flag, so a
|
|
399
|
+
// bare leading positional (`forge create "title"`) would be ignored and the title
|
|
400
|
+
// would default to the minted UUID. For parity on the KERNEL PATH ONLY, translate a
|
|
401
|
+
// single leading bare positional into `--title <value>` when no explicit
|
|
402
|
+
// --title/--title= is present. The Beads backend keeps its native positional
|
|
403
|
+
// handling (this never runs for the Beads path).
|
|
404
|
+
function withKernelCreateTitle(args) {
|
|
405
|
+
const hasTitle = args.some(
|
|
406
|
+
arg => arg === '--title' || (typeof arg === 'string' && arg.startsWith('--title=')),
|
|
407
|
+
);
|
|
408
|
+
if (hasTitle || args.length === 0) {
|
|
409
|
+
return args;
|
|
410
|
+
}
|
|
411
|
+
const leading = args[0];
|
|
412
|
+
if (typeof leading === 'string' && !leading.startsWith('-')) {
|
|
413
|
+
return ['--title', leading, ...args.slice(1)];
|
|
414
|
+
}
|
|
415
|
+
return args;
|
|
98
416
|
}
|
|
99
417
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
418
|
+
// Resolve the operation args passed to the backend. `dep` drops its leading action
|
|
419
|
+
// token (it is carried in the operation name); `create` on the Kernel path gains the
|
|
420
|
+
// positional-title parity translation. Everything else passes its normalized args
|
|
421
|
+
// through verbatim.
|
|
422
|
+
function resolveOperationArgs(subcommand, args, opts = {}) {
|
|
423
|
+
const normalizedArgs = normalizeArgs(args);
|
|
424
|
+
|
|
425
|
+
if (subcommand === 'dep') {
|
|
426
|
+
return normalizedArgs.slice(1);
|
|
104
427
|
}
|
|
105
428
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return [spec.helpCommand, '--help'];
|
|
429
|
+
if (subcommand === 'create' && shouldUseKernelBroker(opts)) {
|
|
430
|
+
return withKernelCreateTitle(normalizedArgs);
|
|
109
431
|
}
|
|
110
432
|
|
|
111
|
-
return
|
|
433
|
+
return normalizedArgs;
|
|
112
434
|
}
|
|
113
435
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
436
|
+
// Validate the dep action up front so a bad `forge issue dep <action>` fails with a
|
|
437
|
+
// usage message instead of routing to a `dep.undefined` operation.
|
|
438
|
+
function validateDepArgs(args) {
|
|
439
|
+
const normalized = normalizeArgs(args);
|
|
440
|
+
const [action, ...rest] = normalized;
|
|
441
|
+
if (!DEP_ACTIONS.includes(action)) {
|
|
442
|
+
return {
|
|
443
|
+
error: `Unsupported dependency action: ${action || '(missing)'}. Usage: forge issue dep <add|remove> <issue-id> <blocks-issue-id>`,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
if (rest.length < 2) {
|
|
447
|
+
return {
|
|
448
|
+
error: `Missing dependency ids. Usage: forge issue dep ${action} <issue-id> <blocks-issue-id>`,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
118
453
|
|
|
119
|
-
|
|
120
|
-
|
|
454
|
+
// Resolve the active issue backend (kernel|beads) and thread it into opts so the
|
|
455
|
+
// shared runIssueOperation deps see it. OPT-IN ONLY: opts is left byte-identical
|
|
456
|
+
// when no explicit signal is present (env/config/explicit), preserving the Beads
|
|
457
|
+
// default path. A copy is returned — the caller's opts object is never mutated.
|
|
458
|
+
function withResolvedIssueBackend(projectRoot, opts = {}) {
|
|
459
|
+
const env = opts.env || process.env;
|
|
460
|
+
const signalContext = { deps: opts, env, projectRoot };
|
|
461
|
+
if (!hasExplicitBackendSignal(signalContext)) {
|
|
462
|
+
return opts;
|
|
121
463
|
}
|
|
122
464
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
465
|
+
// Run EVERY explicit value through the resolver — including an explicit
|
|
466
|
+
// opts.issueBackend — so case is normalized and unknown values warn + fall back.
|
|
467
|
+
// opts.issueBackend still wins precedence over env/config inside the resolver.
|
|
468
|
+
// Identity is preserved when the resolved value already matches, keeping the
|
|
469
|
+
// no-op path byte-identical.
|
|
470
|
+
const issueBackend = resolveIssueBackend(signalContext);
|
|
471
|
+
if (opts.issueBackend === issueBackend) {
|
|
472
|
+
return opts;
|
|
127
473
|
}
|
|
474
|
+
return { ...opts, issueBackend };
|
|
475
|
+
}
|
|
128
476
|
|
|
129
|
-
|
|
477
|
+
// The Kernel broker returns the issue-command contract shape
|
|
478
|
+
// ({ ok, schema_version, command, data, next_commands } or { ok:false, error })
|
|
479
|
+
// rather than the Beads-style { success, output }. The bin/forge.js result printer
|
|
480
|
+
// keys on `success`/`output`, so a raw kernel contract would render as
|
|
481
|
+
// "Command failed". Normalize ONLY the contract shape (ok defined, success
|
|
482
|
+
// undefined) into { success, output } here, at the command boundary — the kernel
|
|
483
|
+
// contract itself stays untouched. Every other result passes through byte-identical.
|
|
484
|
+
//
|
|
485
|
+
// Response-contract parity (the Beads behavior the Kernel replaced):
|
|
486
|
+
// * SUCCESS → the printed envelope carries `ok:true` (consumers gate on it).
|
|
487
|
+
// * FAILURE → the contract `exit_code` is surfaced as `result.exitCode` so the bin
|
|
488
|
+
// printer exits with the error class's code (not always 1); and on
|
|
489
|
+
// `--json` the full forge.issue.error.v1 envelope is emitted on stdout
|
|
490
|
+
// (without --json the human message alone goes to stderr, as before).
|
|
491
|
+
function normalizeIssueResult(result, operation, { json = false, humanRender = null } = {}) {
|
|
492
|
+
if (!result || typeof result !== 'object') {
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
if (result.ok === undefined || result.success !== undefined) {
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
130
498
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
499
|
+
if (result.ok === true) {
|
|
500
|
+
const envelope = {
|
|
501
|
+
ok: true,
|
|
502
|
+
schema_version: result.schema_version,
|
|
503
|
+
command: result.command,
|
|
504
|
+
data: result.data ?? null,
|
|
505
|
+
next_commands: result.next_commands ?? [],
|
|
506
|
+
};
|
|
507
|
+
// Check-after-write outcome (gate.issue_verify): additive keys, present only
|
|
508
|
+
// when verification ran (verified may be null when the read-back failed).
|
|
509
|
+
if (result.verified !== undefined) envelope.verified = result.verified;
|
|
510
|
+
if (result.mismatches !== undefined) envelope.mismatches = result.mismatches;
|
|
135
511
|
return {
|
|
136
|
-
success:
|
|
137
|
-
|
|
512
|
+
success: true,
|
|
513
|
+
operation,
|
|
514
|
+
// Human-first reads (a9bbd065): render the envelope as text unless the
|
|
515
|
+
// caller asked for the contract (--json / FORGE_JSON=1). The envelope —
|
|
516
|
+
// including verified/mismatches — is rendered, never dropped.
|
|
517
|
+
output: humanRender
|
|
518
|
+
? renderIssueEnvelope(humanRender, envelope)
|
|
519
|
+
: JSON.stringify(envelope, null, 2),
|
|
138
520
|
};
|
|
139
521
|
}
|
|
522
|
+
|
|
523
|
+
const message = result.error?.message
|
|
524
|
+
|| result.message
|
|
525
|
+
|| `Issue ${operation} failed`;
|
|
526
|
+
const normalized = { success: false, error: message };
|
|
527
|
+
if (Number.isInteger(result.error?.exit_code)) {
|
|
528
|
+
normalized.exitCode = result.error.exit_code;
|
|
529
|
+
}
|
|
530
|
+
if (json) {
|
|
531
|
+
normalized.output = JSON.stringify({
|
|
532
|
+
ok: false,
|
|
533
|
+
schema_version: result.schema_version || ISSUE_COMMAND_ERROR_SCHEMA_VERSION,
|
|
534
|
+
command: result.command,
|
|
535
|
+
error: result.error,
|
|
536
|
+
next_commands: result.next_commands ?? [],
|
|
537
|
+
}, null, 2);
|
|
538
|
+
}
|
|
539
|
+
return normalized;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// `owns` is a read whose EXIT CODE encodes the ownership verdict: exit 0 when the
|
|
543
|
+
// resolving actor holds the live lease, non-zero otherwise. The kernel read itself
|
|
544
|
+
// succeeds (ok:true) whether or not the actor owns the lease — the verdict rides in
|
|
545
|
+
// `data.owned` — so the plain read normalizer (which maps every ok:true to success)
|
|
546
|
+
// would exit 0 on a NON-owned issue and let a worker mutate someone else's claim.
|
|
547
|
+
// This normalizer converts a `owned:false` verdict into a non-zero conflict result
|
|
548
|
+
// with a clear "you do not own the lease" message, while a genuine read failure (issue
|
|
549
|
+
// not found) still flows through normalizeIssueResult with its contract exit code. The
|
|
550
|
+
// `output` (the full envelope) is preserved even on the non-owned failure so `--json`
|
|
551
|
+
// consumers still receive `{ ok:true, data:{ owned:false, ... } }` on stdout.
|
|
552
|
+
function normalizeOwnsResult(result, operation, { json = false } = {}) {
|
|
553
|
+
// Raw read failure (e.g. not found) or a non-contract shape → defer to the standard
|
|
554
|
+
// normalizer so the contract exit code (notFound=3, ...) and error envelope survive.
|
|
555
|
+
if (!result || typeof result !== 'object' || result.ok !== true) {
|
|
556
|
+
return normalizeIssueResult(result, operation, { json });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const data = result.data && typeof result.data === 'object' ? result.data : {};
|
|
560
|
+
const envelope = JSON.stringify({
|
|
561
|
+
ok: true,
|
|
562
|
+
schema_version: result.schema_version,
|
|
563
|
+
command: result.command,
|
|
564
|
+
data,
|
|
565
|
+
next_commands: result.next_commands ?? [],
|
|
566
|
+
}, null, 2);
|
|
567
|
+
|
|
568
|
+
if (data.owned === true) {
|
|
569
|
+
return { success: true, operation, output: envelope };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const heldBy = typeof data.claimed_by === 'string' && data.claimed_by ? data.claimed_by : 'nobody';
|
|
573
|
+
const expiredNote = data.expired ? ' — the lease has expired and can be reclaimed' : '';
|
|
574
|
+
const issueId = typeof data.id === 'string' && data.id ? data.id : '<issue>';
|
|
575
|
+
const normalized = {
|
|
576
|
+
success: false,
|
|
577
|
+
operation,
|
|
578
|
+
error: `You do not own the lease for ${issueId} (held by ${heldBy}${expiredNote}). `
|
|
579
|
+
+ 'You lost the race or the claim collapsed to a duplicate — reselect via '
|
|
580
|
+
+ '`forge issue ready --json` and re-claim before working.',
|
|
581
|
+
exitCode: ISSUE_COMMAND_EXIT_CODES.conflict,
|
|
582
|
+
};
|
|
583
|
+
// On --json, surface the full ownership envelope on stdout (the bin printer emits
|
|
584
|
+
// result.output before the stderr error) so machine consumers still get the verdict.
|
|
585
|
+
if (json) {
|
|
586
|
+
normalized.output = envelope;
|
|
587
|
+
}
|
|
588
|
+
return normalized;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Split close args into the leading run of positional ids and the trailing flag
|
|
592
|
+
// tokens (everything from the first `-`-prefixed token onward). A flag value such
|
|
593
|
+
// as `done` in `--reason done` sits after a dash token, so it is correctly kept
|
|
594
|
+
// with the flags and never mistaken for an id.
|
|
595
|
+
function splitLeadingIds(args = []) {
|
|
596
|
+
const flagIndex = args.findIndex(arg => typeof arg === 'string' && arg.startsWith('-'));
|
|
597
|
+
if (flagIndex === -1) {
|
|
598
|
+
return { ids: [...args], flags: [] };
|
|
599
|
+
}
|
|
600
|
+
return { ids: args.slice(0, flagIndex), flags: args.slice(flagIndex) };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Kernel close fans out one runner call per id (the kernel close op closes a single
|
|
604
|
+
// id — the first positional), then aggregates the per-id outcomes into ONE
|
|
605
|
+
// forge.issue.v1 envelope (`mutationBatch` in the contract) — NEVER a bare array,
|
|
606
|
+
// which broke envelope parity for multi-id close. `ok` is true only when every id
|
|
607
|
+
// closed; per-id outcomes live in `data.results` and the contract `exit_code` of the
|
|
608
|
+
// first failure is surfaced as `exitCode` so the bin printer exits with the error
|
|
609
|
+
// class's code. KERNEL PATH ONLY: the Beads passthrough keeps its single
|
|
610
|
+
// `close id1 id2 ...` invocation.
|
|
611
|
+
async function runKernelBatchClose(runner, operation, ids, flags, projectRoot, opts, verifyEnabled = false) {
|
|
612
|
+
const results = [];
|
|
613
|
+
let allSucceeded = true;
|
|
614
|
+
let firstFailureExit;
|
|
615
|
+
for (const id of ids) {
|
|
616
|
+
const raw = await runner(operation, [id, ...flags], projectRoot, opts);
|
|
617
|
+
const succeeded = Boolean(raw && (raw.ok === true || raw.success === true));
|
|
618
|
+
if (succeeded) {
|
|
619
|
+
// Echo the CANONICAL id the kernel closed (raw.data.id) rather than the
|
|
620
|
+
// caller's token — with git-style short-id support the broker may have
|
|
621
|
+
// resolved a prefix to the full id, and the envelope should report the
|
|
622
|
+
// resolved id the read-back verified.
|
|
623
|
+
const closedId = (raw.data && typeof raw.data.id === 'string' && raw.data.id) ? raw.data.id : id;
|
|
624
|
+
const entry = { id: closedId, ok: true };
|
|
625
|
+
if (raw.data && Number.isInteger(raw.data.revision)) entry.revision = raw.data.revision;
|
|
626
|
+
if (raw.data && Array.isArray(raw.data.newly_unblocked)) entry.newly_unblocked = raw.data.newly_unblocked;
|
|
627
|
+
// Per-id check-after-write (gate.issue_verify): the batch path fans out one
|
|
628
|
+
// close per id, so each id gets its own read-back and verified/mismatches.
|
|
629
|
+
if (verifyEnabled && raw.ok === true) {
|
|
630
|
+
const verification = await verifyIssueMutation('close', [id, ...flags], raw, runner, projectRoot, opts);
|
|
631
|
+
entry.verified = verification.verified;
|
|
632
|
+
if (Array.isArray(verification.mismatches) && verification.mismatches.length > 0) {
|
|
633
|
+
entry.mismatches = verification.mismatches;
|
|
634
|
+
}
|
|
635
|
+
warnVerifyOutcome('close', id, verification);
|
|
636
|
+
}
|
|
637
|
+
results.push(entry);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
allSucceeded = false;
|
|
641
|
+
const error = (raw && raw.error && typeof raw.error === 'object')
|
|
642
|
+
? raw.error
|
|
643
|
+
: { message: (raw && (raw.error || raw.message)) || `Issue ${operation} failed for ${id}` };
|
|
644
|
+
if (firstFailureExit === undefined && Number.isInteger(error.exit_code)) {
|
|
645
|
+
firstFailureExit = error.exit_code;
|
|
646
|
+
}
|
|
647
|
+
results.push({ id, ok: false, error });
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const envelope = {
|
|
651
|
+
ok: allSucceeded,
|
|
652
|
+
schema_version: ISSUE_COMMAND_SCHEMA_VERSION,
|
|
653
|
+
command: 'issue.close',
|
|
654
|
+
data: {
|
|
655
|
+
results,
|
|
656
|
+
count: results.length,
|
|
657
|
+
closed: results.filter(entry => entry.ok).map(entry => entry.id),
|
|
658
|
+
},
|
|
659
|
+
next_commands: resolveNextCommands('issue.close'),
|
|
660
|
+
};
|
|
661
|
+
// Aggregate the per-id verification into envelope-level verified/mismatches:
|
|
662
|
+
// false if any closed id mismatched, null if any read-back was inconclusive,
|
|
663
|
+
// true only when every closed id verified clean.
|
|
664
|
+
const verifiedEntries = results.filter(entry => entry.ok && entry.verified !== undefined);
|
|
665
|
+
if (verifiedEntries.length > 0) {
|
|
666
|
+
if (verifiedEntries.some(entry => entry.verified === false)) {
|
|
667
|
+
envelope.verified = false;
|
|
668
|
+
envelope.mismatches = verifiedEntries
|
|
669
|
+
.filter(entry => Array.isArray(entry.mismatches))
|
|
670
|
+
.flatMap(entry => entry.mismatches.map(mismatch => `${entry.id}: ${mismatch}`));
|
|
671
|
+
} else if (verifiedEntries.some(entry => entry.verified === null)) {
|
|
672
|
+
envelope.verified = null;
|
|
673
|
+
} else {
|
|
674
|
+
envelope.verified = true;
|
|
675
|
+
envelope.mismatches = [];
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const normalized = {
|
|
680
|
+
success: allSucceeded,
|
|
681
|
+
operation,
|
|
682
|
+
output: JSON.stringify(envelope, null, 2),
|
|
683
|
+
};
|
|
684
|
+
if (!allSucceeded) {
|
|
685
|
+
const failed = results.filter(entry => !entry.ok).map(entry => entry.id);
|
|
686
|
+
normalized.error = `Failed to close ${failed.length} of ${ids.length} issue(s): ${failed.join(', ')}`;
|
|
687
|
+
normalized.exitCode = Number.isInteger(firstFailureExit)
|
|
688
|
+
? firstFailureExit
|
|
689
|
+
: ISSUE_COMMAND_EXIT_CODES.internal;
|
|
690
|
+
}
|
|
691
|
+
return normalized;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function runIssueSubcommand(subcommand, args, projectRoot, rawOpts = {}) {
|
|
695
|
+
const spec = SUBCOMMANDS[subcommand];
|
|
696
|
+
if (!spec) {
|
|
697
|
+
return { success: false, error: `Unknown issue subcommand '${subcommand}'.\n\n${formatIssueHelp()}` };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Backend-agnostic --help short-circuit: print the subcommand's usage and return
|
|
701
|
+
// BEFORE resolving the backend or dispatching any operation. Without this, a help
|
|
702
|
+
// request was forwarded to the active backend as an operation arg — absorbed
|
|
703
|
+
// harmlessly by Beads (which swallowed --help), but broken under the Kernel default:
|
|
704
|
+
// the plural path failed with a bare "Command failed" and the singular path
|
|
705
|
+
// SILENTLY minted a junk issue (and could queue a GitHub projection). Help must
|
|
706
|
+
// never touch a backend.
|
|
707
|
+
if (normalizeArgs(args).some(arg => arg === '--help' || arg === '-h')) {
|
|
708
|
+
return { success: true, output: `${spec.usage}\n\n${spec.description}` };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (subcommand === 'dep') {
|
|
712
|
+
const depError = validateDepArgs(args);
|
|
713
|
+
if (depError) {
|
|
714
|
+
return { success: false, error: depError.error };
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// `children` needs a leading <epic-id> positional; without it the kernel read
|
|
719
|
+
// binds `undefined` and surfaces a raw SQLite error. Fail with usage instead.
|
|
720
|
+
if (subcommand === 'children' && !normalizeArgs(args).some((arg) => !arg.startsWith('--'))) {
|
|
721
|
+
return { success: false, error: `Usage: ${spec.usage}` };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// These subcommands bind their target issue from the first positional. Without it the
|
|
725
|
+
// kernel path fabricates a random UUID and quarantines it ("invalid_claim_scope",
|
|
726
|
+
// exit 0) — surface a clean usage error with a non-zero exit instead (kernel 842a8be7).
|
|
727
|
+
const REQUIRES_LEADING_ID = new Set(['claim', 'release', 'comment', 'show', 'update', 'close', 'owns']);
|
|
728
|
+
if (REQUIRES_LEADING_ID.has(subcommand) && !normalizeArgs(args).some((arg) => !arg.startsWith('--'))) {
|
|
729
|
+
return { success: false, error: `Missing required argument <id>.\nUsage: ${spec.usage}`, exitCode: 6 };
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const opts = withResolvedIssueBackend(projectRoot, rawOpts);
|
|
733
|
+
|
|
734
|
+
// Both backends are reached through the same runIssueOperation seam. Naming the
|
|
735
|
+
// injected local `runIssueOperation` keeps the dispatch a literal call to a binding
|
|
736
|
+
// named `runIssueOperation` (the kernel-evidence gate is syntactic) while still
|
|
737
|
+
// honoring an injected runner; the `kernelBroker: opts.kernelBroker` passthrough is
|
|
738
|
+
// a runtime no-op (undefined under Beads) that documents the Kernel-capable surface.
|
|
739
|
+
const runIssueOperation = opts.runIssueOperation || defaultRunIssueOperation;
|
|
740
|
+
const operation = resolveIssueOperation(subcommand, args);
|
|
741
|
+
const operationArgs = resolveOperationArgs(subcommand, args, opts);
|
|
742
|
+
|
|
743
|
+
// Check-after-write (gate.issue_verify): resolved ONCE per invocation, kernel
|
|
744
|
+
// path only. Reads and the Beads path never trigger a read-back.
|
|
745
|
+
const verifyEnabled = VERIFIED_SUBCOMMANDS.has(subcommand)
|
|
746
|
+
&& shouldUseKernelBroker(opts)
|
|
747
|
+
&& isIssueVerifyEnabled(projectRoot, opts);
|
|
748
|
+
|
|
749
|
+
// Kernel batch close: >1 leading positional id → one runner call per id,
|
|
750
|
+
// aggregated. A single id falls through to the byte-identical single path.
|
|
751
|
+
if (subcommand === 'close' && shouldUseKernelBroker(opts)) {
|
|
752
|
+
const { ids, flags } = splitLeadingIds(operationArgs);
|
|
753
|
+
if (ids.length > 1) {
|
|
754
|
+
return runKernelBatchClose(runIssueOperation, operation, ids, flags, projectRoot, opts, verifyEnabled);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const result = await runIssueOperation(
|
|
759
|
+
operation,
|
|
760
|
+
operationArgs,
|
|
761
|
+
projectRoot,
|
|
762
|
+
{ ...opts, kernelBroker: opts.kernelBroker },
|
|
763
|
+
);
|
|
764
|
+
// Verify only a SUCCESSFUL kernel-contract mutation (ok:true, not a Beads
|
|
765
|
+
// {success,output} shape). Warn-only: attaches verified/mismatches, never
|
|
766
|
+
// changes the result's success or exit code.
|
|
767
|
+
if (verifyEnabled && result && typeof result === 'object' && result.ok === true && result.success === undefined) {
|
|
768
|
+
await applyIssueVerification(subcommand, operationArgs, result, runIssueOperation, projectRoot, opts);
|
|
769
|
+
}
|
|
770
|
+
// Best-effort, non-blocking: mirror a stage-transition comment into stage_runs.
|
|
771
|
+
recordStageTransitionFromComment(subcommand, operationArgs, result, opts);
|
|
772
|
+
// Contract output is opt-in for the human-first reads: an explicit --json flag
|
|
773
|
+
// or FORGE_JSON=1 in the environment (for scripts that cannot alter argv).
|
|
774
|
+
const jsonRequested = normalizeArgs(args).includes('--json')
|
|
775
|
+
|| (opts.env || process.env).FORGE_JSON === '1';
|
|
776
|
+
// `owns` maps the ownership verdict (data.owned) to the process exit code; every
|
|
777
|
+
// other subcommand uses the plain read/mutation normalization.
|
|
778
|
+
if (subcommand === 'owns') {
|
|
779
|
+
return normalizeOwnsResult(result, operation, { json: jsonRequested });
|
|
780
|
+
}
|
|
781
|
+
// Reads render human-first always; writes only when the caller is an interactive
|
|
782
|
+
// terminal. The CLI entry (bin/forge.js) sets rawOpts.isInteractive from
|
|
783
|
+
// process.stdout.isTTY; every other caller (scripts, tests, pipes) defaults to
|
|
784
|
+
// non-interactive and keeps the machine-parseable JSON envelope (842a8be7).
|
|
785
|
+
const interactive = rawOpts.isInteractive === true;
|
|
786
|
+
const humanSubcommand = !jsonRequested
|
|
787
|
+
&& (HUMAN_RENDERED_SUBCOMMANDS.has(subcommand)
|
|
788
|
+
|| (interactive && HUMAN_RENDERED_WRITE_SUBCOMMANDS.has(subcommand)))
|
|
789
|
+
? subcommand
|
|
790
|
+
: null;
|
|
791
|
+
return normalizeIssueResult(result, operation, {
|
|
792
|
+
json: jsonRequested,
|
|
793
|
+
humanRender: humanSubcommand,
|
|
794
|
+
});
|
|
140
795
|
}
|
|
141
796
|
|
|
142
|
-
|
|
797
|
+
// Build a registry command for a single issue subcommand (e.g. `forge claim`).
|
|
798
|
+
// Each routes through the shared runIssueSubcommand dispatch, so the backend
|
|
799
|
+
// abstraction owns all tracker selection and argument translation.
|
|
800
|
+
function createIssueSubcommand(subcommand) {
|
|
143
801
|
const spec = SUBCOMMANDS[subcommand];
|
|
144
802
|
if (!spec) {
|
|
145
803
|
throw new Error(`Unknown issue subcommand '${subcommand}'`);
|
|
@@ -158,8 +816,8 @@ function makeAliasCommand(subcommand) {
|
|
|
158
816
|
function createIssueCommand() {
|
|
159
817
|
return {
|
|
160
818
|
name: 'issue',
|
|
161
|
-
description: 'Manage
|
|
162
|
-
usage: 'forge issue <create|update|claim|close|show|list|ready> [...]',
|
|
819
|
+
description: 'Manage issues through the Forge command surface',
|
|
820
|
+
usage: 'forge issue <create|update|claim|release|close|show|list|ready|search|stats|dep> [...]',
|
|
163
821
|
flags: {},
|
|
164
822
|
handler: async (args, _flags, projectRoot, opts = {}) => {
|
|
165
823
|
const [subcommand, ...rest] = normalizeArgs(args);
|
|
@@ -175,8 +833,14 @@ function createIssueCommand() {
|
|
|
175
833
|
|
|
176
834
|
module.exports = {
|
|
177
835
|
SUBCOMMANDS,
|
|
178
|
-
|
|
836
|
+
WRITE_SUBCOMMANDS,
|
|
179
837
|
createIssueCommand,
|
|
180
|
-
|
|
838
|
+
createIssueSubcommand,
|
|
839
|
+
normalizeArgs,
|
|
840
|
+
normalizeIssueResult,
|
|
841
|
+
normalizeOwnsResult,
|
|
842
|
+
resolveIssueOperation,
|
|
843
|
+
resolveOperationArgs,
|
|
181
844
|
runIssueSubcommand,
|
|
845
|
+
withResolvedIssueBackend,
|
|
182
846
|
};
|