@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
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* autobuild.toml schema (SPEC §16.1, D9): the one declarative per-repo config
|
|
3
|
+
* file. Declarative, not executable — the kernel, dispatcher, CLI, and any
|
|
4
|
+
* future tooling parse it without evaluating anything; commands are plain
|
|
5
|
+
* shell strings.
|
|
6
|
+
*
|
|
7
|
+
* Strictness policy (§16.1): unknown top-level keys/tables and unknown keys
|
|
8
|
+
* inside known tables are ERRORS — a typo must not silently disable a
|
|
9
|
+
* verifier. Open maps ([commands], [roles], [workspace.config], and the named
|
|
10
|
+
* [verify.<step>] and [finalize.<step>] table sets) are exempt by construction:
|
|
11
|
+
* their keys are user-chosen names, while every value remains strictly
|
|
12
|
+
* validated.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod'
|
|
15
|
+
import { prImageHostSchema } from '../ontology'
|
|
16
|
+
import { SANDBOX_FORBIDDEN_ENV } from '../ports/workspace/operator-sandbox'
|
|
17
|
+
import { defineEntry, openMap, ownEntries, parseEntry } from '../open-map'
|
|
18
|
+
import { forwardIssues } from '../zod-issues'
|
|
19
|
+
import { displayName, effectiveRuntimeReferences, tomlKey } from './roles'
|
|
20
|
+
|
|
21
|
+
// ── Open maps ────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
//
|
|
24
|
+
// Every open map below is parsed through `../open-map`, which keeps a
|
|
25
|
+
// user-chosen key intact instead of losing it to the prototype chain: the
|
|
26
|
+
// perfectly valid TOML table `[roles."__proto__"]` would otherwise vanish while
|
|
27
|
+
// parsing SUCCEEDS — never eagerly resolved, never dispatchable, and invisible
|
|
28
|
+
// to the unconsumed-key warning that exists to catch exactly this class of dead
|
|
29
|
+
// configuration (§9). A silent drop is the one outcome the strictness policy
|
|
30
|
+
// above rules out.
|
|
31
|
+
//
|
|
32
|
+
// The TOML parser already hands us null-prototype tables with the key intact,
|
|
33
|
+
// and `openMap` keeps it that way end to end.
|
|
34
|
+
//
|
|
35
|
+
// `[workspace.config]` is the one open map here whose keys are PLUGIN-owned
|
|
36
|
+
// rather than Autobuild-owned, which makes faithful pass-through more important
|
|
37
|
+
// rather than less. Autobuild cannot validate a key it does not interpret, so a
|
|
38
|
+
// dropped one is undetectable downstream: the plugin sees an absent key and
|
|
39
|
+
// cannot tell an operator who omitted it from one who declared it, then reports
|
|
40
|
+
// its own error against a table that plainly contains it.
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A section mixing ONE known key (`steps`) with an open set of named step
|
|
44
|
+
* tables — the same preservation contract as `openMap`.
|
|
45
|
+
*
|
|
46
|
+
* Without it a valid `[verify."__proto__"]` table vanished before
|
|
47
|
+
* cross-validation, which then reported "is listed in verify.steps but has no
|
|
48
|
+
* [verify.__proto__] table" about a table sitting right there in the file. Loud
|
|
49
|
+
* rather than silent, but a diagnostic that contradicts the source is its own
|
|
50
|
+
* defect: §16.1 errors are feedback to whoever edits the file.
|
|
51
|
+
*/
|
|
52
|
+
function stepSection<T, C>(
|
|
53
|
+
label: string,
|
|
54
|
+
stepsSchema: z.ZodType<string[]>,
|
|
55
|
+
tableSchema: z.ZodType<T>,
|
|
56
|
+
compose: (steps: string[], stepConfigs: Record<string, T>) => C,
|
|
57
|
+
) {
|
|
58
|
+
return z
|
|
59
|
+
.unknown()
|
|
60
|
+
.transform((input, ctx): C => {
|
|
61
|
+
const stepConfigs: Record<string, T> = Object.create(null)
|
|
62
|
+
let steps: string[] = []
|
|
63
|
+
for (const [key, raw] of ownEntries(input, label, ctx)) {
|
|
64
|
+
if (key === 'steps') {
|
|
65
|
+
const parsed = stepsSchema.safeParse(raw)
|
|
66
|
+
if (parsed.success) steps = parsed.data
|
|
67
|
+
else forwardIssues(parsed.error.issues, ctx, ['steps'])
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
const table = parseEntry(tableSchema, raw, key, ctx)
|
|
71
|
+
if (table.ok) defineEntry(stepConfigs, key, table.value)
|
|
72
|
+
}
|
|
73
|
+
return compose(steps, stepConfigs)
|
|
74
|
+
})
|
|
75
|
+
.prefault({})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── [pr] / [pr.imageHost] ────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/** Optional public GitHub release used for review-window image copies.
|
|
81
|
+
* Omission is intentionally `undefined`: hosting is off by default. */
|
|
82
|
+
export const imageHostSchema = prImageHostSchema
|
|
83
|
+
export type ImageHostConfig = z.infer<typeof imageHostSchema>
|
|
84
|
+
|
|
85
|
+
export const prSchema = z.strictObject({
|
|
86
|
+
imageHost: imageHostSchema.optional(),
|
|
87
|
+
})
|
|
88
|
+
export type PrConfig = z.infer<typeof prSchema>
|
|
89
|
+
|
|
90
|
+
// ── [workspace] ─────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const envNameSchema = z
|
|
93
|
+
.string()
|
|
94
|
+
.regex(/^[A-Za-z_][A-Za-z0-9_]*$/, 'must be a nonblank environment variable name')
|
|
95
|
+
|
|
96
|
+
/** Strict built-in Vercel configuration. Values are operational policy only;
|
|
97
|
+
* credentials are referenced by variable name and never accepted as literals. */
|
|
98
|
+
const nonblankProvisioningString = (field: string) =>
|
|
99
|
+
z.string().refine((value) => value.trim().length > 0, `${field} must be nonblank`)
|
|
100
|
+
|
|
101
|
+
export const vercelProvisioningStepSchema = z.strictObject({
|
|
102
|
+
name: nonblankProvisioningString('provisioning step name'),
|
|
103
|
+
command: nonblankProvisioningString('provisioning step command'),
|
|
104
|
+
})
|
|
105
|
+
export type VercelProvisioningStep = z.infer<typeof vercelProvisioningStepSchema>
|
|
106
|
+
|
|
107
|
+
const vercelUniversalImageSchema = z
|
|
108
|
+
.string()
|
|
109
|
+
.refine(
|
|
110
|
+
(image) =>
|
|
111
|
+
/^vercel\/sandbox\/universal(?::[A-Za-z0-9_][A-Za-z0-9._-]{0,127}|@sha256:[0-9a-f]{64})?$/.test(
|
|
112
|
+
image,
|
|
113
|
+
),
|
|
114
|
+
'Autobuild Bun provisioning is validated only on the vercel/sandbox/universal managed image; use its bare name, a tag, or a sha256 digest containing exactly 64 lowercase hexadecimal characters',
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
export const runtimeProvisioningEntrySchema = z.strictObject({
|
|
118
|
+
install: z.string().refine((value) => value.trim().length > 0, 'install must be nonblank'),
|
|
119
|
+
preflight: z.string().refine((value) => value.trim().length > 0, 'preflight must be nonblank'),
|
|
120
|
+
})
|
|
121
|
+
export type RuntimeProvisioningEntry = z.infer<typeof runtimeProvisioningEntrySchema>
|
|
122
|
+
|
|
123
|
+
export const runtimeProvisioningSchema = openMap(
|
|
124
|
+
'[workspace.config.runtimeProvisioning]',
|
|
125
|
+
runtimeProvisioningEntrySchema,
|
|
126
|
+
{ keys: 'nonblank' },
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
export const vercelSandboxConfigSchema = z
|
|
130
|
+
.strictObject({
|
|
131
|
+
image: vercelUniversalImageSchema.default('vercel/sandbox/universal:latest'),
|
|
132
|
+
vcpus: z.number().int().min(1).max(32).default(4),
|
|
133
|
+
timeoutSeconds: z.number().int().min(60).max(86_400),
|
|
134
|
+
/** Deadline for each provider acknowledgement; distinct from VM lifetime. */
|
|
135
|
+
operationTimeoutMs: z.number().int().min(1_000).max(300_000).default(30_000),
|
|
136
|
+
/** Billing safety net: snapshots created for this environment expire after
|
|
137
|
+
* this duration instead of the provider's 30-day default. Absent means the
|
|
138
|
+
* provider default. */
|
|
139
|
+
snapshotExpirationSeconds: z.number().int().min(300).max(2_592_000).optional(),
|
|
140
|
+
region: z.string().min(1).optional(),
|
|
141
|
+
failoverRegions: z.array(z.string().min(1)).default([]),
|
|
142
|
+
environmentVariables: z.array(envNameSchema).default([]),
|
|
143
|
+
provisioning: z.array(vercelProvisioningStepSchema).default([]),
|
|
144
|
+
runtimeProvisioning: runtimeProvisioningSchema,
|
|
145
|
+
gitUsernameEnv: envNameSchema.optional(),
|
|
146
|
+
gitPasswordEnv: envNameSchema.optional(),
|
|
147
|
+
})
|
|
148
|
+
.superRefine((value, ctx) => {
|
|
149
|
+
const unique = (entries: readonly string[], path: string) => {
|
|
150
|
+
const seen = new Set<string>()
|
|
151
|
+
entries.forEach((entry, index) => {
|
|
152
|
+
if (seen.has(entry))
|
|
153
|
+
ctx.addIssue({
|
|
154
|
+
code: 'custom',
|
|
155
|
+
path: [path, index],
|
|
156
|
+
message: `duplicate environment variable ${JSON.stringify(entry)}`,
|
|
157
|
+
})
|
|
158
|
+
seen.add(entry)
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
unique(value.environmentVariables, 'environmentVariables')
|
|
162
|
+
unique(value.failoverRegions, 'failoverRegions')
|
|
163
|
+
const provisioningNames = new Map<string, number>()
|
|
164
|
+
value.provisioning.forEach((step, index) => {
|
|
165
|
+
const first = provisioningNames.get(step.name)
|
|
166
|
+
if (first !== undefined) {
|
|
167
|
+
ctx.addIssue({
|
|
168
|
+
code: 'custom',
|
|
169
|
+
path: ['provisioning', index, 'name'],
|
|
170
|
+
message: `duplicate provisioning step name ${JSON.stringify(step.name)} — first declared at provisioning[${first}].name`,
|
|
171
|
+
})
|
|
172
|
+
} else {
|
|
173
|
+
provisioningNames.set(step.name, index)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
if (value.region !== undefined && value.failoverRegions.includes(value.region)) {
|
|
177
|
+
ctx.addIssue({
|
|
178
|
+
code: 'custom',
|
|
179
|
+
path: ['failoverRegions'],
|
|
180
|
+
message: 'failover regions must not include the primary region',
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
if ((value.gitUsernameEnv === undefined) !== (value.gitPasswordEnv === undefined)) {
|
|
184
|
+
ctx.addIssue({
|
|
185
|
+
code: 'custom',
|
|
186
|
+
path: ['gitPasswordEnv'],
|
|
187
|
+
message: 'gitUsernameEnv and gitPasswordEnv must be configured together',
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
for (const [key, name] of [
|
|
191
|
+
['gitUsernameEnv', value.gitUsernameEnv],
|
|
192
|
+
['gitPasswordEnv', value.gitPasswordEnv],
|
|
193
|
+
] as const) {
|
|
194
|
+
if (name !== undefined && value.environmentVariables.includes(name)) {
|
|
195
|
+
ctx.addIssue({
|
|
196
|
+
code: 'custom',
|
|
197
|
+
path: [key],
|
|
198
|
+
message:
|
|
199
|
+
'private repository clone credentials must not be exposed as runtime environment variables',
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
if (
|
|
203
|
+
name !== undefined &&
|
|
204
|
+
/^(?:GITHUB_TOKEN|GH_TOKEN|VERCEL_(?:TOKEN|OIDC_TOKEN|TEAM_ID|PROJECT_ID))$/.test(name)
|
|
205
|
+
) {
|
|
206
|
+
ctx.addIssue({
|
|
207
|
+
code: 'custom',
|
|
208
|
+
path: [key],
|
|
209
|
+
message:
|
|
210
|
+
'private repository read credentials must be separate from Forge and Vercel credentials',
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
type NormalizedVercelSandboxConfig = z.infer<typeof vercelSandboxConfigSchema>
|
|
216
|
+
/** Direct provider construction remains source-compatible; schema-parsed
|
|
217
|
+
* production config always materializes operationTimeoutMs. */
|
|
218
|
+
export type VercelSandboxConfig = Omit<
|
|
219
|
+
NormalizedVercelSandboxConfig,
|
|
220
|
+
'operationTimeoutMs' | 'provisioning' | 'runtimeProvisioning'
|
|
221
|
+
> & {
|
|
222
|
+
operationTimeoutMs?: number
|
|
223
|
+
/** Optional only for source compatibility with direct adapter construction;
|
|
224
|
+
* schema-loaded repository configuration always materializes this list. */
|
|
225
|
+
provisioning?: VercelProvisioningStep[]
|
|
226
|
+
/** Optional only for source compatibility in direct adapter construction;
|
|
227
|
+
* parsed production configuration always materializes this map. */
|
|
228
|
+
runtimeProvisioning?: NormalizedVercelSandboxConfig['runtimeProvisioning']
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Workspace selector. The host validates the selector envelope; plugin nested
|
|
232
|
+
* config remains open, while built-in config is validated by configSchema. */
|
|
233
|
+
export const workspaceSchema = z.strictObject({
|
|
234
|
+
provider: z
|
|
235
|
+
.string()
|
|
236
|
+
.refine(
|
|
237
|
+
(value) => value.trim().length > 0,
|
|
238
|
+
'[workspace].provider must be a nonblank provider name',
|
|
239
|
+
)
|
|
240
|
+
.default('git-worktree'),
|
|
241
|
+
config: openMap('[workspace.config]', z.unknown(), { keys: 'any' }),
|
|
242
|
+
})
|
|
243
|
+
export type WorkspaceConfig = z.infer<typeof workspaceSchema>
|
|
244
|
+
|
|
245
|
+
// ── [commands] ───────────────────────────────────────────────────────────────
|
|
246
|
+
//
|
|
247
|
+
// Open map of deterministic verbs the kernel may run. `setup`, `lint`,
|
|
248
|
+
// `typecheck`, `test` are conventions, not required keys (§16.1).
|
|
249
|
+
|
|
250
|
+
export const commandsSchema = openMap('[commands]', z.string().min(1))
|
|
251
|
+
export type Commands = z.infer<typeof commandsSchema>
|
|
252
|
+
|
|
253
|
+
// ── [verify.<step>] ──────────────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The intentionally small path-selector grammar (§16.1): positive,
|
|
257
|
+
* repository-relative globs with literal characters, `*`, `?`, and `**` only
|
|
258
|
+
* as a complete path segment. Reject every ambiguous/unsafe form at config
|
|
259
|
+
* load rather than allowing a typo to become a never-running safety gate.
|
|
260
|
+
*/
|
|
261
|
+
export function verifyPathGlobError(pattern: string): string | undefined {
|
|
262
|
+
if (pattern.length === 0) return 'path selectors must be nonempty'
|
|
263
|
+
if (pattern.includes('\0')) return 'path selectors must not contain NUL bytes'
|
|
264
|
+
if (pattern.startsWith('/') || /^[A-Za-z]:\//.test(pattern)) {
|
|
265
|
+
return 'path selectors must be repository-relative, not absolute'
|
|
266
|
+
}
|
|
267
|
+
if (pattern.includes('\\')) {
|
|
268
|
+
return 'path selectors use Git-style "/" separators; backslashes and escapes are unsupported'
|
|
269
|
+
}
|
|
270
|
+
if (pattern.startsWith('!')) {
|
|
271
|
+
return 'path selectors are positive globs; negation is unsupported'
|
|
272
|
+
}
|
|
273
|
+
if (/[[\]{}]/.test(pattern)) {
|
|
274
|
+
return 'path selectors support only literal characters, *, ?, and whole-segment **; character classes and brace expansion are unsupported'
|
|
275
|
+
}
|
|
276
|
+
if (/[?*+@!]\(/.test(pattern)) {
|
|
277
|
+
return 'path selectors support only literal characters, *, ?, and whole-segment **; extglobs are unsupported'
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const segments = pattern.split('/')
|
|
281
|
+
if (segments.some((segment) => segment.length === 0)) {
|
|
282
|
+
return 'path selectors must not contain empty path segments'
|
|
283
|
+
}
|
|
284
|
+
if (segments.some((segment) => segment === '.' || segment === '..')) {
|
|
285
|
+
return 'path selectors must not contain "." or ".." traversal segments'
|
|
286
|
+
}
|
|
287
|
+
if (segments.some((segment) => segment.includes('**') && segment !== '**')) {
|
|
288
|
+
return 'the ** wildcard must occupy a complete path segment'
|
|
289
|
+
}
|
|
290
|
+
return undefined
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export const verifyPathGlobSchema = z.string().superRefine((pattern, ctx) => {
|
|
294
|
+
const message = verifyPathGlobError(pattern)
|
|
295
|
+
if (message !== undefined) ctx.addIssue({ code: 'custom', message })
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
const verifyApplicabilityShape = {
|
|
299
|
+
/** Any-match positive path selectors; absent means unconditional. */
|
|
300
|
+
paths: z
|
|
301
|
+
.array(verifyPathGlobSchema)
|
|
302
|
+
.min(1, 'paths must contain at least one repository-relative glob')
|
|
303
|
+
.optional(),
|
|
304
|
+
/** Explicit mandatory-gate guard: true takes precedence over paths and a
|
|
305
|
+
* plan may not deselect the step. */
|
|
306
|
+
always: z.boolean().optional(),
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export const verifyCheckStepSchema = z.strictObject({
|
|
310
|
+
/** Deterministic: command + pass/fail (§16.1). */
|
|
311
|
+
kind: z.literal('check'),
|
|
312
|
+
/** Ref into [commands] — cross-validated below. */
|
|
313
|
+
command: z.string().min(1),
|
|
314
|
+
...verifyApplicabilityShape,
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
export const verifyAgentStepSchema = z.strictObject({
|
|
318
|
+
/** Agent-verify: skill + verdict schema (§16.1). */
|
|
319
|
+
kind: z.literal('agent'),
|
|
320
|
+
skill: z.string().min(1),
|
|
321
|
+
...verifyApplicabilityShape,
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
export const verifyStepConfigSchema = z.discriminatedUnion('kind', [
|
|
325
|
+
verifyCheckStepSchema,
|
|
326
|
+
verifyAgentStepSchema,
|
|
327
|
+
])
|
|
328
|
+
export type VerifyStepConfig = z.infer<typeof verifyStepConfigSchema>
|
|
329
|
+
|
|
330
|
+
export interface VerifyConfig {
|
|
331
|
+
/** Step order — each entry names a `verify:<step>` phase (§15.2.1). */
|
|
332
|
+
steps: string[]
|
|
333
|
+
/** The [verify.<step>] tables, keyed by step name. */
|
|
334
|
+
stepConfigs: Record<string, VerifyStepConfig>
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* [verify] mixes one known key (`steps`) with an open set of per-step
|
|
339
|
+
* subtables, so it cannot be a strictObject; instead every non-`steps` key is
|
|
340
|
+
* validated as a step table — nothing passes through loosely, and no step name
|
|
341
|
+
* is lost to the prototype chain (see `stepSection`).
|
|
342
|
+
*/
|
|
343
|
+
export const verifySectionSchema = stepSection(
|
|
344
|
+
'[verify]',
|
|
345
|
+
z.array(z.string().min(1)).default([]),
|
|
346
|
+
verifyStepConfigSchema,
|
|
347
|
+
(steps, stepConfigs): VerifyConfig => ({ steps, stepConfigs }),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
// ── [finalize.<step>] ────────────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
/** Finalize steps deliberately omit verify-only applicability fields. */
|
|
353
|
+
export const finalizeCheckStepSchema = z.strictObject({
|
|
354
|
+
kind: z.literal('check'),
|
|
355
|
+
/** Ref into [commands] — cross-validated below. */
|
|
356
|
+
command: z.string().min(1),
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
export const finalizeAgentStepSchema = z.strictObject({
|
|
360
|
+
kind: z.literal('agent'),
|
|
361
|
+
/** Exact installed skill name passed to the configured runtime. */
|
|
362
|
+
skill: z.string().min(1),
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
export const finalizeStepConfigSchema = z.discriminatedUnion('kind', [
|
|
366
|
+
finalizeCheckStepSchema,
|
|
367
|
+
finalizeAgentStepSchema,
|
|
368
|
+
])
|
|
369
|
+
export type FinalizeStepConfig = z.infer<typeof finalizeStepConfigSchema>
|
|
370
|
+
|
|
371
|
+
export interface FinalizeConfig {
|
|
372
|
+
/** Ordered, failure-tolerant post-steps (§5). */
|
|
373
|
+
steps: string[]
|
|
374
|
+
/** The [finalize.<step>] tables, keyed by logical step name. */
|
|
375
|
+
stepConfigs: Record<string, FinalizeStepConfig>
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Like [verify], [finalize] mixes `steps` with a strict named table set. */
|
|
379
|
+
export const finalizeSectionSchema = stepSection(
|
|
380
|
+
'[finalize]',
|
|
381
|
+
z.array(z.string().min(1, 'finalize.steps entries must be nonempty step names')).default([]),
|
|
382
|
+
finalizeStepConfigSchema,
|
|
383
|
+
(steps, stepConfigs): FinalizeConfig => ({ steps, stepConfigs }),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
// ── [roles] ──────────────────────────────────────────────────────────────────
|
|
387
|
+
//
|
|
388
|
+
// Open map: role → fields on the runtime, model, and args axes (SPEC §9,
|
|
389
|
+
// §16.1). The reserved `default` entry is the raw inheritance base and must
|
|
390
|
+
// name a runtime; registry-aware enforcement lives in the eager resolver;
|
|
391
|
+
// every other role overrides it independently per field. Registry-dependent
|
|
392
|
+
// compatibility validation happens in the eager runtime resolver, because the
|
|
393
|
+
// config loader does not know the injected runtime registry.
|
|
394
|
+
|
|
395
|
+
const roleArgsSchema = z.array(z.string().min(1))
|
|
396
|
+
|
|
397
|
+
const runtimeAxesSchema = z.strictObject({
|
|
398
|
+
runtime: z.string().min(1).optional(),
|
|
399
|
+
model: z.string().min(1).optional(),
|
|
400
|
+
args: roleArgsSchema.optional(),
|
|
401
|
+
// Deprecated compatibility field. It remains schema-valid but is a warning-only no-op.
|
|
402
|
+
extensions: z.array(z.string().min(1)).optional(),
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
export const roleSchema = runtimeAxesSchema.extend({
|
|
406
|
+
// Wall-clock budget for each session dispatched through this logical role.
|
|
407
|
+
// Absent ⇒ inherit [roles.default], then [policy].sessionBudgetSeconds.
|
|
408
|
+
// Alternates deliberately use runtimeAxesSchema: one logical role owns one
|
|
409
|
+
// budget regardless of which provider target executes the attempt.
|
|
410
|
+
sessionBudgetSeconds: z.number().int().positive().optional(),
|
|
411
|
+
// Extra CLI arguments. Absent ⇒ inherit [roles.default].args; a declared
|
|
412
|
+
// list, including [], replaces the default wholesale rather than unioning.
|
|
413
|
+
args: roleArgsSchema.optional(),
|
|
414
|
+
// Deprecated compatibility field. It is accepted only so dispatch can emit
|
|
415
|
+
// a migration diagnostic; it has no effect on runtime resolution.
|
|
416
|
+
extensions: z.array(z.string().min(1)).optional(),
|
|
417
|
+
// Ordered failure-triggered execution targets. Absent ⇒ inherit the base
|
|
418
|
+
// list; a declared list (including []) replaces it wholesale. Each strict
|
|
419
|
+
// entry overlays the concrete role's resolved primary axes.
|
|
420
|
+
alternates: z.array(runtimeAxesSchema).optional(),
|
|
421
|
+
})
|
|
422
|
+
export type RoleConfig = z.infer<typeof roleSchema>
|
|
423
|
+
|
|
424
|
+
// ── [policy] ─────────────────────────────────────────────────────────────────
|
|
425
|
+
|
|
426
|
+
export const policySchema = z.strictObject({
|
|
427
|
+
/** Wall-clock bound for an agent phase session; roles may override it. */
|
|
428
|
+
sessionBudgetSeconds: z.number().int().positive().default(3600),
|
|
429
|
+
/** Same-finding survival threshold before auto-escalate (§10, §15.4). */
|
|
430
|
+
stallRounds: z.number().int().positive().default(3),
|
|
431
|
+
maxVerifyAttempts: z.number().int().positive().default(3),
|
|
432
|
+
/** Consecutive workspace setup failures before human intervention is required. */
|
|
433
|
+
maxSetupAttempts: z.number().int().positive().default(3),
|
|
434
|
+
/** Consecutive provider lifecycle failures before recovery requires a human retry. */
|
|
435
|
+
maxInfrastructureAttempts: z.number().int().positive().default(3),
|
|
436
|
+
maxReconcileAttempts: z.number().int().positive().default(3),
|
|
437
|
+
/** converge's `maxRounds` for the review loops (SPEC §10). */
|
|
438
|
+
maxReviewRounds: z.number().int().positive().default(6),
|
|
439
|
+
/** Unclaimed observation occurrences required to start one harvest run. */
|
|
440
|
+
harvestThreshold: z.number().int().positive().default(5),
|
|
441
|
+
/** Merged builds after the oldest observation that start harvest; zero disables drift. */
|
|
442
|
+
harvestMaxDrift: z.number().int().nonnegative().default(3),
|
|
443
|
+
})
|
|
444
|
+
export type PolicyConfig = z.infer<typeof policySchema>
|
|
445
|
+
|
|
446
|
+
// ── [tickets] ────────────────────────────────────────────────────────────────
|
|
447
|
+
//
|
|
448
|
+
// Which TicketSource the dispatcher drives and the source-vocabulary states
|
|
449
|
+
// that govern its lifecycle (§3.2, §3.3, §13). Declarative only: the Linear
|
|
450
|
+
// API key comes from the LINEAR_API_KEY environment variable, never from this
|
|
451
|
+
// file.
|
|
452
|
+
|
|
453
|
+
export const ticketsSchema = z.strictObject({
|
|
454
|
+
/** Builtin or plugin-registered TicketSource name. Registry membership is
|
|
455
|
+
* validated after configured plugins load. */
|
|
456
|
+
source: z
|
|
457
|
+
.string()
|
|
458
|
+
.refine(
|
|
459
|
+
(value) => value.trim().length > 0,
|
|
460
|
+
'[tickets].source must be a nonblank builtin or plugin ticket source name',
|
|
461
|
+
),
|
|
462
|
+
/**
|
|
463
|
+
* Ticket labels that additionally narrow the mandatory readyState gate
|
|
464
|
+
* (§3.3). A nonempty list is conjunctive: every configured label must be
|
|
465
|
+
* present. For readyLabels = ["autobuild", "ready"], a ticket carrying only
|
|
466
|
+
* "autobuild" does not satisfy the label gate. An explicit [] disables the
|
|
467
|
+
* label gate; absent uses the ticket source's default (linear: ["autobuild"];
|
|
468
|
+
* file: none). Resolved by readyCriteria in src/processes/dispatcher.ts.
|
|
469
|
+
*/
|
|
470
|
+
readyLabels: z.array(z.string().min(1)).optional(),
|
|
471
|
+
/**
|
|
472
|
+
* The single workflow state a ticket must sit in to be dispatchable — the
|
|
473
|
+
* mandatory ready gate (§3.3). Required and non-blank: without it every
|
|
474
|
+
* ticket from the source would be eligible in any state, including completed
|
|
475
|
+
* ones (the AUT-10 mis-gating). Applied by readyCriteria on top of any label
|
|
476
|
+
* gate. Linear matches exactly and case-sensitively; the file source
|
|
477
|
+
* canonicalizes it to a state directory (`ready` → `ready/`).
|
|
478
|
+
*/
|
|
479
|
+
readyState: z
|
|
480
|
+
.string({
|
|
481
|
+
error:
|
|
482
|
+
'[tickets].readyState is required — name the one workflow state a ticket must sit in to be dispatched (e.g. "ready"). Omitting it would make every ticket from the source eligible, including completed ones.',
|
|
483
|
+
})
|
|
484
|
+
.refine(
|
|
485
|
+
(s) => s.trim().length > 0,
|
|
486
|
+
'[tickets].readyState must not be blank — name the one workflow state a ticket must sit in to be dispatched (e.g. "ready").',
|
|
487
|
+
),
|
|
488
|
+
/** Backend team key (e.g. "ENG") — required for linear and hosted. */
|
|
489
|
+
teamKey: z.string().min(1).optional(),
|
|
490
|
+
/** Workflow state claim() moves an issue to (§12); Linear and hosted. */
|
|
491
|
+
claimedState: z.string().min(1).optional(),
|
|
492
|
+
/** State create() files new tickets into. Absent = the provider's default
|
|
493
|
+
* (Linear: the team's default state, e.g. Backlog; file: Triage). */
|
|
494
|
+
createState: z.string().min(1).optional(),
|
|
495
|
+
/** State the dispatcher hands tickets back to for human triage — spec-gate
|
|
496
|
+
* bounces (§6.3), aborted builds, closed-unmerged PRs. Absent = the
|
|
497
|
+
* provider's default (Linear: Backlog; file: Triage) — resolved by
|
|
498
|
+
* defaultTriageState in src/processes/dispatcher.ts. */
|
|
499
|
+
triageState: z.string().min(1).optional(),
|
|
500
|
+
/** State harvest files its synthesized proposals into (§12). Absent = the
|
|
501
|
+
* triage state, which keeps the constitutional grooming gate: a proposal
|
|
502
|
+
* waits for a human before it can be dispatched. Setting it to readyState
|
|
503
|
+
* waives that gate for this repository — proposals dispatch unread — and is
|
|
504
|
+
* the only supported way to do so. Separate from triageState so the waiver
|
|
505
|
+
* does not also redirect spec-gate bounces, aborts, and closed-unmerged PRs
|
|
506
|
+
* into the ready state, where a bounce would re-dispatch itself. Resolved by
|
|
507
|
+
* defaultProposalState in src/processes/dispatcher.ts. */
|
|
508
|
+
proposalState: z.string().min(1).optional(),
|
|
509
|
+
/** Directory of state dirs (`triage/ ready/ doing/ done/`) holding `<id>.md`
|
|
510
|
+
* ticket files; optional — defaults to `.autobuild/tickets`, resolved
|
|
511
|
+
* relative to the repo. Kept schema-optional (not `.default()`) so the
|
|
512
|
+
* linear-only cross-validation below stays meaningful and the factory can
|
|
513
|
+
* still tell a defaulted dir from an explicit one. */
|
|
514
|
+
dir: z.string().min(1).optional(),
|
|
515
|
+
})
|
|
516
|
+
export type TicketsConfig = z.infer<typeof ticketsSchema>
|
|
517
|
+
|
|
518
|
+
// ── [orchestrator] ──────────────────────────────────────────────────────────
|
|
519
|
+
//
|
|
520
|
+
// The operator-agent gate (AUT-340). Absent table ≡ disabled: with `enabled =
|
|
521
|
+
// false` (the default) the sandbox registry tools are absent from every
|
|
522
|
+
// binding and no environment is ever provisioned.
|
|
523
|
+
|
|
524
|
+
export const orchestratorSandboxSchema = z.strictObject({
|
|
525
|
+
/** Idle minutes without a sandbox tool call before the dispatcher tick's
|
|
526
|
+
* janitor stops the environment, keeping its snapshot for the next resume. */
|
|
527
|
+
idleMinutes: z.number().int().positive().default(30),
|
|
528
|
+
/** Names of non-secret host environment variables forwarded into the
|
|
529
|
+
* sandbox (during provisioning/setup and tool exec/start alike). These are
|
|
530
|
+
* the ONLY non-toolchain environment the sandbox ever sees; forwarding any
|
|
531
|
+
* store, forge, ticket-provider, model, or Vercel credential name is a
|
|
532
|
+
* config error. Unset means none. */
|
|
533
|
+
environmentVariables: z.array(z.string().min(1)).default([]),
|
|
534
|
+
})
|
|
535
|
+
export type OrchestratorSandboxConfig = z.infer<typeof orchestratorSandboxSchema>
|
|
536
|
+
|
|
537
|
+
export const orchestratorSchema = z.strictObject({
|
|
538
|
+
enabled: z.boolean().default(false),
|
|
539
|
+
sandbox: orchestratorSandboxSchema.prefault({}),
|
|
540
|
+
})
|
|
541
|
+
export type OrchestratorConfig = z.infer<typeof orchestratorSchema>
|
|
542
|
+
|
|
543
|
+
// ── Whole file ───────────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
const configRootSchema = z.strictObject({
|
|
546
|
+
/** Base branch builds cut from and target (§16.1). */
|
|
547
|
+
baseBranch: z.string().min(1).default('main'),
|
|
548
|
+
/** Concurrent builds for this repository (§16.1). */
|
|
549
|
+
capacity: z.number().int().positive().default(1),
|
|
550
|
+
/** Forge adapter selected from builtins and configured plugins. */
|
|
551
|
+
forge: z
|
|
552
|
+
.string()
|
|
553
|
+
.refine((value) => value.trim().length > 0, 'forge adapter name must be nonblank')
|
|
554
|
+
.default('github'),
|
|
555
|
+
/** Trusted in-process plugin modules, loaded in declaration order from the
|
|
556
|
+
* consuming repository before production adapters are constructed. Exact
|
|
557
|
+
* specifier strings must be unique (cross-validated below). */
|
|
558
|
+
plugins: z
|
|
559
|
+
.array(
|
|
560
|
+
z
|
|
561
|
+
.string()
|
|
562
|
+
.refine((value) => value.trim().length > 0, 'plugin module specifiers must be nonblank'),
|
|
563
|
+
)
|
|
564
|
+
.default([]),
|
|
565
|
+
pr: prSchema.optional(),
|
|
566
|
+
workspace: workspaceSchema.prefault({}),
|
|
567
|
+
commands: commandsSchema,
|
|
568
|
+
verify: verifySectionSchema,
|
|
569
|
+
finalize: finalizeSectionSchema,
|
|
570
|
+
// `default` is reserved inside this open map (§9). The schema preserves the
|
|
571
|
+
// raw map — EVERY declared key, `__proto__` included, or an entry would
|
|
572
|
+
// disappear ahead of both eager resolution and the unconsumed-key warning;
|
|
573
|
+
// the registry-aware eager resolver requires default.runtime and can
|
|
574
|
+
// therefore include the complete materialized runtime list in its diagnostic.
|
|
575
|
+
roles: openMap('[roles]', roleSchema),
|
|
576
|
+
policy: policySchema.prefault({}),
|
|
577
|
+
orchestrator: orchestratorSchema.prefault({}),
|
|
578
|
+
// An absent [tickets] table must NOT silently default past the mandatory
|
|
579
|
+
// ready gate. Prefault feeds the file-source identity through ticketsSchema,
|
|
580
|
+
// which deliberately fails on missing `readyState` at `tickets.readyState`.
|
|
581
|
+
// The cast only satisfies prefault's input type; `{ source: 'file' }` is
|
|
582
|
+
// intentionally invalid until the repository names its ready state.
|
|
583
|
+
tickets: ticketsSchema.prefault({
|
|
584
|
+
source: 'file',
|
|
585
|
+
} as z.input<typeof ticketsSchema>),
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
/** Root metadata keeps strict error prose and documentation coverage honest. */
|
|
589
|
+
export const TOP_LEVEL_SCALARS = ['baseBranch', 'capacity', 'forge', 'plugins'] as const
|
|
590
|
+
export const TOP_LEVEL_TABLES = Object.keys(configRootSchema.shape).filter(
|
|
591
|
+
(key) => !(TOP_LEVEL_SCALARS as readonly string[]).includes(key),
|
|
592
|
+
)
|
|
593
|
+
export const TOP_LEVEL_KEYS = Object.keys(configRootSchema.shape)
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Cross-validation (§16.1): errors carry the path and what would be accepted,
|
|
597
|
+
* because validation failures are feedback to whoever edits the file (D6
|
|
598
|
+
* discipline applied to config).
|
|
599
|
+
*/
|
|
600
|
+
export const configSchema = configRootSchema.superRefine((config, ctx) => {
|
|
601
|
+
const firstPluginIndex = new Map<string, number>()
|
|
602
|
+
config.plugins.forEach((specifier, index) => {
|
|
603
|
+
const firstIndex = firstPluginIndex.get(specifier)
|
|
604
|
+
if (firstIndex === undefined) {
|
|
605
|
+
firstPluginIndex.set(specifier, index)
|
|
606
|
+
return
|
|
607
|
+
}
|
|
608
|
+
ctx.addIssue({
|
|
609
|
+
code: 'custom',
|
|
610
|
+
path: ['plugins', index],
|
|
611
|
+
message:
|
|
612
|
+
`duplicate plugin module specifier ${JSON.stringify(specifier)} — first declared at ` +
|
|
613
|
+
`plugins[${firstIndex}]; remove or deduplicate this entry`,
|
|
614
|
+
})
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
if (
|
|
618
|
+
config.workspace.provider === 'git-worktree' &&
|
|
619
|
+
Object.keys(config.workspace.config).length > 0
|
|
620
|
+
) {
|
|
621
|
+
ctx.addIssue({
|
|
622
|
+
code: 'custom',
|
|
623
|
+
path: ['workspace', 'config'],
|
|
624
|
+
message:
|
|
625
|
+
'[workspace.config] is not supported by the builtin "git-worktree" provider — remove it or select a plugin workspace provider',
|
|
626
|
+
})
|
|
627
|
+
} else if (config.workspace.provider === 'vercel-sandbox') {
|
|
628
|
+
const parsed = vercelSandboxConfigSchema.safeParse(config.workspace.config)
|
|
629
|
+
if (!parsed.success) {
|
|
630
|
+
forwardIssues(parsed.error.issues, ctx, ['workspace', 'config'])
|
|
631
|
+
} else if (
|
|
632
|
+
config.roles !== undefined &&
|
|
633
|
+
config.verify !== undefined &&
|
|
634
|
+
config.finalize !== undefined
|
|
635
|
+
) {
|
|
636
|
+
for (const group of effectiveRuntimeReferences(config)) {
|
|
637
|
+
if (Object.hasOwn(parsed.data.runtimeProvisioning, group.runtime)) continue
|
|
638
|
+
ctx.addIssue({
|
|
639
|
+
code: 'custom',
|
|
640
|
+
path: ['workspace', 'config', 'runtimeProvisioning', group.runtime],
|
|
641
|
+
message:
|
|
642
|
+
`runtime ${displayName(group.runtime)} is selected by ${group.references.join(', ')} but has no sandbox provisioning; add ` +
|
|
643
|
+
`[workspace.config.runtimeProvisioning.${tomlKey(group.runtime)}] with nonblank install and preflight commands`,
|
|
644
|
+
})
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const commandNames = Object.keys(config.commands)
|
|
650
|
+
const knownCommands =
|
|
651
|
+
commandNames.length > 0
|
|
652
|
+
? `known commands: ${commandNames.join(', ')}`
|
|
653
|
+
: '[commands] has no entries'
|
|
654
|
+
|
|
655
|
+
// A failed transformed subsection can be absent from Zod's partial object
|
|
656
|
+
// while sibling issues are collected. Guard the cross-check so malformed
|
|
657
|
+
// tables report validation feedback instead of throwing inside validation.
|
|
658
|
+
if (config.verify?.stepConfigs !== undefined) {
|
|
659
|
+
config.verify.steps.forEach((step, index) => {
|
|
660
|
+
if (!Object.hasOwn(config.verify.stepConfigs, step)) {
|
|
661
|
+
ctx.addIssue({
|
|
662
|
+
code: 'custom',
|
|
663
|
+
path: ['verify', 'steps', index],
|
|
664
|
+
message:
|
|
665
|
+
`verify step "${step}" is listed in verify.steps but has no [verify.${step}] table — ` +
|
|
666
|
+
`add one with kind = "check" (command = <name in [commands]>) or kind = "agent" (skill = <skill name>)`,
|
|
667
|
+
})
|
|
668
|
+
}
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
const listed = new Set(config.verify.steps)
|
|
672
|
+
for (const [step, stepConfig] of Object.entries(config.verify.stepConfigs)) {
|
|
673
|
+
if (!listed.has(step)) {
|
|
674
|
+
ctx.addIssue({
|
|
675
|
+
code: 'custom',
|
|
676
|
+
path: ['verify', step],
|
|
677
|
+
message:
|
|
678
|
+
`[verify.${step}] is defined but "${step}" is not listed in verify.steps — ` +
|
|
679
|
+
`add "${step}" to verify.steps or remove the table`,
|
|
680
|
+
})
|
|
681
|
+
}
|
|
682
|
+
if (stepConfig.kind === 'check' && !Object.hasOwn(config.commands, stepConfig.command)) {
|
|
683
|
+
ctx.addIssue({
|
|
684
|
+
code: 'custom',
|
|
685
|
+
path: ['verify', step, 'command'],
|
|
686
|
+
message: `[verify.${step}].command = "${stepConfig.command}" does not name a key in [commands] — ${knownCommands}`,
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (config.finalize?.stepConfigs !== undefined) {
|
|
693
|
+
config.finalize.steps.forEach((step, index) => {
|
|
694
|
+
if (!Object.hasOwn(config.finalize.stepConfigs, step)) {
|
|
695
|
+
ctx.addIssue({
|
|
696
|
+
code: 'custom',
|
|
697
|
+
path: ['finalize', 'steps', index],
|
|
698
|
+
message:
|
|
699
|
+
`finalize step "${step}" is listed in finalize.steps but has no [finalize.${step}] table — ` +
|
|
700
|
+
`add one with kind = "check" (command = <name in [commands]>) or kind = "agent" (skill = <skill name>)`,
|
|
701
|
+
})
|
|
702
|
+
}
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
const listedFinalize = new Set(config.finalize.steps)
|
|
706
|
+
for (const [step, stepConfig] of Object.entries(config.finalize.stepConfigs)) {
|
|
707
|
+
if (!listedFinalize.has(step)) {
|
|
708
|
+
ctx.addIssue({
|
|
709
|
+
code: 'custom',
|
|
710
|
+
path: ['finalize', step],
|
|
711
|
+
message:
|
|
712
|
+
`[finalize.${step}] is defined but "${step}" is not listed in finalize.steps — ` +
|
|
713
|
+
`add "${step}" to finalize.steps or remove the table`,
|
|
714
|
+
})
|
|
715
|
+
}
|
|
716
|
+
if (stepConfig.kind === 'check' && !Object.hasOwn(config.commands, stepConfig.command)) {
|
|
717
|
+
ctx.addIssue({
|
|
718
|
+
code: 'custom',
|
|
719
|
+
path: ['finalize', step, 'command'],
|
|
720
|
+
message: `[finalize.${step}].command = "${stepConfig.command}" does not name a key in [commands] — ${knownCommands}`,
|
|
721
|
+
})
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const tickets = config.tickets
|
|
727
|
+
if (tickets.source === 'linear' || tickets.source === 'hosted') {
|
|
728
|
+
if (tickets.teamKey === undefined) {
|
|
729
|
+
ctx.addIssue({
|
|
730
|
+
code: 'custom',
|
|
731
|
+
path: ['tickets', 'teamKey'],
|
|
732
|
+
message: `[tickets].source = "${tickets.source}" requires teamKey${
|
|
733
|
+
tickets.source === 'linear' ? ' — the Linear team key (e.g. "ENG")' : ''
|
|
734
|
+
}`,
|
|
735
|
+
})
|
|
736
|
+
}
|
|
737
|
+
if (tickets.dir !== undefined) {
|
|
738
|
+
ctx.addIssue({
|
|
739
|
+
code: 'custom',
|
|
740
|
+
path: ['tickets', 'dir'],
|
|
741
|
+
message: '[tickets].dir applies only to source = "file" — remove it or set source = "file"',
|
|
742
|
+
})
|
|
743
|
+
}
|
|
744
|
+
} else if (tickets.source === 'file') {
|
|
745
|
+
// dir is optional for the file source: absent = .autobuild/tickets.
|
|
746
|
+
for (const key of ['teamKey', 'claimedState'] as const) {
|
|
747
|
+
if (tickets[key] !== undefined) {
|
|
748
|
+
ctx.addIssue({
|
|
749
|
+
code: 'custom',
|
|
750
|
+
path: ['tickets', key],
|
|
751
|
+
message: `[tickets].${key} applies only to source = "linear" or "hosted" — remove it or select one of those sources`,
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
// Plugin sources receive the existing ticket lifecycle/configuration fields
|
|
757
|
+
// unchanged; adapter-specific validation belongs to their factory.
|
|
758
|
+
|
|
759
|
+
// The credential-free sandbox rule (AUT-340): a forwarded variable name may
|
|
760
|
+
// never name a store, forge, ticket-provider, model, or Vercel credential.
|
|
761
|
+
if (config.orchestrator.enabled) {
|
|
762
|
+
config.orchestrator.sandbox.environmentVariables.forEach((name, index) => {
|
|
763
|
+
if (SANDBOX_FORBIDDEN_ENV.includes(name)) {
|
|
764
|
+
ctx.addIssue({
|
|
765
|
+
code: 'custom',
|
|
766
|
+
path: ['orchestrator', 'sandbox', 'environmentVariables', index],
|
|
767
|
+
message: `environment variable ${JSON.stringify(name)} is a store, forge, ticket-provider, model, or Vercel credential and may never be forwarded into an operator sandbox`,
|
|
768
|
+
})
|
|
769
|
+
}
|
|
770
|
+
})
|
|
771
|
+
}
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
export type Config = z.infer<typeof configSchema>
|