forge-workflow 0.0.10 → 0.1.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (468) 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 +82 -5
  5. package/.forge/hooks/forge-native-hook.js +431 -0
  6. package/.forge/protected-paths.yaml +157 -0
  7. package/AGENTS.md +151 -61
  8. package/CHANGELOG.md +709 -0
  9. package/CLAUDE.md +9 -118
  10. package/QUICKSTART.md +175 -0
  11. package/README.md +275 -365
  12. package/bin/forge-cmd.js +120 -9
  13. package/bin/forge-preflight.js +26 -5
  14. package/bin/forge.js +532 -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/forge/TOOLCHAIN.md +670 -0
  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 +121 -0
  29. package/docs/guides/SUPPORT.md +190 -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 +214 -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/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
  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 +155 -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/activation/ensure-forge-home.js +135 -0
  67. package/lib/adapter-cli.js +307 -0
  68. package/lib/adapters/beads-issue-adapter.js +127 -0
  69. package/lib/adapters/beads-kernel-compat.js +1109 -0
  70. package/lib/adapters/greptile-review-adapter.js +141 -0
  71. package/lib/adapters/kernel-issue-adapter.js +101 -0
  72. package/lib/adapters/pr-state-adapter.js +484 -0
  73. package/lib/adoption-profiles.js +139 -0
  74. package/lib/agents/README.md +2 -6
  75. package/lib/agents/claude.plugin.json +3 -8
  76. package/lib/agents/codex.plugin.json +9 -1
  77. package/lib/agents/cursor.plugin.json +2 -6
  78. package/lib/agents/hermes.plugin.json +22 -0
  79. package/lib/agents-config.js +39 -1236
  80. package/lib/audit-evidence.js +282 -0
  81. package/lib/beads-detect.js +60 -0
  82. package/lib/beads-nudge.js +91 -0
  83. package/lib/beads-setup.js +121 -0
  84. package/lib/beads-sync-scaffold.js +25 -101
  85. package/lib/codex-skills.js +51 -1
  86. package/lib/commands/_aliases.js +248 -0
  87. package/lib/commands/_issue.js +780 -77
  88. package/lib/commands/_manifest.js +93 -0
  89. package/lib/commands/_registry.js +99 -34
  90. package/lib/commands/_resolve-command-opts.js +230 -0
  91. package/lib/commands/_serve-security.js +270 -0
  92. package/lib/commands/adapter.js +12 -0
  93. package/lib/commands/add.js +118 -0
  94. package/lib/commands/audit.js +70 -0
  95. package/lib/commands/blocked.js +5 -0
  96. package/lib/commands/board.js +64 -0
  97. package/lib/commands/claim.js +21 -2
  98. package/lib/commands/claims.js +7 -0
  99. package/lib/commands/clean.js +485 -75
  100. package/lib/commands/close.js +2 -2
  101. package/lib/commands/comment.js +5 -0
  102. package/lib/commands/control.js +148 -0
  103. package/lib/commands/create.js +2 -2
  104. package/lib/commands/dev.js +185 -7
  105. package/lib/commands/doc-gate.js +336 -0
  106. package/lib/commands/doctor.js +156 -0
  107. package/lib/commands/explain.js +15 -0
  108. package/lib/commands/export.js +237 -0
  109. package/lib/commands/gate.js +209 -0
  110. package/lib/commands/hooks.js +377 -0
  111. package/lib/commands/inbox.js +118 -0
  112. package/lib/commands/init.js +604 -0
  113. package/lib/commands/insights.js +79 -0
  114. package/lib/commands/issue.js +12 -1
  115. package/lib/commands/issues.js +17 -0
  116. package/lib/commands/lint.js +5 -0
  117. package/lib/commands/list.js +2 -2
  118. package/lib/commands/memory.js +81 -0
  119. package/lib/commands/merge.js +312 -0
  120. package/lib/commands/migrate.js +362 -0
  121. package/lib/commands/new.js +12 -0
  122. package/lib/commands/options.js +241 -0
  123. package/lib/commands/orient.js +13 -0
  124. package/lib/commands/orphans.js +5 -0
  125. package/lib/commands/patch.js +67 -0
  126. package/lib/commands/plan.js +481 -29
  127. package/lib/commands/pr.js +88 -0
  128. package/lib/commands/preflight.js +211 -0
  129. package/lib/commands/prime.js +13 -0
  130. package/lib/commands/push.js +135 -2
  131. package/lib/commands/ready.js +2 -2
  132. package/lib/commands/recall.js +171 -0
  133. package/lib/commands/recap.js +75 -0
  134. package/lib/commands/recommend.js +0 -1
  135. package/lib/commands/release.js +104 -0
  136. package/lib/commands/remember.js +140 -0
  137. package/lib/commands/role.js +99 -0
  138. package/lib/commands/serve.js +581 -0
  139. package/lib/commands/setup.js +900 -971
  140. package/lib/commands/shepherd.js +501 -0
  141. package/lib/commands/ship.js +59 -1
  142. package/lib/commands/show.js +2 -2
  143. package/lib/commands/stage.js +192 -0
  144. package/lib/commands/stale.js +5 -0
  145. package/lib/commands/status.js +158 -21
  146. package/lib/commands/sync.js +34 -46
  147. package/lib/commands/team.js +4 -1
  148. package/lib/commands/test.js +43 -27
  149. package/lib/commands/update.js +2 -2
  150. package/lib/commands/upgrade.js +47 -0
  151. package/lib/commands/validate.js +43 -18
  152. package/lib/commands/worktree.js +362 -99
  153. package/lib/config-writer.js +202 -0
  154. package/lib/control-plane.js +236 -0
  155. package/lib/core/runtime-graph.js +977 -0
  156. package/lib/dep-guard/keyword-ripple.js +2 -2
  157. package/lib/deprecated-sync-cleanup.js +362 -0
  158. package/lib/detect-agent.js +2 -28
  159. package/lib/detect-worktree.js +35 -9
  160. package/lib/doc-gate/declaration.js +177 -0
  161. package/lib/doc-gate/detect.js +289 -0
  162. package/lib/doc-gate/gate.js +375 -0
  163. package/lib/doc-gate/okf-config.js +128 -0
  164. package/lib/doc-gate/okf.js +429 -0
  165. package/lib/docs-command.js +1161 -6
  166. package/lib/forge-issues.js +382 -11
  167. package/lib/forge-lock.js +262 -0
  168. package/lib/gate-events.js +192 -0
  169. package/lib/global-flags.js +104 -0
  170. package/lib/greptile-match.js +7 -63
  171. package/lib/grounding/context-events.js +230 -0
  172. package/lib/grounding/read-first.js +112 -0
  173. package/lib/harness-capability-matrix.js +380 -0
  174. package/lib/hook-global-installer.js +347 -0
  175. package/lib/hook-renderer.js +541 -0
  176. package/lib/inbox.js +391 -0
  177. package/lib/insights.js +397 -0
  178. package/lib/issue-adapter.js +156 -0
  179. package/lib/issue-backend.js +145 -0
  180. package/lib/issue-render.js +220 -0
  181. package/lib/kernel/backing-issue.js +311 -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/owned-kernel.js +43 -0
  192. package/lib/kernel/planning-buckets-schema.js +109 -0
  193. package/lib/kernel/projection-jsonl-writer.js +450 -0
  194. package/lib/kernel/readiness-model.js +329 -0
  195. package/lib/kernel/schema.js +356 -0
  196. package/lib/kernel/sqlite-driver.js +2540 -0
  197. package/lib/kernel/taxonomy-validator.js +394 -0
  198. package/lib/lefthook-check.js +3 -2
  199. package/lib/lefthook-wiring.js +413 -0
  200. package/lib/mcp-config-renderer.js +288 -0
  201. package/lib/memory/graphiti-mcp.js +106 -0
  202. package/lib/memory/router.js +387 -0
  203. package/lib/memory/typed-api.js +102 -0
  204. package/lib/memory-digest.js +195 -0
  205. package/lib/merge-rules.js +395 -0
  206. package/lib/migrate-dry-run.js +466 -0
  207. package/lib/orientation.js +863 -0
  208. package/lib/package-manager-remediation.js +103 -0
  209. package/lib/package-root.js +381 -0
  210. package/lib/patch-intent.js +890 -0
  211. package/lib/plugin-catalog.js +3 -4
  212. package/lib/plugin-manager.js +0 -5
  213. package/lib/pr-bundle.js +186 -0
  214. package/lib/pr-monitor/auto-actions.js +175 -0
  215. package/lib/pr-monitor/differ.js +195 -0
  216. package/lib/pr-monitor/digest.js +206 -0
  217. package/lib/pr-monitor/events.js +0 -0
  218. package/lib/pr-monitor/gather.js +124 -0
  219. package/lib/pr-monitor/journal.js +299 -0
  220. package/lib/pr-monitor/monitor.js +146 -0
  221. package/lib/pr-monitor/render-sticky.js +192 -0
  222. package/lib/pr-monitor/upsert-sticky.js +169 -0
  223. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  224. package/lib/pr-monitor/watch.js +247 -0
  225. package/lib/pr-pull.js +1314 -0
  226. package/lib/pr-shepherd.js +494 -0
  227. package/lib/pr-state-validator.js +59 -0
  228. package/lib/preflight/gates.js +237 -0
  229. package/lib/preflight/runner.js +116 -0
  230. package/lib/project-discovery.js +0 -53
  231. package/lib/project-memory.js +99 -497
  232. package/lib/protected-path-manifest.js +281 -0
  233. package/lib/protected-state-surfaces.js +387 -0
  234. package/lib/release-readiness.js +2105 -0
  235. package/lib/reset.js +59 -45
  236. package/lib/review-adapter.js +68 -0
  237. package/lib/rules-sync.js +260 -0
  238. package/lib/runtime-health.js +241 -20
  239. package/lib/safety-config-renderer.js +268 -0
  240. package/lib/setup-action-log.js +1 -7
  241. package/lib/setup.js +27 -65
  242. package/lib/shell-utils.js +76 -6
  243. package/lib/skills-sync.js +330 -0
  244. package/lib/smart-status/scoring.js +17 -3
  245. package/lib/status/beads-snapshot.js +45 -2
  246. package/lib/status/presenter.js +169 -18
  247. package/lib/status/snapshot.js +186 -0
  248. package/lib/sync-backend.js +202 -0
  249. package/lib/untrusted-content.js +52 -0
  250. package/lib/upgrade-safety.js +251 -0
  251. package/lib/workflow/enforce-stage.js +351 -45
  252. package/lib/workflow/stage-transition.js +115 -0
  253. package/lib/workflow/stages.js +30 -6
  254. package/lib/workflow/state-manager.js +11 -22
  255. package/lib/workflow/state.js +23 -1
  256. package/lib/workflow-profiles.js +17 -5
  257. package/package.json +37 -35
  258. package/rules/documentation.md +19 -0
  259. package/rules/kernel-tracking.md +26 -0
  260. package/rules/security.md +22 -0
  261. package/rules/tdd.md +20 -0
  262. package/rules/workflow.md +27 -0
  263. package/scripts/auto-backing-issue.js +47 -0
  264. package/scripts/beads-context.sh +81 -57
  265. package/scripts/beads-upgrade-smoke.sh +24 -3
  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.sh +22 -3
  272. package/scripts/file-index.sh +3 -0
  273. package/scripts/forge-team/lib/claim.sh +34 -18
  274. package/scripts/forge-team/lib/dashboard.sh +61 -86
  275. package/scripts/forge-team/lib/epic.sh +99 -263
  276. package/scripts/forge-team/lib/hooks.sh +26 -28
  277. package/scripts/forge-team/lib/identity.sh +4 -4
  278. package/scripts/forge-team/lib/sync-github.sh +49 -84
  279. package/scripts/forge-team/lib/verify.sh +93 -83
  280. package/scripts/forge-team/lib/workload.sh +41 -65
  281. package/scripts/forge-team/tests/claim.test.sh +25 -19
  282. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  283. package/scripts/forge-team/tests/epic.test.sh +52 -71
  284. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  285. package/scripts/forge-team/tests/identity.test.sh +3 -3
  286. package/scripts/forge-team/tests/integration.test.sh +44 -66
  287. package/scripts/forge-team/tests/sync-github.test.sh +50 -83
  288. package/scripts/forge-team/tests/verify.test.sh +37 -46
  289. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  290. package/scripts/forge-team/tests/workload.test.sh +32 -66
  291. package/scripts/gen-command-manifest.js +153 -0
  292. package/scripts/gen-embedded-assets.mjs +129 -0
  293. package/scripts/install.ps1 +139 -0
  294. package/scripts/install.sh +268 -0
  295. package/scripts/lib/release-asset.mjs +84 -0
  296. package/scripts/parity-check.mjs +145 -0
  297. package/scripts/parity-check.test.mjs +58 -0
  298. package/scripts/pin-agentic-workflow-images.js +112 -0
  299. package/scripts/pr-auto-actions.js +93 -0
  300. package/scripts/pr-coordinator.sh +3 -0
  301. package/scripts/pr-verdict-label.js +50 -0
  302. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  303. package/scripts/preflight.sh +21 -94
  304. package/scripts/protected-state-check.js +104 -0
  305. package/scripts/smart-status.sh +60 -57
  306. package/scripts/spikes/config-race-bench.js +111 -0
  307. package/scripts/spikes/harness-capability-matrix.js +13 -0
  308. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  309. package/scripts/spikes/protected-path-manifest.js +20 -0
  310. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  311. package/scripts/sync-agent-skills.js +62 -0
  312. package/scripts/sync-utils.sh +3 -0
  313. package/scripts/test-ci-shard.js +13 -6
  314. package/scripts/test.js +95 -12
  315. package/skills/claim-safety/SKILL.md +102 -0
  316. package/skills/claim-safety/evals/evals.json +46 -0
  317. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
  318. package/skills/dev/evals/evals.json +50 -0
  319. package/skills/hermes-forge/SKILL.md +185 -0
  320. package/skills/hermes-forge/evals/evals.json +46 -0
  321. package/skills/issue-basics/SKILL.md +111 -0
  322. package/skills/issue-basics/evals/evals.json +46 -0
  323. package/skills/kernel/SKILL.md +166 -0
  324. package/skills/kernel/evals/evals.json +50 -0
  325. package/skills/memory/SKILL.md +102 -0
  326. package/skills/parallel-deep-research/SKILL.md +14 -11
  327. package/skills/parallel-deep-research/evals/evals.json +11 -27
  328. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
  329. package/skills/plan/evals/evals.json +42 -0
  330. package/skills/research/SKILL.md +195 -0
  331. package/skills/research/evals/evals.json +42 -0
  332. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  333. package/skills/review/evals/evals.json +42 -0
  334. package/skills/rollback/SKILL.md +110 -0
  335. package/skills/rollback/evals/evals.json +46 -0
  336. package/skills/rollback/references/methods.md +204 -0
  337. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  338. package/skills/shepherd/SKILL.md +66 -0
  339. package/skills/shepherd/evals/evals.json +42 -0
  340. package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
  341. package/skills/ship/evals/evals.json +42 -0
  342. package/skills/smith/SKILL.md +142 -0
  343. package/skills/smith/evals/evals.json +46 -0
  344. package/skills/smith/references/autonomy-and-gates.md +94 -0
  345. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  346. package/skills/sonarcloud/evals/evals.json +46 -0
  347. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  348. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  349. package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
  350. package/skills/status/evals/evals.json +50 -0
  351. package/skills/triage-ready/SKILL.md +121 -0
  352. package/skills/triage-ready/evals/evals.json +42 -0
  353. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  354. package/skills/validate/evals/evals.json +42 -0
  355. package/skills/verify/SKILL.md +299 -0
  356. package/skills/verify/evals/evals.json +50 -0
  357. package/.claude/commands/dev.md +0 -345
  358. package/.claude/commands/plan.md +0 -566
  359. package/.claude/commands/premerge.md +0 -186
  360. package/.claude/commands/research.md +0 -42
  361. package/.claude/commands/review.md +0 -451
  362. package/.claude/commands/rollback.md +0 -721
  363. package/.claude/commands/ship.md +0 -213
  364. package/.claude/commands/sonarcloud.md +0 -152
  365. package/.claude/commands/status.md +0 -90
  366. package/.claude/commands/validate.md +0 -288
  367. package/.claude/commands/verify.md +0 -269
  368. package/.claude/rules/workflow.md +0 -121
  369. package/.cline/workflows/dev.md +0 -342
  370. package/.cline/workflows/plan.md +0 -563
  371. package/.cline/workflows/premerge.md +0 -183
  372. package/.cline/workflows/research.md +0 -39
  373. package/.cline/workflows/review.md +0 -448
  374. package/.cline/workflows/rollback.md +0 -718
  375. package/.cline/workflows/ship.md +0 -210
  376. package/.cline/workflows/sonarcloud.md +0 -146
  377. package/.cline/workflows/status.md +0 -87
  378. package/.cline/workflows/validate.md +0 -285
  379. package/.cline/workflows/verify.md +0 -266
  380. package/.codex/config.toml +0 -11
  381. package/.codex/skills/dev/SKILL.md +0 -345
  382. package/.codex/skills/plan/SKILL.md +0 -566
  383. package/.codex/skills/premerge/SKILL.md +0 -186
  384. package/.codex/skills/research/SKILL.md +0 -42
  385. package/.codex/skills/review/SKILL.md +0 -451
  386. package/.codex/skills/rollback/SKILL.md +0 -721
  387. package/.codex/skills/ship/SKILL.md +0 -213
  388. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  389. package/.codex/skills/status/SKILL.md +0 -90
  390. package/.codex/skills/validate/SKILL.md +0 -288
  391. package/.codex/skills/verify/SKILL.md +0 -269
  392. package/.cursor/commands/dev.md +0 -342
  393. package/.cursor/commands/plan.md +0 -563
  394. package/.cursor/commands/premerge.md +0 -183
  395. package/.cursor/commands/research.md +0 -39
  396. package/.cursor/commands/review.md +0 -448
  397. package/.cursor/commands/ship.md +0 -210
  398. package/.cursor/commands/sonarcloud.md +0 -146
  399. package/.cursor/commands/status.md +0 -87
  400. package/.cursor/commands/validate.md +0 -285
  401. package/.cursor/commands/verify.md +0 -266
  402. package/.cursorrules +0 -149
  403. package/.github/prompts/premerge.prompt.md +0 -188
  404. package/.github/prompts/research.prompt.md +0 -44
  405. package/.github/prompts/rollback.prompt.md +0 -723
  406. package/.github/prompts/verify.prompt.md +0 -271
  407. package/.github/workflows/beads-to-github.yml +0 -89
  408. package/.github/workflows/github-to-beads.yml +0 -100
  409. package/.kilocode/workflows/dev.md +0 -346
  410. package/.kilocode/workflows/plan.md +0 -567
  411. package/.kilocode/workflows/premerge.md +0 -187
  412. package/.kilocode/workflows/research.md +0 -43
  413. package/.kilocode/workflows/review.md +0 -452
  414. package/.kilocode/workflows/rollback.md +0 -722
  415. package/.kilocode/workflows/ship.md +0 -214
  416. package/.kilocode/workflows/sonarcloud.md +0 -150
  417. package/.kilocode/workflows/status.md +0 -91
  418. package/.kilocode/workflows/validate.md +0 -289
  419. package/.kilocode/workflows/verify.md +0 -270
  420. package/.opencode/commands/dev.md +0 -345
  421. package/.opencode/commands/plan.md +0 -566
  422. package/.opencode/commands/premerge.md +0 -186
  423. package/.opencode/commands/research.md +0 -42
  424. package/.opencode/commands/review.md +0 -451
  425. package/.opencode/commands/rollback.md +0 -721
  426. package/.opencode/commands/ship.md +0 -213
  427. package/.opencode/commands/sonarcloud.md +0 -149
  428. package/.opencode/commands/status.md +0 -90
  429. package/.opencode/commands/validate.md +0 -288
  430. package/.opencode/commands/verify.md +0 -269
  431. package/.roo/commands/dev.md +0 -346
  432. package/.roo/commands/plan.md +0 -567
  433. package/.roo/commands/premerge.md +0 -187
  434. package/.roo/commands/research.md +0 -43
  435. package/.roo/commands/review.md +0 -452
  436. package/.roo/commands/rollback.md +0 -722
  437. package/.roo/commands/ship.md +0 -214
  438. package/.roo/commands/sonarcloud.md +0 -150
  439. package/.roo/commands/status.md +0 -91
  440. package/.roo/commands/validate.md +0 -289
  441. package/.roo/commands/verify.md +0 -270
  442. package/docs/BEADS_GITHUB_SYNC.md +0 -281
  443. package/docs/GREPTILE_SETUP.md +0 -400
  444. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  445. package/docs/SETUP.md +0 -663
  446. package/docs/VALIDATION.md +0 -363
  447. package/lib/agents/cline.plugin.json +0 -29
  448. package/lib/agents/copilot.plugin.json +0 -24
  449. package/lib/agents/kilocode.plugin.json +0 -22
  450. package/lib/agents/opencode.plugin.json +0 -23
  451. package/lib/agents/roo.plugin.json +0 -30
  452. package/lib/beads-bootstrap.js +0 -225
  453. package/lib/beads-health-check.js +0 -188
  454. package/lib/commands/commands-reset.js +0 -147
  455. package/opencode.json +0 -67
  456. package/scripts/beads-context.test.js +0 -584
  457. package/scripts/github-beads-sync/comment.mjs +0 -64
  458. package/scripts/github-beads-sync/config.mjs +0 -148
  459. package/scripts/github-beads-sync/github-api.mjs +0 -131
  460. package/scripts/github-beads-sync/index.mjs +0 -356
  461. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  462. package/scripts/github-beads-sync/mapping.mjs +0 -132
  463. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  464. package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
  465. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  466. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  467. package/scripts/github-beads-sync.config.json +0 -26
  468. package/scripts/sync-commands.js +0 -600
