forge-workflow 0.0.10 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +3 -0
- package/.forge/hooks/forge-native-hook.js +245 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +150 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -118
- package/QUICKSTART.md +171 -0
- package/README.md +271 -363
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +461 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/forge/TOOLCHAIN.md +670 -0
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +118 -0
- package/docs/guides/SUPPORT.md +185 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +205 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +115 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1042 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +126 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-setup.js +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +741 -77
- package/lib/commands/_manifest.js +91 -0
- package/lib/commands/_registry.js +85 -34
- package/lib/commands/_resolve-command-opts.js +261 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +192 -0
- package/lib/commands/hooks.js +242 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +598 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +17 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +523 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +436 -24
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +69 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +116 -0
- package/lib/commands/recap.js +61 -0
- package/lib/commands/recommend.js +0 -1
- package/lib/commands/release.js +91 -0
- package/lib/commands/remember.js +74 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +838 -972
- package/lib/commands/shepherd.js +436 -0
- package/lib/commands/ship.js +23 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +158 -21
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +4 -1
- package/lib/commands/test.js +43 -27
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +43 -18
- package/lib/commands/worktree.js +307 -100
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +946 -0
- package/lib/dep-guard/keyword-ripple.js +2 -2
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +35 -9
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +382 -11
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +193 -0
- package/lib/global-flags.js +74 -0
- package/lib/greptile-match.js +7 -63
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +451 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/kernel/backing-issue.js +305 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2504 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +3 -2
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +157 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1273 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +99 -497
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2089 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +241 -20
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/scoring.js +17 -3
- package/lib/status/beads-snapshot.js +45 -2
- package/lib/status/presenter.js +169 -18
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +199 -0
- package/lib/workflow/enforce-stage.js +296 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +11 -22
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +37 -35
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +81 -57
- package/scripts/beads-upgrade-smoke.sh +24 -3
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard.sh +22 -3
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +49 -84
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +50 -83
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +21 -94
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status.sh +60 -57
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +13 -6
- package/scripts/test.js +95 -12
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -89
- package/.github/workflows/github-to-beads.yml +0 -100
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -281
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-bootstrap.js +0 -225
- package/lib/beads-health-check.js +0 -188
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -584
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -356
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -132
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* shepherd command — one bounded monitor pass over a pull request.
|
|
5
|
+
*
|
|
6
|
+
* `forge shepherd <pr> [--auto-rebase]` reads PR/CI state, takes at most one
|
|
7
|
+
* idempotent Tier-A action (rerun a flaky required check), and exits with a
|
|
8
|
+
* terminal state. It NEVER merges and NEVER resolves review threads. A
|
|
9
|
+
* `--watch` loop, if desired, lives in an external scheduler that re-invokes
|
|
10
|
+
* this command on an interval — there is no in-process polling loop here.
|
|
11
|
+
*
|
|
12
|
+
* `forge shepherd <pr> --bundle --json` instead prints the COMPLETE read-only
|
|
13
|
+
* PR-state bundle (all unresolved threads, merge state, CI, divergence,
|
|
14
|
+
* predicted conflicts) the monitor will hand to a fixer-agent. It still decides
|
|
15
|
+
* nothing and takes no action.
|
|
16
|
+
*
|
|
17
|
+
* `forge shepherd <pr> --pull --json` prints a COMPACT, bounded "why it failed +
|
|
18
|
+
* what to fix" payload: per-failed-check log excerpts (matrix-deduped) plus the
|
|
19
|
+
* unresolved review-thread fix-list (CodeRabbit included), alongside the decision
|
|
20
|
+
* state. All the `gh pr checks` / `gh run view --log-failed` / GraphQL work is
|
|
21
|
+
* done IN CODE so an agent gets one payload instead of running those by hand. It
|
|
22
|
+
* still NEVER merges and NEVER resolves threads.
|
|
23
|
+
*
|
|
24
|
+
* State persists via GitHub PR comments/labels and git only.
|
|
25
|
+
*
|
|
26
|
+
* @module commands/shepherd
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const { execFileSync } = require('node:child_process');
|
|
30
|
+
|
|
31
|
+
const { runShepherdPass } = require('../pr-shepherd');
|
|
32
|
+
const { gatherPrBundle } = require('../pr-bundle');
|
|
33
|
+
const { gatherPullSignal, renderPullSummary } = require('../pr-pull');
|
|
34
|
+
const { PrStateAdapter } = require('../adapters/pr-state-adapter');
|
|
35
|
+
const { validatePrStateAdapter } = require('../pr-state-validator');
|
|
36
|
+
const { gatherMonitorSnapshot } = require('../pr-monitor/gather');
|
|
37
|
+
const { pollEvents } = require('../pr-monitor/monitor');
|
|
38
|
+
const { watchLoop } = require('../pr-monitor/watch');
|
|
39
|
+
const monitorJournal = require('../pr-monitor/journal');
|
|
40
|
+
const { EVENT_TYPES: T } = require('../pr-monitor/events');
|
|
41
|
+
|
|
42
|
+
const DEFAULT_RERUN_BUDGET = 3;
|
|
43
|
+
|
|
44
|
+
const defaultGhRunner = (cmd, a) => execFileSync(cmd, a, { encoding: 'utf8', timeout: 30000 });
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolve owner/repo and base branch for the shepherd pass.
|
|
48
|
+
*
|
|
49
|
+
* The base branch is read from the PR itself (`gh pr view <pr> --json
|
|
50
|
+
* baseRefName`) rather than the current checkout's default branch, so PRs
|
|
51
|
+
* targeting `release/*`/`develop` are evaluated against the correct branch.
|
|
52
|
+
* `owner`/`name` come from the repository the PR is queried in — that IS the
|
|
53
|
+
* base repository. `cwd` (the worktree root) is threaded through so divergence
|
|
54
|
+
* is computed against the right checkout.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} deps
|
|
57
|
+
* @returns {Promise<{ pr: string, owner: string, repo: string, base: string, baseRef: string, cwd?: string }>}
|
|
58
|
+
*/
|
|
59
|
+
async function defaultBuildContext({ pr, gh, git, projectRoot }) {
|
|
60
|
+
const prJson = gh('gh', ['pr', 'view', String(pr), '--json', 'baseRefName']);
|
|
61
|
+
const prInfo = JSON.parse(prJson || '{}');
|
|
62
|
+
const base = prInfo.baseRefName || 'master';
|
|
63
|
+
|
|
64
|
+
const repoJson = gh('gh', ['repo', 'view', '--json', 'owner,name']);
|
|
65
|
+
const repo = JSON.parse(repoJson || '{}');
|
|
66
|
+
const owner = repo.owner?.login || '';
|
|
67
|
+
const name = repo.name || '';
|
|
68
|
+
|
|
69
|
+
let baseRemote;
|
|
70
|
+
try {
|
|
71
|
+
baseRemote = git('git', ['remote']).split(/\s+/).filter(Boolean)[0] || 'origin';
|
|
72
|
+
} catch (_err) {
|
|
73
|
+
baseRemote = 'origin';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
pr: String(pr),
|
|
78
|
+
owner,
|
|
79
|
+
repo: name,
|
|
80
|
+
base,
|
|
81
|
+
baseRef: `${baseRemote}/${base}`,
|
|
82
|
+
...(projectRoot ? { cwd: projectRoot } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Detect whether the working tree is clean (precondition for --auto-rebase).
|
|
88
|
+
*
|
|
89
|
+
* @param {Function} git
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
function isWorkingTreeClean(git) {
|
|
93
|
+
try {
|
|
94
|
+
return git('git', ['status', '--porcelain']).trim().length === 0;
|
|
95
|
+
} catch (_err) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Render a single pass action for the human-readable monitor line. Strings pass
|
|
101
|
+
// through; everything else is JSON-encoded, but JSON.stringify can throw on
|
|
102
|
+
// circular refs or BigInt, so surface the reason inline rather than crash the pass.
|
|
103
|
+
function formatAction(action) {
|
|
104
|
+
if (typeof action === 'string') {
|
|
105
|
+
return action;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return JSON.stringify(action);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
return `[unprintable action: ${err.message}]`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build the DEFAULT `check.failed` enrichment hook for the events pull surface.
|
|
116
|
+
*
|
|
117
|
+
* The monitor design specifies that newly-failed checks are enriched with their
|
|
118
|
+
* failure log excerpts before the journal append. `pollEvents` accepts an
|
|
119
|
+
* `enrich` hook but `handleEvents` must supply the default one, or a plain
|
|
120
|
+
* `forge shepherd events` call would emit bare `check.failed` events with no
|
|
121
|
+
* `data.excerpt` (only direct monitor callers could attach them).
|
|
122
|
+
*
|
|
123
|
+
* The hook is BEST-EFFORT: it fetches the compact pull signal ONCE (only when a
|
|
124
|
+
* pass actually produced a `check.failed`), maps excerpts by check name, and
|
|
125
|
+
* decorates matching records. Any failure to gather excerpts leaves the events
|
|
126
|
+
* intact rather than aborting the pass — enrichment must never block the journal.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} pullCtx - ctx forwarded to `gatherPull` (owner/repo/base/adapter/runGh/self).
|
|
129
|
+
* @returns {(records: object[]) => Promise<void>}
|
|
130
|
+
*/
|
|
131
|
+
function makeCheckFailureEnricher(pullCtx) {
|
|
132
|
+
const gatherPull = pullCtx.gatherPull || gatherPullSignal;
|
|
133
|
+
return async (records) => {
|
|
134
|
+
if (!Array.isArray(records) || !records.some((r) => r.type === T.CHECK_FAILED)) return;
|
|
135
|
+
let failures;
|
|
136
|
+
try {
|
|
137
|
+
const pull = await gatherPull(pullCtx);
|
|
138
|
+
failures = Array.isArray(pull?.failures) ? pull.failures : [];
|
|
139
|
+
} catch (err) {
|
|
140
|
+
// best-effort: never let enrichment abort the pass, but surface the reason
|
|
141
|
+
console.error(`[shepherd] check-failure enrichment skipped: ${err.message}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const byName = new Map(failures.map((f) => [f.name, f]));
|
|
145
|
+
for (const r of records) {
|
|
146
|
+
if (r.type !== T.CHECK_FAILED) continue;
|
|
147
|
+
const f = byName.get(r.data?.name);
|
|
148
|
+
if (!f) continue;
|
|
149
|
+
if (f.excerpt) r.data.excerpt = f.excerpt;
|
|
150
|
+
if (f.jobUrl) r.data.jobUrl = f.jobUrl;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Parse `--since <seq>` from the raw arg list (default 0). */
|
|
156
|
+
function parseSince(args) {
|
|
157
|
+
const i = (args || []).indexOf('--since');
|
|
158
|
+
if (i >= 0 && args[i + 1] != null) return Number.parseInt(args[i + 1], 10) || 0;
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build the shared monitor context — journal `dir`, bounded `gather`, and the
|
|
164
|
+
* default `check.failed` enricher — that BOTH the `events` pull surface and the
|
|
165
|
+
* `watch` streaming loop feed to the monitor core. Injected `dir`/`gather`/
|
|
166
|
+
* `enrich` (tests, programmatic callers) short-circuit the live gh build.
|
|
167
|
+
*
|
|
168
|
+
* @param {string|number} pr
|
|
169
|
+
* @param {string} projectRoot
|
|
170
|
+
* @param {object} deps
|
|
171
|
+
* @returns {Promise<{ dir?: string, gather?: Function, enrich?: Function, error?: string }>}
|
|
172
|
+
*/
|
|
173
|
+
async function buildMonitorContext(pr, projectRoot, deps) {
|
|
174
|
+
let dir = deps.dir;
|
|
175
|
+
let gather = deps.gather;
|
|
176
|
+
// enrich decorates newly-failed checks with log excerpts before the journal
|
|
177
|
+
// append. The caller MUST supply the default (not just forward an injected
|
|
178
|
+
// one), or a plain `forge shepherd events`/`watch` emits bare check.failed events.
|
|
179
|
+
let enrich = deps.enrich;
|
|
180
|
+
if (!gather || !dir) {
|
|
181
|
+
const gh = deps.gh || defaultGhRunner;
|
|
182
|
+
const git = deps.git || gh;
|
|
183
|
+
const buildContext = deps.buildContext || defaultBuildContext;
|
|
184
|
+
const ctx = await buildContext({ pr, gh, git, projectRoot });
|
|
185
|
+
const adapter = deps.adapter || new PrStateAdapter({ gh, git });
|
|
186
|
+
const validation = validatePrStateAdapter(adapter);
|
|
187
|
+
if (!validation.valid) {
|
|
188
|
+
return { error: `Invalid pr-state adapter: ${validation.errors.join('; ')}` };
|
|
189
|
+
}
|
|
190
|
+
dir = dir || monitorJournal.journalDir({ root: projectRoot || process.cwd(), repo: ctx.repo, pr: ctx.pr });
|
|
191
|
+
gather = gather || (() => gatherMonitorSnapshot({ ...ctx, adapter, self: deps.self }));
|
|
192
|
+
enrich = enrich || makeCheckFailureEnricher({
|
|
193
|
+
...ctx,
|
|
194
|
+
adapter,
|
|
195
|
+
self: deps.self,
|
|
196
|
+
runGh: (ghArgs) => gh('gh', ghArgs),
|
|
197
|
+
gatherPull: deps.gatherPull,
|
|
198
|
+
});
|
|
199
|
+
} else if (!enrich && deps.gatherPull) {
|
|
200
|
+
// Injected gather (tests / programmatic callers) still gets the default
|
|
201
|
+
// enrichment when a pull-signal source is supplied.
|
|
202
|
+
enrich = makeCheckFailureEnricher({
|
|
203
|
+
pr, adapter: deps.adapter, self: deps.self, runGh: deps.runGh, gatherPull: deps.gatherPull,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
return { dir, gather, enrich };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* `forge shepherd events <pr> --since <seq> [--json]` — the agent-agnostic PULL
|
|
211
|
+
* surface. Runs one bounded gather+diff (inline, unless a watcher owns the PR),
|
|
212
|
+
* appends new events to the per-PR journal, and prints every journaled event
|
|
213
|
+
* with `seq > since` as NDJSON, one per line, to stdout. Nothing under .claude.
|
|
214
|
+
*
|
|
215
|
+
* @param {string[]} args
|
|
216
|
+
* @param {string} projectRoot
|
|
217
|
+
* @param {object} [deps]
|
|
218
|
+
* @returns {Promise<object>}
|
|
219
|
+
*/
|
|
220
|
+
async function handleEvents(args, projectRoot, deps = {}) {
|
|
221
|
+
const rawArgs = args || [];
|
|
222
|
+
const sinceIdx = rawArgs.indexOf('--since');
|
|
223
|
+
const pr = rawArgs.find((a, idx) => !String(a).startsWith('--') && a !== 'events' && idx !== sinceIdx + 1);
|
|
224
|
+
if (!pr) {
|
|
225
|
+
return { success: false, error: 'Usage: forge shepherd events <pr> --since <seq> [--json]' };
|
|
226
|
+
}
|
|
227
|
+
const since = parseSince(rawArgs);
|
|
228
|
+
|
|
229
|
+
const built = await buildMonitorContext(pr, projectRoot, deps);
|
|
230
|
+
if (built.error) return { success: false, error: built.error };
|
|
231
|
+
const { dir, gather, enrich } = built;
|
|
232
|
+
|
|
233
|
+
const poll = deps.pollEvents || pollEvents;
|
|
234
|
+
const result = await poll({ dir, gather, since, now: deps.now, watcherRunning: deps.watcherRunning, enrich });
|
|
235
|
+
// `output` is the agent-agnostic pull surface: NDJSON, one event per line. The
|
|
236
|
+
// registry CLI dispatch prints `result.output` (same contract as --pull/--bundle),
|
|
237
|
+
// so this handler does NOT write to stdout itself (that would double-print).
|
|
238
|
+
const output = result.events.map((e) => JSON.stringify(e)).join('\n');
|
|
239
|
+
return { success: true, events: result.events, since: result.since, output };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Wire an AbortController to SIGINT/SIGTERM so a long-running watch loop stops
|
|
244
|
+
* cleanly on Ctrl-C. Returns the signal plus a `cleanup` that detaches the
|
|
245
|
+
* one-shot handlers (always called in a finally so the loop leaves no listeners).
|
|
246
|
+
*
|
|
247
|
+
* @returns {{ signal: object, cleanup: () => void }}
|
|
248
|
+
*/
|
|
249
|
+
function wireSignals() {
|
|
250
|
+
const controller = new AbortController();
|
|
251
|
+
const onSignal = () => controller.abort();
|
|
252
|
+
process.once('SIGINT', onSignal);
|
|
253
|
+
process.once('SIGTERM', onSignal);
|
|
254
|
+
const cleanup = () => {
|
|
255
|
+
process.off('SIGINT', onSignal);
|
|
256
|
+
process.off('SIGTERM', onSignal);
|
|
257
|
+
};
|
|
258
|
+
return { signal: controller.signal, cleanup };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* `forge shepherd watch <pr>` — the agent-agnostic PUSH surface. A long-running
|
|
263
|
+
* loop that every ~60s (jittered) runs ONE bounded monitor pass and STREAMS each
|
|
264
|
+
* new event as an NDJSON line to stdout, self-stopping on `pr.merged`/`pr.closed`.
|
|
265
|
+
* The loop streams live via the default stdout emit, so this handler returns NO
|
|
266
|
+
* `output` field (returning one would double-print). SIGINT/SIGTERM stop it clean.
|
|
267
|
+
*
|
|
268
|
+
* @param {string[]} args
|
|
269
|
+
* @param {string} projectRoot
|
|
270
|
+
* @param {object} [deps]
|
|
271
|
+
* @returns {Promise<object>}
|
|
272
|
+
*/
|
|
273
|
+
async function handleWatch(args, projectRoot, deps = {}) {
|
|
274
|
+
const rawArgs = args || [];
|
|
275
|
+
const pr = rawArgs.find((a) => !String(a).startsWith('--') && a !== 'watch');
|
|
276
|
+
if (!pr) {
|
|
277
|
+
return { success: false, error: 'Usage: forge shepherd watch <pr>' };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const built = await buildMonitorContext(pr, projectRoot, deps);
|
|
281
|
+
if (built.error) return { success: false, error: built.error };
|
|
282
|
+
const { dir, gather, enrich } = built;
|
|
283
|
+
|
|
284
|
+
const loop = deps.watchLoop || watchLoop;
|
|
285
|
+
// Injected signal (tests) suppresses real process handlers; otherwise wire them.
|
|
286
|
+
const wired = deps.signal ? { signal: deps.signal, cleanup: () => {} } : wireSignals();
|
|
287
|
+
let result;
|
|
288
|
+
try {
|
|
289
|
+
result = await loop({
|
|
290
|
+
dir,
|
|
291
|
+
gather,
|
|
292
|
+
enrich,
|
|
293
|
+
now: deps.now,
|
|
294
|
+
emit: deps.emit,
|
|
295
|
+
sleep: deps.sleep,
|
|
296
|
+
rng: deps.rng,
|
|
297
|
+
intervalMs: deps.intervalMs,
|
|
298
|
+
maxPasses: deps.maxPasses,
|
|
299
|
+
lockOpts: deps.lockOpts,
|
|
300
|
+
signal: wired.signal,
|
|
301
|
+
watcherRunning: deps.watcherRunning,
|
|
302
|
+
writePid: deps.writePid,
|
|
303
|
+
removePid: deps.removePid,
|
|
304
|
+
});
|
|
305
|
+
} finally {
|
|
306
|
+
wired.cleanup();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
success: true,
|
|
311
|
+
started: result.started,
|
|
312
|
+
passes: result.passes,
|
|
313
|
+
stopped: result.stopped,
|
|
314
|
+
...(result.reason ? { reason: result.reason } : {}),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Command handler.
|
|
320
|
+
*
|
|
321
|
+
* @param {string[]} args - Positional + flag args (first positional is the PR).
|
|
322
|
+
* @param {object} _flags - Parsed flags (unused; flags are read from args).
|
|
323
|
+
* @param {string} projectRoot - Project root.
|
|
324
|
+
* @param {object} [deps] - Injected dependencies for testing.
|
|
325
|
+
* @returns {Promise<object>} result envelope.
|
|
326
|
+
*/
|
|
327
|
+
async function handler(args, _flags, projectRoot, deps = {}) {
|
|
328
|
+
const positional = (args || []).filter((a) => !String(a).startsWith('--'));
|
|
329
|
+
const flags = new Set((args || []).filter((a) => String(a).startsWith('--')));
|
|
330
|
+
|
|
331
|
+
// Subcommand routing: `events` is the monitor pull surface (its own arg shape);
|
|
332
|
+
// `watch` is the constant monitor push surface (long-running stream).
|
|
333
|
+
if (positional[0] === 'events') {
|
|
334
|
+
return handleEvents(args, projectRoot, deps);
|
|
335
|
+
}
|
|
336
|
+
if (positional[0] === 'watch') {
|
|
337
|
+
return handleWatch(args, projectRoot, deps);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const pr = positional[0];
|
|
341
|
+
|
|
342
|
+
if (!pr) {
|
|
343
|
+
return { success: false, error: 'Usage: forge shepherd <pr> [--auto-rebase] [--bundle --json] [--pull --json]' };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const gh = deps.gh || ((cmd, a) => execFileSync(cmd, a, { encoding: 'utf8', timeout: 30000 }));
|
|
347
|
+
const git = deps.git || gh;
|
|
348
|
+
const buildContext = deps.buildContext || defaultBuildContext;
|
|
349
|
+
const runPass = deps.runPass || runShepherdPass;
|
|
350
|
+
const gatherBundle = deps.gatherBundle || gatherPrBundle;
|
|
351
|
+
const gatherPull = deps.gatherPull || gatherPullSignal;
|
|
352
|
+
|
|
353
|
+
const autoRebase = flags.has('--auto-rebase');
|
|
354
|
+
const wantBundle = flags.has('--bundle');
|
|
355
|
+
const wantPull = flags.has('--pull');
|
|
356
|
+
const wantJson = flags.has('--json');
|
|
357
|
+
|
|
358
|
+
const ctx = await buildContext({ pr, gh, git, projectRoot });
|
|
359
|
+
|
|
360
|
+
const adapter = deps.adapter || new PrStateAdapter({ gh, git });
|
|
361
|
+
const validation = validatePrStateAdapter(adapter);
|
|
362
|
+
if (!validation.valid) {
|
|
363
|
+
return { success: false, error: `Invalid pr-state adapter: ${validation.errors.join('; ')}` };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// --pull: gather the COMPACT "why it failed + what to fix" payload (failed-check
|
|
367
|
+
// log excerpts, matrix-deduped, plus the review-thread fix-list). STRICTLY
|
|
368
|
+
// READ-ONLY — it computes the decision state via a dry-run pass but takes NO
|
|
369
|
+
// action (no Tier-A rerun, no rebase); acting belongs to plain `forge shepherd`.
|
|
370
|
+
// The `gh` calls to fetch logs run through an injected runner so this stays testable.
|
|
371
|
+
if (wantPull) {
|
|
372
|
+
const runGh = (ghArgs) => gh('gh', ghArgs);
|
|
373
|
+
const pull = await gatherPull({ ...ctx, adapter, runGh, runPass, self: deps.self });
|
|
374
|
+
// `output` is what the registry CLI dispatch (bin/forge.js) actually PRINTS —
|
|
375
|
+
// returning only `pull` silently dropped the whole payload on that path (it
|
|
376
|
+
// prints `result.output`, nothing else). `--json` → machine payload; default
|
|
377
|
+
// → the compact human WHY+fix summary. `pull` is kept for the legacy
|
|
378
|
+
// bin/forge-cmd.js path and for programmatic callers/tests.
|
|
379
|
+
const output = wantJson ? JSON.stringify(pull, null, 2) : renderPullSummary(pull);
|
|
380
|
+
return { success: true, pull, output };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// --bundle: gather the COMPLETE read-only PR-state bundle the monitor will
|
|
384
|
+
// hand to a fixer-agent, and return it for the CLI to print as JSON. This is
|
|
385
|
+
// the gather half only — it decides nothing and takes no action.
|
|
386
|
+
if (wantBundle) {
|
|
387
|
+
const bundle = await gatherBundle({ ...ctx, adapter });
|
|
388
|
+
// Same rationale as --pull: the registry dispatch prints `output`. The bundle
|
|
389
|
+
// is a machine payload, so it is always emitted as JSON.
|
|
390
|
+
return { success: true, bundle, output: JSON.stringify(bundle, null, 2) };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const result = await runPass({
|
|
394
|
+
...ctx,
|
|
395
|
+
adapter,
|
|
396
|
+
autoRebase,
|
|
397
|
+
cleanTree: autoRebase ? isWorkingTreeClean(git) : false,
|
|
398
|
+
rerunBudget: deps.rerunBudget || DEFAULT_RERUN_BUDGET,
|
|
399
|
+
rerunsUsed: deps.rerunsUsed || 0,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Surface the pass outcome so the monitor is legible when run interactively or
|
|
403
|
+
// tailed by a scheduler (the bounded state machine is otherwise silent).
|
|
404
|
+
const passActions = Array.isArray(result.actions) ? result.actions : [];
|
|
405
|
+
const reasonSuffix = result.reason ? ` — ${result.reason}` : '';
|
|
406
|
+
process.stdout.write(`Shepherd pass — PR #${pr}: ${result.state}${reasonSuffix}\n`);
|
|
407
|
+
for (const action of passActions) {
|
|
408
|
+
process.stdout.write(` • ${formatAction(action)}\n`);
|
|
409
|
+
}
|
|
410
|
+
if (!passActions.length) {
|
|
411
|
+
process.stdout.write(' • no actions this pass\n');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
success: result.state !== 'HARD_STOP',
|
|
416
|
+
state: result.state,
|
|
417
|
+
reason: result.reason,
|
|
418
|
+
actions: result.actions || [],
|
|
419
|
+
...(result.authClass ? { authClass: result.authClass } : {}),
|
|
420
|
+
...(result.retryAfter ? { retryAfter: result.retryAfter } : {}),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
module.exports = {
|
|
425
|
+
name: 'shepherd',
|
|
426
|
+
description: 'Run one bounded monitor pass over a PR (rerun flaky checks, escalate, hand off — never merges)',
|
|
427
|
+
usage: 'Usage: forge shepherd <pr> [--auto-rebase] [--bundle --json] [--pull --json] | forge shepherd events <pr> --since <seq> [--json] | forge shepherd watch <pr>',
|
|
428
|
+
handler,
|
|
429
|
+
handleEvents,
|
|
430
|
+
handleWatch,
|
|
431
|
+
buildMonitorContext,
|
|
432
|
+
makeCheckFailureEnricher,
|
|
433
|
+
parseSince,
|
|
434
|
+
defaultBuildContext,
|
|
435
|
+
isWorkingTreeClean,
|
|
436
|
+
};
|
package/lib/commands/ship.js
CHANGED
|
@@ -12,6 +12,8 @@ const { execFileSync } = require('node:child_process');
|
|
|
12
12
|
const fs = require('node:fs');
|
|
13
13
|
const path = require('node:path');
|
|
14
14
|
|
|
15
|
+
const { startPrWatcherDetached } = require('../pr-monitor/watch-lifecycle');
|
|
16
|
+
|
|
15
17
|
function getExecOptions() {
|
|
16
18
|
return { encoding: 'utf8', cwd: process.cwd(), timeout: 120000 };
|
|
17
19
|
}
|
|
@@ -482,8 +484,26 @@ async function createPR(options) { // NOSONAR S3776
|
|
|
482
484
|
}
|
|
483
485
|
}
|
|
484
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Best-effort, non-blocking auto-start of the constant PR monitor once a real PR
|
|
489
|
+
* exists. Skipped on a dry run or when no PR number is known. MUST NEVER fail
|
|
490
|
+
* ship: `startWatcher` (startPrWatcherDetached) already never throws, and this
|
|
491
|
+
* guard keeps even a surprise error from surfacing to the ship caller.
|
|
492
|
+
*
|
|
493
|
+
* @param {{ dryRun: boolean, prNumber?: string|number, startWatcher: Function }} params
|
|
494
|
+
* @returns {{ started: boolean, reason?: string }}
|
|
495
|
+
*/
|
|
496
|
+
function maybeStartPrWatcher({ dryRun, prNumber, startWatcher }) {
|
|
497
|
+
if (dryRun || !prNumber) return { started: false, reason: 'skipped' };
|
|
498
|
+
try {
|
|
499
|
+
return startWatcher({ prNumber, cwd: process.cwd() });
|
|
500
|
+
} catch (err) {
|
|
501
|
+
return { started: false, reason: err.message };
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
485
505
|
async function executeShip(options) {
|
|
486
|
-
const { featureSlug, title, dryRun = false } = options || {};
|
|
506
|
+
const { featureSlug, title, dryRun = false, startWatcher = startPrWatcherDetached } = options || {};
|
|
487
507
|
|
|
488
508
|
// Validate feature slug
|
|
489
509
|
if (!featureSlug || typeof featureSlug !== 'string' || featureSlug.trim() === '') {
|
|
@@ -535,6 +555,7 @@ async function executeShip(options) {
|
|
|
535
555
|
});
|
|
536
556
|
const result = await createPR({ title, body: prBody, dryRun });
|
|
537
557
|
if (!result.success) return result;
|
|
558
|
+
maybeStartPrWatcher({ dryRun, prNumber: result.prNumber, startWatcher });
|
|
538
559
|
return {
|
|
539
560
|
success: true,
|
|
540
561
|
prUrl: result.prUrl,
|
|
@@ -567,6 +588,7 @@ module.exports = {
|
|
|
567
588
|
output: lines.join('\n'),
|
|
568
589
|
};
|
|
569
590
|
},
|
|
591
|
+
maybeStartPrWatcher,
|
|
570
592
|
extractKeyDecisions,
|
|
571
593
|
extractTestScenarios,
|
|
572
594
|
getTestCoverage,
|
package/lib/commands/show.js
CHANGED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Forge Stage Command (f61601ab)
|
|
5
|
+
*
|
|
6
|
+
* Records the REAL workflow phase of an issue into the kernel_stage_runs table so
|
|
7
|
+
* the phase is queryable — instead of being guessed from status+claim (a
|
|
8
|
+
* claimed-open issue with a merged PR would otherwise still show "dev"). Mirrors
|
|
9
|
+
* the worktree-linkage registry: direct kernel writes, idempotent per
|
|
10
|
+
* (issue_id, stage).
|
|
11
|
+
*
|
|
12
|
+
* forge stage <issue-id> <stage> --start Open a stage (status=active)
|
|
13
|
+
* forge stage <issue-id> <stage> --complete Close a stage (status=done)
|
|
14
|
+
* forge stage <issue-id> --current Print the current stage
|
|
15
|
+
* forge stage <issue-id> --list Print the full stage history
|
|
16
|
+
*
|
|
17
|
+
* `<stage>` is one of the canonical workflow stages (plan|dev|validate|ship|review|verify).
|
|
18
|
+
* Read the current stage back on `forge show <id>` (data.current_stage).
|
|
19
|
+
*
|
|
20
|
+
* @module commands/stage
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const { STAGE_IDS, normalizeStageId } = require('../workflow/stages');
|
|
24
|
+
const { resolveIssueId } = require('../kernel/issue-id-resolver');
|
|
25
|
+
|
|
26
|
+
const STAGE_FLAGS = ['--start', '--complete', '--list', '--current', '--json'];
|
|
27
|
+
const VALUE_FLAGS = ['--substage'];
|
|
28
|
+
const USAGE = 'Usage: forge stage <issue-id> <stage> --start|--complete (reads: --current | --list)';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse the stage command's own flags out of the raw args (the global flag parser
|
|
32
|
+
* only recognizes an allowlist, so — like `worktree` — this command extracts its
|
|
33
|
+
* own). Returns { positional, opts } or { error }.
|
|
34
|
+
*/
|
|
35
|
+
function parseStageArgs(args = []) {
|
|
36
|
+
const positional = [];
|
|
37
|
+
const opts = { start: false, complete: false, list: false, current: false, json: false, substage: null };
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
40
|
+
const arg = args[i];
|
|
41
|
+
if (typeof arg !== 'string') continue;
|
|
42
|
+
|
|
43
|
+
const valueFlag = VALUE_FLAGS.find(flag => arg === flag || arg.startsWith(`${flag}=`));
|
|
44
|
+
if (valueFlag) {
|
|
45
|
+
let value;
|
|
46
|
+
if (arg.startsWith(`${valueFlag}=`)) {
|
|
47
|
+
value = arg.slice(`${valueFlag}=`.length);
|
|
48
|
+
} else {
|
|
49
|
+
value = args[i + 1];
|
|
50
|
+
i += 1;
|
|
51
|
+
}
|
|
52
|
+
if (!value || value.startsWith('--')) {
|
|
53
|
+
return { error: `Missing value for ${valueFlag}. ${USAGE}` };
|
|
54
|
+
}
|
|
55
|
+
opts.substage = value;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (STAGE_FLAGS.includes(arg)) {
|
|
60
|
+
opts[arg.slice(2)] = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (arg.startsWith('--')) {
|
|
65
|
+
return { error: `Unknown flag: ${arg}. ${USAGE}` };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
positional.push(arg);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { positional, opts };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function resolveDriver(projectRoot, opts) {
|
|
75
|
+
if (opts._kernelDriver) return opts._kernelDriver;
|
|
76
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
77
|
+
return (await buildMigratedKernelIssueDeps({ projectRoot })).kernelDriver;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function render(opts, data, humanLines) {
|
|
81
|
+
if (opts.json || process.env.FORGE_JSON === '1') {
|
|
82
|
+
return { success: true, output: JSON.stringify(data, null, 2) };
|
|
83
|
+
}
|
|
84
|
+
return { success: true, output: humanLines.join('\n'), ...data };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
name: 'stage',
|
|
89
|
+
description: 'Record or read an issue\'s real workflow stage (stage_runs)',
|
|
90
|
+
usage: USAGE,
|
|
91
|
+
flags: {
|
|
92
|
+
'--start': 'Open a stage run (status=active)',
|
|
93
|
+
'--complete': 'Complete a stage run (status=done)',
|
|
94
|
+
'--substage': 'Optional substage label to record',
|
|
95
|
+
'--current': 'Print the current stage (latest active, else latest completed)',
|
|
96
|
+
'--list': 'Print the full stage-run history for the issue',
|
|
97
|
+
'--json': 'Emit machine-readable JSON',
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string[]} args - Positional + flag args after `stage`
|
|
102
|
+
* @param {object} _flags - Global parsed flags (unused; this command parses its own)
|
|
103
|
+
* @param {string} projectRoot - Project root path
|
|
104
|
+
* @param {object} [opts] - DI options (may inject `_kernelDriver`)
|
|
105
|
+
* @returns {Promise<object>}
|
|
106
|
+
*/
|
|
107
|
+
handler: async (args, _flags, projectRoot, opts = {}) => {
|
|
108
|
+
const parsed = parseStageArgs(args);
|
|
109
|
+
if (parsed.error) {
|
|
110
|
+
return { success: false, error: parsed.error };
|
|
111
|
+
}
|
|
112
|
+
const { positional, opts: stageOpts } = parsed;
|
|
113
|
+
|
|
114
|
+
const issueRef = positional[0];
|
|
115
|
+
if (!issueRef) {
|
|
116
|
+
return { success: false, error: `Missing issue id. ${USAGE}` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let driver;
|
|
120
|
+
try {
|
|
121
|
+
driver = await resolveDriver(projectRoot, opts);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return { success: false, error: `Kernel unavailable: ${error.message}` };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Resolve prefixes / display handles to a full issue id (same resolver the
|
|
127
|
+
// issue commands use), so `forge stage 1a2b3c4d dev --start` works.
|
|
128
|
+
const resolution = await resolveIssueId(
|
|
129
|
+
issueRef,
|
|
130
|
+
(needle, limit) => driver.findIssueIdsByPrefix(needle, limit, {}, {}),
|
|
131
|
+
);
|
|
132
|
+
if (resolution.error) {
|
|
133
|
+
return { success: false, error: resolution.error };
|
|
134
|
+
}
|
|
135
|
+
const issueId = resolution.id;
|
|
136
|
+
|
|
137
|
+
// Read paths -----------------------------------------------------------
|
|
138
|
+
if (stageOpts.current) {
|
|
139
|
+
const current = driver.getCurrentStage({ issue_id: issueId }, {});
|
|
140
|
+
const data = {
|
|
141
|
+
issue_id: issueId,
|
|
142
|
+
current_stage: current ? current.stage : null,
|
|
143
|
+
current_stage_status: current ? current.status : null,
|
|
144
|
+
};
|
|
145
|
+
const human = current
|
|
146
|
+
? `${issueId}: ${current.stage} (${current.status})`
|
|
147
|
+
: `${issueId}: no stage recorded`;
|
|
148
|
+
return render(stageOpts, data, [human]);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (stageOpts.list) {
|
|
152
|
+
const runs = driver.listStageRuns({ issue_id: issueId }, {});
|
|
153
|
+
const data = { issue_id: issueId, stage_runs: runs };
|
|
154
|
+
const human = runs.length === 0
|
|
155
|
+
? [`${issueId}: no stage runs`]
|
|
156
|
+
: runs.map(run => ` ${run.stage.padEnd(9)} ${run.status.padEnd(7)} started ${run.started_at}${run.completed_at ? ` → completed ${run.completed_at}` : ''}`);
|
|
157
|
+
return render(stageOpts, data, [`${issueId} stage history:`, ...human]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Write paths ----------------------------------------------------------
|
|
161
|
+
const stage = normalizeStageId(positional[1]);
|
|
162
|
+
if (!stage) {
|
|
163
|
+
return {
|
|
164
|
+
success: false,
|
|
165
|
+
error: `Invalid or missing stage "${positional[1] ?? ''}". Expected one of: ${STAGE_IDS.join(', ')}. ${USAGE}`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (stageOpts.start && stageOpts.complete) {
|
|
170
|
+
return { success: false, error: `Pass only one of --start or --complete. ${USAGE}` };
|
|
171
|
+
}
|
|
172
|
+
const action = stageOpts.complete ? 'complete' : 'start';
|
|
173
|
+
|
|
174
|
+
let row;
|
|
175
|
+
try {
|
|
176
|
+
row = driver.recordStageRun(
|
|
177
|
+
{ issue_id: issueId, stage, action, substage: stageOpts.substage || null },
|
|
178
|
+
{},
|
|
179
|
+
);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// A FOREIGN KEY failure means the issue id does not exist in the kernel.
|
|
182
|
+
if (/FOREIGN KEY/i.test(String(error.message))) {
|
|
183
|
+
return { success: false, error: `Issue ${issueId} not found in the kernel.` };
|
|
184
|
+
}
|
|
185
|
+
return { success: false, error: `Failed to record stage: ${error.message}` };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const data = { issue_id: issueId, action, stage_run: row };
|
|
189
|
+
const verb = action === 'complete' ? 'completed' : 'started';
|
|
190
|
+
return render(stageOpts, data, [`${issueId}: ${verb} stage ${stage} (${row.status})`]);
|
|
191
|
+
},
|
|
192
|
+
};
|