forge-workflow 0.0.9 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +3 -0
- package/.forge/hooks/forge-native-hook.js +245 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -106
- package/QUICKSTART.md +171 -0
- package/README.md +271 -363
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +466 -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/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +118 -0
- package/docs/guides/SUPPORT.md +185 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +205 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/reference/TOOLCHAIN.md +658 -0
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +115 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1042 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +126 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-setup.js +225 -28
- package/lib/beads-sync-scaffold.js +36 -107
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +744 -70
- package/lib/commands/_manifest.js +91 -0
- package/lib/commands/_registry.js +85 -34
- package/lib/commands/_resolve-command-opts.js +261 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +192 -0
- package/lib/commands/hooks.js +242 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +598 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +66 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +523 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +436 -24
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +69 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +116 -0
- package/lib/commands/recap.js +61 -0
- package/lib/commands/recommend.js +22 -2
- package/lib/commands/release.js +91 -0
- package/lib/commands/remember.js +74 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +851 -979
- package/lib/commands/shepherd.js +436 -0
- package/lib/commands/ship.js +23 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +329 -11
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +15 -2
- package/lib/commands/test.js +58 -7
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +56 -25
- package/lib/commands/worktree.js +308 -128
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +946 -0
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +42 -17
- 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 +697 -0
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +193 -0
- package/lib/global-flags.js +74 -0
- package/lib/greptile-match.js +7 -63
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +451 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/kernel/backing-issue.js +305 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2504 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +8 -4
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +157 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1273 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +166 -0
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2089 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +332 -23
- 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/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +191 -0
- package/lib/status/beads-snapshot.js +145 -0
- package/lib/status/presenter.js +216 -0
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +199 -0
- package/lib/workflow/enforce-stage.js +298 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +159 -14
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +46 -36
- 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 +165 -22
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +284 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- 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-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +64 -232
- 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 +144 -47
- 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 +183 -79
- 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/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +108 -0
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +117 -369
- 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-agentic-workflow.js +48 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +251 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +302 -28
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
- 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} +46 -52
- 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} +134 -159
- 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/skills/ship/SKILL.md +251 -0
- 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/skills/status/SKILL.md +102 -0
- 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/ship.prompt.md +0 -215
- package/.github/prompts/status.prompt.md +0 -92
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -56
- package/.github/workflows/github-to-beads.yml +0 -97
- 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 -255
- 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-health-check.js +0 -143
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -567
- 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 -332
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -78
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
- 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 { detectDefaultBranch, detectBeadsVersion, 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 and migrated Beads), pass no-op
|
|
382
|
+
* deps so init only writes `.forge/config.yaml` instead of re-doing that work —
|
|
383
|
+
* avoids duplicate side effects and warning noise (CodeRabbit on PR #368).
|
|
384
|
+
*/
|
|
385
|
+
function defaultRunInit(root, skipSideEffects) {
|
|
386
|
+
const deps = skipSideEffects
|
|
387
|
+
? { installHooks: () => {}, autoMigrateBeads: () => {} }
|
|
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
|
-
|
|
1871
|
-
|
|
1872
|
-
// Helper: Copy commands for agent
|
|
1873
|
-
function copyAgentCommands(agent, claudeCommands) {
|
|
1874
|
-
if (!claudeCommands) return;
|
|
1875
|
-
if (!agent.needsConversion && !agent.copyCommands && !agent.promptFormat) return;
|
|
1876
|
-
|
|
1877
|
-
Object.entries(claudeCommands).forEach(([cmd, content]) => {
|
|
1878
|
-
const { targetFile, targetContent } = convertCommandToAgentFormat(cmd, content, agent);
|
|
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;
|
|
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 };
|
|
1897
1779
|
|
|
1898
|
-
|
|
1899
|
-
if (
|
|
1900
|
-
|
|
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
|
+
);
|
|
1971
1928
|
return;
|
|
1972
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
|
+
);
|
|
1951
|
+
return;
|
|
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,78 @@ 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
|
-
console.warn(` \u26A0 Skipping lefthook setup: ${lefthookStatus.message}`);
|
|
2682
|
-
} else {
|
|
2683
|
-
console.warn(' \u26A0 Skipping lefthook setup: binary not available');
|
|
2684
|
-
}
|
|
2685
|
-
return;
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
// Check if lefthook.yml exists (it should, as it's in the package)
|
|
2689
|
-
const lefthookConfig = path.join(packageDir, 'lefthook.yml');
|
|
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() {
|
|
2690
2713
|
const targetHooks = path.join(projectRoot, '.forge/hooks');
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
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);
|
|
2698
2725
|
}
|
|
2699
2726
|
}
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2700
2729
|
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
}
|
|
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.
|
|
2708
2736
|
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2737
|
+
function installGitHooks(options = {}) { // NOSONAR — Extracted as-is from bin/forge.js; complexity reduction deferred
|
|
2738
|
+
// loud=true (the `forge setup` handlers — quickSetup + executeSetup) surfaces an
|
|
2739
|
+
// inert-hooks result as a HARD failure — a banner + non-zero exit — so setup never ends
|
|
2740
|
+
// green with TDD enforcement silently off (B3). loud=false (the default: mid-stage
|
|
2741
|
+
// repairRuntimeReadiness AND `forge init` via ensureGitHooksInstalled) only warns, since
|
|
2742
|
+
// init deliberately degrades to a warning rather than failing, and repair runs inside
|
|
2743
|
+
// another command's flow.
|
|
2744
|
+
const loud = options.loud === true;
|
|
2745
|
+
console.log('Installing git hooks (TDD enforcement)...');
|
|
2712
2746
|
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2747
|
+
// Install the Forge hook SCRIPTS first, unconditionally: they back BOTH the lefthook
|
|
2748
|
+
// pre-commit gate AND the native harness hooks that forge setup renders regardless of
|
|
2749
|
+
// whether the lefthook binary is present.
|
|
2750
|
+
installForgeHookScripts();
|
|
2751
|
+
|
|
2752
|
+
const lefthookStatus = checkLefthookStatus(projectRoot);
|
|
2753
|
+
let lefthookInstalled = false;
|
|
2754
|
+
if (!lefthookStatus.binaryAvailable && lefthookStatus.message) {
|
|
2755
|
+
console.warn(` \u26A0 lefthook binary unavailable: ${lefthookStatus.message}`);
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
// Only drive lefthook when its binary is actually present. When it is not, fall
|
|
2759
|
+
// straight through to the native .git/hooks fallback below rather than triggering an
|
|
2760
|
+
// on-demand `npx lefthook` fetch \u2014 that is network-dependent and can hang on Windows,
|
|
2761
|
+
// and it keeps this repair path byte-for-byte consistent with bin/forge.js.
|
|
2762
|
+
if (lefthookStatus.binaryAvailable) {
|
|
2763
|
+
try {
|
|
2764
|
+
// Write the user-facing lefthook.yml (references only .forge/hooks/check-tdd.js and
|
|
2765
|
+
// the project's own tests — never the repo's internal scripts/). Overwrite lefthook's
|
|
2766
|
+
// own disposable stub so Forge's pre-commit/pre-push wiring actually lands; never
|
|
2767
|
+
// clobber a config that already has active jobs (kernel c713fce7).
|
|
2768
|
+
const lefthookTarget = path.join(projectRoot, 'lefthook.yml');
|
|
2769
|
+
if (forgeShouldWriteLefthookConfig(lefthookTarget)) {
|
|
2770
|
+
fs.writeFileSync(lefthookTarget, FORGE_USER_LEFTHOOK_YML, 'utf8');
|
|
2771
|
+
actionLog.add('lefthook.yml', 'created');
|
|
2772
|
+
console.log(' ✓ Created lefthook.yml');
|
|
2721
2773
|
}
|
|
2722
2774
|
|
|
2775
|
+
// (The Forge hook scripts — check-tdd.js + forge-native-hook.js — are installed
|
|
2776
|
+
// unconditionally by installForgeHookScripts() above, before this lefthook path.)
|
|
2777
|
+
|
|
2723
2778
|
// Try to install lefthook hooks
|
|
2724
2779
|
// SECURITY: Using execFileSync with hardcoded commands (no user input)
|
|
2725
2780
|
try {
|
|
@@ -2727,20 +2782,24 @@ function installGitHooks() { // NOSONAR — Extracted as-is from bin/forge.js; c
|
|
|
2727
2782
|
try {
|
|
2728
2783
|
secureExecFileSync('npx', ['lefthook', 'install'], { stdio: 'inherit', cwd: projectRoot });
|
|
2729
2784
|
console.log(' ✓ Lefthook hooks installed (local)');
|
|
2785
|
+
lefthookInstalled = true;
|
|
2730
2786
|
} catch (error_) {
|
|
2731
2787
|
// Fallback to global lefthook
|
|
2732
2788
|
console.warn('npx lefthook failed, trying global:', error_.message);
|
|
2733
|
-
|
|
2734
|
-
|
|
2789
|
+
secureExecFileSync('lefthook', ['version'], { stdio: 'ignore' });
|
|
2790
|
+
secureExecFileSync('lefthook', ['install'], { stdio: 'inherit', cwd: projectRoot });
|
|
2735
2791
|
console.log(' ✓ Lefthook hooks installed (global)');
|
|
2792
|
+
lefthookInstalled = true;
|
|
2736
2793
|
}
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2794
|
+
} catch (err) {
|
|
2795
|
+
console.warn('Lefthook installation failed:', err.message);
|
|
2796
|
+
console.warn(' ⚠ Lefthook hooks were not installed; raw git push remains unsafe in this worktree.');
|
|
2797
|
+
console.log(' ℹ Lefthook not found. Install it:');
|
|
2798
|
+
console.log(' bun add -d lefthook (recommended)');
|
|
2799
|
+
console.log(' OR: bun add -g lefthook (global)');
|
|
2800
|
+
console.log(' Then run: bunx lefthook install');
|
|
2801
|
+
console.log(` Run ${PKG_MANAGER} install in this worktree, then rerun setup.`);
|
|
2802
|
+
}
|
|
2744
2803
|
|
|
2745
2804
|
console.log('');
|
|
2746
2805
|
|
|
@@ -2749,6 +2808,48 @@ function installGitHooks() { // NOSONAR — Extracted as-is from bin/forge.js; c
|
|
|
2749
2808
|
console.log(' You can install manually later with: lefthook install');
|
|
2750
2809
|
console.log('');
|
|
2751
2810
|
}
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
// Native fallback: when lefthook could not be installed, wire native .git/hooks so
|
|
2814
|
+
// raw git commit / git push still enforce the TDD gate — enforcement is never silently
|
|
2815
|
+
// inert (B3). This repair path only warns (never sets a failure exit code) since it can
|
|
2816
|
+
// run mid-stage under enforce-stage's repairWorkflowRuntimeAssets().
|
|
2817
|
+
if (!lefthookInstalled) {
|
|
2818
|
+
const native = installNativeGitHooks(projectRoot);
|
|
2819
|
+
if (native.installed) {
|
|
2820
|
+
console.log(` ✓ Native git hooks installed (${native.written.join(', ')}) - lefthook fallback`);
|
|
2821
|
+
} else if (native.skipped && native.skipped.length > 0) {
|
|
2822
|
+
console.warn(` ⚠ Native hook(s) skipped to preserve existing hooks: ${native.skipped.join(', ')}`);
|
|
2823
|
+
} else {
|
|
2824
|
+
console.warn(` ⚠ Could not install native git hooks: ${native.reason || 'unknown'}`);
|
|
2825
|
+
}
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
const verdict = verifyHooksActive(projectRoot);
|
|
2829
|
+
if (verdict.active) {
|
|
2830
|
+
console.log(` ✓ Git hook enforcement active (${verdict.method}).`);
|
|
2831
|
+
} else if (loud && resolveGitHooksDir(projectRoot)) {
|
|
2832
|
+
// In a git repo but enforcement is inert — the exact silent-inert bug B3 kills.
|
|
2833
|
+
// Fail LOUDLY and non-zero so `forge setup` never ends green with hooks off.
|
|
2834
|
+
// (A non-git dir has nothing to hook into, so it only warns — see the else.)
|
|
2835
|
+
const addCmd = PKG_MANAGER === 'bun'
|
|
2836
|
+
? 'bun add -d'
|
|
2837
|
+
: PKG_MANAGER === 'npm'
|
|
2838
|
+
? 'npm install --save-dev'
|
|
2839
|
+
: `${PKG_MANAGER} add -D`;
|
|
2840
|
+
console.error('');
|
|
2841
|
+
console.error(' ============================================================');
|
|
2842
|
+
console.error(' ⚠ TDD ENFORCEMENT IS NOT ACTIVE');
|
|
2843
|
+
console.error(` ${verdict.reason || 'no pre-commit hook is installed'}.`);
|
|
2844
|
+
console.error(' `forge ship` will block until hooks are active. To fix:');
|
|
2845
|
+
console.error(` ${addCmd} lefthook && npx lefthook install`);
|
|
2846
|
+
console.error(' (or re-run `forge setup` in the repo root).');
|
|
2847
|
+
console.error(' ============================================================');
|
|
2848
|
+
console.error('');
|
|
2849
|
+
process.exitCode = 1;
|
|
2850
|
+
} else {
|
|
2851
|
+
console.warn(` ⚠ TDD enforcement is NOT active: ${verdict.reason || 'no pre-commit hook installed'}.`);
|
|
2852
|
+
}
|
|
2752
2853
|
}
|
|
2753
2854
|
|
|
2754
2855
|
// Check if lefthook is already installed in project (delegates to lib/lefthook-check)
|
|
@@ -2785,136 +2886,42 @@ function repairDeclaredLefthookDependency(selectedAgents) {
|
|
|
2785
2886
|
return { attempted: true, repaired: true };
|
|
2786
2887
|
} catch (err) {
|
|
2787
2888
|
console.warn('Lefthook install failed:', err.message);
|
|
2889
|
+
console.warn(' ⚠ Lefthook repair failed; raw git push remains unsafe in this worktree.');
|
|
2788
2890
|
console.log(` ⚠ ${status.message}`);
|
|
2891
|
+
console.log(` Run ${PKG_MANAGER} install in this worktree, then rerun setup.`);
|
|
2789
2892
|
console.log('');
|
|
2790
2893
|
return { attempted: true, repaired: false, error: err };
|
|
2791
2894
|
}
|
|
2792
2895
|
}
|
|
2793
2896
|
|
|
2794
|
-
//
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
//
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
} catch (err) {
|
|
2804
|
-
// Not global
|
|
2805
|
-
console.warn('Beads not found globally:', err.message);
|
|
2806
|
-
}
|
|
2807
|
-
|
|
2808
|
-
// Check if bunx can run it
|
|
2809
|
-
try {
|
|
2810
|
-
secureExecFileSync('bunx', ['@beads/bd', 'version'], { stdio: 'ignore' });
|
|
2811
|
-
return 'bunx';
|
|
2812
|
-
} catch (err) {
|
|
2813
|
-
// Not bunx-capable
|
|
2814
|
-
console.warn('Beads not available via bunx:', err.message);
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
// Check local project installation
|
|
2818
|
-
const pkgPath = path.join(projectRoot, 'package.json');
|
|
2819
|
-
if (!fs.existsSync(pkgPath)) return null;
|
|
2820
|
-
|
|
2821
|
-
try {
|
|
2822
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
2823
|
-
const isInstalled = pkg.devDependencies?.['@beads/bd'] || pkg.dependencies?.['@beads/bd'];
|
|
2824
|
-
return isInstalled ? 'local' : null;
|
|
2825
|
-
} catch (err) {
|
|
2826
|
-
console.warn('Failed to check Beads in package.json:', err.message);
|
|
2827
|
-
return null;
|
|
2828
|
-
}
|
|
2829
|
-
}
|
|
2830
|
-
// Check if Beads is initialized in project — delegates to lib/beads-setup
|
|
2831
|
-
|
|
2832
|
-
// Check if Beads is initialized in project — delegates to lib/beads-setup
|
|
2833
|
-
function isBeadsInitialized() {
|
|
2834
|
-
return beadsSetupLib.isBeadsInitialized(projectRoot);
|
|
2835
|
-
}
|
|
2836
|
-
|
|
2837
|
-
// Initialize Beads in the project using the defensive safeBeadsInit wrapper
|
|
2838
|
-
// Handles config/gitignore writes, hook snapshot/restore, and JSONL pre-seeding
|
|
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
|
-
function initializeBeads(installType) {
|
|
2844
|
-
console.log('Initializing Beads in project...');
|
|
2845
|
-
|
|
2846
|
-
// Build the execBdInit function based on installType
|
|
2847
|
-
const execBdInit = (root) => {
|
|
2848
|
-
// SECURITY: execFileSync with hardcoded commands
|
|
2849
|
-
if (installType === 'global') {
|
|
2850
|
-
secureExecFileSync('bd', ['init'], { stdio: 'inherit', cwd: root });
|
|
2851
|
-
} else if (installType === 'bunx') {
|
|
2852
|
-
secureExecFileSync('bunx', ['@beads/bd', 'init'], { stdio: 'inherit', cwd: root });
|
|
2853
|
-
} else if (installType === 'local') {
|
|
2854
|
-
secureExecFileSync('npx', ['bd', 'init'], { stdio: 'inherit', cwd: root });
|
|
2855
|
-
}
|
|
2856
|
-
};
|
|
2857
|
-
|
|
2858
|
-
// Derive prefix from package.json name or directory name
|
|
2859
|
-
let prefix;
|
|
2860
|
-
try {
|
|
2861
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
|
|
2862
|
-
prefix = pkg.name || path.basename(projectRoot);
|
|
2863
|
-
} catch (_e) { // NOSONAR — fallback to directory name if package.json unreadable
|
|
2864
|
-
prefix = path.basename(projectRoot);
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2897
|
+
// Ensure the local Forge Kernel issue store exists and is migrated.
|
|
2898
|
+
//
|
|
2899
|
+
// The Kernel is a single-machine SQLite store in the git common dir; building
|
|
2900
|
+
// the migrated deps runs broker.initialize() (idempotent), so the DB + schema
|
|
2901
|
+
// exist before first use. There is no external CLI to install — issue tracking
|
|
2902
|
+
// ships with Forge. Best-effort: never throws, so setup proceeds even if the
|
|
2903
|
+
// SQLite runtime is unavailable (the first kernel command would migrate it
|
|
2904
|
+
// later anyway).
|
|
2905
|
+
async function ensureKernelIssueStore() {
|
|
2867
2906
|
try {
|
|
2868
|
-
const
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
// lefthook may not be installed yet — non-fatal
|
|
2876
|
-
}
|
|
2877
|
-
}
|
|
2878
|
-
});
|
|
2879
|
-
|
|
2880
|
-
if (result.skipped) {
|
|
2881
|
-
console.log(' ✓ Beads already initialized');
|
|
2882
|
-
return true;
|
|
2883
|
-
}
|
|
2884
|
-
|
|
2885
|
-
if (!result.success) {
|
|
2886
|
-
for (const e of result.errors) {
|
|
2887
|
-
console.log(` ⚠ ${e}`);
|
|
2888
|
-
}
|
|
2889
|
-
console.log(' Run manually: bd init');
|
|
2890
|
-
return false;
|
|
2891
|
-
}
|
|
2892
|
-
|
|
2893
|
-
for (const w of result.warnings) {
|
|
2894
|
-
console.warn(` ⚠ ${w}`);
|
|
2895
|
-
}
|
|
2896
|
-
console.log(' ✓ Beads initialized');
|
|
2897
|
-
|
|
2898
|
-
// Run post-init health check (non-fatal)
|
|
2899
|
-
try {
|
|
2900
|
-
const health = beadsHealthCheck(projectRoot);
|
|
2901
|
-
if (health.healthy) {
|
|
2902
|
-
console.log(' ✓ Beads health check passed');
|
|
2903
|
-
} else {
|
|
2904
|
-
console.log(` ⚠ Beads health check failed at ${health.failedStep}: ${health.error}`);
|
|
2907
|
+
const deps = await buildMigratedKernelIssueDeps({ projectRoot });
|
|
2908
|
+
const handle = deps.kernelBroker || deps.kernelDriver;
|
|
2909
|
+
if (handle && typeof handle.close === 'function') {
|
|
2910
|
+
try {
|
|
2911
|
+
await handle.close();
|
|
2912
|
+
} catch (_closeErr) { // NOSONAR — best-effort cleanup of the migration handle
|
|
2913
|
+
// Closing the migration handle is best-effort; the process is short-lived.
|
|
2905
2914
|
}
|
|
2906
|
-
} catch (_healthErr) { // NOSONAR — health check is best-effort, non-fatal
|
|
2907
|
-
// Health check is best-effort — don't block setup
|
|
2908
2915
|
}
|
|
2909
|
-
|
|
2916
|
+
KERNEL_STORE_READY = true;
|
|
2910
2917
|
return true;
|
|
2911
2918
|
} catch (err) {
|
|
2912
|
-
|
|
2913
|
-
console.log(
|
|
2919
|
+
KERNEL_STORE_READY = false;
|
|
2920
|
+
console.log(` ⚠ Could not provision the Kernel issue store: ${err.message}`);
|
|
2921
|
+
addSetupNote(`Kernel issue store not provisioned: ${err.message}. Run \`forge doctor\` to inspect.`);
|
|
2914
2922
|
return false;
|
|
2915
2923
|
}
|
|
2916
2924
|
}
|
|
2917
|
-
|
|
2918
2925
|
// Check if Skills CLI is installed
|
|
2919
2926
|
|
|
2920
2927
|
|
|
@@ -2980,59 +2987,18 @@ function initializeSkills(installType) {
|
|
|
2980
2987
|
}
|
|
2981
2988
|
}
|
|
2982
2989
|
|
|
2983
|
-
//
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
// Prompt for Beads setup - extracted to reduce cognitive complexity
|
|
2987
|
-
async function promptBeadsSetup(question) {
|
|
2990
|
+
// Ensure the issue store during interactive setup. No prompt: the Forge Kernel
|
|
2991
|
+
// ships with Forge and auto-provisions, so there is nothing to install or pick.
|
|
2992
|
+
async function promptBeadsSetup(_question) {
|
|
2988
2993
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2989
|
-
console.log('
|
|
2994
|
+
console.log('Issue Store (Forge Kernel)');
|
|
2990
2995
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2991
2996
|
console.log('');
|
|
2992
2997
|
|
|
2993
|
-
const
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
if (beadsInitialized) {
|
|
2997
|
-
console.log('✓ Beads is already initialized in this project');
|
|
2998
|
-
console.log('');
|
|
2999
|
-
return;
|
|
3000
|
-
}
|
|
3001
|
-
|
|
3002
|
-
if (beadsStatus) {
|
|
3003
|
-
// Already installed, just need to initialize
|
|
3004
|
-
console.log(`ℹ Beads is installed (${beadsStatus}), but not initialized`);
|
|
3005
|
-
const initBeads = await question('Initialize Beads in this project? (y/n): ');
|
|
3006
|
-
|
|
3007
|
-
if (initBeads.toLowerCase() === 'y') {
|
|
3008
|
-
initializeBeads(beadsStatus);
|
|
3009
|
-
} else {
|
|
3010
|
-
console.log('Skipped Beads initialization. Run manually: bd init');
|
|
3011
|
-
}
|
|
3012
|
-
console.log('');
|
|
3013
|
-
return;
|
|
2998
|
+
const ready = await ensureKernelIssueStore();
|
|
2999
|
+
if (ready) {
|
|
3000
|
+
console.log('✓ Kernel issue store ready (single-machine; team sync not configured)');
|
|
3014
3001
|
}
|
|
3015
|
-
|
|
3016
|
-
// Not installed
|
|
3017
|
-
console.log('ℹ Beads is not installed');
|
|
3018
|
-
const installBeads = await question('Install Beads? (y/n): ');
|
|
3019
|
-
|
|
3020
|
-
if (installBeads.toLowerCase() !== 'y') {
|
|
3021
|
-
console.log('Skipped Beads installation');
|
|
3022
|
-
console.log('');
|
|
3023
|
-
return;
|
|
3024
|
-
}
|
|
3025
|
-
|
|
3026
|
-
console.log('');
|
|
3027
|
-
console.log('Choose installation method:');
|
|
3028
|
-
console.log(' 1. Global (recommended) - Available system-wide');
|
|
3029
|
-
console.log(' 2. Local - Project-specific devDependency');
|
|
3030
|
-
console.log(' 3. Bunx - Use via bunx (requires bun)');
|
|
3031
|
-
console.log('');
|
|
3032
|
-
const method = await question('Choose method (1-3): ');
|
|
3033
|
-
|
|
3034
|
-
console.log('');
|
|
3035
|
-
installBeadsWithMethod(method);
|
|
3036
3002
|
console.log('');
|
|
3037
3003
|
}
|
|
3038
3004
|
|
|
@@ -3052,72 +3018,6 @@ function installViaBunx(packageName, versionArgs, initFn, toolName) {
|
|
|
3052
3018
|
}
|
|
3053
3019
|
}
|
|
3054
3020
|
|
|
3055
|
-
// Helper: Install Beads with chosen method - extracted to reduce cognitive complexity
|
|
3056
|
-
// SECURITY NOTE: Downloads and executes a remote PowerShell script.
|
|
3057
|
-
// The npm @beads/bd package is broken on Windows (GitHub Issue #1031, closed "not planned"),
|
|
3058
|
-
// so the official PowerShell installer is the only supported path.
|
|
3059
|
-
// Mitigations: HTTPS transport (prevents MITM), official beads repo, user-visible URL.
|
|
3060
|
-
// Follow-up: pin to a versioned release tag once beads publishes tagged releases (for example v0.49.1).
|
|
3061
|
-
const BEADS_INSTALL_PS1_URL = 'https://raw.githubusercontent.com/steveyegge/beads/main/install.ps1';
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
function installBeadsOnWindows() {
|
|
3066
|
-
console.log(' (Windows detected: using PowerShell installer)');
|
|
3067
|
-
console.log(` Downloading: ${BEADS_INSTALL_PS1_URL}`);
|
|
3068
|
-
secureExecFileSync('powershell.exe', [
|
|
3069
|
-
'-NoProfile', '-NonInteractive', '-Command',
|
|
3070
|
-
`irm ${BEADS_INSTALL_PS1_URL} | iex`
|
|
3071
|
-
], { stdio: 'inherit' });
|
|
3072
|
-
}
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
function installBeadsWithMethod(method) { // NOSONAR — Extracted as-is from bin/forge.js; complexity reduction deferred
|
|
3077
|
-
try {
|
|
3078
|
-
// SECURITY: secureExecFileSync with hardcoded commands
|
|
3079
|
-
if (method === '1') {
|
|
3080
|
-
console.log('Installing Beads globally...');
|
|
3081
|
-
if (process.platform === 'win32') {
|
|
3082
|
-
installBeadsOnWindows();
|
|
3083
|
-
} else {
|
|
3084
|
-
const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
|
|
3085
|
-
secureExecFileSync(pkgManager, ['install', '-g', '@beads/bd'], { stdio: 'inherit' });
|
|
3086
|
-
}
|
|
3087
|
-
console.log(' ✓ Beads installed globally');
|
|
3088
|
-
initializeBeads('global');
|
|
3089
|
-
} else if (method === '2') {
|
|
3090
|
-
console.log('Installing Beads locally...');
|
|
3091
|
-
// On Windows, npm postinstall for @beads/bd runs Expand-Archive which has EPERM file-locking
|
|
3092
|
-
// (GitHub Issue #1031, closed "not planned") — same root cause as global install.
|
|
3093
|
-
// Redirect Windows users to the global PowerShell installer instead.
|
|
3094
|
-
if (process.platform === 'win32') {
|
|
3095
|
-
console.log(' ⚠ Local install not supported on Windows (npm @beads/bd EPERM issue).');
|
|
3096
|
-
console.log(' Falling back to global PowerShell installer...');
|
|
3097
|
-
installBeadsOnWindows();
|
|
3098
|
-
} else {
|
|
3099
|
-
const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
|
|
3100
|
-
secureExecFileSync(pkgManager, ['install', '-D', '@beads/bd'], { stdio: 'inherit', cwd: projectRoot });
|
|
3101
|
-
}
|
|
3102
|
-
console.log(' ✓ Beads installed');
|
|
3103
|
-
// On Windows the fallback was global (PowerShell installer), so init as 'global'
|
|
3104
|
-
initializeBeads(process.platform === 'win32' ? 'global' : 'local');
|
|
3105
|
-
} else if (method === '3') {
|
|
3106
|
-
installViaBunx('@beads/bd', ['version'], initializeBeads, 'Beads');
|
|
3107
|
-
} else {
|
|
3108
|
-
console.log('Invalid choice. Skipping Beads installation.');
|
|
3109
|
-
}
|
|
3110
|
-
} catch (err) {
|
|
3111
|
-
console.warn('Beads installation failed:', err.message);
|
|
3112
|
-
console.log(' ⚠ Failed to install Beads:', err.message);
|
|
3113
|
-
if (process.platform === 'win32') {
|
|
3114
|
-
console.log(` Run manually: irm ${BEADS_INSTALL_PS1_URL} | iex`);
|
|
3115
|
-
} else {
|
|
3116
|
-
console.log(` Run manually: ${PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g'} @beads/bd && bd init`);
|
|
3117
|
-
}
|
|
3118
|
-
}
|
|
3119
|
-
}
|
|
3120
|
-
|
|
3121
3021
|
// Helper: Get package-manager-specific install args for Skills
|
|
3122
3022
|
|
|
3123
3023
|
|
|
@@ -3227,7 +3127,7 @@ async function setupProjectTools(rl, question) {
|
|
|
3227
3127
|
console.log('');
|
|
3228
3128
|
console.log('Forge recommends three tools for enhanced workflows:');
|
|
3229
3129
|
console.log('');
|
|
3230
|
-
console.log('•
|
|
3130
|
+
console.log('• Issue tracking (Forge Kernel) - zero-install, git-backed');
|
|
3231
3131
|
console.log(' Persists tasks across sessions, tracks dependencies.');
|
|
3232
3132
|
console.log(' Command: forge ready, forge create, forge close');
|
|
3233
3133
|
console.log('');
|
|
@@ -3241,44 +3141,6 @@ async function setupProjectTools(rl, question) {
|
|
|
3241
3141
|
await promptSkillsSetup(question);
|
|
3242
3142
|
}
|
|
3243
3143
|
|
|
3244
|
-
// Auto-setup Beads in quick mode - extracted to reduce cognitive complexity
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
// Auto-setup Beads in quick mode - extracted to reduce cognitive complexity
|
|
3248
|
-
function autoSetupBeadsInQuickMode() { // NOSONAR — Extracted as-is from bin/forge.js; complexity reduction deferred
|
|
3249
|
-
const beadsStatus = checkForBeads();
|
|
3250
|
-
const beadsInitialized = isBeadsInitialized();
|
|
3251
|
-
|
|
3252
|
-
if (!beadsInitialized && beadsStatus) {
|
|
3253
|
-
console.log('📦 Initializing Beads...');
|
|
3254
|
-
initializeBeads(beadsStatus);
|
|
3255
|
-
console.log('');
|
|
3256
|
-
} else if (!beadsInitialized && !beadsStatus) {
|
|
3257
|
-
console.log('📦 Installing Beads globally...');
|
|
3258
|
-
try {
|
|
3259
|
-
// SECURITY: use PowerShell on Windows (npm @beads/bd is broken on Windows - Issue #1031)
|
|
3260
|
-
if (process.platform === 'win32') {
|
|
3261
|
-
installBeadsOnWindows();
|
|
3262
|
-
} else {
|
|
3263
|
-
const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
|
|
3264
|
-
secureExecFileSync(pkgManager, ['install', '-g', '@beads/bd'], { stdio: 'inherit' });
|
|
3265
|
-
}
|
|
3266
|
-
console.log(' ✓ Beads installed globally');
|
|
3267
|
-
initializeBeads('global');
|
|
3268
|
-
} catch (err) {
|
|
3269
|
-
// Installation failed - provide manual instructions
|
|
3270
|
-
console.log(' ⚠ Could not install Beads automatically');
|
|
3271
|
-
console.log(` Error: ${err.message}`);
|
|
3272
|
-
if (process.platform === 'win32') {
|
|
3273
|
-
console.log(` Run manually: irm ${BEADS_INSTALL_PS1_URL} | iex`);
|
|
3274
|
-
} else {
|
|
3275
|
-
console.log(` Run manually: ${PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g'} @beads/bd && bd init`);
|
|
3276
|
-
}
|
|
3277
|
-
}
|
|
3278
|
-
console.log('');
|
|
3279
|
-
}
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
3144
|
// Helper: Auto-install lefthook if not present - extracted to reduce cognitive complexity
|
|
3283
3145
|
|
|
3284
3146
|
|
|
@@ -3314,12 +3176,24 @@ function autoInstallLefthook() { // NOSONAR — Extracted as-is from bin/forge.j
|
|
|
3314
3176
|
console.log(' ✓ Lefthook binary restored');
|
|
3315
3177
|
} catch (err) {
|
|
3316
3178
|
console.warn('Lefthook install failed:', err.message);
|
|
3179
|
+
console.warn(' ⚠ Lefthook repair failed; raw git push remains unsafe in this worktree.');
|
|
3317
3180
|
console.log(` ⚠ ${status.message}`);
|
|
3181
|
+
console.log(` Run ${PKG_MANAGER} install in this worktree, then rerun setup.`);
|
|
3318
3182
|
}
|
|
3319
3183
|
console.log('');
|
|
3320
3184
|
return;
|
|
3321
3185
|
}
|
|
3322
3186
|
|
|
3187
|
+
// Not in package.json at all. GUARD (kernel 22e33dbf): with no package.json in
|
|
3188
|
+
// projectRoot, `<pkg-mgr> install lefthook` resolves against the nearest ANCESTOR
|
|
3189
|
+
// package.json and installs into the WRONG project (or fails). Skip it — installGitHooks
|
|
3190
|
+
// then wires native .git/hooks, which need no package.json, so enforcement is still live.
|
|
3191
|
+
if (!fs.existsSync(path.join(projectRoot, 'package.json'))) {
|
|
3192
|
+
console.log(' ℹ No package.json here — skipping lefthook install (would target an ancestor); native hooks will back enforcement.');
|
|
3193
|
+
console.log('');
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3323
3197
|
// Not in package.json at all — full install
|
|
3324
3198
|
console.log('📦 Installing lefthook for git hooks...');
|
|
3325
3199
|
try {
|
|
@@ -3335,32 +3209,13 @@ function autoInstallLefthook() { // NOSONAR — Extracted as-is from bin/forge.j
|
|
|
3335
3209
|
console.log('');
|
|
3336
3210
|
}
|
|
3337
3211
|
|
|
3338
|
-
// Helper: Verify a tool is callable after install - extracted to reduce cognitive complexity
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
// Helper: Verify a tool is callable after install - extracted to reduce cognitive complexity
|
|
3342
|
-
function verifyToolInstall(command, args, toolName) {
|
|
3343
|
-
try {
|
|
3344
|
-
secureExecFileSync(command, args, { stdio: 'ignore' });
|
|
3345
|
-
return true;
|
|
3346
|
-
} catch (_err) { // NOSONAR - S2486: Intentionally ignored; verification failure is handled by caller
|
|
3347
|
-
console.log(` ⚠ ${toolName} installed but not callable. Check your PATH.`);
|
|
3348
|
-
return false;
|
|
3349
|
-
}
|
|
3350
|
-
}
|
|
3351
|
-
|
|
3352
3212
|
// Helper: Auto-setup tools (Skills) in quick mode - extracted to reduce cognitive complexity
|
|
3353
3213
|
|
|
3354
3214
|
|
|
3355
3215
|
// Helper: Auto-setup tools (Skills) in quick mode - extracted to reduce cognitive complexity
|
|
3356
|
-
function autoSetupToolsInQuickMode() {
|
|
3357
|
-
//
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
// Post-install verification for Beads
|
|
3361
|
-
if (isBeadsInitialized()) {
|
|
3362
|
-
verifyToolInstall('bd', ['version'], 'Beads');
|
|
3363
|
-
}
|
|
3216
|
+
async function autoSetupToolsInQuickMode() {
|
|
3217
|
+
// Issue store: ensure the Forge Kernel store exists (auto-provisioned, no CLI)
|
|
3218
|
+
await ensureKernelIssueStore();
|
|
3364
3219
|
|
|
3365
3220
|
// Skills: only initialize if already installed (recommended tool)
|
|
3366
3221
|
const skillsStatus = checkForSkills();
|
|
@@ -3368,11 +3223,10 @@ function autoSetupToolsInQuickMode() {
|
|
|
3368
3223
|
console.log('📦 Initializing Skills...');
|
|
3369
3224
|
initializeSkills(skillsStatus);
|
|
3370
3225
|
console.log('');
|
|
3371
|
-
} else if (!skillsStatus) {
|
|
3372
|
-
const installCmd = PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g';
|
|
3373
|
-
console.log(` ℹ Skills not found — install with: ${installCmd} @forge/skills`);
|
|
3374
|
-
console.log('');
|
|
3375
3226
|
}
|
|
3227
|
+
// No hint when the optional Skills CLI is absent: setup bundles and renders
|
|
3228
|
+
// Forge's skills itself, and the previously advertised "@forge/skills"
|
|
3229
|
+
// package does not exist on npm (kernel issue 6e554b41).
|
|
3376
3230
|
}
|
|
3377
3231
|
|
|
3378
3232
|
// Helper: Configure default external services in quick mode - extracted to reduce cognitive complexity
|
|
@@ -3404,6 +3258,73 @@ function configureDefaultExternalServices(skipExternal) {
|
|
|
3404
3258
|
console.log('Configuration saved to .env.local');
|
|
3405
3259
|
}
|
|
3406
3260
|
|
|
3261
|
+
// Auto-import an existing Beads store into the Kernel during setup.
|
|
3262
|
+
// Idempotent and CLI-free: reuses the `forge migrate --from beads` spine, which
|
|
3263
|
+
// reads the committed Beads jsonl sidecars directly (no external issue-tracker
|
|
3264
|
+
// binary), so it works even when the legacy SQL backend is offline. Failures
|
|
3265
|
+
// degrade to a setup note rather than aborting setup. Returns the migrate
|
|
3266
|
+
// outcome for callers/tests.
|
|
3267
|
+
async function autoMigrateBeadsToKernel(opts = {}) {
|
|
3268
|
+
const migrateModule = require('./migrate');
|
|
3269
|
+
let outcome;
|
|
3270
|
+
try {
|
|
3271
|
+
outcome = await migrateModule.autoMigrateBeadsIfPresent(projectRoot, opts);
|
|
3272
|
+
} catch (err) {
|
|
3273
|
+
addSetupNote(`Beads → Kernel auto-migration failed: ${err.message}`);
|
|
3274
|
+
return { migrated: false };
|
|
3275
|
+
}
|
|
3276
|
+
|
|
3277
|
+
if (!outcome.migrated) {
|
|
3278
|
+
if (outcome.result && outcome.result.success === false) {
|
|
3279
|
+
addSetupNote(`Beads → Kernel auto-migration skipped: ${outcome.result.error}`);
|
|
3280
|
+
}
|
|
3281
|
+
return outcome;
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
const { imported, gaps } = outcome.result;
|
|
3285
|
+
const inserted = imported.issues.inserted;
|
|
3286
|
+
const skipped = imported.issues.skipped;
|
|
3287
|
+
if (inserted > 0) {
|
|
3288
|
+
let line = ` ✓ Migrated ${inserted} issue(s) from Beads to the Kernel`;
|
|
3289
|
+
if (gaps && gaps.count > 0) {
|
|
3290
|
+
line += ` (${gaps.count} field gap(s): ${gaps.items.map(g => g.field).join(', ')})`;
|
|
3291
|
+
}
|
|
3292
|
+
console.log(line);
|
|
3293
|
+
} else {
|
|
3294
|
+
console.log(` ✓ Beads store already present in the Kernel (${skipped} issue(s))`);
|
|
3295
|
+
}
|
|
3296
|
+
return outcome;
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
// Install git hooks for a target project root without a full setup run.
|
|
3300
|
+
// Reuses the same lefthook install path setup performs so `forge init` can
|
|
3301
|
+
// reach a hook-active state (closing the init → HOOKS_NOT_ACTIVE catch-22).
|
|
3302
|
+
// Skips silently when there is no package.json to attach hooks to — keeps
|
|
3303
|
+
// `forge init` lightweight for bare/non-node repos and avoids surprise installs.
|
|
3304
|
+
// Restores mutated module state afterward.
|
|
3305
|
+
async function ensureGitHooksInstalled(targetRoot = projectRoot) {
|
|
3306
|
+
// No early bail on a missing package.json (kernel 22e33dbf / B3): the native
|
|
3307
|
+
// .git/hooks fallback needs no package.json, so `forge init` on a bare repo must
|
|
3308
|
+
// still wire enforcement instead of silently skipping it. autoInstallLefthook
|
|
3309
|
+
// guards the package-manager install itself for the no-package.json case.
|
|
3310
|
+
const previousRoot = projectRoot;
|
|
3311
|
+
const previousInteractive = NON_INTERACTIVE;
|
|
3312
|
+
const previousPkgManager = PKG_MANAGER;
|
|
3313
|
+
projectRoot = targetRoot;
|
|
3314
|
+
NON_INTERACTIVE = true;
|
|
3315
|
+
PKG_MANAGER = detectPackageManager();
|
|
3316
|
+
try {
|
|
3317
|
+
autoInstallLefthook();
|
|
3318
|
+
await handleHuskyMigration();
|
|
3319
|
+
installGitHooks();
|
|
3320
|
+
return { installed: true };
|
|
3321
|
+
} finally {
|
|
3322
|
+
projectRoot = previousRoot;
|
|
3323
|
+
NON_INTERACTIVE = previousInteractive;
|
|
3324
|
+
PKG_MANAGER = previousPkgManager;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3407
3328
|
// Quick setup with defaults
|
|
3408
3329
|
|
|
3409
3330
|
|
|
@@ -3422,8 +3343,11 @@ async function quickSetup(selectedAgents, skipExternal) {
|
|
|
3422
3343
|
});
|
|
3423
3344
|
console.log('');
|
|
3424
3345
|
|
|
3425
|
-
// Copy AGENTS.md (actionLog tracks it via copyFile)
|
|
3426
|
-
|
|
3346
|
+
// Copy AGENTS.md (actionLog tracks it via copyFile). Quick mode is
|
|
3347
|
+
// non-interactive and overwrites unconditionally, so back up a markerless
|
|
3348
|
+
// (pre-Forge) AGENTS.md first to avoid silent data loss (kernel issue a5399f3d).
|
|
3349
|
+
const agentsSrc = path.join(getPackageRoot(packageDir), 'AGENTS.md');
|
|
3350
|
+
backupMarkerlessAgentsMd();
|
|
3427
3351
|
copyFile(agentsSrc, 'AGENTS.md');
|
|
3428
3352
|
console.log('');
|
|
3429
3353
|
|
|
@@ -3436,20 +3360,26 @@ async function quickSetup(selectedAgents, skipExternal) {
|
|
|
3436
3360
|
// Auto-install lefthook if missing
|
|
3437
3361
|
autoInstallLefthook();
|
|
3438
3362
|
|
|
3439
|
-
// Auto-setup project tools (
|
|
3440
|
-
autoSetupToolsInQuickMode();
|
|
3363
|
+
// Auto-setup project tools (Kernel issue store, Skills)
|
|
3364
|
+
await autoSetupToolsInQuickMode();
|
|
3441
3365
|
|
|
3442
|
-
//
|
|
3443
|
-
|
|
3444
|
-
|
|
3366
|
+
// Auto-import an existing Beads store into the Kernel (idempotent, CLI-free)
|
|
3367
|
+
await autoMigrateBeadsToKernel();
|
|
3368
|
+
|
|
3369
|
+
// Setup Claude first if selected, then setup remaining agents
|
|
3370
|
+
if (selectedAgents.includes('claude')) {
|
|
3371
|
+
await setupAgent('claude');
|
|
3372
|
+
}
|
|
3373
|
+
await setupSelectedAgents(selectedAgents);
|
|
3445
3374
|
ensureWorkflowRuntimeAssets(selectedAgents);
|
|
3446
3375
|
|
|
3447
3376
|
// Detect Husky and migrate before installing Lefthook hooks
|
|
3448
3377
|
await handleHuskyMigration();
|
|
3449
3378
|
|
|
3450
|
-
// Install git hooks for TDD enforcement
|
|
3379
|
+
// Install git hooks for TDD enforcement. LOUD: this is the `forge setup` handler,
|
|
3380
|
+
// so an inert-hooks result must fail non-zero (B3), not end green.
|
|
3451
3381
|
console.log('');
|
|
3452
|
-
installGitHooks();
|
|
3382
|
+
installGitHooks({ loud: true });
|
|
3453
3383
|
|
|
3454
3384
|
// Configure external services with defaults (unless skipped)
|
|
3455
3385
|
configureDefaultExternalServices(skipExternal);
|
|
@@ -3463,6 +3393,9 @@ async function quickSetup(selectedAgents, skipExternal) {
|
|
|
3463
3393
|
console.log('');
|
|
3464
3394
|
console.log(renderSetupSummary(actionLog, selectedAgents, VERBOSE_MODE, { status: getSetupSummaryStatus() }));
|
|
3465
3395
|
printSetupNotes();
|
|
3396
|
+
// One-step onboarding: run `forge init` when config is still absent (ac0b38c7).
|
|
3397
|
+
// Hooks + Beads already handled above, so init skips those side effects.
|
|
3398
|
+
await finalizeWorkflowConfig({ hooksAlreadyInstalled: true });
|
|
3466
3399
|
console.log('');
|
|
3467
3400
|
}
|
|
3468
3401
|
|
|
@@ -3503,7 +3436,7 @@ function setupAgentsMdFile(flags, skipFiles) {
|
|
|
3503
3436
|
return;
|
|
3504
3437
|
}
|
|
3505
3438
|
|
|
3506
|
-
const agentsSrc = path.join(packageDir, 'AGENTS.md');
|
|
3439
|
+
const agentsSrc = path.join(getPackageRoot(packageDir), 'AGENTS.md');
|
|
3507
3440
|
const agentsDest = path.join(projectRoot, 'AGENTS.md');
|
|
3508
3441
|
const mergeStrategy = flags.merge || 'smart';
|
|
3509
3442
|
|
|
@@ -3583,7 +3516,6 @@ function displayExistingInstallation(projectStatus) {
|
|
|
3583
3516
|
: 'Found partial installation:');
|
|
3584
3517
|
|
|
3585
3518
|
if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
|
|
3586
|
-
if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
|
|
3587
3519
|
if (projectStatus.hasEnvLocal) console.log(' - .env.local');
|
|
3588
3520
|
console.log('');
|
|
3589
3521
|
}
|
|
@@ -3595,7 +3527,6 @@ function displayExistingInstallation(projectStatus) {
|
|
|
3595
3527
|
async function promptForOverwriteDecisions(question, projectStatus, flags = {}) {
|
|
3596
3528
|
const skipFiles = {
|
|
3597
3529
|
agentsMd: false,
|
|
3598
|
-
claudeCommands: false
|
|
3599
3530
|
};
|
|
3600
3531
|
|
|
3601
3532
|
if (flags.keep) {
|
|
@@ -3603,10 +3534,6 @@ async function promptForOverwriteDecisions(question, projectStatus, flags = {})
|
|
|
3603
3534
|
skipFiles.agentsMd = true;
|
|
3604
3535
|
console.log(' Keeping existing AGENTS.md (--keep)');
|
|
3605
3536
|
}
|
|
3606
|
-
if (projectStatus.hasClaudeCommands) {
|
|
3607
|
-
skipFiles.claudeCommands = true;
|
|
3608
|
-
console.log(' Keeping existing .claude/commands/ (--keep)');
|
|
3609
|
-
}
|
|
3610
3537
|
return skipFiles;
|
|
3611
3538
|
}
|
|
3612
3539
|
|
|
@@ -3616,12 +3543,6 @@ async function promptForOverwriteDecisions(question, projectStatus, flags = {})
|
|
|
3616
3543
|
console.log(overwriteAgents ? ' Will overwrite AGENTS.md' : ' Keeping existing AGENTS.md');
|
|
3617
3544
|
}
|
|
3618
3545
|
|
|
3619
|
-
if (projectStatus.hasClaudeCommands) {
|
|
3620
|
-
const overwriteCommands = await askYesNo(question, 'Found existing .claude/commands/. Overwrite?', true);
|
|
3621
|
-
skipFiles.claudeCommands = !overwriteCommands;
|
|
3622
|
-
console.log(overwriteCommands ? ' Will overwrite .claude/commands/' : ' Keeping existing .claude/commands/');
|
|
3623
|
-
}
|
|
3624
|
-
|
|
3625
3546
|
if (projectStatus.type !== 'fresh') {
|
|
3626
3547
|
console.log('');
|
|
3627
3548
|
}
|
|
@@ -3629,47 +3550,17 @@ async function promptForOverwriteDecisions(question, projectStatus, flags = {})
|
|
|
3629
3550
|
return skipFiles;
|
|
3630
3551
|
}
|
|
3631
3552
|
|
|
3632
|
-
// Helper: Load and setup canonical commands - extracted to reduce cognitive complexity
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
// Helper: Load and setup canonical commands - extracted to reduce cognitive complexity
|
|
3636
|
-
async function loadAndSetupCanonicalCommands(selectedAgents, skipFiles) {
|
|
3637
|
-
const claudeCommands = {};
|
|
3638
|
-
const needsClaudeCommands = selectedAgents.includes('claude') ||
|
|
3639
|
-
selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands);
|
|
3640
|
-
|
|
3641
|
-
if (!needsClaudeCommands) {
|
|
3642
|
-
return claudeCommands;
|
|
3643
|
-
}
|
|
3644
|
-
|
|
3645
|
-
// First ensure Claude is set up
|
|
3646
|
-
if (selectedAgents.includes('claude')) {
|
|
3647
|
-
await setupAgent('claude', null, skipFiles);
|
|
3648
|
-
}
|
|
3649
|
-
|
|
3650
|
-
// Then load the commands (from existing or newly created)
|
|
3651
|
-
getWorkflowCommands().forEach(cmd => {
|
|
3652
|
-
const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
|
|
3653
|
-
const content = readFile(cmdPath);
|
|
3654
|
-
if (content) {
|
|
3655
|
-
claudeCommands[`${cmd}.md`] = content;
|
|
3656
|
-
}
|
|
3657
|
-
});
|
|
3658
|
-
|
|
3659
|
-
return claudeCommands;
|
|
3660
|
-
}
|
|
3661
|
-
|
|
3662
3553
|
// Helper: Setup all selected agents - extracted to reduce cognitive complexity
|
|
3663
3554
|
|
|
3664
3555
|
|
|
3665
3556
|
// Helper: Setup all selected agents - extracted to reduce cognitive complexity
|
|
3666
|
-
async function setupSelectedAgents(selectedAgents,
|
|
3557
|
+
async function setupSelectedAgents(selectedAgents, skipFiles) {
|
|
3667
3558
|
const totalAgents = selectedAgents.length;
|
|
3668
3559
|
for (const [index, agentKey] of selectedAgents.entries()) {
|
|
3669
3560
|
const agent = AGENTS[agentKey];
|
|
3670
3561
|
console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
|
|
3671
3562
|
if (agentKey !== 'claude') { // Claude already done above
|
|
3672
|
-
await setupAgent(agentKey,
|
|
3563
|
+
await setupAgent(agentKey, skipFiles);
|
|
3673
3564
|
}
|
|
3674
3565
|
}
|
|
3675
3566
|
|
|
@@ -3769,7 +3660,7 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3769
3660
|
const selectedAgents = await promptForAgentSelection(question, agentKeys);
|
|
3770
3661
|
|
|
3771
3662
|
// Check GitHub CLI prerequisite now that selectedAgents is known
|
|
3772
|
-
if (requiresGithubCliForSetup(selectedAgents
|
|
3663
|
+
if (requiresGithubCliForSetup(selectedAgents)) {
|
|
3773
3664
|
checkPrerequisites({ requireGithubCli: true });
|
|
3774
3665
|
}
|
|
3775
3666
|
|
|
@@ -3784,11 +3675,13 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3784
3675
|
setupCoreDocs();
|
|
3785
3676
|
console.log('');
|
|
3786
3677
|
|
|
3787
|
-
//
|
|
3788
|
-
|
|
3678
|
+
// Setup Claude first if selected (delegated to helper), then remaining agents
|
|
3679
|
+
if (selectedAgents.includes('claude')) {
|
|
3680
|
+
await setupAgent('claude', skipFiles);
|
|
3681
|
+
}
|
|
3789
3682
|
|
|
3790
3683
|
// Setup each selected agent with progress indication (delegated to helper)
|
|
3791
|
-
await setupSelectedAgents(selectedAgents,
|
|
3684
|
+
await setupSelectedAgents(selectedAgents, skipFiles);
|
|
3792
3685
|
ensureWorkflowRuntimeAssets(selectedAgents);
|
|
3793
3686
|
|
|
3794
3687
|
// Handle external services step (delegated to helper)
|
|
@@ -3799,6 +3692,10 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3799
3692
|
|
|
3800
3693
|
// Display final summary (delegated to helper)
|
|
3801
3694
|
displaySetupSummary(selectedAgents);
|
|
3695
|
+
|
|
3696
|
+
// One-step onboarding: this path never writes .forge/config.yaml itself, so
|
|
3697
|
+
// run `forge init` when the config is still absent (ac0b38c7).
|
|
3698
|
+
await finalizeWorkflowConfig();
|
|
3802
3699
|
}
|
|
3803
3700
|
|
|
3804
3701
|
// Main
|
|
@@ -3897,15 +3794,10 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3897
3794
|
addFileAction(dir + '/', 'Create agent directory');
|
|
3898
3795
|
}
|
|
3899
3796
|
|
|
3900
|
-
// Claude-specific files
|
|
3797
|
+
// Claude-specific files (skills are listed by the per-skill block below).
|
|
3798
|
+
// Claude gets policy via CLAUDE.md → AGENTS.md, not always-on .claude/rules/* files.
|
|
3901
3799
|
if (agentKey === 'claude') {
|
|
3902
|
-
const cmds = getWorkflowCommands();
|
|
3903
|
-
for (const cmd of cmds) {
|
|
3904
|
-
addFileAction(`.claude/commands/${cmd}.md`, 'Workflow command');
|
|
3905
|
-
}
|
|
3906
|
-
addFileAction('.claude/rules/workflow.md', 'Workflow rules');
|
|
3907
3800
|
addFileAction('.claude/scripts/load-env.sh', 'Environment loader script');
|
|
3908
|
-
addFileAction('.claude/skills/forge-workflow/SKILL.md', 'Forge workflow skill');
|
|
3909
3801
|
addFileAction('.mcp.json', 'MCP server configuration');
|
|
3910
3802
|
addFileAction('CLAUDE.md', 'Claude root config (links to AGENTS.md)');
|
|
3911
3803
|
}
|
|
@@ -3916,7 +3808,7 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3916
3808
|
}
|
|
3917
3809
|
}
|
|
3918
3810
|
|
|
3919
|
-
// Cursor-specific files
|
|
3811
|
+
// Cursor-specific files (rendered from the canonical rules/ source)
|
|
3920
3812
|
if (agent.customSetup === 'cursor') {
|
|
3921
3813
|
addFileAction('.cursor/rules/forge-workflow.mdc', 'Cursor workflow rule');
|
|
3922
3814
|
addFileAction('.cursor/rules/tdd-enforcement.mdc', 'Cursor TDD rule');
|
|
@@ -3924,54 +3816,19 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3924
3816
|
addFileAction('.cursor/rules/documentation.mdc', 'Cursor documentation rule');
|
|
3925
3817
|
}
|
|
3926
3818
|
|
|
3927
|
-
if (agentKey === 'kilocode') {
|
|
3928
|
-
addFileAction('.kilocode/workflows/forge-workflow.md', 'Kilo native workflow');
|
|
3929
|
-
addFileAction('.kilocode/rules/workflow.md', 'Kilo native rules');
|
|
3930
|
-
addFileAction('.kilocode/skills/forge-workflow/SKILL.md', 'Kilo native skill');
|
|
3931
|
-
}
|
|
3932
|
-
|
|
3933
|
-
if (agent.customSetup === 'copilot') {
|
|
3934
|
-
addFileAction('.github/copilot-instructions.md', 'Copilot root instructions');
|
|
3935
|
-
addFileAction('.github/instructions/typescript.instructions.md', 'Copilot TypeScript instructions');
|
|
3936
|
-
addFileAction('.github/instructions/testing.instructions.md', 'Copilot testing instructions');
|
|
3937
|
-
addFileAction('.github/prompts/red.prompt.md', 'Copilot RED prompt');
|
|
3938
|
-
addFileAction('.github/prompts/green.prompt.md', 'Copilot GREEN prompt');
|
|
3939
|
-
}
|
|
3940
|
-
|
|
3941
|
-
if (agent.customSetup === 'opencode') {
|
|
3942
|
-
addFileAction('opencode.json', 'OpenCode root config');
|
|
3943
|
-
addFileAction('.opencode/agents/plan-review.md', 'OpenCode plan-review agent');
|
|
3944
|
-
addFileAction('.opencode/agents/tdd-build.md', 'OpenCode tdd-build agent');
|
|
3945
|
-
}
|
|
3946
|
-
|
|
3947
|
-
// Agent commands (converted from Claude format)
|
|
3948
|
-
if (agent.needsConversion || agent.copyCommands || agent.promptFormat) {
|
|
3949
|
-
const cmds = getWorkflowCommands();
|
|
3950
|
-
const targetDir = agent.dirs[0];
|
|
3951
|
-
for (const cmd of cmds) {
|
|
3952
|
-
const ext = agent.promptFormat ? '.prompt.md' : '.md';
|
|
3953
|
-
addFileAction(`${targetDir}/${cmd}${ext}`, 'Converted workflow command');
|
|
3954
|
-
}
|
|
3955
|
-
}
|
|
3956
|
-
|
|
3957
|
-
// Agent rules (copied from Claude)
|
|
3958
|
-
if (agent.needsConversion) {
|
|
3959
|
-
const rulesDir = agent.dirs.find(d => d.includes('/rules'));
|
|
3960
|
-
if (rulesDir) {
|
|
3961
|
-
addFileAction(`${rulesDir}/workflow.md`, 'Workflow rules');
|
|
3962
|
-
}
|
|
3963
|
-
}
|
|
3964
|
-
|
|
3965
3819
|
// Agent skill
|
|
3966
3820
|
if (agentKey === 'codex') {
|
|
3967
|
-
const skillEntries = buildCodexSkillInstallPlan(packageDir, { env: process.env, homeDir: os.homedir() });
|
|
3821
|
+
const skillEntries = buildCodexSkillInstallPlan(getPackageRoot(packageDir), { env: process.env, homeDir: os.homedir() });
|
|
3968
3822
|
for (const entry of skillEntries) {
|
|
3969
3823
|
addFileAction(entry.displayPath, 'Codex stage skill');
|
|
3970
3824
|
}
|
|
3971
|
-
} else if (agent.hasSkill) {
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3825
|
+
} else if (agent.hasSkill && agent.skillsDir) {
|
|
3826
|
+
// Skills ship whole directories (SKILL.md + any nested assets); list every
|
|
3827
|
+
// file so --dry-run reflects the real filesystem changes, not just SKILL.md.
|
|
3828
|
+
for (const skill of listCanonicalSkills(getPackageRoot(packageDir))) {
|
|
3829
|
+
for (const rel of listFilesRecursive(skill.sourcePath)) {
|
|
3830
|
+
addFileAction(`${agent.skillsDir}/${skill.name}/${rel}`, 'Forge skill');
|
|
3831
|
+
}
|
|
3975
3832
|
}
|
|
3976
3833
|
}
|
|
3977
3834
|
|
|
@@ -3984,6 +3841,7 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3984
3841
|
// Git hooks
|
|
3985
3842
|
addFileAction('lefthook.yml', 'Git hook configuration');
|
|
3986
3843
|
addFileAction('.forge/hooks/check-tdd.js', 'TDD enforcement hook');
|
|
3844
|
+
addFileAction('.forge/hooks/forge-native-hook.js', 'Native-hook enforcement adapter (Claude/Cursor)');
|
|
3987
3845
|
|
|
3988
3846
|
// Print dry-run summary
|
|
3989
3847
|
console.log('');
|
|
@@ -4006,7 +3864,7 @@ async function executeSetup(config) {
|
|
|
4006
3864
|
// Check prerequisites
|
|
4007
3865
|
checkPrerequisites({
|
|
4008
3866
|
requireBeadsCli: true,
|
|
4009
|
-
requireGithubCli: requiresGithubCliForSetup(agents
|
|
3867
|
+
requireGithubCli: requiresGithubCliForSetup(agents),
|
|
4010
3868
|
requireJq: true,
|
|
4011
3869
|
commandRunner,
|
|
4012
3870
|
});
|
|
@@ -4017,7 +3875,7 @@ async function executeSetup(config) {
|
|
|
4017
3875
|
if (fs.existsSync(agentsDest)) {
|
|
4018
3876
|
actionLog.add('AGENTS.md', 'skipped', 'already exists');
|
|
4019
3877
|
} else {
|
|
4020
|
-
const agentsSrc = path.join(packageDir, 'AGENTS.md');
|
|
3878
|
+
const agentsSrc = path.join(getPackageRoot(packageDir), 'AGENTS.md');
|
|
4021
3879
|
copyFile(agentsSrc, 'AGENTS.md');
|
|
4022
3880
|
}
|
|
4023
3881
|
console.log('');
|
|
@@ -4028,22 +3886,13 @@ async function executeSetup(config) {
|
|
|
4028
3886
|
|
|
4029
3887
|
const skipFiles = {
|
|
4030
3888
|
agentsMd: keepExisting && fs.existsSync(path.join(projectRoot, 'AGENTS.md')),
|
|
4031
|
-
claudeCommands: keepExisting && fs.existsSync(path.join(projectRoot, '.claude', 'commands'))
|
|
4032
3889
|
};
|
|
4033
3890
|
|
|
4034
|
-
if
|
|
4035
|
-
|
|
3891
|
+
// Setup Claude first if selected, then remaining agents
|
|
3892
|
+
if (agents.includes('claude')) {
|
|
3893
|
+
await setupAgent('claude', skipFiles);
|
|
4036
3894
|
}
|
|
4037
|
-
|
|
4038
|
-
// Load canonical commands — use loadAndSetupCanonicalCommands when claude is selected
|
|
4039
|
-
// so that .claude/commands/ are seeded before reading them
|
|
4040
|
-
const claudeCommands = agents.includes('claude')
|
|
4041
|
-
? await loadAndSetupCanonicalCommands(agents, skipFiles)
|
|
4042
|
-
: loadClaudeCommands(agents);
|
|
4043
|
-
|
|
4044
|
-
// Setup agents with progress output (setupSelectedAgents skips claude internally
|
|
4045
|
-
// since loadAndSetupCanonicalCommands already handled it above)
|
|
4046
|
-
await setupSelectedAgents(agents, claudeCommands, skipFiles);
|
|
3895
|
+
await setupSelectedAgents(agents, skipFiles);
|
|
4047
3896
|
ensureWorkflowRuntimeAssets(agents);
|
|
4048
3897
|
ensureWorkflowShellPolicy(agents);
|
|
4049
3898
|
repairDeclaredLefthookDependency(agents);
|
|
@@ -4051,9 +3900,13 @@ async function executeSetup(config) {
|
|
|
4051
3900
|
// Detect Husky and migrate before installing Lefthook hooks
|
|
4052
3901
|
await handleHuskyMigration();
|
|
4053
3902
|
|
|
4054
|
-
// Install git hooks for TDD enforcement
|
|
3903
|
+
// Install git hooks for TDD enforcement. LOUD: this is the `forge setup` handler,
|
|
3904
|
+
// so an inert-hooks result must fail non-zero (B3), not end green.
|
|
4055
3905
|
console.log('');
|
|
4056
|
-
installGitHooks();
|
|
3906
|
+
installGitHooks({ loud: true });
|
|
3907
|
+
|
|
3908
|
+
// Auto-import an existing Beads store into the Kernel (idempotent, CLI-free)
|
|
3909
|
+
await autoMigrateBeadsToKernel();
|
|
4057
3910
|
|
|
4058
3911
|
// External services (unless skipped)
|
|
4059
3912
|
await handleExternalServices(skipExternal, agents);
|
|
@@ -4067,6 +3920,9 @@ async function executeSetup(config) {
|
|
|
4067
3920
|
console.log('');
|
|
4068
3921
|
console.log(renderSetupSummary(actionLog, agents, VERBOSE_MODE, { status: getSetupSummaryStatus() }));
|
|
4069
3922
|
printSetupNotes();
|
|
3923
|
+
// One-step onboarding: run `forge init` when config is still absent (ac0b38c7).
|
|
3924
|
+
// Hooks + Beads already handled above, so init skips those side effects.
|
|
3925
|
+
await finalizeWorkflowConfig({ hooksAlreadyInstalled: true });
|
|
4070
3926
|
console.log('');
|
|
4071
3927
|
}
|
|
4072
3928
|
|
|
@@ -4076,36 +3932,12 @@ async function executeSetup(config) {
|
|
|
4076
3932
|
// Helper: Scaffold Beads GitHub sync when --sync flag is provided
|
|
4077
3933
|
async function handleSyncScaffold() {
|
|
4078
3934
|
console.log('');
|
|
4079
|
-
console.log('
|
|
3935
|
+
console.log('Beads GitHub sync scaffolding is deprecated (--sync).');
|
|
4080
3936
|
try {
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
for (const f of
|
|
4084
|
-
console.log(`
|
|
4085
|
-
}
|
|
4086
|
-
for (const f of (result.filesSkipped || [])) {
|
|
4087
|
-
console.log(` Skipped: ${f} (already exists)`);
|
|
4088
|
-
}
|
|
4089
|
-
|
|
4090
|
-
// Detect default branch and Beads version, then template workflows
|
|
4091
|
-
const branch = detectDefaultBranch(projectRoot);
|
|
4092
|
-
const beadsVersion = detectBeadsVersion();
|
|
4093
|
-
const workflowDir = path.join(projectRoot, '.github', 'workflows');
|
|
4094
|
-
templateWorkflows(workflowDir, branch, beadsVersion, result.filesCreated || []);
|
|
4095
|
-
console.log(` Branch: ${branch}, Beads version: ${beadsVersion}`);
|
|
4096
|
-
|
|
4097
|
-
// PAT setup: interactive when possible, reminder otherwise
|
|
4098
|
-
try {
|
|
4099
|
-
const patResult = setupPAT(projectRoot, { interactive: !NON_INTERACTIVE });
|
|
4100
|
-
if (patResult.success) {
|
|
4101
|
-
console.log(' PAT configured for Beads sync');
|
|
4102
|
-
} else if (patResult.reminder) {
|
|
4103
|
-
console.log(` ${patResult.reminder}`);
|
|
4104
|
-
} else if (patResult.instructions) {
|
|
4105
|
-
console.log(` ${patResult.instructions.split('\n')[0]}`);
|
|
4106
|
-
}
|
|
4107
|
-
} catch (_patErr) { // NOSONAR — best-effort PAT setup, non-fatal
|
|
4108
|
-
// PAT setup is best-effort — don't block sync scaffold
|
|
3937
|
+
const result = scaffoldBeadsSync(projectRoot, getPackageRoot(packageDir));
|
|
3938
|
+
console.log(` ${result.message}`);
|
|
3939
|
+
for (const f of result.filesRemoved || []) {
|
|
3940
|
+
console.log(` Removed deprecated sync file: ${f}`);
|
|
4109
3941
|
}
|
|
4110
3942
|
} catch (err) {
|
|
4111
3943
|
console.error(` Error scaffolding GitHub-Beads sync: ${err.message}`);
|
|
@@ -4118,7 +3950,7 @@ async function handleSyncScaffold() {
|
|
|
4118
3950
|
// Helper: Handle setup command in non-quick mode
|
|
4119
3951
|
async function handleSetupCommand(selectedAgents, flags) {
|
|
4120
3952
|
if (!Array.isArray(selectedAgents) || selectedAgents.length === 0) {
|
|
4121
|
-
return
|
|
3953
|
+
return runInteractiveSetupFallback(flags);
|
|
4122
3954
|
}
|
|
4123
3955
|
|
|
4124
3956
|
// Allow callers (e.g. reinstall) to override projectRoot without process.chdir()
|
|
@@ -4138,6 +3970,10 @@ async function handleSetupCommand(selectedAgents, flags) {
|
|
|
4138
3970
|
}
|
|
4139
3971
|
}
|
|
4140
3972
|
|
|
3973
|
+
async function runInteractiveSetupFallback(flags, interactiveSetup = interactiveSetupWithFlags) {
|
|
3974
|
+
return interactiveSetup(flags);
|
|
3975
|
+
}
|
|
3976
|
+
|
|
4141
3977
|
// Helper: Handle external services configuration
|
|
4142
3978
|
|
|
4143
3979
|
|
|
@@ -4173,7 +4009,7 @@ async function handleExternalServices(skipExternal, selectedAgents) {
|
|
|
4173
4009
|
* Detect which agents are already configured in a project directory.
|
|
4174
4010
|
* Checks for the presence of each agent's configured directories/files.
|
|
4175
4011
|
* Returns the external-facing setup IDs used by the setup UX, including
|
|
4176
|
-
* legacy aliases such as `claude-code
|
|
4012
|
+
* legacy aliases such as `claude-code`.
|
|
4177
4013
|
* Callers that need raw plugin IDs for internal lookup must normalize first.
|
|
4178
4014
|
*
|
|
4179
4015
|
* @param {string} dir - Project directory to scan
|
|
@@ -4184,8 +4020,6 @@ function detectConfiguredAgents(dir) {
|
|
|
4184
4020
|
const detected = [];
|
|
4185
4021
|
const legacyAgentIds = {
|
|
4186
4022
|
claude: 'claude-code',
|
|
4187
|
-
copilot: 'github-copilot',
|
|
4188
|
-
roo: 'roo-code',
|
|
4189
4023
|
};
|
|
4190
4024
|
|
|
4191
4025
|
pluginManager.getAllPlugins().forEach((plugin, id) => {
|
|
@@ -4207,7 +4041,7 @@ function detectConfiguredAgents(dir) {
|
|
|
4207
4041
|
* Used during setup --clean or reset flows.
|
|
4208
4042
|
*
|
|
4209
4043
|
* @param {string} dir - Project directory
|
|
4210
|
-
* @param {string} agentName - Agent slug (e.g. 'cursor', '
|
|
4044
|
+
* @param {string} agentName - Agent slug (e.g. 'cursor', 'codex')
|
|
4211
4045
|
* @param {object} [manifest] - Optional sync manifest with file paths to remove
|
|
4212
4046
|
* @returns {{ removed: string[], errors: string[] }}
|
|
4213
4047
|
*/
|
|
@@ -4267,6 +4101,9 @@ const SETUP_FLAG_DEFAULTS = Object.freeze({
|
|
|
4267
4101
|
verbose: false,
|
|
4268
4102
|
dryRun: false,
|
|
4269
4103
|
quick: false,
|
|
4104
|
+
minimal: false,
|
|
4105
|
+
standard: false,
|
|
4106
|
+
full: false,
|
|
4270
4107
|
skipExternal: false,
|
|
4271
4108
|
sync: false,
|
|
4272
4109
|
symlink: false,
|
|
@@ -4281,6 +4118,9 @@ const SIMPLE_SETUP_FLAG_UPDATES = Object.freeze({
|
|
|
4281
4118
|
'--verbose': { verbose: true },
|
|
4282
4119
|
'--dry-run': { dryRun: true },
|
|
4283
4120
|
'--quick': { quick: true },
|
|
4121
|
+
'--minimal': { minimal: true },
|
|
4122
|
+
'--standard': { standard: true },
|
|
4123
|
+
'--full': { full: true },
|
|
4284
4124
|
'--skip-external': { skipExternal: true },
|
|
4285
4125
|
'--sync': { sync: true },
|
|
4286
4126
|
'--symlink': { symlink: true },
|
|
@@ -4358,6 +4198,9 @@ function mergeSetupFlags(flags, argv) {
|
|
|
4358
4198
|
verbose: Boolean(flags.verbose || setupFlags.verbose),
|
|
4359
4199
|
dryRun: Boolean(flags.dryRun || setupFlags.dryRun),
|
|
4360
4200
|
quick: Boolean(flags.quick || setupFlags.quick),
|
|
4201
|
+
minimal: Boolean(flags.minimal || setupFlags.minimal),
|
|
4202
|
+
standard: Boolean(flags.standard || setupFlags.standard),
|
|
4203
|
+
full: Boolean(flags.full || setupFlags.full),
|
|
4361
4204
|
skipExternal: Boolean(flags.skipExternal || setupFlags.skipExternal),
|
|
4362
4205
|
sync: Boolean(flags.sync || setupFlags.sync),
|
|
4363
4206
|
symlink: Boolean(flags.symlink || setupFlags.symlink),
|
|
@@ -4368,9 +4211,6 @@ function mergeSetupFlags(flags, argv) {
|
|
|
4368
4211
|
function normalizeDetectedAgent(agentName) {
|
|
4369
4212
|
const aliases = {
|
|
4370
4213
|
'claude-code': 'claude',
|
|
4371
|
-
'github-copilot': 'copilot',
|
|
4372
|
-
'kilo-code': 'kilocode',
|
|
4373
|
-
'roo-code': 'roo',
|
|
4374
4214
|
};
|
|
4375
4215
|
return aliases[agentName] || agentName;
|
|
4376
4216
|
}
|
|
@@ -4414,6 +4254,22 @@ module.exports = {
|
|
|
4414
4254
|
resetSetupNotes();
|
|
4415
4255
|
PKG_MANAGER = detectPackageManager();
|
|
4416
4256
|
|
|
4257
|
+
if (flags.minimal || flags.standard || flags.full) {
|
|
4258
|
+
const selectedProfiles = [];
|
|
4259
|
+
if (flags.minimal) selectedProfiles.push('minimal');
|
|
4260
|
+
if (flags.standard) selectedProfiles.push('standard');
|
|
4261
|
+
if (flags.full) selectedProfiles.push('full');
|
|
4262
|
+
if (selectedProfiles.length > 1) {
|
|
4263
|
+
return {
|
|
4264
|
+
success: false,
|
|
4265
|
+
error: `Conflicting profile flags: ${selectedProfiles.join(', ')}. Choose exactly one of --minimal, --standard, or --full.`,
|
|
4266
|
+
};
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
const [profile] = selectedProfiles;
|
|
4270
|
+
return initCommand.handler([`--profile=${profile}`, '--yes', ...(flags.force ? ['--force'] : [])], flags, projectRoot);
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4417
4273
|
// Determine agents to install
|
|
4418
4274
|
let selectedAgents = determineSelectedAgents(flags);
|
|
4419
4275
|
|
|
@@ -4448,12 +4304,17 @@ module.exports = {
|
|
|
4448
4304
|
return { success: true };
|
|
4449
4305
|
}
|
|
4450
4306
|
|
|
4307
|
+
if (flags.sync && selectedAgents.length === 0) {
|
|
4308
|
+
await handleSyncScaffold();
|
|
4309
|
+
return { success: true };
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4451
4312
|
if (selectedAgents.length > 0) {
|
|
4452
4313
|
await handleSetupCommand(selectedAgents, flags);
|
|
4453
4314
|
return { success: true };
|
|
4454
4315
|
}
|
|
4455
4316
|
|
|
4456
|
-
await
|
|
4317
|
+
await runInteractiveSetupFallback(flags);
|
|
4457
4318
|
return { success: true };
|
|
4458
4319
|
},
|
|
4459
4320
|
|
|
@@ -4461,9 +4322,13 @@ module.exports = {
|
|
|
4461
4322
|
checkPrerequisites,
|
|
4462
4323
|
setupCoreDocs,
|
|
4463
4324
|
displaySetupSummary,
|
|
4325
|
+
printForgeInitNextStep,
|
|
4326
|
+
finalizeWorkflowConfig,
|
|
4327
|
+
backupMarkerlessAgentsMd,
|
|
4464
4328
|
setupAgent,
|
|
4465
4329
|
quickSetup,
|
|
4466
4330
|
interactiveSetupWithFlags,
|
|
4331
|
+
_runInteractiveSetupFallback: runInteractiveSetupFallback,
|
|
4467
4332
|
dryRunSetup,
|
|
4468
4333
|
handleSetupCommand,
|
|
4469
4334
|
executeSetup,
|
|
@@ -4471,29 +4336,36 @@ module.exports = {
|
|
|
4471
4336
|
_interactiveSetup,
|
|
4472
4337
|
configureExternalServices,
|
|
4473
4338
|
configureDefaultExternalServices,
|
|
4474
|
-
installBeadsWithMethod,
|
|
4475
4339
|
installSkillsWithMethod,
|
|
4476
4340
|
installViaBunx,
|
|
4477
4341
|
autoInstallLefthook,
|
|
4478
4342
|
autoSetupToolsInQuickMode,
|
|
4343
|
+
autoMigrateBeadsToKernel,
|
|
4344
|
+
ensureGitHooksInstalled,
|
|
4345
|
+
forgeShouldWriteLefthookConfig,
|
|
4346
|
+
FORGE_USER_LEFTHOOK_YML,
|
|
4479
4347
|
setupClaudeMcpConfig,
|
|
4348
|
+
setupCursorMcpConfig,
|
|
4349
|
+
setupClaudePermissions,
|
|
4350
|
+
setupCursorIgnore,
|
|
4351
|
+
setupClaudeHooksConfig,
|
|
4352
|
+
setupCursorHooksConfig,
|
|
4480
4353
|
displayMcpStatus,
|
|
4481
4354
|
displayEnvTokenResults,
|
|
4482
4355
|
minimalInstall,
|
|
4483
4356
|
determineSelectedAgents,
|
|
4484
4357
|
handlePathSetup,
|
|
4485
|
-
loadAndSetupCanonicalCommands,
|
|
4486
4358
|
detectConfiguredAgents,
|
|
4487
4359
|
removeAgentFiles,
|
|
4488
4360
|
parseSetupFlags,
|
|
4489
4361
|
mergeSetupFlags,
|
|
4490
|
-
getWorkflowCommands,
|
|
4491
4362
|
getWorkflowRuntimeAssets,
|
|
4492
4363
|
findMissingWorkflowRuntimeAssets,
|
|
4493
4364
|
ensureWorkflowShellPolicy,
|
|
4494
4365
|
repairWorkflowRuntimeAssets,
|
|
4495
4366
|
repairRuntimeReadiness,
|
|
4496
4367
|
_showBanner: showBanner,
|
|
4368
|
+
backupAndRemoveLegacyCursorRules,
|
|
4497
4369
|
|
|
4498
4370
|
// State accessors for testing
|
|
4499
4371
|
_getState: () => ({ projectRoot, FORCE_MODE, VERBOSE_MODE, NON_INTERACTIVE, SYMLINK_ONLY, SYNC_ENABLED, PKG_MANAGER }),
|