forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7

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 (268) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +48 -0
  3. package/CLAUDE.md +0 -12
  4. package/CODING_STANDARDS.md +72 -0
  5. package/bin/forge.js +12 -1
  6. package/docs/guides/MIGRATION.md +3 -3
  7. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
  8. package/docs/reference/INSTALL.md +4 -0
  9. package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
  10. package/docs/reference/RELEASE.md +4 -4
  11. package/docs/reference/github-accounts.md +134 -0
  12. package/docs/reference/shepherd.md +63 -13
  13. package/lib/adapters/pr-state-adapter.js +15 -2
  14. package/lib/base-remote.js +138 -0
  15. package/lib/beta5-compatibility-evidence.js +1093 -0
  16. package/lib/bun-lockfile-proof.js +413 -0
  17. package/lib/bun-workflow-pins.js +461 -0
  18. package/lib/capabilities/index.js +9 -0
  19. package/lib/capabilities/model.js +141 -0
  20. package/lib/capabilities/probes.js +347 -0
  21. package/lib/codex-skills.js +2 -2
  22. package/lib/commands/_manifest.js +1 -0
  23. package/lib/commands/_registry.js +48 -18
  24. package/lib/commands/clean.js +57 -1
  25. package/lib/commands/doctor.js +37 -6
  26. package/lib/commands/gate.js +197 -27
  27. package/lib/commands/github.js +215 -0
  28. package/lib/commands/hooks.js +54 -6
  29. package/lib/commands/memory.js +66 -2
  30. package/lib/commands/merge.js +720 -73
  31. package/lib/commands/plan.js +33 -2
  32. package/lib/commands/pr.js +2 -0
  33. package/lib/commands/preflight.js +10 -2
  34. package/lib/commands/push.js +120 -9
  35. package/lib/commands/recall.js +95 -61
  36. package/lib/commands/release.js +23 -2
  37. package/lib/commands/remember.js +28 -4
  38. package/lib/commands/serve.js +26 -9
  39. package/lib/commands/setup.js +132 -4
  40. package/lib/commands/shepherd.js +578 -72
  41. package/lib/commands/ship.js +15 -69
  42. package/lib/commands/skill.js +8 -0
  43. package/lib/commands/team.js +47 -8
  44. package/lib/commands/test.js +163 -4
  45. package/lib/commands/validate.js +78 -22
  46. package/lib/commands/worktree.js +155 -19
  47. package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
  48. package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
  49. package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
  50. package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
  51. package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
  52. package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
  53. package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
  54. package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
  55. package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
  56. package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
  57. package/lib/forge-issues.js +78 -0
  58. package/lib/gate-events.js +98 -10
  59. package/lib/github-context.js +308 -0
  60. package/lib/global-flags.js +1 -0
  61. package/lib/hook-renderer.js +29 -1
  62. package/lib/issue-render.js +19 -0
  63. package/lib/kernel/broker.js +723 -31
  64. package/lib/kernel/claim-reconciler.js +238 -0
  65. package/lib/kernel/lease-enforcer.js +9 -4
  66. package/lib/kernel/legacy-claim-repair.js +442 -0
  67. package/lib/kernel/live-claim-projection.js +26 -0
  68. package/lib/kernel/migrations.js +118 -3
  69. package/lib/kernel/readiness-model.js +184 -12
  70. package/lib/kernel/schema.js +49 -1
  71. package/lib/kernel/sqlite-driver.js +3322 -183
  72. package/lib/kernel/taxonomy-validator.js +4 -1
  73. package/lib/kernel/windows-private-acl.js +239 -0
  74. package/lib/memory/hygiene.js +191 -0
  75. package/lib/memory/router.js +94 -27
  76. package/lib/memory/usage-evidence.js +4 -0
  77. package/lib/memory-digest.js +59 -0
  78. package/lib/merge-rules.js +135 -17
  79. package/lib/npm-publish-workflow.js +233 -40
  80. package/lib/package-root.js +2 -0
  81. package/lib/pr-monitor/auto-actions.js +169 -28
  82. package/lib/pr-monitor/differ.js +110 -4
  83. package/lib/pr-monitor/events.js +0 -0
  84. package/lib/pr-monitor/flow-monitor.js +1424 -0
  85. package/lib/pr-monitor/gather.js +251 -44
  86. package/lib/pr-monitor/journal.js +0 -37
  87. package/lib/pr-monitor/monitor.js +117 -10
  88. package/lib/pr-monitor/process-identity.js +117 -0
  89. package/lib/pr-monitor/reconcile-executor.js +1101 -625
  90. package/lib/pr-monitor/reconcile.js +0 -0
  91. package/lib/pr-monitor/render-summary.js +121 -24
  92. package/lib/pr-monitor/review-preflight.js +269 -0
  93. package/lib/pr-monitor/shepherd-lease.js +28 -19
  94. package/lib/pr-monitor/verdict.js +438 -0
  95. package/lib/pr-monitor/watch-lifecycle.js +144 -38
  96. package/lib/pr-monitor/watch-owner.js +1414 -0
  97. package/lib/pr-monitor/watch.js +129 -58
  98. package/lib/pr-shepherd.js +17 -3
  99. package/lib/project-memory.js +145 -3
  100. package/lib/protected-state-authority.js +799 -4
  101. package/lib/protected-state-surfaces.js +181 -3
  102. package/lib/release-readiness.js +2 -3
  103. package/lib/review-adapter.js +65 -0
  104. package/lib/skills-sync.js +65 -32
  105. package/lib/validation/risk-manifest.js +339 -0
  106. package/lib/validation-receipt.js +190 -0
  107. package/lib/workflow/enforce-stage.js +44 -0
  108. package/lib/workflow/plan-authority.js +225 -0
  109. package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  110. package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  111. package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  112. package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  113. package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  114. package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  115. package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  116. package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  117. package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  118. package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  119. package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  120. package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  121. package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  122. package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  123. package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  124. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  125. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  126. package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  127. package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  128. package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  129. package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  130. package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  131. package/node_modules/@forge/contracts/index.js +32 -0
  132. package/node_modules/@forge/contracts/package.json +35 -0
  133. package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  134. package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  135. package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  136. package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  137. package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  138. package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  139. package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  140. package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  141. package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  142. package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  143. package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  144. package/node_modules/@forge/contracts/src/baseline.js +23 -0
  145. package/node_modules/@forge/contracts/src/canonical.js +151 -0
  146. package/node_modules/@forge/contracts/src/definitions.js +176 -0
  147. package/node_modules/@forge/contracts/src/identity.js +42 -0
  148. package/node_modules/@forge/contracts/src/schema.js +72 -0
  149. package/node_modules/@forge/contracts/src/validate.js +305 -0
  150. package/node_modules/@forge/flow/index.js +114 -0
  151. package/node_modules/@forge/flow/package.json +34 -0
  152. package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
  153. package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
  154. package/node_modules/@forge/flow/src/executor.js +279 -0
  155. package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
  156. package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
  157. package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
  158. package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
  159. package/node_modules/@forge/memory/index.js +331 -0
  160. package/node_modules/@forge/memory/package.json +34 -0
  161. package/node_modules/@forge/memory/src/authority-provider.js +67 -0
  162. package/node_modules/@forge/memory/src/backend-registry.js +166 -0
  163. package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
  164. package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
  165. package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
  166. package/package.json +16 -4
  167. package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  168. package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  169. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  170. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  171. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  172. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  173. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  174. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  175. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  176. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  177. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  178. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  179. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  180. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  181. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  182. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  183. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  184. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  185. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  186. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  187. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  188. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  189. package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
  190. package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
  191. package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  192. package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  193. package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  194. package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  195. package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  196. package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  197. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  198. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  199. package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  200. package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  201. package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  202. package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
  203. package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
  204. package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
  205. package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
  206. package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
  207. package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
  208. package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  209. package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  210. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  211. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  212. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  213. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  214. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  215. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  216. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  217. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  218. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  219. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  220. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  221. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  222. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  223. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  224. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  225. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  226. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  227. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  228. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  229. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  230. package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
  231. package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
  232. package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  233. package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  234. package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  235. package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  236. package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  237. package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  238. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  239. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  240. package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  241. package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  242. package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  243. package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
  244. package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
  245. package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
  246. package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
  247. package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
  248. package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
  249. package/scripts/commitlint.js +13 -15
  250. package/scripts/generate-risk-manifest.js +91 -0
  251. package/scripts/github-context-bridge.sh +10 -0
  252. package/scripts/legacy-claim-repair.js +145 -0
  253. package/scripts/lib/behavioral-eval-runtime.js +3 -2
  254. package/scripts/process-tree.js +14 -2
  255. package/scripts/protected-state-check.js +440 -17
  256. package/scripts/sync-agent-skills.js +333 -34
  257. package/scripts/test-full-suite.js +704 -18
  258. package/scripts/test-profile.js +13 -3
  259. package/scripts/test.js +96 -15
  260. package/skills/coverage.json +1 -0
  261. package/skills/review/SKILL.md +2 -0
  262. package/skills/review/evals/scorecard.json +2 -2
  263. package/skills/setup/SKILL.md +18 -0
  264. package/skills/setup/evals/scorecard.json +3 -3
  265. package/skills/shepherd/SKILL.md +19 -2
  266. package/skills/shepherd/evals/scorecard.json +3 -3
  267. package/skills/validate/SKILL.md +3 -0
  268. package/skills/validate/evals/scorecard.json +1 -1
