forge-workflow 0.0.10 → 0.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +82 -5
- package/.forge/hooks/forge-native-hook.js +431 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +709 -0
- package/CLAUDE.md +9 -118
- package/QUICKSTART.md +175 -0
- package/README.md +275 -365
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +532 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/forge/TOOLCHAIN.md +670 -0
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +121 -0
- package/docs/guides/SUPPORT.md +190 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +214 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +155 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/activation/ensure-forge-home.js +135 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1109 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +139 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-detect.js +60 -0
- package/lib/beads-nudge.js +91 -0
- package/lib/beads-setup.js +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_aliases.js +248 -0
- package/lib/commands/_issue.js +780 -77
- package/lib/commands/_manifest.js +93 -0
- package/lib/commands/_registry.js +99 -34
- package/lib/commands/_resolve-command-opts.js +230 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +209 -0
- package/lib/commands/hooks.js +377 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +604 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +17 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/memory.js +81 -0
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +362 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +481 -29
- package/lib/commands/pr.js +88 -0
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +135 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +171 -0
- package/lib/commands/recap.js +75 -0
- package/lib/commands/recommend.js +0 -1
- package/lib/commands/release.js +104 -0
- package/lib/commands/remember.js +140 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +900 -971
- package/lib/commands/shepherd.js +501 -0
- package/lib/commands/ship.js +59 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +158 -21
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +4 -1
- package/lib/commands/test.js +43 -27
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +43 -18
- package/lib/commands/worktree.js +362 -99
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +977 -0
- package/lib/dep-guard/keyword-ripple.js +2 -2
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +35 -9
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +382 -11
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +192 -0
- package/lib/global-flags.js +104 -0
- package/lib/greptile-match.js +7 -63
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +541 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/kernel/backing-issue.js +311 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/owned-kernel.js +43 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2540 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +3 -2
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/auto-actions.js +175 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/digest.js +206 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +192 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1314 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +99 -497
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2105 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +241 -20
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/scoring.js +17 -3
- package/lib/status/beads-snapshot.js +45 -2
- package/lib/status/presenter.js +169 -18
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +251 -0
- package/lib/workflow/enforce-stage.js +351 -45
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +11 -22
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +37 -35
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +81 -57
- package/scripts/beads-upgrade-smoke.sh +24 -3
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard.sh +22 -3
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +49 -84
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +50 -83
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-auto-actions.js +93 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/pr-verdict-label.js +50 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +21 -94
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status.sh +60 -57
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +13 -6
- package/scripts/test.js +95 -12
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -89
- package/.github/workflows/github-to-beads.yml +0 -100
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -281
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-bootstrap.js +0 -225
- package/lib/beads-health-check.js +0 -188
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -584
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -356
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -132
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ship = require('./ship');
|
|
4
|
+
const preflight = require('./preflight');
|
|
5
|
+
const shepherd = require('./shepherd');
|
|
6
|
+
const merge = require('./merge');
|
|
7
|
+
const { stripGlobalFlags } = require('../global-flags');
|
|
8
|
+
|
|
9
|
+
// One memorable surface over the EXISTING pull-request commands (kernel issue
|
|
10
|
+
// 6ab3f30c): every subcommand delegates to the standalone ship/preflight/shepherd/
|
|
11
|
+
// merge handlers — the same code, not a reimplementation. The standalone
|
|
12
|
+
// `forge ship`/`preflight`/`shepherd`/`merge` commands remain registered as
|
|
13
|
+
// back-compat aliases (see lib/commands/_aliases.js), so nothing that already
|
|
14
|
+
// calls them breaks. `pr ship` is the canonical PR-creation form; bare `ship`
|
|
15
|
+
// stays a visible shortcut.
|
|
16
|
+
//
|
|
17
|
+
// Delegates are referenced by MODULE (not a pre-bound `.handler`) so the routed
|
|
18
|
+
// handler is resolved at dispatch time — dispatch always reaches whatever the
|
|
19
|
+
// command module currently exports, keeping the standalone command the single
|
|
20
|
+
// source of truth for its own behaviour.
|
|
21
|
+
const SUBCOMMANDS = {
|
|
22
|
+
ship: {
|
|
23
|
+
module: ship,
|
|
24
|
+
summary: 'Create a pull request from validated feature work (= forge ship)',
|
|
25
|
+
},
|
|
26
|
+
preflight: {
|
|
27
|
+
module: preflight,
|
|
28
|
+
summary: 'Fast deterministic-gate parity with CI (= forge preflight; supports --all)',
|
|
29
|
+
},
|
|
30
|
+
shepherd: {
|
|
31
|
+
module: shepherd,
|
|
32
|
+
summary: 'Run one bounded monitor pass over a PR (= forge shepherd; --bundle/--pull/--json, events, watch)',
|
|
33
|
+
},
|
|
34
|
+
merge: {
|
|
35
|
+
module: merge,
|
|
36
|
+
summary: 'Opt-in conditional auto-merge, OFF by default (= forge merge --auto <pr>)',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const usage = 'Usage: forge pr <ship|preflight|shepherd|merge> [args]';
|
|
41
|
+
|
|
42
|
+
function renderHelp() {
|
|
43
|
+
const width = Math.max(...Object.keys(SUBCOMMANDS).map(name => name.length));
|
|
44
|
+
const lines = [
|
|
45
|
+
usage,
|
|
46
|
+
'',
|
|
47
|
+
'Subcommands:',
|
|
48
|
+
...Object.entries(SUBCOMMANDS).map(
|
|
49
|
+
([name, { summary }]) => ` ${name.padEnd(width)} ${summary}`
|
|
50
|
+
),
|
|
51
|
+
'',
|
|
52
|
+
'Back-compat: forge ship / forge preflight / forge shepherd / forge merge remain available as aliases.',
|
|
53
|
+
];
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function handler(args, flags, projectRoot, opts) {
|
|
58
|
+
// The subcommand is the first positional token; global flags (e.g. `-p <dir>`) are stripped
|
|
59
|
+
// first so they never masquerade as the subcommand.
|
|
60
|
+
const positional = stripGlobalFlags(args).find(arg => !arg.startsWith('-'));
|
|
61
|
+
|
|
62
|
+
if (!positional || positional === 'help' || args.includes('--help') || args.includes('-h')) {
|
|
63
|
+
return { success: true, output: renderHelp() };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sub = SUBCOMMANDS[positional];
|
|
67
|
+
if (!sub) {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
error: `Unknown pr subcommand: ${positional}\n\n${renderHelp()}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Forward everything EXCEPT the consumed subcommand token to the delegate, preserving
|
|
75
|
+
// every remaining token (including flags like `--pull`/`--json`/`--bundle`, and the
|
|
76
|
+
// `events`/`watch` shepherd sub-shapes) so passthrough stays byte-identical.
|
|
77
|
+
const idx = args.indexOf(positional);
|
|
78
|
+
const childArgs = idx >= 0 ? [...args.slice(0, idx), ...args.slice(idx + 1)] : args;
|
|
79
|
+
return sub.module.handler(childArgs, flags, projectRoot, opts);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
name: 'pr',
|
|
84
|
+
description:
|
|
85
|
+
'Unified pull-request surface: forge pr ship|preflight|shepherd|merge (wraps ship/preflight/shepherd/merge)',
|
|
86
|
+
usage,
|
|
87
|
+
handler,
|
|
88
|
+
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `forge preflight` — fast (~2-4 min) blast-radius gate.
|
|
5
|
+
*
|
|
6
|
+
* Gives DETERMINISTIC-GATE PARITY with CI so an agent catches what CI catches
|
|
7
|
+
* WITHOUT waiting on the hanging full suite. Runs four gates in order, fast-fail:
|
|
8
|
+
*
|
|
9
|
+
* 1. lint — eslint --max-warnings 0 on changed files (--all → whole tree)
|
|
10
|
+
* 2. drift — skills mirror + registry sync + agentic-workflow structural asserts
|
|
11
|
+
* 3. sonar — eslint-plugin-sonarjs cognitive-complexity=15 (+ parity rules)
|
|
12
|
+
* 4. affected — only the tests mapped from the changed files
|
|
13
|
+
*
|
|
14
|
+
* Exit non-zero on any gate failure. Run this before `forge push`.
|
|
15
|
+
*
|
|
16
|
+
* @module commands/preflight
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { execFileSync } = require('node:child_process');
|
|
20
|
+
const { buildGates } = require('../preflight/gates');
|
|
21
|
+
const { runGates } = require('../preflight/runner');
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Strictly resolve a base ref to diff HEAD against. Returns the ref name, or
|
|
25
|
+
* null when no base can be established (fresh repo: no origin/HEAD, no upstream,
|
|
26
|
+
* no main/master ref). Unlike test.js's resolver, this never falls back to a
|
|
27
|
+
* default name that doesn't exist — a missing base must be reported, not faked.
|
|
28
|
+
*
|
|
29
|
+
* @param {Function} exec - execFileSync-compatible
|
|
30
|
+
* @returns {string|null}
|
|
31
|
+
*/
|
|
32
|
+
/** Run a git command and return trimmed stdout, or null on any failure. */
|
|
33
|
+
function gitTryOut(exec, args) {
|
|
34
|
+
try {
|
|
35
|
+
return String(exec('git', args, { encoding: 'utf8', timeout: 3000, stdio: ['ignore', 'pipe', 'ignore'] }) || '').trim();
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** True when `ref` resolves to a real object in this repo. */
|
|
42
|
+
function gitVerifyRef(exec, ref) {
|
|
43
|
+
try {
|
|
44
|
+
exec('git', ['rev-parse', '--verify', '--quiet', ref], { stdio: 'ignore', timeout: 3000 });
|
|
45
|
+
return true;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* True when `ref` resolves to the SAME commit as HEAD. Diffing HEAD against such
|
|
53
|
+
* a ref yields zero files, so it is never a usable integration base.
|
|
54
|
+
*/
|
|
55
|
+
function refEqualsHead(exec, ref) {
|
|
56
|
+
const refSha = gitTryOut(exec, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]);
|
|
57
|
+
if (!refSha) return false;
|
|
58
|
+
const headSha = gitTryOut(exec, ['rev-parse', '--verify', '--quiet', 'HEAD^{commit}']);
|
|
59
|
+
return !!headSha && refSha === headSha;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveBaseRef(exec) {
|
|
63
|
+
const originHead = gitTryOut(exec, ['rev-parse', '--abbrev-ref', 'origin/HEAD']);
|
|
64
|
+
if (originHead && originHead !== 'origin/HEAD') return originHead;
|
|
65
|
+
|
|
66
|
+
// `@{upstream}` is the CURRENT branch's OWN remote-tracking ref. On a fully
|
|
67
|
+
// pushed feature branch it points AT HEAD, so diffing against it produces 0
|
|
68
|
+
// changed files and every gate passes vacuously (fail-OPEN). Accept it as a
|
|
69
|
+
// base only when it is a real integration point ahead of us — never when it
|
|
70
|
+
// resolves to our own HEAD. Fall through to a real base (origin/main, …) or
|
|
71
|
+
// FAIL if none exists, rather than manufacture a wrong-but-'resolved' base.
|
|
72
|
+
const upstream = gitTryOut(exec, ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']);
|
|
73
|
+
if (upstream && !refEqualsHead(exec, upstream)) return upstream;
|
|
74
|
+
|
|
75
|
+
for (const ref of ['origin/main', 'origin/master', 'main', 'master']) {
|
|
76
|
+
if (gitVerifyRef(exec, ref)) return ref;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve the change set for preflight, distinguishing "base resolved, genuinely
|
|
83
|
+
* zero changes" from "no base branch could be resolved". Only the latter is a
|
|
84
|
+
* gate failure — a fresh repo with no base cannot have its blast radius computed,
|
|
85
|
+
* so preflight must NOT pass vacuously.
|
|
86
|
+
*
|
|
87
|
+
* @param {Function} [exec] - execFileSync-compatible (injectable for tests)
|
|
88
|
+
* @param {{ runAll?: boolean }} [opts]
|
|
89
|
+
* @returns {{ resolved: boolean, changedFiles: string[], reason?: string, baseRef?: string }}
|
|
90
|
+
*/
|
|
91
|
+
function resolveChangeSet(exec = execFileSync, { runAll = false } = {}) {
|
|
92
|
+
if (runAll) {
|
|
93
|
+
return { resolved: true, changedFiles: [], reason: 'whole-tree scope (--all)' };
|
|
94
|
+
}
|
|
95
|
+
const baseRef = resolveBaseRef(exec);
|
|
96
|
+
if (!baseRef) {
|
|
97
|
+
return {
|
|
98
|
+
resolved: false,
|
|
99
|
+
changedFiles: [],
|
|
100
|
+
reason: 'could not resolve a base branch (no origin/HEAD, no upstream, no main/master ref)',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// Diff against the SAME base we just resolved. getChangedFiles() re-resolves
|
|
104
|
+
// its own diff ref (and can fall back to a different default branch), so use
|
|
105
|
+
// baseRef directly to keep the file list consistent with the resolved base.
|
|
106
|
+
return { resolved: true, baseRef, changedFiles: changedFilesForBase(exec, baseRef) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Files changed between the merge-base of HEAD and `baseRef` and HEAD. */
|
|
110
|
+
function changedFilesForBase(exec, baseRef) {
|
|
111
|
+
const mergeBase = gitTryOut(exec, ['merge-base', 'HEAD', baseRef]);
|
|
112
|
+
const range = mergeBase ? `${mergeBase}...HEAD` : `${baseRef}...HEAD`;
|
|
113
|
+
const out = gitTryOut(exec, ['diff', '--name-only', range]);
|
|
114
|
+
if (out == null) return [];
|
|
115
|
+
return out.split(/\r?\n/).filter(Boolean);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Pick the change set: an injected resolveChangeSet wins; a legacy injected
|
|
120
|
+
* resolveChangedFiles vouches for resolution; otherwise resolve strictly.
|
|
121
|
+
*/
|
|
122
|
+
function selectChangeSet(deps, runAll) {
|
|
123
|
+
if (deps.resolveChangeSet) {
|
|
124
|
+
return deps.resolveChangeSet({ runAll });
|
|
125
|
+
}
|
|
126
|
+
if (deps.resolveChangedFiles) {
|
|
127
|
+
return { resolved: true, changedFiles: deps.resolveChangedFiles() || [] };
|
|
128
|
+
}
|
|
129
|
+
return resolveChangeSet(execFileSync, { runAll });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Print the fail-closed remedy when no base branch could be resolved. */
|
|
133
|
+
function reportBaseUnresolved(log, reason) {
|
|
134
|
+
log('forge preflight — fast blast-radius gate (deterministic parity with CI)');
|
|
135
|
+
log('');
|
|
136
|
+
log(`preflight FAILED — ${reason}.`);
|
|
137
|
+
log(' Remedy: give preflight a base branch to diff against, e.g.');
|
|
138
|
+
log(' git fetch origin && git branch --set-upstream-to=origin/main # your default branch');
|
|
139
|
+
log(' (or ensure origin/HEAD is set: git remote set-head origin -a)');
|
|
140
|
+
log(' Or scan the whole tree explicitly: forge preflight --all');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Summary mark for one gate result. */
|
|
144
|
+
function gateMark(result) {
|
|
145
|
+
if (result.skipped) return 'SKIP';
|
|
146
|
+
return result.ok ? 'PASS' : 'FAIL';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
name: 'preflight',
|
|
151
|
+
description: 'Fast deterministic-gate parity with CI (lint, drift/registry, sonar, affected tests)',
|
|
152
|
+
usage: 'forge preflight [--all]',
|
|
153
|
+
flags: {
|
|
154
|
+
'--all': 'Lint/scan the whole tree instead of only changed files',
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {string[]} _args
|
|
159
|
+
* @param {Object} flags
|
|
160
|
+
* @param {string} projectRoot
|
|
161
|
+
* @param {Object} [deps] - injectable { log, resolveChangedFiles, buildGates, runGates }
|
|
162
|
+
* @returns {Promise<{ success: boolean, results?: Array }>}
|
|
163
|
+
*/
|
|
164
|
+
handler: async (_args, flags, projectRoot, deps = {}) => {
|
|
165
|
+
const log = deps.log || console.log;
|
|
166
|
+
const build = deps.buildGates || buildGates;
|
|
167
|
+
const run = deps.runGates || runGates;
|
|
168
|
+
|
|
169
|
+
const runAll = !!(flags && (flags['--all'] || flags.all));
|
|
170
|
+
|
|
171
|
+
// Resolve the change set AND whether a base branch could be established.
|
|
172
|
+
const changeSet = selectChangeSet(deps, runAll) || {};
|
|
173
|
+
|
|
174
|
+
// B2 (N1): an unresolvable base is fail-CLOSED. Computing "0 changed files"
|
|
175
|
+
// from a missing base and passing every gate manufactures false confidence.
|
|
176
|
+
// Anything not EXPLICITLY resolved (including a missing `resolved` field from
|
|
177
|
+
// a future override) fails closed — never fail open on an indeterminate base.
|
|
178
|
+
if (!runAll && !changeSet.resolved) {
|
|
179
|
+
const reason = changeSet.reason || 'base branch could not be resolved';
|
|
180
|
+
reportBaseUnresolved(log, reason);
|
|
181
|
+
return { success: false, results: [], reason, baseUnresolved: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const changedFiles = changeSet.changedFiles || [];
|
|
185
|
+
|
|
186
|
+
log('forge preflight — fast blast-radius gate (deterministic parity with CI)');
|
|
187
|
+
log(runAll
|
|
188
|
+
? ' scope: whole tree (--all)'
|
|
189
|
+
: ` scope: ${changedFiles.length} changed file(s) vs base branch`);
|
|
190
|
+
log('');
|
|
191
|
+
|
|
192
|
+
const gates = build({ projectRoot, changedFiles, runAll });
|
|
193
|
+
const { ok, results } = await run(gates, { log: (line) => log(` ${line}`) });
|
|
194
|
+
|
|
195
|
+
log('');
|
|
196
|
+
log('Preflight summary:');
|
|
197
|
+
for (const r of results) {
|
|
198
|
+
const suffix = r.summary ? ` — ${r.summary}` : '';
|
|
199
|
+
log(` [${gateMark(r)}] ${r.name}${suffix}`);
|
|
200
|
+
}
|
|
201
|
+
log('');
|
|
202
|
+
log(ok
|
|
203
|
+
? 'preflight passed — safe to push'
|
|
204
|
+
: 'preflight FAILED — fix the failing gate above before `forge push`');
|
|
205
|
+
|
|
206
|
+
return { success: ok, results };
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
module.exports.resolveChangeSet = resolveChangeSet;
|
|
211
|
+
module.exports.resolveBaseRef = resolveBaseRef;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
buildPrime,
|
|
5
|
+
runOrientationCommand,
|
|
6
|
+
} = require('../orientation');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
name: 'prime',
|
|
10
|
+
description: 'Emit session-entry bounded orientation for agents',
|
|
11
|
+
usage: 'Usage: forge prime [--budget N] [--json]',
|
|
12
|
+
handler: (args, _flags, projectRoot) => runOrientationCommand(buildPrime, args, projectRoot),
|
|
13
|
+
};
|
package/lib/commands/push.js
CHANGED
|
@@ -4,6 +4,8 @@ const { execFileSync, spawnSync } = require('node:child_process');
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const forgeToken = require('../../scripts/check-forge-token');
|
|
7
|
+
const { startPrWatcherDetached } = require('../pr-monitor/watch-lifecycle');
|
|
8
|
+
const { autoShepherdRailEnabled } = require('./ship');
|
|
7
9
|
|
|
8
10
|
const isWindows = process.platform === 'win32';
|
|
9
11
|
|
|
@@ -54,6 +56,113 @@ function isFirstPush(branch, execFn) {
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the git worktree top-level directory.
|
|
61
|
+
*
|
|
62
|
+
* The pre-push hook checks for the nonce token relative to its own cwd, which
|
|
63
|
+
* git sets to the worktree root during `git push`. The handler's `projectRoot`
|
|
64
|
+
* is derived from INIT_CWD/cwd and can point at the MAIN repo root when forge
|
|
65
|
+
* runs from a linked worktree, so the token must be written to the real git
|
|
66
|
+
* top-level instead — otherwise lefthook never finds it and re-runs the suite.
|
|
67
|
+
*
|
|
68
|
+
* @param {function} execFn - execFileSync or mock
|
|
69
|
+
* @param {string} fallback - projectRoot to use when git resolution fails
|
|
70
|
+
* @returns {string} Absolute worktree top-level path, or the fallback
|
|
71
|
+
*/
|
|
72
|
+
function resolveGitToplevel(execFn, fallback) {
|
|
73
|
+
try {
|
|
74
|
+
const top = execFn('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' }).trim();
|
|
75
|
+
return top || fallback;
|
|
76
|
+
} catch (_err) { /* intentional: fall back to projectRoot outside a git worktree */ // NOSONAR S2486
|
|
77
|
+
return fallback;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Auto-file rail: best-effort, NON-BLOCKING ensure that the branch being pushed has a
|
|
83
|
+
* backing Kernel issue (kernel issue a4b8f56f) — so started work that is being pushed
|
|
84
|
+
* never goes unfiled. Idempotent (deduped by branch via ensureBackingIssue); a
|
|
85
|
+
* main/master/detached/ignore branch is skipped, and ANY failure degrades to null and
|
|
86
|
+
* NEVER aborts the push. Injectable via `deps._ensureBackingIssue` for tests.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} projectRoot
|
|
89
|
+
* @param {function} execFn - execFileSync (for branch resolution)
|
|
90
|
+
* @param {object} deps - DI (may inject `_ensureBackingIssue`, `_kernelDriver`, `_kernelBroker`)
|
|
91
|
+
* @returns {Promise<object|null>}
|
|
92
|
+
*/
|
|
93
|
+
async function autoFileBackingIssueForPush(projectRoot, execFn, deps = {}) {
|
|
94
|
+
try {
|
|
95
|
+
// Lazy require INSIDE the try: a module-load failure must degrade to null and
|
|
96
|
+
// NEVER abort the push (mirrors the worktree helper; CodeRabbit #370).
|
|
97
|
+
const ensure = deps._ensureBackingIssue || require('../kernel/backing-issue').ensureBackingIssue;
|
|
98
|
+
const branch = getCurrentBranch(execFn);
|
|
99
|
+
if (!branch || branch === 'unknown') return null;
|
|
100
|
+
let driver = deps._kernelDriver;
|
|
101
|
+
let broker = deps._kernelBroker;
|
|
102
|
+
if (!driver || !broker) {
|
|
103
|
+
if (!fs.existsSync(path.join(projectRoot, '.git'))) return null;
|
|
104
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
105
|
+
const built = await buildMigratedKernelIssueDeps({ projectRoot });
|
|
106
|
+
driver = driver || built.kernelDriver;
|
|
107
|
+
broker = broker || built.kernelBroker;
|
|
108
|
+
}
|
|
109
|
+
return await ensure({ branch, projectRoot, driver, broker });
|
|
110
|
+
} catch (_err) { /* non-blocking: tracking must never break a push */ // NOSONAR S2486
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the OPEN PR number for the current branch via `gh pr view`. Returns
|
|
117
|
+
* null when there is no PR, gh is unavailable, or anything errors (fail-open).
|
|
118
|
+
* NEVER throws.
|
|
119
|
+
*
|
|
120
|
+
* @param {function} execFn - execFileSync or mock
|
|
121
|
+
* @returns {number|null}
|
|
122
|
+
*/
|
|
123
|
+
function resolveOpenPrNumber(execFn) {
|
|
124
|
+
try {
|
|
125
|
+
const out = execFn('gh', ['pr', 'view', '--json', 'number', '-q', '.number'], {
|
|
126
|
+
encoding: 'utf8', timeout: 15000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
127
|
+
});
|
|
128
|
+
const n = Number.parseInt(String(out).trim(), 10);
|
|
129
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
130
|
+
} catch (_err) { /* intentional: no open PR / gh missing → arm nothing */ // NOSONAR S2486
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Best-effort, NON-BLOCKING arm of the constant PR watcher after a successful
|
|
137
|
+
* push, when an OPEN PR exists for the current branch. This closes the gap where
|
|
138
|
+
* PRs not born from `forge ship` (gh pr create, the GitHub UI, an earlier push)
|
|
139
|
+
* never got a watcher. Gated by the default-ON `rail.auto_shepherd`, idempotent
|
|
140
|
+
* via the watch loop's own PID/journal lock, and reusing the same
|
|
141
|
+
* `startPrWatcherDetached` as ship. MUST NEVER throw into or fail the push: a
|
|
142
|
+
* disabled rail, no PR, a gh error, or a spawn error all degrade to
|
|
143
|
+
* `{ armed: false }`.
|
|
144
|
+
*
|
|
145
|
+
* @param {object} params
|
|
146
|
+
* @returns {{ armed: boolean, reason?: string, prNumber?: number }}
|
|
147
|
+
*/
|
|
148
|
+
function maybeArmWatcherAfterPush({
|
|
149
|
+
projectRoot,
|
|
150
|
+
execFn,
|
|
151
|
+
startWatcher = startPrWatcherDetached,
|
|
152
|
+
railEnabled = autoShepherdRailEnabled,
|
|
153
|
+
prLookup = resolveOpenPrNumber,
|
|
154
|
+
}) {
|
|
155
|
+
try {
|
|
156
|
+
if (!railEnabled(projectRoot)) return { armed: false, reason: 'rail.auto_shepherd disabled' };
|
|
157
|
+
const prNumber = prLookup(execFn);
|
|
158
|
+
if (!prNumber) return { armed: false, reason: 'no-open-pr' };
|
|
159
|
+
const res = startWatcher({ prNumber, cwd: projectRoot });
|
|
160
|
+
return { armed: !!(res && res.started), reason: res && res.reason, prNumber };
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return { armed: false, reason: err.message };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
57
166
|
/**
|
|
58
167
|
* Run branch protection check as a subprocess.
|
|
59
168
|
* @param {function} execFn - execFileSync or mock
|
|
@@ -150,6 +259,10 @@ module.exports = {
|
|
|
150
259
|
return { success: false, quickMode, lintPassed: false, pushed: false };
|
|
151
260
|
}
|
|
152
261
|
|
|
262
|
+
// Auto-file rail (non-blocking): ensure the branch being pushed has a backing
|
|
263
|
+
// Kernel issue so started work never goes unfiled. Never aborts the push.
|
|
264
|
+
await autoFileBackingIssueForPush(projectRoot, execFn, deps || {});
|
|
265
|
+
|
|
153
266
|
// Step 2: Lint
|
|
154
267
|
const lintPassed = runLint(spawnFn, pkgManager, projectRoot);
|
|
155
268
|
if (!lintPassed) {
|
|
@@ -163,9 +276,11 @@ module.exports = {
|
|
|
163
276
|
return { success: false, quickMode, lintPassed: true, testsPassed: false, pushed: false };
|
|
164
277
|
}
|
|
165
278
|
|
|
166
|
-
// Step 4: Write nonce token so lefthook skips pre-push hooks
|
|
279
|
+
// Step 4: Write nonce token so lefthook skips pre-push hooks.
|
|
280
|
+
// Write to the git worktree top-level (where the hook checks), not the
|
|
281
|
+
// inherited projectRoot, so the skip works from linked worktrees too.
|
|
167
282
|
try {
|
|
168
|
-
tokenWrite(projectRoot);
|
|
283
|
+
tokenWrite(resolveGitToplevel(execFn, projectRoot));
|
|
169
284
|
} catch (_err) { /* intentional: non-fatal, lefthook will re-run hooks */ // NOSONAR S2486
|
|
170
285
|
log('Warning: Could not write forge push token — lefthook hooks will run again');
|
|
171
286
|
}
|
|
@@ -185,6 +300,17 @@ module.exports = {
|
|
|
185
300
|
};
|
|
186
301
|
}
|
|
187
302
|
|
|
303
|
+
// Arm the constant PR watcher for this branch's open PR (best-effort,
|
|
304
|
+
// gated by rail.auto_shepherd, never fails the push). Covers PRs not born
|
|
305
|
+
// from `forge ship`.
|
|
306
|
+
maybeArmWatcherAfterPush({
|
|
307
|
+
projectRoot,
|
|
308
|
+
execFn,
|
|
309
|
+
startWatcher: deps?.startWatcher,
|
|
310
|
+
railEnabled: deps?.railEnabled,
|
|
311
|
+
prLookup: deps?.prLookup,
|
|
312
|
+
});
|
|
313
|
+
|
|
188
314
|
return {
|
|
189
315
|
success: true,
|
|
190
316
|
quickMode,
|
|
@@ -193,4 +319,11 @@ module.exports = {
|
|
|
193
319
|
pushed: true,
|
|
194
320
|
};
|
|
195
321
|
},
|
|
322
|
+
|
|
323
|
+
// Exposed for unit tests; not part of the CLI surface.
|
|
324
|
+
_internal: {
|
|
325
|
+
autoFileBackingIssueForPush,
|
|
326
|
+
maybeArmWatcherAfterPush,
|
|
327
|
+
resolveOpenPrNumber,
|
|
328
|
+
},
|
|
196
329
|
};
|
package/lib/commands/ready.js
CHANGED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const memoryRouter = require('../memory/router');
|
|
4
|
+
const { stripGlobalFlags } = require('../global-flags');
|
|
5
|
+
const { fenceUntrusted } = require('../untrusted-content');
|
|
6
|
+
|
|
7
|
+
const usage = 'Usage: forge recall [query] [--kind <type>] [--limit N] [--all] [--json]';
|
|
8
|
+
|
|
9
|
+
// Reserved tag prefix that `remember --kind` writes (kernel issue 8cc1db4d). A `--kind`
|
|
10
|
+
// filter keeps only notes carrying this tag; the prefix is stripped when surfacing the
|
|
11
|
+
// derived `type` field so a note's user tags stay clean. The filter FLAG is `--kind` (NOT
|
|
12
|
+
// `--type`): `--type` is a reserved GLOBAL flag hard-validated to workflow classifications.
|
|
13
|
+
const TYPE_TAG_PREFIX = 'type:';
|
|
14
|
+
|
|
15
|
+
// When a `--kind` filter is active the tag filter runs in the command layer (the store is
|
|
16
|
+
// not reimplemented), so scan a generous window of recent notes before filtering rather than
|
|
17
|
+
// the small default page — otherwise the type match could fall outside the default limit.
|
|
18
|
+
const TYPE_FILTER_SCAN = 1000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Separate the optional positional query from `--kind <type>`, `--limit N`, and `--json`.
|
|
22
|
+
* Global flags (e.g. `-p <dir>`, `--all`) are stripped first so they never corrupt the
|
|
23
|
+
* search query (kernel issue c1e090ff). `--all` is a GLOBAL boolean flag, so the handler
|
|
24
|
+
* reads it from its `flags` argument (or the raw args on a direct call) — not from here.
|
|
25
|
+
* `--kind` is NOT a global flag, so it survives the strip and is parsed here.
|
|
26
|
+
*
|
|
27
|
+
* @param {string[]} rawArgs - Raw command arguments.
|
|
28
|
+
* @returns {{ query: string, limit: (number|undefined), type: (string|undefined), json: boolean }}
|
|
29
|
+
*/
|
|
30
|
+
function parseArgs(rawArgs) {
|
|
31
|
+
const args = stripGlobalFlags(rawArgs);
|
|
32
|
+
const words = [];
|
|
33
|
+
let limit;
|
|
34
|
+
let type;
|
|
35
|
+
let json = false;
|
|
36
|
+
|
|
37
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
38
|
+
const arg = args[index];
|
|
39
|
+
if (arg === '--json') {
|
|
40
|
+
json = true;
|
|
41
|
+
} else if (arg === '--limit') {
|
|
42
|
+
const value = Number(args[index + 1]);
|
|
43
|
+
if (Number.isInteger(value) && value > 0) {
|
|
44
|
+
limit = value;
|
|
45
|
+
index += 1;
|
|
46
|
+
}
|
|
47
|
+
} else if (arg.startsWith('--limit=')) {
|
|
48
|
+
const value = Number(arg.slice('--limit='.length));
|
|
49
|
+
if (Number.isInteger(value) && value > 0) {
|
|
50
|
+
limit = value;
|
|
51
|
+
}
|
|
52
|
+
} else if (arg === '--kind') {
|
|
53
|
+
const value = args[index + 1];
|
|
54
|
+
if (value && !value.startsWith('--')) {
|
|
55
|
+
type = value.trim();
|
|
56
|
+
index += 1;
|
|
57
|
+
}
|
|
58
|
+
} else if (arg.startsWith('--kind=')) {
|
|
59
|
+
type = arg.slice('--kind='.length).trim();
|
|
60
|
+
} else {
|
|
61
|
+
words.push(arg);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { query: words.join(' ').trim(), limit, type, json };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Derive a note's `type` from its reserved `type:` tag (undefined when untyped), so any
|
|
69
|
+
// surface — JSON and text — can show and filter by kind without exposing the tag encoding.
|
|
70
|
+
function typeOf(entry) {
|
|
71
|
+
const tag = (entry.tags || []).find(t => t.startsWith(TYPE_TAG_PREFIX));
|
|
72
|
+
return tag ? tag.slice(TYPE_TAG_PREFIX.length) : undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function withType(entry) {
|
|
76
|
+
const type = typeOf(entry);
|
|
77
|
+
return type ? { ...entry, type } : entry;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatEntry(entry) {
|
|
81
|
+
const date = entry.timestamp ? entry.timestamp.slice(0, 10) : '';
|
|
82
|
+
const prefix = date ? `${date} ` : '';
|
|
83
|
+
// The reserved `type:` tag renders as a leading `(kind)` marker, not as a raw tag, so the
|
|
84
|
+
// displayed tags stay the user's own labels.
|
|
85
|
+
const type = typeOf(entry);
|
|
86
|
+
const userTags = (entry.tags || []).filter(t => !t.startsWith(TYPE_TAG_PREFIX));
|
|
87
|
+
const tagSuffix = userTags.length > 0 ? ` [${userTags.join(', ')}]` : '';
|
|
88
|
+
const typeMarker = type ? `(${type}) ` : '';
|
|
89
|
+
// Machine/insights records are LABELED with their source so they are never mistaken for a
|
|
90
|
+
// plain human note; human `remember` notes render clean.
|
|
91
|
+
const marker = entry.machine && entry.sourceAgent ? `(${entry.sourceAgent}) ` : '';
|
|
92
|
+
// Stored note text is UNTRUSTED (a planted memory could carry injected directives),
|
|
93
|
+
// so the human/agent-facing render is provenance-fenced. The `--json` path above
|
|
94
|
+
// keeps the raw note so programmatic consumers/parsers are unaffected.
|
|
95
|
+
const note = fenceUntrusted(entry.note, { source: 'memory' });
|
|
96
|
+
return `- ${prefix}${marker}${typeMarker}${note}${tagSuffix}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function handler(args, flags, projectRoot) {
|
|
100
|
+
const { query, limit, type, json } = parseArgs(args);
|
|
101
|
+
// `--all` is a GLOBAL boolean flag: in production bin/forge.js strips it from
|
|
102
|
+
// args and sets flags.all; on a direct handler call it may still be in args.
|
|
103
|
+
const all = Boolean(flags && flags.all) || args.includes('--all');
|
|
104
|
+
|
|
105
|
+
// A `--type` filter scans a generous recent window, then keeps only matching notes — the
|
|
106
|
+
// read stays entirely in the existing store (no schema change). `--limit` is re-applied
|
|
107
|
+
// AFTER filtering so it caps the typed result set, not the pre-filter scan.
|
|
108
|
+
const recallLimit = type ? Math.max(limit ?? 0, TYPE_FILTER_SCAN) : limit;
|
|
109
|
+
const result = memoryRouter.recall(projectRoot, { query, limit: recallLimit, all });
|
|
110
|
+
let notes = result.notes.map(withType);
|
|
111
|
+
let { total, capped } = result;
|
|
112
|
+
const { scope } = result;
|
|
113
|
+
if (type) {
|
|
114
|
+
notes = notes.filter(entry => entry.type === type);
|
|
115
|
+
total = notes.length;
|
|
116
|
+
if (limit && notes.length > limit) {
|
|
117
|
+
notes = notes.slice(0, limit);
|
|
118
|
+
capped = true;
|
|
119
|
+
} else {
|
|
120
|
+
capped = false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (json) {
|
|
125
|
+
// Object (not a bare array) so programmatic consumers see the total and whether the
|
|
126
|
+
// result was truncated (raise --limit to page further).
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
output: `${JSON.stringify({ notes, total, capped, scope }, null, 2)}\n`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (notes.length === 0) {
|
|
134
|
+
const reason = query
|
|
135
|
+
? `No notes match "${query}".`
|
|
136
|
+
: 'No notes remembered yet. Use "forge remember <note>" to add one.';
|
|
137
|
+
return { success: true, output: reason };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const noun = scope === 'all' && !query ? 'stored memory record(s)' : 'remembered note(s)';
|
|
141
|
+
let header;
|
|
142
|
+
if (query) {
|
|
143
|
+
// BM25 returns at most `limit`; when full, signal it is the TOP-N, not the whole set.
|
|
144
|
+
header = capped
|
|
145
|
+
? `Top ${notes.length} note(s) matching "${query}" (raise --limit for more):`
|
|
146
|
+
: `${notes.length} note(s) matching "${query}":`;
|
|
147
|
+
} else if (capped) {
|
|
148
|
+
// Never a bare full dump: show the newest N and the true total.
|
|
149
|
+
header = `Showing ${notes.length} of ${total} ${noun} (newest first):`;
|
|
150
|
+
} else {
|
|
151
|
+
header = `${total} ${noun}:`;
|
|
152
|
+
}
|
|
153
|
+
const lines = notes.map(formatEntry);
|
|
154
|
+
return {
|
|
155
|
+
success: true,
|
|
156
|
+
output: [header, ...lines].join('\n'),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
name: 'recall',
|
|
162
|
+
description: 'Retrieve project-memory notes from the kernel-backed memory store',
|
|
163
|
+
usage,
|
|
164
|
+
flags: {
|
|
165
|
+
'--kind': 'Filter to notes of a type (decision|bugfix|gotcha|...); --type is reserved',
|
|
166
|
+
'--limit': 'Cap the number of notes returned',
|
|
167
|
+
'--all': 'Include machine/insights records in the no-query listing (query already searches all)',
|
|
168
|
+
'--json': 'Emit machine-readable JSON output',
|
|
169
|
+
},
|
|
170
|
+
handler,
|
|
171
|
+
};
|