@@ -0,0 +1,206 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PR-shepherd digest — the thin CONSUMER half of the constant monitor (epic
5
+ * c2d398e5, 33e1bbd3). The constant watch loop is the PRODUCER: it writes the
6
+ * per-PR NDJSON journals under `.forge/pr-monitor/<repo>-<pr>/` (the `forge
7
+ * shepherd events` pull surface reads those same records back). Nothing, though,
8
+ * surfaced those events to a working agent. This module is a pure READER: it
9
+ * reads the NEW budget events across all PR journals since a persisted per-PR
10
+ * CONSUMER cursor, renders a COMPACT capped summary, and advances the cursor —
11
+ * the exact payload a harness hook (Claude UserPromptSubmit) injects each turn.
12
+ *
13
+ * The CORE (events/journal/watch/monitor) is untouched: this only READS the
14
+ * journal via `journal.readEventsSince` and keeps its OWN `consumer.cursor`
15
+ * (distinct from the watcher's snapshot), so consumption never disturbs
16
+ * production. Every function is fail-open — a bad journal degrades to skipped,
17
+ * never throws — because it feeds a hook that must never block a prompt.
18
+ *
19
+ * @module pr-monitor/digest
20
+ */
21
+
22
+ const fs = require('node:fs');
23
+ const path = require('node:path');
24
+
25
+ const journalMod = require('./journal');
26
+
27
+ /**
28
+ * The event types worth surfacing on each turn — the ACTIONABLE transitions
29
+ * (verdict flip, a failed check, a new review thread, terminal merge/close).
30
+ * Everything else (head pushes, green checks, degraded notices) stays in the
31
+ * journal for `forge shepherd events` but is NOT pushed, to keep the injected
32
+ * context tiny.
33
+ */
34
+ const BUDGET_TYPES = Object.freeze(new Set([
35
+ 'verdict.changed', 'check.failed', 'thread.opened', 'pr.merged', 'pr.closed',
36
+ ]));
37
+
38
+ const DEFAULT_CAP = 8;
39
+ const CONSUMER_CURSOR_FILE = 'consumer.cursor';
40
+
41
+ /** Absolute `.forge/pr-monitor` root for a project. */
42
+ function monitorRoot(root) {
43
+ return path.join(root, '.forge', 'pr-monitor');
44
+ }
45
+
46
+ /** The per-PR consumer cursor path (distinct from the watcher's snapshot/pid). */
47
+ function cursorPath(dir) {
48
+ return path.join(dir, CONSUMER_CURSOR_FILE);
49
+ }
50
+
51
+ /**
52
+ * List absolute PR journal dirs (those containing an events.ndjson). Fail-open:
53
+ * a missing monitor root or unreadable dir yields []. Injectable fs for tests.
54
+ *
55
+ * @param {string} root
56
+ * @param {{ readdirSync?: Function, existsSync?: Function }} [deps]
57
+ * @returns {string[]}
58
+ */
59
+ function discoverPrDirs(root, deps = {}) {
60
+ const readdir = deps.readdirSync || fs.readdirSync;
61
+ const exists = deps.existsSync || fs.existsSync;
62
+ try {
63
+ return readdir(monitorRoot(root), { withFileTypes: true })
64
+ .filter((e) => e.isDirectory())
65
+ .map((e) => path.join(monitorRoot(root), e.name))
66
+ .filter((dir) => exists(path.join(dir, 'events.ndjson')));
67
+ } catch {
68
+ return [];
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Read a PR's consumer cursor (the last consumed seq). Fail-open → 0 (no cursor,
74
+ * unreadable, or malformed all mean "start from the beginning").
75
+ *
76
+ * @param {string} dir
77
+ * @param {{ readFileSync?: Function }} [deps]
78
+ * @returns {number}
79
+ */
80
+ function readConsumerCursor(dir, deps = {}) {
81
+ const readFile = deps.readFileSync || fs.readFileSync;
82
+ try {
83
+ const obj = JSON.parse(readFile(cursorPath(dir), 'utf8'));
84
+ const seq = Number(obj && obj.seq);
85
+ return Number.isFinite(seq) && seq >= 0 ? seq : 0;
86
+ } catch {
87
+ return 0;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Persist a PR's consumer cursor. Fail-open: an unwritable cursor returns false
93
+ * (next turn re-reads the same events — a duplicate nudge, never a crash).
94
+ *
95
+ * @param {string} dir
96
+ * @param {number} seq
97
+ * @param {{ writeFileSync?: Function }} [deps]
98
+ * @returns {boolean}
99
+ */
100
+ function writeConsumerCursor(dir, seq, deps = {}) {
101
+ const writeFile = deps.writeFileSync || fs.writeFileSync;
102
+ try {
103
+ writeFile(cursorPath(dir), `${JSON.stringify({ seq: Number(seq) || 0 })}\n`);
104
+ return true;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * A compact one-line label for a budget event. PURE. Bounded so injected context
112
+ * stays tiny; the full record is always available via `forge shepherd events`.
113
+ *
114
+ * @param {object} e - a journal event.
115
+ * @returns {string}
116
+ */
117
+ function renderEventLine(e) {
118
+ const pr = e.pr != null ? `#${e.pr}` : '#?';
119
+ const d = e.data || {};
120
+ let detail;
121
+ switch (e.type) {
122
+ case 'verdict.changed': detail = (e.verdict && (e.verdict.verdict || e.verdict.state)) || d.verdict || d.to || ''; break;
123
+ case 'check.failed': detail = d.name || d.check || ''; break;
124
+ case 'thread.opened': detail = d.author ? `by ${d.author}` : (d.path || ''); break;
125
+ case 'pr.merged': detail = 'merged'; break;
126
+ case 'pr.closed': detail = 'closed'; break;
127
+ default: detail = '';
128
+ }
129
+ const suffix = detail ? `: ${String(detail).slice(0, 60)}` : '';
130
+ return `- PR ${pr} ${e.type}${suffix}`;
131
+ }
132
+
133
+ /**
134
+ * PURE: filter events to the budget types, cap the count, and render lines.
135
+ *
136
+ * @param {object[]} events
137
+ * @param {{ cap?: number }} [opts]
138
+ * @returns {{ lines: string[], total: number }}
139
+ */
140
+ function renderDigestLines(events, { cap = DEFAULT_CAP } = {}) {
141
+ const budget = (Array.isArray(events) ? events : []).filter((e) => e && BUDGET_TYPES.has(e.type));
142
+ const lines = budget.map(renderEventLine);
143
+ return { lines: lines.slice(0, cap), total: lines.length };
144
+ }
145
+
146
+ /**
147
+ * Format the compact injected block (a header + capped lines + an overflow
148
+ * pointer), or '' when there is nothing to surface. PURE.
149
+ */
150
+ function formatBlock(lines, total, cap, prs) {
151
+ if (lines.length === 0) return '';
152
+ const on = prs.length ? ` on PR(s) ${prs.join(', ')}` : '';
153
+ const more = total > cap ? `\n(+${total - cap} more — see \`forge shepherd events <pr> --since <seq>\`)` : '';
154
+ return `[forge PR shepherd] ${total} new event(s)${on}:\n${lines.join('\n')}${more}`;
155
+ }
156
+
157
+ /**
158
+ * Collect a compact digest of NEW budget events across every PR journal since the
159
+ * per-PR consumer cursor, advancing each cursor past EVERYTHING read (budget and
160
+ * non-budget alike, so skipped types never re-surface). Fail-open throughout.
161
+ *
162
+ * @param {object} args
163
+ * @param {string} args.root - project root.
164
+ * @param {object} [args.journal] - journal module (test injection).
165
+ * @param {number} [args.cap] - max lines in the block.
166
+ * @param {object} [args.fsDeps] - injectable fs for discovery + cursor I/O.
167
+ * @returns {{ text: string, total: number, prs: string[] }}
168
+ */
169
+ function collectDigest({ root, journal = journalMod, cap = DEFAULT_CAP, fsDeps = {} } = {}) {
170
+ const dirs = discoverPrDirs(root, fsDeps);
171
+ const allLines = [];
172
+ const prs = new Set();
173
+ for (const dir of dirs) {
174
+ const cursor = readConsumerCursor(dir, fsDeps);
175
+ let evs;
176
+ try {
177
+ evs = journal.readEventsSince(dir, cursor);
178
+ } catch {
179
+ evs = [];
180
+ }
181
+ if (!Array.isArray(evs) || evs.length === 0) continue;
182
+ const maxSeq = evs.reduce((m, e) => Math.max(m, Number(e.seq) || 0), cursor);
183
+ for (const e of evs) {
184
+ if (!e || !BUDGET_TYPES.has(e.type)) continue;
185
+ allLines.push(renderEventLine(e));
186
+ if (e.pr != null) prs.add(String(e.pr));
187
+ }
188
+ writeConsumerCursor(dir, maxSeq, fsDeps);
189
+ }
190
+ const capped = allLines.slice(0, cap);
191
+ return { text: formatBlock(capped, allLines.length, cap, [...prs]), total: allLines.length, prs: [...prs] };
192
+ }
193
+
194
+ module.exports = {
195
+ BUDGET_TYPES,
196
+ DEFAULT_CAP,
197
+ monitorRoot,
198
+ cursorPath,
199
+ discoverPrDirs,
200
+ readConsumerCursor,
201
+ writeConsumerCursor,
202
+ renderEventLine,
203
+ renderDigestLines,
204
+ formatBlock,
205
+ collectDigest,
206
+ };
Binary file
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PR-monitor gather — turn ONE `gatherPrSnapshot` read (the SAME shared read the
5
+ * `--pull` verdict uses) into the normalized snapshot the differ compares. Because
6
+ * the verdict and the monitor events both derive from this one read, they can
7
+ * never disagree (the frame in docs/work/2026-07-13-pr-monitor/plan.md).
8
+ *
9
+ * The normalized snapshot is the diff subject:
10
+ * { repo, pr, headSha, prState, draft, verdict:{state,reason},
11
+ * checks:[{name,class}], threads:[{threadId,isResolved,isOutdated,commentCount,actionable,path}],
12
+ * reviews:[{author,state,commitOid,submittedAt}], comments:[{id,author}],
13
+ * behind, conflicts:(true|false|null), degraded:[{surface,error}] }
14
+ *
15
+ * @module pr-monitor/gather
16
+ */
17
+
18
+ const { gatherPrSnapshot } = require('../pr-pull');
19
+ const { isFailed, isGreen } = require('../pr-shepherd');
20
+
21
+ /** green | failed | pending — reuses the SAME predicates as the verdict core. */
22
+ function classifyCheck(check) {
23
+ if (isFailed(check)) return 'failed';
24
+ if (isGreen(check)) return 'green';
25
+ return 'pending';
26
+ }
27
+
28
+ function normalizeChecks(checks) {
29
+ return (Array.isArray(checks) ? checks : []).map((c) => ({
30
+ name: c.name || '',
31
+ class: classifyCheck(c),
32
+ }));
33
+ }
34
+
35
+ function normalizeThreads(threads) {
36
+ return (Array.isArray(threads) ? threads : []).map((t) => ({
37
+ threadId: t.threadId || '',
38
+ isResolved: Boolean(t.isResolved),
39
+ isOutdated: Boolean(t.isOutdated),
40
+ commentCount: Array.isArray(t.comments) ? t.comments.length : 0,
41
+ actionable: !t.isResolved && !t.isOutdated,
42
+ path: t.path || null,
43
+ }));
44
+ }
45
+
46
+ function normalizeReviews(reviews) {
47
+ return (Array.isArray(reviews) ? reviews : []).map((r) => ({
48
+ author: r.author || '',
49
+ state: r.state || '',
50
+ commitOid: r.commitOid || null,
51
+ submittedAt: r.submittedAt || null,
52
+ }));
53
+ }
54
+
55
+ function normalizeComments(comments) {
56
+ return (Array.isArray(comments) ? comments : []).map((c) => ({
57
+ id: c.id || `${c.author || ''}:${c.createdAt || ''}`,
58
+ author: c.author || '',
59
+ }));
60
+ }
61
+
62
+ function normalizeDegraded(degraded) {
63
+ return (Array.isArray(degraded) ? degraded : []).map((d) => ({
64
+ surface: d.source || d.surface || 'unknown',
65
+ error: d.error || '',
66
+ }));
67
+ }
68
+
69
+ /**
70
+ * Conflict prediction → tri-state boolean: `true`/`false` when supported,
71
+ * `null` when unknown (unsupported git, unreadable ref) so the differ never
72
+ * emits a false conflict.appeared/cleared on missing data.
73
+ */
74
+ function conflictBool(conflicts) {
75
+ if (conflicts?.supported === true) return Boolean(conflicts.conflicted);
76
+ return null;
77
+ }
78
+
79
+ /**
80
+ * Normalize a raw `gatherPrSnapshot` result into the monitor diff subject.
81
+ *
82
+ * @param {object} snap - gatherPrSnapshot result.
83
+ * @param {{ repo: string, pr: string|number }} ctx
84
+ * @returns {object}
85
+ */
86
+ function normalizeSnapshot(snap, ctx) {
87
+ const state = snap.state || {};
88
+ return {
89
+ repo: ctx.repo,
90
+ pr: String(ctx.pr),
91
+ headSha: state.headSha || '',
92
+ prState: String(state.state || 'OPEN').toUpperCase(),
93
+ draft: Boolean(snap.draft),
94
+ verdict: { state: snap.verdict || 'UNKNOWN', reason: null },
95
+ checks: normalizeChecks(state.checks),
96
+ threads: normalizeThreads(snap.threads),
97
+ reviews: normalizeReviews(snap.reviews),
98
+ comments: normalizeComments(snap.issueComments),
99
+ behind: snap.behind || 0,
100
+ conflicts: conflictBool(snap.conflicts),
101
+ degraded: normalizeDegraded(snap.degraded),
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Gather + normalize the monitor snapshot for a PR. `ctx.gatherSnapshot` is
107
+ * injectable for tests; production uses the shared `gatherPrSnapshot`.
108
+ *
109
+ * @param {object} ctx - the same ctx shape gatherPrSnapshot takes (pr, owner,
110
+ * repo, base, baseRef, cwd, self, adapter, now, settleWindowMs).
111
+ * @returns {Promise<object>} normalized snapshot.
112
+ */
113
+ async function gatherMonitorSnapshot(ctx) {
114
+ const gather = ctx.gatherSnapshot || gatherPrSnapshot;
115
+ const snap = await gather(ctx);
116
+ return normalizeSnapshot(snap, ctx);
117
+ }
118
+
119
+ module.exports = {
120
+ gatherMonitorSnapshot,
121
+ normalizeSnapshot,
122
+ classifyCheck,
123
+ conflictBool,
124
+ };
@@ -0,0 +1,299 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PR-monitor journal — per-PR append-only NDJSON event log plus an atomic
5
+ * snapshot fingerprint, under `.forge/pr-monitor/<repo>-<pr>/`. The journal is
6
+ * the CURSOR AUTHORITY: it survives crashes and is shared across processes and
7
+ * worktrees, and works with no kernel configured. A consumer keeps its own `seq`
8
+ * cursor and reads new events with `readEventsSince`.
9
+ *
10
+ * Ordering contract (exactly-once): a monitor pass APPENDS events, THEN writes
11
+ * the snapshot together with an `appliedSeq` cursor (the highest seq that
12
+ * snapshot accounts for). If a crash lands between the append and the snapshot
13
+ * write, the persisted `appliedSeq` still points BEFORE the just-appended tail,
14
+ * so the next pass re-diffs the old snapshot, recomputes the same `(type,key)`
15
+ * events, and `seenIdentities(dir, appliedSeq)` filters exactly that pending tail
16
+ * — no duplicate is ever journaled. Crucially the dedup is scoped to that
17
+ * pending/crash-recovery window, NOT the whole journal history: a value that
18
+ * flips back to a prior state (fail → green → fail on the same sha) legitimately
19
+ * re-emits, because the earlier identity sits at/below `appliedSeq`.
20
+ *
21
+ * Concurrency: multiple processes/worktrees can poll the same PR. The
22
+ * read→diff→dedup→append→snapshot critical section is serialized across
23
+ * processes by `withJournalLock` (a crash-recoverable lock-directory), so
24
+ * concurrent passes can never interleave appends or reuse a sequence number.
25
+ *
26
+ * @module pr-monitor/journal
27
+ */
28
+
29
+ const fs = require('node:fs');
30
+ const path = require('node:path');
31
+ const { eventIdentity } = require('./events');
32
+
33
+ /** Sanitize a repo slug for a filesystem directory name. */
34
+ function sanitize(part) {
35
+ return String(part || '').replace(/[^A-Za-z0-9._-]+/g, '-');
36
+ }
37
+
38
+ /**
39
+ * Resolve (and create) the per-PR journal directory.
40
+ *
41
+ * @param {{ root: string, repo: string, pr: string|number }} ctx
42
+ * @returns {string} absolute directory path
43
+ */
44
+ function journalDir({ root, repo, pr }) {
45
+ const dir = path.join(root, '.forge', 'pr-monitor', `${sanitize(repo)}-${sanitize(pr)}`);
46
+ fs.mkdirSync(dir, { recursive: true });
47
+ return dir;
48
+ }
49
+
50
+ function journalPath(dir) { return path.join(dir, 'events.ndjson'); }
51
+ function snapshotPath(dir) { return path.join(dir, 'snapshot.json'); }
52
+ function pidPath(dir) { return path.join(dir, 'watch.pid'); }
53
+ function lockPath(dir) { return path.join(dir, 'journal.lock'); }
54
+
55
+ /**
56
+ * Read all journal records (NDJSON), skipping any unparseable line so one
57
+ * corrupt tail line never blinds the cursor.
58
+ *
59
+ * @param {string} dir
60
+ * @returns {object[]}
61
+ */
62
+ function readAllEvents(dir) {
63
+ const file = journalPath(dir);
64
+ if (!fs.existsSync(file)) return [];
65
+ const out = [];
66
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
67
+ const trimmed = line.trim();
68
+ if (!trimmed) continue;
69
+ try { out.push(JSON.parse(trimmed)); } catch { /* skip corrupt line */ }
70
+ }
71
+ return out;
72
+ }
73
+
74
+ /** Records with `seq > sinceSeq`, in journal order. */
75
+ function readEventsSince(dir, sinceSeq) {
76
+ const since = Number(sinceSeq) || 0;
77
+ return readAllEvents(dir).filter((e) => Number(e.seq) > since);
78
+ }
79
+
80
+ /** Highest `seq` recorded so far (0 when empty). */
81
+ function lastSeq(dir) {
82
+ let max = 0;
83
+ for (const e of readAllEvents(dir)) {
84
+ const s = Number(e.seq) || 0;
85
+ if (s > max) max = s;
86
+ }
87
+ return max;
88
+ }
89
+
90
+ /**
91
+ * Set of `(type,key)` identities journaled with `seq > sinceSeq` — the dedup
92
+ * guard, SCOPED to the crash-recovery/pending window above the snapshot cursor.
93
+ *
94
+ * With `sinceSeq = 0` (no snapshot, or an explicit full scan) it covers the
95
+ * whole history, which is the correct behaviour during crash recovery when the
96
+ * snapshot is missing. With `sinceSeq = appliedSeq` it covers only the events
97
+ * the current snapshot has NOT yet accounted for, so an identity that recurs
98
+ * after a full round-trip (e.g. fail → green → fail) is NOT suppressed forever.
99
+ *
100
+ * @param {string} dir
101
+ * @param {number} [sinceSeq=0]
102
+ * @returns {Set<string>}
103
+ */
104
+ function seenIdentities(dir, sinceSeq = 0) {
105
+ const since = Number(sinceSeq) || 0;
106
+ const set = new Set();
107
+ for (const e of readAllEvents(dir)) {
108
+ if ((Number(e.seq) || 0) > since) set.add(eventIdentity(e));
109
+ }
110
+ return set;
111
+ }
112
+
113
+ /** Append finalized records as NDJSON lines (atomic per-line append). */
114
+ function appendEvents(dir, records) {
115
+ if (!records?.length) return;
116
+ const payload = records.map((r) => JSON.stringify(r)).join('\n') + '\n';
117
+ fs.appendFileSync(journalPath(dir), payload);
118
+ }
119
+
120
+ /**
121
+ * Read the persisted snapshot + fingerprint + `appliedSeq`, or null when
122
+ * absent/unreadable. `appliedSeq` is the highest journal seq this snapshot
123
+ * accounts for (0 for legacy snapshots written before the cursor existed); it
124
+ * bounds the dedup window in `seenIdentities`.
125
+ *
126
+ * @param {string} dir
127
+ * @returns {{ snapshot: object, fingerprint: string, appliedSeq: number }|null}
128
+ */
129
+ function readSnapshot(dir) {
130
+ const file = snapshotPath(dir);
131
+ if (!fs.existsSync(file)) return null;
132
+ try {
133
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
134
+ return {
135
+ snapshot: data.snapshot || null,
136
+ fingerprint: data.fingerprint || null,
137
+ appliedSeq: Number(data.appliedSeq) || 0,
138
+ };
139
+ } catch { return null; }
140
+ }
141
+
142
+ /**
143
+ * Atomically persist the snapshot + fingerprint + `appliedSeq` (write temp, then
144
+ * rename). The rename is atomic on the same filesystem, so a reader never sees a
145
+ * half-write.
146
+ *
147
+ * @param {string} dir
148
+ * @param {{ snapshot: object, fingerprint: string, appliedSeq?: number }} payload
149
+ */
150
+ function writeSnapshot(dir, { snapshot, fingerprint, appliedSeq = 0 }) {
151
+ const tmp = path.join(dir, `.snapshot.${process.pid}.${Date.now()}.tmp`);
152
+ fs.writeFileSync(tmp, JSON.stringify({ snapshot, fingerprint, appliedSeq: Number(appliedSeq) || 0 }));
153
+ fs.renameSync(tmp, snapshotPath(dir));
154
+ }
155
+
156
+ /** Non-blocking sleep for the lock retry loop. */
157
+ function delay(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); }
158
+
159
+ /**
160
+ * Is the held lock stale? A lock is stale when its owner process is dead, or
161
+ * when it has out-lived `staleMs` (guards against a reused pid or a crash that
162
+ * left no readable owner file). Either way it is safe to steal.
163
+ */
164
+ function lockIsStale(lock, staleMs) {
165
+ let ownerPid = null;
166
+ let stampedAt = null;
167
+ try {
168
+ const [pidStr, tsStr] = fs.readFileSync(path.join(lock, 'owner'), 'utf8').split(':');
169
+ ownerPid = Number.parseInt(pidStr, 10);
170
+ stampedAt = Number.parseInt(tsStr, 10);
171
+ } catch { /* owner file missing/unreadable → fall through to age check */ }
172
+ if (ownerPid && !pidAlive(ownerPid)) return true;
173
+ let ageBase = stampedAt;
174
+ if (!Number.isFinite(ageBase)) {
175
+ try { ageBase = fs.statSync(lock).mtimeMs; } catch { return true; }
176
+ }
177
+ return Date.now() - ageBase > staleMs;
178
+ }
179
+
180
+ /** Remove a lock directory (owner file + dir), best-effort. */
181
+ function releaseLock(lock) {
182
+ try { fs.rmSync(lock, { recursive: true, force: true }); } catch { /* already gone */ }
183
+ }
184
+
185
+ /**
186
+ * Run `fn` while holding the per-PR journal lock, serializing the
187
+ * read→diff→dedup→append→snapshot critical section across processes and
188
+ * worktrees. The lock is a directory created with an atomic `mkdir` (fails with
189
+ * EEXIST when held); a crash-recoverable staleness check lets a later caller
190
+ * steal a lock whose owner died or that out-lived `staleMs`. The lock is always
191
+ * released in a `finally`, on success or throw.
192
+ *
193
+ * A heartbeat refreshes the owner timestamp for the whole duration of the
194
+ * awaited `fn()`, at `staleMs / 3` intervals. Without this, `lockIsStale()`
195
+ * only looks at the ONE timestamp written at acquisition time — a live but
196
+ * slow pass (close to or past `staleMs`) would look stale to a competing
197
+ * caller purely on age, even though its owner PID is alive, and get its lock
198
+ * stolen mid-flight, letting two passes interleave journal appends and reuse a
199
+ * sequence number. The heartbeat is stopped in the existing `finally` cleanup
200
+ * alongside the lock release.
201
+ *
202
+ * @param {string} dir - journal directory.
203
+ * @param {() => Promise<T>|T} fn - critical section.
204
+ * @param {{ staleMs?: number, retries?: number, waitMs?: number, heartbeatMs?: number }} [opts]
205
+ * `heartbeatMs` defaults to `staleMs / 3` (floor 10ms) and is exposed mainly
206
+ * so tests can exercise the heartbeat without waiting out a full-size
207
+ * `staleMs` window.
208
+ * @returns {Promise<T>}
209
+ * @template T
210
+ */
211
+ async function withJournalLock(dir, fn, opts = {}) {
212
+ const { staleMs = 30000, retries = 600, waitMs = 25 } = opts;
213
+ const heartbeatMs = opts.heartbeatMs ?? Math.max(10, Math.floor(staleMs / 3));
214
+ const lock = lockPath(dir);
215
+ let acquired = false;
216
+ for (let i = 0; i < retries && !acquired; i += 1) {
217
+ try {
218
+ fs.mkdirSync(lock);
219
+ acquired = true;
220
+ } catch (err) {
221
+ if (err.code !== 'EEXIST') throw err;
222
+ if (lockIsStale(lock, staleMs)) { releaseLock(lock); continue; }
223
+ await delay(waitMs);
224
+ }
225
+ }
226
+ if (!acquired) throw new Error(`journal lock busy after ${retries} tries: ${lock}`);
227
+ const ownerFile = path.join(lock, 'owner');
228
+ const stampOwner = () => {
229
+ try { fs.writeFileSync(ownerFile, `${process.pid}:${Date.now()}`); } catch { /* lock released/stolen underneath us */ }
230
+ };
231
+ stampOwner();
232
+ const heartbeat = setInterval(stampOwner, heartbeatMs);
233
+ if (typeof heartbeat.unref === 'function') heartbeat.unref();
234
+ try {
235
+ return await fn();
236
+ } finally {
237
+ clearInterval(heartbeat);
238
+ releaseLock(lock);
239
+ }
240
+ }
241
+
242
+ /** Write the watcher pid file. */
243
+ function writePid(dir, pid) {
244
+ fs.writeFileSync(pidPath(dir), String(pid == null ? process.pid : pid));
245
+ }
246
+
247
+ /** Read the watcher pid (number) or null. */
248
+ function readPid(dir) {
249
+ const file = pidPath(dir);
250
+ if (!fs.existsSync(file)) return null;
251
+ const n = Number.parseInt(fs.readFileSync(file, 'utf8').trim(), 10);
252
+ return Number.isFinite(n) ? n : null;
253
+ }
254
+
255
+ /** Remove the watcher pid file (best-effort). */
256
+ function removePid(dir) {
257
+ try { fs.unlinkSync(pidPath(dir)); } catch { /* already gone */ }
258
+ }
259
+
260
+ /** Is `pid` a live process? `process.kill(pid, 0)` probes without signaling. */
261
+ function pidAlive(pid) {
262
+ if (!pid) return false;
263
+ try { process.kill(pid, 0); return true; } catch (err) { return err?.code === 'EPERM'; }
264
+ }
265
+
266
+ /**
267
+ * A watcher is "running" when the pid file names a live process that is NOT us.
268
+ * Stale pid files (dead process) return false so a poll falls back to an inline
269
+ * pass.
270
+ *
271
+ * @param {string} dir
272
+ * @returns {boolean}
273
+ */
274
+ function watcherRunning(dir) {
275
+ const pid = readPid(dir);
276
+ return pidAlive(pid) && pid !== process.pid;
277
+ }
278
+
279
+ module.exports = {
280
+ sanitize,
281
+ journalDir,
282
+ journalPath,
283
+ snapshotPath,
284
+ pidPath,
285
+ lockPath,
286
+ withJournalLock,
287
+ readAllEvents,
288
+ readEventsSince,
289
+ lastSeq,
290
+ seenIdentities,
291
+ appendEvents,
292
+ readSnapshot,
293
+ writeSnapshot,
294
+ writePid,
295
+ readPid,
296
+ removePid,
297
+ pidAlive,
298
+ watcherRunning,
299
+ };