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
@@ -2,17 +2,112 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const { secureExecFileSync } = require('./shell-utils');
5
6
 
6
7
  const {
7
8
  createProtectedStateAuditRecord,
8
9
  recordProtectedStateAuditEvent,
10
+ removeProtectedFile,
9
11
  writeProtectedFile,
10
12
  } = require('./protected-state-surfaces');
11
13
  const {
14
+ completeNpmPublishWorkflowAuthorization,
12
15
  issueNpmPublishWorkflowAuthorization,
13
16
  } = require('./protected-state-authority');
14
17
 
15
18
  const NPM_PUBLISH_WORKFLOW_PATH = '.github/workflows/npm-publish.yml';
19
+ const FULL_COMMIT_SHA = /^[0-9a-f]{40}$/;
20
+ const BUN_VERSION = /^bun@(\d+\.\d+\.\d+)$/.exec(require('../package.json').packageManager)?.[1];
21
+ if (!BUN_VERSION) throw new Error('package.json must pin an exact stable Bun version.');
22
+
23
+ function resolveCurrentHead(projectRoot, execGit = secureExecFileSync) {
24
+ return execGit('git', ['rev-parse', '--verify', 'HEAD^{commit}'], {
25
+ cwd: projectRoot,
26
+ encoding: 'utf8',
27
+ stdio: ['ignore', 'pipe', 'pipe'],
28
+ windowsHide: true,
29
+ }).trim();
30
+ }
31
+
32
+ function readNpmPublishWorkflow(projectRoot, revision, execGit = secureExecFileSync) {
33
+ const options = {
34
+ cwd: projectRoot,
35
+ encoding: 'utf8',
36
+ stdio: ['ignore', 'pipe', 'pipe'],
37
+ windowsHide: true,
38
+ };
39
+ const indexed = revision === ':';
40
+ const entry = execGit('git', indexed
41
+ ? ['ls-files', '--stage', '--', NPM_PUBLISH_WORKFLOW_PATH]
42
+ : ['ls-tree', revision, '--', NPM_PUBLISH_WORKFLOW_PATH], options).trim();
43
+ const expected = indexed
44
+ ? /^100(?:644|755) [0-9a-f]+ 0\t\.github\/workflows\/npm-publish\.yml$/
45
+ : /^100(?:644|755) blob [0-9a-f]+\t\.github\/workflows\/npm-publish\.yml$/;
46
+ if (!expected.test(entry)) {
47
+ throw new Error(`${NPM_PUBLISH_WORKFLOW_PATH} is not one regular file ${indexed ? 'in the Git index' : `at ${revision}`}.`);
48
+ }
49
+ return Buffer.from(execGit('git', ['show', `${revision}${indexed ? '' : ':'}${NPM_PUBLISH_WORKFLOW_PATH}`], {
50
+ ...options,
51
+ encoding: 'buffer',
52
+ }));
53
+ }
54
+
55
+ function readSourceNpmPublishWorkflow(projectRoot, sourceHead, execGit = secureExecFileSync) {
56
+ return readNpmPublishWorkflow(projectRoot, sourceHead, execGit);
57
+ }
58
+
59
+ function readIndexedNpmPublishWorkflow(projectRoot, execGit = secureExecFileSync) {
60
+ return readNpmPublishWorkflow(projectRoot, ':', execGit);
61
+ }
62
+
63
+ function verifyExpectedHead(projectRoot, expectedHead, resolveHead = resolveCurrentHead) {
64
+ if (!expectedHead) {
65
+ return { success: false, error: '--expect-head is required for protected workflow generation.' };
66
+ }
67
+ if (!FULL_COMMIT_SHA.test(expectedHead)) {
68
+ return { success: false, error: '--expect-head must be a full 40-character lowercase commit SHA.' };
69
+ }
70
+
71
+ let currentHead;
72
+ try {
73
+ currentHead = resolveHead(projectRoot);
74
+ } catch (error) {
75
+ return { success: false, error: `Could not resolve current HEAD: ${error.message}` };
76
+ }
77
+ if (!FULL_COMMIT_SHA.test(currentHead)) {
78
+ return { success: false, error: 'Current HEAD did not resolve to a full 40-character lowercase commit SHA.' };
79
+ }
80
+ if (currentHead !== expectedHead) {
81
+ return {
82
+ success: false,
83
+ error: `Expected HEAD ${expectedHead} does not match current HEAD ${currentHead}.`,
84
+ };
85
+ }
86
+ return { success: true, sourceHead: currentHead };
87
+ }
88
+
89
+ function renderNpmDistTagResolverScript() {
90
+ return `CORE_RE='(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)'
91
+ NUMERIC_ID_RE='(0|[1-9][0-9]*)'
92
+ BUILD_RE='(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?'
93
+ BETA_RE="^\${CORE_RE}-beta(\\.\${NUMERIC_ID_RE})*\${BUILD_RE}$"
94
+ RC_RE="^\${CORE_RE}-rc(\\.\${NUMERIC_ID_RE})*\${BUILD_RE}$"
95
+ STABLE_RE="^\${CORE_RE}\${BUILD_RE}$"
96
+
97
+ if [[ "$VERSION" =~ $BETA_RE ]]; then
98
+ echo "tag=beta" >> "$GITHUB_OUTPUT"
99
+ elif [[ "$VERSION" =~ $RC_RE ]]; then
100
+ echo "tag=rc" >> "$GITHUB_OUTPUT"
101
+ elif [[ "$VERSION" =~ $STABLE_RE ]]; then
102
+ echo "tag=latest" >> "$GITHUB_OUTPUT"
103
+ elif [[ "$VERSION" == *-* ]]; then
104
+ echo "::error::Unsupported npm prerelease: $VERSION"
105
+ exit 1
106
+ else
107
+ echo "::error::Invalid npm version: $VERSION"
108
+ exit 1
109
+ fi`;
110
+ }
16
111
 
