@defrex/autobuild 0.8.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/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
package/SPEC.md
ADDED
|
@@ -0,0 +1,2269 @@
|
|
|
1
|
+
# Autobuild v2 — Specification
|
|
2
|
+
|
|
3
|
+
An agent-driven software development lifecycle system: it takes work from
|
|
4
|
+
*"something should be done"* to a merged PR, with a human in the loop only
|
|
5
|
+
where judgment matters. This is a ground-up rebuild of v1 (preserved in git
|
|
6
|
+
history at `6d4dce3`), keeping v1's validated ideas and replacing what it
|
|
7
|
+
hard-coded with real interfaces.
|
|
8
|
+
|
|
9
|
+
**What this document is.** SPEC.md holds the durable design: principles,
|
|
10
|
+
boundaries between components, invariants, and decisions with their rationale
|
|
11
|
+
(the **[D1]–[D11]** series). It is deliberately not three other things:
|
|
12
|
+
|
|
13
|
+
- **Not a behavior reference.** Exact schemas, flag grammars, validation
|
|
14
|
+
rules, and edge-case handling are specified by the code and its tests —
|
|
15
|
+
event payloads are frozen in `packages/core/src/events/`, and the store contract in
|
|
16
|
+
`packages/core/src/store/contract.ts`. Where this document and the code disagree on
|
|
17
|
+
behavior detail, the code is authoritative.
|
|
18
|
+
- **Not an operating manual.** How to install, configure, and drive the
|
|
19
|
+
system lives in `README.md` and `docs/`.
|
|
20
|
+
- **Not a change log.** Each resolved design question lives in its ticket,
|
|
21
|
+
build, and PR; the paper trail is queryable in the BuildStore. A merged
|
|
22
|
+
change earns a paragraph here only if it altered a principle, boundary, or
|
|
23
|
+
invariant.
|
|
24
|
+
|
|
25
|
+
Status legend: sections describe **decided** design unless marked **[OPEN]**.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 1. Why a rebuild
|
|
30
|
+
|
|
31
|
+
v1 proved the core loop works. Its structural limits, which motivate v2:
|
|
32
|
+
|
|
33
|
+
- **Repo as database.** Build state and artifacts lived in `build/<feature>/`
|
|
34
|
+
dirs committed to the repo — accumulating noise, bloating the repo, with no
|
|
35
|
+
clean archival story.
|
|
36
|
+
- **No observability seam.** State in local files meant no way to monitor
|
|
37
|
+
builds, especially remote ones. The "UI" was tailing `build.log`.
|
|
38
|
+
- **Hard-coded everything.** The phase list, the verdict vocabulary, Linear,
|
|
39
|
+
Sentry, and the review loop (implemented twice, differently) were all baked
|
|
40
|
+
in.
|
|
41
|
+
- **Sentinel parsing.** Verdicts rode on stdout and were regex-parsed —
|
|
42
|
+
fragile by construction.
|
|
43
|
+
- **Opaque agent sessions.** Near-zero insight into transcripts; no corpus
|
|
44
|
+
for improving prompts or writing evals.
|
|
45
|
+
|
|
46
|
+
## 2. Constitution
|
|
47
|
+
|
|
48
|
+
Principles — the first four carried from v1 — that are non-negotiable and that
|
|
49
|
+
every design decision below answers to:
|
|
50
|
+
|
|
51
|
+
1. **Judgment in skills, determinism in code.** Agents never decide phase
|
|
52
|
+
transitions, signal identity, or state. Agent surfaces own the fuzzy parts
|
|
53
|
+
(primarily skills, plus narrow pre-build seams such as naming); plain tested
|
|
54
|
+
code owns the state machine, validation, fallback, dedup, gating, and
|
|
55
|
+
plumbing.
|
|
56
|
+
2. **Resumability is not a feature.** Re-running a build resumes it. There is
|
|
57
|
+
no separate resume path; every phase is a function of durable state.
|
|
58
|
+
3. **Ingesters propose, humans dispatch.** Nothing auto-generated moves past
|
|
59
|
+
Triage without a human grooming it to Ready. That single gate is where
|
|
60
|
+
taste and prioritization live. A repository may waive the gate for its own
|
|
61
|
+
harvest by naming a filing state (`[tickets].proposalState`, §12); the
|
|
62
|
+
waiver is explicit, repository-wide, and configured, and no code path
|
|
63
|
+
reaches Ready without one.
|
|
64
|
+
4. **Every step leaves a paper trail** — queryable, not carried in the repo.
|
|
65
|
+
5. **Processes communicate only through durable state.** There is no private
|
|
66
|
+
channel between Autobuild processes — not kernel to operator UI, not parent
|
|
67
|
+
to child, not kernel to sandbox. A process's liveness may be observed; what
|
|
68
|
+
it has *done* is known only by reading the log. This is what keeps every
|
|
69
|
+
component relocatable, and why a new process boundary never needs a new
|
|
70
|
+
protocol.
|
|
71
|
+
|
|
72
|
+
## 3. System decomposition
|
|
73
|
+
|
|
74
|
+
Four kinds of things with sharp boundaries:
|
|
75
|
+
|
|
76
|
+
### 3.1 The kernel
|
|
77
|
+
|
|
78
|
+
A headless library: pipeline engine, the `converge` review-loop primitive,
|
|
79
|
+
event log handling, state reducer. No I/O opinions — no ticket tracker, no
|
|
80
|
+
telemetry vendor, no terminal.
|
|
81
|
+
|
|
82
|
+
### 3.2 Ports
|
|
83
|
+
|
|
84
|
+
Interfaces to the world, each with swappable adapters:
|
|
85
|
+
|
|
86
|
+
| Port | Duty | Initial adapters |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `TicketSource` | list/claim/comment/transition/create/update tickets; add, remove, and resolve declared dependencies | file-based (default directory); Linear; third-party in-process registrations; later GitHub Issues |
|
|
89
|
+
| `AgentRunner` | run agent sessions (see §9) | Claude Code CLI (headless); Codex CLI (`exec --json`); local pi CLI (`--mode rpc`, minimum 0.84.3); third-party in-process registrations |
|
|
90
|
+
| `Workspace` | provision isolated working copies | git worktree; third-party in-process registrations; later remote sandbox |
|
|
91
|
+
| `Forge` | git + PR plumbing | GitHub; local Git; third-party in-process registrations |
|
|
92
|
+
| `TelemetrySource` | production signals | Sentry; later log streams |
|
|
93
|
+
| `BuildStore` | per-build streams plus repository journals: events, artifacts, transcripts, leases (see §7) | local; remote HTTP |
|
|
94
|
+
|
|
95
|
+
#### 3.2.1 In-process adapter plugins
|
|
96
|
+
|
|
97
|
+
A repository may list trusted Bun modules in `autobuild.toml`. Each module
|
|
98
|
+
default-exports a strict manifest with a diagnostic `name`, an `apiVersion`
|
|
99
|
+
semver range, and optional name-to-factory maps for `ticketSources`,
|
|
100
|
+
`agentRuntimes`, `workspaceProviders`, and `forges`. One manifest may register
|
|
101
|
+
adapters for several ports. A ticket-source entry is either its legacy bare
|
|
102
|
+
factory or `{ factory, requiredEnv?, contract? }`; `requiredEnv` lets the host
|
|
103
|
+
reject unset or empty credential variables before construction with a
|
|
104
|
+
diagnostic that names both source and variables. Plugin API 1.1 introduces the
|
|
105
|
+
descriptor metadata while remaining compatible with manifests accepting
|
|
106
|
+
`^1.0.0`. Factories receive
|
|
107
|
+
adapter-specific config, the process environment, and the absolute repository
|
|
108
|
+
root, and remain lazy during startup registration. Runtime registrations reuse
|
|
109
|
+
§9's capability-bearing `RuntimeRegistration`; the frozen `AgentRunner`
|
|
110
|
+
interface is not widened.
|
|
111
|
+
|
|
112
|
+
The host exposes one versioned authoring surface, `@defrex/autobuild/plugin-sdk`: port
|
|
113
|
+
and manifest types, the reusable TicketSource/AgentRunner/WorkspaceProvider/
|
|
114
|
+
Forge/BuildStore/BlobStore contract suites, and fake/reference adapters. Plugin
|
|
115
|
+
production code can use erased type-only imports, with Autobuild present only
|
|
116
|
+
as a development or peer dependency; a consuming repository needs no bridge
|
|
117
|
+
module.
|
|
118
|
+
|
|
119
|
+
Plugin resolution has two repository-owned roots and one installation root.
|
|
120
|
+
Relative, absolute, and `file:` specifiers resolve from the repository root
|
|
121
|
+
whose config is being read; in a scoped build CLI process, that is the
|
|
122
|
+
immutable build worktree. Bare package specifiers and package export maps
|
|
123
|
+
resolve first from the consuming repository's main checkout, so they use that
|
|
124
|
+
repository's installed dependencies and remain available when a local store
|
|
125
|
+
places linked worktrees outside the checkout's package ancestry; a package the
|
|
126
|
+
repository lacks then resolves from the Autobuild installation the CLI runs
|
|
127
|
+
from, so an extension installed next to the CLI (`bun add -g
|
|
128
|
+
@defrex/autobuild-<extension>`) loads without being added to the repository. A
|
|
129
|
+
repository copy always wins over an installed one. Dispatch and sessionless
|
|
130
|
+
commands naturally use the main checkout for both repository roots. Autobuild
|
|
131
|
+
does not install a missing package.
|
|
132
|
+
|
|
133
|
+
Configured specifier strings must be unique: an exact repeated value fails
|
|
134
|
+
config validation before module resolution or evaluation, identifies the
|
|
135
|
+
duplicate and its first declaration, and directs the operator to remove or
|
|
136
|
+
deduplicate it. Distinct specifier strings remain separate declarations.
|
|
137
|
+
Modules load in declaration order during `ab dispatch`, the sessionless
|
|
138
|
+
`ab ticket` commands, and scoped build CLI composition. Dispatch loads them after
|
|
139
|
+
strict config parsing and before stores, production adapters, ticket claims, or
|
|
140
|
+
build launch; ticket commands load them before ticket access; scoped processes
|
|
141
|
+
load worktree config and repository-path modules, but use the main checkout only
|
|
142
|
+
for package lookup, before opening their store or executing terminal plumbing.
|
|
143
|
+
Resolution/evaluation errors, malformed or missing default manifests, and
|
|
144
|
+
plugin-API incompatibility fail startup with both the configured module and
|
|
145
|
+
available compatibility details.
|
|
146
|
+
Builtin registration names and names registered by an earlier plugin are
|
|
147
|
+
reserved per port; collisions fail atomically, identify the conflicting adapter
|
|
148
|
+
and owners, and shadow nothing. The same name may exist on different ports.
|
|
149
|
+
`[workspace].provider` selects from the workspace catalog; omission selects
|
|
150
|
+
`git-worktree`, while `vercel-sandbox` is the shipped opt-in remote execution
|
|
151
|
+
provider. Plugin factories are invoked lazily with `[workspace.config]`,
|
|
152
|
+
environment, and repository root. The Vercel builtin instead consumes its
|
|
153
|
+
strict typed config and requires an HTTPS Store/scoped token and HTTPS GitHub
|
|
154
|
+
origin before any ticket claim.
|
|
155
|
+
|
|
156
|
+
Each adapter map value may remain a bare factory or may be an object containing
|
|
157
|
+
that factory plus an optional `contract: { factory, live? }` descriptor; ticket
|
|
158
|
+
sources may carry `requiredEnv` in the same object. The contract factory
|
|
159
|
+
receives the same repository context and returns the fixture
|
|
160
|
+
factory required by that port's unchanged shared suite. `ab plugin list`
|
|
161
|
+
projects builtin and configured registrations with provenance, resolution kind,
|
|
162
|
+
API compatibility, and contract availability. `ab plugin doctor` exhaustively
|
|
163
|
+
attempts every configured module and exits nonzero if any fail; this diagnostic
|
|
164
|
+
collection does not weaken `ab dispatch`, which remains fail-fast. `ab plugin
|
|
165
|
+
test <ticket-source|agent-runtime|workspace-provider|forge> <adapter>` delegates
|
|
166
|
+
one selected suite to Bun's test runner and preserves its per-test output and
|
|
167
|
+
exit status. A descriptor marked `live` is never launched unless
|
|
168
|
+
`AB_RUN_LIVE_PORT_CONTRACTS=1` is explicitly present.
|
|
169
|
+
|
|
170
|
+
Plugins execute in-process and are Bun-only. They have the same repository
|
|
171
|
+
trust boundary as declarative shell commands: no sandbox is promised.
|
|
172
|
+
`[tickets].source` may name a loaded plugin registration; unknown names fail
|
|
173
|
+
with all available builtin and plugin names. Dispatch routes one selected
|
|
174
|
+
instance through readiness, dependencies, claim, projection, harvest creation,
|
|
175
|
+
and janitor completion; the same registration backs every source-agnostic
|
|
176
|
+
`ab ticket` operation. Plugin sources receive the existing `[tickets]`
|
|
177
|
+
lifecycle fields unchanged and own any further validation; credentials remain
|
|
178
|
+
environment-only.
|
|
179
|
+
|
|
180
|
+
Forge selection is open through the root `forge` scalar (`github` by default).
|
|
181
|
+
Two adapters ship: `github` publishes through GitHub/`gh`; `local-git` uses only
|
|
182
|
+
the repository's shared local Git database and requires no remote, network, or
|
|
183
|
+
forge credentials. A selected plugin factory receives an empty adapter config, process
|
|
184
|
+
environment, and absolute repository root, and is invoked before store opening.
|
|
185
|
+
Unknown names list the complete available forge catalog. Dispatch and scoped
|
|
186
|
+
build CLI processes resolve the same configured name independently, and all
|
|
187
|
+
forge plumbing receives the selected adapter unchanged. `PrRef.url` is the
|
|
188
|
+
provider's nonblank locator: an HTTP(S) URL when a web UI exists, or a literal
|
|
189
|
+
provider-native identifier such as `refs/heads/ab/<slug>` for local Git. Workspace selection is
|
|
190
|
+
open through `[workspace].provider` as described above.
|
|
191
|
+
|
|
192
|
+
Agent runtime selection is open through every `[roles.*].runtime`: dispatch
|
|
193
|
+
materializes registered runtime factories before eager role validation, and the
|
|
194
|
+
lazy upgrade conflict resolver does the same only when its first conflict needs
|
|
195
|
+
judgment. Plugin registrations use the same exact-name routing, model-family and
|
|
196
|
+
default-model validation, event attribution, and optional one-shot capability
|
|
197
|
+
selection as builtins.
|
|
198
|
+
|
|
199
|
+
`TelemetrySource` remains deferred, and BuildStore's third-party extension
|
|
200
|
+
surface remains the remote HTTP protocol rather than in-process registration.
|
|
201
|
+
|
|
202
|
+
### 3.3 Processes
|
|
203
|
+
|
|
204
|
+
Small, independently runnable, crash-safe:
|
|
205
|
+
|
|
206
|
+
- **build-runner** — one operating-system process per build; owns one pipeline
|
|
207
|
+
execution end to end. The local implementation starts the process beside its
|
|
208
|
+
Git worktree in its own POSIX session/process group and owns teardown of that
|
|
209
|
+
complete group, including agent-tool grandchildren. Workspace execution is a
|
|
210
|
+
substitutable capability, so a future sandbox provider can place both checkout
|
|
211
|
+
and process elsewhere without the dispatcher spawning a local proxy; that
|
|
212
|
+
provider owns equivalent complete-environment teardown. Per-build processes are deliberate: crash
|
|
213
|
+
isolation, and the natural shape once builds run in remote sandboxes.
|
|
214
|
+
- **dispatcher** — watches the TicketSource for tickets passing the configured
|
|
215
|
+
ready gate, claims, establishes the final conforming spec, chooses a short
|
|
216
|
+
immutable build slug, provisions a workspace, and launches build-runners up
|
|
217
|
+
to capacity. On process startup it attempts every current build for its
|
|
218
|
+
repo, so re-running `ab dispatch` resumes durable work rather than only
|
|
219
|
+
looking for new tickets. Every ordinary tick also completes queued dispatch
|
|
220
|
+
records whose pre-run sequence was interrupted: it reconstructs a missing
|
|
221
|
+
`build.created` from the immutable record, executes only missing workspace,
|
|
222
|
+
spec, and ticket-notification boundaries, and records each failed attempt as
|
|
223
|
+
`dispatch.failed`. `build.created` retains any claim-time auto-merge
|
|
224
|
+
attribution until its human-authored command fact is materialized, while
|
|
225
|
+
`dispatch.comment-posted` prevents retries from duplicating the ticket notice.
|
|
226
|
+
It also owns observation back-pressure: settling
|
|
227
|
+
outstanding recoverable harvest runs takes priority over starting new scans
|
|
228
|
+
(§12). Cron-friendly.
|
|
229
|
+
- **harvest-runner** — one staged repository workflow (`scan → synthesize ⇄
|
|
230
|
+
review → file`) under a repository lease; not a build and not a phase.
|
|
231
|
+
- **ingesters** — other outer-loop processes turning signals into proposals (§12).
|
|
232
|
+
- **operator** — UI process(es); see §14.
|
|
233
|
+
|
|
234
|
+
### 3.4 The event log spine
|
|
235
|
+
|
|
236
|
+
Build processes append typed events to per-build logs; repository-scoped outer
|
|
237
|
+
workflows and operator settings append to a separate repository journal in the
|
|
238
|
+
same BuildStore. Consequences, by design:
|
|
239
|
+
|
|
240
|
+
- **State is a reduction of events.** Any state snapshot is a cache, never
|
|
241
|
+
the source of truth. Resumability falls out.
|
|
242
|
+
- **The UI layer is a subscriber** plus a command channel back. TUI, web —
|
|
243
|
+
same adapter pattern. The shipped interactive terminal owns a supervised
|
|
244
|
+
kernel child, and the BuildStore is their only changing-state boundary.
|
|
245
|
+
- **The audit trail is the log**, serialized.
|
|
246
|
+
|
|
247
|
+
The event vocabulary (§15) is deliberately designed early and carefully: it
|
|
248
|
+
is simultaneously the store schema, the kernel's I/O, the UI API, and the
|
|
249
|
+
resume format. A bad event schema calcifies.
|
|
250
|
+
|
|
251
|
+
## 4. Ontology
|
|
252
|
+
|
|
253
|
+
One name, used everywhere. Every noun lives in exactly one layer.
|
|
254
|
+
|
|
255
|
+
| Term | Meaning |
|
|
256
|
+
|---|---|
|
|
257
|
+
| **Signal** | Raw input from the world — a Sentry issue, an observation |
|
|
258
|
+
| **Proposal** | A synthesized candidate ticket sitting in Triage, awaiting grooming |
|
|
259
|
+
| **Ticket** | A groomed, dispatchable unit of work in the TicketSource |
|
|
260
|
+
| **Build** | One pipeline execution for one ticket; has a slug; the unit the operator sees |
|
|
261
|
+
| **Harvest run** | One repository-scoped claimed observation snapshot and staged proposal workflow; never a build |
|
|
262
|
+
| **Phase** | A named stage of the build pipeline grammar (§5) |
|
|
263
|
+
| **Round** | One iteration inside a review loop |
|
|
264
|
+
| **Artifact** | A durable, versioned output: spec, plan, review, report, transcript |
|
|
265
|
+
| **Verdict** | Structured terminal outcome: reviews use `approve` \| `revise(findings)` \| `escalate(reason)`; agent verification uses `pass` \| `fail(report)` \| `skip(reason)` |
|
|
266
|
+
| **Finding** | One structured item inside a `revise` verdict (file refs, description, severity) |
|
|
267
|
+
| **Observation** | An out-of-scope discovery emitted mid-build: `followup` \| `refactor` \| `latent-bug` |
|
|
268
|
+
| **Escalation** | A parked request for human input, answerable from any UI |
|
|
269
|
+
| **Event** | An append-only log record; everything above leaves one |
|
|
270
|
+
|
|
271
|
+
Reserved-word discipline: **build** names the whole pipeline execution and
|
|
272
|
+
nothing else; the coding phase is **implement**.
|
|
273
|
+
|
|
274
|
+
### Naming propagation
|
|
275
|
+
|
|
276
|
+
A phase's name derives its skill, its events, and its artifact kind —
|
|
277
|
+
mechanically:
|
|
278
|
+
|
|
279
|
+
| Phase | Skill | Events | Artifact |
|
|
280
|
+
|---|---|---|---|
|
|
281
|
+
| `plan` | `/plan <build>` | `plan.started`, `plan.completed` | `plan` (rev N) |
|
|
282
|
+
| `plan-review` | `/plan-review <build>` | `plan-review.verdict` | `plan-review` (rev N) |
|
|
283
|
+
| `implement` | `/implement <build>` | `implement.completed` | diff + `implement-notes` |
|
|
284
|
+
| `code-review` | `/code-review <build>` | `code-review.verdict` | `code-review` (rev N) |
|
|
285
|
+
| `verify:e2e` | `/verify-app-e2e <build>` (repository-authored) | `verify.completed {step, outcome}` | `verify-report:e2e` on pass/fail when present |
|
|
286
|
+
| `finalize` | `/finalize <build>` | `finalize.completed` | PR ref, summary |
|
|
287
|
+
| `reconcile` | `/reconcile <build>` | `reconcile.started`, `reconcile.completed` | `reconcile-notes` + merge commit |
|
|
288
|
+
|
|
289
|
+
Every phase skill takes **only the build slug**; everything else it needs
|
|
290
|
+
comes from the store via the `ab` CLI (§8).
|
|
291
|
+
|
|
292
|
+
Canonical skills installed into a repo carry the `ab-` namespace prefix
|
|
293
|
+
(§16.3), for example `/ab-plan` and `/ab-code-review`. Repository-authored
|
|
294
|
+
extension skills use their own names, such as `/verify-app-e2e`. This spec uses
|
|
295
|
+
the bare phase names throughout.
|
|
296
|
+
|
|
297
|
+
## 5. Pipeline grammar
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
spec → plan ⇄ plan-review → implement ⇄ code-review → verify:* → finalize (+ finalize:*)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The grammar is fixed — an opinionated skeleton, not a generic workflow
|
|
304
|
+
engine. Exactly two extension points:
|
|
305
|
+
|
|
306
|
+
- **`verify:*`** — an ordered, configurable list of verifiers, declared in
|
|
307
|
+
per-repo config. Two subtypes: *check* (deterministic command; pass/fail is
|
|
308
|
+
its exit code) and *agent-verify* (an agent run with a `pass|fail|skip`
|
|
309
|
+
verdict schema). The durable outcomes are `pass`, `fail(report)`, and
|
|
310
|
+
`skipped(reason)`. A failure routes back to `implement` with the report,
|
|
311
|
+
re-entering the code loop. A skip requires a human-readable reason,
|
|
312
|
+
satisfies only that step for the current cycle, and consumes no failure
|
|
313
|
+
budget — it is neither passing evidence nor a failure, and it never hides
|
|
314
|
+
another step's failure. A skip is produced either by an agent verifier's
|
|
315
|
+
explicit verdict or by the kernel, when the approved plan did not select an
|
|
316
|
+
optional step or a configured path-applicability rule excludes it (§16.1).
|
|
317
|
+
- **`finalize:*`** — optional ordered post-steps (release notes, changelog,
|
|
318
|
+
publishing, ticket linking). Each configured step is either a deterministic
|
|
319
|
+
command check with no agent session or an agent running an exact skill.
|
|
320
|
+
Independent and failure-tolerant: a failed post-step files an observation;
|
|
321
|
+
it never kills a green build. A step that produces repository content
|
|
322
|
+
selects and commits its files locally and must finish with a clean worktree.
|
|
323
|
+
The runner verifies that the last durably published head remains an ancestor,
|
|
324
|
+
then regular-pushes the new head and checkpoints it on
|
|
325
|
+
`finalize.step-completed`. An unchanged head creates and pushes no commit;
|
|
326
|
+
Git or publication failure is the ordinary failed-step observation, never a
|
|
327
|
+
reason to rewrite history or fail the green build.
|
|
328
|
+
|
|
329
|
+
Everything else — phase order, the two review loops, escalation semantics —
|
|
330
|
+
is kernel-hard-coded. That is what keeps the system inspectable.
|
|
331
|
+
|
|
332
|
+
`spec` is *not* a phase; it is satisfied at or before dispatch (§6). After
|
|
333
|
+
`finalize`, the build enters a post-PR epilogue owned by the dispatcher's
|
|
334
|
+
janitor duty (§15.7): a conflicted PR triggers the `reconcile` phase —
|
|
335
|
+
agent-resolved merge of base into branch, full `verify:*` re-run — repeating
|
|
336
|
+
until merge or close.
|
|
337
|
+
|
|
338
|
+
## 6. The spec
|
|
339
|
+
|
|
340
|
+
Two things wear the name:
|
|
341
|
+
|
|
342
|
+
### 6.1 The spec standard
|
|
343
|
+
|
|
344
|
+
A reference document defining "buildable": what/why not how, acceptance
|
|
345
|
+
criteria, explicit out-of-scope, evidence links. It is a shared resource
|
|
346
|
+
cited by **every ticket-producing surface** — `/spec`, `harvest`, every
|
|
347
|
+
`ingest:*`, and `dispatch`. An ingester's proposal is a spec written to the
|
|
348
|
+
same standard with weaker evidence.
|
|
349
|
+
|
|
350
|
+
### 6.2 The /spec skill
|
|
351
|
+
|
|
352
|
+
The human-interactive, conversational surface over the standard. It takes
|
|
353
|
+
a ticket rather than a build slug, because it runs before a build exists:
|
|
354
|
+
|
|
355
|
+
- `/spec` (no args) — design a feature through conversation, spec-first; on
|
|
356
|
+
completion it creates the ticket via the TicketSource.
|
|
357
|
+
- `/spec <ticket>` — flesh out a thin existing ticket into conforming shape,
|
|
358
|
+
then sync the body back through the TicketSource so unrelated metadata
|
|
359
|
+
remains untouched.
|
|
360
|
+
|
|
361
|
+
### 6.3 The spec artifact
|
|
362
|
+
|
|
363
|
+
The **contract artifact of a build**: kind `spec`, revision 0, sole input to
|
|
364
|
+
`plan`. Pre-build, the spec lives in the ticket body; **dispatch imports it**
|
|
365
|
+
(`spec.imported`) or, for thin-but-groomed tickets, authors one via a
|
|
366
|
+
non-interactive agent pass (`spec.authored`). Three production paths, one
|
|
367
|
+
artifact: human-led via `/spec`, dispatch-authored, ingester-shaped.
|
|
368
|
+
|
|
369
|
+
**Dispatch quality gate:** if a Ready ticket cannot be expanded into a
|
|
370
|
+
conforming spec, dispatch bounces it back to Triage with a comment citing
|
|
371
|
+
the standard — moving failure to the cheapest point instead of launching a
|
|
372
|
+
build that will thrash and escalate.
|
|
373
|
+
|
|
374
|
+
**Build identity at dispatch:** once the final conforming spec is in hand,
|
|
375
|
+
the selected runtime proposes a short kebab-case slug from its substance —
|
|
376
|
+
judgment proposes; the dispatcher validates, deduplicates against the store,
|
|
377
|
+
and falls back to a deterministic title-derived name on any absence, invalid
|
|
378
|
+
output, error, or timeout. Naming failure never prevents build creation. The
|
|
379
|
+
slug and its `ab/<slug>` branch are recorded once and never renamed.
|
|
380
|
+
|
|
381
|
+
**Immutability:** the spec cannot change during a build except through the
|
|
382
|
+
explicit escalation revision protocol. Every downstream reviewer approves
|
|
383
|
+
conformance *to it*; a drifting spec silently converts approvals into
|
|
384
|
+
approvals-of-something-else. A phase discovering the spec itself is wrong
|
|
385
|
+
raises an `escalation`; a human uses `ab answer --revise-spec` (or
|
|
386
|
+
`--revise-spec-from-ticket`), the spec gets rev N+1, and the build restarts
|
|
387
|
+
from `plan` (cheap — downstream was invalidated anyway).
|
|
388
|
+
|
|
389
|
+
## 7. The build store
|
|
390
|
+
|
|
391
|
+
The v1 repo-bloat and no-observability problems share one root cause: the
|
|
392
|
+
repo did double duty as workspace and database. v2 splits the roles:
|
|
393
|
+
|
|
394
|
+
- **Workspace** — the sandbox/worktree. Scratch by construction: the working
|
|
395
|
+
tree plus the gitignored `.ab/` dir agents use as working memory during a
|
|
396
|
+
phase (§8.3). Disposable.
|
|
397
|
+
- **Build store** — the durable home of everything else, one logical place
|
|
398
|
+
whether builds run locally or in ten remote sandboxes.
|
|
399
|
+
|
|
400
|
+
### 7.1 Data model
|
|
401
|
+
|
|
402
|
+
```
|
|
403
|
+
builds id/slug, ticket ref, repo, branch, status (derived), created/updated,
|
|
404
|
+
lease + heartbeat (mutable liveness columns — §15.2.6, never events)
|
|
405
|
+
events build_id, seq, timestamp, actor, type, payload (JSON) — append-only
|
|
406
|
+
artifacts build_id, kind, revision, blobRef, metadata
|
|
407
|
+
repo_streams repo, created/updated, lease + heartbeat
|
|
408
|
+
repo_events repo, seq, timestamp, actor, type, payload (JSON) — append-only
|
|
409
|
+
repo_artifacts repo, kind, revision, blobRef, metadata
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
#### 7.1.1 Operator sessions (the third resource kind)
|
|
413
|
+
|
|
414
|
+
An operator session is durable orchestrator-conversation state — a record keyed
|
|
415
|
+
by repository and operator, hosted-only. It is neither a build nor a
|
|
416
|
+
repository fact: folding it into the build streams would leak conversation
|
|
417
|
+
facts into the build reducer, and folding it into the repository journal would
|
|
418
|
+
mix one operator's chat with dispatcher and harvest history that every process
|
|
419
|
+
reads. The store's data model therefore gains a third resource family with the
|
|
420
|
+
same shape as the other two:
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
sessions id (os_…), repo, operator, title?, created/updated
|
|
424
|
+
session_events session_id, seq, timestamp, actor, type, payload (JSON) — append-only
|
|
425
|
+
session_artifacts session_id, kind, revision, blobRef, metadata
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
The record carries only identity and timestamps; everything else — status
|
|
429
|
+
(`idle | running | suspended | awaiting-approval | archived`), the open turn,
|
|
430
|
+
the pending approval, wake settings, wake cursors, the turn list — is a
|
|
431
|
+
reduction of the session's own event log (a pure reducer, never a column).
|
|
432
|
+
The session event catalog is closed and validated separately from the build
|
|
433
|
+
and repository catalogs, so no build reducer can interpret session facts, and
|
|
434
|
+
per-session sequencing is independent. The stream primitive's scope vocabulary
|
|
435
|
+
gains `{kind: "session"}` with the session id as its reference, so every
|
|
436
|
+
turn's content is one session-scoped stream. Local installs never create
|
|
437
|
+
sessions: local adapters implement the contract uniformly, and the operator
|
|
438
|
+
API (§14) is the only creation surface.
|
|
439
|
+
|
|
440
|
+
Schema requirements (the exact DDL is not design-critical): simple,
|
|
441
|
+
normalized, defined once, with a local embedded target and a remote
|
|
442
|
+
server-database target. Blobs are content-addressed (sha256) behind a narrow
|
|
443
|
+
`BlobStore` adapter (`put(hash, bytes)`, `get(hash)`): a plain directory
|
|
444
|
+
locally; remotely any object store — always an adapter, never an assumption.
|
|
445
|
+
The database stores refs, never bulk content. The design lives in the data
|
|
446
|
+
model and event vocabulary (§15); the schema should stay boring.
|
|
447
|
+
|
|
448
|
+
Transcripts are an artifact kind (`transcript`) with metadata: phase, round,
|
|
449
|
+
role, runner, model, token counts. This one decision produces the analysis
|
|
450
|
+
corpus — prompt improvement, evals against the build skills, replay — as a
|
|
451
|
+
query rather than a project.
|
|
452
|
+
|
|
453
|
+
Reading session events uses the same contract shape as stream reads (§7.6):
|
|
454
|
+
cursor-based, returning events with `seq` strictly greater than `sinceSeq`
|
|
455
|
+
(default 0) in increasing sequence order, and an optional bounded wait —
|
|
456
|
+
`waitSeconds` in whole seconds — that returns as soon as an event lands and
|
|
457
|
+
no later than the §7.6 bound, with a `waitSeconds` above 30 clamping to 30
|
|
458
|
+
exactly as stream reads do. One asymmetry is deliberate: an event log has no
|
|
459
|
+
closed state, so a session-event read wakes only on an appended event or the
|
|
460
|
+
clamped deadline, never on a close. Every store adapter enforces the same
|
|
461
|
+
rules, so a poll loop cannot drift between stream and session-event reads;
|
|
462
|
+
over the remote protocol the read is
|
|
463
|
+
`GET /sessions/{id}/events?since={n}&wait={n}`, parsed exactly like the
|
|
464
|
+
stream read's ([docs/remote-store-protocol.md](docs/remote-store-protocol.md),
|
|
465
|
+
§5).
|
|
466
|
+
|
|
467
|
+
### 7.2 Interface and adapters
|
|
468
|
+
|
|
469
|
+
Deliberately narrow: build runners need `append(event)`, `putArtifact`,
|
|
470
|
+
`getArtifact`, `getEvents(since)`; operator UIs add `listBuilds`, `subscribe`.
|
|
471
|
+
A build stream also exposes `appendIfCurrent(expectedSeq, event)`: one atomic
|
|
472
|
+
compare-and-append that returns no event when the stream has advanced. It is a
|
|
473
|
+
narrow coordination primitive for deterministic event-log deduplication under
|
|
474
|
+
concurrent processors; the event stream remains authoritative, with no
|
|
475
|
+
snapshot or side ledger. Sequence 0 names an empty stream, candidates receive
|
|
476
|
+
ordinary event validation, unknown builds reject, and a comparison miss leaves
|
|
477
|
+
the log and build timestamps untouched.
|
|
478
|
+
The same contract has repository-scoped `ensureRepo`, event/artifact deposit
|
|
479
|
+
and read methods, plus a repository lease. Before execution, a runner obtains
|
|
480
|
+
an interface-enforced handle scoped to exactly its build. Own-build records,
|
|
481
|
+
stream, artifacts, lease, and subscription remain available; another build,
|
|
482
|
+
collection/admin operations, nested foreign scope, and every repository-journal
|
|
483
|
+
operation reject. This guard applies identically to every adapter; remote tokens
|
|
484
|
+
carry the same authority over the wire as defense in depth rather than creating
|
|
485
|
+
it. Two implementations of one contract (`packages/core/src/store/contract.ts` is the shared
|
|
486
|
+
conformance suite):
|
|
487
|
+
|
|
488
|
+
1. **Local** — one self-contained state tree at `<main-repo>/.autobuild/` by
|
|
489
|
+
default: database, content-addressed blobs, Git worktrees, and the file
|
|
490
|
+
ticket source's default directory. The main checkout is derived from Git's
|
|
491
|
+
repository/worktree topology, so a command run inside a linked worktree
|
|
492
|
+
resolves the same state. There is no home-directory fallback or
|
|
493
|
+
machine-global state. Store selection is uniform: explicit `--store` >
|
|
494
|
+
nonempty `AB_STORE` > repository default. When a phase or Harvest CLI
|
|
495
|
+
process reopens this store, a handle wrapper derives exact build-or-repository
|
|
496
|
+
authority and agent-event session attribution from its validated ambient
|
|
497
|
+
identity. The same wrapper applies when `ab builds`, `ab build status`, or
|
|
498
|
+
`ab artifact download` is invoked with a complete ambient phase identity;
|
|
499
|
+
without identity those read forms retain unscoped operator access. This
|
|
500
|
+
preserves the remote adapter's observable resource boundary without making
|
|
501
|
+
the SQLite adapter itself ambient-aware.
|
|
502
|
+
2. **Remote** — the same store interface behind the independently deployed
|
|
503
|
+
hosted HTTP service, selected by an `http(s)://` reference. Kernels,
|
|
504
|
+
sandboxes, terminal dashboards, and later human-facing pages share this
|
|
505
|
+
service tier; it owns token authorization, protocol write validation,
|
|
506
|
+
artifact policy, and exact package/protocol version-skew enforcement. The
|
|
507
|
+
later human routes share its deployment so a browser or second service never
|
|
508
|
+
receives the machine signing secret. Artifacts remain content-by-value. The
|
|
509
|
+
documented [remote store protocol](docs/remote-store-protocol.md) is the
|
|
510
|
+
public BuildStore extension surface for independently implemented servers;
|
|
511
|
+
`packages/core/src/store/contract.ts`, driven through the shipped remote
|
|
512
|
+
client, is the conformance bar. Autobuild does not load in-process BuildStore
|
|
513
|
+
plugins. Git worktrees and default file tickets necessarily remain local.
|
|
514
|
+
|
|
515
|
+
`subscribe` is specced in the interface; the v2.0 implementation is polling
|
|
516
|
+
`getEvents(since)`. True push comes later.
|
|
517
|
+
|
|
518
|
+
### 7.3 Persistence granularity
|
|
519
|
+
|
|
520
|
+
Required phase outputs persist at **phase/round boundaries**; a killed phase
|
|
521
|
+
re-runs from its start. An agent may also explicitly deposit a review artifact
|
|
522
|
+
mid-session, including an atomic PR-attachment designation (§7.5, §8.2); these
|
|
523
|
+
immutable revisions are harmless if a killed phase later retries. The
|
|
524
|
+
streaming exception reserved here is now the stream primitive (§7.6): live
|
|
525
|
+
output persists continuously while an agent runs, so a session that dies
|
|
526
|
+
mid-turn leaves its transcript behind instead of nothing.
|
|
527
|
+
|
|
528
|
+
### 7.4 Resumption across sandboxes
|
|
529
|
+
|
|
530
|
+
Because state is the event log and artifacts live in the store, a *new*
|
|
531
|
+
sandbox can resume a build a dead sandbox started: pull events, rehydrate
|
|
532
|
+
scratch from latest artifact revisions, continue. v1 structurally could not
|
|
533
|
+
do this.
|
|
534
|
+
|
|
535
|
+
Four base-selection invariants make this safe with Git:
|
|
536
|
+
|
|
537
|
+
- **The base is chosen once, at first branch creation**, from the freshly
|
|
538
|
+
fetched origin tip of the configured base branch (fetched into a
|
|
539
|
+
build-scoped internal ref so concurrent dispatches cannot clobber each
|
|
540
|
+
other or the operator's refs). If the remote is unavailable, creation falls
|
|
541
|
+
back to the local base and records the diagnostic.
|
|
542
|
+
- **Re-provisioning never re-cuts.** An existing build branch is resumed at
|
|
543
|
+
its current tip — never rewound, rebased, or re-created from a newer base.
|
|
544
|
+
The first `workspace.provisioned` base remains immutable branch-cut
|
|
545
|
+
provenance; later `existing` facts are resume evidence, not new cuts.
|
|
546
|
+
- **Each implementation completion records the branch's effective target
|
|
547
|
+
divergence.** `ab done` asks the selected Forge to snapshot its authoritative
|
|
548
|
+
base when that capability exists; otherwise it fetches the frozen target
|
|
549
|
+
branch's current remote tip into a build-scoped private ref. It requires
|
|
550
|
+
exactly one merge-base with
|
|
551
|
+
the implementation `HEAD`. That merge-base and head become the durable
|
|
552
|
+
`implement.completed {commits}` range, so target history already absorbed by
|
|
553
|
+
the build is excluded while every branch-unique commit remains reviewable.
|
|
554
|
+
Resolution is refreshed across review rounds and sandbox resumption. Fetch,
|
|
555
|
+
ref, missing-ancestor, malformed-output, or ambiguous-base failure rejects
|
|
556
|
+
the terminal before push, artifact deposit, or completion event; there is no
|
|
557
|
+
stale fallback. This interrogation never moves the build branch, rewrites
|
|
558
|
+
history, writes `FETCH_HEAD`, or updates operator/remote-tracking refs.
|
|
559
|
+
- **Conditional verify keeps its own durable diff base.** Path applicability
|
|
560
|
+
still uses the initial branch-cut SHA until a completed reconcile promotes
|
|
561
|
+
that attempt's refreshed target. It does not consume the focused review base.
|
|
562
|
+
|
|
563
|
+
### 7.5 What the PR gets
|
|
564
|
+
|
|
565
|
+
A summary comment — verdict history, verification results, links into the
|
|
566
|
+
store. The full audit trail is queryable, not committed to the branch.
|
|
567
|
+
|
|
568
|
+
Any agent session can explicitly designate an exact deposited artifact as a PR
|
|
569
|
+
attachment through `ab artifact put <kind> <file> --attach`. Designation is a
|
|
570
|
+
typed, atomic event on the ordinary artifact channel — never an artifact-name,
|
|
571
|
+
verify-step, or report-format convention. The current post-spec-restart
|
|
572
|
+
designation for each artifact kind is projected as text with an exact pinned
|
|
573
|
+
retrieval command; distinct kinds remain distinct. The BuildStore copy is
|
|
574
|
+
always authoritative, and non-image artifacts use this same path.
|
|
575
|
+
|
|
576
|
+
Optionally, `[pr.imageHost]` may name a **public** GitHub release so designated
|
|
577
|
+
`image/*` media render inline during review. The selected Forge must expose the
|
|
578
|
+
optional `PrAttachmentHosting` capability to serve upload and reclamation;
|
|
579
|
+
without it the supported path remains the complete text-only projection.
|
|
580
|
+
Non-images never cross that host boundary. Enabling it is an explicit
|
|
581
|
+
public-disclosure choice made in config, and hosted copies are temporary — the
|
|
582
|
+
dispatcher reclaims them after the build reaches a terminal outcome, while
|
|
583
|
+
store artifacts remain under the store's retention policy. Upload/validation/timeout failures record follow-up
|
|
584
|
+
observations and preserve the complete text projection; they never fail
|
|
585
|
+
verification or block finalize. Agents receive no forge credentials. A
|
|
586
|
+
designation after the PR exists publishes a new complete summary, so finalize
|
|
587
|
+
post-steps and post-reconcile verifiers need no custom phase.
|
|
588
|
+
|
|
589
|
+
### 7.6 Streams (the third primitive)
|
|
590
|
+
|
|
591
|
+
The store's content model is three primitives, each doing one job:
|
|
592
|
+
|
|
593
|
+
- **Events** carry facts — small, typed, append-only, the routing spine.
|
|
594
|
+
- **Artifacts** carry bulk — content-addressed bytes deposited at boundaries.
|
|
595
|
+
- **Streams** carry the agent's running output — token deltas, tool calls and
|
|
596
|
+
results, reasoning — which is too bulky for the event log and arrives too
|
|
597
|
+
continuously to be an artifact until a session ends.
|
|
598
|
+
|
|
599
|
+
A stream is an append-only, per-stream sequenced log of **chunks** (a chunk is
|
|
600
|
+
a nonempty batch of protocol parts) with an open-then-closed lifecycle. The
|
|
601
|
+
chunk vocabulary is the Vercel AI SDK UI Message Stream protocol, version 1 of
|
|
602
|
+
the current major (AI SDK 7): the store holds the protocol's parts rather than
|
|
603
|
+
a private shape, so a read route can re-emit them as Server-Sent Events that
|
|
604
|
+
the AI SDK's own client hooks and any customer's tooling consume unchanged.
|
|
605
|
+
The store performs no protocol validation on append; each part only needs a
|
|
606
|
+
nonempty-string `type`, and the store assigns each batch its per-stream
|
|
607
|
+
sequence (from 1) and timestamp, so producers cannot fake ordering.
|
|
608
|
+
|
|
609
|
+
Closing a stream assembles its chunks into the protocol's `UIMessage[]`
|
|
610
|
+
document — dropping parts of undefined types while counting them — and
|
|
611
|
+
deposits that document as an artifact (`stream:<streamId>`, revision 0) on the
|
|
612
|
+
owning scope in the same atomic operation that marks the stream closed. A
|
|
613
|
+
stream that dies mid-turn stays open and readable, so its output survives.
|
|
614
|
+
|
|
615
|
+
Close serializes against append, per stream: an append issued while a stream
|
|
616
|
+
is being closed — or after it has closed — either lands in the finalized
|
|
617
|
+
artifact or is rejected with `StreamClosedError`; an accepted append is never
|
|
618
|
+
silently omitted from the artifact the close deposits. Appends to other
|
|
619
|
+
streams are unaffected.
|
|
620
|
+
|
|
621
|
+
Closing an already-closed stream is a no-op that returns the closed record.
|
|
622
|
+
The repeated close's outcome argument is ignored; the artifact the first
|
|
623
|
+
close deposited is left untouched.
|
|
624
|
+
|
|
625
|
+
**Rejections write nothing.** Append validation and every rejection below
|
|
626
|
+
happen before any mutation: a rejected operation leaves no chunk, no sequence
|
|
627
|
+
number, and no artifact. An append batch must be a nonempty array of JSON
|
|
628
|
+
objects, each carrying a nonempty-string `type`; a batch that fails this
|
|
629
|
+
shape is rejected with a plain error, and a batch whose serialized JSON
|
|
630
|
+
exceeds 1,048,576 bytes is rejected with `StreamBatchTooLargeError`.
|
|
631
|
+
Operations that address an unknown stream id — append, read, and close — are
|
|
632
|
+
rejected with a plain error; `getStream` alone answers `null` for an unknown
|
|
633
|
+
id, never an error. The contract is common to every shipped store — the
|
|
634
|
+
in-memory reference, the local SQLite adapter, and the remote Postgres-backed
|
|
635
|
+
service. Over the remote protocol the typed rejections cross the wire as
|
|
636
|
+
status codes — the ceiling as 413, the closed-append rejection above as 409,
|
|
637
|
+
the unknown stream as 404 — and the shipped client rehydrates the same error
|
|
638
|
+
types; `docs/remote-store-protocol.md` specifies the wire surface.
|
|
639
|
+
|
|
640
|
+
**Rejection precedence is transport-dependent, deliberately.** The store-side
|
|
641
|
+
adapters reject an append in the order part validation, batch-size ceiling,
|
|
642
|
+
unknown stream, closed stream: a malformed or oversized batch addressed to an
|
|
643
|
+
unknown stream id reports the validation error or `StreamBatchTooLargeError`,
|
|
644
|
+
never unknown-stream — the batch is validated and size-checked before the
|
|
645
|
+
per-stream critical section, and the unknown-stream lookup inside it precedes
|
|
646
|
+
the closed check. Over the remote protocol the server resolves and authorizes
|
|
647
|
+
the stream before parsing the request body, so the same request is rejected
|
|
648
|
+
`404 not-found` (`unknown stream "…"`) before the body schema or the ceiling
|
|
649
|
+
is evaluated. Both orders are deliberate: the 404-first order is the server's
|
|
650
|
+
no-existence-leak shape (an unknown stream and a foreign-scoped stream are the
|
|
651
|
+
same 404), while the local order keeps malformed input failing before any
|
|
652
|
+
per-stream synchronization. Writer-facing clients must therefore not assume a
|
|
653
|
+
single precedence across transports — a malformed batch on an unknown stream
|
|
654
|
+
legitimately rejects with different errors by transport — and both orders
|
|
655
|
+
reject before any mutation: no chunk, no sequence number, no artifact.
|
|
656
|
+
|
|
657
|
+
Chunk retention is deposit-path and count-based, like artifact retention: at
|
|
658
|
+
the next stream create in a scope, every previously closed stream's chunks
|
|
659
|
+
except the most recently closed are deleted; finalized artifacts are never
|
|
660
|
+
touched.
|
|
661
|
+
|
|
662
|
+
Streams are scoped to a build, to a repository, or to an operator session
|
|
663
|
+
(§7.1.1) — the scope vocabulary is closed. Reads are cursor-based
|
|
664
|
+
(`since` sequence) with an optional bounded wait (`waitSeconds`, whole
|
|
665
|
+
seconds) that returns as soon as a chunk lands or the stream closes — the
|
|
666
|
+
resumable live channel every frontend would otherwise invent for itself.
|
|
667
|
+
The wait is bounded at `MAX_STREAM_WAIT_SECONDS` = 30: a `waitSeconds` above
|
|
668
|
+
30 clamps to 30, so a request to wait 61 seconds returns no later than 30.
|
|
669
|
+
Every store — the in-memory reference, the SQLite adapter, and the remote
|
|
670
|
+
client/server path — enforces the same clamp, and session-event reads
|
|
671
|
+
(§7.1.1) honor the identical rule so a poll loop cannot drift between the
|
|
672
|
+
two.
|
|
673
|
+
|
|
674
|
+
**Presentation, never routing.** No kernel, engine, reducer, or dispatcher
|
|
675
|
+
decision reads stream content; outcomes travel only the typed CLI. Writers
|
|
676
|
+
(agent runners emitting parts) and readers (operator UIs) are separate
|
|
677
|
+
tickets; this section defines only the primitive.
|
|
678
|
+
|
|
679
|
+
## 8. The `ab` CLI
|
|
680
|
+
|
|
681
|
+
The **only** channel between agents and the store — and the enforcement
|
|
682
|
+
point of the entire ontology. Designed by walking every phase's session from
|
|
683
|
+
first command to terminal command, plus the failure paths (§8.7). Decisions
|
|
684
|
+
forced by the walkthrough are marked **[D5]–[D8]**, continuing §15's series.
|
|
685
|
+
|
|
686
|
+
It buys three properties at once:
|
|
687
|
+
|
|
688
|
+
1. **Storage-agnostic skills** — local or remote store, same commands.
|
|
689
|
+
2. **An enforced ontology** — the CLI validates schemas at deposit; verdicts
|
|
690
|
+
travel a typed channel, never parsed from stdout (the root-cause fix for
|
|
691
|
+
v1 sentinel parsing).
|
|
692
|
+
3. **Structured observations at the point of capture**, not prose mined from
|
|
693
|
+
`observations.md` after the fact.
|
|
694
|
+
|
|
695
|
+
The kernel's job then reduces to: read the event log, decide the next phase
|
|
696
|
+
per the transition table, ask the AgentRunner to invoke the named skill with
|
|
697
|
+
its runtime-native syntax (`/{skill} {build}` for Claude/Pi, `$<skill> {build}`
|
|
698
|
+
for Codex), wait for the terminal event, repeat.
|
|
699
|
+
|
|
700
|
+
### 8.1 Invocation model and ambient auth [D8]
|
|
701
|
+
|
|
702
|
+
The runner launches every session with environment set:
|
|
703
|
+
|
|
704
|
+
```
|
|
705
|
+
AB_STORE # store URL or local path
|
|
706
|
+
AB_BUILD # build slug
|
|
707
|
+
AB_PHASE # current phase (+ round)
|
|
708
|
+
AB_SESSION # session id
|
|
709
|
+
AB_TOKEN # transports the scoped handle's authority to a remote store
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
A harvest session instead carries `AB_REPO`, `AB_HARVEST`, and an `AB_PHASE`
|
|
713
|
+
of `synthesize@N` or `review@N`. The AgentRunner invocation argument is
|
|
714
|
+
opaque: a build skill receives its slug; a harvest skill receives its run id.
|
|
715
|
+
The CLI resolves identity from ambient auth. Every build process holds an
|
|
716
|
+
interface-enforced build-scoped Store handle. At the phase CLI opening boundary,
|
|
717
|
+
a remote handle forwards the runner-minted token unchanged, while a local handle
|
|
718
|
+
is wrapped with authority over exactly `AB_BUILD` or `AB_REPO`. Build and
|
|
719
|
+
repository resources cannot cross, and naming another resource to a Store
|
|
720
|
+
operation cannot widen either handle. Event-bearing writes attributed to an
|
|
721
|
+
agent must name the ambient `AB_SESSION`; non-agent writes remain allowed so the
|
|
722
|
+
CLI can perform its existing trusted kernel plumbing, including finalize.
|
|
723
|
+
Operator and kernel processes with no agent identity markers retain unscoped
|
|
724
|
+
Store access. Three read-only forms are also valid inside a phase: `ab build
|
|
725
|
+
status` and `ab artifact download` may read only `AB_BUILD`, while `ab builds`
|
|
726
|
+
is denied because collection access cannot represent one build. A complete
|
|
727
|
+
Harvest identity has repository scope and cannot use those build/admin reads.
|
|
728
|
+
If any build or Harvest identity marker is present, the corresponding
|
|
729
|
+
`AB_STORE`/resource/phase/session tuple must be complete, valid, and unambiguous;
|
|
730
|
+
partial, empty, malformed, shared-only, or mixed identity fails before Store
|
|
731
|
+
access rather than falling back to operator authority. `AB_STORE` and
|
|
732
|
+
`AB_TOKEN` alone remain operator selection and credentials, not identity.
|
|
733
|
+
Remote tokens transport the corresponding resource and session authority over
|
|
734
|
+
the wire; build-process resource scope is already enforced by the Store
|
|
735
|
+
interface. Least privilege comes from the Store and runner, not prompt
|
|
736
|
+
instructions.
|
|
737
|
+
|
|
738
|
+
Every phase and harvest turn also receives a runner-controlled `PATH` prefix
|
|
739
|
+
containing a private `ab` launcher from the same Autobuild distribution that
|
|
740
|
+
started the session, applied after ambient and scoped environment merging —
|
|
741
|
+
so a host executable named `ab` can never shadow the typed CLI, and agent
|
|
742
|
+
sessions need no separate global installation.
|
|
743
|
+
|
|
744
|
+
### 8.2 Command surface
|
|
745
|
+
|
|
746
|
+
| Command | Purpose | Terminal? |
|
|
747
|
+
|---|---|---|
|
|
748
|
+
| `ab context` | hydrate `.ab/` with the phase's inputs; print the manifest | no |
|
|
749
|
+
| `ab artifact put <kind> <file> [--attach]` | deposit a versioned artifact → returns rev; optionally designate that exact revision for the PR | no |
|
|
750
|
+
| `ab artifact get <kind>[@rev]` | fetch an artifact within own build | no |
|
|
751
|
+
| `ab artifact download …` | read-only exact-byte retrieval; operator-wide without identity, own-build only with ambient phase identity; works after build termination | no |
|
|
752
|
+
| `ab observe --kind <followup\|refactor\|latent-bug> …` | structured observation, any phase, any time | no |
|
|
753
|
+
| `ab done` | complete a producer phase (validates, then runs phase plumbing) | **yes** |
|
|
754
|
+
| `ab verdict <approve\|revise\|escalate\|pass\|fail\|skip> …` | complete a review/verify phase | **yes** |
|
|
755
|
+
| `ab escalate <question>` | park the build for human input | **yes** |
|
|
756
|
+
| `ab mcp [--store <ref>] [--repo <id>]` | serve the agent tool registry as an MCP server over stdio; fails closed inside a phase | no |
|
|
757
|
+
|
|
758
|
+
The read-only `ab builds`, `ab build status`, and `ab artifact download` forms
|
|
759
|
+
require no session identity for operator use and preserve their repository-wide
|
|
760
|
+
results and store-selection precedence in that case. When a complete ambient
|
|
761
|
+
phase tuple is present, they retain its local or remote Store authority:
|
|
762
|
+
own-build status/download succeed, foreign and repository-scoped targets fail,
|
|
763
|
+
and `ab builds` fails at repository-wide `listBuilds` access rather than
|
|
764
|
+
filtering. Malformed ambient identity fails closed as defined in §8.1.
|
|
765
|
+
|
|
766
|
+
The verdict vocabulary is phase-dependent and the CLI enforces it: review
|
|
767
|
+
phases accept `approve|revise|escalate`; agent-verify steps accept
|
|
768
|
+
`pass|fail|skip` (`fail` requires a report; `skip` requires a reason).
|
|
769
|
+
Deterministic checks never touch the CLI — the kernel runs them directly.
|
|
770
|
+
Exact flags and validation live with the CLI implementation and its tests.
|
|
771
|
+
|
|
772
|
+
### 8.3 What `ab context` materializes
|
|
773
|
+
|
|
774
|
+
A phase-scoped hydration into the gitignored scratch dir:
|
|
775
|
+
|
|
776
|
+
```
|
|
777
|
+
.ab/
|
|
778
|
+
context.json # manifest: build, phase, round, artifact revs,
|
|
779
|
+
# required deposits, allowed terminal commands
|
|
780
|
+
spec.md
|
|
781
|
+
plan.md # per phase needs (see table)
|
|
782
|
+
findings.json # current feedback, when round > 1
|
|
783
|
+
history/ # prior-round artifacts where the phase needs them
|
|
784
|
+
verify/ # failure reports routed back to implement
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
The manifest tells the agent its contract — `required` deposits and
|
|
788
|
+
`allowedTerminals` — so skills are self-checking against the same data the
|
|
789
|
+
CLI validates with. Per-phase inputs and terminals:
|
|
790
|
+
|
|
791
|
+
| Phase | Materialized inputs | Terminal |
|
|
792
|
+
|---|---|---|
|
|
793
|
+
| `plan` | ticket, spec; prior plan rev + findings (round > 1) | `done` (requires `plan` artifact) |
|
|
794
|
+
| `plan-review` | spec, plan@latest, all prior rounds' findings (for `persists` marking) | `verdict` (requires notes artifact) |
|
|
795
|
+
| `implement` | spec, approved plan, feedback (findings **or** verify report), own prior notes | `done` (requires clean worktree + notes) |
|
|
796
|
+
| `code-review` | spec, plan, commit range `{base, head}`, prior findings, implement-notes | `verdict` |
|
|
797
|
+
| `verify:<step>` (agent) | spec (acceptance criteria), step config, commit range | `verdict pass` \| `verdict fail --report` \| `verdict skip --reason` |
|
|
798
|
+
| `finalize` | spec, plan, verify reports, PR template config | `done` (requires `pr-description` artifact) |
|
|
799
|
+
| `reconcile` | spec, plan, implement-notes, conflict `{baseSha}` from this attempt's `reconcile.started` | `done` (requires merge commit present) |
|
|
800
|
+
|
|
801
|
+
Scoping is deliberate: the planner never sees code-review rounds; the
|
|
802
|
+
reviewer sees prior findings but not the producer's session. What a phase
|
|
803
|
+
*can't* see is part of its design.
|
|
804
|
+
|
|
805
|
+
### 8.4 Terminal discipline [D5]
|
|
806
|
+
|
|
807
|
+
**Every phase ends with exactly one terminal command** — `done`, `verdict`,
|
|
808
|
+
or `escalate`. The CLI rejects a second terminal call, and each terminal
|
|
809
|
+
validates its preconditions before emitting the phase event (no `done`
|
|
810
|
+
without the required artifacts; no `done` on a dirty worktree in
|
|
811
|
+
`implement`). A normally completed turn that ends **without** any terminal
|
|
812
|
+
call is an infra failure: the runner emits
|
|
813
|
+
`phase.failed {error: "no-terminal"}` and applies retry policy.
|
|
814
|
+
|
|
815
|
+
Provider/runtime-declared turn failures are a separate `AgentRunner` result
|
|
816
|
+
(§9), never conflated with agent silence. When the selected role has declared
|
|
817
|
+
alternates, availability failures (overload, rate limit, 5xx, timeout,
|
|
818
|
+
transport, and unclassified provider failure) and provider exhaustion (quota,
|
|
819
|
+
usage limit, and billing) start the next target inside the same phase attempt.
|
|
820
|
+
Authentication, permission, and local runtime-configuration failures do not.
|
|
821
|
+
Each failed target's transcript is deposited. Only a stopped or exhausted chain
|
|
822
|
+
emits one `phase.failed` with the final provider message verbatim; that final
|
|
823
|
+
failure controls retry policy. Provider exhaustion skips the retry budget and
|
|
824
|
+
raises a policy escalation, while availability uses the existing bounded
|
|
825
|
+
budget. Every build-agent session also has a kernel-owned wall-clock budget,
|
|
826
|
+
resolved from its logical role and then `[policy].sessionBudgetSeconds`. Expiry
|
|
827
|
+
aborts the turn, ends any available handle, drops producer continuation state,
|
|
828
|
+
and emits retryable `phase.failed` with `phase session budget expired after
|
|
829
|
+
<seconds> seconds`; it is kernel policy and therefore does not select a provider
|
|
830
|
+
alternate. The existing phase-attempt guard retries and then raises an
|
|
831
|
+
answerable policy escalation. Operator cancellation and the captured deadline
|
|
832
|
+
remain independent safeguards: an operator request aborts the adapter promptly,
|
|
833
|
+
but the deadline still releases the wait if the adapter ignores cancellation.
|
|
834
|
+
When the operator request arrived first, that release remains an abort control
|
|
835
|
+
outcome and appends no phase failure or provider substitution. Agent finalize
|
|
836
|
+
post-step expiry remains failure-tolerant. Harvest sessions and direct check
|
|
837
|
+
commands are outside this budget. If the turn already wrote a valid typed
|
|
838
|
+
terminal, that terminal remains authoritative and no contradictory failure is
|
|
839
|
+
appended.
|
|
840
|
+
|
|
841
|
+
This completes the sentinel-parsing replacement: success is only expressible
|
|
842
|
+
through the typed channel, so "the agent rambled and exited" can never be
|
|
843
|
+
misread as completion, while a rejected turn cannot be misreported as agent
|
|
844
|
+
silence.
|
|
845
|
+
|
|
846
|
+
### 8.5 Atomic deposits, validation as feedback [D6]
|
|
847
|
+
|
|
848
|
+
Terminal commands are **atomic bundles**: `ab verdict revise --findings
|
|
849
|
+
f.json --notes review.md` stores the notes artifact, validates and
|
|
850
|
+
id-stamps the findings, and appends the `*.verdict` event in one operation —
|
|
851
|
+
there is no state where an artifact exists without its event or vice versa.
|
|
852
|
+
Validation failures (malformed findings JSON, missing required artifact)
|
|
853
|
+
return the schema and a precise error to the agent *in-session*, so the
|
|
854
|
+
correction loop is immediate and cheap — schema errors are agent feedback,
|
|
855
|
+
not build failures.
|
|
856
|
+
|
|
857
|
+
### 8.6 Agents never touch the remote [D7]
|
|
858
|
+
|
|
859
|
+
All `git push`, PR creation, and forge API calls happen kernel-side. `ab done`
|
|
860
|
+
in `implement` first establishes the focused target merge-base, then pushes
|
|
861
|
+
and records `{base, head}`; `ab done` in `finalize` has the kernel open the PR
|
|
862
|
+
using the deposited `pr-description` artifact. After a successful
|
|
863
|
+
content-producing `finalize:*` step, the runner
|
|
864
|
+
requires a clean worktree and a descendant local `HEAD`, then regular-pushes
|
|
865
|
+
the configured build branch and records that head before completing the step.
|
|
866
|
+
A no-op step has no push. Agents only ever commit locally. Consequences: forge
|
|
867
|
+
credentials **never enter the sandbox** (load-bearing once builds run on
|
|
868
|
+
remote sandboxes), history is extended rather than force-pushed, and the
|
|
869
|
+
push-at-boundary rule from [D3] is enforced by construction rather than
|
|
870
|
+
convention.
|
|
871
|
+
|
|
872
|
+
### 8.7 Walkthroughs
|
|
873
|
+
|
|
874
|
+
**Happy path, one code-loop round-trip:**
|
|
875
|
+
|
|
876
|
+
```
|
|
877
|
+
implementer: ab context → (work, commit) → ab observe --kind refactor "…"
|
|
878
|
+
→ ab done --notes .ab/implement-notes.md
|
|
879
|
+
⇒ validates clean worktree and focused review boundary,
|
|
880
|
+
pushes branch, emits implement.completed {commits, artifact}
|
|
881
|
+
reviewer: ab context (gets spec, plan, {base,head}, prior findings)
|
|
882
|
+
→ ab verdict revise --findings f.json --notes review.md
|
|
883
|
+
⇒ stamps finding ids, stores artifact,
|
|
884
|
+
emits code-review.verdict
|
|
885
|
+
implementer: ab context (findings.json now materialized) → …
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
**Failure paths:**
|
|
889
|
+
|
|
890
|
+
- *Completed turn, no terminal* → `phase.failed {no-terminal}`, retry per
|
|
891
|
+
policy [D5].
|
|
892
|
+
- *Session budget expiry* → abort and best-effort `end`, then retryable
|
|
893
|
+
`phase.failed {error: "phase session budget expired after … seconds"}`; retry
|
|
894
|
+
from the primary and escalate under the existing attempt cap. If operator
|
|
895
|
+
abort already owns cancellation, the same deadline only bounds the wait and
|
|
896
|
+
the runner acknowledges abort without a phase failure [D5].
|
|
897
|
+
- *Provider rejection* → transcript deposited; eligible failures walk the
|
|
898
|
+
role's declared alternates within one attempt. A stopped/exhausted chain
|
|
899
|
+
emits one `phase.failed` with the final verbatim error and every tried target;
|
|
900
|
+
credentials/configuration stop immediately, final exhaustion escalates
|
|
901
|
+
instead of retrying, and final availability uses bounded retry [D5].
|
|
902
|
+
- *Malformed deposit* → rejected in-session with schema + error; agent
|
|
903
|
+
corrects and retries [D6].
|
|
904
|
+
- *Crash after deposits, before terminal* → artifacts are revisioned; the
|
|
905
|
+
re-run phase deposits fresh revs; orphaned revs are harmless history.
|
|
906
|
+
- *Wrong-resource access or wrong-session agent write* → the applicable
|
|
907
|
+
build-scoped or ambient-session Store handle rejects it at the interface;
|
|
908
|
+
a remote token enforces the same scope over the wire [D8].
|
|
909
|
+
- *Store unreachable* → CLI retries with backoff; a phase that cannot
|
|
910
|
+
deposit cannot complete → `phase.failed`, runner-level policy takes over.
|
|
911
|
+
|
|
912
|
+
### 8.8 Outer-loop namespace
|
|
913
|
+
|
|
914
|
+
Human/pre-build ticket grooming uses one configured-source namespace
|
|
915
|
+
(`ab ticket create|update|block|unblock|list|show|move`). These commands are
|
|
916
|
+
sessionless, source-agnostic, and never available as a mid-build spec
|
|
917
|
+
mutation path. Every subcommand accepts `--json` and then writes exactly one
|
|
918
|
+
bare value: `list` emits `Ticket[]`; every other form emits the complete
|
|
919
|
+
created, read, or post-mutation `Ticket`. Human-readable confirmations are not
|
|
920
|
+
a data format and callers must not parse ids from them.
|
|
921
|
+
|
|
922
|
+
`block` and `unblock` accept a comma-separated blocker-id list. The CLI
|
|
923
|
+
first deduplicates and validates the complete list, including blocker existence
|
|
924
|
+
for both operations and direct self-block rejection when adding, then applies
|
|
925
|
+
the existing idempotent one-edge port methods; invalid input writes no edge.
|
|
926
|
+
Dependencies gate only the dispatcher's claim. Changing blockers after a ticket
|
|
927
|
+
has already been claimed does not stop its active build. A repository that
|
|
928
|
+
creates tickets directly in its ready state must therefore create a dependency
|
|
929
|
+
chain in dependency order, carrying each predecessor id into the next create,
|
|
930
|
+
before a later ticket can be claimed.
|
|
931
|
+
|
|
932
|
+
Observation harvest uses a separate typed, repository-scoped namespace
|
|
933
|
+
(`ab harvest context|submit|verdict|status`), mirroring the build session
|
|
934
|
+
commands: context hydration, a producer terminal, a reviewer terminal, and a
|
|
935
|
+
sessionless read-only status projection.
|
|
936
|
+
|
|
937
|
+
`ab repository status [--json] [--store <ref>]` is the sessionless read-only
|
|
938
|
+
projection of the repository journal's dispatcher controls: ticket intake, the
|
|
939
|
+
repository-wide pause, and the claim-time auto-merge default. It uses the same
|
|
940
|
+
dispatch-settings reducer as dispatcher decisions and the dashboard. An absent
|
|
941
|
+
repository stream reduces as an empty journal and reports intake on, repository
|
|
942
|
+
pause off, and auto-merge default off; the query does not create that stream,
|
|
943
|
+
append an event, claim a ticket, attach a runner, or start dispatcher work.
|
|
944
|
+
|
|
945
|
+
Agents never receive TicketSource credentials. Only the deterministic file
|
|
946
|
+
step creates/adopts approved proposals and commits ledger facts.
|
|
947
|
+
|
|
948
|
+
### 8.9 The agent tool registry
|
|
949
|
+
|
|
950
|
+
`packages/core/src/operator/registry.ts` is the agent tool surface: one typed,
|
|
951
|
+
closed table of tools — each naming the tool, describing it for a model,
|
|
952
|
+
declaring its input schema and MCP annotations, carrying an approval class, and
|
|
953
|
+
binding a handler that calls the same operator services the operator API routes
|
|
954
|
+
call. It is the single source of truth for the agent-facing operator surface,
|
|
955
|
+
and every binding is generated from it; the table never widens beyond the
|
|
956
|
+
operator services, and a contract suite proves, tool by tool, that each
|
|
957
|
+
handler's result equals the corresponding route's.
|
|
958
|
+
|
|
959
|
+
The shipped binding is `ab mcp`, a stdio MCP server over the registry: the CLI
|
|
960
|
+
remains the primary local interface for agents that have a shell, while the
|
|
961
|
+
binding exists so the registry can be exercised end to end from a local
|
|
962
|
+
install, for dogfooding, and for local agents that cannot run commands. It
|
|
963
|
+
fails closed inside a phase session — repository-wide operator authority cannot
|
|
964
|
+
be narrowed to the ambient build — and `--repo` constrains every call to one
|
|
965
|
+
repository. Mutating tools attribute their writes to the sessionless operator
|
|
966
|
+
identity, exactly as the other operator commands do.
|
|
967
|
+
|
|
968
|
+
## 9. AgentRunner
|
|
969
|
+
|
|
970
|
+
Session-based, because review loops need memory:
|
|
971
|
+
|
|
972
|
+
```ts
|
|
973
|
+
type Result =
|
|
974
|
+
| { kind: 'completed', text, usage }
|
|
975
|
+
| { kind: 'failed', text, usage,
|
|
976
|
+
failure: { message, permanent, cause?: 'availability' | 'exhaustion' |
|
|
977
|
+
'credentials' | 'configuration' } }
|
|
978
|
+
|
|
979
|
+
interface AgentRunner {
|
|
980
|
+
start(opts: { skill, invocation, workspace, model, … }): { session, result }
|
|
981
|
+
continue(session, message): Result // review-loop rounds
|
|
982
|
+
end(session): Transcript // → store, always
|
|
983
|
+
}
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
The discriminator is a port-level requirement: an SDK/provider-declared error
|
|
987
|
+
must never be returned as `completed`, so every adapter inherits the
|
|
988
|
+
distinction from `no-terminal`. `failure.message` preserves provider text and
|
|
989
|
+
`cause` separates availability, provider exhaustion, credential/permission,
|
|
990
|
+
and local runtime configuration. `permanent` remains the retry-policy and
|
|
991
|
+
legacy-plugin compatibility bit: exhaustion and credential/configuration
|
|
992
|
+
failures are permanent, while availability uses existing bounded retry. A
|
|
993
|
+
legacy plugin may omit `cause`; `permanent: false` remains alternate-eligible
|
|
994
|
+
and `permanent: true` remains a stopping failure. A failed `start` still returns
|
|
995
|
+
an endable session handle, guaranteeing transcript deposition.
|
|
996
|
+
|
|
997
|
+
Narrow non-phase judgments do not widen this contract. A runtime may
|
|
998
|
+
separately register an optional one-shot completion capability (tool-free,
|
|
999
|
+
non-resumable), used for slug naming (§6.3) and vendored-skill conflict
|
|
1000
|
+
resolution (§16.3). A runtime without it is valid; each caller owns its
|
|
1001
|
+
deterministic fail-safe.
|
|
1002
|
+
|
|
1003
|
+
- **Adapters:** three builtins sit behind the interface: the locally installed
|
|
1004
|
+
Claude Code CLI in headless mode for Claude models; the locally installed
|
|
1005
|
+
Codex CLI for unqualified `gpt-*` models; and the locally installed pi CLI
|
|
1006
|
+
(minimum version 0.84.3), driven headlessly through `--mode rpc`, for every
|
|
1007
|
+
provider-qualified model that installation resolves. Each adapter uses its
|
|
1008
|
+
CLI's local authenticated login; pi also owns its local model catalog and
|
|
1009
|
+
keeps one ephemeral RPC process alive across review-loop continuations. All
|
|
1010
|
+
three provide tool-free one-shot completion. Plugins may register additional
|
|
1011
|
+
adapters, each under a *distinct runtime name*, never
|
|
1012
|
+
as a mode flag on an existing one. Plugin adapters must pass the exported
|
|
1013
|
+
AgentRunner contract suite.
|
|
1014
|
+
- **Routing — explicit role inheritance (§16.1):** runtime, model, extra CLI
|
|
1015
|
+
arguments (`args`), and build-session budget live in one open `[roles]` map whose
|
|
1016
|
+
reserved `default` entry is the inheritance base and must explicitly name a
|
|
1017
|
+
runtime. The budget inherits independently, then falls back to
|
|
1018
|
+
`[policy].sessionBudgetSeconds`; it belongs to the logical role, so provider
|
|
1019
|
+
alternates cannot override it.
|
|
1020
|
+
|
|
1021
|
+
**Which key a session selects.** One rule for both kinds of agent step: an
|
|
1022
|
+
agent verify step and an agent finalize step each select `[roles.<step>]` by
|
|
1023
|
+
their *logical step name*; core phases select `[roles.<phase>]`.
|
|
1024
|
+
|
|
1025
|
+
```toml
|
|
1026
|
+
[verify.e2e]
|
|
1027
|
+
kind = "agent"
|
|
1028
|
+
skill = "verify-app-e2e" # repository-authored skill
|
|
1029
|
+
|
|
1030
|
+
[roles.e2e] # the STEP name — not "verify-app-e2e"
|
|
1031
|
+
runtime = "pi"
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
The step's configured skill name remains a deprecated alias for existing
|
|
1035
|
+
configurations and will be removed in a future release. It is consulted only
|
|
1036
|
+
when `[roles.<step>]` is undeclared, so the step name always wins.
|
|
1037
|
+
|
|
1038
|
+
**What is and is not a fallback**, stated separately so the mechanisms are
|
|
1039
|
+
not conflated:
|
|
1040
|
+
|
|
1041
|
+
1. Primary field resolution never hunts for a compatible substitute. Every
|
|
1042
|
+
concrete role merges runtime, model, and `args` over `default`
|
|
1043
|
+
independently; an incompatible merged pair is an eager error. A declared
|
|
1044
|
+
`args` list, including `[]`, replaces the inherited list wholesale.
|
|
1045
|
+
2. A **requested** role key that no `[roles.<key>]` declares resolves to
|
|
1046
|
+
`[roles.default]` wholesale. That is undeclared-key fallback, not outage
|
|
1047
|
+
routing.
|
|
1048
|
+
3. A role may declare `alternates = [{ runtime?, model?, args? }, …]`.
|
|
1049
|
+
The list is independently inherited: a role's own list,
|
|
1050
|
+
including `[]`, replaces the default list wholesale. Each entry overlays
|
|
1051
|
+
that concrete role's effective primary axes and is resolved and validated
|
|
1052
|
+
eagerly with indexed problems in the aggregate startup error.
|
|
1053
|
+
4. A **declared** key that nothing ever requests is resolved, validated, and
|
|
1054
|
+
then never used. `ab dispatch` reports it at startup as a warning.
|
|
1055
|
+
|
|
1056
|
+
Each `args` string is one argv token, preserved in order and appended to
|
|
1057
|
+
every phase and tool-free one-shot invocation for that role. Adapters reject
|
|
1058
|
+
only model selection and the machine-readable mode they emit and parse:
|
|
1059
|
+
Claude owns `-p`/`--print`, `--output-format`, and `--model`; Codex owns
|
|
1060
|
+
`--json` and `--model`/`-m`; Pi owns `--mode` and `--model`. An effective
|
|
1061
|
+
primary or alternate list containing one (including `--option=value` or a
|
|
1062
|
+
documented compact short alias) joins the aggregate eager startup error.
|
|
1063
|
+
Every other option passes through without Autobuild validation, even when it
|
|
1064
|
+
changes sandbox, approval, session, tool, skill, thinking, extension, or
|
|
1065
|
+
lifecycle behavior or supplements another adapter-emitted option. Pi always
|
|
1066
|
+
disables ambient extension discovery and explicitly loads the Autobuild
|
|
1067
|
+
bridge; a role can add repeatable `--extension <path>` tokens through `args`,
|
|
1068
|
+
and all tools from that explicitly loaded extension are available alongside
|
|
1069
|
+
the bridge. With no such args, Pi loads no operator-installed extensions.
|
|
1070
|
+
The deprecated `extensions` field remains schema-valid but has no effect and
|
|
1071
|
+
produces a startup warning directing the operator to `args`.
|
|
1072
|
+
|
|
1073
|
+
All roles
|
|
1074
|
+
resolve **eagerly, before any session launches**, with problems aggregated
|
|
1075
|
+
into one error. A missing default diagnostic includes a copyable table and
|
|
1076
|
+
every materialized runtime name. Builtin and plugin registrations use the same model-family,
|
|
1077
|
+
default-model, session, and optional one-shot capability path; adding a
|
|
1078
|
+
runtime touches only the adapter registry, never the kernel. Mixing models
|
|
1079
|
+
across roles is intentional — a different reviewer catches more. The
|
|
1080
|
+
Within one agent-session attempt, eligible failures try the primary then each
|
|
1081
|
+
alternate in order without spending another phase attempt. Each target has a
|
|
1082
|
+
fresh session bracket. A failed producer continuation may therefore continue
|
|
1083
|
+
on a different runtime only by rehydrating durable context, not by inheriting
|
|
1084
|
+
the failed conversation. Selection is non-sticky: every later phase, loop
|
|
1085
|
+
continuation, and retry starts from the primary. The final failure controls
|
|
1086
|
+
retry/permanent behavior. This applies to core phases, agent verify/finalize,
|
|
1087
|
+
and Harvest, but not tool-free one-shots. Every selected runtime/model/args
|
|
1088
|
+
tuple is recorded on `session.started`; fallback starts cite the preceding target and
|
|
1089
|
+
verbatim error, while exhausted `phase.failed`/`harvest.failed` facts carry
|
|
1090
|
+
the complete ordered attempt list.
|
|
1091
|
+
- **Transcripts come back through the interface**, not scraped from disk, so
|
|
1092
|
+
every adapter must produce one whenever its owning runner reaches the session
|
|
1093
|
+
end boundary, including turns rejected by a provider after a handle exists.
|
|
1094
|
+
A runner process that dies before deposition is the explicit exception: a
|
|
1095
|
+
takeover records transcriptless reclamation rather than fabricating corpus
|
|
1096
|
+
bytes it cannot recover.
|
|
1097
|
+
- **Session streams (the live view).** While a session runs, its runner
|
|
1098
|
+
translates the harness's native output into AI SDK UI Message Stream parts
|
|
1099
|
+
(`ai-ui-message-stream/v1`) and appends them to a session stream (§7.6) as
|
|
1100
|
+
they happen — the live counterpart of the transcript guarantee above. The
|
|
1101
|
+
stream sink is a builtin-runner responsibility expressed as an optional
|
|
1102
|
+
`openSessionStream` capability on the runtime registration (beside the
|
|
1103
|
+
optional one-shot capability; the frozen `AgentRunner` port is untouched):
|
|
1104
|
+
the build-runner creates a store-backed sink per session bracket, opens the
|
|
1105
|
+
stream before the session's first turn with label `session:<sessionId>`
|
|
1106
|
+
keyed to the Autobuild session id (never a harness-native session, thread,
|
|
1107
|
+
or runtime id), appends `session.started` carrying the returned stream id,
|
|
1108
|
+
and closes the stream at the session end boundary — before the
|
|
1109
|
+
`session.ended` event lands — with outcome `completed` when the final turn
|
|
1110
|
+
completed, `aborted` when it failed or was cancelled. A session that ends on
|
|
1111
|
+
a substituted runtime keeps the stream opened for its own bracket. The
|
|
1112
|
+
harvest runner does the same for its synthesize and review session brackets
|
|
1113
|
+
with the identical protocol and part vocabulary: streams are scoped
|
|
1114
|
+
`{ kind: 'repo', repo }` instead of build-scoped, `harvest.session.started`
|
|
1115
|
+
carries the returned stream id, `phase` is spelled `harvest:<step>`, each
|
|
1116
|
+
revise round is its own bracket and its own stream, and a closed stream
|
|
1117
|
+
finalizes onto the repository scope. Harvest journal events
|
|
1118
|
+
(`harvest.step.*`, `harvest.proposals.submitted`, `harvest.review.verdict`,
|
|
1119
|
+
and their kin) are deliberately not translated into parts — they are typed
|
|
1120
|
+
journal facts, observable via `ab watch --repository`, not harness output.
|
|
1121
|
+
|
|
1122
|
+
The part vocabulary: the stream's first part is always a `data-ab-session`
|
|
1123
|
+
part naming the session id, role, runner, model, phase, and round; each
|
|
1124
|
+
turn's prompt (the skill invocation on turn 1, the continuation message on
|
|
1125
|
+
later rounds) is a `data-ab-prompt` part before that turn's output; while a
|
|
1126
|
+
turn runs the runner emits `start-step`/`finish-step` around each harness
|
|
1127
|
+
step, assistant text as `text-start`/`text-delta`/`text-end` at the finest
|
|
1128
|
+
granularity the harness exposes (partial-message deltas for Claude Code and
|
|
1129
|
+
Pi, whole messages where the harness only reports completed messages),
|
|
1130
|
+
reasoning parts when the harness exposes reasoning,
|
|
1131
|
+
`tool-input-available` and `tool-output-available` per tool call, and
|
|
1132
|
+
`finish` on completed turns. A turn that ends in a provider or runtime
|
|
1133
|
+
failure emits an `error` part before the stream closes; a turn cancelled by
|
|
1134
|
+
the caller emits an `abort` part. Text, reasoning, prompt, and tool
|
|
1135
|
+
payloads serialized above 65,536 UTF-8 bytes are truncated on a UTF-8
|
|
1136
|
+
boundary and followed by a `data-ab-truncation` part naming the omitted
|
|
1137
|
+
byte count. Parts reach the store within one second of the harness emitting
|
|
1138
|
+
them under normal conditions: the runner batches appends on a short cadence
|
|
1139
|
+
rather than one request per delta.
|
|
1140
|
+
|
|
1141
|
+
Streams are presentation, never routing: no kernel, engine, reducer, or
|
|
1142
|
+
dispatcher decision reads stream content or presence, and the phase outcome
|
|
1143
|
+
still travels only the typed CLI. A failing stream path never fails a turn:
|
|
1144
|
+
an append error retries on the next batch, a writer that keeps failing
|
|
1145
|
+
stops streaming the session, closes the stream `aborted` when it can,
|
|
1146
|
+
reports once on the build runner's diagnostics, and the phase proceeds
|
|
1147
|
+
exactly as it would without streams; the transcript artifact is deposited
|
|
1148
|
+
regardless. A plugin runtime that declares no capability yields sessions
|
|
1149
|
+
with no stream, and every consumer treats those sessions exactly as before;
|
|
1150
|
+
tool-free one-shot completions produce no stream. A closed session's
|
|
1151
|
+
finalized document is the `stream:<id>` artifact (§7.6).
|
|
1152
|
+
- Adapters without native session resumption implement `continue` as
|
|
1153
|
+
start-with-rehydrate-from-store — which must exist anyway per §7.4.
|
|
1154
|
+
|
|
1155
|
+
## 10. The review loop (`converge`)
|
|
1156
|
+
|
|
1157
|
+
One generic primitive, used for the plan loop, code loop, and harvest's
|
|
1158
|
+
synthesize/review loop:
|
|
1159
|
+
|
|
1160
|
+
```ts
|
|
1161
|
+
converge<A>(
|
|
1162
|
+
produce: (feedback: Feedback | null) => Promise<A>, // planner | implementer
|
|
1163
|
+
review: (artifact: A) => Promise<Verdict>, // plan-reviewer | code-reviewer
|
|
1164
|
+
policy: { maxRounds, escalateOn, reviewerRunner, … }
|
|
1165
|
+
// Feedback = findings from a verdict, or a verify failure report (§5)
|
|
1166
|
+
): Approved<A> | Escalated
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
- **Structured verdicts** (`approve | revise(findings) | escalate(reason)`),
|
|
1170
|
+
deposited via `ab verdict`. Findings are structured (file refs,
|
|
1171
|
+
description, severity) so round N+1's producer prompt is assembled
|
|
1172
|
+
deterministically, not "here's what the reviewer said."
|
|
1173
|
+
- **Memory model:** producer *continues* its session across rounds; reviewer
|
|
1174
|
+
gets a *fresh* session each round (a fresh skeptic catches more). Both are
|
|
1175
|
+
policy knobs.
|
|
1176
|
+
- **Anti-stall rule:** new findings round-over-round is normal; the *same*
|
|
1177
|
+
finding surviving N rounds is a disagreement between two agents and
|
|
1178
|
+
auto-escalates rather than burning rounds (mechanics in §15.4: the
|
|
1179
|
+
reviewer marks persistence — judgment; the kernel applies the threshold —
|
|
1180
|
+
determinism).
|
|
1181
|
+
- **Round ceiling:** `policy.maxReviewRounds` is the default absolute cap for
|
|
1182
|
+
each loop. Once its `review-round-limit` escalation is open, a human may
|
|
1183
|
+
answer it with an optional positive `reviewRoundCeiling`. That value replaces
|
|
1184
|
+
the configured cap for only the escalation's plan or code loop and current
|
|
1185
|
+
spec revision. It is absolute, durable, and cleared by `spec.revised`; the
|
|
1186
|
+
stall check retains precedence.
|
|
1187
|
+
|
|
1188
|
+
## 11. Escalation
|
|
1189
|
+
|
|
1190
|
+
An escalation is an **event**, answerable from any UI — not a file to go
|
|
1191
|
+
find. When a build parks, the operator surface shows it among the blocked
|
|
1192
|
+
builds with the question and an answer channel (an `escalation.answered`
|
|
1193
|
+
event — commands are events, §15.2.7). The durable record is in the store
|
|
1194
|
+
like everything else. An answer carries either bare `retry` or free-text
|
|
1195
|
+
`guidance` that feeds the parked phase's next run; answering is an attempt,
|
|
1196
|
+
not a forced success — an unresolved condition may escalate again. For an
|
|
1197
|
+
agent verifier's own `ab escalate`, that next run is the same `verify:<step>`:
|
|
1198
|
+
`verify.started.feedback` cites the answer and `ab context` materializes it as
|
|
1199
|
+
`.ab/guidance.json`. The citation remains the durable carrier across any crash
|
|
1200
|
+
before launch; only a later `session.started` with the same phase and attempt
|
|
1201
|
+
consumes it. Producer guidance uses the same phase-and-round launch boundary. A
|
|
1202
|
+
bare retry reruns the step without guidance. The intentionally different policy
|
|
1203
|
+
escalation after an exhausted failed verify report feeds `implement`, where its
|
|
1204
|
+
guidance outranks
|
|
1205
|
+
the pending report.
|
|
1206
|
+
|
|
1207
|
+
Only an operator answering an open `review-round-limit` policy escalation may
|
|
1208
|
+
set `escalation.answered.reviewRoundCeiling`. The same event both clears the
|
|
1209
|
+
human gate and replaces the cap; no standalone or pre-emptive form exists. A
|
|
1210
|
+
ceiling cannot accompany a spec revision because `spec.revised` resets both
|
|
1211
|
+
loops to the configured default. The value is attached only to the matching
|
|
1212
|
+
review-limit answer, so unrelated blockers answered in the same command do not
|
|
1213
|
+
acquire it and agents cannot widen their own build's loop.
|
|
1214
|
+
|
|
1215
|
+
Every open escalation remains a human judgment gate until an operator answers
|
|
1216
|
+
it. This includes all policy and setup causes. Starting either `ab dispatch` or
|
|
1217
|
+
`ab dispatch --once` never appends `escalation.answered` and never re-arms a
|
|
1218
|
+
budget. Startup still recovers every actionable current build and delivers
|
|
1219
|
+
pending operator commands, whose decisions take precedence over a blocked wait;
|
|
1220
|
+
an otherwise blocked build is reported as parked and left unchanged.
|
|
1221
|
+
|
|
1222
|
+
Crash-gap and exhaustion deduplication is exact to the escalation's source/class,
|
|
1223
|
+
target, and any durable scope that distinguishes policy conditions. Every new
|
|
1224
|
+
policy-authored `escalation.raised` names a closed `policyCause`; `round` scopes
|
|
1225
|
+
the occurrence but does not identify the policy class. A triggering event is
|
|
1226
|
+
considered acknowledged only by a later raise matching those dimensions;
|
|
1227
|
+
another cause, target, or required scope cannot suppress it. The triggering
|
|
1228
|
+
event's sequence is the boundary, so newer qualifying failure, verdict, or
|
|
1229
|
+
conflict evidence re-arms that exact condition after an answer.
|
|
1230
|
+
|
|
1231
|
+
Occurrence scope and phase-runner failure-budget reset scope are separate.
|
|
1232
|
+
Answering `phase-attempt-limit` or `non-retryable-phase-failure` re-arms only
|
|
1233
|
+
the matching phase and `round`; a missing round cannot broaden either cause.
|
|
1234
|
+
`review-round-limit` retains the same matching-round effect.
|
|
1235
|
+
`verify-failure-limit` is the explicit phase-level case and re-arms every
|
|
1236
|
+
phase-runner failure round for its verify target. `reconcile-no-progress` and
|
|
1237
|
+
`setup-failure-limit` have no phase-runner failure-budget effect (setup's own
|
|
1238
|
+
answer/retry accounting remains separate). Agent and stall raises, and
|
|
1239
|
+
cause-less historical policy raises, retain the legacy shape rule: a matching
|
|
1240
|
+
round resets that round and an absent round resets all rounds for the target.
|
|
1241
|
+
The closed `policyCause` union has an exhaustive reset-scope mapping, so adding
|
|
1242
|
+
a policy cause requires choosing matching-round, all-round, or no-reset
|
|
1243
|
+
semantics rather than acquiring behavior by omitting `round`.
|
|
1244
|
+
|
|
1245
|
+
Reconcile has two distinct policy scopes. Runner retry exhaustion and
|
|
1246
|
+
non-retryable failures at progress-check or base-refresh boundaries are scoped
|
|
1247
|
+
to a concrete reconcile `round`. Unchanged-base no-progress exhaustion is
|
|
1248
|
+
scoped to the current conflict, omits `round`, and has `policyCause =
|
|
1249
|
+
"reconcile-no-progress"`. Only a later open or answered raise with that cause
|
|
1250
|
+
acknowledges the no-progress guard; an explicitly different cause does not,
|
|
1251
|
+
even if it is also roundless. Cause-less historical roundless
|
|
1252
|
+
`policy`/`reconcile` raises retain their former no-progress meaning on replay,
|
|
1253
|
+
without migration. Routing uses these typed event fields and never parses the
|
|
1254
|
+
human-facing `question`.
|
|
1255
|
+
|
|
1256
|
+
## 12. The outer loop
|
|
1257
|
+
|
|
1258
|
+
```
|
|
1259
|
+
signals (telemetry, observations)
|
|
1260
|
+
→ ingest:* / harvest (signal source → dedup ledger → cluster/synthesize → propose)
|
|
1261
|
+
→ Triage (proposals)
|
|
1262
|
+
→ groom (the human gate — named in the ontology, no skill)
|
|
1263
|
+
→ Ready
|
|
1264
|
+
→ dispatch (claim → spec import/author → name → workspace → launch build-runner)
|
|
1265
|
+
→ build → PR → merge
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
Additional scheduled ingesters such as `ingest:sentry` remain an open design
|
|
1269
|
+
thread and have no shipped config surface. Observation harvest is
|
|
1270
|
+
pressure-driven: on the existing one-pass scan of repository build streams, the
|
|
1271
|
+
dispatcher measures both unclaimed observation count and repository drift. A run
|
|
1272
|
+
starts when count reaches `[policy].harvestThreshold` **or** when the number of
|
|
1273
|
+
same-repository `pr.merged` facts strictly after the oldest unclaimed
|
|
1274
|
+
observation reaches `[policy].harvestMaxDrift`, whichever happens first. Drift
|
|
1275
|
+
counts distinct other builds only: the build that recorded the oldest
|
|
1276
|
+
observation is excluded even if it later merges, and aborted or closed-unmerged
|
|
1277
|
+
builds contribute no `pr.merged` fact. The drift limit defaults to `3`; zero
|
|
1278
|
+
disables that condition and restores count-only gating. Empty accumulations
|
|
1279
|
+
never trigger. Either condition claims the whole current accumulation as one
|
|
1280
|
+
immutable snapshot, including newer observations, and `harvest.started` records
|
|
1281
|
+
`count`, `drift`, or `both` as durable trigger provenance. Occurrence identity
|
|
1282
|
+
is `{build slug, event seq}` — never payload id or a scalar high-water mark,
|
|
1283
|
+
because event sequences are per build. Harvest state lives in the repository
|
|
1284
|
+
journal, separate from build streams; the repository lease is the cross-process
|
|
1285
|
+
exclusivity gate, and harvest runs fire-and-forget so dispatch ticks stay
|
|
1286
|
+
responsive.
|
|
1287
|
+
|
|
1288
|
+
The fixed workflow is:
|
|
1289
|
+
|
|
1290
|
+
1. **scan (deterministic)** — subtract all claimed occurrences, reconcile
|
|
1291
|
+
prior proposal tickets through TicketSource lifecycle facts, expose each
|
|
1292
|
+
distinct observation-origin ticket's source match, existence, and resolution
|
|
1293
|
+
state, and atomically store the scan packet with the run's claim.
|
|
1294
|
+
2. **synthesize ⇄ review (judgment through `converge`)** — the continuing
|
|
1295
|
+
producer clusters same-problem records and authors typed
|
|
1296
|
+
create/join/suppress proposals. A create may carry source-local `blockedBy`
|
|
1297
|
+
ids when its evidence establishes a hard prerequisite other than the
|
|
1298
|
+
automatic observation-origin relationship; contextual references and
|
|
1299
|
+
nonbinding ordering do not become blockers. A fresh reviewer checks
|
|
1300
|
+
coverage, semantic dedup, spec quality, evidence, and both directions of
|
|
1301
|
+
that prerequisite rule. Only approval advances.
|
|
1302
|
+
3. **file (deterministic)** — refresh the selected TicketSource lifecycle for
|
|
1303
|
+
every declared blocker and same-source ticket that originated a create's
|
|
1304
|
+
clustered observations. Unknown agent-declared ids fail filing; missing,
|
|
1305
|
+
resolved, foreign-source, and absent origins contribute nothing. The created
|
|
1306
|
+
ticket receives the first-seen deduplicated union of declared blockers and
|
|
1307
|
+
still-existing unresolved origins. The filing fact and report preserve those
|
|
1308
|
+
two provenance sets separately. Creates are rendered to the spec standard
|
|
1309
|
+
and filed into `[tickets].proposalState`, Triage by default, with the reserved
|
|
1310
|
+
`autobuild:proposal` provenance label.
|
|
1311
|
+
Filing is crash-safe by construction: an idempotency ID is durably reserved
|
|
1312
|
+
*before* each external create, so a restart adopts the already-created
|
|
1313
|
+
ticket instead of duplicating it, and a partially filed approved set creates
|
|
1314
|
+
only its missing tickets. Ready projections expose that stable external
|
|
1315
|
+
creation key separately from the human-facing ticket id. Dispatch brackets
|
|
1316
|
+
each ready listing with repository-journal reads and withholds a ticket when
|
|
1317
|
+
its key matches an unfinished reservation, including a reservation first
|
|
1318
|
+
observed during the listing interval. The ticket remains counted as queued,
|
|
1319
|
+
consumes no capacity, and receives a distinct durable standing diagnostic.
|
|
1320
|
+
A later tick releases it after filing settles or the owning run completes,
|
|
1321
|
+
escalates, fails non-retryingly, or exhausts recovery; tickets without a
|
|
1322
|
+
matching reservation are unchanged. The label is informational: Autobuild never reads
|
|
1323
|
+
it as readiness policy and does not remove it during grooming.
|
|
1324
|
+
|
|
1325
|
+
Beyond the workflow, harvest is governed by a small set of invariants (their
|
|
1326
|
+
event-level mechanics live in the repository catalog and reducer tests):
|
|
1327
|
+
|
|
1328
|
+
- **Pause is a repository-wide durable gate**, not a run status — requested
|
|
1329
|
+
by a human, acknowledged by the kernel at a safe boundary, and never
|
|
1330
|
+
destructive to the open run or its claim.
|
|
1331
|
+
- **A failed run parks; settling parked runs outranks new scans.** The
|
|
1332
|
+
dispatcher settles the oldest outstanding recoverable run before starting
|
|
1333
|
+
any new scan, so later runs cannot shadow an older stop.
|
|
1334
|
+
- **Automatic recovery is bounded per run** (two reopens, no config surface),
|
|
1335
|
+
separate from within-step retry policy. Completed steps never re-run; an
|
|
1336
|
+
approved set goes straight to filing.
|
|
1337
|
+
- **Give-up never silently destroys the snapshot.** Exhaustion atomically
|
|
1338
|
+
commits the provable partial dispositions to the ledger, releases only
|
|
1339
|
+
genuinely pending work, and raises a durable human-attention barrier so
|
|
1340
|
+
released work cannot be immediately reclaimed into another hot loop.
|
|
1341
|
+
- **Human resume is repository-wide**: one acknowledgement reopens every
|
|
1342
|
+
ordinary parked run and clears every exhaustion barrier. It never
|
|
1343
|
+
resurrects a terminal run. Completed and escalated runs are irrevocable; a
|
|
1344
|
+
deliberate escalation consumes its snapshot and is never auto-recovered.
|
|
1345
|
+
- **The harvester only proposes.** It never claims, grooms, or dispatches a
|
|
1346
|
+
proposal, and it files every one into the same configured state rather than
|
|
1347
|
+
ranking them. Humans own Triage → Ready by default. A repository that points
|
|
1348
|
+
`[tickets].proposalState` at its ready state has waived that gate for itself:
|
|
1349
|
+
every harvested proposal enters the ordinary dispatch eligibility checks
|
|
1350
|
+
unread, on the strength of the synthesize ⇄ review loop and the spec gate.
|
|
1351
|
+
An unresolved harvested `blockedBy` relationship still prevents claim until
|
|
1352
|
+
the source reports completion or the relationship is deliberately removed.
|
|
1353
|
+
The waiver is a field of
|
|
1354
|
+
its own rather than a reuse of `triageState`, because bounces, aborts, and
|
|
1355
|
+
closed-unmerged PRs must still land where the next tick will not reclaim
|
|
1356
|
+
them — a bounce filed into Ready is claimed and bounced again forever.
|
|
1357
|
+
|
|
1358
|
+
## 13. Ticket source policy
|
|
1359
|
+
|
|
1360
|
+
The TicketSource **initiates and receives projections; it is never consulted
|
|
1361
|
+
mid-build and never used as artifact storage.** Dispatch reads the ticket
|
|
1362
|
+
(including the spec) as part of initiation — at claim time, or on a later tick
|
|
1363
|
+
while an interrupted dispatch still lacks its imported spec. After import, the
|
|
1364
|
+
build never reads the tracker again. Human-legibility projections (spec
|
|
1365
|
+
posted as a comment, final summary, status transitions) flow outward only.
|
|
1366
|
+
This keeps the abstraction honest: a file-based TicketSource with nowhere to
|
|
1367
|
+
put blobs must be fully workable.
|
|
1368
|
+
|
|
1369
|
+
**Claim lifecycle.** Claim acquires nonterminal work: it returns `true` and
|
|
1370
|
+
moves a claimable ticket to the source's claimed state. It returns `false` for
|
|
1371
|
+
an unknown ticket, an already-claimed or contested ticket, and a ticket that
|
|
1372
|
+
the source considers terminal. Refusing a terminal ticket performs no state
|
|
1373
|
+
transition and never implicitly reopens completed work. An explicit transition
|
|
1374
|
+
back to a nonterminal state makes a later claim possible again.
|
|
1375
|
+
|
|
1376
|
+
**Partial listings and source invariants.** A listing returns both valid
|
|
1377
|
+
tickets and diagnostics for individually malformed records, which are
|
|
1378
|
+
excluded but left byte-untouched — one broken ticket never blocks unrelated
|
|
1379
|
+
dispatch. Tracker-wide safety violations (duplicate ids across states,
|
|
1380
|
+
stateless records) remain fatal: continuing could permit double dispatch.
|
|
1381
|
+
|
|
1382
|
+
**Pre-build edits.** Update is partial and strict: it replaces only the named
|
|
1383
|
+
editable fields, and state is never an update field — transitions are a
|
|
1384
|
+
separate operation with their own validation. Labels are values at the port
|
|
1385
|
+
boundary: a label name need not already exist for either create or update.
|
|
1386
|
+
Adapters own reconciliation with provider-side label registries, including
|
|
1387
|
+
concurrent attempts to create the same name; callers never pre-register labels.
|
|
1388
|
+
|
|
1389
|
+
**Ticket dependencies.** A ticket may declare blockers within its source, at
|
|
1390
|
+
creation or later. The source owns representation (how a blocker is stored)
|
|
1391
|
+
and completion semantics (what "done" means); the dispatcher owns the
|
|
1392
|
+
decision — an unresolved blocker means the ticket is not claimed and creates
|
|
1393
|
+
no build. Dependencies are written during grooming or by an approved harvest
|
|
1394
|
+
create, then read at dispatch time. Harvest validates agent-declared source-local ids
|
|
1395
|
+
and automatically derives unresolved same-source observation origins through
|
|
1396
|
+
fresh configured-source lifecycle reads before creating the proposal ticket.
|
|
1397
|
+
The scan packet exposes the earlier informational lifecycle view to producer
|
|
1398
|
+
and reviewer, while the filing read is authoritative. Both paths remain
|
|
1399
|
+
initiation, so the never-consulted-mid-build rule is untouched. A
|
|
1400
|
+
dependency-blocked ticket stays
|
|
1401
|
+
queued source work rather than becoming a blocked build: the runtime `blocked`
|
|
1402
|
+
status is for builds awaiting a human.
|
|
1403
|
+
|
|
1404
|
+
**Crash-safe filing.** Creation supports a state override (harvest targets
|
|
1405
|
+
Triage explicitly) and an idempotency key that must adopt the same ticket on
|
|
1406
|
+
retry across process restarts. Ticket projections may carry this stable
|
|
1407
|
+
external creation key separately from their source-facing identifier; adapters
|
|
1408
|
+
that support idempotent creation preserve it through create, adoption, get, and
|
|
1409
|
+
ready listing. The reservation fact precedes the external side effect — that
|
|
1410
|
+
ordering, not provider behavior, is what makes filing crash-safe. Dispatch
|
|
1411
|
+
uses the key only to withhold unfinished Autobuild creates; its absence fails
|
|
1412
|
+
open for legacy/plugin tickets and introduces no new readiness gate.
|
|
1413
|
+
|
|
1414
|
+
## 14. Operator UI
|
|
1415
|
+
|
|
1416
|
+
The UI layer is defined by the seam, not any implementation: **subscribe to
|
|
1417
|
+
events, render, send commands** — commands being events appended to the
|
|
1418
|
+
applicable build or repository log (§15.2.7). The event vocabulary *is* the
|
|
1419
|
+
UI API; forge mutation remains kernel/dispatcher plumbing. Anything a UI
|
|
1420
|
+
displays is a reduction of the logs, and anything it does is an event — so
|
|
1421
|
+
every frontend (terminal today, web later) is the same adapter pattern
|
|
1422
|
+
against the same store, and a dead runner still receives commands on resume.
|
|
1423
|
+
|
|
1424
|
+
Interactive `ab dispatch` owns the terminal in a frontend process and supervises
|
|
1425
|
+
one private child containing the dispatcher, build runners, Harvest, config and
|
|
1426
|
+
plugin loading, and every ticket/forge/workspace/runtime adapter. The frontend
|
|
1427
|
+
has no handle on those adapters: it reads build and repository streams plus
|
|
1428
|
+
referenced artifacts, writes operator commands, and keeps only presentation
|
|
1429
|
+
state (selection, scroll, composer text, confirmation) in memory. The child
|
|
1430
|
+
publishes a run-correlated effective-config artifact before its first tick and
|
|
1431
|
+
records tick/queue diagnostics, reload outcomes, runner coordination, and
|
|
1432
|
+
normal/abnormal lifecycle as repository facts. Thus a rejected on-disk reload
|
|
1433
|
+
cannot change the displayed effective capacity, and a frontend restart can
|
|
1434
|
+
reconstruct every operational notice from the Store. Observation pressure is a
|
|
1435
|
+
separate display-only reduction: the interactive frontend scans the shared
|
|
1436
|
+
BuildStore directly, pairs the current unclaimed count with the effective
|
|
1437
|
+
config's `policy.harvestThreshold`, and retains the last successful count while
|
|
1438
|
+
showing a refresh diagnostic after a failed read. It appends no event and does
|
|
1439
|
+
not use `dispatcher.tick-completed` to transport the sample; the headless child
|
|
1440
|
+
therefore runs ticks without a dashboard, terminal, or frontend connection.
|
|
1441
|
+
Before the first successful sample the frontend renders only the diagnostic,
|
|
1442
|
+
never a fabricated zero. Non-interactive dispatch performs no frontend-only
|
|
1443
|
+
sampling. Repository polling advances from the last observed sequence rather
|
|
1444
|
+
than replaying the growing journal on every frame. SIGINT, SIGHUP, SIGQUIT, and
|
|
1445
|
+
SIGTERM restore terminal modes immediately and ask the child to stop; repeated
|
|
1446
|
+
signals remain graceful while a dispatcher tick is crossing the ticket-claim
|
|
1447
|
+
boundary. The frontend reaps the child before finishing, and for SIGHUP,
|
|
1448
|
+
SIGQUIT, or SIGTERM only then restores the default disposition by re-signalling
|
|
1449
|
+
itself. A child also stops when its terminal-owning parent dies. A timed-out
|
|
1450
|
+
child is terminated unless such a tick is open, in which case that tick first
|
|
1451
|
+
reaches a recoverable durable boundary. The run-stopped fact and frontend result
|
|
1452
|
+
distinguish a graceful drain, an actual forced termination requested by the
|
|
1453
|
+
operator, and an unsolicited abnormal exit, retaining available exit-code,
|
|
1454
|
+
signal, and error evidence. `--plain`, non-TTY,
|
|
1455
|
+
and `--once` kernel semantics remain the line-oriented/direct compatibility
|
|
1456
|
+
path; `--once` still performs one tick and drains its in-flight work.
|
|
1457
|
+
|
|
1458
|
+
Durable operator settings (intake, the repository-wide pause, the claim-time
|
|
1459
|
+
auto-merge default, the harvest gate) are repository-journal facts: they
|
|
1460
|
+
survive restarts, propagate between dispatchers by ordinary polling, and are
|
|
1461
|
+
never optimistically rendered — the UI shows acknowledged state. The
|
|
1462
|
+
repository-wide pause holds every queued build: while it is set, no dispatcher
|
|
1463
|
+
tick may attach a runner to a build that does not have one yet.
|
|
1464
|
+
|
|
1465
|
+
The **operator sandbox** is the third execution kind beside build and harvest
|
|
1466
|
+
executions: a persistent, credential-free, per-operator-per-repository
|
|
1467
|
+
environment behind registry tools (`sandbox.*`, AUT-340), provisioned like a
|
|
1468
|
+
build workspace and exposed to operator agents only through those tools. The
|
|
1469
|
+
typed tools are the only route to state from inside it — the environment
|
|
1470
|
+
receives no store, forge, ticket-provider, or model credential, and
|
|
1471
|
+
`[orchestrator].sandbox.environmentVariables` may never name one. Its lifecycle
|
|
1472
|
+
is repository-journal fact: provisioned/resumed/activity/stopped/released facts
|
|
1473
|
+
bracket one deterministic environment per operator × repository, the
|
|
1474
|
+
dispatcher tick's idle settlement stops an environment whose evidence is
|
|
1475
|
+
stale (settling an orphan exactly as harvest executions are settled), and
|
|
1476
|
+
`ab repository status` reports each operator's sandbox.
|
|
1477
|
+
|
|
1478
|
+
The operator's job across many concurrent builds: see status at a glance,
|
|
1479
|
+
act on a selected build, find blocked builds, answer escalations, and inspect
|
|
1480
|
+
any build's trail. Each dashboard build row spends its header on identity and
|
|
1481
|
+
lifecycle status, with the pipeline and its per-step elapsed values below it;
|
|
1482
|
+
controls and diagnostic lines expose actions, pending dispatch, abort cleanup,
|
|
1483
|
+
setup failures, and blockers. Dashboard rows derive from build events and
|
|
1484
|
+
effective configuration only, so heartbeat or lease renewal without an event
|
|
1485
|
+
does not alter them. Every nonterminal build, including `queued`, has a
|
|
1486
|
+
dashboard row; a pre-run row names its pending dispatch boundary or latest
|
|
1487
|
+
durable failure. A human may discard only such a queued build. Discard is
|
|
1488
|
+
dispatcher cleanup, returns the ticket to its
|
|
1489
|
+
configured Ready state, and completes with
|
|
1490
|
+
`discarded`; it is deliberately distinct from abort, which returns work to
|
|
1491
|
+
Triage for human judgment. Any selected nonterminal build can be aborted from
|
|
1492
|
+
the list or detail view with `a`; Enter confirms and Escape cancels, so the first
|
|
1493
|
+
keypress never writes destructive intent. CLI and dashboard both append the same
|
|
1494
|
+
`build.abort-requested` fact through the shared control service. The concrete
|
|
1495
|
+
presentation — layout, key bindings, colors — is owned by the dashboard
|
|
1496
|
+
implementation and its tests.
|
|
1497
|
+
|
|
1498
|
+
## 15. Event vocabulary
|
|
1499
|
+
|
|
1500
|
+
Drafted by walking one build's happy path end to end, then three unhappy
|
|
1501
|
+
paths (verify failure, review stall, sandbox death). Four decisions this
|
|
1502
|
+
exercise forced are marked **[D1]–[D4]**; all four are **confirmed**, with
|
|
1503
|
+
[D1] extended to cover merge standardization and conflict resolution (§15.7).
|
|
1504
|
+
|
|
1505
|
+
The complete vocabulary is frozen in code: build payloads in
|
|
1506
|
+
`packages/core/src/events/payloads.ts`, repository workflow and control payloads in
|
|
1507
|
+
`packages/core/src/events/repository.ts`. Every adapter validates before append. The
|
|
1508
|
+
sections below define the envelope, the conventions that govern every event,
|
|
1509
|
+
and the walkthroughs that motivated the design — not a field-by-field
|
|
1510
|
+
catalog.
|
|
1511
|
+
|
|
1512
|
+
### 15.1 Envelope
|
|
1513
|
+
|
|
1514
|
+
Every event shares:
|
|
1515
|
+
|
|
1516
|
+
```jsonc
|
|
1517
|
+
{
|
|
1518
|
+
"build": "auth-rate-limit", // build slug
|
|
1519
|
+
"seq": 42, // per-build, monotonic, assigned by the store on append
|
|
1520
|
+
"ts": "2026-07-15T14:03:22Z", // assigned by the store
|
|
1521
|
+
"actor": { "kind": "agent", "role": "code-review", "session": "s_9f2" },
|
|
1522
|
+
"type": "code-review.verdict",
|
|
1523
|
+
"payload": { /* per-type, frozen in packages/core/src/events/ */ }
|
|
1524
|
+
}
|
|
1525
|
+
```
|
|
1526
|
+
|
|
1527
|
+
`actor.kind ∈ kernel | agent | human | dispatcher | ingester`. Agents carry
|
|
1528
|
+
`role` and `session`; humans carry `user`. The store assigns `seq` and `ts`
|
|
1529
|
+
so producers can't fake ordering. Repository-journal events use the same
|
|
1530
|
+
shape with `repo` in place of `build` and their own per-repository sequence,
|
|
1531
|
+
validated by the separate repository catalog so build reducers cannot
|
|
1532
|
+
accidentally interpret repository state.
|
|
1533
|
+
|
|
1534
|
+
A human actor may carry an optional `via` marker for delegated writes —
|
|
1535
|
+
`{kind: "session", id}` (the operator's orchestrator session) or
|
|
1536
|
+
`{kind: "mcp", client}` (an MCP client) — so a team's audit trail can
|
|
1537
|
+
distinguish "the operator did this" from "the operator's orchestrator session
|
|
1538
|
+
did this" or "an MCP client did this" forever. `via` is valid only on human
|
|
1539
|
+
actors and only on build and repository events (session events are the
|
|
1540
|
+
delegate's own log); validation rejects it anywhere else. The hosted service
|
|
1541
|
+
stamps `via` from the authorizing token and rejects a write claiming a `via`
|
|
1542
|
+
the token does not carry; events without it replay unchanged. Over a
|
|
1543
|
+
transport with token authority (the hosted HTTP store) that rejection is an
|
|
1544
|
+
authority failure — 403 `auth` (`AuthError` on the remote client), applied
|
|
1545
|
+
after token verification and before catalog validation, so "not your
|
|
1546
|
+
delegate" (403) stays distinguishable from "malformed event" (422
|
|
1547
|
+
`validation`). A token without a `via` claim may not write delegated events
|
|
1548
|
+
at all: any human write claiming a `via`, valid or malformed, rejects the
|
|
1549
|
+
same way. Local adapters and the open (no-secret) remote server have no
|
|
1550
|
+
token authority; there the backing catalog is the only `via` gate and every
|
|
1551
|
+
rejection is `EventValidationError`.
|
|
1552
|
+
|
|
1553
|
+
### 15.2 Conventions
|
|
1554
|
+
|
|
1555
|
+
1. **Closed vocabularies live in type names; open ones live in payloads.**
|
|
1556
|
+
Phases are a closed set → `plan.completed`. Verify and finalize steps are
|
|
1557
|
+
config-defined (open) → `verify.completed {step: "e2e"}`.
|
|
1558
|
+
2. **Events carry facts, never derived state.** Build status is a reduction
|
|
1559
|
+
(§15.5); no event ever says "status is now X".
|
|
1560
|
+
3. **Blobs live in artifacts; events carry refs** `{kind, rev}`.
|
|
1561
|
+
4. **[D3] Code travels through the Forge, never the store.**
|
|
1562
|
+
`implement.completed` pushes the branch; events carry commit SHAs only.
|
|
1563
|
+
This is what makes cross-sandbox resume work (§15.6-C) and keeps the
|
|
1564
|
+
store lean.
|
|
1565
|
+
5. **Append-only; corrections are new events.**
|
|
1566
|
+
6. **Liveness is not history.** Heartbeats and runner leases are mutable
|
|
1567
|
+
columns on the `builds` table, never events — they would drown the log.
|
|
1568
|
+
7. **[D2] Operator commands are events in the same log.** Humans append
|
|
1569
|
+
`*-requested`/`*-cancelled` events (including queued-only discard), `escalation.answered`, and dispatcher
|
|
1570
|
+
setting facts; kernel or dispatcher plumbing acknowledges effects that
|
|
1571
|
+
require a boundary. The store is the *only* coordination surface — no side
|
|
1572
|
+
channel — and polling covers commands exactly the way it covers
|
|
1573
|
+
`subscribe`. A runner that is dead still receives pause/resume/abort
|
|
1574
|
+
commands and escalation answers on resume. While a turn is live, the runner
|
|
1575
|
+
subscribes from its session boundary and forwards an abort as caller-owned
|
|
1576
|
+
cancellation to the AgentRunner; it closes the session transcript without
|
|
1577
|
+
recording `phase.failed` and acknowledges abort. The supervising execution
|
|
1578
|
+
retains the lease until the build's complete process tree has been reaped.
|
|
1579
|
+
|
|
1580
|
+
### 15.3 Catalog
|
|
1581
|
+
|
|
1582
|
+
Authoritative in code (`packages/core/src/events/payloads.ts`,
|
|
1583
|
+
`packages/core/src/events/repository.ts`, and the session catalog in
|
|
1584
|
+
`packages/core/src/events/sessions.ts` — the third catalog, validated
|
|
1585
|
+
separately like the repository journal's). The build and repository families,
|
|
1586
|
+
with illustrative members:
|
|
1587
|
+
|
|
1588
|
+
| Family | Examples |
|
|
1589
|
+
|---|---|
|
|
1590
|
+
| Build lifecycle | `build.created`, `workspace.provisioned`, `workspace.released`, `execution.started`, `execution.ended`, `infrastructure.failed`, `infrastructure.cleanup-attempted`, `dispatch.comment-posted`, `dispatch.failed`, `runner.attached`, `runner.setup-failed`, `abort.remote-branch-deleted`, `abort.local-branch-deleted`, `abort.ticket-returned`, `build.completed` |
|
|
1591
|
+
| Operator commands [D2] | `build.pause-requested` → `build.paused`; `build.discard-requested`; `build.auto-merge-requested`; `escalation.answered` (optionally carrying a validated `reviewRoundCeiling`) |
|
|
1592
|
+
| Spec | `spec.imported`, `spec.authored`, `spec.revised` |
|
|
1593
|
+
| Sessions | `session.started`; `session.ended` with transcript ref and usage (ordinary completion — the analysis corpus), or `session.ended {outcome: reclaimed, reclaimedBy: {instance, resumedFromSeq}}` (explicit transcriptless takeover) |
|
|
1594
|
+
| Plan/code loops | `plan.started` … `plan-review.verdict`; `implement.started` … `code-review.verdict` |
|
|
1595
|
+
| Verify/finalize | `verify.started {step, attempt, feedback?}`, `verify.completed {step, outcome}`, `finalize.completed {pr}`, `finalize.step-completed {step, ok, headSha?}` |
|
|
1596
|
+
| PR attachments | `pr-attachment.designated`, `pr-attachment.hosted`, `pr-attachment.reclaimed`, `pr-attachment.reclaim-failed` |
|
|
1597
|
+
| Post-PR [D1] | `pr.merged`, `pr.conflicted`, `reconcile.progress-checked`, `reconcile.started`, `reconcile.completed` |
|
|
1598
|
+
| Cross-cutting | `observation.recorded`, `escalation.raised`, `phase.failed` |
|
|
1599
|
+
| Repository journal | dispatcher setting facts; run lifecycle, effective config, tick/queue diagnostics, reload and runner outcomes; the `harvest.*` workflow, recovery, and ledger facts |
|
|
1600
|
+
|
|
1601
|
+
Abort cleanup is a dispatcher-owned ordered saga. While an execution lease is
|
|
1602
|
+
live, the janitor defers the saga so it cannot race process-tree teardown. Once
|
|
1603
|
+
that supervisor releases the lease, or an abruptly orphaned lease expires, cleanup
|
|
1604
|
+
closes an open unmerged PR without overriding a racing merge, releases the workspace,
|
|
1605
|
+
deletes the exact published remote branch and exact local branch, unions the
|
|
1606
|
+
`autobuild:aborted` label into the current ticket labels, returns the ticket to
|
|
1607
|
+
configured Triage, and only then completes as `abandoned`. External effects are
|
|
1608
|
+
idempotent and the three `abort.*` facts checkpoint projections across crashes;
|
|
1609
|
+
missing Forge capabilities or outages leave the remainder due on the next tick.
|
|
1610
|
+
|
|
1611
|
+
One deliberate subtlety worth recording: `pr.conflicted.baseSha` is
|
|
1612
|
+
detection-time evidence, while `reconcile.progress-checked.baseSha` is the
|
|
1613
|
+
kernel's authoritative observation for deciding whether a completed attempt
|
|
1614
|
+
made progress and `reconcile.started.baseSha` is the separately refreshed merge
|
|
1615
|
+
target for the next attempt. Agent context uses only the started fact, so a
|
|
1616
|
+
reconcile never runs against a persisted or known-stale observation.
|
|
1617
|
+
|
|
1618
|
+
### 15.4 Finding schema and stall mechanics [D4]
|
|
1619
|
+
|
|
1620
|
+
```jsonc
|
|
1621
|
+
Finding {
|
|
1622
|
+
"id": "f_3a91", // kernel-assigned at deposit, stable for the build
|
|
1623
|
+
"severity": "blocking" | "important" | "minor",
|
|
1624
|
+
"file": "packages/core/src/auth.ts", // optional
|
|
1625
|
+
"lines": [40, 62], // optional
|
|
1626
|
+
"summary": "…",
|
|
1627
|
+
"detail": "…", // optional
|
|
1628
|
+
"persists": ["f_1c22"] // reviewer-marked: earlier findings this one continues
|
|
1629
|
+
}
|
|
1630
|
+
```
|
|
1631
|
+
|
|
1632
|
+
Stall detection splits along the constitution's line: deciding whether a new
|
|
1633
|
+
finding is *the same disagreement* as an earlier one is fuzzy → **judgment**
|
|
1634
|
+
→ the reviewer (fresh each round) receives the prior rounds' findings and
|
|
1635
|
+
marks `persists`. Applying the threshold is mechanical → **determinism** →
|
|
1636
|
+
the kernel raises `escalation.raised {source: "stall"}` when any persistence
|
|
1637
|
+
chain survives `policy.stallRounds` rounds.
|
|
1638
|
+
|
|
1639
|
+
### 15.5 Derived state (the reducer)
|
|
1640
|
+
|
|
1641
|
+
`status ∈ queued | running | paused | blocked | done | aborted`, plus
|
|
1642
|
+
`{phase, round, openEscalations[], reviewRoundCeilings?, pr?, autoMerge,
|
|
1643
|
+
lastEvent}`. `blocked` ≡ an `escalation.raised` without a matching
|
|
1644
|
+
`escalation.answered`, matched by id. `reviewRoundCeilings` projects valid
|
|
1645
|
+
review-limit answers independently for the plan and code loops after the latest
|
|
1646
|
+
`spec.revised`; status and dashboard surfaces expose any projected values.
|
|
1647
|
+
`paused` ≡ a `build.paused` without a later `build.resumed`, and takes
|
|
1648
|
+
reducer precedence over blocked. Auto-merge state tracks the latest human
|
|
1649
|
+
*desired* value separately from the latest *applied* fact, settled only when
|
|
1650
|
+
both match — a stale acknowledgement can never erase newer intent.
|
|
1651
|
+
|
|
1652
|
+
Queued state additionally retains ordered `dispatch.failed` diagnostics and an
|
|
1653
|
+
outstanding `build.discard-requested` fact. `build.completed {outcome:
|
|
1654
|
+
"discarded"}` settles that intent without changing the status vocabulary.
|
|
1655
|
+
Runner state retains ordered `runner.setup-failed {command,attempt,exitStatus,
|
|
1656
|
+
output}` facts and projects the latest one until a later `runner.attached`
|
|
1657
|
+
proves successful setup recovery. A legacy `session.started` with no ending
|
|
1658
|
+
continues to project as open. On takeover, the recovering runner explicitly
|
|
1659
|
+
appends a reclaimed `session.ended` for every session open at its captured
|
|
1660
|
+
resume boundary before it appends `runner.attached`; only a subsequently
|
|
1661
|
+
started rerun session can then be open.
|
|
1662
|
+
|
|
1663
|
+
Every projection — operator UI, CLI status, dispatcher decisions — is a
|
|
1664
|
+
reduction of the logs. `ab builds` and `ab build status` additionally present
|
|
1665
|
+
the reduced last event as durable progress beside mutable heartbeat and lease
|
|
1666
|
+
health. On those diagnostic CLI surfaces, a nonterminal build with a live lease
|
|
1667
|
+
whose heartbeat is at least one hour newer than its last event is
|
|
1668
|
+
presentation-marked `diverged`; terminal, no-lease, and expired-lease builds
|
|
1669
|
+
are never marked. Progress age and that marker never feed a reducer or engine
|
|
1670
|
+
decision. Caches serving those liveness-aware consumers may key a reduction by
|
|
1671
|
+
last event sequence, but record-only heartbeat renewal must still refresh their
|
|
1672
|
+
presentation inputs. No decision ever consults a snapshot in place of the
|
|
1673
|
+
append-only log. Separate reducers
|
|
1674
|
+
derive dispatcher settings and harvest state from the repository journal; each
|
|
1675
|
+
ignores the other's facts.
|
|
1676
|
+
|
|
1677
|
+
### 15.6 Walkthroughs
|
|
1678
|
+
|
|
1679
|
+
**Happy path** (elided: `session.started/ended` brackets around every agent
|
|
1680
|
+
run):
|
|
1681
|
+
|
|
1682
|
+
```
|
|
1683
|
+
build.created → workspace.provisioned{base:{source:remote,sha}} → spec.imported → dispatch.comment-posted → runner.attached
|
|
1684
|
+
plan.started{r1} → plan.completed{plan@1, verifySteps}
|
|
1685
|
+
plan-review.started{r1} → plan-review.verdict{approve}
|
|
1686
|
+
implement.started{r1} → implement.completed{commits, notes@1}
|
|
1687
|
+
code-review.started{r1} → code-review.verdict{approve}
|
|
1688
|
+
verify.started{types} → verify.completed{outcome:pass} → …unit
|
|
1689
|
+
verify.started{e2e} → pr-attachment.designated{artifact,filename,mediaType} → verify.completed{outcome:pass}
|
|
1690
|
+
finalize.started → pr-attachment.hosted{designationSeq,asset} → finalize.completed{pr} → finalize.step-completed{release-notes}
|
|
1691
|
+
(later, janitor:) pr.merged → workspace.released → build.completed{merged} → pr-attachment.reclaimed{hostedSeq}
|
|
1692
|
+
```
|
|
1693
|
+
|
|
1694
|
+
**Interrupted dispatch:** if the log stops after `build.created`, a later
|
|
1695
|
+
ordinary dispatcher tick provisions the workspace, atomically imports the spec,
|
|
1696
|
+
posts the ticket notice once, and launches the runner. Claim-time auto-merge
|
|
1697
|
+
attribution retained by `build.created` is materialized as the ordinary
|
|
1698
|
+
human-authored request before launch. A failed boundary appends `dispatch.failed
|
|
1699
|
+
{stage,attempt,error}` and stays queued for another tick. A discard request is
|
|
1700
|
+
honored only while the build remains queued; one that races with runner
|
|
1701
|
+
attachment is inert and cannot tear down live work. A human discard waits out
|
|
1702
|
+
any unexpired execution lease, then releases a partial workspace and stale
|
|
1703
|
+
lease, returns the ticket to Ready, and appends `build.completed {outcome:
|
|
1704
|
+
discarded}` last. No runner is required.
|
|
1705
|
+
|
|
1706
|
+
**A — verify failure:** `verify.completed {step: e2e, outcome: fail,
|
|
1707
|
+
report}` → kernel routes back into the code loop: `implement.started
|
|
1708
|
+
{round: 2, feedback: {verify: {step, report}}}` → fix → `code-review` round
|
|
1709
|
+
2 → approve → verify re-runs **from the first step** (implement changed the
|
|
1710
|
+
code; cheap checks first), `attempt: 2`. `policy.maxVerifyAttempts`
|
|
1711
|
+
exhausted → `escalation.raised {source: "policy"}`. Guidance answering that
|
|
1712
|
+
policy escalation routes to `implement` and outranks the pending report. By
|
|
1713
|
+
contrast, when the agent verifier itself uses `ab escalate`, guidance answering
|
|
1714
|
+
it reruns that same step with `verify.started {feedback: {guidance}}`; `ab context`
|
|
1715
|
+
writes `.ab/guidance.json`. The start citation remains pending through repeated
|
|
1716
|
+
pre-launch recovery. A later `session.started` for that same verifier and
|
|
1717
|
+
attempt consumes the answer once, so a later failure routes its report to
|
|
1718
|
+
`implement` without stale guidance. Producer starts use the same exact-phase,
|
|
1719
|
+
round-matched rule. A bare retry reruns the verifier with no feedback.
|
|
1720
|
+
|
|
1721
|
+
**B — review stall:** round 1 `code-review.verdict {revise, [f1]}` → round 2
|
|
1722
|
+
verdict's finding marks `persists: [f1]` → round 3 again → kernel:
|
|
1723
|
+
`escalation.raised {source: "stall", refs: [chain]}`; status → `blocked`.
|
|
1724
|
+
`escalation.answered` uses the resolution vocabulary `guidance`,
|
|
1725
|
+
`dismiss-finding`, `revise-spec`, `abort`, or `retry`. Operators produce
|
|
1726
|
+
`dismiss-finding` and `revise-spec` through `ab answer`; a `revise-spec` answer
|
|
1727
|
+
names the exact replacement artifact it authorizes. `guidance` feeds the answer
|
|
1728
|
+
into the next producer round as authoritative feedback; `dismiss-finding`
|
|
1729
|
+
marks the chain human-resolved and the next reviewer round is told so. Engine-routed
|
|
1730
|
+
guidance is latest-only per destination (`plan`, `code`, or one exact agent
|
|
1731
|
+
verifier): a newer answer durably supersedes every older answer for that
|
|
1732
|
+
destination before delivery. A guidance-bearing producer or verifier start is
|
|
1733
|
+
the durable carrier, and only its later matching `session.started` launch
|
|
1734
|
+
consumes the winner; until then the winner remains eligible across recovery.
|
|
1735
|
+
After that delivery, no shadowed answer can surface on a later round, while an
|
|
1736
|
+
answer appended after delivery is a new eligible winner. Destinations remain
|
|
1737
|
+
independent, and the kernel derives both the winner and its delivery from the
|
|
1738
|
+
event history so replay and restart preserve the result.
|
|
1739
|
+
|
|
1740
|
+
**C — setup failure:** the first attach retains the normal `runner.attached`
|
|
1741
|
+
fact, then a failed setup appends `runner.setup-failed
|
|
1742
|
+
{command,attempt,exitStatus,output}` and starts no phase or session. A retry runs
|
|
1743
|
+
setup under the claimed lease before announcing another attachment. Another
|
|
1744
|
+
failure appends only a new failure fact; success appends `runner.attached
|
|
1745
|
+
{resumedFromSeq}` and clears the current error projection. After
|
|
1746
|
+
`policy.maxSetupAttempts`, the kernel raises one policy escalation targeted at
|
|
1747
|
+
`setup`; lease sweeps and fresh dispatcher startup leave it parked until a human
|
|
1748
|
+
answer re-arms the setup budget. Its exhaustion guard considers only setup-targeted
|
|
1749
|
+
policy raises, independently of phase-policy exhaustion. After claiming the lease,
|
|
1750
|
+
however, a runner honors an engine-selected pause or abort acknowledgement before
|
|
1751
|
+
setup-failure gating, even while that escalation is open. This control-only path
|
|
1752
|
+
executes no setup, appends no `runner.attached`, and starts no phase or session.
|
|
1753
|
+
Resume and all decisions that use the workspace still require successful setup.
|
|
1754
|
+
The setup target belongs only to escalation metadata and is not a pipeline `Phase`.
|
|
1755
|
+
|
|
1756
|
+
**D — sandbox death:** log ends at `session.started {session: old}` after
|
|
1757
|
+
`implement.started {round: 2}`; heartbeat goes stale → dispatcher expires the
|
|
1758
|
+
lease, records the old execution identity, stops/deletes its exact deterministic
|
|
1759
|
+
sandbox name, and confirms absence before recording `workspace.released` → it
|
|
1760
|
+
provisions the next generation from the published build head (or the original
|
|
1761
|
+
recorded branch-cut SHA when no checkpoint exists) → `workspace.provisioned
|
|
1762
|
+
{base: {source: existing, sha}}` → the workspace execution capability starts a
|
|
1763
|
+
fresh process and records the replacement sandbox/session identity. The
|
|
1764
|
+
supervising kernel first claims the lease for that execution
|
|
1765
|
+
instance; the process reads its workspace location from the durable event,
|
|
1766
|
+
renews the same-holder lease, captures the current resume boundary, and appends
|
|
1767
|
+
`session.ended {session: old, outcome: reclaimed, reclaimedBy: {instance,
|
|
1768
|
+
resumedFromSeq}}` before `runner.attached {resumedFromSeq}`. The reducer now
|
|
1769
|
+
shows no pre-boundary session as open and says implement r2
|
|
1770
|
+
started-not-completed, so the process re-runs the phase from its start and opens
|
|
1771
|
+
one fresh session. For a `publication.requested` fact issued before the old
|
|
1772
|
+
`workspace.released`, the supervisor first compares its exact SHA with the
|
|
1773
|
+
durable remote branch: a match repairs the missing completion fact before the
|
|
1774
|
+
replacement starts, covering a lost post-push acknowledgement; otherwise the
|
|
1775
|
+
commit existed only in the deleted sandbox, the request is abandoned, and the
|
|
1776
|
+
phase rerun must issue a new request. A crash while closing several sessions
|
|
1777
|
+
lands no attachment;
|
|
1778
|
+
the next recovery closes only those still open, so repeated takeover converges.
|
|
1779
|
+
Historical orphan-only logs remain valid and require no repair.
|
|
1780
|
+
|
|
1781
|
+
The re-run retains another `implement.started {round: 2}` as evidence of the
|
|
1782
|
+
execution attempt. Duplicate starts for one logical round or attempt are not
|
|
1783
|
+
policy units: review budget follows verdict rounds, phase retry budget follows
|
|
1784
|
+
`phase.failed`, verify budget follows failed completions while a crashed step
|
|
1785
|
+
reuses its attempt, and reconcile no-progress budget follows distinct completed
|
|
1786
|
+
attempts. Recovery alone therefore consumes none of those budgets. The provider
|
|
1787
|
+
restores the already-created branch at round 1's pushed head [D3]; the Git
|
|
1788
|
+
adapter never re-cuts it from a newer base (§7.4). `ab context` rehydrates
|
|
1789
|
+
scratch from the store into a fresh session. Uncommitted round-2 work is lost by
|
|
1790
|
+
design (§7.3 — phase boundaries are the resume points).
|
|
1791
|
+
|
|
1792
|
+
Two liveness rules complete the picture. Within one dispatcher process,
|
|
1793
|
+
build-runner launches are single-flighted by slug through supervised process
|
|
1794
|
+
liveness — execution completion means the complete environment has been reaped,
|
|
1795
|
+
but remains only a liveness signal, never a pipeline outcome. Build progress,
|
|
1796
|
+
config, diagnostics, and outcomes cross the process boundary only through
|
|
1797
|
+
build-owned Store state. The kernel claims each execution's durable lease before
|
|
1798
|
+
launch and releases that exact holder only after completion; the child renews it.
|
|
1799
|
+
The lease remains the cross-process recovery and workspace-teardown gate (the
|
|
1800
|
+
in-memory guard is deliberately not durable: a dead process's memory disappears
|
|
1801
|
+
with it). Ordinary dispatcher shutdown gracefully signals each local process
|
|
1802
|
+
group, escalates the whole group after the bounded stop delay, and reaps it.
|
|
1803
|
+
Before every build-child terminal path exits — natural success or failure,
|
|
1804
|
+
SIGINT/SIGTERM, or immutable-parent watchdog death — it launches one detached
|
|
1805
|
+
group reaper while the leader still pins the group's identity. That owner
|
|
1806
|
+
applies the same bounded TERM-then-KILL policy and survives abrupt dispatcher
|
|
1807
|
+
death. The live local supervisor independently awaits full-group teardown before
|
|
1808
|
+
publishing execution completion and releasing the lease; lease expiry remains
|
|
1809
|
+
the durable recovery fallback when the dispatcher dies. A new `ab dispatch`
|
|
1810
|
+
process attempts
|
|
1811
|
+
every actionable build on its first tick rather than waiting for the sweep;
|
|
1812
|
+
lease claiming stays the exclusivity gate, so a genuinely live old runner
|
|
1813
|
+
wins harmlessly.
|
|
1814
|
+
|
|
1815
|
+
### 15.7 Post-PR lifecycle [D1 — confirmed]
|
|
1816
|
+
|
|
1817
|
+
Walking the happy path exposed a gap in the grammar: `finalize` creates the
|
|
1818
|
+
PR, but *something* must watch it to merge/close, release the workspace, and
|
|
1819
|
+
emit `build.completed`. v2 makes it a deterministic **janitor duty of the
|
|
1820
|
+
dispatcher** (which already polls on cron): it checks open PRs for its
|
|
1821
|
+
builds, emits `pr.merged`/`pr.closed`/`pr.conflicted`, releases workspaces,
|
|
1822
|
+
and completes builds. Destructive discard, abort, and terminal-PR cleanup defer
|
|
1823
|
+
while an unexpired execution lease exists, so no workspace is released under a
|
|
1824
|
+
live process tree. After any terminal outcome it also reclaims every
|
|
1825
|
+
pending hosted PR-attachment copy. Reclamation success and failure are durable
|
|
1826
|
+
correlated facts; a failed delete remains pending and retries on later ticks,
|
|
1827
|
+
including after the build is already done. A merged-PR fixup request is a *new
|
|
1828
|
+
ticket*, never a reopened build.
|
|
1829
|
+
|
|
1830
|
+
**Merge standard: one rule per direction, never rebase.**
|
|
1831
|
+
|
|
1832
|
+
- **PR → main: squash merge.** Main stays linear, one commit per build —
|
|
1833
|
+
which keeps reverts (one commit → one new ticket), release notes, and
|
|
1834
|
+
history archaeology clean.
|
|
1835
|
+
- **main → feature branch: merge commit.** A stale branch is refreshed by
|
|
1836
|
+
merging base *into* it, resolving conflicts once against current main.
|
|
1837
|
+
- **Rebase is banned**, for two reasons. Operationally: at this system's
|
|
1838
|
+
merge velocity, a rebase re-resolves conflicts commit-by-commit against a
|
|
1839
|
+
target that keeps moving — agents can be stuck in
|
|
1840
|
+
rebase-conflict-after-rebase-conflict nearly indefinitely (observed in
|
|
1841
|
+
practice). Structurally: rebase rewrites the branch and severs the SHAs
|
|
1842
|
+
recorded in `implement.completed` events [D3]. Squash-at-merge is safe on
|
|
1843
|
+
both counts.
|
|
1844
|
+
|
|
1845
|
+
**Merge gates are never bypassed.** The operator's auto-merge command is
|
|
1846
|
+
durable consent to merge, not a bypass. Whenever the base branch has any real
|
|
1847
|
+
merge-blocking gate, consent is applied as GitHub-native auto-merge, so the
|
|
1848
|
+
forge's own checks decide when the PR lands. Only when the forge
|
|
1849
|
+
authoritatively reports no gate may the janitor perform a normal,
|
|
1850
|
+
head-guarded squash itself — never admin, force, or rebase — and only after
|
|
1851
|
+
all verification and finalize work is complete. GitHub's exact documented
|
|
1852
|
+
account-plan refusal for the branch-rulesets endpoint proves that no ruleset
|
|
1853
|
+
can exist only when the independent classic-protection probe also
|
|
1854
|
+
successfully reports no protection. Every near miss, generic authorization
|
|
1855
|
+
failure, malformed or unknown response, and tooling failure remains unknown
|
|
1856
|
+
and fails closed. Gate presence is decided by the mechanism actually in
|
|
1857
|
+
effect: a ruleset probe that proves merge-blocking rules settles presence even
|
|
1858
|
+
when the classic protection response is incomplete, while an incomplete classic
|
|
1859
|
+
response with no proven ruleset gate still fails closed with a reason naming
|
|
1860
|
+
the missing subsection.
|
|
1861
|
+
|
|
1862
|
+
Inability to prove or apply auto-merge is nonfatal pipeline plumbing, not a PR
|
|
1863
|
+
creation failure: finalize still records the open PR and completes, and consent
|
|
1864
|
+
stays pending for later janitor polls, which re-examine it. Deferrals divide by
|
|
1865
|
+
who resolves them. Merge conflicts and not-yet-computed mergeability are
|
|
1866
|
+
pipeline-owned: a conflict re-enters the pipeline through `pr.conflicted` and
|
|
1867
|
+
reconcile (below), and uncomputed mergeability resolves on a later poll —
|
|
1868
|
+
neither records anything, and the reconcile step on the build row is the only
|
|
1869
|
+
indication. A kernel-authored follow-up is recorded only for a deferral a
|
|
1870
|
+
person must fix: the first such refusal for that PR and consent command records
|
|
1871
|
+
one follow-up while the PR is open, and a later `reconcile.completed` or
|
|
1872
|
+
`finalize.completed` supersedes it — the projection shows nothing until the
|
|
1873
|
+
gate re-examines consent and records a fresh one. A repository
|
|
1874
|
+
with native auto-merge disabled is left open for a human; Autobuild never
|
|
1875
|
+
changes the setting. The local-git forge has no external gate or native
|
|
1876
|
+
setting: consent produces the same inspected-head guarded squash candidate,
|
|
1877
|
+
mergeability is recomputed against the current local base, and the exact
|
|
1878
|
+
`pr-description` becomes the single-parent squash commit message. Its author and
|
|
1879
|
+
committer are Git's ordinary resolved repository identity, exactly as for a plain
|
|
1880
|
+
local commit; commit signing remains disabled. Before offering the candidate,
|
|
1881
|
+
the adapter resolves both identities through Git. If either is unavailable,
|
|
1882
|
+
consent remains pending and one reason-bearing follow-up names the missing
|
|
1883
|
+
`user.name` / `user.email` configuration; later polls retry after the operator
|
|
1884
|
+
configures it. When the base branch is checked out, Git's two-tree
|
|
1885
|
+
`read-tree -m -u` transition preflights and lands the old-to-squash tree:
|
|
1886
|
+
tracked/index changes on untouched paths and
|
|
1887
|
+
untracked non-colliding files survive, while a tracked or untracked overwrite
|
|
1888
|
+
leaves consent pending and records one path-bearing follow-up instead of failing
|
|
1889
|
+
the dispatcher. Its durable pending-landing record is written before moving the
|
|
1890
|
+
base ref. A poll after a crash retries that same old-to-landed transition without
|
|
1891
|
+
rolling back the ref or overwriting operator work; the PR remains open and
|
|
1892
|
+
retryable until checkout repair succeeds. No merge is ever assumed: a build
|
|
1893
|
+
reaches `merged` only when a later poll observes the landing and any required
|
|
1894
|
+
checkout synchronization. Without consent, the local branch and PR record
|
|
1895
|
+
remain open for inspection with ordinary Git.
|
|
1896
|
+
|
|
1897
|
+
**Conflicts re-enter the pipeline via `reconcile`.** When the janitor's
|
|
1898
|
+
mergeability check fails it emits `pr.conflicted` and re-attaches a
|
|
1899
|
+
build-runner (the dispatcher itself never runs agents). Immediately before
|
|
1900
|
+
each attempt, the runner asks the Forge for its current authoritative base
|
|
1901
|
+
snapshot when supported, otherwise fetching the build's frozen base branch
|
|
1902
|
+
fresh from `origin`, and records the resolved SHA on `reconcile.started` — known-stale input is never
|
|
1903
|
+
used (§15.3). The agent merges that base into the branch guided by the spec,
|
|
1904
|
+
plan, and implement-notes, with the explicit charge to regress against
|
|
1905
|
+
neither; the resolution lands as a merge commit, and because reconciliation
|
|
1906
|
+
changed code, **`verify:*` re-runs in full**. A resolution the agent judges
|
|
1907
|
+
risky — semantic conflicts, spec-relevant choices — escalates rather than
|
|
1908
|
+
guesses. Reconcile skips `code-review` by default (escalation covers the
|
|
1909
|
+
judgment cases; policy can force it).
|
|
1910
|
+
|
|
1911
|
+
On each repeat conflict, the runner records a fresh authoritative base snapshot
|
|
1912
|
+
on `reconcile.progress-checked`, tied to that conflict and the most recently
|
|
1913
|
+
completed attempt. The kernel compares it with that attempt's matching latest
|
|
1914
|
+
`reconcile.started.baseSha`. A different SHA proves that the reconcile lost a
|
|
1915
|
+
race against a moving base, so another monotonic attempt runs regardless of the
|
|
1916
|
+
configured limit. An equal SHA consumes `policy.maxReconcileAttempts`; reaching
|
|
1917
|
+
the limit escalates because reconciliation made no progress against an unchanged
|
|
1918
|
+
base. This no-progress escalation is conflict-scoped, roundless, and identified
|
|
1919
|
+
by `policyCause = "reconcile-no-progress"`. By contrast, a runner retry
|
|
1920
|
+
exhaustion or non-retryable failure while obtaining the progress check or
|
|
1921
|
+
refreshing the base is scoped to the concrete next reconcile round and carries
|
|
1922
|
+
its own cause. Answering that runner condition re-arms its phase-round failure
|
|
1923
|
+
budget so the authoritative decision can complete, but it does not acknowledge
|
|
1924
|
+
no-progress: if the unchanged-base budget is already exhausted, the separate
|
|
1925
|
+
no-progress escalation is raised before another reconcile session starts.
|
|
1926
|
+
Answering the matching no-progress escalation continues to authorize one
|
|
1927
|
+
subsequent reconcile for the same conflict without resetting runner failures
|
|
1928
|
+
already counted for any reconcile round. An explicitly different cause cannot
|
|
1929
|
+
acknowledge that guard even when roundless. Cause-less historical roundless
|
|
1930
|
+
`policy`/`reconcile` raises retain the former no-progress and phase-level
|
|
1931
|
+
failure-reset interpretations on replay without migration. Routing uses cause
|
|
1932
|
+
for class identity and reset semantics, `round` for occurrence scope, and never
|
|
1933
|
+
`question` text.
|
|
1934
|
+
|
|
1935
|
+
The classification is reduced from the durable log, with the next attempt's
|
|
1936
|
+
authoritative `reconcile.started` serving as the observation for historical logs
|
|
1937
|
+
that predate the progress-check event. A started but incomplete attempt re-runs
|
|
1938
|
+
at the same number and consumes nothing. Every completed reconcile still starts
|
|
1939
|
+
a fresh, fully bounded `verify:*` cycle.
|
|
1940
|
+
|
|
1941
|
+
The grammar's tail is thus an epilogue loop, outside the mainline:
|
|
1942
|
+
|
|
1943
|
+
```
|
|
1944
|
+
finalize → ( pr.conflicted → reconcile → verify:* )* → merged | closed
|
|
1945
|
+
```
|
|
1946
|
+
|
|
1947
|
+
## 16. Per-repo configuration and installation
|
|
1948
|
+
|
|
1949
|
+
Decisions here continue the series: **[D9]** declarative repo config and
|
|
1950
|
+
**[D11]** vendored editable skills.
|
|
1951
|
+
|
|
1952
|
+
### 16.1 `autobuild.toml` [D9]
|
|
1953
|
+
|
|
1954
|
+
One declarative file at the repo root. A running dispatcher owns an accepted
|
|
1955
|
+
snapshot read from the **main checkout** and refreshes it before each dispatch
|
|
1956
|
+
tick. Before launch and after each accepted revision, the dispatcher deposits
|
|
1957
|
+
the composed snapshot into each affected build's artifact namespace. The child
|
|
1958
|
+
samples that durable artifact at setup and pipeline-step boundaries, so a valid
|
|
1959
|
+
change may affect the next action of an in-flight build but never interrupts an agent turn
|
|
1960
|
+
or deterministic command already running. Scoped phase CLI processes separately
|
|
1961
|
+
read the build worktree's branch-owned file. Because the file is repo-versioned,
|
|
1962
|
+
changes still flow through the pipeline itself: once merged, the system can
|
|
1963
|
+
adopt a retuned configuration without restarting the dispatcher.
|
|
1964
|
+
|
|
1965
|
+
A missing or unreadable file and a malformed or routing-invalid candidate are
|
|
1966
|
+
rejected atomically: the last valid snapshot, resolver, and revision remain in
|
|
1967
|
+
force, and no action starts under missing, partial, or defaulted configuration.
|
|
1968
|
+
The dispatcher emits an actionable operator notice without repeating it for the
|
|
1969
|
+
same failure. Restoring a valid `autobuild.toml` clears the rejection and
|
|
1970
|
+
ordinary boundary reload resumes.
|
|
1971
|
+
|
|
1972
|
+
```toml
|
|
1973
|
+
baseBranch = "main"
|
|
1974
|
+
capacity = 3 # concurrent builds for this repo
|
|
1975
|
+
forge = "github" # builtin: github | local-git; or a plugin name
|
|
1976
|
+
plugins = ["./plugins/local.ts", "@acme/autobuild-plugin"]
|
|
1977
|
+
|
|
1978
|
+
#[workspace] # optional; default provider = "git-worktree"
|
|
1979
|
+
#provider = "vercel-sandbox" # shipped remote provider, or plugin name
|
|
1980
|
+
#[workspace.config]
|
|
1981
|
+
#image = "vercel/sandbox/universal:latest"
|
|
1982
|
+
#vcpus = 4
|
|
1983
|
+
#timeoutSeconds = 2700
|
|
1984
|
+
#operationTimeoutMs = 30000 # provider calls, not VM/session lifetime
|
|
1985
|
+
#environmentVariables = ["ANTHROPIC_API_KEY"]
|
|
1986
|
+
|
|
1987
|
+
#[pr.imageHost] # optional public inline rendering for attached images
|
|
1988
|
+
#provider = "github-release"
|
|
1989
|
+
#repository = "owner/public-review-assets"
|
|
1990
|
+
#releaseId = 123456
|
|
1991
|
+
|
|
1992
|
+
[commands] # deterministic verbs the kernel may run
|
|
1993
|
+
setup = "bun install" # after provision / sandbox rehydrate (§15.6-C)
|
|
1994
|
+
typecheck = "bun run type-check"
|
|
1995
|
+
test = "bun run test"
|
|
1996
|
+
publish = "bun run publish"
|
|
1997
|
+
|
|
1998
|
+
[verify]
|
|
1999
|
+
steps = ["types", "unit", "e2e"]
|
|
2000
|
+
[verify.types]
|
|
2001
|
+
kind = "check" # deterministic: command + pass/fail
|
|
2002
|
+
command = "typecheck" # ref into [commands]
|
|
2003
|
+
[verify.unit]
|
|
2004
|
+
kind = "check"
|
|
2005
|
+
command = "test"
|
|
2006
|
+
[verify.e2e]
|
|
2007
|
+
kind = "agent" # agent-verify: skill + pass|fail|skip verdict
|
|
2008
|
+
skill = "verify-app-e2e" # repository-authored agent verifier
|
|
2009
|
+
paths = ["web/**"] # optional changed-path applicability
|
|
2010
|
+
|
|
2011
|
+
[finalize]
|
|
2012
|
+
steps = ["publish", "release-notes"] # ordered, failure-tolerant (§5)
|
|
2013
|
+
[finalize.publish]
|
|
2014
|
+
kind = "check"
|
|
2015
|
+
command = "publish"
|
|
2016
|
+
[finalize.release-notes]
|
|
2017
|
+
kind = "agent"
|
|
2018
|
+
skill = "ab-release-notes"
|
|
2019
|
+
|
|
2020
|
+
[roles.default] # reserved inheritance base, never a phase (§9)
|
|
2021
|
+
runtime = "claude"
|
|
2022
|
+
|
|
2023
|
+
[roles.code-review] # fields override default independently
|
|
2024
|
+
runtime = "pi"
|
|
2025
|
+
model = "moonshotai/kimi-k3"
|
|
2026
|
+
sessionBudgetSeconds = 7200 # optional role override
|
|
2027
|
+
|
|
2028
|
+
[policy]
|
|
2029
|
+
sessionBudgetSeconds = 3600 # non-infinite default for build agent sessions
|
|
2030
|
+
stallRounds = 3
|
|
2031
|
+
maxVerifyAttempts = 3
|
|
2032
|
+
maxSetupAttempts = 3
|
|
2033
|
+
maxInfrastructureAttempts = 3
|
|
2034
|
+
maxReconcileAttempts = 3
|
|
2035
|
+
maxReviewRounds = 6
|
|
2036
|
+
harvestThreshold = 5 # observation-count pressure in dispatch
|
|
2037
|
+
harvestMaxDrift = 3 # merged builds since oldest observation; 0 disables
|
|
2038
|
+
|
|
2039
|
+
[tickets]
|
|
2040
|
+
source = "file"
|
|
2041
|
+
readyState = "ready" # required: the one state a ticket must sit in to dispatch
|
|
2042
|
+
```
|
|
2043
|
+
|
|
2044
|
+
The root scalars must appear before the first table header (TOML otherwise
|
|
2045
|
+
nests them in that table). `forge` defaults to `"github"` and `plugins` defaults
|
|
2046
|
+
to `[]`, preserving repositories with no plugin configuration. Every nonblank
|
|
2047
|
+
`plugins` specifier must be unique by exact string; a repeated entry fails
|
|
2048
|
+
config validation with the duplicate position, first position, and
|
|
2049
|
+
remove/deduplicate guidance. `[workspace]` defaults to
|
|
2050
|
+
`provider = "git-worktree"` and empty config; the strict selector
|
|
2051
|
+
envelope permits plugin-owned values under `[workspace.config]`; selecting the
|
|
2052
|
+
Vercel builtin applies its closed image/vCPU/timeout/region/environment-name
|
|
2053
|
+
schema. Unknown providers fail with the complete available-name list. A
|
|
2054
|
+
workspace records provider `ref`, absolute execution-environment `path`, and an
|
|
2055
|
+
optional dispatcher-reachable `localPath`. Vercel runs setup, all agent/check
|
|
2056
|
+
phases, review/reconciliation, and finalize post-steps in its guest checkout.
|
|
2057
|
+
Its child uses environment-owned supervision rather than a host-PID watchdog.
|
|
2058
|
+
Remote publication is a durable request settled by the local supervisor only
|
|
2059
|
+
after command exit, VM stop, and lease release; credentials are transformed by
|
|
2060
|
+
the firewall only for an exact fixed push, and Forge/PR calls remain local.
|
|
2061
|
+
Declarative (TOML), not executable config: the
|
|
2062
|
+
kernel, dispatcher, CLI, and any future tooling parse it without evaluating
|
|
2063
|
+
anything; commands are plain shell strings. Parsing is strict — an unknown table or key is an error, so a
|
|
2064
|
+
typo cannot silently disable a verifier. The full config surface, field
|
|
2065
|
+
semantics, and validation rules live with the config code and
|
|
2066
|
+
`docs/configuration.md`. The removed `[dashboardFrames]`, `[project]`,
|
|
2067
|
+
`[dispatcher]`, `[harvest]`, and `[outer]` tables have no aliases or migration
|
|
2068
|
+
shims. `[tickets].source` is a nonblank builtin or plugin registration name.
|
|
2069
|
+
The builtin `linear` and `file` branches retain their exact field restrictions
|
|
2070
|
+
and defaults; any other name is resolved after plugin loading, receives the
|
|
2071
|
+
existing ticket table as factory config, and fails with the complete available
|
|
2072
|
+
name set when unregistered. Plugin-declared `requiredEnv` values are never TOML
|
|
2073
|
+
fields: secrets stay in the process environment or local `.env`.
|
|
2074
|
+
|
|
2075
|
+
Two configurable narrowing mechanisms govern which verify steps run, both
|
|
2076
|
+
resolving to the ordinary `skipped` outcome so exclusions stay queryable:
|
|
2077
|
+
|
|
2078
|
+
- **Plan selection.** An approved plan may declare the complete subset of
|
|
2079
|
+
optional verify steps warranted by the spec, in strict front matter
|
|
2080
|
+
validated at deposit. The selection paired with the approving review
|
|
2081
|
+
verdict is authoritative; `always = true` steps can never be deselected.
|
|
2082
|
+
Missing metadata means all configured steps, so historical builds keep
|
|
2083
|
+
their behavior.
|
|
2084
|
+
- **Path applicability.** A step may declare positive changed-path selectors,
|
|
2085
|
+
evaluated by the kernel immediately before the step on every verify cycle,
|
|
2086
|
+
diffing `HEAD` against the build's durable base (promoted by a completed
|
|
2087
|
+
reconcile, so upstream-merged work is not attributed to the build). No
|
|
2088
|
+
match skips without launching anything; a Git failure is infrastructure and
|
|
2089
|
+
fails closed, never a synthetic skip.
|
|
2090
|
+
|
|
2091
|
+
### 16.3 Skill installation: vendored, namespaced, editable [D11]
|
|
2092
|
+
|
|
2093
|
+
This project ships 11 canonical default skills for the fixed phases, the
|
|
2094
|
+
non-phase surfaces (`spec`, `tickets`, and `guide`), and the outer-loop skills.
|
|
2095
|
+
Repository setup guidance ships as `guide/references/setup.md`, not as a skill;
|
|
2096
|
+
agent verifier skills are authored by each repository when needed. `ab init`
|
|
2097
|
+
installs into a repo:
|
|
2098
|
+
|
|
2099
|
+
- **A stack-neutral `autobuild.toml` skeleton**, rendered only when absent. It
|
|
2100
|
+
declares no setup command or verify step and inspects no language or package
|
|
2101
|
+
manifest. Its explicit `[roles.default].runtime` is only a schema placeholder.
|
|
2102
|
+
Existing config is never overwritten, even with `--force`.
|
|
2103
|
+
- **An agent-driven setup handoff.** Init probes every registered runtime and
|
|
2104
|
+
reports each usable/unusable result, then chooses an interactive setup agent
|
|
2105
|
+
using the product-fixed `claude`, `codex`, `pi` preference order. The launcher
|
|
2106
|
+
choice does not constrain the pipeline's final roles or models. On an
|
|
2107
|
+
interactive terminal, init starts that coding-agent CLI directly in the
|
|
2108
|
+
target repository with inherited terminal I/O, no `AB_*` session identity,
|
|
2109
|
+
and a short prompt pointing to the installed editable
|
|
2110
|
+
`.agents/skills/ab-guide/references/setup.md`. The prompt carries the
|
|
2111
|
+
new-config or existing-config preface but does not embed the reference body.
|
|
2112
|
+
Its exit status is init's. This direct process creates no build, session,
|
|
2113
|
+
transcript, event, or BuildStore record. Without a usable shipped runtime or
|
|
2114
|
+
an interactive terminal, installation still succeeds and init prints the
|
|
2115
|
+
exact same pointer prompt for the user to run in a coding agent. A missing
|
|
2116
|
+
setup reference in an older or partial distribution does not fail init. On
|
|
2117
|
+
rerun, the prompt asks the agent to review and improve the preserved existing
|
|
2118
|
+
config.
|
|
2119
|
+
- **Copies** of the 11 default skills into the project skills directory,
|
|
2120
|
+
namespaced `ab-*`. Copies, not references — per-repo customization is the
|
|
2121
|
+
point: this repo's code-review standards and setup reference live in the
|
|
2122
|
+
vendored trees. Harness-specific discovery paths are symlinks to the one
|
|
2123
|
+
canonical editable copy. Setup directs the repository to author and configure
|
|
2124
|
+
its own agent verifier when judgment-driven end-to-end coverage is warranted.
|
|
2125
|
+
- **Model-invocation discipline.** Phase skills are installed
|
|
2126
|
+
non-agent-invocable: they are invoked explicitly by the runner or a human,
|
|
2127
|
+
never auto-triggered by a model pattern-matching a description — a model
|
|
2128
|
+
must not start a pipeline phase by accident. The model-invocable exceptions
|
|
2129
|
+
(`ab-spec`, `ab-tickets`, `ab-guide`) are exactly the three skills that
|
|
2130
|
+
**drive no phase**; membership is decided by that criterion, not taste.
|
|
2131
|
+
|
|
2132
|
+
When `.agents/skills` and `.claude/skills` resolve to the same filesystem
|
|
2133
|
+
entry, that repository-level alias already satisfies harness discovery. Init
|
|
2134
|
+
and upgrade create no per-skill links through it and do not reinterpret its
|
|
2135
|
+
live or pristine files as legacy `.claude` copies. A real
|
|
2136
|
+
`.claude/skills/ab-*` directory that is a distinct filesystem entry remains a
|
|
2137
|
+
conflict: all other skills are still processed, every such conflict is reported
|
|
2138
|
+
with move/remove guidance, setup is not launched by init, and the command exits
|
|
2139
|
+
nonzero.
|
|
2140
|
+
|
|
2141
|
+
**Installation identity** is local to the running distribution. `ab --version`
|
|
2142
|
+
reads its package version, Bun-recorded forge commit when present, and the
|
|
2143
|
+
independent plugin API version; it needs no repository, config, store, or
|
|
2144
|
+
network. A source checkout is identified by its `.git` marker. A managed Bun
|
|
2145
|
+
install has one of two channels, both derived from the owning `package.json`
|
|
2146
|
+
dependency and `bun.lock` record, never hardcoded. An npm registry install is
|
|
2147
|
+
identified when the direct dependency is a semver range the installed version
|
|
2148
|
+
satisfies and the lock resolves that name to the same registry version. A
|
|
2149
|
+
movable Bun forge install is identified only when the distribution's
|
|
2150
|
+
`.bun-tag`, its owning direct `github:owner/repository` dependency, and
|
|
2151
|
+
`bun.lock` record agree; a fork install therefore follows its fork. Unknown or
|
|
2152
|
+
contradictory provenance is a named refusal, never a guessed install command.
|
|
2153
|
+
|
|
2154
|
+
**Upgrades** run only on explicit `ab upgrade`. By default the command resolves
|
|
2155
|
+
the latest published version on the installation's channel: the registry's
|
|
2156
|
+
version document for an npm install, or the latest full GitHub Release of the
|
|
2157
|
+
installation repository for a forge install. If newer, it
|
|
2158
|
+
uses the matching Bun local or global operation to install the release, then
|
|
2159
|
+
hands off to a fresh process from the replaced distribution before touching
|
|
2160
|
+
skills. Thus both defaults and merge logic come from the new version. A local
|
|
2161
|
+
install updates the owning project's `package.json` dependency and `bun.lock`;
|
|
2162
|
+
a global install updates Bun's global package-manager state. An already-current
|
|
2163
|
+
(or locally newer) install proceeds directly to skill merge. A source checkout
|
|
2164
|
+
is never mutated and still merges installed skills, including when an exact
|
|
2165
|
+
release was named. For the default latest operation, an indeterminate mechanism
|
|
2166
|
+
or lookup/install failure warns and also merges installed skills. For an
|
|
2167
|
+
operator-selected `--version <semver>`, indeterminate mechanism, resolution, or
|
|
2168
|
+
install failure is fatal and does not merge against the wrong defaults.
|
|
2169
|
+
`--no-self-update` always selects merge-only
|
|
2170
|
+
behavior. An exact version may be older than the installed version.
|
|
2171
|
+
|
|
2172
|
+
Upgrade records its successful work in one local commit on the target's current
|
|
2173
|
+
HEAD by default. The exact ownership boundary is the reported skills' canonical
|
|
2174
|
+
`.agents` trees, pristine records, and `.claude` discovery paths, plus
|
|
2175
|
+
`package.json` and `bun.lock` only when this run's successful local Bun update
|
|
2176
|
+
wrote those files inside the target repository. A global update contributes no
|
|
2177
|
+
repository path. Additions, modifications, symlinks, and deletions all
|
|
2178
|
+
participate; a run with no owned Git change creates no commit. The message
|
|
2179
|
+
identifies `ab upgrade` and lists every skill with an owned byte change together
|
|
2180
|
+
with its reported outcome. It supplies no authorship or attribution trailers.
|
|
2181
|
+
Unrelated staged, unstaged, and untracked work is neither committed nor
|
|
2182
|
+
unstaged.
|
|
2183
|
+
|
|
2184
|
+
The baseline is captured before self-update and carried to the replacement
|
|
2185
|
+
binary. `--no-commit` also survives that handoff and leaves the merge exactly as
|
|
2186
|
+
written. A replacement child marked as a handoff but missing that pre-update
|
|
2187
|
+
baseline — as when an older parent launches a newer child without the newer
|
|
2188
|
+
context — suppresses the entire automatic commit, names the cross-version
|
|
2189
|
+
compatibility reason, and leaves all upgrade-owned changes uncommitted for the
|
|
2190
|
+
operator. The skill-upgrade results and merge-derived exit status are unchanged.
|
|
2191
|
+
Any content conflict, Claude discovery conflict, pre-existing dirt in an owned
|
|
2192
|
+
path, non-Git target, changed HEAD/worktree identity, or in-progress merge,
|
|
2193
|
+
rebase, or cherry-pick likewise suppresses the whole commit and prints the
|
|
2194
|
+
reason. If upgrade cannot snapshot the worktree's Git index, it warns and
|
|
2195
|
+
declines to stage. A staging or commit failure restores that exact pre-attempt
|
|
2196
|
+
index without touching the merged worktree files, and warns with the original
|
|
2197
|
+
Git failure.
|
|
2198
|
+
Commit suppression or failure never changes the merge-derived exit status:
|
|
2199
|
+
content conflicts remain zero and discovery conflicts remain nonzero. Upgrade
|
|
2200
|
+
never pushes or rewrites existing history.
|
|
2201
|
+
|
|
2202
|
+
Skill handling remains the classic vendoring problem: `ab init` records the
|
|
2203
|
+
pristine version of each installed skill; upgrade three-way merges (pristine
|
|
2204
|
+
base × local edits × new default). Two fixed former defaults, `ab-setup` and
|
|
2205
|
+
`ab-verify-e2e`, additionally have one-time retirement handling when absent
|
|
2206
|
+
from the incoming distribution. A pristine record is required to prove
|
|
2207
|
+
Autobuild provenance; without it, a same-named repository-authored skill is
|
|
2208
|
+
untouched and unreported. Upgrade reports `removed` when the complete live and
|
|
2209
|
+
pristine trees match byte-for-byte, no configured agent verify or finalize step
|
|
2210
|
+
names the skill, and no user-owned Claude discovery entry remains; it also
|
|
2211
|
+
reports `removed` when the canonical live tree is already missing. In the latter
|
|
2212
|
+
state it clears obsolete provenance and removes only an Autobuild-owned dangling
|
|
2213
|
+
discovery link. Any customization, config reference, or inability to inspect
|
|
2214
|
+
config safely preserves the live tree and reports `kept`. When an otherwise
|
|
2215
|
+
removable canonical tree is shadowed by a user-owned
|
|
2216
|
+
`.claude/skills/<name>` discovery entry — either a distinct real directory or a
|
|
2217
|
+
foreign symlink — upgrade reports `kept`, removes only the canonical/pristine
|
|
2218
|
+
Autobuild-owned trees, preserves the entry byte-for-byte (including a symlink's
|
|
2219
|
+
link text and target), and includes it in the existing structured
|
|
2220
|
+
discovery-conflict report and nonzero exit behavior. Every terminal
|
|
2221
|
+
classification clears obsolete pristine ownership, so a second upgrade neither
|
|
2222
|
+
recreates a dangling link nor resurrects or re-reports the retirement. All
|
|
2223
|
+
other installed skills absent from the distribution retain the `unknown`
|
|
2224
|
+
local-addition classification.
|
|
2225
|
+
|
|
2226
|
+
A conflict may be resolved by the optional
|
|
2227
|
+
tool-free `upgrade` one-shot with a standing bias: **prefer the local
|
|
2228
|
+
customization**. The agent output is only an untrusted proposal — deterministic
|
|
2229
|
+
validation verifies skill identity and exact preservation of every already-clean
|
|
2230
|
+
merge region before anything is written. Each per-file judgment has a fixed
|
|
2231
|
+
generous deadline of at least ten minutes. While it runs on an interactive
|
|
2232
|
+
stdout, upgrade redraws a live indicator naming the skill and path, elapsed
|
|
2233
|
+
time, and Ctrl-C cancellation. Ctrl-C there aborts only that invocation: the
|
|
2234
|
+
file becomes an ordinary byte-untouched content conflict and upgrade continues.
|
|
2235
|
+
Outside an active resolution Ctrl-C retains ordinary process termination.
|
|
2236
|
+
Non-TTY output stays byte-identical and line-oriented. Failed, timed-out,
|
|
2237
|
+
cancelled, or unavailable judgment leaves both live and pristine byte-untouched
|
|
2238
|
+
and names the manual merge path. These content conflicts remain exit zero;
|
|
2239
|
+
discovery conflicts retain their nonzero contract. Local customization survives
|
|
2240
|
+
upgrades; divergence is visible instead of silent. No other command checks for
|
|
2241
|
+
or installs releases in the foreground or background.
|
|
2242
|
+
|
|
2243
|
+
## 17. Out of scope for v2.0 (explicitly)
|
|
2244
|
+
|
|
2245
|
+
- True push `subscribe` (interface reserved; polling implementation).
|
|
2246
|
+
- Live transcript streaming for writers and readers (the stream primitive —
|
|
2247
|
+
SPEC §7.6 — has shipped; agent runners emitting parts and operator UIs
|
|
2248
|
+
rendering them remain future work).
|
|
2249
|
+
- Web UI (seam designed; terminal first).
|
|
2250
|
+
- Generic workflow DAGs (the grammar is fixed; extension via `verify:*` and
|
|
2251
|
+
`finalize:*` only).
|
|
2252
|
+
|
|
2253
|
+
## 18. Open threads
|
|
2254
|
+
|
|
2255
|
+
1. **[OPEN] Other-ingester detail** — observation harvest is decided (§8.8,
|
|
2256
|
+
§12); per-source filter design for scheduled `ingest:*` sources remains
|
|
2257
|
+
open.
|
|
2258
|
+
2. **[OPEN] Retention/archival policy** — the v1 archival gap, now a store
|
|
2259
|
+
config concern rather than a repo problem. Needs a default (e.g. prune
|
|
2260
|
+
blobs for merged builds after N months, keep events).
|
|
2261
|
+
3. **[OPEN] Global capacity** — per-repo capacity is the top-level `capacity`
|
|
2262
|
+
scalar (§16.1); whether a cross-repo global cap is needed, and where it
|
|
2263
|
+
lives, is unresolved.
|
|
2264
|
+
4. **[DECIDED] Hosted, multi-host deployment** — independently deployed
|
|
2265
|
+
kernels, build sessions, and operator clients reach the hosted HTTP service
|
|
2266
|
+
tier described in §7.2. The tier owns authentication, write-rule and version
|
|
2267
|
+
enforcement and will colocate later session-authenticated human routes.
|
|
2268
|
+
Artifacts remain content-by-value for now; direct-to-blob references are a
|
|
2269
|
+
possible later protocol revision, not a second current deployment path.
|