@defrex/autobuild 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SPEC.md +138 -16
- package/bin/ab-dispatch-kernel.ts +4 -6
- package/docs/architecture.md +155 -73
- package/docs/configuration.md +41 -9
- package/docs/hosted-service.md +31 -5
- package/docs/operator-api.md +8 -1
- package/docs/remote-store-protocol.md +94 -2
- package/docs/remote-ticket-protocol.md +151 -9
- package/docs/setup.md +33 -7
- package/docs/ticket-migration.md +64 -0
- package/package.json +4 -3
- package/packages/core/src/cli/attention.ts +18 -164
- package/packages/core/src/cli/build-control.ts +23 -16
- package/packages/core/src/cli/bulk-control.ts +3 -2
- package/packages/core/src/cli/connect.ts +141 -38
- package/packages/core/src/cli/context.ts +23 -79
- package/packages/core/src/cli/dashboard/model.ts +36 -22
- package/packages/core/src/cli/dashboard/poll.ts +1 -1
- package/packages/core/src/cli/dispatch-frontend.ts +24 -67
- package/packages/core/src/cli/dispatch-process.ts +14 -7
- package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
- package/packages/core/src/cli/dispatch.ts +304 -82
- package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
- package/packages/core/src/cli/harvest.ts +84 -42
- package/packages/core/src/cli/help.ts +14 -8
- package/packages/core/src/cli/historical-build-state.ts +27 -0
- package/packages/core/src/cli/installation.ts +67 -13
- package/packages/core/src/cli/main.ts +23 -5
- package/packages/core/src/cli/mcp.ts +2 -1
- package/packages/core/src/cli/pr-attachments.ts +10 -13
- package/packages/core/src/cli/pr-summary-publication.ts +27 -60
- package/packages/core/src/cli/pr-summary.ts +43 -13
- package/packages/core/src/cli/publication-settlement.ts +10 -20
- package/packages/core/src/cli/repository-status.ts +38 -2
- package/packages/core/src/cli/self-update.ts +5 -1
- package/packages/core/src/cli/status.ts +62 -32
- package/packages/core/src/cli/terminals.ts +25 -142
- package/packages/core/src/cli/ticket-assets.ts +7 -2
- package/packages/core/src/cli/ticket-editor.ts +82 -0
- package/packages/core/src/cli/ticket.ts +166 -36
- package/packages/core/src/cli/wait.ts +91 -84
- package/packages/core/src/cli/watch.ts +60 -97
- package/packages/core/src/config/pipeline-source.ts +4 -3
- package/packages/core/src/config/schema.ts +35 -3
- package/packages/core/src/connection.ts +3 -3
- package/packages/core/src/events/catalog.ts +3 -2
- package/packages/core/src/events/payloads.ts +6 -0
- package/packages/core/src/events/repository.ts +7 -3
- package/packages/core/src/integration/harness.ts +38 -4
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2444 -9
- package/packages/core/src/kernel/auto-merge.ts +19 -3
- package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
- package/packages/core/src/kernel/generators/build-log.ts +1 -1
- package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
- package/packages/core/src/kernel/harvest.ts +25 -23
- package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
- package/packages/core/src/kernel/log-index-value.ts +38 -0
- package/packages/core/src/kernel/read-facts.ts +637 -0
- package/packages/core/src/kernel/reducer-registry.ts +139 -3
- package/packages/core/src/kernel/repository-attention.ts +179 -0
- package/packages/core/src/kernel/service-facts.ts +135 -0
- package/packages/core/src/kernel/terminal-guard.ts +133 -0
- package/packages/core/src/ontology.ts +1 -0
- package/packages/core/src/operator/annotations.ts +12 -0
- package/packages/core/src/operator/control.ts +5 -6
- package/packages/core/src/operator/index.ts +7 -0
- package/packages/core/src/operator/protocol.ts +19 -2
- package/packages/core/src/operator/query.ts +37 -20
- package/packages/core/src/operator/read-view.ts +4 -13
- package/packages/core/src/operator/registry.ts +61 -8
- package/packages/core/src/operator/requests.ts +2 -14
- package/packages/core/src/operator/sandbox.ts +10 -9
- package/packages/core/src/operator/ticket-source-backend.ts +17 -21
- package/packages/core/src/operator/tickets.ts +132 -22
- package/packages/core/src/plugin-sdk/index.ts +19 -0
- package/packages/core/src/plugins/runtimes.ts +5 -1
- package/packages/core/src/ports/runner/session-env.ts +2 -0
- package/packages/core/src/ports/tickets/contract.ts +66 -1
- package/packages/core/src/ports/tickets/create.ts +48 -12
- package/packages/core/src/ports/tickets/fake.ts +30 -1
- package/packages/core/src/ports/tickets/file-assets.ts +159 -0
- package/packages/core/src/ports/tickets/file-codec.ts +90 -0
- package/packages/core/src/ports/tickets/file.ts +114 -120
- package/packages/core/src/ports/tickets/grants.ts +47 -0
- package/packages/core/src/ports/tickets/hosted-index.ts +17 -0
- package/packages/core/src/ports/tickets/identity.ts +20 -0
- package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
- package/packages/core/src/ports/tickets/linear.ts +74 -9
- package/packages/core/src/ports/tickets/migration.ts +336 -0
- package/packages/core/src/ports/tickets/operations.ts +38 -7
- package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +95 -0
- package/packages/core/src/ports/tickets/remote.ts +60 -3
- package/packages/core/src/ports/tickets/requested-id.ts +46 -0
- package/packages/core/src/ports/tickets/revision.ts +62 -0
- package/packages/core/src/ports/tickets/states.ts +61 -0
- package/packages/core/src/ports/types.ts +40 -3
- package/packages/core/src/ports/workspace/build-execution.ts +1 -0
- package/packages/core/src/ports/workspace/create.ts +5 -1
- package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +8 -4
- package/packages/core/src/processes/build-child.ts +6 -6
- package/packages/core/src/processes/build-runner.ts +62 -70
- package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
- package/packages/core/src/processes/dispatcher.ts +223 -205
- package/packages/core/src/processes/execution-no-progress.ts +176 -0
- package/packages/core/src/processes/harvest-child.ts +30 -4
- package/packages/core/src/processes/harvest-execution-state.ts +15 -9
- package/packages/core/src/processes/harvest-runner.ts +58 -74
- package/packages/core/src/processes/harvest.ts +72 -29
- package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
- package/packages/core/src/processes/orchestrator-tick.ts +2 -2
- package/packages/core/src/processes/phase-failure-budget.ts +1 -0
- package/packages/core/src/processes/publication-loss.ts +35 -21
- package/packages/core/src/processes/repo-view.ts +229 -1099
- package/packages/core/src/processes/tick-publication.ts +110 -0
- package/packages/core/src/store/adapter.ts +23 -0
- package/packages/core/src/store/build-scope.ts +17 -0
- package/packages/core/src/store/contract.ts +283 -1
- package/packages/core/src/store/local/schema.ts +1 -0
- package/packages/core/src/store/local/store.ts +180 -55
- package/packages/core/src/store/memory.ts +119 -29
- package/packages/core/src/store/phase-session.ts +17 -0
- package/packages/core/src/store/reduced-reads.ts +239 -0
- package/packages/core/src/store/reducer-value-shapes.json +14056 -0
- package/packages/core/src/store/reducer-value-validation.ts +96 -0
- package/packages/core/src/store/reducer-values.ts +145 -0
- package/packages/core/src/store/remote/client.ts +41 -0
- package/packages/core/src/store/remote/compatibility.ts +2 -0
- package/packages/core/src/store/remote/index.ts +19 -0
- package/packages/core/src/store/remote/protocol.ts +16 -0
- package/packages/core/src/store/remote/version.ts +3 -1
- package/packages/core/src/store/repo-state-events.ts +1 -1
- package/packages/core/src/store/repo-tick.ts +72 -0
- package/packages/core/src/store/session-handle.ts +19 -0
- package/packages/core/src/store/snapshots.ts +10 -0
- package/packages/core/src/store/ticket-asset-snapshots.ts +192 -0
- package/packages/core/src/store/ticket-assets.ts +86 -8
- package/packages/core/src/store/types.ts +40 -3
- package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
- package/packages/core/src/testing/index.ts +8 -0
- package/packages/core/src/testing/reduced-read-budget.ts +462 -0
- package/skills/guide/SKILL.md +97 -12
- package/skills/guide/references/operator-api.md +8 -1
- package/skills/guide/references/remote-store-protocol.md +94 -2
- package/skills/guide/references/remote-ticket-protocol.md +151 -9
- package/skills/guide/references/setup.md +33 -7
- package/skills/guide/references/ticket-migration.md +64 -0
- package/skills/tickets/SKILL.md +71 -0
- package/templates/hosted-service/package.json +2 -2
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { abDispatch } from '../cli/dispatch'
|
|
5
|
+
import { randomUuids } from '../ids'
|
|
6
|
+
import { ScriptedAgentRunner, defaultTurnResult } from '../ports/runner/fake'
|
|
7
|
+
import { Dispatcher } from '../processes/dispatcher'
|
|
8
|
+
import { FakeTicketSource } from '../ports/tickets/fake'
|
|
9
|
+
import { FakeWorkspaceProvider } from '../ports/workspace/fake'
|
|
10
|
+
import { FakeForge } from '../ports/forge/fake'
|
|
11
|
+
import { sequentialIds } from '../ids'
|
|
12
|
+
import { parseConfig } from '../config/load'
|
|
13
|
+
import {
|
|
14
|
+
getOperatorDashboard,
|
|
15
|
+
listOperatorBuilds,
|
|
16
|
+
getOperatorBuild,
|
|
17
|
+
getRepositoryStatus,
|
|
18
|
+
} from '../operator/query'
|
|
19
|
+
import { buildRegistry } from '../operator/registry'
|
|
20
|
+
import { expect } from 'bun:test'
|
|
21
|
+
import type { BuildStore } from '../store/types'
|
|
22
|
+
import { KERNEL, DISPATCHER, humanActor } from '../events/envelope'
|
|
23
|
+
import {
|
|
24
|
+
readBuildFacts,
|
|
25
|
+
readBuildStreamSeed,
|
|
26
|
+
readRepositoryFacts,
|
|
27
|
+
readRepositoryAttention,
|
|
28
|
+
readBuildDigests,
|
|
29
|
+
readDispatcherRunFacts,
|
|
30
|
+
readOpenTick,
|
|
31
|
+
} from '../store/reduced-reads'
|
|
32
|
+
import { RepoViewStore } from '../processes/repo-view'
|
|
33
|
+
import { abWatch, decodeCursor } from '../cli/watch'
|
|
34
|
+
import { abWait } from '../cli/wait'
|
|
35
|
+
import type { Exec } from '../ports/workspace/git-worktree'
|
|
36
|
+
|
|
37
|
+
export interface ReducedReadCosts {
|
|
38
|
+
events: number
|
|
39
|
+
metadata: number
|
|
40
|
+
bytes: number
|
|
41
|
+
historicalEvents: number
|
|
42
|
+
history?: { repo: string; through: number }
|
|
43
|
+
}
|
|
44
|
+
const repo = '/reduced-read-budget'
|
|
45
|
+
const slug = 'reduced-budget'
|
|
46
|
+
const exec: Exec = async (args) =>
|
|
47
|
+
args[1] === 'remote'
|
|
48
|
+
? { stdout: '', stderr: 'no origin', exitCode: 2 }
|
|
49
|
+
: { stdout: `${repo}/.git\n${repo}/.git\n${repo}\n`, stderr: '', exitCode: 0 }
|
|
50
|
+
|
|
51
|
+
/** Shared actual-consumer scenario. Costs must instrument SQL statement results,
|
|
52
|
+
* including private reducer storage, rather than just public getEvents calls. */
|
|
53
|
+
export async function reducedReadBudget(opts: {
|
|
54
|
+
store: BuildStore
|
|
55
|
+
costs: ReducedReadCosts
|
|
56
|
+
fresh: () => Promise<BuildStore>
|
|
57
|
+
storeRef?: string
|
|
58
|
+
}) {
|
|
59
|
+
const { store, costs } = opts
|
|
60
|
+
await store.ensureRepo(repo)
|
|
61
|
+
const config = parseConfig(
|
|
62
|
+
'[tickets]\nsource="file"\nreadyState="ready"\n[verify]\nsteps=[]\n[finalize]\nsteps=[]',
|
|
63
|
+
)
|
|
64
|
+
const { verify: _verify, finalize: _finalize, ...root } = config
|
|
65
|
+
const configArtifact = await store.putRepoArtifact(repo, {
|
|
66
|
+
kind: 'dispatcher-effective-config',
|
|
67
|
+
content: JSON.stringify({ ...root, verify: { steps: [] }, finalize: { steps: [] } }),
|
|
68
|
+
})
|
|
69
|
+
await store.appendRepo(repo, {
|
|
70
|
+
actor: DISPATCHER,
|
|
71
|
+
type: 'dispatcher.run-started',
|
|
72
|
+
payload: {
|
|
73
|
+
run: 'budget-run',
|
|
74
|
+
pid: 1,
|
|
75
|
+
roleWarnings: [],
|
|
76
|
+
effectiveConfig: { kind: configArtifact.kind, rev: configArtifact.revision },
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
await store.createBuild({ slug, repo })
|
|
80
|
+
await store.append(slug, {
|
|
81
|
+
actor: KERNEL,
|
|
82
|
+
type: 'runner.attached',
|
|
83
|
+
payload: { instance: 'budget', host: 'host' },
|
|
84
|
+
})
|
|
85
|
+
await store.append(slug, {
|
|
86
|
+
actor: KERNEL,
|
|
87
|
+
type: 'finalize.completed',
|
|
88
|
+
payload: { pr: { number: 7, url: 'https://example.test/7', headSha: 'abc' } },
|
|
89
|
+
})
|
|
90
|
+
await store.append(slug, {
|
|
91
|
+
actor: humanActor('human'),
|
|
92
|
+
type: 'build.auto-merge-requested',
|
|
93
|
+
payload: {},
|
|
94
|
+
})
|
|
95
|
+
const reset = () => {
|
|
96
|
+
costs.events = 0
|
|
97
|
+
costs.metadata = 0
|
|
98
|
+
costs.bytes = 0
|
|
99
|
+
}
|
|
100
|
+
let repositoryBytes: number | undefined
|
|
101
|
+
for (const length of [32, 256]) {
|
|
102
|
+
for (let i = 0; i < length; i++) {
|
|
103
|
+
await store.append(slug, {
|
|
104
|
+
actor: humanActor('human'),
|
|
105
|
+
type: 'build.auto-merge-requested',
|
|
106
|
+
payload: {},
|
|
107
|
+
})
|
|
108
|
+
await store.appendRepo(repo, {
|
|
109
|
+
actor: humanActor('human'),
|
|
110
|
+
type: 'dispatcher.intake-set',
|
|
111
|
+
payload: { enabled: true },
|
|
112
|
+
})
|
|
113
|
+
const run = `old-${length}-${String(i).padStart(4, '0')}`
|
|
114
|
+
await store.appendRepo(repo, {
|
|
115
|
+
actor: DISPATCHER,
|
|
116
|
+
type: 'dispatcher.run-started',
|
|
117
|
+
payload: {
|
|
118
|
+
run,
|
|
119
|
+
pid: 1,
|
|
120
|
+
roleWarnings: [],
|
|
121
|
+
effectiveConfig: { kind: configArtifact.kind, rev: configArtifact.revision },
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
await store.appendRepo(repo, {
|
|
125
|
+
actor: DISPATCHER,
|
|
126
|
+
type: 'dispatcher.run-stopped',
|
|
127
|
+
payload: { run, outcome: 'normal' },
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
await store.appendRepo(repo, {
|
|
131
|
+
actor: DISPATCHER,
|
|
132
|
+
type: 'dispatcher.run-started',
|
|
133
|
+
payload: {
|
|
134
|
+
run: 'budget-run',
|
|
135
|
+
pid: 1,
|
|
136
|
+
roleWarnings: [],
|
|
137
|
+
effectiveConfig: { kind: configArtifact.kind, rev: configArtifact.revision },
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
const factsBytes = Buffer.byteLength(
|
|
141
|
+
JSON.stringify((await readRepositoryFacts(store, repo)).value),
|
|
142
|
+
)
|
|
143
|
+
if (repositoryBytes !== undefined) expect(factsBytes).toBeLessThan(repositoryBytes + 512)
|
|
144
|
+
repositoryBytes = factsBytes
|
|
145
|
+
// Warm persisted values independently of any consumer instance.
|
|
146
|
+
await readBuildStreamSeed(store, slug)
|
|
147
|
+
await readBuildFacts(store, slug)
|
|
148
|
+
await readRepositoryFacts(store, repo)
|
|
149
|
+
await readBuildDigests(store, repo)
|
|
150
|
+
await readRepositoryAttention(store, repo)
|
|
151
|
+
const handle = await opts.fresh()
|
|
152
|
+
const cold = new Proxy(handle, {
|
|
153
|
+
get(target, key) {
|
|
154
|
+
if (key === 'close') return async () => {}
|
|
155
|
+
const value = Reflect.get(target, key, target)
|
|
156
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
await coldDispatchOnceBudget(store, cold, length, costs)
|
|
160
|
+
reset()
|
|
161
|
+
const view = new RepoViewStore(cold, { repo })
|
|
162
|
+
await view.refresh()
|
|
163
|
+
expect((await view.buildFacts(slug)).state.pr?.number).toBe(7)
|
|
164
|
+
expect((await view.buildState(slug)).status).toBe('running')
|
|
165
|
+
expect(costs.events).toBe(0)
|
|
166
|
+
expect(costs.metadata).toBeLessThan(100)
|
|
167
|
+
|
|
168
|
+
const dispatcher = () =>
|
|
169
|
+
new Dispatcher({
|
|
170
|
+
store: cold,
|
|
171
|
+
repo,
|
|
172
|
+
config,
|
|
173
|
+
tickets: new FakeTicketSource([]),
|
|
174
|
+
workspaces: new FakeWorkspaceProvider({ root: '/budget-workspaces', mode: 'logical' }),
|
|
175
|
+
forge: new FakeForge(),
|
|
176
|
+
ids: sequentialIds(),
|
|
177
|
+
clock: () => new Date(1000),
|
|
178
|
+
exec,
|
|
179
|
+
launchRunner: async () => 'scheduled',
|
|
180
|
+
})
|
|
181
|
+
await dispatcher().tick({ acceptNewWork: false })
|
|
182
|
+
await getOperatorDashboard({ store: cold, repo, clock: () => new Date(1000) })
|
|
183
|
+
reset()
|
|
184
|
+
await dispatcher().tick({ acceptNewWork: false })
|
|
185
|
+
await getOperatorDashboard({ store: cold, repo, clock: () => new Date(1000) })
|
|
186
|
+
await listOperatorBuilds({ store: cold, repo, scope: 'all', now: new Date(1000) })
|
|
187
|
+
await getOperatorBuild({ store: cold, repo, slug, now: new Date(1000) })
|
|
188
|
+
await getRepositoryStatus(cold, repo, new Date(1000))
|
|
189
|
+
await buildRegistry({ store: cold, clock: () => new Date(1000) }).call('builds.get', {
|
|
190
|
+
repo,
|
|
191
|
+
slug,
|
|
192
|
+
})
|
|
193
|
+
expect(costs.events).toBe(0)
|
|
194
|
+
expect(costs.metadata).toBeLessThan(400)
|
|
195
|
+
expect(costs.bytes).toBeLessThan(1_000_000)
|
|
196
|
+
|
|
197
|
+
// A fresh watch must suppress historical PR eligibility despite armed
|
|
198
|
+
// consent at registration, including its first event-bearing poll.
|
|
199
|
+
reset()
|
|
200
|
+
let ticks = 0
|
|
201
|
+
let time = 0
|
|
202
|
+
const out: string[] = []
|
|
203
|
+
await abWatch({
|
|
204
|
+
targetRepo: repo,
|
|
205
|
+
env: {},
|
|
206
|
+
exec,
|
|
207
|
+
storeRef: opts.storeRef ?? '/budget',
|
|
208
|
+
openStore: () => cold,
|
|
209
|
+
slugs: [slug],
|
|
210
|
+
json: true,
|
|
211
|
+
timeout: '2',
|
|
212
|
+
now: () => new Date(time),
|
|
213
|
+
stdout: (line) => out.push(line),
|
|
214
|
+
stderr: () => {},
|
|
215
|
+
delay: async () => {
|
|
216
|
+
time += 1000
|
|
217
|
+
if (++ticks === 1)
|
|
218
|
+
await store.append(slug, {
|
|
219
|
+
actor: humanActor('human'),
|
|
220
|
+
type: 'build.auto-merge-cancelled',
|
|
221
|
+
payload: {},
|
|
222
|
+
})
|
|
223
|
+
},
|
|
224
|
+
})
|
|
225
|
+
expect(out).toHaveLength(1)
|
|
226
|
+
const cursor = JSON.parse(out[0]!).cursor
|
|
227
|
+
expect(
|
|
228
|
+
decodeCursor(cursor, { store: opts.storeRef ?? '/budget', repo }).deliveredAttention,
|
|
229
|
+
).toEqual([JSON.stringify([slug, 7])])
|
|
230
|
+
expect(costs.events).toBeLessThanOrEqual(4)
|
|
231
|
+
expect(costs.metadata).toBeLessThan(100)
|
|
232
|
+
|
|
233
|
+
await store.append(slug, {
|
|
234
|
+
actor: humanActor('human'),
|
|
235
|
+
type: 'build.auto-merge-requested',
|
|
236
|
+
payload: {},
|
|
237
|
+
})
|
|
238
|
+
await readBuildStreamSeed(store, slug)
|
|
239
|
+
reset()
|
|
240
|
+
let discoveryTick = 0
|
|
241
|
+
time = 0
|
|
242
|
+
const discovered = new Proxy(cold, {
|
|
243
|
+
get(target, key) {
|
|
244
|
+
if (key === 'listBuilds')
|
|
245
|
+
return async () => (discoveryTick === 0 ? [] : target.listBuilds())
|
|
246
|
+
const value = Reflect.get(target, key, target)
|
|
247
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
const discoveryOut: string[] = []
|
|
251
|
+
await abWatch({
|
|
252
|
+
targetRepo: repo,
|
|
253
|
+
env: {},
|
|
254
|
+
exec,
|
|
255
|
+
storeRef: opts.storeRef ?? '/budget',
|
|
256
|
+
openStore: () => discovered,
|
|
257
|
+
json: true,
|
|
258
|
+
timeout: '3',
|
|
259
|
+
now: () => new Date(time),
|
|
260
|
+
stdout: (line) => discoveryOut.push(line),
|
|
261
|
+
stderr: () => {},
|
|
262
|
+
delay: async () => {
|
|
263
|
+
time += 1000
|
|
264
|
+
if (++discoveryTick === 2)
|
|
265
|
+
await store.append(slug, {
|
|
266
|
+
actor: humanActor('human'),
|
|
267
|
+
type: 'build.auto-merge-cancelled',
|
|
268
|
+
payload: {},
|
|
269
|
+
})
|
|
270
|
+
},
|
|
271
|
+
})
|
|
272
|
+
expect(discoveryOut).toHaveLength(1)
|
|
273
|
+
expect(costs.events).toBeLessThanOrEqual(6)
|
|
274
|
+
expect(costs.metadata).toBeLessThan(180)
|
|
275
|
+
|
|
276
|
+
// Wait applies its current-attention policy instead of watch suppression.
|
|
277
|
+
await readBuildStreamSeed(store, slug)
|
|
278
|
+
reset()
|
|
279
|
+
const waitOut: string[] = []
|
|
280
|
+
const result = await abWait({
|
|
281
|
+
targetRepo: repo,
|
|
282
|
+
env: {},
|
|
283
|
+
exec,
|
|
284
|
+
storeRef: opts.storeRef ?? '/budget',
|
|
285
|
+
openStore: () => cold,
|
|
286
|
+
slugs: [slug],
|
|
287
|
+
forValues: ['attention'],
|
|
288
|
+
json: true,
|
|
289
|
+
now: () => new Date(time),
|
|
290
|
+
stdout: (line) => waitOut.push(line),
|
|
291
|
+
stderr: () => {},
|
|
292
|
+
})
|
|
293
|
+
expect(result).toBe(0)
|
|
294
|
+
expect(JSON.parse(waitOut[0]!).observation.pr.number).toBe(7)
|
|
295
|
+
expect(costs.events).toBe(0)
|
|
296
|
+
expect(costs.metadata).toBeLessThan(40)
|
|
297
|
+
await store.append(slug, {
|
|
298
|
+
actor: humanActor('human'),
|
|
299
|
+
type: 'build.auto-merge-requested',
|
|
300
|
+
payload: {},
|
|
301
|
+
})
|
|
302
|
+
await readBuildStreamSeed(store, slug)
|
|
303
|
+
reset()
|
|
304
|
+
let waitTick = 0
|
|
305
|
+
time = 0
|
|
306
|
+
const polled: string[] = []
|
|
307
|
+
const waited = await abWait({
|
|
308
|
+
targetRepo: repo,
|
|
309
|
+
env: {},
|
|
310
|
+
exec,
|
|
311
|
+
storeRef: opts.storeRef ?? '/budget',
|
|
312
|
+
openStore: () => cold,
|
|
313
|
+
slugs: [slug],
|
|
314
|
+
forValues: ['attention'],
|
|
315
|
+
json: true,
|
|
316
|
+
timeout: '4',
|
|
317
|
+
now: () => new Date(time),
|
|
318
|
+
stdout: (line) => polled.push(line),
|
|
319
|
+
stderr: () => {},
|
|
320
|
+
delay: async () => {
|
|
321
|
+
time += 1000
|
|
322
|
+
// The first pass stays idle; the next delivers the first event batch.
|
|
323
|
+
if (++waitTick === 1) {
|
|
324
|
+
expect(costs.events).toBe(0)
|
|
325
|
+
expect(costs.metadata).toBeLessThan(40)
|
|
326
|
+
}
|
|
327
|
+
if (waitTick === 2) {
|
|
328
|
+
expect(costs.events).toBe(0)
|
|
329
|
+
await store.append(slug, {
|
|
330
|
+
actor: humanActor('human'),
|
|
331
|
+
type: 'build.auto-merge-cancelled',
|
|
332
|
+
payload: {},
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
})
|
|
337
|
+
expect(waited).toBe(0)
|
|
338
|
+
expect(waitTick).toBe(2)
|
|
339
|
+
expect(JSON.parse(polled[0]!).observation.pr.number).toBe(7)
|
|
340
|
+
expect(costs.events).toBeLessThanOrEqual(2)
|
|
341
|
+
expect(costs.metadata).toBeLessThan(50)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Exercise the real outer DispatchLoop with a new invocation identity. Only
|
|
346
|
+
* shared values are warmed: no parameterized run value may hide a cold replay. */
|
|
347
|
+
async function coldDispatchOnceBudget(
|
|
348
|
+
store: BuildStore,
|
|
349
|
+
cold: BuildStore,
|
|
350
|
+
length: number,
|
|
351
|
+
costs: ReducedReadCosts,
|
|
352
|
+
) {
|
|
353
|
+
const onceRepo = 'https://github.com/budget/cold-once'
|
|
354
|
+
await store.ensureRepo(onceRepo)
|
|
355
|
+
for (let i = 0; i < length; i++)
|
|
356
|
+
await store.appendRepo(onceRepo, {
|
|
357
|
+
actor: humanActor('human'),
|
|
358
|
+
type: 'dispatcher.intake-set',
|
|
359
|
+
payload: { enabled: false },
|
|
360
|
+
})
|
|
361
|
+
const boundary = (await readRepositoryFacts(store, onceRepo)).cursor
|
|
362
|
+
const directory = await mkdtemp(join(tmpdir(), 'ab-cold-once-'))
|
|
363
|
+
try {
|
|
364
|
+
await writeFile(
|
|
365
|
+
join(directory, 'autobuild.toml'),
|
|
366
|
+
`[roles.default]
|
|
367
|
+
runtime = "claude"
|
|
368
|
+
[tickets]
|
|
369
|
+
source = "file"
|
|
370
|
+
readyState = "ready"
|
|
371
|
+
[verify]
|
|
372
|
+
steps = []
|
|
373
|
+
[finalize]
|
|
374
|
+
steps = []
|
|
375
|
+
`,
|
|
376
|
+
)
|
|
377
|
+
const checked = new Proxy(cold, {
|
|
378
|
+
get(target, key) {
|
|
379
|
+
if (key === 'getReducerValue')
|
|
380
|
+
return (...args: Parameters<BuildStore['getReducerValue']>) => {
|
|
381
|
+
expect(['dispatcherRunFacts', 'openTick']).not.toContain(args[1].reducer)
|
|
382
|
+
return target.getReducerValue(...args)
|
|
383
|
+
}
|
|
384
|
+
const value = Reflect.get(target, key, target)
|
|
385
|
+
return typeof value === 'function' ? value.bind(target) : value
|
|
386
|
+
},
|
|
387
|
+
})
|
|
388
|
+
costs.events = 0
|
|
389
|
+
costs.metadata = 0
|
|
390
|
+
costs.bytes = 0
|
|
391
|
+
costs.historicalEvents = 0
|
|
392
|
+
costs.history = { repo: onceRepo, through: boundary }
|
|
393
|
+
const run = `never-read-once-${length}`
|
|
394
|
+
await abDispatch({
|
|
395
|
+
targetRepo: directory,
|
|
396
|
+
repo: onceRepo,
|
|
397
|
+
env: {},
|
|
398
|
+
exec: async (args) =>
|
|
399
|
+
args[1] === 'remote'
|
|
400
|
+
? { stdout: `${onceRepo}.git\n`, stderr: '', exitCode: 0 }
|
|
401
|
+
: {
|
|
402
|
+
stdout: `${directory}/.git\n${directory}/.git\n${directory}\n`,
|
|
403
|
+
stderr: '',
|
|
404
|
+
exitCode: 0,
|
|
405
|
+
},
|
|
406
|
+
stdout: () => {},
|
|
407
|
+
stderr: () => {},
|
|
408
|
+
once: true,
|
|
409
|
+
plain: true,
|
|
410
|
+
kernelRunId: run,
|
|
411
|
+
storeRef: '/budget',
|
|
412
|
+
wire: () => ({
|
|
413
|
+
store: checked,
|
|
414
|
+
storeRef: '/budget',
|
|
415
|
+
tickets: new FakeTicketSource([]),
|
|
416
|
+
forge: new FakeForge(),
|
|
417
|
+
workspaces: new FakeWorkspaceProvider({ root: '/once-workspaces', mode: 'logical' }),
|
|
418
|
+
buildExecution: {
|
|
419
|
+
start: () => {
|
|
420
|
+
throw new Error('no builds in once budget')
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
runtimes: {
|
|
424
|
+
claude: {
|
|
425
|
+
runner: new ScriptedAgentRunner({ script: () => defaultTurnResult() }),
|
|
426
|
+
servesModels: [],
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
ids: sequentialIds(),
|
|
430
|
+
uuids: randomUuids(),
|
|
431
|
+
clock: () => new Date(1000),
|
|
432
|
+
}),
|
|
433
|
+
})
|
|
434
|
+
const finished = (await readRepositoryFacts(checked, onceRepo)).value.recentRuns.find(
|
|
435
|
+
([id]) => id === run,
|
|
436
|
+
)?.[1]
|
|
437
|
+
expect(finished?.completed?.type).toBe('dispatcher.tick-completed')
|
|
438
|
+
expect(finished?.stop?.type).toBe('dispatcher.run-stopped')
|
|
439
|
+
// The same fresh invocation may fail before writing its start fact.
|
|
440
|
+
expect(
|
|
441
|
+
(await readDispatcherRunFacts(checked, onceRepo, `never-started-${length}`, false)).value,
|
|
442
|
+
).toEqual({})
|
|
443
|
+
expect(await readOpenTick(checked, onceRepo, `never-started-${length}`, false)).toBe(false)
|
|
444
|
+
expect(costs.historicalEvents).toBe(0)
|
|
445
|
+
expect(costs.events).toBeLessThanOrEqual(32)
|
|
446
|
+
expect(costs.metadata).toBeLessThan(300)
|
|
447
|
+
expect(costs.bytes).toBeLessThan(1_000_000)
|
|
448
|
+
} finally {
|
|
449
|
+
delete costs.history
|
|
450
|
+
await rm(directory, { recursive: true, force: true })
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** SQL result instrumentation handles both raw object rows and Drizzle values. */
|
|
455
|
+
export function countHistoricalRows(costs: ReducedReadCosts, rows: unknown): number {
|
|
456
|
+
if (!costs.history || !Array.isArray(rows)) return 0
|
|
457
|
+
return rows.filter((row) => {
|
|
458
|
+
const repo = Array.isArray(row) ? row[0] : row?.repo
|
|
459
|
+
const seq = Number(Array.isArray(row) ? row[1] : row?.seq)
|
|
460
|
+
return repo === costs.history!.repo && seq <= costs.history!.through
|
|
461
|
+
}).length
|
|
462
|
+
}
|
package/skills/guide/SKILL.md
CHANGED
|
@@ -197,7 +197,9 @@ The distinctions that change an administrator's answer:
|
|
|
197
197
|
agent turn or command already running. Missing, unreadable, malformed, and
|
|
198
198
|
routing-invalid candidates retain that snapshot with deduplicated operator
|
|
199
199
|
notice, and restoring a valid file resumes reload. Scoped phase CLI processes
|
|
200
|
-
still read the build worktree's branch-owned file.
|
|
200
|
+
still read the build worktree's branch-owned file. Dispatcher merge readiness,
|
|
201
|
+
resume, and execution recovery use the runner's pinned commands, verify, and
|
|
202
|
+
finalize configuration; builds without a pin retain the live fallback.
|
|
201
203
|
- **Workspaces** are provisioned per build and remain the locally reachable
|
|
202
204
|
execution context for that build.
|
|
203
205
|
- **Ticket providers** (`linear`, `file`) sit behind one port; the dispatcher
|
|
@@ -215,6 +217,22 @@ The distinctions that change an administrator's answer:
|
|
|
215
217
|
guidance and later dispatcher ticks retry. Without consent it remains open for
|
|
216
218
|
ordinary `git diff` inspection; it has no review UI or image host.
|
|
217
219
|
|
|
220
|
+
For moving a project between sources, follow the [ticket migration procedure](references/ticket-migration.md).
|
|
221
|
+
|
|
222
|
+
## Independent hosted tickets
|
|
223
|
+
|
|
224
|
+
Hosted tickets support local builds. Configure `[tickets] source = "hosted"`,
|
|
225
|
+
`teamKey`, and `url`, or run `ab connect --tickets URL` to register the project,
|
|
226
|
+
save its credential and publish ticket policy without changing the build store.
|
|
227
|
+
Without `tickets.url`, tickets use the selected remote build service; local builds
|
|
228
|
+
require an explicit ticket URL. No dispatcher is needed for ticket commands.
|
|
229
|
+
Hosted defaults are `triage`, `ready`, `doing`, `done`; create/proposal use triage.
|
|
230
|
+
Other sources still require readyState. Explicit state names retain their spelling.
|
|
231
|
+
Legacy clients without independent policy retain uploaded configuration defaults.
|
|
232
|
+
Harvest ticket grants are service-bound read/create authority; child phases are
|
|
233
|
+
read-only and cannot publish policy. See [setup](references/setup.md) and the
|
|
234
|
+
[ticket protocol](references/remote-ticket-protocol.md).
|
|
235
|
+
|
|
218
236
|
## `autobuild.toml` reference
|
|
219
237
|
|
|
220
238
|
One declarative file at the repo root. **Declarative, not executable** —
|
|
@@ -737,6 +755,21 @@ exhaustion dispatch parks durably; bare `ab answer <slug>` and text guidance
|
|
|
737
755
|
both grant a fresh budget, while abort terminates without retrying. Workspace
|
|
738
756
|
failures owned by provider infrastructure recovery use only its existing budget.
|
|
739
757
|
|
|
758
|
+
The dispatcher also applies a fixed, non-configurable limit of three consecutive
|
|
759
|
+
executions that complete normally with exit code zero and no durable build
|
|
760
|
+
progress. Execution start/end, runner attachment and workspace lifecycle
|
|
761
|
+
bookkeeping do not count as progress. Pipeline, session, spec, publication/PR,
|
|
762
|
+
observation and acknowledged operator facts reset the streak; infrastructure,
|
|
763
|
+
budget and provider failures do not consume the allowance and retain their own
|
|
764
|
+
retry limits. Before a fourth actionable launch, the dispatcher raises one
|
|
765
|
+
setup-targeted `execution-no-progress` policy escalation naming the count and
|
|
766
|
+
the next action it expected the runner to take. Waiting, paused and terminal
|
|
767
|
+
builds are left parked, and pending operator commands remain executable.
|
|
768
|
+
Answering that escalation with guidance or a bare `ab answer <slug>` retry grants
|
|
769
|
+
three fresh executions. The guard is derived from durable facts, including on
|
|
770
|
+
supervisor restart, and uses the ordinary watch, dashboard and operator API
|
|
771
|
+
attention paths.
|
|
772
|
+
|
|
740
773
|
For each repeat conflict, Autobuild compares the base merged by the latest
|
|
741
774
|
completed reconcile with a fresh authoritative base snapshot. An advanced base
|
|
742
775
|
proves the attempt lost a race and permits another reconcile regardless of the
|
|
@@ -788,13 +821,14 @@ state eligible.
|
|
|
788
821
|
|---|---|---|---|
|
|
789
822
|
| `source` | — | **required**, nonblank builtin or plugin registration name | Which provider backs all ticket operations. Unknown names report every available builtin and plugin source. |
|
|
790
823
|
| `readyLabels` | — (source-aware) | optional; array of nonempty strings | A ticket must carry **every** listed label to be dispatchable. `[]` = **no label gate**. Absent uses the source default below. |
|
|
791
|
-
| `
|
|
824
|
+
| `url` | selected build service | HTTPS URL (loopback HTTP allowed), no userinfo/query/fragment; hosted only | Ticket service independent of builds; required for local builds. |
|
|
825
|
+
| `readyState` | hosted: `ready` | non-blank string; required for other sources | The one workflow state a ticket must sit in to be dispatchable. Linear matches exactly and case-sensitively; file matches it case-insensitively to a state directory (`ready` → `ready/`). There is no any-state mode. |
|
|
792
826
|
| `readyAssignee` | — | optional; `"me"` or an email; `linear` only | Also require the ticket to be assigned to that Linear user (`me` = owner of `LINEAR_API_KEY`); unassigned tickets are never ready. |
|
|
793
|
-
| `teamKey` | — | required by `linear` and `hosted`, forbidden by `file`, allowed for plugins | The Linear team key (e.g. `"ENG"`) or
|
|
827
|
+
| `teamKey` | — | required by `linear` and `hosted`, forbidden by `file`, allowed for plugins | The Linear team key (e.g. `"ENG"`), the captured prefix for new hosted ticket ids (`ENG-1`), or a plugin configuration field. Hosted numbers are project-local and continue across prefix changes; legacy ids remain aliases and readable inputs are case-insensitive. |
|
|
794
828
|
| `claimedState` | — | optional nonempty string; forbidden by `file`, allowed for plugins | Workflow state `claim()` moves a ticket to when a build starts. |
|
|
795
|
-
| `doneState` | hosted: `"
|
|
829
|
+
| `doneState` | hosted: `"done"` | optional nonblank string | Project completion state and merged-ticket transition; hosted tickets only. |
|
|
796
830
|
| `createState` | — | optional, nonempty string | Default state for new tickets when a create does not name one. Absent = the provider's default (Linear: the team's default, e.g. Backlog; file: Triage). |
|
|
797
|
-
| `triageState` | — | optional, nonempty string | State the dispatcher hands tickets back to for human triage — spec-gate bounces, aborted builds, closed-unmerged PRs. Absent = Linear
|
|
831
|
+
| `triageState` | — | optional, nonempty string | State the dispatcher hands tickets back to for human triage — spec-gate bounces, aborted builds, closed-unmerged PRs. Absent = Linear: Backlog; hosted: triage; file/plugin: Triage. Must name a state the tracker actually has — a Linear team only has "Triage" when its triage feature is enabled. |
|
|
798
832
|
| `proposalState` | — | optional, nonempty string | State observation harvest files approved proposals into. Absent = the resolved `triageState`, which keeps the human grooming gate. Setting it to `readyState` waives that gate: harvested proposals dispatch without being read. Keep it distinct from `triageState` — see below. |
|
|
799
833
|
| `dir` | file: `.autobuild/tickets`; plugin: — | optional nonempty string; forbidden by `linear`, allowed for plugins | Root holding file state directories, or an existing plugin configuration field. |
|
|
800
834
|
|
|
@@ -1214,16 +1248,19 @@ transitions, so an invalid state fails with the
|
|
|
1214
1248
|
source's known states. A missing id fails nonzero with an error naming both the
|
|
1215
1249
|
id and configured source.
|
|
1216
1250
|
|
|
1217
|
-
Human-readable output is the default and must not be parsed for ids.
|
|
1218
|
-
|
|
1251
|
+
Human-readable output is the default and must not be parsed for ids. The CRUD
|
|
1252
|
+
subcommands accept `--json`, which emits one bare JSON value and no prose: a
|
|
1219
1253
|
`Ticket[]` for `list`, and the complete resulting `Ticket` for `create`,
|
|
1220
1254
|
`update`, `block`, `unblock`, `show`, or `move`.
|
|
1221
1255
|
|
|
1222
1256
|
### Ticket assets
|
|
1223
1257
|
|
|
1224
|
-
A ticket asset is a file or folder attached to a ticket
|
|
1225
|
-
|
|
1226
|
-
|
|
1258
|
+
A ticket asset is a file or folder attached to a ticket: a design export,
|
|
1259
|
+
reference screenshots, or a sample payload. Hosted tickets store assets in their
|
|
1260
|
+
ticket service, independently of the build store; file tickets use native portable
|
|
1261
|
+
assets composed with legacy BuildStore attachments. Sources without native assets
|
|
1262
|
+
use the BuildStore. Existing service assets need no migration. Old local hosted-ticket
|
|
1263
|
+
assets are not moved and are not a fallback. Each has a kind and a name.
|
|
1227
1264
|
|
|
1228
1265
|
- `ab ticket attach <id> <kind> <path> [--name <name>] [--json]` stores `<path>`
|
|
1229
1266
|
(default name: its basename). Attaching the same kind and name again makes a
|
|
@@ -1235,7 +1272,10 @@ a sample payload. Each has a kind and a name.
|
|
|
1235
1272
|
- `ab ticket asset get <id> <kind> <name> <dest> [--rev <n>]` downloads the
|
|
1236
1273
|
exact bytes of the latest (or given) revision; `ab ticket asset rm <id> <kind>
|
|
1237
1274
|
<name>` drops it from the current assets.
|
|
1238
|
-
- **Frozen at claim.** A claim pins the current revisions into the build.
|
|
1275
|
+
- **Frozen at claim.** A claim pins the current revisions into the build.
|
|
1276
|
+
Hosted revisions are copied into immutable snapshots in the build store,
|
|
1277
|
+
preserving revision numbers and allowing reads without the ticket service.
|
|
1278
|
+
Old pins without snapshots keep reading their original ticket rows. A
|
|
1239
1279
|
later attach or remove leaves a running build alone; a human answer with
|
|
1240
1280
|
`--revise-spec-from-ticket` pins again. `ab build asset get <slug> <kind>
|
|
1241
1281
|
<name> <dest> [--rev <n>]` downloads what the build froze, also after it ends.
|
|
@@ -1783,7 +1823,12 @@ cursor from a different repository or store is rejected.
|
|
|
1783
1823
|
Dispatcher liveness is also visible through `ab repository status --json`:
|
|
1784
1824
|
`dispatcher.holder` identifies the unexpired repository lease owner, independently
|
|
1785
1825
|
of `latestRun`. Heartbeat age measures attributable renewals; tick age measures
|
|
1786
|
-
|
|
1826
|
+
actual tick completion. Successful unchanged ticks leave no journal pair; bounded,
|
|
1827
|
+
lease-fenced repository telemetry still records each actual completion and its
|
|
1828
|
+
outcome. Last-tick age refreshes independently of the event cursor and never uses
|
|
1829
|
+
heartbeat as completion evidence. Open telemetry protects unsafe work during
|
|
1830
|
+
shutdown; historical journals retain their boundary fallback. Missing historical
|
|
1831
|
+
evidence is null. The liveness query uses
|
|
1787
1832
|
bounded repository facts first, with a full-history fallback when a later
|
|
1788
1833
|
invocation hides the holder start or the latest stop. Other state reads retain
|
|
1789
1834
|
their bounded semantics.
|
|
@@ -2038,3 +2083,43 @@ build phase, which is the criterion for membership (§16.3): `ab-spec` and
|
|
|
2038
2083
|
`ab-tickets` are human/agent-facing pre-build surfaces, `ab-guide` is
|
|
2039
2084
|
read-only reference including the setup handoff guidance, and `ab-operate` is
|
|
2040
2085
|
the manual for the orchestrator role, which any agent may take on.
|
|
2086
|
+
|
|
2087
|
+
## Revision-checked ticket writes
|
|
2088
|
+
|
|
2089
|
+
Every ticket read includes an opaque `revision`. Pass it as `expectedRevision`
|
|
2090
|
+
(on MCP/operator requests) or `--expected-revision <opaque>` on `ab ticket update`,
|
|
2091
|
+
`move`, `block` and `unblock`. A stale write fails without changing anything and
|
|
2092
|
+
reports the current revision. Omission preserves unguarded behavior. Blocker
|
|
2093
|
+
checks cover the entire batch. Dispatcher moves remain unguarded and change the
|
|
2094
|
+
revision. Sources without guarded writes, including Linear, refuse supplied
|
|
2095
|
+
revisions rather than ignoring them.
|
|
2096
|
+
|
|
2097
|
+
`ab ticket edit <id>` opens `$VISUAL`, falling back to `$EDITOR`, using TOML
|
|
2098
|
+
frontmatter and a Markdown body. Only title, body and labels are editable;
|
|
2099
|
+
id, blockers, creation key and reference comments are read-only. It saves only
|
|
2100
|
+
changed fields with the opening revision. Unchanged or formatting-only edits
|
|
2101
|
+
write nothing. On conflict, invalid input or editor failure, the error names an
|
|
2102
|
+
absolute file path containing the edited text. Read the current ticket and
|
|
2103
|
+
reapply the intended changes before retrying; conflicts are never merged.
|
|
2104
|
+
|
|
2105
|
+
File and Linear revisions fingerprint title, body, canonical label/blocker sets
|
|
2106
|
+
and state; reverting identical content restores its fingerprint. File checks
|
|
2107
|
+
re-read immediately before writing, with the existing no-lock race window.
|
|
2108
|
+
Direct file edits and state-directory moves change revisions. File revisions also
|
|
2109
|
+
cover structured comments and portable attachments. Legacy file comments change
|
|
2110
|
+
the body; structured comments preserve it. Separate hosted comments do not change
|
|
2111
|
+
ticket revisions. Hosted revisions
|
|
2112
|
+
are monotonic decimal bigint strings, and checks run under the same row lock
|
|
2113
|
+
and transaction as all requested changes. Hosted reads return a coherent
|
|
2114
|
+
snapshot of fields, blockers and revision.
|
|
2115
|
+
|
|
2116
|
+
Read before writing, and copy the opaque revision verbatim:
|
|
2117
|
+
|
|
2118
|
+
```sh
|
|
2119
|
+
ab ticket show AUT-8 --json
|
|
2120
|
+
ab ticket update AUT-8 --title 'Revised title' --expected-revision '<revision from show>'
|
|
2121
|
+
ab ticket edit AUT-8
|
|
2122
|
+
```
|
|
2123
|
+
|
|
2124
|
+
Use `expectedRevision` on MCP `tickets.update`, `tickets.move`, `tickets.block`
|
|
2125
|
+
and `tickets.unblock` with the revision from `tickets.get` or `tickets.list`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Operator API v1
|
|
2
2
|
|
|
3
3
|
The hosted service exposes the polling-oriented operator surface at
|
|
4
|
-
`/operator/v1/repos/{repo}`. It is versioned with remote store protocol **
|
|
4
|
+
`/operator/v1/repos/{repo}`. It is versioned with remote store protocol **4**.
|
|
5
5
|
Every request must send `X-Autobuild-Version`,
|
|
6
6
|
`X-Autobuild-Protocol-Version: 3`, and `Authorization: Bearer …`. Identity is
|
|
7
7
|
checked before authentication or resource lookup.
|
|
@@ -87,6 +87,7 @@ turn runner (a later ticket) is the only producer of turn facts.
|
|
|
87
87
|
| `GET …/harvest/status` | Harvest gate, runs, steps, recovery, and attention projection. |
|
|
88
88
|
| `GET …/builds/{slug}/artifacts/{kind}?rev=N` | Raw bytes (`application/octet-stream`) plus content-disposition and `X-Autobuild-Artifact-*` metadata headers. Omit `rev` for latest. |
|
|
89
89
|
| `GET …/tickets?state=S&label=L` | `{states,tickets,diagnostics,criteria,triageState,readyState}`. The two lifecycle names come from effective repository configuration (including provider fallback). With no `state`, `criteria.state` is `triageState`; repeated `label` parameters narrow that state with AND filters. An explicit `state` selects that exact backend workflow state. |
|
|
90
|
+
| `GET …/tickets/states` | Full state metadata `{name,tickets,about?}[]`, including empty states (`tickets.states` in MCP). |
|
|
90
91
|
| `GET …/tickets/{id}` | `{ticket,blockers,build}`. Blockers include native `exists`/`resolved` status; `build` prefers an active matching repository build, otherwise the most recently updated one. |
|
|
91
92
|
|
|
92
93
|
For artifact reads, an absent `rev` selects the latest revision. A supplied value
|
|
@@ -119,6 +120,7 @@ JSON bodies are strict: unknown fields are rejected.
|
|
|
119
120
|
| `POST …/settings/auto-merge-default/toggle` | Empty body. |
|
|
120
121
|
| `POST …/bulk-control` | `{"action":"pause"\|"resume"}`. The hold fact, intake fact, then eligible build events are written in that order. |
|
|
121
122
|
| `POST …/harvest/control` | `{"action":"toggle-gate"}` or `{"action":"run","run":"…"}`. A run action is bound to that concrete projected run. |
|
|
123
|
+
| `POST …/tickets/states` | `{name,about?}` → `{state,created}`; requires attributed operator identity (`tickets.state.create` in MCP). Hosted duplicate names are refused case-insensitively. |
|
|
122
124
|
| `POST …/tickets` | `{"title":"…","body":"…","labels"?:[],"state"?:"…","blockedBy"?:[]}`; omitted state preserves the backend default. |
|
|
123
125
|
| `PATCH …/tickets/{id}` | Any nonempty subset of `title`, `body`, and complete-replacement `labels`. Body bytes are not normalized. |
|
|
124
126
|
| `POST …/tickets/{id}/move` | `{"state":"backend state name"}`. |
|
|
@@ -284,3 +286,8 @@ even with `--since`; live and backlog matches ignore routine outcomes and
|
|
|
284
286
|
self-healing failures. A merge match uses the watch observation with
|
|
285
287
|
`condition: "attention"`. Explicit `--event` globs remain event-only.
|
|
286
288
|
Watch and wait preserve each other's cursor notification metadata.
|
|
289
|
+
|
|
290
|
+
Hosted ticket tools resolve independent project ticket policy before legacy uploaded
|
|
291
|
+
configuration. Queue, detail, state discovery and mutations work before any build
|
|
292
|
+
or dispatcher run; detail reports a null matching build. Ticket policy publication
|
|
293
|
+
never creates synthetic build or repository history.
|