17
112
  function snapshotWorkflow(projectRoot) {
18
113
  const fullPath = path.resolve(projectRoot, NPM_PUBLISH_WORKFLOW_PATH);
@@ -29,24 +124,57 @@ function snapshotWorkflow(projectRoot) {
29
124
  }
30
125
  }
31
126
 
32
- function rollbackWorkflow(snapshot, projectRoot, protectedWriter, writeOptions) {
127
+ function sameSnapshot(expected, actual) {
128
+ if (!expected || typeof expected !== 'object' || Array.isArray(expected)) return false;
129
+ if (typeof expected.existed !== 'boolean') return false;
130
+ if (expected.existed && !Buffer.isBuffer(expected.content)) return false;
131
+ if (!expected.existed && expected.content !== null) return false;
132
+ return expected.existed === actual.existed
133
+ && (!expected.existed || expected.content.equals(actual.content));
134
+ }
135
+
136
+ function recoverWorkflow(snapshot, projectRoot, generatedContent, protectedWriter, protectedRemover, writeOptions) {
137
+ const recoveryOptions = {
138
+ ...writeOptions,
139
+ operation: 'recover_npm_workflow',
140
+ expectedContent: generatedContent,
141
+ };
33
142
  if (!snapshot.existed) {
34
- fs.rmSync(snapshot.fullPath, { force: true });
35
- return;
143
+ return protectedRemover(projectRoot, NPM_PUBLISH_WORKFLOW_PATH, recoveryOptions);
36
144
  }
37
145
  if (!Buffer.isBuffer(snapshot.content)) {
38
- throw new Error('Previous workflow was not a regular file');
146
+ return { allowed: false, reason: 'Previous workflow was not a regular file' };
39
147
  }
40
- const restored = protectedWriter(
148
+ return protectedWriter(
41
149
  projectRoot,
42
150
  NPM_PUBLISH_WORKFLOW_PATH,
43
151
  snapshot.content,
44
- writeOptions,
152
+ recoveryOptions,
45
153
  );
46
- if (!restored.allowed) throw new Error(restored.reason || 'Protected workflow restore was denied');
47
154
  }
48
155
 
49
- function renderNpmPublishWorkflow() {
156
+ async function prepareWorkflowAuthorization(prepareAuthorization, projectRoot, params, authorizationOptions) {
157
+ try {
158
+ return await prepareAuthorization(projectRoot, params, authorizationOptions);
159
+ } catch (error) {
160
+ return { success: false, error: error.message };
161
+ }
162
+ }
163
+
164
+ function recordWorkflowAudit(auditWriter, auditRecord, projectRoot) {
165
+ try {
166
+ return auditWriter(auditRecord, { cwd: projectRoot });
167
+ } catch (error) {
168
+ return { success: false, error: error.message };
169
+ }
170
+ }
171
+
172
+ function renderNpmPublishWorkflow(bunVersion = BUN_VERSION) {
173
+ if (!/^\d+\.\d+\.\d+$/.test(bunVersion)) throw new Error('npm workflow requires an exact stable Bun version.');
174
+ const distTagResolver = renderNpmDistTagResolverScript()
175
+ .split('\n')
176
+ .map(line => line ? ` ${line}` : '')
177
+ .join('\n');
50
178
  return `# Generated by \`forge release generate-npm-workflow\`. Do not edit directly.
51
179
  name: Bun Package
52
180
 
@@ -61,6 +189,7 @@ jobs:
61
189
  resolve-release:
62
190
  name: Resolve immutable release SHA
63
191
  runs-on: ubuntu-latest
192
+ timeout-minutes: 10
64
193
  permissions:
65
194
  contents: read
66
195
  outputs:
@@ -88,6 +217,7 @@ jobs:
88
217
  build:
89
218
  needs: resolve-release
90
219
  runs-on: ubuntu-latest
220
+ timeout-minutes: 25
91
221
  permissions:
92
222
  contents: read
93
223
  outputs:
@@ -105,7 +235,7 @@ jobs:
105
235
  node-version: 22
106
236
  - uses: oven-sh/setup-bun@v2
107
237
  with:
108
- bun-version: 1.3.12
238
+ bun-version: ${bunVersion}
109
239
  - run: bun install --frozen-lockfile
110
240
  - name: Setup test fixtures
111
241
  shell: bash
@@ -124,6 +254,7 @@ jobs:
124
254
  publish-npm:
125
255
  needs: [resolve-release, build]
126
256
  runs-on: ubuntu-latest
257
+ timeout-minutes: 20
127
258
  permissions:
128
259
  contents: read
129
260
  id-token: write
@@ -146,7 +277,7 @@ jobs:
146
277
  run: node scripts/npm-release-receipt.js verify
147
278
  - uses: oven-sh/setup-bun@v2
148
279
  with:
149
- bun-version: 1.3.12
280
+ bun-version: ${bunVersion}
150
281
  - run: bun install --frozen-lockfile
151
282
  - name: Release tag matches package.json version
152
283
  shell: bash
@@ -164,36 +295,92 @@ jobs:
164
295
  run: npm pack --dry-run
165
296
  - name: Ensure OIDC-capable npm
166
297
  run: npm install --global npm@11.5.1 && npm --version
167
- - name: Publish to npm
298
+ - name: Resolve npm dist-tag
299
+ id: dist-tag
168
300
  shell: bash
169
301
  run: |
170
302
  set -euo pipefail
171
303
  VERSION="$(node -p "require('./package.json').version")"
172
- if echo "$VERSION" | grep -q '-'; then
173
- echo "Prerelease $VERSION -> publishing under the beta dist-tag"
174
- npm publish --provenance --tag beta
175
- else
176
- npm publish --provenance
177
- fi
304
+ ${distTagResolver}
305
+ - name: Publish to npm
306
+ shell: bash
307
+ run: |
308
+ set -euo pipefail
309
+ DIST_TAG="\${{ steps.dist-tag.outputs.tag }}"
310
+ case "$DIST_TAG" in
311
+ beta)
312
+ npm publish --provenance --tag beta
313
+ ;;
314
+ rc)
315
+ npm publish --provenance --tag rc
316
+ ;;
317
+ latest)
318
+ npm publish --provenance
319
+ ;;
320
+ *)
321
+ echo "::error::Unsupported npm dist-tag: $DIST_TAG"
322
+ exit 1
323
+ ;;
324
+ esac
178
325
  `;
179
326
  }
180
327
 
181
328
  async function generateNpmPublishWorkflow(projectRoot, options = {}) {
182
- const content = renderNpmPublishWorkflow();
329
+ const head = verifyExpectedHead(projectRoot, options.expectedHead, options.resolveHead);
330
+ if (!head.success) return head;
331
+ const sourceHead = head.sourceHead;
332
+ let targetBunVersion;
333
+ try {
334
+ targetBunVersion = options.targetBunVersion
335
+ || require('./bun-workflow-pins').resolveBunVersionTarget(projectRoot, options).version;
336
+ } catch (error) {
337
+ return { success: false, error: error.message };
338
+ }
339
+ const bunVersion = options.bunVersion || targetBunVersion;
340
+ if (bunVersion !== targetBunVersion) {
341
+ return { success: false, error: 'npm workflow Bun version must match the staged package.json Bun pin.' };
342
+ }
343
+ const content = renderNpmPublishWorkflow(bunVersion);
183
344
  const actor = options.actor || options.env?.FORGE_ACTOR || process.env.FORGE_ACTOR || process.env.USER || process.env.USERNAME || 'unknown';
184
345
  const protectedWriter = options.writeProtectedFile || writeProtectedFile;
346
+ const protectedRemover = options.removeProtectedFile || removeProtectedFile;
185
347
  const auditWriter = options.recordProtectedStateAuditEvent || recordProtectedStateAuditEvent;
186
- const authorizationWriter = options.issueNpmPublishWorkflowAuthorization || issueNpmPublishWorkflowAuthorization;
348
+ const prepareAuthorization = options.prepareNpmPublishWorkflowAuthorization || issueNpmPublishWorkflowAuthorization;
349
+ const activateAuthorization = options.activateNpmPublishWorkflowAuthorization || completeNpmPublishWorkflowAuthorization;
187
350
  const snapshot = snapshotWorkflow(projectRoot);
351
+ if (options.expectedSnapshot && !sameSnapshot(options.expectedSnapshot, snapshot)) {
352
+ return {
353
+ success: false,
354
+ error: `${NPM_PUBLISH_WORKFLOW_PATH} changed after Bun pin batch preflight.`,
355
+ };
356
+ }
188
357
  const writeOptions = {
189
358
  actor,
190
359
  operation: 'generate_npm_workflow',
191
360
  viaForgeApi: true,
192
361
  surface: 'workflows',
193
362
  };
363
+ const trustedAuthorization = await prepareWorkflowAuthorization(
364
+ prepareAuthorization,
365
+ projectRoot,
366
+ { actor, sourceHead, bunVersion, targetBunVersion },
367
+ { deps: options.kernelDeps, capabilityId: options.capabilityId },
368
+ );
369
+ if (!trustedAuthorization.success) {
370
+ return {
371
+ success: false,
372
+ error: `Could not record trusted protected-state authorization: ${trustedAuthorization.error}`,
373
+ trustedAuthorization,
374
+ };
375
+ }
376
+
194
377
  let write;
195
378
  try {
196
- write = protectedWriter(projectRoot, NPM_PUBLISH_WORKFLOW_PATH, content, writeOptions);
379
+ write = protectedWriter(projectRoot, NPM_PUBLISH_WORKFLOW_PATH, content, {
380
+ ...writeOptions,
381
+ expectedContent: snapshot.existed ? snapshot.content : null,
382
+ ...(options.beforeAtomicCommit ? { beforeAtomicCommit: options.beforeAtomicCommit } : {}),
383
+ });
197
384
  } catch (error) {
198
385
  return { success: false, error: error.message };
199
386
  }
@@ -207,66 +394,72 @@ async function generateNpmPublishWorkflow(projectRoot, options = {}) {
207
394
  path: NPM_PUBLISH_WORKFLOW_PATH,
208
395
  content,
209
396
  operation: 'generate_npm_workflow',
397
+ viaForgeApi: true,
398
+ sourceHead,
210
399
  });
211
- let audit;
212
- try {
213
- audit = auditWriter(auditRecord, { cwd: projectRoot });
214
- } catch (error) {
215
- audit = { success: false, error: error.message };
216
- }
400
+ const audit = recordWorkflowAudit(auditWriter, auditRecord, projectRoot);
217
401
  if (!audit.success) {
218
- let rollbackError;
402
+ let recovery;
219
403
  try {
220
- rollbackWorkflow(snapshot, projectRoot, protectedWriter, writeOptions);
404
+ recovery = recoverWorkflow(snapshot, projectRoot, content, protectedWriter, protectedRemover, writeOptions);
221
405
  } catch (error) {
222
- rollbackError = error.message;
406
+ recovery = { allowed: false, reason: error.message };
223
407
  }
224
408
  return {
225
409
  success: false,
226
410
  error: `Could not record protected-state authorization: ${audit.error}`,
227
411
  audit,
228
- rollbackError,
412
+ recovery,
229
413
  };
230
414
  }
231
415
 
232
- let trustedAuthorization;
416
+ let writeCompletion;
233
417
  try {
234
- trustedAuthorization = await authorizationWriter(
418
+ writeCompletion = await activateAuthorization(
235
419
  projectRoot,
236
- { actor },
420
+ { actor, sourceHead, capabilityId: trustedAuthorization.capabilityId, bunVersion, targetBunVersion },
237
421
  { deps: options.kernelDeps },
238
422
  );
239
423
  } catch (error) {
240
- trustedAuthorization = { success: false, error: error.message };
424
+ writeCompletion = { success: false, error: error.message };
241
425
  }
242
- if (!trustedAuthorization.success) {
243
- let rollbackError;
426
+ if (!writeCompletion.success) {
427
+ let recovery;
244
428
  try {
245
- rollbackWorkflow(snapshot, projectRoot, protectedWriter, writeOptions);
429
+ recovery = recoverWorkflow(snapshot, projectRoot, content, protectedWriter, protectedRemover, writeOptions);
246
430
  } catch (error) {
247
- rollbackError = error.message;
431
+ recovery = { allowed: false, reason: error.message };
248
432
  }
249
433
  return {
250
434
  success: false,
251
- error: `Could not record trusted protected-state authorization: ${trustedAuthorization.error}`,
435
+ error: `Could not complete trusted protected-state authorization: ${writeCompletion.error}`,
252
436
  audit,
253
437
  trustedAuthorization,
254
- rollbackError,
438
+ writeCompletion,
439
+ recovery,
255
440
  };
256
441
  }
257
442
 
258
443
  return {
259
444
  success: true,
260
445
  path: NPM_PUBLISH_WORKFLOW_PATH,
446
+ sourceHead,
447
+ bunVersion,
261
448
  contentHash: write.contentHash,
262
449
  write,
263
450
  audit,
264
451
  trustedAuthorization,
452
+ writeCompletion,
265
453
  };
266
454
  }
267
455
 
268
456
  module.exports = {
269
457
  NPM_PUBLISH_WORKFLOW_PATH,
458
+ readIndexedNpmPublishWorkflow,
459
+ readSourceNpmPublishWorkflow,
460
+ renderNpmDistTagResolverScript,
270
461
  renderNpmPublishWorkflow,
462
+ resolveCurrentHead,
463
+ verifyExpectedHead,
271
464
  generateNpmPublishWorkflow,
272
465
  };
@@ -67,8 +67,10 @@ const ASSET_ROOTS = Object.freeze([
67
67
  'skills',
68
68
  'rules',
69
69
  'AGENTS.md',
70
+ 'CODING_STANDARDS.md',
70
71
  '.claude/scripts',
71
72
  '.forge/hooks',
73
+ 'lib/kernel/windows-private-acl.js',
72
74
  'scripts',
73
75
  ]);
74
76
 
@@ -35,6 +35,17 @@ const INFRA_CONCLUSIONS = new Set(['CANCELLED', 'TIMED_OUT', 'STALE', 'STARTUP_F
35
35
  /** Conclusions that mean the code is genuinely broken — NEVER auto-rerun these. */
36
36
  const REAL_FAILURE_CONCLUSIONS = new Set(['FAILURE', 'ERROR', 'ACTION_REQUIRED']);
37
37
 
38
+ const AUTO_ACTION_STATUS = Object.freeze({
39
+ PASS: 'PASS',
40
+ UNCHANGED: 'UNCHANGED',
41
+ STALE: 'STALE',
42
+ INCOMPLETE: 'INCOMPLETE',
43
+ CONFLICT: 'CONFLICT',
44
+ });
45
+
46
+ const MAX_AUTO_ACTIONS = 8;
47
+ const MAX_AUTO_ATTEMPTS = 5;
48
+
38
49
  /**
39
50
  * Pull the numeric Actions run id from a job/run "details" URL
40
51
  * (`.../actions/runs/<run>/job/<job>` or `.../actions/runs/<run>`). Returns null
@@ -99,16 +110,40 @@ function decideUpdateBranch(payload, opts = {}) {
99
110
  return { should: true, reason: 'otherwise-clean-behind — only blocker is behind-base; merge base in' };
100
111
  }
101
112
 
113
+ function indexFailures(failures) {
114
+ const byName = new Map();
115
+ for (const failure of failures) {
116
+ if (!failure?.name || byName.has(failure.name)) continue;
117
+ byName.set(failure.name, {
118
+ conclusion: String(failure.conclusion || '').toUpperCase(),
119
+ url: failure.jobUrl || failure.detailsUrl || null,
120
+ });
121
+ }
122
+ return byName;
123
+ }
124
+
125
+ function selectRerunChecks(failingNames, failuresByName) {
126
+ const checks = [];
127
+ for (const name of failingNames) {
128
+ const failure = failuresByName.get(name);
129
+ if (!failure?.conclusion) {
130
+ return { reason: `required check "${name}" has no known conclusion — cannot confirm flaky, fail closed` };
131
+ }
132
+ if (REAL_FAILURE_CONCLUSIONS.has(failure.conclusion)) {
133
+ return { reason: `required check "${name}" is a real failure (${failure.conclusion}) — never rerun` };
134
+ }
135
+ if (!INFRA_CONCLUSIONS.has(failure.conclusion)) {
136
+ return { reason: `required check "${name}" conclusion ${failure.conclusion} is not classified infrastructural — fail closed` };
137
+ }
138
+ checks.push({ name, conclusion: failure.conclusion, runId: runIdFromUrl(failure.url) });
139
+ }
140
+ return { checks };
141
+ }
142
+
102
143
  /**
103
144
  * Decide whether to re-run flaky REQUIRED checks. Fires ONLY when EVERY failing
104
- * required check is infrastructural (cancelled/timed-out/stale/startup-failure)
105
- * with a derivable run id, and NONE is a real FAILURE/ERROR/ACTION_REQUIRED. A
106
- * single real failure, an unclassifiable conclusion, a required-failing check
107
- * with no matching `failures[]` entry, or a missing run id fails the WHOLE
108
- * decision closed (never rerun a genuinely-broken PR, never loop on a real bug).
109
- *
110
- * @param {object} payload - the `--pull --json` payload.
111
- * @returns {{ should: boolean, checks: Array<{name:string,conclusion:string,runId:string|null}>, runIds: string[], reason: string }}
145
+ * required check is infrastructural with a derivable run id, and NONE is a real
146
+ * failure or an unclassifiable signal.
112
147
  */
113
148
  function decideRerun(payload) {
114
149
  const empty = (reason) => ({ should: false, checks: [], runIds: [], reason });
@@ -119,28 +154,15 @@ function decideRerun(payload) {
119
154
  const failingNames = Array.isArray(rc.failing) ? rc.failing : [];
120
155
  if (failingNames.length === 0) return empty('no failing required checks');
121
156
 
122
- const failures = Array.isArray(payload.failures) ? payload.failures : [];
123
- const conclByName = new Map();
124
- const urlByName = new Map();
125
- for (const f of failures) {
126
- if (!f || !f.name) continue;
127
- if (!conclByName.has(f.name)) {
128
- conclByName.set(f.name, String(f.conclusion || '').toUpperCase());
129
- urlByName.set(f.name, f.jobUrl || f.detailsUrl || null);
130
- }
131
- }
132
-
133
- const picked = [];
134
- for (const name of failingNames) {
135
- const concl = conclByName.get(name);
136
- if (!concl) return empty(`required check "${name}" has no known conclusion — cannot confirm flaky, fail closed`);
137
- if (REAL_FAILURE_CONCLUSIONS.has(concl)) return empty(`required check "${name}" is a real failure (${concl}) — never rerun`);
138
- if (!INFRA_CONCLUSIONS.has(concl)) return empty(`required check "${name}" conclusion ${concl} is not classified infrastructural — fail closed`);
139
- picked.push({ name, conclusion: concl, runId: runIdFromUrl(urlByName.get(name)) });
140
- }
157
+ const selection = selectRerunChecks(
158
+ failingNames,
159
+ indexFailures(Array.isArray(payload.failures) ? payload.failures : []),
160
+ );
161
+ if (selection.reason) return empty(selection.reason);
141
162
 
163
+ const picked = selection.checks;
142
164
  const runIds = [...new Set(picked.map((p) => p.runId).filter(Boolean))];
143
- if (runIds.length === 0) return empty('no run id derivable from failure jobUrl — fail closed');
165
+ if (picked.some((item) => !item.runId)) return empty('no run id derivable from failure jobUrl — fail closed');
144
166
  return {
145
167
  should: true,
146
168
  checks: picked,
@@ -164,6 +186,123 @@ function decideAutoActions(payload, opts = {}) {
164
186
  };
165
187
  }
166
188
 
189
+ function boundedPolicyValue(value, fallback, maximum) {
190
+ const selected = value ?? fallback;
191
+ return Number.isSafeInteger(selected) && selected > 0 && selected <= maximum ? selected : null;
192
+ }
193
+
194
+ function normalizeActionHistory(history) {
195
+ if (!Array.isArray(history)) return { ok: false, reason: 'action history is incomplete' };
196
+ const byKey = new Map();
197
+ for (const item of history) {
198
+ if (!item || typeof item.key !== 'string' || !item.key
199
+ || !['pending', 'applied', 'failed'].includes(item.state)
200
+ || !Number.isSafeInteger(item.attempts) || item.attempts < 0) {
201
+ return { ok: false, reason: 'action history is incomplete' };
202
+ }
203
+ const prior = byKey.get(item.key);
204
+ if (prior && (prior.state !== item.state || prior.attempts !== item.attempts)) {
205
+ return { ok: false, conflict: true, reason: `action history conflicts for ${item.key}` };
206
+ }
207
+ if (!prior) byKey.set(item.key, item);
208
+ }
209
+ return { ok: true, byKey };
210
+ }
211
+
212
+ function validateAutoActionAuthority(payload) {
213
+ if (!payload?.evidence || !Array.isArray(payload.evidence.unreadable)
214
+ || typeof payload.evidence.tornRead !== 'boolean') {
215
+ return { ok: false, reason: 'verdict evidence is incomplete' };
216
+ }
217
+ if (payload.draft !== false) {
218
+ return { ok: false, reason: 'non-draft authority is incomplete' };
219
+ }
220
+
221
+ const failuresByName = new Map();
222
+ for (const failure of Array.isArray(payload.failures) ? payload.failures : []) {
223
+ if (!failure || typeof failure.name !== 'string' || !failure.name) continue;
224
+ const evidence = {
225
+ conclusion: String(failure.conclusion || '').toUpperCase(),
226
+ url: failure.jobUrl || failure.detailsUrl || null,
227
+ };
228
+ const prior = failuresByName.get(failure.name);
229
+ if (prior && (prior.conclusion !== evidence.conclusion || prior.url !== evidence.url)) {
230
+ return { ok: false, conflict: true, reason: `required check evidence conflicts for ${failure.name}` };
231
+ }
232
+ if (!prior) failuresByName.set(failure.name, evidence);
233
+ }
234
+ return { ok: true };
235
+ }
236
+
237
+ function buildAutoActionCandidates(decision, subjectRevision) {
238
+ const candidates = [];
239
+ if (decision.updateBranch.should) {
240
+ candidates.push({ type: 'updateBranch', key: `updateBranch:${subjectRevision}`, subjectRevision });
241
+ }
242
+ if (decision.rerunFlaky.should) {
243
+ const runIds = [...decision.rerunFlaky.runIds].sort((left, right) => left.localeCompare(right));
244
+ for (const runId of runIds) {
245
+ candidates.push({
246
+ type: 'rerunFlaky',
247
+ key: `rerunFlaky:${subjectRevision}:${runId}`,
248
+ subjectRevision,
249
+ runId,
250
+ });
251
+ }
252
+ }
253
+ return candidates;
254
+ }
255
+
256
+ function selectBoundedActions(candidates, history, maxActions, maxAttempts) {
257
+ const actions = [];
258
+ for (const candidate of candidates) {
259
+ const prior = history.get(candidate.key);
260
+ if (prior && (prior.state === 'pending' || prior.state === 'applied' || prior.attempts >= maxAttempts)) continue;
261
+ actions.push({ ...candidate, attempt: prior ? prior.attempts + 1 : 1 });
262
+ if (actions.length === maxActions) break;
263
+ }
264
+ return actions;
265
+ }
266
+
267
+ /** Select a deterministic, per-head, bounded action/retry batch. */
268
+ function decideBoundedAutoActions(payload, opts = {}) {
269
+ const closed = (status, reason) => ({ status, reason, actions: [] });
270
+ const subjectRevision = payload?.headSha;
271
+ if (typeof subjectRevision !== 'string' || !/^[0-9a-f]{40}$/i.test(subjectRevision)) {
272
+ return closed(AUTO_ACTION_STATUS.INCOMPLETE, 'exact head authority is incomplete');
273
+ }
274
+ if (opts.expectedRevision != null && opts.expectedRevision !== subjectRevision) {
275
+ return closed(AUTO_ACTION_STATUS.STALE, 'payload head does not match expected revision');
276
+ }
277
+ const authority = validateAutoActionAuthority(payload);
278
+ if (!authority.ok) {
279
+ return closed(authority.conflict ? AUTO_ACTION_STATUS.CONFLICT : AUTO_ACTION_STATUS.INCOMPLETE, authority.reason);
280
+ }
281
+ const maxActions = boundedPolicyValue(opts.maxActions, 1, MAX_AUTO_ACTIONS);
282
+ const maxAttempts = boundedPolicyValue(opts.maxAttempts, 2, MAX_AUTO_ATTEMPTS);
283
+ if (maxActions == null || maxAttempts == null) {
284
+ return closed(AUTO_ACTION_STATUS.INCOMPLETE, 'auto-action policy exceeds bounded limits');
285
+ }
286
+ const history = normalizeActionHistory(opts.history || []);
287
+ if (!history.ok) {
288
+ return closed(history.conflict ? AUTO_ACTION_STATUS.CONFLICT : AUTO_ACTION_STATUS.INCOMPLETE, history.reason);
289
+ }
290
+
291
+ const decision = decideAutoActions(payload, opts);
292
+ if (decision.updateBranch.should && decision.rerunFlaky.should) {
293
+ return closed(AUTO_ACTION_STATUS.CONFLICT, 'payload authorizes conflicting update and rerun actions');
294
+ }
295
+
296
+ const candidates = buildAutoActionCandidates(decision, subjectRevision);
297
+ const actions = selectBoundedActions(candidates, history.byKey, maxActions, maxAttempts);
298
+ if (actions.length === 0) {
299
+ return closed(AUTO_ACTION_STATUS.UNCHANGED, candidates.length === 0
300
+ ? 'no safe auto-action transition'
301
+ : 'identical actions are pending, applied, or retry-exhausted');
302
+ }
303
+ return { status: AUTO_ACTION_STATUS.PASS, reason: 'bounded auto-actions selected', actions };
304
+ }
305
+
167
306
  module.exports = {
168
307
  decideAutoActions,
169
308
  decideUpdateBranch,
@@ -172,4 +311,6 @@ module.exports = {
172
311
  isDegraded,
173
312
  INFRA_CONCLUSIONS,
174
313
  REAL_FAILURE_CONCLUSIONS,
314
+ AUTO_ACTION_STATUS,
315
+ decideBoundedAutoActions,
175
316
  };