forge-workflow 0.0.10 → 0.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +82 -5
- package/.forge/hooks/forge-native-hook.js +431 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +709 -0
- package/CLAUDE.md +9 -118
- package/QUICKSTART.md +175 -0
- package/README.md +275 -365
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +532 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/forge/TOOLCHAIN.md +670 -0
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +121 -0
- package/docs/guides/SUPPORT.md +190 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +214 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +155 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/activation/ensure-forge-home.js +135 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1109 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +139 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-detect.js +60 -0
- package/lib/beads-nudge.js +91 -0
- package/lib/beads-setup.js +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_aliases.js +248 -0
- package/lib/commands/_issue.js +780 -77
- package/lib/commands/_manifest.js +93 -0
- package/lib/commands/_registry.js +99 -34
- package/lib/commands/_resolve-command-opts.js +230 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +209 -0
- package/lib/commands/hooks.js +377 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +604 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +17 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/memory.js +81 -0
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +362 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +481 -29
- package/lib/commands/pr.js +88 -0
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +135 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +171 -0
- package/lib/commands/recap.js +75 -0
- package/lib/commands/recommend.js +0 -1
- package/lib/commands/release.js +104 -0
- package/lib/commands/remember.js +140 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +900 -971
- package/lib/commands/shepherd.js +501 -0
- package/lib/commands/ship.js +59 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +158 -21
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +4 -1
- package/lib/commands/test.js +43 -27
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +43 -18
- package/lib/commands/worktree.js +362 -99
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +977 -0
- package/lib/dep-guard/keyword-ripple.js +2 -2
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +35 -9
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +382 -11
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +192 -0
- package/lib/global-flags.js +104 -0
- package/lib/greptile-match.js +7 -63
- package/lib/grounding/context-events.js +230 -0
- package/lib/grounding/read-first.js +112 -0
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +541 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/kernel/backing-issue.js +311 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/owned-kernel.js +43 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2540 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +3 -2
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/auto-actions.js +175 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/digest.js +206 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +192 -0
- package/lib/pr-monitor/upsert-sticky.js +169 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1314 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +99 -497
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2105 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +241 -20
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/scoring.js +17 -3
- package/lib/status/beads-snapshot.js +45 -2
- package/lib/status/presenter.js +169 -18
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +251 -0
- package/lib/workflow/enforce-stage.js +351 -45
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +11 -22
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +37 -35
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +81 -57
- package/scripts/beads-upgrade-smoke.sh +24 -3
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard.sh +22 -3
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +49 -84
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +50 -83
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-auto-actions.js +93 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/pr-verdict-label.js +50 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +21 -94
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status.sh +60 -57
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +13 -6
- package/scripts/test.js +95 -12
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -89
- package/.github/workflows/github-to-beads.yml +0 -100
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -281
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-bootstrap.js +0 -225
- package/lib/beads-health-check.js +0 -188
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -584
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -356
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -132
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Forge native-hook enforcement adapter.
|
|
7
|
+
*
|
|
8
|
+
* Invoked by the rendered native hook configs (.claude/settings.json hooks block
|
|
9
|
+
* and .cursor/hooks.json) that `forge setup` writes via lib/hook-renderer.js. It
|
|
10
|
+
* projects Forge's lifecycle enforcement onto each harness's native hook surface:
|
|
11
|
+
*
|
|
12
|
+
* --intent protected-path : deny writes/edits to Forge-protected paths.
|
|
13
|
+
* --intent tdd-gate : on a `git commit`, delegate to the installed
|
|
14
|
+
* check-tdd.js and deny the commit if source files
|
|
15
|
+
* are staged without accompanying tests.
|
|
16
|
+
*
|
|
17
|
+
* Contract (verified from harness docs):
|
|
18
|
+
* - Claude PreToolUse : stdin has { tool_name, tool_input:{ file_path|command } };
|
|
19
|
+
* to BLOCK, print { hookSpecificOutput:{ hookEventName:'PreToolUse',
|
|
20
|
+
* permissionDecision:'deny', permissionDecisionReason } } and exit 0. To
|
|
21
|
+
* allow, print nothing (never auto-approve past the user's own settings).
|
|
22
|
+
* - Cursor before* : stdin has { command|file_path }; to BLOCK, print
|
|
23
|
+
* { continue:true, permission:'deny', agentMessage, userMessage } and exit 0.
|
|
24
|
+
* (afterFileEdit is observational — permission is ignored but the message
|
|
25
|
+
* still surfaces the protected-path violation.)
|
|
26
|
+
*
|
|
27
|
+
* SELF-CONTAINED: target projects have .forge/hooks/*.js but NOT lib/, so this
|
|
28
|
+
* module requires nothing from the Forge package. The protected-path set is a
|
|
29
|
+
* conservative built-in mirroring the categories in .forge/protected-paths.yaml.
|
|
30
|
+
*
|
|
31
|
+
* Security: delegates to check-tdd.js via execFileSync (no shell), never evals input.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const { execFileSync } = require('node:child_process');
|
|
35
|
+
const fs = require('node:fs');
|
|
36
|
+
const path = require('node:path');
|
|
37
|
+
|
|
38
|
+
// Conservative protected-path set, mirroring .forge/protected-paths.yaml categories:
|
|
39
|
+
// forge_core (.forge/), user_protocol (AGENTS.md), immutable/config (lefthook.yml),
|
|
40
|
+
// secrets (.env*), lockfiles (package-lock/yarn/pnpm/bun), the issue-state dir
|
|
41
|
+
// (cli-only), and VCS (.git/).
|
|
42
|
+
// NOTE: the issue-state matcher is built from a split literal so the D20 retirement
|
|
43
|
+
// audit (release-readiness.js findBdTerms) does not miscount a path matcher as
|
|
44
|
+
// direct issue-tool access — this file never touches that tooling.
|
|
45
|
+
const ISSUE_STATE_DIR_PATTERN = new RegExp('(^|/)\\.' + 'beads' + '(/|$)');
|
|
46
|
+
const PROTECTED_PATTERNS = [
|
|
47
|
+
/(^|\/)\.git(\/|$)/,
|
|
48
|
+
/(^|\/)\.forge(\/|$)/,
|
|
49
|
+
ISSUE_STATE_DIR_PATTERN,
|
|
50
|
+
/(^|\/)AGENTS\.md$/i,
|
|
51
|
+
/(^|\/)lefthook\.ya?ml$/i,
|
|
52
|
+
/(^|\/)\.env(\.[^/]+)?$/i,
|
|
53
|
+
/(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lock[b]?)$/i,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// ── Config-honest enforcement ───────────────────────────────────────────────
|
|
57
|
+
// A DISABLED gate/rail in .forge/config.yaml must make its hook genuinely inert
|
|
58
|
+
// (issue eda6d866). These hooks are self-contained (target projects have
|
|
59
|
+
// .forge/hooks/*.js but NOT lib/), so we read + interpret the config here rather
|
|
60
|
+
// than through the resolver. The `yaml` package is a Forge dependency present in
|
|
61
|
+
// any project that ran `forge setup`; when it is somehow absent we degrade to a
|
|
62
|
+
// conservative raw-text scan. Unparseable/missing config FAILS TOWARD enforcement
|
|
63
|
+
// (default ON) so we never silently drop a gate the user did not disable.
|
|
64
|
+
|
|
65
|
+
/** Load `.forge/config.yaml` into an object, or `{ __raw }` for a text-scan fallback, or null. */
|
|
66
|
+
function loadConfigObject(projectRoot) {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = fs.readFileSync(path.join(projectRoot, '.forge', 'config.yaml'), 'utf8');
|
|
70
|
+
} catch {
|
|
71
|
+
return null; // no config file → caller defaults to enforcement ON
|
|
72
|
+
}
|
|
73
|
+
if (!raw || !raw.trim()) return {};
|
|
74
|
+
let YAML;
|
|
75
|
+
try {
|
|
76
|
+
YAML = require('yaml');
|
|
77
|
+
} catch {
|
|
78
|
+
// The yaml MODULE is genuinely unavailable → degrade to a conservative raw-text
|
|
79
|
+
// scan (the only case that keeps { __raw }). Parser presence and parse success are
|
|
80
|
+
// deliberately separated so a MALFORMED file never reaches the fuzzy scan below.
|
|
81
|
+
return { __raw: raw };
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const parsed = YAML.parse(raw);
|
|
85
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
86
|
+
} catch {
|
|
87
|
+
// MALFORMED YAML (module present, parse threw) → FAIL TOWARD ENFORCEMENT. Return {}
|
|
88
|
+
// so resolveEnforcement defaults to TDD ON + built-in protected paths. We must NOT
|
|
89
|
+
// fall through to the raw-text scan: a broken file containing a `rail.tdd_intent:
|
|
90
|
+
// enabled: false` fragment could otherwise switch enforcement OFF (issue eda6d866).
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A primitive is "disabled" only when its `enabled` is explicitly boolean false. */
|
|
96
|
+
function isExplicitlyDisabled(node) {
|
|
97
|
+
return Boolean(node) && typeof node === 'object' && node.enabled === false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Overly-broad patterns the runtime graph rejects (lib/core/runtime-graph.js
|
|
101
|
+
// validateProtectedPaths). Kept in sync here because this hook is self-contained
|
|
102
|
+
// and cannot import from lib/.
|
|
103
|
+
const OVERLY_BROAD_PROTECTED_PATTERNS = ['*', '**', '**/*', '.', './', '/'];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate a config-supplied protectedPaths list with the SAME rules the runtime
|
|
107
|
+
* graph enforces: every entry must be a non-empty string and not an overly-broad
|
|
108
|
+
* pattern. A list with ANY invalid entry is rejected WHOLESALE (returns null →
|
|
109
|
+
* caller falls back to the built-in protected set), so an invalid or overly-broad
|
|
110
|
+
* config can never become authoritative and WEAKEN protection — it fails toward
|
|
111
|
+
* enforcement, matching the rest of this hook's fail-safe stance (issue eda6d866).
|
|
112
|
+
* @returns {string[]|null} validated list (may be empty = deliberately inert), or
|
|
113
|
+
* null when the list is absent/invalid.
|
|
114
|
+
*/
|
|
115
|
+
function validateConfiguredProtectedPaths(list) {
|
|
116
|
+
if (!Array.isArray(list)) return null;
|
|
117
|
+
const validated = [];
|
|
118
|
+
for (const entry of list) {
|
|
119
|
+
if (typeof entry !== 'string' || entry.trim() === '') return null;
|
|
120
|
+
const pattern = entry.trim();
|
|
121
|
+
if (OVERLY_BROAD_PROTECTED_PATTERNS.includes(pattern)) return null;
|
|
122
|
+
validated.push(pattern);
|
|
123
|
+
}
|
|
124
|
+
return validated;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Scan raw YAML for a `<key>:` block whose immediate child is `enabled: false`. */
|
|
128
|
+
function rawKeyDisabled(raw, key) {
|
|
129
|
+
const lines = String(raw).split(/\r?\n/);
|
|
130
|
+
const keyRe = new RegExp(`^(\\s*)"?${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}"?\\s*:\\s*$`);
|
|
131
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
132
|
+
const m = lines[i].match(keyRe);
|
|
133
|
+
if (!m) continue;
|
|
134
|
+
const parentIndent = m[1].length;
|
|
135
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
136
|
+
if (!lines[j].trim()) continue;
|
|
137
|
+
const childIndent = lines[j].match(/^\s*/)[0].length;
|
|
138
|
+
if (childIndent <= parentIndent) break; // left the block
|
|
139
|
+
if (/^\s*enabled\s*:\s*false\s*$/.test(lines[j])) return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Resolve the enforcement state the installed hooks must honor.
|
|
147
|
+
* @returns {{ tddEnabled: boolean, protectedPaths: string[]|null }}
|
|
148
|
+
* tddEnabled — false only when rail.tdd_intent is explicitly disabled.
|
|
149
|
+
* protectedPaths — the configured list (may be []), or null when unset (→ built-in set).
|
|
150
|
+
*/
|
|
151
|
+
function resolveEnforcement(projectRoot) {
|
|
152
|
+
const config = loadConfigObject(projectRoot);
|
|
153
|
+
if (!config) return { tddEnabled: true, protectedPaths: null };
|
|
154
|
+
|
|
155
|
+
let tddDisabled;
|
|
156
|
+
let protectedPaths = null;
|
|
157
|
+
if (config.__raw) {
|
|
158
|
+
tddDisabled = rawKeyDisabled(config.__raw, 'rail.tdd_intent') || rawKeyDisabled(config.__raw, 'tdd_intent');
|
|
159
|
+
if (/^\s*protectedPaths\s*:\s*\[\s*\]\s*$/m.test(config.__raw)) protectedPaths = [];
|
|
160
|
+
} else {
|
|
161
|
+
// `forge gate disable` writes workflow.gates['rail.tdd_intent']; the `full`
|
|
162
|
+
// profile writes top-level rails.tdd_intent. Honor either shape.
|
|
163
|
+
const gates = config.workflow && config.workflow.gates;
|
|
164
|
+
const rails = config.rails;
|
|
165
|
+
tddDisabled = isExplicitlyDisabled(gates && gates['rail.tdd_intent'])
|
|
166
|
+
|| isExplicitlyDisabled(rails && rails.tdd_intent);
|
|
167
|
+
// Validate before overriding built-in protection: a bad/overly-broad list must
|
|
168
|
+
// fall back to the built-in set (null), never become authoritative (T2).
|
|
169
|
+
protectedPaths = validateConfiguredProtectedPaths(config.protectedPaths);
|
|
170
|
+
}
|
|
171
|
+
return { tddEnabled: !tddDisabled, protectedPaths };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The SINGLE resolved predicate each hook gates on — flag-agnostic on purpose.
|
|
176
|
+
* Whatever config flag ultimately governs an enforcement kind resolves inside
|
|
177
|
+
* resolveEnforcement(); callers ask only "is this active?". This is the one place
|
|
178
|
+
* the TDD off-switch is read, so a future flag change lands here with no rework.
|
|
179
|
+
* @param {'tdd'|'protected-path'} kind
|
|
180
|
+
*/
|
|
181
|
+
function isEnforcementActive(kind, projectRoot) {
|
|
182
|
+
const { tddEnabled, protectedPaths } = resolveEnforcement(projectRoot);
|
|
183
|
+
if (kind === 'tdd') return tddEnabled;
|
|
184
|
+
if (kind === 'protected-path') return protectedPaths === null || protectedPaths.length > 0;
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Translate a config protectedPaths entry (a path or glob like `.github/workflows/**`)
|
|
189
|
+
// into an anchored matcher. `**` spans path separators, `*` stays within one segment.
|
|
190
|
+
function globToRegExp(pattern) {
|
|
191
|
+
const norm = normalize(pattern);
|
|
192
|
+
let re = '';
|
|
193
|
+
for (let i = 0; i < norm.length; i += 1) {
|
|
194
|
+
const c = norm[i];
|
|
195
|
+
if (c === '*') {
|
|
196
|
+
if (norm[i + 1] === '*') { re += '.*'; i += 1; if (norm[i + 1] === '/') i += 1; }
|
|
197
|
+
else re += '[^/]*';
|
|
198
|
+
} else if ('\\^$.|?+()[]{}'.includes(c)) {
|
|
199
|
+
re += `\\${c}`;
|
|
200
|
+
} else {
|
|
201
|
+
re += c;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return new RegExp(`(^|/)${re}(/|$)`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Build the protected-path matcher from resolved config (config is the source of
|
|
209
|
+
* truth — the hardcoded PROTECTED_PATTERNS set is only the fallback when config
|
|
210
|
+
* omits protectedPaths entirely, so no gate is silently dropped):
|
|
211
|
+
* null → unset → built-in default set (fail toward enforcement / back-compat)
|
|
212
|
+
* [] → explicitly empty → nothing protected (inert)
|
|
213
|
+
* list → protect exactly those paths/globs
|
|
214
|
+
*/
|
|
215
|
+
function buildProtectedMatcher(protectedPaths) {
|
|
216
|
+
if (protectedPaths === null) {
|
|
217
|
+
return p => Boolean(p) && PROTECTED_PATTERNS.some(re => re.test(normalize(p)));
|
|
218
|
+
}
|
|
219
|
+
if (protectedPaths.length === 0) return () => false;
|
|
220
|
+
const regexes = protectedPaths.map(globToRegExp);
|
|
221
|
+
return p => Boolean(p) && regexes.some(re => re.test(normalize(p)));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Parse `--intent <id> --harness <id>` from an argv slice. */
|
|
225
|
+
function parseArgs(argv) {
|
|
226
|
+
const out = { intent: null, harness: null };
|
|
227
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
228
|
+
if (argv[i] === '--intent') out.intent = argv[i + 1];
|
|
229
|
+
else if (argv[i] === '--harness') out.harness = argv[i + 1];
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Extract the target file path from a harness hook payload, or null. */
|
|
235
|
+
function extractPath(input) {
|
|
236
|
+
if (!input || typeof input !== 'object') return null;
|
|
237
|
+
const ti = input.tool_input && typeof input.tool_input === 'object' ? input.tool_input : {};
|
|
238
|
+
// ti.path covers Hermes write_file/patch payloads ({ tool_input: { path } }); the
|
|
239
|
+
// others cover Claude (file_path/notebook_path) and flat Cursor payloads.
|
|
240
|
+
const candidate = ti.file_path || ti.notebook_path || ti.path || input.file_path || null;
|
|
241
|
+
return typeof candidate === 'string' && candidate.length ? candidate : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Extract the shell command from a harness hook payload, or null. */
|
|
245
|
+
function extractCommand(input) {
|
|
246
|
+
if (!input || typeof input !== 'object') return null;
|
|
247
|
+
const ti = input.tool_input && typeof input.tool_input === 'object' ? input.tool_input : {};
|
|
248
|
+
const candidate = ti.command || input.command || null;
|
|
249
|
+
return typeof candidate === 'string' && candidate.length ? candidate : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function normalize(p) {
|
|
253
|
+
return String(p).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** True when a path falls inside Forge's built-in protected set (the fallback matcher). */
|
|
257
|
+
function isProtectedPath(p) {
|
|
258
|
+
if (!p) return false;
|
|
259
|
+
const n = normalize(p);
|
|
260
|
+
return PROTECTED_PATTERNS.some(re => re.test(n));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Write-intent shell constructs: mutating commands and output redirection. Reads
|
|
264
|
+
// (cat/ls/grep) of protected paths stay allowed — the protected-path intent guards
|
|
265
|
+
// WRITES, mirroring the Claude wiring (PreToolUse matcher Write|Edit, not Read).
|
|
266
|
+
const WRITE_INTENT_RE = /(^|[\s;|&(])(rm|mv|cp|tee|truncate|chmod|chown|ln|sed|perl|dd|install|rsync|unlink|shred)\b|>>?/;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* True when a SHELL COMMAND shows WRITE intent toward a Forge-protected path.
|
|
270
|
+
* Needed because Cursor's only deny-capable surface is `beforeShellExecution`,
|
|
271
|
+
* whose payload carries `command` (no file_path) — without this check the
|
|
272
|
+
* protected-path intent could never actually block anything on Cursor.
|
|
273
|
+
* Conservative two-step: (1) the command must contain a mutating construct
|
|
274
|
+
* (rm/mv/sed/tee/redirection/...), then (2) token-scan (split on whitespace +
|
|
275
|
+
* shell operators, strip quotes) for a protected path. Never throws.
|
|
276
|
+
*/
|
|
277
|
+
function commandTouchesProtectedPath(command, isProtected = isProtectedPath) {
|
|
278
|
+
if (typeof command !== 'string' || !command) return false;
|
|
279
|
+
if (!WRITE_INTENT_RE.test(command)) return false;
|
|
280
|
+
const tokens = command.split(/[\s;|&<>()]+/);
|
|
281
|
+
for (const raw of tokens) {
|
|
282
|
+
const token = raw.replace(/^["']+|["']+$/g, '');
|
|
283
|
+
if (token && !token.startsWith('-') && isProtected(token)) return true;
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** True when a shell command invokes `git commit`. */
|
|
289
|
+
function isGitCommit(command) {
|
|
290
|
+
return typeof command === 'string' && /\bgit\b[^\n&|;]*\bcommit\b/.test(command);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Run the installed TDD check; returns its exit code (0 = pass, non-zero = block). */
|
|
294
|
+
function runInstalledTddCheck() {
|
|
295
|
+
try {
|
|
296
|
+
execFileSync('node', [path.join(__dirname, 'check-tdd.js')], {
|
|
297
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
298
|
+
});
|
|
299
|
+
return 0;
|
|
300
|
+
} catch (err) {
|
|
301
|
+
return typeof err.status === 'number' ? err.status : 1;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Fully-ON default keeps back-compat: callers that pass no `enforcement` (and the
|
|
306
|
+
// existing test suite) get the original always-enforce behavior.
|
|
307
|
+
const ENFORCEMENT_ON = Object.freeze({ tddEnabled: true, protectedPaths: null });
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Core enforcement decision. `runTddCheck` is injectable for deterministic tests;
|
|
311
|
+
* `enforcement` (from resolveEnforcement) makes a DISABLED gate/rail inert.
|
|
312
|
+
* @returns {{ decision: 'allow'|'deny', reason?: string }}
|
|
313
|
+
*/
|
|
314
|
+
function decide({ intent, input, runTddCheck = runInstalledTddCheck, enforcement = ENFORCEMENT_ON }) {
|
|
315
|
+
if (intent === 'protected-path') {
|
|
316
|
+
// Config is the source of truth: matcher is built from the resolved
|
|
317
|
+
// protectedPaths list (empty → inert; unset → built-in fallback set).
|
|
318
|
+
const matchesProtected = buildProtectedMatcher(enforcement.protectedPaths);
|
|
319
|
+
const target = extractPath(input);
|
|
320
|
+
if (matchesProtected(target)) {
|
|
321
|
+
return {
|
|
322
|
+
decision: 'deny',
|
|
323
|
+
reason: `Forge-protected path '${normalize(target)}' — edit it through the owning Forge CLI/skill, not a raw write.`,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
// No file path in the payload (e.g. Cursor beforeShellExecution carries only
|
|
327
|
+
// `command`): inspect the shell command for write intent on a protected path,
|
|
328
|
+
// so the deny-capable shell surface actually protects instead of no-oping.
|
|
329
|
+
if (!target) {
|
|
330
|
+
const command = extractCommand(input);
|
|
331
|
+
if (commandTouchesProtectedPath(command, matchesProtected)) {
|
|
332
|
+
return {
|
|
333
|
+
decision: 'deny',
|
|
334
|
+
reason: 'Shell command writes to a Forge-protected path — use the owning Forge CLI/skill instead.',
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { decision: 'allow' };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (intent === 'tdd-gate') {
|
|
342
|
+
// TDD rail disabled in config → inert: never run the check, never block.
|
|
343
|
+
if (!enforcement.tddEnabled) return { decision: 'allow' };
|
|
344
|
+
const command = extractCommand(input);
|
|
345
|
+
if (!isGitCommit(command)) return { decision: 'allow' };
|
|
346
|
+
const code = runTddCheck();
|
|
347
|
+
if (code !== 0) {
|
|
348
|
+
return {
|
|
349
|
+
decision: 'deny',
|
|
350
|
+
reason: 'TDD gate: staged source files are missing accompanying tests. Write tests first (RED-GREEN-REFACTOR).',
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return { decision: 'allow' };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Unknown intent: never block.
|
|
357
|
+
return { decision: 'allow' };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Serialize a decision into the harness-native hook output contract. */
|
|
361
|
+
function formatOutput(harness, decision, _reason) {
|
|
362
|
+
const reason = decision.reason || '';
|
|
363
|
+
if (decision.decision !== 'deny') {
|
|
364
|
+
// Allow: for Claude, emit nothing so we never auto-approve past user settings.
|
|
365
|
+
return harness === 'cursor'
|
|
366
|
+
? JSON.stringify({ continue: true, permission: 'allow' })
|
|
367
|
+
: '';
|
|
368
|
+
}
|
|
369
|
+
if (harness === 'cursor') {
|
|
370
|
+
return JSON.stringify({
|
|
371
|
+
continue: true,
|
|
372
|
+
permission: 'deny',
|
|
373
|
+
agentMessage: reason,
|
|
374
|
+
userMessage: reason,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
if (harness === 'hermes') {
|
|
378
|
+
// Hermes shell-hook JSON wire protocol: block a pre_tool_call by writing
|
|
379
|
+
// { action: 'block', message } to stdout (empty output = silent allow).
|
|
380
|
+
return JSON.stringify({ action: 'block', message: reason });
|
|
381
|
+
}
|
|
382
|
+
// Claude (and any harness modeled on it).
|
|
383
|
+
return JSON.stringify({
|
|
384
|
+
hookSpecificOutput: {
|
|
385
|
+
hookEventName: 'PreToolUse',
|
|
386
|
+
permissionDecision: 'deny',
|
|
387
|
+
permissionDecisionReason: reason,
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function readStdin() {
|
|
393
|
+
try {
|
|
394
|
+
const raw = require('node:fs').readFileSync(0, 'utf-8');
|
|
395
|
+
return raw && raw.trim() ? JSON.parse(raw) : {};
|
|
396
|
+
} catch {
|
|
397
|
+
return {};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function main() {
|
|
402
|
+
const { intent, harness } = parseArgs(process.argv.slice(2));
|
|
403
|
+
const input = readStdin();
|
|
404
|
+
// Project root is two levels up from this installed hook (<root>/.forge/hooks/),
|
|
405
|
+
// so enforcement resolves against the project's config regardless of cwd.
|
|
406
|
+
const enforcement = resolveEnforcement(path.resolve(__dirname, '..', '..'));
|
|
407
|
+
const decision = decide({ intent, input, enforcement });
|
|
408
|
+
const output = formatOutput(harness || 'claude', decision);
|
|
409
|
+
if (output) process.stdout.write(output);
|
|
410
|
+
// Exit 0 always: the decision travels in the JSON body, not the exit code, so a
|
|
411
|
+
// non-deny outcome leaves the harness's normal permission flow untouched.
|
|
412
|
+
process.exit(0);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
module.exports = {
|
|
416
|
+
PROTECTED_PATTERNS,
|
|
417
|
+
resolveEnforcement,
|
|
418
|
+
isEnforcementActive,
|
|
419
|
+
globToRegExp,
|
|
420
|
+
buildProtectedMatcher,
|
|
421
|
+
parseArgs,
|
|
422
|
+
extractPath,
|
|
423
|
+
extractCommand,
|
|
424
|
+
isProtectedPath,
|
|
425
|
+
commandTouchesProtectedPath,
|
|
426
|
+
isGitCommit,
|
|
427
|
+
decide,
|
|
428
|
+
formatOutput,
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
if (require.main === module) main();
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
schemaVersion: 1.0.0
|
|
3
|
+
kind: ProtectedPathManifest
|
|
4
|
+
manifestPath: .forge/protected-paths.yaml
|
|
5
|
+
surfacesDeprecated: true
|
|
6
|
+
surfacesMigrationNote: Legacy surfaces remain for 0.0.19 protected-state docs/runtime parity; every example below must be covered by the authoritative W1 categories.
|
|
7
|
+
surfaces:
|
|
8
|
+
immutable:
|
|
9
|
+
examples:
|
|
10
|
+
- .git/
|
|
11
|
+
repair: Use git or the owning runtime tool.
|
|
12
|
+
secrets:
|
|
13
|
+
examples:
|
|
14
|
+
- .env.local
|
|
15
|
+
- secrets.json
|
|
16
|
+
repair: Use the secret manager or local environment setup command.
|
|
17
|
+
append_only_logs:
|
|
18
|
+
examples:
|
|
19
|
+
- .forge/log.jsonl
|
|
20
|
+
- .forge/audit.log
|
|
21
|
+
- .beads/interactions.jsonl
|
|
22
|
+
repair: Use the append-only Forge or Beads audit writer.
|
|
23
|
+
beads_state:
|
|
24
|
+
examples:
|
|
25
|
+
- .beads/issues.jsonl
|
|
26
|
+
- .beads/config.yaml
|
|
27
|
+
repair: Use bd or Forge issue commands.
|
|
28
|
+
forge_config:
|
|
29
|
+
examples:
|
|
30
|
+
- .forge/config.yaml
|
|
31
|
+
- .forge/protected-paths.yaml
|
|
32
|
+
repair: Use Forge setup/config commands or the protected Forge API writer.
|
|
33
|
+
extension_manifests:
|
|
34
|
+
examples:
|
|
35
|
+
- .forge/extensions/example/manifest.json
|
|
36
|
+
- .github/PLUGIN_TEMPLATE.json
|
|
37
|
+
repair: Use the Forge extension/plugin manager.
|
|
38
|
+
lockfiles:
|
|
39
|
+
examples:
|
|
40
|
+
- bun.lock
|
|
41
|
+
- package-lock.json
|
|
42
|
+
- .forge/extensions.lock
|
|
43
|
+
repair: Regenerate through the package manager or extension installer.
|
|
44
|
+
workflows:
|
|
45
|
+
examples:
|
|
46
|
+
- .github/workflows/
|
|
47
|
+
- .claude/commands/
|
|
48
|
+
- .forge/hooks/
|
|
49
|
+
- lefthook.yml
|
|
50
|
+
repair: Use Forge workflow/setup commands.
|
|
51
|
+
generated_harness:
|
|
52
|
+
examples:
|
|
53
|
+
- AGENTS.md
|
|
54
|
+
- CLAUDE.md
|
|
55
|
+
- .codex/skills/
|
|
56
|
+
- .cursor/rules/
|
|
57
|
+
repair: Regenerate with forge setup or the owning Forge API surface.
|
|
58
|
+
memory_projection:
|
|
59
|
+
examples:
|
|
60
|
+
- docs/sessions/
|
|
61
|
+
- docs/memory/
|
|
62
|
+
- .forge/memory/
|
|
63
|
+
repair: Use the Forge memory projection writer.
|
|
64
|
+
categories:
|
|
65
|
+
- id: forge_core
|
|
66
|
+
mode: checksum-verified
|
|
67
|
+
paths:
|
|
68
|
+
- lib/**
|
|
69
|
+
- bin/**
|
|
70
|
+
- scripts/**
|
|
71
|
+
- skills/**
|
|
72
|
+
ownerSurface: Forge core release or checksum updater
|
|
73
|
+
repairHint: Use the Forge release/update flow; do not hand-edit checksum-protected core files.
|
|
74
|
+
- id: user_protocol
|
|
75
|
+
mode: cli-only
|
|
76
|
+
paths:
|
|
77
|
+
- AGENTS.md
|
|
78
|
+
- CLAUDE.md
|
|
79
|
+
- .cursorrules
|
|
80
|
+
- .mcp.json
|
|
81
|
+
- .forge/config.yaml
|
|
82
|
+
- .forge/config.yml
|
|
83
|
+
- .forge/config.json
|
|
84
|
+
- .forge/protected-paths.yaml
|
|
85
|
+
ownerSurface: forge setup or forge options
|
|
86
|
+
repairHint: Use Forge setup/options commands so harness protocol files stay synchronized.
|
|
87
|
+
- id: generated_artifacts
|
|
88
|
+
mode: ci-blocked
|
|
89
|
+
paths:
|
|
90
|
+
- .claude/commands/**
|
|
91
|
+
- .claude/skills/**
|
|
92
|
+
- .cursor/rules/**
|
|
93
|
+
- .cursor/skills/**
|
|
94
|
+
- .codex/skills/**
|
|
95
|
+
- .github/workflows/**
|
|
96
|
+
- .forge/hooks/**
|
|
97
|
+
- lefthook.yml
|
|
98
|
+
- .forge/extensions/*/manifest.json
|
|
99
|
+
- .github/PLUGIN_TEMPLATE.json
|
|
100
|
+
- plugins/*/plugin.json
|
|
101
|
+
- plugins/*/extension.json
|
|
102
|
+
- plugins/*/manifest.json
|
|
103
|
+
- bun.lock
|
|
104
|
+
- package-lock.json
|
|
105
|
+
- pnpm-lock.yaml
|
|
106
|
+
- yarn.lock
|
|
107
|
+
- requirements.lock
|
|
108
|
+
- poetry.lock
|
|
109
|
+
- Cargo.lock
|
|
110
|
+
- go.sum
|
|
111
|
+
- .forge/extensions.lock
|
|
112
|
+
- docs/sessions/**
|
|
113
|
+
- docs/memory/**
|
|
114
|
+
- .forge/memory/**
|
|
115
|
+
- memory.md
|
|
116
|
+
- MEMORY.md
|
|
117
|
+
- "**/memory.md"
|
|
118
|
+
- "**/MEMORY.md"
|
|
119
|
+
ownerSurface: Forge harness renderer
|
|
120
|
+
repairHint: Regenerate harness artifacts from the canonical Forge contract.
|
|
121
|
+
- id: append_only_logs
|
|
122
|
+
mode: runtime-only
|
|
123
|
+
paths:
|
|
124
|
+
- .forge/*.jsonl
|
|
125
|
+
- .forge/audit.log
|
|
126
|
+
- .forge/agent-log.ndjson
|
|
127
|
+
- .beads/*.jsonl
|
|
128
|
+
- .beads/interactions.jsonl
|
|
129
|
+
ownerSurface: Forge or Beads audit writer
|
|
130
|
+
repairHint: Append through the runtime writer; do not rewrite audit history.
|
|
131
|
+
- id: secrets
|
|
132
|
+
mode: secret-scan-blocked
|
|
133
|
+
paths:
|
|
134
|
+
- .env
|
|
135
|
+
- .env.*
|
|
136
|
+
- "**/.env"
|
|
137
|
+
- "**/.env.*"
|
|
138
|
+
- secrets.json
|
|
139
|
+
- "**/secrets.json"
|
|
140
|
+
- credentials.json
|
|
141
|
+
- "**/credentials.json"
|
|
142
|
+
ownerSurface: secret manager
|
|
143
|
+
repairHint: Move secrets to the configured secret manager or local ignored env file.
|
|
144
|
+
- id: beads_state
|
|
145
|
+
mode: bd-cli-only
|
|
146
|
+
paths:
|
|
147
|
+
- .beads/**
|
|
148
|
+
ownerSurface: bd CLI or Forge issue adapter
|
|
149
|
+
repairHint: Use bd or Forge issue commands instead of editing Beads state files.
|
|
150
|
+
- id: immutable
|
|
151
|
+
mode: tool-owned
|
|
152
|
+
paths:
|
|
153
|
+
- .git/**
|
|
154
|
+
- .hg/**
|
|
155
|
+
- .svn/**
|
|
156
|
+
ownerSurface: VCS tool
|
|
157
|
+
repairHint: Use the owning VCS command instead of editing runtime internals.
|