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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const YAML = require('yaml');
|
|
6
|
+
|
|
7
|
+
const PROTECTED_PATH_CATEGORY_IDS = [
|
|
8
|
+
'forge_core',
|
|
9
|
+
'user_protocol',
|
|
10
|
+
'generated_artifacts',
|
|
11
|
+
'append_only_logs',
|
|
12
|
+
'secrets',
|
|
13
|
+
'beads_state',
|
|
14
|
+
'immutable',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const ALLOWED_MODES = new Set([
|
|
18
|
+
'checksum-verified',
|
|
19
|
+
'cli-only',
|
|
20
|
+
'ci-blocked',
|
|
21
|
+
'runtime-only',
|
|
22
|
+
'secret-scan-blocked',
|
|
23
|
+
'bd-cli-only',
|
|
24
|
+
'tool-owned',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const EXPECTED_CATEGORY_MODES = {
|
|
28
|
+
forge_core: 'checksum-verified',
|
|
29
|
+
user_protocol: 'cli-only',
|
|
30
|
+
generated_artifacts: 'ci-blocked',
|
|
31
|
+
append_only_logs: 'runtime-only',
|
|
32
|
+
secrets: 'secret-scan-blocked',
|
|
33
|
+
beads_state: 'bd-cli-only',
|
|
34
|
+
immutable: 'tool-owned',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const DEFAULT_PROTECTED_PATH_MANIFEST_PATH = path.resolve(__dirname, '..', '.forge', 'protected-paths.yaml');
|
|
38
|
+
|
|
39
|
+
const PROTECTED_PATH_HARNESS_ENFORCEMENT = {
|
|
40
|
+
claude: {
|
|
41
|
+
status: 'native',
|
|
42
|
+
surface: 'Claude PreToolUse hook for write/edit tools',
|
|
43
|
+
configPath: '.claude/settings.json',
|
|
44
|
+
delivered: 'Rendered by lib/hook-renderer.js into .claude/settings.json; the installed .forge/hooks/forge-native-hook.js adapter receives tool_input.file_path and denies protected writes with a repair hint.',
|
|
45
|
+
evidenceRequired: ['hook receives target path', 'blocked write returns repair hint'],
|
|
46
|
+
},
|
|
47
|
+
cursor: {
|
|
48
|
+
status: 'fallback',
|
|
49
|
+
surface: 'Forge CLI/pre-commit check or file-watcher fallback',
|
|
50
|
+
configPath: 'lefthook.yml',
|
|
51
|
+
evidenceRequired: ['staged file check', 'watcher proof before native hook claim'],
|
|
52
|
+
knownIssue: 'No verified Cursor hook surface; keep Cursor protected-path enforcement on Forge CLI/pre-commit fallback until proven.',
|
|
53
|
+
},
|
|
54
|
+
codex: {
|
|
55
|
+
status: 'native-hook-contract',
|
|
56
|
+
surface: 'Codex lifecycle hook for write/edit tools',
|
|
57
|
+
configPath: '.codex/config.toml',
|
|
58
|
+
evidenceRequired: ['hook receives target path', 'blocked write returns repair hint'],
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function clone(value) {
|
|
63
|
+
return structuredClone(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getDefaultProtectedPathManifest() {
|
|
67
|
+
return loadProtectedPathManifest(DEFAULT_PROTECTED_PATH_MANIFEST_PATH);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getProtectedPathHarnessEnforcement() {
|
|
71
|
+
return clone(PROTECTED_PATH_HARNESS_ENFORCEMENT);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validateProtectedPathManifest(manifest) {
|
|
75
|
+
const errors = [];
|
|
76
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
77
|
+
return { ok: false, errors: ['Manifest must be an object.'] };
|
|
78
|
+
}
|
|
79
|
+
if (!isSupportedManifestKind(manifest)) {
|
|
80
|
+
errors.push('kind must be ProtectedPathManifest or forge.protectedPaths.');
|
|
81
|
+
}
|
|
82
|
+
if (!isSupportedManifestVersion(manifest)) {
|
|
83
|
+
errors.push('schemaVersion/version must be 1.0.0 or 1.');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (isLegacyProtectedPathsManifest(manifest)) {
|
|
87
|
+
if (!Array.isArray(manifest.paths) || manifest.paths.length === 0) {
|
|
88
|
+
errors.push('Legacy forge.protectedPaths manifest must declare at least one path.');
|
|
89
|
+
}
|
|
90
|
+
return { ok: errors.length === 0, errors };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!Array.isArray(manifest.categories)) errors.push('categories must be an array.');
|
|
94
|
+
|
|
95
|
+
const categories = Array.isArray(manifest.categories) ? manifest.categories : [];
|
|
96
|
+
const ids = categories.map(category => category?.id).filter(Boolean);
|
|
97
|
+
for (const requiredId of PROTECTED_PATH_CATEGORY_IDS) {
|
|
98
|
+
if (!ids.includes(requiredId)) errors.push(`Missing required protected path category: ${requiredId}.`);
|
|
99
|
+
}
|
|
100
|
+
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
|
|
101
|
+
if (duplicates.length > 0) errors.push(`Duplicate protected path categories: ${[...new Set(duplicates)].join(', ')}.`);
|
|
102
|
+
|
|
103
|
+
for (const category of categories) {
|
|
104
|
+
validateCategory(category, errors);
|
|
105
|
+
}
|
|
106
|
+
validateLegacySurfaceCoverage(manifest, categories, errors);
|
|
107
|
+
|
|
108
|
+
return { ok: errors.length === 0, errors };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function validateCategory(category, errors) {
|
|
112
|
+
if (!category || typeof category !== 'object') {
|
|
113
|
+
errors.push('Each category must be an object.');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!category.id || typeof category.id !== 'string') errors.push('Category id must be a string.');
|
|
117
|
+
if (!ALLOWED_MODES.has(category.mode)) {
|
|
118
|
+
const modeLabel = category.mode ?? '<missing>';
|
|
119
|
+
errors.push(`Invalid mode for ${category.id || 'unknown'}: ${modeLabel}.`);
|
|
120
|
+
}
|
|
121
|
+
const expectedMode = EXPECTED_CATEGORY_MODES[category.id];
|
|
122
|
+
if (expectedMode && category.mode !== expectedMode) {
|
|
123
|
+
errors.push(`Category ${category.id} must use mode ${expectedMode}.`);
|
|
124
|
+
}
|
|
125
|
+
if (!Array.isArray(category.paths) || category.paths.length === 0) {
|
|
126
|
+
errors.push(`Category ${category.id || 'unknown'} must declare at least one path.`);
|
|
127
|
+
}
|
|
128
|
+
if (!category.ownerSurface || typeof category.ownerSurface !== 'string') {
|
|
129
|
+
errors.push(`Category ${category.id || 'unknown'} must declare ownerSurface.`);
|
|
130
|
+
}
|
|
131
|
+
if (!category.repairHint || typeof category.repairHint !== 'string') {
|
|
132
|
+
errors.push(`Category ${category.id || 'unknown'} must declare repairHint.`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isSupportedManifestKind(manifest) {
|
|
137
|
+
return manifest.kind === 'ProtectedPathManifest' || manifest.kind === 'forge.protectedPaths';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isSupportedManifestVersion(manifest) {
|
|
141
|
+
if (manifest.kind === 'ProtectedPathManifest') {
|
|
142
|
+
return manifest.schemaVersion === '1.0.0';
|
|
143
|
+
}
|
|
144
|
+
if (manifest.kind === 'forge.protectedPaths') {
|
|
145
|
+
return manifest.version === '1' || manifest.version === 1;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isLegacyProtectedPathsManifest(manifest) {
|
|
151
|
+
return manifest.kind === 'forge.protectedPaths';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function loadProtectedPathManifest(filePath) {
|
|
155
|
+
try {
|
|
156
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
157
|
+
return YAML.parse(content);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
160
|
+
const wrapped = new Error(`Failed to load protected path manifest at ${filePath}: ${message}`, { cause: error });
|
|
161
|
+
if (error && typeof error === 'object' && 'code' in error) {
|
|
162
|
+
wrapped.code = error.code;
|
|
163
|
+
}
|
|
164
|
+
throw wrapped;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildProtectedPathManifestEvidence(manifest = getDefaultProtectedPathManifest()) {
|
|
169
|
+
let categoryIds = [];
|
|
170
|
+
if (Array.isArray(manifest.categories)) {
|
|
171
|
+
categoryIds = manifest.categories.map(category => category?.id).filter(Boolean);
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
schemaVersion: manifest.schemaVersion,
|
|
175
|
+
kind: 'forge.protectedPathManifest',
|
|
176
|
+
manifestPath: manifest.manifestPath || '.forge/protected-paths.yaml',
|
|
177
|
+
categoryIds,
|
|
178
|
+
categories: manifest.categories,
|
|
179
|
+
harnessEnforcement: getProtectedPathHarnessEnforcement(),
|
|
180
|
+
validation: validateProtectedPathManifest(manifest),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function validateLegacySurfaceCoverage(manifest, categories, errors) {
|
|
185
|
+
if (!manifest.surfaces) return;
|
|
186
|
+
if (manifest.surfacesDeprecated !== true) {
|
|
187
|
+
errors.push('surfaces must be marked surfacesDeprecated: true when categories are authoritative.');
|
|
188
|
+
}
|
|
189
|
+
if (!manifest.surfacesMigrationNote || typeof manifest.surfacesMigrationNote !== 'string') {
|
|
190
|
+
errors.push('surfacesMigrationNote must describe the legacy surfaces migration.');
|
|
191
|
+
}
|
|
192
|
+
const categoryPaths = categories.flatMap(category => (Array.isArray(category?.paths) ? category.paths : []));
|
|
193
|
+
for (const [surfaceId, surface] of Object.entries(manifest.surfaces)) {
|
|
194
|
+
if (!Array.isArray(surface?.examples) || surface.examples.length === 0) {
|
|
195
|
+
errors.push(`Legacy surface ${surfaceId} must declare examples for category coverage.`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
for (const example of surface.examples) {
|
|
199
|
+
if (!categoryPaths.some(pattern => pathPatternCoversExample(pattern, example))) {
|
|
200
|
+
errors.push(`Legacy surface ${surfaceId} example is not covered by category paths: ${example}.`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function pathPatternCoversExample(pattern, example) {
|
|
207
|
+
const normalizedPattern = normalizeManifestPath(pattern);
|
|
208
|
+
const normalizedExample = normalizeManifestPath(example);
|
|
209
|
+
if (normalizedPattern === normalizedExample) return true;
|
|
210
|
+
if (normalizedPattern.endsWith('/**')) {
|
|
211
|
+
const prefix = normalizedPattern.slice(0, -3);
|
|
212
|
+
return normalizedExample === prefix || normalizedExample.startsWith(`${prefix}/`);
|
|
213
|
+
}
|
|
214
|
+
return wildcardPathMatches(normalizedPattern, normalizedExample);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function normalizeManifestPath(value) {
|
|
218
|
+
let normalized = String(value || '').replaceAll('\\', '/');
|
|
219
|
+
if (normalized.startsWith('./')) normalized = normalized.slice(2);
|
|
220
|
+
while (normalized.endsWith('/')) normalized = normalized.slice(0, -1);
|
|
221
|
+
return normalized;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function wildcardPathMatches(pattern, candidate) {
|
|
225
|
+
return pathSegmentsMatch(pattern.split('/'), candidate.split('/'), 0, 0, new Map());
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function pathSegmentsMatch(patternParts, candidateParts, patternIndex, candidateIndex, memo) {
|
|
229
|
+
const memoKey = `${patternIndex}:${candidateIndex}`;
|
|
230
|
+
if (memo.has(memoKey)) return memo.get(memoKey);
|
|
231
|
+
if (patternIndex === patternParts.length) return candidateIndex === candidateParts.length;
|
|
232
|
+
|
|
233
|
+
const patternPart = patternParts[patternIndex];
|
|
234
|
+
let matches;
|
|
235
|
+
if (patternPart === '**') {
|
|
236
|
+
matches =
|
|
237
|
+
pathSegmentsMatch(patternParts, candidateParts, patternIndex + 1, candidateIndex, memo) ||
|
|
238
|
+
(candidateIndex < candidateParts.length && pathSegmentsMatch(patternParts, candidateParts, patternIndex, candidateIndex + 1, memo));
|
|
239
|
+
} else {
|
|
240
|
+
matches =
|
|
241
|
+
candidateIndex < candidateParts.length &&
|
|
242
|
+
segmentPatternMatches(patternPart, candidateParts[candidateIndex]) &&
|
|
243
|
+
pathSegmentsMatch(patternParts, candidateParts, patternIndex + 1, candidateIndex + 1, memo);
|
|
244
|
+
}
|
|
245
|
+
memo.set(memoKey, matches);
|
|
246
|
+
return matches;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function segmentPatternMatches(pattern, candidate) {
|
|
250
|
+
let patternIndex = 0;
|
|
251
|
+
let candidateIndex = 0;
|
|
252
|
+
let starIndex = -1;
|
|
253
|
+
let candidateCheckpoint = 0;
|
|
254
|
+
while (candidateIndex < candidate.length) {
|
|
255
|
+
if (pattern[patternIndex] === candidate[candidateIndex]) {
|
|
256
|
+
patternIndex += 1;
|
|
257
|
+
candidateIndex += 1;
|
|
258
|
+
} else if (pattern[patternIndex] === '*') {
|
|
259
|
+
starIndex = patternIndex;
|
|
260
|
+
candidateCheckpoint = candidateIndex;
|
|
261
|
+
patternIndex += 1;
|
|
262
|
+
} else if (starIndex === -1) {
|
|
263
|
+
return false;
|
|
264
|
+
} else {
|
|
265
|
+
patternIndex = starIndex + 1;
|
|
266
|
+
candidateCheckpoint += 1;
|
|
267
|
+
candidateIndex = candidateCheckpoint;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
while (pattern[patternIndex] === '*') patternIndex += 1;
|
|
271
|
+
return patternIndex === pattern.length;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
module.exports = {
|
|
275
|
+
PROTECTED_PATH_CATEGORY_IDS,
|
|
276
|
+
buildProtectedPathManifestEvidence,
|
|
277
|
+
getDefaultProtectedPathManifest,
|
|
278
|
+
getProtectedPathHarnessEnforcement,
|
|
279
|
+
loadProtectedPathManifest,
|
|
280
|
+
validateProtectedPathManifest,
|
|
281
|
+
};
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { execFileSync } = require('node:child_process');
|
|
4
|
+
const { redact } = require('./audit-evidence');
|
|
5
|
+
|
|
6
|
+
function normalizeRepoPath(filePath) {
|
|
7
|
+
return String(filePath || '')
|
|
8
|
+
.replace(/\\/g, '/')
|
|
9
|
+
.replace(/^\.\//, '')
|
|
10
|
+
.replace(/\/+/g, '/');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function startsWithAny(filePath, prefixes) {
|
|
14
|
+
return prefixes.some(prefix => filePath === prefix || filePath.startsWith(`${prefix}/`));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PROTECTED_SURFACES = [
|
|
18
|
+
{
|
|
19
|
+
id: 'immutable',
|
|
20
|
+
label: 'Immutable runtime paths',
|
|
21
|
+
matches: filePath => startsWithAny(filePath, ['.git', '.hg', '.svn']),
|
|
22
|
+
repairHint: 'Do not edit immutable VCS/runtime internals directly; use git or the owning tool command.',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'secrets',
|
|
26
|
+
label: 'Secrets',
|
|
27
|
+
matches: filePath =>
|
|
28
|
+
filePath === '.env' ||
|
|
29
|
+
filePath.startsWith('.env.') ||
|
|
30
|
+
filePath.endsWith('/.env') ||
|
|
31
|
+
filePath.includes('/.env.') ||
|
|
32
|
+
/(^|\/)(secrets?|credentials?)(\.|\/|$)/i.test(filePath),
|
|
33
|
+
repairHint: 'Use the project secret manager or local environment setup command; never hand-edit or commit secrets.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'append_only_logs',
|
|
37
|
+
label: 'Append-only logs',
|
|
38
|
+
matches: filePath =>
|
|
39
|
+
[
|
|
40
|
+
'.forge/log.jsonl',
|
|
41
|
+
'.forge/audit.log',
|
|
42
|
+
'.forge/agent-log.ndjson',
|
|
43
|
+
'.beads/interactions.jsonl',
|
|
44
|
+
].includes(filePath),
|
|
45
|
+
repairHint: 'append-only logs must be written by the Forge or Beads audit writer; do not rewrite existing log content.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'beads_state',
|
|
49
|
+
label: 'Beads state',
|
|
50
|
+
matches: filePath => startsWithAny(filePath, ['.beads']),
|
|
51
|
+
repairHint: 'Use bd or Forge issue commands such as bd update, bd close, forge ready, or forge close.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'forge_config',
|
|
55
|
+
label: 'Forge config',
|
|
56
|
+
matches: filePath =>
|
|
57
|
+
filePath === '.forge/config.yaml' ||
|
|
58
|
+
filePath === '.forge/config.yml' ||
|
|
59
|
+
filePath === '.forge/config.json' ||
|
|
60
|
+
filePath === '.forge/protected-paths.yaml',
|
|
61
|
+
repairHint: 'Use Forge setup/config commands or the protected Forge API writer for .forge configuration.',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'extension_manifests',
|
|
65
|
+
label: 'Extension manifests',
|
|
66
|
+
matches: filePath =>
|
|
67
|
+
filePath === '.github/PLUGIN_TEMPLATE.json' ||
|
|
68
|
+
/^\.forge\/extensions\/[^/]+\/manifest\.json$/i.test(filePath) ||
|
|
69
|
+
/^plugins\/[^/]+\/(?:plugin|extension|manifest)\.json$/i.test(filePath),
|
|
70
|
+
repairHint: 'Use the Forge extension/plugin manager so manifests and trust metadata stay consistent.',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'lockfiles',
|
|
74
|
+
label: 'Lockfiles',
|
|
75
|
+
matches: filePath =>
|
|
76
|
+
/(^|\/)(bun\.lock|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|requirements\.lock|poetry\.lock|Cargo\.lock|go\.sum)$/.test(filePath) ||
|
|
77
|
+
filePath === '.forge/extensions.lock',
|
|
78
|
+
repairHint: 'Regenerate lockfiles through the package manager or Forge extension installer instead of editing them by hand.',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'workflows',
|
|
82
|
+
label: 'Workflow definitions',
|
|
83
|
+
matches: filePath =>
|
|
84
|
+
startsWithAny(filePath, ['.github/workflows', '.forge/hooks']) ||
|
|
85
|
+
filePath === 'lefthook.yml',
|
|
86
|
+
repairHint: 'Use Forge workflow/setup commands so workflow files and hooks remain in sync.',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: 'generated_harness',
|
|
90
|
+
label: 'Generated harness files',
|
|
91
|
+
matches: filePath =>
|
|
92
|
+
startsWithAny(filePath, [
|
|
93
|
+
'.agents/skills',
|
|
94
|
+
'.claude/skills',
|
|
95
|
+
'.codex/skills',
|
|
96
|
+
'.cursor/rules',
|
|
97
|
+
]) ||
|
|
98
|
+
['AGENTS.md', 'CLAUDE.md', '.mcp.json'].includes(filePath),
|
|
99
|
+
repairHint: 'Regenerate harness files with forge setup or the owning Forge API surface instead of direct edits.',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'memory_projection',
|
|
103
|
+
label: 'Memory projection files',
|
|
104
|
+
matches: filePath =>
|
|
105
|
+
startsWithAny(filePath, ['docs/sessions', 'docs/memory', '.forge/memory']) ||
|
|
106
|
+
filePath.endsWith('/memory.md') ||
|
|
107
|
+
filePath.endsWith('/MEMORY.md'),
|
|
108
|
+
repairHint: 'Use the Forge memory projection command or append-only session writer; do not edit projections directly.',
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
function classifyProtectedPath(filePath) {
|
|
113
|
+
const normalized = normalizeRepoPath(filePath);
|
|
114
|
+
if (!normalized) return null;
|
|
115
|
+
const surface = PROTECTED_SURFACES.find(candidate => candidate.matches(normalized));
|
|
116
|
+
if (!surface) return null;
|
|
117
|
+
return {
|
|
118
|
+
path: normalized,
|
|
119
|
+
surface: surface.id,
|
|
120
|
+
label: surface.label,
|
|
121
|
+
repairHint: surface.repairHint,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveRepoRelativePath(projectRoot, filePath) {
|
|
126
|
+
const root = path.resolve(projectRoot);
|
|
127
|
+
const target = path.resolve(root, filePath);
|
|
128
|
+
if (!pathStaysInsideRoot(root, target)) {
|
|
129
|
+
return {
|
|
130
|
+
insideRoot: false,
|
|
131
|
+
root,
|
|
132
|
+
target,
|
|
133
|
+
relativePath: normalizeRepoPath(filePath),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
insideRoot: true,
|
|
138
|
+
root,
|
|
139
|
+
target,
|
|
140
|
+
relativePath: normalizeRepoPath(path.relative(root, target)),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function pathStaysInsideRoot(root, candidate) {
|
|
145
|
+
return candidate === root || candidate.startsWith(`${root}${path.sep}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function lstatIfPresent(candidate) {
|
|
149
|
+
try {
|
|
150
|
+
return fs.lstatSync(candidate);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (error.code === 'ENOENT') return null;
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function nearestExistingPath(candidate) {
|
|
158
|
+
let current = candidate;
|
|
159
|
+
while (!lstatIfPresent(current)) {
|
|
160
|
+
const parent = path.dirname(current);
|
|
161
|
+
if (parent === current) return current;
|
|
162
|
+
current = parent;
|
|
163
|
+
}
|
|
164
|
+
return current;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function assertNoAncestorSymlinkEscape(root, target) {
|
|
168
|
+
const realRoot = fs.realpathSync(root);
|
|
169
|
+
const existing = nearestExistingPath(path.dirname(target));
|
|
170
|
+
const existingStat = lstatIfPresent(existing);
|
|
171
|
+
if (existing !== root && existingStat?.isSymbolicLink()) {
|
|
172
|
+
return {
|
|
173
|
+
allowed: false,
|
|
174
|
+
decision: 'blocked',
|
|
175
|
+
reason: `Write ancestor is a symlink: ${existing}`,
|
|
176
|
+
repairHint: 'Remove the symlink escape or write through a real directory inside the project root.',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const realExisting = fs.realpathSync(existing);
|
|
181
|
+
if (!pathStaysInsideRoot(realRoot, realExisting)) {
|
|
182
|
+
return {
|
|
183
|
+
allowed: false,
|
|
184
|
+
decision: 'blocked',
|
|
185
|
+
reason: `Write ancestor resolves outside project root: ${existing}`,
|
|
186
|
+
repairHint: 'Remove the symlink escape or write through a real directory inside the project root.',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function assertNoSymlinkEscape(root, target) {
|
|
193
|
+
const parent = path.dirname(target);
|
|
194
|
+
const realRoot = fs.realpathSync(root);
|
|
195
|
+
const existingParent = fs.existsSync(parent) ? fs.realpathSync(parent) : parent;
|
|
196
|
+
if (!pathStaysInsideRoot(realRoot, existingParent)) {
|
|
197
|
+
return {
|
|
198
|
+
allowed: false,
|
|
199
|
+
decision: 'blocked',
|
|
200
|
+
reason: `Write parent resolves outside project root: ${parent}`,
|
|
201
|
+
repairHint: 'Remove the symlink escape or write through a real directory inside the project root.',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
if (fs.lstatSync(target).isSymbolicLink()) {
|
|
207
|
+
return {
|
|
208
|
+
allowed: false,
|
|
209
|
+
decision: 'blocked',
|
|
210
|
+
reason: `Write target is a symlink: ${target}`,
|
|
211
|
+
repairHint: 'Replace the symlink with a real file through the owning Forge API before writing.',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error.code !== 'ENOENT') {
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function assertProtectedWriteAllowed(filePath, options = {}) {
|
|
224
|
+
const classification = classifyProtectedPath(filePath);
|
|
225
|
+
const actor = options.actor || process.env.FORGE_ACTOR || process.env.USER || process.env.USERNAME || 'unknown';
|
|
226
|
+
const operation = options.operation || 'write';
|
|
227
|
+
|
|
228
|
+
if (!classification) {
|
|
229
|
+
return {
|
|
230
|
+
allowed: true,
|
|
231
|
+
decision: 'allowed',
|
|
232
|
+
actor,
|
|
233
|
+
path: normalizeRepoPath(filePath),
|
|
234
|
+
operation,
|
|
235
|
+
requiredSurface: null,
|
|
236
|
+
reason: 'Path is not protected.',
|
|
237
|
+
repairHint: null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const requiredSurface = classification.surface;
|
|
242
|
+
const declaredSurface = options.surface || options.requiredSurface;
|
|
243
|
+
const allowed = Boolean(options.viaForgeApi && declaredSurface === requiredSurface);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
allowed,
|
|
247
|
+
decision: allowed ? 'allowed' : 'blocked',
|
|
248
|
+
actor,
|
|
249
|
+
path: classification.path,
|
|
250
|
+
operation,
|
|
251
|
+
requiredSurface,
|
|
252
|
+
surfaceLabel: classification.label,
|
|
253
|
+
declaredSurface: declaredSurface || null,
|
|
254
|
+
reason: allowed
|
|
255
|
+
? `Forge API write declared required surface: ${requiredSurface}.`
|
|
256
|
+
: `Direct edits to protected ${requiredSurface} paths are not allowed.`,
|
|
257
|
+
repairHint: classification.repairHint,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function writeProtectedFile(projectRoot, filePath, content, options = {}) {
|
|
262
|
+
const resolved = resolveRepoRelativePath(projectRoot, filePath);
|
|
263
|
+
if (!resolved.insideRoot) {
|
|
264
|
+
return {
|
|
265
|
+
allowed: false,
|
|
266
|
+
decision: 'blocked',
|
|
267
|
+
actor: options.actor || process.env.FORGE_ACTOR || process.env.USER || process.env.USERNAME || 'unknown',
|
|
268
|
+
path: resolved.relativePath,
|
|
269
|
+
operation: options.operation || 'write',
|
|
270
|
+
requiredSurface: null,
|
|
271
|
+
reason: `Write path escapes project root: ${filePath}`,
|
|
272
|
+
repairHint: 'Write only repo-relative paths inside the project root.',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const decision = assertProtectedWriteAllowed(resolved.relativePath, {
|
|
277
|
+
...options,
|
|
278
|
+
operation: options.operation || 'write',
|
|
279
|
+
});
|
|
280
|
+
if (!decision.allowed) return decision;
|
|
281
|
+
|
|
282
|
+
const ancestorEscape = assertNoAncestorSymlinkEscape(resolved.root, resolved.target);
|
|
283
|
+
if (ancestorEscape) {
|
|
284
|
+
return {
|
|
285
|
+
...decision,
|
|
286
|
+
...ancestorEscape,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
fs.mkdirSync(path.dirname(resolved.target), { recursive: true });
|
|
291
|
+
const symlinkEscape = assertNoSymlinkEscape(resolved.root, resolved.target);
|
|
292
|
+
if (symlinkEscape) {
|
|
293
|
+
return {
|
|
294
|
+
...decision,
|
|
295
|
+
...symlinkEscape,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
fs.writeFileSync(resolved.target, content, options.encoding || 'utf8');
|
|
300
|
+
return { ...decision, fullPath: resolved.target };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function buildProtectedStateAuditEvent(decision) {
|
|
304
|
+
if (!decision || typeof decision !== 'object') {
|
|
305
|
+
throw new TypeError('Protected state decision is required');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const event = {
|
|
309
|
+
kind: 'protected_state_write',
|
|
310
|
+
actor: decision.actor || 'unknown',
|
|
311
|
+
path: decision.path,
|
|
312
|
+
decision: decision.decision,
|
|
313
|
+
requiredSurface: decision.requiredSurface,
|
|
314
|
+
declaredSurface: decision.declaredSurface || null,
|
|
315
|
+
operation: decision.operation || 'write',
|
|
316
|
+
reason: decision.reason,
|
|
317
|
+
repairHint: decision.repairHint,
|
|
318
|
+
metadata: {
|
|
319
|
+
actor: decision.actor || 'unknown',
|
|
320
|
+
path: decision.path,
|
|
321
|
+
operation: decision.operation || 'write',
|
|
322
|
+
requiredSurface: decision.requiredSurface,
|
|
323
|
+
declaredSurface: decision.declaredSurface || null,
|
|
324
|
+
decision: decision.decision,
|
|
325
|
+
reason: decision.reason,
|
|
326
|
+
repairHint: decision.repairHint,
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
return redact(event);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function recordProtectedStateAuditEvent(decision, options = {}) {
|
|
334
|
+
const event = buildProtectedStateAuditEvent(decision);
|
|
335
|
+
const runCommand = options.runCommand || execFileSync;
|
|
336
|
+
try {
|
|
337
|
+
const args = [
|
|
338
|
+
'audit',
|
|
339
|
+
'record',
|
|
340
|
+
'--json',
|
|
341
|
+
'--kind',
|
|
342
|
+
'protected_state_write',
|
|
343
|
+
'--model',
|
|
344
|
+
'forge-protected-state',
|
|
345
|
+
'--prompt',
|
|
346
|
+
JSON.stringify({
|
|
347
|
+
actor: event.actor,
|
|
348
|
+
path: event.path,
|
|
349
|
+
operation: event.operation,
|
|
350
|
+
}),
|
|
351
|
+
'--response',
|
|
352
|
+
JSON.stringify({
|
|
353
|
+
decision: event.decision,
|
|
354
|
+
requiredSurface: event.requiredSurface,
|
|
355
|
+
repairHint: event.repairHint,
|
|
356
|
+
}),
|
|
357
|
+
];
|
|
358
|
+
|
|
359
|
+
if (event.metadata) {
|
|
360
|
+
args.push('--meta-json', JSON.stringify(event.metadata));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const output = runCommand('bd', args, {
|
|
364
|
+
cwd: options.cwd || process.cwd(),
|
|
365
|
+
encoding: 'utf8',
|
|
366
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
367
|
+
timeout: options.timeoutMs || 5000,
|
|
368
|
+
});
|
|
369
|
+
return { success: true, event, output };
|
|
370
|
+
} catch (error) {
|
|
371
|
+
return { success: false, event, error: error.message };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
module.exports = {
|
|
376
|
+
PROTECTED_SURFACES,
|
|
377
|
+
normalizeRepoPath,
|
|
378
|
+
resolveRepoRelativePath,
|
|
379
|
+
lstatIfPresent,
|
|
380
|
+
assertNoAncestorSymlinkEscape,
|
|
381
|
+
assertNoSymlinkEscape,
|
|
382
|
+
classifyProtectedPath,
|
|
383
|
+
assertProtectedWriteAllowed,
|
|
384
|
+
writeProtectedFile,
|
|
385
|
+
buildProtectedStateAuditEvent,
|
|
386
|
+
recordProtectedStateAuditEvent,
|
|
387
|
+
};
|