forge-workflow 0.0.10 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +3 -0
- package/.forge/hooks/forge-native-hook.js +245 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +150 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -118
- package/QUICKSTART.md +171 -0
- package/README.md +271 -363
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +461 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/forge/TOOLCHAIN.md +670 -0
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +118 -0
- package/docs/guides/SUPPORT.md +185 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +205 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +115 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1042 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +126 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-setup.js +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +741 -77
- package/lib/commands/_manifest.js +91 -0
- package/lib/commands/_registry.js +85 -34
- package/lib/commands/_resolve-command-opts.js +261 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +192 -0
- package/lib/commands/hooks.js +242 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +598 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +17 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +523 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +436 -24
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +69 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +116 -0
- package/lib/commands/recap.js +61 -0
- package/lib/commands/recommend.js +0 -1
- package/lib/commands/release.js +91 -0
- package/lib/commands/remember.js +74 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +838 -972
- package/lib/commands/shepherd.js +436 -0
- package/lib/commands/ship.js +23 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +158 -21
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +4 -1
- package/lib/commands/test.js +43 -27
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +43 -18
- package/lib/commands/worktree.js +307 -100
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +946 -0
- package/lib/dep-guard/keyword-ripple.js +2 -2
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +35 -9
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +382 -11
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +193 -0
- package/lib/global-flags.js +74 -0
- package/lib/greptile-match.js +7 -63
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +451 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/kernel/backing-issue.js +305 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2504 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +3 -2
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +157 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1273 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +99 -497
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2089 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +241 -20
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/scoring.js +17 -3
- package/lib/status/beads-snapshot.js +45 -2
- package/lib/status/presenter.js +169 -18
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +199 -0
- package/lib/workflow/enforce-stage.js +296 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +11 -22
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +37 -35
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +81 -57
- package/scripts/beads-upgrade-smoke.sh +24 -3
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard.sh +22 -3
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +49 -84
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +50 -83
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +21 -94
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status.sh +60 -57
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +13 -6
- package/scripts/test.js +95 -12
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -89
- package/.github/workflows/github-to-beads.yml +0 -100
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -281
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-bootstrap.js +0 -225
- package/lib/beads-health-check.js +0 -188
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -584
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -356
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -132
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
package/lib/commands/setup.js
CHANGED
|
@@ -13,30 +13,51 @@ const fs = require('node:fs');
|
|
|
13
13
|
const os = require('node:os');
|
|
14
14
|
const path = require('node:path');
|
|
15
15
|
const readline = require('node:readline');
|
|
16
|
-
const { execSync
|
|
16
|
+
const { execSync } = require('node:child_process');
|
|
17
17
|
|
|
18
|
-
// Compute packageDir relative to this file (lib/commands/setup.js -> project root)
|
|
18
|
+
// Compute packageDir relative to this file (lib/commands/setup.js -> project root).
|
|
19
|
+
// packageDir stays a runtime value for asset copying; requires use static
|
|
20
|
+
// relative paths so `bun build --compile` can bundle the module graph.
|
|
19
21
|
const packageDir = path.resolve(__dirname, '..', '..');
|
|
20
|
-
const packageJson = require(
|
|
22
|
+
const packageJson = require('../../package.json');
|
|
21
23
|
const VERSION = packageJson.version;
|
|
22
24
|
|
|
25
|
+
// Dual-channel asset root. Under npm/npx this returns `packageDir` unchanged;
|
|
26
|
+
// inside a `bun build --compile` binary it lazily extracts the embedded runtime
|
|
27
|
+
// assets (skills/rules/docs/hooks/scripts) to a temp dir and returns that. Every
|
|
28
|
+
// packaged-ASSET read below goes through `getPackageRoot(packageDir)`; the
|
|
29
|
+
// module-`require` paths above keep `packageDir` (bundler handles those).
|
|
30
|
+
const { getPackageRoot } = require('../package-root');
|
|
31
|
+
|
|
23
32
|
// Load PluginManager for discoverable agent architecture
|
|
24
33
|
const PluginManager = require('../plugin-manager');
|
|
25
|
-
const {
|
|
34
|
+
const { populateAgentSkills, listCanonicalSkills, listFilesRecursive } = require('../skills-sync');
|
|
35
|
+
const { renderMcpConfig } = require('../mcp-config-renderer');
|
|
36
|
+
const { renderClaudePermissions, renderCursorIgnore } = require('../safety-config-renderer');
|
|
37
|
+
const { renderHookConfig } = require('../hook-renderer');
|
|
26
38
|
const { copyEssentialDocs } = require('../docs-copy');
|
|
39
|
+
|
|
40
|
+
// Baseline MCP server Forge ships. Uses the generic descriptor contract consumed
|
|
41
|
+
// by lib/mcp-config-renderer.js (envRefs are '${VAR}' references, never secrets).
|
|
42
|
+
const CONTEXT7_MCP_DESCRIPTOR = {
|
|
43
|
+
name: 'context7',
|
|
44
|
+
transport: 'stdio',
|
|
45
|
+
command: 'npx',
|
|
46
|
+
args: ['-y', '@upstash/context7-mcp@latest'],
|
|
47
|
+
envRefs: {},
|
|
48
|
+
};
|
|
27
49
|
const { secureExecFileSync } = require('../shell-utils');
|
|
28
50
|
const { askYesNo: _askYesNoBase } = require('../ui-utils');
|
|
29
51
|
|
|
30
|
-
// Load enhanced onboarding modules
|
|
31
|
-
const contextMerge = require(
|
|
32
|
-
const projectDiscovery = require(
|
|
52
|
+
// Load enhanced onboarding modules (static relative requires — bundleable)
|
|
53
|
+
const contextMerge = require('../context-merge');
|
|
54
|
+
const projectDiscovery = require('../project-discovery');
|
|
33
55
|
|
|
34
56
|
// Load lib modules for symlink, beads, and PAT setup
|
|
35
|
-
const { createSymlinkOrCopy: libCreateSymlinkOrCopy } = require(
|
|
36
|
-
const
|
|
37
|
-
const {
|
|
38
|
-
const {
|
|
39
|
-
const { DEFAULT_BEADS_VERSION, detectDefaultBranch, templateWorkflows, scaffoldBeadsSync } = require(path.join(packageDir, 'lib', 'beads-sync-scaffold'));
|
|
57
|
+
const { createSymlinkOrCopy: libCreateSymlinkOrCopy } = require('../symlink-utils');
|
|
58
|
+
const { scaffoldBeadsSync } = require('../beads-sync-scaffold');
|
|
59
|
+
const { resolveSyncBackend } = require('../sync-backend');
|
|
60
|
+
const { buildMigratedKernelIssueDeps } = require('../kernel/cli-broker-factory');
|
|
40
61
|
|
|
41
62
|
// Load incremental setup modules
|
|
42
63
|
const { detectEnvironment } = require('../detect-agent');
|
|
@@ -46,18 +67,25 @@ const { ActionCollector } = require('../setup-utils');
|
|
|
46
67
|
const { renderSetupSummary } = require('../setup-summary-renderer');
|
|
47
68
|
const { smartMergeAgentsMd } = require('../smart-merge');
|
|
48
69
|
const { checkLefthookStatus } = require('../lefthook-check');
|
|
70
|
+
const {
|
|
71
|
+
FORGE_USER_LEFTHOOK_YML,
|
|
72
|
+
forgeShouldWriteLefthookConfig,
|
|
73
|
+
installNativeGitHooks,
|
|
74
|
+
verifyHooksActive,
|
|
75
|
+
resolveGitHooksDir,
|
|
76
|
+
} = require('../lefthook-wiring');
|
|
49
77
|
const { resolveShellRuntime } = require('../runtime-health');
|
|
50
78
|
const {
|
|
51
79
|
buildCodexSkillInstallPlan,
|
|
52
80
|
formatCodexSkillsInstallDir,
|
|
53
81
|
listCodexSkillEntries,
|
|
82
|
+
populateCodexRepoSkills,
|
|
83
|
+
CODEX_REPO_SKILLS_DIR,
|
|
54
84
|
} = require('../codex-skills');
|
|
55
85
|
const {
|
|
56
|
-
generateCopilotConfig,
|
|
57
86
|
generateCursorConfig,
|
|
58
|
-
generateKiloConfig,
|
|
59
|
-
generateOpenCodeConfig,
|
|
60
87
|
} = require('../agents-config');
|
|
88
|
+
const initCommand = require('./init');
|
|
61
89
|
const fileUtils = require('../file-utils');
|
|
62
90
|
const detectionUtils = require('../detection-utils');
|
|
63
91
|
const { detectHusky, migrateHusky } = require('../husky-migration');
|
|
@@ -76,6 +104,11 @@ let actionLog = new SetupActionLog();
|
|
|
76
104
|
let PKG_MANAGER = 'npm';
|
|
77
105
|
let SETUP_NOTES = [];
|
|
78
106
|
let CODEX_SETUP_REPORT = null;
|
|
107
|
+
// Tracks whether the last ensureKernelIssueStore() run provisioned the store.
|
|
108
|
+
// The setup summary reads this so a provisioning failure isn't masked by a
|
|
109
|
+
// hardcoded "✓ Kernel issue store" line. Defaults true: when the ensure step
|
|
110
|
+
// never ran, the kernel still auto-provisions on first use.
|
|
111
|
+
let KERNEL_STORE_READY = true;
|
|
79
112
|
|
|
80
113
|
/**
|
|
81
114
|
* Load agent definitions from plugin architecture
|
|
@@ -96,7 +129,8 @@ function loadAgentsFromPlugins() {
|
|
|
96
129
|
supportStatus: plugin.support?.status || 'supported',
|
|
97
130
|
needsConversion: plugin.setup?.needsConversion || false,
|
|
98
131
|
copyCommands: plugin.setup?.copyCommands || false,
|
|
99
|
-
promptFormat: plugin.setup?.promptFormat || false
|
|
132
|
+
promptFormat: plugin.setup?.promptFormat || false,
|
|
133
|
+
skillsDir: plugin.directories?.skills || null
|
|
100
134
|
};
|
|
101
135
|
});
|
|
102
136
|
return agents;
|
|
@@ -138,29 +172,6 @@ function detectPackageManager() {
|
|
|
138
172
|
return 'npm';
|
|
139
173
|
}
|
|
140
174
|
|
|
141
|
-
/**
|
|
142
|
-
* Reads workflow command names from commands/*.md in the package directory.
|
|
143
|
-
* Falls back to .claude/commands/ if commands/ does not exist (backwards compat).
|
|
144
|
-
* @returns {string[]} Command names (filenames without .md extension)
|
|
145
|
-
*/
|
|
146
|
-
function getWorkflowCommands() {
|
|
147
|
-
const canonicalDir = path.join(packageDir, 'commands');
|
|
148
|
-
const commandsDir = fs.existsSync(canonicalDir)
|
|
149
|
-
? canonicalDir
|
|
150
|
-
: path.join(packageDir, '.claude', 'commands');
|
|
151
|
-
try {
|
|
152
|
-
return fs.readdirSync(commandsDir)
|
|
153
|
-
.filter(f => f.endsWith('.md'))
|
|
154
|
-
.map(f => f.replace(/\.md$/, ''));
|
|
155
|
-
} catch (err) {
|
|
156
|
-
if (err.code === 'ENOENT') {
|
|
157
|
-
console.warn(`Warning: commands directory not found at ${commandsDir}`);
|
|
158
|
-
} else {
|
|
159
|
-
console.warn(`Warning: failed to read commands — ${err.code}: ${err.message}`);
|
|
160
|
-
}
|
|
161
|
-
return [];
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
175
|
|
|
165
176
|
const WORKFLOW_RUNTIME_ASSETS = Object.freeze([
|
|
166
177
|
'scripts/beads-context.sh',
|
|
@@ -184,7 +195,7 @@ const WORKFLOW_RUNTIME_ASSETS = Object.freeze([
|
|
|
184
195
|
'scripts/forge-team/lib/sync-github.sh',
|
|
185
196
|
'scripts/forge-team/lib/verify.sh',
|
|
186
197
|
'scripts/forge-team/lib/workload.sh',
|
|
187
|
-
'.claude/scripts/
|
|
198
|
+
'.claude/scripts/review-resolve.sh'
|
|
188
199
|
]);
|
|
189
200
|
|
|
190
201
|
/**
|
|
@@ -205,112 +216,11 @@ function validateAgents(agentList) {
|
|
|
205
216
|
return valid;
|
|
206
217
|
}
|
|
207
218
|
|
|
208
|
-
function parseDoltRemoteNames(remoteListOutput) {
|
|
209
|
-
const output = String(remoteListOutput || '').trim();
|
|
210
|
-
if (!output || /^No remotes configured\.?$/i.test(output)) {
|
|
211
|
-
return [];
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return output
|
|
215
|
-
.split(/\r?\n/)
|
|
216
|
-
.map(line => line.trim())
|
|
217
|
-
.filter(Boolean)
|
|
218
|
-
.map(line => line.split(/\s+/)[0])
|
|
219
|
-
.filter(Boolean);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function readBeadsSyncRemoteConfig(projectDir = projectRoot) {
|
|
223
|
-
const jsonConfigPath = path.join(projectDir, '.beads', 'config.json');
|
|
224
|
-
if (fs.existsSync(jsonConfigPath)) {
|
|
225
|
-
try {
|
|
226
|
-
const parsed = JSON.parse(fs.readFileSync(jsonConfigPath, 'utf8'));
|
|
227
|
-
if (typeof parsed.sync_remote === 'string' && parsed.sync_remote.trim()) {
|
|
228
|
-
return parsed.sync_remote.trim();
|
|
229
|
-
}
|
|
230
|
-
} catch (_error) {
|
|
231
|
-
// Ignore malformed config and fall back to other sources.
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const yamlConfigPath = path.join(projectDir, '.beads', 'config.yaml');
|
|
236
|
-
if (fs.existsSync(yamlConfigPath)) {
|
|
237
|
-
const configuredRemote = parseBeadsYamlSyncRemote(fs.readFileSync(yamlConfigPath, 'utf8'));
|
|
238
|
-
if (configuredRemote) {
|
|
239
|
-
return configuredRemote;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return '';
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function parseBeadsYamlSyncRemote(content) {
|
|
247
|
-
for (const rawLine of String(content || '').split(/\r?\n/)) {
|
|
248
|
-
const trimmedLine = rawLine.trimStart();
|
|
249
|
-
if (!trimmedLine.startsWith('sync-remote:')) {
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const rawValue = trimmedLine.slice('sync-remote:'.length).trim();
|
|
254
|
-
if (!rawValue) {
|
|
255
|
-
return '';
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const quote = rawValue[0];
|
|
259
|
-
if ((quote === '"' || quote === '\'') && rawValue.length > 1) {
|
|
260
|
-
const closingQuoteIndex = rawValue.indexOf(quote, 1);
|
|
261
|
-
if (closingQuoteIndex > 1) {
|
|
262
|
-
return rawValue.slice(1, closingQuoteIndex).trim();
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const commentIndex = rawValue.indexOf('#');
|
|
267
|
-
const unquotedValue = commentIndex === -1 ? rawValue : rawValue.slice(0, commentIndex);
|
|
268
|
-
return unquotedValue.trim();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return '';
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function resolveExpectedBeadsRemote(options = {}) {
|
|
275
|
-
const projectDir = options.projectDir || projectRoot;
|
|
276
|
-
const env = options.env || process.env;
|
|
277
|
-
const configuredRemote = readBeadsSyncRemoteConfig(projectDir);
|
|
278
|
-
if (configuredRemote) {
|
|
279
|
-
return configuredRemote;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (typeof env.BD_SYNC_REMOTE === 'string' && env.BD_SYNC_REMOTE.trim()) {
|
|
283
|
-
return env.BD_SYNC_REMOTE.trim();
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const gitRemoteProbe = options.gitRemoteProbe
|
|
287
|
-
|| ((remoteName) => safeExec(`git -C "${projectDir}" remote get-url ${remoteName}`));
|
|
288
|
-
if (gitRemoteProbe('upstream')) {
|
|
289
|
-
return 'upstream';
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return 'origin';
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function hasBeadsDoltRemote(commandRunner, remoteName = 'origin') {
|
|
296
|
-
const output = commandRunner('bd dolt remote list');
|
|
297
|
-
if (!output) {
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return parseDoltRemoteNames(output).includes(remoteName);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
219
|
// Prerequisite check function
|
|
305
220
|
function checkPrerequisites(options = {}) {
|
|
306
221
|
const requireGithubCli = options.requireGithubCli !== false;
|
|
307
222
|
const requireBeadsCli = options.requireBeadsCli === true;
|
|
308
223
|
const requireJq = options.requireJq === true;
|
|
309
|
-
const expectedBeadsRemote = options.expectedBeadsRemote || resolveExpectedBeadsRemote({
|
|
310
|
-
env: options.env,
|
|
311
|
-
gitRemoteProbe: options.gitRemoteProbe,
|
|
312
|
-
projectDir: options.projectDir,
|
|
313
|
-
});
|
|
314
224
|
const commandRunner = options.commandRunner || safeExec;
|
|
315
225
|
const errors = [];
|
|
316
226
|
const warnings = [];
|
|
@@ -346,25 +256,15 @@ function checkPrerequisites(options = {}) {
|
|
|
346
256
|
}
|
|
347
257
|
}
|
|
348
258
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
`Sync will remain local until you run: bd dolt remote add ${expectedBeadsRemote} <url>`
|
|
358
|
-
);
|
|
359
|
-
} else if (hasRemote === null) {
|
|
360
|
-
warnings.push(
|
|
361
|
-
`Unable to inspect Beads Dolt remotes. ` +
|
|
362
|
-
`Sync may remain local until '${expectedBeadsRemote}' is configured and bd dolt remote list succeeds.`
|
|
363
|
-
);
|
|
364
|
-
}
|
|
259
|
+
if (requireBeadsCli) {
|
|
260
|
+
// Issue tracking runs on the local Forge Kernel store, which auto-provisions
|
|
261
|
+
// on first use — there is no external CLI to install. Surface only whether
|
|
262
|
+
// team sync is wired up yet.
|
|
263
|
+
if (resolveSyncBackend({ projectRoot, env: options.env }) === 'local-noop') {
|
|
264
|
+
console.log(' ✓ Kernel issue store (local-noop sync: single-machine until a sync server is configured)');
|
|
265
|
+
} else {
|
|
266
|
+
console.log(' ✓ Kernel issue store');
|
|
365
267
|
}
|
|
366
|
-
} else if (requireBeadsCli) {
|
|
367
|
-
errors.push('bd (Beads CLI) - Install from https://github.com/steveyegge/beads');
|
|
368
268
|
}
|
|
369
269
|
|
|
370
270
|
// Check Node.js version
|
|
@@ -379,7 +279,11 @@ function checkPrerequisites(options = {}) {
|
|
|
379
279
|
if (jqVersion) {
|
|
380
280
|
console.log(` ✓ ${jqVersion.split('\n')[0]}`);
|
|
381
281
|
} else if (requireJq) {
|
|
382
|
-
|
|
282
|
+
// jq is optional at setup time. Some workflow helper scripts shell out to jq,
|
|
283
|
+
// but they degrade or defer when it is absent — a clean box (esp. Windows)
|
|
284
|
+
// must not abort setup just because jq is not installed yet. Surface a
|
|
285
|
+
// warning instead of a fatal error (kernel issue 01468e44).
|
|
286
|
+
warnings.push('jq not found - some workflow helper scripts will be skipped until you install it (https://jqlang.org/download/)');
|
|
383
287
|
}
|
|
384
288
|
|
|
385
289
|
// Detect package manager
|
|
@@ -405,60 +309,121 @@ function checkPrerequisites(options = {}) {
|
|
|
405
309
|
console.log('');
|
|
406
310
|
console.log(` Package manager: ${PKG_MANAGER}`);
|
|
407
311
|
|
|
408
|
-
|
|
312
|
+
// Return a structured result so embedding agents can inspect prerequisite
|
|
313
|
+
// state instead of only observing a process exit. Genuinely-fatal prereqs
|
|
314
|
+
// (git, node version, required gh) still hard-exit above; soft prereqs like
|
|
315
|
+
// jq surface via `warnings` while `ok` stays true.
|
|
316
|
+
return { errors, warnings, ok: errors.length === 0 };
|
|
409
317
|
}
|
|
410
318
|
|
|
411
|
-
function requiresGithubCliForSetup(selectedAgents
|
|
412
|
-
return needsWorkflowRuntimeAssets(selectedAgents)
|
|
319
|
+
function requiresGithubCliForSetup(selectedAgents) {
|
|
320
|
+
return needsWorkflowRuntimeAssets(selectedAgents);
|
|
413
321
|
}
|
|
414
322
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
| Stage | Command | Description |
|
|
439
|
-
|-------|---------|-------------|
|
|
440
|
-
| utility | \`/status\` | Check current context, active work, recent completions |
|
|
441
|
-
| 1 | \`/plan\` | Design intent -> research -> branch + worktree + task list |
|
|
442
|
-
| 2 | \`/dev\` | TDD development (implementer -> spec review -> quality review) |
|
|
443
|
-
| 3 | \`/validate\` | Type check, lint, security, tests - all fresh output |
|
|
444
|
-
| 4 | \`/ship\` | Push branch and create PR with full documentation |
|
|
445
|
-
| 5 | \`/review\` | Address ALL PR feedback (GitHub Actions, Greptile, SonarCloud) |
|
|
446
|
-
| 6 | \`/premerge\` | Update docs, hand off PR to user |
|
|
447
|
-
| 7 | \`/verify\` | Post-merge health check (CI on main, close Beads) |
|
|
323
|
+
/**
|
|
324
|
+
* After agent setup, `.forge/config.yaml` may not exist yet — only the
|
|
325
|
+
* --minimal/--standard/--full routes run `forge init`. Point the user at
|
|
326
|
+
* `forge init` so first-run always ends with a clear next step that creates
|
|
327
|
+
* the workflow config (gates + change classification). No-op when the config
|
|
328
|
+
* already exists (kernel issue 5bdc91d3).
|
|
329
|
+
*/
|
|
330
|
+
function printForgeInitNextStep() {
|
|
331
|
+
const configPath = path.join(projectRoot, '.forge', 'config.yaml');
|
|
332
|
+
if (fs.existsSync(configPath)) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
console.log('');
|
|
336
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
337
|
+
console.log('▶ NEXT: run `forge init` to configure your workflow');
|
|
338
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
339
|
+
console.log('');
|
|
340
|
+
console.log(' forge init # creates .forge/config.yaml');
|
|
341
|
+
console.log('');
|
|
342
|
+
console.log(' Sets up workflow gates and change classification.');
|
|
343
|
+
console.log(' Safe to re-run (existing config is preserved).');
|
|
344
|
+
console.log('');
|
|
345
|
+
}
|
|
448
346
|
|
|
449
|
-
|
|
347
|
+
/**
|
|
348
|
+
* One-step onboarding (kernel issue ac0b38c7): a fresh `forge setup` used to end
|
|
349
|
+
* by telling the user to ALSO run `forge init`. Instead, run init's real handler
|
|
350
|
+
* here (standard profile, non-interactive) so setup finishes with a working
|
|
351
|
+
* `.forge/config.yaml` in one command. No-op when config already exists; on any
|
|
352
|
+
* init failure fall back to the printed `forge init` guidance so setup never
|
|
353
|
+
* hard-fails. `forge init` stays fully usable standalone. Inject `runInit` for
|
|
354
|
+
* tests.
|
|
355
|
+
*/
|
|
356
|
+
async function finalizeWorkflowConfig(options = {}) {
|
|
357
|
+
const configPath = path.join(projectRoot, '.forge', 'config.yaml');
|
|
358
|
+
if (fs.existsSync(configPath)) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const skipSideEffects = options.hooksAlreadyInstalled === true;
|
|
362
|
+
const runInit = options.runInit || defaultRunInit;
|
|
363
|
+
try {
|
|
364
|
+
const result = await runInit(projectRoot, skipSideEffects);
|
|
365
|
+
if (!result || result.success === false) {
|
|
366
|
+
printForgeInitNextStep();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
console.log('');
|
|
370
|
+
console.log('✓ Workflow configured: .forge/config.yaml (standard profile)');
|
|
371
|
+
console.log(' Change it anytime: forge init --profile <minimal|standard|full> --force');
|
|
372
|
+
console.log('');
|
|
373
|
+
} catch (err) {
|
|
374
|
+
console.warn(`Warning: automatic workflow init skipped: ${err.message}`);
|
|
375
|
+
printForgeInitNextStep();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
450
378
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
379
|
+
/**
|
|
380
|
+
* Default init runner for finalizeWorkflowConfig. When `skipSideEffects` is set
|
|
381
|
+
* (setup paths that already installed git hooks 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
|
-
|
|
1772
|
+
// Helper: back up + remove a deprecated `.cursorrules` without data loss.
|
|
1773
|
+
// Copies to `.cursorrules.bak` (then numbered `.bak.1`, `.2`, … so a repeat never
|
|
1774
|
+
// clobbers an earlier snapshot) before deleting. Mirrors the markerless AGENTS.md
|
|
1775
|
+
// backup above. Exported for direct testing.
|
|
1776
|
+
function backupAndRemoveLegacyCursorRules(root) {
|
|
1777
|
+
const legacy = path.join(root, '.cursorrules');
|
|
1778
|
+
if (!fs.existsSync(legacy)) return { removed: false };
|
|
1871
1779
|
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
const targetDir = agent.dirs[0]; // First dir is commands/workflows
|
|
1880
|
-
writeFile(`${targetDir}/${targetFile}`, targetContent);
|
|
1881
|
-
});
|
|
1882
|
-
console.log(` Converted: ${Object.keys(claudeCommands).length} workflow commands`);
|
|
1883
|
-
}
|
|
1884
|
-
|
|
1885
|
-
// Helper: Copy rules for agent
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
// Helper: Copy rules for agent
|
|
1889
|
-
function copyAgentRules(agent) {
|
|
1890
|
-
if (!agent.needsConversion) return;
|
|
1891
|
-
|
|
1892
|
-
const workflowMdPath = path.join(projectRoot, '.claude/rules/workflow.md');
|
|
1893
|
-
if (!fs.existsSync(workflowMdPath)) return;
|
|
1894
|
-
|
|
1895
|
-
const rulesDir = agent.dirs.find(d => d.includes('/rules'));
|
|
1896
|
-
if (!rulesDir) return;
|
|
1897
|
-
|
|
1898
|
-
const ruleContent = readFile(workflowMdPath);
|
|
1899
|
-
if (ruleContent) {
|
|
1900
|
-
writeFile(`${rulesDir}/workflow.md`, ruleContent);
|
|
1780
|
+
let backupPath = path.join(root, '.cursorrules.bak');
|
|
1781
|
+
if (fs.existsSync(backupPath)) {
|
|
1782
|
+
let suffix = 1;
|
|
1783
|
+
while (fs.existsSync(path.join(root, `.cursorrules.bak.${suffix}`))) {
|
|
1784
|
+
suffix += 1;
|
|
1785
|
+
}
|
|
1786
|
+
backupPath = path.join(root, `.cursorrules.bak.${suffix}`);
|
|
1901
1787
|
}
|
|
1788
|
+
fs.copyFileSync(legacy, backupPath);
|
|
1789
|
+
fs.rmSync(legacy, { force: true });
|
|
1790
|
+
return { removed: true, backupPath };
|
|
1902
1791
|
}
|
|
1903
1792
|
|
|
1904
1793
|
// Helper: Create skill file for agent
|
|
@@ -1911,27 +1800,57 @@ function createAgentSkill(agent, agentKey) {
|
|
|
1911
1800
|
return;
|
|
1912
1801
|
}
|
|
1913
1802
|
|
|
1914
|
-
if (!agent.hasSkill) return;
|
|
1803
|
+
if (!agent.hasSkill || !agent.skillsDir) return;
|
|
1915
1804
|
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
}
|
|
1805
|
+
// Skills-only surface: populate every canonical skill into the agent skills dir
|
|
1806
|
+
// (.claude/skills, .cursor/skills) from the packaged canonical `skills/` source.
|
|
1807
|
+
// clean:false so a rerun/upgrade overwrites Forge's skills without deleting
|
|
1808
|
+
// user-authored or third-party skills that share these shared agent dirs.
|
|
1809
|
+
const { written } = populateAgentSkills({
|
|
1810
|
+
sourceRoot: getPackageRoot(packageDir),
|
|
1811
|
+
targetSkillsDir: path.join(projectRoot, agent.skillsDir),
|
|
1812
|
+
clean: false,
|
|
1813
|
+
});
|
|
1814
|
+
console.log(` Created: ${written.length} skills in ${agent.skillsDir}/`);
|
|
1921
1815
|
}
|
|
1922
1816
|
|
|
1923
1817
|
// Helper: Create Codex per-stage skills from canonical commands
|
|
1924
1818
|
function createCodexSkills() {
|
|
1925
|
-
const installPlan = buildCodexSkillInstallPlan(packageDir, { env: process.env, homeDir: os.homedir() });
|
|
1819
|
+
const installPlan = buildCodexSkillInstallPlan(getPackageRoot(packageDir), { env: process.env, homeDir: os.homedir() });
|
|
1926
1820
|
const installRoot = formatCodexSkillsInstallDir({ env: process.env, homeDir: os.homedir() });
|
|
1927
1821
|
|
|
1928
1822
|
CODEX_SETUP_REPORT = {
|
|
1929
1823
|
installRoot,
|
|
1930
1824
|
skillCount: installPlan.length,
|
|
1825
|
+
repoSkillsDir: CODEX_REPO_SKILLS_DIR,
|
|
1826
|
+
repoSkillCount: 0,
|
|
1931
1827
|
status: 'complete',
|
|
1932
1828
|
message: '',
|
|
1933
1829
|
};
|
|
1934
1830
|
|
|
1831
|
+
// Repo-local discovery mirror: generate `.agents/skills/<name>/SKILL.md` from
|
|
1832
|
+
// the canonical skills/ source. This is Codex's documented repo-scope discovery
|
|
1833
|
+
// path (scanned cwd → repo root) and is committed, so a teammate who clones the
|
|
1834
|
+
// repo WITHOUT running `forge setup` still gets Forge skills/stages discovered.
|
|
1835
|
+
// Independent of the GLOBAL $CODEX_HOME install below (which needs canonical
|
|
1836
|
+
// packaging templates) — so it runs before the install-plan early return.
|
|
1837
|
+
try {
|
|
1838
|
+
const { written } = populateCodexRepoSkills({ sourceRoot: getPackageRoot(packageDir), projectRoot });
|
|
1839
|
+
CODEX_SETUP_REPORT.repoSkillCount = written.length;
|
|
1840
|
+
console.log(` Created: ${written.length} repo-local Codex skills in ${CODEX_REPO_SKILLS_DIR}/ (commit for teammate discovery)`);
|
|
1841
|
+
} catch (error) {
|
|
1842
|
+
addSetupNote(`Codex repo-local skill generation failed for ${CODEX_REPO_SKILLS_DIR}: ${error.message}`);
|
|
1843
|
+
console.log(` Warning: could not generate repo-local Codex skills in ${CODEX_REPO_SKILLS_DIR} (${error.message})`);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// Codex is NOT given a committed project-local `.codex/skills` mirror. Its
|
|
1847
|
+
// repo-scope discovery path is the committed `.agents/skills` (generated above),
|
|
1848
|
+
// and its stage skills install GLOBALLY into `$CODEX_HOME/skills` (below). A
|
|
1849
|
+
// committed `.codex/skills` copy would feed nothing at runtime, so setup does
|
|
1850
|
+
// not create one and it stays gitignored (#342). See the long-standing invariant
|
|
1851
|
+
// in test/setup-runtime-flags.test.js ("setup installs Codex stage skills into
|
|
1852
|
+
// CODEX_HOME/skills/<stage>/SKILL.md").
|
|
1853
|
+
|
|
1935
1854
|
if (installPlan.length === 0) {
|
|
1936
1855
|
CODEX_SETUP_REPORT.status = 'partial';
|
|
1937
1856
|
CODEX_SETUP_REPORT.message = 'Codex setup could not find the packaged stage skill templates.';
|
|
@@ -1960,27 +1879,178 @@ function createCodexSkills() {
|
|
|
1960
1879
|
return CODEX_SETUP_REPORT;
|
|
1961
1880
|
}
|
|
1962
1881
|
|
|
1963
|
-
// Helper:
|
|
1964
|
-
|
|
1882
|
+
// Helper: opt-in Graphiti memory MCP descriptor(s).
|
|
1883
|
+
//
|
|
1884
|
+
// Included ONLY when the memory backend resolves to `graphiti` (same precedence
|
|
1885
|
+
// as the memory router: deps > FORGE_MEMORY_BACKEND > .forge/config.yaml) AND
|
|
1886
|
+
// the config passes the same strict validity check `forge doctor` uses
|
|
1887
|
+
// (assertMemoryConfigValid). Local backend — the default — returns [] so the
|
|
1888
|
+
// rendered MCP config stays byte-identical to the Context7-only output. An
|
|
1889
|
+
// invalid/incomplete graphiti config prints a one-line notice and is skipped;
|
|
1890
|
+
// it never crashes setup.
|
|
1891
|
+
function graphitiMcpDescriptors() {
|
|
1892
|
+
const { assertMemoryConfigValid } = require('../memory/router');
|
|
1893
|
+
try {
|
|
1894
|
+
const { backend, graphiti } = assertMemoryConfigValid({ projectRoot });
|
|
1895
|
+
if (backend !== 'graphiti') return [];
|
|
1896
|
+
const { buildGraphitiServerDescriptor } = require('../memory/graphiti-mcp');
|
|
1897
|
+
return [buildGraphitiServerDescriptor(graphiti)];
|
|
1898
|
+
} catch (err) {
|
|
1899
|
+
// Surface the validator's specific message (it names the missing key and
|
|
1900
|
+
// where to configure it) instead of a generic notice that hides the cause.
|
|
1901
|
+
console.log(
|
|
1902
|
+
` Notice: Graphiti MCP server not wired — ${err && err.message ? err.message : 'memory config invalid'}`
|
|
1903
|
+
+ ' Run `forge doctor` for details.',
|
|
1904
|
+
);
|
|
1905
|
+
return [];
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1965
1908
|
|
|
1966
1909
|
// Helper: Setup MCP config for Claude
|
|
1910
|
+
//
|
|
1911
|
+
// Read → merge → write (idempotent). A pre-existing `.mcp.json` is MERGED, not
|
|
1912
|
+
// skipped: the old skip-if-exists behavior silently refused to add the Context7
|
|
1913
|
+
// server whenever any config already existed. User/other servers are preserved.
|
|
1967
1914
|
function setupClaudeMcpConfig() {
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
|
|
1915
|
+
const { existed, skipped, backup } = renderMcpConfig({
|
|
1916
|
+
harness: 'claude',
|
|
1917
|
+
targetRoot: projectRoot,
|
|
1918
|
+
descriptors: [CONTEXT7_MCP_DESCRIPTOR, ...graphitiMcpDescriptors()],
|
|
1919
|
+
});
|
|
1920
|
+
if (skipped) {
|
|
1921
|
+
// Existing .mcp.json was unparseable (JSONC/trailing comma): renderMcpConfig
|
|
1922
|
+
// left it untouched and backed it up rather than clobber the user's servers.
|
|
1923
|
+
// Report that honestly instead of a false "merged" success.
|
|
1924
|
+
console.log(
|
|
1925
|
+
` Skipped: .mcp.json is not valid JSON — left untouched to avoid data loss`
|
|
1926
|
+
+ `${backup ? ` (backed up to ${backup})` : ''}. Add the Context7 MCP server manually.`,
|
|
1927
|
+
);
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
console.log(
|
|
1931
|
+
existed
|
|
1932
|
+
? ' Updated: .mcp.json (merged Context7 MCP, preserved existing servers)'
|
|
1933
|
+
: ' Created: .mcp.json with Context7 MCP',
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// Helper: Setup MCP config for Cursor (project-local .cursor/mcp.json).
|
|
1938
|
+
// Cursor reads a project-local MCP config, so this is a real native delivery
|
|
1939
|
+
// (mirrors setupClaudeMcpConfig). Read → merge → write, preserving user servers.
|
|
1940
|
+
function setupCursorMcpConfig() {
|
|
1941
|
+
const { existed, skipped, backup } = renderMcpConfig({
|
|
1942
|
+
harness: 'cursor',
|
|
1943
|
+
targetRoot: projectRoot,
|
|
1944
|
+
descriptors: [CONTEXT7_MCP_DESCRIPTOR, ...graphitiMcpDescriptors()],
|
|
1945
|
+
});
|
|
1946
|
+
if (skipped) {
|
|
1947
|
+
console.log(
|
|
1948
|
+
` Skipped: .cursor/mcp.json is not valid JSON — left untouched to avoid data loss`
|
|
1949
|
+
+ `${backup ? ` (backed up to ${backup})` : ''}. Add the Context7 MCP server manually.`,
|
|
1950
|
+
);
|
|
1971
1951
|
return;
|
|
1972
1952
|
}
|
|
1953
|
+
console.log(
|
|
1954
|
+
existed
|
|
1955
|
+
? ' Updated: .cursor/mcp.json (merged Context7 MCP, preserved existing servers)'
|
|
1956
|
+
: ' Created: .cursor/mcp.json with Context7 MCP',
|
|
1957
|
+
);
|
|
1958
|
+
}
|
|
1973
1959
|
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1960
|
+
// Safe SAFETY defaults are ON by default (non-surprising) but fully opt-out-able.
|
|
1961
|
+
// Set FORGE_SKIP_SAFETY_DEFAULTS=1 (or `true`) to skip rendering the permission /
|
|
1962
|
+
// ignore defaults — Forge then leaves those surfaces entirely to the user.
|
|
1963
|
+
function safetyDefaultsEnabled() {
|
|
1964
|
+
const flag = String(process.env.FORGE_SKIP_SAFETY_DEFAULTS || '').trim().toLowerCase();
|
|
1965
|
+
return !(flag === '1' || flag === 'true' || flag === 'yes');
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
// Helper: Render safe Claude tool-permission defaults into .claude/settings.json.
|
|
1969
|
+
// Read -> merge -> write (idempotent). Preserves the user's existing settings and
|
|
1970
|
+
// allow/deny/ask entries; an unparseable file is backed up and left untouched.
|
|
1971
|
+
function setupClaudePermissions() {
|
|
1972
|
+
if (!safetyDefaultsEnabled()) {
|
|
1973
|
+
console.log(' Skipped: .claude/settings.json permissions (FORGE_SKIP_SAFETY_DEFAULTS set)');
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
const { existed, skipped, backup } = renderClaudePermissions({ targetRoot: projectRoot });
|
|
1977
|
+
if (skipped) {
|
|
1978
|
+
console.log(
|
|
1979
|
+
' Skipped: .claude/settings.json is not valid JSON — left untouched to avoid data loss'
|
|
1980
|
+
+ `${backup ? ` (backed up to ${backup})` : ''}. Add safe permissions manually.`,
|
|
1981
|
+
);
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
console.log(
|
|
1985
|
+
existed
|
|
1986
|
+
? ' Updated: .claude/settings.json (merged safe permission defaults, preserved your entries)'
|
|
1987
|
+
: ' Created: .claude/settings.json with safe permission defaults',
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// Helper: Render safe .cursorignore defaults (AI read/index boundary).
|
|
1992
|
+
// Read -> merge -> write (idempotent). Preserves user lines; only appends missing
|
|
1993
|
+
// default patterns (secrets/.env/node_modules/build artifacts).
|
|
1994
|
+
function setupCursorIgnore() {
|
|
1995
|
+
if (!safetyDefaultsEnabled()) {
|
|
1996
|
+
console.log(' Skipped: .cursorignore defaults (FORGE_SKIP_SAFETY_DEFAULTS set)');
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const { existed } = renderCursorIgnore({ targetRoot: projectRoot });
|
|
2000
|
+
console.log(
|
|
2001
|
+
existed
|
|
2002
|
+
? ' Updated: .cursorignore (appended safe defaults, preserved your entries)'
|
|
2003
|
+
: ' Created: .cursorignore with safe defaults',
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// Helper: Setup native HOOK config for Claude (project-local .claude/settings.json).
|
|
2008
|
+
// Projects Forge's TDD-gate + protected-path enforcement onto Claude's native hook
|
|
2009
|
+
// surface (a `hooks` block). Read → merge → write, preserving user hooks; an
|
|
2010
|
+
// unparseable settings.json is backed up and left untouched (data-loss safe).
|
|
2011
|
+
function setupClaudeHooksConfig() {
|
|
2012
|
+
const { existed, skipped, backup, wrote } = renderHookConfig({
|
|
2013
|
+
harness: 'claude',
|
|
2014
|
+
targetRoot: projectRoot,
|
|
2015
|
+
});
|
|
2016
|
+
if (skipped) {
|
|
2017
|
+
console.log(
|
|
2018
|
+
` Skipped: .claude/settings.json is not valid JSON — left untouched to avoid data loss`
|
|
2019
|
+
+ `${backup ? ` (backed up to ${backup})` : ''}. Add the Forge hooks block manually.`,
|
|
2020
|
+
);
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
2023
|
+
if (wrote) {
|
|
2024
|
+
console.log(
|
|
2025
|
+
existed
|
|
2026
|
+
? ' Updated: .claude/settings.json (merged Forge hooks, preserved existing hooks)'
|
|
2027
|
+
: ' Created: .claude/settings.json with Forge hooks',
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// Helper: Setup native HOOK config for Cursor (project-local .cursor/hooks.json,
|
|
2033
|
+
// Cursor 1.7+). Mirrors setupClaudeHooksConfig. Read → merge → write, preserving
|
|
2034
|
+
// user hooks; unparseable config is backed up and left untouched.
|
|
2035
|
+
function setupCursorHooksConfig() {
|
|
2036
|
+
const { existed, skipped, backup, wrote } = renderHookConfig({
|
|
2037
|
+
harness: 'cursor',
|
|
2038
|
+
targetRoot: projectRoot,
|
|
2039
|
+
});
|
|
2040
|
+
if (skipped) {
|
|
2041
|
+
console.log(
|
|
2042
|
+
` Skipped: .cursor/hooks.json is not valid JSON — left untouched to avoid data loss`
|
|
2043
|
+
+ `${backup ? ` (backed up to ${backup})` : ''}. Add the Forge hooks manually.`,
|
|
2044
|
+
);
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
if (wrote) {
|
|
2048
|
+
console.log(
|
|
2049
|
+
existed
|
|
2050
|
+
? ' Updated: .cursor/hooks.json (merged Forge hooks, preserved existing hooks)'
|
|
2051
|
+
: ' Created: .cursor/hooks.json with Forge hooks',
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
1984
2054
|
}
|
|
1985
2055
|
|
|
1986
2056
|
// Helper: Create agent link file
|
|
@@ -2002,13 +2072,13 @@ function createAgentLinkFile(agent, symlinkOnly = false) {
|
|
|
2002
2072
|
|
|
2003
2073
|
|
|
2004
2074
|
// Setup specific agent
|
|
2005
|
-
async function setupAgent(agentKey,
|
|
2075
|
+
async function setupAgent(agentKey, skipFiles = {}) {
|
|
2006
2076
|
const agent = AGENTS[agentKey];
|
|
2007
2077
|
if (!agent) return;
|
|
2008
2078
|
|
|
2009
2079
|
console.log(`\nSetting up ${agent.name}...`);
|
|
2010
2080
|
if (agent.supportStatus === 'deprecated') {
|
|
2011
|
-
console.log(` Warning: ${agent.name} is in deprecated compatibility mode; Forge will scaffold
|
|
2081
|
+
console.log(` Warning: ${agent.name} is in deprecated compatibility mode; Forge will scaffold skill files only.`);
|
|
2012
2082
|
}
|
|
2013
2083
|
|
|
2014
2084
|
// Create directories
|
|
@@ -2023,30 +2093,30 @@ async function setupAgent(agentKey, claudeCommands, skipFiles = {}) {
|
|
|
2023
2093
|
await setupCursorAgent();
|
|
2024
2094
|
}
|
|
2025
2095
|
|
|
2026
|
-
if (agentKey === 'kilocode') {
|
|
2027
|
-
await setupKiloAgent();
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
if (agentKey === 'copilot') {
|
|
2031
|
-
await setupCopilotAgent();
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
if (agentKey === 'opencode') {
|
|
2035
|
-
await setupOpenCodeAgent();
|
|
2036
|
-
}
|
|
2037
|
-
|
|
2038
|
-
// Convert/copy commands
|
|
2039
|
-
copyAgentCommands(agent, claudeCommands);
|
|
2040
|
-
|
|
2041
|
-
// Copy rules if needed
|
|
2042
|
-
copyAgentRules(agent);
|
|
2043
|
-
|
|
2044
2096
|
// Create SKILL.md or Codex stage skills
|
|
2045
2097
|
createAgentSkill(agent, agentKey);
|
|
2046
2098
|
|
|
2047
|
-
// Setup MCP configs
|
|
2099
|
+
// Setup MCP configs (project-local, native for both harnesses)
|
|
2048
2100
|
if (agentKey === 'claude') {
|
|
2049
2101
|
setupClaudeMcpConfig();
|
|
2102
|
+
// Native safety surface: declarative tool-permission allowlist.
|
|
2103
|
+
setupClaudePermissions();
|
|
2104
|
+
}
|
|
2105
|
+
if (agent.customSetup === 'cursor') {
|
|
2106
|
+
setupCursorMcpConfig();
|
|
2107
|
+
// Native safety surface: AI read/index ignore boundary.
|
|
2108
|
+
setupCursorIgnore();
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// Setup native HOOK configs (project-local, native for both harnesses).
|
|
2112
|
+
// Projects Forge's TDD-gate + protected-path enforcement onto each harness's
|
|
2113
|
+
// native hook surface. Codex hooks are GLOBAL-config scope and intentionally
|
|
2114
|
+
// not written at project setup (see lib/hook-renderer.js).
|
|
2115
|
+
if (agentKey === 'claude') {
|
|
2116
|
+
setupClaudeHooksConfig();
|
|
2117
|
+
}
|
|
2118
|
+
if (agent.customSetup === 'cursor') {
|
|
2119
|
+
setupCursorHooksConfig();
|
|
2050
2120
|
}
|
|
2051
2121
|
|
|
2052
2122
|
// Create link file (SYMLINK_ONLY = --symlink flag disables copy fallback)
|
|
@@ -2086,7 +2156,6 @@ function displayInstallationStatus(projectStatus) {
|
|
|
2086
2156
|
}
|
|
2087
2157
|
|
|
2088
2158
|
if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
|
|
2089
|
-
if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
|
|
2090
2159
|
if (projectStatus.hasEnvLocal) console.log(' - .env.local');
|
|
2091
2160
|
console.log('');
|
|
2092
2161
|
}
|
|
@@ -2162,7 +2231,6 @@ async function promptForFileOverwrite(question, fileType, exists, skipFiles) {
|
|
|
2162
2231
|
|
|
2163
2232
|
const fileLabels = {
|
|
2164
2233
|
agentsMd: { prompt: 'Found existing AGENTS.md. Overwrite?', message: 'AGENTS.md', key: 'agentsMd' },
|
|
2165
|
-
claudeCommands: { prompt: 'Found existing .claude/commands/. Overwrite?', message: '.claude/commands/', key: 'claudeCommands' }
|
|
2166
2234
|
};
|
|
2167
2235
|
|
|
2168
2236
|
const config = fileLabels[fileType];
|
|
@@ -2182,7 +2250,7 @@ async function promptForFileOverwrite(question, fileType, exists, skipFiles) {
|
|
|
2182
2250
|
}
|
|
2183
2251
|
}
|
|
2184
2252
|
|
|
2185
|
-
// Default behavior: Binary y/n for files with markers
|
|
2253
|
+
// Default behavior: Binary y/n for files with markers
|
|
2186
2254
|
const overwrite = await askYesNo(question, config.prompt, true);
|
|
2187
2255
|
if (overwrite) {
|
|
2188
2256
|
console.log(` Will overwrite ${config.message}`);
|
|
@@ -2334,7 +2402,7 @@ async function installAgentsMd(skipFiles) {
|
|
|
2334
2402
|
return;
|
|
2335
2403
|
}
|
|
2336
2404
|
|
|
2337
|
-
const agentsSrc = path.join(packageDir, 'AGENTS.md');
|
|
2405
|
+
const agentsSrc = path.join(getPackageRoot(packageDir), 'AGENTS.md');
|
|
2338
2406
|
const agentsDest = path.join(projectRoot, 'AGENTS.md');
|
|
2339
2407
|
|
|
2340
2408
|
// Try smart merge if file exists
|
|
@@ -2372,28 +2440,6 @@ async function installAgentsMd(skipFiles) {
|
|
|
2372
2440
|
*/
|
|
2373
2441
|
|
|
2374
2442
|
|
|
2375
|
-
/**
|
|
2376
|
-
* Load Claude commands for conversion
|
|
2377
|
-
*/
|
|
2378
|
-
function loadClaudeCommands(selectedAgents) {
|
|
2379
|
-
const claudeCommands = {};
|
|
2380
|
-
const needsClaudeCommands = selectedAgents.includes('claude') ||
|
|
2381
|
-
selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands);
|
|
2382
|
-
|
|
2383
|
-
if (!needsClaudeCommands) {
|
|
2384
|
-
return claudeCommands;
|
|
2385
|
-
}
|
|
2386
|
-
|
|
2387
|
-
getWorkflowCommands().forEach(cmd => {
|
|
2388
|
-
const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
|
|
2389
|
-
const content = readFile(cmdPath);
|
|
2390
|
-
if (content) {
|
|
2391
|
-
claudeCommands[`${cmd}.md`] = content;
|
|
2392
|
-
}
|
|
2393
|
-
});
|
|
2394
|
-
|
|
2395
|
-
return claudeCommands;
|
|
2396
|
-
}
|
|
2397
2443
|
|
|
2398
2444
|
/**
|
|
2399
2445
|
* Setup agents with progress indication
|
|
@@ -2405,8 +2451,8 @@ function loadClaudeCommands(selectedAgents) {
|
|
|
2405
2451
|
* Setup agents with progress indication
|
|
2406
2452
|
* Delegates to setupSelectedAgents to avoid duplicate implementations (S4144)
|
|
2407
2453
|
*/
|
|
2408
|
-
async function setupAgentsWithProgress(selectedAgents,
|
|
2409
|
-
await setupSelectedAgents(selectedAgents,
|
|
2454
|
+
async function setupAgentsWithProgress(selectedAgents, skipFiles) {
|
|
2455
|
+
await setupSelectedAgents(selectedAgents, skipFiles);
|
|
2410
2456
|
}
|
|
2411
2457
|
|
|
2412
2458
|
/**
|
|
@@ -2427,26 +2473,19 @@ function displaySetupSummary(selectedAgents) {
|
|
|
2427
2473
|
console.log('What\'s installed:');
|
|
2428
2474
|
console.log(' - AGENTS.md (universal instructions)');
|
|
2429
2475
|
|
|
2430
|
-
const workflowCount = getWorkflowCommands().length;
|
|
2431
2476
|
selectedAgents.forEach(key => {
|
|
2432
2477
|
const agent = AGENTS[key];
|
|
2433
2478
|
if (agent.linkFile) {
|
|
2434
2479
|
console.log(` - ${agent.linkFile} (${agent.name})`);
|
|
2435
2480
|
}
|
|
2436
|
-
if (agent.hasCommands && key === 'claude') {
|
|
2437
|
-
console.log(` - .claude/commands/ (${workflowCount} workflow commands)`);
|
|
2438
|
-
} else if (agent.hasCommands && key !== 'codex' && agent.dirs[0]) {
|
|
2439
|
-
console.log(` - ${agent.dirs[0]}/ (${workflowCount} workflow commands)`);
|
|
2440
|
-
}
|
|
2441
2481
|
if (key === 'codex') {
|
|
2442
|
-
const skillCount = CODEX_SETUP_REPORT?.skillCount ?? listCodexSkillEntries(packageDir).length;
|
|
2482
|
+
const skillCount = CODEX_SETUP_REPORT?.skillCount ?? listCodexSkillEntries(getPackageRoot(packageDir)).length;
|
|
2443
2483
|
const codexRoot = CODEX_SETUP_REPORT?.installRoot || formatCodexSkillsInstallDir({ env: process.env, homeDir: os.homedir() });
|
|
2444
2484
|
console.log(` - ${codexRoot}/<stage>/SKILL.md (${skillCount} stage skills)`);
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
}
|
|
2485
|
+
const repoSkillCount = CODEX_SETUP_REPORT?.repoSkillCount ?? 0;
|
|
2486
|
+
console.log(` - ${CODEX_REPO_SKILLS_DIR}/<skill>/SKILL.md (${repoSkillCount} repo-local skills — commit for teammate discovery)`);
|
|
2487
|
+
} else if (agent.hasSkill && agent.skillsDir) {
|
|
2488
|
+
console.log(` - ${agent.skillsDir}/<skill>/SKILL.md`);
|
|
2450
2489
|
}
|
|
2451
2490
|
});
|
|
2452
2491
|
|
|
@@ -2475,13 +2514,11 @@ function displaySetupSummary(selectedAgents) {
|
|
|
2475
2514
|
console.log('');
|
|
2476
2515
|
printSetupNotes();
|
|
2477
2516
|
|
|
2478
|
-
//
|
|
2479
|
-
if (
|
|
2480
|
-
console.log(' ✓
|
|
2481
|
-
} else if (checkForBeads()) {
|
|
2482
|
-
console.log(' ! Beads available - Run: bd init');
|
|
2517
|
+
// Issue store status — the Kernel store is always present (auto-provisioned)
|
|
2518
|
+
if (KERNEL_STORE_READY) {
|
|
2519
|
+
console.log(' ✓ Kernel issue store - Track work: forge ready');
|
|
2483
2520
|
} else {
|
|
2484
|
-
console.log(
|
|
2521
|
+
console.log(' ⚠ Kernel issue store - not provisioned; run: forge doctor');
|
|
2485
2522
|
}
|
|
2486
2523
|
|
|
2487
2524
|
// Skills status
|
|
@@ -2552,12 +2589,10 @@ async function _interactiveSetup() {
|
|
|
2552
2589
|
// Track which files to skip based on user choices
|
|
2553
2590
|
const skipFiles = {
|
|
2554
2591
|
agentsMd: false,
|
|
2555
|
-
claudeCommands: false
|
|
2556
2592
|
};
|
|
2557
2593
|
|
|
2558
2594
|
// Ask about overwriting existing files
|
|
2559
2595
|
await promptForFileOverwrite(question, 'agentsMd', projectStatus.hasAgentsMd, skipFiles);
|
|
2560
|
-
await promptForFileOverwrite(question, 'claudeCommands', projectStatus.hasClaudeCommands, skipFiles);
|
|
2561
2596
|
|
|
2562
2597
|
if (projectStatus.type !== 'fresh') {
|
|
2563
2598
|
console.log('');
|
|
@@ -2580,19 +2615,13 @@ async function _interactiveSetup() {
|
|
|
2580
2615
|
setupCoreDocs();
|
|
2581
2616
|
console.log('');
|
|
2582
2617
|
|
|
2583
|
-
//
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
// First ensure Claude is set up
|
|
2587
|
-
if (selectedAgents.includes('claude')) {
|
|
2588
|
-
await setupAgent('claude', null, skipFiles);
|
|
2589
|
-
}
|
|
2590
|
-
// Then load the commands
|
|
2591
|
-
claudeCommands = loadClaudeCommands(selectedAgents);
|
|
2618
|
+
// Setup Claude first if selected, then setup remaining agents
|
|
2619
|
+
if (selectedAgents.includes('claude')) {
|
|
2620
|
+
await setupAgent('claude', skipFiles);
|
|
2592
2621
|
}
|
|
2593
2622
|
|
|
2594
2623
|
// Setup each selected agent with progress indication
|
|
2595
|
-
await setupAgentsWithProgress(selectedAgents,
|
|
2624
|
+
await setupAgentsWithProgress(selectedAgents, skipFiles);
|
|
2596
2625
|
|
|
2597
2626
|
// =============================================
|
|
2598
2627
|
// STEP 2: Project Tools Setup
|
|
@@ -2615,6 +2644,9 @@ async function _interactiveSetup() {
|
|
|
2615
2644
|
// Final Summary
|
|
2616
2645
|
// =============================================
|
|
2617
2646
|
displaySetupSummary(selectedAgents);
|
|
2647
|
+
|
|
2648
|
+
// One-step onboarding: run `forge init` when config is still absent (ac0b38c7).
|
|
2649
|
+
await finalizeWorkflowConfig();
|
|
2618
2650
|
}
|
|
2619
2651
|
|
|
2620
2652
|
// Parse CLI flags
|
|
@@ -2671,55 +2703,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,12 +2782,14 @@ 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
2794
|
} catch (err) {
|
|
2738
2795
|
console.warn('Lefthook installation failed:', err.message);
|
|
@@ -2751,6 +2808,48 @@ function installGitHooks() { // NOSONAR — Extracted as-is from bin/forge.js; c
|
|
|
2751
2808
|
console.log(' You can install manually later with: lefthook install');
|
|
2752
2809
|
console.log('');
|
|
2753
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
|
+
}
|
|
2754
2853
|
}
|
|
2755
2854
|
|
|
2756
2855
|
// Check if lefthook is already installed in project (delegates to lib/lefthook-check)
|
|
@@ -2795,130 +2894,34 @@ function repairDeclaredLefthookDependency(selectedAgents) {
|
|
|
2795
2894
|
}
|
|
2796
2895
|
}
|
|
2797
2896
|
|
|
2798
|
-
//
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
//
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
} catch (err) {
|
|
2808
|
-
// Not global
|
|
2809
|
-
console.warn('Beads not found globally:', err.message);
|
|
2810
|
-
}
|
|
2811
|
-
|
|
2812
|
-
// Check if bunx can run it
|
|
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() {
|
|
2813
2906
|
try {
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
// Check local project installation
|
|
2822
|
-
const pkgPath = path.join(projectRoot, 'package.json');
|
|
2823
|
-
if (!fs.existsSync(pkgPath)) return null;
|
|
2824
|
-
|
|
2825
|
-
try {
|
|
2826
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
2827
|
-
const isInstalled = pkg.devDependencies?.['@beads/bd'] || pkg.dependencies?.['@beads/bd'];
|
|
2828
|
-
return isInstalled ? 'local' : null;
|
|
2829
|
-
} catch (err) {
|
|
2830
|
-
console.warn('Failed to check Beads in package.json:', err.message);
|
|
2831
|
-
return null;
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
// Check if Beads is initialized in project — delegates to lib/beads-setup
|
|
2835
|
-
|
|
2836
|
-
// Check if Beads is initialized in project — delegates to lib/beads-setup
|
|
2837
|
-
function isBeadsInitialized() {
|
|
2838
|
-
return beadsSetupLib.isBeadsInitialized(projectRoot);
|
|
2839
|
-
}
|
|
2840
|
-
|
|
2841
|
-
// Initialize Beads in the project using the defensive safeBeadsInit wrapper
|
|
2842
|
-
// Handles config/gitignore writes, hook snapshot/restore, and JSONL pre-seeding
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
// Initialize Beads in the project using the defensive safeBeadsInit wrapper
|
|
2846
|
-
// Handles config/gitignore writes, hook snapshot/restore, and JSONL pre-seeding
|
|
2847
|
-
function initializeBeads(installType) {
|
|
2848
|
-
console.log('Initializing Beads in project...');
|
|
2849
|
-
|
|
2850
|
-
// Build the execBdInit function based on installType
|
|
2851
|
-
const execBdInit = (root) => {
|
|
2852
|
-
// SECURITY: execFileSync with hardcoded commands
|
|
2853
|
-
if (installType === 'global') {
|
|
2854
|
-
secureExecFileSync('bd', ['init'], { stdio: 'inherit', cwd: root });
|
|
2855
|
-
} else if (installType === 'bunx') {
|
|
2856
|
-
secureExecFileSync('bunx', ['@beads/bd', 'init'], { stdio: 'inherit', cwd: root });
|
|
2857
|
-
} else if (installType === 'local') {
|
|
2858
|
-
secureExecFileSync('npx', ['bd', 'init'], { stdio: 'inherit', cwd: root });
|
|
2859
|
-
}
|
|
2860
|
-
};
|
|
2861
|
-
|
|
2862
|
-
// Derive prefix from package.json name or directory name
|
|
2863
|
-
let prefix;
|
|
2864
|
-
try {
|
|
2865
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
|
|
2866
|
-
prefix = pkg.name || path.basename(projectRoot);
|
|
2867
|
-
} catch (_e) { // NOSONAR — fallback to directory name if package.json unreadable
|
|
2868
|
-
prefix = path.basename(projectRoot);
|
|
2869
|
-
}
|
|
2870
|
-
|
|
2871
|
-
try {
|
|
2872
|
-
const result = beadsSetupLib.safeBeadsInit(projectRoot, {
|
|
2873
|
-
prefix,
|
|
2874
|
-
execBdInit,
|
|
2875
|
-
restoreLefthook: (root) => {
|
|
2876
|
-
try {
|
|
2877
|
-
secureExecFileSync('lefthook', ['install'], { stdio: 'ignore', cwd: root });
|
|
2878
|
-
} catch (_e) { // NOSONAR — lefthook may not be installed yet, non-fatal
|
|
2879
|
-
// lefthook may not be installed yet — non-fatal
|
|
2880
|
-
}
|
|
2881
|
-
}
|
|
2882
|
-
});
|
|
2883
|
-
|
|
2884
|
-
if (result.skipped) {
|
|
2885
|
-
console.log(' ✓ Beads already initialized');
|
|
2886
|
-
return true;
|
|
2887
|
-
}
|
|
2888
|
-
|
|
2889
|
-
if (!result.success) {
|
|
2890
|
-
for (const e of result.errors) {
|
|
2891
|
-
console.log(` ⚠ ${e}`);
|
|
2892
|
-
}
|
|
2893
|
-
console.log(' Run manually: bd init');
|
|
2894
|
-
return false;
|
|
2895
|
-
}
|
|
2896
|
-
|
|
2897
|
-
for (const w of result.warnings) {
|
|
2898
|
-
console.warn(` ⚠ ${w}`);
|
|
2899
|
-
}
|
|
2900
|
-
console.log(' ✓ Beads initialized');
|
|
2901
|
-
|
|
2902
|
-
// Run post-init health check (non-fatal)
|
|
2903
|
-
try {
|
|
2904
|
-
const health = beadsHealthCheck(projectRoot);
|
|
2905
|
-
if (health.healthy) {
|
|
2906
|
-
console.log(' ✓ Beads health check passed');
|
|
2907
|
-
} else {
|
|
2908
|
-
console.log(` ⚠ Beads health check failed at ${health.failedStep}: ${health.error}`);
|
|
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.
|
|
2909
2914
|
}
|
|
2910
|
-
} catch (_healthErr) { // NOSONAR — health check is best-effort, non-fatal
|
|
2911
|
-
// Health check is best-effort — don't block setup
|
|
2912
2915
|
}
|
|
2913
|
-
|
|
2916
|
+
KERNEL_STORE_READY = true;
|
|
2914
2917
|
return true;
|
|
2915
2918
|
} catch (err) {
|
|
2916
|
-
|
|
2917
|
-
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.`);
|
|
2918
2922
|
return false;
|
|
2919
2923
|
}
|
|
2920
2924
|
}
|
|
2921
|
-
|
|
2922
2925
|
// Check if Skills CLI is installed
|
|
2923
2926
|
|
|
2924
2927
|
|
|
@@ -2984,59 +2987,18 @@ function initializeSkills(installType) {
|
|
|
2984
2987
|
}
|
|
2985
2988
|
}
|
|
2986
2989
|
|
|
2987
|
-
//
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
// Prompt for Beads setup - extracted to reduce cognitive complexity
|
|
2991
|
-
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) {
|
|
2992
2993
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2993
|
-
console.log('
|
|
2994
|
+
console.log('Issue Store (Forge Kernel)');
|
|
2994
2995
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
2995
2996
|
console.log('');
|
|
2996
2997
|
|
|
2997
|
-
const
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
if (beadsInitialized) {
|
|
3001
|
-
console.log('✓ Beads is already initialized in this project');
|
|
3002
|
-
console.log('');
|
|
3003
|
-
return;
|
|
3004
|
-
}
|
|
3005
|
-
|
|
3006
|
-
if (beadsStatus) {
|
|
3007
|
-
// Already installed, just need to initialize
|
|
3008
|
-
console.log(`ℹ Beads is installed (${beadsStatus}), but not initialized`);
|
|
3009
|
-
const initBeads = await question('Initialize Beads in this project? (y/n): ');
|
|
3010
|
-
|
|
3011
|
-
if (initBeads.toLowerCase() === 'y') {
|
|
3012
|
-
initializeBeads(beadsStatus);
|
|
3013
|
-
} else {
|
|
3014
|
-
console.log('Skipped Beads initialization. Run manually: bd init');
|
|
3015
|
-
}
|
|
3016
|
-
console.log('');
|
|
3017
|
-
return;
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
// Not installed
|
|
3021
|
-
console.log('ℹ Beads is not installed');
|
|
3022
|
-
const installBeads = await question('Install Beads? (y/n): ');
|
|
3023
|
-
|
|
3024
|
-
if (installBeads.toLowerCase() !== 'y') {
|
|
3025
|
-
console.log('Skipped Beads installation');
|
|
3026
|
-
console.log('');
|
|
3027
|
-
return;
|
|
2998
|
+
const ready = await ensureKernelIssueStore();
|
|
2999
|
+
if (ready) {
|
|
3000
|
+
console.log('✓ Kernel issue store ready (single-machine; team sync not configured)');
|
|
3028
3001
|
}
|
|
3029
|
-
|
|
3030
|
-
console.log('');
|
|
3031
|
-
console.log('Choose installation method:');
|
|
3032
|
-
console.log(' 1. Global (recommended) - Available system-wide');
|
|
3033
|
-
console.log(' 2. Local - Project-specific devDependency');
|
|
3034
|
-
console.log(' 3. Bunx - Use via bunx (requires bun)');
|
|
3035
|
-
console.log('');
|
|
3036
|
-
const method = await question('Choose method (1-3): ');
|
|
3037
|
-
|
|
3038
|
-
console.log('');
|
|
3039
|
-
installBeadsWithMethod(method);
|
|
3040
3002
|
console.log('');
|
|
3041
3003
|
}
|
|
3042
3004
|
|
|
@@ -3056,72 +3018,6 @@ function installViaBunx(packageName, versionArgs, initFn, toolName) {
|
|
|
3056
3018
|
}
|
|
3057
3019
|
}
|
|
3058
3020
|
|
|
3059
|
-
// Helper: Install Beads with chosen method - extracted to reduce cognitive complexity
|
|
3060
|
-
// SECURITY NOTE: Downloads and executes a remote PowerShell script.
|
|
3061
|
-
// The npm @beads/bd package is broken on Windows (GitHub Issue #1031, closed "not planned"),
|
|
3062
|
-
// so the official PowerShell installer is the only supported path.
|
|
3063
|
-
// Mitigations: HTTPS transport (prevents MITM), official beads repo, user-visible URL.
|
|
3064
|
-
// Follow-up: pin to a versioned release tag once beads publishes tagged releases (for example v0.49.1).
|
|
3065
|
-
const BEADS_INSTALL_PS1_URL = 'https://raw.githubusercontent.com/steveyegge/beads/main/install.ps1';
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
function installBeadsOnWindows() {
|
|
3070
|
-
console.log(' (Windows detected: using PowerShell installer)');
|
|
3071
|
-
console.log(` Downloading: ${BEADS_INSTALL_PS1_URL}`);
|
|
3072
|
-
secureExecFileSync('powershell.exe', [
|
|
3073
|
-
'-NoProfile', '-NonInteractive', '-Command',
|
|
3074
|
-
`irm ${BEADS_INSTALL_PS1_URL} | iex`
|
|
3075
|
-
], { stdio: 'inherit' });
|
|
3076
|
-
}
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
function installBeadsWithMethod(method) { // NOSONAR — Extracted as-is from bin/forge.js; complexity reduction deferred
|
|
3081
|
-
try {
|
|
3082
|
-
// SECURITY: secureExecFileSync with hardcoded commands
|
|
3083
|
-
if (method === '1') {
|
|
3084
|
-
console.log('Installing Beads globally...');
|
|
3085
|
-
if (process.platform === 'win32') {
|
|
3086
|
-
installBeadsOnWindows();
|
|
3087
|
-
} else {
|
|
3088
|
-
const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
|
|
3089
|
-
secureExecFileSync(pkgManager, ['install', '-g', '@beads/bd'], { stdio: 'inherit' });
|
|
3090
|
-
}
|
|
3091
|
-
console.log(' ✓ Beads installed globally');
|
|
3092
|
-
initializeBeads('global');
|
|
3093
|
-
} else if (method === '2') {
|
|
3094
|
-
console.log('Installing Beads locally...');
|
|
3095
|
-
// On Windows, npm postinstall for @beads/bd runs Expand-Archive which has EPERM file-locking
|
|
3096
|
-
// (GitHub Issue #1031, closed "not planned") — same root cause as global install.
|
|
3097
|
-
// Redirect Windows users to the global PowerShell installer instead.
|
|
3098
|
-
if (process.platform === 'win32') {
|
|
3099
|
-
console.log(' ⚠ Local install not supported on Windows (npm @beads/bd EPERM issue).');
|
|
3100
|
-
console.log(' Falling back to global PowerShell installer...');
|
|
3101
|
-
installBeadsOnWindows();
|
|
3102
|
-
} else {
|
|
3103
|
-
const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
|
|
3104
|
-
secureExecFileSync(pkgManager, ['install', '-D', '@beads/bd'], { stdio: 'inherit', cwd: projectRoot });
|
|
3105
|
-
}
|
|
3106
|
-
console.log(' ✓ Beads installed');
|
|
3107
|
-
// On Windows the fallback was global (PowerShell installer), so init as 'global'
|
|
3108
|
-
initializeBeads(process.platform === 'win32' ? 'global' : 'local');
|
|
3109
|
-
} else if (method === '3') {
|
|
3110
|
-
installViaBunx('@beads/bd', ['version'], initializeBeads, 'Beads');
|
|
3111
|
-
} else {
|
|
3112
|
-
console.log('Invalid choice. Skipping Beads installation.');
|
|
3113
|
-
}
|
|
3114
|
-
} catch (err) {
|
|
3115
|
-
console.warn('Beads installation failed:', err.message);
|
|
3116
|
-
console.log(' ⚠ Failed to install Beads:', err.message);
|
|
3117
|
-
if (process.platform === 'win32') {
|
|
3118
|
-
console.log(` Run manually: irm ${BEADS_INSTALL_PS1_URL} | iex`);
|
|
3119
|
-
} else {
|
|
3120
|
-
console.log(` Run manually: ${PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g'} @beads/bd && bd init`);
|
|
3121
|
-
}
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
3021
|
// Helper: Get package-manager-specific install args for Skills
|
|
3126
3022
|
|
|
3127
3023
|
|
|
@@ -3231,7 +3127,7 @@ async function setupProjectTools(rl, question) {
|
|
|
3231
3127
|
console.log('');
|
|
3232
3128
|
console.log('Forge recommends three tools for enhanced workflows:');
|
|
3233
3129
|
console.log('');
|
|
3234
|
-
console.log('•
|
|
3130
|
+
console.log('• Issue tracking (Forge Kernel) - zero-install, git-backed');
|
|
3235
3131
|
console.log(' Persists tasks across sessions, tracks dependencies.');
|
|
3236
3132
|
console.log(' Command: forge ready, forge create, forge close');
|
|
3237
3133
|
console.log('');
|
|
@@ -3245,44 +3141,6 @@ async function setupProjectTools(rl, question) {
|
|
|
3245
3141
|
await promptSkillsSetup(question);
|
|
3246
3142
|
}
|
|
3247
3143
|
|
|
3248
|
-
// Auto-setup Beads in quick mode - extracted to reduce cognitive complexity
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
// Auto-setup Beads in quick mode - extracted to reduce cognitive complexity
|
|
3252
|
-
function autoSetupBeadsInQuickMode() { // NOSONAR — Extracted as-is from bin/forge.js; complexity reduction deferred
|
|
3253
|
-
const beadsStatus = checkForBeads();
|
|
3254
|
-
const beadsInitialized = isBeadsInitialized();
|
|
3255
|
-
|
|
3256
|
-
if (!beadsInitialized && beadsStatus) {
|
|
3257
|
-
console.log('📦 Initializing Beads...');
|
|
3258
|
-
initializeBeads(beadsStatus);
|
|
3259
|
-
console.log('');
|
|
3260
|
-
} else if (!beadsInitialized && !beadsStatus) {
|
|
3261
|
-
console.log('📦 Installing Beads globally...');
|
|
3262
|
-
try {
|
|
3263
|
-
// SECURITY: use PowerShell on Windows (npm @beads/bd is broken on Windows - Issue #1031)
|
|
3264
|
-
if (process.platform === 'win32') {
|
|
3265
|
-
installBeadsOnWindows();
|
|
3266
|
-
} else {
|
|
3267
|
-
const pkgManager = PKG_MANAGER === 'bun' ? 'bun' : 'npm';
|
|
3268
|
-
secureExecFileSync(pkgManager, ['install', '-g', '@beads/bd'], { stdio: 'inherit' });
|
|
3269
|
-
}
|
|
3270
|
-
console.log(' ✓ Beads installed globally');
|
|
3271
|
-
initializeBeads('global');
|
|
3272
|
-
} catch (err) {
|
|
3273
|
-
// Installation failed - provide manual instructions
|
|
3274
|
-
console.log(' ⚠ Could not install Beads automatically');
|
|
3275
|
-
console.log(` Error: ${err.message}`);
|
|
3276
|
-
if (process.platform === 'win32') {
|
|
3277
|
-
console.log(` Run manually: irm ${BEADS_INSTALL_PS1_URL} | iex`);
|
|
3278
|
-
} else {
|
|
3279
|
-
console.log(` Run manually: ${PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g'} @beads/bd && bd init`);
|
|
3280
|
-
}
|
|
3281
|
-
}
|
|
3282
|
-
console.log('');
|
|
3283
|
-
}
|
|
3284
|
-
}
|
|
3285
|
-
|
|
3286
3144
|
// Helper: Auto-install lefthook if not present - extracted to reduce cognitive complexity
|
|
3287
3145
|
|
|
3288
3146
|
|
|
@@ -3326,6 +3184,16 @@ function autoInstallLefthook() { // NOSONAR — Extracted as-is from bin/forge.j
|
|
|
3326
3184
|
return;
|
|
3327
3185
|
}
|
|
3328
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
|
+
|
|
3329
3197
|
// Not in package.json at all — full install
|
|
3330
3198
|
console.log('📦 Installing lefthook for git hooks...');
|
|
3331
3199
|
try {
|
|
@@ -3341,32 +3209,13 @@ function autoInstallLefthook() { // NOSONAR — Extracted as-is from bin/forge.j
|
|
|
3341
3209
|
console.log('');
|
|
3342
3210
|
}
|
|
3343
3211
|
|
|
3344
|
-
// Helper: Verify a tool is callable after install - extracted to reduce cognitive complexity
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
// Helper: Verify a tool is callable after install - extracted to reduce cognitive complexity
|
|
3348
|
-
function verifyToolInstall(command, args, toolName) {
|
|
3349
|
-
try {
|
|
3350
|
-
secureExecFileSync(command, args, { stdio: 'ignore' });
|
|
3351
|
-
return true;
|
|
3352
|
-
} catch (_err) { // NOSONAR - S2486: Intentionally ignored; verification failure is handled by caller
|
|
3353
|
-
console.log(` ⚠ ${toolName} installed but not callable. Check your PATH.`);
|
|
3354
|
-
return false;
|
|
3355
|
-
}
|
|
3356
|
-
}
|
|
3357
|
-
|
|
3358
3212
|
// Helper: Auto-setup tools (Skills) in quick mode - extracted to reduce cognitive complexity
|
|
3359
3213
|
|
|
3360
3214
|
|
|
3361
3215
|
// Helper: Auto-setup tools (Skills) in quick mode - extracted to reduce cognitive complexity
|
|
3362
|
-
function autoSetupToolsInQuickMode() {
|
|
3363
|
-
//
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
// Post-install verification for Beads
|
|
3367
|
-
if (isBeadsInitialized()) {
|
|
3368
|
-
verifyToolInstall('bd', ['version'], 'Beads');
|
|
3369
|
-
}
|
|
3216
|
+
async function autoSetupToolsInQuickMode() {
|
|
3217
|
+
// Issue store: ensure the Forge Kernel store exists (auto-provisioned, no CLI)
|
|
3218
|
+
await ensureKernelIssueStore();
|
|
3370
3219
|
|
|
3371
3220
|
// Skills: only initialize if already installed (recommended tool)
|
|
3372
3221
|
const skillsStatus = checkForSkills();
|
|
@@ -3374,11 +3223,10 @@ function autoSetupToolsInQuickMode() {
|
|
|
3374
3223
|
console.log('📦 Initializing Skills...');
|
|
3375
3224
|
initializeSkills(skillsStatus);
|
|
3376
3225
|
console.log('');
|
|
3377
|
-
} else if (!skillsStatus) {
|
|
3378
|
-
const installCmd = PKG_MANAGER === 'bun' ? 'bun add -g' : 'npm install -g';
|
|
3379
|
-
console.log(` ℹ Skills not found — install with: ${installCmd} @forge/skills`);
|
|
3380
|
-
console.log('');
|
|
3381
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).
|
|
3382
3230
|
}
|
|
3383
3231
|
|
|
3384
3232
|
// Helper: Configure default external services in quick mode - extracted to reduce cognitive complexity
|
|
@@ -3410,6 +3258,73 @@ function configureDefaultExternalServices(skipExternal) {
|
|
|
3410
3258
|
console.log('Configuration saved to .env.local');
|
|
3411
3259
|
}
|
|
3412
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
|
+
|
|
3413
3328
|
// Quick setup with defaults
|
|
3414
3329
|
|
|
3415
3330
|
|
|
@@ -3428,8 +3343,11 @@ async function quickSetup(selectedAgents, skipExternal) {
|
|
|
3428
3343
|
});
|
|
3429
3344
|
console.log('');
|
|
3430
3345
|
|
|
3431
|
-
// Copy AGENTS.md (actionLog tracks it via copyFile)
|
|
3432
|
-
|
|
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();
|
|
3433
3351
|
copyFile(agentsSrc, 'AGENTS.md');
|
|
3434
3352
|
console.log('');
|
|
3435
3353
|
|
|
@@ -3442,20 +3360,26 @@ async function quickSetup(selectedAgents, skipExternal) {
|
|
|
3442
3360
|
// Auto-install lefthook if missing
|
|
3443
3361
|
autoInstallLefthook();
|
|
3444
3362
|
|
|
3445
|
-
// Auto-setup project tools (
|
|
3446
|
-
autoSetupToolsInQuickMode();
|
|
3363
|
+
// Auto-setup project tools (Kernel issue store, Skills)
|
|
3364
|
+
await autoSetupToolsInQuickMode();
|
|
3447
3365
|
|
|
3448
|
-
//
|
|
3449
|
-
|
|
3450
|
-
|
|
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);
|
|
3451
3374
|
ensureWorkflowRuntimeAssets(selectedAgents);
|
|
3452
3375
|
|
|
3453
3376
|
// Detect Husky and migrate before installing Lefthook hooks
|
|
3454
3377
|
await handleHuskyMigration();
|
|
3455
3378
|
|
|
3456
|
-
// 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.
|
|
3457
3381
|
console.log('');
|
|
3458
|
-
installGitHooks();
|
|
3382
|
+
installGitHooks({ loud: true });
|
|
3459
3383
|
|
|
3460
3384
|
// Configure external services with defaults (unless skipped)
|
|
3461
3385
|
configureDefaultExternalServices(skipExternal);
|
|
@@ -3469,6 +3393,9 @@ async function quickSetup(selectedAgents, skipExternal) {
|
|
|
3469
3393
|
console.log('');
|
|
3470
3394
|
console.log(renderSetupSummary(actionLog, selectedAgents, VERBOSE_MODE, { status: getSetupSummaryStatus() }));
|
|
3471
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 });
|
|
3472
3399
|
console.log('');
|
|
3473
3400
|
}
|
|
3474
3401
|
|
|
@@ -3509,7 +3436,7 @@ function setupAgentsMdFile(flags, skipFiles) {
|
|
|
3509
3436
|
return;
|
|
3510
3437
|
}
|
|
3511
3438
|
|
|
3512
|
-
const agentsSrc = path.join(packageDir, 'AGENTS.md');
|
|
3439
|
+
const agentsSrc = path.join(getPackageRoot(packageDir), 'AGENTS.md');
|
|
3513
3440
|
const agentsDest = path.join(projectRoot, 'AGENTS.md');
|
|
3514
3441
|
const mergeStrategy = flags.merge || 'smart';
|
|
3515
3442
|
|
|
@@ -3589,7 +3516,6 @@ function displayExistingInstallation(projectStatus) {
|
|
|
3589
3516
|
: 'Found partial installation:');
|
|
3590
3517
|
|
|
3591
3518
|
if (projectStatus.hasAgentsMd) console.log(' - AGENTS.md');
|
|
3592
|
-
if (projectStatus.hasClaudeCommands) console.log(' - .claude/commands/');
|
|
3593
3519
|
if (projectStatus.hasEnvLocal) console.log(' - .env.local');
|
|
3594
3520
|
console.log('');
|
|
3595
3521
|
}
|
|
@@ -3601,7 +3527,6 @@ function displayExistingInstallation(projectStatus) {
|
|
|
3601
3527
|
async function promptForOverwriteDecisions(question, projectStatus, flags = {}) {
|
|
3602
3528
|
const skipFiles = {
|
|
3603
3529
|
agentsMd: false,
|
|
3604
|
-
claudeCommands: false
|
|
3605
3530
|
};
|
|
3606
3531
|
|
|
3607
3532
|
if (flags.keep) {
|
|
@@ -3609,10 +3534,6 @@ async function promptForOverwriteDecisions(question, projectStatus, flags = {})
|
|
|
3609
3534
|
skipFiles.agentsMd = true;
|
|
3610
3535
|
console.log(' Keeping existing AGENTS.md (--keep)');
|
|
3611
3536
|
}
|
|
3612
|
-
if (projectStatus.hasClaudeCommands) {
|
|
3613
|
-
skipFiles.claudeCommands = true;
|
|
3614
|
-
console.log(' Keeping existing .claude/commands/ (--keep)');
|
|
3615
|
-
}
|
|
3616
3537
|
return skipFiles;
|
|
3617
3538
|
}
|
|
3618
3539
|
|
|
@@ -3622,12 +3543,6 @@ async function promptForOverwriteDecisions(question, projectStatus, flags = {})
|
|
|
3622
3543
|
console.log(overwriteAgents ? ' Will overwrite AGENTS.md' : ' Keeping existing AGENTS.md');
|
|
3623
3544
|
}
|
|
3624
3545
|
|
|
3625
|
-
if (projectStatus.hasClaudeCommands) {
|
|
3626
|
-
const overwriteCommands = await askYesNo(question, 'Found existing .claude/commands/. Overwrite?', true);
|
|
3627
|
-
skipFiles.claudeCommands = !overwriteCommands;
|
|
3628
|
-
console.log(overwriteCommands ? ' Will overwrite .claude/commands/' : ' Keeping existing .claude/commands/');
|
|
3629
|
-
}
|
|
3630
|
-
|
|
3631
3546
|
if (projectStatus.type !== 'fresh') {
|
|
3632
3547
|
console.log('');
|
|
3633
3548
|
}
|
|
@@ -3635,47 +3550,17 @@ async function promptForOverwriteDecisions(question, projectStatus, flags = {})
|
|
|
3635
3550
|
return skipFiles;
|
|
3636
3551
|
}
|
|
3637
3552
|
|
|
3638
|
-
// Helper: Load and setup canonical commands - extracted to reduce cognitive complexity
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
// Helper: Load and setup canonical commands - extracted to reduce cognitive complexity
|
|
3642
|
-
async function loadAndSetupCanonicalCommands(selectedAgents, skipFiles) {
|
|
3643
|
-
const claudeCommands = {};
|
|
3644
|
-
const needsClaudeCommands = selectedAgents.includes('claude') ||
|
|
3645
|
-
selectedAgents.some(a => AGENTS[a].needsConversion || AGENTS[a].copyCommands);
|
|
3646
|
-
|
|
3647
|
-
if (!needsClaudeCommands) {
|
|
3648
|
-
return claudeCommands;
|
|
3649
|
-
}
|
|
3650
|
-
|
|
3651
|
-
// First ensure Claude is set up
|
|
3652
|
-
if (selectedAgents.includes('claude')) {
|
|
3653
|
-
await setupAgent('claude', null, skipFiles);
|
|
3654
|
-
}
|
|
3655
|
-
|
|
3656
|
-
// Then load the commands (from existing or newly created)
|
|
3657
|
-
getWorkflowCommands().forEach(cmd => {
|
|
3658
|
-
const cmdPath = path.join(projectRoot, `.claude/commands/${cmd}.md`);
|
|
3659
|
-
const content = readFile(cmdPath);
|
|
3660
|
-
if (content) {
|
|
3661
|
-
claudeCommands[`${cmd}.md`] = content;
|
|
3662
|
-
}
|
|
3663
|
-
});
|
|
3664
|
-
|
|
3665
|
-
return claudeCommands;
|
|
3666
|
-
}
|
|
3667
|
-
|
|
3668
3553
|
// Helper: Setup all selected agents - extracted to reduce cognitive complexity
|
|
3669
3554
|
|
|
3670
3555
|
|
|
3671
3556
|
// Helper: Setup all selected agents - extracted to reduce cognitive complexity
|
|
3672
|
-
async function setupSelectedAgents(selectedAgents,
|
|
3557
|
+
async function setupSelectedAgents(selectedAgents, skipFiles) {
|
|
3673
3558
|
const totalAgents = selectedAgents.length;
|
|
3674
3559
|
for (const [index, agentKey] of selectedAgents.entries()) {
|
|
3675
3560
|
const agent = AGENTS[agentKey];
|
|
3676
3561
|
console.log(`\n[${index + 1}/${totalAgents}] Setting up ${agent.name}...`);
|
|
3677
3562
|
if (agentKey !== 'claude') { // Claude already done above
|
|
3678
|
-
await setupAgent(agentKey,
|
|
3563
|
+
await setupAgent(agentKey, skipFiles);
|
|
3679
3564
|
}
|
|
3680
3565
|
}
|
|
3681
3566
|
|
|
@@ -3775,7 +3660,7 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3775
3660
|
const selectedAgents = await promptForAgentSelection(question, agentKeys);
|
|
3776
3661
|
|
|
3777
3662
|
// Check GitHub CLI prerequisite now that selectedAgents is known
|
|
3778
|
-
if (requiresGithubCliForSetup(selectedAgents
|
|
3663
|
+
if (requiresGithubCliForSetup(selectedAgents)) {
|
|
3779
3664
|
checkPrerequisites({ requireGithubCli: true });
|
|
3780
3665
|
}
|
|
3781
3666
|
|
|
@@ -3790,11 +3675,13 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3790
3675
|
setupCoreDocs();
|
|
3791
3676
|
console.log('');
|
|
3792
3677
|
|
|
3793
|
-
//
|
|
3794
|
-
|
|
3678
|
+
// Setup Claude first if selected (delegated to helper), then remaining agents
|
|
3679
|
+
if (selectedAgents.includes('claude')) {
|
|
3680
|
+
await setupAgent('claude', skipFiles);
|
|
3681
|
+
}
|
|
3795
3682
|
|
|
3796
3683
|
// Setup each selected agent with progress indication (delegated to helper)
|
|
3797
|
-
await setupSelectedAgents(selectedAgents,
|
|
3684
|
+
await setupSelectedAgents(selectedAgents, skipFiles);
|
|
3798
3685
|
ensureWorkflowRuntimeAssets(selectedAgents);
|
|
3799
3686
|
|
|
3800
3687
|
// Handle external services step (delegated to helper)
|
|
@@ -3805,6 +3692,10 @@ async function interactiveSetupWithFlags(flags) {
|
|
|
3805
3692
|
|
|
3806
3693
|
// Display final summary (delegated to helper)
|
|
3807
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();
|
|
3808
3699
|
}
|
|
3809
3700
|
|
|
3810
3701
|
// Main
|
|
@@ -3903,15 +3794,10 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3903
3794
|
addFileAction(dir + '/', 'Create agent directory');
|
|
3904
3795
|
}
|
|
3905
3796
|
|
|
3906
|
-
// 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.
|
|
3907
3799
|
if (agentKey === 'claude') {
|
|
3908
|
-
const cmds = getWorkflowCommands();
|
|
3909
|
-
for (const cmd of cmds) {
|
|
3910
|
-
addFileAction(`.claude/commands/${cmd}.md`, 'Workflow command');
|
|
3911
|
-
}
|
|
3912
|
-
addFileAction('.claude/rules/workflow.md', 'Workflow rules');
|
|
3913
3800
|
addFileAction('.claude/scripts/load-env.sh', 'Environment loader script');
|
|
3914
|
-
addFileAction('.claude/skills/forge-workflow/SKILL.md', 'Forge workflow skill');
|
|
3915
3801
|
addFileAction('.mcp.json', 'MCP server configuration');
|
|
3916
3802
|
addFileAction('CLAUDE.md', 'Claude root config (links to AGENTS.md)');
|
|
3917
3803
|
}
|
|
@@ -3922,7 +3808,7 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3922
3808
|
}
|
|
3923
3809
|
}
|
|
3924
3810
|
|
|
3925
|
-
// Cursor-specific files
|
|
3811
|
+
// Cursor-specific files (rendered from the canonical rules/ source)
|
|
3926
3812
|
if (agent.customSetup === 'cursor') {
|
|
3927
3813
|
addFileAction('.cursor/rules/forge-workflow.mdc', 'Cursor workflow rule');
|
|
3928
3814
|
addFileAction('.cursor/rules/tdd-enforcement.mdc', 'Cursor TDD rule');
|
|
@@ -3930,54 +3816,19 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3930
3816
|
addFileAction('.cursor/rules/documentation.mdc', 'Cursor documentation rule');
|
|
3931
3817
|
}
|
|
3932
3818
|
|
|
3933
|
-
if (agentKey === 'kilocode') {
|
|
3934
|
-
addFileAction('.kilocode/workflows/forge-workflow.md', 'Kilo native workflow');
|
|
3935
|
-
addFileAction('.kilocode/rules/workflow.md', 'Kilo native rules');
|
|
3936
|
-
addFileAction('.kilocode/skills/forge-workflow/SKILL.md', 'Kilo native skill');
|
|
3937
|
-
}
|
|
3938
|
-
|
|
3939
|
-
if (agent.customSetup === 'copilot') {
|
|
3940
|
-
addFileAction('.github/copilot-instructions.md', 'Copilot root instructions');
|
|
3941
|
-
addFileAction('.github/instructions/typescript.instructions.md', 'Copilot TypeScript instructions');
|
|
3942
|
-
addFileAction('.github/instructions/testing.instructions.md', 'Copilot testing instructions');
|
|
3943
|
-
addFileAction('.github/prompts/red.prompt.md', 'Copilot RED prompt');
|
|
3944
|
-
addFileAction('.github/prompts/green.prompt.md', 'Copilot GREEN prompt');
|
|
3945
|
-
}
|
|
3946
|
-
|
|
3947
|
-
if (agent.customSetup === 'opencode') {
|
|
3948
|
-
addFileAction('opencode.json', 'OpenCode root config');
|
|
3949
|
-
addFileAction('.opencode/agents/plan-review.md', 'OpenCode plan-review agent');
|
|
3950
|
-
addFileAction('.opencode/agents/tdd-build.md', 'OpenCode tdd-build agent');
|
|
3951
|
-
}
|
|
3952
|
-
|
|
3953
|
-
// Agent commands (converted from Claude format)
|
|
3954
|
-
if (agent.needsConversion || agent.copyCommands || agent.promptFormat) {
|
|
3955
|
-
const cmds = getWorkflowCommands();
|
|
3956
|
-
const targetDir = agent.dirs[0];
|
|
3957
|
-
for (const cmd of cmds) {
|
|
3958
|
-
const ext = agent.promptFormat ? '.prompt.md' : '.md';
|
|
3959
|
-
addFileAction(`${targetDir}/${cmd}${ext}`, 'Converted workflow command');
|
|
3960
|
-
}
|
|
3961
|
-
}
|
|
3962
|
-
|
|
3963
|
-
// Agent rules (copied from Claude)
|
|
3964
|
-
if (agent.needsConversion) {
|
|
3965
|
-
const rulesDir = agent.dirs.find(d => d.includes('/rules'));
|
|
3966
|
-
if (rulesDir) {
|
|
3967
|
-
addFileAction(`${rulesDir}/workflow.md`, 'Workflow rules');
|
|
3968
|
-
}
|
|
3969
|
-
}
|
|
3970
|
-
|
|
3971
3819
|
// Agent skill
|
|
3972
3820
|
if (agentKey === 'codex') {
|
|
3973
|
-
const skillEntries = buildCodexSkillInstallPlan(packageDir, { env: process.env, homeDir: os.homedir() });
|
|
3821
|
+
const skillEntries = buildCodexSkillInstallPlan(getPackageRoot(packageDir), { env: process.env, homeDir: os.homedir() });
|
|
3974
3822
|
for (const entry of skillEntries) {
|
|
3975
3823
|
addFileAction(entry.displayPath, 'Codex stage skill');
|
|
3976
3824
|
}
|
|
3977
|
-
} else if (agent.hasSkill) {
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
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
|
+
}
|
|
3981
3832
|
}
|
|
3982
3833
|
}
|
|
3983
3834
|
|
|
@@ -3990,6 +3841,7 @@ function dryRunSetup(agents) { // NOSONAR — Extracted as-is from bin/forge.js;
|
|
|
3990
3841
|
// Git hooks
|
|
3991
3842
|
addFileAction('lefthook.yml', 'Git hook configuration');
|
|
3992
3843
|
addFileAction('.forge/hooks/check-tdd.js', 'TDD enforcement hook');
|
|
3844
|
+
addFileAction('.forge/hooks/forge-native-hook.js', 'Native-hook enforcement adapter (Claude/Cursor)');
|
|
3993
3845
|
|
|
3994
3846
|
// Print dry-run summary
|
|
3995
3847
|
console.log('');
|
|
@@ -4012,7 +3864,7 @@ async function executeSetup(config) {
|
|
|
4012
3864
|
// Check prerequisites
|
|
4013
3865
|
checkPrerequisites({
|
|
4014
3866
|
requireBeadsCli: true,
|
|
4015
|
-
requireGithubCli: requiresGithubCliForSetup(agents
|
|
3867
|
+
requireGithubCli: requiresGithubCliForSetup(agents),
|
|
4016
3868
|
requireJq: true,
|
|
4017
3869
|
commandRunner,
|
|
4018
3870
|
});
|
|
@@ -4023,7 +3875,7 @@ async function executeSetup(config) {
|
|
|
4023
3875
|
if (fs.existsSync(agentsDest)) {
|
|
4024
3876
|
actionLog.add('AGENTS.md', 'skipped', 'already exists');
|
|
4025
3877
|
} else {
|
|
4026
|
-
const agentsSrc = path.join(packageDir, 'AGENTS.md');
|
|
3878
|
+
const agentsSrc = path.join(getPackageRoot(packageDir), 'AGENTS.md');
|
|
4027
3879
|
copyFile(agentsSrc, 'AGENTS.md');
|
|
4028
3880
|
}
|
|
4029
3881
|
console.log('');
|
|
@@ -4034,22 +3886,13 @@ async function executeSetup(config) {
|
|
|
4034
3886
|
|
|
4035
3887
|
const skipFiles = {
|
|
4036
3888
|
agentsMd: keepExisting && fs.existsSync(path.join(projectRoot, 'AGENTS.md')),
|
|
4037
|
-
claudeCommands: keepExisting && fs.existsSync(path.join(projectRoot, '.claude', 'commands'))
|
|
4038
3889
|
};
|
|
4039
3890
|
|
|
4040
|
-
if
|
|
4041
|
-
|
|
3891
|
+
// Setup Claude first if selected, then remaining agents
|
|
3892
|
+
if (agents.includes('claude')) {
|
|
3893
|
+
await setupAgent('claude', skipFiles);
|
|
4042
3894
|
}
|
|
4043
|
-
|
|
4044
|
-
// Load canonical commands — use loadAndSetupCanonicalCommands when claude is selected
|
|
4045
|
-
// so that .claude/commands/ are seeded before reading them
|
|
4046
|
-
const claudeCommands = agents.includes('claude')
|
|
4047
|
-
? await loadAndSetupCanonicalCommands(agents, skipFiles)
|
|
4048
|
-
: loadClaudeCommands(agents);
|
|
4049
|
-
|
|
4050
|
-
// Setup agents with progress output (setupSelectedAgents skips claude internally
|
|
4051
|
-
// since loadAndSetupCanonicalCommands already handled it above)
|
|
4052
|
-
await setupSelectedAgents(agents, claudeCommands, skipFiles);
|
|
3895
|
+
await setupSelectedAgents(agents, skipFiles);
|
|
4053
3896
|
ensureWorkflowRuntimeAssets(agents);
|
|
4054
3897
|
ensureWorkflowShellPolicy(agents);
|
|
4055
3898
|
repairDeclaredLefthookDependency(agents);
|
|
@@ -4057,9 +3900,13 @@ async function executeSetup(config) {
|
|
|
4057
3900
|
// Detect Husky and migrate before installing Lefthook hooks
|
|
4058
3901
|
await handleHuskyMigration();
|
|
4059
3902
|
|
|
4060
|
-
// 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.
|
|
4061
3905
|
console.log('');
|
|
4062
|
-
installGitHooks();
|
|
3906
|
+
installGitHooks({ loud: true });
|
|
3907
|
+
|
|
3908
|
+
// Auto-import an existing Beads store into the Kernel (idempotent, CLI-free)
|
|
3909
|
+
await autoMigrateBeadsToKernel();
|
|
4063
3910
|
|
|
4064
3911
|
// External services (unless skipped)
|
|
4065
3912
|
await handleExternalServices(skipExternal, agents);
|
|
@@ -4073,6 +3920,9 @@ async function executeSetup(config) {
|
|
|
4073
3920
|
console.log('');
|
|
4074
3921
|
console.log(renderSetupSummary(actionLog, agents, VERBOSE_MODE, { status: getSetupSummaryStatus() }));
|
|
4075
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 });
|
|
4076
3926
|
console.log('');
|
|
4077
3927
|
}
|
|
4078
3928
|
|
|
@@ -4082,36 +3932,12 @@ async function executeSetup(config) {
|
|
|
4082
3932
|
// Helper: Scaffold Beads GitHub sync when --sync flag is provided
|
|
4083
3933
|
async function handleSyncScaffold() {
|
|
4084
3934
|
console.log('');
|
|
4085
|
-
console.log('
|
|
3935
|
+
console.log('Beads GitHub sync scaffolding is deprecated (--sync).');
|
|
4086
3936
|
try {
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
for (const f of
|
|
4090
|
-
console.log(`
|
|
4091
|
-
}
|
|
4092
|
-
for (const f of (result.filesSkipped || [])) {
|
|
4093
|
-
console.log(` Skipped: ${f} (already exists)`);
|
|
4094
|
-
}
|
|
4095
|
-
|
|
4096
|
-
// Detect default branch and pin forge-managed workflows to the repo baseline Beads version.
|
|
4097
|
-
const branch = detectDefaultBranch(projectRoot);
|
|
4098
|
-
const beadsVersion = DEFAULT_BEADS_VERSION;
|
|
4099
|
-
const workflowDir = path.join(projectRoot, '.github', 'workflows');
|
|
4100
|
-
templateWorkflows(workflowDir, branch, beadsVersion, result.filesCreated || []);
|
|
4101
|
-
console.log(` Branch: ${branch}, Beads version: ${beadsVersion}`);
|
|
4102
|
-
|
|
4103
|
-
// PAT setup: interactive when possible, reminder otherwise
|
|
4104
|
-
try {
|
|
4105
|
-
const patResult = setupPAT(projectRoot, { interactive: !NON_INTERACTIVE });
|
|
4106
|
-
if (patResult.success) {
|
|
4107
|
-
console.log(' PAT configured for Beads sync');
|
|
4108
|
-
} else if (patResult.reminder) {
|
|
4109
|
-
console.log(` ${patResult.reminder}`);
|
|
4110
|
-
} else if (patResult.instructions) {
|
|
4111
|
-
console.log(` ${patResult.instructions.split('\n')[0]}`);
|
|
4112
|
-
}
|
|
4113
|
-
} catch (_patErr) { // NOSONAR — best-effort PAT setup, non-fatal
|
|
4114
|
-
// PAT setup is best-effort — don't block sync scaffold
|
|
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}`);
|
|
4115
3941
|
}
|
|
4116
3942
|
} catch (err) {
|
|
4117
3943
|
console.error(` Error scaffolding GitHub-Beads sync: ${err.message}`);
|
|
@@ -4124,7 +3950,7 @@ async function handleSyncScaffold() {
|
|
|
4124
3950
|
// Helper: Handle setup command in non-quick mode
|
|
4125
3951
|
async function handleSetupCommand(selectedAgents, flags) {
|
|
4126
3952
|
if (!Array.isArray(selectedAgents) || selectedAgents.length === 0) {
|
|
4127
|
-
return
|
|
3953
|
+
return runInteractiveSetupFallback(flags);
|
|
4128
3954
|
}
|
|
4129
3955
|
|
|
4130
3956
|
// Allow callers (e.g. reinstall) to override projectRoot without process.chdir()
|
|
@@ -4144,6 +3970,10 @@ async function handleSetupCommand(selectedAgents, flags) {
|
|
|
4144
3970
|
}
|
|
4145
3971
|
}
|
|
4146
3972
|
|
|
3973
|
+
async function runInteractiveSetupFallback(flags, interactiveSetup = interactiveSetupWithFlags) {
|
|
3974
|
+
return interactiveSetup(flags);
|
|
3975
|
+
}
|
|
3976
|
+
|
|
4147
3977
|
// Helper: Handle external services configuration
|
|
4148
3978
|
|
|
4149
3979
|
|
|
@@ -4179,7 +4009,7 @@ async function handleExternalServices(skipExternal, selectedAgents) {
|
|
|
4179
4009
|
* Detect which agents are already configured in a project directory.
|
|
4180
4010
|
* Checks for the presence of each agent's configured directories/files.
|
|
4181
4011
|
* Returns the external-facing setup IDs used by the setup UX, including
|
|
4182
|
-
* legacy aliases such as `claude-code
|
|
4012
|
+
* legacy aliases such as `claude-code`.
|
|
4183
4013
|
* Callers that need raw plugin IDs for internal lookup must normalize first.
|
|
4184
4014
|
*
|
|
4185
4015
|
* @param {string} dir - Project directory to scan
|
|
@@ -4190,8 +4020,6 @@ function detectConfiguredAgents(dir) {
|
|
|
4190
4020
|
const detected = [];
|
|
4191
4021
|
const legacyAgentIds = {
|
|
4192
4022
|
claude: 'claude-code',
|
|
4193
|
-
copilot: 'github-copilot',
|
|
4194
|
-
roo: 'roo-code',
|
|
4195
4023
|
};
|
|
4196
4024
|
|
|
4197
4025
|
pluginManager.getAllPlugins().forEach((plugin, id) => {
|
|
@@ -4213,7 +4041,7 @@ function detectConfiguredAgents(dir) {
|
|
|
4213
4041
|
* Used during setup --clean or reset flows.
|
|
4214
4042
|
*
|
|
4215
4043
|
* @param {string} dir - Project directory
|
|
4216
|
-
* @param {string} agentName - Agent slug (e.g. 'cursor', '
|
|
4044
|
+
* @param {string} agentName - Agent slug (e.g. 'cursor', 'codex')
|
|
4217
4045
|
* @param {object} [manifest] - Optional sync manifest with file paths to remove
|
|
4218
4046
|
* @returns {{ removed: string[], errors: string[] }}
|
|
4219
4047
|
*/
|
|
@@ -4273,6 +4101,9 @@ const SETUP_FLAG_DEFAULTS = Object.freeze({
|
|
|
4273
4101
|
verbose: false,
|
|
4274
4102
|
dryRun: false,
|
|
4275
4103
|
quick: false,
|
|
4104
|
+
minimal: false,
|
|
4105
|
+
standard: false,
|
|
4106
|
+
full: false,
|
|
4276
4107
|
skipExternal: false,
|
|
4277
4108
|
sync: false,
|
|
4278
4109
|
symlink: false,
|
|
@@ -4287,6 +4118,9 @@ const SIMPLE_SETUP_FLAG_UPDATES = Object.freeze({
|
|
|
4287
4118
|
'--verbose': { verbose: true },
|
|
4288
4119
|
'--dry-run': { dryRun: true },
|
|
4289
4120
|
'--quick': { quick: true },
|
|
4121
|
+
'--minimal': { minimal: true },
|
|
4122
|
+
'--standard': { standard: true },
|
|
4123
|
+
'--full': { full: true },
|
|
4290
4124
|
'--skip-external': { skipExternal: true },
|
|
4291
4125
|
'--sync': { sync: true },
|
|
4292
4126
|
'--symlink': { symlink: true },
|
|
@@ -4364,6 +4198,9 @@ function mergeSetupFlags(flags, argv) {
|
|
|
4364
4198
|
verbose: Boolean(flags.verbose || setupFlags.verbose),
|
|
4365
4199
|
dryRun: Boolean(flags.dryRun || setupFlags.dryRun),
|
|
4366
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),
|
|
4367
4204
|
skipExternal: Boolean(flags.skipExternal || setupFlags.skipExternal),
|
|
4368
4205
|
sync: Boolean(flags.sync || setupFlags.sync),
|
|
4369
4206
|
symlink: Boolean(flags.symlink || setupFlags.symlink),
|
|
@@ -4374,9 +4211,6 @@ function mergeSetupFlags(flags, argv) {
|
|
|
4374
4211
|
function normalizeDetectedAgent(agentName) {
|
|
4375
4212
|
const aliases = {
|
|
4376
4213
|
'claude-code': 'claude',
|
|
4377
|
-
'github-copilot': 'copilot',
|
|
4378
|
-
'kilo-code': 'kilocode',
|
|
4379
|
-
'roo-code': 'roo',
|
|
4380
4214
|
};
|
|
4381
4215
|
return aliases[agentName] || agentName;
|
|
4382
4216
|
}
|
|
@@ -4420,6 +4254,22 @@ module.exports = {
|
|
|
4420
4254
|
resetSetupNotes();
|
|
4421
4255
|
PKG_MANAGER = detectPackageManager();
|
|
4422
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
|
+
|
|
4423
4273
|
// Determine agents to install
|
|
4424
4274
|
let selectedAgents = determineSelectedAgents(flags);
|
|
4425
4275
|
|
|
@@ -4454,12 +4304,17 @@ module.exports = {
|
|
|
4454
4304
|
return { success: true };
|
|
4455
4305
|
}
|
|
4456
4306
|
|
|
4307
|
+
if (flags.sync && selectedAgents.length === 0) {
|
|
4308
|
+
await handleSyncScaffold();
|
|
4309
|
+
return { success: true };
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4457
4312
|
if (selectedAgents.length > 0) {
|
|
4458
4313
|
await handleSetupCommand(selectedAgents, flags);
|
|
4459
4314
|
return { success: true };
|
|
4460
4315
|
}
|
|
4461
4316
|
|
|
4462
|
-
await
|
|
4317
|
+
await runInteractiveSetupFallback(flags);
|
|
4463
4318
|
return { success: true };
|
|
4464
4319
|
},
|
|
4465
4320
|
|
|
@@ -4467,9 +4322,13 @@ module.exports = {
|
|
|
4467
4322
|
checkPrerequisites,
|
|
4468
4323
|
setupCoreDocs,
|
|
4469
4324
|
displaySetupSummary,
|
|
4325
|
+
printForgeInitNextStep,
|
|
4326
|
+
finalizeWorkflowConfig,
|
|
4327
|
+
backupMarkerlessAgentsMd,
|
|
4470
4328
|
setupAgent,
|
|
4471
4329
|
quickSetup,
|
|
4472
4330
|
interactiveSetupWithFlags,
|
|
4331
|
+
_runInteractiveSetupFallback: runInteractiveSetupFallback,
|
|
4473
4332
|
dryRunSetup,
|
|
4474
4333
|
handleSetupCommand,
|
|
4475
4334
|
executeSetup,
|
|
@@ -4477,29 +4336,36 @@ module.exports = {
|
|
|
4477
4336
|
_interactiveSetup,
|
|
4478
4337
|
configureExternalServices,
|
|
4479
4338
|
configureDefaultExternalServices,
|
|
4480
|
-
installBeadsWithMethod,
|
|
4481
4339
|
installSkillsWithMethod,
|
|
4482
4340
|
installViaBunx,
|
|
4483
4341
|
autoInstallLefthook,
|
|
4484
4342
|
autoSetupToolsInQuickMode,
|
|
4343
|
+
autoMigrateBeadsToKernel,
|
|
4344
|
+
ensureGitHooksInstalled,
|
|
4345
|
+
forgeShouldWriteLefthookConfig,
|
|
4346
|
+
FORGE_USER_LEFTHOOK_YML,
|
|
4485
4347
|
setupClaudeMcpConfig,
|
|
4348
|
+
setupCursorMcpConfig,
|
|
4349
|
+
setupClaudePermissions,
|
|
4350
|
+
setupCursorIgnore,
|
|
4351
|
+
setupClaudeHooksConfig,
|
|
4352
|
+
setupCursorHooksConfig,
|
|
4486
4353
|
displayMcpStatus,
|
|
4487
4354
|
displayEnvTokenResults,
|
|
4488
4355
|
minimalInstall,
|
|
4489
4356
|
determineSelectedAgents,
|
|
4490
4357
|
handlePathSetup,
|
|
4491
|
-
loadAndSetupCanonicalCommands,
|
|
4492
4358
|
detectConfiguredAgents,
|
|
4493
4359
|
removeAgentFiles,
|
|
4494
4360
|
parseSetupFlags,
|
|
4495
4361
|
mergeSetupFlags,
|
|
4496
|
-
getWorkflowCommands,
|
|
4497
4362
|
getWorkflowRuntimeAssets,
|
|
4498
4363
|
findMissingWorkflowRuntimeAssets,
|
|
4499
4364
|
ensureWorkflowShellPolicy,
|
|
4500
4365
|
repairWorkflowRuntimeAssets,
|
|
4501
4366
|
repairRuntimeReadiness,
|
|
4502
4367
|
_showBanner: showBanner,
|
|
4368
|
+
backupAndRemoveLegacyCursorRules,
|
|
4503
4369
|
|
|
4504
4370
|
// State accessors for testing
|
|
4505
4371
|
_getState: () => ({ projectRoot, FORCE_MODE, VERBOSE_MODE, NON_INTERACTIVE, SYMLINK_ONLY, SYNC_ENABLED, PKG_MANAGER }),
|