@defrex/autobuild 0.8.0

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 (251) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +192 -0
  3. package/SPEC.md +2269 -0
  4. package/bin/ab-build-runner.ts +47 -0
  5. package/bin/ab-dev.ts +27 -0
  6. package/bin/ab-dispatch-kernel.ts +92 -0
  7. package/bin/ab-harvest-runner.ts +29 -0
  8. package/bin/ab-init-probe.ts +19 -0
  9. package/bin/ab-process-group-reaper.ts +32 -0
  10. package/bin/ab.ts +6 -0
  11. package/bin/agent/ab +3 -0
  12. package/docs/answering-blocked-builds.md +89 -0
  13. package/docs/architecture.md +726 -0
  14. package/docs/assets/headline-wide.png +0 -0
  15. package/docs/configuration.md +1623 -0
  16. package/docs/hosted-dispatcher-cutover-validation.md +178 -0
  17. package/docs/hosted-dispatcher.md +204 -0
  18. package/docs/mcp.md +119 -0
  19. package/docs/operator-api.md +186 -0
  20. package/docs/remote-store-protocol.md +972 -0
  21. package/docs/remote-ticket-protocol.md +79 -0
  22. package/docs/setup.md +289 -0
  23. package/docs/spec-standard.md +63 -0
  24. package/docs/ticket-dependencies.md +59 -0
  25. package/docs/vercel-sandbox-rollout-probe.md +7 -0
  26. package/package.json +135 -0
  27. package/packages/core/src/cli/args.ts +74 -0
  28. package/packages/core/src/cli/artifact.ts +182 -0
  29. package/packages/core/src/cli/binary.ts +198 -0
  30. package/packages/core/src/cli/build-control.ts +789 -0
  31. package/packages/core/src/cli/build-progress.ts +55 -0
  32. package/packages/core/src/cli/bulk-control.ts +385 -0
  33. package/packages/core/src/cli/context.ts +470 -0
  34. package/packages/core/src/cli/dashboard/actions.ts +59 -0
  35. package/packages/core/src/cli/dashboard/cells.ts +92 -0
  36. package/packages/core/src/cli/dashboard/composer.ts +230 -0
  37. package/packages/core/src/cli/dashboard/detail.ts +100 -0
  38. package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
  39. package/packages/core/src/cli/dashboard/live.ts +152 -0
  40. package/packages/core/src/cli/dashboard/model.ts +1173 -0
  41. package/packages/core/src/cli/dashboard/poll.ts +196 -0
  42. package/packages/core/src/cli/dashboard/render.ts +1568 -0
  43. package/packages/core/src/cli/dashboard/selection.ts +66 -0
  44. package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
  45. package/packages/core/src/cli/dashboard/session-view.ts +506 -0
  46. package/packages/core/src/cli/dashboard/transcript.ts +108 -0
  47. package/packages/core/src/cli/dev.ts +46 -0
  48. package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
  49. package/packages/core/src/cli/dispatch-process.ts +259 -0
  50. package/packages/core/src/cli/dispatch.ts +3692 -0
  51. package/packages/core/src/cli/dotenv.ts +48 -0
  52. package/packages/core/src/cli/env.ts +206 -0
  53. package/packages/core/src/cli/harvest.ts +698 -0
  54. package/packages/core/src/cli/help.ts +713 -0
  55. package/packages/core/src/cli/init-agent.ts +106 -0
  56. package/packages/core/src/cli/init-validation.ts +583 -0
  57. package/packages/core/src/cli/init.ts +756 -0
  58. package/packages/core/src/cli/installation.ts +333 -0
  59. package/packages/core/src/cli/keyboard.ts +261 -0
  60. package/packages/core/src/cli/main.ts +1399 -0
  61. package/packages/core/src/cli/mcp.ts +279 -0
  62. package/packages/core/src/cli/models.ts +65 -0
  63. package/packages/core/src/cli/observe.ts +55 -0
  64. package/packages/core/src/cli/plugin.ts +227 -0
  65. package/packages/core/src/cli/pr-attachments.ts +138 -0
  66. package/packages/core/src/cli/pr-summary.ts +124 -0
  67. package/packages/core/src/cli/publication-settlement.ts +188 -0
  68. package/packages/core/src/cli/repo-state.ts +213 -0
  69. package/packages/core/src/cli/repository-status.ts +80 -0
  70. package/packages/core/src/cli/self-update.ts +443 -0
  71. package/packages/core/src/cli/status.ts +895 -0
  72. package/packages/core/src/cli/store-opening.contract.ts +289 -0
  73. package/packages/core/src/cli/store-opening.ts +167 -0
  74. package/packages/core/src/cli/store-ref.ts +33 -0
  75. package/packages/core/src/cli/terminal-restore.ts +174 -0
  76. package/packages/core/src/cli/terminal.ts +361 -0
  77. package/packages/core/src/cli/terminals.ts +995 -0
  78. package/packages/core/src/cli/testkit.ts +194 -0
  79. package/packages/core/src/cli/ticket.ts +484 -0
  80. package/packages/core/src/cli/upgrade-agent.ts +180 -0
  81. package/packages/core/src/cli/upgrade-commit.ts +468 -0
  82. package/packages/core/src/cli/upgrade-notice.ts +91 -0
  83. package/packages/core/src/cli/upgrade-progress.ts +110 -0
  84. package/packages/core/src/cli/upgrade.ts +888 -0
  85. package/packages/core/src/cli/wait.ts +587 -0
  86. package/packages/core/src/cli/watch.ts +932 -0
  87. package/packages/core/src/config/live.ts +255 -0
  88. package/packages/core/src/config/load.ts +98 -0
  89. package/packages/core/src/config/roles.ts +438 -0
  90. package/packages/core/src/config/schema.ts +774 -0
  91. package/packages/core/src/distribution.ts +24 -0
  92. package/packages/core/src/events/catalog.ts +201 -0
  93. package/packages/core/src/events/envelope.ts +59 -0
  94. package/packages/core/src/events/payloads.ts +618 -0
  95. package/packages/core/src/events/repository.ts +559 -0
  96. package/packages/core/src/events/sessions.ts +180 -0
  97. package/packages/core/src/harvest/schema.ts +147 -0
  98. package/packages/core/src/ids.ts +45 -0
  99. package/packages/core/src/integration/harness.ts +681 -0
  100. package/packages/core/src/kernel/auto-merge.ts +259 -0
  101. package/packages/core/src/kernel/converge.ts +117 -0
  102. package/packages/core/src/kernel/dispatch-settings.ts +60 -0
  103. package/packages/core/src/kernel/dispatch-status.ts +141 -0
  104. package/packages/core/src/kernel/engine.ts +1047 -0
  105. package/packages/core/src/kernel/harvest.ts +823 -0
  106. package/packages/core/src/kernel/origin.ts +69 -0
  107. package/packages/core/src/kernel/phases.ts +196 -0
  108. package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
  109. package/packages/core/src/kernel/pr-attachments.ts +82 -0
  110. package/packages/core/src/kernel/reducer.ts +752 -0
  111. package/packages/core/src/kernel/stall.ts +155 -0
  112. package/packages/core/src/kernel/verify-applicability.ts +52 -0
  113. package/packages/core/src/markdown.ts +67 -0
  114. package/packages/core/src/ontology.ts +288 -0
  115. package/packages/core/src/open-map.ts +203 -0
  116. package/packages/core/src/operator/annotations.ts +187 -0
  117. package/packages/core/src/operator/client.ts +311 -0
  118. package/packages/core/src/operator/control.ts +117 -0
  119. package/packages/core/src/operator/index.ts +62 -0
  120. package/packages/core/src/operator/presentation.ts +22 -0
  121. package/packages/core/src/operator/protocol.ts +98 -0
  122. package/packages/core/src/operator/query.ts +201 -0
  123. package/packages/core/src/operator/registry.ts +1043 -0
  124. package/packages/core/src/operator/requests.ts +138 -0
  125. package/packages/core/src/operator/sandbox.ts +458 -0
  126. package/packages/core/src/operator/server.ts +616 -0
  127. package/packages/core/src/operator/sessions.ts +223 -0
  128. package/packages/core/src/operator/tickets.ts +177 -0
  129. package/packages/core/src/plugin-sdk/index.ts +174 -0
  130. package/packages/core/src/plugins/contract-entry.ts +74 -0
  131. package/packages/core/src/plugins/contract-env.ts +3 -0
  132. package/packages/core/src/plugins/load.ts +259 -0
  133. package/packages/core/src/plugins/manifest.ts +236 -0
  134. package/packages/core/src/plugins/registry.ts +274 -0
  135. package/packages/core/src/plugins/runtimes.ts +75 -0
  136. package/packages/core/src/ports/forge/contract.ts +258 -0
  137. package/packages/core/src/ports/forge/create.ts +74 -0
  138. package/packages/core/src/ports/forge/fake.ts +488 -0
  139. package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
  140. package/packages/core/src/ports/forge/github-transport.ts +352 -0
  141. package/packages/core/src/ports/forge/github.ts +1162 -0
  142. package/packages/core/src/ports/forge/local-git.ts +568 -0
  143. package/packages/core/src/ports/runner/claude.ts +873 -0
  144. package/packages/core/src/ports/runner/cli-stream.ts +90 -0
  145. package/packages/core/src/ports/runner/codex.ts +873 -0
  146. package/packages/core/src/ports/runner/contract.ts +492 -0
  147. package/packages/core/src/ports/runner/fake.ts +213 -0
  148. package/packages/core/src/ports/runner/json-record.ts +2 -0
  149. package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
  150. package/packages/core/src/ports/runner/one-shot.ts +29 -0
  151. package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
  152. package/packages/core/src/ports/runner/pi-bridge.js +6 -0
  153. package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
  154. package/packages/core/src/ports/runner/pi.ts +617 -0
  155. package/packages/core/src/ports/runner/production.ts +74 -0
  156. package/packages/core/src/ports/runner/provider-error.ts +121 -0
  157. package/packages/core/src/ports/runner/routing.ts +234 -0
  158. package/packages/core/src/ports/runner/runtime.ts +214 -0
  159. package/packages/core/src/ports/runner/session-env.ts +34 -0
  160. package/packages/core/src/ports/runner/stream-parts.ts +103 -0
  161. package/packages/core/src/ports/tickets/contract.ts +570 -0
  162. package/packages/core/src/ports/tickets/create.ts +135 -0
  163. package/packages/core/src/ports/tickets/fake.ts +215 -0
  164. package/packages/core/src/ports/tickets/file.ts +559 -0
  165. package/packages/core/src/ports/tickets/linear.ts +811 -0
  166. package/packages/core/src/ports/tickets/operations.ts +161 -0
  167. package/packages/core/src/ports/tickets/remote-index.ts +13 -0
  168. package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
  169. package/packages/core/src/ports/tickets/remote-server.ts +185 -0
  170. package/packages/core/src/ports/tickets/remote.ts +130 -0
  171. package/packages/core/src/ports/tickets/update.ts +51 -0
  172. package/packages/core/src/ports/types.ts +528 -0
  173. package/packages/core/src/ports/workspace/build-execution.ts +123 -0
  174. package/packages/core/src/ports/workspace/contract.ts +136 -0
  175. package/packages/core/src/ports/workspace/create.ts +132 -0
  176. package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
  177. package/packages/core/src/ports/workspace/fake.ts +432 -0
  178. package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
  179. package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
  180. package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
  181. package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
  182. package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
  183. package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
  184. package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
  185. package/packages/core/src/processes/build-child-exit.ts +78 -0
  186. package/packages/core/src/processes/build-child.ts +119 -0
  187. package/packages/core/src/processes/build-execution-state.ts +95 -0
  188. package/packages/core/src/processes/build-parent-watch.ts +35 -0
  189. package/packages/core/src/processes/build-runner.ts +2614 -0
  190. package/packages/core/src/processes/dispatcher.ts +2825 -0
  191. package/packages/core/src/processes/execution-settlement.ts +171 -0
  192. package/packages/core/src/processes/harvest-child.ts +115 -0
  193. package/packages/core/src/processes/harvest-execution-state.ts +121 -0
  194. package/packages/core/src/processes/harvest-runner.ts +1656 -0
  195. package/packages/core/src/processes/harvest.ts +595 -0
  196. package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
  197. package/packages/core/src/processes/phase-failure-budget.ts +52 -0
  198. package/packages/core/src/processes/process-group-reaper.ts +35 -0
  199. package/packages/core/src/processes/process-group.ts +65 -0
  200. package/packages/core/src/processes/publication-loss.ts +62 -0
  201. package/packages/core/src/processes/publication-state.ts +93 -0
  202. package/packages/core/src/processes/sandbox-state.ts +101 -0
  203. package/packages/core/src/registry.ts +24 -0
  204. package/packages/core/src/sessions/reducer.ts +185 -0
  205. package/packages/core/src/skills.ts +7 -0
  206. package/packages/core/src/spec-standard.ts +47 -0
  207. package/packages/core/src/store/adapter.ts +80 -0
  208. package/packages/core/src/store/build-scope.ts +272 -0
  209. package/packages/core/src/store/contract.ts +2581 -0
  210. package/packages/core/src/store/local/blobs.ts +38 -0
  211. package/packages/core/src/store/local/schema.ts +180 -0
  212. package/packages/core/src/store/local/store.ts +1713 -0
  213. package/packages/core/src/store/memory.ts +1138 -0
  214. package/packages/core/src/store/phase-session.ts +425 -0
  215. package/packages/core/src/store/remote/client.ts +780 -0
  216. package/packages/core/src/store/remote/index.ts +12 -0
  217. package/packages/core/src/store/remote/protocol.ts +348 -0
  218. package/packages/core/src/store/remote/server.ts +923 -0
  219. package/packages/core/src/store/remote/token.ts +137 -0
  220. package/packages/core/src/store/remote/version.ts +10 -0
  221. package/packages/core/src/store/retention.ts +91 -0
  222. package/packages/core/src/store/session-handle.ts +265 -0
  223. package/packages/core/src/store/streams/assemble.ts +79 -0
  224. package/packages/core/src/store/streams/lock.ts +42 -0
  225. package/packages/core/src/store/streams/session-writer.ts +306 -0
  226. package/packages/core/src/store/streams/types.ts +171 -0
  227. package/packages/core/src/store/streams/wait.ts +127 -0
  228. package/packages/core/src/store/subscribe.ts +105 -0
  229. package/packages/core/src/store/types.ts +466 -0
  230. package/packages/core/src/testing/fixed.ts +44 -0
  231. package/packages/core/src/testing/store-failures.ts +32 -0
  232. package/packages/core/src/zod-issues.ts +91 -0
  233. package/skills/code-review/SKILL.md +100 -0
  234. package/skills/finalize/SKILL.md +44 -0
  235. package/skills/guide/SKILL.md +1781 -0
  236. package/skills/guide/references/operator-api.md +164 -0
  237. package/skills/guide/references/plugin-authoring.md +354 -0
  238. package/skills/guide/references/remote-store-protocol.md +972 -0
  239. package/skills/guide/references/remote-ticket-protocol.md +79 -0
  240. package/skills/guide/references/setup.md +289 -0
  241. package/skills/guide/references/spec-standard.md +63 -0
  242. package/skills/guide/references/ticket-dependencies.md +59 -0
  243. package/skills/harvest/SKILL.md +66 -0
  244. package/skills/harvest-review/SKILL.md +63 -0
  245. package/skills/implement/SKILL.md +82 -0
  246. package/skills/plan/SKILL.md +123 -0
  247. package/skills/plan-review/SKILL.md +121 -0
  248. package/skills/reconcile/SKILL.md +65 -0
  249. package/skills/spec/SKILL.md +132 -0
  250. package/skills/tickets/SKILL.md +139 -0
  251. package/templates/autobuild.toml +36 -0
