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,377 @@
|
|
|
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, sessionEndCapability } = require('../hook-renderer');
|
|
31
|
+
const { collectDigestData, buildMemoryDigest, defaultFetchIssues, defaultFetchNotes } = require('../memory-digest');
|
|
32
|
+
const { collectInbox, buildInboxNudge } = require('../inbox');
|
|
33
|
+
const { collectDigest } = require('../pr-monitor/digest');
|
|
34
|
+
|
|
35
|
+
function usage() {
|
|
36
|
+
return 'Usage: forge hooks install --global [--harness codex|hermes|all] [--dry-run]\n'
|
|
37
|
+
+ ' forge hooks session-start --harness <claude> (machine-facing; emits SessionStart context)\n'
|
|
38
|
+
+ ' forge hooks inbox-pickup --harness <claude> (machine-facing; emits UserPromptSubmit context)\n'
|
|
39
|
+
+ ' forge hooks shepherd-events --harness <claude> (machine-facing; emits UserPromptSubmit PR-monitor deltas)\n'
|
|
40
|
+
+ ' forge hooks capture --harness <claude> --trigger <precompact|stop> (machine-facing; captures a session summary on exit)';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Parse `--harness <h>` (defaults to claude) from a session-start arg slice. */
|
|
44
|
+
function parseHarness(rest) {
|
|
45
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
46
|
+
if (rest[i] === '--harness') return rest[i + 1] || 'claude';
|
|
47
|
+
if (rest[i].startsWith('--harness=')) return rest[i].slice('--harness='.length);
|
|
48
|
+
}
|
|
49
|
+
return 'claude';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Parse `--trigger <t>` (defaults to stop) from a capture arg slice. */
|
|
53
|
+
function parseTrigger(rest) {
|
|
54
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
55
|
+
if (rest[i] === '--trigger') return rest[i + 1] || 'stop';
|
|
56
|
+
if (rest[i].startsWith('--trigger=')) return rest[i].slice('--trigger='.length);
|
|
57
|
+
}
|
|
58
|
+
return 'stop';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Capture bounds — the snapshot is a small NUDGE, not a manual. A hard issue cap + per-title
|
|
62
|
+
// cap + overall body cap keep the note (which is re-injected at the NEXT session's SessionStart
|
|
63
|
+
// digest) token-bounded. Tags mark it a session-summary typed note AND a Forge auto-capture
|
|
64
|
+
// (the latter is the dedupe/idempotency key that stops per-turn Stop flooding).
|
|
65
|
+
const CAPTURE_ISSUE_CAP = 5;
|
|
66
|
+
const CAPTURE_TITLE_CAP = 80;
|
|
67
|
+
const CAPTURE_NOTE_CAP = 1000;
|
|
68
|
+
const CAPTURE_AUTO_TAG = 'forge:auto-capture';
|
|
69
|
+
const CAPTURE_TAGS = ['type:session-summary', CAPTURE_AUTO_TAG];
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build the deterministic, token-bounded capture note body. PURE. The body deliberately
|
|
73
|
+
* carries NO timestamp (the store stamps its own) so an unchanged session state yields a
|
|
74
|
+
* byte-identical body across repeated Stops — that identity is what the dedupe keys on.
|
|
75
|
+
* @param {string} trigger - 'precompact' | 'stop'
|
|
76
|
+
* @param {object[]} issues - in-progress issues (title/id defensively resolved)
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
function buildCaptureNote(trigger, issues) {
|
|
80
|
+
const capped = issues.slice(0, CAPTURE_ISSUE_CAP);
|
|
81
|
+
const lines = capped.map(issue => {
|
|
82
|
+
const title = String((issue && (issue.title || issue.id)) || 'untitled').replace(/\s+/g, ' ').trim();
|
|
83
|
+
return `- ${title.length > CAPTURE_TITLE_CAP ? `${title.slice(0, CAPTURE_TITLE_CAP)}…` : title}`;
|
|
84
|
+
});
|
|
85
|
+
const more = issues.length > capped.length ? `\n- …and ${issues.length - capped.length} more` : '';
|
|
86
|
+
const body = lines.length
|
|
87
|
+
? `Session boundary (${trigger}) — in-progress:\n${lines.join('\n')}${more}`
|
|
88
|
+
: `Session boundary (${trigger}) — no in-progress issues.`;
|
|
89
|
+
// Reserve one char for the appended ellipsis so the FINAL note (incl. '…') is ≤ the cap,
|
|
90
|
+
// never CAPTURE_NOTE_CAP + 1.
|
|
91
|
+
return body.length > CAPTURE_NOTE_CAP ? `${body.slice(0, CAPTURE_NOTE_CAP - 1)}…` : body;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Wrap a digest into a harness-native SessionStart payload, or '' when unsupported. */
|
|
95
|
+
function formatSessionStart(harness, text) {
|
|
96
|
+
if (harness === 'claude') {
|
|
97
|
+
return JSON.stringify({
|
|
98
|
+
hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: text },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// No other harness has a verified session-start context surface — emit nothing.
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* `forge hooks session-start --harness <h>` — the CONTEXT hook Forge PUSHES to an agent
|
|
107
|
+
* at session start. Machine-facing plumbing: emits harness-native SessionStart JSON on
|
|
108
|
+
* stdout (Claude: { hookSpecificOutput.additionalContext }). FAIL-OPEN by construction —
|
|
109
|
+
* any failure, an unsupported harness, or an empty digest yields '' (the harness then
|
|
110
|
+
* injects nothing). NEVER throws and NEVER emits malformed JSON.
|
|
111
|
+
*
|
|
112
|
+
* @param {string[]} rest - args after the `session-start` action.
|
|
113
|
+
* @param {string} projectRoot
|
|
114
|
+
* @param {object} [opts] - injectable digest fetchers ({ fetchNotes, fetchIssues }).
|
|
115
|
+
* @returns {Promise<{ success: boolean, output: string }>}
|
|
116
|
+
*/
|
|
117
|
+
async function handleSessionStart(rest, projectRoot, opts = {}) {
|
|
118
|
+
try {
|
|
119
|
+
const harness = parseHarness(rest);
|
|
120
|
+
if (!sessionStartCapability(harness).rendered) return { success: true, output: '' };
|
|
121
|
+
const data = await collectDigestData(projectRoot, opts);
|
|
122
|
+
const digest = buildMemoryDigest(data, opts);
|
|
123
|
+
if (digest.empty) return { success: true, output: '' };
|
|
124
|
+
return { success: true, output: formatSessionStart(harness, digest.text) };
|
|
125
|
+
} catch {
|
|
126
|
+
// Fail-open: a context hook must never break a session.
|
|
127
|
+
return { success: true, output: '' };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Wrap a digest into a harness-native UserPromptSubmit payload, or '' when unsupported. */
|
|
132
|
+
function formatUserPromptSubmit(harness, text) {
|
|
133
|
+
if (harness === 'claude') {
|
|
134
|
+
return JSON.stringify({
|
|
135
|
+
hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: text },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// No other harness has a verified UserPromptSubmit context surface — emit nothing.
|
|
139
|
+
return '';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* `forge hooks inbox-pickup --harness <h>` — the COMPLIANT comment-back CONTEXT hook. On each
|
|
144
|
+
* prompt it emits harness-native UserPromptSubmit JSON carrying a COMPACT count+pointer nudge
|
|
145
|
+
* (Claude: { hookSpecificOutput.additionalContext }). It deliberately does NOT re-emit the full
|
|
146
|
+
* fenced bodies: Claude APPENDS UserPromptSubmit additionalContext to history on every prompt
|
|
147
|
+
* (it does not replace prior injections — a documented context-bloat limitation), so a compact
|
|
148
|
+
* nudge keeps per-prompt accumulation negligible while still flagging "you have pending items,
|
|
149
|
+
* run `forge inbox`". The full fenced digest surfaces once at SessionStart and on demand via
|
|
150
|
+
* `forge inbox`. It reads the user's OWN kernel data via a supported hook — it NEVER injects
|
|
151
|
+
* into a running session's stdin and NEVER drives the agent programmatically (Anthropic Usage
|
|
152
|
+
* Policy, kernel issue 6d10c1a1). FAIL-OPEN: any failure, an unsupported harness, or nothing
|
|
153
|
+
* pending yields '' (the harness injects nothing). NEVER throws, NEVER emits malformed JSON.
|
|
154
|
+
*
|
|
155
|
+
* @param {string[]} rest - args after the `inbox-pickup` action.
|
|
156
|
+
* @param {string} projectRoot
|
|
157
|
+
* @param {object} [opts] - injectable inbox fetchers ({ fetchClaims, fetchComments, ... }).
|
|
158
|
+
* @returns {Promise<{ success: boolean, output: string }>}
|
|
159
|
+
*/
|
|
160
|
+
async function handleInboxPickup(rest, projectRoot, opts = {}) {
|
|
161
|
+
try {
|
|
162
|
+
const harness = parseHarness(rest);
|
|
163
|
+
if (!userPromptSubmitCapability(harness).rendered) return { success: true, output: '' };
|
|
164
|
+
const pending = await collectInbox(projectRoot, opts);
|
|
165
|
+
const nudge = buildInboxNudge(pending);
|
|
166
|
+
if (nudge.empty) return { success: true, output: '' };
|
|
167
|
+
return { success: true, output: formatUserPromptSubmit(harness, nudge.text) };
|
|
168
|
+
} catch {
|
|
169
|
+
// Fail-open: a context hook must never break a prompt.
|
|
170
|
+
return { success: true, output: '' };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* `forge hooks shepherd-events --harness <h>` — the PR-shepherd CONTEXT hook. On each
|
|
176
|
+
* prompt it emits harness-native UserPromptSubmit JSON carrying a COMPACT, capped digest
|
|
177
|
+
* of NEW PR-monitor events (verdict changes, failed checks, new threads, merged/closed)
|
|
178
|
+
* since the last read across all open-PR journals, then advances the per-PR consumer
|
|
179
|
+
* cursor so nothing re-surfaces. This is the CONSUMER side of the constant watcher: the
|
|
180
|
+
* watch loop writes the journal, this pushes the deltas to the working agent. It reads the
|
|
181
|
+
* user's OWN local journal via a supported hook — it NEVER injects into stdin and NEVER
|
|
182
|
+
* drives the agent (Anthropic Usage Policy). FAIL-OPEN: any failure, an unsupported
|
|
183
|
+
* harness, or no new events yields '' (the harness injects nothing). NEVER throws.
|
|
184
|
+
*
|
|
185
|
+
* @param {string[]} rest - args after the `shepherd-events` action.
|
|
186
|
+
* @param {string} projectRoot
|
|
187
|
+
* @param {object} [opts] - injectable digest collector ({ collectDigest }).
|
|
188
|
+
* @returns {{ success: boolean, output: string }}
|
|
189
|
+
*/
|
|
190
|
+
function handleShepherdEvents(rest, projectRoot, opts = {}) {
|
|
191
|
+
try {
|
|
192
|
+
const harness = parseHarness(rest);
|
|
193
|
+
const capability = userPromptSubmitCapability(harness);
|
|
194
|
+
// Honest capability matrix: a non-Claude harness gets an explicit skip
|
|
195
|
+
// reason as surface-only result metadata (for callers/telemetry) but NEVER
|
|
196
|
+
// any injected output — `reason` must not drive the agent.
|
|
197
|
+
if (!capability.rendered) return { success: true, output: '', reason: capability.reason };
|
|
198
|
+
const collect = opts.collectDigest || collectDigest;
|
|
199
|
+
const { text } = collect({ root: projectRoot });
|
|
200
|
+
if (!text) return { success: true, output: '' };
|
|
201
|
+
return { success: true, output: formatUserPromptSubmit(harness, text) };
|
|
202
|
+
} catch {
|
|
203
|
+
// Fail-open: a context hook must never break a prompt.
|
|
204
|
+
return { success: true, output: '' };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* `forge hooks capture --harness <h> --trigger <precompact|stop>` — the CAPTURE-on-exit hook.
|
|
210
|
+
* PreCompact (before context compaction) and Stop (turn end) fire it; it snapshots a bounded
|
|
211
|
+
* session-summary note into the memory store BEFORE learnings are lost. This is the WRITE half
|
|
212
|
+
* of Forge memory (SessionStart only INJECTS). It PERSISTS to the store and emits NO stdout — a
|
|
213
|
+
* Stop hook that printed text would inject into the turn, and it never drives the agent
|
|
214
|
+
* (Anthropic Usage Policy). FAIL-OPEN: any failure, an unsupported harness, or nothing worth
|
|
215
|
+
* capturing yields '' and no write. NEVER throws.
|
|
216
|
+
*
|
|
217
|
+
* Flooding guard: a plain Stop with nothing in progress is skipped (Stop fires every turn), and
|
|
218
|
+
* a byte-identical repeat of the newest auto-capture note is skipped — so only meaningful,
|
|
219
|
+
* changed session state is written. PreCompact records a boundary even when nothing is in
|
|
220
|
+
* progress (unlike Stop), but it still goes through the same dedupe — a byte-identical
|
|
221
|
+
* PreCompact repeat is skipped too.
|
|
222
|
+
*
|
|
223
|
+
* @param {string[]} rest - args after the `capture` action.
|
|
224
|
+
* @param {string} projectRoot
|
|
225
|
+
* @param {object} [opts] - injectable { fetchIssues, fetchNotes, append } for tests.
|
|
226
|
+
* @returns {Promise<{ success: boolean, output: string }>}
|
|
227
|
+
*/
|
|
228
|
+
async function handleCapture(rest, projectRoot, opts = {}) {
|
|
229
|
+
try {
|
|
230
|
+
const harness = parseHarness(rest);
|
|
231
|
+
if (!sessionEndCapability(harness).rendered) return { success: true, output: '' };
|
|
232
|
+
const trigger = parseTrigger(rest);
|
|
233
|
+
|
|
234
|
+
const fetchIssues = opts.fetchIssues || defaultFetchIssues;
|
|
235
|
+
const claimed = await fetchIssues(projectRoot, 'in_progress', opts);
|
|
236
|
+
const issues = Array.isArray(claimed) ? claimed : [];
|
|
237
|
+
|
|
238
|
+
// Stop fires every turn; a plain Stop with nothing in progress is not worth a note.
|
|
239
|
+
// PreCompact is rare and precedes real context loss, so it records a boundary even with
|
|
240
|
+
// nothing in progress — but it is NOT exempt from the byte-identical dedupe below.
|
|
241
|
+
if (trigger !== 'precompact' && issues.length === 0) return { success: true, output: '' };
|
|
242
|
+
|
|
243
|
+
const body = buildCaptureNote(trigger, issues);
|
|
244
|
+
|
|
245
|
+
// Content dedupe: if the newest auto-capture note is byte-identical, this is a repeat of
|
|
246
|
+
// an unchanged session — skip the write so the store never floods with duplicates.
|
|
247
|
+
const fetchNotes = opts.fetchNotes || defaultFetchNotes;
|
|
248
|
+
const recent = await fetchNotes(projectRoot, { ...opts, noteLimit: 10 });
|
|
249
|
+
const lastCapture = (Array.isArray(recent) ? recent : [])
|
|
250
|
+
.find(note => Array.isArray(note && note.tags) && note.tags.includes(CAPTURE_AUTO_TAG));
|
|
251
|
+
if (lastCapture && lastCapture.note === body) return { success: true, output: '' };
|
|
252
|
+
|
|
253
|
+
const append = opts.append || require('../memory/router').append;
|
|
254
|
+
append(projectRoot, body, { tags: CAPTURE_TAGS });
|
|
255
|
+
return { success: true, output: '' };
|
|
256
|
+
} catch {
|
|
257
|
+
// Fail-open: a capture hook must never break a session.
|
|
258
|
+
return { success: true, output: '' };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function parseInstallArgs(rest) {
|
|
263
|
+
const parsed = { global: false, dryRun: false, harness: 'all', unknown: [] };
|
|
264
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
265
|
+
const arg = rest[i];
|
|
266
|
+
if (arg === '--global') parsed.global = true;
|
|
267
|
+
else if (arg === '--dry-run') parsed.dryRun = true;
|
|
268
|
+
else if (arg === '--harness') { parsed.harness = rest[i + 1]; i += 1; }
|
|
269
|
+
else if (arg.startsWith('--harness=')) parsed.harness = arg.slice('--harness='.length);
|
|
270
|
+
else parsed.unknown.push(arg);
|
|
271
|
+
}
|
|
272
|
+
return parsed;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function indent(text, prefix) {
|
|
276
|
+
return String(text).replace(/\s+$/, '').split('\n').map(l => prefix + l).join('\n');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const GLOBAL_CONSENT_ERROR = [
|
|
280
|
+
'forge hooks install writes GLOBAL (home-directory) harness config:',
|
|
281
|
+
' - Codex : $CODEX_HOME/config.toml (default ~/.codex/config.toml)',
|
|
282
|
+
' - Hermes: ~/.hermes/config.yaml',
|
|
283
|
+
'Global config affects EVERY project on this machine, so Forge never writes it',
|
|
284
|
+
'silently — re-run with the explicit --global flag to consent, and add --dry-run',
|
|
285
|
+
'first to preview exactly what would be written.',
|
|
286
|
+
usage(),
|
|
287
|
+
].join('\n');
|
|
288
|
+
|
|
289
|
+
const INSTALL_FOOTER = [
|
|
290
|
+
'',
|
|
291
|
+
'Note: the hook commands invoke `node .forge/hooks/forge-native-hook.js` relative',
|
|
292
|
+
'to the workspace root (Codex and Hermes run hooks from there), so enforcement',
|
|
293
|
+
'applies inside Forge-initialized projects; elsewhere the adapter is absent and',
|
|
294
|
+
'the hook fails open (no deny decision is emitted), leaving tool calls untouched.',
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
/** Validate parsed install args; returns an error string or null when valid. */
|
|
298
|
+
function validateInstallArgs(parsed) {
|
|
299
|
+
if (parsed.unknown.length > 0) return `Unknown argument(s): ${parsed.unknown.join(' ')}\n${usage()}`;
|
|
300
|
+
if (!parsed.global) return GLOBAL_CONSENT_ERROR;
|
|
301
|
+
const harnesses = parsed.harness === 'all' ? GLOBAL_HOOK_HARNESSES : [parsed.harness];
|
|
302
|
+
if (!harnesses.every(h => GLOBAL_HOOK_HARNESSES.includes(h))) {
|
|
303
|
+
return `Unknown --harness '${parsed.harness}'. Allowed: codex, hermes, all.\n${usage()}`;
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Format the output lines for a single install result. */
|
|
309
|
+
function renderInstallResult(res, dryRun) {
|
|
310
|
+
const lines = ['', `${res.harness} -> ${res.file}`];
|
|
311
|
+
if (res.skipped) {
|
|
312
|
+
lines.push(` SKIPPED (left untouched): ${res.reason}`);
|
|
313
|
+
if (res.backup) lines.push(` Backed up existing file to: ${res.backup} (.bak)`);
|
|
314
|
+
else if (dryRun) lines.push(' (dry-run: existing file would be backed up to a .bak and skipped)');
|
|
315
|
+
return lines;
|
|
316
|
+
}
|
|
317
|
+
lines.push(indent(renderGlobalHookBlock(res.harness), ' '));
|
|
318
|
+
if (dryRun) {
|
|
319
|
+
lines.push(res.changed === false
|
|
320
|
+
? ' [dry-run] already up to date — a real run would change nothing'
|
|
321
|
+
: ' [dry-run] would merge the block above into this file');
|
|
322
|
+
} else {
|
|
323
|
+
lines.push(res.changed === false
|
|
324
|
+
? ' Already up to date (no changes written).'
|
|
325
|
+
: ` Merged Forge hooks into ${res.existed ? 'existing' : 'new'} config.`);
|
|
326
|
+
}
|
|
327
|
+
return lines;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** The `install` action — consent-guarded GLOBAL hook install (Codex/Hermes). */
|
|
331
|
+
function handleInstall(args, flags, opts) {
|
|
332
|
+
const parsed = parseInstallArgs(args.slice(1));
|
|
333
|
+
const dryRun = parsed.dryRun || Boolean(flags.dryRun);
|
|
334
|
+
|
|
335
|
+
const validationError = validateInstallArgs(parsed);
|
|
336
|
+
if (validationError) return { success: false, error: validationError };
|
|
337
|
+
|
|
338
|
+
const harnesses = parsed.harness === 'all' ? GLOBAL_HOOK_HARNESSES : [parsed.harness];
|
|
339
|
+
// env/homeDir are injectable through the command opts so tests never touch the
|
|
340
|
+
// real home directory; real dispatch passes neither and gets the defaults.
|
|
341
|
+
const results = installGlobalHooks({ harnesses, dryRun, env: opts.env || process.env, homeDir: opts.homeDir });
|
|
342
|
+
|
|
343
|
+
const out = [
|
|
344
|
+
dryRun ? 'forge hooks install --global (dry-run — nothing will be written)' : 'forge hooks install --global',
|
|
345
|
+
'',
|
|
346
|
+
'This merges the following Forge hook block into each GLOBAL config,',
|
|
347
|
+
'preserving all existing user config (idempotent re-runs):',
|
|
348
|
+
...results.flatMap(res => renderInstallResult(res, dryRun)),
|
|
349
|
+
...INSTALL_FOOTER,
|
|
350
|
+
];
|
|
351
|
+
return { success: true, output: out.join('\n'), results };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function handler(args, flags = {}, projectRoot, opts = {}) {
|
|
355
|
+
const action = args[0];
|
|
356
|
+
if (action === 'session-start') return handleSessionStart(args.slice(1), projectRoot, opts);
|
|
357
|
+
if (action === 'inbox-pickup') return handleInboxPickup(args.slice(1), projectRoot, opts);
|
|
358
|
+
if (action === 'shepherd-events') return handleShepherdEvents(args.slice(1), projectRoot, opts);
|
|
359
|
+
if (action === 'capture') return handleCapture(args.slice(1), projectRoot, opts);
|
|
360
|
+
if (action === 'install') return handleInstall(args, flags, opts);
|
|
361
|
+
return {
|
|
362
|
+
success: false,
|
|
363
|
+
error: `forge hooks supports: install, session-start, inbox-pickup, shepherd-events, capture.\n${usage()}`,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
module.exports = {
|
|
368
|
+
name: 'hooks',
|
|
369
|
+
description: 'Opt-in install of Forge native hooks into GLOBAL harness config (Codex/Hermes)',
|
|
370
|
+
usage: usage(),
|
|
371
|
+
flags: {
|
|
372
|
+
'--global': 'Required consent flag — this command writes home-directory config',
|
|
373
|
+
'--harness': 'codex | hermes | all (default: all)',
|
|
374
|
+
'--dry-run': 'Preview the merge without writing anything',
|
|
375
|
+
},
|
|
376
|
+
handler,
|
|
377
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `forge inbox` — the SUPPORTED, Anthropic-compliant "comment back to the agent" surface.
|
|
5
|
+
*
|
|
6
|
+
* COMPLIANCE BOUNDARY (verified Anthropic Usage Policy + Claude Code hooks docs, kernel
|
|
7
|
+
* issue 6d10c1a1, 2026-07-13): this reads/writes KERNEL DATA only. The dashboard EDITS the
|
|
8
|
+
* kernel (comments via the broker); the user's OWN human-driven session reads them via
|
|
9
|
+
* supported hooks (SessionStart / UserPromptSubmit) on its next natural turn. There is NO
|
|
10
|
+
* stdin/tty injection anywhere here and the agent is NEVER driven programmatically.
|
|
11
|
+
*
|
|
12
|
+
* forge inbox List unacked, targeted instruction comments.
|
|
13
|
+
* forge inbox --json Machine-readable envelope for the dashboard / scripts.
|
|
14
|
+
* forge inbox ack <id> Post an `ack:<id>` reply on the instruction's issue.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
collectInbox,
|
|
19
|
+
ackBody,
|
|
20
|
+
DASHBOARD_INBOX_TITLE,
|
|
21
|
+
DASHBOARD_INBOX_TYPE,
|
|
22
|
+
} = require('../inbox');
|
|
23
|
+
const { fenceUntrusted, neutralize } = require('../untrusted-content');
|
|
24
|
+
|
|
25
|
+
function usage() {
|
|
26
|
+
return 'Usage: forge inbox [--json]\n'
|
|
27
|
+
+ ' forge inbox ack <comment_id>';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** True when the arg list requested JSON output (flag or FORGE_JSON=1). */
|
|
31
|
+
function wantsJson(args, opts) {
|
|
32
|
+
return args.includes('--json') || (opts.env || process.env).FORGE_JSON === '1';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolve the shared issue runner (injectable for tests). */
|
|
36
|
+
function issueRunner(opts) {
|
|
37
|
+
return opts.runIssueOperation || require('../forge-issues').runIssueOperation;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One human-readable inbox line. The comment BODY + author are untrusted external content
|
|
42
|
+
* (MINOR-1), so both are neutralize()'d — a planted fence glyph cannot forge a marker — and
|
|
43
|
+
* the whole block is fenced in handleList for parity with the SessionStart/UserPromptSubmit
|
|
44
|
+
* digests (this list is read by the agent too).
|
|
45
|
+
*/
|
|
46
|
+
function renderItem(item) {
|
|
47
|
+
const from = item.actor ? ` — from ${neutralize(item.actor)}` : '';
|
|
48
|
+
return ` [${item.basis}] ${item.comment_id}${from}\n ${neutralize(item.text)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `forge inbox` (list). */
|
|
52
|
+
async function handleList(args, projectRoot, opts) {
|
|
53
|
+
const pending = await collectInbox(projectRoot, opts);
|
|
54
|
+
if (wantsJson(args, opts)) {
|
|
55
|
+
return { success: true, output: JSON.stringify({ ok: true, count: pending.length, inbox: pending }, null, 2) };
|
|
56
|
+
}
|
|
57
|
+
if (!pending.length) {
|
|
58
|
+
return { success: true, output: 'No pending dashboard instructions.' };
|
|
59
|
+
}
|
|
60
|
+
// The item block is untrusted external content (comment bodies), so it is provenance-fenced
|
|
61
|
+
// for parity with the digests — the agent that reads `forge inbox` gets the same DATA-only
|
|
62
|
+
// signal. Bodies are already neutralize()'d in renderItem, so no glyph can forge the fence.
|
|
63
|
+
const body = pending.map(renderItem).join('\n');
|
|
64
|
+
const lines = [
|
|
65
|
+
`${pending.length} pending dashboard instruction(s) — act, then \`forge inbox ack <id>\`:`,
|
|
66
|
+
'',
|
|
67
|
+
fenceUntrusted(body, { source: 'dashboard-comment' }),
|
|
68
|
+
];
|
|
69
|
+
return { success: true, output: lines.join('\n') };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** `forge inbox ack <comment_id>` — post the ack reply on the instruction's issue. */
|
|
73
|
+
async function handleAck(rest, projectRoot, opts) {
|
|
74
|
+
const commentId = (rest || []).find(arg => typeof arg === 'string' && !arg.startsWith('-'));
|
|
75
|
+
if (!commentId) {
|
|
76
|
+
return { success: false, error: `Missing <comment_id>.\n${usage()}`, exitCode: 6 };
|
|
77
|
+
}
|
|
78
|
+
const pending = await collectInbox(projectRoot, opts);
|
|
79
|
+
const target = pending.find(item => item.comment_id === commentId);
|
|
80
|
+
if (!target) {
|
|
81
|
+
return {
|
|
82
|
+
success: false,
|
|
83
|
+
error: `No pending instruction with id ${commentId} is targeted at this session `
|
|
84
|
+
+ '(already acked, or not addressed here). Run `forge inbox` to see pending items.',
|
|
85
|
+
exitCode: 1,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const runIssueOperation = issueRunner(opts);
|
|
89
|
+
const result = await runIssueOperation('comment', [target.issue_id, ackBody(commentId)], projectRoot);
|
|
90
|
+
if (!result || (result.ok !== true && result.success !== true)) {
|
|
91
|
+
return { success: false, error: `Failed to post ack for ${commentId}.`, exitCode: 1 };
|
|
92
|
+
}
|
|
93
|
+
return { success: true, output: `Acked ${commentId} on ${target.issue_id}.` };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function handler(args, _flags = {}, projectRoot, opts = {}) {
|
|
97
|
+
const action = args[0];
|
|
98
|
+
if (action === '--help' || action === '-h') {
|
|
99
|
+
return { success: true, output: usage() };
|
|
100
|
+
}
|
|
101
|
+
if (action === 'ack') {
|
|
102
|
+
return handleAck(args.slice(1), projectRoot, opts);
|
|
103
|
+
}
|
|
104
|
+
return handleList(args, projectRoot, opts);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
name: 'inbox',
|
|
109
|
+
description: 'List and ack targeted dashboard instruction comments (compliant comment-back)',
|
|
110
|
+
usage: usage(),
|
|
111
|
+
flags: {
|
|
112
|
+
'--json': 'Machine-readable envelope for the dashboard / scripts',
|
|
113
|
+
},
|
|
114
|
+
handler,
|
|
115
|
+
// exported for tests / reuse
|
|
116
|
+
DASHBOARD_INBOX_TITLE,
|
|
117
|
+
DASHBOARD_INBOX_TYPE,
|
|
118
|
+
};
|