@defrex/autobuild 0.9.0 → 0.10.1

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.
Files changed (85) hide show
  1. package/README.md +97 -113
  2. package/SPEC.md +17 -4
  3. package/docs/architecture.md +109 -0
  4. package/docs/configuration.md +26 -7
  5. package/docs/remote-store-protocol.md +35 -0
  6. package/docs/website.md +4 -0
  7. package/package.json +6 -4
  8. package/packages/core/src/cli/dashboard/facts.ts +215 -0
  9. package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
  10. package/packages/core/src/cli/dashboard/model.ts +85 -147
  11. package/packages/core/src/cli/dashboard/poll.ts +7 -2
  12. package/packages/core/src/cli/dispatch-process.ts +1 -13
  13. package/packages/core/src/cli/dispatch.ts +80 -37
  14. package/packages/core/src/cli/help.ts +15 -1
  15. package/packages/core/src/cli/init-validation.ts +18 -0
  16. package/packages/core/src/cli/init.ts +3 -2
  17. package/packages/core/src/cli/installation.ts +50 -8
  18. package/packages/core/src/cli/self-update.ts +77 -2
  19. package/packages/core/src/cli/status.ts +1 -1
  20. package/packages/core/src/cli/ticket.ts +146 -1
  21. package/packages/core/src/cli/wait.ts +10 -2
  22. package/packages/core/src/cli/watch.ts +10 -2
  23. package/packages/core/src/config/live.ts +1 -0
  24. package/packages/core/src/config/pipeline-source.ts +17 -5
  25. package/packages/core/src/config/schema.ts +21 -0
  26. package/packages/core/src/integration/harness.ts +113 -1
  27. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
  28. package/packages/core/src/kernel/auto-merge-default.ts +43 -9
  29. package/packages/core/src/kernel/auto-merge.ts +99 -22
  30. package/packages/core/src/kernel/dispatch-settings.ts +42 -17
  31. package/packages/core/src/kernel/dispatch-status.ts +23 -0
  32. package/packages/core/src/kernel/engine.ts +9 -438
  33. package/packages/core/src/kernel/generators/build-log.ts +259 -0
  34. package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
  35. package/packages/core/src/kernel/generators/session-log.ts +72 -0
  36. package/packages/core/src/kernel/harvest.ts +62 -18
  37. package/packages/core/src/kernel/incremental-contract.ts +75 -0
  38. package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
  39. package/packages/core/src/kernel/incremental.ts +53 -0
  40. package/packages/core/src/kernel/log-index.ts +620 -0
  41. package/packages/core/src/kernel/pr-attachments.ts +164 -46
  42. package/packages/core/src/kernel/reducer-registry.ts +164 -0
  43. package/packages/core/src/kernel/reducer.ts +216 -74
  44. package/packages/core/src/operator/query.ts +68 -28
  45. package/packages/core/src/operator/read-view.ts +98 -0
  46. package/packages/core/src/orchestrator/prompt.ts +3 -1
  47. package/packages/core/src/ports/tickets/fake.ts +5 -1
  48. package/packages/core/src/ports/tickets/file.ts +169 -48
  49. package/packages/core/src/ports/tickets/linear.ts +33 -1
  50. package/packages/core/src/ports/tickets/operations.ts +1 -1
  51. package/packages/core/src/ports/tickets/remote.ts +8 -1
  52. package/packages/core/src/ports/types.ts +32 -1
  53. package/packages/core/src/processes/build-execution-state.ts +31 -15
  54. package/packages/core/src/processes/build-runner.ts +184 -90
  55. package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
  56. package/packages/core/src/processes/dispatcher.ts +270 -176
  57. package/packages/core/src/processes/execution-settlement.ts +72 -39
  58. package/packages/core/src/processes/harvest-execution-state.ts +41 -29
  59. package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
  60. package/packages/core/src/processes/orchestrator-tick.ts +44 -3
  61. package/packages/core/src/processes/publication-state.ts +164 -52
  62. package/packages/core/src/processes/repo-view.ts +1436 -0
  63. package/packages/core/src/processes/sandbox-state.ts +26 -2
  64. package/packages/core/src/processes/tick-state.ts +33 -0
  65. package/packages/core/src/store/adapter.ts +10 -1
  66. package/packages/core/src/store/build-scope.ts +26 -0
  67. package/packages/core/src/store/contract.ts +337 -0
  68. package/packages/core/src/store/digest.ts +71 -18
  69. package/packages/core/src/store/local/schema.ts +17 -0
  70. package/packages/core/src/store/local/store.ts +111 -4
  71. package/packages/core/src/store/memory.ts +68 -1
  72. package/packages/core/src/store/phase-session.ts +23 -0
  73. package/packages/core/src/store/remote/client.ts +50 -1
  74. package/packages/core/src/store/remote/index.ts +2 -0
  75. package/packages/core/src/store/remote/protocol.ts +15 -0
  76. package/packages/core/src/store/repo-state-events.ts +72 -7
  77. package/packages/core/src/store/session-handle.ts +30 -0
  78. package/packages/core/src/store/session-reducer.ts +52 -11
  79. package/packages/core/src/store/snapshots.ts +88 -0
  80. package/packages/core/src/store/ticket-assets.ts +41 -8
  81. package/packages/core/src/store/types.ts +38 -0
  82. package/skills/guide/SKILL.md +38 -21
  83. package/skills/guide/references/remote-store-protocol.md +35 -0
  84. package/skills/operate/SKILL.md +146 -19
  85. package/skills/tickets/SKILL.md +57 -5
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Reducer snapshots: a cache beside the event log, never an authority.
3
+ *
4
+ * For one scope (a build log or a repository journal) and one registered
5
+ * reducer, the store may keep that reducer's serialized accumulator as of a
6
+ * cursor, so a process with no hot memory loads it, reads only the events
7
+ * newer than the cursor, and advances — instead of replaying the whole log.
8
+ *
9
+ * The log always regenerates a snapshot. Every consumer treats a missing,
10
+ * version-mismatched, ahead-of-log, malformed or unreadable snapshot as a miss
11
+ * and replays; deleting every snapshot changes no observable output.
12
+ */
13
+ import { z } from 'zod'
14
+ import type { IncrementalReducer } from '../kernel/incremental'
15
+ import type { BuildStore } from './types'
16
+
17
+ export type SnapshotScope = { kind: 'build'; slug: string } | { kind: 'repo'; repo: string }
18
+
19
+ export interface ReducerSnapshot {
20
+ /** The reducer version that wrote `state`; a reader on another version
21
+ * ignores the snapshot. */
22
+ version: number
23
+ /** The greatest event seq folded into `state`. */
24
+ cursor: number
25
+ /** The reducer's JSON accumulator as of `cursor`. */
26
+ state: unknown
27
+ }
28
+
29
+ export const reducerSnapshotWireSchema = z.object({
30
+ version: z.number().int().min(1),
31
+ cursor: z.number().int().min(0),
32
+ state: z.unknown(),
33
+ })
34
+
35
+ /** The registry name a snapshot is keyed by: a short identifier, safe in a URL
36
+ * path segment and a SQL key. */
37
+ export const SNAPSHOT_REDUCER_NAME = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/
38
+
39
+ export function snapshotScopeKey(scope: SnapshotScope): string {
40
+ return scope.kind === 'build' ? `build:${scope.slug}` : `repo:${scope.repo}`
41
+ }
42
+
43
+ /** Whether a put replaces the stored row. The stored cursor never decreases:
44
+ * a higher cursor wins whatever the versions, and an upgraded reducer replaces
45
+ * an older shape at an unchanged cursor. */
46
+ export function snapshotSupersedes(
47
+ next: Pick<ReducerSnapshot, 'version' | 'cursor'>,
48
+ stored: Pick<ReducerSnapshot, 'version' | 'cursor'> | undefined,
49
+ ): boolean {
50
+ if (stored === undefined) return true
51
+ if (next.cursor !== stored.cursor) return next.cursor > stored.cursor
52
+ return next.version > stored.version
53
+ }
54
+
55
+ type SnapshotStore = Pick<BuildStore, 'getReducerSnapshot' | 'putReducerSnapshot'>
56
+
57
+ /** Read a snapshot, degrading every failure to a miss. */
58
+ export async function loadReducerSnapshot(
59
+ store: Partial<SnapshotStore>,
60
+ scope: SnapshotScope,
61
+ name: string,
62
+ reducer: Pick<IncrementalReducer<unknown, unknown, unknown>, 'version'>,
63
+ ): Promise<ReducerSnapshot | null> {
64
+ if (store.getReducerSnapshot === undefined) return null
65
+ try {
66
+ const found = await store.getReducerSnapshot(scope, name, reducer.version)
67
+ if (found === null || found.version !== reducer.version) return null
68
+ if (!Number.isInteger(found.cursor) || found.cursor < 0) return null
69
+ return found
70
+ } catch {
71
+ return null
72
+ }
73
+ }
74
+
75
+ /** Write a snapshot best-effort; a failing store never reaches the caller. */
76
+ export async function persistReducerSnapshot(
77
+ store: Partial<SnapshotStore>,
78
+ scope: SnapshotScope,
79
+ name: string,
80
+ snapshot: ReducerSnapshot,
81
+ ): Promise<boolean> {
82
+ if (store.putReducerSnapshot === undefined) return false
83
+ try {
84
+ return await store.putReducerSnapshot(scope, name, snapshot)
85
+ } catch {
86
+ return false
87
+ }
88
+ }
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { AbEvent } from '../events/catalog'
12
12
  import type { PinnedAsset } from '../events/payloads'