@@ -0,0 +1,488 @@
1
+ /**
2
+ * FakeForge (SPEC §3.2): in-memory Forge for seam tests. Gate existence and
3
+ * current PR merge state are independent inputs, matching GitHubForge's safety
4
+ * model; every native/direct mutation is journaled.
5
+ */
6
+ import {
7
+ classifyAutoMergeEnable,
8
+ type MergeGatePresence,
9
+ type MergeStateStatus,
10
+ } from '../../kernel/auto-merge'
11
+ import type {
12
+ AutoMergeDeferralReason,
13
+ AutoMergeResult,
14
+ ClosePrResult,
15
+ PrAttachmentHosting,
16
+ PrAttachmentReclaimRequest,
17
+ PrAttachmentUploadRequest,
18
+ Forge,
19
+ PrRef,
20
+ PrState,
21
+ } from '../types'
22
+ import type { HostedPrAttachmentAsset } from '../../ontology'
23
+
24
+ export interface PushRecord {
25
+ workspacePath: string
26
+ branch: string
27
+ }
28
+
29
+ export interface OpenPrRecord {
30
+ workspacePath: string
31
+ head: string
32
+ base: string
33
+ title: string
34
+ body: string
35
+ mergeMessage?: string
36
+ }
37
+
38
+ export interface CommentRecord {
39
+ workspacePath: string
40
+ number: number
41
+ body: string
42
+ }
43
+
44
+ export interface GetPrStateRecord {
45
+ workspacePath: string
46
+ number: number
47
+ }
48
+
49
+ export interface AutoMergeRecord {
50
+ workspacePath: string
51
+ number: number
52
+ enabled: boolean
53
+ /** False on an idempotent retry or a result that leaves native state off. */
54
+ changed: boolean
55
+ }
56
+
57
+ export interface SquashMergeRecord {
58
+ workspacePath: string
59
+ number: number
60
+ expectedHeadSha: string
61
+ }
62
+
63
+ export interface ClosePrRecord {
64
+ workspacePath: string
65
+ number: number
66
+ }
67
+
68
+ export interface DeleteBranchRecord {
69
+ workspacePath: string
70
+ branch: string
71
+ }
72
+
73
+ export type PrAttachmentUploadRecord = PrAttachmentUploadRequest
74
+ export type PrAttachmentReclaimRecord = PrAttachmentReclaimRequest
75
+
76
+ /** Constant sha, or derived from the assigned PR number. */
77
+ export type HeadSha = string | ((number: number) => string)
78
+ export type MergeSha = string | ((number: number) => string)
79
+
80
+ type FakeGateState = MergeGatePresence | AutoMergeDeferralReason
81
+
82
+ export class FakeForge implements Forge {
83
+ readonly name = 'fake'
84
+ readonly prAttachments?: PrAttachmentHosting
85
+
86
+ /** Journals — public so tests assert directly on call order and args. */
87
+ readonly pushes: PushRecord[] = []
88
+ readonly opened: OpenPrRecord[] = []
89
+ readonly comments: CommentRecord[] = []
90
+ readonly getPrStateCalls: GetPrStateRecord[] = []
91
+ readonly autoMergeCalls: AutoMergeRecord[] = []
92
+ readonly squashMergeCalls: SquashMergeRecord[] = []
93
+ readonly closePrCalls: ClosePrRecord[] = []
94
+ readonly deleteBranchCalls: DeleteBranchRecord[] = []
95
+ readonly prAttachmentUploads: PrAttachmentUploadRecord[] = []
96
+ readonly prAttachmentReclaims: PrAttachmentReclaimRecord[] = []
97
+
98
+ private nextNumber = 1
99
+ private nextAttachmentAssetId = 1
100
+ private headSha: HeadSha
101
+ private mergeSha: MergeSha
102
+ private readonly defaultGatePresence: MergeGatePresence
103
+ private readonly prs = new Map<number, PrState>()
104
+ private readonly headShas = new Map<number, string>()
105
+ private readonly mergeStates = new Map<number, MergeStateStatus>()
106
+ private readonly gates = new Map<number, FakeGateState>()
107
+ private readonly autoMerge = new Map<number, boolean>()
108
+ private readonly nativeDeferrals = new Map<number, AutoMergeDeferralReason>()
109
+ private readonly squashErrors = new Map<number, string>()
110
+ private readonly attachmentAssets = new Map<string, HostedPrAttachmentAsset>()
111
+ private readonly attachmentUploadErrors: string[] = []
112
+ private readonly attachmentReclaimErrors: string[] = []
113
+ private readonly closePrErrors: string[] = []
114
+ private readonly deleteBranchErrors: string[] = []
115
+ private readonly publishedBranches = new Set<string>()
116
+
117
+ constructor(
118
+ opts: {
119
+ headSha?: HeadSha
120
+ mergeSha?: MergeSha
121
+ gatePresence?: MergeGatePresence
122
+ /** Hosting is unsupported by default, matching a forge without this
123
+ * optional capability. Tests opt in explicitly. */
124
+ prAttachments?: boolean
125
+ } = {},
126
+ ) {
127
+ this.headSha = opts.headSha ?? ((n) => `sha-${n}`)
128
+ this.mergeSha = opts.mergeSha ?? ((n) => `squash-${n}`)
129
+ // Existing tests model the historical, gated native path unless they opt
130
+ // into the ungated repository scenario explicitly.
131
+ this.defaultGatePresence = opts.gatePresence ?? 'present'
132
+ if (opts.prAttachments === true) {
133
+ this.prAttachments = {
134
+ upload: (request) => this.uploadPrAttachment(request),
135
+ reclaim: (request) => this.reclaimPrAttachment(request),
136
+ }
137
+ }
138
+ }
139
+
140
+ private resolveHeadSha(number: number): string {
141
+ return typeof this.headSha === 'string' ? this.headSha : this.headSha(number)
142
+ }
143
+
144
+ private resolveMergeSha(number: number): string {
145
+ return typeof this.mergeSha === 'string' ? this.mergeSha : this.mergeSha(number)
146
+ }
147
+
148
+ private assertPr(number: number): void {
149
+ if (!this.prs.has(number)) throw new Error(`FakeForge: unknown PR #${number}`)
150
+ }
151
+
152
+ private registerPr(number: number, state: PrState): void {
153
+ this.prs.set(number, state)
154
+ if (!this.headShas.has(number)) this.headShas.set(number, this.resolveHeadSha(number))
155
+ if (!this.autoMerge.has(number)) this.autoMerge.set(number, false)
156
+ if (!this.gates.has(number)) this.gates.set(number, this.defaultGatePresence)
157
+ if (!this.mergeStates.has(number)) {
158
+ const mergeState: MergeStateStatus =
159
+ state.state !== 'open'
160
+ ? 'UNKNOWN'
161
+ : state.mergeable === true
162
+ ? 'CLEAN'
163
+ : state.mergeable === false
164
+ ? 'DIRTY'
165
+ : 'UNKNOWN'
166
+ this.mergeStates.set(number, mergeState)
167
+ }
168
+ }
169
+
170
+ /** Overrides the headSha for PRs opened after this call. */
171
+ setHeadSha(headSha: HeadSha): void {
172
+ this.headSha = headSha
173
+ }
174
+
175
+ /** Drives lifecycle/mergeability independently from gate presence. */
176
+ setPrState(number: number, state: PrState): void {
177
+ const known = this.prs.has(number)
178
+ this.registerPr(number, state)
179
+ if (known && state.state === 'open') {
180
+ this.mergeStates.set(
181
+ number,
182
+ state.mergeable === true ? 'CLEAN' : state.mergeable === false ? 'DIRTY' : 'UNKNOWN',
183
+ )
184
+ }
185
+ }
186
+
187
+ /** Seed the exact GitHub mergeStateStatus without changing mergeability. */
188
+ setMergeStateStatus(number: number, state: MergeStateStatus): void {
189
+ this.assertPr(number)
190
+ this.mergeStates.set(number, state)
191
+ }
192
+
193
+ /** Seed gate existence independently from current merge status/check health. */
194
+ setGatePresence(number: number, presence: MergeGatePresence): void {
195
+ this.assertPr(number)
196
+ this.gates.set(number, presence)
197
+ }
198
+
199
+ /** Seed a typed, fail-closed gate-probe/application deferral. */
200
+ setAutoMergeDeferral(number: number, reason: AutoMergeDeferralReason): void {
201
+ this.assertPr(number)
202
+ this.gates.set(number, reason)
203
+ }
204
+
205
+ /** Compatibility convenience for generic gate-probe/auth/schema failures. */
206
+ setGateProbeError(number: number, message: string): void {
207
+ this.setAutoMergeDeferral(number, { code: 'unproven-gate-state', detail: message })
208
+ }
209
+
210
+ setRulesetsPlanLimitation(
211
+ number: number,
212
+ message = 'rulesets require a paid account plan',
213
+ ): void {
214
+ this.setAutoMergeDeferral(number, { code: 'github-plan-limitation', detail: message })
215
+ }
216
+
217
+ setRepositoryAutoMergeDisabled(
218
+ number: number,
219
+ message = 'GitHub reports allow_auto_merge=false',
220
+ ): void {
221
+ this.setAutoMergeDeferral(number, {
222
+ code: 'repository-auto-merge-disabled',
223
+ detail: message,
224
+ })
225
+ }
226
+
227
+ /** Simulate a typed native auto-merge mutation/application failure. */
228
+ setNativeAutoMergeError(number: number, message: string): void {
229
+ this.assertPr(number)
230
+ this.nativeDeferrals.set(number, { code: 'unproven-gate-state', detail: message })
231
+ }
232
+
233
+ clearNativeAutoMergeError(number: number): void {
234
+ this.assertPr(number)
235
+ this.nativeDeferrals.delete(number)
236
+ }
237
+
238
+ /** Simulate a guarded direct-squash command failure. */
239
+ setSquashMergeError(number: number, message: string): void {
240
+ this.assertPr(number)
241
+ this.squashErrors.set(number, message)
242
+ }
243
+
244
+ /** Fail the next opt-in hosting call; queues permit deterministic mid-set failures. */
245
+ failNextPrAttachmentUpload(message: string): void {
246
+ this.attachmentUploadErrors.push(message)
247
+ }
248
+
249
+ /** Fail the next cleanup call; the following retry resumes normal behavior. */
250
+ failNextPrAttachmentReclaim(message: string): void {
251
+ this.attachmentReclaimErrors.push(message)
252
+ }
253
+
254
+ private attachmentAssetKey(request: PrAttachmentUploadRequest): string {
255
+ return [
256
+ request.target.repository,
257
+ request.target.releaseId,
258
+ request.prUrl,
259
+ request.attachment.artifact.kind,
260
+ request.attachment.artifact.rev,
261
+ request.attachment.filename,
262
+ request.attachment.mediaType,
263
+ request.sha256,
264
+ ].join('\0')
265
+ }
266
+
267
+ private async uploadPrAttachment(
268
+ request: PrAttachmentUploadRequest,
269
+ ): Promise<HostedPrAttachmentAsset> {
270
+ this.prAttachmentUploads.push({
271
+ ...request,
272
+ content: request.content.slice(),
273
+ })
274
+ const error = this.attachmentUploadErrors.shift()
275
+ if (error !== undefined) throw new Error(error)
276
+ const key = this.attachmentAssetKey(request)
277
+ const existing = this.attachmentAssets.get(key)
278
+ if (existing !== undefined) return existing
279
+ const assetId = this.nextAttachmentAssetId++
280
+ const asset: HostedPrAttachmentAsset = {
281
+ provider: 'github-release',
282
+ repository: request.target.repository,
283
+ releaseId: request.target.releaseId,
284
+ assetId,
285
+ url:
286
+ `https://fake.forge/pr-attachments/${assetId}/` +
287
+ encodeURIComponent(request.attachment.filename),
288
+ }
289
+ this.attachmentAssets.set(key, asset)
290
+ return asset
291
+ }
292
+
293
+ private async reclaimPrAttachment(request: PrAttachmentReclaimRequest): Promise<void> {
294
+ this.prAttachmentReclaims.push({ ...request, asset: { ...request.asset } })
295
+ const error = this.attachmentReclaimErrors.shift()
296
+ if (error !== undefined) throw new Error(error)
297
+ for (const [key, asset] of this.attachmentAssets) {
298
+ if (asset.assetId === request.asset.assetId) this.attachmentAssets.delete(key)
299
+ }
300
+ // Missing means it was already deleted: cleanup is idempotent.
301
+ }
302
+
303
+ failNextClosePr(message: string): void {
304
+ this.closePrErrors.push(message)
305
+ }
306
+
307
+ failNextDeleteBranch(message: string): void {
308
+ this.deleteBranchErrors.push(message)
309
+ }
310
+
311
+ hasPublishedBranch(branch: string): boolean {
312
+ return this.publishedBranches.has(branch)
313
+ }
314
+
315
+ /** Move the PR head to exercise --match-head-commit race rejection. */
316
+ setPrHeadSha(number: number, headSha: string): void {
317
+ this.assertPr(number)
318
+ this.headShas.set(number, headSha)
319
+ }
320
+
321
+ /** Seed native state without journaling a forge call. */
322
+ setAutoMergeState(number: number, enabled: boolean): void {
323
+ this.assertPr(number)
324
+ this.autoMerge.set(number, enabled)
325
+ }
326
+
327
+ /** Inspect native state in seam/integration assertions. */
328
+ isAutoMergeEnabled(number: number): boolean {
329
+ this.assertPr(number)
330
+ return this.autoMerge.get(number) ?? false
331
+ }
332
+
333
+ async pushBranch(workspacePath: string, branch: string): Promise<void> {
334
+ this.pushes.push({ workspacePath, branch })
335
+ this.publishedBranches.add(branch)
336
+ }
337
+
338
+ async closePr(workspacePath: string, number: number): Promise<ClosePrResult> {
339
+ this.closePrCalls.push({ workspacePath, number })
340
+ const error = this.closePrErrors.shift()
341
+ if (error !== undefined) throw new Error(error)
342
+ const state = this.prs.get(number)
343
+ if (state === undefined) throw new Error(`FakeForge: unknown PR #${number}`)
344
+ if (state.state === 'merged') return state
345
+ if (state.state === 'open') this.prs.set(number, { state: 'closed' })
346
+ return { state: 'closed' }
347
+ }
348
+
349
+ async deleteBranch(workspacePath: string, branch: string): Promise<void> {
350
+ this.deleteBranchCalls.push({ workspacePath, branch })
351
+ const error = this.deleteBranchErrors.shift()
352
+ if (error !== undefined) throw new Error(error)
353
+ this.publishedBranches.delete(branch)
354
+ }
355
+
356
+ async openPr(opts: {
357
+ workspacePath: string
358
+ head: string
359
+ base: string
360
+ title: string
361
+ body: string
362
+ mergeMessage?: string
363
+ }): Promise<PrRef> {
364
+ // Idempotent by head branch, mirroring GitHubForge (SPEC §8.7).
365
+ for (let i = this.opened.length - 1; i >= 0; i -= 1) {
366
+ const number = i + 1
367
+ if (this.opened[i]!.head !== opts.head) continue
368
+ if (this.prs.get(number)?.state !== 'open') continue
369
+ return {
370
+ number,
371
+ url: `https://fake.forge/pr/${number}`,
372
+ headSha: this.headShas.get(number)!,
373
+ }
374
+ }
375
+ const number = this.nextNumber++
376
+ this.opened.push({ ...opts })
377
+ this.registerPr(number, { state: 'open', mergeable: null })
378
+ return {
379
+ number,
380
+ url: `https://fake.forge/pr/${number}`,
381
+ headSha: this.headShas.get(number)!,
382
+ }
383
+ }
384
+
385
+ async getPrState(workspacePath: string, number: number): Promise<PrState> {
386
+ this.getPrStateCalls.push({ workspacePath, number })
387
+ const state = this.prs.get(number)
388
+ if (!state) throw new Error(`FakeForge: unknown PR #${number}`)
389
+ return state
390
+ }
391
+
392
+ async setAutoMerge(
393
+ workspacePath: string,
394
+ number: number,
395
+ enabled: boolean,
396
+ ): Promise<AutoMergeResult> {
397
+ this.assertPr(number)
398
+ const currentlyEnabled = this.autoMerge.get(number) ?? false
399
+
400
+ if (!enabled || currentlyEnabled) {
401
+ const changed = currentlyEnabled !== enabled
402
+ this.autoMerge.set(number, enabled)
403
+ this.autoMergeCalls.push({ workspacePath, number, enabled, changed })
404
+ return { kind: 'applied' }
405
+ }
406
+
407
+ const gate = this.gates.get(number) ?? this.defaultGatePresence
408
+ if (typeof gate === 'object') {
409
+ this.autoMergeCalls.push({ workspacePath, number, enabled: true, changed: false })
410
+ return { kind: 'deferred', reason: gate }
411
+ }
412
+ const mergeState = this.mergeStates.get(number) ?? 'UNKNOWN'
413
+ const disposition = classifyAutoMergeEnable(mergeState, gate)
414
+ switch (disposition.kind) {
415
+ case 'native': {
416
+ const reason = this.nativeDeferrals.get(number)
417
+ if (reason !== undefined) {
418
+ this.autoMergeCalls.push({ workspacePath, number, enabled: true, changed: false })
419
+ return { kind: 'deferred', reason }
420
+ }
421
+ this.autoMerge.set(number, true)
422
+ this.autoMergeCalls.push({
423
+ workspacePath,
424
+ number,
425
+ enabled: true,
426
+ changed: true,
427
+ })
428
+ return { kind: 'applied' }
429
+ }
430
+ case 'direct':
431
+ this.autoMergeCalls.push({
432
+ workspacePath,
433
+ number,
434
+ enabled: true,
435
+ changed: false,
436
+ })
437
+ return { kind: 'ungated', headSha: this.headShas.get(number)! }
438
+ case 'deferred':
439
+ this.autoMergeCalls.push({
440
+ workspacePath,
441
+ number,
442
+ enabled: true,
443
+ changed: false,
444
+ })
445
+ return { kind: 'deferred' }
446
+ case 'error':
447
+ this.autoMergeCalls.push({ workspacePath, number, enabled: true, changed: false })
448
+ return {
449
+ kind: 'deferred',
450
+ reason: { code: 'unproven-gate-state', detail: disposition.reason },
451
+ }
452
+ }
453
+ }
454
+
455
+ async squashMerge(workspacePath: string, number: number, expectedHeadSha: string): Promise<void> {
456
+ this.assertPr(number)
457
+ const error = this.squashErrors.get(number)
458
+ if (error !== undefined) throw new Error(error)
459
+
460
+ const state = this.prs.get(number)!
461
+ if (state.state !== 'open' || state.mergeable !== true) {
462
+ throw new Error(`FakeForge: PR #${number} is not positively mergeable`)
463
+ }
464
+ const actualHeadSha = this.headShas.get(number)!
465
+ if (actualHeadSha !== expectedHeadSha) {
466
+ throw new Error(
467
+ `FakeForge: PR #${number} head changed (expected ${expectedHeadSha}, found ${actualHeadSha})`,
468
+ )
469
+ }
470
+ const gate = this.gates.get(number) ?? this.defaultGatePresence
471
+ if (typeof gate === 'object') throw new Error(gate.detail)
472
+ if (gate !== 'absent') {
473
+ throw new Error(`FakeForge: PR #${number} is protected by a merge-blocking gate`)
474
+ }
475
+ const disposition = classifyAutoMergeEnable(this.mergeStates.get(number) ?? 'UNKNOWN', gate)
476
+ if (disposition.kind !== 'direct') {
477
+ throw new Error(`FakeForge: PR #${number} is no longer direct-merge eligible`)
478
+ }
479
+
480
+ this.squashMergeCalls.push({ workspacePath, number, expectedHeadSha })
481
+ this.prs.set(number, { state: 'merged', sha: this.resolveMergeSha(number) })
482
+ }
483
+
484
+ async commentOnPr(workspacePath: string, number: number, body: string): Promise<void> {
485
+ this.assertPr(number)
486
+ this.comments.push({ workspacePath, number, body })
487
+ }
488
+ }