@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,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role-key consumability diagnostics (SPEC §9).
|
|
3
|
+
*
|
|
4
|
+
* `[roles]` is an open map, so a `[roles.<name>]` entry whose name nothing
|
|
5
|
+
* consumes is accepted, eagerly resolved, validated — and then never looked up.
|
|
6
|
+
* The session that was meant to run on it runs on `[roles.default]` instead.
|
|
7
|
+
* This module is the pure derivation that lets `ab dispatch` say so at startup.
|
|
8
|
+
* It never throws and never changes which runtime, model, or args run.
|
|
9
|
+
*
|
|
10
|
+
* TWO MECHANISMS, DELIBERATELY KEPT APART. No notice, comment, or document may
|
|
11
|
+
* say a declared key "inherits default":
|
|
12
|
+
*
|
|
13
|
+
* - *Declaration.* `createRuntimeResolver` eagerly resolves EVERY declared role
|
|
14
|
+
* by merging its own fields over `default` independently per field
|
|
15
|
+
* (`src/ports/runner/routing.ts`). `[roles.ghost]` with its own runtime and
|
|
16
|
+
* model resolves to exactly that pair, and is validated — an unconsumed role
|
|
17
|
+
* naming an unregistered runtime still fails `ab dispatch` loudly. The
|
|
18
|
+
* warnings here neither suppress nor compete with that error.
|
|
19
|
+
* - *Lookup.* `resolve(role, ...aliases)` returns the validated
|
|
20
|
+
* `[roles.default]` result when NO candidate key is declared. That wholesale
|
|
21
|
+
* fallback is what silently runs a verify step on the default model.
|
|
22
|
+
*
|
|
23
|
+
* The harm named here is the pair of the two: a key declared on one side and
|
|
24
|
+
* never requested on the other.
|
|
25
|
+
*/
|
|
26
|
+
import { CORE_PHASES } from '../ontology'
|
|
27
|
+
import type { Config } from './schema'
|
|
28
|
+
|
|
29
|
+
// ── Vocabulary ───────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/** `ab dispatch`'s pre-build title completion (src/cli/dispatch.ts). */
|
|
32
|
+
export const SLUG_ROLE = 'slug'
|
|
33
|
+
/** `ab upgrade`'s skill-conflict resolver (src/cli/upgrade-agent.ts). */
|
|
34
|
+
export const UPGRADE_ROLE = 'upgrade'
|
|
35
|
+
/** The harvest run's synthesize session (src/processes/harvest-runner.ts). */
|
|
36
|
+
export const HARVEST_ROLE = 'harvest'
|
|
37
|
+
/** The harvest run's review session (src/processes/harvest-runner.ts). */
|
|
38
|
+
export const HARVEST_REVIEW_ROLE = 'harvest-review'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Roles the pipeline requests outside the configured phases and steps. These
|
|
42
|
+
* are shared constants rather than bare string literals at their call sites so
|
|
43
|
+
* a consumer and this diagnostic cannot drift apart — a consumable key missing
|
|
44
|
+
* from here would become a false "unconsumed" warning, which is the same class
|
|
45
|
+
* of bug this module exists to report.
|
|
46
|
+
*/
|
|
47
|
+
export const INTERNAL_ROLES = [SLUG_ROLE, UPGRADE_ROLE, HARVEST_ROLE, HARVEST_REVIEW_ROLE] as const
|
|
48
|
+
|
|
49
|
+
/** The reserved `[roles]` inheritance base (§9). Never reported unconsumed. */
|
|
50
|
+
export const RESERVED_ROLE = 'default'
|
|
51
|
+
|
|
52
|
+
// ── Derivation ───────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/** Every consumer a declared key has, of every kind. */
|
|
55
|
+
export interface RoleKeyConsumers {
|
|
56
|
+
key: string
|
|
57
|
+
/** The human phrase for this key's own canonical use, when it has one. */
|
|
58
|
+
canonicalUse?: string
|
|
59
|
+
/** Agent verify steps this key currently ROUTES through the deprecated
|
|
60
|
+
* skill-name convention — the step declares no `[roles.<step>]` of its own. */
|
|
61
|
+
activeAlias: string[]
|
|
62
|
+
/** Agent verify steps whose configured skill is this key but which declare
|
|
63
|
+
* their own step-named role, so the key is inert for them. */
|
|
64
|
+
supersededAlias: string[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RoleKeyDiagnostics {
|
|
68
|
+
/** The sorted canonical keys this configuration can consume. Deliberately
|
|
69
|
+
* excludes deprecated aliases: the message must not teach the dying form. */
|
|
70
|
+
valid: string[]
|
|
71
|
+
/** Declared keys with no consumer of any kind, sorted. */
|
|
72
|
+
unconsumed: string[]
|
|
73
|
+
/** Declared keys a deprecated skill-name alias reaches, sorted by key —
|
|
74
|
+
* both the still-routing and the superseded-and-inert cases. */
|
|
75
|
+
deprecated: RoleKeyConsumers[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The structural shape both `[verify.<step>]` and `[finalize.<step>]` share. */
|
|
79
|
+
type StepTable = { kind: 'check' } | { kind: 'agent'; skill: string }
|
|
80
|
+
|
|
81
|
+
export interface RuntimeReferenceGroup {
|
|
82
|
+
runtime: string
|
|
83
|
+
/** Human-readable routes selecting this runtime, including alternates. */
|
|
84
|
+
references: string[]
|
|
85
|
+
/** Explicit configured models selected for readiness probing. */
|
|
86
|
+
models: string[]
|
|
87
|
+
/** At least one selected route leaves its model to the runtime registration. */
|
|
88
|
+
usesRuntimeDefaultModel: boolean
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Derive the runtime targets that can actually start sessions. This mirrors the
|
|
93
|
+
* resolver's per-axis inheritance, role/alias precedence, and alternate overlay
|
|
94
|
+
* without requiring a runtime registry, so config validation and remote
|
|
95
|
+
* bootstrap share one source of truth.
|
|
96
|
+
*/
|
|
97
|
+
export function effectiveRuntimeReferences(
|
|
98
|
+
config: Pick<Config, 'roles' | 'verify' | 'finalize'>,
|
|
99
|
+
): RuntimeReferenceGroup[] {
|
|
100
|
+
const defaultSpec = config.roles.default ?? {}
|
|
101
|
+
const routes: Array<{ role: string; aliases?: string[]; label: string }> = [
|
|
102
|
+
...CORE_PHASES.map((role) => ({ role, label: `role ${displayName(role)}` })),
|
|
103
|
+
...INTERNAL_ROLES.map((role) => ({ role, label: `role ${displayName(role)}` })),
|
|
104
|
+
...agentSteps(config.verify.steps, config.verify.stepConfigs).map(({ step, skill }) => ({
|
|
105
|
+
role: step,
|
|
106
|
+
aliases: [skill],
|
|
107
|
+
label: `agent verify role ${displayName(step)}`,
|
|
108
|
+
})),
|
|
109
|
+
...agentSteps(config.finalize.steps, config.finalize.stepConfigs).map(({ step }) => ({
|
|
110
|
+
role: step,
|
|
111
|
+
label: `agent finalize role ${displayName(step)}`,
|
|
112
|
+
})),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
const grouped = new Map<
|
|
116
|
+
string,
|
|
117
|
+
{ references: Set<string>; models: Set<string>; usesRuntimeDefaultModel: boolean }
|
|
118
|
+
>()
|
|
119
|
+
const declaredSpec = (role: string, aliases: readonly string[] = []) => {
|
|
120
|
+
for (const key of [role, ...aliases]) {
|
|
121
|
+
if (key === RESERVED_ROLE) return defaultSpec
|
|
122
|
+
if (Object.hasOwn(config.roles, key)) return config.roles[key]!
|
|
123
|
+
}
|
|
124
|
+
return defaultSpec
|
|
125
|
+
}
|
|
126
|
+
const add = (runtime: string | undefined, model: string | undefined, reference: string) => {
|
|
127
|
+
if (runtime === undefined || runtime.trim() === '') return
|
|
128
|
+
const group = grouped.get(runtime) ?? {
|
|
129
|
+
references: new Set(),
|
|
130
|
+
models: new Set(),
|
|
131
|
+
usesRuntimeDefaultModel: false,
|
|
132
|
+
}
|
|
133
|
+
group.references.add(reference)
|
|
134
|
+
if (model !== undefined) group.models.add(model)
|
|
135
|
+
else group.usesRuntimeDefaultModel = true
|
|
136
|
+
grouped.set(runtime, group)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const route of routes) {
|
|
140
|
+
const spec = declaredSpec(route.role, route.aliases)
|
|
141
|
+
const runtime = spec.runtime ?? defaultSpec.runtime
|
|
142
|
+
const model = spec.model ?? defaultSpec.model
|
|
143
|
+
add(runtime, model, `${route.label} primary`)
|
|
144
|
+
const alternates = spec.alternates ?? defaultSpec.alternates ?? []
|
|
145
|
+
alternates.forEach((alternate, index) => {
|
|
146
|
+
add(
|
|
147
|
+
alternate.runtime ?? runtime,
|
|
148
|
+
alternate.model ?? model,
|
|
149
|
+
`${route.label} alternate[${index}]`,
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return [...grouped.entries()]
|
|
155
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
156
|
+
.map(([runtime, group]) => ({
|
|
157
|
+
runtime,
|
|
158
|
+
references: [...group.references].sort(),
|
|
159
|
+
models: [...group.models].sort(),
|
|
160
|
+
usesRuntimeDefaultModel: group.usesRuntimeDefaultModel,
|
|
161
|
+
}))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Declared steps that actually run, in declaration order, agent kind only —
|
|
165
|
+
* a `kind = "check"` step starts no session and consumes no role. */
|
|
166
|
+
function agentSteps(
|
|
167
|
+
steps: readonly string[],
|
|
168
|
+
stepConfigs: Record<string, StepTable>,
|
|
169
|
+
): { step: string; skill: string }[] {
|
|
170
|
+
const agents: { step: string; skill: string }[] = []
|
|
171
|
+
const seen = new Set<string>()
|
|
172
|
+
for (const step of steps) {
|
|
173
|
+
if (seen.has(step)) continue
|
|
174
|
+
seen.add(step)
|
|
175
|
+
// OWN property, for the same reason `declared` below is spelled out: step
|
|
176
|
+
// names are user-chosen from an open set, so `constructor` and `toString`
|
|
177
|
+
// are legal, and a bare read would answer an inherited function for a
|
|
178
|
+
// listed-but-tableless step. Cross-validation rejects that config before it
|
|
179
|
+
// can reach here, so this is redundant in practice — it keeps the read
|
|
180
|
+
// sound on its own rather than on a guarantee made in another file.
|
|
181
|
+
if (!Object.hasOwn(stepConfigs, step)) continue
|
|
182
|
+
const table = stepConfigs[step]!
|
|
183
|
+
if (table.kind !== 'agent') continue
|
|
184
|
+
agents.push({ step, skill: table.skill })
|
|
185
|
+
}
|
|
186
|
+
return agents
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Rendering a user-chosen name ─────────────────────────────────────────────
|
|
190
|
+
//
|
|
191
|
+
// Role names, verify step names, and finalize step names are ARBITRARY nonempty
|
|
192
|
+
// strings. Every one of them reaches an operator's terminal, so every one is
|
|
193
|
+
// rendered through `displayName`/`tomlKey` below and never hand-quoted or
|
|
194
|
+
// interpolated bare. Two failures follow from getting this wrong, and both have
|
|
195
|
+
// happened: `[roles.ui.visual]` is not the key it looks like (TOML reads the dot
|
|
196
|
+
// as nesting), and a name carrying a newline splits ONE notice into two physical
|
|
197
|
+
// stderr lines, breaking the one-notice-per-line contract every surface
|
|
198
|
+
// downstream assumes.
|
|
199
|
+
|
|
200
|
+
/** TOML bare keys are letters, digits, `-`, and `_` only. */
|
|
201
|
+
const TOML_BARE_KEY = /^[A-Za-z0-9_-]+$/
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The body of a TOML basic string, escaped on an ALLOWLIST: a character is
|
|
205
|
+
* emitted raw only when it is printable ASCII a basic string accepts unescaped.
|
|
206
|
+
* Everything else — every C0 control, U+007F, and every non-ASCII code point —
|
|
207
|
+
* becomes TOML's own `\uXXXX`, or `\UXXXXXXXX` above the BMP.
|
|
208
|
+
*
|
|
209
|
+
* The allowlist is the whole design. `JSON.stringify` was the escaper here for
|
|
210
|
+
* three rounds and is a DENYLIST: it escapes C0 controls, then passes U+007F
|
|
211
|
+
* through raw, then passes every other non-ASCII code point through raw. Each
|
|
212
|
+
* gap was found and patched one code point at a time, which is not a process
|
|
213
|
+
* that terminates. Nothing outside the allowlist can reach a terminal now,
|
|
214
|
+
* whatever it is, and there is no per-character rule left to get wrong.
|
|
215
|
+
*
|
|
216
|
+
* Iterating with `for…of` walks code points, so an astral character is escaped
|
|
217
|
+
* once as `\U0001F600` rather than as two lone surrogates — which TOML rejects
|
|
218
|
+
* outright and which therefore would not round-trip.
|
|
219
|
+
*/
|
|
220
|
+
function escapeBasic(name: string): string {
|
|
221
|
+
let out = ''
|
|
222
|
+
for (const char of name) {
|
|
223
|
+
const code = char.codePointAt(0)!
|
|
224
|
+
if (char === '"' || char === '\\') out += `\\${char}`
|
|
225
|
+
else if (code >= 0x20 && code <= 0x7e) out += char
|
|
226
|
+
else if (code > 0xffff) out += `\\U${code.toString(16).toUpperCase().padStart(8, '0')}`
|
|
227
|
+
else out += `\\u${code.toString(16).toUpperCase().padStart(4, '0')}`
|
|
228
|
+
}
|
|
229
|
+
return out
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** A name IN PROSE — always quoted, always escaped. The one place a name is
|
|
233
|
+
* wrapped in quotes, so a quote cannot break out of the phrase and nothing
|
|
234
|
+
* unprintable can reach a line-oriented sink raw. */
|
|
235
|
+
export function displayName(name: string): string {
|
|
236
|
+
return `"${escapeBasic(name)}"`
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A name AS THE OPERATOR MUST TYPE IT in a table header: bare when TOML's
|
|
241
|
+
* bare-key grammar accepts it — which keeps every ordinary notice
|
|
242
|
+
* byte-identical — and otherwise the quoted prose form, unchanged. The two
|
|
243
|
+
* renderings differ in exactly that one condition and share the escaper, so
|
|
244
|
+
* they cannot drift into disagreeing about what is safe.
|
|
245
|
+
*/
|
|
246
|
+
export function tomlKey(name: string): string {
|
|
247
|
+
return TOML_BARE_KEY.test(name) ? name : displayName(name)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The table header to write, quoted when the name needs it. */
|
|
251
|
+
const roleTable = (name: string): string => `[roles.${tomlKey(name)}]`
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Classify every declared `[roles.<key>]` against every consumer it has.
|
|
255
|
+
* Iterates `steps` rather than the `stepConfigs` key sets so only steps that
|
|
256
|
+
* actually run contribute; cross-validation already guarantees the two agree.
|
|
257
|
+
*/
|
|
258
|
+
export function roleKeyDiagnostics(config: Config): RoleKeyDiagnostics {
|
|
259
|
+
const verifyAgents = agentSteps(config.verify.steps, config.verify.stepConfigs)
|
|
260
|
+
const finalizeAgents = agentSteps(config.finalize.steps, config.finalize.stepConfigs)
|
|
261
|
+
|
|
262
|
+
// `default` is tested FIRST so the reserved phrase always wins, which is also
|
|
263
|
+
// what keeps the rename/delete advice structurally unreachable for it.
|
|
264
|
+
//
|
|
265
|
+
// Every arm renders through `displayName`, including the two whose names come
|
|
266
|
+
// from fixed lists. Those two are safe on their own, but a phrase built here
|
|
267
|
+
// is appended verbatim to a notice by the shared/colliding branch, and one
|
|
268
|
+
// uniform rule is what stops the next arm added to this list from being the
|
|
269
|
+
// hand-quoted one.
|
|
270
|
+
const canonicalUse = (key: string): string | undefined => {
|
|
271
|
+
if (key === RESERVED_ROLE) return `the reserved ${roleTable(RESERVED_ROLE)} inheritance base`
|
|
272
|
+
if ((CORE_PHASES as readonly string[]).includes(key)) {
|
|
273
|
+
return `the ${displayName(key)} core phase role`
|
|
274
|
+
}
|
|
275
|
+
if (verifyAgents.some((agent) => agent.step === key)) {
|
|
276
|
+
return `agent verify step ${displayName(key)}`
|
|
277
|
+
}
|
|
278
|
+
if (finalizeAgents.some((agent) => agent.step === key)) {
|
|
279
|
+
return `agent finalize step ${displayName(key)}`
|
|
280
|
+
}
|
|
281
|
+
if ((INTERNAL_ROLES as readonly string[]).includes(key)) {
|
|
282
|
+
return `the internal ${displayName(key)} role`
|
|
283
|
+
}
|
|
284
|
+
return undefined
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// MUST mirror the resolver's candidate walk (src/ports/runner/routing.ts),
|
|
288
|
+
// reserved arm included: a step named `default` always has its step-named
|
|
289
|
+
// role declared, so its skill-name alias really is inert. The `||` arm is
|
|
290
|
+
// redundant in practice — construction fails without `[roles.default]` — but
|
|
291
|
+
// it keeps the two implementations of "declared" from drifting apart.
|
|
292
|
+
const declared = (step: string): boolean =>
|
|
293
|
+
step === RESERVED_ROLE || Object.hasOwn(config.roles, step)
|
|
294
|
+
|
|
295
|
+
const valid = [
|
|
296
|
+
...new Set([
|
|
297
|
+
RESERVED_ROLE,
|
|
298
|
+
...CORE_PHASES,
|
|
299
|
+
...verifyAgents.map((agent) => agent.step),
|
|
300
|
+
...finalizeAgents.map((agent) => agent.step),
|
|
301
|
+
...INTERNAL_ROLES,
|
|
302
|
+
]),
|
|
303
|
+
].sort()
|
|
304
|
+
|
|
305
|
+
const unconsumed: string[] = []
|
|
306
|
+
const deprecated: RoleKeyConsumers[] = []
|
|
307
|
+
|
|
308
|
+
for (const key of Object.keys(config.roles).sort()) {
|
|
309
|
+
const use = canonicalUse(key)
|
|
310
|
+
// A step whose own name IS the key has no alias at all: the key is
|
|
311
|
+
// canonical for it, and step-name precedence means it routes that way.
|
|
312
|
+
const aliasFor = verifyAgents.filter((agent) => agent.skill === key && agent.step !== key)
|
|
313
|
+
const activeAlias = aliasFor.filter((a) => !declared(a.step)).map((a) => a.step)
|
|
314
|
+
const supersededAlias = aliasFor.filter((a) => declared(a.step)).map((a) => a.step)
|
|
315
|
+
|
|
316
|
+
if (activeAlias.length > 0 || (supersededAlias.length > 0 && use === undefined)) {
|
|
317
|
+
deprecated.push({
|
|
318
|
+
key,
|
|
319
|
+
...(use !== undefined ? { canonicalUse: use } : {}),
|
|
320
|
+
activeAlias,
|
|
321
|
+
supersededAlias,
|
|
322
|
+
})
|
|
323
|
+
continue
|
|
324
|
+
}
|
|
325
|
+
// Fully consumed. An inert alias on an already-canonical key needs no
|
|
326
|
+
// advice, and `canonicalUse(RESERVED_ROLE)` is what exempts the reserved
|
|
327
|
+
// key from the unconsumed bucket — structurally, not by an early skip.
|
|
328
|
+
if (use !== undefined) continue
|
|
329
|
+
unconsumed.push(key)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return { valid, unconsumed, deprecated }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ── Notices ──────────────────────────────────────────────────────────────────
|
|
336
|
+
//
|
|
337
|
+
// One string per key, so advice is never blanket, and every branch's advice is
|
|
338
|
+
// safe to follow literally — which means BOTH that following it converges (one
|
|
339
|
+
// edit, then silence) and that what it tells you to type is valid TOML. Two
|
|
340
|
+
// shape constraints: the key and its replacement land in the FIRST CLAUSE, so
|
|
341
|
+
// they are on the first rendered row at any realistic width; and the valid-key
|
|
342
|
+
// list is its own string, so it starts at column 0 of its own row instead of
|
|
343
|
+
// trailing a paragraph. House style is subject-first, `subject: predicate —
|
|
344
|
+
// detail`, with no `warning:` prefix.
|
|
345
|
+
|
|
346
|
+
function andList(parts: readonly string[]): string {
|
|
347
|
+
if (parts.length <= 1) return parts[0] ?? ''
|
|
348
|
+
if (parts.length === 2) return `${parts[0]} and ${parts[1]}`
|
|
349
|
+
return `${parts.slice(0, -1).join(', ')}, and ${parts[parts.length - 1]}`
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Step names in prose — each through `displayName`, never hand-quoted. */
|
|
353
|
+
const named = (steps: readonly string[]): string => andList(steps.map(displayName))
|
|
354
|
+
|
|
355
|
+
const roleTables = (names: readonly string[]): string => andList(names.map(roleTable))
|
|
356
|
+
|
|
357
|
+
function deprecationNotice(entry: RoleKeyConsumers): string {
|
|
358
|
+
const { key, canonicalUse, activeAlias, supersededAlias } = entry
|
|
359
|
+
|
|
360
|
+
// A SUPERSEDED alias is not a consumer. It is inert by definition — the step
|
|
361
|
+
// declares its own step-named role, which wins — so it can never be a reason
|
|
362
|
+
// to keep this key, and it never blocks the rename. Counting it as one made
|
|
363
|
+
// the advice non-converging: "keep it, it is also the superseded key for
|
|
364
|
+
// e2e", followed on the next dispatch by "it can be deleted".
|
|
365
|
+
|
|
366
|
+
// Inert: the key routes nothing at all.
|
|
367
|
+
if (activeAlias.length === 0) {
|
|
368
|
+
const subject =
|
|
369
|
+
supersededAlias.length === 1
|
|
370
|
+
? `agent verify step ${named(supersededAlias)} declares its own step-named role`
|
|
371
|
+
: `agent verify steps ${named(supersededAlias)} each declare their own step-named role`
|
|
372
|
+
return (
|
|
373
|
+
`autobuild.toml: ${roleTable(key)} can be deleted — ${subject}, so this ` +
|
|
374
|
+
`deprecated skill-name key changes nothing.`
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Safe rename: one step still routed and no OTHER consumer, so the key moves
|
|
379
|
+
// in one edit. Superseded steps are deliberately not "other consumers".
|
|
380
|
+
if (canonicalUse === undefined && activeAlias.length === 1) {
|
|
381
|
+
return (
|
|
382
|
+
`autobuild.toml: ${roleTable(key)} should be ${roleTable(activeAlias[0]!)} — it is the ` +
|
|
383
|
+
`deprecated skill-name key for agent verify step ${named(activeAlias)} and stops ` +
|
|
384
|
+
`working in a future release.`
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Shared or colliding: the key routes several steps, or its name is itself
|
|
389
|
+
// canonical, so the advice is to declare each step and then keep or delete.
|
|
390
|
+
const routed =
|
|
391
|
+
activeAlias.length === 1
|
|
392
|
+
? 'it routes that agent verify step through the deprecated skill-name key'
|
|
393
|
+
: 'it routes those agent verify steps through the deprecated skill-name key'
|
|
394
|
+
const advice =
|
|
395
|
+
canonicalUse !== undefined
|
|
396
|
+
? `Keep ${roleTable(key)}: it is also ${canonicalUse}.`
|
|
397
|
+
: `Delete ${roleTable(key)} once those are declared.`
|
|
398
|
+
return `autobuild.toml: ${roleTable(key)} should be ${roleTables(activeAlias)} — ${routed}. ${advice}`
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* The rendered notices — THE one message set. Empty for a clean configuration.
|
|
403
|
+
*
|
|
404
|
+
* Order: the unconsumed-keys line, then the valid-key line (only when there are
|
|
405
|
+
* unconsumed keys), then one notice per deprecated key in key order. Every
|
|
406
|
+
* surface renders these exact strings in full: nothing here or downstream may
|
|
407
|
+
* cap, summarize, or truncate them, or a surface would report that a class of
|
|
408
|
+
* problem exists without naming the keys the operator has to change.
|
|
409
|
+
*
|
|
410
|
+
* The unconsumed line deliberately stops at the harm and does not explain what
|
|
411
|
+
* runs instead — that belongs in SPEC §9 and `docs/configuration.md`, and
|
|
412
|
+
* putting it here pushed the required valid-key list past the right edge.
|
|
413
|
+
*/
|
|
414
|
+
export function roleKeyWarnings(config: Config): string[] {
|
|
415
|
+
const { valid, unconsumed, deprecated } = roleKeyDiagnostics(config)
|
|
416
|
+
const lines: string[] = []
|
|
417
|
+
if (unconsumed.length > 0) {
|
|
418
|
+
const keys = unconsumed.map(roleTable).join(', ')
|
|
419
|
+
lines.push(
|
|
420
|
+
unconsumed.length === 1
|
|
421
|
+
? `autobuild.toml: ${keys} is declared but nothing requests it — its runtime, model, and args never reach a session.`
|
|
422
|
+
: `autobuild.toml: ${keys} are declared but nothing requests them — their runtime, model, and args never reach a session.`,
|
|
423
|
+
)
|
|
424
|
+
// Rendered as keys, not as prose names: this list is what the operator
|
|
425
|
+
// types next, so a name needing quotes must show them here too.
|
|
426
|
+
lines.push(`Valid role keys: ${valid.map(tomlKey).join(', ')}`)
|
|
427
|
+
}
|
|
428
|
+
for (const entry of deprecated) lines.push(deprecationNotice(entry))
|
|
429
|
+
for (const [role, spec] of Object.entries(config.roles)) {
|
|
430
|
+
const declared =
|
|
431
|
+
spec.extensions !== undefined || spec.alternates?.some((alt) => alt.extensions !== undefined)
|
|
432
|
+
if (!declared) continue
|
|
433
|
+
lines.push(
|
|
434
|
+
`autobuild.toml: ${roleTable(role)} declares deprecated extensions — it has no effect; use args for explicit runtime CLI arguments.`,
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
return lines
|
|
438
|
+
}
|