@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
|
@@ -92,6 +92,9 @@ export type {
|
|
|
92
92
|
OperatorTicketQueue,
|
|
93
93
|
} from './tickets'
|
|
94
94
|
export {
|
|
95
|
+
prepareOperatorTicketAssets,
|
|
96
|
+
listOperatorTicketStates,
|
|
97
|
+
createOperatorTicketState,
|
|
95
98
|
getOperatorTicket,
|
|
96
99
|
listOperatorTickets,
|
|
97
100
|
mutateOperatorTicket,
|
|
@@ -132,3 +135,7 @@ export {
|
|
|
132
135
|
findEffectiveConfigRef,
|
|
133
136
|
decodeEffectiveConfig,
|
|
134
137
|
} from './query'
|
|
138
|
+
|
|
139
|
+
export { ticketStateCreateRequestSchema } from './protocol'
|
|
140
|
+
|
|
141
|
+
export { readBuildFacts } from '../store/reduced-reads'
|
|
@@ -55,15 +55,27 @@ export const ticketCreateRequestSchema = z.strictObject({
|
|
|
55
55
|
state: z.string().min(1).optional(),
|
|
56
56
|
blockedBy: z.array(z.string().min(1)).optional(),
|
|
57
57
|
})
|
|
58
|
+
const expectedRevisionSchema = z
|
|
59
|
+
.string()
|
|
60
|
+
.refine((v) => v.trim().length > 0, 'expectedRevision must be nonblank')
|
|
61
|
+
.optional()
|
|
58
62
|
export const ticketUpdateRequestSchema = z
|
|
59
63
|
.strictObject({
|
|
64
|
+
expectedRevision: expectedRevisionSchema,
|
|
60
65
|
title: z.string().optional(),
|
|
61
66
|
body: z.string().optional(),
|
|
62
67
|
labels: z.array(z.string()).optional(),
|
|
63
68
|
})
|
|
64
|
-
.refine(
|
|
65
|
-
|
|
69
|
+
.refine(
|
|
70
|
+
(value) => value.title !== undefined || value.body !== undefined || value.labels !== undefined,
|
|
71
|
+
'update must name at least one field',
|
|
72
|
+
)
|
|
73
|
+
export const ticketMoveRequestSchema = z.strictObject({
|
|
74
|
+
expectedRevision: expectedRevisionSchema,
|
|
75
|
+
state: z.string().min(1),
|
|
76
|
+
})
|
|
66
77
|
export const ticketBlockerRequestSchema = z.strictObject({
|
|
78
|
+
expectedRevision: expectedRevisionSchema,
|
|
67
79
|
blockerIds: z.array(z.string().min(1)).min(1),
|
|
68
80
|
})
|
|
69
81
|
export type OperatorTicketCreateRequest = z.infer<typeof ticketCreateRequestSchema>
|
|
@@ -96,3 +108,8 @@ export const sessionApprovalRequestSchema = z.strictObject({
|
|
|
96
108
|
decision: z.enum(['approve', 'deny']),
|
|
97
109
|
})
|
|
98
110
|
export type OperatorSessionApprovalRequest = z.infer<typeof sessionApprovalRequestSchema>
|
|
111
|
+
|
|
112
|
+
export const ticketStateCreateRequestSchema = z.strictObject({
|
|
113
|
+
name: z.string(),
|
|
114
|
+
about: z.string().optional(),
|
|
115
|
+
})
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { readBuildDigests } from '../store/reduced-reads'
|
|
2
|
+
import {
|
|
3
|
+
readBuildFacts,
|
|
4
|
+
readRepositoryFacts,
|
|
5
|
+
type RepositoryReadFacts,
|
|
6
|
+
} from '../store/reduced-reads'
|
|
1
7
|
import { readDispatcherLiveness } from '../cli/dispatcher-liveness'
|
|
2
8
|
import { composeBuildConfig } from '../config/live'
|
|
3
9
|
import type { PipelineSourceMeta } from '../config/pipeline-source'
|
|
@@ -9,26 +15,25 @@ import {
|
|
|
9
15
|
} from '../processes/build-execution-state'
|
|
10
16
|
import type { Artifact } from '../store/types'
|
|
11
17
|
import {
|
|
12
|
-
|
|
18
|
+
detailFromFacts,
|
|
13
19
|
statusFilter,
|
|
14
20
|
summarizeFrom,
|
|
15
21
|
type BuildDetail,
|
|
16
22
|
type BuildSummary,
|
|
17
23
|
} from '../cli/status'
|
|
18
|
-
import {
|
|
24
|
+
import { repositoryStatusFromFacts, type RepositoryStatus } from '../cli/repository-status'
|
|
19
25
|
import { projectHarvestStatus, type HarvestStatusView } from '../cli/harvest'
|
|
20
26
|
import {
|
|
21
27
|
buildDashboardFromProjected,
|
|
22
28
|
effectiveStatus,
|
|
23
|
-
projectBuild,
|
|
24
29
|
projectBuildFromFacts,
|
|
25
30
|
type DashboardBuild,
|
|
26
31
|
type DashboardModel,
|
|
27
32
|
} from '../cli/dashboard/model'
|
|
28
|
-
import {
|
|
33
|
+
import type { BuildState } from '../kernel/reducer'
|
|
29
34
|
import { DISPATCHER_EFFECTIVE_CONFIG_ARTIFACT } from '../store/retention'
|
|
30
35
|
import { reduceDispatchStatus } from '../kernel/dispatch-status'
|
|
31
|
-
import {
|
|
36
|
+
import { unclaimedObservationCount } from '../processes/harvest'
|
|
32
37
|
import type { RepoViewStore } from '../processes/repo-view'
|
|
33
38
|
import { withOperatorView } from './read-view'
|
|
34
39
|
import type { RepositoryEvent } from '../events/repository'
|
|
@@ -74,7 +79,7 @@ async function listFromView(
|
|
|
74
79
|
// finished builds accumulate. The `--all` scope includes terminal statuses,
|
|
75
80
|
// so it takes no digest read and skips nothing; its terminal builds are
|
|
76
81
|
// restored from the reducer snapshots the dispatcher leaves at settlement.
|
|
77
|
-
const digests = opts.scope === 'all' ? undefined : await view
|
|
82
|
+
const digests = opts.scope === 'all' ? undefined : await readBuildDigests(view, opts.repo)
|
|
78
83
|
const output: BuildSummary[] = []
|
|
79
84
|
for (const record of records) {
|
|
80
85
|
if (record.repo !== opts.repo) continue
|
|
@@ -114,14 +119,19 @@ export async function effectiveConfig(
|
|
|
114
119
|
repo: string,
|
|
115
120
|
/** The bounded journal, when the caller already holds it (a repository
|
|
116
121
|
* view); otherwise it is read from the store. */
|
|
117
|
-
journal?:
|
|
122
|
+
journal?: RepositoryReadFacts,
|
|
118
123
|
): Promise<{
|
|
119
124
|
config: Config
|
|
120
|
-
repositoryEvents:
|
|
125
|
+
repositoryEvents: RepositoryReadFacts
|
|
121
126
|
status: ReturnType<typeof reduceDispatchStatus>
|
|
122
127
|
}> {
|
|
123
|
-
const repositoryEvents = journal ?? (await
|
|
124
|
-
const status =
|
|
128
|
+
const repositoryEvents = journal ?? (await readRepositoryFacts(store, repo)).value
|
|
129
|
+
const status = repositoryEvents.status
|
|
130
|
+
if (status?.effectiveConfig === undefined)
|
|
131
|
+
throw new OperatorQueryError(
|
|
132
|
+
'effective-config-unavailable',
|
|
133
|
+
`effective config unavailable for repository "${repo}"${repositoryEvents.config.run === undefined ? ': no dispatcher run is recorded' : ''}`,
|
|
134
|
+
)
|
|
125
135
|
const ref = status.effectiveConfig!
|
|
126
136
|
const artifact = await store.getRepoArtifact(repo, ref.kind, ref.rev)
|
|
127
137
|
if (artifact === null) {
|
|
@@ -258,19 +268,19 @@ export async function getOperatorBuild(opts: {
|
|
|
258
268
|
now: Date
|
|
259
269
|
}): Promise<OperatorBuildView> {
|
|
260
270
|
const record = await requireBuild(opts.store, opts.repo, opts.slug)
|
|
261
|
-
const
|
|
262
|
-
const state =
|
|
271
|
+
const facts = (await readBuildFacts(opts.store, opts.slug)).value
|
|
272
|
+
const state = facts.state
|
|
263
273
|
const { config } = await effectiveConfig(opts.store, opts.repo)
|
|
264
274
|
let dashboardRow: DashboardBuild | null = null
|
|
265
275
|
if (!projectsNoDashboardRow(state)) {
|
|
266
276
|
const pinned = await readPinnedConfig(opts.store, opts.slug, config)
|
|
267
|
-
const row =
|
|
277
|
+
const row = projectBuildFromFacts(record, state, facts, config, undefined, pinned.config)
|
|
268
278
|
// Implied by the gate; kept as an explicit check (not an assertion) so the
|
|
269
279
|
// code never lies if `effectiveStatus` and `projectBuild` ever drift.
|
|
270
280
|
if (row !== null) dashboardRow = decorateWithPinnedMeta(row, pinned)
|
|
271
281
|
}
|
|
272
282
|
return {
|
|
273
|
-
detail:
|
|
283
|
+
detail: detailFromFacts(record, facts, opts.now),
|
|
274
284
|
dashboardRow,
|
|
275
285
|
}
|
|
276
286
|
}
|
|
@@ -281,8 +291,12 @@ export async function getRepositoryStatus(
|
|
|
281
291
|
now = new Date(),
|
|
282
292
|
): Promise<RepositoryStatus> {
|
|
283
293
|
return withOperatorView(store, repo, 'journal', async (view) => {
|
|
284
|
-
const { dispatcher } = await readDispatcherLiveness(store, repo, now
|
|
285
|
-
return
|
|
294
|
+
const { dispatcher } = await readDispatcherLiveness(store, repo, now)
|
|
295
|
+
return repositoryStatusFromFacts(
|
|
296
|
+
repo,
|
|
297
|
+
(await readRepositoryFacts(view, repo)).value,
|
|
298
|
+
dispatcher,
|
|
299
|
+
)
|
|
286
300
|
})
|
|
287
301
|
}
|
|
288
302
|
|
|
@@ -291,7 +305,7 @@ export async function getHarvestStatus(
|
|
|
291
305
|
repo: string,
|
|
292
306
|
): Promise<HarvestStatusView> {
|
|
293
307
|
return withOperatorView(store, repo, 'journal', async (view) =>
|
|
294
|
-
projectHarvestStatus(repo, view.
|
|
308
|
+
projectHarvestStatus(repo, view.repositoryFacts()),
|
|
295
309
|
)
|
|
296
310
|
}
|
|
297
311
|
|
|
@@ -321,7 +335,7 @@ async function dashboardFromView(
|
|
|
321
335
|
const { config, repositoryEvents, status } = await effectiveConfig(
|
|
322
336
|
view,
|
|
323
337
|
opts.repo,
|
|
324
|
-
view.
|
|
338
|
+
view.repositoryFacts(),
|
|
325
339
|
)
|
|
326
340
|
const records = await view.listBuilds()
|
|
327
341
|
// One digest read covers every build of the repository (AUT-487): the
|
|
@@ -330,7 +344,7 @@ async function dashboardFromView(
|
|
|
330
344
|
// builds accumulate. Row-rendering builds derive from the repository view's
|
|
331
345
|
// reducer snapshots plus a delta read (AUT-650); every snapshot is a cache
|
|
332
346
|
// the event log regenerates, so the response equals a full replay's.
|
|
333
|
-
const digests = await view
|
|
347
|
+
const digests = await readBuildDigests(view, opts.repo)
|
|
334
348
|
const projected: DashboardBuild[] = []
|
|
335
349
|
let activeCount = 0
|
|
336
350
|
for (const record of records) {
|
|
@@ -372,7 +386,10 @@ async function dashboardFromView(
|
|
|
372
386
|
// `effectiveConfig` already returned — no per-build history reads (AUT-487).
|
|
373
387
|
// The pure core writes nothing — a snapshot performs no store writes and
|
|
374
388
|
// never creates or locks the repository record.
|
|
375
|
-
const observationCount = unclaimedObservationCount({
|
|
389
|
+
const observationCount = unclaimedObservationCount({
|
|
390
|
+
digests,
|
|
391
|
+
harvestEvents: repositoryEvents.harvest,
|
|
392
|
+
})
|
|
376
393
|
const warningLines = [
|
|
377
394
|
...status.roleWarnings,
|
|
378
395
|
...(status.warningNotice !== undefined ? [status.warningNotice] : []),
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The operator read paths' repository view: every poll-driven route (dashboard,
|
|
3
3
|
* repository and harvest status, build listings) derives its state from a
|
|
4
|
-
* `RepoViewStore`
|
|
5
|
-
*
|
|
6
|
-
* last one, never the history, the open tabs, or the watchers.
|
|
4
|
+
* `RepoViewStore` backed by persisted reduced values. Each pass refreshes
|
|
5
|
+
* repository facts and metadata without hydrating raw history.
|
|
7
6
|
*
|
|
8
7
|
* Passes are single-flight per `(store, repo)` within one serving process: at
|
|
9
8
|
* most one pass runs at a time, and requests arriving while it runs join the
|
|
10
9
|
* one trailing pass (each needs state at least as new as its arrival, which the
|
|
11
10
|
* running pass cannot promise). That trailing pass restores from the snapshot
|
|
12
|
-
* the previous pass persisted, so N concurrent pollers cost one
|
|
13
|
-
* pass, not N. The coordination is scheduling inside this process; every datum
|
|
11
|
+
* the previous pass persisted for builds, so N concurrent pollers cost one
|
|
12
|
+
* build delta read per pass, not N. The coordination is scheduling inside this process; every datum
|
|
14
13
|
* still comes from durable state, and a second serving process simply reads the
|
|
15
14
|
* same delta once itself (duplicate reads are bounded by the process count).
|
|
16
15
|
*/
|
|
@@ -58,14 +57,6 @@ async function pass(store: BuildStore, repo: string, waiters: Waiter[]): Promise
|
|
|
58
57
|
return
|
|
59
58
|
}
|
|
60
59
|
await Promise.all(waiters.map((waiter) => waiter.run(view).then(waiter.resolve, waiter.reject)))
|
|
61
|
-
// Leave the advanced state behind for the next pass (this process's or
|
|
62
|
-
// another's). `held` also writes a settled build read for a listing. The
|
|
63
|
-
// cache never fails a read.
|
|
64
|
-
try {
|
|
65
|
-
await view.persistSnapshots({ force: true, held: true })
|
|
66
|
-
} catch {
|
|
67
|
-
// A cache: the next pass retries.
|
|
68
|
-
}
|
|
69
60
|
}
|
|
70
61
|
|
|
71
62
|
async function drain(store: BuildStore, repo: string, flight: Flight): Promise<void> {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TicketRevisionConflict, TicketRevisionUnsupported } from '../ports/tickets/revision'
|
|
1
2
|
/**
|
|
2
3
|
* The agent tool registry — one typed table that is the single source of
|
|
3
4
|
* truth for the agent-facing operator surface (AUT-338).
|
|
@@ -58,6 +59,9 @@ import { answerAction, controlPrechecks, requireRouteBuild, RouteRefusalError }
|
|
|
58
59
|
import { SandboxOperationError } from '../ports/workspace/operator-sandbox'
|
|
59
60
|
import type { OperatorSandboxService } from './sandbox'
|
|
60
61
|
import {
|
|
62
|
+
prepareOperatorTicketAssets,
|
|
63
|
+
listOperatorTicketStates,
|
|
64
|
+
createOperatorTicketState,
|
|
61
65
|
getOperatorTicket,
|
|
62
66
|
listOperatorTickets,
|
|
63
67
|
mutateOperatorTicket,
|
|
@@ -598,7 +602,13 @@ export const TOOLS: readonly ToolEntry[] = [
|
|
|
598
602
|
user: attributed(ctx),
|
|
599
603
|
...(ctx.via !== undefined ? { via: ctx.via } : {}),
|
|
600
604
|
action,
|
|
601
|
-
...(readTicketBody !== undefined
|
|
605
|
+
...(readTicketBody !== undefined
|
|
606
|
+
? {
|
|
607
|
+
readTicketBody,
|
|
608
|
+
prepareTicketAssets: (ref, store, repo) =>
|
|
609
|
+
prepareOperatorTicketAssets({ ref, store, repo, backend: ctx.tickets }),
|
|
610
|
+
}
|
|
611
|
+
: {}),
|
|
602
612
|
})
|
|
603
613
|
},
|
|
604
614
|
),
|
|
@@ -691,6 +701,30 @@ export const TOOLS: readonly ToolEntry[] = [
|
|
|
691
701
|
})
|
|
692
702
|
},
|
|
693
703
|
),
|
|
704
|
+
defineTool(
|
|
705
|
+
'tickets.states',
|
|
706
|
+
'List all ticket states with ticket counts and optional purposes.',
|
|
707
|
+
z.strictObject({ repo: z.string().min(1) }),
|
|
708
|
+
'Array of {name,tickets,about?}.',
|
|
709
|
+
async (raw, ctx) => {
|
|
710
|
+
const input = raw as { repo: string }
|
|
711
|
+
return listOperatorTicketStates({
|
|
712
|
+
store: ctx.store,
|
|
713
|
+
repo: input.repo,
|
|
714
|
+
backend: ticketsOf(ctx),
|
|
715
|
+
})
|
|
716
|
+
},
|
|
717
|
+
),
|
|
718
|
+
defineTool(
|
|
719
|
+
'tickets.state.create',
|
|
720
|
+
'Create a custom ticket state with an optional one-line purpose. Hosted duplicates are refused.',
|
|
721
|
+
z.strictObject({ repo: z.string().min(1), name: z.string(), about: z.string().optional() }),
|
|
722
|
+
'{state: {name,tickets,about?},created}.',
|
|
723
|
+
async (raw, ctx) => {
|
|
724
|
+
const input = raw as { repo: string; name: string; about?: string }
|
|
725
|
+
return createOperatorTicketState({ store: ctx.store, backend: ticketsOf(ctx), ...input })
|
|
726
|
+
},
|
|
727
|
+
),
|
|
694
728
|
defineTool(
|
|
695
729
|
'tickets.list',
|
|
696
730
|
'List the repository’s ticket grooming queue from the configured ticket source.',
|
|
@@ -709,9 +743,9 @@ export const TOOLS: readonly ToolEntry[] = [
|
|
|
709
743
|
),
|
|
710
744
|
defineTool(
|
|
711
745
|
'tickets.get',
|
|
712
|
-
'Show one ticket with its blockers and any related build.',
|
|
746
|
+
'Show one ticket with its hosted comments, blockers and any related build.',
|
|
713
747
|
ticketDetailInput,
|
|
714
|
-
'{ticket, blockers, build}: the ticket, its dependency states, and the most relevant build for it (null when none).',
|
|
748
|
+
'{ticket, blockers, build}: the ticket (including hosted ticket.comments when available), its dependency states, and the most relevant build for it (null when none).',
|
|
715
749
|
async (raw, ctx) => {
|
|
716
750
|
const input = raw as z.infer<typeof ticketDetailInput>
|
|
717
751
|
return getOperatorTicket({
|
|
@@ -752,12 +786,12 @@ export const TOOLS: readonly ToolEntry[] = [
|
|
|
752
786
|
'{ticket, blockers, build}: the updated ticket.',
|
|
753
787
|
async (raw, ctx) => {
|
|
754
788
|
const input = raw as z.infer<typeof ticketsUpdateInput>
|
|
755
|
-
const { repo, id, ...patch } = input
|
|
789
|
+
const { repo, id, expectedRevision, ...patch } = input
|
|
756
790
|
return mutateOperatorTicket({
|
|
757
791
|
store: ctx.store,
|
|
758
792
|
repo,
|
|
759
793
|
backend: ticketsOf(ctx),
|
|
760
|
-
operation: { kind: 'update', id, patch },
|
|
794
|
+
operation: { kind: 'update', id, patch, expectedRevision },
|
|
761
795
|
})
|
|
762
796
|
},
|
|
763
797
|
),
|
|
@@ -772,7 +806,12 @@ export const TOOLS: readonly ToolEntry[] = [
|
|
|
772
806
|
store: ctx.store,
|
|
773
807
|
repo: input.repo,
|
|
774
808
|
backend: ticketsOf(ctx),
|
|
775
|
-
operation: {
|
|
809
|
+
operation: {
|
|
810
|
+
kind: 'block',
|
|
811
|
+
id: input.id,
|
|
812
|
+
blockerIds: input.blockerIds,
|
|
813
|
+
expectedRevision: input.expectedRevision,
|
|
814
|
+
},
|
|
776
815
|
})
|
|
777
816
|
},
|
|
778
817
|
),
|
|
@@ -787,7 +826,12 @@ export const TOOLS: readonly ToolEntry[] = [
|
|
|
787
826
|
store: ctx.store,
|
|
788
827
|
repo: input.repo,
|
|
789
828
|
backend: ticketsOf(ctx),
|
|
790
|
-
operation: {
|
|
829
|
+
operation: {
|
|
830
|
+
kind: 'unblock',
|
|
831
|
+
id: input.id,
|
|
832
|
+
blockerIds: input.blockerIds,
|
|
833
|
+
expectedRevision: input.expectedRevision,
|
|
834
|
+
},
|
|
791
835
|
})
|
|
792
836
|
},
|
|
793
837
|
),
|
|
@@ -802,7 +846,12 @@ export const TOOLS: readonly ToolEntry[] = [
|
|
|
802
846
|
store: ctx.store,
|
|
803
847
|
repo: input.repo,
|
|
804
848
|
backend: ticketsOf(ctx),
|
|
805
|
-
operation: {
|
|
849
|
+
operation: {
|
|
850
|
+
kind: 'move',
|
|
851
|
+
id: input.id,
|
|
852
|
+
state: input.state,
|
|
853
|
+
expectedRevision: input.expectedRevision,
|
|
854
|
+
},
|
|
806
855
|
})
|
|
807
856
|
},
|
|
808
857
|
),
|
|
@@ -964,6 +1013,10 @@ function mapDomainError(error: unknown): RegistryError {
|
|
|
964
1013
|
error: error.refusal.error,
|
|
965
1014
|
})
|
|
966
1015
|
}
|
|
1016
|
+
if (error instanceof TicketRevisionConflict)
|
|
1017
|
+
return new RegistryError('domain', error.message, { kind: 'conflict', error: error.message })
|
|
1018
|
+
if (error instanceof TicketRevisionUnsupported)
|
|
1019
|
+
return new RegistryError('domain', error.message, { kind: 'refusal', error: error.message })
|
|
967
1020
|
if (error instanceof TicketOperationError) {
|
|
968
1021
|
return new RegistryError('domain', error.message, {
|
|
969
1022
|
kind: error.code === 'not-found' ? 'not-found' : 'refusal',
|
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* The route-level glue the operator HTTP server applies around the shared
|
|
3
|
-
* services, re-derived for the agent tool registry (operator/registry.ts).
|
|
4
|
-
*
|
|
5
|
-
* This module is registry-owned on purpose: the operator API routes were
|
|
6
|
-
* declared out of scope for the registry ticket, so the parity between a
|
|
7
|
-
* tool and its route is *proven* by the contract suite
|
|
8
|
-
* (packages/hosted-store-service/src/registry.contract.test.ts) rather than
|
|
9
|
-
* obtained by extraction. Every refusal text here is load-bearing — it must
|
|
10
|
-
* stay byte-identical to the hosted package's `operator-server.ts` so the
|
|
11
|
-
* suite's full-body comparisons hold.
|
|
12
|
-
*/
|
|
13
|
-
import { reduceBuild } from '../kernel/reducer'
|
|
1
|
+
import { readBuildFacts } from '../store/reduced-reads'
|
|
14
2
|
import { effectiveStatus } from '../cli/dashboard/model'
|
|
15
3
|
import { BuildControlError, type BuildControlAction } from '../cli/build-control'
|
|
16
4
|
import type { BuildStore } from '../store/types'
|
|
@@ -80,7 +68,7 @@ export async function controlPrechecks(
|
|
|
80
68
|
): Promise<void> {
|
|
81
69
|
await requireRouteBuild(store, repo, slug)
|
|
82
70
|
if (action === 'pause' || action === 'cancel-pause') {
|
|
83
|
-
const state =
|
|
71
|
+
const state = (await readBuildFacts(store, slug)).value.state
|
|
84
72
|
const display = effectiveStatus(state)
|
|
85
73
|
if (action === 'pause' && display === 'pausing') {
|
|
86
74
|
throw new BuildControlError(
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
import { createHash } from 'node:crypto'
|
|
14
14
|
import { humanActor } from '../events/envelope'
|
|
15
15
|
import type { Via } from '../events/envelope'
|
|
16
|
-
import type { RepositoryEvent } from '../events/repository'
|
|
17
16
|
import type { Forge } from '../ports/types'
|
|
18
17
|
import {
|
|
19
18
|
SANDBOX_FORBIDDEN_ENV,
|
|
@@ -23,8 +22,7 @@ import {
|
|
|
23
22
|
type SandboxWaitResult,
|
|
24
23
|
} from '../ports/workspace/operator-sandbox'
|
|
25
24
|
import type { WorkspaceProvider } from '../ports/types'
|
|
26
|
-
import {
|
|
27
|
-
import { sandboxStates } from '../processes/sandbox-state'
|
|
25
|
+
import { readRepositoryFacts } from '../store/reduced-reads'
|
|
28
26
|
import { systemClock, type BuildStore, type Clock } from '../store/types'
|
|
29
27
|
|
|
30
28
|
/** Hard bound on one sandbox command wait, in seconds — shared with the
|
|
@@ -253,8 +251,11 @@ export async function createOperatorSandboxService(
|
|
|
253
251
|
}
|
|
254
252
|
|
|
255
253
|
/** The operator's journal state, or undefined when no environment exists. */
|
|
256
|
-
const operatorState = (
|
|
257
|
-
|
|
254
|
+
const operatorState = (
|
|
255
|
+
identity: string,
|
|
256
|
+
events: import('../store/reduced-reads').RepositoryReadFacts,
|
|
257
|
+
) =>
|
|
258
|
+
events.sandboxes
|
|
258
259
|
.filter((state) => state.operator === identity && state.state !== 'released')
|
|
259
260
|
.at(-1)
|
|
260
261
|
|
|
@@ -327,7 +328,7 @@ export async function createOperatorSandboxService(
|
|
|
327
328
|
): Promise<T> => {
|
|
328
329
|
const key = `${repo}\0${identity}`
|
|
329
330
|
return serialize(key, async () => {
|
|
330
|
-
const events = await
|
|
331
|
+
const events = (await readRepositoryFacts(store, repo)).value
|
|
331
332
|
const state = operatorState(identity, events)
|
|
332
333
|
const resolved = await ensureWithFacts(identity, state).catch((error: unknown) => {
|
|
333
334
|
if (error instanceof SandboxOperationError) throw error
|
|
@@ -337,7 +338,7 @@ export async function createOperatorSandboxService(
|
|
|
337
338
|
{ cause: error },
|
|
338
339
|
)
|
|
339
340
|
})
|
|
340
|
-
const freshEvents = await
|
|
341
|
+
const freshEvents = (await readRepositoryFacts(store, repo)).value
|
|
341
342
|
const freshState = operatorState(identity, freshEvents)
|
|
342
343
|
try {
|
|
343
344
|
const result = await operation(resolved, freshState)
|
|
@@ -630,7 +631,7 @@ export async function createOperatorSandboxService(
|
|
|
630
631
|
requireRepo(input.repo)
|
|
631
632
|
const key = `${repo}\0${identity}`
|
|
632
633
|
await serialize(key, async () => {
|
|
633
|
-
const events = await
|
|
634
|
+
const events = (await readRepositoryFacts(store, repo)).value
|
|
634
635
|
const state = operatorState(identity, events)
|
|
635
636
|
const resolved = await capability.describe({ repo, operator: identity })
|
|
636
637
|
await append(identity, 'orchestrator.sandbox.reset', {
|
|
@@ -662,7 +663,7 @@ export async function createOperatorSandboxService(
|
|
|
662
663
|
requireRepo(input.repo)
|
|
663
664
|
const key = `${repo}\0${identity}`
|
|
664
665
|
await serialize(key, async () => {
|
|
665
|
-
const events = await
|
|
666
|
+
const events = (await readRepositoryFacts(store, repo)).value
|
|
666
667
|
const state = operatorState(identity, events)
|
|
667
668
|
// No environment ever provisioned: a no-op, with no fact and no
|
|
668
669
|
// provider traffic beyond the pure describe.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ticketLifecycle } from '../ports/tickets/lifecycle'
|
|
1
2
|
/**
|
|
2
3
|
* A dispatcher ticket source as the registry's ticket backend (AUT-342):
|
|
3
4
|
* the tick's turn runner takes an `OperatorToolRegistry`, and
|
|
@@ -14,34 +15,29 @@ import type { Config } from '../config/schema'
|
|
|
14
15
|
import type { OperatorTicketBackend, OperatorTicketContext } from './tickets'
|
|
15
16
|
import type { TicketSource } from '../ports/types'
|
|
16
17
|
|
|
17
|
-
/** The dispatcher's resolved triage state (the same default
|
|
18
|
-
* `processes/dispatcher.ts` applies); inlined rather than imported so this
|
|
19
|
-
* operator-seam module does not pull the dispatcher's module graph. */
|
|
20
|
-
function defaultTriageState(config: Config): string {
|
|
21
|
-
return (
|
|
22
|
-
config.tickets.triageState ??
|
|
23
|
-
(config.tickets.source === 'linear' || config.tickets.source === 'hosted'
|
|
24
|
-
? 'Backlog'
|
|
25
|
-
: 'Triage')
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
18
|
export function ticketBackendFromSource(options: {
|
|
30
19
|
source: TicketSource
|
|
31
20
|
config: Config
|
|
32
21
|
}): OperatorTicketBackend {
|
|
33
22
|
return {
|
|
34
23
|
sourceFor: (_context: OperatorTicketContext) => options.source,
|
|
35
|
-
statesFor: (_context: OperatorTicketContext,
|
|
24
|
+
statesFor: async (_context: OperatorTicketContext, source: TicketSource): Promise<string[]> => {
|
|
25
|
+
if (source.listStates) return (await source.listStates()).map((s) => s.name)
|
|
36
26
|
const tickets = options.config.tickets
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
tickets.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
const lifecycle = ticketLifecycle(tickets)
|
|
28
|
+
const states =
|
|
29
|
+
tickets.source === 'hosted'
|
|
30
|
+
? Object.values(lifecycle)
|
|
31
|
+
: [
|
|
32
|
+
lifecycle.triage,
|
|
33
|
+
tickets.readyState,
|
|
34
|
+
tickets.createState,
|
|
35
|
+
tickets.claimedState,
|
|
36
|
+
tickets.doneState,
|
|
37
|
+
lifecycle.proposal,
|
|
38
|
+
].filter(
|
|
39
|
+
(state): state is string => typeof state === 'string' && state.trim().length > 0,
|
|
40
|
+
)
|
|
45
41
|
return [...new Set(states)]
|
|
46
42
|
},
|
|
47
43
|
}
|