@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
@@ -24,24 +24,62 @@ const positiveInt = z.number().int().positive()
24
24
 
25
25
  const empty = z.strictObject({})
26
26
 
27
- /** The trigger that started a turn: a human message or a wake event. */
28
- const turnTriggerSchema = z.discriminatedUnion('kind', [
29
- z.strictObject({ kind: z.literal('message'), messageSeq: positiveInt }),
30
- z.strictObject({
27
+ /** The trigger that started a turn: a human message or a wake event. A wake
28
+ * trigger names exactly one source — a build (the build-log attention scan)
29
+ * or the repository journal — enforced by the schema's `.check()` below even
30
+ * though the static type (one object with optional fields) cannot express it:
31
+ * a two-member union or a nested discriminated union is not a valid
32
+ * discriminated-union option on zod 4 (both constructions throw at first
33
+ * parse), so the wake variant stays a single object and call sites branch on
34
+ * `journal === true`. */
35
+ const turnWakeTriggerSchema = z
36
+ .strictObject({
31
37
  kind: z.literal('wake'),
32
- build: z.string().min(1),
38
+ build: z.string().min(1).optional(),
39
+ journal: z.literal(true).optional(),
33
40
  seq: positiveInt,
34
41
  type: z.string().min(1),
35
- }),
42
+ })
43
+ .check((ctx) => {
44
+ const hasBuild = ctx.value.build !== undefined
45
+ const hasJournal = ctx.value.journal === true
46
+ if (hasBuild === hasJournal) {
47
+ ctx.issues.push({
48
+ code: 'custom',
49
+ input: ctx.value,
50
+ message: 'a wake trigger names exactly one of build or journal',
51
+ })
52
+ }
53
+ })
54
+
55
+ const turnTriggerSchema = z.discriminatedUnion('kind', [
56
+ z.strictObject({ kind: z.literal('message'), messageSeq: positiveInt }),
57
+ turnWakeTriggerSchema,
36
58
  ])
37
59
 
38
- /** Matches the transcript usage shape (harvest.session.ended, §15.3). */
60
+ /** Matches the transcript usage shape (harvest.session.ended, §15.3). The
61
+ * turn runner records the AI SDK's totalUsage token counts plus the step
62
+ * count. Old `turn.completed` events without `steps` still reduce. */
39
63
  const usageSchema = z.strictObject({
40
64
  inputTokens: z.number().int().nonnegative(),
41
65
  outputTokens: z.number().int().nonnegative(),
66
+ steps: z.number().int().nonnegative(),
42
67
  turns: z.number().int().nonnegative().optional(),
43
68
  })
44
69
 
70
+ /** The typed model-failure vocabulary (AUT-342): provider availability,
71
+ * exhaustion, credentials, and configuration are distinguished so a failed
72
+ * turn's class is visible in the session's reduced state and nothing is ever
73
+ * retried unboundedly. `internal` covers non-model failures — the dispatcher
74
+ * tick's crash reaper among them. */
75
+ const turnFailureKindSchema = z.enum([
76
+ 'provider-unavailable',
77
+ 'exhausted',
78
+ 'credentials',
79
+ 'configuration',
80
+ 'internal',
81
+ ])
82
+
45
83
  export const sessionEventPayloadSchemas = {
46
84
  /** The fact of creation; the session record carries the same title. */
47
85
  'session.created': z.strictObject({ title: z.string().min(1).optional() }),
@@ -52,11 +90,27 @@ export const sessionEventPayloadSchemas = {
52
90
  'session.wake-set': z.strictObject({
53
91
  globs: z.array(z.string().min(1)).max(100),
54
92
  }),
55
- /** The (later) turn runner's start of one turn on its stream. */
93
+ /** The (later turn runner's) start of one turn on its stream. A wake
94
+ * turn additionally carries the delivered input — the attention event
95
+ * record and, for a build wake, the build's reduced state, frozen at wake
96
+ * time — so a later invocation reconstructs the identical user message
97
+ * from durable state alone (live build state keeps moving; the frozen
98
+ * snapshot does not). A journal wake delivers the event record only. */
56
99
  'turn.started': z.strictObject({
57
100
  turn: z.string().min(1),
58
101
  stream: z.string().min(1),
59
102
  trigger: turnTriggerSchema,
103
+ wake: z
104
+ .strictObject({
105
+ event: z.strictObject({
106
+ seq: positiveInt,
107
+ ts: z.string().min(1),
108
+ type: z.string().min(1),
109
+ payload: z.record(z.string(), z.unknown()),
110
+ }),
111
+ buildState: z.record(z.string(), z.unknown()).optional(),
112
+ })
113
+ .optional(),
60
114
  }),
61
115
  /** A turn was suspended for budget or for an approval. */
62
116
  'turn.suspended': z.strictObject({
@@ -80,8 +134,13 @@ export const sessionEventPayloadSchemas = {
80
134
  }),
81
135
  /** One turn finished. */
82
136
  'turn.completed': z.strictObject({ turn: z.string().min(1), usage: usageSchema }),
83
- /** One turn failed. */
84
- 'turn.failed': z.strictObject({ turn: z.string().min(1), error: z.string().min(1) }),
137
+ /** One turn failed: `kind` is the typed failure class, `error` the
138
+ * human-readable message surfaced in the session's reduced state. */
139
+ 'turn.failed': z.strictObject({
140
+ turn: z.string().min(1),
141
+ kind: turnFailureKindSchema,
142
+ error: z.string().min(1),
143
+ }),
85
144
  /** Terminal. The session is read-only afterwards. */
86
145
  'session.archived': empty,
87
146
  } as const
@@ -0,0 +1,12 @@
1
+ import type { EventPayload } from './payloads'
2
+
3
+ /** Whether the workspace this payload records publishes remotely, per the
4
+ * marker recorded at provision. Journals written before the marker existed
5
+ * fall back to the legacy provider-name reading — the only name comparison
6
+ * among the remote/local decisions (removed in AUT-505). */
7
+ export function isRemoteWorkspace(
8
+ payload: Pick<EventPayload<'workspace.provisioned'>, 'provider' | 'remote'>,
9
+ ): boolean {
10
+ if (payload.remote !== undefined) return payload.remote
11
+ return payload.provider === 'vercel-sandbox'
12
+ }
@@ -59,6 +59,13 @@ import { InProcessBuildExecution } from '../ports/workspace/in-process-build-exe
59
59
  import { BuildRunner, LeaseHeldError, SetupFailureError } from '../processes/build-runner'
60
60
  import { diagnosticArtifact } from '../processes/build-execution-state'
61
61
  import { Dispatcher, type LaunchRunnerResult } from '../processes/dispatcher'
62
+ import {
63
+ getHarvestStatus,
64
+ getOperatorDashboard,
65
+ getRepositoryStatus,
66
+ listOperatorBuilds,
67
+ } from '../operator/query'
68
+ import { RepoViewStore } from '../processes/repo-view'
62
69
  import { MemoryBuildStore } from '../store/memory'
63
70
  import type { BuildStore } from '../store/types'
64
71
  import { steppingClock } from '../testing/fixed'
@@ -279,6 +286,106 @@ export interface E2eHarness {
279
286
  cleanup(): Promise<void>
280
287
  }
281
288
 
289
+ /** The store with every reducer snapshot deleted: reads miss, writes no-op. */
290
+ export function withoutSnapshots<T extends BuildStore>(store: T): T {
291
+ return new Proxy(store, {
292
+ get(target, key) {
293
+ if (key === 'getReducerSnapshot') return async () => null
294
+ if (key === 'putReducerSnapshot') return async () => false
295
+ const value = Reflect.get(target, key, target)
296
+ return typeof value === 'function' ? value.bind(target) : value
297
+ },
298
+ })
299
+ }
300
+
301
+ /** A build's facts with the publication view (closures) reduced to its answers. */
302
+ async function plainFacts(view: RepoViewStore, slug: string) {
303
+ const { publication, ...rest } = await view.buildFacts(slug)
304
+ return {
305
+ ...rest,
306
+ publication: {
307
+ pending: publication.pending(),
308
+ abandoned: publication.abandonedPending(),
309
+ latest: publication.latestUncompletedRequest(),
310
+ },
311
+ }
312
+ }
313
+
314
+ /** JSON with sorted keys, so two structurally equal values compare equal. */
315
+ function canonical(value: unknown): string {
316
+ return JSON.stringify(value, (_key, item) =>
317
+ item !== null && typeof item === 'object' && !Array.isArray(item)
318
+ ? Object.fromEntries(Object.entries(item).sort(([a], [b]) => a.localeCompare(b)))
319
+ : item,
320
+ )
321
+ }
322
+
323
+ async function repositoryReading(store: BuildStore, repo: string) {
324
+ const view = new RepoViewStore(store, { repo })
325
+ await view.refresh()
326
+ const slugs = (await view.listBuilds()).map((record) => record.slug).sort()
327
+ const builds: Record<string, unknown> = {}
328
+ for (const slug of slugs) {
329
+ builds[slug] = { facts: await plainFacts(view, slug), state: await view.buildState(slug) }
330
+ }
331
+ return {
332
+ journal: view.recordedJournal(),
333
+ digests: [...(await view.getRepoBuildDigests(repo)).entries()].sort(([a], [b]) =>
334
+ a.localeCompare(b),
335
+ ),
336
+ builds,
337
+ view,
338
+ }
339
+ }
340
+
341
+ /** Reducer snapshots are a cache the log regenerates (SPEC §7): whatever a
342
+ * scenario's dispatcher left behind, a cold view restored from it must read
343
+ * exactly what a cold view over a store with every snapshot deleted reads — and
344
+ * so must one restored from snapshots written at the final cursor. Run for
345
+ * every scenario at cleanup. */
346
+ export async function assertSnapshotEquivalence(store: BuildStore, repo: string): Promise<void> {
347
+ const replayed = await repositoryReading(withoutSnapshots(store), repo)
348
+ for (const round of ['as left by the scenario', 'written at the final cursor']) {
349
+ const restored = await repositoryReading(store, repo)
350
+ const { view: restoredView, ...got } = restored
351
+ const { view: _replayedView, ...want } = replayed
352
+ if (canonical(got) !== canonical(want)) {
353
+ throw new Error(`snapshot-restored reading differs from full replay (${round})`)
354
+ }
355
+ await restoredView.persistSnapshots({ force: true })
356
+ }
357
+ // The operator routes derive from the same view: each response with
358
+ // snapshots present equals the response with every snapshot deleted.
359
+ const at = new Date('2026-01-01T00:00:00.000Z')
360
+ const routes = async (target: BuildStore) => {
361
+ const attempt = async (read: () => Promise<unknown>): Promise<unknown> => {
362
+ try {
363
+ return await read()
364
+ } catch (error) {
365
+ return { error: error instanceof Error ? error.message : String(error) }
366
+ }
367
+ }
368
+ return {
369
+ dashboard: await attempt(() =>
370
+ getOperatorDashboard({ store: target, repo, clock: () => at }),
371
+ ),
372
+ status: await attempt(() => getRepositoryStatus(target, repo)),
373
+ harvest: await attempt(() => getHarvestStatus(target, repo)),
374
+ lists: await Promise.all(
375
+ (['active', 'queued', 'all'] as const).map((scope) =>
376
+ attempt(() => listOperatorBuilds({ store: target, repo, scope, now: at })),
377
+ ),
378
+ ),
379
+ }
380
+ }
381
+ const replayedRoutes = canonical(await routes(withoutSnapshots(store)))
382
+ for (const round of ['as left', 'warmed by a first poll']) {
383
+ if (canonical(await routes(store)) !== replayedRoutes) {
384
+ throw new Error(`snapshot-backed operator routes differ from full replay (${round})`)
385
+ }
386
+ }
387
+ }
388
+
282
389
  export async function makeHarness(opts: {
283
390
  handlers: SkillHandlers
284
391
  tickets?: Ticket[]
@@ -336,7 +443,11 @@ export async function makeHarness(opts: {
336
443
  const clock = steppingClock()
337
444
  const ids = sequentialIds()
338
445
  const uuids = sequentialUuids()
339
- const store = new MemoryBuildStore({ clock })
446
+ // `AB_E2E_SNAPSHOTS=off` runs every scenario with every snapshot deleted, the
447
+ // other arm of the "snapshots change no observable output" proof.
448
+ const snapshotsOff = process.env.AB_E2E_SNAPSHOTS === 'off'
449
+ const memory = new MemoryBuildStore({ clock })
450
+ const store = snapshotsOff ? withoutSnapshots(memory) : memory
340
451
  const adapted = await opts.storeAdapter?.(store)
341
452
  const activeStore = adapted?.store ?? store
342
453
  const config = parseConfig(configToml, 'e2e autobuild.toml')
@@ -347,7 +458,8 @@ export async function makeHarness(opts: {
347
458
  gatePresence: opts.gatePresence ?? 'present',
348
459
  ...(opts.pluginForge?.prAttachments === true ? { prAttachments: true } : {}),
349
460
  })
350
- let selectedForge: Forge = opts.localGitForge === true ? new LocalGitForge() : forge
461
+ let selectedForge: Forge =
462
+ opts.localGitForge === true ? new LocalGitForge({ repoRoot: origin }) : forge
351
463
  let plugins: PluginRegistry | undefined
352
464
  if (opts.pluginForge !== undefined) {
353
465
  plugins = createPluginRegistry()
@@ -611,6 +723,7 @@ export async function makeHarness(opts: {
611
723
  },
612
724
  events: (slug) => store.getEvents(slug),
613
725
  cleanup: async () => {
726
+ if (!snapshotsOff) await assertSnapshotEquivalence(store, origin)
614
727
  await activeStore.close()
615
728
  if (activeStore !== store) await store.close()
616
729
  await adapted?.cleanup?.()