forge-workflow 0.0.9 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +3 -0
- package/.forge/hooks/forge-native-hook.js +245 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -106
- package/QUICKSTART.md +171 -0
- package/README.md +271 -363
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +466 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +118 -0
- package/docs/guides/SUPPORT.md +185 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +205 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/reference/TOOLCHAIN.md +658 -0
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +115 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1042 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +126 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-setup.js +225 -28
- package/lib/beads-sync-scaffold.js +36 -107
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +744 -70
- package/lib/commands/_manifest.js +91 -0
- package/lib/commands/_registry.js +85 -34
- package/lib/commands/_resolve-command-opts.js +261 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +192 -0
- package/lib/commands/hooks.js +242 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +598 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +66 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +523 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +436 -24
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +69 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +116 -0
- package/lib/commands/recap.js +61 -0
- package/lib/commands/recommend.js +22 -2
- package/lib/commands/release.js +91 -0
- package/lib/commands/remember.js +74 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +851 -979
- package/lib/commands/shepherd.js +436 -0
- package/lib/commands/ship.js +23 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +329 -11
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +15 -2
- package/lib/commands/test.js +58 -7
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +56 -25
- package/lib/commands/worktree.js +308 -128
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +946 -0
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +42 -17
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +697 -0
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +193 -0
- package/lib/global-flags.js +74 -0
- package/lib/greptile-match.js +7 -63
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +451 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/kernel/backing-issue.js +305 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2504 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +8 -4
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +157 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1273 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +166 -0
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2089 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +332 -23
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +191 -0
- package/lib/status/beads-snapshot.js +145 -0
- package/lib/status/presenter.js +216 -0
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +199 -0
- package/lib/workflow/enforce-stage.js +298 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +159 -14
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +46 -36
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +284 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +64 -232
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +144 -47
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +183 -79
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +108 -0
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +117 -369
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +251 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +302 -28
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +46 -52
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +134 -159
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/skills/ship/SKILL.md +251 -0
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/skills/status/SKILL.md +102 -0
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/ship.prompt.md +0 -215
- package/.github/prompts/status.prompt.md +0 -92
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -56
- package/.github/workflows/github-to-beads.yml +0 -97
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -255
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-health-check.js +0 -143
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -567
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -332
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -78
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const {
|
|
5
|
+
runJsonlProjectionConsumer,
|
|
6
|
+
readProjection,
|
|
7
|
+
resolveProjectionDir,
|
|
8
|
+
writeProjection,
|
|
9
|
+
DEFAULT_PROJECTION_TARGET,
|
|
10
|
+
} = require('../kernel/projection-jsonl-writer');
|
|
11
|
+
|
|
12
|
+
// Full set the consumer may call — including the retry (recordProjectionFailure)
|
|
13
|
+
// and dead-letter (deadLetterProjection) paths — so a partially-implemented
|
|
14
|
+
// broker is skipped cleanly instead of failing mid-run.
|
|
15
|
+
const PROJECTION_BROKER_METHODS = [
|
|
16
|
+
'listProjectionOutbox',
|
|
17
|
+
'loadProjectionModel',
|
|
18
|
+
'markProjectionDelivered',
|
|
19
|
+
'recordProjectionFailure',
|
|
20
|
+
'deadLetterProjection',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse `forge export` options from positional args (and a parsed flags object
|
|
25
|
+
* as a fallback). Supported: --import, --dry-run, --json, --dir=<path> / --dir <path>.
|
|
26
|
+
*/
|
|
27
|
+
function parseExportOptions(args = [], flags = {}) {
|
|
28
|
+
const options = {
|
|
29
|
+
import: Boolean(flags.import),
|
|
30
|
+
dryRun: Boolean(flags['dry-run'] || flags.dryRun),
|
|
31
|
+
json: Boolean(flags.json),
|
|
32
|
+
dir: flags.dir,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
36
|
+
const arg = args[i];
|
|
37
|
+
if (arg === '--import') options.import = true;
|
|
38
|
+
else if (arg === '--dry-run') options.dryRun = true;
|
|
39
|
+
else if (arg === '--json') options.json = true;
|
|
40
|
+
else if (arg.startsWith('--dir=')) options.dir = arg.slice('--dir='.length);
|
|
41
|
+
else if (arg === '--dir') {
|
|
42
|
+
// only consume the next token when it is a real value, not another flag
|
|
43
|
+
const next = args[i + 1];
|
|
44
|
+
if (next && !next.startsWith('--')) {
|
|
45
|
+
options.dir = next;
|
|
46
|
+
i += 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return options;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function brokerSupportsProjection(broker) {
|
|
55
|
+
return Boolean(broker) && PROJECTION_BROKER_METHODS.every(method => typeof broker[method] === 'function');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveDirArg(dirArg, projectRoot) {
|
|
59
|
+
if (!dirArg) return undefined;
|
|
60
|
+
return path.resolve(projectRoot || process.cwd(), dirArg);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderHuman(payload) {
|
|
64
|
+
if (payload.error) return `forge export failed: ${payload.error}`;
|
|
65
|
+
if (payload.imported === true) {
|
|
66
|
+
const { issues, comments, dependencies } = payload.counts;
|
|
67
|
+
const skipped = issues.skipped + comments.skipped + dependencies.skipped;
|
|
68
|
+
// Report "Imported N" ONLY for records actually written; a re-import that
|
|
69
|
+
// applies nothing (everything already present) must not claim it imported them.
|
|
70
|
+
if (payload.applied === 0) {
|
|
71
|
+
return `Kernel projection at ${payload.dir} is already hydrated — nothing new imported`
|
|
72
|
+
+ ` (${skipped} record${skipped === 1 ? '' : 's'} already present)`;
|
|
73
|
+
}
|
|
74
|
+
return `Imported Kernel projection from ${payload.dir}`
|
|
75
|
+
+ ` (${issues.inserted} issues, ${comments.inserted} comments, ${dependencies.inserted} dependencies`
|
|
76
|
+
+ (skipped ? `; ${skipped} already present` : '') + ')';
|
|
77
|
+
}
|
|
78
|
+
if (payload.imported === false) return payload.message;
|
|
79
|
+
if (payload.dryRun) return `${payload.pending} pending projection entr${payload.pending === 1 ? 'y' : 'ies'} → ${payload.dir} (dry run, nothing written)`;
|
|
80
|
+
if (payload.skipped) return payload.message;
|
|
81
|
+
if (payload.exported) return `Exported Kernel projection to ${payload.dir} (drained ${payload.drained})`;
|
|
82
|
+
return payload.message || 'Nothing to export';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function finalize(options, payload) {
|
|
86
|
+
const output = options.json ? JSON.stringify(payload, null, 2) : renderHuman(payload);
|
|
87
|
+
return { ...payload, output, json: Boolean(options.json) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Forge Export Command (D16 — Kernel JSONL portability projection).
|
|
92
|
+
*
|
|
93
|
+
* Explicit, Kernel-owned projection of issues/comments/dependencies to
|
|
94
|
+
* deterministic git-tracked JSONL under `.forge/kernel/`. This is NOT auto-run on
|
|
95
|
+
* mutation/push (D16 forbids that); it is an on-demand portability/bootstrap
|
|
96
|
+
* surface. The `--import` path reads a committed snapshot back from disk (no
|
|
97
|
+
* broker required) and verifies its manifest integrity.
|
|
98
|
+
*
|
|
99
|
+
* @module commands/export
|
|
100
|
+
*/
|
|
101
|
+
module.exports = {
|
|
102
|
+
name: 'export',
|
|
103
|
+
description: 'Export the Kernel backlog to deterministic git-tracked JSONL (D16 portability projection)',
|
|
104
|
+
usage: 'forge export [--dir <path>] [--dry-run] [--json] [--import]',
|
|
105
|
+
flags: {},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {string[]} args - Positional arguments / flags
|
|
109
|
+
* @param {object} flags - Parsed CLI flags (fallback source)
|
|
110
|
+
* @param {string} projectRoot - Project root path
|
|
111
|
+
* @param {object} [opts] - Dependency injection: _broker, _writer, _now, _fs
|
|
112
|
+
*/
|
|
113
|
+
async handler(args = [], flags = {}, projectRoot, opts = {}) {
|
|
114
|
+
const options = parseExportOptions(args, flags || {});
|
|
115
|
+
const now = opts._now || new Date().toISOString();
|
|
116
|
+
const writer = opts._writer || writeProjection;
|
|
117
|
+
const fsImpl = opts._fs;
|
|
118
|
+
const projectionDir = resolveDirArg(options.dir, projectRoot);
|
|
119
|
+
|
|
120
|
+
// --- Import / hydrate (read committed JSONL → write kernel.sqlite) ---
|
|
121
|
+
// readProjection verifies snapshot integrity (sha256, per-file kind, manifest
|
|
122
|
+
// counts, and schema_version compatibility) and throws on any mismatch, so an
|
|
123
|
+
// incompatible/tampered snapshot never reaches the kernel writer.
|
|
124
|
+
if (options.import) {
|
|
125
|
+
try {
|
|
126
|
+
const snapshot = readProjection({ projectionDir, projectRoot, fsImpl });
|
|
127
|
+
if (!snapshot) {
|
|
128
|
+
return finalize(options, {
|
|
129
|
+
success: true,
|
|
130
|
+
imported: false,
|
|
131
|
+
dir: resolveProjectionDir(projectionDir, projectRoot),
|
|
132
|
+
message: 'No projection snapshot found to import',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const read = {
|
|
137
|
+
issues: snapshot.model.issues.length,
|
|
138
|
+
comments: snapshot.model.comments.length,
|
|
139
|
+
dependencies: snapshot.model.dependencies.length,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Hydration requires a projection-capable Kernel broker (importIssues).
|
|
143
|
+
// The CLI injects one for `export` (a Kernel-tool command); without it we
|
|
144
|
+
// validated the snapshot but cannot write — say so instead of claiming an
|
|
145
|
+
// import happened.
|
|
146
|
+
const importBroker = opts._broker || null;
|
|
147
|
+
if (!importBroker || typeof importBroker.importIssues !== 'function') {
|
|
148
|
+
return finalize(options, {
|
|
149
|
+
success: true,
|
|
150
|
+
imported: false,
|
|
151
|
+
dir: snapshot.dir,
|
|
152
|
+
read,
|
|
153
|
+
message: `Read ${read.issues} issues from ${snapshot.dir} but no Kernel broker is `
|
|
154
|
+
+ 'available to import into — nothing was written',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// importIssues requires the schema/migrations to exist; the CLI broker is
|
|
159
|
+
// pre-initialized, but initialize() is idempotent so calling it is safe.
|
|
160
|
+
if (typeof importBroker.initialize === 'function') {
|
|
161
|
+
await importBroker.initialize();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Idempotent + transactional upsert-by-id (ON CONFLICT DO NOTHING).
|
|
165
|
+
const summary = await importBroker.importIssues(snapshot.model, { now });
|
|
166
|
+
const applied = summary.issues.inserted + summary.comments.inserted + summary.dependencies.inserted;
|
|
167
|
+
|
|
168
|
+
return finalize(options, {
|
|
169
|
+
success: true,
|
|
170
|
+
imported: true,
|
|
171
|
+
dir: snapshot.dir,
|
|
172
|
+
counts: summary,
|
|
173
|
+
applied,
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return finalize(options, { success: false, imported: false, error: error.message });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// --- Export (requires a projection-capable Kernel broker) ------------
|
|
181
|
+
const broker = opts._broker || null;
|
|
182
|
+
if (!brokerSupportsProjection(broker)) {
|
|
183
|
+
return finalize(options, {
|
|
184
|
+
success: true,
|
|
185
|
+
exported: false,
|
|
186
|
+
skipped: true,
|
|
187
|
+
message: 'No Kernel broker available — nothing to export',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (options.dryRun) {
|
|
192
|
+
const pending = (await broker.listProjectionOutbox({
|
|
193
|
+
target: DEFAULT_PROJECTION_TARGET,
|
|
194
|
+
status: 'pending',
|
|
195
|
+
now,
|
|
196
|
+
})) || [];
|
|
197
|
+
return finalize(options, {
|
|
198
|
+
success: true,
|
|
199
|
+
exported: false,
|
|
200
|
+
dryRun: true,
|
|
201
|
+
pending: pending.length,
|
|
202
|
+
dir: resolveProjectionDir(projectionDir, projectRoot),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const run = await runJsonlProjectionConsumer({ broker, projectionDir, projectRoot, now, writer });
|
|
208
|
+
|
|
209
|
+
// A write failure leaves entries retried/dead-lettered without a snapshot;
|
|
210
|
+
// surface it as a failure rather than silently reporting success.
|
|
211
|
+
if (!run.written && run.error) {
|
|
212
|
+
return finalize(options, {
|
|
213
|
+
success: false,
|
|
214
|
+
exported: false,
|
|
215
|
+
error: run.error,
|
|
216
|
+
drained: run.drained,
|
|
217
|
+
retried: run.retried,
|
|
218
|
+
dead: run.dead,
|
|
219
|
+
dir: resolveProjectionDir(projectionDir, projectRoot),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return finalize(options, {
|
|
224
|
+
success: true,
|
|
225
|
+
exported: run.written,
|
|
226
|
+
drained: run.drained,
|
|
227
|
+
delivered: run.delivered,
|
|
228
|
+
retried: run.retried,
|
|
229
|
+
dead: run.dead,
|
|
230
|
+
dir: run.write ? run.write.dir : resolveProjectionDir(projectionDir, projectRoot),
|
|
231
|
+
files: run.write ? run.write.files : [],
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return finalize(options, { success: false, exported: false, error: error.message });
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `forge gate <enable|disable|approve|reject|status|check> ...`
|
|
5
|
+
*
|
|
6
|
+
* Two families over the same known-gate set:
|
|
7
|
+
*
|
|
8
|
+
* TOGGLE (config surface): `enable|disable <gate-id>` set
|
|
9
|
+
* `workflow.gates.<gate-id>.enabled` true/false in `.forge/config.yaml`. The shipped
|
|
10
|
+
* resolver (`applyEnabledConfig`) already consumes this field, so `forge options
|
|
11
|
+
* gates --json` reflects the flip with zero new read code. Write-time validation:
|
|
12
|
+
* an unknown gate id (or a locked gate being disabled) errors BEFORE anything is
|
|
13
|
+
* written — never mid-run.
|
|
14
|
+
*
|
|
15
|
+
* EVENTS (gates-as-kernel-events — Fable's insight): `approve|reject <issue> <gate>`
|
|
16
|
+
* record a durable `gate.approved` / `gate.rejected` event on the issue; `status`
|
|
17
|
+
* lists them (resume-safe after a compaction/crash); `check` exits 0 iff the gate is
|
|
18
|
+
* DISABLED or an approval event exists — the reusable enforcement primitive a stage
|
|
19
|
+
* skill calls. See lib/gate-events.js and docs/work/2026-07-04-kernel-native-skills/
|
|
20
|
+
* decisions.md.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const path = require('node:path');
|
|
24
|
+
const { setConfigOverride } = require('../config-writer');
|
|
25
|
+
const { getDefaultRuntimeGraph, getResolvedRuntimeGraph } = require('../core/runtime-graph');
|
|
26
|
+
const {
|
|
27
|
+
recordGateEvent,
|
|
28
|
+
listGateEvents,
|
|
29
|
+
isGateApproved,
|
|
30
|
+
} = require('../gate-events');
|
|
31
|
+
|
|
32
|
+
const TOGGLE_ACTIONS = new Set(['enable', 'disable']);
|
|
33
|
+
const EVENT_ACTIONS = new Set(['approve', 'reject', 'status', 'check']);
|
|
34
|
+
|
|
35
|
+
function usage() {
|
|
36
|
+
return 'Usage: forge gate <enable|disable|approve|reject|status|check> [<issue-id>] <gate-id> [--reason <text>] [--json]';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The known-toggle set is gates PLUS unlocked toggleable rails (e.g.
|
|
40
|
+
// rail.kernel_tracking): both are governed through the same `forge gate
|
|
41
|
+
// enable|disable` surface and the resolver's rail-aware workflow.gates loop.
|
|
42
|
+
// The gate.* / rail.* id namespaces are disjoint, so one flat map is unambiguous.
|
|
43
|
+
function knownGates() {
|
|
44
|
+
const graph = getDefaultRuntimeGraph();
|
|
45
|
+
return new Map([...graph.gates, ...graph.rails].map(primitive => [primitive.id, primitive]));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Kernel deps + env are threaded through the command opts (4th handler arg) so tests
|
|
49
|
+
// and the orchestrator can inject a shared, already-migrated kernel.
|
|
50
|
+
function kernelDeps(opts = {}) {
|
|
51
|
+
return { kernelBroker: opts.kernelBroker, kernelDriver: opts.kernelDriver };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateKnownGate(gateId) {
|
|
55
|
+
const gates = knownGates();
|
|
56
|
+
const gate = gates.get(gateId);
|
|
57
|
+
if (!gate) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: `Unknown gate '${gateId}'. Known gates: ${[...gates.keys()].join(', ')}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { ok: true, gate };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handleToggle(action, gateId, projectRoot) {
|
|
67
|
+
if (!gateId) {
|
|
68
|
+
return { success: false, error: `Missing gate id.\n${usage()}` };
|
|
69
|
+
}
|
|
70
|
+
const known = validateKnownGate(gateId);
|
|
71
|
+
if (!known.ok) return { success: false, error: known.error };
|
|
72
|
+
if (action === 'disable' && known.gate.locked === true) {
|
|
73
|
+
return { success: false, error: `Cannot disable locked gate '${gateId}'.` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const enabled = action === 'enable';
|
|
77
|
+
const { configPath } = setConfigOverride(
|
|
78
|
+
projectRoot,
|
|
79
|
+
['workflow', 'gates', gateId, 'enabled'],
|
|
80
|
+
enabled,
|
|
81
|
+
);
|
|
82
|
+
const where = path.relative(projectRoot, configPath) || configPath;
|
|
83
|
+
return {
|
|
84
|
+
success: true,
|
|
85
|
+
output: `${action}d gate '${gateId}' (workflow.gates.${gateId}.enabled=${enabled}) in ${where}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function handleDecision(action, issueId, gateId, flags, projectRoot, opts) {
|
|
90
|
+
if (!issueId || !gateId) {
|
|
91
|
+
return { success: false, error: `Missing issue id or gate id.\n${usage()}` };
|
|
92
|
+
}
|
|
93
|
+
const known = validateKnownGate(gateId);
|
|
94
|
+
if (!known.ok) return { success: false, error: known.error };
|
|
95
|
+
|
|
96
|
+
const decision = action === 'approve' ? 'approved' : 'rejected';
|
|
97
|
+
const reason = typeof flags.reason === 'string' ? flags.reason : undefined;
|
|
98
|
+
const result = await recordGateEvent(projectRoot, {
|
|
99
|
+
issueId,
|
|
100
|
+
gateId,
|
|
101
|
+
decision,
|
|
102
|
+
reason,
|
|
103
|
+
env: opts.env,
|
|
104
|
+
deps: kernelDeps(opts),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (result.issueMissing) {
|
|
108
|
+
return { success: false, error: `Issue '${issueId}' not found.` };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const verb = action === 'approve' ? 'approved' : 'rejected';
|
|
112
|
+
const suffix = result.duplicate ? ' (already recorded)' : '';
|
|
113
|
+
const reasonNote = reason ? ` — ${reason}` : '';
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
duplicate: result.duplicate === true,
|
|
117
|
+
actor: result.actor,
|
|
118
|
+
output: `${verb} gate '${gateId}' for ${issueId} (actor ${result.actor})${reasonNote}${suffix}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function handleStatus(issueId, flags, projectRoot, opts) {
|
|
123
|
+
if (!issueId) {
|
|
124
|
+
return { success: false, error: `Missing issue id.\n${usage()}` };
|
|
125
|
+
}
|
|
126
|
+
const events = await listGateEvents(projectRoot, issueId, { deps: kernelDeps(opts) });
|
|
127
|
+
|
|
128
|
+
if (flags.json) {
|
|
129
|
+
return { success: true, output: JSON.stringify({ issue: issueId, events }, null, 2) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (events.length === 0) {
|
|
133
|
+
return { success: true, output: `No gate events for ${issueId}.` };
|
|
134
|
+
}
|
|
135
|
+
const lines = events.map(event => {
|
|
136
|
+
const label = event.event_type === 'gate.approved' ? 'APPROVED' : 'REJECTED';
|
|
137
|
+
const reason = event.reason ? ` — ${event.reason}` : '';
|
|
138
|
+
return `${label} ${event.gate} by ${event.actor} at ${event.created_at}${reason}`;
|
|
139
|
+
});
|
|
140
|
+
return { success: true, output: `Gate events for ${issueId}:\n${lines.join('\n')}` };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function handleCheck(issueId, gateId, projectRoot, opts) {
|
|
144
|
+
if (!issueId || !gateId) {
|
|
145
|
+
return { success: false, error: `Missing issue id or gate id.\n${usage()}` };
|
|
146
|
+
}
|
|
147
|
+
const known = validateKnownGate(gateId);
|
|
148
|
+
if (!known.ok) return { success: false, error: known.error };
|
|
149
|
+
|
|
150
|
+
// Disabled gate/rail → satisfied without any approval event (read via the shipped
|
|
151
|
+
// resolver). Rails share the workflow.gates toggle surface, so check both collections.
|
|
152
|
+
const resolved = getResolvedRuntimeGraph({ projectRoot });
|
|
153
|
+
const resolvedGate = [...resolved.gates, ...resolved.rails].find(gate => gate.id === gateId);
|
|
154
|
+
if (resolvedGate && resolvedGate.enabled === false) {
|
|
155
|
+
return { success: true, output: `gate ${gateId} is disabled — satisfied for ${issueId}` };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const approved = await isGateApproved(projectRoot, issueId, gateId, { deps: kernelDeps(opts) });
|
|
159
|
+
if (approved) {
|
|
160
|
+
return { success: true, output: `gate ${gateId} approved for ${issueId}` };
|
|
161
|
+
}
|
|
162
|
+
return { success: false, error: `gate ${gateId} not approved for ${issueId}` };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function handler(args, flags = {}, projectRoot = process.cwd(), opts = {}) {
|
|
166
|
+
const [action, ...rest] = args;
|
|
167
|
+
|
|
168
|
+
if (TOGGLE_ACTIONS.has(action)) {
|
|
169
|
+
return handleToggle(action, rest[0], projectRoot);
|
|
170
|
+
}
|
|
171
|
+
if (EVENT_ACTIONS.has(action)) {
|
|
172
|
+
if (action === 'status') {
|
|
173
|
+
return handleStatus(rest[0], flags, projectRoot, opts);
|
|
174
|
+
}
|
|
175
|
+
if (action === 'check') {
|
|
176
|
+
return handleCheck(rest[0], rest[1], projectRoot, opts);
|
|
177
|
+
}
|
|
178
|
+
return handleDecision(action, rest[0], rest[1], flags, projectRoot, opts);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: `Expected 'enable', 'disable', 'approve', 'reject', 'status', or 'check'.\n${usage()}`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
name: 'gate',
|
|
189
|
+
description: 'Toggle a workflow gate, or record/query human-gate approval events',
|
|
190
|
+
usage: usage(),
|
|
191
|
+
handler,
|
|
192
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `forge hooks install --global [--harness codex|hermes|all] [--dry-run]`
|
|
5
|
+
*
|
|
6
|
+
* The opt-in, CONSENT-GUARDED delivery path for the last native-hooks gap
|
|
7
|
+
* (kernel issue 66dd5a1f, epics 90f2f631 + 1390e1d1): Codex and Hermes have
|
|
8
|
+
* real native hook surfaces, but both live in GLOBAL (home-dir) config —
|
|
9
|
+
* `$CODEX_HOME/config.toml` and `~/.hermes/config.yaml` — which project
|
|
10
|
+
* `forge setup` intentionally never writes.
|
|
11
|
+
*
|
|
12
|
+
* Consent model:
|
|
13
|
+
* - the explicit `--global` flag IS the consent — without it the command
|
|
14
|
+
* refuses with guidance and writes nothing;
|
|
15
|
+
* - the command prints exactly which files are written and the exact Forge
|
|
16
|
+
* hook block merged into each, BEFORE reporting results;
|
|
17
|
+
* - `--dry-run` shows the same plan without touching disk;
|
|
18
|
+
* - existing user config is preserved (read → merge → write, idempotent via
|
|
19
|
+
* the forge-native-hook.js marker); unmergeable config is backed up and
|
|
20
|
+
* skipped, never overwritten.
|
|
21
|
+
*
|
|
22
|
+
* This command is deliberately NOT wired into project `forge setup`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
GLOBAL_HOOK_HARNESSES,
|
|
27
|
+
renderGlobalHookBlock,
|
|
28
|
+
installGlobalHooks,
|
|
29
|
+
} = require('../hook-global-installer');
|
|
30
|
+
const { sessionStartCapability, userPromptSubmitCapability } = require('../hook-renderer');
|
|
31
|
+
const { collectDigestData, buildMemoryDigest } = require('../memory-digest');
|
|
32
|
+
const { collectInbox, buildInboxNudge } = require('../inbox');
|
|
33
|
+
|
|
34
|
+
function usage() {
|
|
35
|
+
return 'Usage: forge hooks install --global [--harness codex|hermes|all] [--dry-run]\n'
|
|
36
|
+
+ ' forge hooks session-start --harness <claude> (machine-facing; emits SessionStart context)\n'
|
|
37
|
+
+ ' forge hooks inbox-pickup --harness <claude> (machine-facing; emits UserPromptSubmit context)';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Parse `--harness <h>` (defaults to claude) from a session-start arg slice. */
|
|
41
|
+
function parseHarness(rest) {
|
|
42
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
43
|
+
if (rest[i] === '--harness') return rest[i + 1] || 'claude';
|
|
44
|
+
if (rest[i].startsWith('--harness=')) return rest[i].slice('--harness='.length);
|
|
45
|
+
}
|
|
46
|
+
return 'claude';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Wrap a digest into a harness-native SessionStart payload, or '' when unsupported. */
|
|
50
|
+
function formatSessionStart(harness, text) {
|
|
51
|
+
if (harness === 'claude') {
|
|
52
|
+
return JSON.stringify({
|
|
53
|
+
hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: text },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// No other harness has a verified session-start context surface — emit nothing.
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* `forge hooks session-start --harness <h>` — the CONTEXT hook Forge PUSHES to an agent
|
|
62
|
+
* at session start. Machine-facing plumbing: emits harness-native SessionStart JSON on
|
|
63
|
+
* stdout (Claude: { hookSpecificOutput.additionalContext }). FAIL-OPEN by construction —
|
|
64
|
+
* any failure, an unsupported harness, or an empty digest yields '' (the harness then
|
|
65
|
+
* injects nothing). NEVER throws and NEVER emits malformed JSON.
|
|
66
|
+
*
|
|
67
|
+
* @param {string[]} rest - args after the `session-start` action.
|
|
68
|
+
* @param {string} projectRoot
|
|
69
|
+
* @param {object} [opts] - injectable digest fetchers ({ fetchNotes, fetchIssues }).
|
|
70
|
+
* @returns {Promise<{ success: boolean, output: string }>}
|
|
71
|
+
*/
|
|
72
|
+
async function handleSessionStart(rest, projectRoot, opts = {}) {
|
|
73
|
+
try {
|
|
74
|
+
const harness = parseHarness(rest);
|
|
75
|
+
if (!sessionStartCapability(harness).rendered) return { success: true, output: '' };
|
|
76
|
+
const data = await collectDigestData(projectRoot, opts);
|
|
77
|
+
const digest = buildMemoryDigest(data, opts);
|
|
78
|
+
if (digest.empty) return { success: true, output: '' };
|
|
79
|
+
return { success: true, output: formatSessionStart(harness, digest.text) };
|
|
80
|
+
} catch {
|
|
81
|
+
// Fail-open: a context hook must never break a session.
|
|
82
|
+
return { success: true, output: '' };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Wrap a digest into a harness-native UserPromptSubmit payload, or '' when unsupported. */
|
|
87
|
+
function formatUserPromptSubmit(harness, text) {
|
|
88
|
+
if (harness === 'claude') {
|
|
89
|
+
return JSON.stringify({
|
|
90
|
+
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: text },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// No other harness has a verified UserPromptSubmit context surface — emit nothing.
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `forge hooks inbox-pickup --harness <h>` — the COMPLIANT comment-back CONTEXT hook. On each
|
|
99
|
+
* prompt it emits harness-native UserPromptSubmit JSON carrying a COMPACT count+pointer nudge
|
|
100
|
+
* (Claude: { hookSpecificOutput.additionalContext }). It deliberately does NOT re-emit the full
|
|
101
|
+
* fenced bodies: Claude APPENDS UserPromptSubmit additionalContext to history on every prompt
|
|
102
|
+
* (it does not replace prior injections — a documented context-bloat limitation), so a compact
|
|
103
|
+
* nudge keeps per-prompt accumulation negligible while still flagging "you have pending items,
|
|
104
|
+
* run `forge inbox`". The full fenced digest surfaces once at SessionStart and on demand via
|
|
105
|
+
* `forge inbox`. It reads the user's OWN kernel data via a supported hook — it NEVER injects
|
|
106
|
+
* into a running session's stdin and NEVER drives the agent programmatically (Anthropic Usage
|
|
107
|
+
* Policy, kernel issue 6d10c1a1). FAIL-OPEN: any failure, an unsupported harness, or nothing
|
|
108
|
+
* pending yields '' (the harness injects nothing). NEVER throws, NEVER emits malformed JSON.
|
|
109
|
+
*
|
|
110
|
+
* @param {string[]} rest - args after the `inbox-pickup` action.
|
|
111
|
+
* @param {string} projectRoot
|
|
112
|
+
* @param {object} [opts] - injectable inbox fetchers ({ fetchClaims, fetchComments, ... }).
|
|
113
|
+
* @returns {Promise<{ success: boolean, output: string }>}
|
|
114
|
+
*/
|
|
115
|
+
async function handleInboxPickup(rest, projectRoot, opts = {}) {
|
|
116
|
+
try {
|
|
117
|
+
const harness = parseHarness(rest);
|
|
118
|
+
if (!userPromptSubmitCapability(harness).rendered) return { success: true, output: '' };
|
|
119
|
+
const pending = await collectInbox(projectRoot, opts);
|
|
120
|
+
const nudge = buildInboxNudge(pending);
|
|
121
|
+
if (nudge.empty) return { success: true, output: '' };
|
|
122
|
+
return { success: true, output: formatUserPromptSubmit(harness, nudge.text) };
|
|
123
|
+
} catch {
|
|
124
|
+
// Fail-open: a context hook must never break a prompt.
|
|
125
|
+
return { success: true, output: '' };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseInstallArgs(rest) {
|
|
130
|
+
const parsed = { global: false, dryRun: false, harness: 'all', unknown: [] };
|
|
131
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
132
|
+
const arg = rest[i];
|
|
133
|
+
if (arg === '--global') parsed.global = true;
|
|
134
|
+
else if (arg === '--dry-run') parsed.dryRun = true;
|
|
135
|
+
else if (arg === '--harness') { parsed.harness = rest[i + 1]; i += 1; }
|
|
136
|
+
else if (arg.startsWith('--harness=')) parsed.harness = arg.slice('--harness='.length);
|
|
137
|
+
else parsed.unknown.push(arg);
|
|
138
|
+
}
|
|
139
|
+
return parsed;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function indent(text, prefix) {
|
|
143
|
+
return String(text).replace(/\s+$/, '').split('\n').map(l => prefix + l).join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const GLOBAL_CONSENT_ERROR = [
|
|
147
|
+
'forge hooks install writes GLOBAL (home-directory) harness config:',
|
|
148
|
+
' - Codex : $CODEX_HOME/config.toml (default ~/.codex/config.toml)',
|
|
149
|
+
' - Hermes: ~/.hermes/config.yaml',
|
|
150
|
+
'Global config affects EVERY project on this machine, so Forge never writes it',
|
|
151
|
+
'silently — re-run with the explicit --global flag to consent, and add --dry-run',
|
|
152
|
+
'first to preview exactly what would be written.',
|
|
153
|
+
usage(),
|
|
154
|
+
].join('\n');
|
|
155
|
+
|
|
156
|
+
const INSTALL_FOOTER = [
|
|
157
|
+
'',
|
|
158
|
+
'Note: the hook commands invoke `node .forge/hooks/forge-native-hook.js` relative',
|
|
159
|
+
'to the workspace root (Codex and Hermes run hooks from there), so enforcement',
|
|
160
|
+
'applies inside Forge-initialized projects; elsewhere the adapter is absent and',
|
|
161
|
+
'the hook fails open (no deny decision is emitted), leaving tool calls untouched.',
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
/** Validate parsed install args; returns an error string or null when valid. */
|
|
165
|
+
function validateInstallArgs(parsed) {
|
|
166
|
+
if (parsed.unknown.length > 0) return `Unknown argument(s): ${parsed.unknown.join(' ')}\n${usage()}`;
|
|
167
|
+
if (!parsed.global) return GLOBAL_CONSENT_ERROR;
|
|
168
|
+
const harnesses = parsed.harness === 'all' ? GLOBAL_HOOK_HARNESSES : [parsed.harness];
|
|
169
|
+
if (!harnesses.every(h => GLOBAL_HOOK_HARNESSES.includes(h))) {
|
|
170
|
+
return `Unknown --harness '${parsed.harness}'. Allowed: codex, hermes, all.\n${usage()}`;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Format the output lines for a single install result. */
|
|
176
|
+
function renderInstallResult(res, dryRun) {
|
|
177
|
+
const lines = ['', `${res.harness} -> ${res.file}`];
|
|
178
|
+
if (res.skipped) {
|
|
179
|
+
lines.push(` SKIPPED (left untouched): ${res.reason}`);
|
|
180
|
+
if (res.backup) lines.push(` Backed up existing file to: ${res.backup} (.bak)`);
|
|
181
|
+
else if (dryRun) lines.push(' (dry-run: existing file would be backed up to a .bak and skipped)');
|
|
182
|
+
return lines;
|
|
183
|
+
}
|
|
184
|
+
lines.push(indent(renderGlobalHookBlock(res.harness), ' '));
|
|
185
|
+
if (dryRun) {
|
|
186
|
+
lines.push(res.changed === false
|
|
187
|
+
? ' [dry-run] already up to date — a real run would change nothing'
|
|
188
|
+
: ' [dry-run] would merge the block above into this file');
|
|
189
|
+
} else {
|
|
190
|
+
lines.push(res.changed === false
|
|
191
|
+
? ' Already up to date (no changes written).'
|
|
192
|
+
: ` Merged Forge hooks into ${res.existed ? 'existing' : 'new'} config.`);
|
|
193
|
+
}
|
|
194
|
+
return lines;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The `install` action — consent-guarded GLOBAL hook install (Codex/Hermes). */
|
|
198
|
+
function handleInstall(args, flags, opts) {
|
|
199
|
+
const parsed = parseInstallArgs(args.slice(1));
|
|
200
|
+
const dryRun = parsed.dryRun || Boolean(flags.dryRun);
|
|
201
|
+
|
|
202
|
+
const validationError = validateInstallArgs(parsed);
|
|
203
|
+
if (validationError) return { success: false, error: validationError };
|
|
204
|
+
|
|
205
|
+
const harnesses = parsed.harness === 'all' ? GLOBAL_HOOK_HARNESSES : [parsed.harness];
|
|
206
|
+
// env/homeDir are injectable through the command opts so tests never touch the
|
|
207
|
+
// real home directory; real dispatch passes neither and gets the defaults.
|
|
208
|
+
const results = installGlobalHooks({ harnesses, dryRun, env: opts.env || process.env, homeDir: opts.homeDir });
|
|
209
|
+
|
|
210
|
+
const out = [
|
|
211
|
+
dryRun ? 'forge hooks install --global (dry-run — nothing will be written)' : 'forge hooks install --global',
|
|
212
|
+
'',
|
|
213
|
+
'This merges the following Forge hook block into each GLOBAL config,',
|
|
214
|
+
'preserving all existing user config (idempotent re-runs):',
|
|
215
|
+
...results.flatMap(res => renderInstallResult(res, dryRun)),
|
|
216
|
+
...INSTALL_FOOTER,
|
|
217
|
+
];
|
|
218
|
+
return { success: true, output: out.join('\n'), results };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function handler(args, flags = {}, projectRoot, opts = {}) {
|
|
222
|
+
const action = args[0];
|
|
223
|
+
if (action === 'session-start') return handleSessionStart(args.slice(1), projectRoot, opts);
|
|
224
|
+
if (action === 'inbox-pickup') return handleInboxPickup(args.slice(1), projectRoot, opts);
|
|
225
|
+
if (action === 'install') return handleInstall(args, flags, opts);
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
error: `forge hooks supports: install, session-start, inbox-pickup.\n${usage()}`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = {
|
|
233
|
+
name: 'hooks',
|
|
234
|
+
description: 'Opt-in install of Forge native hooks into GLOBAL harness config (Codex/Hermes)',
|
|
235
|
+
usage: usage(),
|
|
236
|
+
flags: {
|
|
237
|
+
'--global': 'Required consent flag — this command writes home-directory config',
|
|
238
|
+
'--harness': 'codex | hermes | all (default: all)',
|
|
239
|
+
'--dry-run': 'Preview the merge without writing anything',
|
|
240
|
+
},
|
|
241
|
+
handler,
|
|
242
|
+
};
|