@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.
Files changed (251) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +192 -0
  3. package/SPEC.md +2269 -0
  4. package/bin/ab-build-runner.ts +47 -0
  5. package/bin/ab-dev.ts +27 -0
  6. package/bin/ab-dispatch-kernel.ts +92 -0
  7. package/bin/ab-harvest-runner.ts +29 -0
  8. package/bin/ab-init-probe.ts +19 -0
  9. package/bin/ab-process-group-reaper.ts +32 -0
  10. package/bin/ab.ts +6 -0
  11. package/bin/agent/ab +3 -0
  12. package/docs/answering-blocked-builds.md +89 -0
  13. package/docs/architecture.md +726 -0
  14. package/docs/assets/headline-wide.png +0 -0
  15. package/docs/configuration.md +1623 -0
  16. package/docs/hosted-dispatcher-cutover-validation.md +178 -0
  17. package/docs/hosted-dispatcher.md +204 -0
  18. package/docs/mcp.md +119 -0
  19. package/docs/operator-api.md +186 -0
  20. package/docs/remote-store-protocol.md +972 -0
  21. package/docs/remote-ticket-protocol.md +79 -0
  22. package/docs/setup.md +289 -0
  23. package/docs/spec-standard.md +63 -0
  24. package/docs/ticket-dependencies.md +59 -0
  25. package/docs/vercel-sandbox-rollout-probe.md +7 -0
  26. package/package.json +135 -0
  27. package/packages/core/src/cli/args.ts +74 -0
  28. package/packages/core/src/cli/artifact.ts +182 -0
  29. package/packages/core/src/cli/binary.ts +198 -0
  30. package/packages/core/src/cli/build-control.ts +789 -0
  31. package/packages/core/src/cli/build-progress.ts +55 -0
  32. package/packages/core/src/cli/bulk-control.ts +385 -0
  33. package/packages/core/src/cli/context.ts +470 -0
  34. package/packages/core/src/cli/dashboard/actions.ts +59 -0
  35. package/packages/core/src/cli/dashboard/cells.ts +92 -0
  36. package/packages/core/src/cli/dashboard/composer.ts +230 -0
  37. package/packages/core/src/cli/dashboard/detail.ts +100 -0
  38. package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
  39. package/packages/core/src/cli/dashboard/live.ts +152 -0
  40. package/packages/core/src/cli/dashboard/model.ts +1173 -0
  41. package/packages/core/src/cli/dashboard/poll.ts +196 -0
  42. package/packages/core/src/cli/dashboard/render.ts +1568 -0
  43. package/packages/core/src/cli/dashboard/selection.ts +66 -0
  44. package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
  45. package/packages/core/src/cli/dashboard/session-view.ts +506 -0
  46. package/packages/core/src/cli/dashboard/transcript.ts +108 -0
  47. package/packages/core/src/cli/dev.ts +46 -0
  48. package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
  49. package/packages/core/src/cli/dispatch-process.ts +259 -0
  50. package/packages/core/src/cli/dispatch.ts +3692 -0
  51. package/packages/core/src/cli/dotenv.ts +48 -0
  52. package/packages/core/src/cli/env.ts +206 -0
  53. package/packages/core/src/cli/harvest.ts +698 -0
  54. package/packages/core/src/cli/help.ts +713 -0
  55. package/packages/core/src/cli/init-agent.ts +106 -0
  56. package/packages/core/src/cli/init-validation.ts +583 -0
  57. package/packages/core/src/cli/init.ts +756 -0
  58. package/packages/core/src/cli/installation.ts +333 -0
  59. package/packages/core/src/cli/keyboard.ts +261 -0
  60. package/packages/core/src/cli/main.ts +1399 -0
  61. package/packages/core/src/cli/mcp.ts +279 -0
  62. package/packages/core/src/cli/models.ts +65 -0
  63. package/packages/core/src/cli/observe.ts +55 -0
  64. package/packages/core/src/cli/plugin.ts +227 -0
  65. package/packages/core/src/cli/pr-attachments.ts +138 -0
  66. package/packages/core/src/cli/pr-summary.ts +124 -0
  67. package/packages/core/src/cli/publication-settlement.ts +188 -0
  68. package/packages/core/src/cli/repo-state.ts +213 -0
  69. package/packages/core/src/cli/repository-status.ts +80 -0
  70. package/packages/core/src/cli/self-update.ts +443 -0
  71. package/packages/core/src/cli/status.ts +895 -0
  72. package/packages/core/src/cli/store-opening.contract.ts +289 -0
  73. package/packages/core/src/cli/store-opening.ts +167 -0
  74. package/packages/core/src/cli/store-ref.ts +33 -0
  75. package/packages/core/src/cli/terminal-restore.ts +174 -0
  76. package/packages/core/src/cli/terminal.ts +361 -0
  77. package/packages/core/src/cli/terminals.ts +995 -0
  78. package/packages/core/src/cli/testkit.ts +194 -0
  79. package/packages/core/src/cli/ticket.ts +484 -0
  80. package/packages/core/src/cli/upgrade-agent.ts +180 -0
  81. package/packages/core/src/cli/upgrade-commit.ts +468 -0
  82. package/packages/core/src/cli/upgrade-notice.ts +91 -0
  83. package/packages/core/src/cli/upgrade-progress.ts +110 -0
  84. package/packages/core/src/cli/upgrade.ts +888 -0
  85. package/packages/core/src/cli/wait.ts +587 -0
  86. package/packages/core/src/cli/watch.ts +932 -0
  87. package/packages/core/src/config/live.ts +255 -0
  88. package/packages/core/src/config/load.ts +98 -0
  89. package/packages/core/src/config/roles.ts +438 -0
  90. package/packages/core/src/config/schema.ts +774 -0
  91. package/packages/core/src/distribution.ts +24 -0
  92. package/packages/core/src/events/catalog.ts +201 -0
  93. package/packages/core/src/events/envelope.ts +59 -0
  94. package/packages/core/src/events/payloads.ts +618 -0
  95. package/packages/core/src/events/repository.ts +559 -0
  96. package/packages/core/src/events/sessions.ts +180 -0
  97. package/packages/core/src/harvest/schema.ts +147 -0
  98. package/packages/core/src/ids.ts +45 -0
  99. package/packages/core/src/integration/harness.ts +681 -0
  100. package/packages/core/src/kernel/auto-merge.ts +259 -0
  101. package/packages/core/src/kernel/converge.ts +117 -0
  102. package/packages/core/src/kernel/dispatch-settings.ts +60 -0
  103. package/packages/core/src/kernel/dispatch-status.ts +141 -0
  104. package/packages/core/src/kernel/engine.ts +1047 -0
  105. package/packages/core/src/kernel/harvest.ts +823 -0
  106. package/packages/core/src/kernel/origin.ts +69 -0
  107. package/packages/core/src/kernel/phases.ts +196 -0
  108. package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
  109. package/packages/core/src/kernel/pr-attachments.ts +82 -0
  110. package/packages/core/src/kernel/reducer.ts +752 -0
  111. package/packages/core/src/kernel/stall.ts +155 -0
  112. package/packages/core/src/kernel/verify-applicability.ts +52 -0
  113. package/packages/core/src/markdown.ts +67 -0
  114. package/packages/core/src/ontology.ts +288 -0
  115. package/packages/core/src/open-map.ts +203 -0
  116. package/packages/core/src/operator/annotations.ts +187 -0
  117. package/packages/core/src/operator/client.ts +311 -0
  118. package/packages/core/src/operator/control.ts +117 -0
  119. package/packages/core/src/operator/index.ts +62 -0
  120. package/packages/core/src/operator/presentation.ts +22 -0
  121. package/packages/core/src/operator/protocol.ts +98 -0
  122. package/packages/core/src/operator/query.ts +201 -0
  123. package/packages/core/src/operator/registry.ts +1043 -0
  124. package/packages/core/src/operator/requests.ts +138 -0
  125. package/packages/core/src/operator/sandbox.ts +458 -0
  126. package/packages/core/src/operator/server.ts +616 -0
  127. package/packages/core/src/operator/sessions.ts +223 -0
  128. package/packages/core/src/operator/tickets.ts +177 -0
  129. package/packages/core/src/plugin-sdk/index.ts +174 -0
  130. package/packages/core/src/plugins/contract-entry.ts +74 -0
  131. package/packages/core/src/plugins/contract-env.ts +3 -0
  132. package/packages/core/src/plugins/load.ts +259 -0
  133. package/packages/core/src/plugins/manifest.ts +236 -0
  134. package/packages/core/src/plugins/registry.ts +274 -0
  135. package/packages/core/src/plugins/runtimes.ts +75 -0
  136. package/packages/core/src/ports/forge/contract.ts +258 -0
  137. package/packages/core/src/ports/forge/create.ts +74 -0
  138. package/packages/core/src/ports/forge/fake.ts +488 -0
  139. package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
  140. package/packages/core/src/ports/forge/github-transport.ts +352 -0
  141. package/packages/core/src/ports/forge/github.ts +1162 -0
  142. package/packages/core/src/ports/forge/local-git.ts +568 -0
  143. package/packages/core/src/ports/runner/claude.ts +873 -0
  144. package/packages/core/src/ports/runner/cli-stream.ts +90 -0
  145. package/packages/core/src/ports/runner/codex.ts +873 -0
  146. package/packages/core/src/ports/runner/contract.ts +492 -0
  147. package/packages/core/src/ports/runner/fake.ts +213 -0
  148. package/packages/core/src/ports/runner/json-record.ts +2 -0
  149. package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
  150. package/packages/core/src/ports/runner/one-shot.ts +29 -0
  151. package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
  152. package/packages/core/src/ports/runner/pi-bridge.js +6 -0
  153. package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
  154. package/packages/core/src/ports/runner/pi.ts +617 -0
  155. package/packages/core/src/ports/runner/production.ts +74 -0
  156. package/packages/core/src/ports/runner/provider-error.ts +121 -0
  157. package/packages/core/src/ports/runner/routing.ts +234 -0
  158. package/packages/core/src/ports/runner/runtime.ts +214 -0
  159. package/packages/core/src/ports/runner/session-env.ts +34 -0
  160. package/packages/core/src/ports/runner/stream-parts.ts +103 -0
  161. package/packages/core/src/ports/tickets/contract.ts +570 -0
  162. package/packages/core/src/ports/tickets/create.ts +135 -0
  163. package/packages/core/src/ports/tickets/fake.ts +215 -0
  164. package/packages/core/src/ports/tickets/file.ts +559 -0
  165. package/packages/core/src/ports/tickets/linear.ts +811 -0
  166. package/packages/core/src/ports/tickets/operations.ts +161 -0
  167. package/packages/core/src/ports/tickets/remote-index.ts +13 -0
  168. package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
  169. package/packages/core/src/ports/tickets/remote-server.ts +185 -0
  170. package/packages/core/src/ports/tickets/remote.ts +130 -0
  171. package/packages/core/src/ports/tickets/update.ts +51 -0
  172. package/packages/core/src/ports/types.ts +528 -0
  173. package/packages/core/src/ports/workspace/build-execution.ts +123 -0
  174. package/packages/core/src/ports/workspace/contract.ts +136 -0
  175. package/packages/core/src/ports/workspace/create.ts +132 -0
  176. package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
  177. package/packages/core/src/ports/workspace/fake.ts +432 -0
  178. package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
  179. package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
  180. package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
  181. package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
  182. package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
  183. package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
  184. package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
  185. package/packages/core/src/processes/build-child-exit.ts +78 -0
  186. package/packages/core/src/processes/build-child.ts +119 -0
  187. package/packages/core/src/processes/build-execution-state.ts +95 -0
  188. package/packages/core/src/processes/build-parent-watch.ts +35 -0
  189. package/packages/core/src/processes/build-runner.ts +2614 -0
  190. package/packages/core/src/processes/dispatcher.ts +2825 -0
  191. package/packages/core/src/processes/execution-settlement.ts +171 -0
  192. package/packages/core/src/processes/harvest-child.ts +115 -0
  193. package/packages/core/src/processes/harvest-execution-state.ts +121 -0
  194. package/packages/core/src/processes/harvest-runner.ts +1656 -0
  195. package/packages/core/src/processes/harvest.ts +595 -0
  196. package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
  197. package/packages/core/src/processes/phase-failure-budget.ts +52 -0
  198. package/packages/core/src/processes/process-group-reaper.ts +35 -0
  199. package/packages/core/src/processes/process-group.ts +65 -0
  200. package/packages/core/src/processes/publication-loss.ts +62 -0
  201. package/packages/core/src/processes/publication-state.ts +93 -0
  202. package/packages/core/src/processes/sandbox-state.ts +101 -0
  203. package/packages/core/src/registry.ts +24 -0
  204. package/packages/core/src/sessions/reducer.ts +185 -0
  205. package/packages/core/src/skills.ts +7 -0
  206. package/packages/core/src/spec-standard.ts +47 -0
  207. package/packages/core/src/store/adapter.ts +80 -0
  208. package/packages/core/src/store/build-scope.ts +272 -0
  209. package/packages/core/src/store/contract.ts +2581 -0
  210. package/packages/core/src/store/local/blobs.ts +38 -0
  211. package/packages/core/src/store/local/schema.ts +180 -0
  212. package/packages/core/src/store/local/store.ts +1713 -0
  213. package/packages/core/src/store/memory.ts +1138 -0
  214. package/packages/core/src/store/phase-session.ts +425 -0
  215. package/packages/core/src/store/remote/client.ts +780 -0
  216. package/packages/core/src/store/remote/index.ts +12 -0
  217. package/packages/core/src/store/remote/protocol.ts +348 -0
  218. package/packages/core/src/store/remote/server.ts +923 -0
  219. package/packages/core/src/store/remote/token.ts +137 -0
  220. package/packages/core/src/store/remote/version.ts +10 -0
  221. package/packages/core/src/store/retention.ts +91 -0
  222. package/packages/core/src/store/session-handle.ts +265 -0
  223. package/packages/core/src/store/streams/assemble.ts +79 -0
  224. package/packages/core/src/store/streams/lock.ts +42 -0
  225. package/packages/core/src/store/streams/session-writer.ts +306 -0
  226. package/packages/core/src/store/streams/types.ts +171 -0
  227. package/packages/core/src/store/streams/wait.ts +127 -0
  228. package/packages/core/src/store/subscribe.ts +105 -0
  229. package/packages/core/src/store/types.ts +466 -0
  230. package/packages/core/src/testing/fixed.ts +44 -0
  231. package/packages/core/src/testing/store-failures.ts +32 -0
  232. package/packages/core/src/zod-issues.ts +91 -0
  233. package/skills/code-review/SKILL.md +100 -0
  234. package/skills/finalize/SKILL.md +44 -0
  235. package/skills/guide/SKILL.md +1781 -0
  236. package/skills/guide/references/operator-api.md +164 -0
  237. package/skills/guide/references/plugin-authoring.md +354 -0
  238. package/skills/guide/references/remote-store-protocol.md +972 -0
  239. package/skills/guide/references/remote-ticket-protocol.md +79 -0
  240. package/skills/guide/references/setup.md +289 -0
  241. package/skills/guide/references/spec-standard.md +63 -0
  242. package/skills/guide/references/ticket-dependencies.md +59 -0
  243. package/skills/harvest/SKILL.md +66 -0
  244. package/skills/harvest-review/SKILL.md +63 -0
  245. package/skills/implement/SKILL.md +82 -0
  246. package/skills/plan/SKILL.md +123 -0
  247. package/skills/plan-review/SKILL.md +121 -0
  248. package/skills/reconcile/SKILL.md +65 -0
  249. package/skills/spec/SKILL.md +132 -0
  250. package/skills/tickets/SKILL.md +139 -0
  251. package/templates/autobuild.toml +36 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Dependency-free repository-origin helpers.
