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,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,192 @@
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 leads with the one-line actionable
9
+ * verdict (mirroring the `pr-verdict:*` label the workflow lands), then lists the
10
+ * unresolved review threads (grouped by author, ANY author) plus the failing and
11
+ * pending checks so async review-bot / human feedback in a window nobody is
12
+ * watching cannot rot. The verdict LABELS state (check-failed / threads-open /
13
+ * mergeable / …); it is NOT a merge action — it never merges, never resolves
14
+ * threads, never blocks, and is fail-closed (`unknown` on unreadable signals).
15
+ *
16
+ * Pure and deterministic: same bundle + same injected clock → same body, which
17
+ * is what lets the workflow rewrite the sticky comment in place without churn.
18
+ *
19
+ * @module pr-monitor/render-sticky
20
+ */
21
+
22
+ /**
23
+ * Presentation-only headline for each canonical merge verdict (lib/pr-pull.js).
24
+ * The verdict VALUE is computed once by pr-pull (`forge shepherd --pull --json`)
25
+ * and passed in — this map only decides how to DISPLAY it, so there is no second
26
+ * verdict ladder to drift.
27
+ */
28
+ const VERDICT_HEADLINE = {
29
+ UNKNOWN: '⚪ **Verdict: `unknown`** — a signal was unreadable; state unconfirmed (fail-closed).',
30
+ 'BLOCKED-CONFLICT': '🔀 **Verdict: `blocked-conflict`** — branch conflicts with base; rebase/merge and resolve.',
31
+ BEHIND: '⬇️ **Verdict: `behind`** — branch is behind base; update/rebase (protection requires up-to-date).',
32
+ 'BLOCKED-CHECKS': '🔴 **Verdict: `blocked-checks`** — a required check is failing/missing; fix it.',
33
+ 'BLOCKED-THREADS': '🟠 **Verdict: `blocked-threads`** — unresolved review threads need addressing.',
34
+ 'REVIEW-PENDING': '🟡 **Verdict: `review-pending`** — awaiting review / settle window; not ready yet.',
35
+ 'CLEAN-MERGEABLE': '🟢 **Verdict: `clean-mergeable`** — green + zero unresolved threads; ready for a human to merge.',
36
+ };
37
+
38
+ /**
39
+ * Render the one-line verdict headline for a canonical verdict string. Unknown or
40
+ * missing input falls closed to the `unknown` headline.
41
+ *
42
+ * @param {string} verdict
43
+ * @returns {string}
44
+ */
45
+ function verdictHeadline(verdict) {
46
+ return VERDICT_HEADLINE[String(verdict || '').toUpperCase()] || VERDICT_HEADLINE.UNKNOWN;
47
+ }
48
+
49
+ /** Hidden HTML marker: the workflow finds its prior comment by this string and
50
+ * UPDATES it in place, so the monitor never spams a PR with new comments. */
51
+ const STICKY_MARKER = '<!-- forge-pr-monitor -->';
52
+
53
+ /** Cap threads listed per author so a noisy PR can't produce an enormous body. */
54
+ const MAX_THREADS_PER_AUTHOR = 8;
55
+
56
+ /** Group unresolved review-thread comments by author → ordered [author, threads]. */
57
+ function groupByAuthor(comments) {
58
+ const byAuthor = new Map();
59
+ for (const c of (Array.isArray(comments) ? comments : [])) {
60
+ const author = String(c.author || 'unknown');
61
+ if (!byAuthor.has(author)) byAuthor.set(author, []);
62
+ byAuthor.get(author).push(c);
63
+ }
64
+ // Sort authors by descending thread count, then name — stable + deterministic.
65
+ return [...byAuthor.entries()].sort((a, b) => (b[1].length - a[1].length) || a[0].localeCompare(b[0]));
66
+ }
67
+
68
+ /** One-line locator for a thread: `path:line` when known, else the threadId. */
69
+ function threadLocator(t) {
70
+ if (t.path) return t.line != null ? `${t.path}:${t.line}` : t.path;
71
+ return t.threadId || '(thread)';
72
+ }
73
+
74
+ /** Render the unresolved-review-threads section (author-agnostic + fail-closed). */
75
+ function renderThreads(bundle, lines) {
76
+ // Fail-closed: if the thread read was not available for ANY reason — it threw
77
+ // (error set) OR the adapter cannot read comments at all (capability absent,
78
+ // error null) — NEVER render "zero / clean". Only a genuine available:true read
79
+ // may report "no unresolved threads". Guard on `!== true` (not `=== false`) so a
80
+ // producer that omits the flag is also treated as unread, never as clean.
81
+ if (bundle.unresolvedCommentsAvailable !== true) {
82
+ const why = bundle.unresolvedCommentsError || 'thread read unavailable (capability absent)';
83
+ lines.push('### Review threads');
84
+ lines.push(`⚠️ Review threads were **unreadable** this pass (\`${why}\`) — not treated as zero. Re-run once the read recovers.`);
85
+ lines.push('');
86
+ return;
87
+ }
88
+
89
+ const comments = Array.isArray(bundle.unresolvedComments) ? bundle.unresolvedComments : [];
90
+ if (comments.length === 0) {
91
+ lines.push('### Review threads');
92
+ lines.push('✅ No unresolved review threads.');
93
+ lines.push('');
94
+ return;
95
+ }
96
+
97
+ const groups = groupByAuthor(comments);
98
+ lines.push(`### Unresolved review threads (${comments.length})`);
99
+ lines.push('');
100
+ for (const [author, threads] of groups) {
101
+ lines.push(`- **${author}** — ${threads.length}`);
102
+ for (const t of threads.slice(0, MAX_THREADS_PER_AUTHOR)) {
103
+ lines.push(` - \`${threadLocator(t)}\``);
104
+ }
105
+ if (threads.length > MAX_THREADS_PER_AUTHOR) {
106
+ lines.push(` - …and ${threads.length - MAX_THREADS_PER_AUTHOR} more`);
107
+ }
108
+ }
109
+ lines.push('');
110
+ }
111
+
112
+ /** Render the failing / pending check sections (author-agnostic + fail-closed). */
113
+ function renderChecks(bundle, lines) {
114
+ // Fail-closed, mirroring renderThreads: empty ci arrays are AMBIGUOUS — they
115
+ // mean either "read, genuinely all-clear" or "never read (gather outage)". Only
116
+ // an explicit ciAvailable === true lets us render the summary; anything else
117
+ // ("!== true": false or missing) surfaces as unreadable, so the monitor never
118
+ // prints a false "no failing checks" for CI it did not actually read.
119
+ if (bundle.ciAvailable !== true) {
120
+ lines.push('### Checks');
121
+ lines.push('⚠️ Checks were **unreadable** this pass — not treated as green. Re-run once the read recovers.');
122
+ lines.push('');
123
+ return;
124
+ }
125
+
126
+ const ci = bundle.ci || {};
127
+ const failing = Array.isArray(ci.failing) ? ci.failing : [];
128
+ const pending = Array.isArray(ci.pending) ? ci.pending : [];
129
+
130
+ lines.push('### Checks');
131
+ if (failing.length === 0 && pending.length === 0) {
132
+ lines.push('✅ No failing or pending checks.');
133
+ } else {
134
+ if (failing.length > 0) {
135
+ lines.push(`- ❌ **Failing (${failing.length}):** ${failing.map((c) => `\`${c.name || '?'}\``).join(', ')}`);
136
+ }
137
+ if (pending.length > 0) {
138
+ lines.push(`- ⏳ **Pending (${pending.length}):** ${pending.map((c) => `\`${c.name || '?'}\``).join(', ')}`);
139
+ }
140
+ }
141
+ lines.push('');
142
+ }
143
+
144
+ /**
145
+ * Render the sticky monitor comment for a PR-state bundle.
146
+ *
147
+ * @param {object} bundle - a `gatherPrBundle` result (lib/pr-bundle.js).
148
+ * @param {object} [opts]
149
+ * @param {Date} [opts.now] - injected clock for deterministic output.
150
+ * @returns {{ marker: string, body: string }}
151
+ */
152
+ function renderStickyComment(bundle = {}, opts = {}) {
153
+ const now = opts.now instanceof Date ? opts.now : new Date();
154
+ const lines = [];
155
+
156
+ // The marker MUST be the very first bytes so the workflow's substring match
157
+ // finds the prior comment regardless of any rendering below it.
158
+ lines.push(STICKY_MARKER);
159
+ lines.push('## 🔭 Forge PR Monitor');
160
+ lines.push('');
161
+ // Lead with the actionable verdict — the SAME value as the pr-verdict:* label
162
+ // and `forge shepherd --pull --json` (passed in via opts.verdict, computed once
163
+ // by pr-pull). Surface only: it labels state; this monitor **does not merge**
164
+ // and never resolves review threads.
165
+ lines.push(verdictHeadline(opts.verdict));
166
+ lines.push('');
167
+ lines.push('_Surfaces open review + check state so async feedback never rots. This monitor **does not merge** and never resolves review threads — a human merges in the GitHub UI._');
168
+ lines.push('');
169
+
170
+ renderThreads(bundle, lines);
171
+ renderChecks(bundle, lines);
172
+
173
+ const branch = bundle.branch || {};
174
+ if ((branch.behind || 0) > 0) {
175
+ lines.push(`> Branch is **${branch.behind}** commit(s) behind base.`);
176
+ lines.push('');
177
+ }
178
+
179
+ lines.push('---');
180
+ lines.push(`<sub>Updated ${now.toISOString()} · surface-only monitor · labels state, never merges, never resolves threads.</sub>`);
181
+
182
+ return { marker: STICKY_MARKER, body: lines.join('\n') };
183
+ }
184
+
185
+ module.exports = {
186
+ renderStickyComment,
187
+ verdictHeadline,
188
+ groupByAuthor,
189
+ threadLocator,
190
+ STICKY_MARKER,
191
+ MAX_THREADS_PER_AUTHOR,
192
+ };
@@ -0,0 +1,169 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * PR-monitor sticky-comment upsert — race-safe, converges to EXACTLY ONE sticky
5
+ * comment per PR even under a concurrent burst of workflow runs.
6
+ *
7
+ * Why this exists: the pr-monitor workflow deliberately has NO `concurrency:`
8
+ * group. A per-PR group does not help — GitHub's queue replacement cancels the
9
+ * previously-PENDING run in a group UNCONDITIONALLY (independent of
10
+ * cancel-in-progress), and this workflow's triggers (check_suite:completed fires
11
+ * ~10+ times per push, plus reviews/comments) burst hard, so a group left a trail
12
+ * of CANCELLED runs that render as red/non-SUCCESS checks and tripped merge-gate
13
+ * tooling (kernel issue 97e6a146). Dropping the group removes the cancellations,
14
+ * but then two concurrent first-runs on a PR with no sticky yet would BOTH find
15
+ * nothing and BOTH create one → duplicate sticky comments. This module closes
16
+ * that race deterministically instead.
17
+ *
18
+ * Reconcile-to-one algorithm:
19
+ * 1. List marker comments. If none, create one, then RE-LIST (a concurrent run
20
+ * may have created its own in the same burst).
21
+ * 2. Pick the deterministic survivor: the LOWEST comment id (oldest). Every
22
+ * concurrent run picks the SAME survivor, so they never fight.
23
+ * 3. Update the survivor with the latest body; delete every other marker
24
+ * comment. Deletes are idempotent (a 404 means a peer already removed it).
25
+ *
26
+ * A create that lands AFTER a run's re-list is self-healed by the next event:
27
+ * every run reconciles to one, and events keep arriving, so the PR converges to a
28
+ * single sticky comment.
29
+ *
30
+ * @module pr-monitor/upsert-sticky
31
+ */
32
+
33
+ const { execFileSync } = require('node:child_process');
34
+
35
+ /** Ids of comments whose body carries the sticky marker. */
36
+ function markerCommentIds(comments, marker) {
37
+ return (Array.isArray(comments) ? comments : [])
38
+ .filter((comment) => typeof comment.body === 'string' && comment.body.includes(marker))
39
+ .map((comment) => comment.id);
40
+ }
41
+
42
+ /** Ascending by numeric id, so the survivor (index 0) is the oldest comment. */
43
+ function sortIdsAscending(ids) {
44
+ return [...ids].sort((left, right) => Number(left) - Number(right));
45
+ }
46
+
47
+ /**
48
+ * Drive a client to exactly one sticky comment. `client` abstracts the GitHub
49
+ * calls so the reconcile logic is unit-testable without the network:
50
+ * - list() → array of { id, body }
51
+ * - create() → create a new sticky comment (body supplied by the client)
52
+ * - update(id) → overwrite comment `id` with the latest body
53
+ * - remove(id) → delete comment `id` (must tolerate an already-deleted 404)
54
+ *
55
+ * @returns {Promise<{ survivor: (number|string|null), deleted: Array<number|string> }>}
56
+ */
57
+ async function upsertStickyComment({ marker }, client) {
58
+ let ids = markerCommentIds(await client.list(), marker);
59
+
60
+ if (ids.length === 0) {
61
+ await client.create();
62
+ // Re-list: a concurrent run may have created its own sticky in this burst.
63
+ ids = markerCommentIds(await client.list(), marker);
64
+ }
65
+
66
+ if (ids.length === 0) {
67
+ // The just-created comment is not visible yet (eventual consistency); its
68
+ // body is already correct, and the next event will reconcile if a peer raced.
69
+ return { survivor: null, deleted: [] };
70
+ }
71
+
72
+ ids = sortIdsAscending(ids);
73
+ const survivor = ids[0];
74
+ await client.update(survivor);
75
+
76
+ const deleted = [];
77
+ for (const id of ids.slice(1)) {
78
+ await client.remove(id);
79
+ deleted.push(id);
80
+ }
81
+
82
+ return { survivor, deleted };
83
+ }
84
+
85
+ // Single choke point for the `gh` CLI, matching lib/commands/merge.js. `gh` is a
86
+ // hardcoded literal (never user input) and args are an array (no shell), so the
87
+ // S4036 PATH-search finding is a false positive in this developer-tool context;
88
+ // one annotation here covers every call site. `encoding: 'utf8'` also pipes
89
+ // stderr onto the thrown error, so isAlreadyGone() below can classify failures.
90
+ function runGh(args) {
91
+ return execFileSync('gh', args, { encoding: 'utf8' }); // NOSONAR S4036 - hardcoded CLI (gh), args array (no shell), developer-tool context
92
+ }
93
+
94
+ /**
95
+ * True only when a `gh api` failure means the target comment is already absent
96
+ * (HTTP 404 / 410) — the benign "a peer got there first" case. Auth failures,
97
+ * rate limits (403), and every other error return false so they propagate and
98
+ * surface a diagnostic instead of silently breaking the exactly-one invariant.
99
+ */
100
+ function isAlreadyGone(error) {
101
+ const text = `${error && error.stderr ? error.stderr : ''} ${error && error.message ? error.message : ''}`;
102
+ return /HTTP 404|HTTP 410|\bNot Found\b|\bGone\b/i.test(text);
103
+ }
104
+
105
+ /**
106
+ * GitHub-backed client (shells to `gh api`, like lib/pr-monitor/gather.js). The
107
+ * reconcile logic above is unit-tested; the error-tolerance in update/remove is
108
+ * too, via an injectable `run`. create/update both send the same pre-rendered
109
+ * payload file the render step wrote, so the body is identical.
110
+ */
111
+ function ghStickyClient({ repo, pr, payloadFile, run = runGh }) {
112
+ return {
113
+ async list() {
114
+ const out = run(['api', `repos/${repo}/issues/${pr}/comments`, '--paginate']);
115
+ return out && out.trim() ? JSON.parse(out) : [];
116
+ },
117
+ async create() {
118
+ run(['api', '-X', 'POST', `repos/${repo}/issues/${pr}/comments`, '--input', payloadFile]);
119
+ },
120
+ async update(id) {
121
+ try {
122
+ run(['api', '-X', 'PATCH', `repos/${repo}/issues/comments/${id}`, '--input', payloadFile]);
123
+ } catch (error) {
124
+ if (!isAlreadyGone(error)) {
125
+ throw error;
126
+ }
127
+ // Our chosen survivor was deleted by a peer whose survivor had a lower id:
128
+ // the peer's sticky wins, exactly-one still holds, so we are done.
129
+ }
130
+ },
131
+ async remove(id) {
132
+ try {
133
+ run(['api', '-X', 'DELETE', `repos/${repo}/issues/comments/${id}`]);
134
+ } catch (error) {
135
+ if (!isAlreadyGone(error)) {
136
+ throw error;
137
+ }
138
+ // Already gone (a concurrent run deleted it first) — the goal state holds.
139
+ }
140
+ },
141
+ };
142
+ }
143
+
144
+ async function main() {
145
+ const repo = process.env.GH_REPO;
146
+ const pr = process.env.PR;
147
+ const payloadFile = process.env.STICKY_PAYLOAD_FILE || 'monitor-payload.json';
148
+ const { STICKY_MARKER } = require('./render-sticky');
149
+
150
+ const client = ghStickyClient({ repo, pr, payloadFile });
151
+ const { survivor, deleted } = await upsertStickyComment({ marker: STICKY_MARKER }, client);
152
+ const survivorLabel = survivor === null ? 'created (not yet visible)' : survivor;
153
+ console.log(`Sticky comment reconciled to one: survivor=${survivorLabel}, deleted=${deleted.length}`);
154
+ }
155
+
156
+ if (require.main === module) {
157
+ main().catch((error) => {
158
+ console.error(error.message);
159
+ process.exit(1);
160
+ });
161
+ }
162
+
163
+ module.exports = {
164
+ upsertStickyComment,
165
+ markerCommentIds,
166
+ sortIdsAscending,
167
+ isAlreadyGone,
168
+ ghStickyClient,
169
+ };
@@ -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
+ };