forge-workflow 0.0.10 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +3 -0
- package/.forge/hooks/forge-native-hook.js +245 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +150 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -118
- package/QUICKSTART.md +171 -0
- package/README.md +271 -363
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +461 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/forge/TOOLCHAIN.md +670 -0
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +118 -0
- package/docs/guides/SUPPORT.md +185 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +205 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +115 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1042 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +126 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-setup.js +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +741 -77
- package/lib/commands/_manifest.js +91 -0
- package/lib/commands/_registry.js +85 -34
- package/lib/commands/_resolve-command-opts.js +261 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +192 -0
- package/lib/commands/hooks.js +242 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +598 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +17 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +523 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +436 -24
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +69 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +116 -0
- package/lib/commands/recap.js +61 -0
- package/lib/commands/recommend.js +0 -1
- package/lib/commands/release.js +91 -0
- package/lib/commands/remember.js +74 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +838 -972
- package/lib/commands/shepherd.js +436 -0
- package/lib/commands/ship.js +23 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +158 -21
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +4 -1
- package/lib/commands/test.js +43 -27
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +43 -18
- package/lib/commands/worktree.js +307 -100
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +946 -0
- package/lib/dep-guard/keyword-ripple.js +2 -2
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +35 -9
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +382 -11
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +193 -0
- package/lib/global-flags.js +74 -0
- package/lib/greptile-match.js +7 -63
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +451 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/kernel/backing-issue.js +305 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2504 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +3 -2
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +157 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1273 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +99 -497
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2089 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +241 -20
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/scoring.js +17 -3
- package/lib/status/beads-snapshot.js +45 -2
- package/lib/status/presenter.js +169 -18
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +199 -0
- package/lib/workflow/enforce-stage.js +296 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +11 -22
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +37 -35
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +81 -57
- package/scripts/beads-upgrade-smoke.sh +24 -3
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard.sh +22 -3
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +49 -84
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +50 -83
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +21 -94
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status.sh +60 -57
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +13 -6
- package/scripts/test.js +95 -12
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -89
- package/.github/workflows/github-to-beads.yml +0 -100
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -281
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-bootstrap.js +0 -225
- package/lib/beads-health-check.js +0 -188
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -584
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -356
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -132
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
package/bin/forge.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* bunx forge setup --agents claude,cursor
|
|
14
14
|
*
|
|
15
15
|
* CLI Flags:
|
|
16
|
-
* --path, -p <dir> Target project directory (
|
|
16
|
+
* --path, -p <dir> Target project directory (docs verify|detect require existing dir)
|
|
17
17
|
* --quick, -q Use all defaults, minimal prompts
|
|
18
18
|
* --skip-external Skip external services configuration
|
|
19
19
|
* --agents <list> Specify agents (--agents claude cursor OR --agents=claude,cursor)
|
|
@@ -38,33 +38,41 @@
|
|
|
38
38
|
const fs = require('node:fs');
|
|
39
39
|
const path = require('node:path');
|
|
40
40
|
const readline = require('node:readline');
|
|
41
|
-
const { execSync
|
|
41
|
+
const { execSync } = require('node:child_process');
|
|
42
42
|
|
|
43
|
-
// Get version from package.json (single source of truth)
|
|
43
|
+
// Get version from package.json (single source of truth). packageDir stays a
|
|
44
|
+
// runtime value for asset copying, but the require is a static relative path so
|
|
45
|
+
// `bun build --compile` can bundle package.json.
|
|
44
46
|
const packageDir = path.dirname(__dirname);
|
|
45
|
-
const packageJson = require(
|
|
47
|
+
const packageJson = require('../package.json');
|
|
46
48
|
const VERSION = packageJson.version;
|
|
47
49
|
|
|
48
50
|
// Load PluginManager for discoverable agent architecture
|
|
49
51
|
const PluginManager = require('../lib/plugin-manager');
|
|
50
|
-
const {
|
|
52
|
+
const { populateAgentSkills, listCanonicalSkills } = require('../lib/skills-sync');
|
|
51
53
|
const { copyEssentialDocs } = require('../lib/docs-copy');
|
|
52
|
-
const {
|
|
54
|
+
const {
|
|
55
|
+
listTopics,
|
|
56
|
+
getTopicContent,
|
|
57
|
+
validateDocs,
|
|
58
|
+
formatDocsValidation,
|
|
59
|
+
writeDocsBaseline,
|
|
60
|
+
} = require('../lib/docs-command');
|
|
53
61
|
const { resetSoft, resetHard, reinstall } = require('../lib/reset');
|
|
54
62
|
const { loadCommands, executeCommand } = require('../lib/commands/_registry');
|
|
63
|
+
const { resolveCommandOpts } = require('../lib/commands/_resolve-command-opts');
|
|
64
|
+
const { getPackageRoot } = require('../lib/package-root');
|
|
55
65
|
const { enforceStageEntry } = require('../lib/workflow/enforce-stage');
|
|
56
66
|
const { normalizeStageId } = require('../lib/workflow/stages');
|
|
57
67
|
|
|
58
|
-
// Load enhanced onboarding modules
|
|
59
|
-
const contextMerge = require(
|
|
60
|
-
const projectDiscovery = require(
|
|
68
|
+
// Load enhanced onboarding modules (static relative requires — bundleable)
|
|
69
|
+
const contextMerge = require('../lib/context-merge');
|
|
70
|
+
const projectDiscovery = require('../lib/project-discovery');
|
|
61
71
|
|
|
62
72
|
// Load lib modules for symlink, beads, and PAT setup
|
|
63
|
-
const { createSymlinkOrCopy: libCreateSymlinkOrCopy } = require(
|
|
64
|
-
const beadsSetupLib = require(
|
|
65
|
-
const {
|
|
66
|
-
const { setupPAT } = require(path.join(packageDir, 'lib', 'pat-setup'));
|
|
67
|
-
const { DEFAULT_BEADS_VERSION, detectDefaultBranch, templateWorkflows, scaffoldBeadsSync } = require(path.join(packageDir, 'lib', 'beads-sync-scaffold'));
|
|
73
|
+
const { createSymlinkOrCopy: libCreateSymlinkOrCopy } = require('../lib/symlink-utils');
|
|
74
|
+
const beadsSetupLib = require('../lib/beads-setup');
|
|
75
|
+
const { scaffoldBeadsSync } = require('../lib/beads-sync-scaffold');
|
|
68
76
|
|
|
69
77
|
// Load incremental setup modules
|
|
70
78
|
const { detectEnvironment } = require('../lib/detect-agent');
|
|
@@ -74,6 +82,12 @@ const { ActionCollector, isNonInteractive } = require('../lib/setup-utils');
|
|
|
74
82
|
const { renderSetupSummary } = require('../lib/setup-summary-renderer');
|
|
75
83
|
const { smartMergeAgentsMd } = require('../lib/smart-merge');
|
|
76
84
|
const { checkLefthookStatus } = require('../lib/lefthook-check');
|
|
85
|
+
const {
|
|
86
|
+
FORGE_USER_LEFTHOOK_YML,
|
|
87
|
+
forgeShouldWriteLefthookConfig,
|
|
88
|
+
installNativeGitHooks,
|
|
89
|
+
verifyHooksActive,
|
|
90
|
+
} = require('../lib/lefthook-wiring');
|
|
77
91
|
const { detectHusky, migrateHusky } = require('../lib/husky-migration');
|
|
78
92
|
// workflowProfiles: module exists and is tested but not yet wired into setup flow
|
|
79
93
|
// Will be activated when workflow profile selection is added to interactive setup
|
|
@@ -94,37 +108,10 @@ let actionLog = new SetupActionLog();
|
|
|
94
108
|
// Detected package manager
|
|
95
109
|
let PKG_MANAGER = 'npm';
|
|
96
110
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
* @param {string[]} args - Command arguments
|
|
102
|
-
* @param {object} options - execFileSync options
|
|
103
|
-
*/
|
|
104
|
-
function secureExecFileSync(command, args = [], options = {}) {
|
|
105
|
-
try {
|
|
106
|
-
// Resolve command's full path to validate it's in a trusted location
|
|
107
|
-
const isWindows = process.platform === 'win32';
|
|
108
|
-
const pathResolver = isWindows ? 'where.exe' : 'which';
|
|
109
|
-
|
|
110
|
-
const result = spawnSync(pathResolver, [command], {
|
|
111
|
-
encoding: 'utf8',
|
|
112
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
if (result.status === 0 && result.stdout) {
|
|
116
|
-
// Command found - use resolved path for execution
|
|
117
|
-
// Handle both CRLF (Windows) and LF (Unix) line endings
|
|
118
|
-
const resolvedPath = result.stdout.trim().split(/\r?\n/)[0].trim();
|
|
119
|
-
return execFileSync(resolvedPath, args, options);
|
|
120
|
-
}
|
|
121
|
-
} catch (_err) { // NOSONAR - S2486: Intentionally ignored; falls back to direct command execution below
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Fallback: execute with command name (maintains compatibility)
|
|
125
|
-
// This is safe for our use case as we only execute known, hardcoded commands
|
|
126
|
-
return execFileSync(command, args, options);
|
|
127
|
-
}
|
|
111
|
+
// Securely execute a command with PATH validation (SonarCloud S4036).
|
|
112
|
+
// Delegates to the shared helper, which also handles Windows npm/npx/lefthook
|
|
113
|
+
// cmd shims that cannot be spawned directly (kernel issue 9997d516).
|
|
114
|
+
const { secureExecFileSync } = require('../lib/shell-utils');
|
|
128
115
|
|
|
129
116
|
/**
|
|
130
117
|
* Load agent definitions from plugin architecture
|
|
@@ -146,7 +133,8 @@ function loadAgentsFromPlugins() {
|
|
|
146
133
|
customSetup: plugin.setup?.customSetup || '',
|
|
147
134
|
needsConversion: plugin.setup?.needsConversion || false,
|
|
148
135
|
copyCommands: plugin.setup?.copyCommands || false,
|
|
149
|
-
promptFormat: plugin.setup?.promptFormat || false
|
|
136
|
+
promptFormat: plugin.setup?.promptFormat || false,
|
|
137
|
+
skillsDir: plugin.directories?.skills || null
|
|
150
138
|
};
|
|
151
139
|
});
|
|
152
140
|
|
|
@@ -295,25 +283,6 @@ function _checkWritePermission(filePath) {
|
|
|
295
283
|
}
|
|
296
284
|
}
|
|
297
285
|
|
|
298
|
-
/**
|
|
299
|
-
* Reads workflow command names from .claude/commands/*.md in the package directory.
|
|
300
|
-
* @returns {string[]} Command names (filenames without .md extension)
|
|
301
|
-
*/
|
|
302
|
-
function getWorkflowCommands() {
|
|
303
|
-
const commandsDir = path.join(packageDir, '.claude', 'commands');
|
|
304
|
-
try {
|
|
305
|
-
return fs.readdirSync(commandsDir)
|
|
306
|
-
.filter(f => f.endsWith('.md'))
|
|
307
|
-
.map(f => f.replace(/\.md$/, ''));
|
|
308
|
-
} catch (err) {
|
|
309
|
-
if (err.code === 'ENOENT') {
|
|
310
|
-
console.warn(`Warning: .claude/commands directory not found at ${commandsDir}`);
|
|
311
|
-
} else {
|
|
312
|
-
console.warn(`Warning: failed to read .claude/commands — ${err.code}: ${err.message}`);
|
|
313
|
-
}
|
|
314
|
-
return [];
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
286
|
|
|
318
287
|
|
|
319
288
|
// Helper function to safely execute commands (no user input)
|
|
@@ -432,72 +401,26 @@ function checkPrerequisites() {
|
|
|
432
401
|
return { errors, warnings };
|
|
433
402
|
}
|
|
434
403
|
|
|
435
|
-
// Universal SKILL.md content
|
|
436
|
-
const SKILL_CONTENT = `---
|
|
437
|
-
name: forge-workflow
|
|
438
|
-
description: 7-stage TDD-first workflow for feature development. Use when building features, fixing bugs, or shipping PRs.
|
|
439
|
-
category: Development Workflow
|
|
440
|
-
tags: [tdd, workflow, pr, git, testing]
|
|
441
|
-
tools: [Bash, Read, Write, Edit, Grep, Glob]
|
|
442
|
-
---
|
|
443
|
-
|
|
444
|
-
# Forge Workflow Skill
|
|
445
|
-
|
|
446
|
-
A TDD-first workflow for AI coding agents. Ship features with confidence.
|
|
447
|
-
|
|
448
|
-
## When to Use
|
|
449
|
-
|
|
450
|
-
Automatically invoke this skill when the user wants to:
|
|
451
|
-
- Build a new feature
|
|
452
|
-
- Fix a bug
|
|
453
|
-
- Create a pull request
|
|
454
|
-
- Run the development workflow
|
|
455
|
-
|
|
456
|
-
## 7 Stages
|
|
457
|
-
|
|
458
|
-
| Stage | Command | Description |
|
|
459
|
-
|-------|---------|-------------|
|
|
460
|
-
| utility | \`/status\` | Check current context, active work, recent completions |
|
|
461
|
-
| 1 | \`/plan\` | Design intent -> research -> branch + worktree + task list |
|
|
462
|
-
| 2 | \`/dev\` | TDD development (implementer -> spec review -> quality review) |
|
|
463
|
-
| 3 | \`/validate\` | Type check, lint, security, tests - all fresh output |
|
|
464
|
-
| 4 | \`/ship\` | Push branch and create PR with full documentation |
|
|
465
|
-
| 5 | \`/review\` | Address ALL PR feedback (GitHub Actions, Greptile, SonarCloud) |
|
|
466
|
-
| 6 | \`/premerge\` | Update docs, hand off PR to user |
|
|
467
|
-
| 7 | \`/verify\` | Post-merge health check (CI on main, close Beads) |
|
|
468
|
-
|
|
469
|
-
## Workflow Flow
|
|
470
|
-
|
|
471
|
-
\`\`\`
|
|
472
|
-
/status -> /plan -> /dev -> /validate -> /ship -> /review -> /premerge -> /verify
|
|
473
|
-
\`\`\`
|
|
474
|
-
|
|
475
|
-
## Core Principles
|
|
476
|
-
|
|
477
|
-
- **TDD-First**: Write tests BEFORE implementation (RED-GREEN-REFACTOR)
|
|
478
|
-
- **Research-First**: Understand before building, document decisions
|
|
479
|
-
- **Security Built-In**: OWASP Top 10 analysis for every feature
|
|
480
|
-
- **Documentation Progressive**: Update at each stage, verify at end
|
|
481
|
-
`;
|
|
482
404
|
|
|
483
405
|
// Cursor MDC rule content
|
|
484
406
|
const CURSOR_RULE = `---
|
|
485
|
-
description: Forge
|
|
407
|
+
description: Forge TDD Workflow Template
|
|
486
408
|
alwaysApply: true
|
|
487
409
|
---
|
|
488
410
|
|
|
489
411
|
# Forge Workflow Commands
|
|
490
412
|
|
|
491
|
-
Use these commands via \`/command-name\`:
|
|
413
|
+
Use these default-template commands via \`/command-name\`:
|
|
492
414
|
|
|
493
415
|
- \`/status\` (utility) - Check current context, active work, recent completions
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
416
|
+
- \`/plan\` - Design intent Q&A, research, branch + task list
|
|
417
|
+
- \`/dev\` - Subagent-driven TDD per task (spec + quality review)
|
|
418
|
+
- \`/validate\` - Type check, lint, security, tests (HARD-GATE)
|
|
419
|
+
- \`/ship\` - Push and create PR with design doc reference
|
|
420
|
+
- \`/review\` - Handle ALL PR issues (Actions, Greptile, SonarCloud)
|
|
421
|
+
- \`/verify\` - Post-merge health check (CI on main, close Beads)
|
|
422
|
+
|
|
423
|
+
Pre-merge gate (not a numbered stage): finish docs + confirm CI green + hand off the PR — embedded in the /ship and /review stages.
|
|
501
424
|
|
|
502
425
|
See AGENTS.md for full workflow details.
|
|
503
426
|
`;
|
|
@@ -543,17 +466,6 @@ function writeFile(filePath, content) {
|
|
|
543
466
|
}
|
|
544
467
|
}
|
|
545
468
|
|
|
546
|
-
function readFile(filePath) {
|
|
547
|
-
try {
|
|
548
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
549
|
-
} catch (err) {
|
|
550
|
-
if (process.env.DEBUG) {
|
|
551
|
-
console.warn(` ⚠ Could not read ${filePath}: ${err.message}`);
|
|
552
|
-
}
|
|
553
|
-
return null;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
469
|
function copyFile(src, dest) {
|
|
558
470
|
try {
|
|
559
471
|
const destPath = path.resolve(projectRoot, dest);
|
|
@@ -612,11 +524,6 @@ function createSymlinkOrCopy(source, target, options = {}) {
|
|
|
612
524
|
return libCreateSymlinkOrCopy(fullSource, fullTarget, options);
|
|
613
525
|
}
|
|
614
526
|
|
|
615
|
-
function stripFrontmatter(content) {
|
|
616
|
-
const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
|
|
617
|
-
return match ? match[1] : content;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
527
|
// Read existing .env.local
|
|
621
528
|
function readEnvFile() {
|
|
622
529
|
const envPath = path.join(projectRoot, '.env.local');
|
|
@@ -781,7 +688,6 @@ async function detectProjectStatus() {
|
|
|
781
688
|
type: 'fresh', // 'fresh', 'upgrade', or 'partial'
|
|
782
689
|
hasAgentsMd: fs.existsSync(path.join(projectRoot, 'AGENTS.md')),
|
|
783
690
|
hasClaudeMd: fs.existsSync(path.join(projectRoot, 'CLAUDE.md')),
|
|
784
|
-
hasClaudeCommands: fs.existsSync(path.join(projectRoot, '.claude/commands')),
|
|
785
691
|
hasEnvLocal: fs.existsSync(path.join(projectRoot, '.env.local')),
|
|
786
692
|
existingEnvVars: {},
|
|
787
693
|
agentsMdSize: 0,
|
|
@@ -815,9 +721,9 @@ async function detectProjectStatus() {
|
|
|
815
721
|
}
|
|
816
722
|
|
|
817
723
|
// Determine installation type
|
|
818
|
-
if (status.hasAgentsMd
|
|
724
|
+
if (status.hasAgentsMd) {
|
|
819
725
|
status.type = 'upgrade'; // Full forge installation exists
|
|
820
|
-
} else if (status.
|
|
726
|
+
} else if (status.hasEnvLocal) {
|
|
821
727
|
status.type = 'partial'; // Agent-specific files exist (not just base files from postinstall)
|
|
822
728
|
}
|
|
823
729
|
// else: 'fresh' - new installation (or just postinstall baseline with AGENTS.md)
|
|
@@ -1665,7 +1571,6 @@ function displayMcpStatus(selectedAgents) {
|
|
|
1665
1571
|
// Show manual setup instructions for GUI-based agents
|
|
1666
1572
|
const manualMcpMap = {
|
|
1667
1573
|
cursor: 'Cursor: Configure via Cursor Settings > MCP',
|
|
1668
|
-
cline: 'Cline: Install via MCP Marketplace',
|
|
1669
1574
|
};
|
|
1670
1575
|
const needsManualMcp = Object.entries(manualMcpMap)
|
|
1671
1576
|
.filter(([key]) => selectedAgents.includes(key))
|
|
@@ -1748,39 +1653,6 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
|
|
|
1748
1653
|
const { added, preserved } = writeEnvTokens(tokens, true);
|
|
1749
1654
|
displayEnvTokenResults(added, preserved);
|
|
1750
1655
|
|
|
1751
|
-
// GitHub-Beads issue sync setup
|
|
1752
|
-
console.log('');
|
|
1753
|
-
const enableSync = await askYesNo(question, 'Enable GitHub ↔ Beads issue sync?', true);
|
|
1754
|
-
if (enableSync) {
|
|
1755
|
-
try {
|
|
1756
|
-
const result = await scaffoldGithubBeadsSync(projectRoot, packageDir);
|
|
1757
|
-
for (const f of result.created) {
|
|
1758
|
-
console.log(` Created: ${f}`);
|
|
1759
|
-
}
|
|
1760
|
-
for (const f of result.skipped) {
|
|
1761
|
-
console.log(` Skipped: ${f} (already exists)`);
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
// PAT setup guidance for Beads sync (non-fatal)
|
|
1765
|
-
// Skip if --sync flag is set — handleSyncScaffold will handle PAT setup
|
|
1766
|
-
if (!SYNC_ENABLED) {
|
|
1767
|
-
try {
|
|
1768
|
-
const patResult = setupPAT(projectRoot, { interactive: !NON_INTERACTIVE });
|
|
1769
|
-
if (patResult.success) {
|
|
1770
|
-
console.log(' ✓ Beads sync PAT configured');
|
|
1771
|
-
} else if (patResult.reminder) {
|
|
1772
|
-
console.log(` ℹ ${patResult.reminder}`);
|
|
1773
|
-
} else if (patResult.instructions) {
|
|
1774
|
-
console.log(` ℹ ${patResult.instructions.split('\n')[0]}`);
|
|
1775
|
-
}
|
|
1776
|
-
} catch (_patErr) {
|
|
1777
|
-
// PAT setup is best-effort — don't block sync scaffold
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
} catch (err) {
|
|
1781
|
-
console.error(` Error scaffolding GitHub-Beads sync: ${err.message}`);
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
1784
1656
|
}
|
|
1785
1657
|
|
|
1786
1658
|
// Display the Forge banner
|
|
@@ -1891,26 +1763,20 @@ function minimalInstall() {
|
|
|
1891
1763
|
console.log('');
|
|
1892
1764
|
}
|
|
1893
1765
|
|
|
1894
|
-
//
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
cmds.forEach(cmd => {
|
|
1903
|
-
const src = path.join(packageDir, `.claude/commands/${cmd}.md`);
|
|
1904
|
-
if (copyFile(src, `.claude/commands/${cmd}.md`)) copied++;
|
|
1905
|
-
});
|
|
1906
|
-
console.log(` Copied: ${copied} workflow commands`);
|
|
1907
|
-
}
|
|
1766
|
+
// ⚠️ LEGACY / DEAD SETUP PATH ⚠️
|
|
1767
|
+
// The functions below (setupClaudeAgent, setupCursorAgent, copyAgentRules,
|
|
1768
|
+
// setupClaudeMcpConfig, setupAgent, quickSetup, dryRunSetup, …) are the OLD
|
|
1769
|
+
// inline setup implementation. `forge setup` is now the registry command in
|
|
1770
|
+
// lib/commands/setup.js, which takes priority in the dispatcher and `return`s
|
|
1771
|
+
// before the inline `if (command === 'setup')` block is ever reached. This copy
|
|
1772
|
+
// is kept only until the inline setup subsystem is fully removed — the authority
|
|
1773
|
+
// is lib/commands/setup.js. Do not add behavior here; edit lib/commands/setup.js.
|
|
1908
1774
|
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
//
|
|
1775
|
+
// Helper: Setup Claude agent
|
|
1776
|
+
function setupClaudeAgent() {
|
|
1777
|
+
// Skills-only surface: per-skill SKILL.md dirs are populated by createAgentSkill.
|
|
1778
|
+
// Claude receives policy via CLAUDE.md → AGENTS.md, NOT an always-on
|
|
1779
|
+
// `.claude/rules/workflow.md` (that canonical file was removed). Only scripts copy.
|
|
1914
1780
|
const scriptSrc = path.join(packageDir, '.claude/scripts/load-env.sh');
|
|
1915
1781
|
copyFile(scriptSrc, '.claude/scripts/load-env.sh');
|
|
1916
1782
|
}
|
|
@@ -1921,61 +1787,23 @@ function setupCursorAgent() {
|
|
|
1921
1787
|
console.log(' Created: .cursor/rules/forge-workflow.mdc');
|
|
1922
1788
|
}
|
|
1923
1789
|
|
|
1924
|
-
// Helper:
|
|
1925
|
-
function
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
if (agent.needsConversion) {
|
|
1930
|
-
targetContent = stripFrontmatter(content);
|
|
1790
|
+
// Helper: Create skill file for agent
|
|
1791
|
+
function createAgentSkill(agent, agentKey) {
|
|
1792
|
+
if (agentKey === 'codex') {
|
|
1793
|
+
// Codex global skill install is handled via buildCodexSkillInstallPlan in setup.js
|
|
1794
|
+
return;
|
|
1931
1795
|
}
|
|
1932
1796
|
|
|
1933
|
-
if (agent.
|
|
1934
|
-
targetFile = cmd.replace('.md', '.prompt.md');
|
|
1935
|
-
targetContent = stripFrontmatter(content);
|
|
1936
|
-
}
|
|
1797
|
+
if (!agent.hasSkill || !agent.skillsDir) return;
|
|
1937
1798
|
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
if (!agent.needsConversion && !agent.copyCommands && !agent.promptFormat) return;
|
|
1945
|
-
|
|
1946
|
-
Object.entries(claudeCommands).forEach(([cmd, content]) => {
|
|
1947
|
-
const { targetFile, targetContent } = convertCommandToAgentFormat(cmd, content, agent);
|
|
1948
|
-
const targetDir = agent.dirs[0]; // First dir is commands/workflows
|
|
1949
|
-
writeFile(`${targetDir}/${targetFile}`, targetContent);
|
|
1799
|
+
// Skills-only surface: populate every canonical skill into the agent skills dir
|
|
1800
|
+
// (.claude/skills, .cursor/skills) from the packaged canonical `skills/` source.
|
|
1801
|
+
const { written } = populateAgentSkills({
|
|
1802
|
+
sourceRoot: packageDir,
|
|
1803
|
+
targetSkillsDir: path.join(projectRoot, agent.skillsDir),
|
|
1804
|
+
clean: true,
|
|
1950
1805
|
});
|
|
1951
|
-
console.log(`
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
// Helper: Copy rules for agent
|
|
1955
|
-
function copyAgentRules(agent) {
|
|
1956
|
-
if (!agent.needsConversion) return;
|
|
1957
|
-
|
|
1958
|
-
const workflowMdPath = path.join(projectRoot, '.claude/rules/workflow.md');
|
|
1959
|
-
if (!fs.existsSync(workflowMdPath)) return;
|
|
1960
|
-
|
|
1961
|
-
const rulesDir = agent.dirs.find(d => d.includes('/rules'));
|
|
1962
|
-
if (!rulesDir) return;
|
|
1963
|
-
|
|
1964
|
-
const ruleContent = readFile(workflowMdPath);
|
|
1965
|
-
if (ruleContent) {
|
|
1966
|
-
writeFile(`${rulesDir}/workflow.md`, ruleContent);
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
// Helper: Create skill file for agent
|
|
1971
|
-
function createAgentSkill(agent) {
|
|
1972
|
-
if (!agent.hasSkill) return;
|
|
1973
|
-
|
|
1974
|
-
const skillDir = agent.dirs.find(d => d.includes('/skills/'));
|
|
1975
|
-
if (skillDir) {
|
|
1976
|
-
writeFile(`${skillDir}/SKILL.md`, SKILL_CONTENT);
|
|
1977
|
-
console.log(' Created: forge-workflow skill');
|
|
1978
|
-
}
|
|
1806
|
+
console.log(` Created: ${written.length} skills in ${agent.skillsDir}/`);
|
|
1979
1807
|
}
|
|
1980
1808
|
|
|
1981
1809
|
// Helper: Setup MCP config for Claude
|
|
@@ -2010,7 +1838,7 @@ function createAgentLinkFile(agent, symlinkOnly = false) {
|
|
|
2010
1838
|
}
|
|
2011
1839
|
|
|
2012
1840
|
// Setup specific agent
|
|
2013
|
-
function setupAgent(agentKey,
|
|
1841
|
+
function setupAgent(agentKey, skipFiles = {}) {
|
|
2014
1842
|
const agent = AGENTS[agentKey];
|
|
2015
1843
|
if (!agent) return;
|
|
2016
1844
|
|
|
@@ -2028,14 +1856,8 @@ function setupAgent(agentKey, claudeCommands, skipFiles = {}) {
|
|
|
2028
1856
|
setupCursorAgent();
|
|
2029
1857
|
}
|
|
2030
1858
|
|
|
2031
|
-
// Convert/copy commands
|
|
2032
|
-
copyAgentCommands(agent, claudeCommands);
|
|
2033
|
-
|
|
2034
|
-
// Copy rules if needed
|
|
2035
|
-
copyAgentRules(agent);
|
|
2036
|
-
|
|
2037
1859
|
// Create SKILL.md
|
|
2038
|
-
createAgentSkill(agent);
|
|
1860
|
+
createAgentSkill(agent, agentKey);
|
|
2039
1861
|
|
|
2040
1862
|
// Setup MCP configs
|
|
2041
1863
|
if (agentKey === 'claude') {
|
|
@@ -2069,7 +1891,6 @@ function displayInstallationStatus(projectStatus) {
|
|
|
2069
1891
|
}
|
|
2070
1892
|
|
|
2071
1893
|
if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
|
|
2072
|
-
if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
|
|
2073
1894
|
if (projectStatus.hasEnvLocal) console.log(' - .env.local');
|
|
2074
1895
|
console.log('');
|
|
2075
1896
|
}
|
|
@@ -2133,7 +1954,6 @@ async function promptForFileOverwrite(question, fileType, exists, skipFiles) {
|
|
|
2133
1954
|
|
|
2134
1955
|
const fileLabels = {
|
|
2135
1956
|
agentsMd: { prompt: 'Found existing AGENTS.md. Overwrite?', message: 'AGENTS.md', key: 'agentsMd' },
|
|
2136
|
-
claudeCommands: { prompt: 'Found existing .claude/commands/. Overwrite?', message: '.claude/commands/', key: 'claudeCommands' }
|
|
2137
1957
|
};
|
|
2138
1958
|
|
|
2139
1959
|
const config = fileLabels[fileType];
|
|
@@ -2308,35 +2128,12 @@ async function installAgentsMd(skipFiles) {
|
|
|
2308
2128
|
}
|
|
2309
2129
|
}
|
|
2310
2130
|
|
|
2311
|
-
/**
|
|
2312
|
-
* Load Claude commands for conversion
|
|
2313
|
-
*/
|
|
2314
|
-
function loadClaudeCommands(selectedAgents) {
|
|
2315
|
-
const claudeCommands = {};
|
|
2316
|
-
const needsClaudeCommands = selectedAgents.includes('claude') ||
|
|
2317
|
-
selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands);
|
|
2318
|
-
|
|
2319
|
-
if (!needsClaudeCommands) {
|
|
2320
|
-
return claudeCommands;
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
getWorkflowCommands().forEach(cmd => {
|
|
2324
|
-
const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
|
|
2325
|
-
const content = readFile(cmdPath);
|
|
2326
|
-
if (content) {
|
|
2327
|
-
claudeCommands[`${cmd}.md`] = content;
|
|
2328
|
-
}
|
|
2329
|
-
});
|
|
2330
|
-
|
|
2331
|
-
return claudeCommands;
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
2131
|
/**
|
|
2335
2132
|
* Setup agents with progress indication
|
|
2336
2133
|
* Delegates to setupSelectedAgents to avoid duplicate implementations (S4144)
|
|
2337
2134
|
*/
|
|
2338
|
-
function setupAgentsWithProgress(selectedAgents,
|
|
2339
|
-
setupSelectedAgents(selectedAgents,
|
|
2135
|
+
function setupAgentsWithProgress(selectedAgents, skipFiles) {
|
|
2136
|
+
setupSelectedAgents(selectedAgents, skipFiles);
|
|
2340
2137
|
}
|
|
2341
2138
|
|
|
2342
2139
|
/**
|
|
@@ -2351,15 +2148,11 @@ function displaySetupSummary(selectedAgents) {
|
|
|
2351
2148
|
console.log('What\'s installed:');
|
|
2352
2149
|
console.log(' - AGENTS.md (universal instructions)');
|
|
2353
2150
|
|
|
2354
|
-
const workflowCount = getWorkflowCommands().length;
|
|
2355
2151
|
selectedAgents.forEach(key => {
|
|
2356
2152
|
const agent = AGENTS[key];
|
|
2357
2153
|
if (agent.linkFile) {
|
|
2358
2154
|
console.log(` - ${agent.linkFile} (${agent.name})`);
|
|
2359
2155
|
}
|
|
2360
|
-
if (agent.hasCommands) {
|
|
2361
|
-
console.log(` - .claude/commands/ (${workflowCount} workflow commands)`);
|
|
2362
|
-
}
|
|
2363
2156
|
if (agent.hasSkill) {
|
|
2364
2157
|
const skillDir = agent.dirs.find(d => d.includes('/skills/'));
|
|
2365
2158
|
if (skillDir) {
|
|
@@ -2464,12 +2257,10 @@ async function _interactiveSetup() {
|
|
|
2464
2257
|
// Track which files to skip based on user choices
|
|
2465
2258
|
const skipFiles = {
|
|
2466
2259
|
agentsMd: false,
|
|
2467
|
-
claudeCommands: false
|
|
2468
2260
|
};
|
|
2469
2261
|
|
|
2470
2262
|
// Ask about overwriting existing files
|
|
2471
2263
|
await promptForFileOverwrite(question, 'agentsMd', projectStatus.hasAgentsMd, skipFiles);
|
|
2472
|
-
await promptForFileOverwrite(question, 'claudeCommands', projectStatus.hasClaudeCommands, skipFiles);
|
|
2473
2264
|
|
|
2474
2265
|
if (projectStatus.type !== 'fresh') {
|
|
2475
2266
|
console.log('');
|
|
@@ -2492,19 +2283,13 @@ async function _interactiveSetup() {
|
|
|
2492
2283
|
setupCoreDocs();
|
|
2493
2284
|
console.log('');
|
|
2494
2285
|
|
|
2495
|
-
//
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
// First ensure Claude is set up
|
|
2499
|
-
if (selectedAgents.includes('claude')) {
|
|
2500
|
-
setupAgent('claude', null, skipFiles);
|
|
2501
|
-
}
|
|
2502
|
-
// Then load the commands
|
|
2503
|
-
claudeCommands = loadClaudeCommands(selectedAgents);
|
|
2286
|
+
// Setup Claude first if selected, then remaining agents
|
|
2287
|
+
if (selectedAgents.includes('claude')) {
|
|
2288
|
+
setupAgent('claude', skipFiles);
|
|
2504
2289
|
}
|
|
2505
2290
|
|
|
2506
2291
|
// Setup each selected agent with progress indication
|
|
2507
|
-
setupAgentsWithProgress(selectedAgents,
|
|
2292
|
+
setupAgentsWithProgress(selectedAgents, skipFiles);
|
|
2508
2293
|
|
|
2509
2294
|
// =============================================
|
|
2510
2295
|
// STEP 2: Project Tools Setup
|
|
@@ -2530,6 +2315,16 @@ async function _interactiveSetup() {
|
|
|
2530
2315
|
}
|
|
2531
2316
|
|
|
2532
2317
|
// Parse CLI flags
|
|
2318
|
+
// Issue command aliases hidden from `forge --help`: the bare passthroughs that
|
|
2319
|
+
// duplicate `forge issue <sub>` plus the plural `issues`. They stay registered and
|
|
2320
|
+
// routable (back-compat), but are omitted from help so `forge issue` reads as the
|
|
2321
|
+
// single canonical issue surface (kernel issue 450c6e34). The canonical `issue` is
|
|
2322
|
+
// deliberately NOT listed here — it stays documented.
|
|
2323
|
+
const ISSUE_ALIAS_COMMANDS = [
|
|
2324
|
+
'create', 'update', 'claim', 'close', 'show', 'list',
|
|
2325
|
+
'ready', 'blocked', 'stale', 'orphans', 'lint', 'claims', 'issues',
|
|
2326
|
+
];
|
|
2327
|
+
|
|
2533
2328
|
function parseFlags() {
|
|
2534
2329
|
const flags = {
|
|
2535
2330
|
quick: false,
|
|
@@ -2554,7 +2349,8 @@ function parseFlags() {
|
|
|
2554
2349
|
|
|
2555
2350
|
// Issue passthrough commands delegate all flags to bd.
|
|
2556
2351
|
// Skip global parsing so flags like --type, -p, --help reach the handler intact.
|
|
2557
|
-
|
|
2352
|
+
// Canonical `issue` plus the hidden back-compat aliases (single source of truth).
|
|
2353
|
+
const issuePassthroughCommands = [...ISSUE_ALIAS_COMMANDS, 'issue'];
|
|
2558
2354
|
if (issuePassthroughCommands.includes(args[0])) {
|
|
2559
2355
|
return flags;
|
|
2560
2356
|
}
|
|
@@ -2757,6 +2553,7 @@ function showHelp() {
|
|
|
2757
2553
|
console.log('Options:');
|
|
2758
2554
|
console.log(' --path, -p <dir> Target project directory (default: current directory)');
|
|
2759
2555
|
console.log(' Creates the directory if it doesn\'t exist');
|
|
2556
|
+
console.log(' Exception: docs verify|detect --path requires an existing directory');
|
|
2760
2557
|
console.log(' --quick, -q Use all defaults, minimal prompts');
|
|
2761
2558
|
console.log(' Auto-selects: all agents, GitHub Code Quality, ESLint');
|
|
2762
2559
|
console.log(' --skip-external Skip external services configuration');
|
|
@@ -2802,9 +2599,29 @@ function showHelp() {
|
|
|
2802
2599
|
console.log('Also works with bun:');
|
|
2803
2600
|
console.log(' bunx forge setup --quick');
|
|
2804
2601
|
console.log('');
|
|
2805
|
-
|
|
2806
|
-
//
|
|
2602
|
+
// Adoption profile + gate/classification config is owned by `forge init`, not
|
|
2603
|
+
// `forge setup`. The --minimal/--standard/--full profile flags are `forge init`
|
|
2604
|
+
// shortcuts (setup only reads them for `--dry-run` preview), so point there.
|
|
2605
|
+
console.log('Adoption profile, gates & classification (configured by `forge init`, not setup):');
|
|
2606
|
+
console.log(' forge init --minimal | --standard | --full # adoption-profile shortcut');
|
|
2607
|
+
console.log(' # (or: --profile minimal|standard|full)');
|
|
2608
|
+
console.log(' forge init --classification critical|standard|refactor');
|
|
2609
|
+
console.log(' `forge init` writes `.forge/config.yaml` (gate + classification config).');
|
|
2610
|
+
console.log(' Run `forge init --help` for all profile/classification/harness flags.');
|
|
2611
|
+
console.log('');
|
|
2612
|
+
|
|
2613
|
+
// Append auto-discovered registry commands. Hidden issue aliases (the bare
|
|
2614
|
+
// passthroughs + plural `issues`, plus any command self-declaring `hidden: true`)
|
|
2615
|
+
// still route and execute, but are trimmed from this enumeration so `forge issue`
|
|
2616
|
+
// reads as the single canonical issue surface. This registry instance is loaded
|
|
2617
|
+
// solely to render help, so deleting from its Map only affects the printed list —
|
|
2618
|
+
// command dispatch (main) uses a separate registry and is unaffected.
|
|
2807
2619
|
const helpRegistry = loadCommands(path.join(__dirname, '..', 'lib', 'commands'));
|
|
2620
|
+
for (const [name, cmd] of [...helpRegistry.commands]) {
|
|
2621
|
+
if (ISSUE_ALIAS_COMMANDS.includes(name) || cmd.hidden === true) {
|
|
2622
|
+
helpRegistry.commands.delete(name);
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2808
2625
|
const registryHelp = helpRegistry.getHelp();
|
|
2809
2626
|
if (registryHelp) {
|
|
2810
2627
|
console.log('Additional commands:');
|
|
@@ -2813,6 +2630,26 @@ function showHelp() {
|
|
|
2813
2630
|
}
|
|
2814
2631
|
}
|
|
2815
2632
|
|
|
2633
|
+
// Per-command help. When `--help` follows a known subcommand (e.g. `forge status
|
|
2634
|
+
// --help`), render THAT command's usage/description/flags instead of the global
|
|
2635
|
+
// setup banner — a newcomer asking for a command's help should not get the
|
|
2636
|
+
// installer's help.
|
|
2637
|
+
function printCommandHelp(cmd) {
|
|
2638
|
+
console.log(`forge ${cmd.name} — ${cmd.description}`);
|
|
2639
|
+
if (cmd.usage) {
|
|
2640
|
+
console.log('');
|
|
2641
|
+
console.log(/^(Usage:|forge )/.test(cmd.usage) ? cmd.usage : `Usage: ${cmd.usage}`);
|
|
2642
|
+
}
|
|
2643
|
+
const flagEntries = cmd.flags && typeof cmd.flags === 'object' ? Object.entries(cmd.flags) : [];
|
|
2644
|
+
if (flagEntries.length > 0) {
|
|
2645
|
+
console.log('');
|
|
2646
|
+
console.log('Flags:');
|
|
2647
|
+
for (const [flag, desc] of flagEntries) {
|
|
2648
|
+
console.log(` ${flag.padEnd(16)} ${desc}`);
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2816
2653
|
// Detect Husky and offer migration to Lefthook
|
|
2817
2654
|
// Called before installGitHooks() in setup flows
|
|
2818
2655
|
async function handleHuskyMigration() {
|
|
@@ -2863,80 +2700,104 @@ async function handleHuskyMigration() {
|
|
|
2863
2700
|
function installGitHooks() {
|
|
2864
2701
|
console.log('Installing git hooks (TDD enforcement)...');
|
|
2865
2702
|
|
|
2866
|
-
//
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
console.warn(` \u26A0 Skipping lefthook setup: ${lefthookStatus.message}`);
|
|
2871
|
-
} else {
|
|
2872
|
-
console.warn(' \u26A0 Skipping lefthook setup: binary not available');
|
|
2873
|
-
}
|
|
2874
|
-
return;
|
|
2875
|
-
}
|
|
2703
|
+
// Install the Forge hook SCRIPTS first, UNCONDITIONALLY - they back BOTH the lefthook
|
|
2704
|
+
// pre-commit job AND the native .git/hooks fallback below, so they must exist whether
|
|
2705
|
+
// or not the lefthook binary is available.
|
|
2706
|
+
installForgeHookScripts();
|
|
2876
2707
|
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
const targetHooks = path.join(projectRoot, '.forge/hooks');
|
|
2708
|
+
const lefthookStatus = checkLefthookStatus(projectRoot);
|
|
2709
|
+
let lefthookInstalled = false;
|
|
2880
2710
|
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2711
|
+
if (lefthookStatus.binaryAvailable) {
|
|
2712
|
+
try {
|
|
2713
|
+
// Write the REAL user-facing lefthook.yml - never the repo's own dev config (which
|
|
2714
|
+
// references repo-internal scripts/ a user project lacks), and never leave
|
|
2715
|
+
// lefthook's stock commented-out example in place. Overwrite only a missing file
|
|
2716
|
+
// or a fully-commented stub, never a config with active jobs (kernel e452422c).
|
|
2717
|
+
const lefthookTarget = path.join(projectRoot, 'lefthook.yml');
|
|
2718
|
+
if (forgeShouldWriteLefthookConfig(lefthookTarget)) {
|
|
2719
|
+
fs.writeFileSync(lefthookTarget, FORGE_USER_LEFTHOOK_YML, 'utf8');
|
|
2886
2720
|
console.log(' ✓ Created lefthook.yml');
|
|
2887
2721
|
}
|
|
2888
|
-
}
|
|
2889
|
-
|
|
2890
|
-
// Copy check-tdd.js hook script
|
|
2891
|
-
const hookSource = path.join(packageDir, '.forge/hooks/check-tdd.js');
|
|
2892
|
-
if (fs.existsSync(hookSource)) {
|
|
2893
|
-
// Ensure .forge/hooks directory exists
|
|
2894
|
-
if (!fs.existsSync(targetHooks)) {
|
|
2895
|
-
fs.mkdirSync(targetHooks, { recursive: true });
|
|
2896
|
-
}
|
|
2897
2722
|
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
console.log(' ✓ Created .forge/hooks/check-tdd.js');
|
|
2901
|
-
|
|
2902
|
-
// Make hook executable (Unix systems)
|
|
2903
|
-
try {
|
|
2904
|
-
fs.chmodSync(hookTarget, 0o755);
|
|
2905
|
-
} catch (err) {
|
|
2906
|
-
// Windows doesn't need chmod
|
|
2907
|
-
console.warn('chmod not available (Windows):', err.message);
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
|
-
}
|
|
2911
|
-
|
|
2912
|
-
// Try to install lefthook hooks
|
|
2913
|
-
// SECURITY: Using execFileSync with hardcoded commands (no user input)
|
|
2914
|
-
try {
|
|
2915
|
-
// Try npx first (local install), fallback to global
|
|
2723
|
+
// Install lefthook's git hooks. Try local (npx) first, then a global binary.
|
|
2724
|
+
// SECURITY: execFileSync with hardcoded commands (no user input).
|
|
2916
2725
|
try {
|
|
2917
2726
|
secureExecFileSync('npx', ['lefthook', 'install'], { stdio: 'inherit', cwd: projectRoot });
|
|
2918
2727
|
console.log(' ✓ Lefthook hooks installed (local)');
|
|
2728
|
+
lefthookInstalled = true;
|
|
2919
2729
|
} catch (error_) {
|
|
2920
|
-
// Fallback to global lefthook
|
|
2921
2730
|
console.warn('npx lefthook failed, trying global:', error_.message);
|
|
2922
|
-
|
|
2923
|
-
|
|
2731
|
+
secureExecFileSync('lefthook', ['version'], { stdio: 'ignore' });
|
|
2732
|
+
secureExecFileSync('lefthook', ['install'], { stdio: 'inherit', cwd: projectRoot });
|
|
2924
2733
|
console.log(' ✓ Lefthook hooks installed (global)');
|
|
2734
|
+
lefthookInstalled = true;
|
|
2925
2735
|
}
|
|
2926
2736
|
} catch (err) {
|
|
2927
2737
|
console.warn('Lefthook installation failed:', err.message);
|
|
2928
|
-
console.log(' ℹ Lefthook not found. Install it:');
|
|
2929
|
-
console.log(' bun add -d lefthook (recommended)');
|
|
2930
|
-
console.log(' OR: bun add -g lefthook (global)');
|
|
2931
|
-
console.log(' Then run: bunx lefthook install');
|
|
2932
2738
|
}
|
|
2739
|
+
} else if (lefthookStatus.message) {
|
|
2740
|
+
console.warn(` ⚠ lefthook binary unavailable: ${lefthookStatus.message}`);
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
// Native fallback: when lefthook is unavailable or its install failed, wire native
|
|
2744
|
+
// .git/hooks so raw `git commit` / `git push` still enforce the TDD gate - the moat is
|
|
2745
|
+
// never silently inert (B3). Runs regardless of whether a package.json exists.
|
|
2746
|
+
if (!lefthookInstalled) {
|
|
2747
|
+
const native = installNativeGitHooks(projectRoot);
|
|
2748
|
+
if (native.installed) {
|
|
2749
|
+
console.log(` ✓ Native git hooks installed (${native.written.join(', ')}) - lefthook fallback`);
|
|
2750
|
+
} else if (native.skipped && native.skipped.length > 0) {
|
|
2751
|
+
console.warn(` ⚠ Native hook(s) skipped to preserve existing hooks: ${native.skipped.join(', ')}`);
|
|
2752
|
+
} else {
|
|
2753
|
+
console.warn(` ⚠ Could not install native git hooks: ${native.reason || 'unknown'}`);
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2933
2756
|
|
|
2934
|
-
|
|
2757
|
+
// VERIFY LOUDLY: never let setup silently no-op. If pre-commit enforcement is not
|
|
2758
|
+
// actually active after all of the above, say so unmistakably and set a failure code.
|
|
2759
|
+
const verdict = verifyHooksActive(projectRoot);
|
|
2760
|
+
if (verdict.active) {
|
|
2761
|
+
console.log(` ✓ Git hook enforcement active (${verdict.method}).`);
|
|
2762
|
+
} else {
|
|
2763
|
+
const addCmd = PKG_MANAGER === 'bun'
|
|
2764
|
+
? 'bun add -d'
|
|
2765
|
+
: PKG_MANAGER === 'npm'
|
|
2766
|
+
? 'npm install --save-dev'
|
|
2767
|
+
: `${PKG_MANAGER} add -D`;
|
|
2768
|
+
console.error('');
|
|
2769
|
+
console.error(' ============================================================');
|
|
2770
|
+
console.error(' ⚠ TDD ENFORCEMENT IS NOT ACTIVE');
|
|
2771
|
+
console.error(` ${verdict.reason || 'no pre-commit hook is installed'}.`);
|
|
2772
|
+
console.error(' `forge ship` will block until hooks are active. To fix:');
|
|
2773
|
+
console.error(` ${addCmd} lefthook && npx lefthook install`);
|
|
2774
|
+
console.error(' (or re-run `forge setup` in the repo root).');
|
|
2775
|
+
console.error(' ============================================================');
|
|
2776
|
+
console.error('');
|
|
2777
|
+
process.exitCode = 1;
|
|
2778
|
+
}
|
|
2935
2779
|
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2780
|
+
console.log('');
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
// Copy the Forge hook scripts (check-tdd.js + the native-hook adapter) into the
|
|
2784
|
+
// project's .forge/hooks/. Runs UNCONDITIONALLY - independent of the lefthook binary -
|
|
2785
|
+
// because both the lefthook pre-commit job and the native fallback invoke them.
|
|
2786
|
+
function installForgeHookScripts() {
|
|
2787
|
+
const targetHooks = path.join(projectRoot, '.forge/hooks');
|
|
2788
|
+
for (const name of ['check-tdd.js', 'forge-native-hook.js']) {
|
|
2789
|
+
const src = path.join(packageDir, '.forge/hooks', name);
|
|
2790
|
+
if (!fs.existsSync(src)) continue;
|
|
2791
|
+
if (!fs.existsSync(targetHooks)) fs.mkdirSync(targetHooks, { recursive: true });
|
|
2792
|
+
const dest = path.join(targetHooks, name);
|
|
2793
|
+
if (copyFile(src, dest)) {
|
|
2794
|
+
console.log(` ✓ Created .forge/hooks/${name}`);
|
|
2795
|
+
try {
|
|
2796
|
+
fs.chmodSync(dest, 0o755); // NOSONAR - hook scripts must be executable
|
|
2797
|
+
} catch (err) {
|
|
2798
|
+
console.warn('chmod not available (Windows):', err.message);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2940
2801
|
}
|
|
2941
2802
|
}
|
|
2942
2803
|
|
|
@@ -2987,6 +2848,18 @@ function isBeadsInitialized() {
|
|
|
2987
2848
|
return beadsSetupLib.isBeadsInitialized(projectRoot);
|
|
2988
2849
|
}
|
|
2989
2850
|
|
|
2851
|
+
function migrateExistingBeadsLocalState() {
|
|
2852
|
+
if (!fs.existsSync(path.join(projectRoot, '.beads'))) {
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
try {
|
|
2857
|
+
beadsSetupLib.ensureBeadsGitExclude(projectRoot);
|
|
2858
|
+
} catch (err) {
|
|
2859
|
+
console.warn(` Warning: failed to migrate Beads local state: ${err.message}`);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2990
2863
|
// Initialize Beads in the project using the defensive safeBeadsInit wrapper
|
|
2991
2864
|
// Handles config/gitignore writes, hook snapshot/restore, and JSONL pre-seeding
|
|
2992
2865
|
function initializeBeads(installType) {
|
|
@@ -3044,18 +2917,6 @@ function initializeBeads(installType) {
|
|
|
3044
2917
|
}
|
|
3045
2918
|
console.log(' ✓ Beads initialized');
|
|
3046
2919
|
|
|
3047
|
-
// Run post-init health check (non-fatal)
|
|
3048
|
-
try {
|
|
3049
|
-
const health = beadsHealthCheck(projectRoot);
|
|
3050
|
-
if (health.healthy) {
|
|
3051
|
-
console.log(' ✓ Beads health check passed');
|
|
3052
|
-
} else {
|
|
3053
|
-
console.log(` ⚠ Beads health check failed at ${health.failedStep}: ${health.error}`);
|
|
3054
|
-
}
|
|
3055
|
-
} catch (_healthErr) {
|
|
3056
|
-
// Health check is best-effort — don't block setup
|
|
3057
|
-
}
|
|
3058
|
-
|
|
3059
2920
|
return true;
|
|
3060
2921
|
} catch (err) {
|
|
3061
2922
|
console.log(' ⚠ Failed to initialize Beads:', err.message);
|
|
@@ -3131,6 +2992,7 @@ async function promptBeadsSetup(question) {
|
|
|
3131
2992
|
const beadsStatus = checkForBeads();
|
|
3132
2993
|
|
|
3133
2994
|
if (beadsInitialized) {
|
|
2995
|
+
migrateExistingBeadsLocalState();
|
|
3134
2996
|
console.log('✓ Beads is already initialized in this project');
|
|
3135
2997
|
console.log('');
|
|
3136
2998
|
return;
|
|
@@ -3364,6 +3226,10 @@ function autoSetupBeadsInQuickMode() {
|
|
|
3364
3226
|
const beadsStatus = checkForBeads();
|
|
3365
3227
|
const beadsInitialized = isBeadsInitialized();
|
|
3366
3228
|
|
|
3229
|
+
if (beadsInitialized) {
|
|
3230
|
+
migrateExistingBeadsLocalState();
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3367
3233
|
if (!beadsInitialized && beadsStatus) {
|
|
3368
3234
|
console.log('📦 Initializing Beads...');
|
|
3369
3235
|
initializeBeads(beadsStatus);
|
|
@@ -3415,7 +3281,19 @@ function autoInstallLefthook() {
|
|
|
3415
3281
|
return;
|
|
3416
3282
|
}
|
|
3417
3283
|
|
|
3418
|
-
// Not in package.json at all — full install
|
|
3284
|
+
// Not in package.json at all — full install.
|
|
3285
|
+
// GUARD (kernel 22e33dbf): with no package.json in projectRoot, an
|
|
3286
|
+
// `npm install --save-dev lefthook` / `bun add lefthook` resolves against the nearest
|
|
3287
|
+
// ANCESTOR package.json and installs lefthook into the WRONG project (or fails). Skip
|
|
3288
|
+
// the package-manager install entirely — installGitHooks() then wires native
|
|
3289
|
+
// .git/hooks, which need no package.json, so enforcement is still live.
|
|
3290
|
+
if (!fs.existsSync(path.join(projectRoot, 'package.json'))) {
|
|
3291
|
+
console.log(' ℹ No package.json here — skipping lefthook npm install (would target an ancestor).');
|
|
3292
|
+
console.log(' Git hook enforcement will use the native .git/hooks fallback instead.');
|
|
3293
|
+
console.log('');
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3419
3297
|
console.log('📦 Installing lefthook for git hooks...');
|
|
3420
3298
|
try {
|
|
3421
3299
|
// SECURITY: secureExecFileSync with PKG_MANAGER — cross-platform support
|
|
@@ -3463,11 +3341,10 @@ function autoSetupToolsInQuickMode() {
|
|
|
3463
3341
|
console.log('📦 Initializing Skills...');
|
|
3464
3342
|
initializeSkills(skillsStatus);
|
|
3465
3343
|
console.log('');
|
|
3466
|
-
} else if (!skillsStatus) {
|
|
3467
|
-
const installCmd = PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g';
|
|
3468
|
-
console.log(` ℹ Skills not found — install with: ${installCmd} @forge/skills`);
|
|
3469
|
-
console.log('');
|
|
3470
3344
|
}
|
|
3345
|
+
// No hint when the optional Skills CLI is absent: setup bundles and renders
|
|
3346
|
+
// Forge's skills itself, and the previously advertised "@forge/skills"
|
|
3347
|
+
// package does not exist on npm (kernel issue 6e554b41).
|
|
3471
3348
|
}
|
|
3472
3349
|
|
|
3473
3350
|
// Helper: Configure default external services in quick mode - extracted to reduce cognitive complexity
|
|
@@ -3522,9 +3399,11 @@ async function quickSetup(selectedAgents, skipExternal) {
|
|
|
3522
3399
|
// Auto-setup project tools (Beads, Skills)
|
|
3523
3400
|
autoSetupToolsInQuickMode();
|
|
3524
3401
|
|
|
3525
|
-
//
|
|
3526
|
-
|
|
3527
|
-
|
|
3402
|
+
// Setup Claude first if selected, then remaining agents
|
|
3403
|
+
if (selectedAgents.includes('claude')) {
|
|
3404
|
+
setupAgent('claude');
|
|
3405
|
+
}
|
|
3406
|
+
setupSelectedAgents(selectedAgents);
|
|
3528
3407
|
|
|
3529
3408
|
// Detect Husky and migrate before installing Lefthook hooks
|
|
3530
3409
|
await handleHuskyMigration();
|
|
@@ -3649,7 +3528,6 @@ function displayExistingInstallation(projectStatus) {
|
|
|
3649
3528
|
: 'Found partial installation:');
|
|
3650
3529
|
|
|
3651
3530
|
if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
|
|
3652
|
-
if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
|
|
3653
3531
|
if (projectStatus.hasEnvLocal) console.log(' - .env.local');
|
|
3654
3532
|
console.log('');
|
|
3655
3533
|
}
|
|
@@ -3658,7 +3536,6 @@ function displayExistingInstallation(projectStatus) {
|
|
|
3658
3536
|
async function promptForOverwriteDecisions(question, projectStatus) {
|
|
3659
3537
|
const skipFiles = {
|
|
3660
3538
|
agentsMd: false,
|
|
3661
|
-
claudeCommands: false
|
|
3662
3539
|
};
|
|
3663
3540
|
|
|
3664
3541
|
if (projectStatus.hasAgentsMd) {
|
|
@@ -3667,12 +3544,6 @@ async function promptForOverwriteDecisions(question, projectStatus) {
|
|
|
3667
3544
|
console.log(overwriteAgents ? ' Will overwrite AGENTS.md' : ' Keeping existing AGENTS.md');
|
|
3668
3545
|
}
|
|
3669
3546
|
|
|
3670
|
-
if (projectStatus.hasClaudeCommands) {
|
|
3671
|
-
const overwriteCommands = await askYesNo(question, 'Found existing .claude/commands/. Overwrite?', true);
|
|
3672
|
-
skipFiles.claudeCommands = !overwriteCommands;
|
|
3673
|
-
console.log(overwriteCommands ? ' Will overwrite .claude/commands/' : ' Keeping existing .claude/commands/');
|
|
3674
|
-
}
|
|
3675
|
-
|
|
3676
3547
|
if (projectStatus.type !== 'fresh') {
|
|
3677
3548
|
console.log('');
|
|
3678
3549
|
}
|
|
@@ -3680,41 +3551,14 @@ async function promptForOverwriteDecisions(question, projectStatus) {
|
|
|
3680
3551
|
return skipFiles;
|
|
3681
3552
|
}
|
|
3682
3553
|
|
|
3683
|
-
// Helper: Load and setup Claude commands - extracted to reduce cognitive complexity
|
|
3684
|
-
function loadAndSetupClaudeCommands(selectedAgents, skipFiles) {
|
|
3685
|
-
const claudeCommands = {};
|
|
3686
|
-
const needsClaudeCommands = selectedAgents.includes('claude') ||
|
|
3687
|
-
selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands);
|
|
3688
|
-
|
|
3689
|
-
if (!needsClaudeCommands) {
|
|
3690
|
-
return claudeCommands;
|
|
3691
|
-
}
|
|
3692
|
-
|
|
3693
|
-
// First ensure Claude is set up
|
|
3694
|
-
if (selectedAgents.includes('claude')) {
|
|
3695
|
-
setupAgent('claude', null, skipFiles);
|
|
3696
|
-
}
|
|
3697
|
-
|
|
3698
|
-
// Then load the commands (from existing or newly created)
|
|
3699
|
-
getWorkflowCommands().forEach(cmd => {
|
|
3700
|
-
const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
|
|
3701
|
-
const content = readFile(cmdPath);
|
|
3702
|
-
if (content) {
|
|
3703
|
-
claudeCommands[`${cmd}.md`] = content;
|
|
3704
|
-
}
|
|
3705
|
-
});
|
|
3706
|
-
|
|
3707
|
-
return claudeCommands;
|
|
3708
|
-
}
|
|
3709
|
-
|
|
3710
3554
|
// Helper: Setup all selected agents - extracted to reduce cognitive complexity
|
|
3711
|
-
function setupSelectedAgents(selectedAgents,
|
|
3555
|
+
function setupSelectedAgents(selectedAgents, skipFiles) {
|
|
3712
3556
|
const totalAgents = selectedAgents.length;
|
|
3713
3557
|
selectedAgents.forEach((agentKey, index) => {
|
|
3714
3558
|
const agent = AGENTS[agentKey];
|
|
3715
3559
|
console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
|
|
3716
3560
|
if (agentKey !== 'claude') { // Claude already done above
|
|
3717
|
-
setupAgent(agentKey,
|
|
3561
|
+
setupAgent(agentKey, skipFiles);
|
|
3718
3562
|
}
|
|
3719
3563
|
});
|
|
3720
3564
|
|
|
@@ -3814,11 +3658,13 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3814
3658
|
setupCoreDocs();
|
|
3815
3659
|
console.log('');
|
|
3816
3660
|
|
|
3817
|
-
//
|
|
3818
|
-
|
|
3661
|
+
// Setup Claude first if selected (delegated to helper), then remaining agents
|
|
3662
|
+
if (selectedAgents.includes('claude')) {
|
|
3663
|
+
setupAgent('claude', skipFiles);
|
|
3664
|
+
}
|
|
3819
3665
|
|
|
3820
3666
|
// Setup each selected agent with progress indication (delegated to helper)
|
|
3821
|
-
setupSelectedAgents(selectedAgents,
|
|
3667
|
+
setupSelectedAgents(selectedAgents, skipFiles);
|
|
3822
3668
|
|
|
3823
3669
|
// Handle external services step (delegated to helper)
|
|
3824
3670
|
await handleExternalServicesStep(flags, rl, question, selectedAgents, projectStatus);
|
|
@@ -3832,14 +3678,20 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3832
3678
|
|
|
3833
3679
|
// Main
|
|
3834
3680
|
// Helper: Handle --path setup
|
|
3835
|
-
function handlePathSetup(targetPath) {
|
|
3681
|
+
function handlePathSetup(targetPath, options = {}) {
|
|
3836
3682
|
const resolvedPath = path.resolve(targetPath);
|
|
3837
3683
|
|
|
3838
3684
|
// Create directory if it doesn't exist
|
|
3839
3685
|
if (!fs.existsSync(resolvedPath)) {
|
|
3686
|
+
if (options.create === false) {
|
|
3687
|
+
console.error(`Error: ${resolvedPath} does not exist`);
|
|
3688
|
+
process.exit(1);
|
|
3689
|
+
}
|
|
3840
3690
|
try {
|
|
3841
|
-
|
|
3691
|
+
fs.mkdirSync(resolvedPath, { recursive: true });
|
|
3692
|
+
if (!options.quiet) {
|
|
3842
3693
|
console.log(`Created directory: ${resolvedPath}`);
|
|
3694
|
+
}
|
|
3843
3695
|
} catch (err) {
|
|
3844
3696
|
console.error(`Error creating directory: ${err.message}`);
|
|
3845
3697
|
process.exit(1);
|
|
@@ -3855,8 +3707,10 @@ function handlePathSetup(targetPath) {
|
|
|
3855
3707
|
// Change to target directory
|
|
3856
3708
|
try {
|
|
3857
3709
|
process.chdir(resolvedPath);
|
|
3858
|
-
|
|
3859
|
-
|
|
3710
|
+
if (!options.quiet) {
|
|
3711
|
+
console.log(`Working directory: ${resolvedPath}`);
|
|
3712
|
+
console.log('');
|
|
3713
|
+
}
|
|
3860
3714
|
} catch (err) {
|
|
3861
3715
|
console.error(`Error changing to directory: ${err.message}`);
|
|
3862
3716
|
process.exit(1);
|
|
@@ -3866,6 +3720,15 @@ function handlePathSetup(targetPath) {
|
|
|
3866
3720
|
return resolvedPath;
|
|
3867
3721
|
}
|
|
3868
3722
|
|
|
3723
|
+
function resolvePathForDryRun(targetPath) {
|
|
3724
|
+
const resolvedPath = path.resolve(targetPath);
|
|
3725
|
+
if (fs.existsSync(resolvedPath) && !fs.statSync(resolvedPath).isDirectory()) {
|
|
3726
|
+
console.error(`Error: ${resolvedPath} is not a directory`);
|
|
3727
|
+
process.exit(1);
|
|
3728
|
+
}
|
|
3729
|
+
return resolvedPath;
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3869
3732
|
// Helper: Determine selected agents from flags
|
|
3870
3733
|
function determineSelectedAgents(flags) {
|
|
3871
3734
|
if (flags.all) {
|
|
@@ -3914,15 +3777,10 @@ function dryRunSetup(agents) {
|
|
|
3914
3777
|
addFileAction(dir + '/', 'Create agent directory');
|
|
3915
3778
|
}
|
|
3916
3779
|
|
|
3917
|
-
// Claude-specific files
|
|
3780
|
+
// Claude-specific files (skills are listed by the per-skill block below)
|
|
3918
3781
|
if (agentKey === 'claude') {
|
|
3919
|
-
const cmds = getWorkflowCommands();
|
|
3920
|
-
for (const cmd of cmds) {
|
|
3921
|
-
addFileAction(`.claude/commands/${cmd}.md`, 'Workflow command');
|
|
3922
|
-
}
|
|
3923
3782
|
addFileAction('.claude/rules/workflow.md', 'Workflow rules');
|
|
3924
3783
|
addFileAction('.claude/scripts/load-env.sh', 'Environment loader script');
|
|
3925
|
-
addFileAction('.claude/skills/forge-workflow/SKILL.md', 'Forge workflow skill');
|
|
3926
3784
|
addFileAction('.mcp.json', 'MCP server configuration');
|
|
3927
3785
|
addFileAction('CLAUDE.md', 'Claude root config (links to AGENTS.md)');
|
|
3928
3786
|
}
|
|
@@ -3932,15 +3790,7 @@ function dryRunSetup(agents) {
|
|
|
3932
3790
|
addFileAction('.cursor/rules/forge-workflow.mdc', 'Cursor workflow rule');
|
|
3933
3791
|
}
|
|
3934
3792
|
|
|
3935
|
-
|
|
3936
|
-
if (agent.needsConversion || agent.copyCommands || agent.promptFormat) {
|
|
3937
|
-
const cmds = getWorkflowCommands();
|
|
3938
|
-
const targetDir = agent.dirs[0];
|
|
3939
|
-
for (const cmd of cmds) {
|
|
3940
|
-
const ext = agent.promptFormat ? '.prompt.md' : '.md';
|
|
3941
|
-
addFileAction(`${targetDir}/${cmd}${ext}`, 'Converted workflow command');
|
|
3942
|
-
}
|
|
3943
|
-
}
|
|
3793
|
+
|
|
3944
3794
|
|
|
3945
3795
|
// Agent rules (copied from Claude)
|
|
3946
3796
|
if (agent.needsConversion) {
|
|
@@ -3950,11 +3800,10 @@ function dryRunSetup(agents) {
|
|
|
3950
3800
|
}
|
|
3951
3801
|
}
|
|
3952
3802
|
|
|
3953
|
-
// Agent skill
|
|
3954
|
-
if (agent.hasSkill) {
|
|
3955
|
-
const
|
|
3956
|
-
|
|
3957
|
-
addFileAction(`${skillDir}/SKILL.md`, 'Forge workflow skill');
|
|
3803
|
+
// Agent skill — enumerate canonical skills per-agent (codex handled globally elsewhere)
|
|
3804
|
+
if (agentKey !== 'codex' && agent.hasSkill && agent.skillsDir) {
|
|
3805
|
+
for (const skill of listCanonicalSkills(packageDir)) {
|
|
3806
|
+
addFileAction(`${agent.skillsDir}/${skill.name}/SKILL.md`, 'Forge stage skill');
|
|
3958
3807
|
}
|
|
3959
3808
|
}
|
|
3960
3809
|
|
|
@@ -3988,6 +3837,8 @@ async function executeSetup(config) {
|
|
|
3988
3837
|
checkPrerequisites();
|
|
3989
3838
|
console.log('');
|
|
3990
3839
|
|
|
3840
|
+
migrateExistingBeadsLocalState();
|
|
3841
|
+
|
|
3991
3842
|
// Copy AGENTS.md (only if not exists — preserve user customizations; actionLog tracks it)
|
|
3992
3843
|
const agentsDest = path.join(projectRoot, 'AGENTS.md');
|
|
3993
3844
|
if (fs.existsSync(agentsDest)) {
|
|
@@ -4002,15 +3853,11 @@ async function executeSetup(config) {
|
|
|
4002
3853
|
setupCoreDocs();
|
|
4003
3854
|
console.log('');
|
|
4004
3855
|
|
|
4005
|
-
//
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
// Setup agents with progress output (setupSelectedAgents skips claude internally
|
|
4012
|
-
// since loadAndSetupClaudeCommands already handled it above)
|
|
4013
|
-
setupSelectedAgents(agents, claudeCommands);
|
|
3856
|
+
// Setup Claude first if selected, then remaining agents
|
|
3857
|
+
if (agents.includes('claude')) {
|
|
3858
|
+
setupAgent('claude');
|
|
3859
|
+
}
|
|
3860
|
+
setupSelectedAgents(agents);
|
|
4014
3861
|
|
|
4015
3862
|
// Detect Husky and migrate before installing Lefthook hooks
|
|
4016
3863
|
await handleHuskyMigration();
|
|
@@ -4036,36 +3883,12 @@ async function executeSetup(config) {
|
|
|
4036
3883
|
// Helper: Scaffold Beads GitHub sync when --sync flag is provided
|
|
4037
3884
|
async function handleSyncScaffold() {
|
|
4038
3885
|
console.log('');
|
|
4039
|
-
console.log('
|
|
3886
|
+
console.log('Beads GitHub sync scaffolding is deprecated (--sync).');
|
|
4040
3887
|
try {
|
|
4041
|
-
// Scaffold sync files using the new lib module
|
|
4042
3888
|
const result = scaffoldBeadsSync(projectRoot, packageDir);
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
for (const f of (result.filesSkipped || [])) {
|
|
4047
|
-
console.log(` Skipped: ${f} (already exists)`);
|
|
4048
|
-
}
|
|
4049
|
-
|
|
4050
|
-
// Detect default branch and pin forge-managed workflows to the repo baseline Beads version.
|
|
4051
|
-
const branch = detectDefaultBranch(projectRoot);
|
|
4052
|
-
const beadsVersion = DEFAULT_BEADS_VERSION;
|
|
4053
|
-
const workflowDir = path.join(projectRoot, '.github', 'workflows');
|
|
4054
|
-
templateWorkflows(workflowDir, branch, beadsVersion, result.filesCreated || []);
|
|
4055
|
-
console.log(` Branch: ${branch}, Beads version: ${beadsVersion}`);
|
|
4056
|
-
|
|
4057
|
-
// PAT setup: interactive when possible, reminder otherwise
|
|
4058
|
-
try {
|
|
4059
|
-
const patResult = setupPAT(projectRoot, { interactive: !NON_INTERACTIVE });
|
|
4060
|
-
if (patResult.success) {
|
|
4061
|
-
console.log(' PAT configured for Beads sync');
|
|
4062
|
-
} else if (patResult.reminder) {
|
|
4063
|
-
console.log(` ${patResult.reminder}`);
|
|
4064
|
-
} else if (patResult.instructions) {
|
|
4065
|
-
console.log(` ${patResult.instructions.split('\n')[0]}`);
|
|
4066
|
-
}
|
|
4067
|
-
} catch (_patErr) {
|
|
4068
|
-
// PAT setup is best-effort — don't block sync scaffold
|
|
3889
|
+
console.log(` ${result.message}`);
|
|
3890
|
+
for (const f of result.filesRemoved || []) {
|
|
3891
|
+
console.log(` Removed deprecated sync file: ${f}`);
|
|
4069
3892
|
}
|
|
4070
3893
|
} catch (err) {
|
|
4071
3894
|
console.error(` Error scaffolding GitHub-Beads sync: ${err.message}`);
|
|
@@ -4089,6 +3912,11 @@ async function handleSetupCommand(selectedAgents, flags) {
|
|
|
4089
3912
|
}
|
|
4090
3913
|
}
|
|
4091
3914
|
|
|
3915
|
+
async function runInteractiveSetupFallback(flags, interactiveSetup = interactiveSetupWithFlags) {
|
|
3916
|
+
migrateExistingBeadsLocalState();
|
|
3917
|
+
return interactiveSetup(flags);
|
|
3918
|
+
}
|
|
3919
|
+
|
|
4092
3920
|
// Helper: Handle external services configuration
|
|
4093
3921
|
async function handleExternalServices(skipExternal, selectedAgents) {
|
|
4094
3922
|
if (skipExternal) {
|
|
@@ -4119,6 +3947,17 @@ async function handleExternalServices(skipExternal, selectedAgents) {
|
|
|
4119
3947
|
async function main() {
|
|
4120
3948
|
const command = args[0];
|
|
4121
3949
|
const flags = parseFlags();
|
|
3950
|
+
const suppressJsonIntrospectionOutput = ['options', 'explain'].includes(command) && args.includes('--json');
|
|
3951
|
+
const suppressCommandJsonOutput = args.includes('--json');
|
|
3952
|
+
const suppressDocsDetectOutput = command === 'docs' && args[1] === 'detect';
|
|
3953
|
+
const profileDryRunArgs = ['--minimal', '--standard', '--full'];
|
|
3954
|
+
const suppressAdoptionDryRunOutput = flags.dryRun && (
|
|
3955
|
+
command === 'init' || (command === 'setup' && profileDryRunArgs.some(arg => args.includes(arg)))
|
|
3956
|
+
);
|
|
3957
|
+
const suppressStructuredOutput = suppressJsonIntrospectionOutput ||
|
|
3958
|
+
suppressCommandJsonOutput ||
|
|
3959
|
+
suppressDocsDetectOutput ||
|
|
3960
|
+
suppressAdoptionDryRunOutput;
|
|
4122
3961
|
|
|
4123
3962
|
// Wire up incremental setup state from parsed flags
|
|
4124
3963
|
FORCE_MODE = flags.force;
|
|
@@ -4128,20 +3967,33 @@ async function main() {
|
|
|
4128
3967
|
SYNC_ENABLED = flags.sync;
|
|
4129
3968
|
actionLog = new SetupActionLog();
|
|
4130
3969
|
|
|
4131
|
-
|
|
3970
|
+
// The non-interactive agent-selection notice is DEBUG-ONLY (kernel issue
|
|
3971
|
+
// a9bbd065): it used to print on stderr for EVERY command run by a non-TTY
|
|
3972
|
+
// agent/CI, polluting otherwise-clean command output. It only matters when
|
|
3973
|
+
// setup-style agent selection actually happens, so it now requires --verbose
|
|
3974
|
+
// (or FORGE_DEBUG=1) to appear.
|
|
3975
|
+
if (NON_INTERACTIVE && (VERBOSE_MODE || process.env.FORGE_DEBUG === '1') && !suppressStructuredOutput) {
|
|
4132
3976
|
const agentFlag = flags.agents;
|
|
4133
3977
|
if (agentFlag && agentFlag.length > 0) {
|
|
4134
3978
|
// flags.agents is a comma-separated string (e.g. "claude,cursor")
|
|
4135
3979
|
const agentDisplay = agentFlag.replace(/,/g, ', ');
|
|
4136
|
-
console.
|
|
3980
|
+
console.error(`Non-interactive mode: using provided agent selection (${agentDisplay})`);
|
|
4137
3981
|
} else {
|
|
4138
|
-
console.
|
|
3982
|
+
console.error('Non-interactive mode: using default agent selection (all)');
|
|
4139
3983
|
}
|
|
4140
3984
|
}
|
|
4141
3985
|
|
|
4142
|
-
// Show help
|
|
3986
|
+
// Show help. `--help` after a known registry subcommand (e.g. `forge status
|
|
3987
|
+
// --help`) renders that command's usage; bare `forge --help` keeps the global
|
|
3988
|
+
// banner. `setup` is special-cased OUT: the global banner IS setup's detailed
|
|
3989
|
+
// help, so `forge setup --help` must keep showing it (not the terse registry line).
|
|
4143
3990
|
if (flags.help) {
|
|
4144
|
-
|
|
3991
|
+
const helpRegistry = loadCommands(path.join(__dirname, '..', 'lib', 'commands'));
|
|
3992
|
+
if (command && command !== 'setup' && helpRegistry.commands.has(command)) {
|
|
3993
|
+
printCommandHelp(helpRegistry.commands.get(command));
|
|
3994
|
+
} else {
|
|
3995
|
+
showHelp();
|
|
3996
|
+
}
|
|
4145
3997
|
return;
|
|
4146
3998
|
}
|
|
4147
3999
|
|
|
@@ -4153,8 +4005,11 @@ async function main() {
|
|
|
4153
4005
|
|
|
4154
4006
|
// Handle --path option: change to target directory
|
|
4155
4007
|
if (flags.path) {
|
|
4008
|
+
const createTargetPath = !(command === 'docs' && ['verify', 'detect'].includes(args[1]));
|
|
4156
4009
|
// Update projectRoot after changing directory to maintain state consistency
|
|
4157
|
-
projectRoot =
|
|
4010
|
+
projectRoot = suppressAdoptionDryRunOutput
|
|
4011
|
+
? resolvePathForDryRun(flags.path)
|
|
4012
|
+
: handlePathSetup(flags.path, { quiet: suppressStructuredOutput, create: createTargetPath });
|
|
4158
4013
|
}
|
|
4159
4014
|
|
|
4160
4015
|
// Load command registry (auto-discovered commands from lib/commands/)
|
|
@@ -4180,24 +4035,51 @@ async function main() {
|
|
|
4180
4035
|
// Registry command dispatch — auto-discovered commands take priority
|
|
4181
4036
|
if (registry.commands.has(command)) {
|
|
4182
4037
|
try {
|
|
4038
|
+
// Resolve the issue backend (flag > env > config > default) and, for issue
|
|
4039
|
+
// commands, strip the selector tokens (--kernel / --issue-backend) from the
|
|
4040
|
+
// args so they never reach the handler or bd. For the kernel backend this
|
|
4041
|
+
// also assembles the driver + migrated broker (B1/B2).
|
|
4042
|
+
const { commandOpts, args: dispatchArgs } = await resolveCommandOpts(
|
|
4043
|
+
command,
|
|
4044
|
+
args.slice(1),
|
|
4045
|
+
{ env: process.env, projectRoot },
|
|
4046
|
+
);
|
|
4183
4047
|
const result = await executeCommand(
|
|
4184
4048
|
registry.commands,
|
|
4185
4049
|
command,
|
|
4186
|
-
|
|
4050
|
+
dispatchArgs,
|
|
4187
4051
|
flags,
|
|
4188
4052
|
projectRoot,
|
|
4189
4053
|
{
|
|
4054
|
+
// Pass interactivity so issue writes render a human confirmation at a real
|
|
4055
|
+
// terminal but stay machine-parseable JSON when piped/scripted (842a8be7).
|
|
4056
|
+
commandOpts: { ...(commandOpts || {}), isInteractive: Boolean(process.stdout.isTTY) },
|
|
4190
4057
|
enforceStage: (context) => enforceStageEntry({
|
|
4191
4058
|
commandName: context.commandName,
|
|
4192
4059
|
args: context.args,
|
|
4193
4060
|
flags: context.flags,
|
|
4194
4061
|
projectRoot: context.projectRoot,
|
|
4062
|
+
// Kernel is the source of truth for workflow stage state: resolve the
|
|
4063
|
+
// active worktree's issue and persist/read currentStage in the kernel
|
|
4064
|
+
// so `ship` is reachable from a pure-CLI plan->dev->validate flow.
|
|
4065
|
+
autoResolveKernel: true,
|
|
4195
4066
|
}),
|
|
4196
4067
|
}
|
|
4197
4068
|
);
|
|
4198
4069
|
if (result && !result.success) {
|
|
4070
|
+
if (
|
|
4071
|
+
typeof result.output === 'string' &&
|
|
4072
|
+
result.output.length > 0 &&
|
|
4073
|
+
result.output !== result.error
|
|
4074
|
+
) {
|
|
4075
|
+
process.stdout.write(result.output.endsWith('\n') ? result.output : `${result.output}\n`);
|
|
4076
|
+
}
|
|
4199
4077
|
console.error(result.error || result.message || 'Command failed');
|
|
4200
|
-
|
|
4078
|
+
// Issue commands surface the issue-command-contract exit_code as
|
|
4079
|
+
// `result.exitCode` (e.g. not-found=3, validation=6). Honor it so failed
|
|
4080
|
+
// commands no longer collapse to exit 1; other registry commands omit
|
|
4081
|
+
// `exitCode` and keep the historical exit 1.
|
|
4082
|
+
process.exit(Number.isInteger(result.exitCode) ? result.exitCode : 1);
|
|
4201
4083
|
}
|
|
4202
4084
|
if (result && typeof result.output === 'string' && result.output.length > 0) {
|
|
4203
4085
|
process.stdout.write(result.output.endsWith('\n') ? result.output : `${result.output}\n`);
|
|
@@ -4217,6 +4099,7 @@ async function main() {
|
|
|
4217
4099
|
args: args.slice(1),
|
|
4218
4100
|
flags,
|
|
4219
4101
|
projectRoot,
|
|
4102
|
+
autoResolveKernel: true,
|
|
4220
4103
|
});
|
|
4221
4104
|
} catch (err) {
|
|
4222
4105
|
console.error(`Error running '${command}':`, err.message);
|
|
@@ -4257,6 +4140,12 @@ async function main() {
|
|
|
4257
4140
|
return;
|
|
4258
4141
|
}
|
|
4259
4142
|
|
|
4143
|
+
if (flags.sync && selectedAgents.length === 0) {
|
|
4144
|
+
migrateExistingBeadsLocalState();
|
|
4145
|
+
await handleSyncScaffold();
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4260
4149
|
// Agents specified via flag or --yes default (non-quick mode)
|
|
4261
4150
|
if (selectedAgents.length > 0) {
|
|
4262
4151
|
await handleSetupCommand(selectedAgents, flags);
|
|
@@ -4264,7 +4153,7 @@ async function main() {
|
|
|
4264
4153
|
}
|
|
4265
4154
|
|
|
4266
4155
|
// Interactive setup (skip-external still applies)
|
|
4267
|
-
await
|
|
4156
|
+
await runInteractiveSetupFallback(flags);
|
|
4268
4157
|
} else if (command === 'recommend') {
|
|
4269
4158
|
const { handleRecommend, formatRecommendations } = require('../lib/commands/recommend');
|
|
4270
4159
|
const result = handleRecommend(flags, projectRoot);
|
|
@@ -4285,9 +4174,68 @@ async function main() {
|
|
|
4285
4174
|
}
|
|
4286
4175
|
console.log('');
|
|
4287
4176
|
console.log(' Usage: forge docs <topic>');
|
|
4177
|
+
console.log(' forge docs verify [--path <dir>] [--json] [--baseline <file>] [--write-baseline <file>] [--min-docstring-coverage <percent>]');
|
|
4178
|
+
console.log(' forge docs detect [--path <dir>] [--json]');
|
|
4288
4179
|
console.log('');
|
|
4180
|
+
console.log(' Note: forge docs verify|detect --path requires an existing directory.');
|
|
4181
|
+
console.log('');
|
|
4182
|
+
} else if (topic === 'verify' || topic === 'detect') {
|
|
4183
|
+
const minCoverageIndex = args.indexOf('--min-docstring-coverage');
|
|
4184
|
+
let minDocstringCoverage = 0;
|
|
4185
|
+
if (minCoverageIndex >= 0) {
|
|
4186
|
+
const rawMinCoverage = args[minCoverageIndex + 1];
|
|
4187
|
+
const parsedMinCoverage = Number(rawMinCoverage);
|
|
4188
|
+
if (!rawMinCoverage || !Number.isFinite(parsedMinCoverage) || parsedMinCoverage < 0 || parsedMinCoverage > 100) {
|
|
4189
|
+
console.error('Error: --min-docstring-coverage must be a number between 0 and 100');
|
|
4190
|
+
process.exitCode = 1;
|
|
4191
|
+
return;
|
|
4192
|
+
}
|
|
4193
|
+
minDocstringCoverage = parsedMinCoverage;
|
|
4194
|
+
}
|
|
4195
|
+
const baselineIndex = args.indexOf('--baseline');
|
|
4196
|
+
const writeBaselineIndex = args.indexOf('--write-baseline');
|
|
4197
|
+
const baselinePath = baselineIndex >= 0 ? args[baselineIndex + 1] : null;
|
|
4198
|
+
const writeBaselinePath = writeBaselineIndex >= 0 ? args[writeBaselineIndex + 1] : null;
|
|
4199
|
+
if (topic === 'detect' && writeBaselineIndex >= 0) {
|
|
4200
|
+
console.error('Error: --write-baseline is only supported with `forge docs verify`');
|
|
4201
|
+
process.exitCode = 1;
|
|
4202
|
+
return;
|
|
4203
|
+
}
|
|
4204
|
+
if (baselineIndex >= 0 && (!baselinePath || baselinePath.startsWith('-'))) {
|
|
4205
|
+
console.error('Error: --baseline requires a file path');
|
|
4206
|
+
process.exitCode = 1;
|
|
4207
|
+
return;
|
|
4208
|
+
}
|
|
4209
|
+
if (writeBaselineIndex >= 0 && (!writeBaselinePath || writeBaselinePath.startsWith('-'))) {
|
|
4210
|
+
console.error('Error: --write-baseline requires a file path');
|
|
4211
|
+
process.exitCode = 1;
|
|
4212
|
+
return;
|
|
4213
|
+
}
|
|
4214
|
+
let result;
|
|
4215
|
+
try {
|
|
4216
|
+
result = validateDocs(projectRoot, { baselinePath, minDocstringCoverage });
|
|
4217
|
+
if (writeBaselinePath) {
|
|
4218
|
+
writeDocsBaseline(projectRoot, writeBaselinePath, result);
|
|
4219
|
+
}
|
|
4220
|
+
} catch (error) {
|
|
4221
|
+
console.error(`Error: ${error.message}`);
|
|
4222
|
+
process.exitCode = 1;
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
if (args.includes('--json') || topic === 'detect') {
|
|
4226
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4227
|
+
} else {
|
|
4228
|
+
console.log(formatDocsValidation(result));
|
|
4229
|
+
}
|
|
4230
|
+
const hasUnbaselinedFailure = result.failures.some((failure) => failure.type !== 'broken-link');
|
|
4231
|
+
if ((!writeBaselinePath && !result.ok) || (writeBaselinePath && hasUnbaselinedFailure)) {
|
|
4232
|
+
process.exitCode = 1;
|
|
4233
|
+
}
|
|
4289
4234
|
} else {
|
|
4290
|
-
|
|
4235
|
+
// `forge docs` reads packaged docs; route through the asset root so any
|
|
4236
|
+
// embedded topic resolves in a compiled binary too. (Full docs/ embedding
|
|
4237
|
+
// for arbitrary topics is deferred to step 3 — see the step-2 plan note.)
|
|
4238
|
+
const result = getTopicContent(topic, getPackageRoot(packageDir));
|
|
4291
4239
|
if (result.error) {
|
|
4292
4240
|
console.error(` Error: ${result.error}`);
|
|
4293
4241
|
process.exitCode = 1;
|
|
@@ -4389,9 +4337,34 @@ async function main() {
|
|
|
4389
4337
|
console.log(' To set up in your project:');
|
|
4390
4338
|
console.log(` ${runCmd} forge setup`);
|
|
4391
4339
|
console.log('');
|
|
4392
|
-
} else {
|
|
4393
|
-
//
|
|
4340
|
+
} else if (command === undefined && fs.existsSync(path.join(projectRoot, 'AGENTS.md'))) {
|
|
4341
|
+
// Bare `forge` in an INITIALIZED project orients instead of mutating: render the
|
|
4342
|
+
// same read-only one-glance view as `forge status`. Newcomers typing `forge` to
|
|
4343
|
+
// get their bearings should never trigger the minimal install.
|
|
4344
|
+
try {
|
|
4345
|
+
const { commandOpts, args: dispatchArgs } = await resolveCommandOpts(
|
|
4346
|
+
'status',
|
|
4347
|
+
[],
|
|
4348
|
+
{ env: process.env, projectRoot },
|
|
4349
|
+
);
|
|
4350
|
+
const result = await executeCommand(registry.commands, 'status', dispatchArgs, flags, projectRoot, { commandOpts });
|
|
4351
|
+
if (result && typeof result.output === 'string' && result.output.length > 0) {
|
|
4352
|
+
process.stdout.write(result.output.endsWith('\n') ? result.output : `${result.output}\n`);
|
|
4353
|
+
}
|
|
4354
|
+
} catch (err) {
|
|
4355
|
+
console.error(`Error running 'status':`, err.message);
|
|
4356
|
+
process.exit(1);
|
|
4357
|
+
}
|
|
4358
|
+
} else if (command === undefined) {
|
|
4359
|
+
// Explicit invocation with no command in an UNINITIALIZED project: run minimal install.
|
|
4394
4360
|
minimalInstall();
|
|
4361
|
+
} else {
|
|
4362
|
+
// Defined but unrecognized command → fail honestly instead of printing the
|
|
4363
|
+
// installer banner with exit 0 (kernel b3bae4f3). A typo like `forge bogusxyz`
|
|
4364
|
+
// must be detectable by scripts (non-zero exit) and humans (a real error).
|
|
4365
|
+
console.error(`Error: Unknown command '${command}'`);
|
|
4366
|
+
console.error("Run 'forge --help' to see the available commands.");
|
|
4367
|
+
process.exit(1);
|
|
4395
4368
|
}
|
|
4396
4369
|
}
|
|
4397
4370
|
|
|
@@ -4815,7 +4788,6 @@ if (require.main === module) {
|
|
|
4815
4788
|
|
|
4816
4789
|
module.exports = {
|
|
4817
4790
|
ensureDirWithNote,
|
|
4818
|
-
getWorkflowCommands,
|
|
4819
4791
|
validateDirectoryPathInput,
|
|
4820
4792
|
validateUserInput,
|
|
4821
4793
|
};
|