forge-workflow 0.0.9 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +3 -0
- package/.forge/hooks/forge-native-hook.js +245 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -106
- package/QUICKSTART.md +171 -0
- package/README.md +271 -363
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +466 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +118 -0
- package/docs/guides/SUPPORT.md +185 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +205 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/reference/TOOLCHAIN.md +658 -0
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +115 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1042 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +126 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-setup.js +225 -28
- package/lib/beads-sync-scaffold.js +36 -107
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +744 -70
- package/lib/commands/_manifest.js +91 -0
- package/lib/commands/_registry.js +85 -34
- package/lib/commands/_resolve-command-opts.js +261 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +192 -0
- package/lib/commands/hooks.js +242 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +598 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +66 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +523 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +436 -24
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +69 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +116 -0
- package/lib/commands/recap.js +61 -0
- package/lib/commands/recommend.js +22 -2
- package/lib/commands/release.js +91 -0
- package/lib/commands/remember.js +74 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +851 -979
- package/lib/commands/shepherd.js +436 -0
- package/lib/commands/ship.js +23 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +329 -11
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +15 -2
- package/lib/commands/test.js +58 -7
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +56 -25
- package/lib/commands/worktree.js +308 -128
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +946 -0
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +42 -17
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +697 -0
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +193 -0
- package/lib/global-flags.js +74 -0
- package/lib/greptile-match.js +7 -63
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +451 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/kernel/backing-issue.js +305 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2504 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +8 -4
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +157 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1273 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +166 -0
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2089 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +332 -23
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +191 -0
- package/lib/status/beads-snapshot.js +145 -0
- package/lib/status/presenter.js +216 -0
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +199 -0
- package/lib/workflow/enforce-stage.js +298 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +159 -14
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +46 -36
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +284 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +64 -232
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +144 -47
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +183 -79
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +108 -0
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +117 -369
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +251 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +302 -28
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +46 -52
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +134 -159
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/skills/ship/SKILL.md +251 -0
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/skills/status/SKILL.md +102 -0
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/ship.prompt.md +0 -215
- package/.github/prompts/status.prompt.md +0 -92
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -56
- package/.github/workflows/github-to-beads.yml +0 -97
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -255
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-health-check.js +0 -143
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -567
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -332
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -78
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared lefthook / git-hook wiring for `forge setup` and `forge init`.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the hook-wiring primitives. The LIVE `forge setup` /
|
|
5
|
+
* `forge init` path is the registry command in lib/commands/setup.js (it wins in the
|
|
6
|
+
* dispatcher and returns before bin/forge.js's inline setup branch is ever reached; the
|
|
7
|
+
* bin copy is LEGACY/DEAD — see the banner at bin/forge.js "LEGACY / DEAD SETUP PATH").
|
|
8
|
+
* The mid-stage repair path (enforce-stage → repairRuntimeReadiness) also consumes these
|
|
9
|
+
* primitives. Earlier the shadow-config fix landed only in one copy while `forge setup`
|
|
10
|
+
* ran a stale copy, leaving a fresh `forge setup` with TDD enforcement SILENTLY INERT
|
|
11
|
+
* (kernel e452422c / c713fce7 / 22e33dbf, beta blocker B3) — hence one shared module.
|
|
12
|
+
*
|
|
13
|
+
* Responsibilities:
|
|
14
|
+
* - Provide the REAL user-facing lefthook.yml (never lefthook's commented example).
|
|
15
|
+
* - Decide when it is safe to (over)write lefthook.yml (replace only a stub).
|
|
16
|
+
* - Install a native `.git/hooks` fallback when the lefthook binary is unavailable,
|
|
17
|
+
* so raw `git commit` / `git push` still enforce the TDD gate.
|
|
18
|
+
* - Verify honestly whether hooks are actually active, so setup can fail LOUDLY
|
|
19
|
+
* instead of no-op'ing.
|
|
20
|
+
*
|
|
21
|
+
* @module lib/lefthook-wiring
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
// Shared secure spawner: resolves the command to its absolute path (which/where.exe)
|
|
29
|
+
// before executing, so we never search a potentially-writable PATH for `git` (S4036).
|
|
30
|
+
const { secureExecFileSync } = require('./shell-utils');
|
|
31
|
+
|
|
32
|
+
// Sentinel embedded in every native hook Forge writes. Lets verifyHooksActive
|
|
33
|
+
// distinguish a Forge-managed hook from a lefthook-managed one or a user's own.
|
|
34
|
+
const FORGE_NATIVE_HOOK_SENTINEL = '# forge-native-git-hook (installed by `forge setup`)';
|
|
35
|
+
|
|
36
|
+
// The lefthook.yml Forge writes into a USER project. The repo's own dev lefthook.yml
|
|
37
|
+
// references repo-internal scripts (scripts/branch-protection.js, lint.js, test.js, …)
|
|
38
|
+
// that a user's project never has, so shipping it produced hooks that fail on the first
|
|
39
|
+
// commit. This minimal config references only what every Forge project gets: the
|
|
40
|
+
// self-contained TDD gate (.forge/hooks/check-tdd.js) on pre-commit, and the project's
|
|
41
|
+
// own test script (a no-op when absent) on pre-push. Both hooks exist, so the
|
|
42
|
+
// HOOKS_NOT_ACTIVE gate is satisfied and the workflow is reachable out of the box.
|
|
43
|
+
const FORGE_USER_LEFTHOOK_YML = `# Forge git hooks — installed by \`forge setup\` / \`forge init\`.
|
|
44
|
+
# pre-commit enforces the TDD gate; pre-push runs your test script if you have one.
|
|
45
|
+
# Edit freely to add your own checks — Forge only replaces a fully-commented stub.
|
|
46
|
+
|
|
47
|
+
pre-commit:
|
|
48
|
+
commands:
|
|
49
|
+
forge-tdd:
|
|
50
|
+
run: node .forge/hooks/check-tdd.js
|
|
51
|
+
|
|
52
|
+
pre-push:
|
|
53
|
+
commands:
|
|
54
|
+
tests:
|
|
55
|
+
run: npm test --if-present
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A fresh `lefthook install` (run by lefthook's own npm postinstall) drops a stock
|
|
60
|
+
* EXAMPLE lefthook.yml with every hook commented out. That disposable stub used to
|
|
61
|
+
* block Forge from writing its config, leaving pre-commit/pre-push unwired so every
|
|
62
|
+
* stage command dead-ended on HOOKS_NOT_ACTIVE right after `forge init` (kernel
|
|
63
|
+
* c713fce7). Overwrite a missing file or such a stub, but never clobber a lefthook.yml
|
|
64
|
+
* that already has active (uncommented) jobs — a user's real config, or Forge's own
|
|
65
|
+
* once written.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} lefthookTarget - Absolute path to the candidate lefthook.yml.
|
|
68
|
+
* @returns {boolean} true when Forge should (over)write its config at that path.
|
|
69
|
+
*/
|
|
70
|
+
function forgeShouldWriteLefthookConfig(lefthookTarget) {
|
|
71
|
+
let stat;
|
|
72
|
+
try {
|
|
73
|
+
stat = fs.lstatSync(lefthookTarget);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error && error.code !== 'ENOENT') {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
return true; // no existing file → write Forge's config
|
|
79
|
+
}
|
|
80
|
+
// Never write THROUGH a symlink (or any non-regular file): a checked-out lefthook.yml
|
|
81
|
+
// pointing outside projectRoot could otherwise be created/overwritten via the
|
|
82
|
+
// follow-through of readFileSync/writeFileSync. Only regular files are safe.
|
|
83
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const content = fs.readFileSync(lefthookTarget, 'utf8');
|
|
87
|
+
const activeLines = content
|
|
88
|
+
.split(/\r?\n/)
|
|
89
|
+
.map((line) => line.trim())
|
|
90
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'));
|
|
91
|
+
return activeLines.length === 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the directory git ACTUALLY reads hooks from for a given project root.
|
|
96
|
+
*
|
|
97
|
+
* Asks git first (`git rev-parse --git-path hooks`), which is authoritative: it
|
|
98
|
+
* honors `core.hooksPath` (husky, a global config, etc.), so verify checks — and the
|
|
99
|
+
* native installer writes to — the directory git will really execute. Without this a
|
|
100
|
+
* project with `core.hooksPath` set would get hooks written to `.git/hooks` that git
|
|
101
|
+
* never runs, and verify would falsely report ACTIVE (a silent-inert false pass, B3).
|
|
102
|
+
*
|
|
103
|
+
* Falls back to a pure-filesystem resolution when git is unavailable or the path is not
|
|
104
|
+
* a git repo — keeps the function testable against synthetic `.git` layouts:
|
|
105
|
+
* - Normal repo: `.git` is a directory → `<root>/.git/hooks`.
|
|
106
|
+
* - Linked worktree: `.git` is a file `gitdir: <path>`; hooks live in the common git
|
|
107
|
+
* dir (resolved via the worktree's `commondir` file) → `<common>/hooks`.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} projectRoot - Absolute path to the project root.
|
|
110
|
+
* @returns {string|null} Absolute hooks dir, or null when not a git repo.
|
|
111
|
+
*/
|
|
112
|
+
function resolveGitHooksDir(projectRoot) {
|
|
113
|
+
return resolveHooksDirViaGit(projectRoot) || resolveGitHooksDirFromFs(projectRoot);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Authoritative resolution via git (honors core.hooksPath). One fast spawn; returns
|
|
117
|
+
// null on any failure (git missing, not a repo) so the filesystem fallback can run.
|
|
118
|
+
function resolveHooksDirViaGit(projectRoot) {
|
|
119
|
+
// SAME-REPO GUARD (kernel N1 / B3): only trust git's answer when projectRoot is its OWN
|
|
120
|
+
// repo — a `.git` directory or a linked-worktree `.git` file present right here. Without
|
|
121
|
+
// this, `git -C <projectRoot> rev-parse` walks UP to an ANCESTOR repo when projectRoot is
|
|
122
|
+
// a not-yet-initialized folder sitting under one (e.g. a project inside a dotfiles-managed
|
|
123
|
+
// $HOME, or a subdir of any repo). We'd then write hooks into — and report ACTIVE from —
|
|
124
|
+
// a repo the user never pointed forge at (cross-repo mutation + a false-active verify).
|
|
125
|
+
// Returning null here defers to the filesystem fallback (which also returns null for such
|
|
126
|
+
// a path), so the caller warns "not a git repository" — the safe pre-F3 behavior.
|
|
127
|
+
if (!fs.existsSync(path.join(projectRoot, '.git'))) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
// secureExecFileSync resolves `git` to its absolute path first (no PATH search, S4036).
|
|
132
|
+
// projectRoot is passed as an ARGUMENT to execFile (never a shell string), so it cannot
|
|
133
|
+
// be interpreted as a command — no command-injection surface.
|
|
134
|
+
const out = secureExecFileSync(
|
|
135
|
+
'git',
|
|
136
|
+
['-C', projectRoot, 'rev-parse', '--path-format=absolute', '--git-path', 'hooks'],
|
|
137
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
|
138
|
+
);
|
|
139
|
+
return (typeof out === 'string' && out.trim()) ? out.trim() : null;
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Pure-filesystem fallback (no git spawn). Does NOT see core.hooksPath — only used when
|
|
146
|
+
// git could not answer.
|
|
147
|
+
function resolveGitHooksDirFromFs(projectRoot) {
|
|
148
|
+
const gitPath = path.join(projectRoot, '.git');
|
|
149
|
+
let stat;
|
|
150
|
+
try {
|
|
151
|
+
stat = fs.lstatSync(gitPath);
|
|
152
|
+
} catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (stat.isDirectory()) {
|
|
156
|
+
return path.join(gitPath, 'hooks');
|
|
157
|
+
}
|
|
158
|
+
if (stat.isFile()) {
|
|
159
|
+
return resolveWorktreeHooksDir(gitPath, projectRoot);
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// A linked worktree's `.git` is a file containing `gitdir: <path>`. Its hooks live in
|
|
165
|
+
// the common repo's hooks dir (resolved via the worktree's `commondir` file). Returns
|
|
166
|
+
// null when the pointer file is unreadable or malformed.
|
|
167
|
+
function resolveWorktreeHooksDir(gitFile, projectRoot) {
|
|
168
|
+
let content;
|
|
169
|
+
try {
|
|
170
|
+
content = fs.readFileSync(gitFile, 'utf8');
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const match = content.match(/gitdir:\s*(.+)/);
|
|
175
|
+
if (!match) return null;
|
|
176
|
+
const rawGitdir = match[1].trim();
|
|
177
|
+
const gitdir = path.isAbsolute(rawGitdir) ? rawGitdir : path.resolve(projectRoot, rawGitdir);
|
|
178
|
+
return path.join(resolveWorktreeCommonDir(gitdir), 'hooks');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Follow the worktree's `commondir` pointer to the common git dir; fall back to the
|
|
182
|
+
// per-worktree gitdir when it is absent or unreadable.
|
|
183
|
+
function resolveWorktreeCommonDir(gitdir) {
|
|
184
|
+
const commonDirFile = path.join(gitdir, 'commondir');
|
|
185
|
+
try {
|
|
186
|
+
if (fs.existsSync(commonDirFile)) {
|
|
187
|
+
const rel = fs.readFileSync(commonDirFile, 'utf8').trim();
|
|
188
|
+
return path.isAbsolute(rel) ? rel : path.resolve(gitdir, rel);
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// fall through to the per-worktree gitdir
|
|
192
|
+
}
|
|
193
|
+
return gitdir;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const NATIVE_HOOK_BODIES = {
|
|
197
|
+
'pre-commit': `#!/bin/sh
|
|
198
|
+
${FORGE_NATIVE_HOOK_SENTINEL}
|
|
199
|
+
# Fallback TDD gate used when the lefthook binary is unavailable. Enforces the same
|
|
200
|
+
# check as the lefthook pre-commit job. Remove this file only if you install lefthook.
|
|
201
|
+
if [ -f ".forge/hooks/check-tdd.js" ]; then
|
|
202
|
+
node ".forge/hooks/check-tdd.js" || exit 1
|
|
203
|
+
fi
|
|
204
|
+
`,
|
|
205
|
+
'pre-push': `#!/bin/sh
|
|
206
|
+
${FORGE_NATIVE_HOOK_SENTINEL}
|
|
207
|
+
# Fallback pre-push gate used when the lefthook binary is unavailable. Runs your test
|
|
208
|
+
# script before pushing (a no-op when the project has none).
|
|
209
|
+
npm test --if-present || exit 1
|
|
210
|
+
`,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Install native `.git/hooks` pre-commit/pre-push scripts that invoke Forge's own
|
|
215
|
+
* TDD gate. This is the fallback that keeps enforcement live when the lefthook binary
|
|
216
|
+
* cannot be installed (e.g. a fresh consumer repo with no package.json), so `git
|
|
217
|
+
* commit` / `git push` are never silently unguarded.
|
|
218
|
+
*
|
|
219
|
+
* A pre-existing hook that is neither Forge-managed nor lefthook-managed is preserved:
|
|
220
|
+
* it is backed up to `<hook>.forge-backup` (once) and left in place (reported in
|
|
221
|
+
* `skipped`) rather than destroyed.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} projectRoot - Absolute path to the project root.
|
|
224
|
+
* @returns {{ installed: boolean, method?: string, hooksDir?: string,
|
|
225
|
+
* written?: string[], skipped?: string[], reason?: string }}
|
|
226
|
+
*/
|
|
227
|
+
function installNativeGitHooks(projectRoot) {
|
|
228
|
+
const hooksDir = resolveGitHooksDir(projectRoot);
|
|
229
|
+
if (!hooksDir) {
|
|
230
|
+
return { installed: false, reason: 'not-a-git-repo' };
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
234
|
+
} catch (error) {
|
|
235
|
+
return { installed: false, reason: `hooks-dir-unwritable: ${error.message}` };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const written = [];
|
|
239
|
+
const skipped = [];
|
|
240
|
+
for (const [name, body] of Object.entries(NATIVE_HOOK_BODIES)) {
|
|
241
|
+
const outcome = writeNativeHook(path.join(hooksDir, name), body);
|
|
242
|
+
(outcome === 'written' ? written : skipped).push(name);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
installed: written.length > 0,
|
|
247
|
+
method: 'native',
|
|
248
|
+
hooksDir,
|
|
249
|
+
written,
|
|
250
|
+
skipped,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Signatures that appear ONLY in a hook lefthook itself GENERATED — never in a user's own
|
|
255
|
+
// hook that merely runs `lefthook ...` among other steps. Lefthook's generated hooks define
|
|
256
|
+
// a `call_lefthook()` shim and honor the `LEFTHOOK`/`LEFTHOOK_BIN` env bypass; a custom hook
|
|
257
|
+
// invoking lefthook does not. Matching these (not the bare 'lefthook' substring) is what
|
|
258
|
+
// stops us from misclassifying a user's hook as a disposable generated artifact (F4 / kernel
|
|
259
|
+
// 269e5d05).
|
|
260
|
+
const LEFTHOOK_GENERATED_MARKERS = ['call_lefthook', 'LEFTHOOK_BIN'];
|
|
261
|
+
|
|
262
|
+
// True only for a hook lefthook GENERATED (safe to regenerate/overwrite), never for a user
|
|
263
|
+
// hook that merely invokes lefthook.
|
|
264
|
+
function isLefthookGeneratedHook(content) {
|
|
265
|
+
return LEFTHOOK_GENERATED_MARKERS.some((marker) => content.includes(marker));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Write a single native hook. A pre-existing hook that is not Forge's own is ALWAYS backed
|
|
269
|
+
// up to `<hook>.forge-backup` first (once), so a user's hook is never destroyed without a
|
|
270
|
+
// copy — whether we preserve it (skip) or overwrite a disposable lefthook-generated one.
|
|
271
|
+
// Returns 'written' or 'skipped'.
|
|
272
|
+
function writeNativeHook(dest, body) {
|
|
273
|
+
if (shouldPreserveExistingHook(dest)) {
|
|
274
|
+
backupHookOnce(dest);
|
|
275
|
+
return 'skipped';
|
|
276
|
+
}
|
|
277
|
+
// Back up before overwriting an existing non-Forge hook (e.g. a lefthook-generated one, or
|
|
278
|
+
// anything a future classification change might reach) — never overwrite without a backup.
|
|
279
|
+
backupExistingNonForgeHook(dest);
|
|
280
|
+
fs.writeFileSync(dest, body, 'utf8');
|
|
281
|
+
try {
|
|
282
|
+
fs.chmodSync(dest, 0o755); // NOSONAR — hooks must be executable
|
|
283
|
+
} catch {
|
|
284
|
+
// Windows filesystems ignore the mode bits — non-fatal.
|
|
285
|
+
}
|
|
286
|
+
return 'written';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// A hook is ours to overwrite ONLY when it is missing, Forge-managed, or a genuine
|
|
290
|
+
// lefthook-GENERATED artifact. Anything else — including a user's custom hook that merely
|
|
291
|
+
// INVOKES lefthook among other steps — is preserved (F4 / kernel 269e5d05: a bare 'lefthook'
|
|
292
|
+
// substring test used to clobber such a user hook without a backup).
|
|
293
|
+
function shouldPreserveExistingHook(dest) {
|
|
294
|
+
if (!fs.existsSync(dest)) return false;
|
|
295
|
+
let existing;
|
|
296
|
+
try {
|
|
297
|
+
existing = fs.readFileSync(dest, 'utf8');
|
|
298
|
+
} catch {
|
|
299
|
+
existing = '';
|
|
300
|
+
}
|
|
301
|
+
if (existing.includes(FORGE_NATIVE_HOOK_SENTINEL)) return false; // our own hook
|
|
302
|
+
if (isLefthookGeneratedHook(existing)) return false; // disposable generated artifact
|
|
303
|
+
return true; // the user's / a third party's hook — preserve it
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Back up an existing hook that is NOT Forge's own before it is overwritten (once, never
|
|
307
|
+
// clobbering an existing backup). No-op when the file is missing/unreadable or already ours.
|
|
308
|
+
function backupExistingNonForgeHook(dest) {
|
|
309
|
+
let existing;
|
|
310
|
+
try {
|
|
311
|
+
existing = fs.readFileSync(dest, 'utf8');
|
|
312
|
+
} catch {
|
|
313
|
+
return; // missing (ENOENT) or unreadable — nothing to back up
|
|
314
|
+
}
|
|
315
|
+
if (!existing.includes(FORGE_NATIVE_HOOK_SENTINEL)) {
|
|
316
|
+
backupHookOnce(dest);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Back up a hook to `<hook>.forge-backup` once (never clobber an existing backup).
|
|
321
|
+
function backupHookOnce(dest) {
|
|
322
|
+
const backup = `${dest}.forge-backup`;
|
|
323
|
+
try {
|
|
324
|
+
if (!fs.existsSync(backup)) fs.copyFileSync(dest, backup);
|
|
325
|
+
} catch {
|
|
326
|
+
// Best-effort backup; the caller still refuses to overwrite the original.
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// The TDD gate script the native hook (and the lefthook pre-commit job) invokes, relative
|
|
331
|
+
// to the project root. The native hook guards its call with `if [ -f … ]`, so when this file
|
|
332
|
+
// is absent the hook runs but enforces nothing.
|
|
333
|
+
const FORGE_TDD_GATE_SCRIPT = path.join('.forge', 'hooks', 'check-tdd.js');
|
|
334
|
+
|
|
335
|
+
// Confirm the TDD gate script exists as a runnable (readable) regular file under projectRoot.
|
|
336
|
+
// Returns { ok: true } or { ok: false, reason } with a loud, specific reason.
|
|
337
|
+
function gateScriptStatus(projectRoot) {
|
|
338
|
+
const gatePath = path.join(projectRoot, FORGE_TDD_GATE_SCRIPT);
|
|
339
|
+
let stat;
|
|
340
|
+
try {
|
|
341
|
+
stat = fs.statSync(gatePath);
|
|
342
|
+
} catch {
|
|
343
|
+
return { ok: false, reason: `native pre-commit hook present but its gate script is missing: ${FORGE_TDD_GATE_SCRIPT}` };
|
|
344
|
+
}
|
|
345
|
+
if (!stat.isFile()) {
|
|
346
|
+
return { ok: false, reason: `native pre-commit hook gate script is not a regular file: ${FORGE_TDD_GATE_SCRIPT}` };
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
fs.accessSync(gatePath, fs.constants.R_OK); // `node <script>` needs it readable
|
|
350
|
+
} catch {
|
|
351
|
+
return { ok: false, reason: `native pre-commit hook gate script is not readable: ${FORGE_TDD_GATE_SCRIPT}` };
|
|
352
|
+
}
|
|
353
|
+
return { ok: true };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Report — honestly — whether git hooks are actually active for the project, so
|
|
358
|
+
* `forge setup` can fail LOUDLY instead of silently no-op'ing. A hook counts as
|
|
359
|
+
* active only when a `pre-commit` hook exists AND is either lefthook-managed or
|
|
360
|
+
* carries Forge's native sentinel with its gate script present and runnable.
|
|
361
|
+
*
|
|
362
|
+
* @param {string} projectRoot - Absolute path to the project root.
|
|
363
|
+
* @returns {{ active: boolean, method: 'lefthook'|'native'|'none'|'unknown', reason?: string, hook?: string }}
|
|
364
|
+
*/
|
|
365
|
+
function verifyHooksActive(projectRoot) {
|
|
366
|
+
const hooksDir = resolveGitHooksDir(projectRoot);
|
|
367
|
+
if (!hooksDir) {
|
|
368
|
+
return { active: false, method: 'none', reason: 'not a git repository' };
|
|
369
|
+
}
|
|
370
|
+
const preCommit = path.join(hooksDir, 'pre-commit');
|
|
371
|
+
if (!fs.existsSync(preCommit)) {
|
|
372
|
+
return { active: false, method: 'none', reason: 'no pre-commit hook installed' };
|
|
373
|
+
}
|
|
374
|
+
let body;
|
|
375
|
+
try {
|
|
376
|
+
body = fs.readFileSync(preCommit, 'utf8');
|
|
377
|
+
} catch (error) {
|
|
378
|
+
return { active: false, method: 'unknown', reason: `pre-commit unreadable: ${error.message}` };
|
|
379
|
+
}
|
|
380
|
+
// Check the Forge native sentinel FIRST: our native hook body mentions "lefthook"
|
|
381
|
+
// in a comment, so a substring test for lefthook would otherwise misclassify it.
|
|
382
|
+
if (body.includes(FORGE_NATIVE_HOOK_SENTINEL)) {
|
|
383
|
+
// F5 (kernel d96af31a): the native hook body only runs the gate when its script exists
|
|
384
|
+
// (`if [ -f ".forge/hooks/check-tdd.js" ]`). A present hook whose gate script is missing
|
|
385
|
+
// silently no-ops — so verify must confirm the script exists AND is runnable, else report
|
|
386
|
+
// NOT active (loud) rather than a false pass. installForgeHookScripts() copies check-tdd.js
|
|
387
|
+
// unconditionally, but silently continues if the packaged source was absent, so this is
|
|
388
|
+
// exactly the case a truthful verify has to catch.
|
|
389
|
+
const gate = gateScriptStatus(projectRoot);
|
|
390
|
+
if (!gate.ok) {
|
|
391
|
+
return { active: false, method: 'native', reason: gate.reason, hook: preCommit };
|
|
392
|
+
}
|
|
393
|
+
return { active: true, method: 'native', hook: preCommit };
|
|
394
|
+
}
|
|
395
|
+
if (body.includes('lefthook')) {
|
|
396
|
+
return { active: true, method: 'lefthook', hook: preCommit };
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
active: false,
|
|
400
|
+
method: 'unknown',
|
|
401
|
+
reason: 'pre-commit hook present but not Forge- or lefthook-managed',
|
|
402
|
+
hook: preCommit,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
module.exports = {
|
|
407
|
+
FORGE_NATIVE_HOOK_SENTINEL,
|
|
408
|
+
FORGE_USER_LEFTHOOK_YML,
|
|
409
|
+
forgeShouldWriteLefthookConfig,
|
|
410
|
+
resolveGitHooksDir,
|
|
411
|
+
installNativeGitHooks,
|
|
412
|
+
verifyHooksActive,
|
|
413
|
+
};
|