@@ -22,22 +22,26 @@ There are two ways to run the shepherd, both under the single `forge shepherd`
22
22
  verb:
23
23
 
24
24
  - **`forge shepherd daemon` — the singleton reconcile daemon, the default
25
- ownership model.** It acquires the machine-wide shepherd lease for this repo
25
+ coordinator.** It acquires the machine-wide election lease for this repo
26
26
  (exiting immediately as a clean no-op if a live daemon already owns it),
27
27
  heartbeats, and converges the *entire* PR world every ~60s: self-registering
28
- hand-opened PRs, restarting killed watchers, reaping verified orphan watchers,
28
+ hand-opened PRs, recovering dead watcher generations, requesting cooperative
29
+ stops,
29
30
  converging CI check state into kernel verdicts, and retiring merged/closed PRs.
30
- It **self-retires** — releases the lease, kills its verified children, exits
31
- once no PRs remain open. Forge wakes it automatically after a successful
31
+ It **self-retires** — releases the election lease and exits once no PRs
32
+ remain open. Per-PR ownership is never stored in that lease. Forge wakes it
33
+ automatically after a successful
32
34
  supported session start, every successful push, and every successful
33
35
  non-dry-run ship. These are the only automatic firing seams; ordinary commands
34
36
  do not launch it. Once running, an agent does not poll: the daemon owns the
