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.
Files changed (479) hide show
  1. package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
  2. package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
  3. package/.cursor/rules/permissions-guidance.mdc +2 -2
  4. package/.forge/hooks/check-tdd.js +3 -0
  5. package/.forge/hooks/forge-native-hook.js +245 -0
  6. package/.forge/protected-paths.yaml +157 -0
  7. package/AGENTS.md +151 -61
  8. package/CHANGELOG.md +681 -0
  9. package/CLAUDE.md +9 -106
  10. package/QUICKSTART.md +171 -0
  11. package/README.md +271 -363
  12. package/bin/forge-cmd.js +120 -9
  13. package/bin/forge-preflight.js +26 -5
  14. package/bin/forge.js +466 -489
  15. package/docs/INDEX.md +93 -0
  16. package/docs/PROJECT_DESIGN.md +685 -0
  17. package/docs/architecture/index.md +66 -0
  18. package/docs/architecture/notes/README.md +35 -0
  19. package/docs/architecture/subsystems/README.md +46 -0
  20. package/docs/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
  21. package/docs/forge/VALIDATION.md +82 -0
  22. package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
  23. package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
  24. package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
  25. package/docs/guides/GREPTILE_SETUP.md +46 -0
  26. package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
  27. package/docs/guides/MIGRATION.md +56 -0
  28. package/docs/guides/SETUP.md +118 -0
  29. package/docs/guides/SUPPORT.md +185 -0
  30. package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
  31. package/docs/guides/memory-backends.md +183 -0
  32. package/docs/reference/ADAPTERS.md +128 -0
  33. package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
  34. package/docs/reference/COMMANDS.md +205 -0
  35. package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
  36. package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
  37. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
  38. package/docs/reference/HERMES_INTEGRATION.md +118 -0
  39. package/docs/reference/INSIGHTS_RECAP.md +63 -0
  40. package/docs/reference/INSTALL.md +164 -0
  41. package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
  42. package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
  43. package/docs/reference/RELEASE.md +68 -0
  44. package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
  45. package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
  46. package/docs/reference/SKILLS.md +35 -0
  47. package/docs/reference/STATUS_BOARD.md +80 -0
  48. package/docs/reference/TEMPLATES.md +106 -0
  49. package/docs/reference/TOOLCHAIN.md +658 -0
  50. package/docs/reference/VALIDATION.md +82 -0
  51. package/docs/reference/agent-permissions.md +169 -0
  52. package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
  53. package/docs/reference/control-plane-guarantees.md +125 -0
  54. package/docs/reference/dependency-chain.md +331 -0
  55. package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
  56. package/docs/reference/forge-kernel-schema.md +72 -0
  57. package/docs/reference/kernel-conflict-evaluators.md +27 -0
  58. package/docs/reference/patch-md-format.md +77 -0
  59. package/docs/reference/protected-state-surfaces.md +59 -0
  60. package/docs/reference/shepherd.md +115 -0
  61. package/docs/reference/superpowers-analysis.md +320 -0
  62. package/docs/reference/superpowers-integration-options.md +404 -0
  63. package/docs/reference/test-environment.md +519 -0
  64. package/docs/reference/upgrade-safety.md +59 -0
  65. package/lefthook.yml +18 -0
  66. package/lib/adapter-cli.js +307 -0
  67. package/lib/adapters/beads-issue-adapter.js +127 -0
  68. package/lib/adapters/beads-kernel-compat.js +1042 -0
  69. package/lib/adapters/greptile-review-adapter.js +141 -0
  70. package/lib/adapters/kernel-issue-adapter.js +101 -0
  71. package/lib/adapters/pr-state-adapter.js +484 -0
  72. package/lib/adoption-profiles.js +126 -0
  73. package/lib/agents/README.md +2 -6
  74. package/lib/agents/claude.plugin.json +3 -8
  75. package/lib/agents/codex.plugin.json +9 -1
  76. package/lib/agents/cursor.plugin.json +2 -6
  77. package/lib/agents/hermes.plugin.json +22 -0
  78. package/lib/agents-config.js +39 -1236
  79. package/lib/audit-evidence.js +282 -0
  80. package/lib/beads-setup.js +225 -28
  81. package/lib/beads-sync-scaffold.js +36 -107
  82. package/lib/codex-skills.js +51 -1
  83. package/lib/commands/_issue.js +744 -70
  84. package/lib/commands/_manifest.js +91 -0
  85. package/lib/commands/_registry.js +85 -34
  86. package/lib/commands/_resolve-command-opts.js +261 -0
  87. package/lib/commands/_serve-security.js +270 -0
  88. package/lib/commands/adapter.js +12 -0
  89. package/lib/commands/add.js +118 -0
  90. package/lib/commands/audit.js +70 -0
  91. package/lib/commands/blocked.js +5 -0
  92. package/lib/commands/board.js +64 -0
  93. package/lib/commands/claim.js +21 -2
  94. package/lib/commands/claims.js +7 -0
  95. package/lib/commands/clean.js +485 -75
  96. package/lib/commands/close.js +2 -2
  97. package/lib/commands/comment.js +5 -0
  98. package/lib/commands/control.js +148 -0
  99. package/lib/commands/create.js +2 -2
  100. package/lib/commands/dev.js +185 -7
  101. package/lib/commands/doc-gate.js +336 -0
  102. package/lib/commands/doctor.js +156 -0
  103. package/lib/commands/explain.js +15 -0
  104. package/lib/commands/export.js +237 -0
  105. package/lib/commands/gate.js +192 -0
  106. package/lib/commands/hooks.js +242 -0
  107. package/lib/commands/inbox.js +118 -0
  108. package/lib/commands/init.js +598 -0
  109. package/lib/commands/insights.js +79 -0
  110. package/lib/commands/issue.js +12 -1
  111. package/lib/commands/issues.js +66 -0
  112. package/lib/commands/lint.js +5 -0
  113. package/lib/commands/list.js +2 -2
  114. package/lib/commands/merge.js +312 -0
  115. package/lib/commands/migrate.js +523 -0
  116. package/lib/commands/new.js +12 -0
  117. package/lib/commands/options.js +241 -0
  118. package/lib/commands/orient.js +13 -0
  119. package/lib/commands/orphans.js +5 -0
  120. package/lib/commands/patch.js +67 -0
  121. package/lib/commands/plan.js +436 -24
  122. package/lib/commands/preflight.js +211 -0
  123. package/lib/commands/prime.js +13 -0
  124. package/lib/commands/push.js +69 -2
  125. package/lib/commands/ready.js +2 -2
  126. package/lib/commands/recall.js +116 -0
  127. package/lib/commands/recap.js +61 -0
  128. package/lib/commands/recommend.js +22 -2
  129. package/lib/commands/release.js +91 -0
  130. package/lib/commands/remember.js +74 -0
  131. package/lib/commands/role.js +99 -0
  132. package/lib/commands/serve.js +581 -0
  133. package/lib/commands/setup.js +851 -979
  134. package/lib/commands/shepherd.js +436 -0
  135. package/lib/commands/ship.js +23 -1
  136. package/lib/commands/show.js +2 -2
  137. package/lib/commands/stage.js +192 -0
  138. package/lib/commands/stale.js +5 -0
  139. package/lib/commands/status.js +329 -11
  140. package/lib/commands/sync.js +34 -46
  141. package/lib/commands/team.js +15 -2
  142. package/lib/commands/test.js +58 -7
  143. package/lib/commands/update.js +2 -2
  144. package/lib/commands/upgrade.js +47 -0
  145. package/lib/commands/validate.js +56 -25
  146. package/lib/commands/worktree.js +308 -128
  147. package/lib/config-writer.js +202 -0
  148. package/lib/control-plane.js +236 -0
  149. package/lib/core/runtime-graph.js +946 -0
  150. package/lib/dep-guard/keyword-ripple.js +184 -0
  151. package/lib/deprecated-sync-cleanup.js +362 -0
  152. package/lib/detect-agent.js +2 -28
  153. package/lib/detect-worktree.js +42 -17
  154. package/lib/doc-gate/declaration.js +177 -0
  155. package/lib/doc-gate/detect.js +289 -0
  156. package/lib/doc-gate/gate.js +375 -0
  157. package/lib/doc-gate/okf-config.js +128 -0
  158. package/lib/doc-gate/okf.js +429 -0
  159. package/lib/docs-command.js +1161 -6
  160. package/lib/forge-issues.js +697 -0
  161. package/lib/forge-lock.js +262 -0
  162. package/lib/gate-events.js +193 -0
  163. package/lib/global-flags.js +74 -0
  164. package/lib/greptile-match.js +7 -63
  165. package/lib/harness-capability-matrix.js +380 -0
  166. package/lib/hook-global-installer.js +347 -0
  167. package/lib/hook-renderer.js +451 -0
  168. package/lib/inbox.js +391 -0
  169. package/lib/insights.js +397 -0
  170. package/lib/issue-adapter.js +156 -0
  171. package/lib/issue-backend.js +145 -0
  172. package/lib/issue-render.js +220 -0
  173. package/lib/issue-sync/authority.js +100 -0
  174. package/lib/issue-sync/github-pull.js +184 -0
  175. package/lib/issue-sync/import-primitives.js +98 -0
  176. package/lib/issue-sync/legacy-link-bridge.js +436 -0
  177. package/lib/issue-sync/link-store.js +292 -0
  178. package/lib/issue-sync/project-github.js +123 -0
  179. package/lib/issue-sync/reconcile.js +195 -0
  180. package/lib/issue-sync/schema.js +126 -0
  181. package/lib/kernel/backing-issue.js +305 -0
  182. package/lib/kernel/broker.js +1218 -0
  183. package/lib/kernel/cli-broker-factory.js +130 -0
  184. package/lib/kernel/conflict-signal.js +82 -0
  185. package/lib/kernel/evaluators.js +195 -0
  186. package/lib/kernel/fs-class.js +495 -0
  187. package/lib/kernel/issue-command-contract.js +559 -0
  188. package/lib/kernel/issue-id-resolver.js +186 -0
  189. package/lib/kernel/lease-enforcer.js +158 -0
  190. package/lib/kernel/migrations.js +333 -0
  191. package/lib/kernel/planning-buckets-schema.js +109 -0
  192. package/lib/kernel/projection-jsonl-writer.js +450 -0
  193. package/lib/kernel/readiness-model.js +329 -0
  194. package/lib/kernel/schema.js +356 -0
  195. package/lib/kernel/sqlite-driver.js +2504 -0
  196. package/lib/kernel/taxonomy-validator.js +394 -0
  197. package/lib/lefthook-check.js +8 -4
  198. package/lib/lefthook-wiring.js +413 -0
  199. package/lib/mcp-config-renderer.js +288 -0
  200. package/lib/memory/graphiti-mcp.js +106 -0
  201. package/lib/memory/router.js +387 -0
  202. package/lib/memory/typed-api.js +102 -0
  203. package/lib/memory-digest.js +195 -0
  204. package/lib/merge-rules.js +395 -0
  205. package/lib/migrate-dry-run.js +466 -0
  206. package/lib/orientation.js +863 -0
  207. package/lib/package-manager-remediation.js +103 -0
  208. package/lib/package-root.js +381 -0
  209. package/lib/patch-intent.js +890 -0
  210. package/lib/plugin-catalog.js +3 -4
  211. package/lib/plugin-manager.js +0 -5
  212. package/lib/pr-bundle.js +186 -0
  213. package/lib/pr-monitor/differ.js +195 -0
  214. package/lib/pr-monitor/events.js +0 -0
  215. package/lib/pr-monitor/gather.js +124 -0
  216. package/lib/pr-monitor/journal.js +299 -0
  217. package/lib/pr-monitor/monitor.js +146 -0
  218. package/lib/pr-monitor/render-sticky.js +157 -0
  219. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  220. package/lib/pr-monitor/watch.js +247 -0
  221. package/lib/pr-pull.js +1273 -0
  222. package/lib/pr-shepherd.js +494 -0
  223. package/lib/pr-state-validator.js +59 -0
  224. package/lib/preflight/gates.js +237 -0
  225. package/lib/preflight/runner.js +116 -0
  226. package/lib/project-discovery.js +0 -53
  227. package/lib/project-memory.js +166 -0
  228. package/lib/protected-path-manifest.js +281 -0
  229. package/lib/protected-state-surfaces.js +387 -0
  230. package/lib/release-readiness.js +2089 -0
  231. package/lib/reset.js +59 -45
  232. package/lib/review-adapter.js +68 -0
  233. package/lib/rules-sync.js +260 -0
  234. package/lib/runtime-health.js +332 -23
  235. package/lib/safety-config-renderer.js +268 -0
  236. package/lib/setup-action-log.js +1 -7
  237. package/lib/setup.js +27 -65
  238. package/lib/shell-utils.js +76 -6
  239. package/lib/skills-sync.js +330 -0
  240. package/lib/smart-status/conflicts.js +205 -0
  241. package/lib/smart-status/scoring.js +191 -0
  242. package/lib/status/beads-snapshot.js +145 -0
  243. package/lib/status/presenter.js +216 -0
  244. package/lib/status/snapshot.js +186 -0
  245. package/lib/sync-backend.js +202 -0
  246. package/lib/untrusted-content.js +52 -0
  247. package/lib/upgrade-safety.js +199 -0
  248. package/lib/workflow/enforce-stage.js +298 -47
  249. package/lib/workflow/stage-transition.js +115 -0
  250. package/lib/workflow/stages.js +30 -6
  251. package/lib/workflow/state-manager.js +159 -14
  252. package/lib/workflow/state.js +23 -1
  253. package/lib/workflow-profiles.js +17 -5
  254. package/package.json +46 -36
  255. package/rules/documentation.md +19 -0
  256. package/rules/kernel-tracking.md +26 -0
  257. package/rules/security.md +22 -0
  258. package/rules/tdd.md +20 -0
  259. package/rules/workflow.md +27 -0
  260. package/scripts/auto-backing-issue.js +47 -0
  261. package/scripts/beads-context.sh +165 -22
  262. package/scripts/beads-migrate-to-dolt.sh +7 -0
  263. package/scripts/beads-upgrade-smoke.sh +284 -0
  264. package/scripts/behavioral-judge.sh +115 -11
  265. package/scripts/benchmark.js +349 -63
  266. package/scripts/bootstrap-windows-tools.sh +78 -0
  267. package/scripts/branch-protection.js +2 -3
  268. package/scripts/check-agents.js +34 -137
  269. package/scripts/commitlint.js +3 -1
  270. package/scripts/conflict-detect.sh +3 -0
  271. package/scripts/dep-guard-analyze.js +52 -17
  272. package/scripts/dep-guard-keyword-ripple.js +29 -0
  273. package/scripts/dep-guard-render-review.js +86 -0
  274. package/scripts/dep-guard.sh +64 -232
  275. package/scripts/file-index.sh +3 -0
  276. package/scripts/forge-team/lib/claim.sh +34 -18
  277. package/scripts/forge-team/lib/dashboard.sh +61 -86
  278. package/scripts/forge-team/lib/epic.sh +99 -263
  279. package/scripts/forge-team/lib/hooks.sh +26 -28
  280. package/scripts/forge-team/lib/identity.sh +4 -4
  281. package/scripts/forge-team/lib/sync-github.sh +144 -47
  282. package/scripts/forge-team/lib/verify.sh +93 -83
  283. package/scripts/forge-team/lib/workload.sh +41 -65
  284. package/scripts/forge-team/tests/claim.test.sh +25 -19
  285. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  286. package/scripts/forge-team/tests/epic.test.sh +52 -71
  287. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  288. package/scripts/forge-team/tests/identity.test.sh +3 -3
  289. package/scripts/forge-team/tests/integration.test.sh +44 -66
  290. package/scripts/forge-team/tests/sync-github.test.sh +183 -79
  291. package/scripts/forge-team/tests/verify.test.sh +37 -46
  292. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  293. package/scripts/forge-team/tests/workload.test.sh +32 -66
  294. package/scripts/gen-command-manifest.js +153 -0
  295. package/scripts/gen-embedded-assets.mjs +129 -0
  296. package/scripts/install.ps1 +139 -0
  297. package/scripts/install.sh +268 -0
  298. package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
  299. package/scripts/lib/release-asset.mjs +84 -0
  300. package/scripts/parity-check.mjs +145 -0
  301. package/scripts/parity-check.test.mjs +58 -0
  302. package/scripts/pin-agentic-workflow-images.js +112 -0
  303. package/scripts/pr-coordinator.sh +3 -0
  304. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  305. package/scripts/preflight.sh +108 -0
  306. package/scripts/protected-state-check.js +104 -0
  307. package/scripts/smart-status-score.js +31 -0
  308. package/scripts/smart-status-sessions.js +51 -0
  309. package/scripts/smart-status.sh +117 -369
  310. package/scripts/spikes/config-race-bench.js +111 -0
  311. package/scripts/spikes/harness-capability-matrix.js +13 -0
  312. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  313. package/scripts/spikes/protected-path-manifest.js +20 -0
  314. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  315. package/scripts/sync-agent-skills.js +62 -0
  316. package/scripts/sync-agentic-workflow.js +48 -0
  317. package/scripts/sync-utils.sh +3 -0
  318. package/scripts/test-ci-shard.js +251 -0
  319. package/scripts/test-dashboard.js +188 -52
  320. package/scripts/test-full-suite.js +186 -0
  321. package/scripts/test-profile.js +278 -0
  322. package/scripts/test.js +302 -28
  323. package/scripts/validate.js +143 -0
  324. package/scripts/validate.sh +18 -1
  325. package/skills/claim-safety/SKILL.md +102 -0
  326. package/skills/claim-safety/evals/evals.json +46 -0
  327. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +46 -52
  328. package/skills/dev/evals/evals.json +50 -0
  329. package/skills/hermes-forge/SKILL.md +185 -0
  330. package/skills/hermes-forge/evals/evals.json +46 -0
  331. package/skills/issue-basics/SKILL.md +111 -0
  332. package/skills/issue-basics/evals/evals.json +46 -0
  333. package/skills/kernel/SKILL.md +166 -0
  334. package/skills/kernel/evals/evals.json +50 -0
  335. package/skills/memory/SKILL.md +102 -0
  336. package/skills/parallel-deep-research/SKILL.md +14 -11
  337. package/skills/parallel-deep-research/evals/evals.json +11 -27
  338. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +134 -159
  339. package/skills/plan/evals/evals.json +42 -0
  340. package/skills/research/SKILL.md +195 -0
  341. package/skills/research/evals/evals.json +42 -0
  342. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  343. package/skills/review/evals/evals.json +42 -0
  344. package/skills/rollback/SKILL.md +110 -0
  345. package/skills/rollback/evals/evals.json +46 -0
  346. package/skills/rollback/references/methods.md +204 -0
  347. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  348. package/skills/shepherd/SKILL.md +66 -0
  349. package/skills/shepherd/evals/evals.json +42 -0
  350. package/skills/ship/SKILL.md +251 -0
  351. package/skills/ship/evals/evals.json +42 -0
  352. package/skills/smith/SKILL.md +142 -0
  353. package/skills/smith/evals/evals.json +46 -0
  354. package/skills/smith/references/autonomy-and-gates.md +94 -0
  355. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  356. package/skills/sonarcloud/evals/evals.json +46 -0
  357. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  358. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  359. package/skills/status/SKILL.md +102 -0
  360. package/skills/status/evals/evals.json +50 -0
  361. package/skills/triage-ready/SKILL.md +121 -0
  362. package/skills/triage-ready/evals/evals.json +42 -0
  363. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  364. package/skills/validate/evals/evals.json +42 -0
  365. package/skills/verify/SKILL.md +299 -0
  366. package/skills/verify/evals/evals.json +50 -0
  367. package/.claude/commands/dev.md +0 -345
  368. package/.claude/commands/plan.md +0 -566
  369. package/.claude/commands/premerge.md +0 -186
  370. package/.claude/commands/research.md +0 -42
  371. package/.claude/commands/review.md +0 -451
  372. package/.claude/commands/rollback.md +0 -721
  373. package/.claude/commands/ship.md +0 -213
  374. package/.claude/commands/sonarcloud.md +0 -152
  375. package/.claude/commands/status.md +0 -90
  376. package/.claude/commands/validate.md +0 -288
  377. package/.claude/commands/verify.md +0 -269
  378. package/.claude/rules/workflow.md +0 -121
  379. package/.cline/workflows/dev.md +0 -342
  380. package/.cline/workflows/plan.md +0 -563
  381. package/.cline/workflows/premerge.md +0 -183
  382. package/.cline/workflows/research.md +0 -39
  383. package/.cline/workflows/review.md +0 -448
  384. package/.cline/workflows/rollback.md +0 -718
  385. package/.cline/workflows/ship.md +0 -210
  386. package/.cline/workflows/sonarcloud.md +0 -146
  387. package/.cline/workflows/status.md +0 -87
  388. package/.cline/workflows/validate.md +0 -285
  389. package/.cline/workflows/verify.md +0 -266
  390. package/.codex/config.toml +0 -11
  391. package/.codex/skills/dev/SKILL.md +0 -345
  392. package/.codex/skills/plan/SKILL.md +0 -566
  393. package/.codex/skills/premerge/SKILL.md +0 -186
  394. package/.codex/skills/research/SKILL.md +0 -42
  395. package/.codex/skills/review/SKILL.md +0 -451
  396. package/.codex/skills/rollback/SKILL.md +0 -721
  397. package/.codex/skills/ship/SKILL.md +0 -213
  398. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  399. package/.codex/skills/status/SKILL.md +0 -90
  400. package/.codex/skills/validate/SKILL.md +0 -288
  401. package/.codex/skills/verify/SKILL.md +0 -269
  402. package/.cursor/commands/dev.md +0 -342
  403. package/.cursor/commands/plan.md +0 -563
  404. package/.cursor/commands/premerge.md +0 -183
  405. package/.cursor/commands/research.md +0 -39
  406. package/.cursor/commands/review.md +0 -448
  407. package/.cursor/commands/ship.md +0 -210
  408. package/.cursor/commands/sonarcloud.md +0 -146
  409. package/.cursor/commands/status.md +0 -87
  410. package/.cursor/commands/validate.md +0 -285
  411. package/.cursor/commands/verify.md +0 -266
  412. package/.cursorrules +0 -149
  413. package/.github/prompts/premerge.prompt.md +0 -188
  414. package/.github/prompts/research.prompt.md +0 -44
  415. package/.github/prompts/rollback.prompt.md +0 -723
  416. package/.github/prompts/ship.prompt.md +0 -215
  417. package/.github/prompts/status.prompt.md +0 -92
  418. package/.github/prompts/verify.prompt.md +0 -271
  419. package/.github/workflows/beads-to-github.yml +0 -56
  420. package/.github/workflows/github-to-beads.yml +0 -97
  421. package/.kilocode/workflows/dev.md +0 -346
  422. package/.kilocode/workflows/plan.md +0 -567
  423. package/.kilocode/workflows/premerge.md +0 -187
  424. package/.kilocode/workflows/research.md +0 -43
  425. package/.kilocode/workflows/review.md +0 -452
  426. package/.kilocode/workflows/rollback.md +0 -722
  427. package/.kilocode/workflows/ship.md +0 -214
  428. package/.kilocode/workflows/sonarcloud.md +0 -150
  429. package/.kilocode/workflows/status.md +0 -91
  430. package/.kilocode/workflows/validate.md +0 -289
  431. package/.kilocode/workflows/verify.md +0 -270
  432. package/.opencode/commands/dev.md +0 -345
  433. package/.opencode/commands/plan.md +0 -566
  434. package/.opencode/commands/premerge.md +0 -186
  435. package/.opencode/commands/research.md +0 -42
  436. package/.opencode/commands/review.md +0 -451
  437. package/.opencode/commands/rollback.md +0 -721
  438. package/.opencode/commands/ship.md +0 -213
  439. package/.opencode/commands/sonarcloud.md +0 -149
  440. package/.opencode/commands/status.md +0 -90
  441. package/.opencode/commands/validate.md +0 -288
  442. package/.opencode/commands/verify.md +0 -269
  443. package/.roo/commands/dev.md +0 -346
  444. package/.roo/commands/plan.md +0 -567
  445. package/.roo/commands/premerge.md +0 -187
  446. package/.roo/commands/research.md +0 -43
  447. package/.roo/commands/review.md +0 -452
  448. package/.roo/commands/rollback.md +0 -722
  449. package/.roo/commands/ship.md +0 -214
  450. package/.roo/commands/sonarcloud.md +0 -150
  451. package/.roo/commands/status.md +0 -91
  452. package/.roo/commands/validate.md +0 -289
  453. package/.roo/commands/verify.md +0 -270
  454. package/docs/BEADS_GITHUB_SYNC.md +0 -255
  455. package/docs/GREPTILE_SETUP.md +0 -400
  456. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  457. package/docs/SETUP.md +0 -663
  458. package/docs/VALIDATION.md +0 -363
  459. package/lib/agents/cline.plugin.json +0 -29
  460. package/lib/agents/copilot.plugin.json +0 -24
  461. package/lib/agents/kilocode.plugin.json +0 -22
  462. package/lib/agents/opencode.plugin.json +0 -23
  463. package/lib/agents/roo.plugin.json +0 -30
  464. package/lib/beads-health-check.js +0 -143
  465. package/lib/commands/commands-reset.js +0 -147
  466. package/opencode.json +0 -67
  467. package/scripts/beads-context.test.js +0 -567
  468. package/scripts/github-beads-sync/comment.mjs +0 -64
  469. package/scripts/github-beads-sync/config.mjs +0 -148
  470. package/scripts/github-beads-sync/github-api.mjs +0 -131
  471. package/scripts/github-beads-sync/index.mjs +0 -332
  472. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  473. package/scripts/github-beads-sync/mapping.mjs +0 -78
  474. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  475. package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
  476. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  477. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  478. package/scripts/github-beads-sync.config.json +0 -26
  479. 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
+ };