forge-workflow 0.0.10 → 0.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +82 -5
- package/.forge/hooks/forge-native-hook.js +431 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +709 -0
- package/CLAUDE.md +9 -118
- package/QUICKSTART.md +175 -0
- package/README.md +275 -365
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +532 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/forge/TOOLCHAIN.md +670 -0
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +121 -0
- package/docs/guides/SUPPORT.md +190 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +214 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +155 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/activation/ensure-forge-home.js +135 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1109 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +139 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-detect.js +60 -0
- package/lib/beads-nudge.js +91 -0
- package/lib/beads-setup.js +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_aliases.js +248 -0
- package/lib/commands/_issue.js +780 -77
- package/lib/commands/_manifest.js +93 -0
- package/lib/commands/_registry.js +99 -34
- package/lib/commands/_resolve-command-opts.js +230 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +209 -0
- package/lib/commands/hooks.js +377 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +604 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +17 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/memory.js +81 -0
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +362 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +481 -29
- package/lib/commands/pr.js +88 -0
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +135 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +171 -0
- package/lib/commands/recap.js +75 -0
- package/lib/commands/recommend.js +0 -1
- package/lib/commands/release.js +104 -0
- package/lib/commands/remember.js +140 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +900 -971
- package/lib/commands/shepherd.js +501 -0
- package/lib/commands/ship.js +59 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +158 -21
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +4 -1
- package/lib/commands/test.js +43 -27
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +43 -18
- package/lib/commands/worktree.js +362 -99
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +977 -0
- package/lib/dep-guard/keyword-ripple.js +2 -2
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +35 -9
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +382 -11
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +192 -0
- package/lib/global-flags.js +104 -0
- package/lib/greptile-match.js +7 -63
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +541 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/kernel/backing-issue.js +311 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/owned-kernel.js +43 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2540 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +3 -2
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/auto-actions.js +175 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/digest.js +206 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +192 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1314 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +99 -497
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2105 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +241 -20
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/scoring.js +17 -3
- package/lib/status/beads-snapshot.js +45 -2
- package/lib/status/presenter.js +169 -18
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +251 -0
- package/lib/workflow/enforce-stage.js +351 -45
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +11 -22
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +37 -35
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +81 -57
- package/scripts/beads-upgrade-smoke.sh +24 -3
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard.sh +22 -3
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +49 -84
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +50 -83
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-auto-actions.js +93 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/pr-verdict-label.js +50 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +21 -94
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status.sh +60 -57
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +13 -6
- package/scripts/test.js +95 -12
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -89
- package/.github/workflows/github-to-beads.yml +0 -100
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -281
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-bootstrap.js +0 -225
- package/lib/beads-health-check.js +0 -188
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -584
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -356
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -132
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
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,52 @@
|
|
|
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 {
|
|
64
|
+
isAlias,
|
|
65
|
+
isHiddenAlias,
|
|
66
|
+
isVisibleAlias,
|
|
67
|
+
resolveAlias,
|
|
68
|
+
resolveDispatch,
|
|
69
|
+
maybeWarnDeprecation,
|
|
70
|
+
passthroughAliasNames,
|
|
71
|
+
visibleAliasNames,
|
|
72
|
+
} = require('../lib/commands/_aliases');
|
|
73
|
+
const { resolveCommandOpts } = require('../lib/commands/_resolve-command-opts');
|
|
74
|
+
const { getPackageRoot } = require('../lib/package-root');
|
|
55
75
|
const { enforceStageEntry } = require('../lib/workflow/enforce-stage');
|
|
56
76
|
const { normalizeStageId } = require('../lib/workflow/stages');
|
|
77
|
+
const { firstPositionalIndex } = require('../lib/global-flags');
|
|
57
78
|
|
|
58
|
-
// Load enhanced onboarding modules
|
|
59
|
-
const contextMerge = require(
|
|
60
|
-
const projectDiscovery = require(
|
|
79
|
+
// Load enhanced onboarding modules (static relative requires — bundleable)
|
|
80
|
+
const contextMerge = require('../lib/context-merge');
|
|
81
|
+
const projectDiscovery = require('../lib/project-discovery');
|
|
61
82
|
|
|
62
83
|
// 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'));
|
|
84
|
+
const { createSymlinkOrCopy: libCreateSymlinkOrCopy } = require('../lib/symlink-utils');
|
|
85
|
+
const beadsSetupLib = require('../lib/beads-setup');
|
|
86
|
+
const { scaffoldBeadsSync } = require('../lib/beads-sync-scaffold');
|
|
68
87
|
|
|
69
88
|
// Load incremental setup modules
|
|
70
89
|
const { detectEnvironment } = require('../lib/detect-agent');
|
|
@@ -74,6 +93,12 @@ const { ActionCollector, isNonInteractive } = require('../lib/setup-utils');
|
|
|
74
93
|
const { renderSetupSummary } = require('../lib/setup-summary-renderer');
|
|
75
94
|
const { smartMergeAgentsMd } = require('../lib/smart-merge');
|
|
76
95
|
const { checkLefthookStatus } = require('../lib/lefthook-check');
|
|
96
|
+
const {
|
|
97
|
+
FORGE_USER_LEFTHOOK_YML,
|
|
98
|
+
forgeShouldWriteLefthookConfig,
|
|
99
|
+
installNativeGitHooks,
|
|
100
|
+
verifyHooksActive,
|
|
101
|
+
} = require('../lib/lefthook-wiring');
|
|
77
102
|
const { detectHusky, migrateHusky } = require('../lib/husky-migration');
|
|
78
103
|
// workflowProfiles: module exists and is tested but not yet wired into setup flow
|
|
79
104
|
// Will be activated when workflow profile selection is added to interactive setup
|
|
@@ -94,37 +119,10 @@ let actionLog = new SetupActionLog();
|
|
|
94
119
|
// Detected package manager
|
|
95
120
|
let PKG_MANAGER = 'npm';
|
|
96
121
|
|
|
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
|
-
}
|
|
122
|
+
// Securely execute a command with PATH validation (SonarCloud S4036).
|
|
123
|
+
// Delegates to the shared helper, which also handles Windows npm/npx/lefthook
|
|
124
|
+
// cmd shims that cannot be spawned directly (kernel issue 9997d516).
|
|
125
|
+
const { secureExecFileSync } = require('../lib/shell-utils');
|
|
128
126
|
|
|
129
127
|
/**
|
|
130
128
|
* Load agent definitions from plugin architecture
|
|
@@ -146,7 +144,8 @@ function loadAgentsFromPlugins() {
|
|
|
146
144
|
customSetup: plugin.setup?.customSetup || '',
|
|
147
145
|
needsConversion: plugin.setup?.needsConversion || false,
|
|
148
146
|
copyCommands: plugin.setup?.copyCommands || false,
|
|
149
|
-
promptFormat: plugin.setup?.promptFormat || false
|
|
147
|
+
promptFormat: plugin.setup?.promptFormat || false,
|
|
148
|
+
skillsDir: plugin.directories?.skills || null
|
|
150
149
|
};
|
|
151
150
|
});
|
|
152
151
|
|
|
@@ -295,25 +294,6 @@ function _checkWritePermission(filePath) {
|
|
|
295
294
|
}
|
|
296
295
|
}
|
|
297
296
|
|
|
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
297
|
|
|
318
298
|
|
|
319
299
|
// Helper function to safely execute commands (no user input)
|
|
@@ -432,72 +412,26 @@ function checkPrerequisites() {
|
|
|
432
412
|
return { errors, warnings };
|
|
433
413
|
}
|
|
434
414
|
|
|
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
415
|
|
|
483
416
|
// Cursor MDC rule content
|
|
484
417
|
const CURSOR_RULE = `---
|
|
485
|
-
description: Forge
|
|
418
|
+
description: Forge TDD Workflow Template
|
|
486
419
|
alwaysApply: true
|
|
487
420
|
---
|
|
488
421
|
|
|
489
422
|
# Forge Workflow Commands
|
|
490
423
|
|
|
491
|
-
Use these commands via \`/command-name\`:
|
|
424
|
+
Use these default-template commands via \`/command-name\`:
|
|
492
425
|
|
|
493
426
|
- \`/status\` (utility) - Check current context, active work, recent completions
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
427
|
+
- \`/plan\` - Design intent Q&A, research, branch + task list
|
|
428
|
+
- \`/dev\` - Subagent-driven TDD per task (spec + quality review)
|
|
429
|
+
- \`/validate\` - Type check, lint, security, tests (HARD-GATE)
|
|
430
|
+
- \`/ship\` - Push and create PR with design doc reference
|
|
431
|
+
- \`/review\` - Handle ALL PR issues (Actions, Greptile, SonarCloud)
|
|
432
|
+
- \`/verify\` - Post-merge health check (CI on main, close Beads)
|
|
433
|
+
|
|
434
|
+
Pre-merge gate (not a numbered stage): finish docs + confirm CI green + hand off the PR — embedded in the /ship and /review stages.
|
|
501
435
|
|
|
502
436
|
See AGENTS.md for full workflow details.
|
|
503
437
|
`;
|
|
@@ -543,17 +477,6 @@ function writeFile(filePath, content) {
|
|
|
543
477
|
}
|
|
544
478
|
}
|
|
545
479
|
|
|
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
480
|
function copyFile(src, dest) {
|
|
558
481
|
try {
|
|
559
482
|
const destPath = path.resolve(projectRoot, dest);
|
|
@@ -612,11 +535,6 @@ function createSymlinkOrCopy(source, target, options = {}) {
|
|
|
612
535
|
return libCreateSymlinkOrCopy(fullSource, fullTarget, options);
|
|
613
536
|
}
|
|
614
537
|
|
|
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
538
|
// Read existing .env.local
|
|
621
539
|
function readEnvFile() {
|
|
622
540
|
const envPath = path.join(projectRoot, '.env.local');
|
|
@@ -781,7 +699,6 @@ async function detectProjectStatus() {
|
|
|
781
699
|
type: 'fresh', // 'fresh', 'upgrade', or 'partial'
|
|
782
700
|
hasAgentsMd: fs.existsSync(path.join(projectRoot, 'AGENTS.md')),
|
|
783
701
|
hasClaudeMd: fs.existsSync(path.join(projectRoot, 'CLAUDE.md')),
|
|
784
|
-
hasClaudeCommands: fs.existsSync(path.join(projectRoot, '.claude/commands')),
|
|
785
702
|
hasEnvLocal: fs.existsSync(path.join(projectRoot, '.env.local')),
|
|
786
703
|
existingEnvVars: {},
|
|
787
704
|
agentsMdSize: 0,
|
|
@@ -815,9 +732,9 @@ async function detectProjectStatus() {
|
|
|
815
732
|
}
|
|
816
733
|
|
|
817
734
|
// Determine installation type
|
|
818
|
-
if (status.hasAgentsMd
|
|
735
|
+
if (status.hasAgentsMd) {
|
|
819
736
|
status.type = 'upgrade'; // Full forge installation exists
|
|
820
|
-
} else if (status.
|
|
737
|
+
} else if (status.hasEnvLocal) {
|
|
821
738
|
status.type = 'partial'; // Agent-specific files exist (not just base files from postinstall)
|
|
822
739
|
}
|
|
823
740
|
// else: 'fresh' - new installation (or just postinstall baseline with AGENTS.md)
|
|
@@ -1665,7 +1582,6 @@ function displayMcpStatus(selectedAgents) {
|
|
|
1665
1582
|
// Show manual setup instructions for GUI-based agents
|
|
1666
1583
|
const manualMcpMap = {
|
|
1667
1584
|
cursor: 'Cursor: Configure via Cursor Settings > MCP',
|
|
1668
|
-
cline: 'Cline: Install via MCP Marketplace',
|
|
1669
1585
|
};
|
|
1670
1586
|
const needsManualMcp = Object.entries(manualMcpMap)
|
|
1671
1587
|
.filter(([key]) => selectedAgents.includes(key))
|
|
@@ -1748,39 +1664,6 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
|
|
|
1748
1664
|
const { added, preserved } = writeEnvTokens(tokens, true);
|
|
1749
1665
|
displayEnvTokenResults(added, preserved);
|
|
1750
1666
|
|
|
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
1667
|
}
|
|
1785
1668
|
|
|
1786
1669
|
// Display the Forge banner
|
|
@@ -1891,26 +1774,20 @@ function minimalInstall() {
|
|
|
1891
1774
|
console.log('');
|
|
1892
1775
|
}
|
|
1893
1776
|
|
|
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
|
-
}
|
|
1777
|
+
// ⚠️ LEGACY / DEAD SETUP PATH ⚠️
|
|
1778
|
+
// The functions below (setupClaudeAgent, setupCursorAgent, copyAgentRules,
|
|
1779
|
+
// setupClaudeMcpConfig, setupAgent, quickSetup, dryRunSetup, …) are the OLD
|
|
1780
|
+
// inline setup implementation. `forge setup` is now the registry command in
|
|
1781
|
+
// lib/commands/setup.js, which takes priority in the dispatcher and `return`s
|
|
1782
|
+
// before the inline `if (command === 'setup')` block is ever reached. This copy
|
|
1783
|
+
// is kept only until the inline setup subsystem is fully removed — the authority
|
|
1784
|
+
// is lib/commands/setup.js. Do not add behavior here; edit lib/commands/setup.js.
|
|
1908
1785
|
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
//
|
|
1786
|
+
// Helper: Setup Claude agent
|
|
1787
|
+
function setupClaudeAgent() {
|
|
1788
|
+
// Skills-only surface: per-skill SKILL.md dirs are populated by createAgentSkill.
|
|
1789
|
+
// Claude receives policy via CLAUDE.md → AGENTS.md, NOT an always-on
|
|
1790
|
+
// `.claude/rules/workflow.md` (that canonical file was removed). Only scripts copy.
|
|
1914
1791
|
const scriptSrc = path.join(packageDir, '.claude/scripts/load-env.sh');
|
|
1915
1792
|
copyFile(scriptSrc, '.claude/scripts/load-env.sh');
|
|
1916
1793
|
}
|
|
@@ -1921,61 +1798,23 @@ function setupCursorAgent() {
|
|
|
1921
1798
|
console.log(' Created: .cursor/rules/forge-workflow.mdc');
|
|
1922
1799
|
}
|
|
1923
1800
|
|
|
1924
|
-
// Helper:
|
|
1925
|
-
function
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
if (agent.needsConversion) {
|
|
1930
|
-
targetContent = stripFrontmatter(content);
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
if (agent.promptFormat) {
|
|
1934
|
-
targetFile = cmd.replace('.md', '.prompt.md');
|
|
1935
|
-
targetContent = stripFrontmatter(content);
|
|
1801
|
+
// Helper: Create skill file for agent
|
|
1802
|
+
function createAgentSkill(agent, agentKey) {
|
|
1803
|
+
if (agentKey === 'codex') {
|
|
1804
|
+
// Codex global skill install is handled via buildCodexSkillInstallPlan in setup.js
|
|
1805
|
+
return;
|
|
1936
1806
|
}
|
|
1937
1807
|
|
|
1938
|
-
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
// Helper: Copy commands for agent
|
|
1942
|
-
function copyAgentCommands(agent, claudeCommands) {
|
|
1943
|
-
if (!claudeCommands) return;
|
|
1944
|
-
if (!agent.needsConversion && !agent.copyCommands && !agent.promptFormat) return;
|
|
1808
|
+
if (!agent.hasSkill || !agent.skillsDir) return;
|
|
1945
1809
|
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1810
|
+
// Skills-only surface: populate every canonical skill into the agent skills dir
|
|
1811
|
+
// (.claude/skills, .cursor/skills) from the packaged canonical `skills/` source.
|
|
1812
|
+
const { written } = populateAgentSkills({
|
|
1813
|
+
sourceRoot: packageDir,
|
|
1814
|
+
targetSkillsDir: path.join(projectRoot, agent.skillsDir),
|
|
1815
|
+
clean: true,
|
|
1950
1816
|
});
|
|
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
|
-
}
|
|
1817
|
+
console.log(` Created: ${written.length} skills in ${agent.skillsDir}/`);
|
|
1979
1818
|
}
|
|
1980
1819
|
|
|
1981
1820
|
// Helper: Setup MCP config for Claude
|
|
@@ -2010,7 +1849,7 @@ function createAgentLinkFile(agent, symlinkOnly = false) {
|
|
|
2010
1849
|
}
|
|
2011
1850
|
|
|
2012
1851
|
// Setup specific agent
|
|
2013
|
-
function setupAgent(agentKey,
|
|
1852
|
+
function setupAgent(agentKey, skipFiles = {}) {
|
|
2014
1853
|
const agent = AGENTS[agentKey];
|
|
2015
1854
|
if (!agent) return;
|
|
2016
1855
|
|
|
@@ -2028,14 +1867,8 @@ function setupAgent(agentKey, claudeCommands, skipFiles = {}) {
|
|
|
2028
1867
|
setupCursorAgent();
|
|
2029
1868
|
}
|
|
2030
1869
|
|
|
2031
|
-
// Convert/copy commands
|
|
2032
|
-
copyAgentCommands(agent, claudeCommands);
|
|
2033
|
-
|
|
2034
|
-
// Copy rules if needed
|
|
2035
|
-
copyAgentRules(agent);
|
|
2036
|
-
|
|
2037
1870
|
// Create SKILL.md
|
|
2038
|
-
createAgentSkill(agent);
|
|
1871
|
+
createAgentSkill(agent, agentKey);
|
|
2039
1872
|
|
|
2040
1873
|
// Setup MCP configs
|
|
2041
1874
|
if (agentKey === 'claude') {
|
|
@@ -2069,7 +1902,6 @@ function displayInstallationStatus(projectStatus) {
|
|
|
2069
1902
|
}
|
|
2070
1903
|
|
|
2071
1904
|
if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
|
|
2072
|
-
if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
|
|
2073
1905
|
if (projectStatus.hasEnvLocal) console.log(' - .env.local');
|
|
2074
1906
|
console.log('');
|
|
2075
1907
|
}
|
|
@@ -2133,7 +1965,6 @@ async function promptForFileOverwrite(question, fileType, exists, skipFiles) {
|
|
|
2133
1965
|
|
|
2134
1966
|
const fileLabels = {
|
|
2135
1967
|
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
1968
|
};
|
|
2138
1969
|
|
|
2139
1970
|
const config = fileLabels[fileType];
|
|
@@ -2308,35 +2139,12 @@ async function installAgentsMd(skipFiles) {
|
|
|
2308
2139
|
}
|
|
2309
2140
|
}
|
|
2310
2141
|
|
|
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
2142
|
/**
|
|
2335
2143
|
* Setup agents with progress indication
|
|
2336
2144
|
* Delegates to setupSelectedAgents to avoid duplicate implementations (S4144)
|
|
2337
2145
|
*/
|
|
2338
|
-
function setupAgentsWithProgress(selectedAgents,
|
|
2339
|
-
setupSelectedAgents(selectedAgents,
|
|
2146
|
+
function setupAgentsWithProgress(selectedAgents, skipFiles) {
|
|
2147
|
+
setupSelectedAgents(selectedAgents, skipFiles);
|
|
2340
2148
|
}
|
|
2341
2149
|
|
|
2342
2150
|
/**
|
|
@@ -2351,15 +2159,11 @@ function displaySetupSummary(selectedAgents) {
|
|
|
2351
2159
|
console.log('What\'s installed:');
|
|
2352
2160
|
console.log(' - AGENTS.md (universal instructions)');
|
|
2353
2161
|
|
|
2354
|
-
const workflowCount = getWorkflowCommands().length;
|
|
2355
2162
|
selectedAgents.forEach(key => {
|
|
2356
2163
|
const agent = AGENTS[key];
|
|
2357
2164
|
if (agent.linkFile) {
|
|
2358
2165
|
console.log(` - ${agent.linkFile} (${agent.name})`);
|
|
2359
2166
|
}
|
|
2360
|
-
if (agent.hasCommands) {
|
|
2361
|
-
console.log(` - .claude/commands/ (${workflowCount} workflow commands)`);
|
|
2362
|
-
}
|
|
2363
2167
|
if (agent.hasSkill) {
|
|
2364
2168
|
const skillDir = agent.dirs.find(d => d.includes('/skills/'));
|
|
2365
2169
|
if (skillDir) {
|
|
@@ -2464,12 +2268,10 @@ async function _interactiveSetup() {
|
|
|
2464
2268
|
// Track which files to skip based on user choices
|
|
2465
2269
|
const skipFiles = {
|
|
2466
2270
|
agentsMd: false,
|
|
2467
|
-
claudeCommands: false
|
|
2468
2271
|
};
|
|
2469
2272
|
|
|
2470
2273
|
// Ask about overwriting existing files
|
|
2471
2274
|
await promptForFileOverwrite(question, 'agentsMd', projectStatus.hasAgentsMd, skipFiles);
|
|
2472
|
-
await promptForFileOverwrite(question, 'claudeCommands', projectStatus.hasClaudeCommands, skipFiles);
|
|
2473
2275
|
|
|
2474
2276
|
if (projectStatus.type !== 'fresh') {
|
|
2475
2277
|
console.log('');
|
|
@@ -2492,19 +2294,13 @@ async function _interactiveSetup() {
|
|
|
2492
2294
|
setupCoreDocs();
|
|
2493
2295
|
console.log('');
|
|
2494
2296
|
|
|
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);
|
|
2297
|
+
// Setup Claude first if selected, then remaining agents
|
|
2298
|
+
if (selectedAgents.includes('claude')) {
|
|
2299
|
+
setupAgent('claude', skipFiles);
|
|
2504
2300
|
}
|
|
2505
2301
|
|
|
2506
2302
|
// Setup each selected agent with progress indication
|
|
2507
|
-
setupAgentsWithProgress(selectedAgents,
|
|
2303
|
+
setupAgentsWithProgress(selectedAgents, skipFiles);
|
|
2508
2304
|
|
|
2509
2305
|
// =============================================
|
|
2510
2306
|
// STEP 2: Project Tools Setup
|
|
@@ -2530,6 +2326,12 @@ async function _interactiveSetup() {
|
|
|
2530
2326
|
}
|
|
2531
2327
|
|
|
2532
2328
|
// Parse CLI flags
|
|
2329
|
+
// The curated back-compat alias allowlist (formerly the inline
|
|
2330
|
+
// ISSUE_ALIAS_COMMANDS array) now lives in lib/commands/_aliases.js as the single
|
|
2331
|
+
// declarative source of truth. `aliasNames()` returns the bare verbs that stay
|
|
2332
|
+
// registered + routable but are hidden from `forge --help` (kernel issue 450c6e34),
|
|
2333
|
+
// so `forge issue` reads as the single canonical issue surface. The canonical
|
|
2334
|
+
// `issue` is deliberately not an alias — it stays documented.
|
|
2533
2335
|
function parseFlags() {
|
|
2534
2336
|
const flags = {
|
|
2535
2337
|
quick: false,
|
|
@@ -2554,7 +2356,11 @@ function parseFlags() {
|
|
|
2554
2356
|
|
|
2555
2357
|
// Issue passthrough commands delegate all flags to bd.
|
|
2556
2358
|
// Skip global parsing so flags like --type, -p, --help reach the handler intact.
|
|
2557
|
-
|
|
2359
|
+
// Canonical `issue` plus the ISSUE-canonical back-compat aliases only. Non-issue
|
|
2360
|
+
// aliases (e.g. the memory shortcuts remember/recall/insights) are excluded so
|
|
2361
|
+
// their global flags still parse exactly as their standalone commands' did —
|
|
2362
|
+
// keeping bare `recall -p <dir>` / `recall --help` byte-identical.
|
|
2363
|
+
const issuePassthroughCommands = [...passthroughAliasNames(), 'issue'];
|
|
2558
2364
|
if (issuePassthroughCommands.includes(args[0])) {
|
|
2559
2365
|
return flags;
|
|
2560
2366
|
}
|
|
@@ -2757,6 +2563,7 @@ function showHelp() {
|
|
|
2757
2563
|
console.log('Options:');
|
|
2758
2564
|
console.log(' --path, -p <dir> Target project directory (default: current directory)');
|
|
2759
2565
|
console.log(' Creates the directory if it doesn\'t exist');
|
|
2566
|
+
console.log(' Exception: docs verify|detect --path requires an existing directory');
|
|
2760
2567
|
console.log(' --quick, -q Use all defaults, minimal prompts');
|
|
2761
2568
|
console.log(' Auto-selects: all agents, GitHub Code Quality, ESLint');
|
|
2762
2569
|
console.log(' --skip-external Skip external services configuration');
|
|
@@ -2802,9 +2609,44 @@ function showHelp() {
|
|
|
2802
2609
|
console.log('Also works with bun:');
|
|
2803
2610
|
console.log(' bunx forge setup --quick');
|
|
2804
2611
|
console.log('');
|
|
2612
|
+
// Adoption profile + gate/classification config is owned by `forge init`, not
|
|
2613
|
+
// `forge setup`. The --minimal/--standard/--full profile flags are `forge init`
|
|
2614
|
+
// shortcuts (setup only reads them for `--dry-run` preview), so point there.
|
|
2615
|
+
console.log('Adoption profile, gates & classification (configured by `forge init`, not setup):');
|
|
2616
|
+
console.log(' forge init --minimal | --standard | --full # adoption-profile shortcut');
|
|
2617
|
+
console.log(' # (or: --profile minimal|standard|full)');
|
|
2618
|
+
console.log(' forge init --classification critical|standard|refactor');
|
|
2619
|
+
console.log(' `forge init` writes `.forge/config.yaml` (gate + classification config).');
|
|
2620
|
+
console.log(' Run `forge init --help` for all profile/classification/harness flags.');
|
|
2621
|
+
console.log('');
|
|
2622
|
+
|
|
2623
|
+
// Shortcuts block: VISIBLE back-compat aliases for a canonical `<noun> <sub>`
|
|
2624
|
+
// form (e.g. `remember` -> `forge memory add`). The bare verb keeps working and
|
|
2625
|
+
// is documented here; the noun form is canonical. Rendered as its own block ABOVE
|
|
2626
|
+
// "Additional commands" (and trimmed from that enumeration below) so the noun
|
|
2627
|
+
// surface reads clean.
|
|
2628
|
+
const shortcutNames = visibleAliasNames();
|
|
2629
|
+
if (shortcutNames.length > 0) {
|
|
2630
|
+
console.log('Shortcuts (bare aliases for canonical noun subcommands):');
|
|
2631
|
+
const shortcutWidth = Math.max(...shortcutNames.map(name => name.length));
|
|
2632
|
+
for (const name of shortcutNames) {
|
|
2633
|
+
console.log(` ${name.padEnd(shortcutWidth)} -> forge ${resolveAlias(name).canonical}`);
|
|
2634
|
+
}
|
|
2635
|
+
console.log('');
|
|
2636
|
+
}
|
|
2805
2637
|
|
|
2806
|
-
// Append auto-discovered registry commands
|
|
2638
|
+
// Append auto-discovered registry commands. Hidden issue aliases (the bare
|
|
2639
|
+
// passthroughs + plural `issues`, plus any command self-declaring `hidden: true`)
|
|
2640
|
+
// AND the visible noun shortcuts rendered above are trimmed from this enumeration
|
|
2641
|
+
// so the canonical noun surface reads clean; both remain routable. This registry
|
|
2642
|
+
// instance is loaded solely to render help, so deleting from its Map only affects
|
|
2643
|
+
// the printed list — command dispatch (main) uses a separate registry.
|
|
2807
2644
|
const helpRegistry = loadCommands(path.join(__dirname, '..', 'lib', 'commands'));
|
|
2645
|
+
for (const [name, cmd] of [...helpRegistry.commands]) {
|
|
2646
|
+
if (isHiddenAlias(name) || isVisibleAlias(name) || cmd.hidden === true) {
|
|
2647
|
+
helpRegistry.commands.delete(name);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2808
2650
|
const registryHelp = helpRegistry.getHelp();
|
|
2809
2651
|
if (registryHelp) {
|
|
2810
2652
|
console.log('Additional commands:');
|
|
@@ -2813,6 +2655,26 @@ function showHelp() {
|
|
|
2813
2655
|
}
|
|
2814
2656
|
}
|
|
2815
2657
|
|
|
2658
|
+
// Per-command help. When `--help` follows a known subcommand (e.g. `forge status
|
|
2659
|
+
// --help`), render THAT command's usage/description/flags instead of the global
|
|
2660
|
+
// setup banner — a newcomer asking for a command's help should not get the
|
|
2661
|
+
// installer's help.
|
|
2662
|
+
function printCommandHelp(cmd) {
|
|
2663
|
+
console.log(`forge ${cmd.name} — ${cmd.description}`);
|
|
2664
|
+
if (cmd.usage) {
|
|
2665
|
+
console.log('');
|
|
2666
|
+
console.log(/^(Usage:|forge )/.test(cmd.usage) ? cmd.usage : `Usage: ${cmd.usage}`);
|
|
2667
|
+
}
|
|
2668
|
+
const flagEntries = cmd.flags && typeof cmd.flags === 'object' ? Object.entries(cmd.flags) : [];
|
|
2669
|
+
if (flagEntries.length > 0) {
|
|
2670
|
+
console.log('');
|
|
2671
|
+
console.log('Flags:');
|
|
2672
|
+
for (const [flag, desc] of flagEntries) {
|
|
2673
|
+
console.log(` ${flag.padEnd(16)} ${desc}`);
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2816
2678
|
// Detect Husky and offer migration to Lefthook
|
|
2817
2679
|
// Called before installGitHooks() in setup flows
|
|
2818
2680
|
async function handleHuskyMigration() {
|
|
@@ -2863,80 +2725,104 @@ async function handleHuskyMigration() {
|
|
|
2863
2725
|
function installGitHooks() {
|
|
2864
2726
|
console.log('Installing git hooks (TDD enforcement)...');
|
|
2865
2727
|
|
|
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
|
-
}
|
|
2728
|
+
// Install the Forge hook SCRIPTS first, UNCONDITIONALLY - they back BOTH the lefthook
|
|
2729
|
+
// pre-commit job AND the native .git/hooks fallback below, so they must exist whether
|
|
2730
|
+
// or not the lefthook binary is available.
|
|
2731
|
+
installForgeHookScripts();
|
|
2876
2732
|
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
const targetHooks = path.join(projectRoot, '.forge/hooks');
|
|
2733
|
+
const lefthookStatus = checkLefthookStatus(projectRoot);
|
|
2734
|
+
let lefthookInstalled = false;
|
|
2880
2735
|
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2736
|
+
if (lefthookStatus.binaryAvailable) {
|
|
2737
|
+
try {
|
|
2738
|
+
// Write the REAL user-facing lefthook.yml - never the repo's own dev config (which
|
|
2739
|
+
// references repo-internal scripts/ a user project lacks), and never leave
|
|
2740
|
+
// lefthook's stock commented-out example in place. Overwrite only a missing file
|
|
2741
|
+
// or a fully-commented stub, never a config with active jobs (kernel e452422c).
|
|
2742
|
+
const lefthookTarget = path.join(projectRoot, 'lefthook.yml');
|
|
2743
|
+
if (forgeShouldWriteLefthookConfig(lefthookTarget)) {
|
|
2744
|
+
fs.writeFileSync(lefthookTarget, FORGE_USER_LEFTHOOK_YML, 'utf8');
|
|
2886
2745
|
console.log(' ✓ Created lefthook.yml');
|
|
2887
2746
|
}
|
|
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
2747
|
|
|
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
|
|
2748
|
+
// Install lefthook's git hooks. Try local (npx) first, then a global binary.
|
|
2749
|
+
// SECURITY: execFileSync with hardcoded commands (no user input).
|
|
2916
2750
|
try {
|
|
2917
2751
|
secureExecFileSync('npx', ['lefthook', 'install'], { stdio: 'inherit', cwd: projectRoot });
|
|
2918
2752
|
console.log(' ✓ Lefthook hooks installed (local)');
|
|
2753
|
+
lefthookInstalled = true;
|
|
2919
2754
|
} catch (error_) {
|
|
2920
|
-
// Fallback to global lefthook
|
|
2921
2755
|
console.warn('npx lefthook failed, trying global:', error_.message);
|
|
2922
|
-
|
|
2923
|
-
|
|
2756
|
+
secureExecFileSync('lefthook', ['version'], { stdio: 'ignore' });
|
|
2757
|
+
secureExecFileSync('lefthook', ['install'], { stdio: 'inherit', cwd: projectRoot });
|
|
2924
2758
|
console.log(' ✓ Lefthook hooks installed (global)');
|
|
2759
|
+
lefthookInstalled = true;
|
|
2925
2760
|
}
|
|
2926
2761
|
} catch (err) {
|
|
2927
2762
|
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
2763
|
}
|
|
2764
|
+
} else if (lefthookStatus.message) {
|
|
2765
|
+
console.warn(` ⚠ lefthook binary unavailable: ${lefthookStatus.message}`);
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
// Native fallback: when lefthook is unavailable or its install failed, wire native
|
|
2769
|
+
// .git/hooks so raw `git commit` / `git push` still enforce the TDD gate - the moat is
|
|
2770
|
+
// never silently inert (B3). Runs regardless of whether a package.json exists.
|
|
2771
|
+
if (!lefthookInstalled) {
|
|
2772
|
+
const native = installNativeGitHooks(projectRoot);
|
|
2773
|
+
if (native.installed) {
|
|
2774
|
+
console.log(` ✓ Native git hooks installed (${native.written.join(', ')}) - lefthook fallback`);
|
|
2775
|
+
} else if (native.skipped && native.skipped.length > 0) {
|
|
2776
|
+
console.warn(` ⚠ Native hook(s) skipped to preserve existing hooks: ${native.skipped.join(', ')}`);
|
|
2777
|
+
} else {
|
|
2778
|
+
console.warn(` ⚠ Could not install native git hooks: ${native.reason || 'unknown'}`);
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2933
2781
|
|
|
2934
|
-
|
|
2782
|
+
// VERIFY LOUDLY: never let setup silently no-op. If pre-commit enforcement is not
|
|
2783
|
+
// actually active after all of the above, say so unmistakably and set a failure code.
|
|
2784
|
+
const verdict = verifyHooksActive(projectRoot);
|
|
2785
|
+
if (verdict.active) {
|
|
2786
|
+
console.log(` ✓ Git hook enforcement active (${verdict.method}).`);
|
|
2787
|
+
} else {
|
|
2788
|
+
const addCmd = PKG_MANAGER === 'bun'
|
|
2789
|
+
? 'bun add -d'
|
|
2790
|
+
: PKG_MANAGER === 'npm'
|
|
2791
|
+
? 'npm install --save-dev'
|
|
2792
|
+
: `${PKG_MANAGER} add -D`;
|
|
2793
|
+
console.error('');
|
|
2794
|
+
console.error(' ============================================================');
|
|
2795
|
+
console.error(' ⚠ TDD ENFORCEMENT IS NOT ACTIVE');
|
|
2796
|
+
console.error(` ${verdict.reason || 'no pre-commit hook is installed'}.`);
|
|
2797
|
+
console.error(' `forge ship` will block until hooks are active. To fix:');
|
|
2798
|
+
console.error(` ${addCmd} lefthook && npx lefthook install`);
|
|
2799
|
+
console.error(' (or re-run `forge setup` in the repo root).');
|
|
2800
|
+
console.error(' ============================================================');
|
|
2801
|
+
console.error('');
|
|
2802
|
+
process.exitCode = 1;
|
|
2803
|
+
}
|
|
2935
2804
|
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2805
|
+
console.log('');
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
// Copy the Forge hook scripts (check-tdd.js + the native-hook adapter) into the
|
|
2809
|
+
// project's .forge/hooks/. Runs UNCONDITIONALLY - independent of the lefthook binary -
|
|
2810
|
+
// because both the lefthook pre-commit job and the native fallback invoke them.
|
|
2811
|
+
function installForgeHookScripts() {
|
|
2812
|
+
const targetHooks = path.join(projectRoot, '.forge/hooks');
|
|
2813
|
+
for (const name of ['check-tdd.js', 'forge-native-hook.js']) {
|
|
2814
|
+
const src = path.join(packageDir, '.forge/hooks', name);
|
|
2815
|
+
if (!fs.existsSync(src)) continue;
|
|
2816
|
+
if (!fs.existsSync(targetHooks)) fs.mkdirSync(targetHooks, { recursive: true });
|
|
2817
|
+
const dest = path.join(targetHooks, name);
|
|
2818
|
+
if (copyFile(src, dest)) {
|
|
2819
|
+
console.log(` ✓ Created .forge/hooks/${name}`);
|
|
2820
|
+
try {
|
|
2821
|
+
fs.chmodSync(dest, 0o755); // NOSONAR - hook scripts must be executable
|
|
2822
|
+
} catch (err) {
|
|
2823
|
+
console.warn('chmod not available (Windows):', err.message);
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2940
2826
|
}
|
|
2941
2827
|
}
|
|
2942
2828
|
|
|
@@ -2987,6 +2873,18 @@ function isBeadsInitialized() {
|
|
|
2987
2873
|
return beadsSetupLib.isBeadsInitialized(projectRoot);
|
|
2988
2874
|
}
|
|
2989
2875
|
|
|
2876
|
+
function migrateExistingBeadsLocalState() {
|
|
2877
|
+
if (!fs.existsSync(path.join(projectRoot, '.beads'))) {
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
try {
|
|
2882
|
+
beadsSetupLib.ensureBeadsGitExclude(projectRoot);
|
|
2883
|
+
} catch (err) {
|
|
2884
|
+
console.warn(` Warning: failed to migrate Beads local state: ${err.message}`);
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2990
2888
|
// Initialize Beads in the project using the defensive safeBeadsInit wrapper
|
|
2991
2889
|
// Handles config/gitignore writes, hook snapshot/restore, and JSONL pre-seeding
|
|
2992
2890
|
function initializeBeads(installType) {
|
|
@@ -3044,18 +2942,6 @@ function initializeBeads(installType) {
|
|
|
3044
2942
|
}
|
|
3045
2943
|
console.log(' ✓ Beads initialized');
|
|
3046
2944
|
|
|
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
2945
|
return true;
|
|
3060
2946
|
} catch (err) {
|
|
3061
2947
|
console.log(' ⚠ Failed to initialize Beads:', err.message);
|
|
@@ -3131,6 +3017,7 @@ async function promptBeadsSetup(question) {
|
|
|
3131
3017
|
const beadsStatus = checkForBeads();
|
|
3132
3018
|
|
|
3133
3019
|
if (beadsInitialized) {
|
|
3020
|
+
migrateExistingBeadsLocalState();
|
|
3134
3021
|
console.log('✓ Beads is already initialized in this project');
|
|
3135
3022
|
console.log('');
|
|
3136
3023
|
return;
|
|
@@ -3364,6 +3251,10 @@ function autoSetupBeadsInQuickMode() {
|
|
|
3364
3251
|
const beadsStatus = checkForBeads();
|
|
3365
3252
|
const beadsInitialized = isBeadsInitialized();
|
|
3366
3253
|
|
|
3254
|
+
if (beadsInitialized) {
|
|
3255
|
+
migrateExistingBeadsLocalState();
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3367
3258
|
if (!beadsInitialized && beadsStatus) {
|
|
3368
3259
|
console.log('📦 Initializing Beads...');
|
|
3369
3260
|
initializeBeads(beadsStatus);
|
|
@@ -3415,7 +3306,19 @@ function autoInstallLefthook() {
|
|
|
3415
3306
|
return;
|
|
3416
3307
|
}
|
|
3417
3308
|
|
|
3418
|
-
// Not in package.json at all — full install
|
|
3309
|
+
// Not in package.json at all — full install.
|
|
3310
|
+
// GUARD (kernel 22e33dbf): with no package.json in projectRoot, an
|
|
3311
|
+
// `npm install --save-dev lefthook` / `bun add lefthook` resolves against the nearest
|
|
3312
|
+
// ANCESTOR package.json and installs lefthook into the WRONG project (or fails). Skip
|
|
3313
|
+
// the package-manager install entirely — installGitHooks() then wires native
|
|
3314
|
+
// .git/hooks, which need no package.json, so enforcement is still live.
|
|
3315
|
+
if (!fs.existsSync(path.join(projectRoot, 'package.json'))) {
|
|
3316
|
+
console.log(' ℹ No package.json here — skipping lefthook npm install (would target an ancestor).');
|
|
3317
|
+
console.log(' Git hook enforcement will use the native .git/hooks fallback instead.');
|
|
3318
|
+
console.log('');
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3419
3322
|
console.log('📦 Installing lefthook for git hooks...');
|
|
3420
3323
|
try {
|
|
3421
3324
|
// SECURITY: secureExecFileSync with PKG_MANAGER — cross-platform support
|
|
@@ -3463,11 +3366,10 @@ function autoSetupToolsInQuickMode() {
|
|
|
3463
3366
|
console.log('📦 Initializing Skills...');
|
|
3464
3367
|
initializeSkills(skillsStatus);
|
|
3465
3368
|
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
3369
|
}
|
|
3370
|
+
// No hint when the optional Skills CLI is absent: setup bundles and renders
|
|
3371
|
+
// Forge's skills itself, and the previously advertised "@forge/skills"
|
|
3372
|
+
// package does not exist on npm (kernel issue 6e554b41).
|
|
3471
3373
|
}
|
|
3472
3374
|
|
|
3473
3375
|
// Helper: Configure default external services in quick mode - extracted to reduce cognitive complexity
|
|
@@ -3522,9 +3424,11 @@ async function quickSetup(selectedAgents, skipExternal) {
|
|
|
3522
3424
|
// Auto-setup project tools (Beads, Skills)
|
|
3523
3425
|
autoSetupToolsInQuickMode();
|
|
3524
3426
|
|
|
3525
|
-
//
|
|
3526
|
-
|
|
3527
|
-
|
|
3427
|
+
// Setup Claude first if selected, then remaining agents
|
|
3428
|
+
if (selectedAgents.includes('claude')) {
|
|
3429
|
+
setupAgent('claude');
|
|
3430
|
+
}
|
|
3431
|
+
setupSelectedAgents(selectedAgents);
|
|
3528
3432
|
|
|
3529
3433
|
// Detect Husky and migrate before installing Lefthook hooks
|
|
3530
3434
|
await handleHuskyMigration();
|
|
@@ -3649,7 +3553,6 @@ function displayExistingInstallation(projectStatus) {
|
|
|
3649
3553
|
: 'Found partial installation:');
|
|
3650
3554
|
|
|
3651
3555
|
if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
|
|
3652
|
-
if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
|
|
3653
3556
|
if (projectStatus.hasEnvLocal) console.log(' - .env.local');
|
|
3654
3557
|
console.log('');
|
|
3655
3558
|
}
|
|
@@ -3658,7 +3561,6 @@ function displayExistingInstallation(projectStatus) {
|
|
|
3658
3561
|
async function promptForOverwriteDecisions(question, projectStatus) {
|
|
3659
3562
|
const skipFiles = {
|
|
3660
3563
|
agentsMd: false,
|
|
3661
|
-
claudeCommands: false
|
|
3662
3564
|
};
|
|
3663
3565
|
|
|
3664
3566
|
if (projectStatus.hasAgentsMd) {
|
|
@@ -3667,12 +3569,6 @@ async function promptForOverwriteDecisions(question, projectStatus) {
|
|
|
3667
3569
|
console.log(overwriteAgents ? ' Will overwrite AGENTS.md' : ' Keeping existing AGENTS.md');
|
|
3668
3570
|
}
|
|
3669
3571
|
|
|
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
3572
|
if (projectStatus.type !== 'fresh') {
|
|
3677
3573
|
console.log('');
|
|
3678
3574
|
}
|
|
@@ -3680,41 +3576,14 @@ async function promptForOverwriteDecisions(question, projectStatus) {
|
|
|
3680
3576
|
return skipFiles;
|
|
3681
3577
|
}
|
|
3682
3578
|
|
|
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
3579
|
// Helper: Setup all selected agents - extracted to reduce cognitive complexity
|
|
3711
|
-
function setupSelectedAgents(selectedAgents,
|
|
3580
|
+
function setupSelectedAgents(selectedAgents, skipFiles) {
|
|
3712
3581
|
const totalAgents = selectedAgents.length;
|
|
3713
3582
|
selectedAgents.forEach((agentKey, index) => {
|
|
3714
3583
|
const agent = AGENTS[agentKey];
|
|
3715
3584
|
console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
|
|
3716
3585
|
if (agentKey !== 'claude') { // Claude already done above
|
|
3717
|
-
setupAgent(agentKey,
|
|
3586
|
+
setupAgent(agentKey, skipFiles);
|
|
3718
3587
|
}
|
|
3719
3588
|
});
|
|
3720
3589
|
|
|
@@ -3814,11 +3683,13 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3814
3683
|
setupCoreDocs();
|
|
3815
3684
|
console.log('');
|
|
3816
3685
|
|
|
3817
|
-
//
|
|
3818
|
-
|
|
3686
|
+
// Setup Claude first if selected (delegated to helper), then remaining agents
|
|
3687
|
+
if (selectedAgents.includes('claude')) {
|
|
3688
|
+
setupAgent('claude', skipFiles);
|
|
3689
|
+
}
|
|
3819
3690
|
|
|
3820
3691
|
// Setup each selected agent with progress indication (delegated to helper)
|
|
3821
|
-
setupSelectedAgents(selectedAgents,
|
|
3692
|
+
setupSelectedAgents(selectedAgents, skipFiles);
|
|
3822
3693
|
|
|
3823
3694
|
// Handle external services step (delegated to helper)
|
|
3824
3695
|
await handleExternalServicesStep(flags, rl, question, selectedAgents, projectStatus);
|
|
@@ -3832,14 +3703,20 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3832
3703
|
|
|
3833
3704
|
// Main
|
|
3834
3705
|
// Helper: Handle --path setup
|
|
3835
|
-
function handlePathSetup(targetPath) {
|
|
3706
|
+
function handlePathSetup(targetPath, options = {}) {
|
|
3836
3707
|
const resolvedPath = path.resolve(targetPath);
|
|
3837
3708
|
|
|
3838
3709
|
// Create directory if it doesn't exist
|
|
3839
3710
|
if (!fs.existsSync(resolvedPath)) {
|
|
3711
|
+
if (options.create === false) {
|
|
3712
|
+
console.error(`Error: ${resolvedPath} does not exist`);
|
|
3713
|
+
process.exit(1);
|
|
3714
|
+
}
|
|
3840
3715
|
try {
|
|
3841
|
-
|
|
3716
|
+
fs.mkdirSync(resolvedPath, { recursive: true });
|
|
3717
|
+
if (!options.quiet) {
|
|
3842
3718
|
console.log(`Created directory: ${resolvedPath}`);
|
|
3719
|
+
}
|
|
3843
3720
|
} catch (err) {
|
|
3844
3721
|
console.error(`Error creating directory: ${err.message}`);
|
|
3845
3722
|
process.exit(1);
|
|
@@ -3855,8 +3732,10 @@ function handlePathSetup(targetPath) {
|
|
|
3855
3732
|
// Change to target directory
|
|
3856
3733
|
try {
|
|
3857
3734
|
process.chdir(resolvedPath);
|
|
3858
|
-
|
|
3859
|
-
|
|
3735
|
+
if (!options.quiet) {
|
|
3736
|
+
console.log(`Working directory: ${resolvedPath}`);
|
|
3737
|
+
console.log('');
|
|
3738
|
+
}
|
|
3860
3739
|
} catch (err) {
|
|
3861
3740
|
console.error(`Error changing to directory: ${err.message}`);
|
|
3862
3741
|
process.exit(1);
|
|
@@ -3866,6 +3745,15 @@ function handlePathSetup(targetPath) {
|
|
|
3866
3745
|
return resolvedPath;
|
|
3867
3746
|
}
|
|
3868
3747
|
|
|
3748
|
+
function resolvePathForDryRun(targetPath) {
|
|
3749
|
+
const resolvedPath = path.resolve(targetPath);
|
|
3750
|
+
if (fs.existsSync(resolvedPath) && !fs.statSync(resolvedPath).isDirectory()) {
|
|
3751
|
+
console.error(`Error: ${resolvedPath} is not a directory`);
|
|
3752
|
+
process.exit(1);
|
|
3753
|
+
}
|
|
3754
|
+
return resolvedPath;
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3869
3757
|
// Helper: Determine selected agents from flags
|
|
3870
3758
|
function determineSelectedAgents(flags) {
|
|
3871
3759
|
if (flags.all) {
|
|
@@ -3914,15 +3802,10 @@ function dryRunSetup(agents) {
|
|
|
3914
3802
|
addFileAction(dir + '/', 'Create agent directory');
|
|
3915
3803
|
}
|
|
3916
3804
|
|
|
3917
|
-
// Claude-specific files
|
|
3805
|
+
// Claude-specific files (skills are listed by the per-skill block below)
|
|
3918
3806
|
if (agentKey === 'claude') {
|
|
3919
|
-
const cmds = getWorkflowCommands();
|
|
3920
|
-
for (const cmd of cmds) {
|
|
3921
|
-
addFileAction(`.claude/commands/${cmd}.md`, 'Workflow command');
|
|
3922
|
-
}
|
|
3923
3807
|
addFileAction('.claude/rules/workflow.md', 'Workflow rules');
|
|
3924
3808
|
addFileAction('.claude/scripts/load-env.sh', 'Environment loader script');
|
|
3925
|
-
addFileAction('.claude/skills/forge-workflow/SKILL.md', 'Forge workflow skill');
|
|
3926
3809
|
addFileAction('.mcp.json', 'MCP server configuration');
|
|
3927
3810
|
addFileAction('CLAUDE.md', 'Claude root config (links to AGENTS.md)');
|
|
3928
3811
|
}
|
|
@@ -3932,15 +3815,7 @@ function dryRunSetup(agents) {
|
|
|
3932
3815
|
addFileAction('.cursor/rules/forge-workflow.mdc', 'Cursor workflow rule');
|
|
3933
3816
|
}
|
|
3934
3817
|
|
|
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
|
-
}
|
|
3818
|
+
|
|
3944
3819
|
|
|
3945
3820
|
// Agent rules (copied from Claude)
|
|
3946
3821
|
if (agent.needsConversion) {
|
|
@@ -3950,11 +3825,10 @@ function dryRunSetup(agents) {
|
|
|
3950
3825
|
}
|
|
3951
3826
|
}
|
|
3952
3827
|
|
|
3953
|
-
// Agent skill
|
|
3954
|
-
if (agent.hasSkill) {
|
|
3955
|
-
const
|
|
3956
|
-
|
|
3957
|
-
addFileAction(`${skillDir}/SKILL.md`, 'Forge workflow skill');
|
|
3828
|
+
// Agent skill — enumerate canonical skills per-agent (codex handled globally elsewhere)
|
|
3829
|
+
if (agentKey !== 'codex' && agent.hasSkill && agent.skillsDir) {
|
|
3830
|
+
for (const skill of listCanonicalSkills(packageDir)) {
|
|
3831
|
+
addFileAction(`${agent.skillsDir}/${skill.name}/SKILL.md`, 'Forge stage skill');
|
|
3958
3832
|
}
|
|
3959
3833
|
}
|
|
3960
3834
|
|
|
@@ -3988,6 +3862,8 @@ async function executeSetup(config) {
|
|
|
3988
3862
|
checkPrerequisites();
|
|
3989
3863
|
console.log('');
|
|
3990
3864
|
|
|
3865
|
+
migrateExistingBeadsLocalState();
|
|
3866
|
+
|
|
3991
3867
|
// Copy AGENTS.md (only if not exists — preserve user customizations; actionLog tracks it)
|
|
3992
3868
|
const agentsDest = path.join(projectRoot, 'AGENTS.md');
|
|
3993
3869
|
if (fs.existsSync(agentsDest)) {
|
|
@@ -4002,15 +3878,11 @@ async function executeSetup(config) {
|
|
|
4002
3878
|
setupCoreDocs();
|
|
4003
3879
|
console.log('');
|
|
4004
3880
|
|
|
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);
|
|
3881
|
+
// Setup Claude first if selected, then remaining agents
|
|
3882
|
+
if (agents.includes('claude')) {
|
|
3883
|
+
setupAgent('claude');
|
|
3884
|
+
}
|
|
3885
|
+
setupSelectedAgents(agents);
|
|
4014
3886
|
|
|
4015
3887
|
// Detect Husky and migrate before installing Lefthook hooks
|
|
4016
3888
|
await handleHuskyMigration();
|
|
@@ -4036,36 +3908,12 @@ async function executeSetup(config) {
|
|
|
4036
3908
|
// Helper: Scaffold Beads GitHub sync when --sync flag is provided
|
|
4037
3909
|
async function handleSyncScaffold() {
|
|
4038
3910
|
console.log('');
|
|
4039
|
-
console.log('
|
|
3911
|
+
console.log('Beads GitHub sync scaffolding is deprecated (--sync).');
|
|
4040
3912
|
try {
|
|
4041
|
-
// Scaffold sync files using the new lib module
|
|
4042
3913
|
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
|
|
3914
|
+
console.log(` ${result.message}`);
|
|
3915
|
+
for (const f of result.filesRemoved || []) {
|
|
3916
|
+
console.log(` Removed deprecated sync file: ${f}`);
|
|
4069
3917
|
}
|
|
4070
3918
|
} catch (err) {
|
|
4071
3919
|
console.error(` Error scaffolding GitHub-Beads sync: ${err.message}`);
|
|
@@ -4089,6 +3937,11 @@ async function handleSetupCommand(selectedAgents, flags) {
|
|
|
4089
3937
|
}
|
|
4090
3938
|
}
|
|
4091
3939
|
|
|
3940
|
+
async function runInteractiveSetupFallback(flags, interactiveSetup = interactiveSetupWithFlags) {
|
|
3941
|
+
migrateExistingBeadsLocalState();
|
|
3942
|
+
return interactiveSetup(flags);
|
|
3943
|
+
}
|
|
3944
|
+
|
|
4092
3945
|
// Helper: Handle external services configuration
|
|
4093
3946
|
async function handleExternalServices(skipExternal, selectedAgents) {
|
|
4094
3947
|
if (skipExternal) {
|
|
@@ -4117,8 +3970,19 @@ async function handleExternalServices(skipExternal, selectedAgents) {
|
|
|
4117
3970
|
}
|
|
4118
3971
|
|
|
4119
3972
|
async function main() {
|
|
4120
|
-
|
|
3973
|
+
let command = args[0];
|
|
4121
3974
|
const flags = parseFlags();
|
|
3975
|
+
const suppressJsonIntrospectionOutput = ['options', 'explain'].includes(command) && args.includes('--json');
|
|
3976
|
+
const suppressCommandJsonOutput = args.includes('--json');
|
|
3977
|
+
const suppressDocsDetectOutput = command === 'docs' && args[1] === 'detect';
|
|
3978
|
+
const profileDryRunArgs = ['--minimal', '--standard', '--full'];
|
|
3979
|
+
const suppressAdoptionDryRunOutput = flags.dryRun && (
|
|
3980
|
+
command === 'init' || (command === 'setup' && profileDryRunArgs.some(arg => args.includes(arg)))
|
|
3981
|
+
);
|
|
3982
|
+
const suppressStructuredOutput = suppressJsonIntrospectionOutput ||
|
|
3983
|
+
suppressCommandJsonOutput ||
|
|
3984
|
+
suppressDocsDetectOutput ||
|
|
3985
|
+
suppressAdoptionDryRunOutput;
|
|
4122
3986
|
|
|
4123
3987
|
// Wire up incremental setup state from parsed flags
|
|
4124
3988
|
FORCE_MODE = flags.force;
|
|
@@ -4128,20 +3992,33 @@ async function main() {
|
|
|
4128
3992
|
SYNC_ENABLED = flags.sync;
|
|
4129
3993
|
actionLog = new SetupActionLog();
|
|
4130
3994
|
|
|
4131
|
-
|
|
3995
|
+
// The non-interactive agent-selection notice is DEBUG-ONLY (kernel issue
|
|
3996
|
+
// a9bbd065): it used to print on stderr for EVERY command run by a non-TTY
|
|
3997
|
+
// agent/CI, polluting otherwise-clean command output. It only matters when
|
|
3998
|
+
// setup-style agent selection actually happens, so it now requires --verbose
|
|
3999
|
+
// (or FORGE_DEBUG=1) to appear.
|
|
4000
|
+
if (NON_INTERACTIVE && (VERBOSE_MODE || process.env.FORGE_DEBUG === '1') && !suppressStructuredOutput) {
|
|
4132
4001
|
const agentFlag = flags.agents;
|
|
4133
4002
|
if (agentFlag && agentFlag.length > 0) {
|
|
4134
4003
|
// flags.agents is a comma-separated string (e.g. "claude,cursor")
|
|
4135
4004
|
const agentDisplay = agentFlag.replace(/,/g, ', ');
|
|
4136
|
-
console.
|
|
4005
|
+
console.error(`Non-interactive mode: using provided agent selection (${agentDisplay})`);
|
|
4137
4006
|
} else {
|
|
4138
|
-
console.
|
|
4007
|
+
console.error('Non-interactive mode: using default agent selection (all)');
|
|
4139
4008
|
}
|
|
4140
4009
|
}
|
|
4141
4010
|
|
|
4142
|
-
// Show help
|
|
4011
|
+
// Show help. `--help` after a known registry subcommand (e.g. `forge status
|
|
4012
|
+
// --help`) renders that command's usage; bare `forge --help` keeps the global
|
|
4013
|
+
// banner. `setup` is special-cased OUT: the global banner IS setup's detailed
|
|
4014
|
+
// help, so `forge setup --help` must keep showing it (not the terse registry line).
|
|
4143
4015
|
if (flags.help) {
|
|
4144
|
-
|
|
4016
|
+
const helpRegistry = loadCommands(path.join(__dirname, '..', 'lib', 'commands'));
|
|
4017
|
+
if (command && command !== 'setup' && helpRegistry.commands.has(command)) {
|
|
4018
|
+
printCommandHelp(helpRegistry.commands.get(command));
|
|
4019
|
+
} else {
|
|
4020
|
+
showHelp();
|
|
4021
|
+
}
|
|
4145
4022
|
return;
|
|
4146
4023
|
}
|
|
4147
4024
|
|
|
@@ -4153,8 +4030,11 @@ async function main() {
|
|
|
4153
4030
|
|
|
4154
4031
|
// Handle --path option: change to target directory
|
|
4155
4032
|
if (flags.path) {
|
|
4033
|
+
const createTargetPath = !(command === 'docs' && ['verify', 'detect'].includes(args[1]));
|
|
4156
4034
|
// Update projectRoot after changing directory to maintain state consistency
|
|
4157
|
-
projectRoot =
|
|
4035
|
+
projectRoot = suppressAdoptionDryRunOutput
|
|
4036
|
+
? resolvePathForDryRun(flags.path)
|
|
4037
|
+
: handlePathSetup(flags.path, { quiet: suppressStructuredOutput, create: createTargetPath });
|
|
4158
4038
|
}
|
|
4159
4039
|
|
|
4160
4040
|
// Load command registry (auto-discovered commands from lib/commands/)
|
|
@@ -4177,27 +4057,100 @@ async function main() {
|
|
|
4177
4057
|
}
|
|
4178
4058
|
}
|
|
4179
4059
|
|
|
4060
|
+
// Back-compat alias handling (lib/commands/_aliases.js, the single declarative
|
|
4061
|
+
// source of truth generalised from the former ISSUE_ALIAS_COMMANDS array). Emit
|
|
4062
|
+
// an opt-in deprecation hint (stderr only, gated on FORGE_DEPRECATION_WARNINGS +
|
|
4063
|
+
// the alias being marked deprecated — never on stdout, so `--json` stays clean),
|
|
4064
|
+
// then resolve any bare alias whose name is NOT itself a registered command to
|
|
4065
|
+
// its canonical `<noun> <sub>` handler. For P0 every alias is also a registered
|
|
4066
|
+
// command file and none are deprecated, so nothing warns and nothing is
|
|
4067
|
+
// rewritten — dispatch stays byte-identical. Registered commands skip the
|
|
4068
|
+
// rewrite via the first clause; this activates only when a later phase folds a
|
|
4069
|
+
// bare verb into a noun handler.
|
|
4070
|
+
maybeWarnDeprecation(command);
|
|
4071
|
+
let dispatchArgv = args;
|
|
4072
|
+
if (!registry.commands.has(command) && isAlias(command)) {
|
|
4073
|
+
const resolved = resolveDispatch(command, args, (name) => registry.commands.has(name));
|
|
4074
|
+
command = resolved.command;
|
|
4075
|
+
dispatchArgv = resolved.args;
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
// A workflow stage reached through its canonical noun form (e.g. `pr ship`,
|
|
4079
|
+
// whose bare alias `ship` IS a stage) must dispatch through the SAME top-level
|
|
4080
|
+
// stage path as the bare verb — stage-entry enforcement, kernel stage-run
|
|
4081
|
+
// recording, and ensureForgeHome all key on the stage token, which sits at the
|
|
4082
|
+
// subcommand position under a noun. Rewrite `<noun> <stage> [args]` to the
|
|
4083
|
+
// top-level `<stage> [args]` so behavior is byte-identical to the bare stage
|
|
4084
|
+
// alias. Detected precisely via the declarative alias map: the subcommand is a
|
|
4085
|
+
// stage AND its bare alias's canonical is exactly `<command> <sub>` (so only a
|
|
4086
|
+
// genuine noun→stage form matches; non-stage pr/gate subcommands route via
|
|
4087
|
+
// their noun handler as usual). Only `pr ship` matches today.
|
|
4088
|
+
//
|
|
4089
|
+
// The stage token is located as the first POSITIONAL after the noun (scanning
|
|
4090
|
+
// past any global flags and their values), so `forge pr --path /tmp ship ...`
|
|
4091
|
+
// shares the same stage path as `forge pr ship ...` instead of falling through
|
|
4092
|
+
// to pr.handler and missing stage-entry enforcement. Intervening global flags
|
|
4093
|
+
// are preserved into the rewritten argv so the stage handler still sees them.
|
|
4094
|
+
const stageIdx = firstPositionalIndex(dispatchArgv, 1);
|
|
4095
|
+
const nounSub = stageIdx >= 0 ? dispatchArgv[stageIdx] : undefined;
|
|
4096
|
+
if (nounSub && normalizeStageId(nounSub) && registry.commands.has(nounSub)) {
|
|
4097
|
+
const bareAlias = resolveAlias(nounSub);
|
|
4098
|
+
if (bareAlias && bareAlias.canonical === `${command} ${nounSub}`) {
|
|
4099
|
+
command = nounSub;
|
|
4100
|
+
// Drop only the consumed stage token (the noun at index 0 is dropped by the
|
|
4101
|
+
// registry dispatch's own `.slice(1)`); keep the intervening global flags.
|
|
4102
|
+
dispatchArgv = [nounSub, ...dispatchArgv.slice(1, stageIdx), ...dispatchArgv.slice(stageIdx + 1)];
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4180
4106
|
// Registry command dispatch — auto-discovered commands take priority
|
|
4181
4107
|
if (registry.commands.has(command)) {
|
|
4182
4108
|
try {
|
|
4109
|
+
// Resolve the issue backend (flag > env > config > default) and, for issue
|
|
4110
|
+
// commands, strip the selector tokens (--kernel / --issue-backend) from the
|
|
4111
|
+
// args so they never reach the handler or bd. For the kernel backend this
|
|
4112
|
+
// also assembles the driver + migrated broker (B1/B2).
|
|
4113
|
+
const { commandOpts, args: dispatchArgs } = await resolveCommandOpts(
|
|
4114
|
+
command,
|
|
4115
|
+
dispatchArgv.slice(1),
|
|
4116
|
+
{ env: process.env, projectRoot },
|
|
4117
|
+
);
|
|
4183
4118
|
const result = await executeCommand(
|
|
4184
4119
|
registry.commands,
|
|
4185
4120
|
command,
|
|
4186
|
-
|
|
4121
|
+
dispatchArgs,
|
|
4187
4122
|
flags,
|
|
4188
4123
|
projectRoot,
|
|
4189
4124
|
{
|
|
4125
|
+
// Pass interactivity so issue writes render a human confirmation at a real
|
|
4126
|
+
// terminal but stay machine-parseable JSON when piped/scripted (842a8be7).
|
|
4127
|
+
commandOpts: { ...(commandOpts || {}), isInteractive: Boolean(process.stdout.isTTY) },
|
|
4190
4128
|
enforceStage: (context) => enforceStageEntry({
|
|
4191
4129
|
commandName: context.commandName,
|
|
4192
4130
|
args: context.args,
|
|
4193
4131
|
flags: context.flags,
|
|
4194
4132
|
projectRoot: context.projectRoot,
|
|
4133
|
+
// Kernel is the source of truth for workflow stage state: resolve the
|
|
4134
|
+
// active worktree's issue and persist/read currentStage in the kernel
|
|
4135
|
+
// so `ship` is reachable from a pure-CLI plan->dev->validate flow.
|
|
4136
|
+
autoResolveKernel: true,
|
|
4195
4137
|
}),
|
|
4196
4138
|
}
|
|
4197
4139
|
);
|
|
4198
4140
|
if (result && !result.success) {
|
|
4141
|
+
if (
|
|
4142
|
+
typeof result.output === 'string' &&
|
|
4143
|
+
result.output.length > 0 &&
|
|
4144
|
+
result.output !== result.error
|
|
4145
|
+
) {
|
|
4146
|
+
process.stdout.write(result.output.endsWith('\n') ? result.output : `${result.output}\n`);
|
|
4147
|
+
}
|
|
4199
4148
|
console.error(result.error || result.message || 'Command failed');
|
|
4200
|
-
|
|
4149
|
+
// Issue commands surface the issue-command-contract exit_code as
|
|
4150
|
+
// `result.exitCode` (e.g. not-found=3, validation=6). Honor it so failed
|
|
4151
|
+
// commands no longer collapse to exit 1; other registry commands omit
|
|
4152
|
+
// `exitCode` and keep the historical exit 1.
|
|
4153
|
+
process.exit(Number.isInteger(result.exitCode) ? result.exitCode : 1);
|
|
4201
4154
|
}
|
|
4202
4155
|
if (result && typeof result.output === 'string' && result.output.length > 0) {
|
|
4203
4156
|
process.stdout.write(result.output.endsWith('\n') ? result.output : `${result.output}\n`);
|
|
@@ -4217,6 +4170,7 @@ async function main() {
|
|
|
4217
4170
|
args: args.slice(1),
|
|
4218
4171
|
flags,
|
|
4219
4172
|
projectRoot,
|
|
4173
|
+
autoResolveKernel: true,
|
|
4220
4174
|
});
|
|
4221
4175
|
} catch (err) {
|
|
4222
4176
|
console.error(`Error running '${command}':`, err.message);
|
|
@@ -4257,6 +4211,12 @@ async function main() {
|
|
|
4257
4211
|
return;
|
|
4258
4212
|
}
|
|
4259
4213
|
|
|
4214
|
+
if (flags.sync && selectedAgents.length === 0) {
|
|
4215
|
+
migrateExistingBeadsLocalState();
|
|
4216
|
+
await handleSyncScaffold();
|
|
4217
|
+
return;
|
|
4218
|
+
}
|
|
4219
|
+
|
|
4260
4220
|
// Agents specified via flag or --yes default (non-quick mode)
|
|
4261
4221
|
if (selectedAgents.length > 0) {
|
|
4262
4222
|
await handleSetupCommand(selectedAgents, flags);
|
|
@@ -4264,7 +4224,7 @@ async function main() {
|
|
|
4264
4224
|
}
|
|
4265
4225
|
|
|
4266
4226
|
// Interactive setup (skip-external still applies)
|
|
4267
|
-
await
|
|
4227
|
+
await runInteractiveSetupFallback(flags);
|
|
4268
4228
|
} else if (command === 'recommend') {
|
|
4269
4229
|
const { handleRecommend, formatRecommendations } = require('../lib/commands/recommend');
|
|
4270
4230
|
const result = handleRecommend(flags, projectRoot);
|
|
@@ -4285,9 +4245,68 @@ async function main() {
|
|
|
4285
4245
|
}
|
|
4286
4246
|
console.log('');
|
|
4287
4247
|
console.log(' Usage: forge docs <topic>');
|
|
4248
|
+
console.log(' forge docs verify [--path <dir>] [--json] [--baseline <file>] [--write-baseline <file>] [--min-docstring-coverage <percent>]');
|
|
4249
|
+
console.log(' forge docs detect [--path <dir>] [--json]');
|
|
4288
4250
|
console.log('');
|
|
4251
|
+
console.log(' Note: forge docs verify|detect --path requires an existing directory.');
|
|
4252
|
+
console.log('');
|
|
4253
|
+
} else if (topic === 'verify' || topic === 'detect') {
|
|
4254
|
+
const minCoverageIndex = args.indexOf('--min-docstring-coverage');
|
|
4255
|
+
let minDocstringCoverage = 0;
|
|
4256
|
+
if (minCoverageIndex >= 0) {
|
|
4257
|
+
const rawMinCoverage = args[minCoverageIndex + 1];
|
|
4258
|
+
const parsedMinCoverage = Number(rawMinCoverage);
|
|
4259
|
+
if (!rawMinCoverage || !Number.isFinite(parsedMinCoverage) || parsedMinCoverage < 0 || parsedMinCoverage > 100) {
|
|
4260
|
+
console.error('Error: --min-docstring-coverage must be a number between 0 and 100');
|
|
4261
|
+
process.exitCode = 1;
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
minDocstringCoverage = parsedMinCoverage;
|
|
4265
|
+
}
|
|
4266
|
+
const baselineIndex = args.indexOf('--baseline');
|
|
4267
|
+
const writeBaselineIndex = args.indexOf('--write-baseline');
|
|
4268
|
+
const baselinePath = baselineIndex >= 0 ? args[baselineIndex + 1] : null;
|
|
4269
|
+
const writeBaselinePath = writeBaselineIndex >= 0 ? args[writeBaselineIndex + 1] : null;
|
|
4270
|
+
if (topic === 'detect' && writeBaselineIndex >= 0) {
|
|
4271
|
+
console.error('Error: --write-baseline is only supported with `forge docs verify`');
|
|
4272
|
+
process.exitCode = 1;
|
|
4273
|
+
return;
|
|
4274
|
+
}
|
|
4275
|
+
if (baselineIndex >= 0 && (!baselinePath || baselinePath.startsWith('-'))) {
|
|
4276
|
+
console.error('Error: --baseline requires a file path');
|
|
4277
|
+
process.exitCode = 1;
|
|
4278
|
+
return;
|
|
4279
|
+
}
|
|
4280
|
+
if (writeBaselineIndex >= 0 && (!writeBaselinePath || writeBaselinePath.startsWith('-'))) {
|
|
4281
|
+
console.error('Error: --write-baseline requires a file path');
|
|
4282
|
+
process.exitCode = 1;
|
|
4283
|
+
return;
|
|
4284
|
+
}
|
|
4285
|
+
let result;
|
|
4286
|
+
try {
|
|
4287
|
+
result = validateDocs(projectRoot, { baselinePath, minDocstringCoverage });
|
|
4288
|
+
if (writeBaselinePath) {
|
|
4289
|
+
writeDocsBaseline(projectRoot, writeBaselinePath, result);
|
|
4290
|
+
}
|
|
4291
|
+
} catch (error) {
|
|
4292
|
+
console.error(`Error: ${error.message}`);
|
|
4293
|
+
process.exitCode = 1;
|
|
4294
|
+
return;
|
|
4295
|
+
}
|
|
4296
|
+
if (args.includes('--json') || topic === 'detect') {
|
|
4297
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4298
|
+
} else {
|
|
4299
|
+
console.log(formatDocsValidation(result));
|
|
4300
|
+
}
|
|
4301
|
+
const hasUnbaselinedFailure = result.failures.some((failure) => failure.type !== 'broken-link');
|
|
4302
|
+
if ((!writeBaselinePath && !result.ok) || (writeBaselinePath && hasUnbaselinedFailure)) {
|
|
4303
|
+
process.exitCode = 1;
|
|
4304
|
+
}
|
|
4289
4305
|
} else {
|
|
4290
|
-
|
|
4306
|
+
// `forge docs` reads packaged docs; route through the asset root so any
|
|
4307
|
+
// embedded topic resolves in a compiled binary too. (Full docs/ embedding
|
|
4308
|
+
// for arbitrary topics is deferred to step 3 — see the step-2 plan note.)
|
|
4309
|
+
const result = getTopicContent(topic, getPackageRoot(packageDir));
|
|
4291
4310
|
if (result.error) {
|
|
4292
4311
|
console.error(` Error: ${result.error}`);
|
|
4293
4312
|
process.exitCode = 1;
|
|
@@ -4389,9 +4408,34 @@ async function main() {
|
|
|
4389
4408
|
console.log(' To set up in your project:');
|
|
4390
4409
|
console.log(` ${runCmd} forge setup`);
|
|
4391
4410
|
console.log('');
|
|
4392
|
-
} else {
|
|
4393
|
-
//
|
|
4411
|
+
} else if (command === undefined && fs.existsSync(path.join(projectRoot, 'AGENTS.md'))) {
|
|
4412
|
+
// Bare `forge` in an INITIALIZED project orients instead of mutating: render the
|
|
4413
|
+
// same read-only one-glance view as `forge status`. Newcomers typing `forge` to
|
|
4414
|
+
// get their bearings should never trigger the minimal install.
|
|
4415
|
+
try {
|
|
4416
|
+
const { commandOpts, args: dispatchArgs } = await resolveCommandOpts(
|
|
4417
|
+
'status',
|
|
4418
|
+
[],
|
|
4419
|
+
{ env: process.env, projectRoot },
|
|
4420
|
+
);
|
|
4421
|
+
const result = await executeCommand(registry.commands, 'status', dispatchArgs, flags, projectRoot, { commandOpts });
|
|
4422
|
+
if (result && typeof result.output === 'string' && result.output.length > 0) {
|
|
4423
|
+
process.stdout.write(result.output.endsWith('\n') ? result.output : `${result.output}\n`);
|
|
4424
|
+
}
|
|
4425
|
+
} catch (err) {
|
|
4426
|
+
console.error(`Error running 'status':`, err.message);
|
|
4427
|
+
process.exit(1);
|
|
4428
|
+
}
|
|
4429
|
+
} else if (command === undefined) {
|
|
4430
|
+
// Explicit invocation with no command in an UNINITIALIZED project: run minimal install.
|
|
4394
4431
|
minimalInstall();
|
|
4432
|
+
} else {
|
|
4433
|
+
// Defined but unrecognized command → fail honestly instead of printing the
|
|
4434
|
+
// installer banner with exit 0 (kernel b3bae4f3). A typo like `forge bogusxyz`
|
|
4435
|
+
// must be detectable by scripts (non-zero exit) and humans (a real error).
|
|
4436
|
+
console.error(`Error: Unknown command '${command}'`);
|
|
4437
|
+
console.error("Run 'forge --help' to see the available commands.");
|
|
4438
|
+
process.exit(1);
|
|
4395
4439
|
}
|
|
4396
4440
|
}
|
|
4397
4441
|
|
|
@@ -4815,7 +4859,6 @@ if (require.main === module) {
|
|
|
4815
4859
|
|
|
4816
4860
|
module.exports = {
|
|
4817
4861
|
ensureDirWithNote,
|
|
4818
|
-
getWorkflowCommands,
|
|
4819
4862
|
validateDirectoryPathInput,
|
|
4820
4863
|
validateUserInput,
|
|
4821
4864
|
};
|