@defrex/autobuild 0.8.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.
- package/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settlement of foreign build executions from durable facts plus provider
|
|
3
|
+
* liveness (AUT-301). Today's completion handling hangs off the in-memory
|
|
4
|
+
* `handle.completion` promise of the process that launched the runner; when
|
|
5
|
+
* that process dies, nothing writes `execution.ended`, releases the lease, or
|
|
6
|
+
* settles publication. These helpers let any dispatcher tick re-derive the
|
|
7
|
+
* same decisions from the event log and one bounded provider observation —
|
|
8
|
+
* mirroring the engine, which is already a pure function over the log.
|
|
9
|
+
*
|
|
10
|
+
* Conservatism rule: an observation that cannot be resolved (provider error)
|
|
11
|
+
* is treated as `running`. Supervision decisions reap or settle only on
|
|
12
|
+
* provider proof, never on an unknown.
|
|
13
|
+
*/
|
|
14
|
+
import { DISPATCHER } from '../events/envelope'
|
|
15
|
+
import type { AbEvent, EventWrite } from '../events/catalog'
|
|
16
|
+
import type {
|
|
17
|
+
BuildExecution,
|
|
18
|
+
BuildExecutionIdentity,
|
|
19
|
+
ExecutionObservation,
|
|
20
|
+
} from '../ports/workspace/build-execution'
|
|
21
|
+
import type { BuildStore } from '../store/types'
|
|
22
|
+
|
|
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[]): {
|
|
26
|
+
instance: string
|
|
27
|
+
workspaceRef: string
|
|
28
|
+
provider: string
|
|
29
|
+
environmentId?: string
|
|
30
|
+
sessionId?: string
|
|
31
|
+
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 } : {}),
|
|
51
|
+
}
|
|
52
|
+
} else if (event.type === 'execution.ended' && open !== null) {
|
|
53
|
+
if (event.payload.instance === open.instance) open = null
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return open
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The lifecycle state of the latest recorded execution: `open` (no matching
|
|
60
|
+
* 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
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return state
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function observationIdentity(open: {
|
|
76
|
+
instance: string
|
|
77
|
+
workspaceRef: string
|
|
78
|
+
provider: string
|
|
79
|
+
environmentId?: string
|
|
80
|
+
sessionId?: string
|
|
81
|
+
commandId?: string
|
|
82
|
+
}): BuildExecutionIdentity {
|
|
83
|
+
return {
|
|
84
|
+
provider: open.provider,
|
|
85
|
+
workspaceRef: open.workspaceRef,
|
|
86
|
+
...(open.environmentId !== undefined ? { environmentId: open.environmentId } : {}),
|
|
87
|
+
...(open.sessionId !== undefined ? { sessionId: open.sessionId } : {}),
|
|
88
|
+
...(open.commandId !== undefined ? { commandId: open.commandId } : {}),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Durable settlement outcome for one foreign open execution. `settled` means
|
|
93
|
+
* a recorded completion was written and publication was settled; `lost` means
|
|
94
|
+
* the provider proved the execution gone without an exit code (publication is
|
|
95
|
+
* deliberately left to the existing abandonment flow); `running` means the
|
|
96
|
+
* guest is alive or unknowable — no writes. */
|
|
97
|
+
export type ExecutionSettlement = 'running' | 'settled' | 'lost'
|
|
98
|
+
|
|
99
|
+
export interface ExecutionSettlementDeps {
|
|
100
|
+
store: BuildStore
|
|
101
|
+
/** The observing provider's executor. Settlement requires the `observe`
|
|
102
|
+
* capability; without it the caller supervises as before. */
|
|
103
|
+
execution: BuildExecution
|
|
104
|
+
/** Trusted publication settlement, invoked only after a recorded completion. */
|
|
105
|
+
settlePublication?: (slug: string) => Promise<void>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Release the open execution's exact lease, but only while that instance
|
|
109
|
+
* still holds it — a renewed runner lease under a different instance is never
|
|
110
|
+
* touched. */
|
|
111
|
+
async function releaseSettledLease(
|
|
112
|
+
store: BuildStore,
|
|
113
|
+
slug: string,
|
|
114
|
+
instance: string,
|
|
115
|
+
): Promise<void> {
|
|
116
|
+
const record = await store.getBuild(slug)
|
|
117
|
+
if (record?.lease !== undefined && record.lease.holder === instance) {
|
|
118
|
+
await store.releaseLease(slug, instance)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Observe one foreign open execution and settle it from the observation.
|
|
124
|
+
* Observation errors are contained and read as `running` (conservative: never
|
|
125
|
+
* reap on an unknown); the next tick retries.
|
|
126
|
+
*/
|
|
127
|
+
export async function settleExecution(
|
|
128
|
+
deps: ExecutionSettlementDeps,
|
|
129
|
+
slug: string,
|
|
130
|
+
events: AbEvent[],
|
|
131
|
+
): Promise<ExecutionSettlement> {
|
|
132
|
+
const open = openExecution(events)
|
|
133
|
+
if (open === null) return 'settled'
|
|
134
|
+
if (open.commandId === undefined || deps.execution.observe === undefined) return 'running'
|
|
135
|
+
let observation: ExecutionObservation
|
|
136
|
+
try {
|
|
137
|
+
observation = await deps.execution.observe(observationIdentity(open))
|
|
138
|
+
} catch {
|
|
139
|
+
return 'running'
|
|
140
|
+
}
|
|
141
|
+
if (observation.state === 'running') return 'running'
|
|
142
|
+
|
|
143
|
+
const ended: EventWrite<'execution.ended'> =
|
|
144
|
+
observation.state === 'ended'
|
|
145
|
+
? {
|
|
146
|
+
actor: DISPATCHER,
|
|
147
|
+
type: 'execution.ended',
|
|
148
|
+
payload: {
|
|
149
|
+
instance: open.instance,
|
|
150
|
+
workspaceRef: open.workspaceRef,
|
|
151
|
+
outcome: 'completed',
|
|
152
|
+
exitCode: observation.exitCode ?? null,
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
: {
|
|
156
|
+
actor: DISPATCHER,
|
|
157
|
+
type: 'execution.ended',
|
|
158
|
+
payload: {
|
|
159
|
+
instance: open.instance,
|
|
160
|
+
workspaceRef: open.workspaceRef,
|
|
161
|
+
outcome: 'lost',
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
await deps.store.append(slug, ended)
|
|
165
|
+
await releaseSettledLease(deps.store, slug, open.instance)
|
|
166
|
+
if (observation.state === 'ended') {
|
|
167
|
+
await deps.settlePublication?.(slug)
|
|
168
|
+
return 'settled'
|
|
169
|
+
}
|
|
170
|
+
return 'lost'
|
|
171
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash-safe repository-scoped harvest workflow: scan -> synthesize <-> review
|
|
3
|
+
* -> file. It reuses `converge` for adversarial review semantics while every
|
|
4
|
+
* durable boundary lives in the repository journal, so a replacement process
|
|
5
|
+
* resumes the claimed snapshot rather than starting a duplicate run.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
import { loadConfig } from '../config/load'
|
|
9
|
+
import type { HarvestExecutionEnvironment } from '../ports/workspace/harvest-execution'
|
|
10
|
+
import type { HarvestRunnerLaunch } from '../ports/workspace/harvest-execution'
|
|
11
|
+
import { loadPlugins } from '../plugins/load'
|
|
12
|
+
import { materializePluginRuntimes } from '../plugins/runtimes'
|
|
13
|
+
import { randomIds, randomUuids } from '../ids'
|
|
14
|
+
import { createTicketSource } from '../ports/tickets/create'
|
|
15
|
+
import { createProductionRuntimes } from '../ports/runner/production'
|
|
16
|
+
import type { BuildStore } from '../store/types'
|
|
17
|
+
import { systemClock } from '../store/types'
|
|
18
|
+
import { openProductionStore, type StoreOpener } from '../cli/store-opening'
|
|
19
|
+
import { resolveRepoStatePaths } from '../cli/repo-state'
|
|
20
|
+
import { DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS } from '../kernel/harvest'
|
|
21
|
+
import { HarvestRunner } from './harvest-runner'
|
|
22
|
+
|
|
23
|
+
export type { HarvestExecutionEnvironment }
|
|
24
|
+
|
|
25
|
+
/** Guest-side ticket-source arguments, identical to what a local harvest in
|
|
26
|
+
* this checkout would pass: targetRepo is the physical guest checkout, and
|
|
27
|
+
* localStateRoot follows the same store-selection precedence rule the host's
|
|
28
|
+
* `resolveRepoStatePaths` applies (remote store → checkout-local `.autobuild`,
|
|
29
|
+
* local store → the resolved store ref). A relative or defaulted
|
|
30
|
+
* `[tickets].dir` therefore resolves identically on host and guest. */
|
|
31
|
+
export function guestTicketSourceArgs(
|
|
32
|
+
input: HarvestRunnerLaunch,
|
|
33
|
+
workspacePath: string,
|
|
34
|
+
): { targetRepo: string; localStateRoot: string } {
|
|
35
|
+
const paths = resolveRepoStatePaths({
|
|
36
|
+
repo: input.repo,
|
|
37
|
+
checkout: workspacePath,
|
|
38
|
+
storeRef: input.storeRef,
|
|
39
|
+
})
|
|
40
|
+
return { targetRepo: paths.checkout, localStateRoot: paths.localStateRoot }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Guest-side composition. The launch envelope supplies identity, lease
|
|
44
|
+
* adoption, and environment provenance only; configuration comes from the
|
|
45
|
+
* guest checkout and the hosted Store/ticket authority from the forwarded
|
|
46
|
+
* environment — the same kernel class a local harvest runs, so filed
|
|
47
|
+
* proposals, creation keys, and idempotency are byte-identical. Ticket-source
|
|
48
|
+
* composition likewise matches a local harvest in this checkout: a relative
|
|
49
|
+
* or defaulted `[tickets].dir` resolves against the guest checkout and the
|
|
50
|
+
* selected local state root (see `guestTicketSourceArgs`). */
|
|
51
|
+
export async function runHarvestChild(
|
|
52
|
+
input: HarvestRunnerLaunch,
|
|
53
|
+
env: Record<string, string | undefined> = process.env,
|
|
54
|
+
openStore: StoreOpener = openProductionStore,
|
|
55
|
+
/** Guest checkout root. The provider launches the runner with this as cwd. */
|
|
56
|
+
workspacePath: string = process.cwd(),
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
const fullStore: BuildStore = openStore(input.storeRef, env.AB_TOKEN)
|
|
59
|
+
try {
|
|
60
|
+
const config = await loadConfig(join(workspacePath, 'autobuild.toml'))
|
|
61
|
+
// Full build-child composition parity, in the same order: plugins load
|
|
62
|
+
// first or materializePluginRuntimes silently no-ops and leaves any
|
|
63
|
+
// plugin-provided runtime unrouted in the guest.
|
|
64
|
+
const plugins = await loadPlugins(config.plugins, workspacePath, {
|
|
65
|
+
packageRoot: input.repo,
|
|
66
|
+
})
|
|
67
|
+
const { runtimes: builtins } = createProductionRuntimes()
|
|
68
|
+
const runtimes = await materializePluginRuntimes(builtins, plugins, {
|
|
69
|
+
repoRoot: workspacePath,
|
|
70
|
+
env,
|
|
71
|
+
})
|
|
72
|
+
// Ticket-source composition matches a local harvest in this checkout: a
|
|
73
|
+
// relative or defaulted [tickets].dir resolves against the guest checkout
|
|
74
|
+
// and the selected local state root, never the host-side repo identity.
|
|
75
|
+
const { targetRepo, localStateRoot } = guestTicketSourceArgs(input, workspacePath)
|
|
76
|
+
const tickets = await createTicketSource(
|
|
77
|
+
config.tickets,
|
|
78
|
+
env,
|
|
79
|
+
targetRepo,
|
|
80
|
+
localStateRoot,
|
|
81
|
+
plugins,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const runner = new HarvestRunner({
|
|
85
|
+
store: fullStore,
|
|
86
|
+
tickets,
|
|
87
|
+
config,
|
|
88
|
+
runtimes,
|
|
89
|
+
repo: input.repo,
|
|
90
|
+
workspacePath,
|
|
91
|
+
ids: randomIds(),
|
|
92
|
+
uuids: randomUuids(),
|
|
93
|
+
clock: systemClock,
|
|
94
|
+
instance: input.instance,
|
|
95
|
+
// Adopted holder: the guest skips its own claim, heartbeats the owning
|
|
96
|
+
// dispatch loop's repository lease, and never releases it.
|
|
97
|
+
...(input.leaseHolder !== undefined ? { leaseHolder: input.leaseHolder } : {}),
|
|
98
|
+
...(input.environment !== undefined ? { environment: input.environment } : {}),
|
|
99
|
+
sessionEnv: {
|
|
100
|
+
AB_STORE: input.storeRef,
|
|
101
|
+
...(env.AB_TOKEN !== undefined && env.AB_TOKEN !== '' ? { AB_TOKEN: env.AB_TOKEN } : {}),
|
|
102
|
+
},
|
|
103
|
+
opts: {
|
|
104
|
+
maxRecoveryAttempts: DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS,
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
await runner.run()
|
|
108
|
+
} finally {
|
|
109
|
+
try {
|
|
110
|
+
await fullStore.close()
|
|
111
|
+
} catch {
|
|
112
|
+
// Process teardown cannot change the already-durable runner outcome.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable harvest execution state: the repository-journal projection that
|
|
3
|
+
* hosted-harvest supervision (AUT-305) reads instead of process memory. The
|
|
4
|
+
* launch/close facts (`harvest.execution.started`/`.released`) bracket one
|
|
5
|
+
* disposable environment; the run's own facts decide the outcome. Every
|
|
6
|
+
* helper here is a pure function over the journal, mirroring the build
|
|
7
|
+
* execution settlement, so any dispatcher invocation can settle a run whose
|
|
8
|
+
* supervisor died.
|
|
9
|
+
*/
|
|
10
|
+
import type { RepositoryEvent } from '../events/repository'
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS,
|
|
13
|
+
actionableHarvestRun,
|
|
14
|
+
decideHarvestControl,
|
|
15
|
+
reduceHarvest,
|
|
16
|
+
} from '../kernel/harvest'
|
|
17
|
+
import type { HarvestRunnerResult } from './harvest-runner'
|
|
18
|
+
|
|
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<{
|
|
22
|
+
execution: string
|
|
23
|
+
provider: string
|
|
24
|
+
environmentId: string
|
|
25
|
+
sessionId?: string
|
|
26
|
+
commandId?: string
|
|
27
|
+
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)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...open.values()]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface HarvestExecutionClassificationInput {
|
|
58
|
+
/** Repo-journal seq of this execution's `harvest.execution.started` fact. */
|
|
59
|
+
executionStartedSeq: number
|
|
60
|
+
/** The repository-lease holder this execution adopted from the dispatch
|
|
61
|
+
* loop. Absent (local runner shape) makes the held check inapplicable. */
|
|
62
|
+
adoptedHolder?: string
|
|
63
|
+
/** The repository lease holder observed after the guest exited. */
|
|
64
|
+
leaseHolder?: string
|
|
65
|
+
maxRecoveryAttempts?: number
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Classify one finished hosted harvest execution from the repository journal
|
|
69
|
+
* alone — the guest's exit code carries no pipeline outcome. The latest run's
|
|
70
|
+
* terminal fact wins (`completed`/`escalated`/unresolved `failed`) — but only
|
|
71
|
+
* when that terminal fact postdates this execution's start: a stale terminal
|
|
72
|
+
* fact was already counted and announced by the execution that produced it,
|
|
73
|
+
* and re-reporting it would double-count. Launch attribution is by whether
|
|
74
|
+
* the run started inside this execution; a run still open under a
|
|
75
|
+
* parked/paused control decision is `parked`; a lease held by someone else is
|
|
76
|
+
* `held`; anything else is `idle`. The result feeds the same counters a
|
|
77
|
+
* locally run harvest reports. */
|
|
78
|
+
export function classifyHarvestOutcome(
|
|
79
|
+
events: readonly RepositoryEvent[],
|
|
80
|
+
input: HarvestExecutionClassificationInput,
|
|
81
|
+
): HarvestRunnerResult {
|
|
82
|
+
const state = reduceHarvest(events)
|
|
83
|
+
const latestRun = state.runs.at(-1)
|
|
84
|
+
if (latestRun !== undefined) {
|
|
85
|
+
const launch = latestRun.startedSeq > input.executionStartedSeq ? 'started' : 'resumed'
|
|
86
|
+
// Recency gate: a terminal outcome counts only if its fact — the run's
|
|
87
|
+
// terminal fact, or for an unresolved failure the failure fact itself
|
|
88
|
+
// (`failureSeq`), which also covers exhaustion via `terminalSeq` —
|
|
89
|
+
// postdates this execution's `harvest.execution.started`.
|
|
90
|
+
const outcomeSeq = Math.max(latestRun.terminalSeq ?? 0, latestRun.failureSeq ?? 0)
|
|
91
|
+
if (outcomeSeq > input.executionStartedSeq) {
|
|
92
|
+
if (latestRun.status === 'completed') {
|
|
93
|
+
return { outcome: 'completed', launch, run: latestRun.run }
|
|
94
|
+
}
|
|
95
|
+
if (latestRun.status === 'escalated') {
|
|
96
|
+
return { outcome: 'escalated', launch, run: latestRun.run }
|
|
97
|
+
}
|
|
98
|
+
// An unresolved harvest.failed — including recovery exhaustion — is a
|
|
99
|
+
// durable stop, not an idle exit.
|
|
100
|
+
if (latestRun.failure !== undefined) {
|
|
101
|
+
return { outcome: 'failed', launch, run: latestRun.run }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const decision = decideHarvestControl(
|
|
106
|
+
state,
|
|
107
|
+
input.maxRecoveryAttempts ?? DEFAULT_MAX_HARVEST_RECOVERY_ATTEMPTS,
|
|
108
|
+
)
|
|
109
|
+
if (decision.kind === 'park') {
|
|
110
|
+
const run = actionableHarvestRun(state)?.run
|
|
111
|
+
return run === undefined ? { outcome: 'parked' } : { outcome: 'parked', run }
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
input.adoptedHolder !== undefined &&
|
|
115
|
+
input.leaseHolder !== undefined &&
|
|
116
|
+
input.leaseHolder !== input.adoptedHolder
|
|
117
|
+
) {
|
|
118
|
+
return { outcome: 'held' }
|
|
119
|
+
}
|
|
120
|
+
return { outcome: 'idle' }
|
|
121
|
+
}
|