@defrex/autobuild 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SPEC.md +10 -0
- package/docs/architecture.md +109 -0
- package/docs/configuration.md +24 -3
- package/docs/remote-store-protocol.md +35 -0
- package/docs/website.md +4 -0
- package/package.json +6 -4
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +85 -147
- package/packages/core/src/cli/dashboard/poll.ts +7 -2
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +80 -37
- package/packages/core/src/cli/help.ts +15 -1
- package/packages/core/src/cli/init-validation.ts +18 -0
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +1 -1
- package/packages/core/src/cli/ticket.ts +146 -1
- package/packages/core/src/cli/wait.ts +10 -2
- package/packages/core/src/cli/watch.ts +10 -2
- package/packages/core/src/config/live.ts +1 -0
- package/packages/core/src/config/pipeline-source.ts +17 -5
- package/packages/core/src/config/schema.ts +21 -0
- package/packages/core/src/integration/harness.ts +113 -1
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +43 -9
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +216 -74
- package/packages/core/src/operator/query.ts +68 -28
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +32 -1
- package/packages/core/src/processes/build-execution-state.ts +31 -15
- package/packages/core/src/processes/build-runner.ts +184 -90
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +270 -176
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +44 -3
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +26 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +10 -1
- package/packages/core/src/store/build-scope.ts +26 -0
- package/packages/core/src/store/contract.ts +337 -0
- package/packages/core/src/store/digest.ts +71 -18
- package/packages/core/src/store/local/schema.ts +17 -0
- package/packages/core/src/store/local/store.ts +111 -4
- package/packages/core/src/store/memory.ts +68 -1
- package/packages/core/src/store/phase-session.ts +23 -0
- package/packages/core/src/store/remote/client.ts +50 -1
- package/packages/core/src/store/remote/index.ts +2 -0
- package/packages/core/src/store/remote/protocol.ts +15 -0
- package/packages/core/src/store/repo-state-events.ts +72 -7
- package/packages/core/src/store/session-handle.ts +30 -0
- package/packages/core/src/store/session-reducer.ts +52 -11
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/ticket-assets.ts +41 -8
- package/packages/core/src/store/types.ts +38 -0
- package/skills/guide/SKILL.md +26 -7
- package/skills/guide/references/remote-store-protocol.md +35 -0
- package/skills/tickets/SKILL.md +57 -5
package/README.md
CHANGED
package/SPEC.md
CHANGED
|
@@ -241,6 +241,16 @@ same BuildStore. Consequences, by design:
|
|
|
241
241
|
|
|
242
242
|
- **State is a reduction of events.** Any state snapshot is a cache, never
|
|
243
243
|
the source of truth. Resumability falls out.
|
|
244
|
+
- **Reducer snapshots are a cache the log regenerates, never an authority.**
|
|
245
|
+
The store may keep, per scope (one build log or the repository journal) and
|
|
246
|
+
reducer, that reducer's serialized state as of a cursor, keyed by scope,
|
|
247
|
+
reducer, reducer version, and cursor, so a process with no hot memory loads
|
|
248
|
+
it, reads only the newer events, and advances. A missing snapshot, one
|
|
249
|
+
written by a different reducer version, or one ahead of the log is ignored
|
|
250
|
+
and the state is rebuilt by full replay with identical results; stored
|
|
251
|
+
cursors never decrease; deleting every snapshot changes no observable
|
|
252
|
+
output. A store without snapshot storage behaves as if every snapshot were
|
|
253
|
+
missing.
|
|
244
254
|
- **The UI layer is a subscriber** plus a command channel back. TUI, web —
|
|
245
255
|
same adapter pattern. The shipped interactive terminal owns a supervised
|
|
246
256
|
kernel child, and the BuildStore is their only changing-state boundary.
|
package/docs/architecture.md
CHANGED
|
@@ -131,6 +131,115 @@ session's status, open turn, pending approval, and wake cursors from its own
|
|
|
131
131
|
closed catalog. No decision anywhere
|
|
132
132
|
consults a snapshot in place of the append-only log.
|
|
133
133
|
|
|
134
|
+
**Incremental reducers.** Every pure projection of an event array is a fold plus
|
|
135
|
+
a final derivation, defined once with `defineReducer` in
|
|
136
|
+
`packages/core/src/kernel/incremental.ts`: `initial()` builds an empty
|
|
137
|
+
accumulator, `advance(acc, events)` folds in only the events newer than the
|
|
138
|
+
accumulator, and `finish(acc)` derives the public state. The whole-array
|
|
139
|
+
functions (`reduceBuild`, `reduceHarvest`, `reduceSession`, and the rest) are
|
|
140
|
+
wrappers over the same fold, so results cannot diverge, and a property test
|
|
141
|
+
checks every split point of fixture and generated logs. The accumulator is plain
|
|
142
|
+
JSON, so it survives a serialize-and-parse round trip. `advance` never mutates
|
|
143
|
+
its input: it clones, then folds, so a state derived earlier is never disturbed
|
|
144
|
+
by a later advance. Each reducer publishes a `version`; bump it whenever the
|
|
145
|
+
accumulator shape or the fold semantics change, because a cached accumulator is
|
|
146
|
+
only valid for the version that wrote it. `kernel/reducer-registry.ts` lists
|
|
147
|
+
every reducer with its version, and its snapshot test fails when an accumulator
|
|
148
|
+
shape changes without a bump. Most callers still read whole logs (the terminal
|
|
149
|
+
dashboard poll advances a carried build accumulator by per-build deltas; the
|
|
150
|
+
dispatcher view below restores from stored accumulators).
|
|
151
|
+
|
|
152
|
+
**Reducer snapshots.** The `BuildStore` port can keep, per scope (one build log
|
|
153
|
+
or the repository journal) and registered reducer, that reducer's accumulator as
|
|
154
|
+
of a cursor: `getReducerSnapshot(scope, reducer, version)` and
|
|
155
|
+
`putReducerSnapshot(scope, reducer, {version, cursor, state})`
|
|
156
|
+
(`packages/core/src/store/snapshots.ts`). The log stays the only authority. The
|
|
157
|
+
store enforces the invariants: the stored cursor never decreases (a write whose
|
|
158
|
+
cursor does not advance is a no-op, so concurrent writers cannot undo each
|
|
159
|
+
other), a row of another reducer version reads as absent and is replaced once a
|
|
160
|
+
writer advances past it, and a cursor beyond the log's tail is refused and never
|
|
161
|
+
returned. Every consumer treats a missing, mismatched, ahead-of-log, malformed
|
|
162
|
+
or unreadable snapshot as a miss and replays the log, with identical results;
|
|
163
|
+
deleting all snapshots changes no observable output (the integration harness
|
|
164
|
+
audits this for every scenario at cleanup). Memory, SQLite, Postgres and the
|
|
165
|
+
remote client implement it, and the hosted store service serves it on
|
|
166
|
+
`GET|PUT /builds|repos/{id}/snapshots/{reducer}`
|
|
167
|
+
(`docs/remote-store-protocol.md`). SQLite creates the `reducer_snapshots` table
|
|
168
|
+
at open. Postgres keeps it outside the versioned schema: `reducer_snapshots` is
|
|
169
|
+
created by the idempotent `SNAPSHOT_SCHEMA_DDL` in `migratePostgres`, with no
|
|
170
|
+
marker, version or checksum change, so a deployment at the current schema keeps
|
|
171
|
+
serving across the upgrade, a new client against a database that lacks the table
|
|
172
|
+
reads every snapshot as missing and replays (SQLSTATE `42P01` is never an
|
|
173
|
+
error), and an older client never asks. Run `bun run postgres:migrate` once to
|
|
174
|
+
gain the table; until then the dispatcher and every other reader replay in full.
|
|
175
|
+
|
|
176
|
+
**Dispatcher repository view.** A dispatcher process builds its view of the
|
|
177
|
+
repository once and refreshes it by deltas
|
|
178
|
+
(`packages/core/src/processes/repo-view.ts`). `RepoViewStore` decorates the
|
|
179
|
+
`BuildStore` port: the first `refresh()` reads the journal's high-water mark
|
|
180
|
+
(`getRepoHighWater`), the bounded journal subset, the build listing and digests,
|
|
181
|
+
and the full log of every build that still has work (the same predicate the tick
|
|
182
|
+
loops use); every later `refresh()` reads only events newer than its cursors —
|
|
183
|
+
one journal delta, one delta per held build, plus the listing and digest reads
|
|
184
|
+
that discover new builds. Every tick stage reads and writes through the view, so
|
|
185
|
+
an event a stage appends is folded in at once and later stages in the same tick
|
|
186
|
+
see it with no re-read; a foreign append interleaved with ours leaves a seq gap
|
|
187
|
+
that marks the log dirty, a lost compare-and-set does the same, and any delta
|
|
188
|
+
that is not contiguous from its cursor discards the log and re-reads it in full,
|
|
189
|
+
so no event is skipped. Every state transition of one source (a build, the
|
|
190
|
+
journal) runs on that source's single promise chain — snapshot and delta reads,
|
|
191
|
+
window opening and widening, the fold after an own append, a lost
|
|
192
|
+
compare-and-set's invalidation — and readers queue on it too, so overlapping
|
|
193
|
+
readers join the repair in flight instead of observing half-applied state.
|
|
194
|
+
Decisions that deliberately re-read at the last moment
|
|
195
|
+
(`refreshBuild`) take an explicit delta. Cursor-bearing readers such as the
|
|
196
|
+
orchestrator wake pass get per-source windows that hold only events above the
|
|
197
|
+
lowest session cursor. A `--once` invocation builds the view cold and reads each
|
|
198
|
+
log once; a long-running dispatcher pays that replay at its first tick and not
|
|
199
|
+
again.
|
|
200
|
+
|
|
201
|
+
The view restores from reducer snapshots on cold start. The journal comes from
|
|
202
|
+
the `journalView` reducer (the journal pruned on arrival: durable types plus the
|
|
203
|
+
tail from the latest `dispatcher.run-started`); each work build comes from one
|
|
204
|
+
`work` snapshot row — the `build`, `logIndex`, `openExecution`,
|
|
205
|
+
`openBuildWorkspace`, `publicationState`, `lastExecutionOutcome`, `buildDigest`,
|
|
206
|
+
`dashboardFacts` and `currentDeferralObservation` accumulators at a single
|
|
207
|
+
cursor. One row is one atomic write on every adapter, so a reader in another
|
|
208
|
+
process sees the old bundle or the new, never a mix; the bundle version is
|
|
209
|
+
derived from the component reducer versions, so a stale bundle misses and the
|
|
210
|
+
build is replayed once. The view
|
|
211
|
+
reads only the events newer than that cursor and holds, for a snapshot-backed
|
|
212
|
+
build, just that delta plus the reduced state of everything before it, so a tick
|
|
213
|
+
with no new events reads zero event rows (apart from the discovery listing and
|
|
214
|
+
digests) and one with M new events reads exactly M. Tick stages that run for every
|
|
215
|
+
build each tick take `buildFacts(slug)` (state, log index, open execution, open
|
|
216
|
+
workspace, publication view) instead of the event array, and read raw history
|
|
217
|
+
only on the branches that act on it. `persistSnapshots()` writes the advanced
|
|
218
|
+
state back at the end of each tick and on shutdown — best effort, throttled in a
|
|
219
|
+
resident dispatcher, always run by a cold single-tick process — and never writes
|
|
220
|
+
a cursor that was not read from the log.
|
|
221
|
+
|
|
222
|
+
**Operator reads.** The web dashboard, the repository and Harvest status
|
|
223
|
+
routes, the operator build listings, and the `ab watch`/`ab wait` discovery
|
|
224
|
+
passes derive from that same view instead of replaying the log
|
|
225
|
+
(`packages/core/src/operator/read-view.ts`). A dashboard row renders from the
|
|
226
|
+
`BuildState` plus the `dashboardFacts` reducer
|
|
227
|
+
(`packages/core/src/cli/dashboard/facts.ts`: queued-row dispatch text, session
|
|
228
|
+
brackets, phase intervals, pause time, and the per-round loop-output seqs) and
|
|
229
|
+
the auto-merge deferral ledger; `projectBuild(…, events)` is the same projection
|
|
230
|
+
over the whole-array reduction, so the snapshot and replay paths cannot differ.
|
|
231
|
+
Within one serving process, passes are single-flight per repository: requests
|
|
232
|
+
arriving while a pass runs join the one trailing pass, which restores from the
|
|
233
|
+
snapshot the previous pass persisted. A poll at idle therefore reads no event
|
|
234
|
+
rows beyond snapshot lookups and build discovery, and a poll after M new events
|
|
235
|
+
reads those M rows once, however many tabs or watchers the process serves.
|
|
236
|
+
Serving processes do not coordinate: with P of them, each may read the same M
|
|
237
|
+
rows once, so duplicate reads are bounded by P and never by history, tabs, or
|
|
238
|
+
watchers. A settled build listed under `--all` is restored from the snapshot the
|
|
239
|
+
dispatcher left at settlement, or read once and then persisted. The first
|
|
240
|
+
operator poll after upgrading replays each work build once, because the stored
|
|
241
|
+
bundle layout changed.
|
|
242
|
+
|
|
134
243
|
**Session Store authority.** `packages/core/src/cli/binary.ts` validates a complete build or
|
|
135
244
|
Harvest ambient tuple before opening its phase Store through
|
|
136
245
|
`packages/core/src/cli/store-opening.ts`. `packages/core/src/cli/env.ts` also classifies optional ambient
|
package/docs/configuration.md
CHANGED
|
@@ -202,7 +202,7 @@ These fields hot-reload:
|
|
|
202
202
|
| `[workspace]` provider and config (including `provisioning`) | Next launch of a build whose OWN branch carries it. A base-branch reload never re-provisions an in-flight build. |
|
|
203
203
|
| `[roles]` | Next agent invocation for that role. Runtime, model, args, alternates, and the session budget are captured together and delivered live. |
|
|
204
204
|
| `[policy]` | Next policy, convergence, or agent-session boundary, delivered live. A running session keeps its captured budget. |
|
|
205
|
-
| `tickets.readyLabels`, `tickets.readyState` | Next ready-ticket scan. |
|
|
205
|
+
| `tickets.readyLabels`, `tickets.readyState`, `tickets.readyAssignee` | Next ready-ticket scan. |
|
|
206
206
|
| `tickets.triageState`, `tickets.proposalState` | Next dispatcher handback or harvest filing boundary. |
|
|
207
207
|
|
|
208
208
|
Adapter selectors and factory inputs are constructed once and require a
|
|
@@ -1167,6 +1167,7 @@ state. Within a present table, `source` and `readyState` are required.
|
|
|
1167
1167
|
| `source` | — | required nonblank builtin or plugin registration name | Select the ticket adapter after configured plugins load. |
|
|
1168
1168
|
| `readyLabels` | source-aware | optional array of nonempty strings; `[]` allowed | Require every listed label in addition to the state gate. |
|
|
1169
1169
|
| `readyState` | — | required nonblank string | The one workflow state eligible for dispatch. |
|
|
1170
|
+
| `readyAssignee` | — | optional; `"me"` or an email address; Linear only (rejected for file, hosted, and plugin sources) | Require the ticket to be assigned to one Linear user, in addition to the state and label gates. |
|
|
1170
1171
|
| `teamKey` | — | required for Linear and hosted; forbidden for file; optional for plugins | Backend team key such as `"ENG"`, or an existing plugin config field. |
|
|
1171
1172
|
| `claimedState` | `"In Progress"` for direct Linear; hosted backend default otherwise | optional nonempty string; forbidden for file; allowed for plugins | Workflow state entered when a ticket is claimed. |
|
|
1172
1173
|
| `createState` | provider default | optional nonempty string | Default state for newly created tickets when `ab ticket create` omits `--state`. |
|
|
@@ -1174,13 +1175,33 @@ state. Within a present table, `source` and `readyState` are required.
|
|
|
1174
1175
|
| `proposalState` | the resolved `triageState` | optional nonempty string | State harvest files its synthesized proposals into. Setting it to `readyState` waives the human grooming gate, not dependency eligibility. |
|
|
1175
1176
|
| `dir` | file: selected local state root's `tickets/`; plugin: omitted | optional nonempty path; forbidden for Linear; allowed for plugins | Root containing file-source state directories, or an existing plugin config field. Relative file paths resolve from the repository. |
|
|
1176
1177
|
|
|
1178
|
+
`readyAssignee = "me"` resolves to the Linear user who owns `LINEAR_API_KEY`,
|
|
1179
|
+
so one committed value makes each person's local dispatcher take only the work
|
|
1180
|
+
assigned to them; an email names a specific user, for a shared or bot key. The
|
|
1181
|
+
gate is conjunctive with `readyState` and `readyLabels`, and a ticket with no
|
|
1182
|
+
assignee is never ready while it is set. It applies to every ready surface
|
|
1183
|
+
(dispatch, the queued count, and `ab ticket list` without filters); blockers
|
|
1184
|
+
still prevent claim. It is empty or blank, or neither `"me"` nor an email,
|
|
1185
|
+
fails validation. `ab init --validate` prints the user the gate resolves to and
|
|
1186
|
+
fails when an email matches no workspace user. The field reloads without a
|
|
1187
|
+
restart.
|
|
1188
|
+
|
|
1177
1189
|
When `readyLabels` is absent, direct Linear uses `["autobuild"]`; hosted, file,
|
|
1178
1190
|
and plugin sources use `[]`, meaning no host-imposed label gate. An explicit value always
|
|
1179
1191
|
wins. A nonempty list is
|
|
1180
1192
|
conjunctive: every configured label must be present. `readyState` remains
|
|
1181
1193
|
mandatory regardless of labels. Linear compares state and label names exactly
|
|
1182
|
-
and case-sensitively. The file source
|
|
1183
|
-
|
|
1194
|
+
and case-sensitively. The file source matches state names case-insensitively against the directories
|
|
1195
|
+
under the tracker root: the four lifecycle directories (`triage/`, `ready/`,
|
|
1196
|
+
`doing/`, `done/`, always present) plus any directory the repository adds, such
|
|
1197
|
+
as `icebox/`. A name matching no directory is an error listing those that
|
|
1198
|
+
exist, so creating a state stays an explicit act: `ab ticket state create <name>
|
|
1199
|
+
[--about <text>]` makes one, and `ab ticket states` lists every state with its
|
|
1200
|
+
roles, ticket count, and purpose. A purpose is stored as a one-line `.about` file
|
|
1201
|
+
inside the state directory, so it travels with a git-tracked `dir` and is never
|
|
1202
|
+
read as a ticket. `createState`, `readyState`,
|
|
1203
|
+
`triageState`, and `proposalState` may each name any existing directory;
|
|
1204
|
+
`ab ticket list` with no filter still lists only `readyState`.
|
|
1184
1205
|
|
|
1185
1206
|
Source-specific validation is strict:
|
|
1186
1207
|
|
|
@@ -297,6 +297,8 @@ it to exist. An unknown build returns `404 not-found`.
|
|
|
297
297
|
| `appendIfCurrent` | `POST /builds/{slug}/events/conditional` | `{"expectedSeq": nonnegative integer, "event": event write}` | appended: `201` + build event envelope; stream advanced: `200 null` |
|
|
298
298
|
| `getEvents` | `GET /builds/{slug}/events?since={n}&wait={s}` | optional `since` query value, parsed below; absence defaults to `0`. Optional `wait` in whole seconds, parsed per the event-wait rules below | `200` + envelopes whose `seq` is strictly greater than parsed `since`, in increasing sequence order |
|
|
299
299
|
| `appendWithArtifacts` | `POST /builds/{slug}/deposits` | atomic deposit request | `201` + `{event, artifacts}` |
|
|
300
|
+
| `getReducerSnapshot` | `GET /builds/{slug}/snapshots/{reducer}?version={n}` | required `version` query value, a positive integer; `reducer` is a registry name (`^[A-Za-z][A-Za-z0-9._-]{0,63}$`) | `200` + `{"version", "cursor", "state"}` or `200 null` when absent, written by another reducer version, or ahead of the build log's tail; an unknown build is `404`, which the shipped client also maps to `null` |
|
|
301
|
+
| `putReducerSnapshot` | `PUT /builds/{slug}/snapshots/{reducer}` | `{"version": int >= 1, "cursor": int >= 0, "state": JSON}`, at most 16 MiB (`413` above) | `200 {"written": boolean}`; the stored cursor never decreases, so a write at an equal or lower cursor (or an equal cursor at an equal or lower version) and one past the log's tail answer `written: false` |
|
|
300
302
|
| `putArtifact` | `POST /builds/{slug}/artifacts` | artifact input | `201` + build artifact metadata |
|
|
301
303
|
| `getArtifact` | `GET /builds/{slug}/artifacts?kind={kind}&rev={n}` | `kind` is required and nonempty; optional `rev` is parsed below | `200` + artifact read; an absent `rev` parameter selects the latest revision; a missing kind/revision is `200 null` |
|
|
302
304
|
| `listArtifacts` | `GET /builds/{slug}/artifact-list?kind={kind}` | optional `kind`; absence means all kinds | `200` + metadata ordered by kind and then increasing revision |
|
|
@@ -336,6 +338,32 @@ part of this protocol. Retention is enforced server-side inside the store at
|
|
|
336
338
|
deposit time (bounded pruning of dispatcher run/config artifact revisions),
|
|
337
339
|
not through any endpoint.
|
|
338
340
|
|
|
341
|
+
### Reducer snapshots
|
|
342
|
+
|
|
343
|
+
A reducer snapshot is one registered reducer's serialized state as of a log
|
|
344
|
+
cursor, kept per scope (one build log or one repository journal) and reducer
|
|
345
|
+
name. It is a cache the log regenerates, never an authority: a process with no
|
|
346
|
+
hot memory loads it, reads only the events newer than its cursor, advances the
|
|
347
|
+
reducer, and writes the advanced state back, and deleting every snapshot changes
|
|
348
|
+
no observable output. The routes above and in section 4 are additive and carry
|
|
349
|
+
no protocol-version bump. The store, not the client, enforces the invariants:
|
|
350
|
+
|
|
351
|
+
- the stored cursor never decreases, whatever the versions of the competing
|
|
352
|
+
writers; a write succeeds only when no row exists, when its cursor is above the
|
|
353
|
+
stored one, or when its cursor equals the stored one and its version is higher
|
|
354
|
+
(an upgraded reducer replacing the old shape at an unchanged tail);
|
|
355
|
+
- a read answers `null` for a row whose version differs from the requested one,
|
|
356
|
+
and for one whose cursor is beyond the scope's log tail; a write beyond the
|
|
357
|
+
tail is refused;
|
|
358
|
+
- an unknown build or repository answers `null` (read) or `written: false`
|
|
359
|
+
(write) and never creates the scope.
|
|
360
|
+
|
|
361
|
+
A client treats `404` and `405` from either route as "no snapshot", so a client
|
|
362
|
+
newer than its store replays the log with no error, and a client older than its
|
|
363
|
+
store never calls the routes. Authentication and transport failures still
|
|
364
|
+
surface normally. A build token may touch only its own build's snapshots and a
|
|
365
|
+
repository token only its own repository's, exactly as for events.
|
|
366
|
+
|
|
339
367
|
### The event-read bounded wait
|
|
340
368
|
|
|
341
369
|
Both event reads (`getEvents` and, symmetrically, the `getRepoEvents` read of
|
|
@@ -382,6 +410,8 @@ without a journal record, or a repository without builds.
|
|
|
382
410
|
| `appendRepo` | `POST /repos/{repo}/events` | event write | `201` + repository event envelope |
|
|
383
411
|
| `getRepoEvents` | `GET /repos/{repo}/events?since={n}&wait={s}` | optional `since` query value, parsed as in section 3; absence defaults to `0`. Optional `wait` in whole seconds, per the event-wait rules of section 3 | `200` + envelopes with `seq >` parsed `since`, in increasing sequence order |
|
|
384
412
|
| `getRepoStateEvents` | `GET /repos/{repo}/state-events` | none | `200` + the bounded repository-journal subset (AUT-489): every durable event type (`harvest.*`, `orchestrator.sandbox.*`, the three dispatcher setting types) plus, when the journal has one, the tail from the latest `dispatcher.run-started`, in increasing sequence order; served inside the repository-existence gate like `GET events`, so an unknown repository is `404` |
|
|
413
|
+
| `getReducerSnapshot` (repo scope) | `GET /repos/{repo}/snapshots/{reducer}?version={n}` | required `version` query value, a positive integer; `reducer` is a registry name (`^[A-Za-z][A-Za-z0-9._-]{0,63}$`) | `200` + `{"version", "cursor", "state"}` or `200 null` when absent, written by another reducer version, ahead of the journal's tail, or the repository has no journal record; an unknown repository is `404`, which the shipped client also maps to `null` |
|
|
414
|
+
| `putReducerSnapshot` (repo scope) | `PUT /repos/{repo}/snapshots/{reducer}` | `{"version": int >= 1, "cursor": int >= 0, "state": JSON}`, at most 16 MiB (`413` above) | `200 {"written": boolean}`; the stored cursor never decreases, so a write at an equal or lower cursor (or an equal cursor at an equal or lower version) and one past the journal's tail answer `written: false` |
|
|
385
415
|
| `appendRepoWithArtifacts` | `POST /repos/{repo}/deposits` | atomic deposit request | `201` + `{event, artifacts}` using repository shapes |
|
|
386
416
|
| `putRepoArtifact` | `POST /repos/{repo}/artifacts` | artifact input | `201` + repository artifact metadata |
|
|
387
417
|
| `getRepoArtifact` | `GET /repos/{repo}/artifacts?kind={kind}&rev={n}` | required nonempty `kind`; optional `rev` is parsed as in section 3 | `200` + artifact read; latest only when the `rev` parameter is absent; missing kind/revision is `200 null` |
|
|
@@ -834,6 +864,11 @@ resource access; an explicit resource whose id is `"*"` is not admin.
|
|
|
834
864
|
| repo | no | no | no | exact id only | yes | exact id only | exact id only | exact repo only | no |
|
|
835
865
|
| session | no | no | no | no | no | no | no | no | exact id only |
|
|
836
866
|
|
|
867
|
+
The snapshot routes (`/builds/{slug}/snapshots/{reducer}` and
|
|
868
|
+
`/repos/{repo}/snapshots/{reducer}`) follow the one-matching-build and
|
|
869
|
+
one-matching-repo columns: a build token reads and writes only its own build's
|
|
870
|
+
snapshots, a repo token only its own repository's.
|
|
871
|
+
|
|
837
872
|
A valid token used for the wrong resource receives `403 auth`. Resource scope
|
|
838
873
|
gates all operations, including reads, artifact operations, and leases. A
|
|
839
874
|
session resource token's authority is exactly its one session — it cannot
|
package/docs/website.md
CHANGED
|
@@ -5,6 +5,10 @@ The marketing site is one static page in `packages/website`, reproduced from the
|
|
|
5
5
|
- `bun run website:dev` serves it on port 3300 (`PORT` overrides), rebuilding on every request.
|
|
6
6
|
- `bun run website:build` writes `packages/website/dist/` (`index.html`, `site.css`, `site.js`).
|
|
7
7
|
|
|
8
|
+
The hero is the real `ab dispatch` dashboard: `packages/website/src/hero-frame.txt` holds the exact colored lines the terminal dashboard painted for the scripted `website-hero` capture, and the page renders them as preformatted HTML with each terminal color mapped to its design token. `bun run capture:website-hero` regenerates the file from the capture, and `bun run check` fails when it is stale.
|
|
9
|
+
|
|
10
|
+
Shared links preview through Open Graph and Twitter card tags in the page head, pointing at `og.png`, a 1200×630 card drawn from the same frame in the site's own styles. `bun run capture:website-og` screenshots it with a local Chromium into `packages/website/src/og.png`; rerun it after changing the headline, the hero frame, or the styles, and look at the result, since font rasterization differs between machines and nothing checks the bytes. The canonical origin every absolute URL is built from is `SITE_URL` in `packages/website/src/constants.ts`.
|
|
11
|
+
|
|
8
12
|
`dist/` is plain static files: any static host can serve it with no server runtime, credentials, or environment variables. On Vercel, set the root directory to `packages/website`, the build command to `bun run build`, and the output directory to `dist`.
|
|
9
13
|
|
|
10
14
|
The page loads one webfont from Google Fonts and makes no other third-party requests.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@defrex/autobuild",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Agent-driven software delivery from groomed ticket to merged PR",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -117,9 +117,11 @@
|
|
|
117
117
|
"website:dev": "bun packages/website/src/dev.ts",
|
|
118
118
|
"website:build": "bun packages/website/src/build.ts",
|
|
119
119
|
"capture:readme-headline": "bun tools/readme-headline.ts",
|
|
120
|
+
"capture:website-hero": "bun tools/website-hero.ts",
|
|
121
|
+
"capture:website-og": "bun tools/website-og.ts",
|
|
120
122
|
"release": "bun tools/release.ts",
|
|
121
123
|
"postgres:migrate": "bun packages/postgres-store/src/bin.ts migrate",
|
|
122
|
-
"check": "bun tools/biome-check.ts && bun tools/product-name-check.ts && bun tools/docs-asset-check.ts && bun tools/skill-docs-asset-check.ts && bun tools/readme-headline.ts --check && bun tools/workspace-manifest-check.ts && bun tools/package-boundary-check.ts && bun tools/bin-mode-check.ts && bun tools/publish-contents-check.ts && bun tools/publish-imports-check.ts",
|
|
124
|
+
"check": "bun tools/biome-check.ts && bun tools/product-name-check.ts && bun tools/docs-asset-check.ts && bun tools/skill-docs-asset-check.ts && bun tools/readme-headline.ts --check && bun tools/website-hero.ts --check && bun tools/workspace-manifest-check.ts && bun tools/package-boundary-check.ts && bun tools/bin-mode-check.ts && bun tools/publish-contents-check.ts && bun tools/publish-imports-check.ts",
|
|
123
125
|
"format:staged": "git-format-staged --formatter './node_modules/.bin/biome format --stdin-file-path {}' '*.js' '*.jsx' '*.ts' '*.tsx' '*.json' '*.jsonc' '*.css' '*.graphql' '*.gql'",
|
|
124
126
|
"prepare": "husky",
|
|
125
127
|
"test": "bun test packages/*/src tools bin packages/hosted-store-service/app",
|
|
@@ -137,8 +139,8 @@
|
|
|
137
139
|
},
|
|
138
140
|
"devDependencies": {
|
|
139
141
|
"@biomejs/biome": "2.5.5",
|
|
140
|
-
"@defrex/autobuild-postgres-store": "0.
|
|
141
|
-
"@defrex/autobuild-vercel-sandbox": "0.
|
|
142
|
+
"@defrex/autobuild-postgres-store": "0.9.0",
|
|
143
|
+
"@defrex/autobuild-vercel-sandbox": "0.9.0",
|
|
142
144
|
"@resvg/resvg-js": "2.6.2",
|
|
143
145
|
"@types/bun": "^1.3.14",
|
|
144
146
|
"@types/node": "^24.0.0",
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The facts a dashboard row reads from a build's event array, as an
|
|
3
|
+
* incremental reducer: the queued-row dispatch text, the session brackets, the
|
|
4
|
+
* per-phase wall-clock intervals, the pause timestamp, and the seqs of the four
|
|
5
|
+
* loop outputs. `projectBuildFromFacts` renders a row from these plus the
|
|
6
|
+
* reduced `BuildState`, so a row derives from a stored accumulator and a delta
|
|
7
|
+
* read with no event array, and `projectBuild(…, events, …)` is the same
|
|
8
|
+
* projection over `dashboardFactsReducer.reduce(events)` — one implementation,
|
|
9
|
+
* so the snapshot and replay paths agree by construction.
|
|
10
|
+
*
|
|
11
|
+
* The accumulator is plain JSON (see `kernel/incremental.ts`).
|
|
12
|
+
*/
|
|
13
|
+
import type { AbEvent } from '../../events/catalog'
|
|
14
|
+
import { defineReducer } from '../../kernel/incremental'
|
|
15
|
+
import { verifyPhase } from '../../ontology'
|
|
16
|
+
|
|
17
|
+
export const DASHBOARD_FACTS_REDUCER_VERSION = 1
|
|
18
|
+
|
|
19
|
+
/** One phase occurrence's wall-clock span, keyed by phase. `startSeq` is the
|
|
20
|
+
* seq of the `*.started` event, so callers can scope by the same seq
|
|
21
|
+
* boundaries (`restartSince`/`cycleSince`) the step STATES already use. */
|
|
22
|
+
export interface PhaseInterval {
|
|
23
|
+
start: number
|
|
24
|
+
end: number
|
|
25
|
+
startSeq: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface PhaseTiming {
|
|
29
|
+
closed: PhaseInterval[]
|
|
30
|
+
open?: { start: number; startSeq: number }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DispatchFailureFact {
|
|
34
|
+
seq: number
|
|
35
|
+
stage: string
|
|
36
|
+
attempt: number
|
|
37
|
+
error: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Max seq per round of one loop output event. */
|
|
41
|
+
export type RoundSeqs = Record<string, number>
|
|
42
|
+
|
|
43
|
+
type SessionEvent = Extract<AbEvent, { type: 'session.started' | 'session.ended' }>
|
|
44
|
+
|
|
45
|
+
export interface DashboardFacts {
|
|
46
|
+
/** Seq of the latest event of each queued-row kind (`undefined` until seen). */
|
|
47
|
+
created?: number
|
|
48
|
+
workspaceProvisioned?: number
|
|
49
|
+
workspaceReleased?: number
|
|
50
|
+
specLanded?: number
|
|
51
|
+
commentPosted?: number
|
|
52
|
+
latestFailure?: DispatchFailureFact
|
|
53
|
+
/** The session brackets, in log order; `projectSessions` pairs them. */
|
|
54
|
+
sessionEvents: SessionEvent[]
|
|
55
|
+
intervals: Record<string, PhaseTiming>
|
|
56
|
+
/** Timestamp of the latest `build.paused`. */
|
|
57
|
+
pausedAt?: string
|
|
58
|
+
produced: {
|
|
59
|
+
planCompleted: RoundSeqs
|
|
60
|
+
planReviewVerdict: RoundSeqs
|
|
61
|
+
implementCompleted: RoundSeqs
|
|
62
|
+
codeReviewVerdict: RoundSeqs
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Whether a loop output of `round` landed after `since` (the restart boundary). */
|
|
67
|
+
export function producedAfter(seqs: RoundSeqs, round: number, since: number): boolean {
|
|
68
|
+
const seq = seqs[String(round)]
|
|
69
|
+
return seq !== undefined && seq > since
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function raise(seqs: RoundSeqs, round: number, seq: number): void {
|
|
73
|
+
const key = String(round)
|
|
74
|
+
const current = seqs[key]
|
|
75
|
+
seqs[key] = current === undefined ? seq : Math.max(current, seq)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function foldInterval(intervals: Record<string, PhaseTiming>, ev: AbEvent): void {
|
|
79
|
+
const ms = Date.parse(ev.ts)
|
|
80
|
+
const get = (key: string): PhaseTiming => {
|
|
81
|
+
let t = intervals[key]
|
|
82
|
+
if (t === undefined) {
|
|
83
|
+
t = { closed: [] }
|
|
84
|
+
intervals[key] = t
|
|
85
|
+
}
|
|
86
|
+
return t
|
|
87
|
+
}
|
|
88
|
+
const open = (key: string): void => {
|
|
89
|
+
get(key).open = { start: ms, startSeq: ev.seq }
|
|
90
|
+
}
|
|
91
|
+
const close = (key: string): void => {
|
|
92
|
+
const t = get(key)
|
|
93
|
+
if (t.open === undefined) return
|
|
94
|
+
t.closed.push({ start: t.open.start, end: ms, startSeq: t.open.startSeq })
|
|
95
|
+
delete t.open
|
|
96
|
+
}
|
|
97
|
+
// Each `*.started` opens an interval for its phase key and its terminal event
|
|
98
|
+
// (`.completed`/`.verdict`) closes it. A second `*.started` while one is still
|
|
99
|
+
// open REPLACES the open start — a §15.6-C cross-sandbox re-run starts the
|
|
100
|
+
// phase afresh, so the crashed attempt contributes nothing. Finalize
|
|
101
|
+
// post-steps (`finalize.step-completed`) have no `.started` and no interval.
|
|
102
|
+
switch (ev.type) {
|
|
103
|
+
case 'plan.started':
|
|
104
|
+
open('plan')
|
|
105
|
+
break
|
|
106
|
+
case 'plan.completed':
|
|
107
|
+
close('plan')
|
|
108
|
+
break
|
|
109
|
+
case 'plan-review.started':
|
|
110
|
+
open('plan-review')
|
|
111
|
+
break
|
|
112
|
+
case 'plan-review.verdict':
|
|
113
|
+
close('plan-review')
|
|
114
|
+
break
|
|
115
|
+
case 'implement.started':
|
|
116
|
+
open('implement')
|
|
117
|
+
break
|
|
118
|
+
case 'implement.completed':
|
|
119
|
+
close('implement')
|
|
120
|
+
break
|
|
121
|
+
case 'code-review.started':
|
|
122
|
+
open('code-review')
|
|
123
|
+
break
|
|
124
|
+
case 'code-review.verdict':
|
|
125
|
+
close('code-review')
|
|
126
|
+
break
|
|
127
|
+
case 'verify.started':
|
|
128
|
+
open(verifyPhase(ev.payload.step))
|
|
129
|
+
break
|
|
130
|
+
case 'verify.completed':
|
|
131
|
+
close(verifyPhase(ev.payload.step))
|
|
132
|
+
break
|
|
133
|
+
case 'finalize.started':
|
|
134
|
+
open('finalize')
|
|
135
|
+
break
|
|
136
|
+
case 'finalize.completed':
|
|
137
|
+
close('finalize')
|
|
138
|
+
break
|
|
139
|
+
case 'reconcile.started':
|
|
140
|
+
open('reconcile')
|
|
141
|
+
break
|
|
142
|
+
case 'reconcile.completed':
|
|
143
|
+
close('reconcile')
|
|
144
|
+
break
|
|
145
|
+
default:
|
|
146
|
+
break
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const dashboardFactsReducer = defineReducer<DashboardFacts, AbEvent, DashboardFacts>({
|
|
151
|
+
version: DASHBOARD_FACTS_REDUCER_VERSION,
|
|
152
|
+
initial: () => ({
|
|
153
|
+
sessionEvents: [],
|
|
154
|
+
intervals: {},
|
|
155
|
+
produced: {
|
|
156
|
+
planCompleted: {},
|
|
157
|
+
planReviewVerdict: {},
|
|
158
|
+
implementCompleted: {},
|
|
159
|
+
codeReviewVerdict: {},
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
fold(acc, events) {
|
|
163
|
+
for (const event of events) {
|
|
164
|
+
foldInterval(acc.intervals, event)
|
|
165
|
+
switch (event.type) {
|
|
166
|
+
case 'build.created':
|
|
167
|
+
acc.created = event.seq
|
|
168
|
+
break
|
|
169
|
+
case 'workspace.provisioned':
|
|
170
|
+
acc.workspaceProvisioned = event.seq
|
|
171
|
+
break
|
|
172
|
+
case 'workspace.released':
|
|
173
|
+
acc.workspaceReleased = event.seq
|
|
174
|
+
break
|
|
175
|
+
case 'spec.imported':
|
|
176
|
+
case 'spec.authored':
|
|
177
|
+
acc.specLanded = event.seq
|
|
178
|
+
break
|
|
179
|
+
case 'dispatch.comment-posted':
|
|
180
|
+
acc.commentPosted = event.seq
|
|
181
|
+
break
|
|
182
|
+
case 'dispatch.failed':
|
|
183
|
+
acc.latestFailure = {
|
|
184
|
+
seq: event.seq,
|
|
185
|
+
stage: event.payload.stage,
|
|
186
|
+
attempt: event.payload.attempt,
|
|
187
|
+
error: event.payload.error,
|
|
188
|
+
}
|
|
189
|
+
break
|
|
190
|
+
case 'session.started':
|
|
191
|
+
case 'session.ended':
|
|
192
|
+
acc.sessionEvents.push(event)
|
|
193
|
+
break
|
|
194
|
+
case 'build.paused':
|
|
195
|
+
acc.pausedAt = event.ts
|
|
196
|
+
break
|
|
197
|
+
case 'plan.completed':
|
|
198
|
+
raise(acc.produced.planCompleted, event.payload.round, event.seq)
|
|
199
|
+
break
|
|
200
|
+
case 'plan-review.verdict':
|
|
201
|
+
raise(acc.produced.planReviewVerdict, event.payload.round, event.seq)
|
|
202
|
+
break
|
|
203
|
+
case 'implement.completed':
|
|
204
|
+
raise(acc.produced.implementCompleted, event.payload.round, event.seq)
|
|
205
|
+
break
|
|
206
|
+
case 'code-review.verdict':
|
|
207
|
+
raise(acc.produced.codeReviewVerdict, event.payload.round, event.seq)
|
|
208
|
+
break
|
|
209
|
+
default:
|
|
210
|
+
break
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
finish: (acc) => structuredClone(acc),
|
|
215
|
+
})
|