@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,121 @@
1
+ import type { AgentTurnFailure } from '../types'
2
+
3
+ /** Optional structured signals exposed by a runtime in addition to its error
4
+ * text. Unknown hints are deliberately treated as availability failures: that
5
+ * preserves the existing bounded retry policy and permits an explicitly
6
+ * configured alternate without hiding credential evidence. */
7
+ export interface ProviderErrorHints {
8
+ status?: number | null
9
+ codes?: readonly (string | number | null | undefined)[]
10
+ }
11
+
12
+ const EXHAUSTION_CODE_PATTERNS = [
13
+ /(?:^|[_-])(?:billing|payment|quota|credit|balance|budget)(?:[_-]|$)/,
14
+ /^(?:insufficient|out_of)_(?:quota|credits?|balance|funds)$/,
15
+ /^usage_(?:limit|quota)(?:_|$)/,
16
+ ] as const
17
+
18
+ const CREDENTIAL_CODE_PATTERNS = [
19
+ /^(?:auth|authentication|authorization)(?:_|-)/,
20
+ /^(?:unauthorized|forbidden)$/,
21
+ /^(?:invalid|expired|missing|revoked)_(?:api_)?(?:key|token|credential)s?$/,
22
+ /^(?:permission|access)(?:_|-)(?:denied|error|required)$/,
23
+ /^oauth_org_not_allowed$/,
24
+ ] as const
25
+
26
+ const EXHAUSTION_TEXT_PATTERNS = [
27
+ /(?:^|[^a-z0-9])(?:quota(?:[_ -](?:exceeded|exhausted|depleted|refreshes?))?|(?:insufficient|exhausted|depleted)[_ -]quota)(?:$|[^a-z0-9])/i,
28
+ /\busage[_ -]?(?:limit|quota)\b/i,
29
+ /\b(?:billing|spending|monthly)\s+(?:limit|cap|cycle)\b/i,
30
+ /\bbilling(?:_error|\s+(?:error|issue|required|disabled))\b/i,
31
+ /\bpayment\s+(?:is\s+)?required\b/i,
32
+ /\bsubscription\s+(?:is\s+)?(?:expired|inactive|required|disabled)\b/i,
33
+ /\b(?:insufficient|no|out of|exhausted|depleted)\s+(?:credits?|balance|funds)\b/i,
34
+ /\bcredits?\s+(?:are\s+|is\s+)?(?:required|exhausted|depleted)\b/i,
35
+ /\bbudget\s+(?:is\s+)?(?:exceeded|exhausted|depleted)\b/i,
36
+ ] as const
37
+
38
+ const CREDENTIAL_TEXT_PATTERNS = [
39
+ /\b(?:authentication|authorization)[_ -](?:failed|required|error|invalid)\b/i,
40
+ /\bunauthori[sz]ed\b/i,
41
+ /\bforbidden\b/i,
42
+ /\b(?:invalid|expired|missing|revoked)[_ -](?:api[_ -]?)?(?:key|token|credentials?)\b/i,
43
+ /\b(?:api[- ]?key|token|credentials?)\s+(?:is\s+|are\s+)?(?:invalid|expired|missing|revoked)\b/i,
44
+ /\bpermission(?:_error|\s+(?:denied|required|error))\b/i,
45
+ /\b(?:do not|does not|don't|doesn't)\s+have\s+permission\b/i,
46
+ /\b(?:access|request)\s+(?:is\s+)?(?:denied|not permitted)\b/i,
47
+ ] as const
48
+
49
+ /** Classify a provider/SDK-declared turn error without rewriting its message.
50
+ * Exhaustion evidence intentionally wins over generic 402/403 and permission
51
+ * evidence: providers commonly wrap usage-limit responses in those shapes. */
52
+ export function classifyProviderError(
53
+ message: string,
54
+ hints: ProviderErrorHints = {},
55
+ ): AgentTurnFailure {
56
+ const codes = hints.codes ?? []
57
+ const exhaustion =
58
+ hints.status === 402 ||
59
+ codes.some((code) => code === 402 || matchesCode(code, EXHAUSTION_CODE_PATTERNS)) ||
60
+ hasHttpStatus(message, [402]) ||
61
+ EXHAUSTION_TEXT_PATTERNS.some((pattern) => pattern.test(message))
62
+ if (exhaustion) return { message, permanent: true, cause: 'exhaustion' }
63
+
64
+ const credentials =
65
+ hints.status === 401 ||
66
+ hints.status === 403 ||
67
+ codes.some(
68
+ (code) => code === 401 || code === 403 || matchesCode(code, CREDENTIAL_CODE_PATTERNS),
69
+ ) ||
70
+ hasHttpStatus(message, [401, 403]) ||
71
+ CREDENTIAL_TEXT_PATTERNS.some((pattern) => pattern.test(message))
72
+ if (credentials) return { message, permanent: true, cause: 'credentials' }
73
+
74
+ return { message, permanent: false, cause: 'availability' }
75
+ }
76
+
77
+ /** Whether another declared target may be tried inside this phase attempt.
78
+ * Legacy plugins have no cause: preserve their old safe split. */
79
+ export function isAlternateEligible(failure: AgentTurnFailure): boolean {
80
+ return failure.cause === undefined
81
+ ? !failure.permanent
82
+ : failure.cause === 'availability' || failure.cause === 'exhaustion'
83
+ }
84
+
85
+ /** Final-failure retry behavior remains governed by the compatibility bit. */
86
+ export function mayRetryPhase(failure: AgentTurnFailure): boolean {
87
+ return !failure.permanent
88
+ }
89
+
90
+ /** Explicit local configuration failures bypass both alternates and phase retries. */
91
+ export function configurationFailure(message: string): AgentTurnFailure {
92
+ return { message, permanent: true, cause: 'configuration' }
93
+ }
94
+
95
+ /** Explicit credential failures bypass both alternates and phase retries. */
96
+ export function credentialFailure(message: string): AgentTurnFailure {
97
+ return { message, permanent: true, cause: 'credentials' }
98
+ }
99
+
100
+ function matchesCode(
101
+ code: string | number | null | undefined,
102
+ patterns: readonly RegExp[],
103
+ ): boolean {
104
+ if (code === null || code === undefined || typeof code === 'number') return false
105
+ const normalized = code.trim().toLowerCase()
106
+ return patterns.some((pattern) => pattern.test(normalized))
107
+ }
108
+
109
+ function hasHttpStatus(message: string, statuses: readonly number[]): boolean {
110
+ const alternatives = statuses.join('|')
111
+ return (
112
+ new RegExp(
113
+ `^\\s*(?:(?:http(?:\\/\\d(?:\\.\\d)?)?|status(?:\\s+code)?)\\s*[:=]?\\s*)?(?:${alternatives})\\b`,
114
+ 'i',
115
+ ).test(message) ||
116
+ new RegExp(`\\bhttp(?:\\s+status)?\\s*[:=]?\\s*(?:${alternatives})\\b`, 'i').test(message) ||
117
+ new RegExp(
118
+ `["']?(?:status|status_code|statusCode)["']?\\s*[:=]\\s*["']?(?:${alternatives})\\b`,
119
+ ).test(message)
120
+ )
121
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Eager runtime/model/args role resolution (SPEC §9, §16.1). Primary
3
+ * fields and the ordered alternate list inherit independently from the
4
+ * reserved `[roles.default]` entry. A declared role list, including `[]`,
5
+ * replaces the inherited list wholesale; each entry overlays that concrete
6
+ * role's effective primary axes and is validated at construction.
7
+ */
8
+ import type { AgentRunner } from '../types'
9
+ import { serves, type RuntimeRegistration, type RuntimeRegistry } from './runtime'
10
+
11
+ /** A resolved runtime/model pair plus the adapter to run it (§9). */
12
+ export interface ResolvedRuntime {
13
+ runner: AgentRunner
14
+ /** The registry key == the frozen `session.started.runner` value. */
15
+ runtime: string
16
+ /** Absent ⇒ the adapter's built-in default model. */
17
+ model?: string
18
+ /** Extra CLI argv tokens, preserved in declared order. */
19
+ args: readonly string[]
20
+ /** Session-stream capability passthrough from the registration (SPEC §9).
21
+ * Absent ⇒ this runtime's sessions get no stream. */
22
+ openSessionStream?: RuntimeRegistration['openSessionStream']
23
+ }
24
+
25
+ /** A role's primary target plus its failure-triggered targets in declaration order. */
26
+ export interface ResolvedRole extends ResolvedRuntime {
27
+ /** Wall-clock budget shared by the logical role's primary and alternates. */
28
+ sessionBudgetSeconds: number
29
+ alternates: readonly ResolvedRuntime[]
30
+ }
31
+
32
+ export interface RuntimeAxes {
33
+ runtime?: string
34
+ model?: string
35
+ args?: readonly string[]
36
+ /** Deprecated compatibility field; accepted by the schema but ignored. */
37
+ extensions?: readonly string[]
38
+ }
39
+
40
+ /** One role entry as it arrives from `[roles]`: all fields are optional. */
41
+ export interface RuntimeSpec extends RuntimeAxes {
42
+ /** Inherits from [roles.default], then the policy fallback. */
43
+ sessionBudgetSeconds?: number
44
+ alternates?: readonly RuntimeAxes[]
45
+ }
46
+
47
+ export class RuntimeConfigError extends Error {
48
+ constructor(readonly problems: string[]) {
49
+ super(
50
+ `invalid runtime/model/args configuration (SPEC §9):\n` +
51
+ problems.map((p) => ` - ${p}`).join('\n'),
52
+ )
53
+ this.name = 'RuntimeConfigError'
54
+ }
55
+ }
56
+
57
+ export interface RuntimeResolver {
58
+ /** Resolve the earliest declared role/alias, otherwise `[roles.default]`. */
59
+ resolve(role: string, ...aliases: string[]): ResolvedRole
60
+ }
61
+
62
+ function runtimeNames(registry: RuntimeRegistry): string {
63
+ return Object.keys(registry).join(', ') || 'none'
64
+ }
65
+
66
+ /** The declared model families for one runtime, for error messages. */
67
+ function servedModels(registration: RuntimeRegistration): string {
68
+ return registration.servesModels.join(', ') || 'no models'
69
+ }
70
+
71
+ function mergeAxes(spec: RuntimeAxes, base: RuntimeAxes): RuntimeAxes {
72
+ const runtime = spec.runtime ?? base.runtime
73
+ const model = spec.model ?? base.model
74
+ return {
75
+ ...(runtime !== undefined ? { runtime } : {}),
76
+ ...(model !== undefined ? { model } : {}),
77
+ args: spec.args ?? base.args ?? [],
78
+ }
79
+ }
80
+
81
+ /** Resolve one already-merged raw target. Registry defaults apply only when no
82
+ * configured model exists on that target. Problems aggregate instead of throw. */
83
+ function resolveAxes(
84
+ axes: RuntimeAxes,
85
+ registry: RuntimeRegistry,
86
+ label: string,
87
+ problems: string[],
88
+ ): ResolvedRuntime | undefined {
89
+ const runtime = axes.runtime
90
+ if (runtime === undefined || runtime.trim().length === 0) {
91
+ problems.push(
92
+ `${label} is missing required runtime. Add:\n\n` +
93
+ `[roles.default]\n` +
94
+ `runtime = "<runtime>"\n\n` +
95
+ `Available runtimes: ${runtimeNames(registry)}`,
96
+ )
97
+ return undefined
98
+ }
99
+ if (!Object.hasOwn(registry, runtime)) {
100
+ problems.push(
101
+ `${label} resolves to runtime "${runtime}", which is not registered ` +
102
+ `(registered runtimes: ${runtimeNames(registry)})`,
103
+ )
104
+ return undefined
105
+ }
106
+ const reg = registry[runtime]!
107
+
108
+ const model = axes.model ?? reg.defaultModel
109
+ const modelValid = model === undefined || serves(reg, model)
110
+ if (!modelValid) {
111
+ problems.push(
112
+ `${label} resolves runtime "${runtime}" with model "${model}", but ` +
113
+ `"${runtime}" serves only [${servedModels(reg)}]`,
114
+ )
115
+ }
116
+
117
+ const args = axes.args ?? []
118
+ const ownedArgs = new Set(reg.ownedArgs ?? [])
119
+ for (const arg of args) {
120
+ if (arg === reg.promptBoundary) {
121
+ problems.push(
122
+ `${label} argument ${JSON.stringify(arg)} conflicts with the prompt boundary for runtime "${runtime}". ` +
123
+ 'Remove it because Autobuild owns and appends this prompt separator.',
124
+ )
125
+ continue
126
+ }
127
+
128
+ const spelling = arg.startsWith('--') ? arg.split('=', 1)[0]! : arg
129
+ const owned =
130
+ ownedArgs.has(spelling) ||
131
+ [...ownedArgs].some(
132
+ (candidate) =>
133
+ candidate.startsWith('-') &&
134
+ !candidate.startsWith('--') &&
135
+ candidate.length === 2 &&
136
+ arg.startsWith(candidate) &&
137
+ arg.length > candidate.length,
138
+ )
139
+ if (owned) {
140
+ problems.push(
141
+ `${label} argument ${JSON.stringify(arg)} conflicts with an option owned by runtime "${runtime}"`,
142
+ )
143
+ }
144
+ }
145
+
146
+ if (!modelValid) return undefined
147
+
148
+ return {
149
+ runner: reg.runner,
150
+ runtime,
151
+ ...(model !== undefined ? { model } : {}),
152
+ args,
153
+ ...(reg.openSessionStream !== undefined ? { openSessionStream: reg.openSessionStream } : {}),
154
+ }
155
+ }
156
+
157
+ function resolveRole(
158
+ spec: RuntimeSpec,
159
+ defaultSpec: RuntimeSpec,
160
+ policySessionBudgetSeconds: number,
161
+ registry: RuntimeRegistry,
162
+ label: string,
163
+ problems: string[],
164
+ ): ResolvedRole | undefined {
165
+ const primaryAxes = mergeAxes(spec, defaultSpec)
166
+ const primary = resolveAxes(primaryAxes, registry, label, problems)
167
+ const alternateSpecs = spec.alternates ?? defaultSpec.alternates ?? []
168
+ const alternates: ResolvedRuntime[] = []
169
+ alternateSpecs.forEach((alternate, index) => {
170
+ const resolved = resolveAxes(
171
+ mergeAxes(alternate, primaryAxes),
172
+ registry,
173
+ `${label}.alternates[${index}]`,
174
+ problems,
175
+ )
176
+ if (resolved !== undefined) alternates.push(resolved)
177
+ })
178
+ return primary === undefined
179
+ ? undefined
180
+ : {
181
+ ...primary,
182
+ sessionBudgetSeconds:
183
+ spec.sessionBudgetSeconds ??
184
+ defaultSpec.sessionBudgetSeconds ??
185
+ policySessionBudgetSeconds,
186
+ alternates,
187
+ }
188
+ }
189
+
190
+ /** Build and eagerly validate the complete role resolver. */
191
+ export function createRuntimeResolver(
192
+ registry: RuntimeRegistry,
193
+ roles: Record<string, RuntimeSpec>,
194
+ policySessionBudgetSeconds = 3600,
195
+ ): RuntimeResolver {
196
+ const problems: string[] = []
197
+ const defaultSpec = roles.default ?? {}
198
+ const resolvedDefault = resolveRole(
199
+ defaultSpec,
200
+ {},
201
+ policySessionBudgetSeconds,
202
+ registry,
203
+ '[roles.default]',
204
+ problems,
205
+ )
206
+
207
+ const resolvedRoles: Record<string, ResolvedRole> = Object.create(null)
208
+ for (const [role, spec] of Object.entries(roles)) {
209
+ if (role === 'default') continue
210
+ const resolved = resolveRole(
211
+ spec,
212
+ defaultSpec,
213
+ policySessionBudgetSeconds,
214
+ registry,
215
+ `[roles.${role}]`,
216
+ problems,
217
+ )
218
+ if (resolved !== undefined) resolvedRoles[role] = resolved
219
+ }
220
+
221
+ if (problems.length > 0) throw new RuntimeConfigError(problems)
222
+ const fallback = resolvedDefault!
223
+
224
+ return {
225
+ resolve(role: string, ...aliases: string[]): ResolvedRole {
226
+ for (const key of [role, ...aliases]) {
227
+ if (key === 'default') return fallback
228
+ const resolved = resolvedRoles[key]
229
+ if (resolved !== undefined) return resolved
230
+ }
231
+ return fallback
232
+ },
233
+ }
234
+ }
@@ -0,0 +1,214 @@
1
+ /**
2
+ * The runtime registry (SPEC §9): the "adapter registry" the spec says adding
3
+ * a runtime should touch — and ONLY it. Each registration pairs an AgentRunner
4
+ * adapter with its capabilities: model families/default for compatibility
5
+ * validation, plus an
6
+ * optional tool-free, non-phase one-shot completion. Keeping capabilities HERE, in a
7
+ * wrapper around the adapter, is deliberate: the `AgentRunner` port itself is
8
+ * frozen (spec "out of scope" — no renaming/extending it), so capability data
9
+ * lives beside the registry rather than inside the port. The kernel never
10
+ * learns a runtime's name; it asks the resolver, and the resolver reads this.
11
+ *
12
+ * "runtime" is the config-surface vocabulary for what the code has always
13
+ * called a runner: the registry KEY is the runtime name (== the frozen
14
+ * `session.started.runner` value the build-runner records), while the adapter's
15
+ * own `.name` is a separate, also-frozen thing that fills
16
+ * `AgentSessionHandle.runner`.
17
+ */
18
+ import type { AgentRunner, SessionStreamSink } from '../types'
19
+ import type { OneShotCompletion } from './one-shot'
20
+
21
+ /** Bracket metadata for a session stream's `data-ab-session` part. */
22
+ export interface SessionStreamInfo {
23
+ session: string
24
+ role: string
25
+ runner: string
26
+ model?: string
27
+ phase: string
28
+ round?: number
29
+ }
30
+
31
+ export interface RuntimeUsabilityInput {
32
+ cwd: string
33
+ env: Readonly<Record<string, string | undefined>>
34
+ /** Exact configured models the prospective init profile will write. */
35
+ models: readonly string[]
36
+ }
37
+
38
+ export interface RuntimeUsabilityDiagnostic {
39
+ usable: boolean
40
+ /** Human-readable prerequisite/auth result. Must not contain credentials. */
41
+ reason: string
42
+ }
43
+
44
+ /** Boolean remains accepted for backward compatibility with plugin probes. */
45
+ export type RuntimeUsabilityResult = boolean | RuntimeUsabilityDiagnostic
46
+
47
+ /** Optional onboarding probe. Absence means init must not suggest the runtime. */
48
+ export type RuntimeUsabilityProbe = (
49
+ input: RuntimeUsabilityInput,
50
+ ) => Promise<RuntimeUsabilityResult>
51
+
52
+ export interface RuntimeRegistration {
53
+ /** The adapter behind this runtime (§9). */
54
+ runner: AgentRunner
55
+ /**
56
+ * Optional tool-free, non-resumable, non-phase completion capability. Its
57
+ * absence is a normal capability boundary, not a configuration error;
58
+ * deterministic callers decide their own fail-safe fallback.
59
+ */
60
+ oneShot?: OneShotCompletion
61
+ /**
62
+ * Optional session-stream capability (SPEC §9): given a bracket-scoped
63
+ * sink, create the runtime's one stream for this session bracket and
64
+ * return its store id (or `undefined` to decline streaming for the
65
+ * bracket). Its absence means the runtime emits no parts and its sessions
66
+ * have no stream — a supported boundary, not a configuration error.
67
+ */
68
+ openSessionStream?: (
69
+ sink: SessionStreamSink,
70
+ info: SessionStreamInfo,
71
+ ) => Promise<string | undefined>
72
+ /** Runtime-local prerequisite/auth check used only by `ab init`. */
73
+ initUsable?: RuntimeUsabilityProbe
74
+ /**
75
+ * Model-id PREFIXES this runtime can serve, e.g. `['openai/',
76
+ * 'kimi-coding/']`. Prefix families — not an exhaustive id list — because the
77
+ * model landscape moves faster than the pipeline and the spec forbids
78
+ * hardcoding served ids: a new model from either provider validates without
79
+ * editing this list. These families are validation data only: role resolution
80
+ * never searches them to choose or substitute a runtime. `serves()` below is
81
+ * the matcher.
82
+ */
83
+ servesModels: string[]
84
+ /**
85
+ * This runtime's own default model, used when a step selects the runtime but
86
+ * names no model. When set, it must match `servesModels` like any other
87
+ * resolved model. `undefined` ⇒ the adapter's un-named built-in default (e.g.
88
+ * Claude's subscription default) — the path that preserves no-model behavior.
89
+ */
90
+ defaultModel?: string
91
+ /** Model-selection and parsed protocol/output-mode spellings owned by the
92
+ * adapter, plus documented aliases of those options. Every other role arg
93
+ * remains freeform, including options the adapter may also emit. */
94
+ ownedArgs?: readonly string[]
95
+ /**
96
+ * Exact structural token Autobuild appends before the positional prompt.
97
+ * Role args may not duplicate this boundary.
98
+ */
99
+ promptBoundary?: string
100
+ }
101
+
102
+ /** name → registration. The one place a runtime's name lives (§9). */
103
+ export type RuntimeRegistry = Record<string, RuntimeRegistration>
104
+
105
+ function isObject(value: unknown): value is Record<string, unknown> {
106
+ return typeof value === 'object' && value !== null
107
+ }
108
+
109
+ /**
110
+ * Validate an untrusted plugin factory result at the composition boundary.
111
+ * TypeScript's structural types disappear at runtime, so factories must not be
112
+ * able to smuggle a partial adapter or inconsistent capability declaration
113
+ * into the eager role resolver.
114
+ */
115
+ export function validateRuntimeRegistration(value: unknown): RuntimeRegistration {
116
+ if (!isObject(value)) {
117
+ throw new Error('must be an object')
118
+ }
119
+
120
+ const runner = value.runner
121
+ if (!isObject(runner)) {
122
+ throw new Error('runner must be an AgentRunner-shaped object')
123
+ }
124
+ if (typeof runner.name !== 'string' || runner.name.trim().length === 0) {
125
+ throw new Error('runner.name must be a nonblank string')
126
+ }
127
+ for (const method of ['start', 'continue', 'end'] as const) {
128
+ if (typeof runner[method] !== 'function') {
129
+ throw new Error(`runner.${method} must be a function`)
130
+ }
131
+ }
132
+
133
+ const servesModels = value.servesModels
134
+ if (
135
+ !Array.isArray(servesModels) ||
136
+ servesModels.some(
137
+ (family) =>
138
+ typeof family !== 'string' ||
139
+ family.trim().length === 0 ||
140
+ (family.includes('*') && family !== '*/*'),
141
+ )
142
+ ) {
143
+ throw new Error(
144
+ 'servesModels must be an array of nonblank strings containing prefixes or the provider-qualified wildcard "*/*"',
145
+ )
146
+ }
147
+
148
+ const defaultModel = value.defaultModel
149
+ if (
150
+ defaultModel !== undefined &&
151
+ (typeof defaultModel !== 'string' || defaultModel.trim().length === 0)
152
+ ) {
153
+ throw new Error('defaultModel must be a nonblank string when provided')
154
+ }
155
+
156
+ const ownedArgs = value.ownedArgs
157
+ if (
158
+ ownedArgs !== undefined &&
159
+ (!Array.isArray(ownedArgs) ||
160
+ ownedArgs.some(
161
+ (arg) => typeof arg !== 'string' || !arg.startsWith('-') || arg.trim().length === 0,
162
+ ) ||
163
+ new Set(ownedArgs).size !== ownedArgs.length)
164
+ ) {
165
+ throw new Error('ownedArgs must be a unique array of nonblank option spellings')
166
+ }
167
+
168
+ const promptBoundary = value.promptBoundary
169
+ if (
170
+ promptBoundary !== undefined &&
171
+ (typeof promptBoundary !== 'string' || promptBoundary.trim().length === 0)
172
+ ) {
173
+ throw new Error('promptBoundary must be a nonblank string when provided')
174
+ }
175
+
176
+ const oneShot = value.oneShot
177
+ if (oneShot !== undefined && (!isObject(oneShot) || typeof oneShot.complete !== 'function')) {
178
+ throw new Error('oneShot.complete must be a function when provided')
179
+ }
180
+
181
+ if (value.openSessionStream !== undefined && typeof value.openSessionStream !== 'function') {
182
+ throw new Error('openSessionStream must be a function when provided')
183
+ }
184
+
185
+ if (value.initUsable !== undefined && typeof value.initUsable !== 'function') {
186
+ throw new Error('initUsable must be a function when provided')
187
+ }
188
+
189
+ const registration = value as unknown as RuntimeRegistration
190
+ if (typeof defaultModel === 'string' && !serves(registration, defaultModel)) {
191
+ throw new Error(
192
+ `defaultModel "${defaultModel}" is not served by servesModels ` +
193
+ `[${servesModels.join(', ')}]`,
194
+ )
195
+ }
196
+ return registration
197
+ }
198
+
199
+ /**
200
+ * Does this registration serve `model`? Prefix-family match:
201
+ * `openai/gpt-5.6-sol` is served by a registration declaring `openai/`. Empty
202
+ * `servesModels` serves nothing (a runtime that can run only with its adapter's
203
+ * un-named built-in default, never with a configured model id).
204
+ */
205
+ export function serves(reg: RuntimeRegistration, model: string): boolean {
206
+ return reg.servesModels.some((family) =>
207
+ family === '*/*' ? isProviderQualified(model) : model.startsWith(family),
208
+ )
209
+ }
210
+
211
+ function isProviderQualified(model: string): boolean {
212
+ const slash = model.indexOf('/')
213
+ return slash > 0 && slash < model.length - 1
214
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Build the environment passed to an agent runtime for one turn.
3
+ *
4
+ * Scoped values (AB_STORE, AB_PHASE, and so on) override the ambient process
5
+ * environment. The distribution-owned launcher is then forced to the front of
6
+ * PATH so an inherited executable with the public `ab` name cannot shadow the
7
+ * Autobuild CLI. A fresh object is returned on every call; neither input nor
8
+ * process.env is mutated, which keeps concurrent builds isolated.
9
+ */
10
+ import { delimiter } from 'node:path'
11
+ import { distributionPath } from '../../distribution'
12
+
13
+ /** Private command directory shipped by this Autobuild distribution. */
14
+ export const AGENT_BIN_DIR = distributionPath('bin', 'agent')
15
+
16
+ export function sessionEnv(
17
+ scoped: Readonly<Record<string, string>>,
18
+ ambient: Readonly<Record<string, string | undefined>> = process.env,
19
+ ): Record<string, string> {
20
+ const env: Record<string, string> = {}
21
+ for (const [key, value] of Object.entries(ambient)) {
22
+ if (value !== undefined) env[key] = value
23
+ }
24
+ Object.assign(env, scoped)
25
+
26
+ const inheritedPath = env.PATH
27
+ const inheritedEntries =
28
+ inheritedPath === undefined || inheritedPath === ''
29
+ ? []
30
+ : inheritedPath.split(delimiter).filter((entry) => entry !== AGENT_BIN_DIR)
31
+ env.PATH = [AGENT_BIN_DIR, ...inheritedEntries].join(delimiter)
32
+
33
+ return env
34
+ }