@defrex/autobuild 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/SPEC.md +222 -22
- package/docs/architecture.md +66 -4
- package/docs/configuration.md +105 -16
- package/docs/hosted-dispatcher.md +17 -2
- package/docs/hosted-service.md +214 -0
- package/docs/mcp.md +8 -2
- package/docs/operator-api.md +90 -7
- package/docs/remote-store-protocol.md +189 -144
- package/docs/remote-ticket-protocol.md +107 -19
- package/docs/setup.md +173 -16
- package/docs/ticket-dependencies.md +1 -1
- package/package.json +7 -6
- package/packages/core/src/cli/attention.ts +209 -0
- package/packages/core/src/cli/configured-store.ts +17 -0
- package/packages/core/src/cli/connect.ts +261 -0
- package/packages/core/src/cli/dashboard/model.ts +4 -5
- package/packages/core/src/cli/dispatch.ts +183 -41
- package/packages/core/src/cli/help.ts +39 -10
- package/packages/core/src/cli/init-validation.ts +27 -2
- package/packages/core/src/cli/main.ts +41 -15
- package/packages/core/src/cli/mcp.ts +7 -1
- package/packages/core/src/cli/pr-summary-publication.ts +220 -0
- package/packages/core/src/cli/pr-summary.ts +2 -0
- package/packages/core/src/cli/project-credentials.ts +132 -0
- package/packages/core/src/cli/repo-state.ts +15 -2
- package/packages/core/src/cli/self-update.ts +10 -0
- package/packages/core/src/cli/store-opening.ts +22 -14
- package/packages/core/src/cli/terminals.ts +0 -10
- package/packages/core/src/cli/ticket.ts +8 -1
- package/packages/core/src/cli/upgrade-compatibility.ts +80 -0
- package/packages/core/src/cli/wait.ts +84 -17
- package/packages/core/src/cli/watch.ts +197 -60
- package/packages/core/src/config/live.ts +3 -0
- package/packages/core/src/config/schema.ts +56 -9
- package/packages/core/src/connection.ts +86 -0
- package/packages/core/src/events/catalog.ts +10 -2
- package/packages/core/src/events/payloads.ts +44 -0
- package/packages/core/src/events/repository.ts +13 -0
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +130 -83
- package/packages/core/src/kernel/engine.ts +24 -0
- package/packages/core/src/kernel/generators/build-log.ts +10 -0
- package/packages/core/src/kernel/log-index.ts +17 -0
- package/packages/core/src/kernel/pr-summary.ts +89 -0
- package/packages/core/src/kernel/reducer-registry.ts +8 -0
- package/packages/core/src/kernel/reducer.ts +8 -0
- package/packages/core/src/ontology.ts +1 -0
- package/packages/core/src/operator/index.ts +7 -0
- package/packages/core/src/operator/query.ts +38 -16
- package/packages/core/src/operator/ticket-source-backend.ts +1 -0
- package/packages/core/src/operator/tickets.ts +6 -8
- package/packages/core/src/plugin-sdk/index.ts +1 -0
- package/packages/core/src/plugins/manifest.ts +5 -3
- package/packages/core/src/plugins/runtimes.ts +3 -1
- package/packages/core/src/ports/forge/fake.ts +13 -1
- package/packages/core/src/ports/forge/github-transport.ts +36 -1
- package/packages/core/src/ports/forge/github.ts +29 -5
- package/packages/core/src/ports/forge/local-git.ts +12 -4
- package/packages/core/src/ports/forge/summary-marker.ts +8 -0
- package/packages/core/src/ports/runner/session-env.ts +1 -0
- package/packages/core/src/ports/tickets/create.ts +11 -3
- package/packages/core/src/ports/tickets/hosted-index.ts +0 -3
- package/packages/core/src/ports/tickets/remote-protocol.ts +1 -0
- package/packages/core/src/ports/tickets/remote.ts +1 -0
- package/packages/core/src/ports/workspace/build-execution.ts +5 -1
- package/packages/core/src/ports/workspace/create.ts +11 -2
- package/packages/core/src/ports/workspace/distribution-archive.ts +3 -5
- package/packages/core/src/ports/workspace/harvest-execution.ts +3 -1
- package/packages/core/src/ports/workspace/local-build-execution.ts +8 -1
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +47 -19
- package/packages/core/src/processes/build-execution-state.ts +1 -1
- package/packages/core/src/processes/build-runner.ts +87 -26
- package/packages/core/src/processes/dispatch-failure-budget.ts +83 -0
- package/packages/core/src/processes/dispatcher.ts +59 -10
- package/packages/core/src/processes/harvest-child.ts +1 -0
- package/packages/core/src/processes/harvest-execution-state.ts +19 -2
- package/packages/core/src/processes/harvest-runner.ts +142 -100
- package/packages/core/src/processes/phase-failure-budget.ts +1 -0
- package/packages/core/src/processes/publication-loss.ts +18 -1
- package/packages/core/src/store/remote/client.ts +1 -1
- package/packages/core/src/store/remote/compatibility.ts +90 -0
- package/packages/core/src/store/remote/credentials.ts +28 -0
- package/packages/core/src/store/remote/index.ts +14 -1
- package/packages/core/src/store/remote/protocol.ts +25 -0
- package/packages/core/src/store/remote/token.ts +74 -13
- package/packages/core/src/testing/index.ts +7 -0
- package/skills/guide/SKILL.md +144 -26
- package/skills/guide/references/operator-api.md +135 -13
- package/skills/guide/references/plugin-authoring.md +10 -2
- package/skills/guide/references/remote-store-protocol.md +189 -144
- package/skills/guide/references/remote-ticket-protocol.md +111 -18
- package/skills/guide/references/setup.md +173 -16
- package/skills/guide/references/ticket-dependencies.md +1 -1
- package/skills/operate/SKILL.md +126 -36
- package/templates/hosted-service/package.json +13 -0
- package/templates/hosted-service/vercel.json +7 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import semver from 'semver'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { AUTOBUILD_VERSION, REMOTE_STORE_PROTOCOL_VERSION } from './version'
|
|
4
|
+
|
|
5
|
+
/** Stable releases only: prefixes, whitespace, prereleases and build suffixes are not identities. */
|
|
6
|
+
export function stableVersion(value: unknown): semver.SemVer | undefined {
|
|
7
|
+
if (typeof value !== 'string' || !/^\d+\.\d+\.\d+$/.test(value)) return undefined
|
|
8
|
+
const parsed = semver.parse(value)
|
|
9
|
+
return parsed && parsed.version === value ? parsed : undefined
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const stableVersionSchema = z.string().refine((value) => stableVersion(value) !== undefined)
|
|
13
|
+
export const supportedClientRangeSchema = z
|
|
14
|
+
.strictObject({
|
|
15
|
+
minVersion: stableVersionSchema,
|
|
16
|
+
maxVersionExclusive: stableVersionSchema,
|
|
17
|
+
})
|
|
18
|
+
.refine((range) => semver.lt(range.minVersion, range.maxVersionExclusive))
|
|
19
|
+
export type SupportedClientRange = z.infer<typeof supportedClientRangeSchema>
|
|
20
|
+
|
|
21
|
+
/** A major starts a new family: X.0 has one line, X.1 two, X.2+ three. */
|
|
22
|
+
export function supportedClientRange(serviceVersion = AUTOBUILD_VERSION): SupportedClientRange {
|
|
23
|
+
const version = stableVersion(serviceVersion)
|
|
24
|
+
if (!version) throw new Error('service must have a stable release version')
|
|
25
|
+
return {
|
|
26
|
+
minVersion: `${version.major}.${Math.max(0, version.minor - 2)}.0`,
|
|
27
|
+
maxVersionExclusive: `${version.major}.${version.minor + 1}.0`,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function classifyClient(
|
|
32
|
+
version: unknown,
|
|
33
|
+
range: SupportedClientRange,
|
|
34
|
+
): 'supported' | 'older' | 'newer' | 'invalid' {
|
|
35
|
+
const parsed = stableVersion(version)
|
|
36
|
+
if (!parsed) return 'invalid'
|
|
37
|
+
if (semver.lt(parsed, range.minVersion)) return 'older'
|
|
38
|
+
if (semver.gte(parsed, range.maxVersionExclusive)) return 'newer'
|
|
39
|
+
return 'supported'
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatClientRange(range: SupportedClientRange): string {
|
|
43
|
+
return `>=${range.minVersion} <${range.maxVersionExclusive}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function compatibilityAdvice(
|
|
47
|
+
client: unknown,
|
|
48
|
+
serviceVersion: string,
|
|
49
|
+
range: SupportedClientRange,
|
|
50
|
+
): string | undefined {
|
|
51
|
+
const classification = classifyClient(client, range)
|
|
52
|
+
const description = `service Autobuild ${serviceVersion} supports clients ${formatClientRange(range)}`
|
|
53
|
+
if (classification === 'supported') return undefined
|
|
54
|
+
if (classification === 'newer') {
|
|
55
|
+
const version = stableVersion(client)!
|
|
56
|
+
return `${description}; upgrade the service to at least ${version.major}.${version.minor}.0 before upgrading projects (service first)`
|
|
57
|
+
}
|
|
58
|
+
if (classification === 'older')
|
|
59
|
+
return `${description}; upgrade the project's Autobuild into ${formatClientRange(range)}`
|
|
60
|
+
return `${description}; supply a stable Autobuild client version`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Retain these wire identities and handlers until their lines leave the window.
|
|
64
|
+
* Historical lines require explicit entries; the service's own line uses its
|
|
65
|
+
* current protocol identity. Unknown historical protocols fail closed. */
|
|
66
|
+
export const RELEASE_LINE_PROTOCOLS: Readonly<Record<string, string>> = {
|
|
67
|
+
'0.9': '3',
|
|
68
|
+
'0.10': '3',
|
|
69
|
+
'0.11': '3',
|
|
70
|
+
'0.12': '3',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function clientIdentityError(
|
|
74
|
+
client: string | null,
|
|
75
|
+
protocol: string | null,
|
|
76
|
+
serviceVersion = AUTOBUILD_VERSION,
|
|
77
|
+
serviceProtocol = REMOTE_STORE_PROTOCOL_VERSION,
|
|
78
|
+
): string | undefined {
|
|
79
|
+
const range = supportedClientRange(serviceVersion)
|
|
80
|
+
const service = stableVersion(serviceVersion)!
|
|
81
|
+
const advice = compatibilityAdvice(client, serviceVersion, range)
|
|
82
|
+
const parsed = stableVersion(client)
|
|
83
|
+
const expected =
|
|
84
|
+
parsed &&
|
|
85
|
+
(parsed.major === service.major && parsed.minor === service.minor
|
|
86
|
+
? serviceProtocol
|
|
87
|
+
: RELEASE_LINE_PROTOCOLS[`${parsed.major}.${parsed.minor}`])
|
|
88
|
+
if (!advice && expected !== undefined && protocol === expected) return undefined
|
|
89
|
+
return `remote store version mismatch: client Autobuild ${client ?? '(missing)'} protocol ${protocol ?? '(missing)'}; server Autobuild ${serviceVersion} protocol ${serviceProtocol}; ${advice ?? `supported clients ${formatClientRange(range)}; unsupported protocol identity`}`
|
|
90
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { delegateCredentialResponseSchema, type DelegateCredentialTarget } from './protocol'
|
|
2
|
+
import {
|
|
3
|
+
AUTOBUILD_VERSION,
|
|
4
|
+
AUTOBUILD_VERSION_HEADER,
|
|
5
|
+
REMOTE_STORE_PROTOCOL_VERSION,
|
|
6
|
+
REMOTE_STORE_PROTOCOL_VERSION_HEADER,
|
|
7
|
+
} from './version'
|
|
8
|
+
|
|
9
|
+
/** Fail closed: a remote execution must never fall back to its parent token. */
|
|
10
|
+
export async function delegateCredential(
|
|
11
|
+
storeRef: string,
|
|
12
|
+
token: string | undefined,
|
|
13
|
+
target: DelegateCredentialTarget,
|
|
14
|
+
): Promise<string> {
|
|
15
|
+
if (!token) throw new Error('credential delegation requires parent authority')
|
|
16
|
+
const response = await fetch(`${storeRef.replace(/\/$/, '')}/credentials/delegate`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: {
|
|
19
|
+
authorization: `Bearer ${token}`,
|
|
20
|
+
'content-type': 'application/json',
|
|
21
|
+
[AUTOBUILD_VERSION_HEADER]: AUTOBUILD_VERSION,
|
|
22
|
+
[REMOTE_STORE_PROTOCOL_VERSION_HEADER]: REMOTE_STORE_PROTOCOL_VERSION,
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify(target),
|
|
25
|
+
})
|
|
26
|
+
if (!response.ok) throw new Error(`credential delegation refused (${response.status})`)
|
|
27
|
+
return delegateCredentialResponseSchema.parse(await response.json()).token
|
|
28
|
+
}
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
export { AuthError, RemoteBuildStore } from './client'
|
|
5
5
|
export type { RemoteBuildStoreOptions, RemoteStoreIdentity } from './client'
|
|
6
6
|
export { mintToken, tokenResource, verifyToken } from './token'
|
|
7
|
-
export type {
|
|
7
|
+
export type {
|
|
8
|
+
OperatorTokenScope,
|
|
9
|
+
InternalOperatorTokenScope,
|
|
10
|
+
AdminTokenScope,
|
|
11
|
+
TokenScope,
|
|
12
|
+
DelegatedTokenScope,
|
|
13
|
+
} from './token'
|
|
8
14
|
export {
|
|
9
15
|
AUTOBUILD_VERSION,
|
|
10
16
|
AUTOBUILD_VERSION_HEADER,
|
|
@@ -22,3 +28,10 @@ export type { SessionEvent, SessionEventWrite } from '../../events/sessions'
|
|
|
22
28
|
export type { RepositoryEvent, RepositoryEventWrite } from '../../events/repository'
|
|
23
29
|
export { SNAPSHOT_REDUCER_NAME } from '../snapshots'
|
|
24
30
|
export type { ReducerSnapshot, SnapshotScope } from '../snapshots'
|
|
31
|
+
|
|
32
|
+
export * from './compatibility'
|
|
33
|
+
|
|
34
|
+
export { delegateCredential } from './credentials'
|
|
35
|
+
|
|
36
|
+
export { reduceBuild } from '../../kernel/reducer'
|
|
37
|
+
export { reduceHarvest } from '../../kernel/harvest'
|
|
@@ -450,3 +450,28 @@ export function substitutePlaceholderRefs(
|
|
|
450
450
|
}
|
|
451
451
|
return value
|
|
452
452
|
}
|
|
453
|
+
|
|
454
|
+
/** Issuance is an authentication operation, separate from the BuildStore port. */
|
|
455
|
+
export const delegateCredentialBodySchema = z.discriminatedUnion('kind', [
|
|
456
|
+
z.strictObject({ kind: z.literal('build-runner'), id: z.string().min(1) }),
|
|
457
|
+
z.strictObject({
|
|
458
|
+
kind: z.literal('build-phase'),
|
|
459
|
+
id: z.string().min(1),
|
|
460
|
+
session: z.string().min(1),
|
|
461
|
+
}),
|
|
462
|
+
z.strictObject({
|
|
463
|
+
kind: z.literal('harvest-runner'),
|
|
464
|
+
execution: z.string().min(1),
|
|
465
|
+
repo: z.string().min(1),
|
|
466
|
+
id: z.string().min(1),
|
|
467
|
+
leaseHolder: z.string().min(1),
|
|
468
|
+
}),
|
|
469
|
+
z.strictObject({
|
|
470
|
+
kind: z.literal('harvest-phase'),
|
|
471
|
+
repo: z.string().min(1),
|
|
472
|
+
id: z.string().min(1),
|
|
473
|
+
session: z.string().min(1),
|
|
474
|
+
}),
|
|
475
|
+
])
|
|
476
|
+
export type DelegateCredentialTarget = z.infer<typeof delegateCredentialBodySchema>
|
|
477
|
+
export const delegateCredentialResponseSchema = z.strictObject({ token: z.string().min(1) })
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* runner minting a narrow token, not from prompt instructions.
|
|
6
|
-
*
|
|
7
|
-
* A token is `base64url(JSON scope) + '.' + base64url(hmac-sha256(secret,
|
|
8
|
-
* payload))`. The scope IS the token — the server keeps no session state;
|
|
9
|
-
* it verifies the signature (timing-safe) and the expiry, then trusts the
|
|
10
|
-
* embedded scope.
|
|
1
|
+
/** Signed lifecycle-bound execution grants, internal owner delegation and admin.
|
|
2
|
+
* Historical resource/deployment/operator scopes remain decodable for tests
|
|
3
|
+
* and migration diagnostics; hosted machine gates reject those retired forms.
|
|
4
|
+
* Project credentials are opaque stored secrets, not these stateless tokens.
|
|
11
5
|
*/
|
|
12
6
|
import { createHmac, timingSafeEqual } from 'node:crypto'
|
|
13
7
|
import { z } from 'zod'
|
|
@@ -52,8 +46,35 @@ export interface DeploymentTokenScope {
|
|
|
52
46
|
exp: number
|
|
53
47
|
}
|
|
54
48
|
|
|
55
|
-
/**
|
|
49
|
+
/** Includes retired wire shapes for recognition, never server authorization. */
|
|
50
|
+
export interface InternalOperatorTokenScope {
|
|
51
|
+
version: 1
|
|
52
|
+
internalOperator: { user: string; repo: string }
|
|
53
|
+
exp: number
|
|
54
|
+
via?: Via
|
|
55
|
+
}
|
|
56
|
+
export interface AdminTokenScope {
|
|
57
|
+
version: 1
|
|
58
|
+
ownerAdmin: { user: string }
|
|
59
|
+
exp: number
|
|
60
|
+
}
|
|
61
|
+
export interface DelegatedTokenScope {
|
|
62
|
+
version: 2
|
|
63
|
+
delegation: {
|
|
64
|
+
repo: string
|
|
65
|
+
kind: 'build-runner' | 'build-phase' | 'harvest-runner' | 'harvest-phase'
|
|
66
|
+
id: string
|
|
67
|
+
anchor: number
|
|
68
|
+
session?: string
|
|
69
|
+
execution?: string
|
|
70
|
+
leaseHolder?: string
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
export type TokenScope =
|
|
75
|
+
| DelegatedTokenScope
|
|
76
|
+
| InternalOperatorTokenScope
|
|
77
|
+
| AdminTokenScope
|
|
57
78
|
| LegacyBuildTokenScope
|
|
58
79
|
| ResourceTokenScope
|
|
59
80
|
| OperatorTokenScope
|
|
@@ -63,7 +84,38 @@ const commonScope = {
|
|
|
63
84
|
session: z.string().min(1),
|
|
64
85
|
exp: z.number().int(),
|
|
65
86
|
}
|
|
87
|
+
const exactIdentity = z
|
|
88
|
+
.string()
|
|
89
|
+
.min(1)
|
|
90
|
+
.refine((value) => value !== '*')
|
|
91
|
+
const delegatedClaims = z
|
|
92
|
+
.strictObject({
|
|
93
|
+
repo: exactIdentity,
|
|
94
|
+
kind: z.enum(['build-runner', 'build-phase', 'harvest-runner', 'harvest-phase']),
|
|
95
|
+
id: exactIdentity,
|
|
96
|
+
anchor: z.number().int().positive(),
|
|
97
|
+
session: exactIdentity.optional(),
|
|
98
|
+
execution: exactIdentity.optional(),
|
|
99
|
+
leaseHolder: exactIdentity.optional(),
|
|
100
|
+
})
|
|
101
|
+
.refine(
|
|
102
|
+
(claims) =>
|
|
103
|
+
claims.kind.endsWith('-phase') === (claims.session !== undefined) &&
|
|
104
|
+
claims.kind.startsWith('harvest') === (claims.execution !== undefined),
|
|
105
|
+
)
|
|
66
106
|
const tokenScopeSchema = z.union([
|
|
107
|
+
z.strictObject({ version: z.literal(2), delegation: delegatedClaims }),
|
|
108
|
+
z.strictObject({
|
|
109
|
+
version: z.literal(1),
|
|
110
|
+
internalOperator: z.strictObject({ user: z.string().trim().min(1), repo: z.string().min(1) }),
|
|
111
|
+
exp: z.number().int(),
|
|
112
|
+
via: viaSchema.optional(),
|
|
113
|
+
}),
|
|
114
|
+
z.strictObject({
|
|
115
|
+
version: z.literal(1),
|
|
116
|
+
ownerAdmin: z.strictObject({ user: z.string().trim().min(1) }),
|
|
117
|
+
exp: z.number().int(),
|
|
118
|
+
}),
|
|
67
119
|
z.strictObject({ build: z.string().min(1), ...commonScope }),
|
|
68
120
|
z.strictObject({
|
|
69
121
|
resource: z.strictObject({
|
|
@@ -85,6 +137,13 @@ export function tokenResource(scope: TokenScope): {
|
|
|
85
137
|
kind: 'build' | 'repo' | 'session' | 'admin' | 'operator' | 'deployment'
|
|
86
138
|
id: string
|
|
87
139
|
} {
|
|
140
|
+
if ('delegation' in scope)
|
|
141
|
+
return {
|
|
142
|
+
kind: scope.delegation.kind.startsWith('build') ? 'build' : 'repo',
|
|
143
|
+
id: scope.delegation.kind.startsWith('build') ? scope.delegation.id : scope.delegation.repo,
|
|
144
|
+
}
|
|
145
|
+
if ('ownerAdmin' in scope) return { kind: 'admin', id: '*' }
|
|
146
|
+
if ('internalOperator' in scope) return { kind: 'operator', id: scope.internalOperator.user }
|
|
88
147
|
if ('operator' in scope) {
|
|
89
148
|
return scope.operator === true
|
|
90
149
|
? { kind: 'deployment', id: '*' }
|
|
@@ -108,7 +167,9 @@ export function mintToken(secret: string, scope: TokenScope): string {
|
|
|
108
167
|
/**
|
|
109
168
|
* Bad signature, malformed token, unparseable scope, or expired → null —
|
|
110
169
|
* verification never explains itself to the caller (the server maps null to
|
|
111
|
-
* 401).
|
|
170
|
+
* 401). Version-2 grants deliberately have no fixed expiration: the service
|
|
171
|
+
* must additionally validate their registered project and durable lifecycle.
|
|
172
|
+
* The signature compare is timing-safe via `crypto.timingSafeEqual`.
|
|
112
173
|
*/
|
|
113
174
|
export function verifyToken(secret: string, token: string, now: Date): TokenScope | null {
|
|
114
175
|
const parts = token.split('.')
|
|
@@ -132,6 +193,6 @@ export function verifyToken(secret: string, token: string, now: Date): TokenScop
|
|
|
132
193
|
}
|
|
133
194
|
const scope = tokenScopeSchema.safeParse(raw)
|
|
134
195
|
if (!scope.success) return null
|
|
135
|
-
if (scope.data.exp <= now.getTime()) return null
|
|
196
|
+
if ('exp' in scope.data && scope.data.exp <= now.getTime()) return null
|
|
136
197
|
return scope.data
|
|
137
198
|
}
|
|
@@ -57,3 +57,10 @@ export {
|
|
|
57
57
|
type E2eHarness,
|
|
58
58
|
type SkillHandlers,
|
|
59
59
|
} from '../integration/harness'
|
|
60
|
+
|
|
61
|
+
// Harvest lifecycle and CLI seams for delegated-credential integration tests.
|
|
62
|
+
export { HarvestRunner } from '../processes/harvest-runner'
|
|
63
|
+
export { resolveHarvestCliEnv } from '../cli/env'
|
|
64
|
+
export { buildHarvestContext, submitHarvestProposals, submitHarvestVerdict } from '../cli/harvest'
|
|
65
|
+
export { abConnect, abDisconnect } from '../cli/connect'
|
|
66
|
+
export { ProjectCredentials } from '../cli/project-credentials'
|
package/skills/guide/SKILL.md
CHANGED
|
@@ -283,6 +283,19 @@ through the strict versioned manifest and environment-only secrets, to the
|
|
|
283
283
|
unchanged shared contract suite and optional npm publication—read
|
|
284
284
|
[`references/plugin-authoring.md`](references/plugin-authoring.md).
|
|
285
285
|
|
|
286
|
+
### `[store]`
|
|
287
|
+
|
|
288
|
+
| Key | Required | Values / purpose |
|
|
289
|
+
|---|---|---|
|
|
290
|
+
| `url` | When `[store]` exists | Absolute HTTPS service base URL; loopback HTTP is allowed for development. No credentials, query or fragment. Trailing slashes are normalized. |
|
|
291
|
+
|
|
292
|
+
Selection is explicit `--store` > nonblank `AB_STORE` > `[store] url` > the main
|
|
293
|
+
checkout's `.autobuild/`. A configured service without a user credential fails
|
|
294
|
+
with `ab connect` guidance. `AB_TOKEN` overrides the saved service/project
|
|
295
|
+
credential. See [repository setup](references/setup.md#connect-a-project).
|
|
296
|
+
Changing the store requires restarting dispatch.
|
|
297
|
+
|
|
298
|
+
|
|
286
299
|
### `[pr]`
|
|
287
300
|
|
|
288
301
|
Optional PR-projection settings. Omission leaves artifact attachments text-only.
|
|
@@ -408,6 +421,28 @@ manager.
|
|
|
408
421
|
|
|
409
422
|
### `[verify]`
|
|
410
423
|
|
|
424
|
+
Post-reconcile verification defaults to enabled: `[verify].afterReconcile = true`.
|
|
425
|
+
Set it to `false` to retain pre-reconcile verification and skip all steps after
|
|
426
|
+
reconciliation. An optional boolean `[verify.<step>].afterReconcile` overrides
|
|
427
|
+
the repository setting in either direction; overrides require a configured step.
|
|
428
|
+
For example, set `afterReconcile = false` in `[verify]` and
|
|
429
|
+
`afterReconcile = true` in `[verify.types]` to keep only that step re-running.
|
|
430
|
+
|
|
431
|
+
The kernel freezes the resolved ordered policy in
|
|
432
|
+
`verify.reconcile-policy-captured` at the next engine action after reconcile
|
|
433
|
+
completion. Live configuration changes apply at that boundary; a cycle already
|
|
434
|
+
started keeps its policy through recovery. Historical cycles already started
|
|
435
|
+
without a captured policy retain the rerun-all behavior. Initial verification
|
|
436
|
+
and verification after subsequent code review always use ordinary applicability.
|
|
437
|
+
Policy exclusion precedes plan selection and paths, including `always = true`.
|
|
438
|
+
Enabled steps still follow plan selection and path rules. Excluded steps record
|
|
439
|
+
`verify.completed` with outcome `skipped`, `skipCause = "post-reconcile-policy"`,
|
|
440
|
+
and a reason naming the setting. Dashboards and PR summaries call this
|
|
441
|
+
"skipped by post-reconcile policy"; it is never a pass. Forge-side required
|
|
442
|
+
checks and branch protections still gate merge. For excluded steps, merged code
|
|
443
|
+
is not re-verified, so breakage that only exists in the combination can land.
|
|
444
|
+
|
|
445
|
+
|
|
411
446
|
`steps` orders the verify phases; each entry `"<step>"` becomes a
|
|
412
447
|
`verify:<step>` phase and needs its own `[verify.<step>]` subtable. Those
|
|
413
448
|
subtables are part of this section — their fields are listed here.
|
|
@@ -419,6 +454,7 @@ subtables are part of this section — their fields are listed here.
|
|
|
419
454
|
| `command` | — | **required when `kind = "check"`**, nonempty string | Ref into `[commands]` — the key, not a shell string. Pass/fail is the command's exit status. |
|
|
420
455
|
| `skill` | — | **required when `kind = "agent"`**, nonempty string | Installed skill name to run (e.g. a repository-authored `"verify-app-e2e"`). |
|
|
421
456
|
| `paths` | — | optional nonempty array of positive repository-relative globs | Makes either step kind apply when any changed path matches any selector. Omitted means unconditional. |
|
|
457
|
+
| `afterReconcile` | repository: `true`; step: inherited | boolean | Step override wins over `[verify].afterReconcile`; false skips this step after reconciliation only. |
|
|
422
458
|
| `always` | — | optional boolean | `true` makes the step unconditional and mandatory (a plan cannot deselect it); `false` is equivalent to omission. |
|
|
423
459
|
|
|
424
460
|
A plan may begin with strict TOML front matter selecting the complete set of
|
|
@@ -488,7 +524,7 @@ A selected step's `paths` are evaluated immediately before the step against
|
|
|
488
524
|
current `HEAD`, relative to the initial branch-cut SHA or the refreshed base
|
|
489
525
|
from the latest completed reconcile. It uses a NUL-delimited, no-rename Git diff, so
|
|
490
526
|
adds/modifications/deletions and both rename sides participate. A reconcile
|
|
491
|
-
starts a fresh cycle and evaluation repeats; upstream-only merged paths are
|
|
527
|
+
starts a fresh cycle and evaluation repeats for policy-enabled steps; upstream-only merged paths are
|
|
492
528
|
excluded while build-owned resolutions remain visible. A miss starts no
|
|
493
529
|
command, agent, or session and records `skipped` with
|
|
494
530
|
`excluded by [verify.<step>].paths: no changed path matched <JSON paths>`.
|
|
@@ -688,6 +724,7 @@ nonnegative integer so zero can disable that trigger.
|
|
|
688
724
|
| `sessionBudgetSeconds` | `3600` | positive integer | Wall-clock bound for each build agent session unless its logical role overrides it. |
|
|
689
725
|
| `stallRounds` | `3` | positive integer | The same finding surviving this many review rounds auto-escalates to a human — the anti-loop guard. |
|
|
690
726
|
| `maxVerifyAttempts` | `3` | positive integer | Caps the `verify → implement → verify` cycle before escalation. |
|
|
727
|
+
| `maxDispatchAttempts` | `3` | positive integer | Caps consecutive dispatch failures before a setup-targeted `dispatch-failure-limit` escalation. |
|
|
691
728
|
| `maxSetupAttempts` | `3` | positive integer | Caps consecutive `[commands].setup` failures before a setup-targeted human escalation. |
|
|
692
729
|
| `maxInfrastructureAttempts` | `3` | positive integer | Caps consecutive remote provider lifecycle failures before a setup-targeted human escalation; a `retry` answer re-arms the budget. |
|
|
693
730
|
| `maxReconcileAttempts` | `3` | positive integer | Caps completed reconciles that leave the PR conflicted against an unchanged authoritative base. Moving-base races do not consume the budget. |
|
|
@@ -695,6 +732,11 @@ nonnegative integer so zero can disable that trigger.
|
|
|
695
732
|
| `harvestThreshold` | `5` | positive integer | Newly unclaimed `observation.recorded` occurrences required to start one repository harvest run. |
|
|
696
733
|
| `harvestMaxDrift` | `3` | nonnegative integer | Other builds merged after the oldest unclaimed observation required to start a run; `0` disables drift. |
|
|
697
734
|
|
|
735
|
+
Dispatch failures share one budget across stages until runner attachment. At
|
|
736
|
+
exhaustion dispatch parks durably; bare `ab answer <slug>` and text guidance
|
|
737
|
+
both grant a fresh budget, while abort terminates without retrying. Workspace
|
|
738
|
+
failures owned by provider infrastructure recovery use only its existing budget.
|
|
739
|
+
|
|
698
740
|
For each repeat conflict, Autobuild compares the base merged by the latest
|
|
699
741
|
completed reconcile with a fresh authoritative base snapshot. An advanced base
|
|
700
742
|
proves the attempt lost a race and permits another reconcile regardless of the
|
|
@@ -712,7 +754,7 @@ roundless no-progress answer. An already exhausted no-progress budget raises its
|
|
|
712
754
|
matching cause separately before another reconcile. Cause-less historical
|
|
713
755
|
raises retain their former round-shaped reset behavior, and roundless
|
|
714
756
|
policy/reconcile raises retain their former no-progress meaning, on replay
|
|
715
|
-
without migration. Post-reconcile verification
|
|
757
|
+
without migration. Post-reconcile verification follows the captured policy and remains
|
|
716
758
|
independently bounded by `maxVerifyAttempts`.
|
|
717
759
|
|
|
718
760
|
`stallRounds` counts *persistence chains*, which reviewers mark and the kernel
|
|
@@ -748,10 +790,11 @@ state eligible.
|
|
|
748
790
|
| `readyLabels` | — (source-aware) | optional; array of nonempty strings | A ticket must carry **every** listed label to be dispatchable. `[]` = **no label gate**. Absent uses the source default below. |
|
|
749
791
|
| `readyState` | — | **required**, non-blank string | The one workflow state a ticket must sit in to be dispatchable. Linear matches exactly and case-sensitively; file matches it case-insensitively to a state directory (`ready` → `ready/`). There is no any-state mode. |
|
|
750
792
|
| `readyAssignee` | — | optional; `"me"` or an email; `linear` only | Also require the ticket to be assigned to that Linear user (`me` = owner of `LINEAR_API_KEY`); unassigned tickets are never ready. |
|
|
751
|
-
| `teamKey` | — | required by `linear`, forbidden by `file`, allowed for plugins | The Linear team key (e.g. `"ENG"`) or an existing plugin configuration field. |
|
|
793
|
+
| `teamKey` | — | required by `linear` and `hosted`, forbidden by `file`, allowed for plugins | The Linear team key (e.g. `"ENG"`) or an existing plugin configuration field. |
|
|
752
794
|
| `claimedState` | — | optional nonempty string; forbidden by `file`, allowed for plugins | Workflow state `claim()` moves a ticket to when a build starts. |
|
|
795
|
+
| `doneState` | hosted: `"Done"` | optional nonblank string | Project completion state and merged-ticket transition; hosted tickets only. |
|
|
753
796
|
| `createState` | — | optional, nonempty string | Default state for new tickets when a create does not name one. Absent = the provider's default (Linear: the team's default, e.g. Backlog; file: Triage). |
|
|
754
|
-
| `triageState` | — | optional, nonempty string | State the dispatcher hands tickets back to for human triage — spec-gate bounces, aborted builds, closed-unmerged PRs. Absent = Linear: Backlog; file/plugin: Triage. Must name a state the tracker actually has — a Linear team only has "Triage" when its triage feature is enabled. |
|
|
797
|
+
| `triageState` | — | optional, nonempty string | State the dispatcher hands tickets back to for human triage — spec-gate bounces, aborted builds, closed-unmerged PRs. Absent = Linear/hosted: Backlog; file/plugin: Triage. Must name a state the tracker actually has — a Linear team only has "Triage" when its triage feature is enabled. |
|
|
755
798
|
| `proposalState` | — | optional, nonempty string | State observation harvest files approved proposals into. Absent = the resolved `triageState`, which keeps the human grooming gate. Setting it to `readyState` waives that gate: harvested proposals dispatch without being read. Keep it distinct from `triageState` — see below. |
|
|
756
799
|
| `dir` | file: `.autobuild/tickets`; plugin: — | optional nonempty string; forbidden by `linear`, allowed for plugins | Root holding file state directories, or an existing plugin configuration field. |
|
|
757
800
|
|
|
@@ -829,7 +872,7 @@ changing it requires a dispatcher restart.
|
|
|
829
872
|
| `model` | — (required when `enabled = true`) | nonblank provider-qualified string, same vocabulary as `[roles]` | The turn runner's model, resolved through the deployment's gateway credential; a leading `vercel-ai-gateway/` prefix is accepted and stripped. |
|
|
830
873
|
| `invocationBudgetSeconds` | `240` | positive integer, clamped to 300 | Wall-clock budget for one turn invocation. A larger value is clamped — not rejected — to the 300-second hosted function limit the runner executes under. |
|
|
831
874
|
| `approvals` | `["builds.control:abort", "builds.control:discard", "builds.answer:revise-spec", "sandbox.publish", "tickets.move:ready"]` | array of `tool` or `tool:qualifier` strings | Registry tool calls that suspend a turn until the operator answers. Empty (`[]`) means none — autonomous operation. An entry naming an absent or unknown tool is **inert by design**, never an error. A qualified entry applies to one discriminator value only (`tickets.move:ready` approves moving a ticket to `ready`); for a tool without a discriminator, only the bare form matches. |
|
|
832
|
-
| `wake` | inherits the default | array of event-type globs | The attention events a **new** session wakes for. Absent inherits the
|
|
875
|
+
| `wake` | inherits the default | array of event-type globs | The attention events a **new** session wakes for. Absent inherits the legacy hosted wake event set, independent of CLI watch defaults; an explicit `[]` means never wake — message-only. Each glob must match at least one build event type. Existing sessions never retroactively inherit; an explicit wake update always wins. |
|
|
833
876
|
| `sandbox` | — | strict subtable | The sandbox behavior knobs; absence reads as the defaults below. |
|
|
834
877
|
|
|
835
878
|
`[orchestrator.sandbox]` fields:
|
|
@@ -1049,7 +1092,7 @@ There is no home-directory fallback: a repository with no state tree starts
|
|
|
1049
1092
|
empty.
|
|
1050
1093
|
|
|
1051
1094
|
Every command uses the same precedence: explicit `--store <ref>` > nonempty
|
|
1052
|
-
`AB_STORE` > `<main-repo>/.autobuild`. Relative local overrides are normalized
|
|
1095
|
+
`AB_STORE` > `[store] url` > `<main-repo>/.autobuild`. Relative local overrides are normalized
|
|
1053
1096
|
against the main checkout. A local override relocates the whole state tree,
|
|
1054
1097
|
including `worktrees/` and default file tickets; an explicit `[tickets].dir`
|
|
1055
1098
|
remains repo-relative. An HTTP(S) URL still selects the remote store unchanged,
|
|
@@ -1057,6 +1100,46 @@ while worktrees and default file tickets remain under the repository-default
|
|
|
1057
1100
|
`.autobuild/` directory. The dispatcher passes its normalized selection to
|
|
1058
1101
|
every agent session as `AB_STORE`.
|
|
1059
1102
|
|
|
1103
|
+
## Connect a project
|
|
1104
|
+
|
|
1105
|
+
From a checkout with an origin remote, run `ab connect https://YOUR_ORIGIN`.
|
|
1106
|
+
The CLI prints a verification URL and short code and attempts to open a browser.
|
|
1107
|
+
Sign in as an allowed owner, check the full repository identity and code, and
|
|
1108
|
+
approve or deny. Another device can open the printed URL, or visit `/connect`
|
|
1109
|
+
and enter the code. Approval requires the web-enabled service origin; a
|
|
1110
|
+
standalone machine-only listener cannot present browser consent. Requests
|
|
1111
|
+
expire after ten minutes. Denial and timeout leave local configuration unchanged.
|
|
1112
|
+
|
|
1113
|
+
On approval the service registers the project and issues a project credential.
|
|
1114
|
+
The CLI keeps it in `$XDG_CONFIG_HOME/autobuild/credentials` (or
|
|
1115
|
+
`~/.config/autobuild/credentials`), keyed by normalized service and origin,
|
|
1116
|
+
with directory mode 0700 and file mode 0600. Credentials never belong in the
|
|
1117
|
+
repository. It adds the non-secret `[store] url` to `autobuild.toml` when
|
|
1118
|
+
absent; review and commit that change yourself. An existing different URL
|
|
1119
|
+
must be changed explicitly before connecting. Reconnecting replaces the saved
|
|
1120
|
+
credential only after approval and revokes the previous credential.
|
|
1121
|
+
|
|
1122
|
+
All worktrees share the main checkout's configuration and the user's saved
|
|
1123
|
+
credential. A service manager running as that user uses the same HOME and
|
|
1124
|
+
XDG_CONFIG_HOME, with no AB_STORE or AB_TOKEN exports. A fresh clone with
|
|
1125
|
+
committed store configuration but no saved credential fails with `ab connect`
|
|
1126
|
+
guidance; it never silently creates local state. Run connect on each host/user.
|
|
1127
|
+
|
|
1128
|
+
`ab connect` without a URL reports service, project, and credential status:
|
|
1129
|
+
accepted, rejected, missing, or unreachable. `ab disconnect` revokes the saved
|
|
1130
|
+
credential remotely before removing it locally, keeping the project registered,
|
|
1131
|
+
its data, and store configuration. A network/server failure retains the local
|
|
1132
|
+
entry for retry; an already revoked credential can be removed. If saving a new
|
|
1133
|
+
connection fails, the CLI cancels/revokes it and retains the previous entry.
|
|
1134
|
+
If remote cleanup cannot be confirmed, check the printed credential id with
|
|
1135
|
+
the owner's administrative tools before retrying.
|
|
1136
|
+
|
|
1137
|
+
Store selection is `--store` > nonblank `AB_STORE` > `[store] url` > the
|
|
1138
|
+
main checkout's `.autobuild/`. `AB_TOKEN` overrides the exact service/project's
|
|
1139
|
+
saved credential for CI and other non-interactive hosts. A service override
|
|
1140
|
+
never borrows another service's credential. Build and harvest sessions keep
|
|
1141
|
+
their explicit scoped authority.
|
|
1142
|
+
|
|
1060
1143
|
## Source-agnostic ticket operations
|
|
1061
1144
|
|
|
1062
1145
|
The sessionless `ab ticket` namespace constructs whichever TicketSource the
|
|
@@ -1111,7 +1194,7 @@ to a ticket that has already been claimed into a build does not stop that build.
|
|
|
1111
1194
|
For dependent tickets, follow the
|
|
1112
1195
|
[safe blocker-filing workflow](references/ticket-dependencies.md): determine the
|
|
1113
1196
|
intended final destination, stage outside ready, verify all blocker relationships,
|
|
1114
|
-
and move to ready last. Direct
|
|
1197
|
+
and move to ready last. Direct Linear creates the issue and its blockers
|
|
1115
1198
|
in separate requests; one `create --blocked-by` call is not atomic publication.
|
|
1116
1199
|
Harvest's creation reservations cover Harvest only, not interactive CLI creates.
|
|
1117
1200
|
Dependency order and JSON id extraction are still required, but do not alone
|
|
@@ -1173,7 +1256,7 @@ judge against it.
|
|
|
1173
1256
|
is the read-only binary retrieval form. Operators need no session identity: it
|
|
1174
1257
|
works after a build is terminal, writes exact bytes (creating parent
|
|
1175
1258
|
directories), verifies that the build belongs to this repository, and uses the
|
|
1176
|
-
standard explicit `--store` > nonblank `AB_STORE` > repository-local selection.
|
|
1259
|
+
standard explicit `--store` > nonblank `AB_STORE` > `[store] url` > repository-local selection.
|
|
1177
1260
|
Inside a phase, a complete ambient tuple permits only the ambient build; a
|
|
1178
1261
|
foreign build or malformed/partial identity is rejected. Use the exact pinned
|
|
1179
1262
|
`@rev` from a PR attachment command.
|
|
@@ -1507,7 +1590,7 @@ Every command requires the target to exist in this repository and be active
|
|
|
1507
1590
|
`ab answer` additionally requires an open escalation. A stale, missing, done,
|
|
1508
1591
|
aborted, or otherwise inapplicable target is an error and gets no new event. Attribution uses `USER`, then `USERNAME`, then the
|
|
1509
1592
|
same stable fallback on both surfaces. All five commands run sessionless and
|
|
1510
|
-
accept `--store <ref>` with the usual explicit flag > `AB_STORE` > repository
|
|
1593
|
+
accept `--store <ref>` with the usual explicit flag > `AB_STORE` > `[store] url` > repository
|
|
1511
1594
|
local precedence. If nonblank `AB_SESSION` and matching `AB_BUILD` identify the
|
|
1512
1595
|
caller's own phase build, the command refuses it; a phase cannot pause, resume,
|
|
1513
1596
|
auto-merge, answer, or abort itself. `ab pause --all` and `ab resume --all`
|
|
@@ -1553,7 +1636,7 @@ repository-wide pause (`paused`, default `false`), and the claim-time auto-merge
|
|
|
1553
1636
|
default (`defaultAutoMerge`, default `false`). Those defaults also describe a
|
|
1554
1637
|
repository with no journal row; the read does not create one. Human output is
|
|
1555
1638
|
the default. `--json` emits one bare object with those exact fields plus the
|
|
1556
|
-
canonical `repo`, and `--store` follows the usual explicit flag > `AB_STORE` >
|
|
1639
|
+
canonical `repo`, and `--store` follows the usual explicit flag > `AB_STORE` > `[store] url` >
|
|
1557
1640
|
repository-local precedence. The command uses the same reducer as the
|
|
1558
1641
|
dispatcher and dashboard, writes no state, claims no ticket, attaches no runner,
|
|
1559
1642
|
and starts no dispatcher work.
|
|
@@ -1636,19 +1719,51 @@ streams matching events as they land instead of answering once. With no slug it
|
|
|
1636
1719
|
follows every nonterminal build in the repository — a build created while the
|
|
1637
1720
|
watch runs joins it without a restart — and naming one or more slugs watches
|
|
1638
1721
|
exactly those builds, ending the watch when all of them reach a terminal status.
|
|
1639
|
-
|
|
1640
|
-
repository
|
|
1641
|
-
|
|
1642
|
-
The default
|
|
1643
|
-
|
|
1644
|
-
`
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
`harvest.escalated`, `harvest.
|
|
1648
|
-
`dispatcher.config-rejected`, `dispatcher.
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1722
|
+
With no slugs the repository journal is included; named watches add it with
|
|
1723
|
+
`--repository`.
|
|
1724
|
+
|
|
1725
|
+
The default watch reports actionable conditions only:
|
|
1726
|
+
|
|
1727
|
+
- Build `escalation.raised`, or `build.merge-decision`: a finalized open PR
|
|
1728
|
+
with auto-merge off or a current actionable merge-gate deferral. Armed consent
|
|
1729
|
+
awaiting application is silent. One merge decision is reported per build/PR.
|
|
1730
|
+
- Repository `harvest.escalated`, `harvest.recovery-exhausted`,
|
|
1731
|
+
`dispatcher.config-rejected`, `dispatcher.config-reloaded` with nonempty
|
|
1732
|
+
`restartRequired`, abnormal `dispatcher.run-stopped`, and `dispatcher.lost`.
|
|
1733
|
+
- `dispatcher.ticket-source-persistent`: degradation past its advertised
|
|
1734
|
+
reset time, once a tick started at or after reset has completed or failed
|
|
1735
|
+
with the episode still open; crossing reset alone is silent. Without a reset,
|
|
1736
|
+
the deadline is 15 minutes after onset. Repeated evidence never postpones
|
|
1737
|
+
the deadline; recovery ends the episode.
|
|
1738
|
+
- `dispatcher.tick-failure-persistent`: continuously failing completed ticks
|
|
1739
|
+
for five minutes in one run. A successful tick, new run, or matching stop
|
|
1740
|
+
ends the episode. Source grace and cooldown-driven failures do not start
|
|
1741
|
+
this clock; after grace, the first qualifying failure starts a new five-minute
|
|
1742
|
+
interval. Each persistence episode is reported once.
|
|
1743
|
+
|
|
1744
|
+
Repository conditions are included with no slugs or with `--repository`.
|
|
1745
|
+
Routine outcomes and self-healing failures are silent. Repeatable
|
|
1746
|
+
`--event <glob>` replaces the default and selects exactly matching catalog
|
|
1747
|
+
events (plus the existing `dispatcher.lost` observation); use explicit globs
|
|
1748
|
+
to opt into failures, finalize, merge, or completion events.
|
|
1749
|
+
|
|
1750
|
+
Derived records use `observation` instead of `event`, without inventing a
|
|
1751
|
+
durable sequence or actor. Merge observations carry `type`, `pr`, `reason`,
|
|
1752
|
+
and `observedAt`, plus the build slug and normal reduced state. Persistence
|
|
1753
|
+
observations carry source/cause/message/resetAt when advertised (or
|
|
1754
|
+
run/firstFailure/error), `episodeSeq`, and `observedAt`; repository records
|
|
1755
|
+
have `repo` and null build/state. Source observations also report
|
|
1756
|
+
`dependencyLimited`.
|
|
1757
|
+
|
|
1758
|
+
Every record carries a cursor, including timer-only records. Resuming suppresses
|
|
1759
|
+
delivered merge decisions and persistence episodes even when stream positions
|
|
1760
|
+
did not advance. Old v1 cursors remain valid: historical eligible merge
|
|
1761
|
+
decisions are treated as consumed; active overdue repository episodes may
|
|
1762
|
+
report because old watches had no persistence notification. Fresh attachment
|
|
1763
|
+
silently baselines historical direct events and merge decisions, but retains
|
|
1764
|
+
active repository timers. Reconcile existing conditions through status after
|
|
1765
|
+
arming the watch. Named watches still end when every named build is terminal;
|
|
1766
|
+
learn their outcomes from status reconciliation at exit.
|
|
1652
1767
|
|
|
1653
1768
|
With `--json`, stdout is newline-delimited JSON — exactly one object per
|
|
1654
1769
|
matching event, flushed as soon as it is written — with fields `build` (slug,
|
|
@@ -1745,7 +1860,7 @@ vocabulary is closed: `blocked`, `paused`, `running`, `done`, `aborted`,
|
|
|
1745
1860
|
`terminal` (done or aborted), `pr=open`, `pr=merged`, `pr=closed`,
|
|
1746
1861
|
`pr=conflicted`, `phase=<phase>` (a core phase — `plan`, `plan-review`,
|
|
1747
1862
|
`implement`, `code-review`, `finalize`, `reconcile` — or `verify:<step>`), and
|
|
1748
|
-
`attention` (
|
|
1863
|
+
`attention` (an open escalation or a build waiting for a merge decision). Repeatable `--event`
|
|
1749
1864
|
globs follow the `ab watch` rules. Multiple values are alternatives: the first
|
|
1750
1865
|
one satisfied ends the wait — all `--for` values in supply order, then all
|
|
1751
1866
|
globs in supply order — and the record names what matched.
|
|
@@ -1756,8 +1871,11 @@ means an open escalation exactly as everywhere else. In the manner of `kubectl
|
|
|
1756
1871
|
wait`, a condition that already holds when the command starts returns
|
|
1757
1872
|
immediately with that build's latest event (`--since` does not suppress this);
|
|
1758
1873
|
otherwise the first later event whose reduction produces it ends the wait.
|
|
1759
|
-
`attention`
|
|
1760
|
-
|
|
1874
|
+
`attention` is also a current-state condition: an already open escalation or
|
|
1875
|
+
merge decision returns immediately, including with `--since`. Merge matches
|
|
1876
|
+
use the same observation as watch. During replay, only new escalation events
|
|
1877
|
+
or undelivered eligible merge decisions satisfy attention. `--event` conditions
|
|
1878
|
+
fire only on new events or a cursor-resumed backlog.
|
|
1761
1879
|
|
|
1762
1880
|
The record is the `ab watch` record plus a `condition` field naming the
|
|
1763
1881
|
satisfied `--for` value or `--event` glob. Unlike `ab watch`, there is no
|