3
+ *
4
+ * Repository identity in the BuildStore, dispatcher, `ab builds`,
5
+ * `ab repository status`, and the web app is the repository's normalized
6
+ * origin URL, not a checkout path — a path-based identity makes two hosts
7
+ * disagree about which repository they operate. These helpers live in the
8
+ * kernel layer so ports code (the GitHub REST forge) and the hosted store
9
+ * service can share them without importing the CLI layer.
10
+ */
11
+
12
+ /**
13
+ * Location-independent form of a git remote URL, for comparing a recorded
14
+ * repository origin with the origin of the current checkout. Trims; maps
15
+ * scp-like `git@host:path` (and bare `host:path`) remotes and explicit
16
+ * `ssh://`/`git://` URLs to their `https://` spelling; drops credentials;
17
+ * lowercases the host; strips a trailing `.git` and trailing slashes.
18
+ * Anything unparseable — notably a local-path remote — is returned trimmed
19
+ * as-is, so such remotes only ever compare equal to themselves.
20
+ */
21
+ export function normalizeGitRemoteUrl(raw: string): string {
22
+ const trimmed = raw.trim()
23
+ // scp-like syntax: `[user@]host:relative/path`. A colon followed by an
24
+ // absolute path (or a Windows drive letter) is not scp-like — leave it for
25
+ // URL parsing, which fails and falls through to the trimmed passthrough.
26
+ const scp =
27
+ trimmed.includes('://') || /^[A-Za-z]:/.test(trimmed)
28
+ ? null
29
+ : /^(?:[^@/]+@)?([^/:]+):([^/].*)$/.exec(trimmed)
30
+ const candidate = scp !== null ? `https://${scp[1]}/${scp[2]}` : trimmed
31
+ try {
32
+ const url = new URL(candidate)
33
+ // Only network URLs carry a host; a Windows drive or relative path parses
34
+ // as a scheme-only URL and passes through untouched.
35
+ if (url.hostname === '') return trimmed
36
+ const path = url.pathname.replace(/\.git\/?$/i, '').replace(/\/+$/, '')
37
+ // ssh and git URLs name the same repository as their https spelling, so
38
+ // an ssh-origin host checkout and the sandbox guest's pinned https origin
39
+ // must normalize to one form (the scp-like branch already does).
40
+ const protocol =
41
+ url.protocol === 'ssh:' || url.protocol === 'git:' || url.protocol === 'git+ssh:'
42
+ ? 'https:'
43
+ : url.protocol
44
+ return `${protocol}//${url.host.toLowerCase()}${path}`
45
+ } catch {
46
+ return trimmed
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Whether `ref` is a valid exact Git branch name, by git-check-ref-format(1)'s
52
+ * rules: no leading `-`, no `..`, none of `~^:?*[\` anywhere, no ASCII
53
+ * control characters or space, no `@{`, not bare `@`, no trailing `.lock`
54
+ * or `.`, and no empty slash-delimited components. The REST forge uses this
55
+ * to reject a branch name before interpolating it into a ref endpoint,
56
+ * replacing the host `git check-ref-format` subprocess.
57
+ */
58
+ export function isValidGitBranchName(branch: string): boolean {
59
+ if (branch === '' || branch.startsWith('-')) return false
60
+ if (branch.endsWith('.lock') || branch.endsWith('.')) return false
61
+ if (branch.includes('..') || branch.includes('@{') || branch === '@') return false
62
+ for (const char of branch) {
63
+ const code = char.codePointAt(0)!
64
+ if (code < 0x20 || code === 0x7f) return false
65
+ if ('~^:?*[\\ '.includes(char)) return false
66
+ }
67
+ // No empty slash-delimited component (`a//b`, leading/trailing slash).
68
+ return branch.split('/').every((component) => component !== '')
69
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * The phase table (SPEC §4): a phase's name derives its skill, its events,
3
+ * and its artifact kind — mechanically. This table also encodes each phase's
4
+ * terminal discipline (D5), required deposits, context inputs (§8.3), and
5
+ * plumbing (D7), so the CLI, the context builder, and the engine all validate
6
+ * against the same data.
7
+ */
8
+ import { isVerifyPhase, type CorePhase, type Phase } from '../ontology'
9
+ import type { EventType } from '../events/payloads'
10
+ import { installedSkillName } from '../skills'
11
+
12
+ export type PhaseKind = 'producer' | 'review' | 'agent-verify'
13
+ export type TerminalCommand = 'done' | 'verdict' | 'escalate'
14
+
15
+ /**
16
+ * Kernel-side plumbing triggered by the phase's terminal (D7, §8.6):
17
+ * agents only ever commit locally; push/PR happen at the boundary.
18
+ */
19
+ export type PhasePlumbing = 'push-branch' | 'open-pr' | 'push-merge-commit'
20
+
21
+ /**
22
+ * What `ab context` materializes for the phase (§8.3). Scoping is
23
+ * deliberate: what a phase *can't* see is part of its design.
24
+ */
25
+ export interface ContextInputs {
26
+ ticket?: boolean
27
+ spec?: boolean
28
+ plan?: 'latest' | 'approved'
29
+ /**
30
+ * `current`: this round's feedback only (producers).
31
+ * `all-rounds`: every prior round's findings, for `persists` marking
32
+ * (reviewers — §15.4).
33
+ */
34
+ findings?: 'current' | 'all-rounds'
35
+ /** Feedback cited by this exact phase occurrence's started event. Agent
36
+ * verify uses this for answered guidance without widening findings input. */
37
+ currentFeedback?: boolean
38
+ /** Producer's own prior-round artifacts (plan revs / implement notes). */
39
+ priorOwnArtifacts?: boolean
40
+ commitRange?: boolean
41
+ implementNotes?: boolean
42
+ verifyReports?: boolean
43
+ /** Reconcile: the fresh `{baseSha}` from this attempt's `reconcile.started` (§15.7). */
44
+ conflict?: boolean
45
+ prTemplate?: boolean
46
+ /** Agent-verify: the step's config from autobuild.toml (§16.1). */
47
+ stepConfig?: boolean
48
+ /**
49
+ * Answered guidance escalations addressed to THIS phase (§15.6-B, §11).
50
+ * Finalize and reconcile have no producer round for the engine to feed the
51
+ * answer into, so `ab context` is their delivery channel — the engine's
52
+ * loop routing (loopOfPhase → 'other') relies on this input existing.
53
+ */
54
+ answeredGuidance?: boolean
55
+ }
56
+
57
+ export interface PhaseSpec {
58
+ name: CorePhase | 'verify'
59
+ kind: PhaseKind
60
+ /** Installed, namespaced skill name. For verify steps the actual skill
61
+ * comes from step config (§16.1). */
62
+ skill: string
63
+ startedEvent: EventType
64
+ terminal: 'done' | 'verdict'
65
+ terminalEvent: EventType
66
+ /**
67
+ * Artifact kind the terminal requires deposited (D5). For agent-verify
68
+ * steps the report kind is per-step (`verify-report:<step>`) and required
69
+ * only on `fail` — enforced by the CLI, not listed here.
70
+ */
71
+ requiredArtifact?: string
72
+ /** Verdict vocabulary the CLI enforces (§8.2). */
73
+ verdictVocabulary?: readonly string[]
74
+ /** `implement` only: no `done` on a dirty worktree (D5). */
75
+ requiresCleanWorktree?: boolean
76
+ plumbing?: PhasePlumbing
77
+ inputs: ContextInputs
78
+ }
79
+
80
+ export const PHASE_SPECS: Record<CorePhase | 'verify', PhaseSpec> = {
81
+ plan: {
82
+ name: 'plan',
83
+ kind: 'producer',
84
+ skill: installedSkillName('plan'),
85
+ startedEvent: 'plan.started',
86
+ terminal: 'done',
87
+ terminalEvent: 'plan.completed',
88
+ requiredArtifact: 'plan',
89
+ inputs: {
90
+ ticket: true,
91
+ spec: true,
92
+ priorOwnArtifacts: true,
93
+ findings: 'current',
94
+ },
95
+ },
96
+ 'plan-review': {
97
+ name: 'plan-review',
98
+ kind: 'review',
99
+ skill: installedSkillName('plan-review'),
100
+ startedEvent: 'plan-review.started',
101
+ terminal: 'verdict',
102
+ terminalEvent: 'plan-review.verdict',
103
+ requiredArtifact: 'plan-review',
104
+ verdictVocabulary: ['approve', 'revise', 'escalate'],
105
+ inputs: { spec: true, plan: 'latest', findings: 'all-rounds' },
106
+ },
107
+ implement: {
108
+ name: 'implement',
109
+ kind: 'producer',
110
+ skill: installedSkillName('implement'),
111
+ startedEvent: 'implement.started',
112
+ terminal: 'done',
113
+ terminalEvent: 'implement.completed',
114
+ requiredArtifact: 'implement-notes',
115
+ requiresCleanWorktree: true,
116
+ plumbing: 'push-branch',
117
+ inputs: {
118
+ spec: true,
119
+ plan: 'approved',
120
+ findings: 'current',
121
+ verifyReports: true,
122
+ priorOwnArtifacts: true,
123
+ },
124
+ },
125
+ 'code-review': {
126
+ name: 'code-review',
127
+ kind: 'review',
128
+ skill: installedSkillName('code-review'),
129
+ startedEvent: 'code-review.started',
130
+ terminal: 'verdict',
131
+ terminalEvent: 'code-review.verdict',
132
+ requiredArtifact: 'code-review',
133
+ verdictVocabulary: ['approve', 'revise', 'escalate'],
134
+ inputs: {
135
+ spec: true,
136
+ plan: 'approved',
137
+ commitRange: true,
138
+ findings: 'all-rounds',
139
+ implementNotes: true,
140
+ },
141
+ },
142
+ verify: {
143
+ name: 'verify',
144
+ kind: 'agent-verify',
145
+ skill: installedSkillName('verify'),
146
+ startedEvent: 'verify.started',
147
+ terminal: 'verdict',
148
+ terminalEvent: 'verify.completed',
149
+ verdictVocabulary: ['pass', 'fail', 'skip'],
150
+ inputs: { spec: true, stepConfig: true, commitRange: true, currentFeedback: true },
151
+ },
152
+ finalize: {
153
+ name: 'finalize',
154
+ kind: 'producer',
155
+ skill: installedSkillName('finalize'),
156
+ startedEvent: 'finalize.started',
157
+ terminal: 'done',
158
+ terminalEvent: 'finalize.completed',
159
+ requiredArtifact: 'pr-description',
160
+ plumbing: 'open-pr',
161
+ inputs: {
162
+ spec: true,
163
+ plan: 'approved',
164
+ verifyReports: true,
165
+ prTemplate: true,
166
+ answeredGuidance: true,
167
+ },
168
+ },
169
+ reconcile: {
170
+ name: 'reconcile',
171
+ kind: 'producer',
172
+ skill: installedSkillName('reconcile'),
173
+ startedEvent: 'reconcile.started',
174
+ terminal: 'done',
175
+ terminalEvent: 'reconcile.completed',
176
+ requiredArtifact: 'reconcile-notes',
177
+ plumbing: 'push-merge-commit',
178
+ inputs: {
179
+ spec: true,
180
+ plan: 'approved',
181
+ implementNotes: true,
182
+ conflict: true,
183
+ answeredGuidance: true,
184
+ },
185
+ },
186
+ }
187
+
188
+ export function phaseSpecFor(phase: Phase): PhaseSpec {
189
+ return isVerifyPhase(phase) ? PHASE_SPECS.verify : PHASE_SPECS[phase]
190
+ }
191
+
192
+ /** Every phase ends with exactly one terminal command (D5); `escalate` is
193
+ * always available in addition to the phase's own terminal. */
194
+ export function allowedTerminals(phase: Phase): TerminalCommand[] {
195
+ return [phaseSpecFor(phase).terminal, 'escalate']
196
+ }
@@ -0,0 +1,105 @@
1
+ import { parse as parseToml } from 'smol-toml'
2
+ import { z } from 'zod'
3
+ import type { Config } from '../config/schema'
4
+
5
+ const planMetadataSchema = z.strictObject({
6
+ verifySteps: z.array(
7
+ z
8
+ .string()
9
+ .min(1, 'verify step names must be nonempty')
10
+ .refine((step) => step.trim() === step, 'verify step names must not be blank or padded'),
11
+ ),
12
+ })
13
+
14
+ /**
15
+ * Read an opening TOML `+++` front-matter block. A document without an
16
+ * opening fence has no metadata; once an opening fence is attempted, malformed
17
+ * fences are errors rather than silently becoming Markdown body text.
18
+ */
19
+ function parsePlanMetadata(plan: string): z.infer<typeof planMetadataSchema> | undefined {
20
+ if (!plan.startsWith('+++')) return undefined
21
+
22
+ const firstBreak = plan.indexOf('\n')
23
+ const opening = (firstBreak === -1 ? plan : plan.slice(0, firstBreak)).replace(/\r$/, '')
24
+ if (opening !== '+++') {
25
+ throw new Error(
26
+ 'plan front matter has a malformed opening fence — expected a line containing only "+++"',
27
+ )
28
+ }
29
+ if (firstBreak === -1) {
30
+ throw new Error('plan front matter is missing its closing "+++" fence')
31
+ }
32
+
33
+ const metadataStart = firstBreak + 1
34
+ let cursor = metadataStart
35
+ let metadataEnd: number | undefined
36
+ while (cursor <= plan.length) {
37
+ const nextBreak = plan.indexOf('\n', cursor)
38
+ const lineEnd = nextBreak === -1 ? plan.length : nextBreak
39
+ const line = plan.slice(cursor, lineEnd).replace(/\r$/, '')
40
+ if (line === '+++') {
41
+ metadataEnd = cursor
42
+ break
43
+ }
44
+ if (nextBreak === -1) break
45
+ cursor = nextBreak + 1
46
+ }
47
+ if (metadataEnd === undefined) {
48
+ throw new Error('plan front matter is missing its closing "+++" fence')
49
+ }
50
+
51
+ let parsed: unknown
52
+ try {
53
+ parsed = parseToml(plan.slice(metadataStart, metadataEnd))
54
+ } catch (error) {
55
+ const message = error instanceof Error ? error.message : String(error)
56
+ throw new Error(`plan front matter is not valid TOML: ${message}`)
57
+ }
58
+
59
+ const result = planMetadataSchema.safeParse(parsed)
60
+ if (!result.success) {
61
+ const issues = result.error.issues
62
+ .map((issue) => ` ${issue.path.join('.') || '(root)'}: ${issue.message}`)
63
+ .join('\n')
64
+ throw new Error(`plan front matter is invalid:\n${issues}`)
65
+ }
66
+ return result.data
67
+ }
68
+
69
+ /**
70
+ * Resolve a plan's requested verification set against repository config.
71
+ *
72
+ * Missing front matter preserves the historical default (all configured
73
+ * steps). An explicit list is a set, while returned order always comes from
74
+ * `[verify].steps`. `always = true` is the mandatory/non-deselectable marker.
75
+ */
76
+ export function resolvePlanVerifySteps(plan: string, config: Config): string[] {
77
+ const metadata = parsePlanMetadata(plan)
78
+ if (metadata === undefined) return [...config.verify.steps]
79
+
80
+ const selected = new Set<string>()
81
+ for (const step of metadata.verifySteps) {
82
+ if (selected.has(step)) {
83
+ throw new Error(`plan verifySteps contains duplicate step ${JSON.stringify(step)}`)
84
+ }
85
+ selected.add(step)
86
+
87
+ if (!config.verify.steps.includes(step) || !Object.hasOwn(config.verify.stepConfigs, step)) {
88
+ throw new Error(
89
+ `plan verifySteps names unknown step ${JSON.stringify(step)} — ` +
90
+ `no [verify.${step}] table is configured`,
91
+ )
92
+ }
93
+ }
94
+
95
+ for (const step of config.verify.steps) {
96
+ if (config.verify.stepConfigs[step]?.always === true && !selected.has(step)) {
97
+ throw new Error(
98
+ `plan verifySteps omits mandatory step ${JSON.stringify(step)} — ` +
99
+ `[verify.${step}].always = true steps cannot be deselected`,
100
+ )
101
+ }
102
+ }
103
+
104
+ return config.verify.steps.filter((step) => selected.has(step))
105
+ }
@@ -0,0 +1,82 @@
1
+ import type { AbEvent } from '../events/catalog'
2
+ import type { PrImageHostTarget } from '../ontology'
3
+
4
+ export type PrAttachmentDesignationEvent = Extract<AbEvent, { type: 'pr-attachment.designated' }>
5
+ export type PrAttachmentHostedEvent = Extract<AbEvent, { type: 'pr-attachment.hosted' }>
6
+
7
+ /** The host consent frozen into the build at claim time. */
8
+ export function frozenPrImageHost(events: readonly AbEvent[]): PrImageHostTarget | undefined {
9
+ let created: Extract<AbEvent, { type: 'build.created' }> | undefined
10
+ for (const event of events) {
11
+ if (event.type === 'build.created' && (created === undefined || event.seq < created.seq)) {
12
+ created = event
13
+ }
14
+ }
15
+ return created?.payload.pr?.imageHost
16
+ }
17
+
18
+ /**
19
+ * Current PR attachments are explicit designations after the latest spec
20
+ * restart, with the newest exact revision replacing earlier designations of
21
+ * the same artifact kind. Distinct kinds remain distinct attachments.
22
+ */
23
+ export function currentPrAttachments(events: readonly AbEvent[]): PrAttachmentDesignationEvent[] {
24
+ let restartSeq = 0
25
+ for (const event of events) {
26
+ if (event.type === 'spec.revised') restartSeq = Math.max(restartSeq, event.seq)
27
+ }
28
+
29
+ const byKind = new Map<string, PrAttachmentDesignationEvent>()
30
+ for (const event of events) {
31
+ if (event.type !== 'pr-attachment.designated' || event.seq <= restartSeq) {
32
+ continue
33
+ }
34
+ const previous = byKind.get(event.payload.artifact.kind)
35
+ if (previous === undefined || event.seq > previous.seq) {
36
+ byKind.set(event.payload.artifact.kind, event)
37
+ }
38
+ }
39
+ return [...byKind.values()].sort((left, right) => left.seq - right.seq)
40
+ }
41
+
42
+ /**
43
+ * Correlate one durable hosted copy to each current designation. Unknown,
44
+ * backwards, and stale correlations are ignored rather than throwing so the
45
+ * selector remains total over any structurally valid event ordering.
46
+ */
47
+ export function hostedPrAttachments(
48
+ events: readonly AbEvent[],
49
+ designations = currentPrAttachments(events),
50
+ ): Map<number, PrAttachmentHostedEvent> {
51
+ const designationBySeq = new Map(designations.map((event) => [event.seq, event]))
52
+ const hosted = new Map<number, PrAttachmentHostedEvent>()
53
+ for (const event of events) {
54
+ if (event.type !== 'pr-attachment.hosted') continue
55
+ const designation = designationBySeq.get(event.payload.designationSeq)
56
+ if (designation === undefined || event.seq <= designation.seq) continue
57
+ const previous = hosted.get(designation.seq)
58
+ if (previous === undefined || event.seq > previous.seq) {
59
+ hosted.set(designation.seq, event)
60
+ }
61
+ }
62
+ return hosted
63
+ }
64
+
65
+ /** Every durable hosted copy that has not yet received a later reclaim fact. */
66
+ export function pendingPrAttachmentReclaims(events: readonly AbEvent[]): PrAttachmentHostedEvent[] {
67
+ const hosted = events
68
+ .filter((event): event is PrAttachmentHostedEvent => event.type === 'pr-attachment.hosted')
69
+ .sort((left, right) => left.seq - right.seq)
70
+ const hostedSeqs = new Set(hosted.map((event) => event.seq))
71
+ const reclaimed = new Set<number>()
72
+ for (const event of events) {
73
+ if (
74
+ event.type === 'pr-attachment.reclaimed' &&
75
+ hostedSeqs.has(event.payload.hostedSeq) &&
76
+ event.seq > event.payload.hostedSeq
77
+ ) {
78
+ reclaimed.add(event.payload.hostedSeq)
79
+ }
80
+ }
81
+ return hosted.filter((event) => !reclaimed.has(event.seq))
82
+ }