@defrex/autobuild 0.8.0 → 0.10.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 (182) hide show
  1. package/README.md +1 -1
  2. package/SPEC.md +323 -26
  3. package/bin/ab-build-runner.ts +8 -2
  4. package/docs/architecture.md +166 -14
  5. package/docs/assets/headline-wide.png +0 -0
  6. package/docs/configuration.md +175 -28
  7. package/docs/hosted-dispatcher.md +69 -13
  8. package/docs/operator-api.md +2 -2
  9. package/docs/remote-store-protocol.md +204 -16
  10. package/docs/remote-ticket-protocol.md +1 -1
  11. package/docs/setup.md +12 -9
  12. package/docs/website.md +14 -0
  13. package/package.json +45 -23
  14. package/packages/core/src/cli/artifact.ts +130 -6
  15. package/packages/core/src/cli/binary.ts +9 -1
  16. package/packages/core/src/cli/build-control.ts +110 -13
  17. package/packages/core/src/cli/context.ts +46 -0
  18. package/packages/core/src/cli/dashboard/facts.ts +215 -0
  19. package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
  20. package/packages/core/src/cli/dashboard/model.ts +141 -153
  21. package/packages/core/src/cli/dashboard/poll.ts +76 -12
  22. package/packages/core/src/cli/dashboard/render.ts +34 -5
  23. package/packages/core/src/cli/dispatch-frontend.ts +54 -5
  24. package/packages/core/src/cli/dispatch-process.ts +1 -13
  25. package/packages/core/src/cli/dispatch.ts +392 -75
  26. package/packages/core/src/cli/harvest.ts +122 -8
  27. package/packages/core/src/cli/help.ts +62 -9
  28. package/packages/core/src/cli/init-readiness-remote.ts +110 -0
  29. package/packages/core/src/cli/init-readiness-shared.ts +72 -0
  30. package/packages/core/src/cli/init-validation.ts +266 -222
  31. package/packages/core/src/cli/installation.ts +50 -8
  32. package/packages/core/src/cli/main.ts +70 -11
  33. package/packages/core/src/cli/mcp.ts +23 -1
  34. package/packages/core/src/cli/plugin.ts +7 -4
  35. package/packages/core/src/cli/remote-poll.ts +277 -0
  36. package/packages/core/src/cli/repo-state.ts +22 -0
  37. package/packages/core/src/cli/repository-status.ts +39 -1
  38. package/packages/core/src/cli/self-update.ts +77 -2
  39. package/packages/core/src/cli/status.ts +150 -28
  40. package/packages/core/src/cli/terminals.ts +2 -1
  41. package/packages/core/src/cli/ticket-assets.ts +247 -0
  42. package/packages/core/src/cli/ticket.ts +453 -8
  43. package/packages/core/src/cli/wait.ts +157 -41
  44. package/packages/core/src/cli/watch.ts +138 -168
  45. package/packages/core/src/config/live.ts +23 -0
  46. package/packages/core/src/config/pipeline-source.ts +388 -0
  47. package/packages/core/src/config/schema.ts +251 -19
  48. package/packages/core/src/events/catalog.ts +1 -0
  49. package/packages/core/src/events/globs.ts +47 -0
  50. package/packages/core/src/events/payloads.ts +52 -2
  51. package/packages/core/src/events/repository.ts +52 -1
  52. package/packages/core/src/events/sessions.ts +69 -10
  53. package/packages/core/src/events/workspace-remote.ts +12 -0
  54. package/packages/core/src/integration/harness.ts +115 -2
  55. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
  56. package/packages/core/src/kernel/auto-merge-default.ts +157 -0
  57. package/packages/core/src/kernel/auto-merge.ts +99 -22
  58. package/packages/core/src/kernel/dispatch-settings.ts +42 -17
  59. package/packages/core/src/kernel/dispatch-status.ts +23 -0
  60. package/packages/core/src/kernel/engine.ts +9 -438
  61. package/packages/core/src/kernel/generators/build-log.ts +259 -0
  62. package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
  63. package/packages/core/src/kernel/generators/session-log.ts +72 -0
  64. package/packages/core/src/kernel/harvest.ts +62 -18
  65. package/packages/core/src/kernel/incremental-contract.ts +75 -0
  66. package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
  67. package/packages/core/src/kernel/incremental.ts +53 -0
  68. package/packages/core/src/kernel/log-index.ts +620 -0
  69. package/packages/core/src/kernel/phases.ts +16 -2
  70. package/packages/core/src/kernel/pr-attachments.ts +164 -46
  71. package/packages/core/src/kernel/reducer-registry.ts +164 -0
  72. package/packages/core/src/kernel/reducer.ts +254 -73
  73. package/packages/core/src/operator/annotations.ts +6 -0
  74. package/packages/core/src/operator/control.ts +19 -5
  75. package/packages/core/src/operator/index.ts +71 -6
  76. package/packages/core/src/operator/query.ts +253 -24
  77. package/packages/core/src/operator/read-view.ts +98 -0
  78. package/packages/core/src/operator/registry.ts +55 -10
  79. package/packages/core/src/operator/requests.ts +9 -6
  80. package/packages/core/src/operator/sandbox-host.ts +140 -0
  81. package/packages/core/src/operator/sandbox.ts +247 -14
  82. package/packages/core/src/operator/ticket-source-backend.ts +47 -0
  83. package/packages/core/src/orchestrator/approvals.ts +115 -0
  84. package/packages/core/src/orchestrator/conversation.ts +167 -0
  85. package/packages/core/src/orchestrator/model.ts +86 -0
  86. package/packages/core/src/orchestrator/prompt.ts +61 -0
  87. package/packages/core/src/orchestrator/tools.ts +117 -0
  88. package/packages/core/src/orchestrator/turn-runner.ts +457 -0
  89. package/packages/core/src/plugin-sdk/index.ts +51 -1
  90. package/packages/core/src/plugins/load.ts +135 -7
  91. package/packages/core/src/plugins/manifest.ts +183 -10
  92. package/packages/core/src/plugins/registry.ts +73 -4
  93. package/packages/core/src/ports/forge/create.ts +1 -1
  94. package/packages/core/src/ports/forge/fake.ts +31 -0
  95. package/packages/core/src/ports/forge/github-transport.ts +13 -2
  96. package/packages/core/src/ports/forge/github.ts +13 -2
  97. package/packages/core/src/ports/forge/local-git.ts +31 -1
  98. package/packages/core/src/ports/runner/claude.ts +2 -2
  99. package/packages/core/src/ports/runner/codex.ts +6 -2
  100. package/packages/core/src/ports/runner/fake.ts +7 -1
  101. package/packages/core/src/ports/runner/pi.ts +8 -6
  102. package/packages/core/src/ports/runner/provider-error.ts +13 -3
  103. package/packages/core/src/ports/tickets/fake.ts +5 -1
  104. package/packages/core/src/ports/tickets/file.ts +169 -48
  105. package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
  106. package/packages/core/src/ports/tickets/linear.ts +33 -1
  107. package/packages/core/src/ports/tickets/operations.ts +1 -1
  108. package/packages/core/src/ports/tickets/remote.ts +8 -1
  109. package/packages/core/src/ports/types.ts +49 -3
  110. package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
  111. package/packages/core/src/ports/workspace/create.ts +74 -39
  112. package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
  113. package/packages/core/src/ports/workspace/fake.ts +169 -4
  114. package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
  115. package/packages/core/src/ports/workspace/github-origin.ts +36 -0
  116. package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
  117. package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
  118. package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
  119. package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
  120. package/packages/core/src/processes/build-child.ts +17 -2
  121. package/packages/core/src/processes/build-execution-state.ts +68 -15
  122. package/packages/core/src/processes/build-runner.ts +238 -93
  123. package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
  124. package/packages/core/src/processes/dispatcher.ts +501 -201
  125. package/packages/core/src/processes/execution-settlement.ts +72 -39
  126. package/packages/core/src/processes/harvest-child.ts +5 -0
  127. package/packages/core/src/processes/harvest-execution-state.ts +41 -29
  128. package/packages/core/src/processes/harvest-runner.ts +66 -19
  129. package/packages/core/src/processes/harvest.ts +196 -24
  130. package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
  131. package/packages/core/src/processes/orchestrator-tick.ts +360 -0
  132. package/packages/core/src/processes/publication-state.ts +164 -52
  133. package/packages/core/src/processes/repo-view.ts +1436 -0
  134. package/packages/core/src/processes/sandbox-state.ts +36 -2
  135. package/packages/core/src/processes/tick-state.ts +33 -0
  136. package/packages/core/src/store/adapter.ts +41 -0
  137. package/packages/core/src/store/build-scope.ts +90 -0
  138. package/packages/core/src/store/contract.ts +1205 -0
  139. package/packages/core/src/store/digest.ts +102 -0
  140. package/packages/core/src/store/local/schema.ts +46 -2
  141. package/packages/core/src/store/local/store.ts +445 -4
  142. package/packages/core/src/store/memory.ts +246 -0
  143. package/packages/core/src/store/new-build.ts +17 -0
  144. package/packages/core/src/store/phase-session.ts +94 -0
  145. package/packages/core/src/store/remote/client.ts +310 -2
  146. package/packages/core/src/store/remote/index.ts +14 -2
  147. package/packages/core/src/store/remote/protocol.ts +115 -12
  148. package/packages/core/src/store/remote/version.ts +6 -2
  149. package/packages/core/src/store/repo-state-events.ts +205 -0
  150. package/packages/core/src/store/retention.ts +7 -0
  151. package/packages/core/src/store/session-handle.ts +94 -0
  152. package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
  153. package/packages/core/src/store/snapshots.ts +88 -0
  154. package/packages/core/src/store/streams/session-writer.ts +42 -2
  155. package/packages/core/src/store/streams/wait.ts +4 -5
  156. package/packages/core/src/store/ticket-assets.ts +458 -0
  157. package/packages/core/src/store/types.ts +160 -1
  158. package/packages/core/src/testing/index.ts +59 -0
  159. package/skills/code-review/SKILL.md +5 -1
  160. package/skills/guide/SKILL.md +152 -40
  161. package/skills/guide/references/operator-api.md +2 -2
  162. package/skills/guide/references/plugin-authoring.md +17 -0
  163. package/skills/guide/references/remote-store-protocol.md +204 -16
  164. package/skills/guide/references/remote-ticket-protocol.md +1 -1
  165. package/skills/guide/references/setup.md +12 -9
  166. package/skills/implement/SKILL.md +3 -0
  167. package/skills/operate/SKILL.md +126 -0
  168. package/skills/plan/SKILL.md +4 -0
  169. package/skills/plan-review/SKILL.md +5 -1
  170. package/skills/spec/SKILL.md +46 -3
  171. package/skills/tickets/SKILL.md +57 -5
  172. package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
  173. package/packages/core/src/cli/store-opening.contract.ts +0 -289
  174. package/packages/core/src/cli/testkit.ts +0 -194
  175. package/packages/core/src/markdown.ts +0 -67
  176. package/packages/core/src/operator/client.ts +0 -311
  177. package/packages/core/src/operator/server.ts +0 -616
  178. package/packages/core/src/operator/sessions.ts +0 -223
  179. package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
  180. package/packages/core/src/ports/tickets/remote-server.ts +0 -185
  181. package/packages/core/src/store/remote/server.ts +0 -923
  182. package/packages/core/src/testing/store-failures.ts +0 -32
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Filesystem and note plumbing for `ab ticket attach` / `asset get` /
3
+ * `asset rm` (SPEC §8.8). The store owns the bytes and the validation
4
+ * (store/ticket-assets.ts); this module owns the two edges that touch the
5
+ * local filesystem and the one outward projection: the note left on the
6
+ * ticket in its source (SPEC §13).
7
+ */
8
+ import { lstat, mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises'
9
+ import { basename, dirname, join, resolve, sep } from 'node:path'
10
+ import {
11
+ ticketAssetEntriesError,
12
+ ticketAssetSizeError,
13
+ validateTicketAssetPath,
14
+ validateTicketAssetStructure,
15
+ type TicketAsset,
16
+ type TicketAssetEntryInput,
17
+ type TicketAssetInput,
18
+ type TicketAssetLayout,
19
+ type TicketAssetLimits,
20
+ type TicketAssetMeta,
21
+ } from '../store/ticket-assets'
22
+
23
+ export type TicketAssetNoteEvent = 'attached' | 'replaced' | 'removed'
24
+
25
+ export function formatAssetSize(bytes: number): string {
26
+ if (bytes < 1024) return `${bytes} B`
27
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`
28
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`
29
+ }
30
+
31
+ function plural(count: number, word: string): string {
32
+ return `${count} ${word}${count === 1 ? '' : 's'}`
33
+ }
34
+
35
+ /**
36
+ * The note left on the ticket in its source. Plain Markdown with backticks
37
+ * (no bold, no source-specific syntax) so every source and plugin renders it.
38
+ * It names the asset and how to fetch it, and states that the contents are in
39
+ * the Autobuild store, not in this ticket.
40
+ */
41
+ export function ticketAssetNote(
42
+ event: TicketAssetNoteEvent,
43
+ meta: TicketAssetMeta,
44
+ cli = 'ab',
45
+ ): string {
46
+ const fileCount = meta.entries.filter((entry) => entry.type === 'file').length
47
+ const label = `\`${meta.kind}/${meta.name}\``
48
+ const get = `${cli} ticket asset get ${meta.ticketId} ${meta.kind} ${meta.name} <dest>`
49
+ const contents = `${plural(fileCount, 'file')}, ${formatAssetSize(meta.size)}`
50
+ switch (event) {
51
+ case 'attached':
52
+ return [
53
+ `Ticket asset attached: ${label} (revision ${meta.revision}, ${contents}).`,
54
+ '',
55
+ 'The contents live in the Autobuild store, not in this ticket.',
56
+ `Download the latest revision: \`${get}\``,
57
+ `Download this exact revision: \`${get} --rev ${meta.revision}\``,
58
+ ].join('\n')
59
+ case 'replaced':
60
+ return [
61
+ `Ticket asset replaced: ${label} is now revision ${meta.revision} (${contents}).`,
62
+ '',
63
+ 'The contents live in the Autobuild store, not in this ticket. Earlier revisions stay retrievable.',
64
+ `Download the latest revision: \`${get}\``,
65
+ `Download this exact revision: \`${get} --rev ${meta.revision}\``,
66
+ ].join('\n')
67
+ case 'removed':
68
+ return [
69
+ `Ticket asset removed: ${label} is no longer one of this ticket's current assets (last revision ${meta.revision}).`,
70
+ '',
71
+ 'The contents live in the Autobuild store, not in this ticket. That revision stays retrievable by number.',
72
+ `Download it: \`${get} --rev ${meta.revision}\``,
73
+ ].join('\n')
74
+ }
75
+ }
76
+
77
+ export interface AssetWalk {
78
+ layout: TicketAssetLayout
79
+ /** Default asset name: the path's basename. */
80
+ basename: string
81
+ files: { path: string; abs: string; size: number }[]
82
+ dirs: string[]
83
+ }
84
+
85
+ /**
86
+ * Walk a file or folder for attachment. Symlinks and non-regular files are
87
+ * refused. The folder root is never an entry — it is represented only by the
88
+ * `tree` layout — so every entry path is relative, with no empty or `.`
89
+ * segment, and an empty folder walks to zero entries.
90
+ */
91
+ export async function walkAssetPath(path: string): Promise<AssetWalk> {
92
+ const root = resolve(path)
93
+ const info = await lstat(root).catch((error: NodeJS.ErrnoException) => {
94
+ if (error.code === 'ENOENT') throw new Error(`${path}: no such file or directory`)
95
+ throw error
96
+ })
97
+ if (info.isSymbolicLink()) {
98
+ throw new Error(`${path}: symbolic links cannot be attached — attach the real file or folder`)
99
+ }
100
+ if (info.isFile()) {
101
+ return {
102
+ layout: 'file',
103
+ basename: basename(root),
104
+ files: [{ path: basename(root), abs: root, size: info.size }],
105
+ dirs: [],
106
+ }
107
+ }
108
+ if (!info.isDirectory()) {
109
+ throw new Error(`${path}: only regular files and folders can be attached`)
110
+ }
111
+ const files: AssetWalk['files'] = []
112
+ const dirs: string[] = []
113
+ const visit = async (abs: string, rel: string): Promise<void> => {
114
+ const names = (await readdir(abs)).sort()
115
+ for (const name of names) {
116
+ const childAbs = join(abs, name)
117
+ const childRel = rel === '' ? name : `${rel}/${name}`
118
+ const child = await lstat(childAbs)
119
+ if (child.isSymbolicLink()) {
120
+ throw new Error(
121
+ `${childAbs}: symbolic links cannot be attached — remove it or attach its target`,
122
+ )
123
+ }
124
+ if (child.isDirectory()) {
125
+ dirs.push(childRel)
126
+ await visit(childAbs, childRel)
127
+ } else if (child.isFile()) {
128
+ files.push({ path: childRel, abs: childAbs, size: child.size })
129
+ } else {
130
+ throw new Error(`${childAbs}: only regular files and folders can be attached`)
131
+ }
132
+ }
133
+ }
134
+ await visit(root, '')
135
+ return { layout: 'tree', basename: basename(root), files, dirs }
136
+ }
137
+
138
+ /**
139
+ * Build the put input from a walk. Everything the stat sizes can refuse —
140
+ * names, paths, structure, entry count, total bytes — is refused here, before
141
+ * any file's bytes are read.
142
+ */
143
+ export async function buildAssetInput(
144
+ walk: AssetWalk,
145
+ asset: { kind: string; name: string },
146
+ limits: TicketAssetLimits,
147
+ ): Promise<TicketAssetInput> {
148
+ const entryCount = walk.files.length + walk.dirs.length
149
+ if (entryCount > limits.maxEntries) throw ticketAssetEntriesError(entryCount, limits.maxEntries)
150
+ const total = walk.files.reduce((sum, file) => sum + file.size, 0)
151
+ if (total > limits.maxBytes) throw ticketAssetSizeError(total, limits.maxBytes)
152
+ validateTicketAssetStructure(
153
+ {
154
+ kind: asset.kind,
155
+ name: asset.name,
156
+ layout: walk.layout,
157
+ entries: [
158
+ ...walk.dirs.map((path) => ({ type: 'dir' as const, path })),
159
+ ...walk.files.map((file) => ({ type: 'file' as const, path: file.path, size: file.size })),
160
+ ],
161
+ },
162
+ limits,
163
+ )
164
+ const entries: TicketAssetEntryInput[] = walk.dirs.map((path) => ({ type: 'dir', path }))
165
+ for (const file of walk.files) {
166
+ entries.push({
167
+ type: 'file',
168
+ path: file.path,
169
+ content: new Uint8Array(await readFile(file.abs)),
170
+ })
171
+ }
172
+ return { kind: asset.kind, name: asset.name, layout: walk.layout, entries }
173
+ }
174
+
175
+ /** Resolve a stored entry path under `root`, refusing anything that would land
176
+ * outside it. Stored paths are never trusted, even though upload validates. */
177
+ function guardedTarget(root: string, path: string): string {
178
+ validateTicketAssetPath(path)
179
+ const target = resolve(root, path)
180
+ if (target !== root && !target.startsWith(`${root}${sep}`)) {
181
+ throw new Error(`ticket asset path "${path}" escapes the destination`)
182
+ }
183
+ return target
184
+ }
185
+
186
+ /** Refuse to write through a symlink: every existing component of `target`
187
+ * below `root`, and `target` itself, must not be one. Containment is checked
188
+ * against the real filesystem, since mkdir and writeFile follow links. */
189
+ async function assertNoSymlinks(root: string, target: string): Promise<void> {
190
+ const rel = target === root ? '' : target.slice(root.length + 1)
191
+ let current = root
192
+ const parts = rel === '' ? [] : rel.split(sep)
193
+ for (const part of ['', ...parts]) {
194
+ if (part !== '') current = join(current, part)
195
+ if (current === root) continue
196
+ const info = await lstat(current).catch(() => null)
197
+ if (info === null) return
198
+ if (info.isSymbolicLink()) {
199
+ throw new Error(`${current}: refusing to write through a symbolic link in the destination`)
200
+ }
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Write a downloaded asset to `dest`. A `file` asset becomes the file `dest`
206
+ * (or `dest/<filename>` when `dest` is an existing directory); a `tree` asset
207
+ * becomes the directory `dest` itself — created even when empty — with every
208
+ * `dir` entry made, then every file written.
209
+ */
210
+ export async function writeAssetTo(asset: TicketAsset, dest: string): Promise<string> {
211
+ const root = resolve(dest)
212
+ if (asset.meta.layout === 'file') {
213
+ const file = asset.entries.find((entry) => entry.type === 'file')
214
+ if (file === undefined || file.type !== 'file') {
215
+ throw new Error(`ticket asset ${asset.meta.kind}/${asset.meta.name} holds no file`)
216
+ }
217
+ const isDir = await stat(root)
218
+ .then((info) => info.isDirectory())
219
+ .catch(() => false)
220
+ const target = isDir ? guardedTarget(root, file.path) : root
221
+ if (isDir) await assertNoSymlinks(root, target)
222
+ else {
223
+ const existing = await lstat(target).catch(() => null)
224
+ if (existing?.isSymbolicLink()) {
225
+ throw new Error(`${target}: refusing to write through a symbolic link`)
226
+ }
227
+ }
228
+ await mkdir(dirname(target), { recursive: true })
229
+ await writeFile(target, file.content)
230
+ return target
231
+ }
232
+ await mkdir(root, { recursive: true })
233
+ for (const entry of asset.entries) {
234
+ if (entry.type !== 'dir') continue
235
+ const target = guardedTarget(root, entry.path)
236
+ await assertNoSymlinks(root, target)
237
+ await mkdir(target, { recursive: true })
238
+ }
239
+ for (const entry of asset.entries) {
240
+ if (entry.type !== 'file') continue
241
+ const target = guardedTarget(root, entry.path)
242
+ await assertNoSymlinks(root, target)
243
+ await mkdir(dirname(target), { recursive: true })
244
+ await writeFile(target, entry.content)
245
+ }
246
+ return root
247
+ }