35
37
  convergence loop.
36
- - **`forge shepherd <pr>` — one bounded pass.** Reads one PR's state, takes at
37
- most one Tier-A action, exits. The point-in-time surface for a single PR (see
38
- *Bounded-pass model* below).
38
+ - **`forge shepherd <pr>` — one bounded convergence pass.** Runs local review
39
+ preflight, reads one PR's state, takes at most one Tier-A action, persists
40
+ bounded deltas/receipts, and exits. The point-in-time surface for a single PR
41
+ (see *Bounded-pass model* below).
39
42
  - **`forge shepherd watch <pr>` / `watch --adopt`** — foreground streaming watch of
40
- one PR, or (`--adopt`) adopt every currently-open PR into the watcher set.
43
+ one PR, or (`--adopt`) reserve owner rows and start watchers for every
44
+ currently-open PR.
41
45
  - **`forge shepherd events <pr> --since <seq>`** — the event deltas for a PR since a
42
46
  cursor; **`forge shepherd <pr> --pull --json` / `--bundle`** read the kernel verdict
43
47
  + rollup without taking any action.
@@ -48,6 +52,12 @@ bare CLI use falls back to a detached launch from the stable common repository
48
52
  root, never a disposable worktree cwd. No liveness check is needed first — the
49
53
  O_EXCL singleton lease makes a duplicate start a clean no-op.
