@defrex/autobuild 0.10.0 → 0.11.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 +97 -113
- package/SPEC.md +22 -5
- package/bin/ab-dispatch-kernel.ts +1 -1
- package/docs/configuration.md +34 -7
- package/docs/operator-api.md +17 -0
- package/docs/remote-ticket-protocol.md +5 -0
- package/package.json +3 -3
- package/packages/core/src/cli/binary.ts +3 -3
- package/packages/core/src/cli/dashboard/model.ts +4 -0
- package/packages/core/src/cli/dashboard/render.ts +37 -7
- package/packages/core/src/cli/dispatch-frontend.ts +16 -1
- package/packages/core/src/cli/dispatch-process.ts +25 -7
- package/packages/core/src/cli/dispatch.ts +64 -6
- package/packages/core/src/cli/dispatcher-liveness.ts +91 -0
- package/packages/core/src/cli/help.ts +8 -5
- package/packages/core/src/cli/init.ts +3 -2
- package/packages/core/src/cli/repository-status.ts +45 -5
- package/packages/core/src/cli/watch.ts +159 -14
- package/packages/core/src/config/live.ts +1 -0
- package/packages/core/src/config/schema.ts +9 -2
- package/packages/core/src/events/globs.ts +8 -3
- package/packages/core/src/events/repository.ts +51 -2
- package/packages/core/src/integration/harness.ts +2 -1
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +18 -0
- package/packages/core/src/kernel/dispatch-status.ts +9 -1
- package/packages/core/src/kernel/dispatcher-liveness.ts +70 -0
- package/packages/core/src/kernel/reducer-registry.ts +4 -0
- package/packages/core/src/kernel/ticket-source-health.ts +30 -0
- package/packages/core/src/operator/query.ts +6 -3
- package/packages/core/src/orchestrator/prompt.ts +3 -1
- package/packages/core/src/ports/tickets/contract.ts +25 -0
- package/packages/core/src/ports/tickets/fake.ts +10 -8
- package/packages/core/src/ports/tickets/file.ts +1 -0
- package/packages/core/src/ports/tickets/linear.ts +241 -49
- package/packages/core/src/ports/tickets/remote-protocol.ts +2 -0
- package/packages/core/src/ports/types.ts +17 -0
- package/packages/core/src/processes/dispatcher.ts +277 -73
- package/packages/core/src/processes/repo-view.ts +2 -1
- package/packages/core/src/store/adapter.ts +1 -0
- package/packages/core/src/store/contract.ts +109 -0
- package/packages/core/src/store/digest.ts +14 -2
- package/packages/core/src/store/local/store.ts +18 -5
- package/packages/core/src/store/remote/protocol.ts +1 -0
- package/packages/core/src/store/repo-state-events.ts +49 -59
- package/packages/core/src/store/types.ts +5 -1
- package/skills/guide/SKILL.md +43 -14
- package/skills/operate/SKILL.md +180 -18
|
@@ -45,6 +45,7 @@ export type DigestEvent = DigestEventRow
|
|
|
45
45
|
|
|
46
46
|
/** The carried accumulator for `reduceBuildDigest`. */
|
|
47
47
|
export interface DigestAcc {
|
|
48
|
+
readinessChangeSeq?: number
|
|
48
49
|
terminal?: BuildDigest['terminal']
|
|
49
50
|
merged?: string
|
|
50
51
|
observations: { seq: number; ts: string }[]
|
|
@@ -57,7 +58,7 @@ export interface DigestAcc {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/** Bump when `DigestAcc`'s shape or fold semantics change. */
|
|
60
|
-
export const BUILD_DIGEST_REDUCER_VERSION =
|
|
61
|
+
export const BUILD_DIGEST_REDUCER_VERSION = 3
|
|
61
62
|
|
|
62
63
|
export const buildDigestReducer: IncrementalReducer<
|
|
63
64
|
DigestAcc,
|
|
@@ -68,6 +69,8 @@ export const buildDigestReducer: IncrementalReducer<
|
|
|
68
69
|
initial: (): DigestAcc => ({ observations: [], hosted: [], reclaimed: [], openInstance: null }),
|
|
69
70
|
fold(acc, events) {
|
|
70
71
|
for (const event of events) {
|
|
72
|
+
if (['pr.merged', 'build.completed', 'build.aborted'].includes(event.type))
|
|
73
|
+
acc.readinessChangeSeq = event.seq
|
|
71
74
|
if (event.type === 'build.completed') acc.terminal = 'done'
|
|
72
75
|
else if (event.type === 'build.aborted') acc.terminal = 'aborted'
|
|
73
76
|
else if (event.type === 'pr.merged') acc.merged = event.ts
|
|
@@ -84,11 +87,20 @@ export const buildDigestReducer: IncrementalReducer<
|
|
|
84
87
|
}
|
|
85
88
|
}
|
|
86
89
|
},
|
|
87
|
-
finish: ({
|
|
90
|
+
finish: ({
|
|
91
|
+
readinessChangeSeq,
|
|
92
|
+
terminal,
|
|
93
|
+
merged,
|
|
94
|
+
observations,
|
|
95
|
+
hosted,
|
|
96
|
+
reclaimed,
|
|
97
|
+
openInstance,
|
|
98
|
+
}) => {
|
|
88
99
|
const reclaimedSet = new Set(reclaimed)
|
|
89
100
|
const reclaimPending = hosted.some((seq) => !reclaimedSet.has(seq))
|
|
90
101
|
return {
|
|
91
102
|
observations: [...observations],
|
|
103
|
+
...(readinessChangeSeq !== undefined ? { readinessChangeSeq } : {}),
|
|
92
104
|
...(merged !== undefined ? { merged } : {}),
|
|
93
105
|
...(terminal !== undefined ? { terminal } : {}),
|
|
94
106
|
...(reclaimPending ? { reclaimPending: true as const } : {}),
|
|
@@ -46,6 +46,7 @@ import { createSessionScopedStore } from '../session-handle'
|
|
|
46
46
|
import { DIGEST_EVENT_TYPES, type DigestEventRow, reduceBuildDigest } from '../digest'
|
|
47
47
|
import {
|
|
48
48
|
REPOSITORY_STATE_EVENT_TYPES,
|
|
49
|
+
REPOSITORY_LATEST_EVENT_TYPES,
|
|
49
50
|
readRepoStateEventsWithAnchorRecheck,
|
|
50
51
|
} from '../repo-state-events'
|
|
51
52
|
import {
|
|
@@ -1188,14 +1189,26 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
1188
1189
|
},
|
|
1189
1190
|
async (anchor) => {
|
|
1190
1191
|
const durable = inArray(repoEvents.type, [...REPOSITORY_STATE_EVENT_TYPES])
|
|
1192
|
+
const latest = REPOSITORY_LATEST_EVENT_TYPES.map((type) =>
|
|
1193
|
+
and(
|
|
1194
|
+
eq(repoEvents.type, type),
|
|
1195
|
+
sql`${repoEvents.seq} = (SELECT seq FROM repo_events WHERE repo = ${repo} AND type = ${type} ORDER BY seq DESC LIMIT 1)`,
|
|
1196
|
+
),
|
|
1197
|
+
)
|
|
1198
|
+
const tail =
|
|
1199
|
+
anchor === undefined
|
|
1200
|
+
? undefined
|
|
1201
|
+
: and(
|
|
1202
|
+
gte(repoEvents.seq, anchor),
|
|
1203
|
+
sql`${repoEvents.type} NOT IN (${sql.join(
|
|
1204
|
+
REPOSITORY_LATEST_EVENT_TYPES.map((type) => sql`${type}`),
|
|
1205
|
+
sql`, `,
|
|
1206
|
+
)})`,
|
|
1207
|
+
)
|
|
1191
1208
|
const rows = this.db
|
|
1192
1209
|
.select()
|
|
1193
1210
|
.from(repoEvents)
|
|
1194
|
-
.where(
|
|
1195
|
-
anchor === undefined
|
|
1196
|
-
? and(eq(repoEvents.repo, repo), durable)
|
|
1197
|
-
: and(eq(repoEvents.repo, repo), or(gte(repoEvents.seq, anchor), durable)),
|
|
1198
|
-
)
|
|
1211
|
+
.where(and(eq(repoEvents.repo, repo), or(durable, tail, ...latest)))
|
|
1199
1212
|
.orderBy(asc(repoEvents.seq))
|
|
1200
1213
|
.all()
|
|
1201
1214
|
return rows.map(
|
|
@@ -285,6 +285,7 @@ export const buildDigestWireSchema = z.object({
|
|
|
285
285
|
}),
|
|
286
286
|
),
|
|
287
287
|
merged: z.string().min(1).optional(),
|
|
288
|
+
readinessChangeSeq: z.number().int().positive().optional(),
|
|
288
289
|
reclaimPending: z.literal(true).optional(),
|
|
289
290
|
executionOpen: z.literal(true).optional(),
|
|
290
291
|
})
|
|
@@ -1,54 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* The hosted dispatcher appends ~4 journal events per invocation (run-started,
|
|
7
|
-
* tick-started, tick-completed/tick-yielded, run-stopped) whether or not it
|
|
8
|
-
* did anything, so the journal grows forever with wall-clock time while the
|
|
9
|
-
* durable signal stays small. Every repository-state consumer reduces the
|
|
10
|
-
* journal through one of four pure reducers whose event-type dependencies
|
|
11
|
-
* partition the catalog:
|
|
12
|
-
*
|
|
13
|
-
* - `reduceDispatchSettings` — only the three dispatcher setting types
|
|
14
|
-
* (latest-wins by seq).
|
|
15
|
-
* - `reduceDispatchStatus(events, latestRun)` — only run-correlated
|
|
16
|
-
* dispatcher facts, and it skips every event whose `payload.run` is not the
|
|
17
|
-
* latest run. A run's facts are appended under its own run id, which is
|
|
18
|
-
* minted at `dispatcher.run-started`, so all facts the reducer keeps live at
|
|
19
|
-
* or after the latest run-started.
|
|
20
|
-
* - `reduceHarvest` (and everything built on it) — only `harvest.*` events
|
|
21
|
-
* plus the three setting types, which it explicitly ignores.
|
|
22
|
-
* - `sandboxStates` — only `orchestrator.sandbox.*` events.
|
|
23
|
-
*
|
|
24
|
-
* So the bounded subset is: **every event of a durable (slow-growing) type —
|
|
25
|
-
* all `harvest.*`, all `orchestrator.sandbox.*`, and the three dispatcher
|
|
26
|
-
* setting types — plus, when the journal contains at least one
|
|
27
|
-
* `dispatcher.run-started`, the tail from that latest run-started to the end
|
|
28
|
-
* of the journal.** For a hosted repository with N no-op invocations this
|
|
29
|
-
* subset's size is a constant (one invocation's ~4 facts plus the durable
|
|
30
|
-
* signal), independent of N.
|
|
31
|
-
*
|
|
32
|
-
* INVARIANT FOR AUTHORS OF NEW REPOSITORY EVENT TYPES: a new type must either
|
|
33
|
-
* (a) join `REPOSITORY_STATE_EVENT_TYPES` — it is durable, slow-growing state
|
|
34
|
-
* some reducer depends on at full replay depth — or (b) join
|
|
35
|
-
* `REPOSITORY_RUN_SCOPED_EVENT_TYPES` — it is run-correlated and its only
|
|
36
|
-
* consumers reduce it for the latest run, so the tail from the latest
|
|
37
|
-
* `dispatcher.run-started` covers it. `tests/store/repo-state-events.test.ts`
|
|
38
|
-
* asserts the two sets exactly partition `REPOSITORY_EVENT_TYPES`; adding a
|
|
39
|
-
* type to neither fails the build until the author makes the call. A type in
|
|
40
|
-
* neither set would be silently dropped from bounded reads.
|
|
41
|
-
*
|
|
42
|
-
* Reducing this subset with the state readers yields exactly what a full
|
|
43
|
-
* replay yields: `reduceDispatchSettings`, `reduceHarvest`, `sandboxStates`
|
|
44
|
-
* see identical event lists, and `reduceDispatchStatus` sees every event it
|
|
45
|
-
* keeps (the rest are skipped by the `payload.run !== latestRun` guard). The
|
|
46
|
-
* journal itself stays append-only; no event is deleted, rewritten, or
|
|
47
|
-
* re-ordered, and a replay from the start is untouched.
|
|
1
|
+
/** Bounded journal state has three disjoint, exhaustive categories:
|
|
2
|
+
* durable facts retain all history; run-scoped facts retain the latest run
|
|
3
|
+
* tail; latest-only facts retain exactly the greatest sequence per type,
|
|
4
|
+
* including before any run anchor. The full journal remains append-only.
|
|
48
5
|
*/
|
|
49
6
|
import type { RepositoryEvent, RepositoryEventType } from '../events/repository'
|
|
50
7
|
import { defineReducer, type IncrementalReducer } from '../kernel/incremental'
|
|
51
8
|
import {
|
|
9
|
+
ticketSourceHealthEventPayloadSchemas,
|
|
52
10
|
dispatcherSettingEventPayloadSchemas,
|
|
53
11
|
dispatcherStatusEventPayloadSchemas,
|
|
54
12
|
harvestEventPayloadSchemas,
|
|
@@ -64,6 +22,7 @@ const RUN_STARTED = 'dispatcher.run-started'
|
|
|
64
22
|
* setting types are enumerated because they share one schema group with the
|
|
65
23
|
* run-correlated `dispatcher.config-reloaded`. */
|
|
66
24
|
export const REPOSITORY_STATE_EVENT_TYPES: readonly RepositoryEventType[] = [
|
|
25
|
+
...Object.keys(ticketSourceHealthEventPayloadSchemas),
|
|
67
26
|
...Object.keys(harvestEventPayloadSchemas),
|
|
68
27
|
...Object.keys(orchestratorSandboxEventPayloadSchemas),
|
|
69
28
|
...Object.keys(dispatcherSettingEventPayloadSchemas).filter(
|
|
@@ -71,11 +30,19 @@ export const REPOSITORY_STATE_EVENT_TYPES: readonly RepositoryEventType[] = [
|
|
|
71
30
|
),
|
|
72
31
|
] as RepositoryEventType[]
|
|
73
32
|
|
|
33
|
+
/** Latest-only measurements survive run turnover without retaining scan history. */
|
|
34
|
+
export const REPOSITORY_LATEST_EVENT_TYPES: readonly RepositoryEventType[] = [
|
|
35
|
+
'dispatcher.ticket-scan-completed',
|
|
36
|
+
]
|
|
37
|
+
const LATEST_TYPES = new Set<string>(REPOSITORY_LATEST_EVENT_TYPES)
|
|
38
|
+
|
|
74
39
|
/** Run-correlated repository event types: facts whose only state consumers
|
|
75
40
|
* reduce them for the latest dispatcher run, so the tail from the latest
|
|
76
41
|
* `dispatcher.run-started` contains everything those consumers keep. */
|
|
77
42
|
export const REPOSITORY_RUN_SCOPED_EVENT_TYPES: readonly RepositoryEventType[] = [
|
|
78
|
-
...Object.keys(dispatcherStatusEventPayloadSchemas)
|
|
43
|
+
...Object.keys(dispatcherStatusEventPayloadSchemas).filter(
|
|
44
|
+
(type) => !REPOSITORY_LATEST_EVENT_TYPES.includes(type as RepositoryEventType),
|
|
45
|
+
),
|
|
79
46
|
'dispatcher.config-reloaded',
|
|
80
47
|
] as RepositoryEventType[]
|
|
81
48
|
|
|
@@ -122,8 +89,8 @@ export async function readRepoStateEventsWithAnchorRecheck(
|
|
|
122
89
|
}
|
|
123
90
|
|
|
124
91
|
/** Carried state for the bounded read: the latest run-started seq (the last
|
|
125
|
-
* in array order, as the whole-array pass overwrites it) and every event seen,
|
|
126
|
-
* in array order.
|
|
92
|
+
* in array order, as the whole-array pass overwrites it) and every ordinary event seen,
|
|
93
|
+
* in array order, plus one latest-only fact per type. Ordinary history is retained: a later run-started may carry a lower seq
|
|
127
94
|
* than an earlier one and restore events below the earlier anchor, so only
|
|
128
95
|
* `finish` can apply the filter exactly. Pruning below a monotone anchor is
|
|
129
96
|
* valid only for seq-ordered input and belongs with the cached-state
|
|
@@ -134,10 +101,11 @@ export interface RepositoryStateEventsAcc {
|
|
|
134
101
|
}
|
|
135
102
|
|
|
136
103
|
/** Bump when `RepositoryStateEventsAcc` or its fold changes. */
|
|
137
|
-
export const REPOSITORY_STATE_EVENTS_REDUCER_VERSION =
|
|
104
|
+
export const REPOSITORY_STATE_EVENTS_REDUCER_VERSION = 4
|
|
138
105
|
|
|
139
106
|
const keeps = (event: RepositoryEvent, anchor: number | undefined): boolean =>
|
|
140
|
-
STATE_TYPES.has(event.type) ||
|
|
107
|
+
STATE_TYPES.has(event.type) ||
|
|
108
|
+
(!LATEST_TYPES.has(event.type) && anchor !== undefined && event.seq >= anchor)
|
|
141
109
|
|
|
142
110
|
export const repositoryStateEventsReducer: IncrementalReducer<
|
|
143
111
|
RepositoryStateEventsAcc,
|
|
@@ -149,10 +117,24 @@ export const repositoryStateEventsReducer: IncrementalReducer<
|
|
|
149
117
|
fold(acc, events) {
|
|
150
118
|
for (const event of events) {
|
|
151
119
|
if (event.type === RUN_STARTED) acc.anchor = event.seq
|
|
120
|
+
if (LATEST_TYPES.has(event.type)) {
|
|
121
|
+
const held = acc.retained.find((held) => held.type === event.type)
|
|
122
|
+
if (held !== undefined && held.seq >= event.seq) continue
|
|
123
|
+
acc.retained = acc.retained.filter((held) => held.type !== event.type)
|
|
124
|
+
}
|
|
152
125
|
acc.retained.push(event)
|
|
153
126
|
}
|
|
154
127
|
},
|
|
155
|
-
finish: (acc) =>
|
|
128
|
+
finish: (acc) => {
|
|
129
|
+
const latest = new Map<string, RepositoryEvent>()
|
|
130
|
+
for (const event of acc.retained) {
|
|
131
|
+
if (LATEST_TYPES.has(event.type) && event.seq > (latest.get(event.type)?.seq ?? -1))
|
|
132
|
+
latest.set(event.type, event)
|
|
133
|
+
}
|
|
134
|
+
return [...acc.retained.filter((event) => keeps(event, acc.anchor)), ...latest.values()].sort(
|
|
135
|
+
(a, b) => a.seq - b.seq,
|
|
136
|
+
)
|
|
137
|
+
},
|
|
156
138
|
})
|
|
157
139
|
|
|
158
140
|
/** Carried state for the pruned journal view: the latest run-started seq and
|
|
@@ -167,16 +149,23 @@ export interface JournalViewAcc {
|
|
|
167
149
|
}
|
|
168
150
|
|
|
169
151
|
/** Bump when `JournalViewAcc` or its fold changes. */
|
|
170
|
-
export const JOURNAL_VIEW_REDUCER_VERSION =
|
|
152
|
+
export const JOURNAL_VIEW_REDUCER_VERSION = 4
|
|
171
153
|
|
|
172
154
|
/** Fold seq-ordered events into a journal view in place. */
|
|
173
155
|
export function foldJournalView(acc: JournalViewAcc, events: readonly RepositoryEvent[]): void {
|
|
174
156
|
for (const event of events) {
|
|
175
157
|
if (event.type === RUN_STARTED) {
|
|
176
158
|
acc.anchor = event.seq
|
|
177
|
-
acc.retained = acc.retained.filter(
|
|
159
|
+
acc.retained = acc.retained.filter(
|
|
160
|
+
(held) => STATE_TYPES.has(held.type) || LATEST_TYPES.has(held.type),
|
|
161
|
+
)
|
|
178
162
|
}
|
|
179
|
-
if (
|
|
163
|
+
if (LATEST_TYPES.has(event.type)) {
|
|
164
|
+
const held = acc.retained.find((held) => held.type === event.type)
|
|
165
|
+
if (held !== undefined && held.seq >= event.seq) continue
|
|
166
|
+
acc.retained = acc.retained.filter((held) => held.type !== event.type)
|
|
167
|
+
acc.retained.push(event)
|
|
168
|
+
} else if (STATE_TYPES.has(event.type) || acc.anchor !== undefined) acc.retained.push(event)
|
|
180
169
|
}
|
|
181
170
|
}
|
|
182
171
|
|
|
@@ -193,9 +182,10 @@ export const journalViewReducer: IncrementalReducer<
|
|
|
193
182
|
|
|
194
183
|
/** The normative subset derivation every adapter implements and the contract
|
|
195
184
|
* tests use as the oracle: durable types across the whole journal, plus —
|
|
196
|
-
* only when the journal has one — every event from the latest
|
|
197
|
-
* `dispatcher.run-started` onward,
|
|
198
|
-
*
|
|
185
|
+
* only when the journal has one — every non-latest-only event from the latest
|
|
186
|
+
* `dispatcher.run-started` onward, plus the maximum-sequence fact of each
|
|
187
|
+
* latest-only type across the whole journal, ordered by seq. A journal with no
|
|
188
|
+
* run-started has **no tail at all** (durable and latest-only facts only); the anchor is
|
|
199
189
|
* never defaulted to 0, which would make a `seq >= anchor` clause select the
|
|
200
190
|
* whole journal and defeat the bound. */
|
|
201
191
|
export function projectRepositoryStateEvents(
|
|
@@ -105,6 +105,8 @@ export interface ArtifactMeta {
|
|
|
105
105
|
* the event log on each call, so the log stays the sole authority and the
|
|
106
106
|
* digest can always be discarded and rebuilt. */
|
|
107
107
|
export interface BuildDigest {
|
|
108
|
+
/** Latest merge, completion or abort fact sequence. */
|
|
109
|
+
readinessChangeSeq?: number
|
|
108
110
|
slug: string
|
|
109
111
|
terminal?: 'done' | 'aborted'
|
|
110
112
|
/** Ascending-seq `{seq, ts}` pairs for every `observation.recorded` in the
|
|
@@ -368,7 +370,9 @@ export interface BuildStore {
|
|
|
368
370
|
): Promise<RepositoryEvent[]>
|
|
369
371
|
/** Bounded repository-journal read for stateless full-state readers
|
|
370
372
|
* (AUT-489): every durable fact (REPOSITORY_STATE_EVENT_TYPES) plus — when
|
|
371
|
-
* the journal has one — the tail from the latest `dispatcher.run-started
|
|
373
|
+
* the journal has one — the tail from the latest `dispatcher.run-started`,
|
|
374
|
+
* excluding latest-only types; plus exactly the maximum-sequence fact for
|
|
375
|
+
* each REPOSITORY_LATEST_EVENT_TYPES type across the whole journal.
|
|
372
376
|
* Reducing this subset with the state readers yields exactly what a full
|
|
373
377
|
* replay yields (`projectRepositoryStateEvents` is the normative oracle).
|
|
374
378
|
* Unknown repo rejects like `getRepoEvents`. */
|
package/skills/guide/SKILL.md
CHANGED
|
@@ -234,6 +234,19 @@ Every other known table is closed to unknown keys. The removed `[dashboardFrames
|
|
|
234
234
|
`[dispatcher]`, `[harvest]`, and `[outer]` tables have no aliases or migration
|
|
235
235
|
shims; they fail as ordinary unknown top-level keys.
|
|
236
236
|
|
|
237
|
+
Ticket-source ready scans run immediately when no completed measurement exists,
|
|
238
|
+
then every
|
|
239
|
+
`ticketScanIntervalSeconds` (default 60 seconds, a positive integer). Actual
|
|
240
|
+
tick spacing is the floor when the configured interval is shorter. A
|
|
241
|
+
same-repository `pr.merged`, `build.completed`, or `build.aborted` fact forces
|
|
242
|
+
another scan on the next tick; changed ready criteria also force a scan.
|
|
243
|
+
Housekeeping keeps its ordinary tick cadence. Between scans, queue depth and
|
|
244
|
+
standing diagnostics repeat the last completed measurement, including across
|
|
245
|
+
process restarts. Tickets groomed directly in the tracker are picked up within
|
|
246
|
+
one scan interval plus one tick when capacity permits. Observation while intake
|
|
247
|
+
is off shares the cadence; an observation measurement cannot suppress a fresh
|
|
248
|
+
admission scan when intake is enabled.
|
|
249
|
+
|
|
237
250
|
### Root scalars
|
|
238
251
|
|
|
239
252
|
Root scalars must appear before the first TOML table; TOML otherwise nests them
|
|
@@ -242,6 +255,7 @@ under the preceding table.
|
|
|
242
255
|
| Field | Default | Allowed / constraints | Effect |
|
|
243
256
|
|---|---|---|---|
|
|
244
257
|
| `baseBranch` | `"main"` | nonempty string | The branch builds branch from and target with their PR; what `reconcile` merges into the build branch. |
|
|
258
|
+
| `ticketScanIntervalSeconds` | `60` | positive integer seconds | Hot-reloaded ready scan cadence; tick spacing is the floor, and same-repository merge/completion/abort forces an early scan. |
|
|
245
259
|
| `capacity` | `1` | positive integer | Maximum concurrent builds for this repository. |
|
|
246
260
|
| `forge` | `"github"` | nonblank string | Selects shipped `github` or `local-git`, or a plugin-registered Forge adapter. `github` authenticates with `GITHUB_TOKEN`, then `GH_TOKEN`, then the gh CLI's stored login. |
|
|
247
261
|
| `plugins` | `[]` | array of unique nonblank module specifiers | Trusted Bun plugin modules loaded before dispatch, `ab ticket`, and scoped phase wiring. |
|
|
@@ -899,7 +913,7 @@ repo path, needs no `AB_*` environment, and is safe to re-run. It:
|
|
|
899
913
|
base for `ab upgrade`'s three-way merges.
|
|
900
914
|
- Rewrites frontmatter on install: `name` → `ab-<name>`, and
|
|
901
915
|
`disable-model-invocation: true` on every skill outside the model-invocable
|
|
902
|
-
set (`ab-spec`, `ab-tickets`, `ab-guide`).
|
|
916
|
+
set (`ab-spec`, `ab-tickets`, `ab-guide`, `ab-operate`).
|
|
903
917
|
|
|
904
918
|
The final report states `autobuild.toml: written|skipped`, aggregates all skill
|
|
905
919
|
counts, names only attention-worthy `kept` and `overwritten` skills, and reports
|
|
@@ -1650,6 +1664,23 @@ matching event after that cursor is delivered exactly once, in per-stream
|
|
|
1650
1664
|
sequence order — including events that landed while no watch was running. A
|
|
1651
1665
|
cursor from a different repository or store is rejected.
|
|
1652
1666
|
|
|
1667
|
+
|
|
1668
|
+
Dispatcher liveness is also visible through `ab repository status --json`:
|
|
1669
|
+
`dispatcher.holder` identifies the unexpired repository lease owner, independently
|
|
1670
|
+
of `latestRun`. Heartbeat age measures attributable renewals; tick age measures
|
|
1671
|
+
durable progress. Missing historical evidence is null. The liveness query uses
|
|
1672
|
+
bounded repository facts first, with a full-history fallback when a later
|
|
1673
|
+
invocation hides the holder start or the latest stop. Other state reads retain
|
|
1674
|
+
their bounded semantics.
|
|
1675
|
+
|
|
1676
|
+
Repository-following `ab watch` additionally emits a read-only `dispatcher.lost`
|
|
1677
|
+
observation when an observed lease expires without a matching stop. JSON carries
|
|
1678
|
+
`observation` instead of `event`, with run, observedAt, leaseExpiresAt and available
|
|
1679
|
+
identity, plus the usual repo/build/state/cursor fields. It invents no durable
|
|
1680
|
+
sequence or actor. Retain the cursor to avoid repeat reports across invocations.
|
|
1681
|
+
No lease alone is not evidence of death. SIGINT, SIGHUP and SIGTERM drain dispatch,
|
|
1682
|
+
record the first signal in the stop fact, and release its lease before exit.
|
|
1683
|
+
|
|
1653
1684
|
The watch ends with exit 0 when `--timeout` elapses (30 minutes by default;
|
|
1654
1685
|
`0` unbounded), after `--count <n>` records, or when named builds have all
|
|
1655
1686
|
gone terminal; with no slugs named, an empty set of active builds does not end
|
|
@@ -1878,16 +1909,14 @@ default, when you need to know what this repo's version says).
|
|
|
1878
1909
|
| `ab-code-review` | `code-review` phase | Fresh skeptic: review the implementation diff against spec and plan, same verdict vocabulary. |
|
|
1879
1910
|
| `ab-reconcile` | `reconcile` phase (epilogue) | Resolve a conflicted PR with one merge commit, base merged *into* the build branch. Never rebases. |
|
|
1880
1911
|
| `ab-finalize` | `finalize` phase | Write the PR description for a green build; the kernel opens the PR. |
|
|
1881
|
-
| `ab-operate` |
|
|
1882
|
-
|
|
1883
|
-
Autobuild installs 12 skills. Everything except `ab-spec`, `ab-tickets`,
|
|
1884
|
-
`ab-guide` carries `disable-model-invocation: true` — do not
|
|
1885
|
-
skill yourself, and do not remove that key to make one
|
|
1886
|
-
model starting a pipeline phase by pattern-matching a
|
|
1887
|
-
what the flag prevents. Those
|
|
1888
|
-
the criterion for membership (§16.3): `ab-spec` and
|
|
1889
|
-
human/agent-facing pre-build surfaces,
|
|
1890
|
-
including the setup handoff guidance
|
|
1891
|
-
|
|
1892
|
-
embedded orchestrator's own operating manual, read by the hosted turn runner as
|
|
1893
|
-
its system prompt and never invoked by a model in a build session.
|
|
1912
|
+
| `ab-operate` | outside the pipeline; model-invocable | The orchestrator role's operating manual, for any agent: file and groom tickets, watch builds with a resumable `ab watch` / `ab wait` loop, answer escalations, pause, resume, abort, control auto-merge, and keep going until every shepherded build is merged, closed, or aborted. Also read by the hosted turn runner as its system prompt (registry tools, approval and budget suspension, wake turns). |
|
|
1913
|
+
|
|
1914
|
+
Autobuild installs 12 skills. Everything except `ab-spec`, `ab-tickets`,
|
|
1915
|
+
`ab-guide`, and `ab-operate` carries `disable-model-invocation: true` — do not
|
|
1916
|
+
invoke a phase skill yourself, and do not remove that key to make one
|
|
1917
|
+
convenient to call. A model starting a pipeline phase by pattern-matching a
|
|
1918
|
+
description is exactly what the flag prevents. Those four exceptions drive no
|
|
1919
|
+
build phase, which is the criterion for membership (§16.3): `ab-spec` and
|
|
1920
|
+
`ab-tickets` are human/agent-facing pre-build surfaces, `ab-guide` is
|
|
1921
|
+
read-only reference including the setup handoff guidance, and `ab-operate` is
|
|
1922
|
+
the manual for the orchestrator role, which any agent may take on.
|
package/skills/operate/SKILL.md
CHANGED
|
@@ -1,17 +1,175 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: operate
|
|
3
|
-
description:
|
|
3
|
+
description: Orchestrate a repository's builds - file and groom tickets, watch builds, answer escalations, pause, resume, or abort builds, control auto-merge, and drive work to merge. Use when asked to watch, shepherd, or orchestrate builds, whether from a coding-agent session at the repository root using the ab CLI or inside the hosted service with the operator tool registry.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Operating the repository
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
The orchestrator is a role, not a deployment. It is whichever agent is
|
|
9
|
+
pointed at a repository to file tickets, watch builds, unblock them, answer
|
|
10
|
+
operator questions, and drive work to merge. Any agent can take it on: a
|
|
11
|
+
coding-agent session at the repository root using the `ab` CLI (or `ab mcp`),
|
|
12
|
+
or the hosted service's embedded turn runner. There is no initialization
|
|
13
|
+
step — deciding to orchestrate is enough.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
The orchestrator is not a build phase: it never runs pipeline steps, never
|
|
16
|
+
edits the repository, and never touches a build's checkout. Its reach is the
|
|
17
|
+
operator surface — the `ab` CLI, or the operator tool registry bound into a
|
|
18
|
+
hosted turn.
|
|
19
|
+
|
|
20
|
+
Find your setting and read that section: **Running locally with the `ab`
|
|
21
|
+
CLI** if you are a session with a shell, **Running in the hosted service** if
|
|
22
|
+
your tools are the registry's `builds.*` / `tickets.*` family. The sections
|
|
23
|
+
after them apply to both.
|
|
24
|
+
|
|
25
|
+
## Running locally with the `ab` CLI
|
|
26
|
+
|
|
27
|
+
Run the commands below from the repository root. Each acts with the operator's
|
|
28
|
+
authority, so say what you are doing before you do anything destructive.
|
|
29
|
+
|
|
30
|
+
### Watch for attention events
|
|
31
|
+
|
|
32
|
+
`ab watch --json` streams the attention set (escalations, failures, finalize,
|
|
33
|
+
PR conflicted/merged/closed, build completed/aborted) as newline-delimited
|
|
34
|
+
records. Every record carries a `cursor`; a bounded watch ends with a trailing
|
|
35
|
+
`{"cursor": …}` line. Resume with `--since <cursor>` and every event after it
|
|
36
|
+
is delivered exactly once, including events that landed while nothing was
|
|
37
|
+
watching. Name slugs to watch only those builds; with none, every active build
|
|
38
|
+
(including ones created later) and repository attention are watched. Named watches need `--repository` to also receive repository attention. `--event <glob>` replaces the
|
|
39
|
+
default filter, `--timeout <dur>` bounds it (default 30 minutes, `0` is
|
|
40
|
+
unbounded), `--count <n>` ends it after n records.
|
|
41
|
+
|
|
42
|
+
`ab wait [<slug>...] (--for <condition> | --event <glob>) [--since <cursor>] [--json]`
|
|
43
|
+
blocks until a condition holds and prints one record. `--for` state conditions
|
|
44
|
+
are also satisfied immediately by a build already in that state; `--event` is
|
|
45
|
+
satisfied only by new events. Conditions include `attention`,
|
|
46
|
+
`blocked`, `paused`, `running`, `done`, `aborted`, `terminal`, `pr=merged`,
|
|
47
|
+
`pr=closed`, `pr=conflicted`, and `phase=<phase>`. Exit codes: 0 satisfied,
|
|
48
|
+
1 usage or store error, 2 every named build is terminal without satisfying a
|
|
49
|
+
condition, 3 timeout, 4 interrupted. On 3 and 4 with `--json`, the output is a
|
|
50
|
+
bare `{"cursor": …}` to resume from.
|
|
51
|
+
|
|
52
|
+
Run long watches and waits in whatever background or monitor facility your
|
|
53
|
+
harness provides, so the session stays responsive and you are woken when they
|
|
54
|
+
emit. Never block a foreground command for their whole duration.
|
|
55
|
+
|
|
56
|
+
Ticket-source degradation records mean dispatch intake cannot evaluate the source;
|
|
57
|
+
existing builds may still advance. Inspect `ab repository status --json` for the
|
|
58
|
+
active cause, operation, message, and expected reset. Reset times are advisory;
|
|
59
|
+
recovery means reads actually succeeded. Allow the existing cooldown and retry
|
|
60
|
+
behavior, investigate persistent ready-scan failures or a condition still active
|
|
61
|
+
after its reset estimate, and keep watching through recovery. Empty builds do
|
|
62
|
+
not establish idle intake; repeatedly restarting dispatch does not clear it.
|
|
63
|
+
|
|
64
|
+
Repository records have null build and state. Handle them with repository
|
|
65
|
+
status rather than build commands, keep their cursor, and re-arm. A legacy
|
|
66
|
+
cursor without a repository position baselines that stream at attachment and
|
|
67
|
+
cannot recover its earlier interval: reconcile current repository status.
|
|
68
|
+
`ab wait --for attention` remains build-only and does not replace a repository
|
|
69
|
+
attention watch.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
Check dispatcher liveness with `ab repository status --json` before starting another
|
|
73
|
+
`ab dispatch`, and after a watch timeout or `dispatcher.lost` observation. Do not
|
|
74
|
+
use process lists to decide whether a dispatcher owns this repository. The
|
|
75
|
+
`dispatcher` projection distinguishes `held` (an unexpired lease), `lost`
|
|
76
|
+
(expired ownership without a recorded stop), `stopped`, and `absent`. Its holder
|
|
77
|
+
identity resolves a yield message to run, pid, host, start time, and heartbeat
|
|
78
|
+
age. Historical identity and a new holder's heartbeat may be unknown until its
|
|
79
|
+
first attributable renewal. Source degradation or an old tick alone does not
|
|
80
|
+
establish loss: a healthy lease can accompany a slow tick.
|
|
81
|
+
|
|
82
|
+
An unscoped watch, or a named watch with `--repository`, reports loss as a
|
|
83
|
+
read-only observation: `{repo, build:null, state:null, observation:{type:
|
|
84
|
+
"dispatcher.lost", run, observedAt, leaseExpiresAt, ...identity}, cursor}`.
|
|
85
|
+
It has no durable event envelope. Keep its cursor just as for event records;
|
|
86
|
+
resuming it suppresses repeated loss reports for that run. Inspect repository
|
|
87
|
+
status before deciding how to recover; watch never restarts a dispatcher.
|
|
88
|
+
|
|
89
|
+
### Keep watching until the work is done
|
|
90
|
+
|
|
91
|
+
Checking once is not shepherding. When you take responsibility for builds:
|
|
92
|
+
|
|
93
|
+
1. Record the set you are shepherding (`ab builds --all --json`, or the slugs
|
|
94
|
+
you were given).
|
|
95
|
+
2. Arm an event-only watch and keep its cursor:
|
|
96
|
+
`ab watch --json --since <cursor>` (or named slugs with `--repository`) (omit `--since` the first
|
|
97
|
+
time). Events are delivered exactly once, so a build that already finished
|
|
98
|
+
is never replayed. Do not loop on `ab wait --for terminal|attention|…`:
|
|
99
|
+
state conditions are satisfied at once by any build already in that state,
|
|
100
|
+
even with `--since`, so a finished build would be returned again and the
|
|
101
|
+
loop would spin on it. If you use `ab wait`, use only `--event <glob>`
|
|
102
|
+
conditions (satisfied by new events only), never `--for`.
|
|
103
|
+
3. Reconcile current state right after the first watch is armed, and again
|
|
104
|
+
whenever a watch ends without records: a watch baselines each build at its
|
|
105
|
+
current position and does not emit what already happened, so a build that
|
|
106
|
+
merged, or an escalation already open, before registration would otherwise
|
|
107
|
+
never reach you. Run `ab build status <slug> --json` for every slug in the
|
|
108
|
+
set, and `ab repository status --json` for source health, initially and after
|
|
109
|
+
every timeout or legacy-cursor attachment. Reading status after arming leaves no gap — anything earlier shows in
|
|
110
|
+
the status, anything later arrives as an event. Treat a terminal status
|
|
111
|
+
(merged, closed, aborted) as that build's outcome, and an open escalation
|
|
112
|
+
or failed phase as attention to handle now.
|
|
113
|
+
4. When a watch returns records, react: read the record, inspect the build,
|
|
114
|
+
answer or escalate; for a repository record inspect repository status and act
|
|
115
|
+
on its condition without assuming a build slug. Then re-arm from the last cursor. Keep your own
|
|
116
|
+
outcome table: mark a build done when you see `pr.merged`, `pr.closed`, or
|
|
117
|
+
`build.aborted`, or when status shows it. A timeout (exit 3, or a watch
|
|
118
|
+
that ends with only a `{"cursor": …}` line) is not completion — reconcile
|
|
119
|
+
as in step 3, then re-arm from that cursor.
|
|
120
|
+
5. Stop only when every build in the set has an outcome in your table.
|
|
121
|
+
`pr.conflicted` is not terminal — reconcile follows. An open escalation
|
|
122
|
+
(`blocked`) needs a response, not patience. A watch that exits because
|
|
123
|
+
every named build is terminal still needs the step 3 status read to learn
|
|
124
|
+
how each ended.
|
|
125
|
+
6. Report each outcome to the operator, and file or move tickets when the
|
|
126
|
+
outcome makes that obviously right.
|
|
127
|
+
|
|
128
|
+
### Inspect
|
|
129
|
+
|
|
130
|
+
- `ab builds [--queued] [--all] [--json]` — builds and their effective status.
|
|
131
|
+
- `ab build status <slug> [--events N]` — one build's detail and recent events.
|
|
132
|
+
- `ab repository status` — the repository's derived status.
|
|
133
|
+
- `ab artifact` — fetch one artifact's bytes.
|
|
134
|
+
|
|
135
|
+
Read events with a cursor or a small `--events` count; logs are long.
|
|
136
|
+
|
|
137
|
+
### Act
|
|
138
|
+
|
|
139
|
+
- `ab answer <slug> [text]` — answer an open escalation with guidance; omit the
|
|
140
|
+
text for a bare retry. `--dismiss` retires cited review findings,
|
|
141
|
+
`--review-round-ceiling <n>` raises the round limit, `--revise-spec <file>`
|
|
142
|
+
resolves with a revised spec. Run `ab help answer` for the full menu.
|
|
143
|
+
- `ab pause <slug>` / `ab resume <slug>`; `ab pause --all` / `ab resume --all`
|
|
144
|
+
hold and release the whole repository.
|
|
145
|
+
- `ab abort <slug>` — destructive: closes the PR, removes branches, and
|
|
146
|
+
returns the ticket to triage.
|
|
147
|
+
- `ab auto-merge <slug> on|off` — request or cancel native auto-merge.
|
|
148
|
+
|
|
149
|
+
### Tickets
|
|
150
|
+
|
|
151
|
+
`ab ticket create|update|block|unblock|list|show|move|states` file, edit, and
|
|
152
|
+
move tickets; the `ab-tickets` skill covers the tracker's details. Moving a
|
|
153
|
+
ticket into the ready state sends it to dispatch — do that only when the
|
|
154
|
+
ticket genuinely conforms and the operator's intent is clear.
|
|
155
|
+
|
|
156
|
+
### What the CLI does not offer
|
|
157
|
+
|
|
158
|
+
The registry has operator notes, harvest control, and `discard`, which have no
|
|
159
|
+
CLI equivalent, and no bulk control beyond `pause --all` / `resume --all`. Say
|
|
160
|
+
so rather than improvising; tell the operator, or record it with `ab observe`
|
|
161
|
+
when you are inside a build. `ab mcp` mounts the same registry over MCP for
|
|
162
|
+
harnesses that support it — if you have those tools, the hosted section's tool
|
|
163
|
+
descriptions apply.
|
|
164
|
+
|
|
165
|
+
## Running in the hosted service
|
|
166
|
+
|
|
167
|
+
This section applies when your tools are the operator registry bound into
|
|
168
|
+
your turn. If you are a session with a shell and the `ab` CLI, use the
|
|
169
|
+
section above and ignore this one. In the hosted service you never supply a
|
|
170
|
+
repository argument; it is bound for you.
|
|
171
|
+
|
|
172
|
+
### How you run
|
|
15
173
|
|
|
16
174
|
Each of your turns is one durable agent loop:
|
|
17
175
|
|
|
@@ -26,7 +184,7 @@ Each of your turns is one durable agent loop:
|
|
|
26
184
|
- Your tools target exactly one repository, the session's own. You never
|
|
27
185
|
supply a repository argument; it is bound for you.
|
|
28
186
|
|
|
29
|
-
|
|
187
|
+
### Suspension
|
|
30
188
|
|
|
31
189
|
Your turn can stop in two ways, and both are normal:
|
|
32
190
|
|
|
@@ -43,7 +201,7 @@ Your turn can stop in two ways, and both are normal:
|
|
|
43
201
|
When suspended, just stop cleanly. Anything important you want to say should
|
|
44
202
|
already be in your streamed output before the suspension point.
|
|
45
203
|
|
|
46
|
-
|
|
204
|
+
### The tool surface
|
|
47
205
|
|
|
48
206
|
Your tools are the operator registry. The read-only ones are always safe:
|
|
49
207
|
|
|
@@ -81,7 +239,7 @@ Every tool returns the operator API's shapes: results, or a typed failure
|
|
|
81
239
|
(`kind`, `error`, `code`). A refusal is information, not an exception:
|
|
82
240
|
report it, adapt, and carry on.
|
|
83
241
|
|
|
84
|
-
|
|
242
|
+
### The attention set
|
|
85
243
|
|
|
86
244
|
When the session has wake settings, the dispatcher wakes you when a build
|
|
87
245
|
event matching them lands. A wake turn's first message carries the event
|
|
@@ -100,7 +258,11 @@ record and the build's reduced state as JSON. Typical wakes and what to do:
|
|
|
100
258
|
Do not re-derive history the build's events already tell you; read events
|
|
101
259
|
with a cursor and keep it short.
|
|
102
260
|
|
|
103
|
-
##
|
|
261
|
+
## Shared rules
|
|
262
|
+
|
|
263
|
+
These apply in both settings. Where the text below says "plain streamed text", a local session means plain text to the human in the session.
|
|
264
|
+
|
|
265
|
+
### When to escalate instead of act
|
|
104
266
|
|
|
105
267
|
Stop and ask the operator — in plain streamed text — whenever:
|
|
106
268
|
|
|
@@ -109,18 +271,18 @@ Stop and ask the operator — in plain streamed text — whenever:
|
|
|
109
271
|
- Two readings of the operator's intent diverge in cost or risk.
|
|
110
272
|
- You would be guessing about product direction, priorities, or scope.
|
|
111
273
|
- A tool refusal repeats and you cannot resolve it with the tools you have.
|
|
112
|
-
- The attention event is one you cannot act on with the
|
|
274
|
+
- The attention event is one you cannot act on with the tools you have at all.
|
|
113
275
|
|
|
114
276
|
Filing a ticket IS acting — do it when the request is concrete. Asking is
|
|
115
277
|
not a failure; an unasked destructive action is.
|
|
116
278
|
|
|
117
|
-
|
|
279
|
+
### Boundaries
|
|
118
280
|
|
|
119
|
-
- You never claim to have done anything outside the
|
|
281
|
+
- You never claim to have done anything outside the tools you have. If you cannot
|
|
120
282
|
observe it with a tool, say you cannot observe it.
|
|
121
283
|
- You never invent repository identity, ticket ids, or build slugs; every
|
|
122
284
|
id you act on came from a tool result or the conversation.
|
|
123
|
-
-
|
|
124
|
-
arrives mid-turn is answered after the current turn ends.
|
|
125
|
-
-
|
|
126
|
-
when you learn something durable.
|
|
285
|
+
- In the hosted service you do not run concurrent work: one turn at a time,
|
|
286
|
+
and a message that arrives mid-turn is answered after the current turn ends.
|
|
287
|
+
- Where operator notes exist (hosted), they are shared ground truth; keep
|
|
288
|
+
them current when you learn something durable.
|