forge-workflow 0.0.9 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
- package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
- package/.cursor/rules/permissions-guidance.mdc +2 -2
- package/.forge/hooks/check-tdd.js +3 -0
- package/.forge/hooks/forge-native-hook.js +245 -0
- package/.forge/protected-paths.yaml +157 -0
- package/AGENTS.md +151 -61
- package/CHANGELOG.md +681 -0
- package/CLAUDE.md +9 -106
- package/QUICKSTART.md +171 -0
- package/README.md +271 -363
- package/bin/forge-cmd.js +120 -9
- package/bin/forge-preflight.js +26 -5
- package/bin/forge.js +466 -489
- package/docs/INDEX.md +93 -0
- package/docs/PROJECT_DESIGN.md +685 -0
- package/docs/architecture/index.md +66 -0
- package/docs/architecture/notes/README.md +35 -0
- package/docs/architecture/subsystems/README.md +46 -0
- package/docs/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
- package/docs/forge/VALIDATION.md +82 -0
- package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
- package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
- package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
- package/docs/guides/GREPTILE_SETUP.md +46 -0
- package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
- package/docs/guides/MIGRATION.md +56 -0
- package/docs/guides/SETUP.md +118 -0
- package/docs/guides/SUPPORT.md +185 -0
- package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
- package/docs/guides/memory-backends.md +183 -0
- package/docs/reference/ADAPTERS.md +128 -0
- package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
- package/docs/reference/COMMANDS.md +205 -0
- package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
- package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
- package/docs/reference/HERMES_INTEGRATION.md +118 -0
- package/docs/reference/INSIGHTS_RECAP.md +63 -0
- package/docs/reference/INSTALL.md +164 -0
- package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
- package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
- package/docs/reference/RELEASE.md +68 -0
- package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
- package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
- package/docs/reference/SKILLS.md +35 -0
- package/docs/reference/STATUS_BOARD.md +80 -0
- package/docs/reference/TEMPLATES.md +106 -0
- package/docs/reference/TOOLCHAIN.md +658 -0
- package/docs/reference/VALIDATION.md +82 -0
- package/docs/reference/agent-permissions.md +169 -0
- package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
- package/docs/reference/control-plane-guarantees.md +125 -0
- package/docs/reference/dependency-chain.md +331 -0
- package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
- package/docs/reference/forge-kernel-schema.md +72 -0
- package/docs/reference/kernel-conflict-evaluators.md +27 -0
- package/docs/reference/patch-md-format.md +77 -0
- package/docs/reference/protected-state-surfaces.md +59 -0
- package/docs/reference/shepherd.md +115 -0
- package/docs/reference/superpowers-analysis.md +320 -0
- package/docs/reference/superpowers-integration-options.md +404 -0
- package/docs/reference/test-environment.md +519 -0
- package/docs/reference/upgrade-safety.md +59 -0
- package/lefthook.yml +18 -0
- package/lib/adapter-cli.js +307 -0
- package/lib/adapters/beads-issue-adapter.js +127 -0
- package/lib/adapters/beads-kernel-compat.js +1042 -0
- package/lib/adapters/greptile-review-adapter.js +141 -0
- package/lib/adapters/kernel-issue-adapter.js +101 -0
- package/lib/adapters/pr-state-adapter.js +484 -0
- package/lib/adoption-profiles.js +126 -0
- package/lib/agents/README.md +2 -6
- package/lib/agents/claude.plugin.json +3 -8
- package/lib/agents/codex.plugin.json +9 -1
- package/lib/agents/cursor.plugin.json +2 -6
- package/lib/agents/hermes.plugin.json +22 -0
- package/lib/agents-config.js +39 -1236
- package/lib/audit-evidence.js +282 -0
- package/lib/beads-setup.js +225 -28
- package/lib/beads-sync-scaffold.js +36 -107
- package/lib/codex-skills.js +51 -1
- package/lib/commands/_issue.js +744 -70
- package/lib/commands/_manifest.js +91 -0
- package/lib/commands/_registry.js +85 -34
- package/lib/commands/_resolve-command-opts.js +261 -0
- package/lib/commands/_serve-security.js +270 -0
- package/lib/commands/adapter.js +12 -0
- package/lib/commands/add.js +118 -0
- package/lib/commands/audit.js +70 -0
- package/lib/commands/blocked.js +5 -0
- package/lib/commands/board.js +64 -0
- package/lib/commands/claim.js +21 -2
- package/lib/commands/claims.js +7 -0
- package/lib/commands/clean.js +485 -75
- package/lib/commands/close.js +2 -2
- package/lib/commands/comment.js +5 -0
- package/lib/commands/control.js +148 -0
- package/lib/commands/create.js +2 -2
- package/lib/commands/dev.js +185 -7
- package/lib/commands/doc-gate.js +336 -0
- package/lib/commands/doctor.js +156 -0
- package/lib/commands/explain.js +15 -0
- package/lib/commands/export.js +237 -0
- package/lib/commands/gate.js +192 -0
- package/lib/commands/hooks.js +242 -0
- package/lib/commands/inbox.js +118 -0
- package/lib/commands/init.js +598 -0
- package/lib/commands/insights.js +79 -0
- package/lib/commands/issue.js +12 -1
- package/lib/commands/issues.js +66 -0
- package/lib/commands/lint.js +5 -0
- package/lib/commands/list.js +2 -2
- package/lib/commands/merge.js +312 -0
- package/lib/commands/migrate.js +523 -0
- package/lib/commands/new.js +12 -0
- package/lib/commands/options.js +241 -0
- package/lib/commands/orient.js +13 -0
- package/lib/commands/orphans.js +5 -0
- package/lib/commands/patch.js +67 -0
- package/lib/commands/plan.js +436 -24
- package/lib/commands/preflight.js +211 -0
- package/lib/commands/prime.js +13 -0
- package/lib/commands/push.js +69 -2
- package/lib/commands/ready.js +2 -2
- package/lib/commands/recall.js +116 -0
- package/lib/commands/recap.js +61 -0
- package/lib/commands/recommend.js +22 -2
- package/lib/commands/release.js +91 -0
- package/lib/commands/remember.js +74 -0
- package/lib/commands/role.js +99 -0
- package/lib/commands/serve.js +581 -0
- package/lib/commands/setup.js +851 -979
- package/lib/commands/shepherd.js +436 -0
- package/lib/commands/ship.js +23 -1
- package/lib/commands/show.js +2 -2
- package/lib/commands/stage.js +192 -0
- package/lib/commands/stale.js +5 -0
- package/lib/commands/status.js +329 -11
- package/lib/commands/sync.js +34 -46
- package/lib/commands/team.js +15 -2
- package/lib/commands/test.js +58 -7
- package/lib/commands/update.js +2 -2
- package/lib/commands/upgrade.js +47 -0
- package/lib/commands/validate.js +56 -25
- package/lib/commands/worktree.js +308 -128
- package/lib/config-writer.js +202 -0
- package/lib/control-plane.js +236 -0
- package/lib/core/runtime-graph.js +946 -0
- package/lib/dep-guard/keyword-ripple.js +184 -0
- package/lib/deprecated-sync-cleanup.js +362 -0
- package/lib/detect-agent.js +2 -28
- package/lib/detect-worktree.js +42 -17
- package/lib/doc-gate/declaration.js +177 -0
- package/lib/doc-gate/detect.js +289 -0
- package/lib/doc-gate/gate.js +375 -0
- package/lib/doc-gate/okf-config.js +128 -0
- package/lib/doc-gate/okf.js +429 -0
- package/lib/docs-command.js +1161 -6
- package/lib/forge-issues.js +697 -0
- package/lib/forge-lock.js +262 -0
- package/lib/gate-events.js +193 -0
- package/lib/global-flags.js +74 -0
- package/lib/greptile-match.js +7 -63
- package/lib/harness-capability-matrix.js +380 -0
- package/lib/hook-global-installer.js +347 -0
- package/lib/hook-renderer.js +451 -0
- package/lib/inbox.js +391 -0
- package/lib/insights.js +397 -0
- package/lib/issue-adapter.js +156 -0
- package/lib/issue-backend.js +145 -0
- package/lib/issue-render.js +220 -0
- package/lib/issue-sync/authority.js +100 -0
- package/lib/issue-sync/github-pull.js +184 -0
- package/lib/issue-sync/import-primitives.js +98 -0
- package/lib/issue-sync/legacy-link-bridge.js +436 -0
- package/lib/issue-sync/link-store.js +292 -0
- package/lib/issue-sync/project-github.js +123 -0
- package/lib/issue-sync/reconcile.js +195 -0
- package/lib/issue-sync/schema.js +126 -0
- package/lib/kernel/backing-issue.js +305 -0
- package/lib/kernel/broker.js +1218 -0
- package/lib/kernel/cli-broker-factory.js +130 -0
- package/lib/kernel/conflict-signal.js +82 -0
- package/lib/kernel/evaluators.js +195 -0
- package/lib/kernel/fs-class.js +495 -0
- package/lib/kernel/issue-command-contract.js +559 -0
- package/lib/kernel/issue-id-resolver.js +186 -0
- package/lib/kernel/lease-enforcer.js +158 -0
- package/lib/kernel/migrations.js +333 -0
- package/lib/kernel/planning-buckets-schema.js +109 -0
- package/lib/kernel/projection-jsonl-writer.js +450 -0
- package/lib/kernel/readiness-model.js +329 -0
- package/lib/kernel/schema.js +356 -0
- package/lib/kernel/sqlite-driver.js +2504 -0
- package/lib/kernel/taxonomy-validator.js +394 -0
- package/lib/lefthook-check.js +8 -4
- package/lib/lefthook-wiring.js +413 -0
- package/lib/mcp-config-renderer.js +288 -0
- package/lib/memory/graphiti-mcp.js +106 -0
- package/lib/memory/router.js +387 -0
- package/lib/memory/typed-api.js +102 -0
- package/lib/memory-digest.js +195 -0
- package/lib/merge-rules.js +395 -0
- package/lib/migrate-dry-run.js +466 -0
- package/lib/orientation.js +863 -0
- package/lib/package-manager-remediation.js +103 -0
- package/lib/package-root.js +381 -0
- package/lib/patch-intent.js +890 -0
- package/lib/plugin-catalog.js +3 -4
- package/lib/plugin-manager.js +0 -5
- package/lib/pr-bundle.js +186 -0
- package/lib/pr-monitor/differ.js +195 -0
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/gather.js +124 -0
- package/lib/pr-monitor/journal.js +299 -0
- package/lib/pr-monitor/monitor.js +146 -0
- package/lib/pr-monitor/render-sticky.js +157 -0
- package/lib/pr-monitor/watch-lifecycle.js +95 -0
- package/lib/pr-monitor/watch.js +247 -0
- package/lib/pr-pull.js +1273 -0
- package/lib/pr-shepherd.js +494 -0
- package/lib/pr-state-validator.js +59 -0
- package/lib/preflight/gates.js +237 -0
- package/lib/preflight/runner.js +116 -0
- package/lib/project-discovery.js +0 -53
- package/lib/project-memory.js +166 -0
- package/lib/protected-path-manifest.js +281 -0
- package/lib/protected-state-surfaces.js +387 -0
- package/lib/release-readiness.js +2089 -0
- package/lib/reset.js +59 -45
- package/lib/review-adapter.js +68 -0
- package/lib/rules-sync.js +260 -0
- package/lib/runtime-health.js +332 -23
- package/lib/safety-config-renderer.js +268 -0
- package/lib/setup-action-log.js +1 -7
- package/lib/setup.js +27 -65
- package/lib/shell-utils.js +76 -6
- package/lib/skills-sync.js +330 -0
- package/lib/smart-status/conflicts.js +205 -0
- package/lib/smart-status/scoring.js +191 -0
- package/lib/status/beads-snapshot.js +145 -0
- package/lib/status/presenter.js +216 -0
- package/lib/status/snapshot.js +186 -0
- package/lib/sync-backend.js +202 -0
- package/lib/untrusted-content.js +52 -0
- package/lib/upgrade-safety.js +199 -0
- package/lib/workflow/enforce-stage.js +298 -47
- package/lib/workflow/stage-transition.js +115 -0
- package/lib/workflow/stages.js +30 -6
- package/lib/workflow/state-manager.js +159 -14
- package/lib/workflow/state.js +23 -1
- package/lib/workflow-profiles.js +17 -5
- package/package.json +46 -36
- package/rules/documentation.md +19 -0
- package/rules/kernel-tracking.md +26 -0
- package/rules/security.md +22 -0
- package/rules/tdd.md +20 -0
- package/rules/workflow.md +27 -0
- package/scripts/auto-backing-issue.js +47 -0
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-migrate-to-dolt.sh +7 -0
- package/scripts/beads-upgrade-smoke.sh +284 -0
- package/scripts/behavioral-judge.sh +115 -11
- package/scripts/benchmark.js +349 -63
- package/scripts/bootstrap-windows-tools.sh +78 -0
- package/scripts/branch-protection.js +2 -3
- package/scripts/check-agents.js +34 -137
- package/scripts/commitlint.js +3 -1
- package/scripts/conflict-detect.sh +3 -0
- package/scripts/dep-guard-analyze.js +52 -17
- package/scripts/dep-guard-keyword-ripple.js +29 -0
- package/scripts/dep-guard-render-review.js +86 -0
- package/scripts/dep-guard.sh +64 -232
- package/scripts/file-index.sh +3 -0
- package/scripts/forge-team/lib/claim.sh +34 -18
- package/scripts/forge-team/lib/dashboard.sh +61 -86
- package/scripts/forge-team/lib/epic.sh +99 -263
- package/scripts/forge-team/lib/hooks.sh +26 -28
- package/scripts/forge-team/lib/identity.sh +4 -4
- package/scripts/forge-team/lib/sync-github.sh +144 -47
- package/scripts/forge-team/lib/verify.sh +93 -83
- package/scripts/forge-team/lib/workload.sh +41 -65
- package/scripts/forge-team/tests/claim.test.sh +25 -19
- package/scripts/forge-team/tests/dashboard.test.sh +31 -46
- package/scripts/forge-team/tests/epic.test.sh +52 -71
- package/scripts/forge-team/tests/hooks.test.sh +38 -50
- package/scripts/forge-team/tests/identity.test.sh +3 -3
- package/scripts/forge-team/tests/integration.test.sh +44 -66
- package/scripts/forge-team/tests/sync-github.test.sh +183 -79
- package/scripts/forge-team/tests/verify.test.sh +37 -46
- package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
- package/scripts/forge-team/tests/workload.test.sh +32 -66
- package/scripts/gen-command-manifest.js +153 -0
- package/scripts/gen-embedded-assets.mjs +129 -0
- package/scripts/install.ps1 +139 -0
- package/scripts/install.sh +268 -0
- package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
- package/scripts/lib/release-asset.mjs +84 -0
- package/scripts/parity-check.mjs +145 -0
- package/scripts/parity-check.test.mjs +58 -0
- package/scripts/pin-agentic-workflow-images.js +112 -0
- package/scripts/pr-coordinator.sh +3 -0
- package/scripts/preflight-sonar.eslint.config.mjs +44 -0
- package/scripts/preflight.sh +108 -0
- package/scripts/protected-state-check.js +104 -0
- package/scripts/smart-status-score.js +31 -0
- package/scripts/smart-status-sessions.js +51 -0
- package/scripts/smart-status.sh +117 -369
- package/scripts/spikes/config-race-bench.js +111 -0
- package/scripts/spikes/harness-capability-matrix.js +13 -0
- package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
- package/scripts/spikes/protected-path-manifest.js +20 -0
- package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
- package/scripts/sync-agent-skills.js +62 -0
- package/scripts/sync-agentic-workflow.js +48 -0
- package/scripts/sync-utils.sh +3 -0
- package/scripts/test-ci-shard.js +251 -0
- package/scripts/test-dashboard.js +188 -52
- package/scripts/test-full-suite.js +186 -0
- package/scripts/test-profile.js +278 -0
- package/scripts/test.js +302 -28
- package/scripts/validate.js +143 -0
- package/scripts/validate.sh +18 -1
- package/skills/claim-safety/SKILL.md +102 -0
- package/skills/claim-safety/evals/evals.json +46 -0
- package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +46 -52
- package/skills/dev/evals/evals.json +50 -0
- package/skills/hermes-forge/SKILL.md +185 -0
- package/skills/hermes-forge/evals/evals.json +46 -0
- package/skills/issue-basics/SKILL.md +111 -0
- package/skills/issue-basics/evals/evals.json +46 -0
- package/skills/kernel/SKILL.md +166 -0
- package/skills/kernel/evals/evals.json +50 -0
- package/skills/memory/SKILL.md +102 -0
- package/skills/parallel-deep-research/SKILL.md +14 -11
- package/skills/parallel-deep-research/evals/evals.json +11 -27
- package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +134 -159
- package/skills/plan/evals/evals.json +42 -0
- package/skills/research/SKILL.md +195 -0
- package/skills/research/evals/evals.json +42 -0
- package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
- package/skills/review/evals/evals.json +42 -0
- package/skills/rollback/SKILL.md +110 -0
- package/skills/rollback/evals/evals.json +46 -0
- package/skills/rollback/references/methods.md +204 -0
- package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
- package/skills/shepherd/SKILL.md +66 -0
- package/skills/shepherd/evals/evals.json +42 -0
- package/skills/ship/SKILL.md +251 -0
- package/skills/ship/evals/evals.json +42 -0
- package/skills/smith/SKILL.md +142 -0
- package/skills/smith/evals/evals.json +46 -0
- package/skills/smith/references/autonomy-and-gates.md +94 -0
- package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
- package/skills/sonarcloud/evals/evals.json +46 -0
- package/skills/sonarcloud-analysis/SKILL.md +18 -13
- package/skills/sonarcloud-analysis/evals/evals.json +11 -15
- package/skills/status/SKILL.md +102 -0
- package/skills/status/evals/evals.json +50 -0
- package/skills/triage-ready/SKILL.md +121 -0
- package/skills/triage-ready/evals/evals.json +42 -0
- package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
- package/skills/validate/evals/evals.json +42 -0
- package/skills/verify/SKILL.md +299 -0
- package/skills/verify/evals/evals.json +50 -0
- package/.claude/commands/dev.md +0 -345
- package/.claude/commands/plan.md +0 -566
- package/.claude/commands/premerge.md +0 -186
- package/.claude/commands/research.md +0 -42
- package/.claude/commands/review.md +0 -451
- package/.claude/commands/rollback.md +0 -721
- package/.claude/commands/ship.md +0 -213
- package/.claude/commands/sonarcloud.md +0 -152
- package/.claude/commands/status.md +0 -90
- package/.claude/commands/validate.md +0 -288
- package/.claude/commands/verify.md +0 -269
- package/.claude/rules/workflow.md +0 -121
- package/.cline/workflows/dev.md +0 -342
- package/.cline/workflows/plan.md +0 -563
- package/.cline/workflows/premerge.md +0 -183
- package/.cline/workflows/research.md +0 -39
- package/.cline/workflows/review.md +0 -448
- package/.cline/workflows/rollback.md +0 -718
- package/.cline/workflows/ship.md +0 -210
- package/.cline/workflows/sonarcloud.md +0 -146
- package/.cline/workflows/status.md +0 -87
- package/.cline/workflows/validate.md +0 -285
- package/.cline/workflows/verify.md +0 -266
- package/.codex/config.toml +0 -11
- package/.codex/skills/dev/SKILL.md +0 -345
- package/.codex/skills/plan/SKILL.md +0 -566
- package/.codex/skills/premerge/SKILL.md +0 -186
- package/.codex/skills/research/SKILL.md +0 -42
- package/.codex/skills/review/SKILL.md +0 -451
- package/.codex/skills/rollback/SKILL.md +0 -721
- package/.codex/skills/ship/SKILL.md +0 -213
- package/.codex/skills/sonarcloud/SKILL.md +0 -149
- package/.codex/skills/status/SKILL.md +0 -90
- package/.codex/skills/validate/SKILL.md +0 -288
- package/.codex/skills/verify/SKILL.md +0 -269
- package/.cursor/commands/dev.md +0 -342
- package/.cursor/commands/plan.md +0 -563
- package/.cursor/commands/premerge.md +0 -183
- package/.cursor/commands/research.md +0 -39
- package/.cursor/commands/review.md +0 -448
- package/.cursor/commands/ship.md +0 -210
- package/.cursor/commands/sonarcloud.md +0 -146
- package/.cursor/commands/status.md +0 -87
- package/.cursor/commands/validate.md +0 -285
- package/.cursor/commands/verify.md +0 -266
- package/.cursorrules +0 -149
- package/.github/prompts/premerge.prompt.md +0 -188
- package/.github/prompts/research.prompt.md +0 -44
- package/.github/prompts/rollback.prompt.md +0 -723
- package/.github/prompts/ship.prompt.md +0 -215
- package/.github/prompts/status.prompt.md +0 -92
- package/.github/prompts/verify.prompt.md +0 -271
- package/.github/workflows/beads-to-github.yml +0 -56
- package/.github/workflows/github-to-beads.yml +0 -97
- package/.kilocode/workflows/dev.md +0 -346
- package/.kilocode/workflows/plan.md +0 -567
- package/.kilocode/workflows/premerge.md +0 -187
- package/.kilocode/workflows/research.md +0 -43
- package/.kilocode/workflows/review.md +0 -452
- package/.kilocode/workflows/rollback.md +0 -722
- package/.kilocode/workflows/ship.md +0 -214
- package/.kilocode/workflows/sonarcloud.md +0 -150
- package/.kilocode/workflows/status.md +0 -91
- package/.kilocode/workflows/validate.md +0 -289
- package/.kilocode/workflows/verify.md +0 -270
- package/.opencode/commands/dev.md +0 -345
- package/.opencode/commands/plan.md +0 -566
- package/.opencode/commands/premerge.md +0 -186
- package/.opencode/commands/research.md +0 -42
- package/.opencode/commands/review.md +0 -451
- package/.opencode/commands/rollback.md +0 -721
- package/.opencode/commands/ship.md +0 -213
- package/.opencode/commands/sonarcloud.md +0 -149
- package/.opencode/commands/status.md +0 -90
- package/.opencode/commands/validate.md +0 -288
- package/.opencode/commands/verify.md +0 -269
- package/.roo/commands/dev.md +0 -346
- package/.roo/commands/plan.md +0 -567
- package/.roo/commands/premerge.md +0 -187
- package/.roo/commands/research.md +0 -43
- package/.roo/commands/review.md +0 -452
- package/.roo/commands/rollback.md +0 -722
- package/.roo/commands/ship.md +0 -214
- package/.roo/commands/sonarcloud.md +0 -150
- package/.roo/commands/status.md +0 -91
- package/.roo/commands/validate.md +0 -289
- package/.roo/commands/verify.md +0 -270
- package/docs/BEADS_GITHUB_SYNC.md +0 -255
- package/docs/GREPTILE_SETUP.md +0 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
- package/docs/SETUP.md +0 -663
- package/docs/VALIDATION.md +0 -363
- package/lib/agents/cline.plugin.json +0 -29
- package/lib/agents/copilot.plugin.json +0 -24
- package/lib/agents/kilocode.plugin.json +0 -22
- package/lib/agents/opencode.plugin.json +0 -23
- package/lib/agents/roo.plugin.json +0 -30
- package/lib/beads-health-check.js +0 -143
- package/lib/commands/commands-reset.js +0 -147
- package/opencode.json +0 -67
- package/scripts/beads-context.test.js +0 -567
- package/scripts/github-beads-sync/comment.mjs +0 -64
- package/scripts/github-beads-sync/config.mjs +0 -148
- package/scripts/github-beads-sync/github-api.mjs +0 -131
- package/scripts/github-beads-sync/index.mjs +0 -332
- package/scripts/github-beads-sync/label-mapper.mjs +0 -54
- package/scripts/github-beads-sync/mapping.mjs +0 -78
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
- package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
- package/scripts/github-beads-sync/run-bd.mjs +0 -161
- package/scripts/github-beads-sync/sanitize.mjs +0 -121
- package/scripts/github-beads-sync.config.json +0 -26
- package/scripts/sync-commands.js +0 -600
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
const { execFileSync } = require('node:child_process');
|
|
7
|
+
const YAML = require('yaml');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_PATCH_PATH = '.forge/patch.md';
|
|
10
|
+
const PATCH_SOURCE = 'git-diff';
|
|
11
|
+
const RECORD_START = '<!-- forge-patch-intent:v1';
|
|
12
|
+
const RECORD_END = '<!-- /forge-patch-intent -->';
|
|
13
|
+
const ANCHOR_ID_PATTERN = '[A-Za-z0-9._:-]+';
|
|
14
|
+
const ANCHOR_PATTERNS = [
|
|
15
|
+
new RegExp(String.raw`^\s*<!--\s*forge-anchor:(${ANCHOR_ID_PATTERN})\s*-->\s*$`),
|
|
16
|
+
new RegExp(String.raw`^\s*<!--\s*forge-anchor\s+id=["']?(${ANCHOR_ID_PATTERN})["']?\s*-->\s*$`),
|
|
17
|
+
new RegExp(String.raw`^\s*<!--\s*forge:anchor\s+id=["'](${ANCHOR_ID_PATTERN})["']\s*-->\s*$`),
|
|
18
|
+
];
|
|
19
|
+
const DIFF_HEADER_PREFIX = 'diff --git a/';
|
|
20
|
+
const HUNK_HEADER_PATTERN = /@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,(\d+))?\s+@@/;
|
|
21
|
+
const PATCH_DIFF_FENCE = '\n```diff\n';
|
|
22
|
+
const PATCH_FENCE_END = '\n```';
|
|
23
|
+
const KNOWN_DIFF_PATH_PREFIXES = new Set(['a', 'b', 'c', 'i', 'o', 'w']);
|
|
24
|
+
const EXCLUDED_DIRS = new Set([
|
|
25
|
+
'.git',
|
|
26
|
+
'.beads',
|
|
27
|
+
'node_modules',
|
|
28
|
+
'test-results',
|
|
29
|
+
'.worktrees',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function toPosixPath(value) {
|
|
33
|
+
const normalized = String(value || '').replaceAll('\\', '/');
|
|
34
|
+
return normalized.startsWith('./') ? normalized.slice(2) : normalized;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hashText(value) {
|
|
38
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stripSingleTrailingNewline(value) {
|
|
42
|
+
return String(value || '').endsWith('\n') ? String(value || '').slice(0, -1) : String(value || '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ensureTrailingNewline(value) {
|
|
46
|
+
return String(value || '').endsWith('\n') ? String(value || '') : `${String(value || '')}\n`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function patchId(anchorId, diff) {
|
|
50
|
+
const safeAnchor = normalizePatchIdAnchor(anchorId);
|
|
51
|
+
const hashInput = `${String(anchorId || '')}\0${diff}`;
|
|
52
|
+
return `patch_${safeAnchor}_${hashText(hashInput).slice(0, 12)}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizePatchIdAnchor(anchorId) {
|
|
56
|
+
let normalized = '';
|
|
57
|
+
let previousWasSeparator = true;
|
|
58
|
+
for (const character of String(anchorId || '')) {
|
|
59
|
+
const isAlphaNumeric = (
|
|
60
|
+
(character >= 'A' && character <= 'Z')
|
|
61
|
+
|| (character >= 'a' && character <= 'z')
|
|
62
|
+
|| (character >= '0' && character <= '9')
|
|
63
|
+
);
|
|
64
|
+
if (isAlphaNumeric) {
|
|
65
|
+
normalized += character;
|
|
66
|
+
previousWasSeparator = false;
|
|
67
|
+
} else if (!previousWasSeparator) {
|
|
68
|
+
normalized += '_';
|
|
69
|
+
previousWasSeparator = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return normalized.endsWith('_') ? normalized.slice(0, -1) : normalized || 'anchor';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseAnchorLine(line) {
|
|
76
|
+
for (const pattern of ANCHOR_PATTERNS) {
|
|
77
|
+
const match = pattern.exec(line);
|
|
78
|
+
if (match) return match[1];
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function shouldScanFile(relativePath, excludedPatchPath = DEFAULT_PATCH_PATH) {
|
|
84
|
+
const normalized = toPosixPath(relativePath);
|
|
85
|
+
if (normalized === DEFAULT_PATCH_PATH) return false;
|
|
86
|
+
if (normalized === toPosixPath(excludedPatchPath)) return false;
|
|
87
|
+
return isAnchorDeclarationFile(normalized);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function walkFiles(root, dir = root, files = [], options = {}) {
|
|
91
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
92
|
+
if (entry.isDirectory() && EXCLUDED_DIRS.has(entry.name)) continue;
|
|
93
|
+
const fullPath = path.join(dir, entry.name);
|
|
94
|
+
if (!isPhysicalPathInsideProject(root, fullPath)) continue;
|
|
95
|
+
if (entry.isDirectory()) {
|
|
96
|
+
walkFiles(root, fullPath, files, options);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (!entry.isFile()) continue;
|
|
100
|
+
const relativePath = toPosixPath(path.relative(root, fullPath));
|
|
101
|
+
if (shouldScanFile(relativePath, options.excludedPatchPath)) files.push(relativePath);
|
|
102
|
+
}
|
|
103
|
+
return files;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function discoverAnchors(projectRoot, options = {}) {
|
|
107
|
+
const anchors = new Map();
|
|
108
|
+
for (const relativePath of walkFiles(projectRoot, projectRoot, [], options)) {
|
|
109
|
+
for (const anchor of scanFileAnchors(projectRoot, relativePath)) {
|
|
110
|
+
addDiscoveredAnchor(anchors, anchor);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return anchors;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function scanFileAnchors(projectRoot, relativePath) {
|
|
117
|
+
const fullPath = path.join(projectRoot, relativePath);
|
|
118
|
+
const lines = fs.readFileSync(fullPath, 'utf8').split(/\r?\n/);
|
|
119
|
+
const anchors = [];
|
|
120
|
+
let openFence = null;
|
|
121
|
+
const shouldTrackFences = isMarkdownFile(relativePath);
|
|
122
|
+
|
|
123
|
+
for (const [index, line] of lines.entries()) {
|
|
124
|
+
const fence = shouldTrackFences ? parseMarkdownFence(line) : null;
|
|
125
|
+
if (fence) {
|
|
126
|
+
openFence = nextMarkdownFenceState(openFence, fence);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (openFence) continue;
|
|
130
|
+
if (shouldTrackFences && isMarkdownIndentedCodeLine(line)) continue;
|
|
131
|
+
const id = parseAnchorLine(line);
|
|
132
|
+
if (id) {
|
|
133
|
+
anchors.push({ id, path: relativePath, line: index + 1 });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return anchors;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function addDiscoveredAnchor(anchors, anchor) {
|
|
141
|
+
if (anchors.has(anchor.id)) {
|
|
142
|
+
const existing = anchors.get(anchor.id);
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Duplicate forge anchor '${anchor.id}' in '${existing.path}' and '${anchor.path}'. Anchor IDs must be unique.`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
anchors.set(anchor.id, anchor);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isMarkdownFile(relativePath) {
|
|
151
|
+
const lowerPath = relativePath.toLowerCase();
|
|
152
|
+
return lowerPath.endsWith('.md') || lowerPath.endsWith('.mdx');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isAnchorDeclarationFile(relativePath) {
|
|
156
|
+
const lowerPath = relativePath.toLowerCase();
|
|
157
|
+
return lowerPath.endsWith('.md') || lowerPath.endsWith('.mdx') || lowerPath.endsWith('.txt');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function parseMarkdownFence(line) {
|
|
161
|
+
const match = /^(?: {0,3})(`{3,}|~{3,})/.exec(line);
|
|
162
|
+
if (!match) return null;
|
|
163
|
+
return {
|
|
164
|
+
marker: match[1][0],
|
|
165
|
+
length: match[1].length,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function nextMarkdownFenceState(openFence, fence) {
|
|
170
|
+
if (!openFence) return fence;
|
|
171
|
+
if (fence.marker === openFence.marker && fence.length >= openFence.length) return null;
|
|
172
|
+
return openFence;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isMarkdownIndentedCodeLine(line) {
|
|
176
|
+
return /^(?: {4,}|\t)/.test(line);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function nearestAnchorBefore(content, lineNumber, relativePath) {
|
|
180
|
+
const lines = content.split(/\r?\n/);
|
|
181
|
+
const end = Math.max(0, Math.min(lineNumber - 1, lines.length - 1));
|
|
182
|
+
let nearest = null;
|
|
183
|
+
let openFence = null;
|
|
184
|
+
const shouldTrackFences = isMarkdownFile(relativePath);
|
|
185
|
+
|
|
186
|
+
for (let index = 0; index <= end; index += 1) {
|
|
187
|
+
const fence = shouldTrackFences ? parseMarkdownFence(lines[index]) : null;
|
|
188
|
+
if (fence) {
|
|
189
|
+
openFence = nextMarkdownFenceState(openFence, fence);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (openFence) continue;
|
|
193
|
+
if (shouldTrackFences && isMarkdownIndentedCodeLine(lines[index])) continue;
|
|
194
|
+
const id = parseAnchorLine(lines[index]);
|
|
195
|
+
if (id) nearest = { id, line: index + 1 };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return nearest;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function anchorInDiffHunk(diff, relativePath) {
|
|
202
|
+
let openFence = null;
|
|
203
|
+
const shouldTrackFences = isMarkdownFile(relativePath);
|
|
204
|
+
|
|
205
|
+
for (const line of diff.split(/\r?\n/)) {
|
|
206
|
+
const scan = scanAnchorDiffContextLine(line, { openFence, shouldTrackFences });
|
|
207
|
+
openFence = scan.openFence;
|
|
208
|
+
if (scan.stop) break;
|
|
209
|
+
const id = scan.anchorId;
|
|
210
|
+
if (id) return { id, line: null };
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function scanAnchorDiffContextLine(line, state) {
|
|
216
|
+
const { shouldTrackFences } = state;
|
|
217
|
+
if (isDiffContentChange(line)) return { ...state, stop: true, anchorId: null };
|
|
218
|
+
if (!line.startsWith(' ')) return { ...state, stop: false, anchorId: null };
|
|
219
|
+
|
|
220
|
+
const content = line.slice(1);
|
|
221
|
+
const fence = shouldTrackFences ? parseMarkdownFence(content) : null;
|
|
222
|
+
if (fence) {
|
|
223
|
+
return {
|
|
224
|
+
shouldTrackFences,
|
|
225
|
+
openFence: nextMarkdownFenceState(state.openFence, fence),
|
|
226
|
+
stop: false,
|
|
227
|
+
anchorId: null,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (state.openFence || (shouldTrackFences && isMarkdownIndentedCodeLine(content))) {
|
|
231
|
+
return { ...state, stop: false, anchorId: null };
|
|
232
|
+
}
|
|
233
|
+
return { ...state, stop: false, anchorId: parseAnchorLine(content) };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function isDiffContentChange(line) {
|
|
237
|
+
if (!line.startsWith('+') && !line.startsWith('-')) return false;
|
|
238
|
+
return !line.startsWith('+++ ') && !line.startsWith('--- ');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parseHunkNewRange(header) {
|
|
242
|
+
const match = HUNK_HEADER_PATTERN.exec(header);
|
|
243
|
+
if (!match) return { start: 1, count: 1, end: 1 };
|
|
244
|
+
const start = Number(match[1]);
|
|
245
|
+
const count = match[2] === undefined ? 1 : Number(match[2]);
|
|
246
|
+
return { start, count, end: start + count - 1 };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function parseUnifiedDiff(diff) {
|
|
250
|
+
const lines = String(diff || '').split(/\r?\n/);
|
|
251
|
+
const records = [];
|
|
252
|
+
let index = 0;
|
|
253
|
+
|
|
254
|
+
while (index < lines.length) {
|
|
255
|
+
if (!lines[index].startsWith('diff --git ')) {
|
|
256
|
+
index += 1;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const parsedFile = parseUnifiedDiffFile(lines, index);
|
|
261
|
+
records.push(...parsedFile.records);
|
|
262
|
+
index = parsedFile.nextIndex;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return records;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function parseUnifiedDiffFile(lines, startIndex) {
|
|
269
|
+
const fileHeader = [lines[startIndex]];
|
|
270
|
+
const records = [];
|
|
271
|
+
let index = startIndex + 1;
|
|
272
|
+
|
|
273
|
+
while (index < lines.length && !lines[index].startsWith('@@ ') && !lines[index].startsWith('diff --git ')) {
|
|
274
|
+
fileHeader.push(lines[index]);
|
|
275
|
+
index += 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const currentFile = parseUnifiedDiffCurrentPath(fileHeader);
|
|
279
|
+
if (!currentFile) {
|
|
280
|
+
return { nextIndex: index, records };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
while (index < lines.length && lines[index].startsWith('@@ ')) {
|
|
284
|
+
const parsedHunk = parseUnifiedDiffHunk(lines, index);
|
|
285
|
+
records.push({
|
|
286
|
+
path: currentFile,
|
|
287
|
+
newStart: parsedHunk.newStart,
|
|
288
|
+
newEnd: parsedHunk.newEnd,
|
|
289
|
+
diff: ensureTrailingNewline([...fileHeader, ...parsedHunk.hunk].join('\n')),
|
|
290
|
+
});
|
|
291
|
+
index = parsedHunk.nextIndex;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return { nextIndex: index, records };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseUnifiedDiffCurrentPath(fileHeader) {
|
|
298
|
+
const newFileHeader = fileHeader.find(line => line.startsWith('+++ '));
|
|
299
|
+
const newPath = newFileHeader ? parseDiffHeaderPath(newFileHeader, '+++ ') : null;
|
|
300
|
+
const normalizedNewPath = normalizeDiffSidePath(newPath);
|
|
301
|
+
if (normalizedNewPath) return toPosixPath(normalizedNewPath);
|
|
302
|
+
|
|
303
|
+
return parseUnifiedDiffCurrentPathFallback(fileHeader[0] || '');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function normalizeDiffSidePath(diffPath) {
|
|
307
|
+
if (!diffPath || diffPath === '/dev/null') return null;
|
|
308
|
+
const slashIndex = diffPath.indexOf('/');
|
|
309
|
+
if (slashIndex > 0 && KNOWN_DIFF_PATH_PREFIXES.has(diffPath.slice(0, slashIndex))) {
|
|
310
|
+
return diffPath.slice(slashIndex + 1);
|
|
311
|
+
}
|
|
312
|
+
return diffPath;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function parseUnifiedDiffCurrentPathFallback(diffHeader) {
|
|
316
|
+
if (!diffHeader.startsWith(DIFF_HEADER_PREFIX)) {
|
|
317
|
+
const quotedMatch = /^diff --git "a\/(?:[^"\\]|\\.)*" "b\/((?:[^"\\]|\\.)*)"/.exec(diffHeader);
|
|
318
|
+
return quotedMatch ? toPosixPath(unescapeGitQuotedPath(quotedMatch[1])) : null;
|
|
319
|
+
}
|
|
320
|
+
const separatorIndex = diffHeader.indexOf(' b/', DIFF_HEADER_PREFIX.length);
|
|
321
|
+
if (separatorIndex === -1) return null;
|
|
322
|
+
return toPosixPath(stripDiffPathMetadata(diffHeader.slice(separatorIndex + ' b/'.length)));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function parseDiffHeaderPath(line, prefix) {
|
|
326
|
+
if (!line.startsWith(prefix)) return null;
|
|
327
|
+
const value = line.slice(prefix.length);
|
|
328
|
+
if (!value.startsWith('"')) return stripDiffPathMetadata(value);
|
|
329
|
+
const parsed = parseGitQuotedPathToken(value);
|
|
330
|
+
return parsed ? parsed.path : null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function parseGitQuotedPathToken(value) {
|
|
334
|
+
let pathValue = '';
|
|
335
|
+
let index = 1;
|
|
336
|
+
while (index < value.length) {
|
|
337
|
+
const character = value[index];
|
|
338
|
+
if (character === '"') return { path: pathValue, rest: value.slice(index + 1) };
|
|
339
|
+
if (character !== '\\') {
|
|
340
|
+
pathValue += character;
|
|
341
|
+
index += 1;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (isOctalDigit(value[index + 1])) {
|
|
346
|
+
const parsed = parseGitOctalBytes(value, index);
|
|
347
|
+
pathValue += Buffer.from(parsed.bytes).toString('utf8');
|
|
348
|
+
index = parsed.nextIndex;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const escaped = value[index + 1];
|
|
353
|
+
pathValue += decodeGitSimpleEscape(escaped);
|
|
354
|
+
index += 2;
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function unescapeGitQuotedPath(value) {
|
|
360
|
+
const parsed = parseGitQuotedPathToken(`"${value}"`);
|
|
361
|
+
return parsed ? parsed.path : value;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function parseGitOctalBytes(value, startIndex) {
|
|
365
|
+
const bytes = [];
|
|
366
|
+
let index = startIndex;
|
|
367
|
+
while (value[index] === '\\' && isOctalDigit(value[index + 1])) {
|
|
368
|
+
let digits = '';
|
|
369
|
+
index += 1;
|
|
370
|
+
while (digits.length < 3 && isOctalDigit(value[index])) {
|
|
371
|
+
digits += value[index];
|
|
372
|
+
index += 1;
|
|
373
|
+
}
|
|
374
|
+
bytes.push(Number.parseInt(digits, 8));
|
|
375
|
+
}
|
|
376
|
+
return { bytes, nextIndex: index };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function isOctalDigit(character) {
|
|
380
|
+
return character >= '0' && character <= '7';
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function decodeGitSimpleEscape(character) {
|
|
384
|
+
if (character === 't') return '\t';
|
|
385
|
+
if (character === 'n') return '\n';
|
|
386
|
+
if (character === 'r') return '\r';
|
|
387
|
+
if (character === undefined) return '';
|
|
388
|
+
return character;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function stripDiffPathMetadata(value) {
|
|
392
|
+
const tabIndex = value.indexOf('\t');
|
|
393
|
+
const pathValue = tabIndex === -1 ? value : value.slice(0, tabIndex);
|
|
394
|
+
return pathValue.trimEnd();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function parseUnifiedDiffHunk(lines, startIndex) {
|
|
398
|
+
const hunk = [lines[startIndex]];
|
|
399
|
+
const { start: newStart, end: newEnd } = parseHunkNewRange(lines[startIndex]);
|
|
400
|
+
let index = startIndex + 1;
|
|
401
|
+
while (index < lines.length && !lines[index].startsWith('@@ ') && !lines[index].startsWith('diff --git ')) {
|
|
402
|
+
hunk.push(lines[index]);
|
|
403
|
+
index += 1;
|
|
404
|
+
}
|
|
405
|
+
return { hunk, newStart, newEnd, nextIndex: index };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function resolvePatchPath(projectRoot, configuredPath, errors) {
|
|
409
|
+
if (configuredPath === undefined || configuredPath === null || configuredPath === '') {
|
|
410
|
+
return DEFAULT_PATCH_PATH;
|
|
411
|
+
}
|
|
412
|
+
if (typeof configuredPath !== 'string') {
|
|
413
|
+
errors.push({
|
|
414
|
+
code: 'PATCH_INTENT_PATH_INVALID',
|
|
415
|
+
message: 'patchIntent.path must be a non-empty string.',
|
|
416
|
+
});
|
|
417
|
+
return DEFAULT_PATCH_PATH;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const rawPatchPath = configuredPath.trim();
|
|
421
|
+
if (!rawPatchPath) return DEFAULT_PATCH_PATH;
|
|
422
|
+
|
|
423
|
+
const root = path.resolve(projectRoot);
|
|
424
|
+
const resolved = path.resolve(projectRoot, rawPatchPath);
|
|
425
|
+
const relativeToRoot = path.relative(root, resolved);
|
|
426
|
+
if (
|
|
427
|
+
relativeToRoot === ''
|
|
428
|
+
|| relativeToRoot.startsWith('..')
|
|
429
|
+
|| path.isAbsolute(relativeToRoot)
|
|
430
|
+
|| !isPhysicalPathInsideProject(projectRoot, resolved)
|
|
431
|
+
) {
|
|
432
|
+
errors.push({
|
|
433
|
+
code: 'PATCH_INTENT_PATH_OUTSIDE_ROOT',
|
|
434
|
+
message: 'patchIntent.path must stay inside the project root.',
|
|
435
|
+
});
|
|
436
|
+
return DEFAULT_PATCH_PATH;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return toPosixPath(relativeToRoot);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function isPhysicalPathInsideProject(projectRoot, targetPath) {
|
|
443
|
+
const rootRealPath = fs.realpathSync.native(projectRoot);
|
|
444
|
+
const targetRealPath = fs.realpathSync.native(nearestExistingPath(targetPath));
|
|
445
|
+
const relativeToRoot = path.relative(rootRealPath, targetRealPath);
|
|
446
|
+
return relativeToRoot === '' || (!relativeToRoot.startsWith('..') && !path.isAbsolute(relativeToRoot));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function nearestExistingPath(targetPath) {
|
|
450
|
+
let currentPath = targetPath;
|
|
451
|
+
while (!fs.existsSync(currentPath)) {
|
|
452
|
+
const parentPath = path.dirname(currentPath);
|
|
453
|
+
if (parentPath === currentPath) return currentPath;
|
|
454
|
+
currentPath = parentPath;
|
|
455
|
+
}
|
|
456
|
+
return currentPath;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function resolveProjectFilePath(projectRoot, relativePath) {
|
|
460
|
+
const root = path.resolve(projectRoot);
|
|
461
|
+
const resolved = path.resolve(root, relativePath);
|
|
462
|
+
const relativeToRoot = path.relative(root, resolved);
|
|
463
|
+
if (
|
|
464
|
+
relativeToRoot.startsWith('..')
|
|
465
|
+
|| path.isAbsolute(relativeToRoot)
|
|
466
|
+
|| !isPhysicalPathInsideProject(projectRoot, resolved)
|
|
467
|
+
) {
|
|
468
|
+
throw new Error(`Diff path must stay inside the project root: ${relativePath}`);
|
|
469
|
+
}
|
|
470
|
+
return resolved;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function resolveManagedPatchPath(projectRoot, patchPath) {
|
|
474
|
+
const errors = [];
|
|
475
|
+
const normalized = resolvePatchPath(projectRoot, patchPath, errors);
|
|
476
|
+
if (errors.length > 0) {
|
|
477
|
+
throw new Error(errors.map(error => `${error.code}: ${error.message}`).join('\n'));
|
|
478
|
+
}
|
|
479
|
+
return normalized;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function loadPatchIntentConfig(projectRoot) {
|
|
483
|
+
const { loadRuntimeGraphConfig } = require('./core/runtime-graph');
|
|
484
|
+
const loaded = loadRuntimeGraphConfig({ projectRoot });
|
|
485
|
+
const section = Object.hasOwn(loaded.config, 'patchIntent') ? loaded.config.patchIntent : {};
|
|
486
|
+
const errors = [...loaded.errors];
|
|
487
|
+
|
|
488
|
+
if (!section || typeof section !== 'object' || Array.isArray(section)) {
|
|
489
|
+
errors.push({
|
|
490
|
+
code: 'PATCH_INTENT_CONFIG_INVALID',
|
|
491
|
+
message: 'patchIntent must be an object.',
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const config = section && typeof section === 'object' && !Array.isArray(section) ? section : {};
|
|
496
|
+
const enabled = Object.hasOwn(config, 'enabled') ? config.enabled : true;
|
|
497
|
+
if (typeof enabled !== 'boolean') {
|
|
498
|
+
errors.push({
|
|
499
|
+
code: 'PATCH_INTENT_ENABLED_INVALID',
|
|
500
|
+
message: 'patchIntent.enabled must be a boolean.',
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const patchPath = resolvePatchPath(projectRoot, config.path, errors);
|
|
505
|
+
const anchorAliases = config.anchorAliases && typeof config.anchorAliases === 'object' && !Array.isArray(config.anchorAliases)
|
|
506
|
+
? Object.fromEntries(Object.entries(config.anchorAliases).map(([from, to]) => [String(from), String(to)]))
|
|
507
|
+
: {};
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
enabled: enabled !== false,
|
|
511
|
+
path: patchPath,
|
|
512
|
+
anchorAliases,
|
|
513
|
+
errors,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function assertUsableConfig(config) {
|
|
518
|
+
if (config.errors.length > 0) {
|
|
519
|
+
throw new Error(config.errors.map(error => `${error.code}: ${error.message}`).join('\n'));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function recordBlock(record) {
|
|
524
|
+
const metadata = {
|
|
525
|
+
id: record.id,
|
|
526
|
+
anchorId: record.anchorId,
|
|
527
|
+
path: record.path,
|
|
528
|
+
createdAt: record.createdAt,
|
|
529
|
+
source: record.source || PATCH_SOURCE,
|
|
530
|
+
status: record.status || 'active',
|
|
531
|
+
};
|
|
532
|
+
if (record.anchorLine) metadata.anchorLine = record.anchorLine;
|
|
533
|
+
if (record.baseAnchorHash) metadata.baseAnchorHash = record.baseAnchorHash;
|
|
534
|
+
|
|
535
|
+
return [
|
|
536
|
+
RECORD_START,
|
|
537
|
+
YAML.stringify(metadata).trimEnd(),
|
|
538
|
+
'-->',
|
|
539
|
+
'```diff',
|
|
540
|
+
stripSingleTrailingNewline(record.diff),
|
|
541
|
+
'```',
|
|
542
|
+
RECORD_END,
|
|
543
|
+
].join('\n');
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function parsePatchIntentMarkdown(content) {
|
|
547
|
+
const normalizedContent = String(content || '').replace(/\r\n?/g, '\n');
|
|
548
|
+
const records = [];
|
|
549
|
+
let cursor = 0;
|
|
550
|
+
while (cursor < normalizedContent.length) {
|
|
551
|
+
const start = normalizedContent.indexOf(RECORD_START, cursor);
|
|
552
|
+
if (start < 0) break;
|
|
553
|
+
const metadataEnd = normalizedContent.indexOf('\n-->', start + RECORD_START.length);
|
|
554
|
+
const diffStartFence = metadataEnd < 0 ? -1 : normalizedContent.indexOf(PATCH_DIFF_FENCE, metadataEnd);
|
|
555
|
+
const diffStart = diffStartFence < 0 ? -1 : diffStartFence + PATCH_DIFF_FENCE.length;
|
|
556
|
+
const diffEnd = diffStart < 0 ? -1 : normalizedContent.indexOf(PATCH_FENCE_END, diffStart);
|
|
557
|
+
const end = diffEnd < 0 ? -1 : normalizedContent.indexOf(RECORD_END, diffEnd + PATCH_FENCE_END.length);
|
|
558
|
+
if (metadataEnd < 0 || diffStart < 0 || diffEnd < 0 || end < 0) {
|
|
559
|
+
throw new Error(`Malformed patch intent record block near offset ${start}.`);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const metadata = YAML.parse(normalizedContent.slice(start + RECORD_START.length, metadataEnd).trim()) || {};
|
|
563
|
+
assertPatchIntentRecordMetadata(metadata, `Malformed patch intent record block near offset ${start}`);
|
|
564
|
+
records.push({
|
|
565
|
+
...metadata,
|
|
566
|
+
id: String(metadata.id),
|
|
567
|
+
anchorId: String(metadata.anchorId || ''),
|
|
568
|
+
path: toPosixPath(metadata.path || ''),
|
|
569
|
+
diff: ensureTrailingNewline(normalizedContent.slice(diffStart, diffEnd)),
|
|
570
|
+
});
|
|
571
|
+
cursor = end + RECORD_END.length;
|
|
572
|
+
}
|
|
573
|
+
return records;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function assertPatchIntentRecordMetadata(record, context = 'Patch intent record') {
|
|
577
|
+
const missing = ['id', 'anchorId', 'path'].filter(field => {
|
|
578
|
+
return typeof record[field] !== 'string' || record[field].trim() === '';
|
|
579
|
+
});
|
|
580
|
+
if (missing.length > 0) {
|
|
581
|
+
throw new Error(`${context} missing required metadata: ${missing.join(', ')}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function loadPatchIntentRecords(projectRoot, options = {}) {
|
|
586
|
+
const config = options.config || loadPatchIntentConfig(projectRoot);
|
|
587
|
+
const patchPath = resolveManagedPatchPath(projectRoot, options.patchPath || config.path);
|
|
588
|
+
const fullPath = path.join(projectRoot, patchPath);
|
|
589
|
+
if (!fs.existsSync(fullPath)) {
|
|
590
|
+
return { path: patchPath, records: [] };
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
path: patchPath,
|
|
594
|
+
records: parsePatchIntentMarkdown(fs.readFileSync(fullPath, 'utf8')),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function writePatchIntentRecords(projectRoot, records, options = {}) {
|
|
599
|
+
const config = options.config || loadPatchIntentConfig(projectRoot);
|
|
600
|
+
const patchPath = resolveManagedPatchPath(projectRoot, options.patchPath || config.path);
|
|
601
|
+
const fullPath = path.join(projectRoot, patchPath);
|
|
602
|
+
const existing = loadPatchIntentRecords(projectRoot, { config, patchPath }).records;
|
|
603
|
+
for (const record of records) {
|
|
604
|
+
assertPatchIntentRecordMetadata(record);
|
|
605
|
+
}
|
|
606
|
+
const byId = new Map(existing.map(record => [record.id, record]));
|
|
607
|
+
for (const record of records) {
|
|
608
|
+
byId.set(record.id, record);
|
|
609
|
+
}
|
|
610
|
+
const ordered = [...byId.values()].sort((left, right) => {
|
|
611
|
+
return `${left.anchorId}:${left.id}`.localeCompare(`${right.anchorId}:${right.id}`);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
615
|
+
fs.writeFileSync(fullPath, [
|
|
616
|
+
'# Forge Patch Intent',
|
|
617
|
+
'',
|
|
618
|
+
'<!-- Managed by forge patch record. Edit with care; records are keyed by stable anchors. -->',
|
|
619
|
+
'',
|
|
620
|
+
...ordered.map(recordBlock),
|
|
621
|
+
'',
|
|
622
|
+
].join('\n'), 'utf8');
|
|
623
|
+
|
|
624
|
+
return { path: patchPath, records: ordered };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function readGitDiff(projectRoot, excludedPatchPath = DEFAULT_PATCH_PATH) {
|
|
628
|
+
const excluded = new Set([DEFAULT_PATCH_PATH, toPosixPath(excludedPatchPath)]);
|
|
629
|
+
const pathspecArgs = [
|
|
630
|
+
'--',
|
|
631
|
+
'.',
|
|
632
|
+
...[...excluded].filter(Boolean).map(patchPath => `:(exclude)${patchPath}`),
|
|
633
|
+
];
|
|
634
|
+
const args = [
|
|
635
|
+
'diff',
|
|
636
|
+
'--no-ext-diff',
|
|
637
|
+
'--unified=3',
|
|
638
|
+
'HEAD',
|
|
639
|
+
...pathspecArgs,
|
|
640
|
+
];
|
|
641
|
+
|
|
642
|
+
if (hasGitHead(projectRoot)) return execGit(projectRoot, args);
|
|
643
|
+
return readGitDiffWithoutHead(projectRoot, pathspecArgs);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function hasGitHead(projectRoot) {
|
|
647
|
+
try {
|
|
648
|
+
execGit(projectRoot, ['rev-parse', '--verify', 'HEAD'], { stdio: 'ignore' });
|
|
649
|
+
return true;
|
|
650
|
+
} catch {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function readGitDiffWithoutHead(projectRoot, pathspecArgs) {
|
|
656
|
+
const cachedDiff = execGit(projectRoot, ['diff', '--cached', '--no-ext-diff', '--unified=3', ...pathspecArgs]);
|
|
657
|
+
const unstagedDiff = execGit(projectRoot, ['diff', '--no-ext-diff', '--unified=3', ...pathspecArgs]);
|
|
658
|
+
const duplicatePaths = pathsInBothDiffs(cachedDiff, unstagedDiff);
|
|
659
|
+
if (duplicatePaths.size === 0) return [cachedDiff, unstagedDiff].filter(Boolean).join('\n');
|
|
660
|
+
|
|
661
|
+
const duplicateExcludes = [...duplicatePaths].map(relativePath => `:(exclude)${relativePath}`);
|
|
662
|
+
const filteredPathspecArgs = [...pathspecArgs, ...duplicateExcludes];
|
|
663
|
+
return [
|
|
664
|
+
execGit(projectRoot, ['diff', '--cached', '--no-ext-diff', '--unified=3', ...filteredPathspecArgs]),
|
|
665
|
+
execGit(projectRoot, ['diff', '--no-ext-diff', '--unified=3', ...filteredPathspecArgs]),
|
|
666
|
+
...[...duplicatePaths].map(relativePath => addedFileDiffFromWorkingTree(projectRoot, relativePath)),
|
|
667
|
+
].filter(Boolean).join('\n');
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function pathsInBothDiffs(leftDiff, rightDiff) {
|
|
671
|
+
const leftPaths = new Set(parseUnifiedDiff(leftDiff).map(hunk => hunk.path));
|
|
672
|
+
return new Set(parseUnifiedDiff(rightDiff)
|
|
673
|
+
.map(hunk => hunk.path)
|
|
674
|
+
.filter(relativePath => leftPaths.has(relativePath)));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function addedFileDiffFromWorkingTree(projectRoot, relativePath) {
|
|
678
|
+
const fullPath = resolveProjectFilePath(projectRoot, relativePath);
|
|
679
|
+
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) return '';
|
|
680
|
+
const content = fs.readFileSync(fullPath, 'utf8').replace(/\r\n?/g, '\n');
|
|
681
|
+
const lines = content === '' ? [] : content.replace(/\n$/, '').split('\n');
|
|
682
|
+
return [
|
|
683
|
+
`diff --git a/${relativePath} b/${relativePath}`,
|
|
684
|
+
'new file mode 100644',
|
|
685
|
+
'--- /dev/null',
|
|
686
|
+
`+++ b/${relativePath}`,
|
|
687
|
+
`@@ -0,0 +1,${lines.length} @@`,
|
|
688
|
+
...lines.map(line => `+${line}`),
|
|
689
|
+
'',
|
|
690
|
+
].join('\n');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function execGit(projectRoot, args, options = {}) {
|
|
694
|
+
return execFileSync('git', args, {
|
|
695
|
+
cwd: projectRoot,
|
|
696
|
+
encoding: 'utf8',
|
|
697
|
+
...options,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function createRecordsFromDiff(projectRoot, diff, options = {}) {
|
|
702
|
+
const hunks = parseUnifiedDiff(diff);
|
|
703
|
+
const createdAt = (options.now || new Date()).toISOString();
|
|
704
|
+
const records = [];
|
|
705
|
+
|
|
706
|
+
for (const hunk of hunks) {
|
|
707
|
+
const fullPath = resolveProjectFilePath(projectRoot, hunk.path);
|
|
708
|
+
if (!fs.existsSync(fullPath)) {
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
712
|
+
const anchor = resolveAnchorForHunk(content, hunk);
|
|
713
|
+
if (!anchor) {
|
|
714
|
+
throw new Error(`Patch target '${hunk.path}' has no declared forge anchor before diff hunk. Add '<!-- forge-anchor:<id> -->' near the managed block.`);
|
|
715
|
+
}
|
|
716
|
+
assertSingleAnchorHunk(content, hunk, anchor);
|
|
717
|
+
records.push({
|
|
718
|
+
id: patchId(anchor.id, hunk.diff),
|
|
719
|
+
anchorId: anchor.id,
|
|
720
|
+
anchorLine: anchor.line,
|
|
721
|
+
path: hunk.path,
|
|
722
|
+
createdAt,
|
|
723
|
+
source: PATCH_SOURCE,
|
|
724
|
+
status: 'active',
|
|
725
|
+
baseAnchorHash: `sha256:${hashText(anchor.id).slice(0, 16)}`,
|
|
726
|
+
diff: hunk.diff,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return records;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function resolveAnchorForHunk(content, hunk) {
|
|
734
|
+
return anchorInDiffHunk(hunk.diff, hunk.path)
|
|
735
|
+
|| nearestAnchorBefore(content, hunk.newStart, hunk.path)
|
|
736
|
+
|| firstAnchorInAddedFileHunk(content, hunk);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function firstAnchorInAddedFileHunk(content, hunk) {
|
|
740
|
+
if (!isAddedFileHunk(hunk.diff)) return null;
|
|
741
|
+
return collectAnchorsInLineRange(content, hunk.newStart, hunk.newEnd, hunk.path)[0] || null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function isAddedFileHunk(diff) {
|
|
745
|
+
return diff.split(/\r?\n/).includes('--- /dev/null');
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function assertSingleAnchorHunk(content, hunk, anchor) {
|
|
749
|
+
const anchors = [
|
|
750
|
+
...collectAnchorsInLineRange(content, hunk.newStart, hunk.newEnd, hunk.path),
|
|
751
|
+
...collectRemovedAnchorsInHunkDiff(hunk.diff, hunk.path),
|
|
752
|
+
];
|
|
753
|
+
const anchorIds = new Set([anchor.id, ...anchors.map(found => found.id)]);
|
|
754
|
+
if (anchorIds.size <= 1) return;
|
|
755
|
+
throw new Error(`Patch target '${hunk.path}' diff hunk crosses multiple forge anchors (${[...anchorIds].join(', ')}). Split the change into separate hunks before recording patch intent.`);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function collectRemovedAnchorsInHunkDiff(diff, relativePath) {
|
|
759
|
+
const anchors = [];
|
|
760
|
+
let openFence = null;
|
|
761
|
+
const shouldTrackFences = isMarkdownFile(relativePath);
|
|
762
|
+
|
|
763
|
+
for (const { content, removedLine } of iterateOldDiffContentLines(diff)) {
|
|
764
|
+
const fence = shouldTrackFences ? parseMarkdownFence(content) : null;
|
|
765
|
+
if (fence) {
|
|
766
|
+
openFence = nextMarkdownFenceState(openFence, fence);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if (openFence || !removedLine) continue;
|
|
770
|
+
if (shouldTrackFences && isMarkdownIndentedCodeLine(content)) continue;
|
|
771
|
+
const id = parseAnchorLine(content);
|
|
772
|
+
if (id) anchors.push({ id, line: null });
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return anchors;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function* iterateOldDiffContentLines(diff) {
|
|
779
|
+
let inHunk = false;
|
|
780
|
+
for (const line of diff.split(/\r?\n/)) {
|
|
781
|
+
if (line.startsWith('@@ ')) {
|
|
782
|
+
inHunk = true;
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
if (!inHunk) continue;
|
|
786
|
+
if (line.startsWith('diff --git ')) return;
|
|
787
|
+
if (line.startsWith('-')) {
|
|
788
|
+
yield { content: line.slice(1), removedLine: true };
|
|
789
|
+
} else if (line.startsWith(' ')) {
|
|
790
|
+
yield { content: line.slice(1), removedLine: false };
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function collectAnchorsInLineRange(content, startLine, endLine, relativePath) {
|
|
796
|
+
if (endLine < startLine) return [];
|
|
797
|
+
const lines = content.split(/\r?\n/);
|
|
798
|
+
const start = Math.max(1, startLine);
|
|
799
|
+
const end = Math.min(endLine, lines.length);
|
|
800
|
+
const anchors = [];
|
|
801
|
+
let openFence = null;
|
|
802
|
+
const shouldTrackFences = isMarkdownFile(relativePath);
|
|
803
|
+
|
|
804
|
+
for (let index = 0; index < end; index += 1) {
|
|
805
|
+
const line = lines[index];
|
|
806
|
+
const fence = shouldTrackFences ? parseMarkdownFence(line) : null;
|
|
807
|
+
if (fence) {
|
|
808
|
+
openFence = nextMarkdownFenceState(openFence, fence);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (openFence || index + 1 < start) continue;
|
|
812
|
+
if (shouldTrackFences && isMarkdownIndentedCodeLine(line)) continue;
|
|
813
|
+
const id = parseAnchorLine(line);
|
|
814
|
+
if (id) anchors.push({ id, line: index + 1 });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return anchors;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function recordPatchIntentFromDiff(projectRoot, options = {}) {
|
|
821
|
+
const config = loadPatchIntentConfig(projectRoot);
|
|
822
|
+
assertUsableConfig(config);
|
|
823
|
+
if (!config.enabled) {
|
|
824
|
+
throw new Error('Patch intent recording is disabled by .forge/config.yaml.');
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const diff = options.diff ?? readGitDiff(projectRoot, config.path);
|
|
828
|
+
const records = createRecordsFromDiff(projectRoot, diff, options);
|
|
829
|
+
if (records.length === 0) {
|
|
830
|
+
return { path: config.path, records: [] };
|
|
831
|
+
}
|
|
832
|
+
return writePatchIntentRecords(projectRoot, records, { config });
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function resolvePatchIntentRecords(projectRoot, options = {}) {
|
|
836
|
+
const config = options.config || loadPatchIntentConfig(projectRoot);
|
|
837
|
+
assertUsableConfig(config);
|
|
838
|
+
const loaded = loadPatchIntentRecords(projectRoot, { config });
|
|
839
|
+
const anchors = discoverAnchors(projectRoot, { excludedPatchPath: config.path });
|
|
840
|
+
const records = [];
|
|
841
|
+
const orphans = [];
|
|
842
|
+
|
|
843
|
+
for (const record of loaded.records) {
|
|
844
|
+
const resolvedAnchorId = config.anchorAliases[record.anchorId] || record.anchorId;
|
|
845
|
+
const anchor = anchors.get(resolvedAnchorId);
|
|
846
|
+
if (!anchor) {
|
|
847
|
+
const orphan = {
|
|
848
|
+
...record,
|
|
849
|
+
resolvedAnchorId,
|
|
850
|
+
status: 'orphaned',
|
|
851
|
+
};
|
|
852
|
+
records.push(orphan);
|
|
853
|
+
orphans.push(orphan);
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const status = anchor.path === record.path ? 'active' : 'renamed';
|
|
858
|
+
records.push({
|
|
859
|
+
...record,
|
|
860
|
+
resolvedAnchorId,
|
|
861
|
+
currentPath: anchor.path,
|
|
862
|
+
currentLine: anchor.line,
|
|
863
|
+
status,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return {
|
|
868
|
+
path: loaded.path,
|
|
869
|
+
records,
|
|
870
|
+
orphans,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function buildUnifiedDiffFromRecords(records) {
|
|
875
|
+
return records.map(record => record.diff.trimEnd()).join('\n') + '\n';
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
module.exports = {
|
|
879
|
+
DEFAULT_PATCH_PATH,
|
|
880
|
+
buildUnifiedDiffFromRecords,
|
|
881
|
+
createRecordsFromDiff,
|
|
882
|
+
discoverAnchors,
|
|
883
|
+
loadPatchIntentConfig,
|
|
884
|
+
loadPatchIntentRecords,
|
|
885
|
+
parsePatchIntentMarkdown,
|
|
886
|
+
parseUnifiedDiff,
|
|
887
|
+
recordPatchIntentFromDiff,
|
|
888
|
+
resolvePatchIntentRecords,
|
|
889
|
+
writePatchIntentRecords,
|
|
890
|
+
};
|