50
54
 
55
+ The daemon never kills watcher PIDs. A stop is a generation-conditional
56
+ `running -> stop_requested` transition in the Kernel; the matching watcher sees
57
+ that state, exits its loop, and releases only its own generation. A dead watcher
58
+ is recovered only after fresh PID and provider evidence is gathered outside the
59
+ transaction and the exact owner-row snapshot still matches inside it.
60
+
51
61
  Embedding boundary:
52
62
 
53
63
  ```js
@@ -115,6 +125,16 @@ project's documented ergonomic — poll briefly, then stop and hand off. A pass
115
125
  that finds checks still pending returns `PENDING`; the next scheduled pass picks
116
126
  up from there.
117
127
 
128
+ Before remote mutation, the plain pass consolidates a probed CodeRabbit CLI with
129
+ strict lint, structural drift, Sonar parity, and affected tests. The result is
130
+ returned as `localPreflight`; CodeRabbit absence/auth loss is explicitly
131
+ `UNAVAILABLE`/`INCOMPLETE`, not green. Deterministic failure or actual local
132
+ review findings makes the remote decision dry-run for that pass. A local
133
+ checkout that is not the exact PR head is explicitly `NOT_APPLICABLE`. Output is
134
+ bounded to 128 `deltas` plus receipt IDs and one outcome handoff: `review` for
135
+ semantic feedback, explicit human-approved `merge` at `MERGE_READY`, or `verify`
136
+ after merged evidence. Shepherd never resolves threads and never merges.
137
+
118
138
  `--watch`-style behavior, if desired, belongs in an external scheduler (cron, or
119
139
  a `/loop`) that re-invokes the bounded pass with a debounce of at least 60
120
140
  seconds and cancel-in-progress. The shepherd itself never waits in-process.
@@ -164,6 +184,7 @@ same verb (or `forge shepherd events`) on its own cadence.
164
184
  | `MERGE_READY` | Required checks are green and the branch is up to date. The shepherd hands off — **a human merges in the GitHub UI.** |
165
185
  | `ESCALATE` | A Tier-C condition (conflict, unreadable required set, persistent failure, oscillation, budget exhaustion). Context is posted to the PR. |
166
186
  | `PENDING` | A Tier-A action was taken, or checks are still pending. Exit and await the next scheduled pass. |
187
+ | `INCOMPLETE` | Exact-head local review or durable convergence evidence is unavailable. Fail closed and retry after evidence is restored. |
167
188
  | `HARD_STOP` | A permanent auth/scope failure that retrying cannot fix. Escalate to a human to widen token scope. |
168
189
 
169
190
  ## Action ladder
@@ -189,6 +210,13 @@ same verb (or `forge shepherd events`) on its own cadence.
189
210
  - **HEAD-changed abort.** Before any mutating action it re-reads the head SHA; if
190
211
  HEAD moved during the pass, the action aborts. The `shepherd:active` marker is
191
212
  advisory only — it is not mutual exclusion.
213
+ - **One per-PR authority.** `kernel_pr_watch_owners` is the sole watcher owner
214
+ record. The daemon lease elects one reconciler but contains no watcher list;
215
+ journals and legacy PID/marker files cannot authorize a transition.
216
+ - **Fail-closed evidence.** Kernel unavailability, corrupt owner rows, stale
217
+ generations, changed provider/PID/receipt evidence, and an incomplete legacy
218
+ migration gate block mutation and are retried. No unlocked or filesystem
219
+ fallback becomes authority.
192
220
  - **Auth taxonomy.** 401 (expiry) pauses and surfaces; 403 insufficient-scope is
193
221
  a hard-stop; 403 with `Retry-After` honors the delay and resumes next pass.
194
222
 
@@ -248,8 +276,30 @@ neutral `forge/pr-monitor` check.
248
276
 
249
277
  ## State
250
278
 
251
- Progress is durable in the Actions job summary, the single verdict label, and
252
- `git`. The one local store is the constant monitor's per-PR journal under
253
- `.forge/pr-monitor/<repo>-<pr>/` (the append-only `events.ndjson` + snapshot and
254
- consumer cursors) the delivery/replay surface for `forge shepherd watch` and
255
- `events --since`. The bounded shepherd pass itself keeps no separate local state.
279
+ The shared Kernel database stores one versioned `kernel_pr_watch_owners` row per
280
+ canonical `(repo, pr)`. Its generation-conditional phases are `starting`,
281
+ `running`, `stop_requested`, `terminal_pending`, `complete`, and `blocked`.
282
+ Starts reserve a store-minted generation before spawning; the child binds its
283
+ PID to that exact generation; heartbeats, stop requests, terminal receipts,
284
+ recovery, release, and reopen are narrow compare-and-swap transitions. Provider,
285
+ PID, and receipt checks happen outside the short SQLite transaction, followed by
286
+ an exact snapshot check inside it.
287
+
288
+ Public Memory and the per-PR journal under
289
+ `.forge/pr-monitor/<repo>-<pr>/` remain durable delivery and replay surfaces for
290
+ monitor events, verdicts, `forge shepherd watch`, and `events --since`; neither
291
+ is watcher ownership authority. A terminal receipt first moves the owner row to
292
+ `terminal_pending`; a later transaction completes it only after the watcher PID
293
+ is confirmed dead and the receipt/provider evidence still matches.
294
+
295
+ Upgrade from the retired filesystem model is fenced by the repository-local
296
+ `kernel_pr_watch_migration_gate`. The importer publishes `quarantined`, obtains
297
+ two identical bounded snapshots of the old lease/PID/marker evidence, binds that
298
+ snapshot hash before importing one PR per transaction, durably rereads the rows
299
+ and source, and then completes the exact gate/hash. Corrupt, changed, live-PID,
300
+ or unmappable evidence stays fail-closed as `blocked` or `conflict`. After the
301
+ gate is complete, legacy evidence is cleanup-only and never regains authority.
302
+
303
+ For 0.1, no receipt grants continuing lease authority. Consequential paths
304
+ re-probe live ownership, while canonical LeaseReceipt epoch/scope and the
305
+ same-actor/session release-reclaim ABA fix remain explicitly deferred.
@@ -102,6 +102,9 @@ function classifyAuthError(error) {
102
102
  if (status === 401 || /HTTP 401|bad credentials|token expired/i.test(text)) {
103
103
  return { class: 'expired' };
104
104
  }
105
+ if (status === 429 || /HTTP 429/i.test(text)) {
106
+ return { class: 'rate-limit', retryAfter };
107
+ }
105
108
  if (status === 403 || /HTTP 403/i.test(text)) {
106
109
  if (retryAfter || /rate limit|secondary rate/i.test(text)) {
107
110
  return { class: 'rate-limit', retryAfter };
@@ -116,6 +119,7 @@ class PrStateAdapter {
116
119
  * @param {object} [options]
117
120
  * @param {Function} [options.gh] - Runner for `gh` (cmd, args[]) → string.
118
121
  * @param {Function} [options.git] - Runner for `git` (cmd, args[]) → string.
122
+ * @param {string} [options.repository] - Exact owner/repo for numeric PR reads.
119
123
  */
120
124
  constructor(options = {}) {
121
125
  this.id = options.id || 'pr-state-adapter';
@@ -131,6 +135,7 @@ class PrStateAdapter {
131
135
  });
132
136
  this._gh = options.gh || defaultRunner;
133
137
  this._git = options.git || defaultRunner;
138
+ this._repository = options.repository || null;
134
139
  this.lastProtectionStatus = null;
135
140
  }
136
141
 
@@ -141,7 +146,11 @@ class PrStateAdapter {
141
146
  * @returns {Promise<{ headSha: string, mergeable: string, mergeStateStatus: string, checks: object[], threads: object[] }>}
142
147
  */
143
148
  async readState(pr) {
144
- const raw = this._gh('gh', ['pr', 'view', String(pr), '--json', PR_VIEW_FIELDS]);
149
+ const raw = this._gh('gh', [
150
+ 'pr', 'view', String(pr),
151
+ ...(this._repository ? ['--repo', this._repository] : []),
152
+ '--json', PR_VIEW_FIELDS,
153
+ ]);
145
154
  const data = JSON.parse(raw || '{}');
146
155
  const lifecycleReadable = typeof data.state === 'string'
147
156
  && ['OPEN', 'MERGED', 'CLOSED'].includes(data.state.toUpperCase())
@@ -732,7 +741,11 @@ class PrStateAdapter {
732
741
  * @returns {Promise<number|null>} epoch ms of the head commit, or null.
733
742
  */
734
743
  async readHeadCommitTime({ pr }) {
735
- const raw = this._gh('gh', ['pr', 'view', String(pr), '--json', 'commits', '-q', '.commits[-1].committedDate']);
744
+ const raw = this._gh('gh', [
745
+ 'pr', 'view', String(pr),
746
+ ...(this._repository ? ['--repo', this._repository] : []),
747
+ '--json', 'commits', '-q', '.commits[-1].committedDate',
748
+ ]);
736
749
  const t = Date.parse(String(raw || '').trim());
737
750
  return Number.isFinite(t) ? t : null;
738
751
  }
@@ -0,0 +1,138 @@
1
+ 'use strict';
2
+
3
+ const { execFileSync } = require('node:child_process');
4
+
5
+ const BASE_REMOTE_CANDIDATES = ['upstream', 'origin'];
6
+ const DEFAULT_BRANCH_CANDIDATES = ['main', 'master'];
7
+ const GIT_PROBE_TIMEOUT_MS = 120000;
8
+
9
+ /**
10
+ * Quiet git probe options. `stdio: 'pipe'` keeps failed probes from leaking to
11
+ * the terminal — these are speculative queries, not user-facing commands.
12
+ *
13
+ * @param {string} cwd - Working directory for the probe.
14
+ * @returns {object} execFileSync options.
15
+ */
16
+ function getQuietProbeOptions(cwd) {
17
+ return { encoding: 'utf8', cwd, timeout: GIT_PROBE_TIMEOUT_MS, stdio: 'pipe' };
18
+ }
19
+
20
+ /**
21
+ * Resolve `refs/remotes/<remote>/HEAD` to the ref it points at, verifying the
22
+ * target actually exists.
23
+ *
24
+ * @param {Function} [exec] - Injected execFileSync.
25
+ * @param {string} [cwd] - Repository directory.
26
+ * @param {string} [remoteName] - Remote to probe.
27
+ * @returns {string|null} Fully-qualified ref, or null.
28
+ */
29
+ function resolveRemoteHeadTarget(exec = execFileSync, cwd = process.cwd(), remoteName = undefined) {
30
+ if (!remoteName) {
31
+ return null;
32
+ }
33
+ try {
34
+ const symbolicRef = exec('git', ['symbolic-ref', `refs/remotes/${remoteName}/HEAD`], getQuietProbeOptions(cwd)).trim();
35
+ if (!symbolicRef) {
36
+ return null;
37
+ }
38
+ exec('git', ['rev-parse', '--verify', symbolicRef], getQuietProbeOptions(cwd));
39
+ return symbolicRef;
40
+ } catch (_error) { // NOSONAR S2486 - intentional: missing ref is the expected probe failure
41
+ void _error;
42
+ return null;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * True when the remote has fetched tracking refs we can treat as a base:
48
+ * a recorded HEAD, or one of the conventional default branches.
49
+ *
50
+ * @param {Function} [exec] - Injected execFileSync.
51
+ * @param {string} [cwd] - Repository directory.
52
+ * @param {string} [remoteName] - Remote to probe.
53
+ * @returns {boolean} Whether a usable tracking base exists.
54
+ */
55
+ function remoteHasTrackingBase(exec = execFileSync, cwd = process.cwd(), remoteName = undefined) {
56
+ if (!remoteName) {
57
+ return false;
58
+ }
59
+ if (resolveRemoteHeadTarget(exec, cwd, remoteName)) {
60
+ return true;
61
+ }
62
+
63
+ for (const candidate of DEFAULT_BRANCH_CANDIDATES) {
64
+ try {
65
+ exec('git', ['rev-parse', '--verify', `refs/remotes/${remoteName}/${candidate}`], getQuietProbeOptions(cwd));
66
+ return true;
67
+ } catch (_error) { // NOSONAR S2486 - intentional: speculative probe, try next candidate
68
+ void _error;
69
+ // Probe next candidate.
70
+ }
71
+ }
72
+
73
+ return false;
74
+ }
75
+
76
+ /**
77
+ * Resolve the remote that owns the integration base. `upstream` wins over
78
+ * `origin` so a fork-style checkout resolves the official repository rather
79
+ * than the contributor's fork.
80
+ *
81
+ * @param {Function} [exec] - Injected execFileSync.
82
+ * @param {string} [cwd] - Repository directory.
83
+ * @returns {string} Remote name ('origin' when nothing else qualifies).
84
+ */
85
+ function resolveBaseRemote(exec = execFileSync, cwd = process.cwd()) {
86
+ for (const candidate of BASE_REMOTE_CANDIDATES) {
87
+ try {
88
+ exec('git', ['remote', 'get-url', candidate], getQuietProbeOptions(cwd));
89
+ if (remoteHasTrackingBase(exec, cwd, candidate)) {
90
+ return candidate;
91
+ }
92
+ } catch (_error) { // NOSONAR S2486 - intentional: speculative probe, try next candidate
93
+ void _error;
94
+ // Probe next candidate.
95
+ }
96
+ }
97
+
98
+ return 'origin';
99
+ }
100
+
101
+ /**
102
+ * Resolve the base remote's default branch name.
103
+ *
104
+ * @param {Function} [exec] - Injected execFileSync.
105
+ * @param {string} [cwd] - Repository directory.
106
+ * @param {string} [remoteName] - Remote to probe; defaults to the base remote.
107
+ * @returns {string} Branch name ('master' when nothing else resolves).
108
+ */
109
+ function resolveBaseBranch(exec = execFileSync, cwd = process.cwd(), remoteName = resolveBaseRemote(exec, cwd)) {
110
+ const symbolicRef = resolveRemoteHeadTarget(exec, cwd, remoteName);
111
+ if (symbolicRef) {
112
+ const match = new RegExp(`^refs/remotes/${remoteName}/(.+)$`).exec(symbolicRef);
113
+ if (match?.[1]) {
114
+ return match[1];
115
+ }
116
+ }
117
+
118
+ for (const candidate of DEFAULT_BRANCH_CANDIDATES) {
119
+ try {
120
+ exec('git', ['rev-parse', '--verify', `refs/remotes/${remoteName}/${candidate}`], getQuietProbeOptions(cwd));
121
+ return candidate;
122
+ } catch (_error) { // NOSONAR S2486 - intentional: speculative probe, try next candidate
123
+ void _error;
124
+ // Probe next candidate.
125
+ }
126
+ }
127
+
128
+ return 'master';
129
+ }
130
+
131
+ module.exports = {
132
+ BASE_REMOTE_CANDIDATES,
133
+ DEFAULT_BRANCH_CANDIDATES,
134
+ resolveRemoteHeadTarget,
135
+ remoteHasTrackingBase,
136
+ resolveBaseRemote,
137
+ resolveBaseBranch,
138
+ };