forge-workflow 0.0.10 → 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 +150 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -118
- 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 +461 -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 +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/{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 +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 +121 -0
- package/lib/beads-sync-scaffold.js +25 -101
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +741 -77
- 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 +17 -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 +0 -1
- 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 +838 -972
- 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 +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 +307 -100
- 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 +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 +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/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 +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/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 +99 -497
- 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 +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 +199 -0
- package/lib/workflow/enforce-stage.js +296 -47
- 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-coordinator.sh +3 -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
package/lib/runtime-health.js
CHANGED
|
@@ -12,6 +12,8 @@ const path = require('node:path');
|
|
|
12
12
|
const { execFileSync: defaultExecFileSync } = require('node:child_process');
|
|
13
13
|
|
|
14
14
|
const { checkLefthookStatus } = require('./lefthook-check');
|
|
15
|
+
const { resolveIssueBackend } = require('./issue-backend');
|
|
16
|
+
const { getInstallCommand, getAddDevCommand } = require('./package-manager-remediation');
|
|
15
17
|
|
|
16
18
|
const WINDOWS_GIT_BASH_CANDIDATES = [
|
|
17
19
|
String.raw`C:\Program Files\Git\bin\bash.exe`,
|
|
@@ -43,11 +45,149 @@ function normalizeHooksPath(value, platform = process.platform) {
|
|
|
43
45
|
return normalized;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
function
|
|
48
|
+
function isAbsoluteForPlatform(value, platform = process.platform) {
|
|
49
|
+
const raw = toText(value).trim();
|
|
50
|
+
return platform === 'win32' ? path.win32.isAbsolute(raw) : path.posix.isAbsolute(raw);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolvesToExecutableHookCommand(line, matcher) {
|
|
54
|
+
const trimmed = line.trim();
|
|
55
|
+
if (!trimmed || trimmed.startsWith('#')) return false;
|
|
56
|
+
const firstCommand = commandName(commandTokens(trimmed)[0]);
|
|
57
|
+
if (firstCommand === 'echo' || firstCommand === 'printf') {
|
|
58
|
+
const chainIndex = trimmed.indexOf('&&');
|
|
59
|
+
return chainIndex === -1 ? false : matcher(trimmed.slice(chainIndex + 2).trim());
|
|
60
|
+
}
|
|
61
|
+
return matcher(trimmed);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function hasExecutableHookCommand(content, matcher) {
|
|
65
|
+
let heredocEnd = null;
|
|
66
|
+
|
|
67
|
+
for (const line of toText(content).split(/\r?\n/)) {
|
|
68
|
+
const trimmed = line.trim();
|
|
69
|
+
if (heredocEnd) {
|
|
70
|
+
if (trimmed === heredocEnd) heredocEnd = null;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (resolvesToExecutableHookCommand(line, matcher)) return true;
|
|
75
|
+
|
|
76
|
+
const delimiter = heredocDelimiter(line);
|
|
77
|
+
if (delimiter) heredocEnd = delimiter;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function commandTokens(line) {
|
|
84
|
+
return line
|
|
85
|
+
.replace(/[;"'()]/g, ' ')
|
|
86
|
+
.split(/\s+/)
|
|
87
|
+
.filter(Boolean);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function commandName(token) {
|
|
91
|
+
return toText(token)
|
|
92
|
+
.replaceAll('\\', '/')
|
|
93
|
+
.split('/')
|
|
94
|
+
.pop()
|
|
95
|
+
.toLowerCase()
|
|
96
|
+
.replace(/\.(?:exe|cmd)$/, '');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isWhitespace(char) {
|
|
100
|
+
return char === ' ' || char === '\t' || char === '\r' || char === '\n';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isVariableNameStart(char) {
|
|
104
|
+
return (char >= 'A' && char <= 'Z') || (char >= 'a' && char <= 'z') || char === '_';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isVariableNameChar(char) {
|
|
108
|
+
return isVariableNameStart(char) || (char >= '0' && char <= '9');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isAssignmentToken(token) {
|
|
112
|
+
const text = toText(token);
|
|
113
|
+
const equalsIndex = text.indexOf('=');
|
|
114
|
+
if (equalsIndex <= 0 || !isVariableNameStart(text[0])) return false;
|
|
115
|
+
|
|
116
|
+
for (let index = 1; index < equalsIndex; index += 1) {
|
|
117
|
+
if (!isVariableNameChar(text[index])) return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function heredocDelimiter(line) {
|
|
124
|
+
const markerIndex = line.indexOf('<<');
|
|
125
|
+
if (markerIndex === -1 || line[markerIndex + 2] === '<') return null;
|
|
126
|
+
|
|
127
|
+
let cursor = markerIndex + 2;
|
|
128
|
+
if (line[cursor] === '-') cursor += 1;
|
|
129
|
+
while (cursor < line.length && isWhitespace(line[cursor])) cursor += 1;
|
|
130
|
+
|
|
131
|
+
const quote = line[cursor];
|
|
132
|
+
if (quote === '"' || quote === "'") {
|
|
133
|
+
cursor += 1;
|
|
134
|
+
const endQuote = line.indexOf(quote, cursor);
|
|
135
|
+
return endQuote === -1 ? line.slice(cursor).trim() || null : line.slice(cursor, endQuote) || null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const start = cursor;
|
|
139
|
+
while (cursor < line.length && !isWhitespace(line[cursor])) cursor += 1;
|
|
140
|
+
return line.slice(start, cursor).trim() || null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isExecutingTokenPosition(tokens, index) {
|
|
144
|
+
const nonExecutingPreviousTokens = new Set(['-e', '-f', '-r', '-x', '[', 'test']);
|
|
145
|
+
const executingPreviousCommands = new Set(['bash', 'bun', 'env', 'exec', 'if', 'node', 'sh', 'then', '&&']);
|
|
146
|
+
|
|
147
|
+
let previousIndex = index - 1;
|
|
148
|
+
while (previousIndex >= 0 && isAssignmentToken(tokens[previousIndex])) {
|
|
149
|
+
previousIndex -= 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (previousIndex < 0) return true;
|
|
153
|
+
|
|
154
|
+
const previous = toText(tokens[previousIndex]).toLowerCase();
|
|
155
|
+
if (nonExecutingPreviousTokens.has(previous)) return false;
|
|
156
|
+
return executingPreviousCommands.has(commandName(previous));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function invokesLefthook(line) {
|
|
160
|
+
return commandTokens(line).some((token) => commandName(token) === 'lefthook');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function invokesForgeHook(line) {
|
|
164
|
+
return commandTokens(line).some((token, index, tokens) => {
|
|
165
|
+
if (token.includes('=')) return false;
|
|
166
|
+
const normalized = token.replaceAll('\\', '/').toLowerCase();
|
|
167
|
+
if (!normalized.endsWith('.forge/hooks/check-tdd.js') && commandName(token) !== 'check-tdd.js') {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return isExecutingTokenPosition(tokens, index);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function invokesBeadsHook(line, hookName) {
|
|
176
|
+
const tokens = commandTokens(line);
|
|
177
|
+
return tokens.some((token, index) => (
|
|
178
|
+
commandName(token) === 'bd'
|
|
179
|
+
&& toText(tokens[index + 1]).toLowerCase() === 'hooks'
|
|
180
|
+
&& toText(tokens[index + 2]).toLowerCase() === 'run'
|
|
181
|
+
&& toText(tokens[index + 3]).toLowerCase() === hookName
|
|
182
|
+
&& isExecutingTokenPosition(tokens, index)
|
|
183
|
+
));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function createDiagnostic(code, subject, message, repair, severity = 'hard-stop') {
|
|
47
187
|
return {
|
|
48
188
|
code,
|
|
49
189
|
subject,
|
|
50
|
-
severity
|
|
190
|
+
severity,
|
|
51
191
|
message,
|
|
52
192
|
...(repair ? { repair } : {})
|
|
53
193
|
};
|
|
@@ -76,7 +216,7 @@ function isUsableWindowsShellCandidate(candidate, options = {}) {
|
|
|
76
216
|
function checkHookInstallation(projectRoot, options = {}) {
|
|
77
217
|
const exec = options._exec || defaultExecFileSync;
|
|
78
218
|
const platform = options.platform || process.platform;
|
|
79
|
-
const
|
|
219
|
+
const expectedRelativeHooksPaths = ['.lefthook/hooks', '.beads/hooks'];
|
|
80
220
|
const requiredHooks = ['pre-commit', 'pre-push'];
|
|
81
221
|
|
|
82
222
|
function resolveGitRoot() {
|
|
@@ -92,7 +232,10 @@ function checkHookInstallation(projectRoot, options = {}) {
|
|
|
92
232
|
}
|
|
93
233
|
|
|
94
234
|
const gitRoot = resolveGitRoot();
|
|
95
|
-
const
|
|
235
|
+
const expectedHooksPaths = new Set(expectedRelativeHooksPaths.flatMap((hooksPath) => [
|
|
236
|
+
normalizeHooksPath(hooksPath, platform),
|
|
237
|
+
normalizeHooksPath(`${gitRoot}/${normalizeHooksPath(hooksPath, platform)}`, platform)
|
|
238
|
+
]));
|
|
96
239
|
|
|
97
240
|
function resolveGitHooksDir() {
|
|
98
241
|
try {
|
|
@@ -108,17 +251,44 @@ function checkHookInstallation(projectRoot, options = {}) {
|
|
|
108
251
|
}
|
|
109
252
|
}
|
|
110
253
|
|
|
111
|
-
function
|
|
254
|
+
function classifyHookFile(filePath, hookName, visited = new Set()) {
|
|
112
255
|
try {
|
|
256
|
+
const normalizedFilePath = path.resolve(filePath);
|
|
257
|
+
if (visited.has(normalizedFilePath)) {
|
|
258
|
+
return { active: false, provider: 'cycle' };
|
|
259
|
+
}
|
|
260
|
+
visited.add(normalizedFilePath);
|
|
261
|
+
|
|
113
262
|
const stat = fs.statSync(filePath);
|
|
114
263
|
const executable = platform === 'win32'
|
|
115
264
|
|| (typeof options._canExecuteHook === 'function'
|
|
116
265
|
? Boolean(options._canExecuteHook(filePath, stat))
|
|
117
266
|
: (stat.mode & 0o111) !== 0);
|
|
118
|
-
if (!stat.isFile() || stat.size === 0 || !executable) return false;
|
|
119
|
-
|
|
267
|
+
if (!stat.isFile() || stat.size === 0 || !executable) return { active: false, provider: 'missing' };
|
|
268
|
+
|
|
269
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
270
|
+
const runsLefthook = hasExecutableHookCommand(content, invokesLefthook);
|
|
271
|
+
const runsForgeHook = hasExecutableHookCommand(content, invokesForgeHook);
|
|
272
|
+
const runsBeadsHook = hasExecutableHookCommand(content, (line) => invokesBeadsHook(line, hookName));
|
|
273
|
+
|
|
274
|
+
if (runsLefthook) return { active: true, provider: 'lefthook' };
|
|
275
|
+
if (runsForgeHook) return { active: true, provider: 'forge' };
|
|
276
|
+
|
|
277
|
+
if (runsBeadsHook) {
|
|
278
|
+
const chainedHookPath = path.join(gitRoot, '.beads', 'hooks', hookName);
|
|
279
|
+
if (path.resolve(chainedHookPath) !== normalizedFilePath && fs.existsSync(chainedHookPath)) {
|
|
280
|
+
const chained = classifyHookFile(chainedHookPath, hookName, visited);
|
|
281
|
+
if (chained.active) {
|
|
282
|
+
return { active: true, provider: `beads->${chained.provider}` };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { active: false, provider: 'beads-unverified' };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { active: false, provider: 'unknown' };
|
|
120
290
|
} catch {
|
|
121
|
-
return false;
|
|
291
|
+
return { active: false, provider: 'missing' };
|
|
122
292
|
}
|
|
123
293
|
}
|
|
124
294
|
|
|
@@ -130,11 +300,18 @@ function checkHookInstallation(projectRoot, options = {}) {
|
|
|
130
300
|
});
|
|
131
301
|
|
|
132
302
|
hooksPath = normalizeHooksPath(output, platform);
|
|
133
|
-
const active = hooksPath
|
|
303
|
+
const active = expectedHooksPaths.has(hooksPath);
|
|
134
304
|
|
|
135
305
|
if (active) {
|
|
136
|
-
const
|
|
137
|
-
const
|
|
306
|
+
const rawHooksPath = toText(output).trim();
|
|
307
|
+
const hooksDir = isAbsoluteForPlatform(rawHooksPath, platform)
|
|
308
|
+
? rawHooksPath
|
|
309
|
+
: path.resolve(gitRoot, normalizeHooksPath(rawHooksPath, platform));
|
|
310
|
+
const hookChecks = requiredHooks.map((hook) => ({
|
|
311
|
+
hook,
|
|
312
|
+
...classifyHookFile(path.join(hooksDir, hook), hook)
|
|
313
|
+
}));
|
|
314
|
+
const missingHooks = hookChecks.filter(check => !check.active).map(check => check.hook);
|
|
138
315
|
const verifiedActive = missingHooks.length === 0;
|
|
139
316
|
return {
|
|
140
317
|
active: verifiedActive,
|
|
@@ -142,6 +319,7 @@ function checkHookInstallation(projectRoot, options = {}) {
|
|
|
142
319
|
verification: 'core.hooksPath',
|
|
143
320
|
hooksPath: hooksPath || null,
|
|
144
321
|
hooksDir,
|
|
322
|
+
providers: Object.fromEntries(hookChecks.map(check => [check.hook, check.provider])),
|
|
145
323
|
missingHooks,
|
|
146
324
|
message: verifiedActive
|
|
147
325
|
? ''
|
|
@@ -155,7 +333,7 @@ function checkHookInstallation(projectRoot, options = {}) {
|
|
|
155
333
|
state: 'inactive',
|
|
156
334
|
verification: 'core.hooksPath',
|
|
157
335
|
hooksPath,
|
|
158
|
-
message: `Git core.hooksPath is set to "${hooksPath}", not ".lefthook/hooks".`
|
|
336
|
+
message: `Git core.hooksPath is set to "${hooksPath}", not ".lefthook/hooks" or ".beads/hooks".`
|
|
159
337
|
};
|
|
160
338
|
}
|
|
161
339
|
} catch {
|
|
@@ -164,7 +342,11 @@ function checkHookInstallation(projectRoot, options = {}) {
|
|
|
164
342
|
|
|
165
343
|
const hooksDir = resolveGitHooksDir();
|
|
166
344
|
if (hooksDir) {
|
|
167
|
-
const
|
|
345
|
+
const hookChecks = requiredHooks.map((hook) => ({
|
|
346
|
+
hook,
|
|
347
|
+
...classifyHookFile(path.join(hooksDir, hook), hook)
|
|
348
|
+
}));
|
|
349
|
+
const missingHooks = hookChecks.filter(check => !check.active).map(check => check.hook);
|
|
168
350
|
const active = missingHooks.length === 0;
|
|
169
351
|
return {
|
|
170
352
|
active,
|
|
@@ -172,6 +354,7 @@ function checkHookInstallation(projectRoot, options = {}) {
|
|
|
172
354
|
verification: 'git-path-hooks',
|
|
173
355
|
hooksPath: hooksPath || null,
|
|
174
356
|
hooksDir,
|
|
357
|
+
providers: Object.fromEntries(hookChecks.map(check => [check.hook, check.provider])),
|
|
175
358
|
missingHooks,
|
|
176
359
|
message: active
|
|
177
360
|
? ''
|
|
@@ -300,6 +483,22 @@ function normalizeProjectRoot(projectRoot) {
|
|
|
300
483
|
: process.cwd();
|
|
301
484
|
}
|
|
302
485
|
|
|
486
|
+
// Resolve the active issue backend for the health gate. An already-normalized
|
|
487
|
+
// `options.issueBackend` (passed by enforce-stage) wins; otherwise fall back to the
|
|
488
|
+
// shared resolver (env > .forge/config.yaml > default 'kernel'). Never throws and
|
|
489
|
+
// never warns from the health path.
|
|
490
|
+
function resolveHealthIssueBackend(options, projectRoot) {
|
|
491
|
+
if (options.issueBackend === 'kernel' || options.issueBackend === 'beads') {
|
|
492
|
+
return options.issueBackend;
|
|
493
|
+
}
|
|
494
|
+
return resolveIssueBackend({
|
|
495
|
+
deps: options.backendDeps || {},
|
|
496
|
+
env: options.env || process.env,
|
|
497
|
+
projectRoot,
|
|
498
|
+
warn: () => {}
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
303
502
|
function checkRuntimeHealth(projectRoot, options = {}) {
|
|
304
503
|
const platform = options.platform || process.platform;
|
|
305
504
|
const root = normalizeProjectRoot(projectRoot);
|
|
@@ -311,17 +510,23 @@ function checkRuntimeHealth(projectRoot, options = {}) {
|
|
|
311
510
|
const jq = checkCommandAvailability('jq', root, options);
|
|
312
511
|
const shell = normalizeShellRuntime(options.shellRuntime, platform, options);
|
|
313
512
|
|
|
513
|
+
const issueBackend = resolveHealthIssueBackend(options, root);
|
|
514
|
+
|
|
314
515
|
const diagnostics = [];
|
|
516
|
+
const advisories = [];
|
|
315
517
|
|
|
316
518
|
const lefthookMissing = lefthook.state === 'missing-dependency'
|
|
317
519
|
|| lefthook.state === 'missing-binary';
|
|
318
520
|
|
|
319
521
|
if (lefthookMissing) {
|
|
522
|
+
const lefthookRepair = lefthook.state === 'missing-binary'
|
|
523
|
+
? getInstallCommand(root)
|
|
524
|
+
: getAddDevCommand(root, 'lefthook');
|
|
320
525
|
diagnostics.push(createDiagnostic(
|
|
321
526
|
'LEFTHOOK_MISSING',
|
|
322
527
|
'lefthook',
|
|
323
528
|
lefthook.message || 'lefthook is required for hook installation.',
|
|
324
|
-
|
|
529
|
+
lefthookRepair
|
|
325
530
|
));
|
|
326
531
|
}
|
|
327
532
|
|
|
@@ -335,11 +540,25 @@ function checkRuntimeHealth(projectRoot, options = {}) {
|
|
|
335
540
|
}
|
|
336
541
|
|
|
337
542
|
if (!bd.available) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
543
|
+
if (issueBackend === 'beads') {
|
|
544
|
+
// Beads is the active issue backend, so bd is a genuine hard prerequisite.
|
|
545
|
+
diagnostics.push(createDiagnostic(
|
|
546
|
+
'BD_MISSING',
|
|
547
|
+
'bd',
|
|
548
|
+
'bd is required for stage-entry workflow checks when the beads issue backend is selected.'
|
|
549
|
+
));
|
|
550
|
+
} else {
|
|
551
|
+
// Kernel is the default issue backend and needs no bd binary, so a missing bd
|
|
552
|
+
// must NOT hard-stop stage entry (the "no Beads install required" contract).
|
|
553
|
+
// Surface it as a non-blocking advisory instead.
|
|
554
|
+
advisories.push(createDiagnostic(
|
|
555
|
+
'BD_MISSING',
|
|
556
|
+
'bd',
|
|
557
|
+
'bd is not installed. The kernel issue backend does not require it; install bd only if you switch to the beads backend.',
|
|
558
|
+
undefined,
|
|
559
|
+
'advisory'
|
|
560
|
+
));
|
|
561
|
+
}
|
|
343
562
|
}
|
|
344
563
|
|
|
345
564
|
if (!gh.available) {
|
|
@@ -373,6 +592,7 @@ function checkRuntimeHealth(projectRoot, options = {}) {
|
|
|
373
592
|
ready: healthy,
|
|
374
593
|
hardStop: !healthy,
|
|
375
594
|
diagnostics,
|
|
595
|
+
advisories,
|
|
376
596
|
checks: {
|
|
377
597
|
projectRoot: root,
|
|
378
598
|
lefthook,
|
|
@@ -380,7 +600,8 @@ function checkRuntimeHealth(projectRoot, options = {}) {
|
|
|
380
600
|
bd,
|
|
381
601
|
gh,
|
|
382
602
|
jq,
|
|
383
|
-
shell
|
|
603
|
+
shell,
|
|
604
|
+
issueBackend
|
|
384
605
|
}
|
|
385
606
|
};
|
|
386
607
|
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native SAFETY-surface renderer.
|
|
5
|
+
*
|
|
6
|
+
* Renders sane, non-surprising security defaults into the two safety surfaces
|
|
7
|
+
* Forge can write PROJECT-LOCALLY today:
|
|
8
|
+
*
|
|
9
|
+
* - Claude Code : `.claude/settings.json` -> `permissions` (allow/deny/ask)
|
|
10
|
+
* - Cursor : `.cursorignore` (gitignore-style AI read/index boundary)
|
|
11
|
+
*
|
|
12
|
+
* Both use read -> merge -> write (idempotent, preserves user entries). An existing
|
|
13
|
+
* `.claude/settings.json` that cannot be parsed is BACKED UP and left untouched
|
|
14
|
+
* (mirrors the MCP renderer's data-loss guard) rather than clobbering user settings.
|
|
15
|
+
*
|
|
16
|
+
* NOT rendered here (honest deferral): the Codex execution sandbox / approvals policy
|
|
17
|
+
* (`sandbox_mode` / `approval_policy`). Codex honors a project-local `.codex/config.toml`
|
|
18
|
+
* ONLY when the project is marked trusted in the GLOBAL `$CODEX_HOME/config.toml`
|
|
19
|
+
* (`projects.<path>.trust_level`). Forge cannot grant that global trust during
|
|
20
|
+
* project-local setup, so a rendered project-local sandbox policy would not take
|
|
21
|
+
* effect. It is deferred to global-scope wiring and marked `not-delivered` in the
|
|
22
|
+
* capability matrix (kernel epic 90f2f631 / #311).
|
|
23
|
+
*
|
|
24
|
+
* Defaults follow the principle: ALLOW the routine forge/dev workflow, DENY the
|
|
25
|
+
* obviously-dangerous, and let everything else fall through to the harness's normal
|
|
26
|
+
* "ask" behavior (Claude evaluates deny -> ask -> allow; unmatched tools still prompt).
|
|
27
|
+
* We intentionally do NOT set `defaultMode`, so nothing is silently auto-approved.
|
|
28
|
+
*
|
|
29
|
+
* Dependency-free beyond node builtins so it runs under `bun test` and release gates.
|
|
30
|
+
*
|
|
31
|
+
* @module safety-config-renderer
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const fs = require('node:fs');
|
|
35
|
+
const path = require('node:path');
|
|
36
|
+
|
|
37
|
+
const { backupFile } = require('./mcp-config-renderer');
|
|
38
|
+
|
|
39
|
+
/** Thrown when an existing settings file cannot be parsed — signals "do not overwrite". */
|
|
40
|
+
class SafetyConfigParseError extends Error {
|
|
41
|
+
constructor(message, cause) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = 'SafetyConfigParseError';
|
|
44
|
+
this.cause = cause;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Safe Claude Code permission defaults. Rule syntax per Claude Code settings docs
|
|
49
|
+
// (https://docs.claude.com/en/docs/claude-code/settings ,
|
|
50
|
+
// https://docs.claude.com/en/docs/claude-code/permissions):
|
|
51
|
+
// Bash(<prefix>:*) — command-prefix match; Read(<glob>) — gitignore-style path.
|
|
52
|
+
// Evaluated deny -> ask -> allow (deny always wins), so the deny Read(...) rules
|
|
53
|
+
// protect secrets even though bare Read is not blanket-allowed.
|
|
54
|
+
const CLAUDE_PERMISSION_DEFAULTS = Object.freeze({
|
|
55
|
+
// Routine, reversible forge/dev workflow — safe to run without a prompt.
|
|
56
|
+
allow: Object.freeze([
|
|
57
|
+
'Bash(git status:*)',
|
|
58
|
+
'Bash(git log:*)',
|
|
59
|
+
'Bash(git diff:*)',
|
|
60
|
+
'Bash(git branch:*)',
|
|
61
|
+
'Bash(git show:*)',
|
|
62
|
+
'Bash(git add:*)',
|
|
63
|
+
'Bash(git commit:*)',
|
|
64
|
+
'Bash(git stash:*)',
|
|
65
|
+
'Bash(git checkout:*)',
|
|
66
|
+
'Bash(git switch:*)',
|
|
67
|
+
'Bash(git pull:*)',
|
|
68
|
+
'Bash(git fetch:*)',
|
|
69
|
+
'Bash(git push:*)',
|
|
70
|
+
'Bash(ls:*)',
|
|
71
|
+
'Bash(cat:*)',
|
|
72
|
+
'Bash(pwd)',
|
|
73
|
+
'Bash(which:*)',
|
|
74
|
+
'Bash(mkdir:*)',
|
|
75
|
+
'Bash(cp:*)',
|
|
76
|
+
'Bash(mv:*)',
|
|
77
|
+
'Bash(bun run:*)',
|
|
78
|
+
'Bash(bun test:*)',
|
|
79
|
+
'Bash(bun install)',
|
|
80
|
+
'Bash(npm run:*)',
|
|
81
|
+
'Bash(npm test:*)',
|
|
82
|
+
'Bash(forge:*)',
|
|
83
|
+
'Bash(gh pr view:*)',
|
|
84
|
+
'Bash(gh pr list:*)',
|
|
85
|
+
'Bash(gh issue list:*)',
|
|
86
|
+
]),
|
|
87
|
+
// Careful, rewrites-history / rewinds-state — worth a confirmation, not a hard block.
|
|
88
|
+
ask: Object.freeze([
|
|
89
|
+
'Bash(git rebase:*)',
|
|
90
|
+
'Bash(git reset:*)',
|
|
91
|
+
'Bash(git clean:*)',
|
|
92
|
+
]),
|
|
93
|
+
// Obviously-dangerous commands + secret reads. deny wins over allow/ask.
|
|
94
|
+
deny: Object.freeze([
|
|
95
|
+
'Bash(rm -rf:*)',
|
|
96
|
+
'Bash(rm -fr:*)',
|
|
97
|
+
'Bash(git push --force:*)',
|
|
98
|
+
'Bash(git push -f:*)',
|
|
99
|
+
'Bash(git reset --hard:*)',
|
|
100
|
+
// Read/secret boundary — private keys and secret env files are never readable.
|
|
101
|
+
'Read(./.env)',
|
|
102
|
+
'Read(**/.env)',
|
|
103
|
+
'Read(**/.env.local)',
|
|
104
|
+
'Read(**/.env.*.local)',
|
|
105
|
+
'Read(**/*.pem)',
|
|
106
|
+
'Read(**/*.key)',
|
|
107
|
+
'Read(**/id_rsa)',
|
|
108
|
+
'Read(**/id_ed25519)',
|
|
109
|
+
'Read(./secrets/**)',
|
|
110
|
+
'Read(**/credentials.json)',
|
|
111
|
+
]),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Safe `.cursorignore` defaults (gitignore-style, per
|
|
115
|
+
// https://cursor.com/docs/reference/ignore-file). Cursor already ignores .gitignore
|
|
116
|
+
// and common lock/.env files by default; listing them here makes the secret/large-dir
|
|
117
|
+
// boundary explicit and self-documenting. `.cursorignore` blocks Cursor AI features +
|
|
118
|
+
// indexing (it is not a hard security boundary — terminal/MCP tools can still reach files).
|
|
119
|
+
const CURSORIGNORE_DEFAULTS = Object.freeze([
|
|
120
|
+
'.env',
|
|
121
|
+
'.env.local',
|
|
122
|
+
'.env.*.local',
|
|
123
|
+
'*.pem',
|
|
124
|
+
'*.key',
|
|
125
|
+
'id_rsa',
|
|
126
|
+
'id_ed25519',
|
|
127
|
+
'secrets/',
|
|
128
|
+
'node_modules/',
|
|
129
|
+
'dist/',
|
|
130
|
+
'build/',
|
|
131
|
+
'out/',
|
|
132
|
+
'coverage/',
|
|
133
|
+
'.next/',
|
|
134
|
+
'*.log',
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
const CURSORIGNORE_HEADER = [
|
|
138
|
+
'# ---- Forge safe defaults (managed) ----',
|
|
139
|
+
'# Keeps secrets and large/generated dirs out of Cursor AI context + index.',
|
|
140
|
+
'# Edit freely — Forge only appends missing lines and never removes yours.',
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
function unionPreserve(existing, defaults) {
|
|
144
|
+
const base = Array.isArray(existing) ? existing.filter(v => typeof v === 'string') : [];
|
|
145
|
+
const seen = new Set(base);
|
|
146
|
+
for (const rule of defaults) {
|
|
147
|
+
if (!seen.has(rule)) {
|
|
148
|
+
base.push(rule);
|
|
149
|
+
seen.add(rule);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return base;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Merge safe permission defaults into an existing `.claude/settings.json` string
|
|
157
|
+
* (read -> merge -> write). Preserves every other settings key and every existing
|
|
158
|
+
* allow/deny/ask entry (union, no duplicates). Idempotent.
|
|
159
|
+
* @param {string} existingText
|
|
160
|
+
* @returns {string} pretty JSON with a trailing newline
|
|
161
|
+
*/
|
|
162
|
+
function mergeClaudePermissions(existingText) {
|
|
163
|
+
let obj = {};
|
|
164
|
+
if (existingText && existingText.trim()) {
|
|
165
|
+
try {
|
|
166
|
+
obj = JSON.parse(existingText);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
// DATA-LOSS GUARD: never silently discard a populated-but-unparseable settings
|
|
169
|
+
// file (e.g. JSONC with comments). Signal the caller to back up and skip.
|
|
170
|
+
throw new SafetyConfigParseError('existing .claude/settings.json is not valid JSON', err);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) obj = {};
|
|
174
|
+
const existingPerms = (obj.permissions && typeof obj.permissions === 'object' && !Array.isArray(obj.permissions))
|
|
175
|
+
? obj.permissions
|
|
176
|
+
: {};
|
|
177
|
+
existingPerms.allow = unionPreserve(existingPerms.allow, CLAUDE_PERMISSION_DEFAULTS.allow);
|
|
178
|
+
existingPerms.ask = unionPreserve(existingPerms.ask, CLAUDE_PERMISSION_DEFAULTS.ask);
|
|
179
|
+
existingPerms.deny = unionPreserve(existingPerms.deny, CLAUDE_PERMISSION_DEFAULTS.deny);
|
|
180
|
+
obj.permissions = existingPerms;
|
|
181
|
+
return JSON.stringify(obj, null, 2) + '\n';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Merge safe ignore defaults into an existing `.cursorignore` string (read -> merge
|
|
186
|
+
* -> write). Preserves all user lines/comments verbatim and only appends default
|
|
187
|
+
* patterns that are not already present. Idempotent.
|
|
188
|
+
* @param {string} existingText
|
|
189
|
+
* @returns {string}
|
|
190
|
+
*/
|
|
191
|
+
function mergeCursorIgnore(existingText) {
|
|
192
|
+
const text = typeof existingText === 'string' ? existingText : '';
|
|
193
|
+
const existingPatterns = new Set(
|
|
194
|
+
text.split(/\r?\n/).map(line => line.trim()).filter(line => line && !line.startsWith('#')),
|
|
195
|
+
);
|
|
196
|
+
const missing = CURSORIGNORE_DEFAULTS.filter(pattern => !existingPatterns.has(pattern));
|
|
197
|
+
const base = text.replace(/\s+$/, '');
|
|
198
|
+
if (missing.length === 0) {
|
|
199
|
+
return base ? `${base}\n` : '';
|
|
200
|
+
}
|
|
201
|
+
const lines = [];
|
|
202
|
+
if (base) {
|
|
203
|
+
lines.push(base, '');
|
|
204
|
+
}
|
|
205
|
+
// Only add the Forge header block if it isn't already present — otherwise a later
|
|
206
|
+
// re-run (once CURSORIGNORE_DEFAULTS gains a new entry) would duplicate the header.
|
|
207
|
+
if (!text.includes(CURSORIGNORE_HEADER[0])) {
|
|
208
|
+
lines.push(...CURSORIGNORE_HEADER);
|
|
209
|
+
}
|
|
210
|
+
lines.push(...missing);
|
|
211
|
+
return lines.join('\n') + '\n';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Render safe Claude permission defaults into `.claude/settings.json`.
|
|
216
|
+
* Read -> merge -> write. An unparseable existing file is BACKED UP and left
|
|
217
|
+
* untouched (never overwritten) to avoid destroying user settings.
|
|
218
|
+
* @param {object} params
|
|
219
|
+
* @param {string} params.targetRoot - Project root.
|
|
220
|
+
* @returns {{ file: string, existed: boolean, skipped: boolean, backup?: string }}
|
|
221
|
+
*/
|
|
222
|
+
function renderClaudePermissions({ targetRoot }) {
|
|
223
|
+
const filePath = path.join(targetRoot, '.claude', 'settings.json');
|
|
224
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
225
|
+
const existed = fs.existsSync(filePath);
|
|
226
|
+
const existing = existed ? fs.readFileSync(filePath, 'utf-8') : '';
|
|
227
|
+
|
|
228
|
+
let merged;
|
|
229
|
+
try {
|
|
230
|
+
merged = mergeClaudePermissions(existing);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
if (err instanceof SafetyConfigParseError && existed) {
|
|
233
|
+
const backup = backupFile(filePath);
|
|
234
|
+
return { file: filePath, existed, skipped: true, backup };
|
|
235
|
+
}
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fs.writeFileSync(filePath, merged, 'utf-8');
|
|
240
|
+
return { file: filePath, existed, skipped: false };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Render safe `.cursorignore` defaults at the project root.
|
|
245
|
+
* Read -> merge -> write, preserving user lines. Line-based, so there is no
|
|
246
|
+
* parse-failure path (never destructive).
|
|
247
|
+
* @param {object} params
|
|
248
|
+
* @param {string} params.targetRoot - Project root.
|
|
249
|
+
* @returns {{ file: string, existed: boolean, skipped: boolean }}
|
|
250
|
+
*/
|
|
251
|
+
function renderCursorIgnore({ targetRoot }) {
|
|
252
|
+
const filePath = path.join(targetRoot, '.cursorignore');
|
|
253
|
+
const existed = fs.existsSync(filePath);
|
|
254
|
+
const existing = existed ? fs.readFileSync(filePath, 'utf-8') : '';
|
|
255
|
+
const merged = mergeCursorIgnore(existing);
|
|
256
|
+
fs.writeFileSync(filePath, merged, 'utf-8');
|
|
257
|
+
return { file: filePath, existed, skipped: false };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
SafetyConfigParseError,
|
|
262
|
+
CLAUDE_PERMISSION_DEFAULTS,
|
|
263
|
+
CURSORIGNORE_DEFAULTS,
|
|
264
|
+
mergeClaudePermissions,
|
|
265
|
+
mergeCursorIgnore,
|
|
266
|
+
renderClaudePermissions,
|
|
267
|
+
renderCursorIgnore,
|
|
268
|
+
};
|
package/lib/setup-action-log.js
CHANGED
|
@@ -11,15 +11,9 @@
|
|
|
11
11
|
const AGENT_PREFIXES = {
|
|
12
12
|
'.claude/': 'Claude Code',
|
|
13
13
|
'.cursor/': 'Cursor',
|
|
14
|
-
'.windsurf/': 'Windsurf',
|
|
15
|
-
'.cline/': 'Cline',
|
|
16
14
|
'.codex/': 'Codex',
|
|
17
15
|
'~/.codex/': 'Codex',
|
|
18
|
-
'$CODEX_HOME/': 'Codex'
|
|
19
|
-
'.opencode/': 'OpenCode',
|
|
20
|
-
'.kilocode/': 'Kilocode',
|
|
21
|
-
'.roo/': 'Roo Code',
|
|
22
|
-
'.github/prompts/': 'GitHub Copilot'
|
|
16
|
+
'$CODEX_HOME/': 'Codex'
|
|
23
17
|
};
|
|
24
18
|
|
|
25
19
|
/**
|