forge-workflow 0.0.9 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +3 -0
- package/.forge/hooks/forge-native-hook.js +245 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -106
- package/QUICKSTART.md +171 -0
- package/README.md +271 -363
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +466 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +118 -0
- package/docs/guides/SUPPORT.md +185 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +205 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/reference/TOOLCHAIN.md +658 -0
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +115 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1042 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +126 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-setup.js +225 -28
- package/lib/beads-sync-scaffold.js +36 -107
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +744 -70
- package/lib/commands/_manifest.js +91 -0
- package/lib/commands/_registry.js +85 -34
- package/lib/commands/_resolve-command-opts.js +261 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +192 -0
- package/lib/commands/hooks.js +242 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +598 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +66 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +523 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +436 -24
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +69 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +116 -0
- package/lib/commands/recap.js +61 -0
- package/lib/commands/recommend.js +22 -2
- package/lib/commands/release.js +91 -0
- package/lib/commands/remember.js +74 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +851 -979
- package/lib/commands/shepherd.js +436 -0
- package/lib/commands/ship.js +23 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +329 -11
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +15 -2
- package/lib/commands/test.js +58 -7
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +56 -25
- package/lib/commands/worktree.js +308 -128
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +946 -0
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +42 -17
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +697 -0
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +193 -0
- package/lib/global-flags.js +74 -0
- package/lib/greptile-match.js +7 -63
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +451 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/kernel/backing-issue.js +305 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2504 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +8 -4
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +157 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1273 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +166 -0
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2089 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +332 -23
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +191 -0
- package/lib/status/beads-snapshot.js +145 -0
- package/lib/status/presenter.js +216 -0
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +199 -0
- package/lib/workflow/enforce-stage.js +298 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +159 -14
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +46 -36
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +284 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +64 -232
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +144 -47
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +183 -79
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +108 -0
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +117 -369
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +251 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +302 -28
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +46 -52
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +134 -159
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/skills/ship/SKILL.md +251 -0
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/skills/status/SKILL.md +102 -0
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/ship.prompt.md +0 -215
- package/.github/prompts/status.prompt.md +0 -92
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -56
- package/.github/workflows/github-to-beads.yml +0 -97
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -255
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-health-check.js +0 -143
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -567
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -332
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -78
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module merge-rules
|
|
5
|
+
*
|
|
6
|
+
* PURE, network-free evaluator for the opt-in conditional auto-merge RULES
|
|
7
|
+
* ENGINE (the vision-roadmap "Candidate feature" that promotes the proven
|
|
8
|
+
* `settle-merge.sh` baseline to a native, config-driven forge capability).
|
|
9
|
+
*
|
|
10
|
+
* `evaluateMergeRules(prContext, rules)` decides whether a merge is ALLOWED by
|
|
11
|
+
* ANDing a list of composable rules over an already-fetched PR context. It does
|
|
12
|
+
* NO I/O: the caller fetches the PR context (checks, threads, approvals,
|
|
13
|
+
* comments, timestamps) and passes it in, which keeps this the single
|
|
14
|
+
* unit-testable decision surface. The command layer (`lib/commands/merge.js`)
|
|
15
|
+
* owns the gh-fetch and the merge action, and only ever runs this when the user
|
|
16
|
+
* has explicitly opted in — so the never-auto-merge-by-default invariant holds.
|
|
17
|
+
*
|
|
18
|
+
* Fail-closed is the guiding principle: an unknown rule type, a malformed rule,
|
|
19
|
+
* or a piece of context we cannot read is surfaced as UNMET, never silently
|
|
20
|
+
* satisfied. A merge only proceeds when every rule is affirmatively met.
|
|
21
|
+
*
|
|
22
|
+
* PR context shape (all fields optional; absence is treated fail-closed by the
|
|
23
|
+
* rules that need them):
|
|
24
|
+
* - checks: Array<{ name, conclusion }> CI check rollup
|
|
25
|
+
* - requiredChecksKnown: boolean is the required set readable?
|
|
26
|
+
* - unresolvedThreads: number | Array open review threads
|
|
27
|
+
* - behindBase: number | boolean commits behind base
|
|
28
|
+
* - approvals: Array<string | { author }> approving reviewers
|
|
29
|
+
* - comments: Array<{ author, at }> PR/review comments
|
|
30
|
+
* - lastActivityAt: number | string (epoch ms or ISO)
|
|
31
|
+
* - now: number | string (epoch ms or ISO)
|
|
32
|
+
* - conflicting: boolean true if the branch conflicts with base
|
|
33
|
+
* - isDraft: boolean true if the PR is a draft
|
|
34
|
+
* - state: string (OPEN | MERGED | CLOSED) read by the command pre-flight, not a rule
|
|
35
|
+
*
|
|
36
|
+
* Built-in rule types (compose as a list, ANDed by default):
|
|
37
|
+
* checks_green | threads_resolved | not_behind | no_conflicts | not_draft |
|
|
38
|
+
* min_approvals:N | settle_min:N | idle_min:N | last_comment_by:X |
|
|
39
|
+
* approved_by:[..] | not_commented_by:[..]
|
|
40
|
+
* `checks_green` is bare (ALL checks must be green) or scoped:
|
|
41
|
+
* { checks_green: { ignore: ["Coverage"] } } exempt the named checks
|
|
42
|
+
* { checks_green: { only: ["build"] } } require ONLY the named checks
|
|
43
|
+
* Composition wrappers:
|
|
44
|
+
* { any_of: [rule, ...] } passes if ANY member passes
|
|
45
|
+
* { not: rule } passes if the inner rule FAILS
|
|
46
|
+
*
|
|
47
|
+
* A custom-predicate seam (bring-your-own script via `forge add`) is a
|
|
48
|
+
* documented follow-up and intentionally NOT implemented here.
|
|
49
|
+
*
|
|
50
|
+
* Follow-ups (documented, NOT built here): an opt-in `auto_update` executor
|
|
51
|
+
* (update-branch when behind → wait for CI → re-check → merge); required-checks
|
|
52
|
+
* SCOPING for `checks_green` (read the branch-protection required set instead of
|
|
53
|
+
* requiring every rollup check green); a configurable merge `method`
|
|
54
|
+
* (squash | merge | rebase); and post-merge branch deletion.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
const SUCCESS_CONCLUSIONS = new Set(['SUCCESS', 'NEUTRAL', 'SKIPPED', 'PASS', 'PASSED']);
|
|
58
|
+
const MS_PER_MIN = 60_000;
|
|
59
|
+
|
|
60
|
+
/** Coerce an epoch-ms number, a Date, or an ISO string to epoch ms (NaN if unparseable). */
|
|
61
|
+
function toMs(value) {
|
|
62
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
63
|
+
if (value instanceof Date) return value.getTime();
|
|
64
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
65
|
+
const parsed = Date.parse(value);
|
|
66
|
+
return Number.isNaN(parsed) ? NaN : parsed;
|
|
67
|
+
}
|
|
68
|
+
return NaN;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Normalize an actor reference (string or `{ author | login | user | actor }`) to a lowercased login. */
|
|
72
|
+
function normLogin(entry) {
|
|
73
|
+
if (typeof entry === 'string') return entry.trim().toLowerCase();
|
|
74
|
+
if (entry && typeof entry === 'object') {
|
|
75
|
+
const login = entry.author ?? entry.login ?? entry.user ?? entry.actor ?? '';
|
|
76
|
+
return String(login).trim().toLowerCase();
|
|
77
|
+
}
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Coerce a rule argument to a lowercased list of accounts. */
|
|
82
|
+
function toAccountList(arg) {
|
|
83
|
+
if (Array.isArray(arg)) return arg.map((x) => String(x).trim().toLowerCase()).filter(Boolean);
|
|
84
|
+
if (arg === undefined || arg === null || arg === '') return [];
|
|
85
|
+
return [String(arg).trim().toLowerCase()];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A CI check is green if its conclusion is a success-class conclusion. */
|
|
89
|
+
function isCheckGreen(check) {
|
|
90
|
+
if (!check || typeof check !== 'object') return false;
|
|
91
|
+
const conclusion = String(check.conclusion ?? check.state ?? check.status ?? '').toUpperCase();
|
|
92
|
+
return SUCCESS_CONCLUSIONS.has(conclusion);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The most recent comment by timestamp; ties and unparseable stamps fall back to array order. */
|
|
96
|
+
function lastComment(prContext) {
|
|
97
|
+
const comments = Array.isArray(prContext.comments) ? prContext.comments : [];
|
|
98
|
+
let latest = null;
|
|
99
|
+
let latestRank = -Infinity;
|
|
100
|
+
for (const comment of comments) {
|
|
101
|
+
const ms = toMs(comment && comment.at);
|
|
102
|
+
const rank = Number.isNaN(ms) ? -Infinity : ms;
|
|
103
|
+
if (rank >= latestRank) {
|
|
104
|
+
latestRank = rank;
|
|
105
|
+
latest = comment;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return latest;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Lowercased logins of every approving reviewer. */
|
|
112
|
+
function approverLogins(prContext) {
|
|
113
|
+
const approvals = Array.isArray(prContext.approvals) ? prContext.approvals : [];
|
|
114
|
+
return approvals.map(normLogin).filter(Boolean);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const pass = () => ({ ok: true });
|
|
118
|
+
const fail = (reason) => ({ ok: false, reason });
|
|
119
|
+
|
|
120
|
+
// --- Built-in rule predicates. Each returns { ok } or { ok:false, reason }. ---
|
|
121
|
+
|
|
122
|
+
function checkName(check) {
|
|
123
|
+
return String((check && check.name) || '');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* `checks_green` — every CI check must be green per `isCheckGreen`. Optionally scoped:
|
|
128
|
+
* - bare string / no arg → ALL checks must be green (strict default).
|
|
129
|
+
* - { ignore: ["A","B"] } → the named checks are EXEMPT; every OTHER check
|
|
130
|
+
* must be green. `ignore: []` is identical to the bare form.
|
|
131
|
+
* - { only: ["A"] } → ONLY the named checks must be green (present AND
|
|
132
|
+
* green per `isCheckGreen`); all others are ignored.
|
|
133
|
+
* `ignore` + `only` together is malformed → fail-closed. Exact name match.
|
|
134
|
+
* (`required_only` — scope to the branch-protection required set — is a
|
|
135
|
+
* documented follow-up, not built here.)
|
|
136
|
+
*/
|
|
137
|
+
function checksGreen(ctx, arg) {
|
|
138
|
+
let ignore = [];
|
|
139
|
+
let only = null;
|
|
140
|
+
|
|
141
|
+
if (arg !== undefined && arg !== null) {
|
|
142
|
+
if (typeof arg !== 'object' || Array.isArray(arg)) {
|
|
143
|
+
return fail('checks_green argument must be an object with `ignore` or `only` (fail-closed).');
|
|
144
|
+
}
|
|
145
|
+
const hasIgnore = arg.ignore !== undefined;
|
|
146
|
+
const hasOnly = arg.only !== undefined;
|
|
147
|
+
if (hasIgnore && hasOnly) {
|
|
148
|
+
return fail('checks_green cannot combine `ignore` and `only` — choose one (malformed, fail-closed).');
|
|
149
|
+
}
|
|
150
|
+
if (hasIgnore) {
|
|
151
|
+
if (!Array.isArray(arg.ignore)) return fail('checks_green `ignore` must be an array of check names (fail-closed).');
|
|
152
|
+
ignore = arg.ignore.map(String);
|
|
153
|
+
}
|
|
154
|
+
if (hasOnly) {
|
|
155
|
+
if (!Array.isArray(arg.only)) return fail('checks_green `only` must be an array of check names (fail-closed).');
|
|
156
|
+
if (arg.only.length === 0) return fail('checks_green `only` names no checks (fail-closed).');
|
|
157
|
+
only = arg.only.map(String);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (ctx.requiredChecksKnown !== true) {
|
|
162
|
+
return fail('required-check set is not known to be readable — cannot confirm green (fail-closed).');
|
|
163
|
+
}
|
|
164
|
+
if (!Array.isArray(ctx.checks)) {
|
|
165
|
+
return fail('check results are missing from the PR context (fail-closed).');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (only) {
|
|
169
|
+
// Only the named checks matter — each must be PRESENT and green.
|
|
170
|
+
const byName = new Map(ctx.checks.map((c) => [checkName(c), c]));
|
|
171
|
+
const problems = only.map((name) => {
|
|
172
|
+
const check = byName.get(name);
|
|
173
|
+
if (!check) return `${name} (missing)`;
|
|
174
|
+
if (!isCheckGreen(check)) return `${name} (${check.conclusion || 'not green'})`;
|
|
175
|
+
return null;
|
|
176
|
+
}).filter(Boolean);
|
|
177
|
+
if (problems.length > 0) return fail(`required check(s) not green: ${problems.join(', ')}.`);
|
|
178
|
+
return pass();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Default / ignore: every check EXCEPT the exempt ones must be green.
|
|
182
|
+
const exempt = new Set(ignore);
|
|
183
|
+
const notGreen = ctx.checks.filter((c) => !exempt.has(checkName(c)) && !isCheckGreen(c));
|
|
184
|
+
if (notGreen.length > 0) {
|
|
185
|
+
const names = notGreen.map((c) => (c && c.name) || '?').join(', ');
|
|
186
|
+
return fail(`${notGreen.length} check(s) not green: ${names}.`);
|
|
187
|
+
}
|
|
188
|
+
return pass();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function threadsResolved(ctx) {
|
|
192
|
+
const unresolved = ctx.unresolvedThreads;
|
|
193
|
+
if (typeof unresolved === 'number') {
|
|
194
|
+
return unresolved > 0 ? fail(`${unresolved} unresolved review thread(s).`) : pass();
|
|
195
|
+
}
|
|
196
|
+
if (Array.isArray(unresolved)) {
|
|
197
|
+
return unresolved.length > 0 ? fail(`${unresolved.length} unresolved review thread(s).`) : pass();
|
|
198
|
+
}
|
|
199
|
+
return fail('unresolved-thread count is unknown (fail-closed).');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function notBehind(ctx) {
|
|
203
|
+
const behind = ctx.behindBase;
|
|
204
|
+
if (typeof behind === 'boolean') return behind ? fail('branch is behind its base.') : pass();
|
|
205
|
+
if (typeof behind === 'number') {
|
|
206
|
+
return behind > 0 ? fail(`branch is ${behind} commit(s) behind base.`) : pass();
|
|
207
|
+
}
|
|
208
|
+
return fail('behind-base status is unknown (fail-closed).');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function noConflicts(ctx) {
|
|
212
|
+
const conflicting = ctx.conflicting;
|
|
213
|
+
if (conflicting === false) return pass();
|
|
214
|
+
if (conflicting === true) return fail('branch has merge conflicts with base.');
|
|
215
|
+
// undefined/unknown mergeability → never merge blind.
|
|
216
|
+
return fail('conflict status is unknown (fail-closed).');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function notDraft(ctx) {
|
|
220
|
+
const isDraft = ctx.isDraft;
|
|
221
|
+
if (isDraft === false) return pass();
|
|
222
|
+
if (isDraft === true) return fail('PR is a draft.');
|
|
223
|
+
return fail('draft status is unknown (fail-closed).');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function minApprovals(ctx, arg) {
|
|
227
|
+
const need = Number.parseInt(arg, 10);
|
|
228
|
+
if (!Number.isFinite(need)) return fail('min_approvals requires a numeric argument, e.g. "min_approvals:2".');
|
|
229
|
+
const count = approverLogins(ctx).length;
|
|
230
|
+
return count >= need ? pass() : fail(`only ${count} approval(s); need ${need}.`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function settleMin(ctx, arg) {
|
|
234
|
+
if (arg === null || arg === undefined || arg === '') return fail('settle_min requires a numeric minute argument (missing → fail-closed).');
|
|
235
|
+
const need = Number(arg);
|
|
236
|
+
if (!Number.isFinite(need) || need < 0) return fail('settle_min requires a non-negative numeric minute argument.');
|
|
237
|
+
const nowMs = toMs(ctx.now);
|
|
238
|
+
if (Number.isNaN(nowMs)) return fail('current time (now) is missing/unparseable (fail-closed).');
|
|
239
|
+
const lc = lastComment(ctx);
|
|
240
|
+
if (!lc) return pass(); // nothing has been said → nothing to settle
|
|
241
|
+
const atMs = toMs(lc.at);
|
|
242
|
+
if (Number.isNaN(atMs)) return fail('last comment timestamp is unparseable (fail-closed).');
|
|
243
|
+
const ageMin = (nowMs - atMs) / MS_PER_MIN;
|
|
244
|
+
return ageMin >= need ? pass() : fail(`last comment was ${ageMin.toFixed(1)}m ago; need >= ${need}m of quiet.`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function idleMin(ctx, arg) {
|
|
248
|
+
if (arg === null || arg === undefined || arg === '') return fail('idle_min requires a numeric minute argument (missing → fail-closed).');
|
|
249
|
+
const need = Number(arg);
|
|
250
|
+
if (!Number.isFinite(need) || need < 0) return fail('idle_min requires a non-negative numeric minute argument.');
|
|
251
|
+
const nowMs = toMs(ctx.now);
|
|
252
|
+
if (Number.isNaN(nowMs)) return fail('current time (now) is missing/unparseable (fail-closed).');
|
|
253
|
+
const activityMs = toMs(ctx.lastActivityAt);
|
|
254
|
+
if (Number.isNaN(activityMs)) return fail('lastActivityAt is missing/unparseable (fail-closed).');
|
|
255
|
+
const ageMin = (nowMs - activityMs) / MS_PER_MIN;
|
|
256
|
+
return ageMin >= need ? pass() : fail(`last activity was ${ageMin.toFixed(1)}m ago; need >= ${need}m idle.`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function lastCommentBy(ctx, arg) {
|
|
260
|
+
const allowed = toAccountList(arg);
|
|
261
|
+
if (allowed.length === 0) return fail('last_comment_by requires at least one account.');
|
|
262
|
+
const lc = lastComment(ctx);
|
|
263
|
+
if (!lc) return fail(`no comments yet; last comment is not by ${allowed.join('/')}.`);
|
|
264
|
+
const author = normLogin(lc);
|
|
265
|
+
return allowed.includes(author) ? pass() : fail(`last comment is by "${author}", not ${allowed.join('/')}.`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function approvedBy(ctx, arg) {
|
|
269
|
+
const required = toAccountList(arg);
|
|
270
|
+
if (required.length === 0) return fail('approved_by requires at least one account.');
|
|
271
|
+
const approvers = new Set(approverLogins(ctx));
|
|
272
|
+
const missing = required.filter((account) => !approvers.has(account));
|
|
273
|
+
return missing.length === 0 ? pass() : fail(`missing approval from: ${missing.join(', ')}.`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function notCommentedBy(ctx, arg) {
|
|
277
|
+
const blocked = toAccountList(arg);
|
|
278
|
+
if (blocked.length === 0) return fail('not_commented_by requires at least one account.');
|
|
279
|
+
const lc = lastComment(ctx);
|
|
280
|
+
if (!lc) return pass(); // no comments → no blocked account had the last word
|
|
281
|
+
const author = normLogin(lc);
|
|
282
|
+
return blocked.includes(author)
|
|
283
|
+
? fail(`the most recent comment is by "${author}", which is blocked from having the last word.`)
|
|
284
|
+
: pass();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function anyOf(ctx, arg) {
|
|
288
|
+
if (!Array.isArray(arg) || arg.length === 0) {
|
|
289
|
+
return fail('any_of requires a non-empty list of member rules (fail-closed).');
|
|
290
|
+
}
|
|
291
|
+
const memberReasons = [];
|
|
292
|
+
for (const member of arg) {
|
|
293
|
+
const res = evalRule(ctx, member);
|
|
294
|
+
if (res.ok) return pass();
|
|
295
|
+
memberReasons.push(`${res.label}: ${res.reason}`);
|
|
296
|
+
}
|
|
297
|
+
return fail(`none of the any_of members passed — ${memberReasons.join(' | ')}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function notWrapper(ctx, arg) {
|
|
301
|
+
if (arg === undefined || arg === null) return fail('not requires an inner rule (fail-closed).');
|
|
302
|
+
const res = evalRule(ctx, arg);
|
|
303
|
+
return res.ok
|
|
304
|
+
? fail(`inner rule "${res.label}" passed, but "not" requires it to fail.`)
|
|
305
|
+
: pass();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const PREDICATES = {
|
|
309
|
+
checks_green: (ctx, arg) => checksGreen(ctx, arg),
|
|
310
|
+
threads_resolved: (ctx) => threadsResolved(ctx),
|
|
311
|
+
not_behind: (ctx) => notBehind(ctx),
|
|
312
|
+
no_conflicts: (ctx) => noConflicts(ctx),
|
|
313
|
+
not_draft: (ctx) => notDraft(ctx),
|
|
314
|
+
min_approvals: (ctx, arg) => minApprovals(ctx, arg),
|
|
315
|
+
settle_min: (ctx, arg) => settleMin(ctx, arg),
|
|
316
|
+
idle_min: (ctx, arg) => idleMin(ctx, arg),
|
|
317
|
+
last_comment_by: (ctx, arg) => lastCommentBy(ctx, arg),
|
|
318
|
+
approved_by: (ctx, arg) => approvedBy(ctx, arg),
|
|
319
|
+
not_commented_by: (ctx, arg) => notCommentedBy(ctx, arg),
|
|
320
|
+
any_of: (ctx, arg) => anyOf(ctx, arg),
|
|
321
|
+
not: (ctx, arg) => notWrapper(ctx, arg),
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/** Produce a stable label for an unmet-list entry from a rule's type + scalar arg. */
|
|
325
|
+
function scalarLabel(type, arg) {
|
|
326
|
+
if (arg === undefined || arg === null || Array.isArray(arg) || typeof arg === 'object') return type;
|
|
327
|
+
return `${type}:${arg}`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Normalize a rule (string `"type"` / `"type:arg"` or single-key object
|
|
332
|
+
* `{ type: arg }`) to `{ type, arg, label, malformed }`.
|
|
333
|
+
*/
|
|
334
|
+
function normalizeRule(rule) {
|
|
335
|
+
if (typeof rule === 'string') {
|
|
336
|
+
const idx = rule.indexOf(':');
|
|
337
|
+
if (idx === -1) return { type: rule.trim(), arg: undefined, label: rule.trim() };
|
|
338
|
+
const type = rule.slice(0, idx).trim();
|
|
339
|
+
const arg = rule.slice(idx + 1).trim();
|
|
340
|
+
return { type, arg, label: rule.trim() };
|
|
341
|
+
}
|
|
342
|
+
if (rule && typeof rule === 'object' && !Array.isArray(rule)) {
|
|
343
|
+
const keys = Object.keys(rule);
|
|
344
|
+
if (keys.length !== 1) {
|
|
345
|
+
return { type: null, arg: undefined, label: `object(${keys.join(',') || 'empty'})`, malformed: true };
|
|
346
|
+
}
|
|
347
|
+
const type = keys[0];
|
|
348
|
+
return { type, arg: rule[type], label: scalarLabel(type, rule[type]) };
|
|
349
|
+
}
|
|
350
|
+
return { type: null, arg: undefined, label: String(rule), malformed: true };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Evaluate a single rule against the context. Returns `{ ok, reason, label }`. */
|
|
354
|
+
function evalRule(prContext, rule) {
|
|
355
|
+
const { type, arg, label, malformed } = normalizeRule(rule);
|
|
356
|
+
let res;
|
|
357
|
+
if (malformed || !type) {
|
|
358
|
+
res = fail('malformed rule: expected a string or a single-key object (exactly one key).');
|
|
359
|
+
} else if (Object.prototype.hasOwnProperty.call(PREDICATES, type)) {
|
|
360
|
+
res = PREDICATES[type](prContext, arg);
|
|
361
|
+
} else {
|
|
362
|
+
res = fail(`unknown rule type "${type}" (fail-closed).`);
|
|
363
|
+
}
|
|
364
|
+
return { ok: res.ok, reason: res.reason, label };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Evaluate a list of merge rules over a PR context.
|
|
369
|
+
*
|
|
370
|
+
* Rules are ANDed: a merge is ALLOWED only when every rule is met. An empty (or
|
|
371
|
+
* non-array) rule list is vacuously allowed — the command layer separately
|
|
372
|
+
* requires `merge.auto.enabled === true` before ever calling this, which is
|
|
373
|
+
* what preserves the opt-in / never-auto-merge-by-default invariant.
|
|
374
|
+
*
|
|
375
|
+
* @param {object} prContext - Already-fetched PR context (no I/O performed here).
|
|
376
|
+
* @param {Array} rules - Composable rule list.
|
|
377
|
+
* @returns {{ allowed: boolean, unmet: Array<{ rule: string, reason: string }> }}
|
|
378
|
+
*/
|
|
379
|
+
function evaluateMergeRules(prContext, rules) {
|
|
380
|
+
const context = prContext && typeof prContext === 'object' ? prContext : {};
|
|
381
|
+
const list = Array.isArray(rules) ? rules : [];
|
|
382
|
+
const unmet = [];
|
|
383
|
+
for (const rule of list) {
|
|
384
|
+
const res = evalRule(context, rule);
|
|
385
|
+
if (!res.ok) unmet.push({ rule: res.label, reason: res.reason });
|
|
386
|
+
}
|
|
387
|
+
return { allowed: unmet.length === 0, unmet };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
module.exports = {
|
|
391
|
+
evaluateMergeRules,
|
|
392
|
+
// Exported for focused testing / reuse; not part of the stable public surface.
|
|
393
|
+
isCheckGreen,
|
|
394
|
+
normalizeRule,
|
|
395
|
+
};
|