@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,223 @@
1
+ /**
2
+ * Operator-session operations behind the operator API (SPEC §7.1.1): the
3
+ * signed-in operator's orchestrator conversations. Every write is attributed
4
+ * to the signed-in operator — the token's user, never a client-supplied
5
+ * identity — and `via` is never set on session events: session events are the
6
+ * delegate's own log, and here the operator IS the actor.
7
+ *
8
+ * Ownership lives here, in the server seam, because the store has no
9
+ * principal concept: a session belongs to its creating operator; other
10
+ * operators of the same repository can read it and cannot write to it. The
11
+ * store stays neutral; the API is the gatekeeper — writes of any kind to an
12
+ * archived session are refusals here, not store errors.
13
+ */
14
+ import { humanActor } from '../events/envelope'
15
+ import type { SessionEventWrite } from '../events/sessions'
16
+ import { reduceSession, type SessionState, type SessionTurn } from '../sessions/reducer'
17
+ import type { StreamRead } from '../store/streams/types'
18
+ import type { BuildStore, SessionRecord } from '../store/types'
19
+ import type { OperatorSandboxService } from './sandbox'
20
+
21
+ export class OperatorSessionError extends Error {
22
+ constructor(
23
+ readonly code: 'not-found' | 'refusal' | 'forbidden',
24
+ message: string,
25
+ ) {
26
+ super(message)
27
+ this.name = 'OperatorSessionError'
28
+ }
29
+ }
30
+
31
+ async function requireSession(
32
+ store: BuildStore,
33
+ repo: string,
34
+ sid: string,
35
+ ): Promise<SessionRecord> {
36
+ const record = await store.getSession(sid)
37
+ if (record === null || record.repo !== repo) {
38
+ throw new OperatorSessionError('not-found', `unknown session "${sid}"`)
39
+ }
40
+ return record
41
+ }
42
+
43
+ function requireOwner(record: SessionRecord, user: string, operation: string): void {
44
+ if (record.operator !== user) {
45
+ throw new OperatorSessionError(
46
+ 'forbidden',
47
+ `session "${record.id}" belongs to operator ${JSON.stringify(record.operator)}; ${JSON.stringify(user)} may not ${operation}`,
48
+ )
49
+ }
50
+ }
51
+
52
+ /** Every append funnels through here: one attribution point, one archived
53
+ * gate. The store has no principal concept, so this is where a token's user
54
+ * meets a session's operator. */
55
+ async function appendAs(
56
+ store: BuildStore,
57
+ record: SessionRecord,
58
+ user: string,
59
+ event: SessionEventWrite,
60
+ ): Promise<void> {
61
+ requireOwner(record, user, `write ${event.type}`)
62
+ const state = reduceSession(await store.getSessionEvents(record.id))
63
+ if (state.status === 'archived') {
64
+ throw new OperatorSessionError('refusal', `session "${record.id}" is archived and read-only`)
65
+ }
66
+ await store.appendSessionEvent(record.id, event)
67
+ }
68
+
69
+ export async function listOperatorSessions(
70
+ store: BuildStore,
71
+ repo: string,
72
+ ): Promise<SessionRecord[]> {
73
+ const sessions = await store.listSessions(repo)
74
+ return sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
75
+ }
76
+
77
+ export async function createOperatorSession(
78
+ store: BuildStore,
79
+ repo: string,
80
+ user: string,
81
+ title?: string,
82
+ ): Promise<SessionRecord> {
83
+ return store.createSession({
84
+ repo,
85
+ operator: user,
86
+ ...(title !== undefined ? { title } : {}),
87
+ })
88
+ }
89
+
90
+ export interface OperatorSessionView {
91
+ session: SessionRecord
92
+ state: SessionState
93
+ turns: SessionTurn[]
94
+ }
95
+
96
+ export async function getOperatorSession(
97
+ store: BuildStore,
98
+ repo: string,
99
+ sid: string,
100
+ ): Promise<OperatorSessionView> {
101
+ const record = await requireSession(store, repo, sid)
102
+ const state = reduceSession(await store.getSessionEvents(record.id))
103
+ return { session: record, state, turns: state.turns }
104
+ }
105
+
106
+ export async function postOperatorMessage(
107
+ store: BuildStore,
108
+ repo: string,
109
+ sid: string,
110
+ user: string,
111
+ text: string,
112
+ ): Promise<void> {
113
+ const record = await requireSession(store, repo, sid)
114
+ await appendAs(store, record, user, {
115
+ actor: humanActor(user),
116
+ type: 'message.posted',
117
+ payload: { text },
118
+ })
119
+ }
120
+
121
+ export async function setOperatorWake(
122
+ store: BuildStore,
123
+ repo: string,
124
+ sid: string,
125
+ user: string,
126
+ globs: string[],
127
+ ): Promise<void> {
128
+ const record = await requireSession(store, repo, sid)
129
+ await appendAs(store, record, user, {
130
+ actor: humanActor(user),
131
+ type: 'session.wake-set',
132
+ payload: { globs },
133
+ })
134
+ }
135
+
136
+ export async function answerOperatorApproval(
137
+ store: BuildStore,
138
+ repo: string,
139
+ sid: string,
140
+ user: string,
141
+ turn: string,
142
+ toolCallId: string,
143
+ decision: 'approve' | 'deny',
144
+ ): Promise<void> {
145
+ const record = await requireSession(store, repo, sid)
146
+ requireOwner(record, user, 'answer an approval')
147
+ const state = reduceSession(await store.getSessionEvents(record.id))
148
+ const pending = state.pendingApproval
149
+ if (
150
+ state.status === 'archived' ||
151
+ pending === undefined ||
152
+ pending.turn !== turn ||
153
+ pending.toolCallId !== toolCallId
154
+ ) {
155
+ throw new OperatorSessionError(
156
+ 'refusal',
157
+ `session "${record.id}" has no pending approval for turn ${JSON.stringify(turn)} tool call ${JSON.stringify(toolCallId)}`,
158
+ )
159
+ }
160
+ await appendAs(store, record, user, {
161
+ actor: humanActor(user),
162
+ type: 'approval.answered',
163
+ payload: { turn, toolCallId, decision },
164
+ })
165
+ }
166
+
167
+ export async function archiveOperatorSession(
168
+ store: BuildStore,
169
+ repo: string,
170
+ sid: string,
171
+ user: string,
172
+ sandbox?: OperatorSandboxService,
173
+ ): Promise<void> {
174
+ const record = await requireSession(store, repo, sid)
175
+ requireOwner(record, user, 'archive')
176
+ const state = reduceSession(await store.getSessionEvents(record.id))
177
+ if (state.status === 'archived') {
178
+ throw new OperatorSessionError('refusal', `session "${record.id}" is already archived`)
179
+ }
180
+ await store.appendSessionEvent(record.id, {
181
+ actor: humanActor(user),
182
+ type: 'session.archived',
183
+ payload: {},
184
+ })
185
+ // When the archive leaves the operator with no open session for this
186
+ // repository, their sandbox environment is released (snapshot purge,
187
+ // journal fact). The service derives the environment via its pure
188
+ // describe — never provisioning — and a never-provisioned operator is a
189
+ // no-op. Contained: a release failure must not fail an accepted archive.
190
+ if (sandbox !== undefined) {
191
+ const sessions = await store.listSessions(repo)
192
+ const open: SessionRecord[] = []
193
+ for (const session of sessions) {
194
+ if (session.operator !== user) continue
195
+ const sessionState = reduceSession(await store.getSessionEvents(session.id))
196
+ if (sessionState.status !== 'archived') open.push(session)
197
+ }
198
+ if (open.length === 0) {
199
+ try {
200
+ await sandbox.release(user, { repo })
201
+ } catch {
202
+ // The archive already succeeded; the environment stays journaled and
203
+ // the dispatcher's idle settlement stops it later.
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ export async function readOperatorTurnStream(
210
+ store: BuildStore,
211
+ repo: string,
212
+ sid: string,
213
+ turn: string,
214
+ opts?: { since?: number; waitSeconds?: number },
215
+ ): Promise<StreamRead> {
216
+ const record = await requireSession(store, repo, sid)
217
+ const state = reduceSession(await store.getSessionEvents(record.id))
218
+ const target = state.turns.find((candidate) => candidate.turn === turn)
219
+ if (target === undefined) {
220
+ throw new OperatorSessionError('not-found', `unknown turn "${turn}" in session "${record.id}"`)
221
+ }
222
+ return store.readStream(target.stream, opts)
223
+ }
@@ -0,0 +1,177 @@
1
+ import type { Config } from '../config/schema'
2
+ import { reduceBuild } from '../kernel/reducer'
3
+ import {
4
+ changeBlockers,
5
+ createTicket,
6
+ moveTicket,
7
+ requireTicket,
8
+ updateTicket,
9
+ } from '../ports/tickets/operations'
10
+ import type { DependencyState, Ticket, TicketSource, TicketUpdate } from '../ports/types'
11
+ import type { BuildStore } from '../store/types'
12
+ import { defaultTriageState } from '../processes/dispatcher'
13
+ import { effectiveConfig, OperatorQueryError } from './query'
14
+
15
+ export interface OperatorTicketContext {
16
+ teamKey: string
17
+ claimedState?: string
18
+ createState?: string
19
+ }
20
+
21
+ export interface OperatorTicketBackend {
22
+ sourceFor(context: OperatorTicketContext): TicketSource | Promise<TicketSource>
23
+ statesFor(context: OperatorTicketContext, source: TicketSource): string[] | Promise<string[]>
24
+ }
25
+
26
+ export interface OperatorTicketQueue {
27
+ states: string[]
28
+ tickets: Ticket[]
29
+ diagnostics: string[]
30
+ criteria: { state?: string; labels?: string[] }
31
+ /** Effective repository lifecycle names; clients must not invent provider states. */
32
+ triageState: string
33
+ readyState: string
34
+ }
35
+
36
+ export interface OperatorTicketBuild {
37
+ slug: string
38
+ status: string
39
+ updatedAt: string
40
+ link: string
41
+ }
42
+
43
+ export interface OperatorTicketDetail {
44
+ ticket: Ticket
45
+ blockers: DependencyState[]
46
+ build: OperatorTicketBuild | null
47
+ }
48
+
49
+ function contextFor(config: Config, repo: string): OperatorTicketContext {
50
+ const tickets = config.tickets
51
+ if (tickets.teamKey === undefined) {
52
+ throw new OperatorQueryError(
53
+ 'effective-config-unavailable',
54
+ `effective config unavailable for repository "${repo}": [tickets].teamKey is required for hosted ticket access`,
55
+ )
56
+ }
57
+ return {
58
+ teamKey: tickets.teamKey,
59
+ ...(tickets.claimedState !== undefined ? { claimedState: tickets.claimedState } : {}),
60
+ ...(tickets.createState !== undefined ? { createState: tickets.createState } : {}),
61
+ }
62
+ }
63
+
64
+ export async function openOperatorTickets(opts: {
65
+ store: BuildStore
66
+ repo: string
67
+ backend: OperatorTicketBackend
68
+ }): Promise<{ config: Config; context: OperatorTicketContext; source: TicketSource }> {
69
+ const { config } = await effectiveConfig(opts.store, opts.repo)
70
+ const context = contextFor(config, opts.repo)
71
+ return { config, context, source: await opts.backend.sourceFor(context) }
72
+ }
73
+
74
+ export async function listOperatorTickets(opts: {
75
+ store: BuildStore
76
+ repo: string
77
+ backend: OperatorTicketBackend
78
+ state?: string
79
+ labels?: string[]
80
+ }): Promise<OperatorTicketQueue> {
81
+ const { config, context, source } = await openOperatorTickets(opts)
82
+ const triageState = defaultTriageState(config)
83
+ const readyState = config.tickets.readyState
84
+ // The operator surface is a grooming queue. Unlike the CLI's ticket list,
85
+ // labels narrow the default state instead of replacing its lifecycle gate.
86
+ const criteria = {
87
+ state: opts.state ?? triageState,
88
+ ...(opts.labels !== undefined ? { labels: opts.labels } : {}),
89
+ }
90
+ const [listing, states] = await Promise.all([
91
+ source.listReady(criteria),
92
+ opts.backend.statesFor(context, source),
93
+ ])
94
+ return {
95
+ states,
96
+ tickets: listing.tickets,
97
+ diagnostics: listing.diagnostics,
98
+ criteria,
99
+ triageState,
100
+ readyState,
101
+ }
102
+ }
103
+
104
+ async function matchingBuild(
105
+ store: BuildStore,
106
+ repo: string,
107
+ id: string,
108
+ ): Promise<OperatorTicketBuild | null> {
109
+ const matches = (await store.listBuilds()).filter(
110
+ (record) => record.repo === repo && record.ticket?.id === id,
111
+ )
112
+ const projected = await Promise.all(
113
+ matches.map(async (record) => ({
114
+ record,
115
+ state: reduceBuild(await store.getEvents(record.slug)),
116
+ })),
117
+ )
118
+ projected.sort((a, b) => {
119
+ const aActive = a.state.status !== 'done' && a.state.status !== 'aborted'
120
+ const bActive = b.state.status !== 'done' && b.state.status !== 'aborted'
121
+ if (aActive !== bActive) return aActive ? -1 : 1
122
+ return b.record.updatedAt.localeCompare(a.record.updatedAt)
123
+ })
124
+ const found = projected[0]
125
+ return found
126
+ ? {
127
+ slug: found.record.slug,
128
+ status: found.state.status,
129
+ updatedAt: found.record.updatedAt,
130
+ link: `#build-${encodeURIComponent(found.record.slug)}`,
131
+ }
132
+ : null
133
+ }
134
+
135
+ export async function getOperatorTicket(opts: {
136
+ store: BuildStore
137
+ repo: string
138
+ backend: OperatorTicketBackend
139
+ id: string
140
+ }): Promise<OperatorTicketDetail> {
141
+ const { source } = await openOperatorTickets(opts)
142
+ const ticket = await requireTicket(source, opts.id)
143
+ const blockers = await source.dependencyStates(ticket.blockedBy ?? [])
144
+ return { ticket, blockers, build: await matchingBuild(opts.store, opts.repo, opts.id) }
145
+ }
146
+
147
+ export async function mutateOperatorTicket(opts: {
148
+ store: BuildStore
149
+ repo: string
150
+ backend: OperatorTicketBackend
151
+ operation:
152
+ | {
153
+ kind: 'create'
154
+ title: string
155
+ body: string
156
+ labels?: string[]
157
+ state?: string
158
+ blockedBy?: string[]
159
+ }
160
+ | { kind: 'update'; id: string; patch: TicketUpdate }
161
+ | { kind: 'move'; id: string; state: string }
162
+ | { kind: 'block' | 'unblock'; id: string; blockerIds: string[] }
163
+ }): Promise<OperatorTicketDetail> {
164
+ const { source } = await openOperatorTickets(opts)
165
+ const operation = opts.operation
166
+ if (operation.kind === 'update') await requireTicket(source, operation.id)
167
+ const ticket =
168
+ operation.kind === 'create'
169
+ ? await createTicket(source, operation)
170
+ : operation.kind === 'update'
171
+ ? (await updateTicket(source, operation.id, operation.patch))!
172
+ : operation.kind === 'move'
173
+ ? await moveTicket(source, operation.id, operation.state)
174
+ : await changeBlockers(source, operation.id, operation.blockerIds, operation.kind)
175
+ const blockers = await source.dependencyStates(ticket.blockedBy ?? [])
176
+ return { ticket, blockers, build: await matchingBuild(opts.store, opts.repo, ticket.ref.id) }
177
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Stable plugin-authoring surface. Internal source paths are not public API;
3
+ * plugins should import only `@defrex/autobuild/plugin-sdk` (normally with
4
+ * `import type`) and run these same contracts against their adapters.
5
+ */
6
+ export type * from '../ports/types'
7
+ export type * from '../ports/workspace/build-execution'
8
+ export type * from '../store/types'
9
+ export type {
10
+ RuntimeRegistration,
11
+ RuntimeRegistry,
12
+ SessionStreamInfo,
13
+ } from '../ports/runner/runtime'
14
+ export {
15
+ STREAM_PART_MAX_BYTES,
16
+ abortPart,
17
+ errorPart,
18
+ finishPart,
19
+ finishStepPart,
20
+ promptPart,
21
+ reasoningDeltaPart,
22
+ reasoningEndPart,
23
+ reasoningStartPart,
24
+ sessionPart,
25
+ startPart,
26
+ startStepPart,
27
+ textDeltaPart,
28
+ textEndPart,
29
+ textStartPart,
30
+ toolInputPart,
31
+ toolOutputPart,
32
+ truncationPart,
33
+ } from '../ports/runner/stream-parts'
34
+ export type {
35
+ OneShotCompletion,
36
+ OneShotCompletionInput,
37
+ OneShotCompletionResult,
38
+ } from '../ports/runner/one-shot'
39
+
40
+ export {
41
+ CONTRACT_FOLLOW_UP,
42
+ CONTRACT_INVOCATION,
43
+ CONTRACT_ONE_SHOT_PROMPT,
44
+ CONTRACT_ONE_SHOT_TEXT,
45
+ CONTRACT_PERMANENT_FAILURE,
46
+ CONTRACT_RETRYABLE_FAILURE,
47
+ CONTRACT_SKILL,
48
+ describeAgentRunnerContract,
49
+ } from '../ports/runner/contract'
50
+ export type {
51
+ AgentRunnerContractFactory,
52
+ AgentRunnerContractHarness,
53
+ AgentRunnerContractOneShotHarness,
54
+ AgentRunnerContractOneShotObservation,
55
+ AgentRunnerContractScenario,
56
+ AgentRunnerContractTurnObservation,
57
+ } from '../ports/runner/contract'
58
+
59
+ export {
60
+ PLUGIN_API_VERSION,
61
+ PluginApiCompatibilityError,
62
+ pluginApiCompatibility,
63
+ pluginManifestSchema,
64
+ parsePluginManifest,
65
+ } from '../plugins/manifest'
66
+ export type {
67
+ AgentRuntimePluginFactory,
68
+ AgentRuntimePluginRegistration,
69
+ AutobuildPluginManifest,
70
+ ForgePluginFactory,
71
+ ForgePluginRegistration,
72
+ PluginAdapterRegistration,
73
+ PluginApiCompatibility,
74
+ PluginContractDescriptor,
75
+ PluginFactory,
76
+ PluginFactoryContext,
77
+ TicketSourcePluginDescriptor,
78
+ TicketSourcePluginFactory,
79
+ TicketSourcePluginRegistration,
80
+ WorkspaceProviderPluginFactory,
81
+ WorkspaceProviderPluginRegistration,
82
+ } from '../plugins/manifest'
83
+
84
+ export {
85
+ describeTicketSourceContract,
86
+ CONTRACT_TICKET_BODY,
87
+ contractIdempotencyKey,
88
+ contractLabelName,
89
+ contractTicketTitle,
90
+ } from '../ports/tickets/contract'
91
+ export type {
92
+ TicketSourceContractFactory,
93
+ TicketSourceContractHarness,
94
+ TicketSourceContractStates,
95
+ } from '../ports/tickets/contract'
96
+
97
+ export { describeWorkspaceProviderContract } from '../ports/workspace/contract'
98
+ export type {
99
+ WorkspaceProviderContractFactory,
100
+ WorkspaceProviderContractHarness,
101
+ } from '../ports/workspace/contract'
102
+
103
+ export { describeForgeContract } from '../ports/forge/contract'
104
+ export type {
105
+ ForgeContractControls,
106
+ ForgeContractFactory,
107
+ ForgeContractFactoryOptions,
108
+ ForgeContractHarness,
109
+ } from '../ports/forge/contract'
110
+
111
+ export {
112
+ describeBlobStoreContract,
113
+ describeBuildStoreContract,
114
+ CONTRACT_T0,
115
+ ISO_TS,
116
+ buildCreatedWrite,
117
+ harvestStartedWrite,
118
+ messagePostedWrite,
119
+ planCompletedWrite,
120
+ sampleBuildInput,
121
+ sampleEventWrite,
122
+ } from '../store/contract'
123
+ export type {
124
+ BlobStoreFactory,
125
+ BlobStoreHarness,
126
+ BuildStoreFactory,
127
+ BuildStoreHarness,
128
+ } from '../store/contract'
129
+
130
+ export { FakeTicketSource } from '../ports/tickets/fake'
131
+ export { validateTicketUpdate } from '../ports/tickets/update'
132
+ export { FakeWorkspaceProvider } from '../ports/workspace/fake'
133
+ export { FakeForge } from '../ports/forge/fake'
134
+ export { ScriptedAgentRunner } from '../ports/runner/fake'
135
+ export { MemoryBlobStore, MemoryBuildStore } from '../store/memory'
136
+
137
+ // Runtime primitives needed by out-of-tree BuildStore adapters.
138
+ export { validateEventWrite } from '../events/catalog'
139
+ export type { AbEvent, EventEnvelope, EventWrite } from '../events/catalog'
140
+ export type { EventType } from '../events/payloads'
141
+ export { validateRepositoryEventWrite } from '../events/repository'
142
+ export type {
143
+ RepositoryEvent,
144
+ RepositoryEventEnvelope,
145
+ RepositoryEventType,
146
+ RepositoryEventWrite,
147
+ } from '../events/repository'
148
+ export { BuildScopeError, createBuildScopedStore } from '../store/build-scope'
149
+ export { createSessionStreamSink } from '../store/streams/session-writer'
150
+ export { pollingSubscribe } from '../store/subscribe'
151
+ export { assembleUIMessageDocument } from '../store/streams/assemble'
152
+ export { readStreamWithWait, STREAM_WAIT_POLL_MS } from '../store/streams/wait'
153
+ export {
154
+ clampWaitSeconds,
155
+ MAX_STREAM_WAIT_SECONDS,
156
+ serializedBatchSize,
157
+ STREAM_BATCH_MAX_BYTES,
158
+ STREAM_FORMAT,
159
+ StreamBatchTooLargeError,
160
+ StreamClosedError,
161
+ streamArtifactInput,
162
+ validateStreamParts,
163
+ } from '../store/streams/types'
164
+ export type {
165
+ StreamArtifactRef,
166
+ StreamChunk,
167
+ StreamOutcome,
168
+ StreamPart,
169
+ StreamRead,
170
+ StreamRecord,
171
+ StreamScope,
172
+ StreamStatus,
173
+ } from '../store/streams/types'
174
+ export { contentHash, systemClock, toBytes, validateExpectedSeq } from '../store/types'
@@ -0,0 +1,74 @@
1
+ import { join } from 'node:path'
2
+ import { loadConfig } from '../config/load'
3
+ import { describeForgeContract, type ForgeContractFactory } from '../ports/forge/contract'
4
+ import {
5
+ describeAgentRunnerContract,
6
+ type AgentRunnerContractFactory,
7
+ } from '../ports/runner/contract'
8
+ import {
9
+ describeTicketSourceContract,
10
+ type TicketSourceContractFactory,
11
+ } from '../ports/tickets/contract'
12
+ import {
13
+ describeWorkspaceProviderContract,
14
+ type WorkspaceProviderContractFactory,
15
+ } from '../ports/workspace/contract'
16
+ import { CONTRACT_ADAPTER_ENV, CONTRACT_PORT_ENV, CONTRACT_REPO_ENV } from './contract-env'
17
+ import { loadPlugins } from './load'
18
+ import { PLUGIN_PORTS, type PluginPort } from './registry'
19
+
20
+ function requiredEnv(name: string): string {
21
+ const value = process.env[name]
22
+ if (value === undefined || value.trim() === '') {
23
+ throw new Error(`plugin contract runner requires private environment ${name}`)
24
+ }
25
+ return value
26
+ }
27
+
28
+ const repoRoot = requiredEnv(CONTRACT_REPO_ENV)
29
+ const rawPort = requiredEnv(CONTRACT_PORT_ENV)
30
+ const adapter = requiredEnv(CONTRACT_ADAPTER_ENV)
31
+ if (!PLUGIN_PORTS.includes(rawPort as PluginPort)) {
32
+ throw new Error(
33
+ `${CONTRACT_PORT_ENV} must be one of ${PLUGIN_PORTS.join(', ')}; got "${rawPort}"`,
34
+ )
35
+ }
36
+ const port = rawPort as PluginPort
37
+ const config = await loadConfig(join(repoRoot, 'autobuild.toml'))
38
+ const registry = await loadPlugins(config.plugins, repoRoot)
39
+ const registration = registry.registration(port, adapter)
40
+ if (registration === undefined) {
41
+ throw new Error(`unknown ${port} adapter "${adapter}"`)
42
+ }
43
+ if (registration.contract === undefined) {
44
+ throw new Error(`${port} adapter "${adapter}" has no contract fixture descriptor`)
45
+ }
46
+ if (registration.contract.live === true && process.env.AB_RUN_LIVE_PORT_CONTRACTS !== '1') {
47
+ throw new Error('live plugin contracts require AB_RUN_LIVE_PORT_CONTRACTS=1')
48
+ }
49
+
50
+ const contractFactory = await registration.contract.factory({
51
+ config: {},
52
+ env: process.env,
53
+ repoRoot,
54
+ })
55
+ if (typeof contractFactory !== 'function') {
56
+ throw new Error(
57
+ `${port} adapter "${adapter}" contract.factory must return the shared suite's harness factory`,
58
+ )
59
+ }
60
+
61
+ switch (port) {
62
+ case 'ticket-source':
63
+ describeTicketSourceContract(adapter, contractFactory as TicketSourceContractFactory)
64
+ break
65
+ case 'agent-runtime':
66
+ describeAgentRunnerContract(adapter, contractFactory as AgentRunnerContractFactory)
67
+ break
68
+ case 'workspace-provider':
69
+ describeWorkspaceProviderContract(adapter, contractFactory as WorkspaceProviderContractFactory)
70
+ break
71
+ case 'forge':
72
+ describeForgeContract(adapter, contractFactory as ForgeContractFactory)
73
+ break
74
+ }
@@ -0,0 +1,3 @@
1
+ export const CONTRACT_REPO_ENV = 'AB_PLUGIN_CONTRACT_REPO'
2
+ export const CONTRACT_PORT_ENV = 'AB_PLUGIN_CONTRACT_PORT'
3
+ export const CONTRACT_ADAPTER_ENV = 'AB_PLUGIN_CONTRACT_ADAPTER'