@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
@@ -35,7 +35,7 @@ import {
35
35
  loadScanPacket,
36
36
  validateProposalCoverage,
37
37
  } from '../processes/harvest'
38
- import type { BuildStore } from '../store/types'
38
+ import type { BuildStore, StreamRecord } from '../store/types'
39
39
  import type { HarvestCliEnv } from './env'
40
40
  import { withSessionlessStore, type StoreOpener } from './store-opening'
41
41
 
@@ -121,7 +121,8 @@ async function readArtifactJson(
121
121
 
122
122
  export async function buildHarvestContext(deps: HarvestCliDeps): Promise<HarvestContextManifest> {
123
123
  const { store, env, workspacePath } = deps
124
- const events = await store.getRepoEvents(env.repo)
124
+ // Bounded read (AUT-489): runForEnv reduces harvest facts only.
125
+ const events = await store.getRepoStateEvents(env.repo)
125
126
  const run = runForEnv(events, env)
126
127
  const packet = await loadScanPacket(store, env.repo, run.scan)
127
128
  const abDir = join(workspacePath, '.ab')
@@ -217,7 +218,9 @@ export async function submitHarvestProposals(
217
218
  `'ab harvest submit' is only valid in synthesize@<round>; current phase is ${env.phase}@${env.round}`,
218
219
  )
219
220
  }
220
- const events = await store.getRepoEvents(env.repo)
221
+ // Bounded read (AUT-489): runForEnv and assertNoSessionTerminal consume
222
+ // harvest facts only.
223
+ const events = await store.getRepoStateEvents(env.repo)
221
224
  const run = runForEnv(events, env)
222
225
  assertNoSessionTerminal(events, env)
223
226
 
@@ -307,7 +310,9 @@ export async function submitHarvestVerdict(
307
310
  `harvest verdict "${opts.verdict}" is invalid — expected approve | revise | escalate`,
308
311
  )
309
312
  }
310
- const events = await store.getRepoEvents(env.repo)
313
+ // Bounded read (AUT-489): runForEnv and assertNoSessionTerminal consume
314
+ // harvest facts only.
315
+ const events = await store.getRepoStateEvents(env.repo)
311
316
  const run = runForEnv(events, env)
312
317
  assertNoSessionTerminal(events, env)