13
+ import { defineReducer } from '../kernel/incremental'
13
14
  import { contentHash, type BlobStore, type BuildStore } from './types'
14
15
 
15
16
  export type { PinnedAsset }
@@ -379,19 +380,40 @@ export async function samplePinnedAssets(
379
380
  return (await store.listTicketAssets(repo, ticketId)).map(pinnedAssetOf)
380
381
  }
381
382
 
382
- /** Latest-pinned revision of `kind/name` as the build's events record it, or
383
- * (with `rev`) the revision when any pin in the log named it. */
384
- export function findPinnedRevision(
385
- events: AbEvent[],
383
+ export const PINNED_ASSETS_REDUCER_VERSION = 1
384
+
385
+ /** The asset sets recorded by every `build.created`/`spec.revised` pin event
386
+ * that carries one, in array order. Each pin event replaces the set, so the
387
+ * newest decides the live pin; all of them answer "was this revision ever
388
+ * named". `pinnedRevision` is the query over the finished state. */
389
+ export const pinnedAssetsReducer = defineReducer<
390
+ { pins: PinnedAsset[][] },
391
+ AbEvent,
392
+ { pins: PinnedAsset[][] }
393
+ >({
394
+ version: PINNED_ASSETS_REDUCER_VERSION,
395
+ initial: () => ({ pins: [] }),
396
+ fold(acc, events) {
397
+ for (const event of events) {
398
+ if (event.type !== 'build.created' && event.type !== 'spec.revised') continue
399
+ const assets = event.payload.assets
400
+ if (assets === undefined) continue
401
+ acc.pins.push(assets)
402
+ }
403
+ },
404
+ finish: (acc) => ({ pins: acc.pins.slice() }),
405
+ })
406
+
407
+ /** Latest-pinned revision of `kind/name` over a reduced pin state, or (with
408
+ * `rev`) the revision when any pin named it. */
409
+ export function pinnedRevision(
410
+ state: { pins: readonly (readonly PinnedAsset[])[] },
386
411
  kind: string,
387
412
  name: string,
388
413
  rev?: number,
389
414
  ): number | undefined {
390
415
  let latest: number | undefined
391
- for (const event of events) {
392
- if (event.type !== 'build.created' && event.type !== 'spec.revised') continue
393
- const assets = event.payload.assets
394
- if (assets === undefined) continue
416
+ for (const assets of state.pins) {
395
417
  if (rev !== undefined) {
396
418
  if (assets.some((a) => a.kind === kind && a.name === name && a.revision === rev)) return rev
397
419
  continue
@@ -403,6 +425,17 @@ export function findPinnedRevision(
403
425
  return latest
404
426
  }
405
427
 
428
+ /** Latest-pinned revision of `kind/name` as the build's events record it, or
429
+ * (with `rev`) the revision when any pin in the log named it. */
430
+ export function findPinnedRevision(
431
+ events: AbEvent[],
432
+ kind: string,
433
+ name: string,
434
+ rev?: number,
435
+ ): number | undefined {
436
+ return pinnedRevision(pinnedAssetsReducer.reduce(events), kind, name, rev)
437
+ }
438
+
406
439
  /** `BuildStore.getPinnedTicketAsset` for any adapter: read the build's own
407
440
  * events to find the pinned revision, then fetch exactly that revision from the
408
441
  * ticket's assets. Earlier revisions stay retrievable after a replace or a
@@ -26,6 +26,8 @@ import type {
26
26
  } from '../events/sessions'
27
27
  export type { SessionEventEnvelope }
28
28
  import type { TicketRef } from '../ontology'
29
+ import type { ReducerSnapshot, SnapshotScope } from './snapshots'
30
+ export type { ReducerSnapshot, SnapshotScope }
29
31
  import type {
30
32
  TicketAsset,
31
33
  TicketAssetInput,
@@ -115,6 +117,12 @@ export interface BuildDigest {
115
117
  * when any of its merges is strictly after the oldest unclaimed
116
118
  * observation). */
117
119
  merged?: string
120
+ /** A `pr-attachment.hosted` has no later matching `pr-attachment.reclaimed`
121
+ * (the `pendingPrAttachmentReclaims` rule). Absent otherwise. */
122
+ reclaimPending?: true
123
+ /** The latest `execution.started` has no later `execution.ended` with the
124
+ * same `instance` (the `openExecution` rule). Absent otherwise. */
125
+ executionOpen?: true
118
126
  }
119
127
 
120
128
  export interface RepositoryRecord {
@@ -365,6 +373,36 @@ export interface BuildStore {
365
373
  * replay yields (`projectRepositoryStateEvents` is the normative oracle).
366
374
  * Unknown repo rejects like `getRepoEvents`. */
367
375
  getRepoStateEvents(repo: string): Promise<RepositoryEvent[]>
376
+ /** The journal's true high-water mark: the greatest assigned seq of any
377
+ * event type, 0 for an empty journal. Returns no event rows — the cursor a
378
+ * long-lived reader (the dispatcher's repository view) needs to resume with
379
+ * `getRepoEvents(repo, cursor)` even when the bounded subset excludes the
380
+ * journal's trailing events. Unknown repo rejects like `getRepoEvents`;
381
+ * never an `ensureRepo` side effect. */
382
+ getRepoHighWater(repo: string): Promise<number>
383
+ /** The stored reducer snapshot for a scope (a build log or a repository
384
+ * journal), a cache beside the log and never an authority. `null` when
385
+ * absent, when the stored version differs from `version`, when the stored
386
+ * cursor is ahead of the scope's log tail, or when the scope is unknown (no
387
+ * `ensureRepo` side effect, no rejection). A store without snapshot storage
388
+ * answers `null`. */
389
+ getReducerSnapshot(
390
+ scope: SnapshotScope,
391
+ reducer: string,
392
+ version: number,
393
+ ): Promise<ReducerSnapshot | null>
394
+ /** Upsert a reducer snapshot; the stored cursor never decreases. Writes only
395
+ * when no row exists, when `cursor` is above the stored one (whatever the
396
+ * versions — a mismatched row is replaced as soon as any writer advances
397
+ * past it), or when the cursor is equal and `version` is higher. Everything
398
+ * else, and any cursor beyond the log tail, is a no-op. Atomic, so
399
+ * concurrent writers cannot undo each other's progress. Resolves `true`
400
+ * when the row was written. */
401
+ putReducerSnapshot(
402
+ scope: SnapshotScope,
403
+ reducer: string,
404
+ snapshot: ReducerSnapshot,
405
+ ): Promise<boolean>
368
406
  putRepoArtifact(repo: string, artifact: ArtifactInput): Promise<RepositoryArtifactMeta>
369
407
  getRepoArtifact(repo: string, kind: string, rev?: number): Promise<RepositoryArtifact | null>
370
408
  listRepoArtifacts(repo: string, kind?: string): Promise<RepositoryArtifactMeta[]>
@@ -732,7 +732,8 @@ state eligible.
732
732
  |---|---|---|---|
733
733
  | `source` | — | **required**, nonblank builtin or plugin registration name | Which provider backs all ticket operations. Unknown names report every available builtin and plugin source. |
734
734
  | `readyLabels` | — (source-aware) | optional; array of nonempty strings | A ticket must carry **every** listed label to be dispatchable. `[]` = **no label gate**. Absent uses the source default below. |
735
- | `readyState` | — | **required**, non-blank string | The one workflow state a ticket must sit in to be dispatchable. Linear matches exactly and case-sensitively; file canonicalizes it to a state directory (`ready` → `ready/`). There is no any-state mode. |
735
+ | `readyState` | — | **required**, non-blank string | The one workflow state a ticket must sit in to be dispatchable. Linear matches exactly and case-sensitively; file matches it case-insensitively to a state directory (`ready` → `ready/`). There is no any-state mode. |
736
+ | `readyAssignee` | — | optional; `"me"` or an email; `linear` only | Also require the ticket to be assigned to that Linear user (`me` = owner of `LINEAR_API_KEY`); unassigned tickets are never ready. |
736
737
  | `teamKey` | — | required by `linear`, forbidden by `file`, allowed for plugins | The Linear team key (e.g. `"ENG"`) or an existing plugin configuration field. |
737
738
  | `claimedState` | — | optional nonempty string; forbidden by `file`, allowed for plugins | Workflow state `claim()` moves a ticket to when a build starts. |
738
739
  | `createState` | — | optional, nonempty string | Default state for new tickets when a create does not name one. Absent = the provider's default (Linear: the team's default, e.g. Backlog; file: Triage). |
@@ -772,11 +773,16 @@ Cross-field rules, each an **error**:
772
773
  janitor completion. Every source-agnostic `ab ticket` command loads and uses
773
774
  the same configured registration.
774
775
 
775
- The file tracker is **directory-per-state**: `<dir>/<state>/<id>.md` over
776
- `triage/ ready/ doing/ done/`. The directory *is* the state, so a transition or
776
+ The file tracker is **directory-per-state**: `<dir>/<state>/<id>.md`, where
777
+ every directory directly under the root is a state: the four lifecycle
778
+ directories `triage/ ready/ doing/ done/` (always present, created on first use)
779
+ plus any others the repository adds, such as `icebox/`. A state exists when its
780
+ directory exists; state names match directories case-insensitively, and a name
781
+ with no directory is an error listing those that exist. The directory *is* the
782
+ state, so a transition or
777
783
  claim is a rename — frontmatter carries no `state`/`claimedBy`, and a ticket
778
784
  body survives byte-exactly because a move never rewrites the file. A malformed
779
- record in any lifecycle directory is diagnosed with its path and validation
785
+ record in any state directory is diagnosed with its path and validation
780
786
  problem, left untouched, and excluded from listing/claim so unrelated valid
781
787
  ready work can continue. `ab ticket list` sends those diagnostics to stderr and
782
788
  keeps JSON stdout as one bare valid-ticket array. Tracker-wide safety checks
@@ -784,8 +790,8 @@ remain fatal: the same id in two state dirs is a loud error naming both paths,
784
790
  as are root-level ticket files with no state and filesystem failures. When
785
791
  `dir` is defaulted, the backlog writes its own `.gitignore` of `*`, so git never sees it; an
786
792
  explicit `dir` is the user's and is left alone. Agents and operators drive it
787
- through the source-agnostic `ab ticket` commands rather than running `mv` by
788
- hand.
793
+ through the source-agnostic `ab ticket` commands rather than running `mv` or
794
+ `mkdir` by hand; `ab ticket state create` makes a new state.
789
795
 
790
796
  **Secrets never live in this file.** `LINEAR_API_KEY` and plugin-declared
791
797
  `requiredEnv` credentials are environment variables (a local `.env` works).
@@ -893,7 +899,7 @@ repo path, needs no `AB_*` environment, and is safe to re-run. It:
893
899
  base for `ab upgrade`'s three-way merges.
894
900
  - Rewrites frontmatter on install: `name` → `ab-<name>`, and
895
901
  `disable-model-invocation: true` on every skill outside the model-invocable
896
- set (`ab-spec`, `ab-tickets`, `ab-guide`).
902
+ set (`ab-spec`, `ab-tickets`, `ab-guide`, `ab-operate`).
897
903
 
898
904
  The final report states `autobuild.toml: written|skipped`, aggregates all skill
899
905
  counts, names only attention-worthy `kept` and `overwritten` skills, and reports
@@ -1063,6 +1069,19 @@ with Linear and the file tracker, without provider-specific API/MCP calls:
1063
1069
  labeled metadata and the body verbatim, so it can read a stored spec back.
1064
1070
  - `ab ticket move <id> <state> [--json]` transitions one ticket and reports its
1065
1071
  post-transition value.
1072
+ - `ab ticket states [--json]` lists every state of the configured source:
1073
+ lifecycle states first (triage, ready, doing, done), then custom states
1074
+ alphabetically. Each row has the state name, the roles it plays under the
1075
+ current config (create, ready, claimed, triage, proposal, done, or none), its
1076
+ ticket count, and its purpose when set. `--json` emits one bare array of
1077
+ `{name, roles, tickets, about?}`.
1078
+ - `ab ticket state create <name> [--about <text>] [--json]` makes a state, for
1079
+ example `icebox`. It is idempotent: an existing state is reported as already
1080
+ existing, and `--about` (one line) records or replaces its purpose. A name is
1081
+ one to forty lowercase letters, digits, and hyphens starting with a letter,
1082
+ and may not match an existing state case-insensitively. There is no rename or
1083
+ delete. Only the file source supports `states` and `state create`; other
1084
+ sources exit nonzero naming the source and the unsupported operation.
1066
1085
 
1067
1086
  State names and ids are source-local. For block and unblock, the first id is the
1068
1087
  ticket being changed and the comma-separated second operand names its blockers.
@@ -1849,7 +1868,7 @@ default, when you need to know what this repo's version says).
1849
1868
  | Skill | Place in the lifecycle | Purpose |
1850
1869
  |---|---|---|
1851
1870
  | `ab-spec` | Before a build exists | Design a feature spec-first through conversation, or flesh out a ticket to the spec standard. The human-interactive surface; takes a ticket, not a build slug. **Model-invocable.** |
1852
- | `ab-tickets` | Before a build exists | Drive this repo's local file tracker: create a ticket, report the backlog, groom or move one between `triage/ ready/ doing/ done/`. The agent-facing surface on the tracker — use it instead of `mv`. **Model-invocable.** |
1871
+ | `ab-tickets` | Before a build exists | Drive this repo's local file tracker: create a ticket, report the backlog, groom or move one between the lifecycle directories (`triage/ ready/ doing/ done/`) and any repository-defined states, including the icebox. The agent-facing surface on the tracker — use it instead of `mv`. **Model-invocable.** |
1853
1872
  | `ab-guide` | Outside the pipeline | This skill: reference for the lifecycle, config surface, setup/upgrade behavior, and the installed skills. Its editable `references/setup.md` guides the init handoff. **Model-invocable.** |
1854
1873
  | `ab-harvest` | harvest `synthesize` step | Continue the producer across review rounds: cluster the claimed structured observations and author typed spec-standard create/join/suppress proposals. Runner-only. |
1855
1874
  | `ab-harvest-review` | harvest `review` step | Fresh adversarial reviewer for proposal coverage, semantic dedup, spec quality, and evidence; returns `approve`/`revise`/`escalate`. Runner-only. |
@@ -1859,16 +1878,14 @@ default, when you need to know what this repo's version says).
1859
1878
  | `ab-code-review` | `code-review` phase | Fresh skeptic: review the implementation diff against spec and plan, same verdict vocabulary. |
1860
1879
  | `ab-reconcile` | `reconcile` phase (epilogue) | Resolve a conflicted PR with one merge commit, base merged *into* the build branch. Never rebases. |
1861
1880
  | `ab-finalize` | `finalize` phase | Write the PR description for a green build; the kernel opens the PR. |
1862
- | `ab-operate` | hosted only | The embedded orchestrator's own operating manual: its role, the registry tool surface with the approval semantics, the attention set, wake-trigger input, budget/approval suspension, and when to escalate to the operator rather than act. Invoked by the embedded orchestrator, never vendored into a build session. |
1863
-
1864
- Autobuild installs 12 skills. Everything except `ab-spec`, `ab-tickets`, and
1865
- `ab-guide` carries `disable-model-invocation: true` — do not invoke a phase
1866
- skill yourself, and do not remove that key to make one convenient to call. A
1867
- model starting a pipeline phase by pattern-matching a description is exactly
1868
- what the flag prevents. Those three exceptions drive no build phase, which is
1869
- the criterion for membership (§16.3): `ab-spec` and `ab-tickets` are
1870
- human/agent-facing pre-build surfaces, and `ab-guide` is read-only reference
1871
- including the setup handoff guidance. The fourth skill outside the
1872
- model-invocable set, `ab-operate`, is not a build-phase skill either: it is the
1873
- embedded orchestrator's own operating manual, read by the hosted turn runner as
1874
- its system prompt and never invoked by a model in a build session.
1881
+ | `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). |
1882
+
1883
+ Autobuild installs 12 skills. Everything except `ab-spec`, `ab-tickets`,
1884
+ `ab-guide`, and `ab-operate` carries `disable-model-invocation: true` — do not
1885
+ invoke a phase skill yourself, and do not remove that key to make one
1886
+ convenient to call. A model starting a pipeline phase by pattern-matching a
1887
+ description is exactly what the flag prevents. Those four exceptions drive no
1888
+ build phase, which is the criterion for membership (§16.3): `ab-spec` and
1889
+ `ab-tickets` are human/agent-facing pre-build surfaces, `ab-guide` is
1890
+ read-only reference including the setup handoff guidance, and `ab-operate` is
1891
+ the manual for the orchestrator role, which any agent may take on.
@@ -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
@@ -1,17 +1,140 @@
1
1
  ---
2
2
  name: operate
3
- description: You are the embedded orchestrator for one repository - the agent that answers its operator sessions, watches its builds, unblocks them, and discusses direction. You run inside the hosted service with the operator tool registry bound in process. Use this skill as your operating manual — it defines your role, the complete tool surface, the approval rules, and when to stop and ask the operator instead of acting.
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
- You are the orchestrator session for one repository. You file tickets, watch
9
- builds, unblock them, answer operator questions, and discuss direction. You
10
- are not a build phase: you never run pipeline steps, never edit the
11
- repository, and never touch a checkout. Your entire reach is the operator
12
- tool registry bound into your turn.
13
-
14
- ## How you run
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.
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) is watched. `--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
+ ### Keep watching until the work is done
57
+
58
+ Checking once is not shepherding. When you take responsibility for builds:
59
+
60
+ 1. Record the set you are shepherding (`ab builds --all --json`, or the slugs
61
+ you were given).
62
+ 2. Arm an event-only watch and keep its cursor:
63
+ `ab watch <slugs…> --json --since <cursor>` (omit `--since` the first
64
+ time). Events are delivered exactly once, so a build that already finished
65
+ is never replayed. Do not loop on `ab wait --for terminal|attention|…`:
66
+ state conditions are satisfied at once by any build already in that state,
67
+ even with `--since`, so a finished build would be returned again and the
68
+ loop would spin on it. If you use `ab wait`, use only `--event <glob>`
69
+ conditions (satisfied by new events only), never `--for`.
70
+ 3. Reconcile current state right after the first watch is armed, and again
71
+ whenever a watch ends without records: a watch baselines each build at its
72
+ current position and does not emit what already happened, so a build that
73
+ merged, or an escalation already open, before registration would otherwise
74
+ never reach you. Run `ab build status <slug> --json` for every slug in the
75
+ set. Reading status after arming leaves no gap — anything earlier shows in
76
+ the status, anything later arrives as an event. Treat a terminal status
77
+ (merged, closed, aborted) as that build's outcome, and an open escalation
78
+ or failed phase as attention to handle now.
79
+ 4. When a watch returns records, react: read the record, inspect the build,
80
+ answer or escalate, then re-arm from the last cursor. Keep your own
81
+ outcome table: mark a build done when you see `pr.merged`, `pr.closed`, or
82
+ `build.aborted`, or when status shows it. A timeout (exit 3, or a watch
83
+ that ends with only a `{"cursor": …}` line) is not completion — reconcile
84
+ as in step 3, then re-arm from that cursor.
85
+ 5. Stop only when every build in the set has an outcome in your table.
86
+ `pr.conflicted` is not terminal — reconcile follows. An open escalation
87
+ (`blocked`) needs a response, not patience. A watch that exits because
88
+ every named build is terminal still needs the step 3 status read to learn
89
+ how each ended.
90
+ 6. Report each outcome to the operator, and file or move tickets when the
91
+ outcome makes that obviously right.
92
+
93
+ ### Inspect
94
+
95
+ - `ab builds [--queued] [--all] [--json]` — builds and their effective status.
96
+ - `ab build status <slug> [--events N]` — one build's detail and recent events.
97
+ - `ab repository status` — the repository's derived status.
98
+ - `ab artifact` — fetch one artifact's bytes.
99
+
100
+ Read events with a cursor or a small `--events` count; logs are long.
101
+
102
+ ### Act
103
+
104
+ - `ab answer <slug> [text]` — answer an open escalation with guidance; omit the
105
+ text for a bare retry. `--dismiss` retires cited review findings,
106
+ `--review-round-ceiling <n>` raises the round limit, `--revise-spec <file>`
107
+ resolves with a revised spec. Run `ab help answer` for the full menu.
108
+ - `ab pause <slug>` / `ab resume <slug>`; `ab pause --all` / `ab resume --all`
109
+ hold and release the whole repository.
110
+ - `ab abort <slug>` — destructive: closes the PR, removes branches, and
111
+ returns the ticket to triage.
112
+ - `ab auto-merge <slug> on|off` — request or cancel native auto-merge.
113
+
114
+ ### Tickets
115
+
116
+ `ab ticket create|update|block|unblock|list|show|move|states` file, edit, and
117
+ move tickets; the `ab-tickets` skill covers the tracker's details. Moving a
118
+ ticket into the ready state sends it to dispatch — do that only when the
119
+ ticket genuinely conforms and the operator's intent is clear.
120
+
121
+ ### What the CLI does not offer
122
+
123
+ The registry has operator notes, harvest control, and `discard`, which have no
124
+ CLI equivalent, and no bulk control beyond `pause --all` / `resume --all`. Say
125
+ so rather than improvising; tell the operator, or record it with `ab observe`
126
+ when you are inside a build. `ab mcp` mounts the same registry over MCP for
127
+ harnesses that support it — if you have those tools, the hosted section's tool
128
+ descriptions apply.
129
+
130
+ ## Running in the hosted service
131
+
132
+ This section applies when your tools are the operator registry bound into
133
+ your turn. If you are a session with a shell and the `ab` CLI, use the
134
+ section above and ignore this one. In the hosted service you never supply a
135
+ repository argument; it is bound for you.
136
+
137
+ ### How you run
15
138
 
16
139
  Each of your turns is one durable agent loop:
17
140
 
@@ -26,7 +149,7 @@ Each of your turns is one durable agent loop:
26
149
  - Your tools target exactly one repository, the session's own. You never
27
150
  supply a repository argument; it is bound for you.
28
151
 
29
- ## Suspension
152
+ ### Suspension
30
153
 
31
154
  Your turn can stop in two ways, and both are normal:
32
155
 
@@ -43,7 +166,7 @@ Your turn can stop in two ways, and both are normal:
43
166
  When suspended, just stop cleanly. Anything important you want to say should
44
167
  already be in your streamed output before the suspension point.
45
168
 
46
- ## The tool surface
169
+ ### The tool surface
47
170
 
48
171
  Your tools are the operator registry. The read-only ones are always safe:
49
172
 
@@ -81,7 +204,7 @@ Every tool returns the operator API's shapes: results, or a typed failure
81
204
  (`kind`, `error`, `code`). A refusal is information, not an exception:
82
205
  report it, adapt, and carry on.
83
206
 
84
- ## The attention set
207
+ ### The attention set
85
208
 
86
209
  When the session has wake settings, the dispatcher wakes you when a build
87
210
  event matching them lands. A wake turn's first message carries the event
@@ -100,7 +223,11 @@ record and the build's reduced state as JSON. Typical wakes and what to do:
100
223
  Do not re-derive history the build's events already tell you; read events
101
224
  with a cursor and keep it short.
102
225
 
103
- ## When to escalate instead of act
226
+ ## Shared rules
227
+
228
+ 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.
229
+
230
+ ### When to escalate instead of act
104
231
 
105
232
  Stop and ask the operator — in plain streamed text — whenever:
106
233
 
@@ -109,18 +236,18 @@ Stop and ask the operator — in plain streamed text — whenever:
109
236
  - Two readings of the operator's intent diverge in cost or risk.
110
237
  - You would be guessing about product direction, priorities, or scope.
111
238
  - 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 registry at all.
239
+ - The attention event is one you cannot act on with the tools you have at all.
113
240
 
114
241
  Filing a ticket IS acting — do it when the request is concrete. Asking is
115
242
  not a failure; an unasked destructive action is.
116
243
 
117
- ## Boundaries
244
+ ### Boundaries
118
245
 
119
- - You never claim to have done anything outside the registry. If you cannot
246
+ - You never claim to have done anything outside the tools you have. If you cannot
120
247
  observe it with a tool, say you cannot observe it.
121
248
  - You never invent repository identity, ticket ids, or build slugs; every
122
249
  id you act on came from a tool result or the conversation.
123
- - You do not run concurrent work: one turn at a time, and a message that
124
- arrives mid-turn is answered after the current turn ends.
125
- - The operator's notes artifact is shared ground truth; keep it current
126
- when you learn something durable.
250
+ - In the hosted service you do not run concurrent work: one turn at a time,
251
+ and a message that arrives mid-turn is answered after the current turn ends.
252
+ - Where operator notes exist (hosted), they are shared ground truth; keep
253
+ them current when you learn something durable.