@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
@@ -18,58 +18,80 @@ import type {
18
18
  BuildExecutionIdentity,
19
19
  ExecutionObservation,
20
20
  } from '../ports/workspace/build-execution'
21
+ import { defineReducer } from '../kernel/incremental'
21
22
  import type { BuildStore } from '../store/types'
22
23
 
23
- /** The latest `execution.started` not followed by an instance-matched
24
- * `execution.ended`, or null when no execution is open. */
25
- export function openExecution(events: AbEvent[]): {
24
+ export interface OpenExecution {
26
25
  instance: string
27
26
  workspaceRef: string
28
27
  provider: string
29
28
  environmentId?: string
30
29
  sessionId?: string
31
30
  commandId?: string
32
- } | null {
33
- let open: {
34
- instance: string
35
- workspaceRef: string
36
- provider: string
37
- environmentId?: string
38
- sessionId?: string
39
- commandId?: string
40
- } | null = null
41
- for (const event of events) {
42
- if (event.type === 'execution.started') {
43
- const payload = event.payload
44
- open = {
45
- instance: payload.instance,
46
- workspaceRef: payload.workspaceRef,
47
- provider: payload.provider,
48
- ...(payload.environmentId !== undefined ? { environmentId: payload.environmentId } : {}),
49
- ...(payload.sessionId !== undefined ? { sessionId: payload.sessionId } : {}),
50
- ...(payload.commandId !== undefined ? { commandId: payload.commandId } : {}),
31
+ }
32
+
33
+ export const OPEN_EXECUTION_REDUCER_VERSION = 1
34
+
35
+ /** The latest `execution.started` not followed by an instance-matched
36
+ * `execution.ended`, or null when no execution is open. */
37
+ export const openExecutionReducer = defineReducer<
38
+ { open: OpenExecution | null },
39
+ AbEvent,
40
+ OpenExecution | null
41
+ >({
42
+ version: OPEN_EXECUTION_REDUCER_VERSION,
43
+ initial: () => ({ open: null }),
44
+ fold(acc, events) {
45
+ for (const event of events) {
46
+ if (event.type === 'execution.started') {
47
+ const payload = event.payload
48
+ acc.open = {
49
+ instance: payload.instance,
50
+ workspaceRef: payload.workspaceRef,
51
+ provider: payload.provider,
52
+ ...(payload.environmentId !== undefined ? { environmentId: payload.environmentId } : {}),
53
+ ...(payload.sessionId !== undefined ? { sessionId: payload.sessionId } : {}),
54
+ ...(payload.commandId !== undefined ? { commandId: payload.commandId } : {}),
55
+ }
56
+ } else if (event.type === 'execution.ended' && acc.open !== null) {
57
+ if (event.payload.instance === acc.open.instance) acc.open = null
51
58
  }
52
- } else if (event.type === 'execution.ended' && open !== null) {
53
- if (event.payload.instance === open.instance) open = null
54
59
  }
55
- }
56
- return open
60
+ },
61
+ finish: (acc) => (acc.open === null ? null : { ...acc.open }),
62
+ })
63
+
64
+ export function openExecution(events: AbEvent[]): OpenExecution | null {
65
+ return openExecutionReducer.reduce(events)
57
66
  }
58
67
 
68
+ export type ExecutionOutcome = 'open' | 'completed' | 'stopped' | 'lost' | 'none'
69
+
70
+ export const LAST_EXECUTION_OUTCOME_REDUCER_VERSION = 1
71
+
59
72
  /** The lifecycle state of the latest recorded execution: `open` (no matching
60
73
  * end yet), `completed`/`stopped`/`lost` (its recorded end), or `none`. */
61
- export function lastExecutionOutcome(
62
- events: AbEvent[],
63
- ): 'open' | 'completed' | 'stopped' | 'lost' | 'none' {
64
- let state: 'open' | 'completed' | 'stopped' | 'lost' | 'none' = 'none'
65
- for (const event of events) {
66
- if (event.type === 'execution.started') {
67
- state = 'open'
68
- } else if (event.type === 'execution.ended' && state === 'open') {
69
- state = event.payload.outcome
74
+ export const lastExecutionOutcomeReducer = defineReducer<
75
+ { state: ExecutionOutcome },
76
+ AbEvent,
77
+ ExecutionOutcome
78
+ >({
79
+ version: LAST_EXECUTION_OUTCOME_REDUCER_VERSION,
80
+ initial: () => ({ state: 'none' }),
81
+ fold(acc, events) {
82
+ for (const event of events) {
83
+ if (event.type === 'execution.started') {
84
+ acc.state = 'open'
85
+ } else if (event.type === 'execution.ended' && acc.state === 'open') {
86
+ acc.state = event.payload.outcome
87
+ }
70
88
  }
71
- }
72
- return state
89
+ },
90
+ finish: (acc) => acc.state,
91
+ })
92
+
93
+ export function lastExecutionOutcome(events: AbEvent[]): ExecutionOutcome {
94
+ return lastExecutionOutcomeReducer.reduce(events)
73
95
  }
74
96
 
75
97
  function observationIdentity(open: {
@@ -124,12 +146,23 @@ async function releaseSettledLease(
124
146
  * Observation errors are contained and read as `running` (conservative: never
125
147
  * reap on an unknown); the next tick retries.
126
148
  */
127
- export async function settleExecution(
149
+ export function settleExecution(
128
150
  deps: ExecutionSettlementDeps,
129
151
  slug: string,
130
152
  events: AbEvent[],
131
153
  ): Promise<ExecutionSettlement> {
132
- const open = openExecution(events)
154
+ return settleOpenExecution(deps, slug, openExecution(events))
155
+ }
156
+
157
+ /** `settleExecution` over an already-derived open execution, for callers that
158
+ * hold the projection (a reducer snapshot advanced to the log tail) and so
159
+ * never need the event array. A `running` result or an observation error
160
+ * touches no event rows. */
161
+ export async function settleOpenExecution(
162
+ deps: ExecutionSettlementDeps,
163
+ slug: string,
164
+ open: OpenExecution | null,
165
+ ): Promise<ExecutionSettlement> {
133
166
  if (open === null) return 'settled'
134
167
  if (open.commandId === undefined || deps.execution.observe === undefined) return 'running'
135
168
  let observation: ExecutionObservation
@@ -61,8 +61,13 @@ export async function runHarvestChild(
61
61
  // Full build-child composition parity, in the same order: plugins load
62
62
  // first or materializePluginRuntimes silently no-ops and leaves any
63
63
  // plugin-provided runtime unrouted in the guest.
64
+ // Guest tolerance (AUT-517): a harvest runner launch is always
65
+ // environment-supervised — the process is structurally a guest and never
66
+ // constructs a workspace provider — so a configured provider plugin the
67
+ // guest cannot resolve is skipped with a notice instead of failing.
64
68
  const plugins = await loadPlugins(config.plugins, workspacePath, {
65
69
  packageRoot: input.repo,
70
+ ...(input.supervision.kind === 'environment' ? { guest: true } : {}),
66
71
  })
67
72
  const { runtimes: builtins } = createProductionRuntimes()
68
73
  const runtimes = await materializePluginRuntimes(builtins, plugins, {
@@ -8,6 +8,7 @@
8
8
  * supervisor died.
9
9
  */
10
10
  import type { RepositoryEvent } from '../events/repository'
11
+ import { defineReducer } from '../kernel/incremental'
11
12
  import {
12
13
  DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS,
13
14
  actionableHarvestRun,
@@ -16,42 +17,53 @@ import {
16
17
  } from '../kernel/harvest'
17
18
  import type { HarvestRunnerResult } from './harvest-runner'
18
19
 
19
- /** Open harvest executions: every `harvest.execution.started` not followed by
20
- * a matching `harvest.execution.released`, in journal order. */
21
- export function openHarvestExecutions(events: readonly RepositoryEvent[]): Array<{
20
+ export interface OpenHarvestExecution {
22
21
  execution: string
23
22
  provider: string
24
23
  environmentId: string
25
24
  sessionId?: string
26
25
  commandId?: string
27
26
  seq: number
28
- }> {
29
- const open = new Map<
30
- string,
31
- {
32
- execution: string
33
- provider: string
34
- environmentId: string
35
- sessionId?: string
36
- commandId?: string
37
- seq: number
38
- }
39
- >()
40
- for (const event of events) {
41
- if (event.type === 'harvest.execution.started') {
42
- open.set(event.payload.execution, {
43
- execution: event.payload.execution,
44
- provider: event.payload.provider,
45
- environmentId: event.payload.environmentId,
46
- ...(event.payload.sessionId !== undefined ? { sessionId: event.payload.sessionId } : {}),
47
- ...(event.payload.commandId !== undefined ? { commandId: event.payload.commandId } : {}),
48
- seq: event.seq,
49
- })
50
- } else if (event.type === 'harvest.execution.released') {
51
- open.delete(event.payload.execution)
27
+ }
28
+
29
+ export const OPEN_HARVEST_EXECUTIONS_REDUCER_VERSION = 1
30
+
31
+ /** Open harvest executions: every `harvest.execution.started` not followed by
32
+ * a matching `harvest.execution.released`, in journal order. The accumulator is
33
+ * an ordered array: a re-started execution keeps its original position, as it
34
+ * did in the `Map` this replaces. */
35
+ export const openHarvestExecutionsReducer = defineReducer<
36
+ { open: OpenHarvestExecution[] },
37
+ RepositoryEvent,
38
+ OpenHarvestExecution[]
39
+ >({
40
+ version: OPEN_HARVEST_EXECUTIONS_REDUCER_VERSION,
41
+ initial: () => ({ open: [] }),
42
+ fold(acc, events) {
43
+ for (const event of events) {
44
+ if (event.type === 'harvest.execution.started') {
45
+ const entry: OpenHarvestExecution = {
46
+ execution: event.payload.execution,
47
+ provider: event.payload.provider,
48
+ environmentId: event.payload.environmentId,
49
+ ...(event.payload.sessionId !== undefined ? { sessionId: event.payload.sessionId } : {}),
50
+ ...(event.payload.commandId !== undefined ? { commandId: event.payload.commandId } : {}),
51
+ seq: event.seq,
52
+ }
53
+ const index = acc.open.findIndex((item) => item.execution === entry.execution)
54
+ if (index === -1) acc.open.push(entry)
55
+ else acc.open[index] = entry
56
+ } else if (event.type === 'harvest.execution.released') {
57
+ const index = acc.open.findIndex((item) => item.execution === event.payload.execution)
58
+ if (index !== -1) acc.open.splice(index, 1)
59
+ }
52
60
  }
53
- }
54
- return [...open.values()]
61
+ },
62
+ finish: (acc) => acc.open.map((item) => ({ ...item })),
63
+ })
64
+
65
+ export function openHarvestExecutions(events: readonly RepositoryEvent[]): OpenHarvestExecution[] {
66
+ return openHarvestExecutionsReducer.reduce(events)
55
67
  }
56
68
 
57
69
  export interface HarvestExecutionClassificationInput {
@@ -46,6 +46,7 @@ import { defaultProposalState } from './dispatcher'
46
46
  import {
47
47
  artifactRef,
48
48
  evaluateHarvestPressure,
49
+ evaluateHarvestPressureFromDigests,
49
50
  HARVEST_REPORT_ARTIFACT,
50
51
  HARVEST_SCAN_ARTIFACT,
51
52
  HARVEST_TRANSCRIPT_ARTIFACT,
@@ -230,7 +231,9 @@ export class HarvestRunner {
230
231
  try {
231
232
  await this.startHeartbeat()
232
233
  await this.ensureLease()
233
- let events = await store.getRepoEvents(repo)
234
+ // Bounded read (AUT-489): reduceHarvest and the verdict/failure filters
235
+ // below consume durable types only, so the subset is replay-equivalent.
236
+ let events = await store.getRepoStateEvents(repo)
234
237
  let state = reduceHarvest(events)
235
238
  run = actionableHarvestRun(state)
236
239
 
@@ -238,19 +241,48 @@ export class HarvestRunner {
238
241
  // selected run comes from the full journal, so a shadowed failure or
239
242
  // exhaustion barrier is recovered/reported before any later open run.
240
243
  await this.controlBoundary(run?.run)
241
- events = await store.getRepoEvents(repo)
244
+ events = await store.getRepoStateEvents(repo)
242
245
  state = reduceHarvest(events)
243
246
  run = openHarvestRun(state)
244
247
 
245
248
  if (!run) {
246
249
  await this.controlBoundary()
247
- const scan = await scanUnclaimedObservations(store, repo)
248
- // Scanning may span many build streams. Treat its completion as a
249
- // boundary even when the threshold is not met, so a request arriving
250
- // during the read is acknowledged before this runner returns idle.
250
+ // Flat-cost gate (AUT-521): the pressure trigger is evaluated from
251
+ // the journal and one repo-scoped digest batch read — no per-build
252
+ // history reads — so the idle evaluation no longer grows with
253
+ // accumulated finished builds. Only a tripped gate pays for the full
254
+ // unclaimed-observation scan, because the packet artifact needs the
255
+ // observation payloads no bounded source carries.
256
+ // Bounded read (AUT-489): the gate's journal input is consumed only
257
+ // through reduceHarvest, so the subset is replay-equivalent here too.
258
+ const [gateEvents, digests] = await Promise.all([
259
+ store.getRepoStateEvents(repo),
260
+ store.getRepoBuildDigests(repo),
261
+ ])
262
+ const pressure = evaluateHarvestPressureFromDigests({
263
+ digests,
264
+ harvestEvents: gateEvents,
265
+ policy: this.currentConfig().policy,
266
+ })
267
+ if (pressure.trigger === undefined) {
268
+ // Treat the read's completion as a boundary even when the gate did
269
+ // not trip, so a request arriving during the read is acknowledged
270
+ // before this runner returns idle.
271
+ await this.controlBoundary()
272
+ return { outcome: 'idle' }
273
+ }
251
274
  await this.controlBoundary()
252
- const pressure = evaluateHarvestPressure(scan, this.currentConfig().policy)
253
- if (pressure.trigger === undefined) return { outcome: 'idle' }
275
+ const scan = await scanUnclaimedObservations(store, repo)
276
+ // The trigger came from an earlier read than this scan, and a claim
277
+ // may have landed in the window. Re-confirm the gate against the
278
+ // fresh scan (pure, free): an emptied window returns idle exactly as
279
+ // if the gate had not tripped, so a packet is only ever built from a
280
+ // scan that itself triggered.
281
+ const confirmed = evaluateHarvestPressure(scan, this.currentConfig().policy)
282
+ if (confirmed.trigger === undefined) {
283
+ await this.controlBoundary()
284
+ return { outcome: 'idle' }
285
+ }
254
286
  const runId = this.deps.ids('harvest')
255
287
  const packet = await makeHarvestScanPacket({
256
288
  store,
@@ -280,14 +312,15 @@ export class HarvestRunner {
280
312
  run: runId,
281
313
  observations: packet.observations.map((item) => item.occurrence),
282
314
  scan: artifactRef(deposited[0]!),
283
- trigger: pressure.trigger,
315
+ trigger: confirmed.trigger,
284
316
  ...(this.deps.environment !== undefined
285
317
  ? { environment: this.deps.environment }
286
318
  : {}),
287
319
  },
288
320
  }),
289
321
  )
290
- events = await store.getRepoEvents(repo)
322
+ // Bounded read (AUT-489): reduceHarvest consumes durable types only.
323
+ events = await store.getRepoStateEvents(repo)
291
324
  run = reduceHarvest(events).runs.find((candidate) => candidate.run === runId)
292
325
  if (!run) throw new Error(`harvest.started did not reduce run "${runId}"`)
293
326
  // A request racing the claim parks this exact run; its snapshot stays
@@ -448,7 +481,9 @@ export class HarvestRunner {
448
481
  existing.verdict,
449
482
  existing.artifact,
450
483
  )
451
- const event = (await this.deps.store.getRepoEvents(this.deps.repo)).find(
484
+ // Bounded read (AUT-489): only harvest.review.verdict facts are
485
+ // filtered here, and those are durable.
486
+ const event = (await this.deps.store.getRepoStateEvents(this.deps.repo)).find(
452
487
  (
453
488
  candidate,
454
489
  ): candidate is Extract<RepositoryEvent, { type: 'harvest.review.verdict' }> =>
@@ -515,7 +550,9 @@ export class HarvestRunner {
515
550
  event.actor.kind === 'agent' &&
516
551
  event.actor.session === session,
517
552
  })
518
- const events = await this.deps.store.getRepoEvents(this.deps.repo)
553
+ // Bounded read (AUT-489): only harvest.review.verdict facts are filtered
554
+ // here, and those are durable.
555
+ const events = await this.deps.store.getRepoStateEvents(this.deps.repo)
519
556
  const verdict = [...events]
520
557
  .reverse()
521
558
  .find(
@@ -565,7 +602,9 @@ export class HarvestRunner {
565
602
  ): Promise<void> {
566
603
  const { store, repo, ids, workspacePath } = this.deps
567
604
  await this.ensureLease()
568
- const events = await store.getRepoEvents(repo)
605
+ // Bounded read (AUT-489): reduceHarvest and the harvest.failed filter
606
+ // consume durable types only.
607
+ const events = await store.getRepoStateEvents(repo)
569
608
  const matchingFailures = events.filter(
570
609
  (event): event is Extract<RepositoryEvent, { type: 'harvest.failed' }> =>
571
610
  event.type === 'harvest.failed' &&
@@ -823,7 +862,9 @@ export class HarvestRunner {
823
862
  }): Promise<void> {
824
863
  const { store, repo, ids, workspacePath } = this.deps
825
864
  await this.ensureLease()
826
- const events = await store.getRepoEvents(repo)
865
+ // Bounded read (AUT-489): reduceHarvest and the harvest.failed filter
866
+ // consume durable types only.
867
+ const events = await store.getRepoStateEvents(repo)
827
868
  const matchingFailures = events.filter(
828
869
  (event): event is Extract<RepositoryEvent, { type: 'harvest.failed' }> =>
829
870
  event.type === 'harvest.failed' &&
@@ -1296,7 +1337,9 @@ export class HarvestRunner {
1296
1337
 
1297
1338
  private async recordWorkflowFailure(run: string, error: unknown): Promise<void> {
1298
1339
  await this.ensureLease()
1299
- const events = await this.deps.store.getRepoEvents(this.deps.repo)
1340
+ // Bounded read (AUT-489): reduceHarvest and the harvest.failed filter
1341
+ // consume durable types only.
1342
+ const events = await this.deps.store.getRepoStateEvents(this.deps.repo)
1300
1343
  const state = reduceHarvest(events)
1301
1344
  const current = state.runs.find((candidate) => candidate.run === run)
1302
1345
  if (current?.status !== 'running') return
@@ -1349,7 +1392,8 @@ export class HarvestRunner {
1349
1392
  * outer budget and therefore remain recoverable. */
1350
1393
  private async settleRecoveryExhaustion(run: string): Promise<void> {
1351
1394
  await this.ensureLease()
1352
- const state = reduceHarvest(await this.deps.store.getRepoEvents(this.deps.repo))
1395
+ // Bounded read (AUT-489): reduceHarvest consumes durable types only.
1396
+ const state = reduceHarvest(await this.deps.store.getRepoStateEvents(this.deps.repo))
1353
1397
  const decision = decideHarvestControl(state, this.maxRecoveryAttempts)
1354
1398
  if (decision.kind !== 'exhaust-recovery' || decision.run !== run) return
1355
1399
  await this.finalizeRecoveryExhaustion(decision.run, decision.attempts, decision.limit)
@@ -1374,7 +1418,8 @@ export class HarvestRunner {
1374
1418
  })
1375
1419
 
1376
1420
  await this.ensureLease()
1377
- const state = reduceHarvest(await this.deps.store.getRepoEvents(this.deps.repo))
1421
+ // Bounded read (AUT-489): reduceHarvest consumes durable types only.
1422
+ const state = reduceHarvest(await this.deps.store.getRepoStateEvents(this.deps.repo))
1378
1423
  const decision = decideHarvestControl(state, this.maxRecoveryAttempts)
1379
1424
  if (
1380
1425
  decision.kind !== 'exhaust-recovery' ||
@@ -1410,7 +1455,8 @@ export class HarvestRunner {
1410
1455
  private async controlBoundary(run?: string): Promise<void> {
1411
1456
  while (true) {
1412
1457
  await this.ensureLease()
1413
- const state = reduceHarvest(await this.deps.store.getRepoEvents(this.deps.repo))
1458
+ // Bounded read (AUT-489): reduceHarvest consumes durable types only.
1459
+ const state = reduceHarvest(await this.deps.store.getRepoStateEvents(this.deps.repo))
1414
1460
  const decision = decideHarvestControl(state, this.maxRecoveryAttempts)
1415
1461
  if (decision.kind === 'proceed') return
1416
1462
  if (decision.kind === 'park') {
@@ -1453,7 +1499,8 @@ export class HarvestRunner {
1453
1499
  }
1454
1500
 
1455
1501
  private async refreshRun(run: string): Promise<HarvestRunState> {
1456
- const state = reduceHarvest(await this.deps.store.getRepoEvents(this.deps.repo))
1502
+ // Bounded read (AUT-489): reduceHarvest consumes durable types only.
1503
+ const state = reduceHarvest(await this.deps.store.getRepoStateEvents(this.deps.repo))
1457
1504
  const found = state.runs.find((candidate) => candidate.run === run)
1458
1505
  if (!found) throw new Error(`unknown harvest run "${run}"`)
1459
1506
  return found