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/lib/commands/setup.js
CHANGED
|
@@ -13,30 +13,51 @@ const fs = require('node:fs');
|
|
|
13
13
|
const os = require('node:os');
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const readline = require('node:readline');
|
|
16
|
-
const { execSync
|
|
16
|
+
const { execSync } = require('node:child_process');
|
|
17
17
|
|
|
18
|
-
// Compute packageDir relative to this file (lib/commands/setup.js -> project root)
|
|
18
|
+
// Compute packageDir relative to this file (lib/commands/setup.js -> project root).
|
|
19
|
+
// packageDir stays a runtime value for asset copying; requires use static
|
|
20
|
+
// relative paths so `bun build --compile` can bundle the module graph.
|
|
19
21
|
const packageDir = path.resolve(__dirname, '..', '..');
|
|
20
|
-
const packageJson = require(
|
|
22
|
+
const packageJson = require('../../package.json');
|
|
21
23
|
const VERSION = packageJson.version;
|
|
22
24
|
|
|
25
|
+
// Dual-channel asset root. Under npm/npx this returns `packageDir` unchanged;
|
|
26
|
+
// inside a `bun build --compile` binary it lazily extracts the embedded runtime
|
|
27
|
+
// assets (skills/rules/docs/hooks/scripts) to a temp dir and returns that. Every
|
|
28
|
+
// packaged-ASSET read below goes through `getPackageRoot(packageDir)`; the
|
|
29
|
+
// module-`require` paths above keep `packageDir` (bundler handles those).
|
|
30
|
+
const { getPackageRoot } = require('../package-root');
|
|
31
|
+
|
|
23
32
|
// Load PluginManager for discoverable agent architecture
|
|
24
33
|
const PluginManager = require('../plugin-manager');
|
|
25
|
-
const {
|
|
34
|
+
const { populateAgentSkills, listCanonicalSkills, listFilesRecursive } = require('../skills-sync');
|
|
35
|
+
const { renderMcpConfig } = require('../mcp-config-renderer');
|
|
36
|
+
const { renderClaudePermissions, renderCursorIgnore } = require('../safety-config-renderer');
|
|
37
|
+
const { renderHookConfig } = require('../hook-renderer');
|
|
26
38
|
const { copyEssentialDocs } = require('../docs-copy');
|
|
39
|
+
|
|
40
|
+
// Baseline MCP server Forge ships. Uses the generic descriptor contract consumed
|
|
41
|
+
// by lib/mcp-config-renderer.js (envRefs are '${VAR}' references, never secrets).
|
|
42
|
+
const CONTEXT7_MCP_DESCRIPTOR = {
|
|
43
|
+
name: 'context7',
|
|
44
|
+
transport: 'stdio',
|
|
45
|
+
command: 'npx',
|
|
46
|
+
args: ['-y', '@upstash/context7-mcp@latest'],
|
|
47
|
+
envRefs: {},
|
|
48
|
+
};
|
|
27
49
|
const { secureExecFileSync } = require('../shell-utils');
|
|
28
50
|
const { askYesNo: _askYesNoBase } = require('../ui-utils');
|
|
29
51
|
|
|
30
|
-
// Load enhanced onboarding modules
|
|
31
|
-
const contextMerge = require(
|
|
32
|
-
const projectDiscovery = require(
|
|
52
|
+
// Load enhanced onboarding modules (static relative requires — bundleable)
|
|
53
|
+
const contextMerge = require('../context-merge');
|
|
54
|
+
const projectDiscovery = require('../project-discovery');
|
|
33
55
|
|
|
34
56
|
// Load lib modules for symlink, beads, and PAT setup
|
|
35
|
-
const { createSymlinkOrCopy: libCreateSymlinkOrCopy } = require(
|
|
36
|
-
const
|
|
37
|
-
const {
|
|
38
|
-
const {
|
|
39
|
-
const { DEFAULT_BEADS_VERSION, detectDefaultBranch, templateWorkflows, scaffoldBeadsSync } = require(path.join(packageDir, 'lib', 'beads-sync-scaffold'));
|
|
57
|
+
const { createSymlinkOrCopy: libCreateSymlinkOrCopy } = require('../symlink-utils');
|
|
58
|
+
const { scaffoldBeadsSync } = require('../beads-sync-scaffold');
|
|
59
|
+
const { resolveSyncBackend } = require('../sync-backend');
|
|
60
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
40
61
|
|
|
41
62
|
// Load incremental setup modules
|
|
42
63
|
const { detectEnvironment } = require('../detect-agent');
|
|
@@ -46,18 +67,25 @@ const { ActionCollector } = require('../setup-utils');
|
|
|
46
67
|
const { renderSetupSummary } = require('../setup-summary-renderer');
|
|
47
68
|
const { smartMergeAgentsMd } = require('../smart-merge');
|
|
48
69
|
const { checkLefthookStatus } = require('../lefthook-check');
|
|
70
|
+
const {
|
|
71
|
+
FORGE_USER_LEFTHOOK_YML,
|
|
72
|
+
forgeShouldWriteLefthookConfig,
|
|
73
|
+
installNativeGitHooks,
|
|
74
|
+
verifyHooksActive,
|
|
75
|
+
resolveGitHooksDir,
|
|
76
|
+
} = require('../lefthook-wiring');
|
|
49
77
|
const { resolveShellRuntime } = require('../runtime-health');
|
|
50
78
|
const {
|
|
51
79
|
buildCodexSkillInstallPlan,
|
|
52
80
|
formatCodexSkillsInstallDir,
|
|
53
81
|
listCodexSkillEntries,
|
|
82
|
+
populateCodexRepoSkills,
|
|
83
|
+
CODEX_REPO_SKILLS_DIR,
|
|
54
84
|
} = require('../codex-skills');
|
|
55
85
|
const {
|
|
56
|
-
generateCopilotConfig,
|
|
57
86
|
generateCursorConfig,
|
|
58
|
-
generateKiloConfig,
|
|
59
|
-
generateOpenCodeConfig,
|
|
60
87
|
} = require('../agents-config');
|
|
88
|
+
const initCommand = require('./init');
|
|
61
89
|
const fileUtils = require('../file-utils');
|
|
62
90
|
const detectionUtils = require('../detection-utils');
|
|
63
91
|
const { detectHusky, migrateHusky } = require('../husky-migration');
|
|
@@ -76,6 +104,11 @@ let actionLog = new SetupActionLog();
|
|
|
76
104
|
let PKG_MANAGER = 'npm';
|
|
77
105
|
let SETUP_NOTES = [];
|
|
78
106
|
let CODEX_SETUP_REPORT = null;
|
|
107
|
+
// Tracks whether the last ensureKernelIssueStore() run provisioned the store.
|
|
108
|
+
// The setup summary reads this so a provisioning failure isn't masked by a
|
|
109
|
+
// hardcoded "✓ Kernel issue store" line. Defaults true: when the ensure step
|
|
110
|
+
// never ran, the kernel still auto-provisions on first use.
|
|
111
|
+
let KERNEL_STORE_READY = true;
|
|
79
112
|
|
|
80
113
|
/**
|
|
81
114
|
* Load agent definitions from plugin architecture
|
|
@@ -96,7 +129,8 @@ function loadAgentsFromPlugins() {
|
|
|
96
129
|
supportStatus: plugin.support?.status || 'supported',
|
|
97
130
|
needsConversion: plugin.setup?.needsConversion || false,
|
|
98
131
|
copyCommands: plugin.setup?.copyCommands || false,
|
|
99
|
-
promptFormat: plugin.setup?.promptFormat || false
|
|
132
|
+
promptFormat: plugin.setup?.promptFormat || false,
|
|
133
|
+
skillsDir: plugin.directories?.skills || null
|
|
100
134
|
};
|
|
101
135
|
});
|
|
102
136
|
return agents;
|
|
@@ -138,29 +172,6 @@ function detectPackageManager() {
|
|
|
138
172
|
return 'npm';
|
|
139
173
|
}
|
|
140
174
|
|
|
141
|
-
/**
|
|
142
|
-
* Reads workflow command names from commands/*.md in the package directory.
|
|
143
|
-
* Falls back to .claude/commands/ if commands/ does not exist (backwards compat).
|
|
144
|
-
* @returns {string[]} Command names (filenames without .md extension)
|
|
145
|
-
*/
|
|
146
|
-
function getWorkflowCommands() {
|
|
147
|
-
const canonicalDir = path.join(packageDir, 'commands');
|
|
148
|
-
const commandsDir = fs.existsSync(canonicalDir)
|
|
149
|
-
? canonicalDir
|
|
150
|
-
: path.join(packageDir, '.claude', 'commands');
|
|
151
|
-
try {
|
|
152
|
-
return fs.readdirSync(commandsDir)
|
|
153
|
-
.filter(f => f.endsWith('.md'))
|
|
154
|
-
.map(f => f.replace(/\.md$/, ''));
|
|
155
|
-
} catch (err) {
|
|
156
|
-
if (err.code === 'ENOENT') {
|
|
157
|
-
console.warn(`Warning: commands directory not found at ${commandsDir}`);
|
|
158
|
-
} else {
|
|
159
|
-
console.warn(`Warning: failed to read commands — ${err.code}: ${err.message}`);
|
|
160
|
-
}
|
|
161
|
-
return [];
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
175
|
|
|
165
176
|
const WORKFLOW_RUNTIME_ASSETS = Object.freeze([
|
|
166
177
|
'scripts/beads-context.sh',
|
|
@@ -184,7 +195,7 @@ const WORKFLOW_RUNTIME_ASSETS = Object.freeze([
|
|
|
184
195
|
'scripts/forge-team/lib/sync-github.sh',
|
|
185
196
|
'scripts/forge-team/lib/verify.sh',
|
|
186
197
|
'scripts/forge-team/lib/workload.sh',
|
|
187
|
-
'.claude/scripts/
|
|
198
|
+
'.claude/scripts/review-resolve.sh'
|
|
188
199
|
]);
|
|
189
200
|
|
|
190
201
|
/**
|
|
@@ -205,112 +216,11 @@ function validateAgents(agentList) {
|
|
|
205
216
|
return valid;
|
|
206
217
|
}
|
|
207
218
|
|
|
208
|
-
function parseDoltRemoteNames(remoteListOutput) {
|
|
209
|
-
const output = String(remoteListOutput || '').trim();
|
|
210
|
-
if (!output || /^No remotes configured\.?$/i.test(output)) {
|
|
211
|
-
return [];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return output
|
|
215
|
-
.split(/\r?\n/)
|
|
216
|
-
.map(line => line.trim())
|
|
217
|
-
.filter(Boolean)
|
|
218
|
-
.map(line => line.split(/\s+/)[0])
|
|
219
|
-
.filter(Boolean);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function readBeadsSyncRemoteConfig(projectDir = projectRoot) {
|
|
223
|
-
const jsonConfigPath = path.join(projectDir, '.beads', 'config.json');
|
|
224
|
-
if (fs.existsSync(jsonConfigPath)) {
|
|
225
|
-
try {
|
|
226
|
-
const parsed = JSON.parse(fs.readFileSync(jsonConfigPath, 'utf8'));
|
|
227
|
-
if (typeof parsed.sync_remote === 'string' && parsed.sync_remote.trim()) {
|
|
228
|
-
return parsed.sync_remote.trim();
|
|
229
|
-
}
|
|
230
|
-
} catch (_error) {
|
|
231
|
-
// Ignore malformed config and fall back to other sources.
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const yamlConfigPath = path.join(projectDir, '.beads', 'config.yaml');
|
|
236
|
-
if (fs.existsSync(yamlConfigPath)) {
|
|
237
|
-
const configuredRemote = parseBeadsYamlSyncRemote(fs.readFileSync(yamlConfigPath, 'utf8'));
|
|
238
|
-
if (configuredRemote) {
|
|
239
|
-
return configuredRemote;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return '';
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function parseBeadsYamlSyncRemote(content) {
|
|
247
|
-
for (const rawLine of String(content || '').split(/\r?\n/)) {
|
|
248
|
-
const trimmedLine = rawLine.trimStart();
|
|
249
|
-
if (!trimmedLine.startsWith('sync-remote:')) {
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const rawValue = trimmedLine.slice('sync-remote:'.length).trim();
|
|
254
|
-
if (!rawValue) {
|
|
255
|
-
return '';
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const quote = rawValue[0];
|
|
259
|
-
if ((quote === '"' || quote === '\'') && rawValue.length > 1) {
|
|
260
|
-
const closingQuoteIndex = rawValue.indexOf(quote, 1);
|
|
261
|
-
if (closingQuoteIndex > 1) {
|
|
262
|
-
return rawValue.slice(1, closingQuoteIndex).trim();
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const commentIndex = rawValue.indexOf('#');
|
|
267
|
-
const unquotedValue = commentIndex === -1 ? rawValue : rawValue.slice(0, commentIndex);
|
|
268
|
-
return unquotedValue.trim();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return '';
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function resolveExpectedBeadsRemote(options = {}) {
|
|
275
|
-
const projectDir = options.projectDir || projectRoot;
|
|
276
|
-
const env = options.env || process.env;
|
|
277
|
-
const configuredRemote = readBeadsSyncRemoteConfig(projectDir);
|
|
278
|
-
if (configuredRemote) {
|
|
279
|
-
return configuredRemote;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (typeof env.BD_SYNC_REMOTE === 'string' && env.BD_SYNC_REMOTE.trim()) {
|
|
283
|
-
return env.BD_SYNC_REMOTE.trim();
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const gitRemoteProbe = options.gitRemoteProbe
|
|
287
|
-
|| ((remoteName) => safeExec(`git -C "${projectDir}" remote get-url ${remoteName}`));
|
|
288
|
-
if (gitRemoteProbe('upstream')) {
|
|
289
|
-
return 'upstream';
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return 'origin';
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function hasBeadsDoltRemote(commandRunner, remoteName = 'origin') {
|
|
296
|
-
const output = commandRunner('bd dolt remote list');
|
|
297
|
-
if (!output) {
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return parseDoltRemoteNames(output).includes(remoteName);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
219
|
// Prerequisite check function
|
|
305
220
|
function checkPrerequisites(options = {}) {
|
|
306
221
|
const requireGithubCli = options.requireGithubCli !== false;
|
|
307
222
|
const requireBeadsCli = options.requireBeadsCli === true;
|
|
308
223
|
const requireJq = options.requireJq === true;
|
|
309
|
-
const expectedBeadsRemote = options.expectedBeadsRemote || resolveExpectedBeadsRemote({
|
|
310
|
-
env: options.env,
|
|
311
|
-
gitRemoteProbe: options.gitRemoteProbe,
|
|
312
|
-
projectDir: options.projectDir,
|
|
313
|
-
});
|
|
314
224
|
const commandRunner = options.commandRunner || safeExec;
|
|
315
225
|
const errors = [];
|
|
316
226
|
const warnings = [];
|
|
@@ -346,25 +256,15 @@ function checkPrerequisites(options = {}) {
|
|
|
346
256
|
}
|
|
347
257
|
}
|
|
348
258
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
`Sync will remain local until you run: bd dolt remote add ${expectedBeadsRemote} <url>`
|
|
358
|
-
);
|
|
359
|
-
} else if (hasRemote === null) {
|
|
360
|
-
warnings.push(
|
|
361
|
-
`Unable to inspect Beads Dolt remotes. ` +
|
|
362
|
-
`Sync may remain local until '${expectedBeadsRemote}' is configured and bd dolt remote list succeeds.`
|
|
363
|
-
);
|
|
364
|
-
}
|
|
259
|
+
if (requireBeadsCli) {
|
|
260
|
+
// Issue tracking runs on the local Forge Kernel store, which auto-provisions
|
|
261
|
+
// on first use — there is no external CLI to install. Surface only whether
|
|
262
|
+
// team sync is wired up yet.
|
|
263
|
+
if (resolveSyncBackend({ projectRoot, env: options.env }) === 'local-noop') {
|
|
264
|
+
console.log(' ✓ Kernel issue store (local-noop sync: single-machine until a sync server is configured)');
|
|
265
|
+
} else {
|
|
266
|
+
console.log(' ✓ Kernel issue store');
|
|
365
267
|
}
|
|
366
|
-
} else if (requireBeadsCli) {
|
|
367
|
-
errors.push('bd (Beads CLI) - Install from https://github.com/steveyegge/beads');
|
|
368
268
|
}
|
|
369
269
|
|
|
370
270
|
// Check Node.js version
|
|
@@ -379,7 +279,11 @@ function checkPrerequisites(options = {}) {
|
|
|
379
279
|
if (jqVersion) {
|
|
380
280
|
console.log(` ✓ ${jqVersion.split('\n')[0]}`);
|
|
381
281
|
} else if (requireJq) {
|
|
382
|
-
|
|
282
|
+
// jq is optional at setup time. Some workflow helper scripts shell out to jq,
|
|
283
|
+
// but they degrade or defer when it is absent — a clean box (esp. Windows)
|
|
284
|
+
// must not abort setup just because jq is not installed yet. Surface a
|
|
285
|
+
// warning instead of a fatal error (kernel issue 01468e44).
|
|
286
|
+
warnings.push('jq not found - some workflow helper scripts will be skipped until you install it (https://jqlang.org/download/)');
|
|
383
287
|
}
|
|
384
288
|
|
|
385
289
|
// Detect package manager
|
|
@@ -405,60 +309,121 @@ function checkPrerequisites(options = {}) {
|
|
|
405
309
|
console.log('');
|
|
406
310
|
console.log(` Package manager: ${PKG_MANAGER}`);
|
|
407
311
|
|
|
408
|
-
|
|
312
|
+
// Return a structured result so embedding agents can inspect prerequisite
|
|
313
|
+
// state instead of only observing a process exit. Genuinely-fatal prereqs
|
|
314
|
+
// (git, node version, required gh) still hard-exit above; soft prereqs like
|
|
315
|
+
// jq surface via `warnings` while `ok` stays true.
|
|
316
|
+
return { errors, warnings, ok: errors.length === 0 };
|
|
409
317
|
}
|
|
410
318
|
|
|
411
|
-
function requiresGithubCliForSetup(selectedAgents
|
|
412
|
-
return needsWorkflowRuntimeAssets(selectedAgents)
|
|
319
|
+
function requiresGithubCliForSetup(selectedAgents) {
|
|
320
|
+
return needsWorkflowRuntimeAssets(selectedAgents);
|
|
413
321
|
}
|
|
414
322
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
| Stage | Command | Description |
|
|
439
|
-
|-------|---------|-------------|
|
|
440
|
-
| utility | \`/status\` | Check current context, active work, recent completions |
|
|
441
|
-
| 1 | \`/plan\` | Design intent -> research -> branch + worktree + task list |
|
|
442
|
-
| 2 | \`/dev\` | TDD development (implementer -> spec review -> quality review) |
|
|
443
|
-
| 3 | \`/validate\` | Type check, lint, security, tests - all fresh output |
|
|
444
|
-
| 4 | \`/ship\` | Push branch and create PR with full documentation |
|
|
445
|
-
| 5 | \`/review\` | Address ALL PR feedback (GitHub Actions, Greptile, SonarCloud) |
|
|
446
|
-
| 6 | \`/premerge\` | Update docs, hand off PR to user |
|
|
447
|
-
| 7 | \`/verify\` | Post-merge health check (CI on main, close Beads) |
|
|
323
|
+
/**
|
|
324
|
+
* After agent setup, `.forge/config.yaml` may not exist yet — only the
|
|
325
|
+
* --minimal/--standard/--full routes run `forge init`. Point the user at
|
|
326
|
+
* `forge init` so first-run always ends with a clear next step that creates
|
|
327
|
+
* the workflow config (gates + change classification). No-op when the config
|
|
328
|
+
* already exists (kernel issue 5bdc91d3).
|
|
329
|
+
*/
|
|
330
|
+
function printForgeInitNextStep() {
|
|
331
|
+
const configPath = path.join(projectRoot, '.forge', 'config.yaml');
|
|
332
|
+
if (fs.existsSync(configPath)) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
console.log('');
|
|
336
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
337
|
+
console.log('▶ NEXT: run `forge init` to configure your workflow');
|
|
338
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
339
|
+
console.log('');
|
|
340
|
+
console.log(' forge init # creates .forge/config.yaml');
|
|
341
|
+
console.log('');
|
|
342
|
+
console.log(' Sets up workflow gates and change classification.');
|
|
343
|
+
console.log(' Safe to re-run (existing config is preserved).');
|
|
344
|
+
console.log('');
|
|
345
|
+
}
|
|
448
346
|
|
|
449
|
-
|
|
347
|
+
/**
|
|
348
|
+
* One-step onboarding (kernel issue ac0b38c7): a fresh `forge setup` used to end
|
|
349
|
+
* by telling the user to ALSO run `forge init`. Instead, run init's real handler
|
|
350
|
+
* here (standard profile, non-interactive) so setup finishes with a working
|
|
351
|
+
* `.forge/config.yaml` in one command. No-op when config already exists; on any
|
|
352
|
+
* init failure fall back to the printed `forge init` guidance so setup never
|
|
353
|
+
* hard-fails. `forge init` stays fully usable standalone. Inject `runInit` for
|
|
354
|
+
* tests.
|
|
355
|
+
*/
|
|
356
|
+
async function finalizeWorkflowConfig(options = {}) {
|
|
357
|
+
const configPath = path.join(projectRoot, '.forge', 'config.yaml');
|
|
358
|
+
if (fs.existsSync(configPath)) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const skipSideEffects = options.hooksAlreadyInstalled === true;
|
|
362
|
+
const runInit = options.runInit || defaultRunInit;
|
|
363
|
+
try {
|
|
364
|
+
const result = await runInit(projectRoot, skipSideEffects);
|
|
365
|
+
if (!result || result.success === false) {
|
|
366
|
+
printForgeInitNextStep();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
console.log('');
|
|
370
|
+
console.log('✓ Workflow configured: .forge/config.yaml (standard profile)');
|
|
371
|
+
console.log(' Change it anytime: forge init --profile <minimal|standard|full> --force');
|
|
372
|
+
console.log('');
|
|
373
|
+
} catch (err) {
|
|
374
|
+
console.warn(`Warning: automatic workflow init skipped: ${err.message}`);
|
|
375
|
+
printForgeInitNextStep();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
450
378
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
379
|
+
/**
|
|
380
|
+
* Default init runner for finalizeWorkflowConfig. When `skipSideEffects` is set
|
|
381
|
+
* (setup paths that already installed git hooks), pass no-op deps so init only
|
|
382
|
+
* writes `.forge/config.yaml` instead of re-doing that work — avoids duplicate
|
|
383
|
+
* side effects and warning noise (CodeRabbit on PR #368).
|
|
384
|
+
*/
|
|
385
|
+
function defaultRunInit(root, skipSideEffects) {
|
|
386
|
+
const deps = skipSideEffects
|
|
387
|
+
? { installHooks: () => {} }
|
|
388
|
+
: {};
|
|
389
|
+
return initCommand.handler(['--yes'], {}, root, deps);
|
|
390
|
+
}
|
|
454
391
|
|
|
455
|
-
|
|
392
|
+
/**
|
|
393
|
+
* Guard against silent data loss when a non-interactive setup path overwrites
|
|
394
|
+
* an existing AGENTS.md that predates Forge's USER/FORGE merge markers. Old
|
|
395
|
+
* hand-edited files have no markers, so a plain copy would clobber them with no
|
|
396
|
+
* way to recover. Before that happens, snapshot the file to AGENTS.md.bak and
|
|
397
|
+
* warn (kernel issue a5399f3d). Returns true when a backup was written.
|
|
398
|
+
*/
|
|
399
|
+
function backupMarkerlessAgentsMd() {
|
|
400
|
+
const agentsPath = path.join(projectRoot, 'AGENTS.md');
|
|
401
|
+
if (!fs.existsSync(agentsPath)) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
const existingContent = fs.readFileSync(agentsPath, 'utf8');
|
|
405
|
+
const hasUserMarkers = existingContent.includes('<!-- USER:START');
|
|
406
|
+
const hasForgeMarkers = existingContent.includes('<!-- FORGE:START');
|
|
407
|
+
if (hasUserMarkers || hasForgeMarkers) {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
// Never clobber an earlier snapshot: keep the original AGENTS.md.bak stable
|
|
411
|
+
// and fall back to numbered AGENTS.md.bak.1, .2, ... so a repeated markerless
|
|
412
|
+
// overwrite (e.g. re-running --quick after markers were stripped) preserves
|
|
413
|
+
// every prior backup instead of losing it (CodeRabbit review on PR #300).
|
|
414
|
+
let backupPath = path.join(projectRoot, 'AGENTS.md.bak');
|
|
415
|
+
if (fs.existsSync(backupPath)) {
|
|
416
|
+
let suffix = 1;
|
|
417
|
+
while (fs.existsSync(path.join(projectRoot, `AGENTS.md.bak.${suffix}`))) {
|
|
418
|
+
suffix += 1;
|
|
419
|
+
}
|
|
420
|
+
backupPath = path.join(projectRoot, `AGENTS.md.bak.${suffix}`);
|
|
421
|
+
}
|
|
422
|
+
fs.writeFileSync(backupPath, existingContent, 'utf8');
|
|
423
|
+
console.log(` ⚠ Existing AGENTS.md has no Forge markers - backed up to ${path.basename(backupPath)} before overwrite`);
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
456
426
|
|
|
457
|
-
- **TDD-First**: Write tests BEFORE implementation (RED-GREEN-REFACTOR)
|
|
458
|
-
- **Research-First**: Understand before building, document decisions
|
|
459
|
-
- **Security Built-In**: OWASP Top 10 analysis for every feature
|
|
460
|
-
- **Documentation Progressive**: Update at each stage, verify at end
|
|
461
|
-
`;
|
|
462
427
|
|
|
463
428
|
// Helper functions
|
|
464
429
|
|
|
@@ -472,10 +437,6 @@ function ensureDir(dir) {
|
|
|
472
437
|
|
|
473
438
|
|
|
474
439
|
|
|
475
|
-
function writeFile(filePath, content) {
|
|
476
|
-
return fileUtils.writeFile(filePath, content, projectRoot);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
440
|
function writeManagedAbsoluteFile(absolutePath, content, displayPath) {
|
|
480
441
|
try {
|
|
481
442
|
if (!FORCE_MODE && fileMatchesContent(absolutePath, content)) {
|
|
@@ -500,13 +461,10 @@ function writeManagedAbsoluteFile(absolutePath, content, displayPath) {
|
|
|
500
461
|
|
|
501
462
|
|
|
502
463
|
|
|
503
|
-
function readFile(filePath) {
|
|
504
|
-
return fileUtils.readFile(filePath);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
464
|
function resetSetupNotes() {
|
|
508
465
|
SETUP_NOTES = [];
|
|
509
466
|
CODEX_SETUP_REPORT = null;
|
|
467
|
+
KERNEL_STORE_READY = true;
|
|
510
468
|
}
|
|
511
469
|
|
|
512
470
|
function addSetupNote(message) {
|
|
@@ -579,7 +537,7 @@ function copyFile(src, dest) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
579
537
|
function needsWorkflowRuntimeAssets(selectedAgents) {
|
|
580
538
|
return selectedAgents.some((agentKey) => {
|
|
581
539
|
const agent = AGENTS[agentKey];
|
|
582
|
-
return Boolean(agent && (agent.hasCommands || agent.needsConversion || agent.copyCommands || agent.promptFormat));
|
|
540
|
+
return Boolean(agent && (agent.hasCommands || agent.hasSkill || agent.needsConversion || agent.copyCommands || agent.promptFormat));
|
|
583
541
|
});
|
|
584
542
|
}
|
|
585
543
|
|
|
@@ -618,7 +576,7 @@ function scaffoldWorkflowRuntimeAssets(selectedAgents) {
|
|
|
618
576
|
}
|
|
619
577
|
|
|
620
578
|
for (const relativePath of WORKFLOW_RUNTIME_ASSETS) {
|
|
621
|
-
const sourcePath = path.join(packageDir, relativePath);
|
|
579
|
+
const sourcePath = path.join(getPackageRoot(packageDir), relativePath);
|
|
622
580
|
copyFile(sourcePath, relativePath);
|
|
623
581
|
}
|
|
624
582
|
|
|
@@ -722,13 +680,40 @@ function resolveWorkflowShellPolicy(selectedAgents, options = {}) {
|
|
|
722
680
|
};
|
|
723
681
|
}
|
|
724
682
|
|
|
683
|
+
/**
|
|
684
|
+
* Print install guidance when Git Bash is absent on Windows. Kept separate so
|
|
685
|
+
* the graceful-degrade branch in ensureWorkflowShellPolicy stays trivial.
|
|
686
|
+
*/
|
|
687
|
+
function printGitBashGuidance(shellPolicy) {
|
|
688
|
+
console.log('');
|
|
689
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
690
|
+
console.log('⚠ Git Bash not found — continuing in reduced-capability mode');
|
|
691
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
692
|
+
console.log('');
|
|
693
|
+
console.log(` ${shellPolicy.message || 'Git Bash is required on Windows for helper-backed flows.'}`);
|
|
694
|
+
console.log('');
|
|
695
|
+
console.log(' Core setup will finish. The shell scripts behind some /plan, /ship,');
|
|
696
|
+
console.log(' and /review helper steps are UNAVAILABLE until Git Bash is installed.');
|
|
697
|
+
console.log(' Agent slash-skills and typeable `forge` CLI verbs keep working.');
|
|
698
|
+
console.log('');
|
|
699
|
+
console.log(' Install Git Bash: https://git-scm.com/download/win');
|
|
700
|
+
console.log(' Then re-run `forge setup` to re-enable helper-backed flows.');
|
|
701
|
+
console.log('');
|
|
702
|
+
}
|
|
703
|
+
|
|
725
704
|
function ensureWorkflowShellPolicy(selectedAgents, options = {}) {
|
|
726
705
|
const shellPolicy = resolveWorkflowShellPolicy(selectedAgents, options);
|
|
727
706
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
707
|
+
const gitBashMissing = shellPolicy.required
|
|
708
|
+
&& shellPolicy.platform === 'win32'
|
|
709
|
+
&& !shellPolicy.available;
|
|
710
|
+
|
|
711
|
+
if (gitBashMissing) {
|
|
712
|
+
// Graceful degrade (kernel issue 048c1e6d): a Windows newcomer without Git
|
|
713
|
+
// Bash must not be dead on arrival. Print install guidance and continue in a
|
|
714
|
+
// reduced-capability mode instead of hard-throwing and aborting all of setup.
|
|
715
|
+
printGitBashGuidance(shellPolicy);
|
|
716
|
+
return { ...shellPolicy, available: false, degraded: true };
|
|
732
717
|
}
|
|
733
718
|
|
|
734
719
|
return shellPolicy;
|
|
@@ -756,16 +741,11 @@ function createSymlinkOrCopy(source, target, options = {}) {
|
|
|
756
741
|
}
|
|
757
742
|
|
|
758
743
|
function shouldLinkAgentsMd(agent) {
|
|
759
|
-
|
|
760
|
-
return !['copilot', 'opencode'].includes(agent.customSetup);
|
|
744
|
+
return Boolean(agent?.linkFile);
|
|
761
745
|
}
|
|
762
746
|
|
|
763
747
|
|
|
764
748
|
|
|
765
|
-
function stripFrontmatter(content) {
|
|
766
|
-
return fileUtils.stripFrontmatter(content);
|
|
767
|
-
}
|
|
768
|
-
|
|
769
749
|
// Read existing .env.local
|
|
770
750
|
|
|
771
751
|
|
|
@@ -813,17 +793,17 @@ async function detectProjectStatus() {
|
|
|
813
793
|
type: 'fresh', // 'fresh', 'upgrade', or 'partial'
|
|
814
794
|
hasAgentsMd: fs.existsSync(path.join(projectRoot, 'AGENTS.md')),
|
|
815
795
|
hasClaudeMd: fs.existsSync(path.join(projectRoot, 'CLAUDE.md')),
|
|
816
|
-
hasClaudeCommands: fs.existsSync(path.join(projectRoot, '.claude/commands')),
|
|
817
796
|
hasEnvLocal: fs.existsSync(path.join(projectRoot, '.env.local')),
|
|
818
797
|
existingEnvVars: {},
|
|
819
798
|
agentsMdSize: 0,
|
|
820
799
|
claudeMdSize: 0,
|
|
821
800
|
agentsMdLines: 0,
|
|
822
801
|
claudeMdLines: 0,
|
|
823
|
-
// Project tools status
|
|
824
|
-
|
|
802
|
+
// Project tools status — the Kernel issue store is always present (it
|
|
803
|
+
// auto-provisions on first use), so issue tracking needs no install probe.
|
|
804
|
+
hasBeads: true,
|
|
825
805
|
hasSkills: isSkillsInitialized(),
|
|
826
|
-
beadsInstallType:
|
|
806
|
+
beadsInstallType: 'kernel',
|
|
827
807
|
skillsInstallType: checkForSkills(),
|
|
828
808
|
// Enhanced: Auto-detected project context
|
|
829
809
|
autoDetected: null
|
|
@@ -847,9 +827,9 @@ async function detectProjectStatus() {
|
|
|
847
827
|
}
|
|
848
828
|
|
|
849
829
|
// Determine installation type
|
|
850
|
-
if (status.hasAgentsMd
|
|
830
|
+
if (status.hasAgentsMd) {
|
|
851
831
|
status.type = 'upgrade'; // Full forge installation exists
|
|
852
|
-
} else if (status.
|
|
832
|
+
} else if (status.hasEnvLocal) {
|
|
853
833
|
status.type = 'partial'; // Agent-specific files exist (not just base files from postinstall)
|
|
854
834
|
}
|
|
855
835
|
// else: 'fresh' - new installation (or just postinstall baseline with AGENTS.md)
|
|
@@ -1544,24 +1524,13 @@ function displayMcpStatus(selectedAgents) {
|
|
|
1544
1524
|
console.log('Provides up-to-date library docs for AI coding agents.');
|
|
1545
1525
|
console.log('');
|
|
1546
1526
|
|
|
1547
|
-
// Show what was/will be auto-installed
|
|
1527
|
+
// Show what was/will be auto-installed. Both Claude and Cursor read a
|
|
1528
|
+
// project-local MCP config, so Forge auto-wires both (no manual step needed).
|
|
1548
1529
|
if (selectedAgents.includes('claude')) {
|
|
1549
1530
|
console.log(' ✓ Auto-installed for Claude Code (.mcp.json)');
|
|
1550
1531
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
cursor: 'Cursor: Configure via Cursor Settings > MCP',
|
|
1554
|
-
cline: 'Cline: Install via MCP Marketplace',
|
|
1555
|
-
};
|
|
1556
|
-
const needsManualMcp = Object.entries(manualMcpMap)
|
|
1557
|
-
.filter(([key]) => selectedAgents.includes(key))
|
|
1558
|
-
.map(([, msg]) => msg);
|
|
1559
|
-
|
|
1560
|
-
if (needsManualMcp.length > 0) {
|
|
1561
|
-
needsManualMcp.forEach(msg => console.log(` ! ${msg}`));
|
|
1562
|
-
console.log('');
|
|
1563
|
-
console.log(' Package: @upstash/context7-mcp@latest');
|
|
1564
|
-
console.log(' Docs: https://github.com/upstash/context7-mcp');
|
|
1532
|
+
if (selectedAgents.includes('cursor')) {
|
|
1533
|
+
console.log(' ✓ Auto-installed for Cursor (.cursor/mcp.json)');
|
|
1565
1534
|
}
|
|
1566
1535
|
}
|
|
1567
1536
|
|
|
@@ -1640,39 +1609,6 @@ async function configureExternalServices(rl, question, selectedAgents = [], proj
|
|
|
1640
1609
|
const { added, preserved } = writeEnvTokens(tokens, true);
|
|
1641
1610
|
displayEnvTokenResults(added, preserved);
|
|
1642
1611
|
|
|
1643
|
-
// GitHub-Beads issue sync setup
|
|
1644
|
-
console.log('');
|
|
1645
|
-
const enableSync = await askYesNo(question, 'Enable GitHub ↔ Beads issue sync?', true);
|
|
1646
|
-
if (enableSync) {
|
|
1647
|
-
try {
|
|
1648
|
-
const result = await scaffoldGithubBeadsSync(projectRoot, packageDir);
|
|
1649
|
-
for (const f of result.created) {
|
|
1650
|
-
console.log(` Created: ${f}`);
|
|
1651
|
-
}
|
|
1652
|
-
for (const f of result.skipped) {
|
|
1653
|
-
console.log(` Skipped: ${f} (already exists)`);
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
// PAT setup guidance for Beads sync (non-fatal)
|
|
1657
|
-
// Skip if --sync flag is set — handleSyncScaffold will handle PAT setup
|
|
1658
|
-
if (!SYNC_ENABLED) {
|
|
1659
|
-
try {
|
|
1660
|
-
const patResult = setupPAT(projectRoot, { interactive: !NON_INTERACTIVE });
|
|
1661
|
-
if (patResult.success) {
|
|
1662
|
-
console.log(' ✓ Beads sync PAT configured');
|
|
1663
|
-
} else if (patResult.reminder) {
|
|
1664
|
-
console.log(` ℹ ${patResult.reminder}`);
|
|
1665
|
-
} else if (patResult.instructions) {
|
|
1666
|
-
console.log(` ℹ ${patResult.instructions.split('\n')[0]}`);
|
|
1667
|
-
}
|
|
1668
|
-
} catch (_patErr) { // NOSONAR — best-effort PAT setup, non-fatal
|
|
1669
|
-
// PAT setup is best-effort — don't block sync scaffold
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
} catch (err) {
|
|
1673
|
-
console.error(` Error scaffolding GitHub-Beads sync: ${err.message}`);
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
1612
|
}
|
|
1677
1613
|
|
|
1678
1614
|
// Display the Forge banner
|
|
@@ -1724,7 +1660,7 @@ function setupCoreDocs() {
|
|
|
1724
1660
|
// TEMPLATE.md and PROGRESS.md are also deferred to first use.
|
|
1725
1661
|
|
|
1726
1662
|
// Copy essential docs (TOOLCHAIN.md, VALIDATION.md) to consumer's docs/forge/
|
|
1727
|
-
const result = copyEssentialDocs(projectRoot, packageDir);
|
|
1663
|
+
const result = copyEssentialDocs(projectRoot, getPackageRoot(packageDir));
|
|
1728
1664
|
for (const f of result.created) {
|
|
1729
1665
|
console.log(` Created: ${f}`);
|
|
1730
1666
|
}
|
|
@@ -1768,7 +1704,7 @@ function minimalInstall() {
|
|
|
1768
1704
|
if (fs.existsSync(agentsPath)) {
|
|
1769
1705
|
console.log(' Skipped: AGENTS.md (already exists)');
|
|
1770
1706
|
} else {
|
|
1771
|
-
const agentsSrc = path.join(packageDir, 'AGENTS.md');
|
|
1707
|
+
const agentsSrc = path.join(getPackageRoot(packageDir), 'AGENTS.md');
|
|
1772
1708
|
if (copyFile(agentsSrc, 'AGENTS.md')) {
|
|
1773
1709
|
console.log(' Created: AGENTS.md (universal standard)');
|
|
1774
1710
|
|
|
@@ -1799,26 +1735,19 @@ function minimalInstall() {
|
|
|
1799
1735
|
|
|
1800
1736
|
|
|
1801
1737
|
// Helper: Setup Claude agent
|
|
1802
|
-
function setupClaudeAgent(
|
|
1803
|
-
//
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
});
|
|
1813
|
-
console.log(` Copied: ${copied} workflow commands`);
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
// Copy rules
|
|
1817
|
-
const rulesSrc = path.join(packageDir, '.claude/rules/workflow.md');
|
|
1818
|
-
copyFile(rulesSrc, '.claude/rules/workflow.md');
|
|
1738
|
+
function setupClaudeAgent(_skipFiles) {
|
|
1739
|
+
// _skipFiles is accepted for call-site arity parity only; it is unused because
|
|
1740
|
+
// this skills-only surface writes scripts unconditionally (no skip prompts).
|
|
1741
|
+
// Skills-only surface: per-skill SKILL.md dirs are populated by createAgentSkill.
|
|
1742
|
+
//
|
|
1743
|
+
// Claude receives workflow/TDD/security/documentation policy through the
|
|
1744
|
+
// CLAUDE.md → AGENTS.md instruction projection, NOT always-on `.claude/rules/*`
|
|
1745
|
+
// files — those would triple-deliver the same policy into every session as
|
|
1746
|
+
// token bloat. See lib/rules-sync.js and lib/harness-capability-matrix.js.
|
|
1747
|
+
// Only Cursor has a first-class native rule surface (rendered by setupCursorAgent).
|
|
1819
1748
|
|
|
1820
1749
|
// Copy scripts
|
|
1821
|
-
const scriptSrc = path.join(packageDir, '.claude/scripts/load-env.sh');
|
|
1750
|
+
const scriptSrc = path.join(getPackageRoot(packageDir), '.claude/scripts/load-env.sh');
|
|
1822
1751
|
copyFile(scriptSrc, '.claude/scripts/load-env.sh');
|
|
1823
1752
|
}
|
|
1824
1753
|
|
|
@@ -1827,78 +1756,38 @@ function setupClaudeAgent(skipFiles = {}) {
|
|
|
1827
1756
|
|
|
1828
1757
|
// Helper: Setup Cursor agent
|
|
1829
1758
|
async function setupCursorAgent() {
|
|
1759
|
+
// Drop the deprecated root config, but NEVER destroy a user's hand-authored
|
|
1760
|
+
// `.cursorrules` — it predates AGENTS.md, so real users have curated ones.
|
|
1761
|
+
const { removed, backupPath } = backupAndRemoveLegacyCursorRules(projectRoot);
|
|
1762
|
+
if (removed) {
|
|
1763
|
+
console.log(
|
|
1764
|
+
` Removed: .cursorrules (deprecated — Cursor reads AGENTS.md + .cursor/rules/*.mdc); ` +
|
|
1765
|
+
`backed up to ${path.basename(backupPath)}`,
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1830
1768
|
await generateCursorConfig(projectRoot, { overwrite: false });
|
|
1831
1769
|
console.log(' Created: Cursor native rules');
|
|
1832
1770
|
}
|
|
1833
1771
|
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
console.log(' Created: Copilot native config');
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
async function setupOpenCodeAgent() {
|
|
1845
|
-
await generateOpenCodeConfig(projectRoot, { overwrite: false });
|
|
1846
|
-
console.log(' Created: OpenCode native config');
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
// Helper: Convert command to agent-specific format
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
// Helper: Convert command to agent-specific format
|
|
1853
|
-
function convertCommandToAgentFormat(cmd, content, agent) {
|
|
1854
|
-
let targetContent = content;
|
|
1855
|
-
let targetFile = cmd;
|
|
1856
|
-
|
|
1857
|
-
if (agent.needsConversion) {
|
|
1858
|
-
targetContent = stripFrontmatter(content);
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
if (agent.promptFormat) {
|
|
1862
|
-
targetFile = cmd.replace('.md', '.prompt.md');
|
|
1863
|
-
targetContent = stripFrontmatter(content);
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
return { targetFile, targetContent };
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
// Helper: Copy commands for agent
|
|
1870
|
-
|
|
1772
|
+
// Helper: back up + remove a deprecated `.cursorrules` without data loss.
|
|
1773
|
+
// Copies to `.cursorrules.bak` (then numbered `.bak.1`, `.2`, … so a repeat never
|
|
1774
|
+
// clobbers an earlier snapshot) before deleting. Mirrors the markerless AGENTS.md
|
|
1775
|
+
// backup above. Exported for direct testing.
|
|
1776
|
+
function backupAndRemoveLegacyCursorRules(root) {
|
|
1777
|
+
const legacy = path.join(root, '.cursorrules');
|
|
1778
|
+
if (!fs.existsSync(legacy)) return { removed: false };
|
|
1871
1779
|
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
const targetDir = agent.dirs[0]; // First dir is commands/workflows
|
|
1880
|
-
writeFile(`${targetDir}/${targetFile}`, targetContent);
|
|
1881
|
-
});
|
|
1882
|
-
console.log(` Converted: ${Object.keys(claudeCommands).length} workflow commands`);
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
// Helper: Copy rules for agent
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
// Helper: Copy rules for agent
|
|
1889
|
-
function copyAgentRules(agent) {
|
|
1890
|
-
if (!agent.needsConversion) return;
|
|
1891
|
-
|
|
1892
|
-
const workflowMdPath = path.join(projectRoot, '.claude/rules/workflow.md');
|
|
1893
|
-
if (!fs.existsSync(workflowMdPath)) return;
|
|
1894
|
-
|
|
1895
|
-
const rulesDir = agent.dirs.find(d => d.includes('/rules'));
|
|
1896
|
-
if (!rulesDir) return;
|
|
1897
|
-
|
|
1898
|
-
const ruleContent = readFile(workflowMdPath);
|
|
1899
|
-
if (ruleContent) {
|
|
1900
|
-
writeFile(`${rulesDir}/workflow.md`, ruleContent);
|
|
1780
|
+
let backupPath = path.join(root, '.cursorrules.bak');
|
|
1781
|
+
if (fs.existsSync(backupPath)) {
|
|
1782
|
+
let suffix = 1;
|
|
1783
|
+
while (fs.existsSync(path.join(root, `.cursorrules.bak.${suffix}`))) {
|
|
1784
|
+
suffix += 1;
|
|
1785
|
+
}
|
|
1786
|
+
backupPath = path.join(root, `.cursorrules.bak.${suffix}`);
|
|
1901
1787
|
}
|
|
1788
|
+
fs.copyFileSync(legacy, backupPath);
|
|
1789
|
+
fs.rmSync(legacy, { force: true });
|
|
1790
|
+
return { removed: true, backupPath };
|
|
1902
1791
|
}
|
|
1903
1792
|
|
|
1904
1793
|
// Helper: Create skill file for agent
|
|
@@ -1911,27 +1800,57 @@ function createAgentSkill(agent, agentKey) {
|
|
|
1911
1800
|
return;
|
|
1912
1801
|
}
|
|
1913
1802
|
|
|
1914
|
-
if (!agent.hasSkill) return;
|
|
1803
|
+
if (!agent.hasSkill || !agent.skillsDir) return;
|
|
1915
1804
|
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
}
|
|
1805
|
+
// Skills-only surface: populate every canonical skill into the agent skills dir
|
|
1806
|
+
// (.claude/skills, .cursor/skills) from the packaged canonical `skills/` source.
|
|
1807
|
+
// clean:false so a rerun/upgrade overwrites Forge's skills without deleting
|
|
1808
|
+
// user-authored or third-party skills that share these shared agent dirs.
|
|
1809
|
+
const { written } = populateAgentSkills({
|
|
1810
|
+
sourceRoot: getPackageRoot(packageDir),
|
|
1811
|
+
targetSkillsDir: path.join(projectRoot, agent.skillsDir),
|
|
1812
|
+
clean: false,
|
|
1813
|
+
});
|
|
1814
|
+
console.log(` Created: ${written.length} skills in ${agent.skillsDir}/`);
|
|
1921
1815
|
}
|
|
1922
1816
|
|
|
1923
1817
|
// Helper: Create Codex per-stage skills from canonical commands
|
|
1924
1818
|
function createCodexSkills() {
|
|
1925
|
-
const installPlan = buildCodexSkillInstallPlan(packageDir, { env: process.env, homeDir: os.homedir() });
|
|
1819
|
+
const installPlan = buildCodexSkillInstallPlan(getPackageRoot(packageDir), { env: process.env, homeDir: os.homedir() });
|
|
1926
1820
|
const installRoot = formatCodexSkillsInstallDir({ env: process.env, homeDir: os.homedir() });
|
|
1927
1821
|
|
|
1928
1822
|
CODEX_SETUP_REPORT = {
|
|
1929
1823
|
installRoot,
|
|
1930
1824
|
skillCount: installPlan.length,
|
|
1825
|
+
repoSkillsDir: CODEX_REPO_SKILLS_DIR,
|
|
1826
|
+
repoSkillCount: 0,
|
|
1931
1827
|
status: 'complete',
|
|
1932
1828
|
message: '',
|
|
1933
1829
|
};
|
|
1934
1830
|
|
|
1831
|
+
// Repo-local discovery mirror: generate `.agents/skills/<name>/SKILL.md` from
|
|
1832
|
+
// the canonical skills/ source. This is Codex's documented repo-scope discovery
|
|
1833
|
+
// path (scanned cwd → repo root) and is committed, so a teammate who clones the
|
|
1834
|
+
// repo WITHOUT running `forge setup` still gets Forge skills/stages discovered.
|
|
1835
|
+
// Independent of the GLOBAL $CODEX_HOME install below (which needs canonical
|
|
1836
|
+
// packaging templates) — so it runs before the install-plan early return.
|
|
1837
|
+
try {
|
|
1838
|
+
const { written } = populateCodexRepoSkills({ sourceRoot: getPackageRoot(packageDir), projectRoot });
|
|
1839
|
+
CODEX_SETUP_REPORT.repoSkillCount = written.length;
|
|
1840
|
+
console.log(` Created: ${written.length} repo-local Codex skills in ${CODEX_REPO_SKILLS_DIR}/ (commit for teammate discovery)`);
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
addSetupNote(`Codex repo-local skill generation failed for ${CODEX_REPO_SKILLS_DIR}: ${error.message}`);
|
|
1843
|
+
console.log(` Warning: could not generate repo-local Codex skills in ${CODEX_REPO_SKILLS_DIR} (${error.message})`);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// Codex is NOT given a committed project-local `.codex/skills` mirror. Its
|
|
1847
|
+
// repo-scope discovery path is the committed `.agents/skills` (generated above),
|
|
1848
|
+
// and its stage skills install GLOBALLY into `$CODEX_HOME/skills` (below). A
|
|
1849
|
+
// committed `.codex/skills` copy would feed nothing at runtime, so setup does
|
|
1850
|
+
// not create one and it stays gitignored (#342). See the long-standing invariant
|
|
1851
|
+
// in test/setup-runtime-flags.test.js ("setup installs Codex stage skills into
|
|
1852
|
+
// CODEX_HOME/skills/<stage>/SKILL.md").
|
|
1853
|
+
|
|
1935
1854
|
if (installPlan.length === 0) {
|
|
1936
1855
|
CODEX_SETUP_REPORT.status = 'partial';
|
|
1937
1856
|
CODEX_SETUP_REPORT.message = 'Codex setup could not find the packaged stage skill templates.';
|
|
@@ -1960,27 +1879,178 @@ function createCodexSkills() {
|
|
|
1960
1879
|
return CODEX_SETUP_REPORT;
|
|
1961
1880
|
}
|
|
1962
1881
|
|
|
1963
|
-
// Helper:
|
|
1964
|
-
|
|
1882
|
+
// Helper: opt-in Graphiti memory MCP descriptor(s).
|
|
1883
|
+
//
|
|
1884
|
+
// Included ONLY when the memory backend resolves to `graphiti` (same precedence
|
|
1885
|
+
// as the memory router: deps > FORGE_MEMORY_BACKEND > .forge/config.yaml) AND
|
|
1886
|
+
// the config passes the same strict validity check `forge doctor` uses
|
|
1887
|
+
// (assertMemoryConfigValid). Local backend — the default — returns [] so the
|
|
1888
|
+
// rendered MCP config stays byte-identical to the Context7-only output. An
|
|
1889
|
+
// invalid/incomplete graphiti config prints a one-line notice and is skipped;
|
|
1890
|
+
// it never crashes setup.
|
|
1891
|
+
function graphitiMcpDescriptors() {
|
|
1892
|
+
const { assertMemoryConfigValid } = require('../memory/router');
|
|
1893
|
+
try {
|
|
1894
|
+
const { backend, graphiti } = assertMemoryConfigValid({ projectRoot });
|
|
1895
|
+
if (backend !== 'graphiti') return [];
|
|
1896
|
+
const { buildGraphitiServerDescriptor } = require('../memory/graphiti-mcp');
|
|
1897
|
+
return [buildGraphitiServerDescriptor(graphiti)];
|
|
1898
|
+
} catch (err) {
|
|
1899
|
+
// Surface the validator's specific message (it names the missing key and
|
|
1900
|
+
// where to configure it) instead of a generic notice that hides the cause.
|
|
1901
|
+
console.log(
|
|
1902
|
+
` Notice: Graphiti MCP server not wired — ${err && err.message ? err.message : 'memory config invalid'}`
|
|
1903
|
+
+ ' Run `forge doctor` for details.',
|
|
1904
|
+
);
|
|
1905
|
+
return [];
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1965
1908
|
|
|
1966
1909
|
// Helper: Setup MCP config for Claude
|
|
1910
|
+
//
|
|
1911
|
+
// Read → merge → write (idempotent). A pre-existing `.mcp.json` is MERGED, not
|
|
1912
|
+
// skipped: the old skip-if-exists behavior silently refused to add the Context7
|
|
1913
|
+
// server whenever any config already existed. User/other servers are preserved.
|
|
1967
1914
|
function setupClaudeMcpConfig() {
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
|
|
1915
|
+
const { existed, skipped, backup } = renderMcpConfig({
|
|
1916
|
+
harness: 'claude',
|
|
1917
|
+
targetRoot: projectRoot,
|
|
1918
|
+
descriptors: [CONTEXT7_MCP_DESCRIPTOR, ...graphitiMcpDescriptors()],
|
|
1919
|
+
});
|
|
1920
|
+
if (skipped) {
|
|
1921
|
+
// Existing .mcp.json was unparseable (JSONC/trailing comma): renderMcpConfig
|
|
1922
|
+
// left it untouched and backed it up rather than clobber the user's servers.
|
|
1923
|
+
// Report that honestly instead of a false "merged" success.
|
|
1924
|
+
console.log(
|
|
1925
|
+
` Skipped: .mcp.json is not valid JSON — left untouched to avoid data loss`
|
|
1926
|
+
+ `${backup ? ` (backed up to ${backup})` : ''}. Add the Context7 MCP server manually.`,
|
|
1927
|
+
);
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
console.log(
|
|
1931
|
+
existed
|
|
1932
|
+
? ' Updated: .mcp.json (merged Context7 MCP, preserved existing servers)'
|
|
1933
|
+
: ' Created: .mcp.json with Context7 MCP',
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// Helper: Setup MCP config for Cursor (project-local .cursor/mcp.json).
|
|
1938
|
+
// Cursor reads a project-local MCP config, so this is a real native delivery
|
|
1939
|
+
// (mirrors setupClaudeMcpConfig). Read → merge → write, preserving user servers.
|
|
1940
|
+
function setupCursorMcpConfig() {
|
|
1941
|
+
const { existed, skipped, backup } = renderMcpConfig({
|
|
1942
|
+
harness: 'cursor',
|
|
1943
|
+
targetRoot: projectRoot,
|
|
1944
|
+
descriptors: [CONTEXT7_MCP_DESCRIPTOR, ...graphitiMcpDescriptors()],
|
|
1945
|
+
});
|
|
1946
|
+
if (skipped) {
|
|
1947
|
+
console.log(
|
|
1948
|
+
` Skipped: .cursor/mcp.json is not valid JSON — left untouched to avoid data loss`
|
|
1949
|
+
+ `${backup ? ` (backed up to ${backup})` : ''}. Add the Context7 MCP server manually.`,
|
|
1950
|
+
);
|
|
1971
1951
|
return;
|
|
1972
1952
|
}
|
|
1953
|
+
console.log(
|
|
1954
|
+
existed
|
|
1955
|
+
? ' Updated: .cursor/mcp.json (merged Context7 MCP, preserved existing servers)'
|
|
1956
|
+
: ' Created: .cursor/mcp.json with Context7 MCP',
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1973
1959
|
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1960
|
+
// Safe SAFETY defaults are ON by default (non-surprising) but fully opt-out-able.
|
|
1961
|
+
// Set FORGE_SKIP_SAFETY_DEFAULTS=1 (or `true`) to skip rendering the permission /
|
|
1962
|
+
// ignore defaults — Forge then leaves those surfaces entirely to the user.
|
|
1963
|
+
function safetyDefaultsEnabled() {
|
|
1964
|
+
const flag = String(process.env.FORGE_SKIP_SAFETY_DEFAULTS || '').trim().toLowerCase();
|
|
1965
|
+
return !(flag === '1' || flag === 'true' || flag === 'yes');
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
// Helper: Render safe Claude tool-permission defaults into .claude/settings.json.
|
|
1969
|
+
// Read -> merge -> write (idempotent). Preserves the user's existing settings and
|
|
1970
|
+
// allow/deny/ask entries; an unparseable file is backed up and left untouched.
|
|
1971
|
+
function setupClaudePermissions() {
|
|
1972
|
+
if (!safetyDefaultsEnabled()) {
|
|
1973
|
+
console.log(' Skipped: .claude/settings.json permissions (FORGE_SKIP_SAFETY_DEFAULTS set)');
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
const { existed, skipped, backup } = renderClaudePermissions({ targetRoot: projectRoot });
|
|
1977
|
+
if (skipped) {
|
|
1978
|
+
console.log(
|
|
1979
|
+
' Skipped: .claude/settings.json is not valid JSON — left untouched to avoid data loss'
|
|
1980
|
+
+ `${backup ? ` (backed up to ${backup})` : ''}. Add safe permissions manually.`,
|
|
1981
|
+
);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
console.log(
|
|
1985
|
+
existed
|
|
1986
|
+
? ' Updated: .claude/settings.json (merged safe permission defaults, preserved your entries)'
|
|
1987
|
+
: ' Created: .claude/settings.json with safe permission defaults',
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// Helper: Render safe .cursorignore defaults (AI read/index boundary).
|
|
1992
|
+
// Read -> merge -> write (idempotent). Preserves user lines; only appends missing
|
|
1993
|
+
// default patterns (secrets/.env/node_modules/build artifacts).
|
|
1994
|
+
function setupCursorIgnore() {
|
|
1995
|
+
if (!safetyDefaultsEnabled()) {
|
|
1996
|
+
console.log(' Skipped: .cursorignore defaults (FORGE_SKIP_SAFETY_DEFAULTS set)');
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const { existed } = renderCursorIgnore({ targetRoot: projectRoot });
|
|
2000
|
+
console.log(
|
|
2001
|
+
existed
|
|
2002
|
+
? ' Updated: .cursorignore (appended safe defaults, preserved your entries)'
|
|
2003
|
+
: ' Created: .cursorignore with safe defaults',
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// Helper: Setup native HOOK config for Claude (project-local .claude/settings.json).
|
|
2008
|
+
// Projects Forge's TDD-gate + protected-path enforcement onto Claude's native hook
|
|
2009
|
+
// surface (a `hooks` block). Read → merge → write, preserving user hooks; an
|
|
2010
|
+
// unparseable settings.json is backed up and left untouched (data-loss safe).
|
|
2011
|
+
function setupClaudeHooksConfig() {
|
|
2012
|
+
const { existed, skipped, backup, wrote } = renderHookConfig({
|
|
2013
|
+
harness: 'claude',
|
|
2014
|
+
targetRoot: projectRoot,
|
|
2015
|
+
});
|
|
2016
|
+
if (skipped) {
|
|
2017
|
+
console.log(
|
|
2018
|
+
` Skipped: .claude/settings.json is not valid JSON — left untouched to avoid data loss`
|
|
2019
|
+
+ `${backup ? ` (backed up to ${backup})` : ''}. Add the Forge hooks block manually.`,
|
|
2020
|
+
);
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
if (wrote) {
|
|
2024
|
+
console.log(
|
|
2025
|
+
existed
|
|
2026
|
+
? ' Updated: .claude/settings.json (merged Forge hooks, preserved existing hooks)'
|
|
2027
|
+
: ' Created: .claude/settings.json with Forge hooks',
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// Helper: Setup native HOOK config for Cursor (project-local .cursor/hooks.json,
|
|
2033
|
+
// Cursor 1.7+). Mirrors setupClaudeHooksConfig. Read → merge → write, preserving
|
|
2034
|
+
// user hooks; unparseable config is backed up and left untouched.
|
|
2035
|
+
function setupCursorHooksConfig() {
|
|
2036
|
+
const { existed, skipped, backup, wrote } = renderHookConfig({
|
|
2037
|
+
harness: 'cursor',
|
|
2038
|
+
targetRoot: projectRoot,
|
|
2039
|
+
});
|
|
2040
|
+
if (skipped) {
|
|
2041
|
+
console.log(
|
|
2042
|
+
` Skipped: .cursor/hooks.json is not valid JSON — left untouched to avoid data loss`
|
|
2043
|
+
+ `${backup ? ` (backed up to ${backup})` : ''}. Add the Forge hooks manually.`,
|
|
2044
|
+
);
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
if (wrote) {
|
|
2048
|
+
console.log(
|
|
2049
|
+
existed
|
|
2050
|
+
? ' Updated: .cursor/hooks.json (merged Forge hooks, preserved existing hooks)'
|
|
2051
|
+
: ' Created: .cursor/hooks.json with Forge hooks',
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
1984
2054
|
}
|
|
1985
2055
|
|
|
1986
2056
|
// Helper: Create agent link file
|
|
@@ -2002,13 +2072,13 @@ function createAgentLinkFile(agent, symlinkOnly = false) {
|
|
|
2002
2072
|
|
|
2003
2073
|
|
|
2004
2074
|
// Setup specific agent
|
|
2005
|
-
async function setupAgent(agentKey,
|
|
2075
|
+
async function setupAgent(agentKey, skipFiles = {}) {
|
|
2006
2076
|
const agent = AGENTS[agentKey];
|
|
2007
2077
|
if (!agent) return;
|
|
2008
2078
|
|
|
2009
2079
|
console.log(`\nSetting up ${agent.name}...`);
|
|
2010
2080
|
if (agent.supportStatus === 'deprecated') {
|
|
2011
|
-
console.log(` Warning: ${agent.name} is in deprecated compatibility mode; Forge will scaffold
|
|
2081
|
+
console.log(` Warning: ${agent.name} is in deprecated compatibility mode; Forge will scaffold skill files only.`);
|
|
2012
2082
|
}
|
|
2013
2083
|
|
|
2014
2084
|
// Create directories
|
|
@@ -2023,30 +2093,30 @@ async function setupAgent(agentKey, claudeCommands, skipFiles = {}) {
|
|
|
2023
2093
|
await setupCursorAgent();
|
|
2024
2094
|
}
|
|
2025
2095
|
|
|
2026
|
-
if (agentKey === 'kilocode') {
|
|
2027
|
-
await setupKiloAgent();
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
if (agentKey === 'copilot') {
|
|
2031
|
-
await setupCopilotAgent();
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
if (agentKey === 'opencode') {
|
|
2035
|
-
await setupOpenCodeAgent();
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
// Convert/copy commands
|
|
2039
|
-
copyAgentCommands(agent, claudeCommands);
|
|
2040
|
-
|
|
2041
|
-
// Copy rules if needed
|
|
2042
|
-
copyAgentRules(agent);
|
|
2043
|
-
|
|
2044
2096
|
// Create SKILL.md or Codex stage skills
|
|
2045
2097
|
createAgentSkill(agent, agentKey);
|
|
2046
2098
|
|
|
2047
|
-
// Setup MCP configs
|
|
2099
|
+
// Setup MCP configs (project-local, native for both harnesses)
|
|
2048
2100
|
if (agentKey === 'claude') {
|
|
2049
2101
|
setupClaudeMcpConfig();
|
|
2102
|
+
// Native safety surface: declarative tool-permission allowlist.
|
|
2103
|
+
setupClaudePermissions();
|
|
2104
|
+
}
|
|
2105
|
+
if (agent.customSetup === 'cursor') {
|
|
2106
|
+
setupCursorMcpConfig();
|
|
2107
|
+
// Native safety surface: AI read/index ignore boundary.
|
|
2108
|
+
setupCursorIgnore();
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// Setup native HOOK configs (project-local, native for both harnesses).
|
|
2112
|
+
// Projects Forge's TDD-gate + protected-path enforcement onto each harness's
|
|
2113
|
+
// native hook surface. Codex hooks are GLOBAL-config scope and intentionally
|
|
2114
|
+
// not written at project setup (see lib/hook-renderer.js).
|
|
2115
|
+
if (agentKey === 'claude') {
|
|
2116
|
+
setupClaudeHooksConfig();
|
|
2117
|
+
}
|
|
2118
|
+
if (agent.customSetup === 'cursor') {
|
|
2119
|
+
setupCursorHooksConfig();
|
|
2050
2120
|
}
|
|
2051
2121
|
|
|
2052
2122
|
// Create link file (SYMLINK_ONLY = --symlink flag disables copy fallback)
|
|
@@ -2086,7 +2156,6 @@ function displayInstallationStatus(projectStatus) {
|
|
|
2086
2156
|
}
|
|
2087
2157
|
|
|
2088
2158
|
if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
|
|
2089
|
-
if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
|
|
2090
2159
|
if (projectStatus.hasEnvLocal) console.log(' - .env.local');
|
|
2091
2160
|
console.log('');
|
|
2092
2161
|
}
|
|
@@ -2162,7 +2231,6 @@ async function promptForFileOverwrite(question, fileType, exists, skipFiles) {
|
|
|
2162
2231
|
|
|
2163
2232
|
const fileLabels = {
|
|
2164
2233
|
agentsMd: { prompt: 'Found existing AGENTS.md. Overwrite?', message: 'AGENTS.md', key: 'agentsMd' },
|
|
2165
|
-
claudeCommands: { prompt: 'Found existing .claude/commands/. Overwrite?', message: '.claude/commands/', key: 'claudeCommands' }
|
|
2166
2234
|
};
|
|
2167
2235
|
|
|
2168
2236
|
const config = fileLabels[fileType];
|
|
@@ -2182,7 +2250,7 @@ async function promptForFileOverwrite(question, fileType, exists, skipFiles) {
|
|
|
2182
2250
|
}
|
|
2183
2251
|
}
|
|
2184
2252
|
|
|
2185
|
-
// Default behavior: Binary y/n for files with markers
|
|
2253
|
+
// Default behavior: Binary y/n for files with markers
|
|
2186
2254
|
const overwrite = await askYesNo(question, config.prompt, true);
|
|
2187
2255
|
if (overwrite) {
|
|
2188
2256
|
console.log(` Will overwrite ${config.message}`);
|
|
@@ -2334,7 +2402,7 @@ async function installAgentsMd(skipFiles) {
|
|
|
2334
2402
|
return;
|
|
2335
2403
|
}
|
|
2336
2404
|
|
|
2337
|
-
const agentsSrc = path.join(packageDir, 'AGENTS.md');
|
|
2405
|
+
const agentsSrc = path.join(getPackageRoot(packageDir), 'AGENTS.md');
|
|
2338
2406
|
const agentsDest = path.join(projectRoot, 'AGENTS.md');
|
|
2339
2407
|
|
|
2340
2408
|
// Try smart merge if file exists
|
|
@@ -2372,28 +2440,6 @@ async function installAgentsMd(skipFiles) {
|
|
|
2372
2440
|
*/
|
|
2373
2441
|
|
|
2374
2442
|
|
|
2375
|
-
/**
|
|
2376
|
-
* Load Claude commands for conversion
|
|
2377
|
-
*/
|
|
2378
|
-
function loadClaudeCommands(selectedAgents) {
|
|
2379
|
-
const claudeCommands = {};
|
|
2380
|
-
const needsClaudeCommands = selectedAgents.includes('claude') ||
|
|
2381
|
-
selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands);
|
|
2382
|
-
|
|
2383
|
-
if (!needsClaudeCommands) {
|
|
2384
|
-
return claudeCommands;
|
|
2385
|
-
}
|
|
2386
|
-
|
|
2387
|
-
getWorkflowCommands().forEach(cmd => {
|
|
2388
|
-
const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
|
|
2389
|
-
const content = readFile(cmdPath);
|
|
2390
|
-
if (content) {
|
|
2391
|
-
claudeCommands[`${cmd}.md`] = content;
|
|
2392
|
-
}
|
|
2393
|
-
});
|
|
2394
|
-
|
|
2395
|
-
return claudeCommands;
|
|
2396
|
-
}
|
|
2397
2443
|
|
|
2398
2444
|
/**
|
|
2399
2445
|
* Setup agents with progress indication
|
|
@@ -2405,8 +2451,8 @@ function loadClaudeCommands(selectedAgents) {
|
|
|
2405
2451
|
* Setup agents with progress indication
|
|
2406
2452
|
* Delegates to setupSelectedAgents to avoid duplicate implementations (S4144)
|
|
2407
2453
|
*/
|
|
2408
|
-
async function setupAgentsWithProgress(selectedAgents,
|
|
2409
|
-
await setupSelectedAgents(selectedAgents,
|
|
2454
|
+
async function setupAgentsWithProgress(selectedAgents, skipFiles) {
|
|
2455
|
+
await setupSelectedAgents(selectedAgents, skipFiles);
|
|
2410
2456
|
}
|
|
2411
2457
|
|
|
2412
2458
|
/**
|
|
@@ -2427,26 +2473,19 @@ function displaySetupSummary(selectedAgents) {
|
|
|
2427
2473
|
console.log('What\'s installed:');
|
|
2428
2474
|
console.log(' - AGENTS.md (universal instructions)');
|
|
2429
2475
|
|
|
2430
|
-
const workflowCount = getWorkflowCommands().length;
|
|
2431
2476
|
selectedAgents.forEach(key => {
|
|
2432
2477
|
const agent = AGENTS[key];
|
|
2433
2478
|
if (agent.linkFile) {
|
|
2434
2479
|
console.log(` - ${agent.linkFile} (${agent.name})`);
|
|
2435
2480
|
}
|
|
2436
|
-
if (agent.hasCommands && key === 'claude') {
|
|
2437
|
-
console.log(` - .claude/commands/ (${workflowCount} workflow commands)`);
|
|
2438
|
-
} else if (agent.hasCommands && key !== 'codex' && agent.dirs[0]) {
|
|
2439
|
-
console.log(` - ${agent.dirs[0]}/ (${workflowCount} workflow commands)`);
|
|
2440
|
-
}
|
|
2441
2481
|
if (key === 'codex') {
|
|
2442
|
-
const skillCount = CODEX_SETUP_REPORT?.skillCount ?? listCodexSkillEntries(packageDir).length;
|
|
2482
|
+
const skillCount = CODEX_SETUP_REPORT?.skillCount ?? listCodexSkillEntries(getPackageRoot(packageDir)).length;
|
|
2443
2483
|
const codexRoot = CODEX_SETUP_REPORT?.installRoot || formatCodexSkillsInstallDir({ env: process.env, homeDir: os.homedir() });
|
|
2444
2484
|
console.log(` - ${codexRoot}/<stage>/SKILL.md (${skillCount} stage skills)`);
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
}
|
|
2485
|
+
const repoSkillCount = CODEX_SETUP_REPORT?.repoSkillCount ?? 0;
|
|
2486
|
+
console.log(` - ${CODEX_REPO_SKILLS_DIR}/<skill>/SKILL.md (${repoSkillCount} repo-local skills — commit for teammate discovery)`);
|
|
2487
|
+
} else if (agent.hasSkill && agent.skillsDir) {
|
|
2488
|
+
console.log(` - ${agent.skillsDir}/<skill>/SKILL.md`);
|
|
2450
2489
|
}
|
|
2451
2490
|
});
|
|
2452
2491
|
|
|
@@ -2475,13 +2514,11 @@ function displaySetupSummary(selectedAgents) {
|
|
|
2475
2514
|
console.log('');
|
|
2476
2515
|
printSetupNotes();
|
|
2477
2516
|
|
|
2478
|
-
//
|
|
2479
|
-
if (
|
|
2480
|
-
console.log(' ✓
|
|
2481
|
-
} else if (checkForBeads()) {
|
|
2482
|
-
console.log(' ! Beads available - Run: bd init');
|
|
2517
|
+
// Issue store status — the Kernel store is always present (auto-provisioned)
|
|
2518
|
+
if (KERNEL_STORE_READY) {
|
|
2519
|
+
console.log(' ✓ Kernel issue store - Track work: forge ready');
|
|
2483
2520
|
} else {
|
|
2484
|
-
console.log(
|
|
2521
|
+
console.log(' ⚠ Kernel issue store - not provisioned; run: forge doctor');
|
|
2485
2522
|
}
|
|
2486
2523
|
|
|
2487
2524
|
// Skills status
|
|
@@ -2552,12 +2589,10 @@ async function _interactiveSetup() {
|
|
|
2552
2589
|
// Track which files to skip based on user choices
|
|
2553
2590
|
const skipFiles = {
|
|
2554
2591
|
agentsMd: false,
|
|
2555
|
-
claudeCommands: false
|
|
2556
2592
|
};
|
|
2557
2593
|
|
|
2558
2594
|
// Ask about overwriting existing files
|
|
2559
2595
|
await promptForFileOverwrite(question, 'agentsMd', projectStatus.hasAgentsMd, skipFiles);
|
|
2560
|
-
await promptForFileOverwrite(question, 'claudeCommands', projectStatus.hasClaudeCommands, skipFiles);
|
|
2561
2596
|
|
|
2562
2597
|
if (projectStatus.type !== 'fresh') {
|
|
2563
2598
|
console.log('');
|
|
@@ -2580,19 +2615,13 @@ async function _interactiveSetup() {
|
|
|
2580
2615
|
setupCoreDocs();
|
|
2581
2616
|
console.log('');
|
|
2582
2617
|
|
|
2583
|
-
//
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
// First ensure Claude is set up
|
|
2587
|
-
if (selectedAgents.includes('claude')) {
|
|
2588
|
-
await setupAgent('claude', null, skipFiles);
|
|
2589
|
-
}
|
|
2590
|
-
// Then load the commands
|
|
2591
|
-
claudeCommands = loadClaudeCommands(selectedAgents);
|
|
2618
|
+
// Setup Claude first if selected, then setup remaining agents
|
|
2619
|
+
if (selectedAgents.includes('claude')) {
|
|
2620
|
+
await setupAgent('claude', skipFiles);
|
|
2592
2621
|
}
|
|
2593
2622
|
|
|
2594
2623
|
// Setup each selected agent with progress indication
|
|
2595
|
-
await setupAgentsWithProgress(selectedAgents,
|
|
2624
|
+
await setupAgentsWithProgress(selectedAgents, skipFiles);
|
|
2596
2625
|
|
|
2597
2626
|
// =============================================
|
|
2598
2627
|
// STEP 2: Project Tools Setup
|
|
@@ -2615,6 +2644,9 @@ async function _interactiveSetup() {
|
|
|
2615
2644
|
// Final Summary
|
|
2616
2645
|
// =============================================
|
|
2617
2646
|
displaySetupSummary(selectedAgents);
|
|
2647
|
+
|
|
2648
|
+
// One-step onboarding: run `forge init` when config is still absent (ac0b38c7).
|
|
2649
|
+
await finalizeWorkflowConfig();
|
|
2618
2650
|
}
|
|
2619
2651
|
|
|
2620
2652
|
// Parse CLI flags
|
|
@@ -2671,55 +2703,188 @@ async function handleHuskyMigration() {
|
|
|
2671
2703
|
|
|
2672
2704
|
// Install git hooks via lefthook
|
|
2673
2705
|
// SECURITY: Uses execSync with HARDCODED strings only (no user input)
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2706
|
+
// Copy the Forge hook scripts (check-tdd.js + the native-hook adapter) into the
|
|
2707
|
+
// project's .forge/hooks/. Runs UNCONDITIONALLY — independent of the lefthook binary.
|
|
2708
|
+
// The native harness hooks rendered by lib/hook-renderer.js invoke forge-native-hook.js,
|
|
2709
|
+
// which delegates the TDD gate to check-tdd.js, so BOTH must always be installed; if we
|
|
2710
|
+
// only copied them on the lefthook path, a machine without lefthook would get native
|
|
2711
|
+
// hooks pointing at an adapter that was never installed.
|
|
2712
|
+
function installForgeHookScripts() {
|
|
2713
|
+
const targetHooks = path.join(projectRoot, '.forge/hooks');
|
|
2714
|
+
for (const name of ['check-tdd.js', 'forge-native-hook.js']) {
|
|
2715
|
+
const src = path.join(getPackageRoot(packageDir), '.forge/hooks', name);
|
|
2716
|
+
if (!fs.existsSync(src)) continue;
|
|
2717
|
+
if (!fs.existsSync(targetHooks)) fs.mkdirSync(targetHooks, { recursive: true });
|
|
2718
|
+
const dest = path.join(targetHooks, name);
|
|
2719
|
+
if (copyFile(src, dest)) {
|
|
2720
|
+
console.log(` ✓ Created .forge/hooks/${name}`);
|
|
2721
|
+
try {
|
|
2722
|
+
fs.chmodSync(dest, 0o755); // NOSONAR — 755 is intentional: hook scripts must be executable
|
|
2723
|
+
} catch (err) {
|
|
2724
|
+
console.warn('chmod not available (Windows):', err.message);
|
|
2725
|
+
}
|
|
2684
2726
|
}
|
|
2685
|
-
return;
|
|
2686
2727
|
}
|
|
2728
|
+
}
|
|
2687
2729
|
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2730
|
+
// FORGE_USER_LEFTHOOK_YML and forgeShouldWriteLefthookConfig are the single source of
|
|
2731
|
+
// truth in lib/lefthook-wiring.js, shared with bin/forge.js so the live `forge setup`
|
|
2732
|
+
// path and this repair path can never drift again — that drift is exactly what let the
|
|
2733
|
+
// stub-shadow fix (kernel e452422c / c713fce7) live in this module while `forge setup`
|
|
2734
|
+
// kept shipping the broken behaviour. They are imported at the top of this file and
|
|
2735
|
+
// re-exported below for the existing test surface.
|
|
2736
|
+
|
|
2737
|
+
// Honest, resolved-config description of what the installed hooks will actually
|
|
2738
|
+
// enforce (issue eda6d866). The hook scripts are installed unconditionally but are
|
|
2739
|
+
// INERT when their gate/rail is disabled in .forge/config.yaml, so setup must not
|
|
2740
|
+
// claim "enforcement active" when the resolved config says otherwise.
|
|
2741
|
+
// Resolve — from the SAME .forge/config.yaml the hooks read at run time — whether the TDD
|
|
2742
|
+
// gate is active and how many protected paths are guarded. The reporting/exit layer gates
|
|
2743
|
+
// its verdict on this so setup never claims (or fails over) a state the user deliberately
|
|
2744
|
+
// disabled (issue eda6d866). On resolver THROW (missing/corrupt config), fail TOWARD
|
|
2745
|
+
// enforcement (tddActive:true, resolved:false) so a broken config can never silently
|
|
2746
|
+
// downgrade the human-facing verdict.
|
|
2747
|
+
function resolveHookEnforcementState(root = projectRoot) {
|
|
2692
2748
|
try {
|
|
2693
|
-
|
|
2694
|
-
const
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2749
|
+
const { getResolvedRuntimeGraph } = require('../core/runtime-graph');
|
|
2750
|
+
const graph = getResolvedRuntimeGraph({ projectRoot: root });
|
|
2751
|
+
const tddRail = (graph.rails || []).find(rail => rail.id === 'rail.tdd_intent');
|
|
2752
|
+
const tddActive = !tddRail || tddRail.enabled !== false;
|
|
2753
|
+
const protectedCount = Array.isArray(graph.protectedPaths) ? graph.protectedPaths.length : 0;
|
|
2754
|
+
return { tddActive, protectedCount, resolved: true };
|
|
2755
|
+
} catch {
|
|
2756
|
+
return { tddActive: true, protectedCount: 0, resolved: false };
|
|
2757
|
+
}
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
function describeHookEnforcement(root = projectRoot) {
|
|
2761
|
+
const state = resolveHookEnforcementState(root);
|
|
2762
|
+
if (!state.resolved) {
|
|
2763
|
+
return null; // never let an honest-status line block setup (resolver threw)
|
|
2764
|
+
}
|
|
2765
|
+
const tdd = state.tddActive ? 'active' : 'disabled in config (hook inert)';
|
|
2766
|
+
const pp = state.protectedCount > 0
|
|
2767
|
+
? `active (${state.protectedCount} path${state.protectedCount === 1 ? '' : 's'})`
|
|
2768
|
+
: 'disabled in config (hook inert)';
|
|
2769
|
+
return `TDD gate: ${tdd}; protected-path: ${pp}`;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// Pure reporting/exit-layer verdict (issue eda6d866): fold the file-presence hook check
|
|
2773
|
+
// (verifyHooksActive) and the resolved config state (resolveHookEnforcementState) into the
|
|
2774
|
+
// message/level and whether setup should exit non-zero. Kept pure + exported so the four
|
|
2775
|
+
// honesty cases are unit-testable without spawning setup. The caller folds "are we in a git
|
|
2776
|
+
// repo" into `loud`, so a non-git dir (nothing to hook into) only ever warns.
|
|
2777
|
+
function buildHookVerdict(verdict, state, options = {}) {
|
|
2778
|
+
const loud = options.loud === true;
|
|
2779
|
+
const tddActive = !state || state.tddActive !== false;
|
|
2780
|
+
if (verdict.active) {
|
|
2781
|
+
if (tddActive) {
|
|
2782
|
+
return {
|
|
2783
|
+
message: ` ✓ Git hook enforcement active (${verdict.method}).`,
|
|
2784
|
+
level: 'log',
|
|
2785
|
+
exitFailure: false,
|
|
2786
|
+
};
|
|
2699
2787
|
}
|
|
2788
|
+
// Hooks installed but the TDD gate is OFF in config → the scripts are inert by the
|
|
2789
|
+
// user's own choice. Say so honestly instead of over-claiming "enforcement active".
|
|
2790
|
+
return {
|
|
2791
|
+
message: ` ✓ Git hooks installed (${verdict.method}) — TDD gate disabled in .forge/config.yaml, hooks are inert. Re-enable: forge gate enable rail.tdd_intent`,
|
|
2792
|
+
level: 'log',
|
|
2793
|
+
exitFailure: false,
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
if (!tddActive) {
|
|
2797
|
+
// Hooks not active AND the user disabled enforcement — this is the CHOSEN state (e.g.
|
|
2798
|
+
// `init --profile minimal`), so never banner or exit non-zero for it, even under loud.
|
|
2799
|
+
return {
|
|
2800
|
+
message: ' ℹ Git hooks not active — enforcement is disabled in .forge/config.yaml anyway.',
|
|
2801
|
+
level: 'info',
|
|
2802
|
+
exitFailure: false,
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
// TDD is ON but hooks are inert — the silent-inert bug B3. Loud setup in a git repo fails
|
|
2806
|
+
// LOUDLY + non-zero; otherwise (init/repair path, or non-git dir folded into !loud) warn.
|
|
2807
|
+
if (loud) {
|
|
2808
|
+
const addCmd = PKG_MANAGER === 'bun'
|
|
2809
|
+
? 'bun add -d'
|
|
2810
|
+
: PKG_MANAGER === 'npm'
|
|
2811
|
+
? 'npm install --save-dev'
|
|
2812
|
+
: `${PKG_MANAGER} add -D`;
|
|
2813
|
+
const message = [
|
|
2814
|
+
'',
|
|
2815
|
+
' ============================================================',
|
|
2816
|
+
' ⚠ TDD ENFORCEMENT IS NOT ACTIVE',
|
|
2817
|
+
` ${verdict.reason || 'no pre-commit hook is installed'}.`,
|
|
2818
|
+
' `forge ship` will block until hooks are active. To fix:',
|
|
2819
|
+
` ${addCmd} lefthook && npx lefthook install`,
|
|
2820
|
+
' (or re-run `forge setup` in the repo root).',
|
|
2821
|
+
' ============================================================',
|
|
2822
|
+
'',
|
|
2823
|
+
].join('\n');
|
|
2824
|
+
return { message, level: 'error', exitFailure: true };
|
|
2825
|
+
}
|
|
2826
|
+
return {
|
|
2827
|
+
message: ` ⚠ TDD enforcement is NOT active: ${verdict.reason || 'no pre-commit hook installed'}.`,
|
|
2828
|
+
level: 'warn',
|
|
2829
|
+
exitFailure: false,
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2700
2832
|
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2833
|
+
function installGitHooks(options = {}) { // NOSONAR — Extracted as-is from bin/forge.js; complexity reduction deferred
|
|
2834
|
+
// loud=true (the `forge setup` handlers — quickSetup + executeSetup) surfaces an
|
|
2835
|
+
// inert-hooks result as a HARD failure — a banner + non-zero exit — so setup never ends
|
|
2836
|
+
// green with TDD enforcement silently off (B3). loud=false (the default: mid-stage
|
|
2837
|
+
// repairRuntimeReadiness AND `forge init` via ensureGitHooksInstalled) only warns, since
|
|
2838
|
+
// init deliberately degrades to a warning rather than failing, and repair runs inside
|
|
2839
|
+
// another command's flow.
|
|
2840
|
+
const loud = options.loud === true;
|
|
2841
|
+
// Honest header: when the TDD gate is disabled in config the hooks install but are inert,
|
|
2842
|
+
// so don't announce "(TDD enforcement)" for a state the user turned off (issue eda6d866).
|
|
2843
|
+
console.log(
|
|
2844
|
+
resolveHookEnforcementState(projectRoot).tddActive
|
|
2845
|
+
? 'Installing git hooks (TDD enforcement)...'
|
|
2846
|
+
: 'Installing git hooks (TDD gate disabled in config — hooks will be inert)...'
|
|
2847
|
+
);
|
|
2848
|
+
|
|
2849
|
+
// Install the Forge hook SCRIPTS first, unconditionally: they back BOTH the lefthook
|
|
2850
|
+
// pre-commit gate AND the native harness hooks that forge setup renders regardless of
|
|
2851
|
+
// whether the lefthook binary is present.
|
|
2852
|
+
installForgeHookScripts();
|
|
2853
|
+
|
|
2854
|
+
// Report what the hooks will ACTUALLY enforce given the resolved config — a
|
|
2855
|
+
// disabled gate/rail installs an inert hook, so say so instead of over-claiming.
|
|
2856
|
+
const enforcementStatus = describeHookEnforcement();
|
|
2857
|
+
if (enforcementStatus) {
|
|
2858
|
+
console.log(` Enforcement (from .forge/config.yaml): ${enforcementStatus}`);
|
|
2859
|
+
}
|
|
2712
2860
|
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2861
|
+
// Skip lefthook.yml creation if binary is not available
|
|
2862
|
+
const lefthookStatus = checkLefthookStatus(projectRoot);
|
|
2863
|
+
let lefthookInstalled = false;
|
|
2864
|
+
if (!lefthookStatus.binaryAvailable && lefthookStatus.message) {
|
|
2865
|
+
console.warn(` \u26A0 lefthook binary unavailable: ${lefthookStatus.message}`);
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
// Only drive lefthook when its binary is actually present. When it is not, fall
|
|
2869
|
+
// straight through to the native .git/hooks fallback below rather than triggering an
|
|
2870
|
+
// on-demand `npx lefthook` fetch \u2014 that is network-dependent and can hang on Windows,
|
|
2871
|
+
// and it keeps this repair path byte-for-byte consistent with bin/forge.js.
|
|
2872
|
+
if (lefthookStatus.binaryAvailable) {
|
|
2873
|
+
try {
|
|
2874
|
+
// Write the user-facing lefthook.yml (references only .forge/hooks/check-tdd.js and
|
|
2875
|
+
// the project's own tests — never the repo's internal scripts/). Overwrite lefthook's
|
|
2876
|
+
// own disposable stub so Forge's pre-commit/pre-push wiring actually lands; never
|
|
2877
|
+
// clobber a config that already has active jobs (kernel c713fce7).
|
|
2878
|
+
const lefthookTarget = path.join(projectRoot, 'lefthook.yml');
|
|
2879
|
+
if (forgeShouldWriteLefthookConfig(lefthookTarget)) {
|
|
2880
|
+
fs.writeFileSync(lefthookTarget, FORGE_USER_LEFTHOOK_YML, 'utf8');
|
|
2881
|
+
actionLog.add('lefthook.yml', 'created');
|
|
2882
|
+
console.log(' ✓ Created lefthook.yml');
|
|
2721
2883
|
}
|
|
2722
2884
|
|
|
2885
|
+
// (The Forge hook scripts — check-tdd.js + forge-native-hook.js — are installed
|
|
2886
|
+
// unconditionally by installForgeHookScripts() above, before this lefthook path.)
|
|
2887
|
+
|
|
2723
2888
|
// Try to install lefthook hooks
|
|
2724
2889
|
// SECURITY: Using execFileSync with hardcoded commands (no user input)
|
|
2725
2890
|
try {
|
|
@@ -2727,12 +2892,14 @@ function installGitHooks() { // NOSONAR — Extracted as-is from bin/forge.js; c
|
|
|
2727
2892
|
try {
|
|
2728
2893
|
secureExecFileSync('npx', ['lefthook', 'install'], { stdio: 'inherit', cwd: projectRoot });
|
|
2729
2894
|
console.log(' ✓ Lefthook hooks installed (local)');
|
|
2895
|
+
lefthookInstalled = true;
|
|
2730
2896
|
} catch (error_) {
|
|
2731
2897
|
// Fallback to global lefthook
|
|
2732
2898
|
console.warn('npx lefthook failed, trying global:', error_.message);
|
|
2733
|
-
|
|
2734
|
-
|
|
2899
|
+
secureExecFileSync('lefthook', ['version'], { stdio: 'ignore' });
|
|
2900
|
+
secureExecFileSync('lefthook', ['install'], { stdio: 'inherit', cwd: projectRoot });
|
|
2735
2901
|
console.log(' ✓ Lefthook hooks installed (global)');
|
|
2902
|
+
lefthookInstalled = true;
|
|
2736
2903
|
}
|
|
2737
2904
|
} catch (err) {
|
|
2738
2905
|
console.warn('Lefthook installation failed:', err.message);
|
|
@@ -2751,6 +2918,43 @@ function installGitHooks() { // NOSONAR — Extracted as-is from bin/forge.js; c
|
|
|
2751
2918
|
console.log(' You can install manually later with: lefthook install');
|
|
2752
2919
|
console.log('');
|
|
2753
2920
|
}
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// Native fallback: when lefthook could not be installed, wire native .git/hooks so
|
|
2924
|
+
// raw git commit / git push still enforce the TDD gate — enforcement is never silently
|
|
2925
|
+
// inert (B3). This repair path only warns (never sets a failure exit code) since it can
|
|
2926
|
+
// run mid-stage under enforce-stage's repairWorkflowRuntimeAssets().
|
|
2927
|
+
if (!lefthookInstalled) {
|
|
2928
|
+
const native = installNativeGitHooks(projectRoot);
|
|
2929
|
+
if (native.installed) {
|
|
2930
|
+
console.log(` ✓ Native git hooks installed (${native.written.join(', ')}) - lefthook fallback`);
|
|
2931
|
+
} else if (native.skipped && native.skipped.length > 0) {
|
|
2932
|
+
console.warn(` ⚠ Native hook(s) skipped to preserve existing hooks: ${native.skipped.join(', ')}`);
|
|
2933
|
+
} else {
|
|
2934
|
+
console.warn(` ⚠ Could not install native git hooks: ${native.reason || 'unknown'}`);
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
const verdict = verifyHooksActive(projectRoot);
|
|
2939
|
+
const state = resolveHookEnforcementState(projectRoot);
|
|
2940
|
+
// A non-git dir has nothing to hook into, so it must only ever WARN (never banner/exit).
|
|
2941
|
+
// Fold that into `loud` before building the verdict so buildHookVerdict stays pure. When
|
|
2942
|
+
// the TDD gate is disabled in config, the verdict is info-level with no failure exit even
|
|
2943
|
+
// under loud setup — enforcement honestly follows the resolved config (issue eda6d866).
|
|
2944
|
+
const inGitRepo = Boolean(resolveGitHooksDir(projectRoot));
|
|
2945
|
+
const built = buildHookVerdict(verdict, state, { loud: loud && inGitRepo });
|
|
2946
|
+
if (built.level === 'error') {
|
|
2947
|
+
console.error(built.message);
|
|
2948
|
+
} else if (built.level === 'warn') {
|
|
2949
|
+
console.warn(built.message);
|
|
2950
|
+
} else if (built.level === 'info') {
|
|
2951
|
+
console.info(built.message);
|
|
2952
|
+
} else {
|
|
2953
|
+
console.log(built.message);
|
|
2954
|
+
}
|
|
2955
|
+
if (built.exitFailure) {
|
|
2956
|
+
process.exitCode = 1;
|
|
2957
|
+
}
|
|
2754
2958
|
}
|
|
2755
2959
|
|
|
2756
2960
|
// Check if lefthook is already installed in project (delegates to lib/lefthook-check)
|
|
@@ -2795,130 +2999,34 @@ function repairDeclaredLefthookDependency(selectedAgents) {
|
|
|
2795
2999
|
}
|
|
2796
3000
|
}
|
|
2797
3001
|
|
|
2798
|
-
//
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
//
|
|
2802
|
-
|
|
2803
|
-
|
|
3002
|
+
// Ensure the local Forge Kernel issue store exists and is migrated.
|
|
3003
|
+
//
|
|
3004
|
+
// The Kernel is a single-machine SQLite store in the git common dir; building
|
|
3005
|
+
// the migrated deps runs broker.initialize() (idempotent), so the DB + schema
|
|
3006
|
+
// exist before first use. There is no external CLI to install — issue tracking
|
|
3007
|
+
// ships with Forge. Best-effort: never throws, so setup proceeds even if the
|
|
3008
|
+
// SQLite runtime is unavailable (the first kernel command would migrate it
|
|
3009
|
+
// later anyway).
|
|
3010
|
+
async function ensureKernelIssueStore() {
|
|
2804
3011
|
try {
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
// Check if bunx can run it
|
|
2813
|
-
try {
|
|
2814
|
-
secureExecFileSync('bunx', ['@beads/bd', 'version'], { stdio: 'ignore' });
|
|
2815
|
-
return 'bunx';
|
|
2816
|
-
} catch (err) {
|
|
2817
|
-
// Not bunx-capable
|
|
2818
|
-
console.warn('Beads not available via bunx:', err.message);
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
// Check local project installation
|
|
2822
|
-
const pkgPath = path.join(projectRoot, 'package.json');
|
|
2823
|
-
if (!fs.existsSync(pkgPath)) return null;
|
|
2824
|
-
|
|
2825
|
-
try {
|
|
2826
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
2827
|
-
const isInstalled = pkg.devDependencies?.['@beads/bd'] || pkg.dependencies?.['@beads/bd'];
|
|
2828
|
-
return isInstalled ? 'local' : null;
|
|
2829
|
-
} catch (err) {
|
|
2830
|
-
console.warn('Failed to check Beads in package.json:', err.message);
|
|
2831
|
-
return null;
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
// Check if Beads is initialized in project — delegates to lib/beads-setup
|
|
2835
|
-
|
|
2836
|
-
// Check if Beads is initialized in project — delegates to lib/beads-setup
|
|
2837
|
-
function isBeadsInitialized() {
|
|
2838
|
-
return beadsSetupLib.isBeadsInitialized(projectRoot);
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
|
-
// Initialize Beads in the project using the defensive safeBeadsInit wrapper
|
|
2842
|
-
// Handles config/gitignore writes, hook snapshot/restore, and JSONL pre-seeding
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
// Initialize Beads in the project using the defensive safeBeadsInit wrapper
|
|
2846
|
-
// Handles config/gitignore writes, hook snapshot/restore, and JSONL pre-seeding
|
|
2847
|
-
function initializeBeads(installType) {
|
|
2848
|
-
console.log('Initializing Beads in project...');
|
|
2849
|
-
|
|
2850
|
-
// Build the execBdInit function based on installType
|
|
2851
|
-
const execBdInit = (root) => {
|
|
2852
|
-
// SECURITY: execFileSync with hardcoded commands
|
|
2853
|
-
if (installType === 'global') {
|
|
2854
|
-
secureExecFileSync('bd', ['init'], { stdio: 'inherit', cwd: root });
|
|
2855
|
-
} else if (installType === 'bunx') {
|
|
2856
|
-
secureExecFileSync('bunx', ['@beads/bd', 'init'], { stdio: 'inherit', cwd: root });
|
|
2857
|
-
} else if (installType === 'local') {
|
|
2858
|
-
secureExecFileSync('npx', ['bd', 'init'], { stdio: 'inherit', cwd: root });
|
|
2859
|
-
}
|
|
2860
|
-
};
|
|
2861
|
-
|
|
2862
|
-
// Derive prefix from package.json name or directory name
|
|
2863
|
-
let prefix;
|
|
2864
|
-
try {
|
|
2865
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
|
|
2866
|
-
prefix = pkg.name || path.basename(projectRoot);
|
|
2867
|
-
} catch (_e) { // NOSONAR — fallback to directory name if package.json unreadable
|
|
2868
|
-
prefix = path.basename(projectRoot);
|
|
2869
|
-
}
|
|
2870
|
-
|
|
2871
|
-
try {
|
|
2872
|
-
const result = beadsSetupLib.safeBeadsInit(projectRoot, {
|
|
2873
|
-
prefix,
|
|
2874
|
-
execBdInit,
|
|
2875
|
-
restoreLefthook: (root) => {
|
|
2876
|
-
try {
|
|
2877
|
-
secureExecFileSync('lefthook', ['install'], { stdio: 'ignore', cwd: root });
|
|
2878
|
-
} catch (_e) { // NOSONAR — lefthook may not be installed yet, non-fatal
|
|
2879
|
-
// lefthook may not be installed yet — non-fatal
|
|
2880
|
-
}
|
|
2881
|
-
}
|
|
2882
|
-
});
|
|
2883
|
-
|
|
2884
|
-
if (result.skipped) {
|
|
2885
|
-
console.log(' ✓ Beads already initialized');
|
|
2886
|
-
return true;
|
|
2887
|
-
}
|
|
2888
|
-
|
|
2889
|
-
if (!result.success) {
|
|
2890
|
-
for (const e of result.errors) {
|
|
2891
|
-
console.log(` ⚠ ${e}`);
|
|
2892
|
-
}
|
|
2893
|
-
console.log(' Run manually: bd init');
|
|
2894
|
-
return false;
|
|
2895
|
-
}
|
|
2896
|
-
|
|
2897
|
-
for (const w of result.warnings) {
|
|
2898
|
-
console.warn(` ⚠ ${w}`);
|
|
2899
|
-
}
|
|
2900
|
-
console.log(' ✓ Beads initialized');
|
|
2901
|
-
|
|
2902
|
-
// Run post-init health check (non-fatal)
|
|
2903
|
-
try {
|
|
2904
|
-
const health = beadsHealthCheck(projectRoot);
|
|
2905
|
-
if (health.healthy) {
|
|
2906
|
-
console.log(' ✓ Beads health check passed');
|
|
2907
|
-
} else {
|
|
2908
|
-
console.log(` ⚠ Beads health check failed at ${health.failedStep}: ${health.error}`);
|
|
3012
|
+
const deps = await buildMigratedKernelIssueDeps({ projectRoot });
|
|
3013
|
+
const handle = deps.kernelBroker || deps.kernelDriver;
|
|
3014
|
+
if (handle && typeof handle.close === 'function') {
|
|
3015
|
+
try {
|
|
3016
|
+
await handle.close();
|
|
3017
|
+
} catch (_closeErr) { // NOSONAR — best-effort cleanup of the migration handle
|
|
3018
|
+
// Closing the migration handle is best-effort; the process is short-lived.
|
|
2909
3019
|
}
|
|
2910
|
-
} catch (_healthErr) { // NOSONAR — health check is best-effort, non-fatal
|
|
2911
|
-
// Health check is best-effort — don't block setup
|
|
2912
3020
|
}
|
|
2913
|
-
|
|
3021
|
+
KERNEL_STORE_READY = true;
|
|
2914
3022
|
return true;
|
|
2915
3023
|
} catch (err) {
|
|
2916
|
-
|
|
2917
|
-
console.log(
|
|
3024
|
+
KERNEL_STORE_READY = false;
|
|
3025
|
+
console.log(` ⚠ Could not provision the Kernel issue store: ${err.message}`);
|
|
3026
|
+
addSetupNote(`Kernel issue store not provisioned: ${err.message}. Run \`forge doctor\` to inspect.`);
|
|
2918
3027
|
return false;
|
|
2919
3028
|
}
|
|
2920
3029
|
}
|
|
2921
|
-
|
|
2922
3030
|
// Check if Skills CLI is installed
|
|
2923
3031
|
|
|
2924
3032
|
|
|
@@ -2984,59 +3092,18 @@ function initializeSkills(installType) {
|
|
|
2984
3092
|
}
|
|
2985
3093
|
}
|
|
2986
3094
|
|
|
2987
|
-
//
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
// Prompt for Beads setup - extracted to reduce cognitive complexity
|
|
2991
|
-
async function promptBeadsSetup(question) {
|
|
3095
|
+
// Ensure the issue store during interactive setup. No prompt: the Forge Kernel
|
|
3096
|
+
// ships with Forge and auto-provisions, so there is nothing to install or pick.
|
|
3097
|
+
async function promptBeadsSetup(_question) {
|
|
2992
3098
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2993
|
-
console.log('
|
|
3099
|
+
console.log('Issue Store (Forge Kernel)');
|
|
2994
3100
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2995
3101
|
console.log('');
|
|
2996
3102
|
|
|
2997
|
-
const
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
if (beadsInitialized) {
|
|
3001
|
-
console.log('✓ Beads is already initialized in this project');
|
|
3002
|
-
console.log('');
|
|
3003
|
-
return;
|
|
3004
|
-
}
|
|
3005
|
-
|
|
3006
|
-
if (beadsStatus) {
|
|
3007
|
-
// Already installed, just need to initialize
|
|
3008
|
-
console.log(`ℹ Beads is installed (${beadsStatus}), but not initialized`);
|
|
3009
|
-
const initBeads = await question('Initialize Beads in this project? (y/n): ');
|
|
3010
|
-
|
|
3011
|
-
if (initBeads.toLowerCase() === 'y') {
|
|
3012
|
-
initializeBeads(beadsStatus);
|
|
3013
|
-
} else {
|
|
3014
|
-
console.log('Skipped Beads initialization. Run manually: bd init');
|
|
3015
|
-
}
|
|
3016
|
-
console.log('');
|
|
3017
|
-
return;
|
|
3103
|
+
const ready = await ensureKernelIssueStore();
|
|
3104
|
+
if (ready) {
|
|
3105
|
+
console.log('✓ Kernel issue store ready (single-machine; team sync not configured)');
|
|
3018
3106
|
}
|
|
3019
|
-
|
|
3020
|
-
// Not installed
|
|
3021
|
-
console.log('ℹ Beads is not installed');
|
|
3022
|
-
const installBeads = await question('Install Beads? (y/n): ');
|
|
3023
|
-
|
|
3024
|
-
if (installBeads.toLowerCase() !== 'y') {
|
|
3025
|
-
console.log('Skipped Beads installation');
|
|
3026
|
-
console.log('');
|
|
3027
|
-
return;
|
|
3028
|
-
}
|
|
3029
|
-
|
|
3030
|
-
console.log('');
|
|
3031
|
-
console.log('Choose installation method:');
|
|
3032
|
-
console.log(' 1. Global (recommended) - Available system-wide');
|
|
3033
|
-
console.log(' 2. Local - Project-specific devDependency');
|
|
3034
|
-
console.log(' 3. Bunx - Use via bunx (requires bun)');
|
|
3035
|
-
console.log('');
|
|
3036
|
-
const method = await question('Choose method (1-3): ');
|
|
3037
|
-
|
|
3038
|
-
console.log('');
|
|
3039
|
-
installBeadsWithMethod(method);
|
|
3040
3107
|
console.log('');
|
|
3041
3108
|
}
|
|
3042
3109
|
|
|
@@ -3056,72 +3123,6 @@ function installViaBunx(packageName, versionArgs, initFn, toolName) {
|
|
|
3056
3123
|
}
|
|
3057
3124
|
}
|
|
3058
3125
|
|
|
3059
|
-
// Helper: Install Beads with chosen method - extracted to reduce cognitive complexity
|
|
3060
|
-
// SECURITY NOTE: Downloads and executes a remote PowerShell script.
|
|
3061
|
-
// The npm @beads/bd package is broken on Windows (GitHub Issue #1031, closed "not planned"),
|
|
3062
|
-
// so the official PowerShell installer is the only supported path.
|
|
3063
|
-
// Mitigations: HTTPS transport (prevents MITM), official beads repo, user-visible URL.
|
|
3064
|
-
// Follow-up: pin to a versioned release tag once beads publishes tagged releases (for example v0.49.1).
|
|
3065
|
-
const BEADS_INSTALL_PS1_URL = 'https://raw.githubusercontent.com/steveyegge/beads/main/install.ps1';
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
function installBeadsOnWindows() {
|
|
3070
|
-
console.log(' (Windows detected: using PowerShell installer)');
|
|
3071
|
-
console.log(` Downloading: ${BEADS_INSTALL_PS1_URL}`);
|
|
3072
|
-
secureExecFileSync('powershell.exe', [
|
|
3073
|
-
'-NoProfile', '-NonInteractive', '-Command',
|
|
3074
|
-
`irm ${BEADS_INSTALL_PS1_URL} | iex`
|
|
3075
|
-
], { stdio: 'inherit' });
|
|
3076
|
-
}
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
function installBeadsWithMethod(method) { // NOSONAR — Extracted as-is from bin/forge.js; complexity reduction deferred
|
|
3081
|
-
try {
|
|
3082
|
-
// SECURITY: secureExecFileSync with hardcoded commands
|
|
3083
|
-
if (method === '1') {
|
|
3084
|
-
console.log('Installing Beads globally...');
|
|
3085
|
-
if (process.platform === 'win32') {
|
|
3086
|
-
installBeadsOnWindows();
|
|
3087
|
-
} else {
|
|
3088
|
-
const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
|
|
3089
|
-
secureExecFileSync(pkgManager, ['install', '-g', '@beads/bd'], { stdio: 'inherit' });
|
|
3090
|
-
}
|
|
3091
|
-
console.log(' ✓ Beads installed globally');
|
|
3092
|
-
initializeBeads('global');
|
|
3093
|
-
} else if (method === '2') {
|
|
3094
|
-
console.log('Installing Beads locally...');
|
|
3095
|
-
// On Windows, npm postinstall for @beads/bd runs Expand-Archive which has EPERM file-locking
|
|
3096
|
-
// (GitHub Issue #1031, closed "not planned") — same root cause as global install.
|
|
3097
|
-
// Redirect Windows users to the global PowerShell installer instead.
|
|
3098
|
-
if (process.platform === 'win32') {
|
|
3099
|
-
console.log(' ⚠ Local install not supported on Windows (npm @beads/bd EPERM issue).');
|
|
3100
|
-
console.log(' Falling back to global PowerShell installer...');
|
|
3101
|
-
installBeadsOnWindows();
|
|
3102
|
-
} else {
|
|
3103
|
-
const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
|
|
3104
|
-
secureExecFileSync(pkgManager, ['install', '-D', '@beads/bd'], { stdio: 'inherit', cwd: projectRoot });
|
|
3105
|
-
}
|
|
3106
|
-
console.log(' ✓ Beads installed');
|
|
3107
|
-
// On Windows the fallback was global (PowerShell installer), so init as 'global'
|
|
3108
|
-
initializeBeads(process.platform === 'win32' ? 'global' : 'local');
|
|
3109
|
-
} else if (method === '3') {
|
|
3110
|
-
installViaBunx('@beads/bd', ['version'], initializeBeads, 'Beads');
|
|
3111
|
-
} else {
|
|
3112
|
-
console.log('Invalid choice. Skipping Beads installation.');
|
|
3113
|
-
}
|
|
3114
|
-
} catch (err) {
|
|
3115
|
-
console.warn('Beads installation failed:', err.message);
|
|
3116
|
-
console.log(' ⚠ Failed to install Beads:', err.message);
|
|
3117
|
-
if (process.platform === 'win32') {
|
|
3118
|
-
console.log(` Run manually: irm ${BEADS_INSTALL_PS1_URL} | iex`);
|
|
3119
|
-
} else {
|
|
3120
|
-
console.log(` Run manually: ${PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g'} @beads/bd && bd init`);
|
|
3121
|
-
}
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
3126
|
// Helper: Get package-manager-specific install args for Skills
|
|
3126
3127
|
|
|
3127
3128
|
|
|
@@ -3231,7 +3232,7 @@ async function setupProjectTools(rl, question) {
|
|
|
3231
3232
|
console.log('');
|
|
3232
3233
|
console.log('Forge recommends three tools for enhanced workflows:');
|
|
3233
3234
|
console.log('');
|
|
3234
|
-
console.log('•
|
|
3235
|
+
console.log('• Issue tracking (Forge Kernel) - zero-install, git-backed');
|
|
3235
3236
|
console.log(' Persists tasks across sessions, tracks dependencies.');
|
|
3236
3237
|
console.log(' Command: forge ready, forge create, forge close');
|
|
3237
3238
|
console.log('');
|
|
@@ -3245,44 +3246,6 @@ async function setupProjectTools(rl, question) {
|
|
|
3245
3246
|
await promptSkillsSetup(question);
|
|
3246
3247
|
}
|
|
3247
3248
|
|
|
3248
|
-
// Auto-setup Beads in quick mode - extracted to reduce cognitive complexity
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
// Auto-setup Beads in quick mode - extracted to reduce cognitive complexity
|
|
3252
|
-
function autoSetupBeadsInQuickMode() { // NOSONAR — Extracted as-is from bin/forge.js; complexity reduction deferred
|
|
3253
|
-
const beadsStatus = checkForBeads();
|
|
3254
|
-
const beadsInitialized = isBeadsInitialized();
|
|
3255
|
-
|
|
3256
|
-
if (!beadsInitialized && beadsStatus) {
|
|
3257
|
-
console.log('📦 Initializing Beads...');
|
|
3258
|
-
initializeBeads(beadsStatus);
|
|
3259
|
-
console.log('');
|
|
3260
|
-
} else if (!beadsInitialized && !beadsStatus) {
|
|
3261
|
-
console.log('📦 Installing Beads globally...');
|
|
3262
|
-
try {
|
|
3263
|
-
// SECURITY: use PowerShell on Windows (npm @beads/bd is broken on Windows - Issue #1031)
|
|
3264
|
-
if (process.platform === 'win32') {
|
|
3265
|
-
installBeadsOnWindows();
|
|
3266
|
-
} else {
|
|
3267
|
-
const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
|
|
3268
|
-
secureExecFileSync(pkgManager, ['install', '-g', '@beads/bd'], { stdio: 'inherit' });
|
|
3269
|
-
}
|
|
3270
|
-
console.log(' ✓ Beads installed globally');
|
|
3271
|
-
initializeBeads('global');
|
|
3272
|
-
} catch (err) {
|
|
3273
|
-
// Installation failed - provide manual instructions
|
|
3274
|
-
console.log(' ⚠ Could not install Beads automatically');
|
|
3275
|
-
console.log(` Error: ${err.message}`);
|
|
3276
|
-
if (process.platform === 'win32') {
|
|
3277
|
-
console.log(` Run manually: irm ${BEADS_INSTALL_PS1_URL} | iex`);
|
|
3278
|
-
} else {
|
|
3279
|
-
console.log(` Run manually: ${PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g'} @beads/bd && bd init`);
|
|
3280
|
-
}
|
|
3281
|
-
}
|
|
3282
|
-
console.log('');
|
|
3283
|
-
}
|
|
3284
|
-
}
|
|
3285
|
-
|
|
3286
3249
|
// Helper: Auto-install lefthook if not present - extracted to reduce cognitive complexity
|
|
3287
3250
|
|
|
3288
3251
|
|
|
@@ -3326,6 +3289,16 @@ function autoInstallLefthook() { // NOSONAR — Extracted as-is from bin/forge.j
|
|
|
3326
3289
|
return;
|
|
3327
3290
|
}
|
|
3328
3291
|
|
|
3292
|
+
// Not in package.json at all. GUARD (kernel 22e33dbf): with no package.json in
|
|
3293
|
+
// projectRoot, `<pkg-mgr> install lefthook` resolves against the nearest ANCESTOR
|
|
3294
|
+
// package.json and installs into the WRONG project (or fails). Skip it — installGitHooks
|
|
3295
|
+
// then wires native .git/hooks, which need no package.json, so enforcement is still live.
|
|
3296
|
+
if (!fs.existsSync(path.join(projectRoot, 'package.json'))) {
|
|
3297
|
+
console.log(' ℹ No package.json here — skipping lefthook install (would target an ancestor); native hooks will back enforcement.');
|
|
3298
|
+
console.log('');
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3329
3302
|
// Not in package.json at all — full install
|
|
3330
3303
|
console.log('📦 Installing lefthook for git hooks...');
|
|
3331
3304
|
try {
|
|
@@ -3341,32 +3314,13 @@ function autoInstallLefthook() { // NOSONAR — Extracted as-is from bin/forge.j
|
|
|
3341
3314
|
console.log('');
|
|
3342
3315
|
}
|
|
3343
3316
|
|
|
3344
|
-
// Helper: Verify a tool is callable after install - extracted to reduce cognitive complexity
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
// Helper: Verify a tool is callable after install - extracted to reduce cognitive complexity
|
|
3348
|
-
function verifyToolInstall(command, args, toolName) {
|
|
3349
|
-
try {
|
|
3350
|
-
secureExecFileSync(command, args, { stdio: 'ignore' });
|
|
3351
|
-
return true;
|
|
3352
|
-
} catch (_err) { // NOSONAR - S2486: Intentionally ignored; verification failure is handled by caller
|
|
3353
|
-
console.log(` ⚠ ${toolName} installed but not callable. Check your PATH.`);
|
|
3354
|
-
return false;
|
|
3355
|
-
}
|
|
3356
|
-
}
|
|
3357
|
-
|
|
3358
3317
|
// Helper: Auto-setup tools (Skills) in quick mode - extracted to reduce cognitive complexity
|
|
3359
3318
|
|
|
3360
3319
|
|
|
3361
3320
|
// Helper: Auto-setup tools (Skills) in quick mode - extracted to reduce cognitive complexity
|
|
3362
|
-
function autoSetupToolsInQuickMode() {
|
|
3363
|
-
//
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
// Post-install verification for Beads
|
|
3367
|
-
if (isBeadsInitialized()) {
|
|
3368
|
-
verifyToolInstall('bd', ['version'], 'Beads');
|
|
3369
|
-
}
|
|
3321
|
+
async function autoSetupToolsInQuickMode() {
|
|
3322
|
+
// Issue store: ensure the Forge Kernel store exists (auto-provisioned, no CLI)
|
|
3323
|
+
await ensureKernelIssueStore();
|
|
3370
3324
|
|
|
3371
3325
|
// Skills: only initialize if already installed (recommended tool)
|
|
3372
3326
|
const skillsStatus = checkForSkills();
|
|
@@ -3374,11 +3328,10 @@ function autoSetupToolsInQuickMode() {
|
|
|
3374
3328
|
console.log('📦 Initializing Skills...');
|
|
3375
3329
|
initializeSkills(skillsStatus);
|
|
3376
3330
|
console.log('');
|
|
3377
|
-
} else if (!skillsStatus) {
|
|
3378
|
-
const installCmd = PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g';
|
|
3379
|
-
console.log(` ℹ Skills not found — install with: ${installCmd} @forge/skills`);
|
|
3380
|
-
console.log('');
|
|
3381
3331
|
}
|
|
3332
|
+
// No hint when the optional Skills CLI is absent: setup bundles and renders
|
|
3333
|
+
// Forge's skills itself, and the previously advertised "@forge/skills"
|
|
3334
|
+
// package does not exist on npm (kernel issue 6e554b41).
|
|
3382
3335
|
}
|
|
3383
3336
|
|
|
3384
3337
|
// Helper: Configure default external services in quick mode - extracted to reduce cognitive complexity
|
|
@@ -3410,6 +3363,35 @@ function configureDefaultExternalServices(skipExternal) {
|
|
|
3410
3363
|
console.log('Configuration saved to .env.local');
|
|
3411
3364
|
}
|
|
3412
3365
|
|
|
3366
|
+
// Install git hooks for a target project root without a full setup run.
|
|
3367
|
+
// Reuses the same lefthook install path setup performs so `forge init` can
|
|
3368
|
+
// reach a hook-active state (closing the init → HOOKS_NOT_ACTIVE catch-22).
|
|
3369
|
+
// Skips silently when there is no package.json to attach hooks to — keeps
|
|
3370
|
+
// `forge init` lightweight for bare/non-node repos and avoids surprise installs.
|
|
3371
|
+
// Restores mutated module state afterward.
|
|
3372
|
+
async function ensureGitHooksInstalled(targetRoot = projectRoot) {
|
|
3373
|
+
// No early bail on a missing package.json (kernel 22e33dbf / B3): the native
|
|
3374
|
+
// .git/hooks fallback needs no package.json, so `forge init` on a bare repo must
|
|
3375
|
+
// still wire enforcement instead of silently skipping it. autoInstallLefthook
|
|
3376
|
+
// guards the package-manager install itself for the no-package.json case.
|
|
3377
|
+
const previousRoot = projectRoot;
|
|
3378
|
+
const previousInteractive = NON_INTERACTIVE;
|
|
3379
|
+
const previousPkgManager = PKG_MANAGER;
|
|
3380
|
+
projectRoot = targetRoot;
|
|
3381
|
+
NON_INTERACTIVE = true;
|
|
3382
|
+
PKG_MANAGER = detectPackageManager();
|
|
3383
|
+
try {
|
|
3384
|
+
autoInstallLefthook();
|
|
3385
|
+
await handleHuskyMigration();
|
|
3386
|
+
installGitHooks();
|
|
3387
|
+
return { installed: true };
|
|
3388
|
+
} finally {
|
|
3389
|
+
projectRoot = previousRoot;
|
|
3390
|
+
NON_INTERACTIVE = previousInteractive;
|
|
3391
|
+
PKG_MANAGER = previousPkgManager;
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3413
3395
|
// Quick setup with defaults
|
|
3414
3396
|
|
|
3415
3397
|
|
|
@@ -3428,8 +3410,11 @@ async function quickSetup(selectedAgents, skipExternal) {
|
|
|
3428
3410
|
});
|
|
3429
3411
|
console.log('');
|
|
3430
3412
|
|
|
3431
|
-
// Copy AGENTS.md (actionLog tracks it via copyFile)
|
|
3432
|
-
|
|
3413
|
+
// Copy AGENTS.md (actionLog tracks it via copyFile). Quick mode is
|
|
3414
|
+
// non-interactive and overwrites unconditionally, so back up a markerless
|
|
3415
|
+
// (pre-Forge) AGENTS.md first to avoid silent data loss (kernel issue a5399f3d).
|
|
3416
|
+
const agentsSrc = path.join(getPackageRoot(packageDir), 'AGENTS.md');
|
|
3417
|
+
backupMarkerlessAgentsMd();
|
|
3433
3418
|
copyFile(agentsSrc, 'AGENTS.md');
|
|
3434
3419
|
console.log('');
|
|
3435
3420
|
|
|
@@ -3442,20 +3427,23 @@ async function quickSetup(selectedAgents, skipExternal) {
|
|
|
3442
3427
|
// Auto-install lefthook if missing
|
|
3443
3428
|
autoInstallLefthook();
|
|
3444
3429
|
|
|
3445
|
-
// Auto-setup project tools (
|
|
3446
|
-
autoSetupToolsInQuickMode();
|
|
3430
|
+
// Auto-setup project tools (Kernel issue store, Skills)
|
|
3431
|
+
await autoSetupToolsInQuickMode();
|
|
3447
3432
|
|
|
3448
|
-
//
|
|
3449
|
-
|
|
3450
|
-
|
|
3433
|
+
// Setup Claude first if selected, then setup remaining agents
|
|
3434
|
+
if (selectedAgents.includes('claude')) {
|
|
3435
|
+
await setupAgent('claude');
|
|
3436
|
+
}
|
|
3437
|
+
await setupSelectedAgents(selectedAgents);
|
|
3451
3438
|
ensureWorkflowRuntimeAssets(selectedAgents);
|
|
3452
3439
|
|
|
3453
3440
|
// Detect Husky and migrate before installing Lefthook hooks
|
|
3454
3441
|
await handleHuskyMigration();
|
|
3455
3442
|
|
|
3456
|
-
// Install git hooks for TDD enforcement
|
|
3443
|
+
// Install git hooks for TDD enforcement. LOUD: this is the `forge setup` handler,
|
|
3444
|
+
// so an inert-hooks result must fail non-zero (B3), not end green.
|
|
3457
3445
|
console.log('');
|
|
3458
|
-
installGitHooks();
|
|
3446
|
+
installGitHooks({ loud: true });
|
|
3459
3447
|
|
|
3460
3448
|
// Configure external services with defaults (unless skipped)
|
|
3461
3449
|
configureDefaultExternalServices(skipExternal);
|
|
@@ -3469,6 +3457,9 @@ async function quickSetup(selectedAgents, skipExternal) {
|
|
|
3469
3457
|
console.log('');
|
|
3470
3458
|
console.log(renderSetupSummary(actionLog, selectedAgents, VERBOSE_MODE, { status: getSetupSummaryStatus() }));
|
|
3471
3459
|
printSetupNotes();
|
|
3460
|
+
// One-step onboarding: run `forge init` when config is still absent (ac0b38c7).
|
|
3461
|
+
// Hooks + Beads already handled above, so init skips those side effects.
|
|
3462
|
+
await finalizeWorkflowConfig({ hooksAlreadyInstalled: true });
|
|
3472
3463
|
console.log('');
|
|
3473
3464
|
}
|
|
3474
3465
|
|
|
@@ -3509,7 +3500,7 @@ function setupAgentsMdFile(flags, skipFiles) {
|
|
|
3509
3500
|
return;
|
|
3510
3501
|
}
|
|
3511
3502
|
|
|
3512
|
-
const agentsSrc = path.join(packageDir, 'AGENTS.md');
|
|
3503
|
+
const agentsSrc = path.join(getPackageRoot(packageDir), 'AGENTS.md');
|
|
3513
3504
|
const agentsDest = path.join(projectRoot, 'AGENTS.md');
|
|
3514
3505
|
const mergeStrategy = flags.merge || 'smart';
|
|
3515
3506
|
|
|
@@ -3589,7 +3580,6 @@ function displayExistingInstallation(projectStatus) {
|
|
|
3589
3580
|
: 'Found partial installation:');
|
|
3590
3581
|
|
|
3591
3582
|
if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
|
|
3592
|
-
if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
|
|
3593
3583
|
if (projectStatus.hasEnvLocal) console.log(' - .env.local');
|
|
3594
3584
|
console.log('');
|
|
3595
3585
|
}
|
|
@@ -3601,7 +3591,6 @@ function displayExistingInstallation(projectStatus) {
|
|
|
3601
3591
|
async function promptForOverwriteDecisions(question, projectStatus, flags = {}) {
|
|
3602
3592
|
const skipFiles = {
|
|
3603
3593
|
agentsMd: false,
|
|
3604
|
-
claudeCommands: false
|
|
3605
3594
|
};
|
|
3606
3595
|
|
|
3607
3596
|
if (flags.keep) {
|
|
@@ -3609,10 +3598,6 @@ async function promptForOverwriteDecisions(question, projectStatus, flags = {})
|
|
|
3609
3598
|
skipFiles.agentsMd = true;
|
|
3610
3599
|
console.log(' Keeping existing AGENTS.md (--keep)');
|
|
3611
3600
|
}
|
|
3612
|
-
if (projectStatus.hasClaudeCommands) {
|
|
3613
|
-
skipFiles.claudeCommands = true;
|
|
3614
|
-
console.log(' Keeping existing .claude/commands/ (--keep)');
|
|
3615
|
-
}
|
|
3616
3601
|
return skipFiles;
|
|
3617
3602
|
}
|
|
3618
3603
|
|
|
@@ -3622,12 +3607,6 @@ async function promptForOverwriteDecisions(question, projectStatus, flags = {})
|
|
|
3622
3607
|
console.log(overwriteAgents ? ' Will overwrite AGENTS.md' : ' Keeping existing AGENTS.md');
|
|
3623
3608
|
}
|
|
3624
3609
|
|
|
3625
|
-
if (projectStatus.hasClaudeCommands) {
|
|
3626
|
-
const overwriteCommands = await askYesNo(question, 'Found existing .claude/commands/. Overwrite?', true);
|
|
3627
|
-
skipFiles.claudeCommands = !overwriteCommands;
|
|
3628
|
-
console.log(overwriteCommands ? ' Will overwrite .claude/commands/' : ' Keeping existing .claude/commands/');
|
|
3629
|
-
}
|
|
3630
|
-
|
|
3631
3610
|
if (projectStatus.type !== 'fresh') {
|
|
3632
3611
|
console.log('');
|
|
3633
3612
|
}
|
|
@@ -3635,47 +3614,17 @@ async function promptForOverwriteDecisions(question, projectStatus, flags = {})
|
|
|
3635
3614
|
return skipFiles;
|
|
3636
3615
|
}
|
|
3637
3616
|
|
|
3638
|
-
// Helper: Load and setup canonical commands - extracted to reduce cognitive complexity
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
// Helper: Load and setup canonical commands - extracted to reduce cognitive complexity
|
|
3642
|
-
async function loadAndSetupCanonicalCommands(selectedAgents, skipFiles) {
|
|
3643
|
-
const claudeCommands = {};
|
|
3644
|
-
const needsClaudeCommands = selectedAgents.includes('claude') ||
|
|
3645
|
-
selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands);
|
|
3646
|
-
|
|
3647
|
-
if (!needsClaudeCommands) {
|
|
3648
|
-
return claudeCommands;
|
|
3649
|
-
}
|
|
3650
|
-
|
|
3651
|
-
// First ensure Claude is set up
|
|
3652
|
-
if (selectedAgents.includes('claude')) {
|
|
3653
|
-
await setupAgent('claude', null, skipFiles);
|
|
3654
|
-
}
|
|
3655
|
-
|
|
3656
|
-
// Then load the commands (from existing or newly created)
|
|
3657
|
-
getWorkflowCommands().forEach(cmd => {
|
|
3658
|
-
const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
|
|
3659
|
-
const content = readFile(cmdPath);
|
|
3660
|
-
if (content) {
|
|
3661
|
-
claudeCommands[`${cmd}.md`] = content;
|
|
3662
|
-
}
|
|
3663
|
-
});
|
|
3664
|
-
|
|
3665
|
-
return claudeCommands;
|
|
3666
|
-
}
|
|
3667
|
-
|
|
3668
3617
|
// Helper: Setup all selected agents - extracted to reduce cognitive complexity
|
|
3669
3618
|
|
|
3670
3619
|
|
|
3671
3620
|
// Helper: Setup all selected agents - extracted to reduce cognitive complexity
|
|
3672
|
-
async function setupSelectedAgents(selectedAgents,
|
|
3621
|
+
async function setupSelectedAgents(selectedAgents, skipFiles) {
|
|
3673
3622
|
const totalAgents = selectedAgents.length;
|
|
3674
3623
|
for (const [index, agentKey] of selectedAgents.entries()) {
|
|
3675
3624
|
const agent = AGENTS[agentKey];
|
|
3676
3625
|
console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
|
|
3677
3626
|
if (agentKey !== 'claude') { // Claude already done above
|
|
3678
|
-
await setupAgent(agentKey,
|
|
3627
|
+
await setupAgent(agentKey, skipFiles);
|
|
3679
3628
|
}
|
|
3680
3629
|
}
|
|
3681
3630
|
|
|
@@ -3775,7 +3724,7 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3775
3724
|
const selectedAgents = await promptForAgentSelection(question, agentKeys);
|
|
3776
3725
|
|
|
3777
3726
|
// Check GitHub CLI prerequisite now that selectedAgents is known
|
|
3778
|
-
if (requiresGithubCliForSetup(selectedAgents
|
|
3727
|
+
if (requiresGithubCliForSetup(selectedAgents)) {
|
|
3779
3728
|
checkPrerequisites({ requireGithubCli: true });
|
|
3780
3729
|
}
|
|
3781
3730
|
|
|
@@ -3790,11 +3739,13 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3790
3739
|
setupCoreDocs();
|
|
3791
3740
|
console.log('');
|
|
3792
3741
|
|
|
3793
|
-
//
|
|
3794
|
-
|
|
3742
|
+
// Setup Claude first if selected (delegated to helper), then remaining agents
|
|
3743
|
+
if (selectedAgents.includes('claude')) {
|
|
3744
|
+
await setupAgent('claude', skipFiles);
|
|
3745
|
+
}
|
|
3795
3746
|
|
|
3796
3747
|
// Setup each selected agent with progress indication (delegated to helper)
|
|
3797
|
-
await setupSelectedAgents(selectedAgents,
|
|
3748
|
+
await setupSelectedAgents(selectedAgents, skipFiles);
|
|
3798
3749
|
ensureWorkflowRuntimeAssets(selectedAgents);
|
|
3799
3750
|
|
|
3800
3751
|
// Handle external services step (delegated to helper)
|
|
@@ -3805,6 +3756,10 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3805
3756
|
|
|
3806
3757
|
// Display final summary (delegated to helper)
|
|
3807
3758
|
displaySetupSummary(selectedAgents);
|
|
3759
|
+
|
|
3760
|
+
// One-step onboarding: this path never writes .forge/config.yaml itself, so
|
|
3761
|
+
// run `forge init` when the config is still absent (ac0b38c7).
|
|
3762
|
+
await finalizeWorkflowConfig();
|
|
3808
3763
|
}
|
|
3809
3764
|
|
|
3810
3765
|
// Main
|
|
@@ -3903,15 +3858,10 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3903
3858
|
addFileAction(dir + '/', 'Create agent directory');
|
|
3904
3859
|
}
|
|
3905
3860
|
|
|
3906
|
-
// Claude-specific files
|
|
3861
|
+
// Claude-specific files (skills are listed by the per-skill block below).
|
|
3862
|
+
// Claude gets policy via CLAUDE.md → AGENTS.md, not always-on .claude/rules/* files.
|
|
3907
3863
|
if (agentKey === 'claude') {
|
|
3908
|
-
const cmds = getWorkflowCommands();
|
|
3909
|
-
for (const cmd of cmds) {
|
|
3910
|
-
addFileAction(`.claude/commands/${cmd}.md`, 'Workflow command');
|
|
3911
|
-
}
|
|
3912
|
-
addFileAction('.claude/rules/workflow.md', 'Workflow rules');
|
|
3913
3864
|
addFileAction('.claude/scripts/load-env.sh', 'Environment loader script');
|
|
3914
|
-
addFileAction('.claude/skills/forge-workflow/SKILL.md', 'Forge workflow skill');
|
|
3915
3865
|
addFileAction('.mcp.json', 'MCP server configuration');
|
|
3916
3866
|
addFileAction('CLAUDE.md', 'Claude root config (links to AGENTS.md)');
|
|
3917
3867
|
}
|
|
@@ -3922,7 +3872,7 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3922
3872
|
}
|
|
3923
3873
|
}
|
|
3924
3874
|
|
|
3925
|
-
// Cursor-specific files
|
|
3875
|
+
// Cursor-specific files (rendered from the canonical rules/ source)
|
|
3926
3876
|
if (agent.customSetup === 'cursor') {
|
|
3927
3877
|
addFileAction('.cursor/rules/forge-workflow.mdc', 'Cursor workflow rule');
|
|
3928
3878
|
addFileAction('.cursor/rules/tdd-enforcement.mdc', 'Cursor TDD rule');
|
|
@@ -3930,54 +3880,19 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3930
3880
|
addFileAction('.cursor/rules/documentation.mdc', 'Cursor documentation rule');
|
|
3931
3881
|
}
|
|
3932
3882
|
|
|
3933
|
-
if (agentKey === 'kilocode') {
|
|
3934
|
-
addFileAction('.kilocode/workflows/forge-workflow.md', 'Kilo native workflow');
|
|
3935
|
-
addFileAction('.kilocode/rules/workflow.md', 'Kilo native rules');
|
|
3936
|
-
addFileAction('.kilocode/skills/forge-workflow/SKILL.md', 'Kilo native skill');
|
|
3937
|
-
}
|
|
3938
|
-
|
|
3939
|
-
if (agent.customSetup === 'copilot') {
|
|
3940
|
-
addFileAction('.github/copilot-instructions.md', 'Copilot root instructions');
|
|
3941
|
-
addFileAction('.github/instructions/typescript.instructions.md', 'Copilot TypeScript instructions');
|
|
3942
|
-
addFileAction('.github/instructions/testing.instructions.md', 'Copilot testing instructions');
|
|
3943
|
-
addFileAction('.github/prompts/red.prompt.md', 'Copilot RED prompt');
|
|
3944
|
-
addFileAction('.github/prompts/green.prompt.md', 'Copilot GREEN prompt');
|
|
3945
|
-
}
|
|
3946
|
-
|
|
3947
|
-
if (agent.customSetup === 'opencode') {
|
|
3948
|
-
addFileAction('opencode.json', 'OpenCode root config');
|
|
3949
|
-
addFileAction('.opencode/agents/plan-review.md', 'OpenCode plan-review agent');
|
|
3950
|
-
addFileAction('.opencode/agents/tdd-build.md', 'OpenCode tdd-build agent');
|
|
3951
|
-
}
|
|
3952
|
-
|
|
3953
|
-
// Agent commands (converted from Claude format)
|
|
3954
|
-
if (agent.needsConversion || agent.copyCommands || agent.promptFormat) {
|
|
3955
|
-
const cmds = getWorkflowCommands();
|
|
3956
|
-
const targetDir = agent.dirs[0];
|
|
3957
|
-
for (const cmd of cmds) {
|
|
3958
|
-
const ext = agent.promptFormat ? '.prompt.md' : '.md';
|
|
3959
|
-
addFileAction(`${targetDir}/${cmd}${ext}`, 'Converted workflow command');
|
|
3960
|
-
}
|
|
3961
|
-
}
|
|
3962
|
-
|
|
3963
|
-
// Agent rules (copied from Claude)
|
|
3964
|
-
if (agent.needsConversion) {
|
|
3965
|
-
const rulesDir = agent.dirs.find(d => d.includes('/rules'));
|
|
3966
|
-
if (rulesDir) {
|
|
3967
|
-
addFileAction(`${rulesDir}/workflow.md`, 'Workflow rules');
|
|
3968
|
-
}
|
|
3969
|
-
}
|
|
3970
|
-
|
|
3971
3883
|
// Agent skill
|
|
3972
3884
|
if (agentKey === 'codex') {
|
|
3973
|
-
const skillEntries = buildCodexSkillInstallPlan(packageDir, { env: process.env, homeDir: os.homedir() });
|
|
3885
|
+
const skillEntries = buildCodexSkillInstallPlan(getPackageRoot(packageDir), { env: process.env, homeDir: os.homedir() });
|
|
3974
3886
|
for (const entry of skillEntries) {
|
|
3975
3887
|
addFileAction(entry.displayPath, 'Codex stage skill');
|
|
3976
3888
|
}
|
|
3977
|
-
} else if (agent.hasSkill) {
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3889
|
+
} else if (agent.hasSkill && agent.skillsDir) {
|
|
3890
|
+
// Skills ship whole directories (SKILL.md + any nested assets); list every
|
|
3891
|
+
// file so --dry-run reflects the real filesystem changes, not just SKILL.md.
|
|
3892
|
+
for (const skill of listCanonicalSkills(getPackageRoot(packageDir))) {
|
|
3893
|
+
for (const rel of listFilesRecursive(skill.sourcePath)) {
|
|
3894
|
+
addFileAction(`${agent.skillsDir}/${skill.name}/${rel}`, 'Forge skill');
|
|
3895
|
+
}
|
|
3981
3896
|
}
|
|
3982
3897
|
}
|
|
3983
3898
|
|
|
@@ -3990,6 +3905,7 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3990
3905
|
// Git hooks
|
|
3991
3906
|
addFileAction('lefthook.yml', 'Git hook configuration');
|
|
3992
3907
|
addFileAction('.forge/hooks/check-tdd.js', 'TDD enforcement hook');
|
|
3908
|
+
addFileAction('.forge/hooks/forge-native-hook.js', 'Native-hook enforcement adapter (Claude/Cursor)');
|
|
3993
3909
|
|
|
3994
3910
|
// Print dry-run summary
|
|
3995
3911
|
console.log('');
|
|
@@ -4012,7 +3928,7 @@ async function executeSetup(config) {
|
|
|
4012
3928
|
// Check prerequisites
|
|
4013
3929
|
checkPrerequisites({
|
|
4014
3930
|
requireBeadsCli: true,
|
|
4015
|
-
requireGithubCli: requiresGithubCliForSetup(agents
|
|
3931
|
+
requireGithubCli: requiresGithubCliForSetup(agents),
|
|
4016
3932
|
requireJq: true,
|
|
4017
3933
|
commandRunner,
|
|
4018
3934
|
});
|
|
@@ -4023,7 +3939,7 @@ async function executeSetup(config) {
|
|
|
4023
3939
|
if (fs.existsSync(agentsDest)) {
|
|
4024
3940
|
actionLog.add('AGENTS.md', 'skipped', 'already exists');
|
|
4025
3941
|
} else {
|
|
4026
|
-
const agentsSrc = path.join(packageDir, 'AGENTS.md');
|
|
3942
|
+
const agentsSrc = path.join(getPackageRoot(packageDir), 'AGENTS.md');
|
|
4027
3943
|
copyFile(agentsSrc, 'AGENTS.md');
|
|
4028
3944
|
}
|
|
4029
3945
|
console.log('');
|
|
@@ -4034,22 +3950,13 @@ async function executeSetup(config) {
|
|
|
4034
3950
|
|
|
4035
3951
|
const skipFiles = {
|
|
4036
3952
|
agentsMd: keepExisting && fs.existsSync(path.join(projectRoot, 'AGENTS.md')),
|
|
4037
|
-
claudeCommands: keepExisting && fs.existsSync(path.join(projectRoot, '.claude', 'commands'))
|
|
4038
3953
|
};
|
|
4039
3954
|
|
|
4040
|
-
if
|
|
4041
|
-
|
|
3955
|
+
// Setup Claude first if selected, then remaining agents
|
|
3956
|
+
if (agents.includes('claude')) {
|
|
3957
|
+
await setupAgent('claude', skipFiles);
|
|
4042
3958
|
}
|
|
4043
|
-
|
|
4044
|
-
// Load canonical commands — use loadAndSetupCanonicalCommands when claude is selected
|
|
4045
|
-
// so that .claude/commands/ are seeded before reading them
|
|
4046
|
-
const claudeCommands = agents.includes('claude')
|
|
4047
|
-
? await loadAndSetupCanonicalCommands(agents, skipFiles)
|
|
4048
|
-
: loadClaudeCommands(agents);
|
|
4049
|
-
|
|
4050
|
-
// Setup agents with progress output (setupSelectedAgents skips claude internally
|
|
4051
|
-
// since loadAndSetupCanonicalCommands already handled it above)
|
|
4052
|
-
await setupSelectedAgents(agents, claudeCommands, skipFiles);
|
|
3959
|
+
await setupSelectedAgents(agents, skipFiles);
|
|
4053
3960
|
ensureWorkflowRuntimeAssets(agents);
|
|
4054
3961
|
ensureWorkflowShellPolicy(agents);
|
|
4055
3962
|
repairDeclaredLefthookDependency(agents);
|
|
@@ -4057,9 +3964,10 @@ async function executeSetup(config) {
|
|
|
4057
3964
|
// Detect Husky and migrate before installing Lefthook hooks
|
|
4058
3965
|
await handleHuskyMigration();
|
|
4059
3966
|
|
|
4060
|
-
// Install git hooks for TDD enforcement
|
|
3967
|
+
// Install git hooks for TDD enforcement. LOUD: this is the `forge setup` handler,
|
|
3968
|
+
// so an inert-hooks result must fail non-zero (B3), not end green.
|
|
4061
3969
|
console.log('');
|
|
4062
|
-
installGitHooks();
|
|
3970
|
+
installGitHooks({ loud: true });
|
|
4063
3971
|
|
|
4064
3972
|
// External services (unless skipped)
|
|
4065
3973
|
await handleExternalServices(skipExternal, agents);
|
|
@@ -4073,6 +3981,9 @@ async function executeSetup(config) {
|
|
|
4073
3981
|
console.log('');
|
|
4074
3982
|
console.log(renderSetupSummary(actionLog, agents, VERBOSE_MODE, { status: getSetupSummaryStatus() }));
|
|
4075
3983
|
printSetupNotes();
|
|
3984
|
+
// One-step onboarding: run `forge init` when config is still absent (ac0b38c7).
|
|
3985
|
+
// Hooks + Beads already handled above, so init skips those side effects.
|
|
3986
|
+
await finalizeWorkflowConfig({ hooksAlreadyInstalled: true });
|
|
4076
3987
|
console.log('');
|
|
4077
3988
|
}
|
|
4078
3989
|
|
|
@@ -4082,36 +3993,12 @@ async function executeSetup(config) {
|
|
|
4082
3993
|
// Helper: Scaffold Beads GitHub sync when --sync flag is provided
|
|
4083
3994
|
async function handleSyncScaffold() {
|
|
4084
3995
|
console.log('');
|
|
4085
|
-
console.log('
|
|
3996
|
+
console.log('Beads GitHub sync scaffolding is deprecated (--sync).');
|
|
4086
3997
|
try {
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
for (const f of
|
|
4090
|
-
console.log(`
|
|
4091
|
-
}
|
|
4092
|
-
for (const f of (result.filesSkipped || [])) {
|
|
4093
|
-
console.log(` Skipped: ${f} (already exists)`);
|
|
4094
|
-
}
|
|
4095
|
-
|
|
4096
|
-
// Detect default branch and pin forge-managed workflows to the repo baseline Beads version.
|
|
4097
|
-
const branch = detectDefaultBranch(projectRoot);
|
|
4098
|
-
const beadsVersion = DEFAULT_BEADS_VERSION;
|
|
4099
|
-
const workflowDir = path.join(projectRoot, '.github', 'workflows');
|
|
4100
|
-
templateWorkflows(workflowDir, branch, beadsVersion, result.filesCreated || []);
|
|
4101
|
-
console.log(` Branch: ${branch}, Beads version: ${beadsVersion}`);
|
|
4102
|
-
|
|
4103
|
-
// PAT setup: interactive when possible, reminder otherwise
|
|
4104
|
-
try {
|
|
4105
|
-
const patResult = setupPAT(projectRoot, { interactive: !NON_INTERACTIVE });
|
|
4106
|
-
if (patResult.success) {
|
|
4107
|
-
console.log(' PAT configured for Beads sync');
|
|
4108
|
-
} else if (patResult.reminder) {
|
|
4109
|
-
console.log(` ${patResult.reminder}`);
|
|
4110
|
-
} else if (patResult.instructions) {
|
|
4111
|
-
console.log(` ${patResult.instructions.split('\n')[0]}`);
|
|
4112
|
-
}
|
|
4113
|
-
} catch (_patErr) { // NOSONAR — best-effort PAT setup, non-fatal
|
|
4114
|
-
// PAT setup is best-effort — don't block sync scaffold
|
|
3998
|
+
const result = scaffoldBeadsSync(projectRoot, getPackageRoot(packageDir));
|
|
3999
|
+
console.log(` ${result.message}`);
|
|
4000
|
+
for (const f of result.filesRemoved || []) {
|
|
4001
|
+
console.log(` Removed deprecated sync file: ${f}`);
|
|
4115
4002
|
}
|
|
4116
4003
|
} catch (err) {
|
|
4117
4004
|
console.error(` Error scaffolding GitHub-Beads sync: ${err.message}`);
|
|
@@ -4124,7 +4011,7 @@ async function handleSyncScaffold() {
|
|
|
4124
4011
|
// Helper: Handle setup command in non-quick mode
|
|
4125
4012
|
async function handleSetupCommand(selectedAgents, flags) {
|
|
4126
4013
|
if (!Array.isArray(selectedAgents) || selectedAgents.length === 0) {
|
|
4127
|
-
return
|
|
4014
|
+
return runInteractiveSetupFallback(flags);
|
|
4128
4015
|
}
|
|
4129
4016
|
|
|
4130
4017
|
// Allow callers (e.g. reinstall) to override projectRoot without process.chdir()
|
|
@@ -4144,6 +4031,10 @@ async function handleSetupCommand(selectedAgents, flags) {
|
|
|
4144
4031
|
}
|
|
4145
4032
|
}
|
|
4146
4033
|
|
|
4034
|
+
async function runInteractiveSetupFallback(flags, interactiveSetup = interactiveSetupWithFlags) {
|
|
4035
|
+
return interactiveSetup(flags);
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4147
4038
|
// Helper: Handle external services configuration
|
|
4148
4039
|
|
|
4149
4040
|
|
|
@@ -4179,7 +4070,7 @@ async function handleExternalServices(skipExternal, selectedAgents) {
|
|
|
4179
4070
|
* Detect which agents are already configured in a project directory.
|
|
4180
4071
|
* Checks for the presence of each agent's configured directories/files.
|
|
4181
4072
|
* Returns the external-facing setup IDs used by the setup UX, including
|
|
4182
|
-
* legacy aliases such as `claude-code
|
|
4073
|
+
* legacy aliases such as `claude-code`.
|
|
4183
4074
|
* Callers that need raw plugin IDs for internal lookup must normalize first.
|
|
4184
4075
|
*
|
|
4185
4076
|
* @param {string} dir - Project directory to scan
|
|
@@ -4190,8 +4081,6 @@ function detectConfiguredAgents(dir) {
|
|
|
4190
4081
|
const detected = [];
|
|
4191
4082
|
const legacyAgentIds = {
|
|
4192
4083
|
claude: 'claude-code',
|
|
4193
|
-
copilot: 'github-copilot',
|
|
4194
|
-
roo: 'roo-code',
|
|
4195
4084
|
};
|
|
4196
4085
|
|
|
4197
4086
|
pluginManager.getAllPlugins().forEach((plugin, id) => {
|
|
@@ -4213,7 +4102,7 @@ function detectConfiguredAgents(dir) {
|
|
|
4213
4102
|
* Used during setup --clean or reset flows.
|
|
4214
4103
|
*
|
|
4215
4104
|
* @param {string} dir - Project directory
|
|
4216
|
-
* @param {string} agentName - Agent slug (e.g. 'cursor', '
|
|
4105
|
+
* @param {string} agentName - Agent slug (e.g. 'cursor', 'codex')
|
|
4217
4106
|
* @param {object} [manifest] - Optional sync manifest with file paths to remove
|
|
4218
4107
|
* @returns {{ removed: string[], errors: string[] }}
|
|
4219
4108
|
*/
|
|
@@ -4273,6 +4162,9 @@ const SETUP_FLAG_DEFAULTS = Object.freeze({
|
|
|
4273
4162
|
verbose: false,
|
|
4274
4163
|
dryRun: false,
|
|
4275
4164
|
quick: false,
|
|
4165
|
+
minimal: false,
|
|
4166
|
+
standard: false,
|
|
4167
|
+
full: false,
|
|
4276
4168
|
skipExternal: false,
|
|
4277
4169
|
sync: false,
|
|
4278
4170
|
symlink: false,
|
|
@@ -4287,6 +4179,9 @@ const SIMPLE_SETUP_FLAG_UPDATES = Object.freeze({
|
|
|
4287
4179
|
'--verbose': { verbose: true },
|
|
4288
4180
|
'--dry-run': { dryRun: true },
|
|
4289
4181
|
'--quick': { quick: true },
|
|
4182
|
+
'--minimal': { minimal: true },
|
|
4183
|
+
'--standard': { standard: true },
|
|
4184
|
+
'--full': { full: true },
|
|
4290
4185
|
'--skip-external': { skipExternal: true },
|
|
4291
4186
|
'--sync': { sync: true },
|
|
4292
4187
|
'--symlink': { symlink: true },
|
|
@@ -4364,6 +4259,9 @@ function mergeSetupFlags(flags, argv) {
|
|
|
4364
4259
|
verbose: Boolean(flags.verbose || setupFlags.verbose),
|
|
4365
4260
|
dryRun: Boolean(flags.dryRun || setupFlags.dryRun),
|
|
4366
4261
|
quick: Boolean(flags.quick || setupFlags.quick),
|
|
4262
|
+
minimal: Boolean(flags.minimal || setupFlags.minimal),
|
|
4263
|
+
standard: Boolean(flags.standard || setupFlags.standard),
|
|
4264
|
+
full: Boolean(flags.full || setupFlags.full),
|
|
4367
4265
|
skipExternal: Boolean(flags.skipExternal || setupFlags.skipExternal),
|
|
4368
4266
|
sync: Boolean(flags.sync || setupFlags.sync),
|
|
4369
4267
|
symlink: Boolean(flags.symlink || setupFlags.symlink),
|
|
@@ -4374,9 +4272,6 @@ function mergeSetupFlags(flags, argv) {
|
|
|
4374
4272
|
function normalizeDetectedAgent(agentName) {
|
|
4375
4273
|
const aliases = {
|
|
4376
4274
|
'claude-code': 'claude',
|
|
4377
|
-
'github-copilot': 'copilot',
|
|
4378
|
-
'kilo-code': 'kilocode',
|
|
4379
|
-
'roo-code': 'roo',
|
|
4380
4275
|
};
|
|
4381
4276
|
return aliases[agentName] || agentName;
|
|
4382
4277
|
}
|
|
@@ -4420,6 +4315,22 @@ module.exports = {
|
|
|
4420
4315
|
resetSetupNotes();
|
|
4421
4316
|
PKG_MANAGER = detectPackageManager();
|
|
4422
4317
|
|
|
4318
|
+
if (flags.minimal || flags.standard || flags.full) {
|
|
4319
|
+
const selectedProfiles = [];
|
|
4320
|
+
if (flags.minimal) selectedProfiles.push('minimal');
|
|
4321
|
+
if (flags.standard) selectedProfiles.push('standard');
|
|
4322
|
+
if (flags.full) selectedProfiles.push('full');
|
|
4323
|
+
if (selectedProfiles.length > 1) {
|
|
4324
|
+
return {
|
|
4325
|
+
success: false,
|
|
4326
|
+
error: `Conflicting profile flags: ${selectedProfiles.join(', ')}. Choose exactly one of --minimal, --standard, or --full.`,
|
|
4327
|
+
};
|
|
4328
|
+
}
|
|
4329
|
+
|
|
4330
|
+
const [profile] = selectedProfiles;
|
|
4331
|
+
return initCommand.handler([`--profile=${profile}`, '--yes', ...(flags.force ? ['--force'] : [])], flags, projectRoot);
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4423
4334
|
// Determine agents to install
|
|
4424
4335
|
let selectedAgents = determineSelectedAgents(flags);
|
|
4425
4336
|
|
|
@@ -4454,22 +4365,34 @@ module.exports = {
|
|
|
4454
4365
|
return { success: true };
|
|
4455
4366
|
}
|
|
4456
4367
|
|
|
4368
|
+
if (flags.sync && selectedAgents.length === 0) {
|
|
4369
|
+
await handleSyncScaffold();
|
|
4370
|
+
return { success: true };
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4457
4373
|
if (selectedAgents.length > 0) {
|
|
4458
4374
|
await handleSetupCommand(selectedAgents, flags);
|
|
4459
4375
|
return { success: true };
|
|
4460
4376
|
}
|
|
4461
4377
|
|
|
4462
|
-
await
|
|
4378
|
+
await runInteractiveSetupFallback(flags);
|
|
4463
4379
|
return { success: true };
|
|
4464
4380
|
},
|
|
4465
4381
|
|
|
4466
4382
|
// Expose internals for testing and cross-command use
|
|
4383
|
+
describeHookEnforcement,
|
|
4384
|
+
resolveHookEnforcementState,
|
|
4385
|
+
buildHookVerdict,
|
|
4467
4386
|
checkPrerequisites,
|
|
4468
4387
|
setupCoreDocs,
|
|
4469
4388
|
displaySetupSummary,
|
|
4389
|
+
printForgeInitNextStep,
|
|
4390
|
+
finalizeWorkflowConfig,
|
|
4391
|
+
backupMarkerlessAgentsMd,
|
|
4470
4392
|
setupAgent,
|
|
4471
4393
|
quickSetup,
|
|
4472
4394
|
interactiveSetupWithFlags,
|
|
4395
|
+
_runInteractiveSetupFallback: runInteractiveSetupFallback,
|
|
4473
4396
|
dryRunSetup,
|
|
4474
4397
|
handleSetupCommand,
|
|
4475
4398
|
executeSetup,
|
|
@@ -4477,29 +4400,35 @@ module.exports = {
|
|
|
4477
4400
|
_interactiveSetup,
|
|
4478
4401
|
configureExternalServices,
|
|
4479
4402
|
configureDefaultExternalServices,
|
|
4480
|
-
installBeadsWithMethod,
|
|
4481
4403
|
installSkillsWithMethod,
|
|
4482
4404
|
installViaBunx,
|
|
4483
4405
|
autoInstallLefthook,
|
|
4484
4406
|
autoSetupToolsInQuickMode,
|
|
4407
|
+
ensureGitHooksInstalled,
|
|
4408
|
+
forgeShouldWriteLefthookConfig,
|
|
4409
|
+
FORGE_USER_LEFTHOOK_YML,
|
|
4485
4410
|
setupClaudeMcpConfig,
|
|
4411
|
+
setupCursorMcpConfig,
|
|
4412
|
+
setupClaudePermissions,
|
|
4413
|
+
setupCursorIgnore,
|
|
4414
|
+
setupClaudeHooksConfig,
|
|
4415
|
+
setupCursorHooksConfig,
|
|
4486
4416
|
displayMcpStatus,
|
|
4487
4417
|
displayEnvTokenResults,
|
|
4488
4418
|
minimalInstall,
|
|
4489
4419
|
determineSelectedAgents,
|
|
4490
4420
|
handlePathSetup,
|
|
4491
|
-
loadAndSetupCanonicalCommands,
|
|
4492
4421
|
detectConfiguredAgents,
|
|
4493
4422
|
removeAgentFiles,
|
|
4494
4423
|
parseSetupFlags,
|
|
4495
4424
|
mergeSetupFlags,
|
|
4496
|
-
getWorkflowCommands,
|
|
4497
4425
|
getWorkflowRuntimeAssets,
|
|
4498
4426
|
findMissingWorkflowRuntimeAssets,
|
|
4499
4427
|
ensureWorkflowShellPolicy,
|
|
4500
4428
|
repairWorkflowRuntimeAssets,
|
|
4501
4429
|
repairRuntimeReadiness,
|
|
4502
4430
|
_showBanner: showBanner,
|
|
4431
|
+
backupAndRemoveLegacyCursorRules,
|
|
4503
4432
|
|
|
4504
4433
|
// State accessors for testing
|
|
4505
4434
|
_getState: () => ({ projectRoot, FORCE_MODE, VERBOSE_MODE, NON_INTERACTIVE, SYMLINK_ONLY, SYNC_ENABLED, PKG_MANAGER }),
|