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,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
+ };
@@ -0,0 +1,146 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PR-monitor orchestration — one bounded pass (gather → diff → dedup → append →
5
+ * persist) and the `events --since` poll surface. The watch streaming loop and
6
+ * the ship lifecycle hook land in a follow-up (Tier-1 PR-B).
7
+ *
8
+ * @module pr-monitor/monitor
9
+ */
10
+
11
+ const { finalizeEvent, eventIdentity, fingerprint } = require('./events');
12
+ const { diffSnapshots } = require('./differ');
13
+ const journal = require('./journal');
14
+
15
+ /** ISO-8601 timestamp; injectable via ctx.now for deterministic tests. */
16
+ function defaultNow() { return new Date().toISOString(); }
17
+
18
+ /**
19
+ * Filter candidate events down to those whose `(type,key)` identity has NOT
20
+ * already been journaled — the crash-safety dedup guard.
21
+ */
22
+ function dedupeAgainstJournal(candidates, seen) {
23
+ return candidates.filter((c) => !seen.has(eventIdentity(c)));
24
+ }
25
+
26
+ /**
27
+ * Envelope filtered candidates into journal records with monotonic seq + ts.
28
+ */
29
+ function finalizeRecords(candidates, { baseSeq, ts, snapshot }) {
30
+ return candidates.map((c, i) => finalizeEvent(c, {
31
+ seq: baseSeq + i + 1,
32
+ ts,
33
+ repo: snapshot.repo,
34
+ pr: snapshot.pr,
35
+ headSha: snapshot.headSha,
36
+ verdict: snapshot.verdict,
37
+ }));
38
+ }
39
+
40
+ /**
41
+ * The read→diff→dedup→sequence→append→snapshot critical section of a single
42
+ * pass. Runs ONLY while holding the journal lock (see `runMonitorPass`), so it
43
+ * may assume no concurrent writer.
44
+ *
45
+ * Dedup is SCOPED to the snapshot's `appliedSeq` cursor: only identities the
46
+ * current snapshot has not yet accounted for filter a candidate. When the
47
+ * snapshot is missing (crash recovery) `appliedSeq` is 0, so the guard falls
48
+ * back to the full history and no duplicate survives a crash between append and
49
+ * snapshot write. When the snapshot is current, the window is empty, so a state
50
+ * that flips back to a prior value (fail → green → fail on the same sha) emits a
51
+ * fresh event instead of being suppressed forever.
52
+ */
53
+ async function runMonitorPassLocked(ctx) {
54
+ const { dir, gather, now = defaultNow, enrich } = ctx;
55
+ const next = await gather();
56
+ const prevRecord = journal.readSnapshot(dir);
57
+ const prev = prevRecord ? prevRecord.snapshot : null;
58
+ const appliedSeq = prevRecord ? prevRecord.appliedSeq || 0 : 0;
59
+ const fp = fingerprint(next);
60
+
61
+ const candidates = diffSnapshots(prev, next);
62
+ const filtered = dedupeAgainstJournal(candidates, journal.seenIdentities(dir, appliedSeq));
63
+
64
+ if (!filtered.length) {
65
+ // Backpressure: only rewrite the snapshot when the fingerprint actually moved.
66
+ const changed = prevRecord?.fingerprint !== fp;
67
+ // Even with no new events, advance the cursor to the current tail so the
68
+ // dedup window stays anchored at the snapshot (prevents unbounded scans).
69
+ if (changed) {
70
+ journal.writeSnapshot(dir, { snapshot: next, fingerprint: fp, appliedSeq: journal.lastSeq(dir) });
71
+ }
72
+ return { events: [], changed, fingerprint: fp };
73
+ }
74
+
75
+ const baseSeq = journal.lastSeq(dir);
76
+ const records = finalizeRecords(filtered, {
77
+ baseSeq,
78
+ ts: now(),
79
+ snapshot: next,
80
+ });
81
+ if (typeof enrich === 'function') await enrich(records);
82
+
83
+ journal.appendEvents(dir, records);
84
+ // appliedSeq = the new tail, so the next pass's dedup window starts empty and
85
+ // only a crash BEFORE this write leaves the tail inside the recovery window.
86
+ journal.writeSnapshot(dir, { snapshot: next, fingerprint: fp, appliedSeq: baseSeq + records.length });
87
+ return { events: records, changed: true, fingerprint: fp };
88
+ }
89
+
90
+ /**
91
+ * Run ONE bounded monitor pass: gather the current snapshot, diff it against the
92
+ * persisted one, dedup by content identity, APPEND new events, THEN persist the
93
+ * snapshot (this order is what makes a crash between the two idempotent). The
94
+ * whole critical section — including the gather and the no-events snapshot
95
+ * update path — runs under a cross-process journal lock so concurrent passes
96
+ * from other processes/worktrees can never interleave appends or reuse a seq.
97
+ *
98
+ * @param {object} ctx
99
+ * @param {string} ctx.dir - journal directory (from journal.journalDir).
100
+ * @param {() => Promise<object>} ctx.gather - returns a normalized snapshot.
101
+ * @param {() => string} [ctx.now] - timestamp source (test injection).
102
+ * @param {(records: object[]) => Promise<void>|void} [ctx.enrich] - optional hook
103
+ * to enrich records (e.g. attach log excerpts to check.failed) before append.
104
+ * @param {object} [ctx.lockOpts] - override lock staleMs/retries/waitMs (tests).
105
+ * @returns {Promise<{ events: object[], changed: boolean, fingerprint: string }>}
106
+ */
107
+ async function runMonitorPass(ctx) {
108
+ return journal.withJournalLock(ctx.dir, () => runMonitorPassLocked(ctx), ctx.lockOpts);
109
+ }
110
+
111
+ /**
112
+ * `forge shepherd events <pr> --since <seq>` core: run one inline pass when no
113
+ * watcher owns this PR, then return every journaled event with `seq > since`.
114
+ * This is the agent-agnostic PULL surface — stdout NDJSON, nothing under .claude.
115
+ *
116
+ * @param {object} ctx
117
+ * @param {string} ctx.dir
118
+ * @param {() => Promise<object>} ctx.gather
119
+ * @param {number} [ctx.since]
120
+ * @param {() => string} [ctx.now]
121
+ * @param {(dir: string) => boolean} [ctx.watcherRunning]
122
+ * @param {(records: object[]) => Promise<void>|void} [ctx.enrich]
123
+ * @returns {Promise<{ events: object[], since: number, ranPass: boolean }>}
124
+ */
125
+ async function pollEvents(ctx) {
126
+ const { dir, gather, since = 0 } = ctx;
127
+ const isRunning = (ctx.watcherRunning || journal.watcherRunning)(dir);
128
+ let ranPass = false;
129
+ if (!isRunning) {
130
+ await runMonitorPass({ dir, gather, now: ctx.now, enrich: ctx.enrich });
131
+ ranPass = true;
132
+ }
133
+ return {
134
+ events: journal.readEventsSince(dir, since),
135
+ since: Number(since) || 0,
136
+ ranPass,
137
+ };
138
+ }
139
+
140
+ module.exports = {
141
+ runMonitorPass,
142
+ pollEvents,
143
+ dedupeAgainstJournal,
144
+ finalizeRecords,
145
+ defaultNow,
146
+ };
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PR-monitor sticky-comment renderer — turn ONE read-only `gatherPrBundle`
5
+ * result (lib/pr-bundle.js) into the Markdown body of the single sticky PR
6
+ * comment the pr-monitor GitHub workflow keeps up to date.
7
+ *
8
+ * This is the SURFACE half of the monitor: it lists the unresolved review
9
+ * threads (grouped by author, ANY author) plus the failing and pending checks
10
+ * so async review-bot / human feedback in a window nobody is watching cannot
11
+ * rot. It is deliberately NOT a merge decision — it never emits a pass/fail
12
+ * verdict and never claims a PR is ready to merge. That belongs to the
13
+ * trustworthy-shepherd redesign, not here.
14
+ *
15
+ * Pure and deterministic: same bundle + same injected clock → same body, which
16
+ * is what lets the workflow rewrite the sticky comment in place without churn.
17
+ *
18
+ * @module pr-monitor/render-sticky
19
+ */
20
+
21
+ /** Hidden HTML marker: the workflow finds its prior comment by this string and
22
+ * UPDATES it in place, so the monitor never spams a PR with new comments. */
23
+ const STICKY_MARKER = '<!-- forge-pr-monitor -->';
24
+
25
+ /** Cap threads listed per author so a noisy PR can't produce an enormous body. */
26
+ const MAX_THREADS_PER_AUTHOR = 8;
27
+
28
+ /** Group unresolved review-thread comments by author → ordered [author, threads]. */
29
+ function groupByAuthor(comments) {
30
+ const byAuthor = new Map();
31
+ for (const c of (Array.isArray(comments) ? comments : [])) {
32
+ const author = String(c.author || 'unknown');
33
+ if (!byAuthor.has(author)) byAuthor.set(author, []);
34
+ byAuthor.get(author).push(c);
35
+ }
36
+ // Sort authors by descending thread count, then name — stable + deterministic.
37
+ return [...byAuthor.entries()].sort((a, b) => (b[1].length - a[1].length) || a[0].localeCompare(b[0]));
38
+ }
39
+
40
+ /** One-line locator for a thread: `path:line` when known, else the threadId. */
41
+ function threadLocator(t) {
42
+ if (t.path) return t.line != null ? `${t.path}:${t.line}` : t.path;
43
+ return t.threadId || '(thread)';
44
+ }
45
+
46
+ /** Render the unresolved-review-threads section (author-agnostic + fail-closed). */
47
+ function renderThreads(bundle, lines) {
48
+ // Fail-closed: if the thread read was not available for ANY reason — it threw
49
+ // (error set) OR the adapter cannot read comments at all (capability absent,
50
+ // error null) — NEVER render "zero / clean". Only a genuine available:true read
51
+ // may report "no unresolved threads". Guard on `!== true` (not `=== false`) so a
52
+ // producer that omits the flag is also treated as unread, never as clean.
53
+ if (bundle.unresolvedCommentsAvailable !== true) {
54
+ const why = bundle.unresolvedCommentsError || 'thread read unavailable (capability absent)';
55
+ lines.push('### Review threads');
56
+ lines.push(`⚠️ Review threads were **unreadable** this pass (\`${why}\`) — not treated as zero. Re-run once the read recovers.`);
57
+ lines.push('');
58
+ return;
59
+ }
60
+
61
+ const comments = Array.isArray(bundle.unresolvedComments) ? bundle.unresolvedComments : [];
62
+ if (comments.length === 0) {
63
+ lines.push('### Review threads');
64
+ lines.push('✅ No unresolved review threads.');
65
+ lines.push('');
66
+ return;
67
+ }
68
+
69
+ const groups = groupByAuthor(comments);
70
+ lines.push(`### Unresolved review threads (${comments.length})`);
71
+ lines.push('');
72
+ for (const [author, threads] of groups) {
73
+ lines.push(`- **${author}** — ${threads.length}`);
74
+ for (const t of threads.slice(0, MAX_THREADS_PER_AUTHOR)) {
75
+ lines.push(` - \`${threadLocator(t)}\``);
76
+ }
77
+ if (threads.length > MAX_THREADS_PER_AUTHOR) {
78
+ lines.push(` - …and ${threads.length - MAX_THREADS_PER_AUTHOR} more`);
79
+ }
80
+ }
81
+ lines.push('');
82
+ }
83
+
84
+ /** Render the failing / pending check sections (author-agnostic + fail-closed). */
85
+ function renderChecks(bundle, lines) {
86
+ // Fail-closed, mirroring renderThreads: empty ci arrays are AMBIGUOUS — they
87
+ // mean either "read, genuinely all-clear" or "never read (gather outage)". Only
88
+ // an explicit ciAvailable === true lets us render the summary; anything else
89
+ // ("!== true": false or missing) surfaces as unreadable, so the monitor never
90
+ // prints a false "no failing checks" for CI it did not actually read.
91
+ if (bundle.ciAvailable !== true) {
92
+ lines.push('### Checks');
93
+ lines.push('⚠️ Checks were **unreadable** this pass — not treated as green. Re-run once the read recovers.');
94
+ lines.push('');
95
+ return;
96
+ }
97
+
98
+ const ci = bundle.ci || {};
99
+ const failing = Array.isArray(ci.failing) ? ci.failing : [];
100
+ const pending = Array.isArray(ci.pending) ? ci.pending : [];
101
+
102
+ lines.push('### Checks');
103
+ if (failing.length === 0 && pending.length === 0) {
104
+ lines.push('✅ No failing or pending checks.');
105
+ } else {
106
+ if (failing.length > 0) {
107
+ lines.push(`- ❌ **Failing (${failing.length}):** ${failing.map((c) => `\`${c.name || '?'}\``).join(', ')}`);
108
+ }
109
+ if (pending.length > 0) {
110
+ lines.push(`- ⏳ **Pending (${pending.length}):** ${pending.map((c) => `\`${c.name || '?'}\``).join(', ')}`);
111
+ }
112
+ }
113
+ lines.push('');
114
+ }
115
+
116
+ /**
117
+ * Render the sticky monitor comment for a PR-state bundle.
118
+ *
119
+ * @param {object} bundle - a `gatherPrBundle` result (lib/pr-bundle.js).
120
+ * @param {object} [opts]
121
+ * @param {Date} [opts.now] - injected clock for deterministic output.
122
+ * @returns {{ marker: string, body: string }}
123
+ */
124
+ function renderStickyComment(bundle = {}, opts = {}) {
125
+ const now = opts.now instanceof Date ? opts.now : new Date();
126
+ const lines = [];
127
+
128
+ // The marker MUST be the very first bytes so the workflow's substring match
129
+ // finds the prior comment regardless of any rendering below it.
130
+ lines.push(STICKY_MARKER);
131
+ lines.push('## 🔭 Forge PR Monitor');
132
+ lines.push('');
133
+ lines.push('_Surfaces open review + check state so async feedback never rots. This monitor **does not merge** and does not post a pass/fail verdict._');
134
+ lines.push('');
135
+
136
+ renderThreads(bundle, lines);
137
+ renderChecks(bundle, lines);
138
+
139
+ const branch = bundle.branch || {};
140
+ if ((branch.behind || 0) > 0) {
141
+ lines.push(`> Branch is **${branch.behind}** commit(s) behind base.`);
142
+ lines.push('');
143
+ }
144
+
145
+ lines.push('---');
146
+ lines.push(`<sub>Updated ${now.toISOString()} · surface-only monitor · never merges, never verdicts.</sub>`);
147
+
148
+ return { marker: STICKY_MARKER, body: lines.join('\n') };
149
+ }
150
+
151
+ module.exports = {
152
+ renderStickyComment,
153
+ groupByAuthor,
154
+ threadLocator,
155
+ STICKY_MARKER,
156
+ MAX_THREADS_PER_AUTHOR,
157
+ };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PR-monitor lifecycle — auto-start the watch loop, detached and idempotent, on
5
+ * `forge ship` success. This is what makes the monitor CONSTANT without an agent
6
+ * having to remember to run it: the moment a PR exists, a background
7
+ * `forge shepherd watch <pr>` begins keeping the journal warm, and any harness
8
+ * re-attaches later with `forge shepherd events <pr> --since <seq>`.
9
+ *
10
+ * Contract (all guaranteed here): NEVER throws, NEVER blocks, NEVER fails ship.
11
+ * The detached child is `unref`'d so it cannot keep the ship process alive, and
12
+ * every branch is wrapped so a spawn/gh failure degrades to "not started" rather
13
+ * than surfacing to the caller. Stop-on-merge belongs to the watch loop's
14
+ * terminal pass, not here.
15
+ *
16
+ * @module pr-monitor/watch-lifecycle
17
+ */
18
+
19
+ const path = require('node:path');
20
+ const { spawn, execFileSync } = require('node:child_process');
21
+
22
+ const journal = require('./journal');
23
+
24
+ /** Absolute path to the forge CLI entrypoint (this file is lib/pr-monitor/). */
25
+ function forgeBin() {
26
+ return path.join(__dirname, '..', '..', 'bin', 'forge.js');
27
+ }
28
+
29
+ /**
30
+ * Best-effort repo slug (the bare repo NAME, matching the shepherd's `ctx.repo`)
31
+ * from `git remote get-url origin`, so the idempotency check hits the same
32
+ * journal dir the watcher itself uses. Returns null on any failure — the caller
33
+ * then falls through to spawn and relies on the watch loop's own de-dup.
34
+ */
35
+ function defaultResolveSlug({ cwd, exec = execFileSync }) {
36
+ try {
37
+ const url = exec('git', ['remote', 'get-url', 'origin'], {
38
+ cwd, encoding: 'utf8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'],
39
+ }).trim();
40
+ const match = /[/:][^/]+\/([^/]+?)(?:\.git)?$/.exec(url);
41
+ return match ? match[1] : null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Start (or no-op) a detached `forge shepherd watch <pr>`.
49
+ *
50
+ * @param {object} opts
51
+ * @param {string|number} opts.prNumber - the PR to watch.
52
+ * @param {string} [opts.cwd] - repo root (default process.cwd()).
53
+ * @param {Function} [opts.spawn] - child spawner (test injection).
54
+ * @param {Function} [opts.exec] - git runner for slug resolution (test injection).
55
+ * @param {object} [opts.journal] - journal module (test injection).
56
+ * @param {Function} [opts.resolveSlug] - slug resolver (test injection).
57
+ * @returns {{ started: boolean, pid?: number|null, reason?: string }} — never throws.
58
+ */
59
+ function startPrWatcherDetached(opts = {}) {
60
+ const { prNumber, cwd = process.cwd() } = opts;
61
+ const spawnFn = opts.spawn || spawn;
62
+ const journalMod = opts.journal || journal;
63
+ const resolveSlug = opts.resolveSlug || defaultResolveSlug;
64
+ try {
65
+ if (!prNumber) return { started: false, reason: 'no-pr' };
66
+
67
+ const slug = resolveSlug({ cwd, exec: opts.exec });
68
+ if (slug) {
69
+ const dir = journalMod.journalDir({ root: cwd, repo: slug, pr: prNumber });
70
+ if (journalMod.watcherRunning(dir)) return { started: false, reason: 'already-running' };
71
+ }
72
+
73
+ const child = spawnFn(
74
+ process.execPath,
75
+ [forgeBin(), 'shepherd', 'watch', String(prNumber)],
76
+ { cwd, detached: true, stdio: 'ignore', windowsHide: true },
77
+ );
78
+ // spawn can emit an ASYNC 'error' (ENOENT/EACCES) AFTER returning; with no
79
+ // listener that becomes an unhandled exception that could crash ship. A no-op
80
+ // handler keeps a failed detached start best-effort (the watch loop's own
81
+ // journal claim is the authoritative de-dup anyway).
82
+ if (child && typeof child.on === 'function') child.on('error', () => {});
83
+ if (child && typeof child.unref === 'function') child.unref();
84
+ return { started: true, pid: child?.pid ?? null };
85
+ } catch (err) {
86
+ // Lifecycle auto-start must never fail ship — degrade to "not started".
87
+ return { started: false, reason: err.message };
88
+ }
89
+ }
90
+
91
+ module.exports = {
92
+ startPrWatcherDetached,
93
+ defaultResolveSlug,
94
+ forgeBin,
95
+ };