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,541 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-harness native HOOK config renderer.
|
|
5
|
+
*
|
|
6
|
+
* Projects Forge's lifecycle enforcement — the TDD gate (source changes require
|
|
7
|
+
* tests) and the protected-path guard — onto each harness's REAL native hook
|
|
8
|
+
* surface, using read → merge → write (idempotent, preserves user hooks). This is
|
|
9
|
+
* the hook analogue of lib/mcp-config-renderer.js and closes the honesty gap that
|
|
10
|
+
* #311 recorded: the capability matrix advertised native hooks that no renderer
|
|
11
|
+
* actually wrote.
|
|
12
|
+
*
|
|
13
|
+
* Verified native hook surfaces:
|
|
14
|
+
* - Claude : `.claude/settings.json` → a `hooks` block
|
|
15
|
+
* (PreToolUse matcher groups → { type:'command', command })
|
|
16
|
+
* - Cursor : `.cursor/hooks.json` → { version: 1, hooks: { <event>: [ { command } ] } }
|
|
17
|
+
* (Cursor 1.7+; only `before*` events can DENY — there is NO pre-edit
|
|
18
|
+
* deny event, so protected-path write-blocking + commit gating run on
|
|
19
|
+
* `beforeShellExecution`, and `afterFileEdit` is an observational audit.)
|
|
20
|
+
* - Codex : `.codex/config.toml` → `[hooks]` matcher groups. Codex reads the
|
|
21
|
+
* GLOBAL `~/.codex/config.toml` (per #311 / lib/agents-config.js), so
|
|
22
|
+
* project `forge setup` MUST NOT write it. Rendered + tested here for a
|
|
23
|
+
* global-config follow-up only.
|
|
24
|
+
* - Hermes : `~/.hermes/config.yaml` → a `hooks:` block of shell hooks (matcher +
|
|
25
|
+
* command; JSON-stdin/stdout wire protocol). A `pre_tool_call` hook CAN
|
|
26
|
+
* deny a tool call (it even accepts Claude's {decision:block} shape). Lives
|
|
27
|
+
* in GLOBAL (home) config, so project `forge setup` MUST NOT write it —
|
|
28
|
+
* rendered + tested here for a global-config follow-up only.
|
|
29
|
+
*
|
|
30
|
+
* The rendered `command` invokes Forge's installed native-hook adapter
|
|
31
|
+
* (`.forge/hooks/forge-native-hook.js`), which the setup flow installs alongside
|
|
32
|
+
* `.forge/hooks/check-tdd.js`. The adapter reads the harness's hook stdin, enforces
|
|
33
|
+
* the protected-path set, and delegates the TDD gate to the real `check-tdd.js`.
|
|
34
|
+
*
|
|
35
|
+
* Dependency-free (JSON only; no TOML lib) so it runs under `bun test` and the
|
|
36
|
+
* release gates. Reuses the MCP renderer's `backupFile` for the data-loss guard.
|
|
37
|
+
*
|
|
38
|
+
* @module hook-renderer
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const fs = require('node:fs');
|
|
42
|
+
const path = require('node:path');
|
|
43
|
+
const { backupFile } = require('./mcp-config-renderer');
|
|
44
|
+
|
|
45
|
+
const HARNESS_HOOK_FILES = {
|
|
46
|
+
claude: '.claude/settings.json',
|
|
47
|
+
cursor: '.cursor/hooks.json',
|
|
48
|
+
codex: '.codex/config.toml',
|
|
49
|
+
hermes: '~/.hermes/config.yaml',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// The adapter Forge installs into every project (see lib/commands/setup.js). The
|
|
53
|
+
// stable `forge-native-hook.js` token also MARKS Forge-owned hook entries so a
|
|
54
|
+
// re-merge replaces them in place instead of duplicating (idempotency) and never
|
|
55
|
+
// clobbers a user's own hooks.
|
|
56
|
+
const FORGE_HOOK_ADAPTER_REL = '.forge/hooks/forge-native-hook.js';
|
|
57
|
+
const FORGE_HOOK_ADAPTER = `node ${FORGE_HOOK_ADAPTER_REL}`;
|
|
58
|
+
const FORGE_HOOK_MARKER = 'forge-native-hook.js';
|
|
59
|
+
|
|
60
|
+
// CONTEXT intents route to the `forge` CLI (which loads lib/), NOT the self-contained
|
|
61
|
+
// adapter — memory injection needs kernel/FTS access and must FAIL OPEN. The marker
|
|
62
|
+
// token below stamps Forge-owned context-hook entries so a re-merge replaces them in
|
|
63
|
+
// place (idempotency), the same role FORGE_HOOK_MARKER plays for enforcement entries.
|
|
64
|
+
//
|
|
65
|
+
// The command is a RESOLVED `node <abs bin/forge.js>` invocation — NOT a bare `forge`.
|
|
66
|
+
// A bare `forge` on a hook's minimal PATH either does not resolve (feature silently
|
|
67
|
+
// never fires) or resolves to the WRONG binary (e.g. a global `forge` that misroutes),
|
|
68
|
+
// whose stdout would then be injected as session context. Resolving the exact CLI that
|
|
69
|
+
// rendered the hook (via __dirname, works in the repo and under node_modules) removes
|
|
70
|
+
// both failure modes. FORGE_CONTEXT_MARKER is the stable idempotency token.
|
|
71
|
+
const FORGE_CLI_BIN = path.join(__dirname, '..', 'bin', 'forge.js');
|
|
72
|
+
const FORGE_CLI = `node "${FORGE_CLI_BIN}"`;
|
|
73
|
+
const FORGE_CONTEXT_MARKER = 'hooks session-start';
|
|
74
|
+
// The inbox-pickup context hook (UserPromptSubmit tier). A SECOND context marker so a
|
|
75
|
+
// re-merge recognizes + replaces the Forge-owned UserPromptSubmit entry in place, exactly
|
|
76
|
+
// as FORGE_CONTEXT_MARKER does for the SessionStart entry.
|
|
77
|
+
const FORGE_INBOX_CONTEXT_MARKER = 'hooks inbox-pickup';
|
|
78
|
+
// The PR-shepherd events context hook (a SECOND UserPromptSubmit-tier hook). Its own
|
|
79
|
+
// marker so a re-merge recognizes + replaces the Forge-owned entry in place. The whole
|
|
80
|
+
// Forge UserPromptSubmit group is already recognized via the inbox marker, but this keeps
|
|
81
|
+
// the shepherd-events command independently identifiable (symmetry with the other tiers).
|
|
82
|
+
const FORGE_SHEPHERD_EVENTS_MARKER = 'hooks shepherd-events';
|
|
83
|
+
// The capture-on-exit context hook (PreCompact + Stop tier). A THIRD context marker so a
|
|
84
|
+
// re-merge recognizes + replaces the Forge-owned PreCompact/Stop entries in place. Both
|
|
85
|
+
// events share this one marker (they differ only by a --trigger suffix on the command).
|
|
86
|
+
//
|
|
87
|
+
// It matches the FULL resolved Forge invocation (`node "<abs bin/forge.js>" hooks capture`),
|
|
88
|
+
// NOT the bare `hooks capture` verb: a bare-substring check would treat ANY user hook command
|
|
89
|
+
// that merely mentions "hooks capture" as Forge-owned and DELETE it on re-merge (data-integrity
|
|
90
|
+
// bug, CodeRabbit on #397). Only Forge's own rendered capture command contains this prefix, so
|
|
91
|
+
// the merge replaces exactly Forge's group and preserves the user's.
|
|
92
|
+
const FORGE_CAPTURE_CONTEXT_MARKER = `${FORGE_CLI} hooks capture`;
|
|
93
|
+
|
|
94
|
+
// Per-harness SessionStart context-injection capability. Honest capability matrix —
|
|
95
|
+
// only Claude exposes a native session-start surface that can inject additionalContext.
|
|
96
|
+
// Cursor's 1.7 hooks are deny-oriented (no session-start context surface); Codex and
|
|
97
|
+
// Hermes hooks live in GLOBAL home config that project setup never writes. We NEVER
|
|
98
|
+
// fake parity: each non-Claude harness carries an explicit, tested skip reason.
|
|
99
|
+
const SESSION_START_SUPPORT = Object.freeze({
|
|
100
|
+
claude: Object.freeze({ rendered: true }),
|
|
101
|
+
cursor: Object.freeze({ rendered: false, reason: 'no-session-start-surface' }),
|
|
102
|
+
codex: Object.freeze({ rendered: false, reason: 'global-config' }),
|
|
103
|
+
hermes: Object.freeze({ rendered: false, reason: 'global-config' }),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Per-harness UserPromptSubmit context-injection capability (the near-real-time inbox
|
|
107
|
+
// tier). Only Claude has a verified UserPromptSubmit surface that injects
|
|
108
|
+
// additionalContext alongside the submitted prompt. Same honesty rule as SessionStart:
|
|
109
|
+
// every non-Claude harness carries an explicit, tested skip reason — no faked parity.
|
|
110
|
+
const USER_PROMPT_SUBMIT_SUPPORT = Object.freeze({
|
|
111
|
+
claude: Object.freeze({ rendered: true }),
|
|
112
|
+
cursor: Object.freeze({ rendered: false, reason: 'no-user-prompt-surface' }),
|
|
113
|
+
codex: Object.freeze({ rendered: false, reason: 'global-config' }),
|
|
114
|
+
hermes: Object.freeze({ rendered: false, reason: 'global-config' }),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Per-harness session-END (capture-on-exit) capability. Only Claude exposes native
|
|
118
|
+
// PreCompact + Stop hook surfaces where Forge can snapshot session learnings BEFORE
|
|
119
|
+
// context is compacted or the session ends. Cursor 1.7 hooks are deny-oriented with no
|
|
120
|
+
// session-end surface; Codex and Hermes hooks live in GLOBAL home config project setup
|
|
121
|
+
// never writes. Same honesty rule as the other context tiers — no faked parity.
|
|
122
|
+
const SESSION_END_SUPPORT = Object.freeze({
|
|
123
|
+
claude: Object.freeze({ rendered: true }),
|
|
124
|
+
cursor: Object.freeze({ rendered: false, reason: 'no-session-end-surface' }),
|
|
125
|
+
codex: Object.freeze({ rendered: false, reason: 'global-config' }),
|
|
126
|
+
hermes: Object.freeze({ rendered: false, reason: 'global-config' }),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Claude exposes $CLAUDE_PROJECT_DIR (absolute project root) to hook commands and
|
|
130
|
+
// documents it as THE cwd-independent way to reference project-local hook scripts —
|
|
131
|
+
// a bare relative path breaks whenever Claude runs the hook from another cwd. Cursor
|
|
132
|
+
// and Codex execute hooks from the workspace root, so a repo-relative path resolves.
|
|
133
|
+
function adapterInvocation(harness) {
|
|
134
|
+
if (harness === 'claude') return `node "$CLAUDE_PROJECT_DIR/${FORGE_HOOK_ADAPTER_REL}"`;
|
|
135
|
+
return FORGE_HOOK_ADAPTER;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The frozen Forge hook contract: the two enforcement intents projected onto each
|
|
140
|
+
* harness. Order matters — protected-path (per-write) is listed before tdd-gate
|
|
141
|
+
* (per-commit) so the rendered Claude PreToolUse groups read write-guard first.
|
|
142
|
+
*/
|
|
143
|
+
const FORGE_HOOK_CONTRACT = Object.freeze({
|
|
144
|
+
schemaVersion: '1.2.0',
|
|
145
|
+
kind: 'forge.hookContract',
|
|
146
|
+
adapter: FORGE_HOOK_ADAPTER,
|
|
147
|
+
intents: Object.freeze([
|
|
148
|
+
Object.freeze({
|
|
149
|
+
id: 'protected-path',
|
|
150
|
+
kind: 'enforcement',
|
|
151
|
+
enforces: 'Protected-path guard: block writes/edits to Forge-protected paths (.forge/, .git/, AGENTS.md, secrets, generated artifacts).',
|
|
152
|
+
lifecycle: 'pre-write',
|
|
153
|
+
command: `${FORGE_HOOK_ADAPTER} --intent protected-path`,
|
|
154
|
+
}),
|
|
155
|
+
Object.freeze({
|
|
156
|
+
id: 'tdd-gate',
|
|
157
|
+
kind: 'enforcement',
|
|
158
|
+
enforces: 'TDD gate: source changes must ship with accompanying tests (blocks bare git commit).',
|
|
159
|
+
lifecycle: 'pre-commit',
|
|
160
|
+
command: `${FORGE_HOOK_ADAPTER} --intent tdd-gate`,
|
|
161
|
+
}),
|
|
162
|
+
Object.freeze({
|
|
163
|
+
id: 'memory-inject',
|
|
164
|
+
kind: 'context',
|
|
165
|
+
cliAction: 'session-start',
|
|
166
|
+
enforces: 'Memory push: inject a bounded, token-capped digest (remembered notes + top open issues) at session start. Additive and FAIL-OPEN — a missing digest never blocks a session.',
|
|
167
|
+
lifecycle: 'session-start',
|
|
168
|
+
command: `${FORGE_CLI} hooks session-start`,
|
|
169
|
+
}),
|
|
170
|
+
Object.freeze({
|
|
171
|
+
id: 'inbox-pickup',
|
|
172
|
+
kind: 'context',
|
|
173
|
+
cliAction: 'inbox-pickup',
|
|
174
|
+
// COMPLIANT COMMENT-BACK: surfaces pending targeted dashboard instruction comments
|
|
175
|
+
// (kernel DATA, fenced) on each prompt. Reads the user's own kernel data via a
|
|
176
|
+
// supported hook — NEVER injects into a running session's stdin, never drives the
|
|
177
|
+
// agent programmatically (Anthropic Usage Policy, kernel issue 6d10c1a1).
|
|
178
|
+
enforces: 'Comment-back pickup: surface a compact count+pointer nudge for pending targeted dashboard instruction comments on each UserPromptSubmit (compact to avoid additionalContext accumulation; full fenced digest is at SessionStart + `forge inbox`). Additive and FAIL-OPEN — a missing nudge never blocks a prompt.',
|
|
179
|
+
lifecycle: 'user-prompt-submit',
|
|
180
|
+
command: `${FORGE_CLI} hooks inbox-pickup`,
|
|
181
|
+
}),
|
|
182
|
+
Object.freeze({
|
|
183
|
+
id: 'shepherd-events',
|
|
184
|
+
kind: 'context',
|
|
185
|
+
cliAction: 'shepherd-events',
|
|
186
|
+
// PR-SHEPHERD DELTAS: surfaces a compact, capped digest of NEW PR-monitor events
|
|
187
|
+
// (verdict changes, failed checks, new threads, merged/closed) since the last read,
|
|
188
|
+
// then advances the per-PR consumer cursor. This is the CONSUMER side of the constant
|
|
189
|
+
// watcher — the watch loop writes the journal, this pushes the deltas each turn. Reads
|
|
190
|
+
// the user's OWN local journal via a supported hook — NEVER stdin injection, never
|
|
191
|
+
// drives the agent (Anthropic Usage Policy).
|
|
192
|
+
enforces: 'PR shepherd events: on each UserPromptSubmit, surface a compact, capped digest of NEW PR-monitor events (verdict changes, failed checks, new threads, merged/closed) since the last read across open-PR journals, then advance the cursor. Compact to avoid additionalContext accumulation. Additive and FAIL-OPEN — a missing digest never blocks a prompt.',
|
|
193
|
+
lifecycle: 'user-prompt-submit',
|
|
194
|
+
command: `${FORGE_CLI} hooks shepherd-events`,
|
|
195
|
+
}),
|
|
196
|
+
Object.freeze({
|
|
197
|
+
id: 'memory-capture',
|
|
198
|
+
kind: 'context',
|
|
199
|
+
cliAction: 'capture',
|
|
200
|
+
// CAPTURE-ON-EXIT: the write half of Forge memory. SessionStart INJECTS remembered
|
|
201
|
+
// notes but nothing ever CAPTURES on the way out, so long sessions lose learnings and
|
|
202
|
+
// memory stays orphaned (the eval scored memory pull-only). PreCompact + Stop fire this
|
|
203
|
+
// to snapshot a bounded session-summary note BEFORE context is compacted / the session
|
|
204
|
+
// ends. Persists to the memory store — it NEVER injects into the turn and never drives
|
|
205
|
+
// the agent (Anthropic Usage Policy). The trigger (precompact|stop) rides as a --trigger
|
|
206
|
+
// suffix stamped by the rendered hook, so the CLI never has to read hook stdin.
|
|
207
|
+
enforces: 'Capture-on-exit: snapshot a bounded session-summary note (trigger + in-progress issues) into the memory store on PreCompact and Stop, BEFORE context is compacted or the session ends. Additive and FAIL-OPEN — a capture failure never blocks a session.',
|
|
208
|
+
lifecycle: 'session-end',
|
|
209
|
+
command: `${FORGE_CLI} hooks capture`,
|
|
210
|
+
}),
|
|
211
|
+
]),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/** Thrown when an existing hook config cannot be parsed — signals "do not overwrite". */
|
|
215
|
+
class HookConfigParseError extends Error {
|
|
216
|
+
constructor(message, cause) {
|
|
217
|
+
super(message);
|
|
218
|
+
this.name = 'HookConfigParseError';
|
|
219
|
+
this.cause = cause;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function intentById(contract, id) {
|
|
224
|
+
const intent = contract.intents.find(i => i.id === id);
|
|
225
|
+
if (!intent) throw new Error(`Forge hook contract is missing intent '${id}'`);
|
|
226
|
+
return intent;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function harnessCommand(contract, id, harness) {
|
|
230
|
+
const intent = intentById(contract, id);
|
|
231
|
+
// CONTEXT intents route to the `forge` CLI (fail-open memory injection), NOT the
|
|
232
|
+
// self-contained enforcement adapter. Hooks often run with a minimal PATH; if `forge`
|
|
233
|
+
// does not resolve the command simply fails and the harness ignores it (fail-open).
|
|
234
|
+
if (intent.kind === 'context') {
|
|
235
|
+
return `${FORGE_CLI} hooks ${intent.cliAction} --harness ${harness}`;
|
|
236
|
+
}
|
|
237
|
+
// ENFORCEMENT intents build from the harness-specific adapter invocation (Claude →
|
|
238
|
+
// $CLAUDE_PROJECT_DIR; Cursor/Codex → repo-relative). intent.id is the `--intent` value.
|
|
239
|
+
return `${adapterInvocation(harness)} --intent ${intent.id} --harness ${harness}`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Report the per-harness SessionStart context-injection capability (the honest matrix).
|
|
244
|
+
* @param {string} harness
|
|
245
|
+
* @returns {{ rendered: boolean, reason?: string }}
|
|
246
|
+
*/
|
|
247
|
+
function sessionStartCapability(harness) {
|
|
248
|
+
return SESSION_START_SUPPORT[harness] || { rendered: false, reason: 'unknown-harness' };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Report the per-harness UserPromptSubmit context-injection capability (the honest matrix
|
|
253
|
+
* for the near-real-time inbox tier).
|
|
254
|
+
* @param {string} harness
|
|
255
|
+
* @returns {{ rendered: boolean, reason?: string }}
|
|
256
|
+
*/
|
|
257
|
+
function userPromptSubmitCapability(harness) {
|
|
258
|
+
return USER_PROMPT_SUBMIT_SUPPORT[harness] || { rendered: false, reason: 'unknown-harness' };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Report the per-harness session-END (capture-on-exit) capability (the honest matrix for
|
|
263
|
+
* the PreCompact + Stop capture tier).
|
|
264
|
+
* @param {string} harness
|
|
265
|
+
* @returns {{ rendered: boolean, reason?: string }}
|
|
266
|
+
*/
|
|
267
|
+
function sessionEndCapability(harness) {
|
|
268
|
+
return SESSION_END_SUPPORT[harness] || { rendered: false, reason: 'unknown-harness' };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Render the Claude `.claude/settings.json` `hooks` block (PreToolUse groups only).
|
|
273
|
+
* Write/Edit/MultiEdit/NotebookEdit → protected-path deny; Bash → TDD gate.
|
|
274
|
+
* @param {object} contract
|
|
275
|
+
* @returns {{ PreToolUse: object[] }}
|
|
276
|
+
*/
|
|
277
|
+
function renderClaudeHooks(contract) {
|
|
278
|
+
return {
|
|
279
|
+
PreToolUse: [
|
|
280
|
+
{
|
|
281
|
+
matcher: 'Write|Edit|MultiEdit|NotebookEdit',
|
|
282
|
+
hooks: [{ type: 'command', command: harnessCommand(contract, 'protected-path', 'claude') }],
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
matcher: 'Bash',
|
|
286
|
+
hooks: [{ type: 'command', command: harnessCommand(contract, 'tdd-gate', 'claude') }],
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
// SessionStart context injection (memory push). No matcher → applies to every
|
|
290
|
+
// session source; the command emits { hookSpecificOutput.additionalContext }.
|
|
291
|
+
SessionStart: [
|
|
292
|
+
{ hooks: [{ type: 'command', command: harnessCommand(contract, 'memory-inject', 'claude') }] },
|
|
293
|
+
],
|
|
294
|
+
// UserPromptSubmit context injection (compliant comment-back — near-real-time tier).
|
|
295
|
+
// Surfaces pending targeted dashboard instruction comments (fenced kernel DATA) on each
|
|
296
|
+
// prompt; the command emits { hookSpecificOutput.additionalContext }. Reads the user's
|
|
297
|
+
// own kernel data via a supported hook — NEVER stdin injection (Anthropic Usage Policy).
|
|
298
|
+
// Both UserPromptSubmit context hooks share ONE Forge-owned group (inbox-pickup +
|
|
299
|
+
// PR-shepherd deltas). Claude runs every hook in the group and appends each hook's
|
|
300
|
+
// additionalContext; keeping them in one group means a re-merge replaces the pair
|
|
301
|
+
// atomically (the group is Forge-owned via either marker). Both are compact + fail-open.
|
|
302
|
+
UserPromptSubmit: [
|
|
303
|
+
{
|
|
304
|
+
hooks: [
|
|
305
|
+
{ type: 'command', command: harnessCommand(contract, 'inbox-pickup', 'claude') },
|
|
306
|
+
{ type: 'command', command: harnessCommand(contract, 'shepherd-events', 'claude') },
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
// Capture-on-exit (memory capture). PreCompact fires before context is compacted; Stop
|
|
311
|
+
// fires when the agent finishes. Both call the same capture command; the event stamps the
|
|
312
|
+
// --trigger so the CLI never reads hook stdin. The command persists a bounded session
|
|
313
|
+
// summary — it emits NO stdout (a Stop hook that printed text would inject into the turn).
|
|
314
|
+
PreCompact: [
|
|
315
|
+
{ hooks: [{ type: 'command', command: `${harnessCommand(contract, 'memory-capture', 'claude')} --trigger precompact` }] },
|
|
316
|
+
],
|
|
317
|
+
Stop: [
|
|
318
|
+
{ hooks: [{ type: 'command', command: `${harnessCommand(contract, 'memory-capture', 'claude')} --trigger stop` }] },
|
|
319
|
+
],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Render the Cursor `.cursor/hooks.json` config. Cursor 1.7+ has NO pre-edit deny
|
|
325
|
+
* event, so write-blocking + commit gating run on `beforeShellExecution` (git /
|
|
326
|
+
* redirects), and `afterFileEdit` carries an observational protected-path audit.
|
|
327
|
+
* @param {object} contract
|
|
328
|
+
* @returns {{ version: number, hooks: object }}
|
|
329
|
+
*/
|
|
330
|
+
function renderCursorHooks(contract) {
|
|
331
|
+
return {
|
|
332
|
+
version: 1,
|
|
333
|
+
hooks: {
|
|
334
|
+
beforeShellExecution: [
|
|
335
|
+
{ command: harnessCommand(contract, 'tdd-gate', 'cursor') },
|
|
336
|
+
{ command: harnessCommand(contract, 'protected-path', 'cursor') },
|
|
337
|
+
],
|
|
338
|
+
afterFileEdit: [
|
|
339
|
+
{ command: harnessCommand(contract, 'protected-path', 'cursor') },
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Render a Codex `[hooks]` TOML block (mirrors the Claude matcher-group model).
|
|
347
|
+
* Returned for a GLOBAL-config follow-up ONLY — never written by project setup.
|
|
348
|
+
* @param {object} contract
|
|
349
|
+
* @returns {string}
|
|
350
|
+
*/
|
|
351
|
+
function renderCodexHooksToml(contract) {
|
|
352
|
+
const groups = [
|
|
353
|
+
{ matcher: 'Write|Edit', command: harnessCommand(contract, 'protected-path', 'codex') },
|
|
354
|
+
{ matcher: 'Bash', command: harnessCommand(contract, 'tdd-gate', 'codex') },
|
|
355
|
+
];
|
|
356
|
+
const blocks = groups.map(g =>
|
|
357
|
+
`[[hooks.PreToolUse]]\nmatcher = ${tomlString(g.matcher)}\n\n`
|
|
358
|
+
+ `[[hooks.PreToolUse.hooks]]\ntype = "command"\ncommand = ${tomlString(g.command)}\n`,
|
|
359
|
+
);
|
|
360
|
+
return blocks.join('\n');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function tomlString(value) {
|
|
364
|
+
return '"' + String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Render a Hermes shell-hooks YAML block (the `hooks:` section of ~/.hermes/config.yaml).
|
|
369
|
+
* Hermes runs shell hooks as subprocesses over a JSON-stdin/stdout wire protocol; a
|
|
370
|
+
* `pre_tool_call` hook can DENY a tool call. Matchers are regexes over the Hermes
|
|
371
|
+
* tool_name (write_file/patch = edits → protected-path; terminal = shell → tdd-gate).
|
|
372
|
+
* Returned for a GLOBAL-config follow-up ONLY — Hermes reads ~/.hermes/config.yaml
|
|
373
|
+
* (home dir), so project setup never writes it (mirrors renderCodexHooksToml).
|
|
374
|
+
* @param {object} contract
|
|
375
|
+
* @returns {string}
|
|
376
|
+
*/
|
|
377
|
+
function renderHermesHooksYaml(contract) {
|
|
378
|
+
const groups = [
|
|
379
|
+
{ matcher: 'write_file|patch', command: harnessCommand(contract, 'protected-path', 'hermes') },
|
|
380
|
+
{ matcher: 'terminal', command: harnessCommand(contract, 'tdd-gate', 'hermes') },
|
|
381
|
+
];
|
|
382
|
+
const lines = ['hooks:', ' pre_tool_call:'];
|
|
383
|
+
for (const g of groups) {
|
|
384
|
+
lines.push(` - matcher: ${yamlString(g.matcher)}`);
|
|
385
|
+
lines.push(` command: ${yamlString(g.command)}`);
|
|
386
|
+
}
|
|
387
|
+
return lines.join('\n') + '\n';
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function yamlString(value) {
|
|
391
|
+
return '"' + String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** True when a command is Forge-owned — the enforcement adapter OR a context CLI hook. */
|
|
395
|
+
function isForgeCommand(command) {
|
|
396
|
+
return typeof command === 'string'
|
|
397
|
+
&& (command.includes(FORGE_HOOK_MARKER)
|
|
398
|
+
|| command.includes(FORGE_CONTEXT_MARKER)
|
|
399
|
+
|| command.includes(FORGE_INBOX_CONTEXT_MARKER)
|
|
400
|
+
|| command.includes(FORGE_SHEPHERD_EVENTS_MARKER)
|
|
401
|
+
|| command.includes(FORGE_CAPTURE_CONTEXT_MARKER));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** True when a hook group/entry is Forge-owned (any inner command is Forge-owned). */
|
|
405
|
+
function isForgeClaudeGroup(group) {
|
|
406
|
+
const hooks = Array.isArray(group?.hooks) ? group.hooks : [];
|
|
407
|
+
return hooks.some(h => isForgeCommand(h?.command));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function isForgeCursorEntry(entry) {
|
|
411
|
+
return isForgeCommand(entry?.command);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function parseJsonConfig(existingText) {
|
|
415
|
+
if (!existingText || !existingText.trim()) return {};
|
|
416
|
+
let obj;
|
|
417
|
+
try {
|
|
418
|
+
obj = JSON.parse(existingText);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
// DATA-LOSS GUARD: never silently discard a populated-but-unparseable config
|
|
421
|
+
// (JSONC comments / trailing commas). Signal the caller to back up + skip.
|
|
422
|
+
throw new HookConfigParseError('existing hook config is not valid JSON', err);
|
|
423
|
+
}
|
|
424
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return {};
|
|
425
|
+
return obj;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Merge Forge's hooks into an existing `.claude/settings.json` string.
|
|
430
|
+
* Preserves all other settings keys, all non-Forge events, and the user's own
|
|
431
|
+
* matcher-groups; replaces only Forge-owned groups (idempotent).
|
|
432
|
+
* @param {string} existingText
|
|
433
|
+
* @param {object} contract
|
|
434
|
+
* @returns {string}
|
|
435
|
+
*/
|
|
436
|
+
function mergeClaudeSettings(existingText, contract) {
|
|
437
|
+
const obj = parseJsonConfig(existingText);
|
|
438
|
+
if (!obj.hooks || typeof obj.hooks !== 'object' || Array.isArray(obj.hooks)) obj.hooks = {};
|
|
439
|
+
const rendered = renderClaudeHooks(contract);
|
|
440
|
+
for (const [event, forgeGroups] of Object.entries(rendered)) {
|
|
441
|
+
const existingGroups = Array.isArray(obj.hooks[event]) ? obj.hooks[event] : [];
|
|
442
|
+
const userGroups = existingGroups.filter(group => !isForgeClaudeGroup(group));
|
|
443
|
+
obj.hooks[event] = [...userGroups, ...forgeGroups];
|
|
444
|
+
}
|
|
445
|
+
return JSON.stringify(obj, null, 2) + '\n';
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Merge Forge's hooks into an existing `.cursor/hooks.json` string.
|
|
450
|
+
* Forces `version: 1`, preserves all non-Forge events and the user's own entries;
|
|
451
|
+
* replaces only Forge-owned entries (idempotent).
|
|
452
|
+
* @param {string} existingText
|
|
453
|
+
* @param {object} contract
|
|
454
|
+
* @returns {string}
|
|
455
|
+
*/
|
|
456
|
+
function mergeCursorHooks(existingText, contract) {
|
|
457
|
+
const obj = parseJsonConfig(existingText);
|
|
458
|
+
obj.version = 1;
|
|
459
|
+
if (!obj.hooks || typeof obj.hooks !== 'object' || Array.isArray(obj.hooks)) obj.hooks = {};
|
|
460
|
+
const rendered = renderCursorHooks(contract);
|
|
461
|
+
for (const [event, forgeEntries] of Object.entries(rendered.hooks)) {
|
|
462
|
+
const existingEntries = Array.isArray(obj.hooks[event]) ? obj.hooks[event] : [];
|
|
463
|
+
const userEntries = existingEntries.filter(entry => !isForgeCursorEntry(entry));
|
|
464
|
+
obj.hooks[event] = [...userEntries, ...forgeEntries];
|
|
465
|
+
}
|
|
466
|
+
return JSON.stringify(obj, null, 2) + '\n';
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const MERGERS = {
|
|
470
|
+
claude: mergeClaudeSettings,
|
|
471
|
+
cursor: mergeCursorHooks,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Render (merge) Forge's native hooks into one harness's native config on disk.
|
|
476
|
+
* Read → merge → write. Unparseable existing file → BACKED UP + left untouched
|
|
477
|
+
* (never overwritten), mirroring the MCP renderer's data-loss safety.
|
|
478
|
+
*
|
|
479
|
+
* Codex is GLOBAL-config scope: project setup cannot write it, so this returns a
|
|
480
|
+
* `scope: 'global-config'` skip WITHOUT touching disk (keeps Codex honest).
|
|
481
|
+
*
|
|
482
|
+
* @param {object} params
|
|
483
|
+
* @param {'claude'|'cursor'|'codex'} params.harness
|
|
484
|
+
* @param {string} params.targetRoot - Project root.
|
|
485
|
+
* @param {object} [params.contract] - Forge hook contract (defaults to FORGE_HOOK_CONTRACT).
|
|
486
|
+
* @returns {{ file?: string, existed?: boolean, skipped: boolean, wrote: boolean, backup?: string, scope?: string }}
|
|
487
|
+
*/
|
|
488
|
+
function renderHookConfig({ harness, targetRoot, contract = FORGE_HOOK_CONTRACT }) {
|
|
489
|
+
if (harness === 'codex' || harness === 'hermes') {
|
|
490
|
+
// Honest: Codex (~/.codex/config.toml) and Hermes (~/.hermes/config.yaml) hooks
|
|
491
|
+
// both live in GLOBAL (home) config; project setup must not write global config.
|
|
492
|
+
// Their renderers are kept + tested for a global-config follow-up.
|
|
493
|
+
return { harness, scope: 'global-config', skipped: true, wrote: false };
|
|
494
|
+
}
|
|
495
|
+
const merge = MERGERS[harness];
|
|
496
|
+
const rel = HARNESS_HOOK_FILES[harness];
|
|
497
|
+
if (!merge || !rel) throw new Error(`Unknown hook harness: ${harness}`);
|
|
498
|
+
|
|
499
|
+
const filePath = path.join(targetRoot, rel);
|
|
500
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
501
|
+
const existed = fs.existsSync(filePath);
|
|
502
|
+
const existing = existed ? fs.readFileSync(filePath, 'utf-8') : '';
|
|
503
|
+
|
|
504
|
+
let merged;
|
|
505
|
+
try {
|
|
506
|
+
merged = merge(existing, contract);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
if (err instanceof HookConfigParseError && existed) {
|
|
509
|
+
const backup = backupFile(filePath);
|
|
510
|
+
return { file: filePath, existed, skipped: true, wrote: false, backup };
|
|
511
|
+
}
|
|
512
|
+
throw err;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
fs.writeFileSync(filePath, merged, 'utf-8');
|
|
516
|
+
return { file: filePath, existed, skipped: false, wrote: true };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
module.exports = {
|
|
520
|
+
FORGE_HOOK_CONTRACT,
|
|
521
|
+
HARNESS_HOOK_FILES,
|
|
522
|
+
FORGE_HOOK_ADAPTER,
|
|
523
|
+
FORGE_HOOK_MARKER,
|
|
524
|
+
FORGE_CONTEXT_MARKER,
|
|
525
|
+
FORGE_INBOX_CONTEXT_MARKER,
|
|
526
|
+
FORGE_CAPTURE_CONTEXT_MARKER,
|
|
527
|
+
SESSION_START_SUPPORT,
|
|
528
|
+
USER_PROMPT_SUBMIT_SUPPORT,
|
|
529
|
+
SESSION_END_SUPPORT,
|
|
530
|
+
sessionStartCapability,
|
|
531
|
+
userPromptSubmitCapability,
|
|
532
|
+
sessionEndCapability,
|
|
533
|
+
HookConfigParseError,
|
|
534
|
+
renderClaudeHooks,
|
|
535
|
+
renderCursorHooks,
|
|
536
|
+
renderCodexHooksToml,
|
|
537
|
+
renderHermesHooksYaml,
|
|
538
|
+
mergeClaudeSettings,
|
|
539
|
+
mergeCursorHooks,
|
|
540
|
+
renderHookConfig,
|
|
541
|
+
};
|