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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { repairWorkflowRuntimeAssets } = require('../commands/setup');
|
|
4
4
|
const { checkRuntimeHealth } = require('../runtime-health');
|
|
5
|
+
const { resolveIssueBackend } = require('../issue-backend');
|
|
5
6
|
const { normalizeStageId } = require('./stages');
|
|
6
7
|
const {
|
|
7
8
|
getAllowedTransitionsForWorkflowState,
|
|
@@ -12,6 +13,197 @@ const { loadState, WORKFLOW_STATE_FILENAME } = require('./state-manager');
|
|
|
12
13
|
|
|
13
14
|
const STATELESS_ENTRY_STAGES = new Set(['plan', 'dev', 'validate', 'verify']);
|
|
14
15
|
|
|
16
|
+
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
|
|
17
|
+
|
|
18
|
+
// Canonical full-ladder path (critical superset). Used to derive a stage's
|
|
19
|
+
// immediate predecessor for the kernel completion gate.
|
|
20
|
+
const STAGE_PATH = Object.freeze(['plan', 'dev', 'validate', 'ship', 'review', 'verify']);
|
|
21
|
+
|
|
22
|
+
function stagePredecessor(stageId) {
|
|
23
|
+
const index = STAGE_PATH.indexOf(stageId);
|
|
24
|
+
return index > 0 ? STAGE_PATH[index - 1] : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Emit a warning to stderr. Writes even under FORGE_JSON=1 on purpose: stderr
|
|
28
|
+
// never pollutes machine-readable stdout, and a dropped kernel write must always
|
|
29
|
+
// leave a trace ("never silent") so a JSON-mode agent can see it.
|
|
30
|
+
function defaultStageWarn(message) {
|
|
31
|
+
process.stderr.write(`${message}\n`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Best-effort kernel write (action 'start' | 'complete') at the stage
|
|
35
|
+
// chokepoint. Idempotent per (issue_id, stage). A failure warns to stderr but
|
|
36
|
+
// never throws — ship's gate stays tolerant of whatever IS durably recorded.
|
|
37
|
+
function recordStageRunSafe(driver, issueId, stageId, action, warn = defaultStageWarn) {
|
|
38
|
+
if (!driver || !issueId || !stageId || typeof driver.recordStageRun !== 'function') {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
driver.recordStageRun({ issue_id: issueId, stage: stageId, action }, {});
|
|
44
|
+
return true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
warn(`[forge] could not record stage '${stageId}' (${action}) for ${issueId} in the kernel: ${error.message}`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Latest stage-run row for a specific (issue, stage), or null.
|
|
52
|
+
function findLatestStageRun(driver, issueId, stage) {
|
|
53
|
+
if (!driver || typeof driver.listStageRuns !== 'function') {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const runs = driver.listStageRuns({ issue_id: issueId }, {}) || [];
|
|
59
|
+
let latest = null;
|
|
60
|
+
for (const run of runs) {
|
|
61
|
+
if (run?.stage === stage) {
|
|
62
|
+
latest = run;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return latest;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// (Re-)entering an earlier stage invalidates the work that followed it: any
|
|
72
|
+
// DOWNSTREAM stage previously recorded 'done' is reopened to 'active' so a gated
|
|
73
|
+
// stage (ship/review) re-requires a fresh completion (R2 — the dev<->validate
|
|
74
|
+
// rework loop must not let a stale validate=done pass ship). Reuses the idempotent
|
|
75
|
+
// 'start' write, which keeps the row's id + original started_at but clears its
|
|
76
|
+
// done status. Stages the entered stage does not precede (and non-'done' rows) are
|
|
77
|
+
// left untouched, so forward progression records nothing extra.
|
|
78
|
+
function invalidateDownstreamStages(driver, issueId, stageId, warn) {
|
|
79
|
+
const index = STAGE_PATH.indexOf(stageId);
|
|
80
|
+
if (index < 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (let next = index + 1; next < STAGE_PATH.length; next += 1) {
|
|
84
|
+
const downstream = STAGE_PATH[next];
|
|
85
|
+
const run = findLatestStageRun(driver, issueId, downstream);
|
|
86
|
+
if (run?.status === 'done') {
|
|
87
|
+
recordStageRunSafe(driver, issueId, downstream, 'start', warn);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Decide whether entering stageId is allowed given ONLY the kernel's recorded
|
|
93
|
+
// stage history (used when no inline/file workflow state exists):
|
|
94
|
+
// - Stateless stages (plan/dev/validate/verify) are always re-entrant, so the
|
|
95
|
+
// dev<->validate rework loop never dead-ends.
|
|
96
|
+
// - A gated stage (ship/review) requires its immediate predecessor to be
|
|
97
|
+
// COMPLETED (status 'done'); merely ENTERING validate does not unlock ship.
|
|
98
|
+
// - When nothing is recorded for the predecessor, returns { kernelEmpty } so
|
|
99
|
+
// the caller falls through to the fail-closed hard block.
|
|
100
|
+
function evaluateKernelStageGate(driver, issueId, stageId) {
|
|
101
|
+
if (STATELESS_ENTRY_STAGES.has(stageId)) {
|
|
102
|
+
return { allowed: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const predecessor = stagePredecessor(stageId);
|
|
106
|
+
const predecessorRun = predecessor ? findLatestStageRun(driver, issueId, predecessor) : null;
|
|
107
|
+
if (predecessorRun?.status === 'done') {
|
|
108
|
+
return { allowed: true };
|
|
109
|
+
}
|
|
110
|
+
if (predecessorRun) {
|
|
111
|
+
return {
|
|
112
|
+
allowed: false,
|
|
113
|
+
reason: `Stage ${stageId} requires ${predecessor} to be completed first (currently ${predecessorRun.status}).`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { allowed: false, kernelEmpty: true };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Verify a kernel issue exists before binding stage state to it (F4a: a UUID
|
|
120
|
+
// parsed from a branch name must not bind to a phantom issue).
|
|
121
|
+
async function kernelIssueExists(driver, issueId) {
|
|
122
|
+
if (!driver || typeof driver.findIssueIdsByPrefix !== 'function') {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// findIssueIdsByPrefix returns rows ({ id, title }); tolerate plain-id shapes too.
|
|
128
|
+
const matches = await driver.findIssueIdsByPrefix(issueId, 6, {}, {});
|
|
129
|
+
return Array.isArray(matches) && matches.some(row => (row?.id ?? row) === issueId);
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Resolve the kernel issue that THIS worktree/branch is working on, so stage
|
|
136
|
+
// state can be read and written without an explicit issue argument on `forge
|
|
137
|
+
// ship`. Prefers the branch->issue linkage registry (authoritative); falls back
|
|
138
|
+
// to a UUID encoded in the branch name, but ONLY after verifying it exists.
|
|
139
|
+
async function resolveActiveIssueId(driver, branch) {
|
|
140
|
+
if (!driver || !branch) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
if (typeof driver.listWorktrees === 'function') {
|
|
146
|
+
const rows = driver.listWorktrees() || [];
|
|
147
|
+
// Match only ACTIVE (live) linkage rows, newest first (listWorktrees orders
|
|
148
|
+
// registered_at DESC). A stale/superseded registration for a reused branch
|
|
149
|
+
// name must not rebind stage state to the OLD issue (be18881c). Tolerate a
|
|
150
|
+
// null state for rows written before the state column was populated.
|
|
151
|
+
const match = rows.find(row => row && row.branch === branch && row.issue_id
|
|
152
|
+
&& (row.state === 'active' || row.state == null));
|
|
153
|
+
if (match) {
|
|
154
|
+
return match.issue_id;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Fall through to branch-name parsing.
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const encoded = UUID_RE.exec(String(branch));
|
|
162
|
+
if (encoded && await kernelIssueExists(driver, encoded[0])) {
|
|
163
|
+
return encoded[0];
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Lazily build a kernel driver from the project root (the real CLI path).
|
|
169
|
+
// Best-effort: returns null when the kernel is unavailable so the caller
|
|
170
|
+
// degrades to legacy file/beads state instead of crashing a stage command.
|
|
171
|
+
async function buildKernelDriver(projectRoot) {
|
|
172
|
+
if (!projectRoot) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
178
|
+
const deps = await buildMigratedKernelIssueDeps({ projectRoot });
|
|
179
|
+
return deps.kernelDriver || null;
|
|
180
|
+
} catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function detectBranchName(projectRoot) {
|
|
186
|
+
try {
|
|
187
|
+
const { detectWorktree } = require('../detect-worktree');
|
|
188
|
+
const info = detectWorktree(projectRoot || process.cwd());
|
|
189
|
+
return info?.branch || null;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Resolve { driver, issueId } for kernel stage-state: build the driver from the
|
|
196
|
+
// project root and resolve the active issue from the branch when not explicitly
|
|
197
|
+
// injected. Best-effort — returns nulls when the kernel is absent.
|
|
198
|
+
async function resolveKernelContext({ kernelDriver, activeIssueId, branch, projectRoot }) {
|
|
199
|
+
const driver = kernelDriver || await buildKernelDriver(projectRoot);
|
|
200
|
+
let issueId = activeIssueId || null;
|
|
201
|
+
if (driver && !issueId) {
|
|
202
|
+
issueId = await resolveActiveIssueId(driver, branch || detectBranchName(projectRoot));
|
|
203
|
+
}
|
|
204
|
+
return { driver, issueId };
|
|
205
|
+
}
|
|
206
|
+
|
|
15
207
|
function getOverrideInput(flags = {}) {
|
|
16
208
|
if (Object.hasOwn(flags, 'overrideStage')) {
|
|
17
209
|
return flags.overrideStage;
|
|
@@ -103,7 +295,88 @@ function formatDiagnostics(diagnostics = []) {
|
|
|
103
295
|
.join('; ');
|
|
104
296
|
}
|
|
105
297
|
|
|
106
|
-
|
|
298
|
+
// Enforce a stage entry against authoritative FILE/inline workflow state
|
|
299
|
+
// (unchanged legacy behavior: normal path-transition + override rules).
|
|
300
|
+
function enforceWithFileState(currentState, stageId, flags, args, finish) {
|
|
301
|
+
const currentStage = currentState.currentStage;
|
|
302
|
+
const classification = currentState.workflowDecisions?.classification;
|
|
303
|
+
if (!currentStage || !classification || stageId === currentStage) {
|
|
304
|
+
return finish({ allowed: true, stage: stageId, workflowState: currentState });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const allowedTransitions = getAllowedTransitionsForWorkflowState(currentState);
|
|
308
|
+
if (allowedTransitions.includes(stageId)) {
|
|
309
|
+
return finish({ allowed: true, stage: stageId, workflowState: currentState });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const override = parseOverride(flags, args);
|
|
313
|
+
if (!override) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Stage ${stageId} is blocked from ${currentStage}. ` +
|
|
316
|
+
`Provide an explicit override payload via overrideStage or --override-stage.`
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (override.fromStage !== currentStage || override.toStage !== stageId) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`Stage override does not match workflow state. Expected ${currentStage} -> ${stageId}.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return finish({ allowed: true, stage: stageId, workflowState: currentState, override });
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isInlineStateProvided(workflowState, flags, args) {
|
|
330
|
+
return Boolean(
|
|
331
|
+
workflowState || flags.workflowState || flags['--workflow-state'] || getCliFlagValue('--workflow-state', args)
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Enforce a stage entry against kernel-recorded stage state (completion gate).
|
|
336
|
+
// Returns a decided enforcement result, or null to fall through to the
|
|
337
|
+
// stateless / hard-block rules.
|
|
338
|
+
function enforceWithKernelState(driver, issueId, stageId, finish) {
|
|
339
|
+
const gate = evaluateKernelStageGate(driver, issueId, stageId);
|
|
340
|
+
if (gate.allowed) {
|
|
341
|
+
return finish({ allowed: true, stage: stageId, workflowState: null });
|
|
342
|
+
}
|
|
343
|
+
if (!gate.kernelEmpty) {
|
|
344
|
+
throw new Error(gate.reason);
|
|
345
|
+
}
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function resolveStageRuntimeHealth({ health, checkHealth, projectRoot, issueBackend, commandName, flags, workflowState, repairRuntime }) {
|
|
350
|
+
const runHealthCheck = checkHealth || checkRuntimeHealth;
|
|
351
|
+
let runtimeHealth = health || runHealthCheck(projectRoot, { issueBackend });
|
|
352
|
+
if (runtimeHealth.hardStop && typeof repairRuntime === 'function') {
|
|
353
|
+
const repaired = await repairRuntime({ commandName, flags, projectRoot, workflowState, health: runtimeHealth });
|
|
354
|
+
if (repaired) {
|
|
355
|
+
runtimeHealth = repaired;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return runtimeHealth;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function enforceStageEntry({
|
|
362
|
+
commandName,
|
|
363
|
+
args = [],
|
|
364
|
+
flags = {},
|
|
365
|
+
projectRoot,
|
|
366
|
+
workflowState,
|
|
367
|
+
health,
|
|
368
|
+
repairRuntime,
|
|
369
|
+
checkHealth,
|
|
370
|
+
// B1 — kernel stage-state authority. Injectable for tests; the real CLI sets
|
|
371
|
+
// autoResolveKernel:true so the driver + active issue are resolved from the
|
|
372
|
+
// worktree. When neither a driver nor autoResolveKernel is provided, the
|
|
373
|
+
// kernel path is inert and behavior matches the legacy file/beads state.
|
|
374
|
+
kernelDriver,
|
|
375
|
+
activeIssueId,
|
|
376
|
+
branch,
|
|
377
|
+
autoResolveKernel = false,
|
|
378
|
+
warn = defaultStageWarn,
|
|
379
|
+
} = {}) {
|
|
107
380
|
const stageId = normalizeStageId(commandName);
|
|
108
381
|
if (!stageId) {
|
|
109
382
|
return { allowed: true };
|
|
@@ -113,67 +386,96 @@ async function enforceStageEntry({ commandName, args = [], flags = {}, projectRo
|
|
|
113
386
|
repairWorkflowRuntimeAssets(projectRoot);
|
|
114
387
|
}
|
|
115
388
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
health: runtimeHealth,
|
|
124
|
-
});
|
|
125
|
-
if (repairedHealth) {
|
|
126
|
-
runtimeHealth = repairedHealth;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
389
|
+
// Resolve the active issue backend (env > .forge/config.yaml > default 'kernel') so
|
|
390
|
+
// the runtime gate only treats bd as a hard prerequisite for the beads backend. The
|
|
391
|
+
// kernel default needs no bd, so stages must run without it.
|
|
392
|
+
const issueBackend = resolveIssueBackend({ deps: {}, env: process.env, projectRoot, warn: () => {} });
|
|
393
|
+
const runtimeHealth = await resolveStageRuntimeHealth({
|
|
394
|
+
health, checkHealth, projectRoot, issueBackend, commandName, flags, workflowState, repairRuntime,
|
|
395
|
+
});
|
|
129
396
|
if (runtimeHealth.hardStop) {
|
|
130
397
|
throw new Error(`Stage ${stageId} blocked by runtime prerequisites: ${formatDiagnostics(runtimeHealth.diagnostics)}`);
|
|
131
398
|
}
|
|
132
399
|
|
|
133
400
|
const stateInput = resolveWorkflowStateInput(workflowState, flags, args, projectRoot);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
401
|
+
|
|
402
|
+
// Kernel stage-state authority is active when a driver is injected (tests) or
|
|
403
|
+
// the caller opts in (real CLI via autoResolveKernel). Inline/flag state
|
|
404
|
+
// disables it: the caller is explicitly driving state, so no kernel side
|
|
405
|
+
// effects should occur.
|
|
406
|
+
const kernelEnabled = !isInlineStateProvided(workflowState, flags, args)
|
|
407
|
+
&& (Boolean(kernelDriver) || autoResolveKernel === true);
|
|
408
|
+
const { driver, issueId } = kernelEnabled
|
|
409
|
+
? await resolveKernelContext({ kernelDriver, activeIssueId, branch, projectRoot })
|
|
410
|
+
: { driver: null, issueId: null };
|
|
411
|
+
const kernelActive = Boolean(kernelEnabled && driver && issueId);
|
|
412
|
+
|
|
413
|
+
// On an allowed entry, record the stage as started (active) AND return a
|
|
414
|
+
// recordCompletion() the command runner calls after the handler SUCCEEDS — so
|
|
415
|
+
// a stage only counts as 'done' when its command actually passed (the ship
|
|
416
|
+
// gate below requires the predecessor to be done, not merely entered).
|
|
417
|
+
const finish = (result) => {
|
|
418
|
+
if (kernelActive) {
|
|
419
|
+
recordStageRunSafe(driver, issueId, stageId, 'start', warn);
|
|
420
|
+
invalidateDownstreamStages(driver, issueId, stageId, warn);
|
|
421
|
+
result.recordCompletion = () => recordStageRunSafe(driver, issueId, stageId, 'complete', warn);
|
|
138
422
|
}
|
|
423
|
+
return result;
|
|
424
|
+
};
|
|
139
425
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
);
|
|
426
|
+
const currentState = readWorkflowStateInput(stateInput);
|
|
427
|
+
if (currentState) {
|
|
428
|
+
return enforceWithFileState(currentState, stageId, flags, args, finish);
|
|
144
429
|
}
|
|
145
430
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
431
|
+
// B1 — strict mode is the explicit opt-in to the legacy fail-closed behavior:
|
|
432
|
+
// require prior stages / authoritative state. The default (unset) degrades to a
|
|
433
|
+
// loud warning and seeds the kernel so future gating gets real data. A recorded
|
|
434
|
+
// history that CONTRADICTS (e.g. validate started-but-not-done) still throws in
|
|
435
|
+
// both modes — that rework protection lives in enforceWithKernelState below and
|
|
436
|
+
// is never weakened by this flag.
|
|
437
|
+
const strict = process.env.FORGE_STAGE_GATE === 'strict';
|
|
151
438
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
439
|
+
// No inline/file state: the kernel is authoritative. Gate on recorded stage
|
|
440
|
+
// completions (tolerant read) so `ship` is reachable from a pure-CLI
|
|
441
|
+
// plan->dev->validate progression with no .forge-state.json.
|
|
442
|
+
if (kernelActive) {
|
|
443
|
+
const decided = enforceWithKernelState(driver, issueId, stageId, finish);
|
|
444
|
+
if (decided) {
|
|
445
|
+
return decided;
|
|
446
|
+
}
|
|
447
|
+
// enforceWithKernelState returned null → the kernel has an issue linked but
|
|
448
|
+
// NO recorded predecessor (a contradiction would have thrown above). Degrade
|
|
449
|
+
// to warn + seed unless strict: allow the stage and let finish() record it so
|
|
450
|
+
// future gating has real history.
|
|
451
|
+
if (!strict) {
|
|
452
|
+
warn(
|
|
453
|
+
`[forge] no recorded workflow history for issue ${issueId} — allowing '${stageId}' ` +
|
|
454
|
+
`and recording it in the kernel. Set FORGE_STAGE_GATE=strict to require prior stages.`
|
|
455
|
+
);
|
|
456
|
+
return finish({ allowed: true, stage: stageId, workflowState: null, degradedGate: 'kernel-empty' });
|
|
457
|
+
}
|
|
155
458
|
}
|
|
156
459
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
throw new Error(
|
|
160
|
-
`Stage ${stageId} is blocked from ${currentStage}. ` +
|
|
161
|
-
`Provide an explicit override payload via overrideStage or --override-stage.`
|
|
162
|
-
);
|
|
460
|
+
if (STATELESS_ENTRY_STAGES.has(stageId)) {
|
|
461
|
+
return finish({ allowed: true, stage: stageId, workflowState: null });
|
|
163
462
|
}
|
|
164
463
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
464
|
+
// No workflow state AND no kernel-linked issue for this branch. Degrade to warn
|
|
465
|
+
// unless strict so incremental adoption (fresh setup, in-flight branch, manual
|
|
466
|
+
// commit) is not blocked.
|
|
467
|
+
if (!strict) {
|
|
468
|
+
warn(
|
|
469
|
+
`[forge] no workflow state and no kernel-linked issue for this branch — stage gate skipped for '${stageId}'. ` +
|
|
470
|
+
`Link an issue with 'forge worktree create <slug>' to enable stage tracking.`
|
|
168
471
|
);
|
|
472
|
+
return finish({ allowed: true, stage: stageId, workflowState: null, degradedGate: 'no-kernel-context' });
|
|
169
473
|
}
|
|
170
474
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
override,
|
|
176
|
-
};
|
|
475
|
+
throw new Error(
|
|
476
|
+
`Stage ${stageId} requires authoritative workflow state. ` +
|
|
477
|
+
`Provide --workflow-state or restore ${WORKFLOW_STATE_FILENAME} before continuing (or unset FORGE_STAGE_GATE).`
|
|
478
|
+
);
|
|
177
479
|
}
|
|
178
480
|
|
|
179
481
|
module.exports = {
|
|
@@ -182,4 +484,8 @@ module.exports = {
|
|
|
182
484
|
parseOverride,
|
|
183
485
|
resolveWorkflowStateInput,
|
|
184
486
|
readWorkflowStateFile,
|
|
487
|
+
resolveActiveIssueId,
|
|
488
|
+
evaluateKernelStageGate,
|
|
489
|
+
recordStageRunSafe,
|
|
490
|
+
stagePredecessor,
|
|
185
491
|
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Stage-transition auto-recording (5a5ba3a6).
|
|
5
|
+
*
|
|
6
|
+
* PR #348 (f61601ab) added the stage_runs record/read capability but nothing
|
|
7
|
+
* populated it automatically — `current_stage` stayed unknown. The Descriptive
|
|
8
|
+
* Context Convention (AGENTS.md) already has agents record each stage boundary as
|
|
9
|
+
* a kernel issue comment shaped:
|
|
10
|
+
*
|
|
11
|
+
* stage: <from> -> <to>
|
|
12
|
+
* summary: ...
|
|
13
|
+
*
|
|
14
|
+
* This module turns that existing, already-followed convention into a structured
|
|
15
|
+
* stage_run WITHOUT any new manual verb call: parse the `stage:` line and, best
|
|
16
|
+
* effort, complete the from-stage and start the to-stage. Recording is strictly
|
|
17
|
+
* non-blocking — a stage_run write failure must NEVER break the comment that
|
|
18
|
+
* triggered it.
|
|
19
|
+
*
|
|
20
|
+
* @module workflow/stage-transition
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const { normalizeStageId } = require('./stages');
|
|
24
|
+
|
|
25
|
+
// `stage: <from> -> <to>` on its own line, case-insensitive, arrow spacing optional.
|
|
26
|
+
const STAGE_LINE = /^\s*stage:\s*([a-z]+)\s*->\s*([a-z]+)\s*$/im;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse a stage-transition line out of a comment body.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} body - Comment body (may be multi-line).
|
|
32
|
+
* @returns {{from: string, to: string}|null} Normalized stages, or null when the
|
|
33
|
+
* body has no valid `stage: X -> Y` line (both tokens must be canonical stages).
|
|
34
|
+
*/
|
|
35
|
+
function parseStageTransition(body) {
|
|
36
|
+
if (typeof body !== 'string' || body.length === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const match = body.match(STAGE_LINE);
|
|
40
|
+
if (!match) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
// STAGE_LINE is case-insensitive, so an uppercase token (e.g. `stage: DEV ->
|
|
44
|
+
// VALIDATE`) matches. normalizeStageId only knows lowercase canonical ids, so
|
|
45
|
+
// lowercase both captures first or an uppercase-but-valid stage is wrongly rejected.
|
|
46
|
+
const from = normalizeStageId(match[1].toLowerCase());
|
|
47
|
+
const to = normalizeStageId(match[2].toLowerCase());
|
|
48
|
+
if (!from || !to) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return { from, to };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Best-effort record of a stage transition into stage_runs. Parses the comment
|
|
56
|
+
* body; on a valid transition it completes the from-stage and starts the to-stage.
|
|
57
|
+
*
|
|
58
|
+
* The write must be ATOMIC: completing `from` and starting `to` are one logical
|
|
59
|
+
* transition, so a failure partway through must leave neither persisted (otherwise
|
|
60
|
+
* `current_stage` reflects a half-transition — from marked done, to never started).
|
|
61
|
+
* The preferred path is a single transactional `driver.recordStageTransition` that
|
|
62
|
+
* wraps both writes in one transaction; a driver that only exposes `recordStageRun`
|
|
63
|
+
* falls back to two sequential writes (non-atomic, tolerated only because the whole
|
|
64
|
+
* operation is non-blocking).
|
|
65
|
+
*
|
|
66
|
+
* ANY failure (parse miss, missing driver, DB error) is swallowed and reported as
|
|
67
|
+
* `{ recorded: false }` — this function never throws.
|
|
68
|
+
*
|
|
69
|
+
* @param {object} params
|
|
70
|
+
* @param {object} [params.driver] - Kernel driver exposing recordStageTransition
|
|
71
|
+
* (preferred) and/or recordStageRun(input, config).
|
|
72
|
+
* @param {string} params.issueId - Full kernel issue id the comment targets.
|
|
73
|
+
* @param {string} params.body - The comment body just written.
|
|
74
|
+
* @param {object} [params.config] - Driver config (e.g. { databasePath }).
|
|
75
|
+
* @returns {{recorded: boolean, from?: string, to?: string}}
|
|
76
|
+
*/
|
|
77
|
+
function recordStageTransition({ driver, issueId, body, config = {} } = {}) {
|
|
78
|
+
try {
|
|
79
|
+
const transition = parseStageTransition(body);
|
|
80
|
+
if (!transition || !issueId || !driver) {
|
|
81
|
+
return { recorded: false };
|
|
82
|
+
}
|
|
83
|
+
// Preferred: one atomic driver op — complete(from) + start(to) inside a single
|
|
84
|
+
// transaction, so a mid-transition failure rolls back and never persists a
|
|
85
|
+
// wrong `current_stage`.
|
|
86
|
+
if (typeof driver.recordStageTransition === 'function') {
|
|
87
|
+
driver.recordStageTransition(
|
|
88
|
+
{ issue_id: issueId, from: transition.from, to: transition.to },
|
|
89
|
+
config,
|
|
90
|
+
);
|
|
91
|
+
return { from: transition.from, to: transition.to, recorded: true };
|
|
92
|
+
}
|
|
93
|
+
// Fallback for a minimal driver without the atomic op: two sequential writes.
|
|
94
|
+
if (typeof driver.recordStageRun === 'function') {
|
|
95
|
+
driver.recordStageRun(
|
|
96
|
+
{ issue_id: issueId, stage: transition.from, action: 'complete' },
|
|
97
|
+
config,
|
|
98
|
+
);
|
|
99
|
+
driver.recordStageRun(
|
|
100
|
+
{ issue_id: issueId, stage: transition.to, action: 'start' },
|
|
101
|
+
config,
|
|
102
|
+
);
|
|
103
|
+
return { from: transition.from, to: transition.to, recorded: true };
|
|
104
|
+
}
|
|
105
|
+
return { recorded: false };
|
|
106
|
+
} catch {
|
|
107
|
+
// Non-blocking by contract: never let a stage_run write break the comment.
|
|
108
|
+
return { recorded: false };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
parseStageTransition,
|
|
114
|
+
recordStageTransition,
|
|
115
|
+
};
|
package/lib/workflow/stages.js
CHANGED
|
@@ -15,7 +15,6 @@ const STAGE_IDS = Object.freeze([
|
|
|
15
15
|
'validate',
|
|
16
16
|
'ship',
|
|
17
17
|
'review',
|
|
18
|
-
'premerge',
|
|
19
18
|
'verify',
|
|
20
19
|
]);
|
|
21
20
|
|
|
@@ -25,7 +24,6 @@ const STAGE_LABELS = Object.freeze({
|
|
|
25
24
|
validate: 'Validate',
|
|
26
25
|
ship: 'Ship',
|
|
27
26
|
review: 'Review',
|
|
28
|
-
premerge: 'Premerge',
|
|
29
27
|
verify: 'Verify',
|
|
30
28
|
});
|
|
31
29
|
|
|
@@ -35,14 +33,13 @@ const STAGE_COMMANDS = Object.freeze({
|
|
|
35
33
|
validate: '/validate',
|
|
36
34
|
ship: '/ship',
|
|
37
35
|
review: '/review',
|
|
38
|
-
premerge: '/premerge',
|
|
39
36
|
verify: '/verify',
|
|
40
37
|
});
|
|
41
38
|
|
|
42
39
|
const WORKFLOW_STAGE_MATRIX = Object.freeze({
|
|
43
|
-
critical: Object.freeze(['plan', 'dev', 'validate', 'ship', 'review', '
|
|
44
|
-
standard: Object.freeze(['plan', 'dev', 'validate', 'ship', 'review'
|
|
45
|
-
refactor: Object.freeze(['plan', 'dev', 'validate', 'ship'
|
|
40
|
+
critical: Object.freeze(['plan', 'dev', 'validate', 'ship', 'review', 'verify']),
|
|
41
|
+
standard: Object.freeze(['plan', 'dev', 'validate', 'ship', 'review']),
|
|
42
|
+
refactor: Object.freeze(['plan', 'dev', 'validate', 'ship']),
|
|
46
43
|
simple: Object.freeze(['dev', 'validate', 'ship']),
|
|
47
44
|
hotfix: Object.freeze(['dev', 'validate', 'ship']),
|
|
48
45
|
// Docs-only work intentionally reuses /verify as a pre-ship content check to
|
|
@@ -51,6 +48,18 @@ const WORKFLOW_STAGE_MATRIX = Object.freeze({
|
|
|
51
48
|
docs: Object.freeze(['verify', 'ship']),
|
|
52
49
|
});
|
|
53
50
|
|
|
51
|
+
// Pre-merge is a task-type gate/checkpoint embedded inside existing stages
|
|
52
|
+
// (the doc-completion + PR-handoff checks that run before merge), not a
|
|
53
|
+
// standalone universal workflow stage. It is keyed with a hyphen ('pre-merge')
|
|
54
|
+
// so it never re-enters the stage model. `enabledFor` lists the classifications
|
|
55
|
+
// that run the gate; `embeddedIn` names the stages where it fires.
|
|
56
|
+
const WORKFLOW_GATES = Object.freeze({
|
|
57
|
+
'pre-merge': Object.freeze({
|
|
58
|
+
embeddedIn: Object.freeze(['ship', 'review']),
|
|
59
|
+
enabledFor: Object.freeze(['critical', 'standard', 'refactor']),
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
|
|
54
63
|
const WORKFLOW_TERMINAL_STAGES = Object.freeze(Object.entries(WORKFLOW_STAGE_MATRIX).reduce((accumulator, [classification, path]) => {
|
|
55
64
|
accumulator[classification] = path.at(-1);
|
|
56
65
|
return accumulator;
|
|
@@ -77,6 +86,19 @@ function getWorkflowPath(classification) {
|
|
|
77
86
|
return normalized ? WORKFLOW_STAGE_MATRIX[normalized] : Object.freeze([]);
|
|
78
87
|
}
|
|
79
88
|
|
|
89
|
+
function getGatesForClassification(classification) {
|
|
90
|
+
const normalized = normalizeClassification(classification);
|
|
91
|
+
if (!normalized) {
|
|
92
|
+
return Object.freeze([]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return Object.freeze(
|
|
96
|
+
Object.entries(WORKFLOW_GATES)
|
|
97
|
+
.filter(([, gate]) => gate.enabledFor.includes(normalized))
|
|
98
|
+
.map(([gateId]) => gateId),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
80
102
|
function getStageWorkflow(stageId, classification) {
|
|
81
103
|
const normalizedStage = normalizeStageId(stageId);
|
|
82
104
|
const normalizedClassification = normalizeClassification(classification);
|
|
@@ -186,6 +208,7 @@ module.exports = {
|
|
|
186
208
|
STAGE_LABELS,
|
|
187
209
|
STAGE_COMMANDS,
|
|
188
210
|
WORKFLOW_STAGE_MATRIX,
|
|
211
|
+
WORKFLOW_GATES,
|
|
189
212
|
WORKFLOW_TERMINAL_STAGES,
|
|
190
213
|
STAGE_TRANSITIONS,
|
|
191
214
|
STAGE_MODEL,
|
|
@@ -193,6 +216,7 @@ module.exports = {
|
|
|
193
216
|
normalizeStageId,
|
|
194
217
|
isCanonicalStageId,
|
|
195
218
|
getWorkflowPath,
|
|
219
|
+
getGatesForClassification,
|
|
196
220
|
getStageWorkflow,
|
|
197
221
|
getAllowedTransitions,
|
|
198
222
|
canTransition,
|