@defrex/autobuild 0.11.0 → 0.13.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 +335 -31
- package/docs/architecture.md +125 -7
- package/docs/configuration.md +142 -21
- package/docs/hosted-dispatcher.md +17 -2
- package/docs/hosted-service.md +240 -0
- package/docs/mcp.md +8 -2
- package/docs/operator-api.md +97 -7
- package/docs/remote-store-protocol.md +199 -144
- package/docs/remote-ticket-protocol.md +249 -19
- package/docs/setup.md +199 -16
- package/docs/ticket-dependencies.md +1 -1
- package/docs/ticket-migration.md +64 -0
- package/package.json +7 -6
- package/packages/core/src/cli/attention.ts +209 -0
- package/packages/core/src/cli/build-control.ts +12 -1
- package/packages/core/src/cli/configured-store.ts +17 -0
- package/packages/core/src/cli/connect.ts +364 -0
- package/packages/core/src/cli/context.ts +7 -1
- package/packages/core/src/cli/dashboard/model.ts +4 -5
- package/packages/core/src/cli/dashboard/poll.ts +1 -1
- package/packages/core/src/cli/dispatch.ts +336 -41
- package/packages/core/src/cli/harvest.ts +13 -3
- package/packages/core/src/cli/help.ts +51 -16
- package/packages/core/src/cli/init-validation.ts +27 -2
- package/packages/core/src/cli/installation.ts +67 -13
- package/packages/core/src/cli/main.ts +60 -16
- package/packages/core/src/cli/mcp.ts +9 -2
- 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 +15 -1
- 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-assets.ts +7 -2
- package/packages/core/src/cli/ticket-editor.ts +82 -0
- package/packages/core/src/cli/ticket.ts +172 -35
- 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 +90 -11
- package/packages/core/src/connection.ts +86 -0
- package/packages/core/src/events/catalog.ts +11 -2
- package/packages/core/src/events/payloads.ts +50 -0
- package/packages/core/src/events/repository.ts +13 -0
- package/packages/core/src/integration/harness.ts +37 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +145 -83
- package/packages/core/src/kernel/engine.ts +24 -0
- package/packages/core/src/kernel/generators/build-log.ts +11 -1
- package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
- 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 +10 -0
- package/packages/core/src/kernel/reducer.ts +8 -0
- package/packages/core/src/ontology.ts +2 -0
- package/packages/core/src/operator/annotations.ts +12 -0
- package/packages/core/src/operator/index.ts +12 -0
- package/packages/core/src/operator/protocol.ts +19 -2
- package/packages/core/src/operator/query.ts +38 -16
- package/packages/core/src/operator/registry.ts +61 -8
- package/packages/core/src/operator/ticket-source-backend.ts +17 -20
- package/packages/core/src/operator/tickets.ts +136 -28
- package/packages/core/src/plugin-sdk/index.ts +7 -0
- package/packages/core/src/plugins/manifest.ts +5 -3
- package/packages/core/src/plugins/runtimes.ts +7 -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 +3 -0
- package/packages/core/src/ports/tickets/contract.ts +66 -1
- package/packages/core/src/ports/tickets/create.ts +54 -10
- package/packages/core/src/ports/tickets/fake.ts +30 -1
- package/packages/core/src/ports/tickets/file-assets.ts +159 -0
- package/packages/core/src/ports/tickets/file-codec.ts +90 -0
- package/packages/core/src/ports/tickets/file.ts +114 -120
- package/packages/core/src/ports/tickets/grants.ts +47 -0
- package/packages/core/src/ports/tickets/hosted-index.ts +17 -3
- package/packages/core/src/ports/tickets/identity.ts +20 -0
- package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
- package/packages/core/src/ports/tickets/linear.ts +74 -9
- package/packages/core/src/ports/tickets/migration.ts +336 -0
- package/packages/core/src/ports/tickets/operations.ts +38 -7
- package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +96 -0
- package/packages/core/src/ports/tickets/remote.ts +61 -3
- package/packages/core/src/ports/tickets/requested-id.ts +46 -0
- package/packages/core/src/ports/tickets/revision.ts +62 -0
- package/packages/core/src/ports/tickets/states.ts +61 -0
- package/packages/core/src/ports/types.ts +40 -3
- package/packages/core/src/ports/workspace/build-execution.ts +6 -1
- package/packages/core/src/ports/workspace/create.ts +15 -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/operator-sandbox.ts +2 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +51 -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 +168 -41
- package/packages/core/src/processes/execution-no-progress.ts +176 -0
- package/packages/core/src/processes/harvest-child.ts +31 -4
- package/packages/core/src/processes/harvest-execution-state.ts +19 -2
- package/packages/core/src/processes/harvest-runner.ts +166 -104
- package/packages/core/src/processes/harvest.ts +34 -14
- package/packages/core/src/processes/phase-failure-budget.ts +2 -0
- package/packages/core/src/processes/publication-loss.ts +18 -1
- package/packages/core/src/processes/repo-view.ts +35 -0
- package/packages/core/src/store/contract.ts +32 -1
- package/packages/core/src/store/local/store.ts +1 -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/store/ticket-asset-snapshots.ts +192 -0
- package/packages/core/src/store/ticket-assets.ts +86 -8
- package/packages/core/src/store/types.ts +1 -1
- package/packages/core/src/testing/index.ts +7 -0
- package/skills/guide/SKILL.md +232 -34
- package/skills/guide/references/operator-api.md +142 -13
- package/skills/guide/references/plugin-authoring.md +10 -2
- package/skills/guide/references/remote-store-protocol.md +199 -144
- package/skills/guide/references/remote-ticket-protocol.md +253 -18
- package/skills/guide/references/setup.md +199 -16
- package/skills/guide/references/ticket-dependencies.md +1 -1
- package/skills/guide/references/ticket-migration.md +64 -0
- package/skills/operate/SKILL.md +126 -36
- package/skills/tickets/SKILL.md +71 -0
- package/templates/hosted-service/package.json +13 -0
- package/templates/hosted-service/vercel.json +7 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { validateTicketAssetInput, type TicketAssetInput } from '../../store/ticket-assets'
|
|
3
|
+
|
|
4
|
+
export const portableAssetSchema = z
|
|
5
|
+
.strictObject({
|
|
6
|
+
kind: z.string(),
|
|
7
|
+
name: z.string(),
|
|
8
|
+
layout: z.enum(['file', 'tree']),
|
|
9
|
+
entries: z.array(
|
|
10
|
+
z.discriminatedUnion('type', [
|
|
11
|
+
z.strictObject({ type: z.literal('dir'), path: z.string() }),
|
|
12
|
+
z.strictObject({
|
|
13
|
+
type: z.literal('file'),
|
|
14
|
+
path: z.string(),
|
|
15
|
+
base64: z
|
|
16
|
+
.string()
|
|
17
|
+
.regex(/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/),
|
|
18
|
+
}),
|
|
19
|
+
]),
|
|
20
|
+
),
|
|
21
|
+
})
|
|
22
|
+
.superRefine((value, ctx) => {
|
|
23
|
+
try {
|
|
24
|
+
validateTicketAssetInput(decodePortableAsset(value))
|
|
25
|
+
} catch (error) {
|
|
26
|
+
ctx.addIssue({ code: 'custom', message: String(error) })
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
export type PortableAsset = z.infer<typeof portableAssetSchema>
|
|
30
|
+
export function decodePortableAsset(value: PortableAsset): TicketAssetInput {
|
|
31
|
+
return {
|
|
32
|
+
kind: value.kind,
|
|
33
|
+
name: value.name,
|
|
34
|
+
layout: value.layout,
|
|
35
|
+
entries: value.entries.map((entry) =>
|
|
36
|
+
entry.type === 'dir'
|
|
37
|
+
? entry
|
|
38
|
+
: { type: 'file', path: entry.path, content: Buffer.from(entry.base64, 'base64') },
|
|
39
|
+
),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function encodePortableAsset(input: TicketAssetInput): PortableAsset {
|
|
43
|
+
validateTicketAssetInput(input)
|
|
44
|
+
return {
|
|
45
|
+
kind: input.kind,
|
|
46
|
+
name: input.name,
|
|
47
|
+
layout: input.layout,
|
|
48
|
+
entries: input.entries.map((entry) =>
|
|
49
|
+
entry.type === 'dir'
|
|
50
|
+
? entry
|
|
51
|
+
: { type: 'file', path: entry.path, base64: Buffer.from(entry.content).toString('base64') },
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { ticketRefSchema } from '../../ontology'
|
|
3
3
|
|
|
4
|
+
/** Explicit opt-in: released clients through 0.12 use strict decoders without revisions.
|
|
5
|
+
* Source checkouts can share their release version, so version alone is insufficient. */
|
|
6
|
+
export const TICKET_REVISIONS_HEADER = 'x-autobuild-ticket-revisions'
|
|
7
|
+
export const TICKET_REVISIONS_CAPABILITY = '1'
|
|
8
|
+
|
|
4
9
|
/** Repository-owned policy sent with every hosted ticket operation. */
|
|
5
10
|
export const hostedTicketContextSchema = z.strictObject({
|
|
6
11
|
teamKey: z.string().min(1),
|
|
12
|
+
repo: z.string().min(1).optional(),
|
|
7
13
|
claimedState: z.string().min(1).optional(),
|
|
8
14
|
createState: z.string().min(1).optional(),
|
|
9
15
|
})
|
|
10
16
|
export type HostedTicketContext = z.infer<typeof hostedTicketContextSchema>
|
|
11
17
|
|
|
18
|
+
export const TICKET_COMMENTS_HEADER = 'x-autobuild-ticket-comments'
|
|
19
|
+
export const TICKET_COMMENTS_CAPABILITY = '1'
|
|
20
|
+
|
|
21
|
+
export const ticketCommentWireSchema = z.strictObject({
|
|
22
|
+
body: z.string(),
|
|
23
|
+
createdAt: z.string(),
|
|
24
|
+
author: z.string().optional(),
|
|
25
|
+
})
|
|
26
|
+
|
|
12
27
|
export const ticketWireSchema = z.strictObject({
|
|
28
|
+
revision: z.string().min(1),
|
|
13
29
|
ref: ticketRefSchema,
|
|
30
|
+
comments: z.array(ticketCommentWireSchema).optional(),
|
|
14
31
|
creationKey: z.string().optional(),
|
|
15
32
|
title: z.string(),
|
|
16
33
|
body: z.string(),
|
|
@@ -46,6 +63,7 @@ const draftSchema = z.strictObject({
|
|
|
46
63
|
blockedBy: z.array(z.string()).optional(),
|
|
47
64
|
})
|
|
48
65
|
const createOptionsSchema = z.strictObject({
|
|
66
|
+
requestedId: z.string().min(1).optional(),
|
|
49
67
|
state: z.string().optional(),
|
|
50
68
|
idempotencyKey: z.string().optional(),
|
|
51
69
|
})
|
|
@@ -61,8 +79,27 @@ function envelope<T extends z.ZodType>(input: T) {
|
|
|
61
79
|
return z.strictObject({ context: hostedTicketContextSchema, input })
|
|
62
80
|
}
|
|
63
81
|
|
|
82
|
+
const mutationSchema = z.discriminatedUnion('kind', [
|
|
83
|
+
z.strictObject({ kind: z.literal('update'), patch: updateSchema }),
|
|
84
|
+
z.strictObject({ kind: z.literal('transition'), state: z.string().min(1) }),
|
|
85
|
+
z.strictObject({
|
|
86
|
+
kind: z.literal('blockers'),
|
|
87
|
+
operation: z.enum(['block', 'unblock']),
|
|
88
|
+
ids: z.array(z.string().min(1)),
|
|
89
|
+
}),
|
|
90
|
+
])
|
|
91
|
+
|
|
64
92
|
export const hostedTicketRequestSchemas = {
|
|
93
|
+
'mutate-if-revision': envelope(
|
|
94
|
+
z.strictObject({
|
|
95
|
+
id: z.string().min(1),
|
|
96
|
+
expectedRevision: z.string().refine((v) => v.trim().length > 0),
|
|
97
|
+
mutation: mutationSchema,
|
|
98
|
+
}),
|
|
99
|
+
),
|
|
65
100
|
'list-ready': envelope(criteriaSchema),
|
|
101
|
+
states: envelope(z.strictObject({})),
|
|
102
|
+
'add-state': envelope(z.strictObject({ name: z.string(), about: z.string().optional() })),
|
|
66
103
|
get: envelope(idSchema),
|
|
67
104
|
claim: envelope(idSchema),
|
|
68
105
|
comment: envelope(z.strictObject({ id: z.string().min(1), body: z.string() })),
|
|
@@ -80,3 +117,62 @@ export type HostedTicketOperation = keyof typeof hostedTicketRequestSchemas
|
|
|
80
117
|
export const HOSTED_TICKET_OPERATIONS = Object.keys(
|
|
81
118
|
hostedTicketRequestSchemas,
|
|
82
119
|
) as HostedTicketOperation[]
|
|
120
|
+
|
|
121
|
+
/** Project policy is separate from CRUD context and cannot be supplied by delegates. */
|
|
122
|
+
export const hostedTicketPolicySchema = z.strictObject({
|
|
123
|
+
version: z.literal(1),
|
|
124
|
+
tickets: z.strictObject({
|
|
125
|
+
source: z.literal('hosted'),
|
|
126
|
+
teamKey: z.string().min(1),
|
|
127
|
+
readyState: z
|
|
128
|
+
.string()
|
|
129
|
+
.min(1)
|
|
130
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank'),
|
|
131
|
+
triageState: z
|
|
132
|
+
.string()
|
|
133
|
+
.min(1)
|
|
134
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank'),
|
|
135
|
+
claimedState: z
|
|
136
|
+
.string()
|
|
137
|
+
.min(1)
|
|
138
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank'),
|
|
139
|
+
doneState: z
|
|
140
|
+
.string()
|
|
141
|
+
.min(1)
|
|
142
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank'),
|
|
143
|
+
createState: z
|
|
144
|
+
.string()
|
|
145
|
+
.min(1)
|
|
146
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank'),
|
|
147
|
+
proposalState: z
|
|
148
|
+
.string()
|
|
149
|
+
.min(1)
|
|
150
|
+
.refine((value) => value.trim().length > 0, 'state must be nonblank'),
|
|
151
|
+
readyLabels: z.array(z.string().min(1)).optional(),
|
|
152
|
+
}),
|
|
153
|
+
})
|
|
154
|
+
export type HostedTicketPolicy = z.infer<typeof hostedTicketPolicySchema>
|
|
155
|
+
|
|
156
|
+
export const ticketGrantSchema = z.strictObject({
|
|
157
|
+
id: z.string().min(1),
|
|
158
|
+
repo: z.string().min(1),
|
|
159
|
+
execution: z.string().min(1),
|
|
160
|
+
kind: z.enum(['runner', 'phase']),
|
|
161
|
+
parent: z.string().nullable(),
|
|
162
|
+
expiresAt: z.string().datetime(),
|
|
163
|
+
})
|
|
164
|
+
export const ticketGrantResponseSchema = z.strictObject({
|
|
165
|
+
token: z.string().startsWith('abt_'),
|
|
166
|
+
grant: ticketGrantSchema,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
export const ticketStateWireSchema = z.strictObject({
|
|
170
|
+
name: z.string(),
|
|
171
|
+
tickets: z.number().int().nonnegative(),
|
|
172
|
+
about: z.string().optional(),
|
|
173
|
+
})
|
|
174
|
+
export const ticketStatesWireSchema = z.array(ticketStateWireSchema)
|
|
175
|
+
export const ticketStateCreateWireSchema = z.strictObject({
|
|
176
|
+
state: ticketStateWireSchema,
|
|
177
|
+
created: z.boolean(),
|
|
178
|
+
})
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { hostedRequestedIds } from './requested-id'
|
|
2
|
+
import { TicketRevisionConflict } from './revision'
|
|
1
3
|
import type {
|
|
2
4
|
DependencyState,
|
|
3
5
|
Ticket,
|
|
@@ -6,16 +8,25 @@ import type {
|
|
|
6
8
|
TicketListing,
|
|
7
9
|
TicketSource,
|
|
8
10
|
TicketUpdate,
|
|
11
|
+
TicketMutation,
|
|
9
12
|
} from '../types'
|
|
10
|
-
import { AuthError } from '../../store/remote/client'
|
|
13
|
+
import { RemoteBuildStore, AuthError } from '../../store/remote/client'
|
|
11
14
|
import { errorBodySchema } from '../../store/remote/protocol'
|
|
12
15
|
import {
|
|
13
16
|
claimWireSchema,
|
|
17
|
+
TICKET_REVISIONS_HEADER,
|
|
18
|
+
TICKET_REVISIONS_CAPABILITY,
|
|
19
|
+
hostedTicketPolicySchema,
|
|
20
|
+
type HostedTicketPolicy,
|
|
21
|
+
TICKET_COMMENTS_HEADER,
|
|
22
|
+
TICKET_COMMENTS_CAPABILITY,
|
|
14
23
|
dependencyStatesWireSchema,
|
|
15
24
|
type HostedTicketContext,
|
|
16
25
|
successWireSchema,
|
|
17
26
|
ticketListingWireSchema,
|
|
18
27
|
ticketWireSchema,
|
|
28
|
+
ticketStatesWireSchema,
|
|
29
|
+
ticketStateCreateWireSchema,
|
|
19
30
|
} from './remote-protocol'
|
|
20
31
|
import {
|
|
21
32
|
AUTOBUILD_VERSION,
|
|
@@ -38,22 +49,42 @@ export interface HostedTicketSourceOptions extends HostedTicketContext {
|
|
|
38
49
|
/** Full TicketSource client for a hosted Autobuild service. */
|
|
39
50
|
export class HostedTicketSource implements TicketSource {
|
|
40
51
|
readonly name = 'hosted'
|
|
52
|
+
readonly requestedIds: ReturnType<typeof hostedRequestedIds>
|
|
53
|
+
readonly assets: import('../types').TicketAssetStore
|
|
41
54
|
private readonly base: string
|
|
42
55
|
private readonly token: string
|
|
43
56
|
private readonly fetchFn: HostedTicketFetch
|
|
44
57
|
private readonly context: HostedTicketContext
|
|
45
58
|
|
|
46
59
|
constructor(options: HostedTicketSourceOptions) {
|
|
60
|
+
this.requestedIds = hostedRequestedIds(options.teamKey)
|
|
61
|
+
const client = new RemoteBuildStore({
|
|
62
|
+
url: options.url,
|
|
63
|
+
token: options.token,
|
|
64
|
+
...(options.fetchFn ? { fetchFn: options.fetchFn as typeof fetch } : {}),
|
|
65
|
+
})
|
|
66
|
+
this.assets = {
|
|
67
|
+
ticketAssetLimits: client.ticketAssetLimits.bind(client),
|
|
68
|
+
putTicketAsset: client.putTicketAsset.bind(client),
|
|
69
|
+
getTicketAsset: client.getTicketAsset.bind(client),
|
|
70
|
+
listTicketAssets: client.listTicketAssets.bind(client),
|
|
71
|
+
removeTicketAsset: client.removeTicketAsset.bind(client),
|
|
72
|
+
}
|
|
47
73
|
this.base = options.url.replace(/\/+$/, '')
|
|
48
74
|
this.token = options.token
|
|
49
75
|
this.fetchFn = options.fetchFn ?? fetch
|
|
50
76
|
this.context = {
|
|
51
77
|
teamKey: options.teamKey,
|
|
78
|
+
...(options.repo !== undefined ? { repo: options.repo } : {}),
|
|
52
79
|
...(options.claimedState !== undefined ? { claimedState: options.claimedState } : {}),
|
|
53
80
|
...(options.createState !== undefined ? { createState: options.createState } : {}),
|
|
54
81
|
}
|
|
55
82
|
}
|
|
56
83
|
|
|
84
|
+
async synchronizePolicy(policy: HostedTicketPolicy): Promise<void> {
|
|
85
|
+
await this.request('policy', hostedTicketPolicySchema.parse(policy), successWireSchema)
|
|
86
|
+
}
|
|
87
|
+
|
|
57
88
|
private project(ticket: Ticket): Ticket {
|
|
58
89
|
return { ...ticket, ref: { ...ticket.ref, source: this.name } }
|
|
59
90
|
}
|
|
@@ -67,7 +98,9 @@ export class HostedTicketSource implements TicketSource {
|
|
|
67
98
|
method: 'POST',
|
|
68
99
|
headers: {
|
|
69
100
|
'content-type': 'application/json',
|
|
101
|
+
[TICKET_COMMENTS_HEADER]: TICKET_COMMENTS_CAPABILITY,
|
|
70
102
|
authorization: `Bearer ${this.token}`,
|
|
103
|
+
[TICKET_REVISIONS_HEADER]: TICKET_REVISIONS_CAPABILITY,
|
|
71
104
|
[AUTOBUILD_VERSION_HEADER]: AUTOBUILD_VERSION,
|
|
72
105
|
[REMOTE_STORE_PROTOCOL_VERSION_HEADER]: REMOTE_STORE_PROTOCOL_VERSION,
|
|
73
106
|
},
|
|
@@ -76,8 +109,17 @@ export class HostedTicketSource implements TicketSource {
|
|
|
76
109
|
if (!response.ok) {
|
|
77
110
|
let message = `ticket service responded ${response.status}`
|
|
78
111
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
112
|
+
const body = (await response.json()) as Record<string, unknown>
|
|
113
|
+
if (
|
|
114
|
+
response.status === 409 &&
|
|
115
|
+
typeof body.currentRevision === 'string' &&
|
|
116
|
+
typeof body.expectedRevision === 'string' &&
|
|
117
|
+
typeof body.id === 'string'
|
|
118
|
+
)
|
|
119
|
+
throw new TicketRevisionConflict(body.id, body.expectedRevision, body.currentRevision)
|
|
120
|
+
message = errorBodySchema.parse(body).error
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (error instanceof TicketRevisionConflict) throw error
|
|
81
123
|
// Keep the status-only safe fallback for non-protocol responses.
|
|
82
124
|
}
|
|
83
125
|
if (response.status === 401 || response.status === 403) throw new AuthError(message)
|
|
@@ -98,6 +140,14 @@ export class HostedTicketSource implements TicketSource {
|
|
|
98
140
|
return { ...listing, tickets: listing.tickets.map((ticket) => this.project(ticket)) }
|
|
99
141
|
}
|
|
100
142
|
|
|
143
|
+
async listStates() {
|
|
144
|
+
return this.request('states', {}, ticketStatesWireSchema)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async addState(name: string, opts: { about?: string } = {}) {
|
|
148
|
+
return this.request('add-state', { name, ...opts }, ticketStateCreateWireSchema)
|
|
149
|
+
}
|
|
150
|
+
|
|
101
151
|
async get(id: string): Promise<Ticket | null> {
|
|
102
152
|
const ticket = await this.request('get', { id }, ticketWireSchema.nullable())
|
|
103
153
|
return ticket === null ? null : this.project(ticket)
|
|
@@ -119,6 +169,14 @@ export class HostedTicketSource implements TicketSource {
|
|
|
119
169
|
return this.project(await this.request('create', { draft, options }, ticketWireSchema))
|
|
120
170
|
}
|
|
121
171
|
|
|
172
|
+
async mutateIfRevision(
|
|
173
|
+
id: string,
|
|
174
|
+
expectedRevision: string,
|
|
175
|
+
mutation: TicketMutation,
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
await this.request('mutate-if-revision', { id, expectedRevision, mutation }, successWireSchema)
|
|
178
|
+
}
|
|
179
|
+
|
|
122
180
|
async update(id: string, patch: TicketUpdate): Promise<void> {
|
|
123
181
|
await this.request('update', { id, patch }, successWireSchema)
|
|
124
182
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Requested-id policies shared by adapter writes and migration preflight. */
|
|
2
|
+
export interface RequestedIdPolicy {
|
|
3
|
+
preserve(id: string): string | undefined
|
|
4
|
+
allocate(occupied: string[]): string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function validateFileTicketId(id: string): string {
|
|
8
|
+
if (!id || id === '.' || /[/\\\0]/.test(id) || id.includes('..'))
|
|
9
|
+
throw new Error(`file ticket source: invalid ticket id "${id}"`)
|
|
10
|
+
return id
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const fileRequestedIds: RequestedIdPolicy = {
|
|
14
|
+
preserve: validateFileTicketId,
|
|
15
|
+
allocate(occupied) {
|
|
16
|
+
let n = 1
|
|
17
|
+
while (occupied.includes(`file-${n}`)) n++
|
|
18
|
+
return `file-${n}`
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const MAX_TICKET_NUMBER = 9223372036854775807n
|
|
23
|
+
export function hostedRequestedIds(teamKey: string): RequestedIdPolicy {
|
|
24
|
+
const numberOf = (id: string): bigint | undefined => {
|
|
25
|
+
if (!id.startsWith(`${teamKey}-`)) return undefined
|
|
26
|
+
const suffix = id.slice(teamKey.length + 1)
|
|
27
|
+
if (!/^[1-9][0-9]*$/.test(suffix)) return undefined
|
|
28
|
+
const n = BigInt(suffix)
|
|
29
|
+
if (n > MAX_TICKET_NUMBER) throw new Error(`ticket number exceeds PostgreSQL bigint: ${id}`)
|
|
30
|
+
return n
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
preserve(id) {
|
|
34
|
+
return numberOf(id) === undefined ? undefined : id
|
|
35
|
+
},
|
|
36
|
+
allocate(occupied) {
|
|
37
|
+
let max = 0n
|
|
38
|
+
for (const id of occupied) {
|
|
39
|
+
const n = numberOf(id)
|
|
40
|
+
if (n !== undefined && n > max) max = n
|
|
41
|
+
}
|
|
42
|
+
if (max === MAX_TICKET_NUMBER) throw new Error('ticket numbers exhausted')
|
|
43
|
+
return `${teamKey}-${max + 1n}`
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import type { Ticket, TicketMutation, TicketSource } from '../types'
|
|
3
|
+
|
|
4
|
+
/** Content versions intentionally return to the same value after an exact revert. */
|
|
5
|
+
export function ticketRevision(
|
|
6
|
+
ticket: Pick<Ticket, 'title' | 'body' | 'labels' | 'state' | 'blockedBy'> & {
|
|
7
|
+
comments?: Ticket['comments']
|
|
8
|
+
attachments?: unknown
|
|
9
|
+
},
|
|
10
|
+
): string {
|
|
11
|
+
return createHash('sha256')
|
|
12
|
+
.update(
|
|
13
|
+
JSON.stringify([
|
|
14
|
+
ticket.title,
|
|
15
|
+
ticket.body,
|
|
16
|
+
[...new Set(ticket.labels)].sort(),
|
|
17
|
+
ticket.state ?? null,
|
|
18
|
+
[...new Set(ticket.blockedBy ?? [])].sort(),
|
|
19
|
+
...(ticket.comments !== undefined || ticket.attachments !== undefined
|
|
20
|
+
? [ticket.comments ?? null, ticket.attachments ?? null]
|
|
21
|
+
: []),
|
|
22
|
+
]),
|
|
23
|
+
)
|
|
24
|
+
.digest('hex')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class TicketRevisionConflict extends Error {
|
|
28
|
+
constructor(
|
|
29
|
+
readonly id: string,
|
|
30
|
+
readonly expectedRevision: string,
|
|
31
|
+
readonly currentRevision: string,
|
|
32
|
+
) {
|
|
33
|
+
super(
|
|
34
|
+
`ticket "${id}" changed: expected revision ${expectedRevision}, current revision ${currentRevision}`,
|
|
35
|
+
)
|
|
36
|
+
this.name = 'TicketRevisionConflict'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class TicketRevisionUnsupported extends Error {
|
|
41
|
+
constructor(source: string) {
|
|
42
|
+
super(`${source} ticket source does not support revision-checked writes`)
|
|
43
|
+
this.name = 'TicketRevisionUnsupported'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function checkTicketRevision(ticket: Ticket, expected: string): void {
|
|
48
|
+
if (!expected.trim()) throw new Error('expected revision must be nonblank')
|
|
49
|
+
if (ticket.revision !== expected)
|
|
50
|
+
throw new TicketRevisionConflict(ticket.ref.id, expected, ticket.revision)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function mutateTicketIfRevision(
|
|
54
|
+
source: TicketSource,
|
|
55
|
+
id: string,
|
|
56
|
+
expected: string,
|
|
57
|
+
mutation: TicketMutation,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
if (!expected.trim()) throw new Error('expected revision must be nonblank')
|
|
60
|
+
if (!source.mutateIfRevision) throw new TicketRevisionUnsupported(source.name)
|
|
61
|
+
await source.mutateIfRevision(id, expected, mutation)
|
|
62
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Shared state rules; configured names are deliberately not create-validated. */
|
|
2
|
+
export const STATE_NAME_RULE =
|
|
3
|
+
'a state name is one to forty characters of lowercase letters, digits, and hyphens, starting with a letter'
|
|
4
|
+
|
|
5
|
+
export function validateStateName(name: string, source = 'file ticket source'): void {
|
|
6
|
+
if (!/^[a-z][a-z0-9-]{0,39}$/.test(name)) {
|
|
7
|
+
throw new Error(`${source}: invalid state name "${name}" — ${STATE_NAME_RULE}`)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function validateStatePurpose(
|
|
12
|
+
value: string | undefined,
|
|
13
|
+
source = 'file ticket source',
|
|
14
|
+
): string | undefined {
|
|
15
|
+
const about = value?.trim()
|
|
16
|
+
if (about !== undefined && (about === '' || /[\r\n]/.test(about))) {
|
|
17
|
+
throw new Error(`${source}: a state purpose must be one nonblank line`)
|
|
18
|
+
}
|
|
19
|
+
return about
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function orderStates(names: string[], lifecycle: readonly string[]): string[] {
|
|
23
|
+
const first = [...new Set(lifecycle)].filter((name) => names.includes(name))
|
|
24
|
+
return [...first, ...names.filter((name) => !first.includes(name)).sort()]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveState(
|
|
28
|
+
name: string,
|
|
29
|
+
existing: string[],
|
|
30
|
+
source = 'file ticket source',
|
|
31
|
+
): string {
|
|
32
|
+
if (existing.includes(name)) return name
|
|
33
|
+
const matches = existing.filter((s) => s.toLowerCase() === name.toLowerCase())
|
|
34
|
+
if (matches.length === 1) return matches[0] as string
|
|
35
|
+
const directories = source === 'file ticket source'
|
|
36
|
+
if (matches.length > 1) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`${source}: ambiguous state "${name}" — it matches the ${directories ? 'directories' : 'states'} ${matches.join(', ')}`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
throw new Error(
|
|
42
|
+
`${source}: unknown state "${name}" — this tracker's states are ${directories ? 'the directories: ' : ''}${existing.join(', ')}`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Portable directory vocabulary, without guessing lifecycle roles. */
|
|
47
|
+
export function normalizeStateName(name: string): string {
|
|
48
|
+
if (/^[a-z][a-z0-9-]{0,39}$/.test(name)) return name
|
|
49
|
+
const candidate = name
|
|
50
|
+
.trim()
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.replace(/[^a-z0-9-]+/g, '-')
|
|
53
|
+
.replace(/-+/g, '-')
|
|
54
|
+
.replace(/^-|-$/g, '')
|
|
55
|
+
try {
|
|
56
|
+
validateStateName(candidate)
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error(`invalid state "${name}" (normalized: "${candidate}")`)
|
|
59
|
+
}
|
|
60
|
+
return candidate
|
|
61
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BuildStore } from '../store/types'
|
|
1
2
|
/**
|
|
2
3
|
* Ports (SPEC §3.2): interfaces to the world, each with swappable adapters.
|
|
3
4
|
* The kernel and processes depend only on these types; adapters live in
|
|
@@ -49,7 +50,19 @@ export interface SessionStreamSink extends SessionStreamEmitter {
|
|
|
49
50
|
// never consulted mid-build and never used as artifact storage. A file-based
|
|
50
51
|
// adapter with nowhere to put blobs must be fully workable.
|
|
51
52
|
|
|
53
|
+
/** A recorded comment, with its exact body and source timestamp. */
|
|
54
|
+
export interface TicketComment {
|
|
55
|
+
body: string
|
|
56
|
+
createdAt: string
|
|
57
|
+
author?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
export interface Ticket {
|
|
61
|
+
/** Opaque source version of title, body, labels, state and blockers. */
|
|
62
|
+
revision: string
|
|
63
|
+
/** Complete history oldest first when exposed by this read. Absent means
|
|
64
|
+
* unsupported; an empty array means no comments. Bodies are unchanged. */
|
|
65
|
+
comments?: TicketComment[]
|
|
53
66
|
ref: TicketRef
|
|
54
67
|
/** Stable source-native key used to correlate an externally created ticket
|
|
55
68
|
* with Autobuild's durable creation reservation. It is separate from the
|
|
@@ -106,6 +119,8 @@ export interface DependencyState {
|
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
export interface TicketCreateOptions {
|
|
122
|
+
/** Requested source id. Unsupported adapters must reject it. */
|
|
123
|
+
requestedId?: string
|
|
109
124
|
/** Explicit workflow state for this create (harvest always targets Triage). */
|
|
110
125
|
state?: string
|
|
111
126
|
/** Source-level adoption key for crash-safe external creation. File and fake
|
|
@@ -148,7 +163,29 @@ export class TicketSourceRateLimitError extends Error {
|
|
|
148
163
|
}
|
|
149
164
|
}
|
|
150
165
|
|
|
166
|
+
export type TicketAssetStore = {
|
|
167
|
+
/** Native ownership, including tombstones, prevents resurrection from legacy stores. */
|
|
168
|
+
hasTicketAssetHistory?(repo: string, id: string, kind: string, name: string): Promise<boolean>
|
|
169
|
+
} & Pick<
|
|
170
|
+
BuildStore,
|
|
171
|
+
| 'ticketAssetLimits'
|
|
172
|
+
| 'putTicketAsset'
|
|
173
|
+
| 'getTicketAsset'
|
|
174
|
+
| 'listTicketAssets'
|
|
175
|
+
| 'removeTicketAsset'
|
|
176
|
+
>
|
|
177
|
+
|
|
178
|
+
export type TicketMutation =
|
|
179
|
+
| { kind: 'update'; patch: TicketUpdate }
|
|
180
|
+
| { kind: 'transition'; state: string }
|
|
181
|
+
| { kind: 'blockers'; operation: 'block' | 'unblock'; ids: string[] }
|
|
182
|
+
|
|
151
183
|
export interface TicketSource {
|
|
184
|
+
/** Import support: the same pure id policy used by create, for mutation-free preflight. */
|
|
185
|
+
readonly requestedIds?: import('./tickets/requested-id').RequestedIdPolicy
|
|
186
|
+
readonly assets?: TicketAssetStore
|
|
187
|
+
/** Check and mutate at the source boundary; absent means checks are unsupported. */
|
|
188
|
+
mutateIfRevision?(id: string, expectedRevision: string, mutation: TicketMutation): Promise<void>
|
|
152
189
|
readonly name: string
|
|
153
190
|
/** Ready tickets matching the dispatch criteria (label/state — §3.3), plus
|
|
154
191
|
* actionable diagnostics for individual source records excluded as invalid.
|
|
@@ -193,9 +230,9 @@ export interface TicketSource {
|
|
|
193
230
|
*/
|
|
194
231
|
listStates?(): Promise<TicketStateInfo[]>
|
|
195
232
|
/**
|
|
196
|
-
* Optional state creation (`ab ticket state create`).
|
|
197
|
-
*
|
|
198
|
-
*
|
|
233
|
+
* Optional state creation (`ab ticket state create`). Duplicate policy is
|
|
234
|
+
* adapter-specific: file creation is idempotent and can replace purpose;
|
|
235
|
+
* hosted creation refuses existing names in any case. Rejects malformed names.
|
|
199
236
|
* Named `addState` because adapters already hold a `createState` option field.
|
|
200
237
|
*/
|
|
201
238
|
addState?(
|
|
@@ -114,8 +114,13 @@ export type ExecutionObservation =
|
|
|
114
114
|
* provider may implement this by starting the fixed build process beside its
|
|
115
115
|
* checkout; the kernel need not know where that is. Every provider owns full
|
|
116
116
|
* teardown of the environment and descendants it starts before completion. */
|
|
117
|
+
export interface ExecutionLaunchAuth {
|
|
118
|
+
token: string
|
|
119
|
+
tickets?: { url: string; token: string }
|
|
120
|
+
}
|
|
121
|
+
|
|
117
122
|
export interface BuildExecution {
|
|
118
|
-
start(input: BuildExecutionStart): Promise<BuildExecutionHandle>
|
|
123
|
+
start(input: BuildExecutionStart, auth?: ExecutionLaunchAuth): Promise<BuildExecutionHandle>
|
|
119
124
|
/** Optional liveness observation of a previously recorded execution, from
|
|
120
125
|
* provider state alone. Absent = this provider cannot observe; callers
|
|
121
126
|
* supervise exactly as before ("cannot observe" never means "act"). */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { delegateCredential } from '../../store/remote/credentials'
|
|
1
2
|
import { resolve } from 'node:path'
|
|
2
3
|
import { SANDBOX_FORBIDDEN_ENV } from './operator-sandbox'
|
|
3
4
|
import type { WorkspaceConfig } from '../../config/schema'
|
|
@@ -131,10 +132,22 @@ export async function createWorkspaceProvider(
|
|
|
131
132
|
// receives through `opts` reach the plugin factory through the context.
|
|
132
133
|
return await factory({
|
|
133
134
|
config: config.config,
|
|
134
|
-
env:
|
|
135
|
+
env: Object.fromEntries(
|
|
136
|
+
Object.entries(opts.env).filter(
|
|
137
|
+
([key]) => !['AB_TOKEN', 'AB_TICKET_TOKEN', 'AB_TICKET_URL'].includes(key),
|
|
138
|
+
),
|
|
139
|
+
),
|
|
135
140
|
repoRoot: resolve(opts.repoRoot),
|
|
136
141
|
...(opts.storeRef !== undefined ? { storeRef: opts.storeRef } : {}),
|
|
137
|
-
...(opts.
|
|
142
|
+
...(opts.storeRef !== undefined && opts.storeToken !== undefined
|
|
143
|
+
? {
|
|
144
|
+
delegateCredential: (
|
|
145
|
+
_ref: string,
|
|
146
|
+
_token: string | undefined,
|
|
147
|
+
target: Parameters<typeof delegateCredential>[2],
|
|
148
|
+
) => delegateCredential(opts.storeRef!, opts.storeToken, target),
|
|
149
|
+
}
|
|
150
|
+
: {}),
|
|
138
151
|
...(opts.runtimeReferences !== undefined
|
|
139
152
|
? { runtimeReferences: opts.runtimeReferences }
|
|
140
153
|
: {}),
|
|
@@ -18,11 +18,9 @@
|
|
|
18
18
|
* version in both modes: origin mode's release-asset tag is the running
|
|
19
19
|
* version by construction, and source mode packs `distributionRoot()`, whose
|
|
20
20
|
* package.json is the same file `readDistributionIdentity()` reads. The hosted
|
|
21
|
-
* remote store
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* move together through a release cut (`tools/release.ts`) plus
|
|
25
|
-
* store/dispatcher upgrades; there is no code path that safely mixes versions.
|
|
21
|
+
* remote store accepts only its published client release-line window and known
|
|
22
|
+
* protocol identities (409 outside that contract). Upgrade the service before
|
|
23
|
+
* a dispatcher moves beyond its window; projects can then upgrade independently.
|
|
26
24
|
* Provisioning records the installed distribution's identity stamp
|
|
27
25
|
* (`readDistributionIdentityStamp()`) in the guest's `.distribution-version`
|
|
28
26
|
* marker and reinstalls the archive on a later mismatch, so an upgraded
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
BuildExecutionHandle,
|
|
3
3
|
BuildExecutionIdentity,
|
|
4
4
|
ExecutionObservation,
|
|
5
|
+
ExecutionLaunchAuth,
|
|
5
6
|
} from './build-execution'
|
|
6
7
|
|
|
7
8
|
/** Guest environment variable carrying the serialized harvest launch
|
|
@@ -23,6 +24,7 @@ export interface HarvestExecutionEnvironment {
|
|
|
23
24
|
* advisory. Run state, config, and outcomes are obtained only through the
|
|
24
25
|
* repository journal in the Store. */
|
|
25
26
|
export interface HarvestExecutionStart {
|
|
27
|
+
preparedRun?: { run: string }
|
|
26
28
|
storeRef: string
|
|
27
29
|
/** Repository identity (normalized origin) the guest serves. */
|
|
28
30
|
repo: string
|
|
@@ -101,7 +103,7 @@ export function parseHarvestRunnerLaunch(value: unknown): HarvestRunnerLaunch |
|
|
|
101
103
|
* observation types so supervision semantics (long-poll wait, teardown
|
|
102
104
|
* outcome, provider liveness) are identical to builds. */
|
|
103
105
|
export interface HarvestExecution {
|
|
104
|
-
start(input: HarvestExecutionStart): Promise<BuildExecutionHandle>
|
|
106
|
+
start(input: HarvestExecutionStart, auth?: ExecutionLaunchAuth): Promise<BuildExecutionHandle>
|
|
105
107
|
/** Optional liveness observation of a previously recorded execution, from
|
|
106
108
|
* provider state alone. Absent = this provider cannot observe; callers
|
|
107
109
|
* supervise exactly as before ("cannot observe" never means "act"). */
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
BuildExecutionExit,
|
|
9
9
|
BuildExecutionHandle,
|
|
10
10
|
BuildExecutionStart,
|
|
11
|
+
ExecutionLaunchAuth,
|
|
11
12
|
} from './build-execution'
|
|
12
13
|
|
|
13
14
|
export const BUILD_RUNNER_OPTIONS_ENV = 'AB_BUILD_RUNNER_OPTIONS'
|
|
@@ -34,10 +35,16 @@ export class LocalBuildExecution implements BuildExecution {
|
|
|
34
35
|
this.spawn = opts.spawn ?? Bun.spawn
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
async start(
|
|
38
|
+
async start(
|
|
39
|
+
input: BuildExecutionStart,
|
|
40
|
+
auth?: ExecutionLaunchAuth,
|
|
41
|
+
): Promise<BuildExecutionHandle> {
|
|
42
|
+
if (/^https?:/i.test(input.storeRef) && !auth)
|
|
43
|
+
throw new Error('remote build launch requires delegated authority')
|
|
38
44
|
const child = this.spawn([process.execPath, this.entrypoint], {
|
|
39
45
|
env: {
|
|
40
46
|
...this.env,
|
|
47
|
+
AB_TOKEN: auth?.token,
|
|
41
48
|
[BUILD_RUNNER_OPTIONS_ENV]: JSON.stringify({
|
|
42
49
|
...input,
|
|
43
50
|
supervision: { kind: 'local-parent', parentPid: process.pid },
|