@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.
- package/README.md +1 -1
- package/SPEC.md +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +204 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/skills/tickets/SKILL.md +57 -5
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -1,16 +1,36 @@
|
|
|
1
|
+
import { composeBuildConfig } from '../config/live'
|
|
2
|
+
import type { PipelineSourceMeta } from '../config/pipeline-source'
|
|
1
3
|
import { configSchema, type Config } from '../config/schema'
|
|
2
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
BUILD_EFFECTIVE_CONFIG_ARTIFACT,
|
|
6
|
+
parseBuildConfigMetadata,
|
|
7
|
+
parseEffectiveBuildConfig,
|
|
8
|
+
} from '../processes/build-execution-state'
|
|
9
|
+
import type { Artifact } from '../store/types'
|
|
10
|
+
import {
|
|
11
|
+
detail,
|
|
12
|
+
statusFilter,
|
|
13
|
+
summarizeFrom,
|
|
14
|
+
type BuildDetail,
|
|
15
|
+
type BuildSummary,
|
|
16
|
+
} from '../cli/status'
|
|
3
17
|
import { projectRepositoryStatus, type RepositoryStatus } from '../cli/repository-status'
|
|
4
18
|
import { projectHarvestStatus, type HarvestStatusView } from '../cli/harvest'
|
|
5
19
|
import {
|
|
6
20
|
buildDashboardFromProjected,
|
|
21
|
+
effectiveStatus,
|
|
7
22
|
projectBuild,
|
|
23
|
+
projectBuildFromFacts,
|
|
8
24
|
type DashboardBuild,
|
|
9
25
|
type DashboardModel,
|
|
10
26
|
} from '../cli/dashboard/model'
|
|
11
|
-
import { reduceBuild } from '../kernel/reducer'
|
|
27
|
+
import { reduceBuild, type BuildState } from '../kernel/reducer'
|
|
28
|
+
import { DISPATCHER_EFFECTIVE_CONFIG_ARTIFACT } from '../store/retention'
|
|
12
29
|
import { reduceDispatchStatus } from '../kernel/dispatch-status'
|
|
13
|
-
import {
|
|
30
|
+
import { readRepoEventsIfRecorded, unclaimedObservationCount } from '../processes/harvest'
|
|
31
|
+
import type { RepoViewStore } from '../processes/repo-view'
|
|
32
|
+
import { withOperatorView } from './read-view'
|
|
33
|
+
import type { RepositoryEvent } from '../events/repository'
|
|
14
34
|
import type { BuildStore, Clock } from '../store/types'
|
|
15
35
|
|
|
16
36
|
export type BuildListScope = 'active' | 'queued' | 'all'
|
|
@@ -25,21 +45,55 @@ export class OperatorQueryError extends Error {
|
|
|
25
45
|
}
|
|
26
46
|
}
|
|
27
47
|
|
|
28
|
-
async function repoEvents(store: BuildStore, repo: string) {
|
|
29
|
-
return (await store.getRepo(repo)) === null ? [] : store.getRepoEvents(repo)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
48
|
export async function listOperatorBuilds(opts: {
|
|
33
49
|
store: BuildStore
|
|
34
50
|
repo: string
|
|
35
51
|
scope: BuildListScope
|
|
36
52
|
now: Date
|
|
37
53
|
}): Promise<BuildSummary[]> {
|
|
54
|
+
return withOperatorView(opts.store, opts.repo, 'builds', (view) => listFromView(view, opts))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function listFromView(
|
|
58
|
+
view: RepoViewStore,
|
|
59
|
+
opts: { repo: string; scope: BuildListScope; now: Date },
|
|
60
|
+
): Promise<BuildSummary[]> {
|
|
38
61
|
const statuses = new Set(statusFilter(opts.scope === 'all', opts.scope === 'queued'))
|
|
62
|
+
// Records are read and held before the digest read: builds are never
|
|
63
|
+
// deleted, so every record iterated below is guaranteed an entry in the
|
|
64
|
+
// digest map fetched after it — a build created between the two reads
|
|
65
|
+
// appears only in the digest map, which is harmless — and the completeness
|
|
66
|
+
// check can only ever catch a genuine adapter bug, never a concurrent
|
|
67
|
+
// dispatch. The reverse order would let a concurrent creation abort the
|
|
68
|
+
// whole listing (AUT-488 finding f_3cb67aba).
|
|
69
|
+
const records = await view.listBuilds()
|
|
70
|
+
// One digest read gates the per-build reads (AUT-488): the active and queued
|
|
71
|
+
// scopes never show a terminal build, so a build whose digest already
|
|
72
|
+
// carries a terminal fact is skipped and the listing's cost stays flat as
|
|
73
|
+
// finished builds accumulate. The `--all` scope includes terminal statuses,
|
|
74
|
+
// so it takes no digest read and skips nothing; its terminal builds are
|
|
75
|
+
// restored from the reducer snapshots the dispatcher leaves at settlement.
|
|
76
|
+
const digests = opts.scope === 'all' ? undefined : await view.getRepoBuildDigests(opts.repo)
|
|
39
77
|
const output: BuildSummary[] = []
|
|
40
|
-
for (const record of
|
|
78
|
+
for (const record of records) {
|
|
41
79
|
if (record.repo !== opts.repo) continue
|
|
42
|
-
|
|
80
|
+
if (digests !== undefined) {
|
|
81
|
+
// Completeness is contractual (one entry per repo build); a missing
|
|
82
|
+
// entry is an adapter bug and must fail loudly rather than silently
|
|
83
|
+
// drop the build from the listing.
|
|
84
|
+
const digest = digests.get(record.slug)
|
|
85
|
+
if (digest === undefined) {
|
|
86
|
+
throw new Error(`getRepoBuildDigests is missing an entry for build "${record.slug}"`)
|
|
87
|
+
}
|
|
88
|
+
// The digest's `terminal` follows `reduceBuild`'s terminal rule exactly
|
|
89
|
+
// (in-order overwrite of `build.completed`/`build.aborted`, never
|
|
90
|
+
// cleared — pinned against `reduceBuild` in store/digest.test.ts), so
|
|
91
|
+
// `terminal !== undefined` is exactly reduced status `done`/`aborted`,
|
|
92
|
+
// which the active and queued status sets never include. Skipping the
|
|
93
|
+
// build therefore cannot change the emitted summaries.
|
|
94
|
+
if (digest.terminal !== undefined) continue
|
|
95
|
+
}
|
|
96
|
+
const projected = summarizeFrom(record, (await view.buildFacts(record.slug)).state, opts.now)
|
|
43
97
|
if (statuses.has(projected.status)) output.push(projected)
|
|
44
98
|
}
|
|
45
99
|
return output.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
|
|
@@ -57,12 +111,15 @@ async function requireBuild(store: BuildStore, repo: string, slug: string) {
|
|
|
57
111
|
export async function effectiveConfig(
|
|
58
112
|
store: BuildStore,
|
|
59
113
|
repo: string,
|
|
114
|
+
/** The bounded journal, when the caller already holds it (a repository
|
|
115
|
+
* view); otherwise it is read from the store. */
|
|
116
|
+
journal?: RepositoryEvent[],
|
|
60
117
|
): Promise<{
|
|
61
118
|
config: Config
|
|
62
|
-
repositoryEvents:
|
|
119
|
+
repositoryEvents: RepositoryEvent[]
|
|
63
120
|
status: ReturnType<typeof reduceDispatchStatus>
|
|
64
121
|
}> {
|
|
65
|
-
const repositoryEvents = await
|
|
122
|
+
const repositoryEvents = journal ?? (await readRepoEventsIfRecorded(store, repo))
|
|
66
123
|
let latestRun: string | undefined
|
|
67
124
|
for (const event of repositoryEvents) {
|
|
68
125
|
if (event.type === 'dispatcher.run-started') latestRun = event.payload.run
|
|
@@ -112,6 +169,65 @@ export interface OperatorBuildView {
|
|
|
112
169
|
dashboardRow: DashboardBuild | null
|
|
113
170
|
}
|
|
114
171
|
|
|
172
|
+
interface PinnedProjection {
|
|
173
|
+
config: Config
|
|
174
|
+
revision?: number
|
|
175
|
+
pipelineSource?: PipelineSourceMeta
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Read one build's pinned effective-config artifact (SPEC §16.1): its parsed
|
|
179
|
+
* build-owned sections composed over the live snapshot's deployment-owned
|
|
180
|
+
* ones, plus the artifact's metadata projection. Every row must be projected
|
|
181
|
+
* against the pipeline the build actually runs — the artifact's build-owned
|
|
182
|
+
* sections — never the dispatcher's live base-branch snapshot, which a pinned
|
|
183
|
+
* build may never execute. All read failures are display-only: an absent or
|
|
184
|
+
* malformed artifact degrades to the live config, the pre-pin behavior. */
|
|
185
|
+
async function readPinnedConfig(
|
|
186
|
+
store: BuildStore,
|
|
187
|
+
slug: string,
|
|
188
|
+
live: Config,
|
|
189
|
+
): Promise<PinnedProjection> {
|
|
190
|
+
let artifact: Artifact | null
|
|
191
|
+
try {
|
|
192
|
+
artifact = await store.getArtifact(slug, BUILD_EFFECTIVE_CONFIG_ARTIFACT)
|
|
193
|
+
} catch {
|
|
194
|
+
artifact = null
|
|
195
|
+
}
|
|
196
|
+
if (artifact === null) return { config: live }
|
|
197
|
+
let config: Config
|
|
198
|
+
try {
|
|
199
|
+
config = composeBuildConfig(parseEffectiveBuildConfig(artifact), live)
|
|
200
|
+
} catch {
|
|
201
|
+
config = live
|
|
202
|
+
}
|
|
203
|
+
return { config, ...parseBuildConfigMetadata(artifact) }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The shared null-row gate (AUT-486 snapshot, AUT-496 getOperatorBuild,
|
|
207
|
+
* AUT-487 digest): `effectiveStatus` maps only `done` outside the visible set
|
|
208
|
+
* (cli/dashboard/model.ts `isVisible`), so `projectBuild` returns null
|
|
209
|
+
* exactly for this state, and a pinned effective-config read for such a
|
|
210
|
+
* build could never be surfaced. One predicate so every consumer cannot
|
|
211
|
+
* diverge from each other or from the row projection: the digest's `terminal`
|
|
212
|
+
* is a third consumer of the same gate — `terminal === 'done'` is exactly
|
|
213
|
+
* `reduceBuild(...).status === 'done'` — and the snapshot uses it to skip a
|
|
214
|
+
* finished build's history and pinned-config reads without weakening this
|
|
215
|
+
* predicate as the authority. If cli/dashboard/model.ts ever adds a
|
|
216
|
+
* non-visible status, this predicate must follow — the snapshot
|
|
217
|
+
* byte-identical differential test and the done/aborted query tests pin the
|
|
218
|
+
* coupling. */
|
|
219
|
+
function projectsNoDashboardRow(state: BuildState): boolean {
|
|
220
|
+
return effectiveStatus(state) === 'done'
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Attach the effective-config metadata (SPEC §16.1) to a projected row so an
|
|
224
|
+
* operator can see which autobuild.toml the build runs under. */
|
|
225
|
+
function decorateWithPinnedMeta(row: DashboardBuild, pinned: PinnedProjection): DashboardBuild {
|
|
226
|
+
if (pinned.revision !== undefined) row.effectiveConfigRev = pinned.revision
|
|
227
|
+
if (pinned.pipelineSource !== undefined) row.pipelineSource = pinned.pipelineSource
|
|
228
|
+
return row
|
|
229
|
+
}
|
|
230
|
+
|
|
115
231
|
export async function getOperatorBuild(opts: {
|
|
116
232
|
store: BuildStore
|
|
117
233
|
repo: string
|
|
@@ -122,22 +238,36 @@ export async function getOperatorBuild(opts: {
|
|
|
122
238
|
const events = await opts.store.getEvents(opts.slug)
|
|
123
239
|
const state = reduceBuild(events)
|
|
124
240
|
const { config } = await effectiveConfig(opts.store, opts.repo)
|
|
125
|
-
|
|
126
|
-
|
|
241
|
+
let dashboardRow: DashboardBuild | null = null
|
|
242
|
+
if (!projectsNoDashboardRow(state)) {
|
|
243
|
+
const pinned = await readPinnedConfig(opts.store, opts.slug, config)
|
|
244
|
+
const row = projectBuild(record, state, config, events, undefined, pinned.config)
|
|
245
|
+
// Implied by the gate; kept as an explicit check (not an assertion) so the
|
|
246
|
+
// code never lies if `effectiveStatus` and `projectBuild` ever drift.
|
|
247
|
+
if (row !== null) dashboardRow = decorateWithPinnedMeta(row, pinned)
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
detail: detail(record, events, opts.now),
|
|
251
|
+
dashboardRow,
|
|
252
|
+
}
|
|
127
253
|
}
|
|
128
254
|
|
|
129
255
|
export async function getRepositoryStatus(
|
|
130
256
|
store: BuildStore,
|
|
131
257
|
repo: string,
|
|
132
258
|
): Promise<RepositoryStatus> {
|
|
133
|
-
return
|
|
259
|
+
return withOperatorView(store, repo, 'journal', async (view) =>
|
|
260
|
+
projectRepositoryStatus(repo, view.recordedJournal()),
|
|
261
|
+
)
|
|
134
262
|
}
|
|
135
263
|
|
|
136
264
|
export async function getHarvestStatus(
|
|
137
265
|
store: BuildStore,
|
|
138
266
|
repo: string,
|
|
139
267
|
): Promise<HarvestStatusView> {
|
|
140
|
-
return
|
|
268
|
+
return withOperatorView(store, repo, 'journal', async (view) =>
|
|
269
|
+
projectHarvestStatus(repo, view.recordedJournal()),
|
|
270
|
+
)
|
|
141
271
|
}
|
|
142
272
|
|
|
143
273
|
export interface OperatorDashboardSnapshot {
|
|
@@ -156,18 +286,68 @@ export async function getOperatorDashboard(opts: {
|
|
|
156
286
|
repo: string
|
|
157
287
|
clock: Clock
|
|
158
288
|
}): Promise<OperatorDashboardSnapshot> {
|
|
159
|
-
|
|
289
|
+
return withOperatorView(opts.store, opts.repo, 'builds', (view) => dashboardFromView(view, opts))
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function dashboardFromView(
|
|
293
|
+
view: RepoViewStore,
|
|
294
|
+
opts: { repo: string; clock: Clock },
|
|
295
|
+
): Promise<OperatorDashboardSnapshot> {
|
|
296
|
+
const { config, repositoryEvents, status } = await effectiveConfig(
|
|
297
|
+
view,
|
|
298
|
+
opts.repo,
|
|
299
|
+
view.recordedJournal(),
|
|
300
|
+
)
|
|
301
|
+
const records = await view.listBuilds()
|
|
302
|
+
// One digest read covers every build of the repository (AUT-487): the
|
|
303
|
+
// row gate (a `done` digest means no row) and the observation count need no
|
|
304
|
+
// per-build histories, so the snapshot's traffic stays flat as finished
|
|
305
|
+
// builds accumulate. Row-rendering builds derive from the repository view's
|
|
306
|
+
// reducer snapshots plus a delta read (AUT-650); every snapshot is a cache
|
|
307
|
+
// the event log regenerates, so the response equals a full replay's.
|
|
308
|
+
const digests = await view.getRepoBuildDigests(opts.repo)
|
|
160
309
|
const projected: DashboardBuild[] = []
|
|
161
310
|
let activeCount = 0
|
|
162
|
-
for (const record of
|
|
311
|
+
for (const record of records) {
|
|
163
312
|
if (record.repo !== opts.repo) continue
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (
|
|
313
|
+
const digest = digests.get(record.slug)
|
|
314
|
+
// Completeness is contractual (one entry per repo build); a missing entry
|
|
315
|
+
// is an adapter bug and must fail loudly rather than silently drop the
|
|
316
|
+
// build from the active count or the row list.
|
|
317
|
+
if (digest === undefined) {
|
|
318
|
+
throw new Error(`getRepoBuildDigests is missing an entry for build "${record.slug}"`)
|
|
319
|
+
}
|
|
320
|
+
// Reduced status is `done`/`aborted` exactly when a terminal fact exists,
|
|
321
|
+
// so `terminal === undefined` is exactly the old active test — no history
|
|
322
|
+
// read needed for the count.
|
|
323
|
+
if (digest.terminal === undefined) activeCount += 1
|
|
324
|
+
if (digest.terminal === 'done') continue
|
|
325
|
+
// Both query surfaces share the `projectsNoDashboardRow` gate (AUT-486
|
|
326
|
+
// snapshot, AUT-496 detail query): a build that projects no row —
|
|
327
|
+
// `effectiveStatus` maps only `done` outside the visible set — never
|
|
328
|
+
// reads its pinned effective-config artifact, because the read result
|
|
329
|
+
// could never be surfaced. The digest already excluded `done` builds
|
|
330
|
+
// above; the gate stays as the authority for the row projection so the
|
|
331
|
+
// digest and the reducer cannot silently diverge. Every row-rendering
|
|
332
|
+
// build still gets its facts and pinned pipeline.
|
|
333
|
+
const facts = await view.buildFacts(record.slug)
|
|
334
|
+
if (projectsNoDashboardRow(facts.state)) continue
|
|
335
|
+
const pinned = await readPinnedConfig(view, record.slug, config)
|
|
336
|
+
const row = projectBuildFromFacts(
|
|
337
|
+
record,
|
|
338
|
+
facts.state,
|
|
339
|
+
{ dashboard: facts.dashboard, log: facts.log, deferral: facts.deferral },
|
|
340
|
+
config,
|
|
341
|
+
undefined,
|
|
342
|
+
pinned.config,
|
|
343
|
+
)
|
|
344
|
+
if (row !== null) projected.push(decorateWithPinnedMeta(row, pinned))
|
|
169
345
|
}
|
|
170
|
-
|
|
346
|
+
// The unclaimed-observation count comes from the digests and the journal
|
|
347
|
+
// `effectiveConfig` already returned — no per-build history reads (AUT-487).
|
|
348
|
+
// The pure core writes nothing — a snapshot performs no store writes and
|
|
349
|
+
// never creates or locks the repository record.
|
|
350
|
+
const observationCount = unclaimedObservationCount({ digests, harvestEvents: repositoryEvents })
|
|
171
351
|
const warningLines = [
|
|
172
352
|
...status.roleWarnings,
|
|
173
353
|
...(status.warningNotice !== undefined ? [status.warningNotice] : []),
|
|
@@ -179,7 +359,7 @@ export async function getOperatorDashboard(opts: {
|
|
|
179
359
|
queued: status.queued ?? 0,
|
|
180
360
|
activeCount,
|
|
181
361
|
capacity: config.capacity,
|
|
182
|
-
observationCount
|
|
362
|
+
observationCount,
|
|
183
363
|
observationLimit: config.policy.harvestThreshold,
|
|
184
364
|
...(status.availableUpgrade !== undefined
|
|
185
365
|
? { availableUpgrade: status.availableUpgrade }
|
|
@@ -199,3 +379,52 @@ export async function getOperatorDashboard(opts: {
|
|
|
199
379
|
},
|
|
200
380
|
}
|
|
201
381
|
}
|
|
382
|
+
|
|
383
|
+
/** Thrown when a deposited `dispatcher-effective-config` artifact exists but
|
|
384
|
+
* cannot be parsed or validated: the hosted message route maps this to a 500
|
|
385
|
+
* `internal` — a broken config must never silently disable the orchestrator
|
|
386
|
+
* (AUT-342). */
|
|
387
|
+
export class OrchestratorConfigError extends Error {
|
|
388
|
+
constructor(message: string) {
|
|
389
|
+
super(message)
|
|
390
|
+
this.name = 'OrchestratorConfigError'
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** The repository's orchestrator configuration, resolved from the latest
|
|
395
|
+
* deposited `dispatcher-effective-config` artifact (AUT-342). Returns null
|
|
396
|
+
* when no artifact exists — the orchestrator is disabled, and message
|
|
397
|
+
* posting behaves exactly as it did before this feature. Throws
|
|
398
|
+
* `OperatorQueryError` (code `effective-config-unavailable`) when an
|
|
399
|
+
* artifact exists but is unreadable or invalid: a deployment that deposited
|
|
400
|
+
* a broken config must fail loudly (a 500 on the message route), never
|
|
401
|
+
* silently disable the orchestrator. */
|
|
402
|
+
export async function orchestratorConfig(store: BuildStore, repo: string): Promise<Config | null> {
|
|
403
|
+
// A repository the store has no record for has no deposited artifact and
|
|
404
|
+
// no dispatcher history: the orchestrator is disabled there.
|
|
405
|
+
if ((await store.getRepo(repo)) === null) return null
|
|
406
|
+
const artifacts = await store.listRepoArtifacts(repo, DISPATCHER_EFFECTIVE_CONFIG_ARTIFACT)
|
|
407
|
+
if (artifacts.length === 0) return null
|
|
408
|
+
const latest = [...artifacts].sort((a, b) => b.revision - a.revision)[0]!
|
|
409
|
+
const artifact = await store.getRepoArtifact(repo, latest.kind, latest.revision)
|
|
410
|
+
if (artifact === null) {
|
|
411
|
+
throw new OrchestratorConfigError(
|
|
412
|
+
`effective config ${latest.kind}@${latest.revision} is not retrievable`,
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
let raw: unknown
|
|
416
|
+
try {
|
|
417
|
+
raw = JSON.parse(new TextDecoder().decode(artifact.content))
|
|
418
|
+
} catch {
|
|
419
|
+
throw new OrchestratorConfigError(
|
|
420
|
+
`effective config ${latest.kind}@${latest.revision} is not valid JSON`,
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
const parsed = configSchema.safeParse(raw)
|
|
424
|
+
if (!parsed.success) {
|
|
425
|
+
throw new OrchestratorConfigError(
|
|
426
|
+
`effective config ${latest.kind}@${latest.revision} is invalid: ${parsed.error.message}`,
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
return parsed.data.orchestrator.enabled ? parsed.data : null
|
|
430
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operator read paths' repository view: every poll-driven route (dashboard,
|
|
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.
|
|
7
|
+
*
|
|
8
|
+
* Passes are single-flight per `(store, repo)` within one serving process: at
|
|
9
|
+
* most one pass runs at a time, and requests arriving while it runs join the
|
|
10
|
+
* one trailing pass (each needs state at least as new as its arrival, which the
|
|
11
|
+
* 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
|
|
14
|
+
* still comes from durable state, and a second serving process simply reads the
|
|
15
|
+
* same delta once itself (duplicate reads are bounded by the process count).
|
|
16
|
+
*/
|
|
17
|
+
import { RepoViewStore } from '../processes/repo-view'
|
|
18
|
+
import type { BuildStore } from '../store/types'
|
|
19
|
+
|
|
20
|
+
/** What a request reads: the journal alone, or the builds beside it. */
|
|
21
|
+
export type OperatorViewNeed = 'journal' | 'builds'
|
|
22
|
+
|
|
23
|
+
interface Waiter {
|
|
24
|
+
need: OperatorViewNeed
|
|
25
|
+
run: (view: RepoViewStore) => Promise<unknown>
|
|
26
|
+
resolve: (value: unknown) => void
|
|
27
|
+
reject: (error: unknown) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface Flight {
|
|
31
|
+
running: boolean
|
|
32
|
+
queue: Waiter[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const flights = new WeakMap<BuildStore, Map<string, Flight>>()
|
|
36
|
+
|
|
37
|
+
function flightFor(store: BuildStore, repo: string): Flight {
|
|
38
|
+
let byRepo = flights.get(store)
|
|
39
|
+
if (byRepo === undefined) {
|
|
40
|
+
byRepo = new Map()
|
|
41
|
+
flights.set(store, byRepo)
|
|
42
|
+
}
|
|
43
|
+
let flight = byRepo.get(repo)
|
|
44
|
+
if (flight === undefined) {
|
|
45
|
+
flight = { running: false, queue: [] }
|
|
46
|
+
byRepo.set(repo, flight)
|
|
47
|
+
}
|
|
48
|
+
return flight
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function pass(store: BuildStore, repo: string, waiters: Waiter[]): Promise<void> {
|
|
52
|
+
const view = new RepoViewStore(store, { repo })
|
|
53
|
+
try {
|
|
54
|
+
if (waiters.some((waiter) => waiter.need === 'builds')) await view.refresh()
|
|
55
|
+
else await view.startJournal()
|
|
56
|
+
} catch (error) {
|
|
57
|
+
for (const waiter of waiters) waiter.reject(error)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
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
|
+
}
|
|
70
|
+
|
|
71
|
+
async function drain(store: BuildStore, repo: string, flight: Flight): Promise<void> {
|
|
72
|
+
flight.running = true
|
|
73
|
+
try {
|
|
74
|
+
while (flight.queue.length > 0) {
|
|
75
|
+
const waiters = flight.queue
|
|
76
|
+
flight.queue = []
|
|
77
|
+
await pass(store, repo, waiters)
|
|
78
|
+
}
|
|
79
|
+
} finally {
|
|
80
|
+
flight.running = false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Run `run` against a freshly advanced repository view. The view is only valid
|
|
85
|
+
* inside `run`; the response must not hold it. */
|
|
86
|
+
export function withOperatorView<T>(
|
|
87
|
+
store: BuildStore,
|
|
88
|
+
repo: string,
|
|
89
|
+
need: OperatorViewNeed,
|
|
90
|
+
run: (view: RepoViewStore) => Promise<T>,
|
|
91
|
+
): Promise<T> {
|
|
92
|
+
const flight = flightFor(store, repo)
|
|
93
|
+
const result = new Promise<T>((resolve, reject) => {
|
|
94
|
+
flight.queue.push({ need, run, resolve: resolve as (value: unknown) => void, reject })
|
|
95
|
+
})
|
|
96
|
+
if (!flight.running) void drain(store, repo, flight)
|
|
97
|
+
return result
|
|
98
|
+
}
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* carried as base64 strings at the entry level (`builds.artifact`'s
|
|
16
16
|
* `contentBase64` is the base64 of the exact artifact bytes), so a binding's
|
|
17
17
|
* `JSON.stringify` can never silently corrupt bytes. The contract suite
|
|
18
|
-
* (registry.contract.test.ts) asserts the
|
|
18
|
+
* (packages/hosted-store-service/src/registry.contract.test.ts) asserts the
|
|
19
|
+
* round-trip for every tool.
|
|
19
20
|
*
|
|
20
21
|
* Risk classes live in `operator/annotations.ts` (checked in, enforced by the
|
|
21
22
|
* contract suite). The registry itself enforces only what every binding needs
|
|
@@ -428,6 +429,25 @@ const sandboxWriteFileInput = z.strictObject({
|
|
|
428
429
|
|
|
429
430
|
const sandboxResetInput = z.strictObject({ repo: repoField })
|
|
430
431
|
|
|
432
|
+
const sandboxPublishInput = z.strictObject({
|
|
433
|
+
repo: repoField,
|
|
434
|
+
title: z.string().min(1).max(200).describe('Pull-request title, 1-200 characters.'),
|
|
435
|
+
body: z
|
|
436
|
+
.string()
|
|
437
|
+
.max(65_536)
|
|
438
|
+
.optional()
|
|
439
|
+
.describe(
|
|
440
|
+
'Optional pull-request body, at most 65,536 characters; an attribution block naming the operator and session is appended automatically.',
|
|
441
|
+
),
|
|
442
|
+
commit: z
|
|
443
|
+
.string()
|
|
444
|
+
.min(1)
|
|
445
|
+
.optional()
|
|
446
|
+
.describe(
|
|
447
|
+
'Optional commit-ish to publish instead of the checkout HEAD; must resolve to a commit that is a descendant of the base head recorded at provision or reset time.',
|
|
448
|
+
),
|
|
449
|
+
})
|
|
450
|
+
|
|
431
451
|
// ── The closed version-one table ─────────────────────────────────────────────
|
|
432
452
|
|
|
433
453
|
function defineTool(
|
|
@@ -455,7 +475,7 @@ function defineTool(
|
|
|
455
475
|
|
|
456
476
|
/** The closed version-one tool table. Adding a tool means adding an entry
|
|
457
477
|
* here and a row in `operator/annotations.ts`; the contract suite proves the
|
|
458
|
-
* two stay in lockstep.
|
|
478
|
+
* two stay in lockstep. */
|
|
459
479
|
export const TOOLS: readonly ToolEntry[] = [
|
|
460
480
|
defineTool(
|
|
461
481
|
'builds.list',
|
|
@@ -893,6 +913,22 @@ export const TOOLS: readonly ToolEntry[] = [
|
|
|
893
913
|
return { ok: true as const }
|
|
894
914
|
},
|
|
895
915
|
),
|
|
916
|
+
defineTool(
|
|
917
|
+
'sandbox.publish',
|
|
918
|
+
"Publish your operator sandbox's work as a pull request: push the checkout's current head commit (or an explicit commit) to your operator's dedicated branch and open or update a PR against the base branch. Refuses when the checkout has uncommitted changes (tracked or untracked; the sandbox's own provisioning marker is excluded), when the commit is not a descendant of the base head recorded at provision or reset time, or when the base branch head itself would be published. The sandbox itself never pushes and holds no credential — the push and PR creation are kernel plumbing; a publication is a durable repository-journal fact. PR only: the base branch is never pushed to.",
|
|
919
|
+
sandboxPublishInput,
|
|
920
|
+
'{branch, sha, pr: {number, url, headSha}}: the publication branch, the published commit, and the opened or updated pull request.',
|
|
921
|
+
async (raw, ctx) => {
|
|
922
|
+
const input = raw as z.infer<typeof sandboxPublishInput>
|
|
923
|
+
return sandboxOf(ctx).publish(attributed(ctx), {
|
|
924
|
+
repo: input.repo,
|
|
925
|
+
title: input.title,
|
|
926
|
+
...(input.body !== undefined ? { body: input.body } : {}),
|
|
927
|
+
...(input.commit !== undefined ? { commit: input.commit } : {}),
|
|
928
|
+
...(ctx.via !== undefined ? { via: ctx.via } : {}),
|
|
929
|
+
})
|
|
930
|
+
},
|
|
931
|
+
),
|
|
896
932
|
]
|
|
897
933
|
|
|
898
934
|
// ── The registry constructor ─────────────────────────────────────────────────
|
|
@@ -900,8 +936,9 @@ export const TOOLS: readonly ToolEntry[] = [
|
|
|
900
936
|
export interface RegistryOptions {
|
|
901
937
|
store: BuildStore
|
|
902
938
|
tickets?: OperatorTicketBackend
|
|
903
|
-
/** The operator-sandbox backend; when absent the
|
|
904
|
-
* neither advertised nor callable (the compatibility case).
|
|
939
|
+
/** The operator-sandbox backend; when absent the sandbox tools are
|
|
940
|
+
* neither advertised nor callable (the compatibility case). When present
|
|
941
|
+
* but `canPublish` is false, only `sandbox.publish` is filtered. */
|
|
905
942
|
sandbox?: OperatorSandboxService
|
|
906
943
|
clock?: Clock
|
|
907
944
|
/** Constrain every call to this repository identity (the `ab mcp --repo` flag). */
|
|
@@ -917,7 +954,8 @@ export interface OperatorToolRegistry {
|
|
|
917
954
|
}
|
|
918
955
|
|
|
919
956
|
/** Map a service error to the registry's operator-API-shaped failure body —
|
|
920
|
-
* the same kind mapping as the catch block in
|
|
957
|
+
* the same kind mapping as the catch block in the hosted package's
|
|
958
|
+
* operator-server.ts. */
|
|
921
959
|
function mapDomainError(error: unknown): RegistryError {
|
|
922
960
|
if (error instanceof RegistryError) return error
|
|
923
961
|
if (error instanceof RouteRefusalError) {
|
|
@@ -976,15 +1014,22 @@ function mapDomainError(error: unknown): RegistryError {
|
|
|
976
1014
|
/** Bind the closed table to an opened store (and optional ticket and sandbox
|
|
977
1015
|
* backends). The returned registry is the in-process binding; every other
|
|
978
1016
|
* binding is generated from `entries`. Advertisement and dispatch share ONE
|
|
979
|
-
* table:
|
|
980
|
-
* `entries` AND from `call()` dispatch, so a binding that cannot provision
|
|
981
|
-
* sandboxes never advertises or serves them. */
|
|
1017
|
+
* table: a filtered tool is neither advertised nor served. */
|
|
982
1018
|
export function buildRegistry(options: RegistryOptions): OperatorToolRegistry {
|
|
983
1019
|
const sandboxToolNames = new Set(
|
|
984
1020
|
Object.keys(OPERATOR_TOOL_ANNOTATIONS).filter((name) => name.startsWith('sandbox.')),
|
|
985
1021
|
)
|
|
986
|
-
|
|
987
|
-
|
|
1022
|
+
// Without a sandbox backend every sandbox tool is filtered; with a backend
|
|
1023
|
+
// that cannot publish (no forge or no provider publication capability),
|
|
1024
|
+
// only `sandbox.publish` is filtered. Filtered tools are absent from
|
|
1025
|
+
// `entries` AND from `call()` dispatch.
|
|
1026
|
+
const filtered = new Set<string>()
|
|
1027
|
+
if (options.sandbox === undefined) {
|
|
1028
|
+
for (const name of sandboxToolNames) filtered.add(name)
|
|
1029
|
+
} else if (options.sandbox.canPublish === false) {
|
|
1030
|
+
filtered.add('sandbox.publish')
|
|
1031
|
+
}
|
|
1032
|
+
const table = filtered.size === 0 ? TOOLS : TOOLS.filter((tool) => !filtered.has(tool.name))
|
|
988
1033
|
return {
|
|
989
1034
|
entries: table,
|
|
990
1035
|
async call(name, input, partial = {}) {
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* This module is registry-owned on purpose: the operator API routes were
|
|
6
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
|
-
* .test.ts) rather than
|
|
9
|
-
* load-bearing — it must
|
|
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
|
|
10
11
|
* suite's full-body comparisons hold.
|
|
11
12
|
*/
|
|
12
13
|
import { reduceBuild } from '../kernel/reducer'
|
|
@@ -34,7 +35,8 @@ export class RouteRefusalError extends Error {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
/** `requireRouteBuild` (operator
|
|
38
|
+
/** `requireRouteBuild` (the hosted package's operator-server.ts): 404 unless
|
|
39
|
+
* the slug exists and
|
|
38
40
|
* belongs to the named repository. */
|
|
39
41
|
export async function requireRouteBuild(
|
|
40
42
|
store: BuildStore,
|
|
@@ -61,7 +63,7 @@ export function parseArtifactRevision(raw: string): number {
|
|
|
61
63
|
/**
|
|
62
64
|
* The route's control-request prechecks: build existence/repo, then the
|
|
63
65
|
* pending-state checks for pause and cancel-pause. Byte-identical to the
|
|
64
|
-
* `POST …/control` branch in operator
|
|
66
|
+
* `POST …/control` branch in the hosted package's operator-server.ts.
|
|
65
67
|
*/
|
|
66
68
|
export async function controlPrechecks(
|
|
67
69
|
store: BuildStore,
|
|
@@ -97,7 +99,8 @@ export async function controlPrechecks(
|
|
|
97
99
|
|
|
98
100
|
/**
|
|
99
101
|
* The route's answer request → `BuildControlAction` translation, byte-identical
|
|
100
|
-
* to the `POST …/answer` branch in operator
|
|
102
|
+
* to the `POST …/answer` branch in the hosted package's operator-server.ts.
|
|
103
|
+
* The ticket-origin body
|
|
101
104
|
* reader travels separately, exactly as the route passes `readTicketBody`.
|
|
102
105
|
*/
|
|
103
106
|
export function answerAction(request: OperatorAnswerRequest): {
|