@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,203 @@
1
+ /**
2
+ * The open-map preservation contract: author-chosen keys → strictly validated
3
+ * values, with EVERY declared key surviving validation verbatim.
4
+ *
5
+ * `z.record` and `z.looseObject` build their result by assignment, so a
6
+ * perfectly valid entry named `__proto__` invokes the legacy
7
+ * `Object.prototype.__proto__` setter instead of creating an own key. Parsing
8
+ * SUCCEEDS and the entry vanishes — never resolved, never selectable, and
9
+ * invisible to whatever diagnostic exists to catch dead configuration. A silent
10
+ * drop is the one outcome an otherwise strict validator must never produce.
11
+ *
12
+ * So these keep the key intact end to end: own descriptors in, `defineProperty`
13
+ * out, null prototype on the result. Consumers can then read an author-chosen
14
+ * key off these maps without `Object.hasOwn` ceremony, and a name nobody
15
+ * declared answers `undefined` rather than an inherited object member.
16
+ *
17
+ * The entry boundary keeps the other half of the same promise: it must not
18
+ * narrow what the value schema said. A value that validates *to* nothing is
19
+ * kept — success is carried in the result's `ok`, never inferred from the value,
20
+ * or a legitimately absent value would be read as a rejection and produce the
21
+ * silent drop above from the other direction. And a rejection reaches the map's
22
+ * caller as validation reported it — every issue forwarded whole, with only its
23
+ * path prefixed — because re-encoding an issue as prose discards the structure a
24
+ * diagnostic needs to say what was actually wrong (see `../zod-issues`).
25
+ *
26
+ * Two surfaces consume this. `autobuild.toml`'s open maps ([commands],
27
+ * [roles], [workspace.config], and the named [verify.<step>] / [finalize.<step>]
28
+ * table sets) are one; the plugin manifest's adapter maps (`ticketSources`,
29
+ * `agentRuntimes`, `workspaceProviders`, `forges`) are the other, whose keys are
30
+ * likewise author-chosen — there, by the plugin author rather than the operator.
31
+ */
32
+ import { z } from 'zod'
33
+ import { forwardIssues, type IssueSink } from './zod-issues'
34
+
35
+ /** How an entry name is validated. Autobuild-owned maps must be able to address
36
+ * every entry, so a nameless one is dead configuration; a map whose names belong
37
+ * to a plugin is not Autobuild's to narrow. */
38
+ export type OpenMapKeyPolicy = 'nonempty' | 'nonblank' | 'any'
39
+
40
+ export interface OpenMapOptions {
41
+ keys?: OpenMapKeyPolicy
42
+ /** What the container is called when the input is not one. */
43
+ shape?: string
44
+ }
45
+
46
+ const DEFAULT_SHAPE = 'a table of named entries'
47
+
48
+ function keyIsValid(key: string, policy: OpenMapKeyPolicy): boolean {
49
+ switch (policy) {
50
+ case 'nonempty':
51
+ return key.length > 0
52
+ case 'nonblank':
53
+ return key.trim().length > 0
54
+ case 'any':
55
+ return true
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Whether a container is a plain record of named entries — a parsed TOML table
61
+ * (null prototype), an object literal, or any object built on top of those.
62
+ *
63
+ * The exclusions are load-bearing rather than pedantic. A `Map`, a `Set`, a
64
+ * `Date`, an array, or a class instance has no own string-keyed entries to
65
+ * collect, so accepting one would produce an EMPTY map from an input that
66
+ * plainly meant to declare something — the same silent drop this module exists
67
+ * to prevent, just arrived at from the other direction. `null` is rejected for
68
+ * the same reason: absence is spelled by omission, which `openMap`'s
69
+ * `.prefault({})` turns into `{}` before this ever runs.
70
+ *
71
+ * The decision itself is `z.record`'s, deliberately: this module replaced that
72
+ * schema on the manifest surface, and a container it accepted must keep
73
+ * parsing. Hence "was it built by something other than a plain-object
74
+ * constructor" rather than an identity test against the local `Object` — which
75
+ * would turn away a record whose prototype declares a non-function
76
+ * `constructor`, one whose constructor prototype owns `isPrototypeOf`, and
77
+ * every plain object arriving from another realm.
78
+ *
79
+ * The one departure: `constructor` is read off the PROTOTYPE, never off
80
+ * `input`. Reading it off the input is what `z.record` does, and it is the same
81
+ * prototype hazard this module exists to close — a map declaring an adapter
82
+ * named `constructor` shadows the property the check depends on, and the whole
83
+ * container is rejected over an entry that is merely unusually named. Starting
84
+ * one link up cannot be shadowed by any declared entry, and answers identically
85
+ * for every container that does not declare that name.
86
+ */
87
+ function isRecord(input: unknown): input is object {
88
+ if (typeof input !== 'object' || input === null) return false
89
+ const prototype = Object.getPrototypeOf(input)
90
+ if (prototype === null) return true
91
+ const builder = (prototype as { constructor?: unknown }).constructor
92
+ if (typeof builder !== 'function') return true
93
+ const builderPrototype: unknown = (builder as { prototype?: unknown }).prototype
94
+ if (typeof builderPrototype !== 'object' || builderPrototype === null) return false
95
+ return Object.hasOwn(builderPrototype, 'isPrototypeOf')
96
+ }
97
+
98
+ /**
99
+ * The own ENUMERABLE STRING entries of a parsed container, read BY DESCRIPTOR.
100
+ * `input[key]` is wrong here: on any object that has a prototype, reading
101
+ * `"__proto__"` answers that prototype rather than the declared entry.
102
+ *
103
+ * The key set is `z.record`'s — own and enumerable — because a container that
104
+ * schema collected must still be collected the same way. Non-enumerable
105
+ * properties are internal bookkeeping rather than declarations, and are skipped
106
+ * in both.
107
+ *
108
+ * An own enumerable SYMBOL is an error rather than a skip. `z.record` ran every
109
+ * own key through its string key schema, so a symbol failed validation and the
110
+ * whole container was invalid; passing over it silently instead would be the
111
+ * exact drop this module exists to prevent, applied to the one key shape that
112
+ * can never name an entry in the map it produces.
113
+ *
114
+ * `label` is rendered verbatim into the diagnostic — `'[roles]'` for a TOML
115
+ * table, `'forges'` for a manifest map.
116
+ */
117
+ export function ownEntries(
118
+ input: unknown,
119
+ label: string,
120
+ ctx: IssueSink,
121
+ shape?: string,
122
+ ): [string, unknown][] {
123
+ if (!isRecord(input)) {
124
+ ctx.addIssue({ code: 'custom', message: `${label} must be ${shape ?? DEFAULT_SHAPE}` })
125
+ return []
126
+ }
127
+ for (const symbol of Object.getOwnPropertySymbols(input)) {
128
+ if (Object.getOwnPropertyDescriptor(input, symbol)?.enumerable !== true) continue
129
+ ctx.addIssue({
130
+ code: 'custom',
131
+ message: `${label} entry names must be strings; ${String(symbol)} cannot name an entry`,
132
+ })
133
+ }
134
+ const entries: [string, unknown][] = []
135
+ for (const key of Object.getOwnPropertyNames(input)) {
136
+ const descriptor = Object.getOwnPropertyDescriptor(input, key)
137
+ if (descriptor === undefined || !descriptor.enumerable) continue
138
+ // TOML only ever produces data properties, but a manifest is real
139
+ // JavaScript: `forges: { get acme() { … } }` is a valid declaration, and
140
+ // `descriptor.value` is `undefined` for it. Invoking the getter still never
141
+ // consults the prototype chain.
142
+ entries.push([key, 'value' in descriptor ? descriptor.value : descriptor.get?.call(input)])
143
+ }
144
+ return entries
145
+ }
146
+
147
+ /** Record a named entry so the key survives verbatim. Plain assignment is what
148
+ * loses `__proto__`; `defineProperty` on a null-prototype target cannot. */
149
+ export function defineEntry<T>(entries: Record<string, T>, key: string, value: T): void {
150
+ Object.defineProperty(entries, key, {
151
+ value,
152
+ enumerable: true,
153
+ writable: true,
154
+ configurable: true,
155
+ })
156
+ }
157
+
158
+ /** Whether one entry's value validated, and what it validated to. Success is
159
+ * explicit rather than read off the value: a schema may legitimately validate to
160
+ * `undefined`, and that entry is kept like any other. */
161
+ export type ParsedEntry<T> = { ok: true; value: T } | { ok: false }
162
+
163
+ /** Validate one open-map value, forwarding every issue under its own key. */
164
+ export function parseEntry<T>(
165
+ schema: z.ZodType<T>,
166
+ raw: unknown,
167
+ key: string,
168
+ ctx: IssueSink,
169
+ ): ParsedEntry<T> {
170
+ const parsed = schema.safeParse(raw)
171
+ if (parsed.success) return { ok: true, value: parsed.data }
172
+ forwardIssues(parsed.error.issues, ctx, [key])
173
+ return { ok: false }
174
+ }
175
+
176
+ /**
177
+ * An open map: author-chosen keys → strictly validated values, keys preserved.
178
+ *
179
+ * Preservation is the shared contract; the key vocabulary is not. An
180
+ * Autobuild-owned map names entries Autobuild itself must be able to address —
181
+ * a role to dispatch, a command to run — so a nameless entry there is dead
182
+ * configuration and an error. A plugin-owned pass-through map is not
183
+ * Autobuild's to narrow, and a map of adapter names is narrower still: a name
184
+ * made only of whitespace is unaddressable from configuration.
185
+ */
186
+ export function openMap<T>(label: string, valueSchema: z.ZodType<T>, opts: OpenMapOptions = {}) {
187
+ const keys = opts.keys ?? 'nonempty'
188
+ return z
189
+ .unknown()
190
+ .transform((input, ctx): Record<string, T> => {
191
+ const entries: Record<string, T> = Object.create(null)
192
+ for (const [key, raw] of ownEntries(input, label, ctx, opts.shape)) {
193
+ if (!keyIsValid(key, keys)) {
194
+ ctx.addIssue({ code: 'custom', message: `${label} entry names must be ${keys}` })
195
+ continue
196
+ }
197
+ const parsed = parseEntry(valueSchema, raw, key, ctx)
198
+ if (parsed.ok) defineEntry(entries, key, parsed.value)
199
+ }
200
+ return entries
201
+ })
202
+ .prefault({})
203
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * The checked-in risk table for the agent tool registry (operator/registry.ts).
3
+ *
4
+ * MCP annotations are per-tool hints, never per-argument, so a tool whose
5
+ * action set *may* destroy work carries `destructiveHint: true` for the whole
6
+ * tool, and each destructive action value is additionally named in the input
7
+ * field's description so a model still sees the per-value risk. Today exactly
8
+ * three tools may destroy work: `builds.control` (abort, discard),
9
+ * `builds.answer` (revise-spec), and `tickets.move` (a move into the
10
+ * configured ready state).
11
+ *
12
+ * The registry entries and the contract suite both read this table, so a
13
+ * change to any tool's risk class is a one-line visible diff this suite
14
+ * enforces. `approval` is the registry's own vocabulary (`'never'` for reads,
15
+ * `'default'` for mutators): the registry carries the class, and each binding
16
+ * decides what to do with it — the registry itself enforces only that a
17
+ * mutator refuses to run without an attributed identity.
18
+ */
19
+
20
+ export interface ToolAnnotationsTableEntry {
21
+ readOnlyHint: boolean
22
+ destructiveHint: boolean
23
+ idempotentHint: boolean
24
+ approval: 'never' | 'default'
25
+ }
26
+
27
+ /** The exact version-one tool names; the registry's closed table must match. */
28
+ export const OPERATOR_TOOL_ANNOTATIONS = {
29
+ 'builds.list': {
30
+ readOnlyHint: true,
31
+ destructiveHint: false,
32
+ idempotentHint: true,
33
+ approval: 'never',
34
+ },
35
+ 'builds.get': {
36
+ readOnlyHint: true,
37
+ destructiveHint: false,
38
+ idempotentHint: true,
39
+ approval: 'never',
40
+ },
41
+ 'builds.events': {
42
+ readOnlyHint: true,
43
+ destructiveHint: false,
44
+ idempotentHint: true,
45
+ approval: 'never',
46
+ },
47
+ 'builds.artifact': {
48
+ readOnlyHint: true,
49
+ destructiveHint: false,
50
+ idempotentHint: true,
51
+ approval: 'never',
52
+ },
53
+ 'builds.control': {
54
+ readOnlyHint: false,
55
+ destructiveHint: true,
56
+ idempotentHint: false,
57
+ approval: 'default',
58
+ },
59
+ 'builds.answer': {
60
+ readOnlyHint: false,
61
+ destructiveHint: true,
62
+ idempotentHint: false,
63
+ approval: 'default',
64
+ },
65
+ 'repository.status': {
66
+ readOnlyHint: true,
67
+ destructiveHint: false,
68
+ idempotentHint: true,
69
+ approval: 'never',
70
+ },
71
+ 'repository.settings': {
72
+ readOnlyHint: false,
73
+ destructiveHint: false,
74
+ idempotentHint: false,
75
+ approval: 'default',
76
+ },
77
+ 'repository.bulk_control': {
78
+ readOnlyHint: false,
79
+ destructiveHint: false,
80
+ idempotentHint: false,
81
+ approval: 'default',
82
+ },
83
+ 'harvest.status': {
84
+ readOnlyHint: true,
85
+ destructiveHint: false,
86
+ idempotentHint: true,
87
+ approval: 'never',
88
+ },
89
+ 'harvest.control': {
90
+ readOnlyHint: false,
91
+ destructiveHint: false,
92
+ idempotentHint: false,
93
+ approval: 'default',
94
+ },
95
+ 'tickets.list': {
96
+ readOnlyHint: true,
97
+ destructiveHint: false,
98
+ idempotentHint: true,
99
+ approval: 'never',
100
+ },
101
+ 'tickets.get': {
102
+ readOnlyHint: true,
103
+ destructiveHint: false,
104
+ idempotentHint: true,
105
+ approval: 'never',
106
+ },
107
+ 'tickets.create': {
108
+ readOnlyHint: false,
109
+ destructiveHint: false,
110
+ idempotentHint: false,
111
+ approval: 'default',
112
+ },
113
+ 'tickets.update': {
114
+ readOnlyHint: false,
115
+ destructiveHint: false,
116
+ idempotentHint: false,
117
+ approval: 'default',
118
+ },
119
+ 'tickets.block': {
120
+ readOnlyHint: false,
121
+ destructiveHint: false,
122
+ idempotentHint: false,
123
+ approval: 'default',
124
+ },
125
+ 'tickets.unblock': {
126
+ readOnlyHint: false,
127
+ destructiveHint: false,
128
+ idempotentHint: false,
129
+ approval: 'default',
130
+ },
131
+ 'tickets.move': {
132
+ readOnlyHint: false,
133
+ destructiveHint: true,
134
+ idempotentHint: false,
135
+ approval: 'default',
136
+ },
137
+ 'notes.read': {
138
+ readOnlyHint: true,
139
+ destructiveHint: false,
140
+ idempotentHint: true,
141
+ approval: 'never',
142
+ },
143
+ 'notes.write': {
144
+ readOnlyHint: false,
145
+ destructiveHint: false,
146
+ idempotentHint: false,
147
+ approval: 'default',
148
+ },
149
+ 'sandbox.exec': {
150
+ readOnlyHint: false,
151
+ destructiveHint: false,
152
+ idempotentHint: false,
153
+ approval: 'default',
154
+ },
155
+ 'sandbox.start': {
156
+ readOnlyHint: false,
157
+ destructiveHint: false,
158
+ idempotentHint: false,
159
+ approval: 'default',
160
+ },
161
+ 'sandbox.wait': {
162
+ readOnlyHint: false,
163
+ destructiveHint: false,
164
+ idempotentHint: false,
165
+ approval: 'default',
166
+ },
167
+ 'sandbox.read_file': {
168
+ readOnlyHint: false,
169
+ destructiveHint: false,
170
+ idempotentHint: false,
171
+ approval: 'default',
172
+ },
173
+ 'sandbox.write_file': {
174
+ readOnlyHint: false,
175
+ destructiveHint: false,
176
+ idempotentHint: false,
177
+ approval: 'default',
178
+ },
179
+ 'sandbox.reset': {
180
+ readOnlyHint: false,
181
+ destructiveHint: true,
182
+ idempotentHint: false,
183
+ approval: 'default',
184
+ },
185
+ } as const satisfies Record<string, ToolAnnotationsTableEntry>
186
+
187
+ export type OperatorToolName = keyof typeof OPERATOR_TOOL_ANNOTATIONS
@@ -0,0 +1,311 @@
1
+ import type { BuildControlResult } from '../cli/build-control'
2
+ import type { BulkControlSummary } from '../cli/bulk-control'
3
+ import type { HarvestStatusView } from '../cli/harvest'
4
+ import type { RepositoryStatus } from '../cli/repository-status'
5
+ import type { BuildSummary } from '../cli/status'
6
+ import {
7
+ AUTOBUILD_VERSION,
8
+ AUTOBUILD_VERSION_HEADER,
9
+ REMOTE_STORE_PROTOCOL_VERSION,
10
+ REMOTE_STORE_PROTOCOL_VERSION_HEADER,
11
+ } from '../store/remote/version'
12
+ import {
13
+ operatorErrorSchema,
14
+ type OperatorAnswerRequest,
15
+ type OperatorBuildControlRequest,
16
+ type OperatorTicketBlockerRequest,
17
+ type OperatorTicketCreateRequest,
18
+ type OperatorTicketMoveRequest,
19
+ type OperatorTicketUpdateRequest,
20
+ } from './protocol'
21
+ import type { OperatorBuildView, OperatorDashboardSnapshot } from './query'
22
+ import type { OperatorTicketDetail, OperatorTicketQueue } from './tickets'
23
+ import type { OperatorSessionView } from './sessions'
24
+ import type { SessionRecord } from '../store/types'
25
+ import type { StreamRead } from '../store/streams/types'
26
+ import type {
27
+ OperatorSessionApprovalRequest,
28
+ OperatorSessionCreateRequest,
29
+ OperatorSessionMessageRequest,
30
+ OperatorSessionWakeRequest,
31
+ } from './protocol'
32
+
33
+ export class OperatorApiError extends Error {
34
+ constructor(
35
+ readonly status: number,
36
+ readonly kind: string,
37
+ readonly code?: string,
38
+ readonly progress?: unknown,
39
+ ) {
40
+ super(kind)
41
+ this.name = 'OperatorApiError'
42
+ }
43
+ }
44
+
45
+ export interface OperatorApiClientOptions {
46
+ url: string
47
+ token: string
48
+ fetchFn?: typeof fetch
49
+ }
50
+
51
+ export interface DownloadedArtifact {
52
+ content: Uint8Array
53
+ kind: string
54
+ revision: number
55
+ blobRef: string
56
+ disposition?: string
57
+ }
58
+
59
+ export class OperatorApiClient {
60
+ private readonly base: string
61
+ private readonly fetchFn: typeof fetch
62
+ constructor(private readonly options: OperatorApiClientOptions) {
63
+ this.base = options.url.replace(/\/+$/, '')
64
+ this.fetchFn = options.fetchFn ?? fetch
65
+ }
66
+
67
+ private repoPath(repo: string, suffix: string): string {
68
+ return `/operator/v1/repos/${encodeURIComponent(repo)}/${suffix}`
69
+ }
70
+
71
+ private async request<T>(repo: string, suffix: string, init: RequestInit = {}): Promise<T> {
72
+ const headers = new Headers(init.headers)
73
+ headers.set(AUTOBUILD_VERSION_HEADER, AUTOBUILD_VERSION)
74
+ headers.set(REMOTE_STORE_PROTOCOL_VERSION_HEADER, REMOTE_STORE_PROTOCOL_VERSION)
75
+ headers.set('authorization', `Bearer ${this.options.token}`)
76
+ if (init.body !== undefined) headers.set('content-type', 'application/json')
77
+ const response = await this.fetchFn(`${this.base}${this.repoPath(repo, suffix)}`, {
78
+ ...init,
79
+ headers,
80
+ })
81
+ if (!response.ok) {
82
+ const parsed = operatorErrorSchema.safeParse(await response.json().catch(() => null))
83
+ if (!parsed.success) throw new Error(`operator API returned HTTP ${response.status}`)
84
+ const error = new OperatorApiError(
85
+ response.status,
86
+ parsed.data.kind,
87
+ parsed.data.code,
88
+ parsed.data.progress,
89
+ )
90
+ error.message = parsed.data.error
91
+ throw error
92
+ }
93
+ return (await response.json()) as T
94
+ }
95
+
96
+ listBuilds(repo: string, scope: 'active' | 'queued' | 'all' = 'active'): Promise<BuildSummary[]> {
97
+ return this.request(repo, `builds?scope=${scope}`)
98
+ }
99
+ getBuild(repo: string, slug: string): Promise<OperatorBuildView> {
100
+ return this.request(repo, `builds/${encodeURIComponent(slug)}`)
101
+ }
102
+ dashboard(repo: string): Promise<OperatorDashboardSnapshot> {
103
+ return this.request(repo, 'dashboard')
104
+ }
105
+ repositoryStatus(repo: string): Promise<RepositoryStatus> {
106
+ return this.request(repo, 'status')
107
+ }
108
+ harvestStatus(repo: string): Promise<HarvestStatusView> {
109
+ return this.request(repo, 'harvest/status')
110
+ }
111
+ listTickets(
112
+ repo: string,
113
+ filters: { state?: string; labels?: string[] } = {},
114
+ ): Promise<OperatorTicketQueue> {
115
+ const query = new URLSearchParams()
116
+ if (filters.state !== undefined) query.set('state', filters.state)
117
+ for (const label of filters.labels ?? []) query.append('label', label)
118
+ const suffix = query.size > 0 ? `tickets?${query}` : 'tickets'
119
+ return this.request(repo, suffix)
120
+ }
121
+ getTicket(repo: string, id: string): Promise<OperatorTicketDetail> {
122
+ return this.request(repo, `tickets/${encodeURIComponent(id)}`)
123
+ }
124
+ createTicket(repo: string, request: OperatorTicketCreateRequest): Promise<OperatorTicketDetail> {
125
+ return this.request(repo, 'tickets', { method: 'POST', body: JSON.stringify(request) })
126
+ }
127
+ updateTicket(
128
+ repo: string,
129
+ id: string,
130
+ request: OperatorTicketUpdateRequest,
131
+ ): Promise<OperatorTicketDetail> {
132
+ return this.request(repo, `tickets/${encodeURIComponent(id)}`, {
133
+ method: 'PATCH',
134
+ body: JSON.stringify(request),
135
+ })
136
+ }
137
+ moveTicket(
138
+ repo: string,
139
+ id: string,
140
+ request: OperatorTicketMoveRequest,
141
+ ): Promise<OperatorTicketDetail> {
142
+ return this.request(repo, `tickets/${encodeURIComponent(id)}/move`, {
143
+ method: 'POST',
144
+ body: JSON.stringify(request),
145
+ })
146
+ }
147
+ blockTicket(
148
+ repo: string,
149
+ id: string,
150
+ request: OperatorTicketBlockerRequest,
151
+ ): Promise<OperatorTicketDetail> {
152
+ return this.request(repo, `tickets/${encodeURIComponent(id)}/block`, {
153
+ method: 'POST',
154
+ body: JSON.stringify(request),
155
+ })
156
+ }
157
+ unblockTicket(
158
+ repo: string,
159
+ id: string,
160
+ request: OperatorTicketBlockerRequest,
161
+ ): Promise<OperatorTicketDetail> {
162
+ return this.request(repo, `tickets/${encodeURIComponent(id)}/unblock`, {
163
+ method: 'POST',
164
+ body: JSON.stringify(request),
165
+ })
166
+ }
167
+ listSessions(repo: string): Promise<SessionRecord[]> {
168
+ return this.request(repo, 'sessions')
169
+ }
170
+ createSession(repo: string, request: OperatorSessionCreateRequest): Promise<SessionRecord> {
171
+ return this.request(repo, 'sessions', { method: 'POST', body: JSON.stringify(request) })
172
+ }
173
+ getSession(repo: string, sid: string): Promise<OperatorSessionView> {
174
+ return this.request(repo, `sessions/${encodeURIComponent(sid)}`)
175
+ }
176
+ postSessionMessage(
177
+ repo: string,
178
+ sid: string,
179
+ request: OperatorSessionMessageRequest,
180
+ ): Promise<{ ok: boolean }> {
181
+ return this.request(repo, `sessions/${encodeURIComponent(sid)}/messages`, {
182
+ method: 'POST',
183
+ body: JSON.stringify(request),
184
+ })
185
+ }
186
+ setSessionWake(
187
+ repo: string,
188
+ sid: string,
189
+ request: OperatorSessionWakeRequest,
190
+ ): Promise<{ ok: boolean }> {
191
+ return this.request(repo, `sessions/${encodeURIComponent(sid)}/wake`, {
192
+ method: 'PUT',
193
+ body: JSON.stringify(request),
194
+ })
195
+ }
196
+ answerSessionApproval(
197
+ repo: string,
198
+ sid: string,
199
+ request: OperatorSessionApprovalRequest,
200
+ ): Promise<{ ok: boolean }> {
201
+ return this.request(repo, `sessions/${encodeURIComponent(sid)}/approvals`, {
202
+ method: 'POST',
203
+ body: JSON.stringify(request),
204
+ })
205
+ }
206
+ archiveSession(repo: string, sid: string): Promise<{ ok: boolean }> {
207
+ return this.request(repo, `sessions/${encodeURIComponent(sid)}/archive`, {
208
+ method: 'POST',
209
+ })
210
+ }
211
+ readSessionTurnStream(
212
+ repo: string,
213
+ sid: string,
214
+ turn: string,
215
+ opts: { since?: number; waitSeconds?: number } = {},
216
+ ): Promise<StreamRead> {
217
+ const query = new URLSearchParams({ since: String(opts?.since ?? 0) })
218
+ if (opts?.waitSeconds !== undefined) query.set('wait', String(opts.waitSeconds))
219
+ return this.request(
220
+ repo,
221
+ `sessions/${encodeURIComponent(sid)}/turns/${encodeURIComponent(turn)}/stream?${query}`,
222
+ )
223
+ }
224
+ controlBuild(
225
+ repo: string,
226
+ slug: string,
227
+ request: OperatorBuildControlRequest,
228
+ ): Promise<BuildControlResult> {
229
+ return this.request(repo, `builds/${encodeURIComponent(slug)}/control`, {
230
+ method: 'POST',
231
+ body: JSON.stringify(request),
232
+ })
233
+ }
234
+ /** The registry's generic protocol face: execute any tool from the closed
235
+ * table by name. The body is the tool's input (repo included); failures
236
+ * parse into OperatorApiError exactly like the typed routes. */
237
+ callTool(repo: string, tool: string, input: unknown): Promise<unknown> {
238
+ return this.request(repo, `tools/${encodeURIComponent(tool)}`, {
239
+ method: 'POST',
240
+ body: JSON.stringify(input),
241
+ })
242
+ }
243
+ answer(repo: string, slug: string, request: OperatorAnswerRequest): Promise<BuildControlResult> {
244
+ return this.request(repo, `builds/${encodeURIComponent(slug)}/answer`, {
245
+ method: 'POST',
246
+ body: JSON.stringify(request),
247
+ })
248
+ }
249
+ setIntake(repo: string, enabled: boolean): Promise<unknown> {
250
+ return this.request(repo, 'settings/intake', {
251
+ method: 'PUT',
252
+ body: JSON.stringify({ enabled }),
253
+ })
254
+ }
255
+ toggleIntake(repo: string): Promise<unknown> {
256
+ return this.request(repo, 'settings/intake/toggle', { method: 'POST' })
257
+ }
258
+ setAutoMergeDefault(repo: string, enabled: boolean): Promise<unknown> {
259
+ return this.request(repo, 'settings/auto-merge-default', {
260
+ method: 'PUT',
261
+ body: JSON.stringify({ enabled }),
262
+ })
263
+ }
264
+ toggleAutoMergeDefault(repo: string): Promise<unknown> {
265
+ return this.request(repo, 'settings/auto-merge-default/toggle', { method: 'POST' })
266
+ }
267
+ bulkControl(repo: string, action: 'pause' | 'resume'): Promise<BulkControlSummary> {
268
+ return this.request(repo, 'bulk-control', { method: 'POST', body: JSON.stringify({ action }) })
269
+ }
270
+ toggleHarvest(repo: string): Promise<unknown> {
271
+ return this.request(repo, 'harvest/control', {
272
+ method: 'POST',
273
+ body: JSON.stringify({ action: 'toggle-gate' }),
274
+ })
275
+ }
276
+ controlHarvestRun(repo: string, run: string): Promise<unknown> {
277
+ return this.request(repo, 'harvest/control', {
278
+ method: 'POST',
279
+ body: JSON.stringify({ action: 'run', run }),
280
+ })
281
+ }
282
+ async downloadArtifact(
283
+ repo: string,
284
+ slug: string,
285
+ kind: string,
286
+ rev?: number,
287
+ ): Promise<DownloadedArtifact> {
288
+ const suffix = `builds/${encodeURIComponent(slug)}/artifacts/${encodeURIComponent(kind)}${rev === undefined ? '' : `?rev=${rev}`}`
289
+ const headers = new Headers({
290
+ [AUTOBUILD_VERSION_HEADER]: AUTOBUILD_VERSION,
291
+ [REMOTE_STORE_PROTOCOL_VERSION_HEADER]: REMOTE_STORE_PROTOCOL_VERSION,
292
+ authorization: `Bearer ${this.options.token}`,
293
+ })
294
+ const response = await this.fetchFn(`${this.base}${this.repoPath(repo, suffix)}`, { headers })
295
+ if (!response.ok) {
296
+ const parsed = operatorErrorSchema.parse(await response.json())
297
+ const error = new OperatorApiError(response.status, parsed.kind, parsed.code, parsed.progress)
298
+ error.message = parsed.error
299
+ throw error
300
+ }
301
+ return {
302
+ content: new Uint8Array(await response.arrayBuffer()),
303
+ kind: response.headers.get('x-autobuild-artifact-kind') ?? kind,
304
+ revision: Number(response.headers.get('x-autobuild-artifact-revision')),
305
+ blobRef: response.headers.get('x-autobuild-artifact-blob-ref') ?? '',
306
+ ...(response.headers.get('content-disposition') !== null
307
+ ? { disposition: response.headers.get('content-disposition')! }
308
+ : {}),
309
+ }
310
+ }
311
+ }