@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,74 @@
1
+ /**
2
+ * Dependency-free argv parsing for one exact `ab` command form.
3
+ *
4
+ * Callers supply the complete flag vocabulary for the receiving command. The
5
+ * parser deliberately owns only syntax: command-specific positional arity,
6
+ * numeric conversion, and mutually exclusive flags remain with the route.
7
+ */
8
+ /** `'multi'` is a value flag that may repeat: values accumulate in order. */
9
+ export type FlagKind = 'value' | 'boolean' | 'multi'
10
+
11
+ export type FlagSpec = Readonly<Record<string, FlagKind>>
12
+
13
+ export interface ParsedArgs {
14
+ positionals: string[]
15
+ flags: Map<string, string | true | string[]>
16
+ }
17
+
18
+ export function parseArgs(
19
+ args: readonly string[],
20
+ allowedFlags: FlagSpec,
21
+ usage: string,
22
+ ): ParsedArgs {
23
+ const positionals: string[] = []
24
+ const flags = new Map<string, string | true | string[]>()
25
+
26
+ for (let index = 0; index < args.length; index += 1) {
27
+ const arg = args[index]!
28
+ if (!arg.startsWith('--')) {
29
+ positionals.push(arg)
30
+ continue
31
+ }
32
+
33
+ const name = arg.slice(2)
34
+ const kind = Object.hasOwn(allowedFlags, name) ? allowedFlags[name] : undefined
35
+ if (kind === undefined) {
36
+ throw new Error(`unknown flag --${name} — ${usage}`)
37
+ }
38
+ if (kind !== 'multi' && flags.has(name)) {
39
+ throw new Error(`--${name} may be supplied only once — ${usage}`)
40
+ }
41
+ if (kind === 'boolean') {
42
+ flags.set(name, true)
43
+ continue
44
+ }
45
+
46
+ const value = args[index + 1]
47
+ if (value === undefined || value.startsWith('--')) {
48
+ throw new Error(
49
+ `--${name} requires a value${value !== undefined ? `, got "${value}"` : ''} — ${usage}`,
50
+ )
51
+ }
52
+ if (kind === 'multi') {
53
+ const collected = flags.get(name)
54
+ const previous: string[] = Array.isArray(collected) ? collected : []
55
+ flags.set(name, [...previous, value])
56
+ } else {
57
+ flags.set(name, value)
58
+ }
59
+ index += 1
60
+ }
61
+
62
+ return { positionals, flags }
63
+ }
64
+
65
+ export function stringFlag(parsed: ParsedArgs, name: string): string | undefined {
66
+ const value = parsed.flags.get(name)
67
+ return typeof value === 'string' ? value : undefined
68
+ }
69
+
70
+ /** Collected values of a repeatable flag, in supply order; `[]` when absent. */
71
+ export function multiFlag(parsed: ParsedArgs, name: string): string[] {
72
+ const value = parsed.flags.get(name)
73
+ return Array.isArray(value) ? value : []
74
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Artifact command core (SPEC §8.2). In-session `put|get` are own-build by
3
+ * construction: their store handle and remote token come from the phase tuple.
4
+ * `download` is the read-only operator form; when invoked with complete
5
+ * ambient phase identity it keeps that identity's Store authority.
6
+ */
7
+ import { mkdir, writeFile } from 'node:fs/promises'
8
+ import { basename, dirname, resolve } from 'node:path'
9
+ import { agentActor } from '../events/envelope'
10
+ import { prAttachmentSchema } from '../ontology'
11
+ import type { Exec } from '../ports/workspace/git-worktree'
12
+ import type { Artifact, ArtifactMeta, BuildStore } from '../store/types'
13
+ import type { CliEnv } from './env'
14
+ import { buildInRepository } from './repo-state'
15
+ import { withAmbientReadStore, type StoreOpener } from './store-opening'
16
+
17
+ export interface ArtifactDeps {
18
+ store: BuildStore
19
+ env: CliEnv
20
+ }
21
+
22
+ /** `plan@1` → `{kind: 'plan', rev: 1}`; no `@` → latest (rev omitted). */
23
+ export function parseArtifactSpec(spec: string): { kind: string; rev?: number } {
24
+ const at = spec.lastIndexOf('@')
25
+ if (at === -1) return { kind: spec }
26
+ const kind = spec.slice(0, at)
27
+ const revPart = spec.slice(at + 1)
28
+ if (kind === '' || !/^\d+$/.test(revPart)) {
29
+ throw new Error(
30
+ `invalid artifact ref "${spec}" — expected '<kind>' or '<kind>@<rev>' ` +
31
+ '(rev is a nonnegative integer; revisions are 0-based, §6.3)',
32
+ )
33
+ }
34
+ return { kind, rev: Number(revPart) }
35
+ }
36
+
37
+ export interface ArtifactPutOpts {
38
+ /** Atomically designate this exact deposited revision for PR projection. */
39
+ attach?: boolean
40
+ }
41
+
42
+ function normalizedMediaType(file: ReturnType<typeof Bun.file>): string {
43
+ const type = file.type.split(';', 1)[0]?.trim().toLowerCase()
44
+ return type === undefined || type === '' ? 'application/octet-stream' : type
45
+ }
46
+
47
+ /** Deposit a revision of `kind` from `filePath`; the caller prints the rev. */
48
+ export async function artifactPut(
49
+ deps: ArtifactDeps,
50
+ kind: string,
51
+ filePath: string,
52
+ opts: ArtifactPutOpts = {},
53
+ ): Promise<ArtifactMeta> {
54
+ if (kind.trim() === '') {
55
+ throw new Error("'ab artifact put' requires a non-empty <kind> (§8.2)")
56
+ }
57
+ // §6.3 immutability: the spec cannot change during a build. It enters via
58
+ // dispatch (spec.imported / spec.authored — dispatcher plumbing, never this
59
+ // CLI) and changes only through a human-answered escalation (spec.revised).
60
+ // Without this gate, any phase's agent could silently swap the contract
61
+ // every later reviewer approves conformance to.
62
+ if (kind === 'spec') {
63
+ throw new Error(
64
+ "'ab artifact put spec' is rejected: the spec is immutable during a build " +
65
+ '(§6.3) — a phase that finds the spec itself wrong should raise ' +
66
+ "'ab escalate' so a human can revise it (spec.revised restarts from plan).",
67
+ )
68
+ }
69
+ const file = Bun.file(filePath)
70
+ if (!(await file.exists())) {
71
+ throw new Error(`'ab artifact put ${kind}': file not found: ${filePath}`)
72
+ }
73
+ // Artifacts are byte streams. Text deposits remain byte-for-byte compatible,
74
+ // while PNG and other binary evidence must never pass through UTF-8 decode.
75
+ const content = await file.bytes()
76
+ if (opts.attach !== true) {
77
+ return deps.store.putArtifact(deps.env.build, { kind, content })
78
+ }
79
+
80
+ // Validate every display/provider field before entering the atomic store
81
+ // operation. `basename` ensures callers cannot publish an output path.
82
+ const attachment = prAttachmentSchema.parse({
83
+ artifact: { kind, rev: 0 }, // replaced with the store-assigned rev below
84
+ filename: basename(filePath),
85
+ mediaType: normalizedMediaType(file),
86
+ })
87
+ const { artifacts } = await deps.store.appendWithArtifacts(
88
+ deps.env.build,
89
+ [{ kind, content }],
90
+ (deposited) => ({
91
+ actor: agentActor(deps.env.phase, deps.env.session),
92
+ type: 'pr-attachment.designated',
93
+ payload: {
94
+ ...attachment,
95
+ artifact: {
96
+ kind: deposited[0]!.kind,
97
+ rev: deposited[0]!.revision,
98
+ },
99
+ },
100
+ }),
101
+ )
102
+ const meta = artifacts[0]
103
+ if (meta === undefined) {
104
+ throw new Error('attachment deposit produced no artifact meta — this is a store bug')
105
+ }
106
+ return meta
107
+ }
108
+
109
+ /** Fetch `<kind>[@rev]` within the own build; latest when rev is omitted. */
110
+ export async function artifactGet(deps: ArtifactDeps, spec: string): Promise<Artifact> {
111
+ const { kind, rev } = parseArtifactSpec(spec)
112
+ const artifact = await deps.store.getArtifact(deps.env.build, kind, rev)
113
+ if (artifact === null) {
114
+ const metas = await deps.store.listArtifacts(deps.env.build)
115
+ const kinds = [...new Set(metas.map((meta) => meta.kind))]
116
+ throw new Error(
117
+ `no "${kind}" artifact${rev !== undefined ? ` at rev ${rev}` : ''} in ` +
118
+ `build "${deps.env.build}" — deposited kinds: ${kinds.join(', ') || '(none)'}`,
119
+ )
120
+ }
121
+ return artifact
122
+ }
123
+
124
+ export interface ArtifactDownloadOpts {
125
+ /** Current checkout; repository identity resolves to its main worktree. */
126
+ targetRepo: string
127
+ env: Record<string, string | undefined>
128
+ exec: Exec
129
+ build: string
130
+ spec: string
131
+ outputPath: string
132
+ /** Explicit --store; precedence remains flag > AB_STORE > local default. */
133
+ storeRef?: string
134
+ /** Adapter seam for local/remote selection tests. */
135
+ openStore?: StoreOpener
136
+ }
137
+
138
+ export interface ArtifactDownloadResult {
139
+ artifact: Artifact
140
+ outputPath: string
141
+ }
142
+
143
+ /** Read-only binary retrieval whose invocation requires no session. It
144
+ * deliberately accepts terminal builds: artifacts are evidence, not control. */
145
+ export async function artifactDownload(
146
+ opts: ArtifactDownloadOpts,
147
+ ): Promise<ArtifactDownloadResult> {
148
+ if (opts.build.trim() === '') {
149
+ throw new Error("'ab artifact download' requires a non-empty <build>")
150
+ }
151
+ const { kind, rev } = parseArtifactSpec(opts.spec)
152
+ if (kind.trim() === '') {
153
+ throw new Error("'ab artifact download' requires a non-empty <kind>[@rev]")
154
+ }
155
+ return withAmbientReadStore(opts, async (context) => {
156
+ const record = await context.store.getBuild(opts.build)
157
+ if (record === null) {
158
+ throw new Error(
159
+ `no build "${opts.build}" in this store — run 'ab builds --all' or pass --store <ref>`,
160
+ )
161
+ }
162
+ if (!(await buildInRepository(record, context.checkout, opts.exec))) {
163
+ throw new Error(
164
+ `build "${opts.build}" belongs to repository "${record.repo}", not "${context.repo}"`,
165
+ )
166
+ }
167
+ const artifact = await context.store.getArtifact(opts.build, kind, rev)
168
+ if (artifact === null) {
169
+ const available = await context.store.listArtifacts(opts.build)
170
+ const refs = available.map((meta) => `${meta.kind}@${meta.revision}`)
171
+ throw new Error(
172
+ `no "${kind}" artifact${rev !== undefined ? ` at rev ${rev}` : ''} in ` +
173
+ `build "${opts.build}" — available refs: ${refs.join(', ') || '(none)'}`,
174
+ )
175
+ }
176
+
177
+ const outputPath = resolve(opts.targetRepo, opts.outputPath)
178
+ await mkdir(dirname(outputPath), { recursive: true })
179
+ await writeFile(outputPath, artifact.content)
180
+ return { artifact, outputPath }
181
+ })
182
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Shared wiring for the `ab` process entry points. Production calls this with
3
+ * argv only; the repo-local hot entry may additionally supply a per-paint
4
+ * dashboard renderer resolver. All command routing and resource cleanup stay
5
+ * identical between the two entries.
6
+ */
7
+ import { join } from 'node:path'
8
+ import { isSessionlessInvocation, runCli } from './main'
9
+ import { createUpgradeAgentResolver } from './upgrade-agent'
10
+ import { loadDotEnv } from './dotenv'
11
+ import { MissingAmbientContextError, resolveCliEnv, resolveHarvestCliEnv } from './env'
12
+ import { openProductionSessionStore } from './store-opening'
13
+ import { processTerminal, processTerminalInput, type TerminalOut } from './terminal'
14
+ import { installTerminalRestoreHook, type TerminalRestoreProcess } from './terminal-restore'
15
+ import type { DashboardRendererResolver } from './dashboard/render'
16
+ import { loadConfig } from '../config/load'
17
+ import { loadPlugins } from '../plugins/load'
18
+ import { createForge } from '../ports/forge/create'
19
+ import { spawnExec } from '../ports/workspace/git-worktree'
20
+ import { randomIds } from '../ids'
21
+ import { systemClock } from '../store/types'
22
+ import { resolveMainRepo } from './repo-state'
23
+
24
+ /** Upgrade (including its `update` alias) owns Ctrl-C only while raw per-file
25
+ * progress is active. Everywhere else its process must retain Node's ordinary
26
+ * immediate SIGINT termination. */
27
+ export function usesGenericSessionlessSigintHandler(command: string | undefined): boolean {
28
+ return command !== 'upgrade' && command !== 'update'
29
+ }
30
+
31
+ export interface SessionlessSigintBoundary {
32
+ on(event: 'SIGINT', listener: () => void): unknown
33
+ once(event: 'SIGINT', listener: () => void): unknown
34
+ removeListener(event: 'SIGINT', listener: () => void): unknown
35
+ }
36
+
37
+ /** Dispatch keeps its listener through the complete child drain; ordinary
38
+ * sessionless commands preserve the historical one-shot cancellation. */
39
+ export function installSessionlessSigintHandler(
40
+ command: string | undefined,
41
+ onSigint: () => void,
42
+ boundary: SessionlessSigintBoundary = process,
43
+ ): () => void {
44
+ if (!usesGenericSessionlessSigintHandler(command)) return () => {}
45
+ if (command === 'dispatch') boundary.on('SIGINT', onSigint)
46
+ else boundary.once('SIGINT', onSigint)
47
+ return () => boundary.removeListener('SIGINT', onSigint)
48
+ }
49
+
50
+ /** Install the dispatch-only process fallback around the exact CLI lifetime. */
51
+ export async function withDispatchTerminalRestore<T>(
52
+ command: string | undefined,
53
+ terminal: TerminalOut,
54
+ run: () => Promise<T>,
55
+ boundary: TerminalRestoreProcess = process as unknown as TerminalRestoreProcess,
56
+ requestStop: () => void = () => {},
57
+ ): Promise<T> {
58
+ const hook =
59
+ command === 'dispatch'
60
+ ? installTerminalRestoreHook(terminal.modes, boundary, requestStop)
61
+ : undefined
62
+ try {
63
+ return await run()
64
+ } finally {
65
+ hook?.close()
66
+ }
67
+ }
68
+
69
+ export async function runBinary(
70
+ argv: string[],
71
+ resolveDashboardRenderer?: DashboardRendererResolver,
72
+ ): Promise<number> {
73
+ const command = argv[0]
74
+ // Version identity is installation-local and offline; do not inspect even a
75
+ // repository-local .env before routing it.
76
+ if (command !== '--version') {
77
+ // Local .env supplies developer-set secrets (e.g. LINEAR_API_KEY); real
78
+ // environment variables always win over .env values.
79
+ loadDotEnv(join(process.cwd(), '.env'), process.env)
80
+ }
81
+
82
+ const unscopedDeps = {
83
+ workspacePath: process.cwd(),
84
+ processEnv: process.env,
85
+ exec: spawnExec,
86
+ stdout: (line: string) => console.log(line),
87
+ stderr: (line: string) => console.error(line),
88
+ }
89
+
90
+ // Sessionless commands resolve their own repository/store and do not require
91
+ // a phase tuple; durable controls also take a target slug and inspect raw
92
+ // AB_SESSION/AB_BUILD only to reject self-control. The flat-name set and
93
+ // mixed nested-command classifier live beside the switch in main.ts.
94
+ if (isSessionlessInvocation(argv)) {
95
+ // The dispatch watch loop runs until SIGINT; abort the signal so it exits
96
+ // cleanly at the next tick boundary (§15.6-C: in-flight leases expire and
97
+ // a future dispatch re-attaches).
98
+ const controller = new AbortController()
99
+ const onSigint = (): void => controller.abort()
100
+ const ownsSigint = usesGenericSessionlessSigintHandler(command)
101
+ const terminal = processTerminal(process.stdout)
102
+ const removeSigint = installSessionlessSigintHandler(command, onSigint)
103
+ try {
104
+ return await withDispatchTerminalRestore(
105
+ command,
106
+ terminal,
107
+ () =>
108
+ runCli(argv, {
109
+ ...unscopedDeps,
110
+ ...(ownsSigint ? { signal: controller.signal } : {}),
111
+ // `ab dispatch`'s dashboard seam: interactive iff stdout is a real
112
+ // TTY, so a pipe or redirect silently gets plain output.
113
+ terminal,
114
+ input: processTerminalInput(process.stdin),
115
+ ...(command === 'init'
116
+ ? { initInteractive: process.stdin.isTTY === true && process.stdout.isTTY === true }
117
+ : {}),
118
+ upgradeResolverFactory: createUpgradeAgentResolver,
119
+ ...(resolveDashboardRenderer !== undefined ? { resolveDashboardRenderer } : {}),
120
+ }),
121
+ process as unknown as TerminalRestoreProcess,
122
+ onSigint,
123
+ )
124
+ } finally {
125
+ removeSigint()
126
+ }
127
+ }
128
+
129
+ if (command === 'harvest') {
130
+ let harvestEnv: ReturnType<typeof resolveHarvestCliEnv>
131
+ try {
132
+ harvestEnv = resolveHarvestCliEnv(process.env)
133
+ } catch (error) {
134
+ if (error instanceof MissingAmbientContextError) {
135
+ return runCli(argv, unscopedDeps)
136
+ }
137
+ console.error(error instanceof Error ? error.message : String(error))
138
+ return 1
139
+ }
140
+ const store = openProductionSessionStore(harvestEnv)
141
+ try {
142
+ return await runCli(argv, {
143
+ ...unscopedDeps,
144
+ store,
145
+ harvestEnv,
146
+ ids: randomIds(),
147
+ clock: systemClock,
148
+ })
149
+ } finally {
150
+ await store.close()
151
+ }
152
+ }
153
+
154
+ let cliEnv: ReturnType<typeof resolveCliEnv>
155
+ try {
156
+ cliEnv = resolveCliEnv(process.env)
157
+ } catch (error) {
158
+ if (error instanceof MissingAmbientContextError) {
159
+ return runCli(argv, unscopedDeps)
160
+ }
161
+ console.error(error instanceof Error ? error.message : String(error))
162
+ return 1
163
+ }
164
+
165
+ // A scoped phase process is composed from the build worktree's immutable
166
+ // configuration just like dispatch. Load and select the forge before opening
167
+ // the store so plugin failures cannot partially execute terminal plumbing.
168
+ let forge: Awaited<ReturnType<typeof createForge>>
169
+ try {
170
+ const repoRoot = process.cwd()
171
+ const packageRoot = await resolveMainRepo(repoRoot, spawnExec)
172
+ const config = await loadConfig(join(repoRoot, 'autobuild.toml'))
173
+ const plugins = await loadPlugins(config.plugins, repoRoot, { packageRoot })
174
+ forge = await createForge({
175
+ name: config.forge,
176
+ registry: plugins,
177
+ env: process.env,
178
+ repoRoot,
179
+ })
180
+ } catch (error) {
181
+ console.error(error instanceof Error ? error.message : String(error))
182
+ return 1
183
+ }
184
+
185
+ const store = openProductionSessionStore(cliEnv)
186
+ try {
187
+ return await runCli(argv, {
188
+ ...unscopedDeps,
189
+ store,
190
+ env: cliEnv,
191
+ forge,
192
+ ids: randomIds(),
193
+ clock: systemClock,
194
+ })
195
+ } finally {
196
+ await store.close()
197
+ }
198
+ }