313
318
  if (proposalArtifactForRound(run, env.round) === undefined) {
@@ -398,6 +403,22 @@ export interface HarvestFiledStatusView {
398
403
  blockers?: { declared: string[]; derived: string[] }
399
404
  }
400
405
 
406
+ /** One harvest session's discoverability row: its stream id and open/closed
407
+ * status, at parity with the build-session rows. `stream` is absent on
408
+ * sessions that never streamed. */
409
+ export interface HarvestSessionStatusView {
410
+ session: string
411
+ role: 'harvest' | 'harvest-review'
412
+ step: 'synthesize' | 'review'
413
+ round: number
414
+ /** Live-view stream id (SPEC §9); absent on sessions that never streamed. */
415
+ stream?: string
416
+ /** Open until the session's `harvest.session.ended` lands; the store's
417
+ * records refine this to the stream's authoritative status when available. */
418
+ streamStatus: 'open' | 'closed'
419
+ status: 'open' | 'ended'
420
+ }
421
+
401
422
  export interface HarvestRunStatusView {
402
423
  run: string
403
424
  status: HarvestRunState['status']
@@ -408,6 +429,7 @@ export interface HarvestRunStatusView {
408
429
  steps: HarvestRunState['steps']
409
430
  rounds: number
410
431
  filed: HarvestFiledStatusView[]
432
+ sessions: HarvestSessionStatusView[]
411
433
  escalation?: HarvestRunState['escalation']
412
434
  failure?: HarvestRunState['failure']
413
435
  recovery: HarvestRecoveryStatus
@@ -481,7 +503,73 @@ function projectRecovery(run: HarvestRunState | undefined): HarvestRecoveryStatu
481
503
  }
482
504
  }
483
505
 
484
- function projectHarvestRunStatus(run: HarvestRunState): HarvestRunStatusView {
506
+ /** Pair the append-only `harvest.session.started` / `harvest.session.ended`
507
+ * facts per run without introducing transition state. `streams` is the
508
+ * optional authoritative enrichment from `listStreams` on the repo scope:
509
+ * records whose label matches `session:<id>` supply each session's stream id
510
+ * and open/closed status.
511
+ *
512
+ * Verified (AUT-574): no repo-identity comparison happens in this code —
513
+ * membership is decided store-side by `listStreams({ kind: 'repo', repo })`,
514
+ * scoped by the same resolved identity the caller passes, and the `scope.kind`
515
+ * check below is only a defensive kind filter over records the store already
516
+ * scoped. Store scoping semantics are out of scope here. */
517
+ function projectHarvestSessions(
518
+ events: RepositoryEvent[],
519
+ streams?: readonly StreamRecord[],
520
+ ): Map<string, HarvestSessionStatusView[]> {
521
+ const recordsByLabel = new Map<string, StreamRecord>()
522
+ if (streams !== undefined) {
523
+ for (const record of streams) {
524
+ if (record.scope.kind === 'repo') recordsByLabel.set(record.label, record)
525
+ }
526
+ }
527
+
528
+ const byRun = new Map<string, HarvestSessionStatusView[]>()
529
+ const open = new Map<string, HarvestSessionStatusView>()
530
+ for (const event of events) {
531
+ if (event.type === 'harvest.session.started') {
532
+ const record = recordsByLabel.get(`session:${event.payload.session}`)
533
+ const view: HarvestSessionStatusView = {
534
+ session: event.payload.session,
535
+ role: event.payload.role,
536
+ step: event.payload.step,
537
+ round: event.payload.round,
538
+ status: 'open',
539
+ // The store's records are authoritative when available; without them
540
+ // the pairing degrades gracefully (open until `harvest.session.ended`).
541
+ ...(record !== undefined
542
+ ? { stream: record.id, streamStatus: record.status }
543
+ : {
544
+ ...(event.payload.stream !== undefined ? { stream: event.payload.stream } : {}),
545
+ streamStatus: event.payload.stream !== undefined ? 'open' : 'closed',
546
+ }),
547
+ }
548
+ const list = byRun.get(event.payload.run) ?? []
549
+ list.push(view)
550
+ byRun.set(event.payload.run, list)
551
+ open.set(view.session, view)
552
+ continue
553
+ }
554
+ if (event.type !== 'harvest.session.ended') continue
555
+ const started = open.get(event.payload.session)
556
+ if (started === undefined) continue
557
+ started.status = 'ended'
558
+ // A session bracket has ended: without an authoritative record its
559
+ // stream reads closed.
560
+ const record = recordsByLabel.get(`session:${started.session}`)
561
+ if (record !== undefined) {
562
+ started.stream = record.id
563
+ started.streamStatus = record.status
564
+ } else {
565
+ started.streamStatus = 'closed'
566
+ }
567
+ open.delete(started.session)
568
+ }
569
+ return byRun
570
+ }
571
+
572
+ function projectHarvestRunStatus(run: HarvestRunState): Omit<HarvestRunStatusView, 'sessions'> {
485
573
  return {
486
574
  run: run.run,
487
575
  status: run.status,
@@ -506,6 +594,7 @@ export function projectHarvestStatus(
506
594
  repo: string,
507
595
  events: RepositoryEvent[],
508
596
  newestEvents?: number,
597
+ streams?: readonly StreamRecord[],
509
598
  ): HarvestStatusView {
510
599
  const state = reduceHarvest(events)
511
600
  const history =
@@ -515,7 +604,13 @@ export function projectHarvestStatus(
515
604
  const included = new Set(
516
605
  [...unresolved, ...open, ...(state.latest ? [state.latest] : [])].map((run) => run.run),
517
606
  )
518
- const runs = state.runs.filter((run) => included.has(run.run)).map(projectHarvestRunStatus)
607
+ const sessionsByRun = projectHarvestSessions(events, streams)
608
+ const runs = state.runs
609
+ .filter((run) => included.has(run.run))
610
+ .map((run) => ({
611
+ ...projectHarvestRunStatus(run),
612
+ sessions: sessionsByRun.get(run.run) ?? [],
613
+ }))
519
614
  const unresolvedIds = new Set(unresolved.map((run) => run.run))
520
615
  const primary =
521
616
  runs.find((run) => unresolvedIds.has(run.run)) ??
@@ -568,6 +663,13 @@ function renderHarvestRunStatus(run: HarvestRunStatusView, paused: boolean): str
568
663
  `${step.outcome ?? (step.completedSeq === undefined ? 'running' : 'done')}`,
569
664
  )
570
665
  }
666
+ for (const session of run.sessions) {
667
+ const stream =
668
+ session.stream !== undefined ? ` stream ${session.stream} (${session.streamStatus})` : ''
669
+ lines.push(
670
+ ` session ${session.session} (${session.role}, ${session.step}@${session.round})${stream}`,
671
+ )
672
+ }
571
673
  if (run.recovery.stopped !== undefined) {
572
674
  lines.push(
573
675
  `stopped at: ${run.recovery.stopped.step}${
@@ -689,8 +791,20 @@ export async function abHarvestStatus(opts: HarvestStatusOpts): Promise<void> {
689
791
  },
690
792
  async ({ store, repo }) => {
691
793
  const record = await store.getRepo(repo)
692
- const events = record === null ? [] : await store.getRepoEvents(repo)
693
- const view = projectHarvestStatus(repo, events, opts.events)
794
+ // Bounded read (AUT-489): projectHarvestStatus reduces harvest facts only.
795
+ const events = record === null ? [] : await store.getRepoStateEvents(repo)
796
+ // Best-effort stream enrichment for the session rows, mirroring the
797
+ // degrade-gracefully pattern of 'ab build status': a store that cannot
798
+ // list streams keeps the payload-derived projection instead of failing.
799
+ let streams: StreamRecord[] | undefined
800
+ if (record !== null) {
801
+ try {
802
+ streams = await store.listStreams({ kind: 'repo', repo })
803
+ } catch {
804
+ streams = undefined
805
+ }
806
+ }
807
+ const view = projectHarvestStatus(repo, events, opts.events, streams)
694
808
  if (opts.json === true) opts.stdout(JSON.stringify(view, null, 2))
695
809
  else for (const line of renderHarvestStatus(view)) opts.stdout(line)
696
810
  },
@@ -163,8 +163,13 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
163
163
  ' ab ticket block <id> <blocker-id[,blocker-id...]> [--json]',
164
164
  ' ab ticket unblock <id> <blocker-id[,blocker-id...]> [--json]',
165
165
  ' ab ticket list [--state <state>] [--labels a,b] [--json]',
166
- ' ab ticket show <id> [--json]',
166
+ ' ab ticket show <id> [--store <ref>] [--json]',
167
167
  ' ab ticket move <id> <state> [--json]',
168
+ ' ab ticket states [--json]',
169
+ ' ab ticket state create <name> [--about <text>] [--json]',
170
+ ' ab ticket attach <id> <kind> <path> [--name <name>] [--store <ref>] [--json]',
171
+ ' ab ticket asset get <id> <kind> <name> <dest> [--rev <n>] [--store <ref>] [--json]',
172
+ ' ab ticket asset rm <id> <kind> <name> [--store <ref>] [--json]',
168
173
  '',
169
174
  'Operate on the configured [tickets] source (§8.8); all ticket commands run',
170
175
  'outside sessions. create files a ticket and may establish initial blockers.',
@@ -177,11 +182,35 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
177
182
  '',
178
183
  'With no list filters, list uses the same ready criteria as dispatch. show one ticket, including its body/spec.',
179
184
  'Human output is the default. --json emits exactly one bare value: Ticket[]',
180
- 'for list and the complete resulting Ticket for every other subcommand.',
185
+ 'for list and the complete resulting Ticket for every other subcommand; show',
186
+ 'adds an assets array (kind, name, revision, size, layout, fileCount).',
187
+ '',
188
+ 'Ticket assets are files or folders (a design export, a reference screenshot, a',
189
+ 'sample payload) attached to a ticket and kept in the BuildStore, never in the',
190
+ 'ticket source. attach stores <path> under <kind> and a name (default: the',
191
+ 'basename); attaching the same kind and name again makes a new revision and',
192
+ 'earlier ones stay retrievable. asset get writes the exact bytes of the latest',
193
+ '(or --rev) revision to <dest>: a file asset to that file, a folder asset to',
194
+ 'that directory. asset rm drops it from the current assets; revisions already',
195
+ 'pinned by a build stay retrievable. attach and rm leave a note on the ticket in',
196
+ 'its source naming the asset and how to download it. Assets over the store',
197
+ 'limit (25 MiB by default, lower on a hosted deployment) are refused.',
198
+ '',
199
+ 'states lists every state of the source: lifecycle states first (triage, ready,',
200
+ 'doing, done), then custom ones alphabetically, each with the roles it plays',
201
+ 'under the current config (create, ready, claimed, triage, proposal, done, or',
202
+ 'none), its ticket count, and its purpose when set. --json emits one bare',
203
+ 'array of {name, roles, tickets, about?}. state create makes a state, e.g.',
204
+ 'icebox, and is idempotent: an existing state is reported, and --about (one',
205
+ 'line) records or replaces its purpose. A name is one to forty lowercase',
206
+ 'letters, digits, and hyphens, starting with a letter, and may not match an',
207
+ 'existing state case-insensitively. There is no rename or delete. Only the file',
208
+ 'source supports both; others exit nonzero naming the source. A state with no',
209
+ 'purpose and no tickets is an empty directory that git does not track.',
181
210
  '',
182
211
  'Ticket ids are source-local (for example AUT-8 or file-1). For block and',
183
212
  'unblock, the first id is always the ticket being changed. State names and',
184
- 'unknown-id errors come from the configured source.',
213
+ 'unknown-id errors come from the configured source; ab ticket states lists them.',
185
214
  ]),
186
215
  },
187
216
  {
@@ -227,6 +256,7 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
227
256
  detail: page([
228
257
  'Usage:',
229
258
  ' ab build status <slug> [--events <n>] [--json] [--store <ref>]',
259
+ ' ab build asset get <slug> <kind> <name> <dest> [--rev <n>] [--json] [--store <ref>]',
230
260
  '',
231
261
  'Show detailed state for one build: unresolved escalations, open sessions,',
232
262
  'verify progress, PR state, durable-event progress, heartbeat, and lease health.',
@@ -234,6 +264,10 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
234
264
  'order. --json emits the complete projection; --store overrides the',
235
265
  'BuildStore path or URL.',
236
266
  '',
267
+ 'asset get downloads a ticket asset exactly as the build froze it at its claim',
268
+ '(or at the last spec revision from the ticket), also after the build has',
269
+ 'finished. --rev <n> names an earlier pinned revision.',
270
+ '',
237
271
  'This command is read-only and requires no session identity for operator use.',
238
272
  'Inside a phase, a complete ambient tuple permits only the ambient build;',
239
273
  'foreign builds and malformed or partial identity are rejected.',
@@ -294,9 +328,12 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
294
328
  'Exit codes: the watch ends with exit 0 when --timeout elapses, when --count records',
295
329
  'have been emitted, or, only when slugs were named, when every named build has reached a',
296
330
  'terminal status. Usage errors, scope violations, unknown slugs, cursor rejection, and a',
297
- 'store that cannot be opened at start exit 1 with a message on stderr. A store read that',
298
- 'fails after the watch has started is reported once on stderr and retried at the next',
299
- 'interval without ending the watch or duplicating or skipping any event.',
331
+ 'store that cannot be opened at start exit 1 with a message on stderr. A store read',
332
+ 'that fails after the watch has started is reported on stderr',
333
+ 'once per failing read source — locally the poll cycle is one source, while against',
334
+ 'an http(s) store each tracked stream and the discovery pass carries its own failure',
335
+ 'streak — and retried at the next interval without ending the watch or duplicating or',
336
+ 'skipping any event.',
300
337
  '',
301
338
  'This command is read-only: it appends no event, takes no lease, and creates no build or',
302
339
  'repository record, and requires no session identity for operator use. Inside a phase, a',
@@ -355,9 +392,18 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
355
392
  'any condition was satisfied, with the same cursor behavior as exit 3.',
356
393
  '',
357
394
  'The default poll interval and the --interval override match ab watch: 5s for an http(s)',
358
- 'store, 1s for a local store. A store read that fails while waiting is reported once on',
359
- 'stderr, retried at the next interval, and never causes a condition to be missed or',
360
- 'reported twice.',
395
+ 'store, 1s for a local store. Cadence follows the store kind, as in ab watch: a local',
396
+ 'store ticks sequentially at the interval, while against an http(s) store each targeted',
397
+ 'build keeps one held read in flight — one long-held request per wait window (AUT-334),',
398
+ 'so an appended event is seen within about a second — with --interval the floor between',
399
+ 'request starts. A held read’s window is the remote default (25s, whole seconds, clamped',
400
+ 'by the store’s ceiling), capped at the remaining --timeout budget, and every stop — a',
401
+ 'satisfied condition, an all-terminal named set, the timeout, an interrupt — cancels',
402
+ 'in-flight holds promptly. A store read that fails while waiting is reported on stderr',
403
+ 'once per failing read source — locally the poll cycle is one source, while against an',
404
+ 'http(s) store each tracked build and the discovery pass carries its own failure',
405
+ 'streak — retried at the next interval (the next request on the remote path), and never',
406
+ 'causes a condition to be missed or reported twice.',
361
407
  '',
362
408
  'This command is read-only: it appends no event, takes no lease, and creates no build or',
363
409
  'repository record, and requires no session identity for operator use. Inside a phase, a',
@@ -565,6 +611,7 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
565
611
  ' ab artifact put <kind> <file> [--attach]',
566
612
  ' ab artifact get <kind>[@rev]',
567
613
  ' ab artifact download <build> <kind>[@rev] --output <file> [--store <ref>]',
614
+ ' ab artifact download stream:<id>[@rev] --output <file> [--store <ref>]',
568
615
  '',
569
616
  'put deposits a versioned artifact and prints its assigned revision. --attach',
570
617
  'also designates that exact revision for the PR. get fetches an artifact within',
@@ -575,6 +622,12 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
575
622
  'read-only and requires no session identity for operator use; --store overrides',
576
623
  'the BuildStore path or URL. Inside a phase, a complete ambient tuple permits',
577
624
  'only the ambient build; foreign builds and malformed or partial identity fail.',
625
+ '',
626
+ 'The stream:<id> form retrieves a finalized repository-scoped stream artifact',
627
+ "(for example a harvest session's closed stream). The stream id is globally",
628
+ 'unique, so no build argument is needed; find ids in the session rows of',
629
+ "'ab harvest status'. This form is strictly an operator read: any ambient",
630
+ 'build or Harvest identity fails closed (§8.2).',
578
631
  ]),
579
632
  },
580
633
  {
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The builtin `vercel-sandbox` provider's remote readiness validation
3
+ * (AUT-516): the body of `validateInitReadiness`'s remote branch, moved here
4
+ * so the capability declaration can point at it without
5
+ * `cli/init-validation.ts` naming the provider. Store precondition checks
6
+ * stay with the caller, driven by the `storeRequirements` declaration.
7
+ */
8
+ import type { VercelSandboxConfig } from '../config/schema'
9
+ import type {
10
+ InitValidationReport,
11
+ WorkspaceReadinessContext,
12
+ } from '../ports/workspace/provider-capabilities'
13
+ import { validateVercelSandbox, type VercelSandboxFacade } from '../ports/workspace/vercel-sandbox'
14
+ import { gitText, parseGuestOutput } from './init-readiness-shared'
15
+
16
+ export async function validateRemoteReadiness(
17
+ ctx: WorkspaceReadinessContext,
18
+ ): Promise<InitValidationReport> {
19
+ const vercel = ctx.providerConfig as VercelSandboxConfig
20
+ const remoteLine = await gitText(
21
+ ctx.exec,
22
+ ctx.repo,
23
+ ['ls-remote', '--heads', 'origin', `refs/heads/${ctx.baseBranch}`],
24
+ ctx.signal,
25
+ )
26
+ const remoteRevision = remoteLine.split(/\s+/)[0]
27
+ if (!/^[0-9a-f]{40,64}$/i.test(remoteRevision ?? '')) {
28
+ throw new Error(
29
+ `remote base ${ctx.baseBranch} does not exist; commit and push setup changes before validating`,
30
+ )
31
+ }
32
+ await gitText(
33
+ ctx.exec,
34
+ ctx.repo,
35
+ [
36
+ 'fetch',
37
+ '--no-tags',
38
+ '--no-write-fetch-head',
39
+ '--refmap=',
40
+ 'origin',
41
+ `refs/heads/${ctx.baseBranch}`,
42
+ ],
43
+ ctx.signal,
44
+ )
45
+ const shownConfig = await ctx.exec(['git', 'show', `${remoteRevision}:autobuild.toml`], {
46
+ cwd: ctx.repo,
47
+ ...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
48
+ })
49
+ if (shownConfig.exitCode !== 0) {
50
+ throw new Error(
51
+ `remote ${ctx.baseBranch} does not contain autobuild.toml; commit and push setup changes before validating`,
52
+ )
53
+ }
54
+ if (shownConfig.stdout !== ctx.configBytes) {
55
+ throw new Error(
56
+ `remote ${ctx.baseBranch} autobuild.toml differs from this checkout; commit and push setup changes before validating`,
57
+ )
58
+ }
59
+ let remote: Awaited<ReturnType<typeof validateVercelSandbox>>
60
+ try {
61
+ remote = await validateVercelSandbox({
62
+ config: vercel,
63
+ env: ctx.env,
64
+ storeRef: ctx.storeRef,
65
+ storeToken: ctx.storeToken,
66
+ repo: ctx.repo,
67
+ baseBranch: ctx.baseBranch,
68
+ ...(ctx.facade !== undefined ? { facade: ctx.facade as VercelSandboxFacade } : {}),
69
+ exec: ctx.exec,
70
+ ...(ctx.packageArchive !== undefined ? { packageArchive: ctx.packageArchive } : {}),
71
+ runtimeReferences: [...ctx.runtimeReferences],
72
+ ...(ctx.signal !== undefined ? { signal: ctx.signal } : {}),
73
+ onSandbox: (name) => ctx.stdout(`Disposable Vercel Sandbox: ${name} (active)`),
74
+ })
75
+ } catch (error) {
76
+ throw new Error(ctx.redact(error))
77
+ }
78
+ let guest: Awaited<ReturnType<typeof parseGuestOutput>>
79
+ try {
80
+ guest = parseGuestOutput(remote.output)
81
+ } catch (error) {
82
+ throw new Error(
83
+ `disposable sandbox ${remote.sandbox} was deleted, but its readiness output was invalid: ${ctx.redact(error)}`,
84
+ )
85
+ }
86
+ return {
87
+ provider: 'vercel-sandbox',
88
+ context: 'Vercel Sandbox',
89
+ workspace: remote.sandbox,
90
+ revision: remote.revision,
91
+ snapshotsDeleted: remote.snapshotsDeleted,
92
+ checks: [
93
+ {
94
+ name: 'repository acquisition',
95
+ status: 'pass',
96
+ detail: `${remote.origin} ${remote.revision}`,
97
+ },
98
+ {
99
+ name: 'system provisioning',
100
+ status: 'pass',
101
+ detail:
102
+ remote.provisioning.length === 0
103
+ ? 'no workspace.config.provisioning steps declared'
104
+ : `completed: ${remote.provisioning.join(', ')}`,
105
+ },
106
+ ...guest.checks,
107
+ ],
108
+ exitCode: guest.checks.some((check) => check.status === 'fail') ? 1 : 0,
109
+ }
110
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Readiness-probe helpers shared by the local-worktree and remote validation
3
+ * paths (AUT-516). Moved out of `cli/init-validation.ts` so the remote
4
+ * capability implementation can use them without init-validation importing
5
+ * the provider implementation; `init-validation.ts` re-exports for
6
+ * compatibility.
7
+ */
8
+ import type { Exec } from '../ports/workspace/git-worktree'
9
+ import type { GuestProbeReport } from '../ports/workspace/provider-capabilities'
10
+
11
+ export const INIT_PROBE_MARKER = 'AB_INIT_READINESS_V1='
12
+
13
+ function message(error: unknown): string {
14
+ if (error instanceof AggregateError) {
15
+ return [error.message, ...error.errors.map(message)].filter(Boolean).join('; ')
16
+ }
17
+ if (error instanceof Error)
18
+ return `${error.message}${error.cause === undefined ? '' : `; ${message(error.cause)}`}`
19
+ return String(error)
20
+ }
21
+
22
+ /** Replace every supplied nonempty value, longest first, in all diagnostics. */
23
+ export function createReadinessRedactor(
24
+ env: Readonly<Record<string, string | undefined>>,
25
+ explicitSecretNames: readonly string[] = [],
26
+ ): (value: unknown) => string {
27
+ const namedSecrets = new Set(explicitSecretNames)
28
+ const secrets = [
29
+ ...new Set(
30
+ Object.entries(env)
31
+ .filter(
32
+ ([name, value]) =>
33
+ value !== undefined &&
34
+ value !== '' &&
35
+ (namedSecrets.has(name) || /(?:TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|AUTH)/i.test(name)),
36
+ )
37
+ .map(([, value]) => value as string),
38
+ ),
39
+ ].sort((left, right) => right.length - left.length)
40
+ return (value) => {
41
+ let text = message(value)
42
+ for (const secret of secrets) text = text.split(secret).join('[REDACTED]')
43
+ return text
44
+ }
45
+ }
46
+
47
+ export function parseGuestOutput(output: string): GuestProbeReport {
48
+ const line = output.split(/\r?\n/).find((candidate) => candidate.startsWith(INIT_PROBE_MARKER))
49
+ if (line === undefined)
50
+ throw new Error('remote readiness probe returned malformed output (result marker absent)')
51
+ const value = JSON.parse(line.slice(INIT_PROBE_MARKER.length)) as GuestProbeReport
52
+ if (!Array.isArray(value.checks))
53
+ throw new Error('remote readiness probe returned malformed checks')
54
+ return value
55
+ }
56
+
57
+ export async function gitText(
58
+ exec: Exec,
59
+ repo: string,
60
+ args: string[],
61
+ signal?: AbortSignal,
62
+ ): Promise<string> {
63
+ const result = await exec(['git', ...args], {
64
+ cwd: repo,
65
+ ...(signal === undefined ? {} : { signal }),
66
+ })
67
+ if (result.exitCode !== 0)
68
+ throw new Error(
69
+ `git ${args.join(' ')} exited ${result.exitCode}: ${result.stderr.trim() || result.stdout.trim()}`,
70
+ )
71
+ return result.stdout.trim()
72
+ }