@defrex/autobuild 0.13.0 → 0.14.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 (94) hide show
  1. package/README.md +1 -1
  2. package/SPEC.md +19 -1
  3. package/bin/ab-dispatch-kernel.ts +4 -6
  4. package/docs/architecture.md +96 -70
  5. package/docs/hosted-service.md +2 -2
  6. package/docs/operator-api.md +1 -1
  7. package/docs/remote-store-protocol.md +84 -2
  8. package/package.json +4 -3
  9. package/packages/core/src/cli/attention.ts +18 -164
  10. package/packages/core/src/cli/build-control.ts +11 -15
  11. package/packages/core/src/cli/bulk-control.ts +3 -2
  12. package/packages/core/src/cli/context.ts +16 -78
  13. package/packages/core/src/cli/dashboard/model.ts +36 -22
  14. package/packages/core/src/cli/dispatch-frontend.ts +24 -67
  15. package/packages/core/src/cli/dispatch-process.ts +14 -7
  16. package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
  17. package/packages/core/src/cli/dispatch.ts +148 -79
  18. package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
  19. package/packages/core/src/cli/harvest.ts +71 -39
  20. package/packages/core/src/cli/historical-build-state.ts +27 -0
  21. package/packages/core/src/cli/main.ts +3 -3
  22. package/packages/core/src/cli/pr-attachments.ts +10 -13
  23. package/packages/core/src/cli/pr-summary-publication.ts +27 -60
  24. package/packages/core/src/cli/pr-summary.ts +43 -13
  25. package/packages/core/src/cli/publication-settlement.ts +10 -20
  26. package/packages/core/src/cli/repository-status.ts +38 -2
  27. package/packages/core/src/cli/status.ts +62 -32
  28. package/packages/core/src/cli/terminals.ts +25 -142
  29. package/packages/core/src/cli/ticket.ts +2 -2
  30. package/packages/core/src/cli/wait.ts +91 -84
  31. package/packages/core/src/cli/watch.ts +60 -97
  32. package/packages/core/src/config/pipeline-source.ts +4 -3
  33. package/packages/core/src/events/repository.ts +7 -3
  34. package/packages/core/src/integration/harness.ts +1 -2
  35. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2429 -9
  36. package/packages/core/src/kernel/auto-merge.ts +19 -3
  37. package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
  38. package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
  39. package/packages/core/src/kernel/harvest.ts +25 -23
  40. package/packages/core/src/kernel/log-index-value.ts +38 -0
  41. package/packages/core/src/kernel/read-facts.ts +637 -0
  42. package/packages/core/src/kernel/reducer-registry.ts +137 -3
  43. package/packages/core/src/kernel/repository-attention.ts +179 -0
  44. package/packages/core/src/kernel/service-facts.ts +135 -0
  45. package/packages/core/src/kernel/terminal-guard.ts +133 -0
  46. package/packages/core/src/operator/control.ts +5 -6
  47. package/packages/core/src/operator/index.ts +2 -0
  48. package/packages/core/src/operator/query.ts +37 -20
  49. package/packages/core/src/operator/read-view.ts +4 -13
  50. package/packages/core/src/operator/requests.ts +2 -14
  51. package/packages/core/src/operator/sandbox.ts +10 -9
  52. package/packages/core/src/operator/tickets.ts +2 -2
  53. package/packages/core/src/plugin-sdk/index.ts +13 -0
  54. package/packages/core/src/processes/build-child.ts +6 -6
  55. package/packages/core/src/processes/build-runner.ts +62 -70
  56. package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
  57. package/packages/core/src/processes/dispatcher.ts +113 -173
  58. package/packages/core/src/processes/harvest-execution-state.ts +15 -9
  59. package/packages/core/src/processes/harvest-runner.ts +34 -70
  60. package/packages/core/src/processes/harvest.ts +38 -15
  61. package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
  62. package/packages/core/src/processes/orchestrator-tick.ts +2 -2
  63. package/packages/core/src/processes/publication-loss.ts +35 -21
  64. package/packages/core/src/processes/repo-view.ts +229 -1134
  65. package/packages/core/src/processes/tick-publication.ts +110 -0
  66. package/packages/core/src/store/adapter.ts +23 -0
  67. package/packages/core/src/store/build-scope.ts +17 -0
  68. package/packages/core/src/store/contract.ts +251 -0
  69. package/packages/core/src/store/local/schema.ts +1 -0
  70. package/packages/core/src/store/local/store.ts +179 -54
  71. package/packages/core/src/store/memory.ts +119 -29
  72. package/packages/core/src/store/phase-session.ts +17 -0
  73. package/packages/core/src/store/reduced-reads.ts +239 -0
  74. package/packages/core/src/store/reducer-value-shapes.json +14056 -0
  75. package/packages/core/src/store/reducer-value-validation.ts +96 -0
  76. package/packages/core/src/store/reducer-values.ts +145 -0
  77. package/packages/core/src/store/remote/client.ts +41 -0
  78. package/packages/core/src/store/remote/compatibility.ts +2 -0
  79. package/packages/core/src/store/remote/index.ts +19 -0
  80. package/packages/core/src/store/remote/protocol.ts +16 -0
  81. package/packages/core/src/store/remote/version.ts +3 -1
  82. package/packages/core/src/store/repo-state-events.ts +1 -1
  83. package/packages/core/src/store/repo-tick.ts +72 -0
  84. package/packages/core/src/store/session-handle.ts +19 -0
  85. package/packages/core/src/store/snapshots.ts +10 -0
  86. package/packages/core/src/store/ticket-assets.ts +4 -4
  87. package/packages/core/src/store/types.ts +39 -2
  88. package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
  89. package/packages/core/src/testing/index.ts +8 -0
  90. package/packages/core/src/testing/reduced-read-budget.ts +462 -0
  91. package/skills/guide/SKILL.md +6 -1
  92. package/skills/guide/references/operator-api.md +1 -1
  93. package/skills/guide/references/remote-store-protocol.md +84 -2
  94. package/templates/hosted-service/package.json +2 -2
@@ -1,3 +1,9 @@
1
+ import { readBuildDigests } from '../store/reduced-reads'
2
+ import {
3
+ readBuildFacts,
4
+ readRepositoryFacts,
5
+ type RepositoryReadFacts,
6
+ } from '../store/reduced-reads'
1
7
  import { readDispatcherLiveness } from '../cli/dispatcher-liveness'
2
8
  import { composeBuildConfig } from '../config/live'
3
9
  import type { PipelineSourceMeta } from '../config/pipeline-source'
@@ -9,26 +15,25 @@ import {
9
15
  } from '../processes/build-execution-state'
10
16
  import type { Artifact } from '../store/types'
11
17
  import {
12
- detail,
18
+ detailFromFacts,
13
19
  statusFilter,
14
20
  summarizeFrom,
15
21
  type BuildDetail,
16
22
  type BuildSummary,
17
23
  } from '../cli/status'
18
- import { projectRepositoryStatus, type RepositoryStatus } from '../cli/repository-status'
24
+ import { repositoryStatusFromFacts, type RepositoryStatus } from '../cli/repository-status'
19
25
  import { projectHarvestStatus, type HarvestStatusView } from '../cli/harvest'
20
26
  import {
21
27
  buildDashboardFromProjected,
22
28
  effectiveStatus,
23
- projectBuild,
24
29
  projectBuildFromFacts,
25
30
  type DashboardBuild,
26
31
  type DashboardModel,
27
32
  } from '../cli/dashboard/model'
28
- import { reduceBuild, type BuildState } from '../kernel/reducer'
33
+ import type { BuildState } from '../kernel/reducer'
29
34
  import { DISPATCHER_EFFECTIVE_CONFIG_ARTIFACT } from '../store/retention'
30
35
  import { reduceDispatchStatus } from '../kernel/dispatch-status'
31
- import { readRepoEventsIfRecorded, unclaimedObservationCount } from '../processes/harvest'
36
+ import { unclaimedObservationCount } from '../processes/harvest'
32
37
  import type { RepoViewStore } from '../processes/repo-view'
33
38
  import { withOperatorView } from './read-view'
34
39
  import type { RepositoryEvent } from '../events/repository'
@@ -74,7 +79,7 @@ async function listFromView(
74
79
  // finished builds accumulate. The `--all` scope includes terminal statuses,
75
80
  // so it takes no digest read and skips nothing; its terminal builds are
76
81
  // restored from the reducer snapshots the dispatcher leaves at settlement.
77
- const digests = opts.scope === 'all' ? undefined : await view.getRepoBuildDigests(opts.repo)
82
+ const digests = opts.scope === 'all' ? undefined : await readBuildDigests(view, opts.repo)
78
83
  const output: BuildSummary[] = []
79
84
  for (const record of records) {
80
85
  if (record.repo !== opts.repo) continue
@@ -114,14 +119,19 @@ export async function effectiveConfig(
114
119
  repo: string,
115
120
  /** The bounded journal, when the caller already holds it (a repository
116
121
  * view); otherwise it is read from the store. */
117
- journal?: RepositoryEvent[],
122
+ journal?: RepositoryReadFacts,
118
123
  ): Promise<{
119
124
  config: Config
120
- repositoryEvents: RepositoryEvent[]
125
+ repositoryEvents: RepositoryReadFacts
121
126
  status: ReturnType<typeof reduceDispatchStatus>
122
127
  }> {
123
- const repositoryEvents = journal ?? (await readRepoEventsIfRecorded(store, repo))
124
- const status = selectEffectiveConfig(repositoryEvents, repo)
128
+ const repositoryEvents = journal ?? (await readRepositoryFacts(store, repo)).value
129
+ const status = repositoryEvents.status
130
+ if (status?.effectiveConfig === undefined)
131
+ throw new OperatorQueryError(
132
+ 'effective-config-unavailable',
133
+ `effective config unavailable for repository "${repo}"${repositoryEvents.config.run === undefined ? ': no dispatcher run is recorded' : ''}`,
134
+ )
125
135
  const ref = status.effectiveConfig!
126
136
  const artifact = await store.getRepoArtifact(repo, ref.kind, ref.rev)
127
137
  if (artifact === null) {
@@ -258,19 +268,19 @@ export async function getOperatorBuild(opts: {
258
268
  now: Date
259
269
  }): Promise<OperatorBuildView> {
260
270
  const record = await requireBuild(opts.store, opts.repo, opts.slug)
261
- const events = await opts.store.getEvents(opts.slug)
262
- const state = reduceBuild(events)
271
+ const facts = (await readBuildFacts(opts.store, opts.slug)).value
272
+ const state = facts.state
263
273
  const { config } = await effectiveConfig(opts.store, opts.repo)
264
274
  let dashboardRow: DashboardBuild | null = null
265
275
  if (!projectsNoDashboardRow(state)) {
266
276
  const pinned = await readPinnedConfig(opts.store, opts.slug, config)
267
- const row = projectBuild(record, state, config, events, undefined, pinned.config)
277
+ const row = projectBuildFromFacts(record, state, facts, config, undefined, pinned.config)
268
278
  // Implied by the gate; kept as an explicit check (not an assertion) so the
269
279
  // code never lies if `effectiveStatus` and `projectBuild` ever drift.
270
280
  if (row !== null) dashboardRow = decorateWithPinnedMeta(row, pinned)
271
281
  }
272
282
  return {
273
- detail: detail(record, events, opts.now),
283
+ detail: detailFromFacts(record, facts, opts.now),
274
284
  dashboardRow,
275
285
  }
276
286
  }
@@ -281,8 +291,12 @@ export async function getRepositoryStatus(
281
291
  now = new Date(),
282
292
  ): Promise<RepositoryStatus> {
283
293
  return withOperatorView(store, repo, 'journal', async (view) => {
284
- const { dispatcher } = await readDispatcherLiveness(store, repo, now, view.recordedJournal())
285
- return projectRepositoryStatus(repo, view.recordedJournal(), dispatcher)
294
+ const { dispatcher } = await readDispatcherLiveness(store, repo, now)
295
+ return repositoryStatusFromFacts(
296
+ repo,
297
+ (await readRepositoryFacts(view, repo)).value,
298
+ dispatcher,
299
+ )
286
300
  })
287
301
  }
288
302
 
@@ -291,7 +305,7 @@ export async function getHarvestStatus(
291
305
  repo: string,
292
306
  ): Promise<HarvestStatusView> {
293
307
  return withOperatorView(store, repo, 'journal', async (view) =>
294
- projectHarvestStatus(repo, view.recordedJournal()),
308
+ projectHarvestStatus(repo, view.repositoryFacts()),
295
309
  )
296
310
  }
297
311
 
@@ -321,7 +335,7 @@ async function dashboardFromView(
321
335
  const { config, repositoryEvents, status } = await effectiveConfig(
322
336
  view,
323
337
  opts.repo,
324
- view.recordedJournal(),
338
+ view.repositoryFacts(),
325
339
  )
326
340
  const records = await view.listBuilds()
327
341
  // One digest read covers every build of the repository (AUT-487): the
@@ -330,7 +344,7 @@ async function dashboardFromView(
330
344
  // builds accumulate. Row-rendering builds derive from the repository view's
331
345
  // reducer snapshots plus a delta read (AUT-650); every snapshot is a cache
332
346
  // the event log regenerates, so the response equals a full replay's.
333
- const digests = await view.getRepoBuildDigests(opts.repo)
347
+ const digests = await readBuildDigests(view, opts.repo)
334
348
  const projected: DashboardBuild[] = []
335
349
  let activeCount = 0
336
350
  for (const record of records) {
@@ -372,7 +386,10 @@ async function dashboardFromView(
372
386
  // `effectiveConfig` already returned — no per-build history reads (AUT-487).
373
387
  // The pure core writes nothing — a snapshot performs no store writes and
374
388
  // never creates or locks the repository record.
375
- const observationCount = unclaimedObservationCount({ digests, harvestEvents: repositoryEvents })
389
+ const observationCount = unclaimedObservationCount({
390
+ digests,
391
+ harvestEvents: repositoryEvents.harvest,
392
+ })
376
393
  const warningLines = [
377
394
  ...status.roleWarnings,
378
395
  ...(status.warningNotice !== undefined ? [status.warningNotice] : []),
@@ -1,16 +1,15 @@
1
1
  /**
2
2
  * The operator read paths' repository view: every poll-driven route (dashboard,
3
3
  * repository and harvest status, build listings) derives its state from a
4
- * `RepoViewStore` cold-started from stored reducer snapshots and advanced with
5
- * delta reads, so a poll's event-row cost follows the events appended since the
6
- * last one, never the history, the open tabs, or the watchers.
4
+ * `RepoViewStore` backed by persisted reduced values. Each pass refreshes
5
+ * repository facts and metadata without hydrating raw history.
7
6
  *
8
7
  * Passes are single-flight per `(store, repo)` within one serving process: at
9
8
  * most one pass runs at a time, and requests arriving while it runs join the
10
9
  * one trailing pass (each needs state at least as new as its arrival, which the
11
10
  * running pass cannot promise). That trailing pass restores from the snapshot
12
- * the previous pass persisted, so N concurrent pollers cost one delta read per
13
- * pass, not N. The coordination is scheduling inside this process; every datum
11
+ * the previous pass persisted for builds, so N concurrent pollers cost one
12
+ * build delta read per pass, not N. The coordination is scheduling inside this process; every datum
14
13
  * still comes from durable state, and a second serving process simply reads the
15
14
  * same delta once itself (duplicate reads are bounded by the process count).
16
15
  */
@@ -58,14 +57,6 @@ async function pass(store: BuildStore, repo: string, waiters: Waiter[]): Promise
58
57
  return
59
58
  }
60
59
  await Promise.all(waiters.map((waiter) => waiter.run(view).then(waiter.resolve, waiter.reject)))
61
- // Leave the advanced state behind for the next pass (this process's or
62
- // another's). `held` also writes a settled build read for a listing. The
63
- // cache never fails a read.
64
- try {
65
- await view.persistSnapshots({ force: true, held: true })
66
- } catch {
67
- // A cache: the next pass retries.
68
- }
69
60
  }
70
61
 
71
62
  async function drain(store: BuildStore, repo: string, flight: Flight): Promise<void> {
@@ -1,16 +1,4 @@
1
- /**
2
- * The route-level glue the operator HTTP server applies around the shared
3
- * services, re-derived for the agent tool registry (operator/registry.ts).
4
- *
5
- * This module is registry-owned on purpose: the operator API routes were
6
- * declared out of scope for the registry ticket, so the parity between a
7
- * tool and its route is *proven* by the contract suite
8
- * (packages/hosted-store-service/src/registry.contract.test.ts) rather than
9
- * obtained by extraction. Every refusal text here is load-bearing — it must
10
- * stay byte-identical to the hosted package's `operator-server.ts` so the
11
- * suite's full-body comparisons hold.
12
- */
13
- import { reduceBuild } from '../kernel/reducer'
1
+ import { readBuildFacts } from '../store/reduced-reads'
14
2
  import { effectiveStatus } from '../cli/dashboard/model'
15
3
  import { BuildControlError, type BuildControlAction } from '../cli/build-control'
16
4
  import type { BuildStore } from '../store/types'
@@ -80,7 +68,7 @@ export async function controlPrechecks(
80
68
  ): Promise<void> {
81
69
  await requireRouteBuild(store, repo, slug)
82
70
  if (action === 'pause' || action === 'cancel-pause') {
83
- const state = reduceBuild(await store.getEvents(slug))
71
+ const state = (await readBuildFacts(store, slug)).value.state
84
72
  const display = effectiveStatus(state)
85
73
  if (action === 'pause' && display === 'pausing') {
86
74
  throw new BuildControlError(
@@ -13,7 +13,6 @@
13
13
  import { createHash } from 'node:crypto'
14
14
  import { humanActor } from '../events/envelope'
15
15
  import type { Via } from '../events/envelope'
16
- import type { RepositoryEvent } from '../events/repository'
17
16
  import type { Forge } from '../ports/types'
18
17
  import {
19
18
  SANDBOX_FORBIDDEN_ENV,
@@ -23,8 +22,7 @@ import {
23
22
  type SandboxWaitResult,
24
23
  } from '../ports/workspace/operator-sandbox'
25
24
  import type { WorkspaceProvider } from '../ports/types'
26
- import { readRepoEventsIfRecorded } from '../processes/harvest'
27
- import { sandboxStates } from '../processes/sandbox-state'
25
+ import { readRepositoryFacts } from '../store/reduced-reads'
28
26
  import { systemClock, type BuildStore, type Clock } from '../store/types'
29
27
 
30
28
  /** Hard bound on one sandbox command wait, in seconds — shared with the
@@ -253,8 +251,11 @@ export async function createOperatorSandboxService(
253
251
  }
254
252
 
255
253
  /** The operator's journal state, or undefined when no environment exists. */
256
- const operatorState = (identity: string, events: RepositoryEvent[]) =>
257
- sandboxStates(events)
254
+ const operatorState = (
255
+ identity: string,
256
+ events: import('../store/reduced-reads').RepositoryReadFacts,
257
+ ) =>
258
+ events.sandboxes
258
259
  .filter((state) => state.operator === identity && state.state !== 'released')
259
260
  .at(-1)
260
261
 
@@ -327,7 +328,7 @@ export async function createOperatorSandboxService(
327
328
  ): Promise<T> => {
328
329
  const key = `${repo}\0${identity}`
329
330
  return serialize(key, async () => {
330
- const events = await readRepoEventsIfRecorded(store, repo)
331
+ const events = (await readRepositoryFacts(store, repo)).value
331
332
  const state = operatorState(identity, events)
332
333
  const resolved = await ensureWithFacts(identity, state).catch((error: unknown) => {
333
334
  if (error instanceof SandboxOperationError) throw error
@@ -337,7 +338,7 @@ export async function createOperatorSandboxService(
337
338
  { cause: error },
338
339
  )
339
340
  })
340
- const freshEvents = await readRepoEventsIfRecorded(store, repo)
341
+ const freshEvents = (await readRepositoryFacts(store, repo)).value
341
342
  const freshState = operatorState(identity, freshEvents)
342
343
  try {
343
344
  const result = await operation(resolved, freshState)
@@ -630,7 +631,7 @@ export async function createOperatorSandboxService(
630
631
  requireRepo(input.repo)
631
632
  const key = `${repo}\0${identity}`
632
633
  await serialize(key, async () => {
633
- const events = await readRepoEventsIfRecorded(store, repo)
634
+ const events = (await readRepositoryFacts(store, repo)).value
634
635
  const state = operatorState(identity, events)
635
636
  const resolved = await capability.describe({ repo, operator: identity })
636
637
  await append(identity, 'orchestrator.sandbox.reset', {
@@ -662,7 +663,7 @@ export async function createOperatorSandboxService(
662
663
  requireRepo(input.repo)
663
664
  const key = `${repo}\0${identity}`
664
665
  await serialize(key, async () => {
665
- const events = await readRepoEventsIfRecorded(store, repo)
666
+ const events = (await readRepositoryFacts(store, repo)).value
666
667
  const state = operatorState(identity, events)
667
668
  // No environment ever provisioned: a no-op, with no fact and no
668
669
  // provider traffic beyond the pure describe.
@@ -1,7 +1,7 @@
1
+ import { readBuildFacts } from '../store/reduced-reads'
1
2
  import { samplePinnedAssets, preparePinnedAssets } from '../store/ticket-assets'
2
3
  import type { TicketRef } from '../ontology'
3
4
  import type { Config } from '../config/schema'
4
- import { reduceBuild } from '../kernel/reducer'
5
5
  import {
6
6
  TicketOperationError,
7
7
  changeBlockers,
@@ -141,7 +141,7 @@ async function matchingBuild(
141
141
  const projected = await Promise.all(
142
142
  matches.map(async (record) => ({
143
143
  record,
144
- state: reduceBuild(await store.getEvents(record.slug)),
144
+ state: (await readBuildFacts(store, record.slug)).value.state,
145
145
  })),
146
146
  )
147
147
  projected.sort((a, b) => {
@@ -229,3 +229,16 @@ export type {
229
229
  TicketAssetMeta,
230
230
  TicketAssetSummary,
231
231
  } from '../store/ticket-assets'
232
+
233
+ export {
234
+ readReducerValue,
235
+ resolveReducerValue,
236
+ reducerValueRequestSchema,
237
+ reducerValueSchema,
238
+ } from '../store/reducer-values'
239
+ export type {
240
+ ReducerValue,
241
+ ReducerValueRequest,
242
+ ReducerValueStorage,
243
+ ValueCacheRow,
244
+ } from '../store/reducer-values'
@@ -1,3 +1,4 @@
1
+ import { readBuildFacts } from '../store/reduced-reads'
1
2
  import { hostname } from 'node:os'
2
3
  import { createForge } from '../ports/forge/create'
3
4
  import { createProductionRuntimes } from '../ports/runner/production'
@@ -13,7 +14,6 @@ import {
13
14
  buildOwnedSectionsDiffer,
14
15
  diagnosticArtifact,
15
16
  parseEffectiveBuildConfig,
16
- selectOpenWorkspace,
17
17
  type BuildRunnerDiagnosticOutcome,
18
18
  } from './build-execution-state'
19
19
  import type { BuildExecutionStart } from '../ports/workspace/build-execution'
@@ -40,7 +40,7 @@ export async function runBuildChild(
40
40
  try {
41
41
  const record = await store.getBuild(input.slug)
42
42
  if (record === null) throw new Error(`unknown build ${JSON.stringify(input.slug)}`)
43
- const workspace = selectOpenWorkspace(await store.getEvents(input.slug))
43
+ const workspace = (await readBuildFacts(store, input.slug)).value.workspace
44
44
  if (workspace === null) {
45
45
  throw new Error(`build ${JSON.stringify(input.slug)} has no open durable workspace`)
46
46
  }
@@ -55,20 +55,20 @@ export async function runBuildChild(
55
55
  throw new Error(`build ${JSON.stringify(input.slug)} has no effective config artifact`)
56
56
  }
57
57
  let currentConfig = parseEffectiveBuildConfig(configArtifact)
58
- const plugins = await loadPlugins(currentConfig.plugins, workspace.path, {
58
+ const plugins = await loadPlugins(currentConfig.plugins, workspace.path ?? workspace.ref, {
59
59
  packageRoot: record.repo,
60
60
  ...(opts.guest === true ? { guest: true } : {}),
61
61
  })
62
62
  const { runtimes: builtins } = createProductionRuntimes()
63
63
  const runtimes = await materializePluginRuntimes(builtins, plugins, {
64
- repoRoot: workspace.path,
64
+ repoRoot: workspace.path ?? workspace.ref,
65
65
  env,
66
66
  })
67
67
  const forge = await createForge({
68
68
  name: currentConfig.forge,
69
69
  registry: plugins,
70
70
  env,
71
- repoRoot: workspace.path,
71
+ repoRoot: workspace.path ?? workspace.ref,
72
72
  })
73
73
  const getConfig = async () => {
74
74
  const latest = await store.getArtifact(input.slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)
@@ -94,7 +94,7 @@ export async function runBuildChild(
94
94
  config: currentConfig,
95
95
  getConfig,
96
96
  runtimes,
97
- workspacePath: workspace.path,
97
+ workspacePath: workspace.path ?? workspace.ref,
98
98
  branch: workspace.branch,
99
99
  slug: input.slug,
100
100
  exec: spawnExec,