@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,155 @@
1
+ /**
2
+ * Persistence-chain mechanics for stall detection (SPEC §15.4, D4).
3
+ *
4
+ * The split follows the constitution: deciding whether a new finding is *the
5
+ * same disagreement* as an earlier one is fuzzy → judgment → the reviewer
6
+ * (fresh each round) marks `persists`. Everything in this module is the
7
+ * deterministic half: following the marked links and applying the threshold.
8
+ *
9
+ * Chain model:
10
+ * - A chain is identified by its root — a finding whose `persists` resolves to
11
+ * no earlier-round finding. `persists` links are honored only when the
12
+ * target exists in a strictly earlier round; unknown ids and same/later
13
+ * round targets are not links (ids are deposit-stamped, so a reviewer can
14
+ * only ever reference rounds it was shown — §8.3).
15
+ * - Branching persists (two findings continuing one ancestor) extend the same
16
+ * chain; a finding with multiple `persists` entries joins every chain its
17
+ * ancestors belong to.
18
+ * - A chain's `rounds` is the longest run of *consecutive* rounds it occupies
19
+ * (SPEC §15.4: a chain "survives" N rounds). A round in which no reviewer
20
+ * continued the chain breaks the streak: that round's fresh skeptic saw all
21
+ * prior findings (§8.3) and judged the disagreement resolved, so a later
22
+ * re-raise starts surviving anew — though the chain's `ids` keep the full
23
+ * history.
24
+ */
25
+ import type { Finding } from '../ontology'
26
+
27
+ export interface FindingChain {
28
+ /** Member finding ids in round order, chain root first. */
29
+ ids: string[]
30
+ /** Count of distinct consecutive rounds the chain spans (longest streak). */
31
+ rounds: number
32
+ }
33
+
34
+ interface ChainMembers {
35
+ ids: string[]
36
+ /** Parallel to `ids`: the 1-based round each member was found in. */
37
+ memberRounds: number[]
38
+ }
39
+
40
+ /**
41
+ * Follow `persists` links across rounds (`roundFindings[0]` is round 1) and
42
+ * group findings into chains, one per root. Single fresh findings are chains
43
+ * of `rounds: 1` — staying below any sane threshold is what "fresh" means.
44
+ */
45
+ export function persistenceChains(roundFindings: Finding[][]): FindingChain[] {
46
+ return buildChains(roundFindings).map((chain) => ({
47
+ ids: chain.ids,
48
+ rounds: longestConsecutiveRun(chain.memberRounds),
49
+ }))
50
+ }
51
+
52
+ /**
53
+ * Chains whose streak has reached `stallRounds` (SPEC §15.4) — minus chains a
54
+ * human already resolved via `escalation.answered {dismiss-finding}`
55
+ * (§15.6-B).
56
+ *
57
+ * Dismissal semantics: a chain is human-resolved when every finding in its
58
+ * most recent round (its tip — the live continuations of the disagreement) is
59
+ * dismissed. Earlier members are history and need no dismissal. If a later
60
+ * round persists into a dismissed chain with a finding that is not itself
61
+ * dismissed, the tip moves and the chain resurrects — with its full streak
62
+ * intact, so a reviewer overriding a human dismissal re-escalates at the very
63
+ * next revise rather than burning `stallRounds` more rounds.
64
+ */
65
+ export function stalledChains(
66
+ roundFindings: Finding[][],
67
+ stallRounds: number,
68
+ dismissedIds?: ReadonlySet<string>,
69
+ ): FindingChain[] {
70
+ const stalled: FindingChain[] = []
71
+ for (const chain of buildChains(roundFindings)) {
72
+ const rounds = longestConsecutiveRun(chain.memberRounds)
73
+ if (rounds < stallRounds) continue
74
+ if (dismissedIds !== undefined && tipDismissed(chain, dismissedIds)) continue
75
+ stalled.push({ ids: chain.ids, rounds })
76
+ }
77
+ return stalled
78
+ }
79
+
80
+ function buildChains(roundFindings: Finding[][]): ChainMembers[] {
81
+ const byId = new Map<string, { finding: Finding; round: number }>()
82
+ const traversal: string[] = []
83
+ roundFindings.forEach((findings, index) => {
84
+ for (const finding of findings) {
85
+ if (byId.has(finding.id)) continue
86
+ byId.set(finding.id, { finding, round: index + 1 })
87
+ traversal.push(finding.id)
88
+ }
89
+ })
90
+
91
+ // Links point strictly to earlier rounds, so recursion terminates; memoized
92
+ // so shared ancestors are resolved once.
93
+ const rootsMemo = new Map<string, ReadonlySet<string>>()
94
+ function rootsOf(id: string): ReadonlySet<string> {
95
+ const memoized = rootsMemo.get(id)
96
+ if (memoized !== undefined) return memoized
97
+ const node = byId.get(id)
98
+ if (node === undefined) return new Set()
99
+ const parents = node.finding.persists.filter((parentId) => {
100
+ const parent = byId.get(parentId)
101
+ return parent !== undefined && parent.round < node.round
102
+ })
103
+ const roots = new Set<string>()
104
+ if (parents.length === 0) {
105
+ roots.add(id)
106
+ } else {
107
+ for (const parentId of parents) {
108
+ for (const root of rootsOf(parentId)) roots.add(root)
109
+ }
110
+ }
111
+ rootsMemo.set(id, roots)
112
+ return roots
113
+ }
114
+
115
+ // Traversal is round-major, and a root is always its chain's earliest
116
+ // member, so each chain lists its root first and members in round order.
117
+ const chains = new Map<string, ChainMembers>()
118
+ for (const id of traversal) {
119
+ const node = byId.get(id)
120
+ if (node === undefined) continue
121
+ for (const root of rootsOf(id)) {
122
+ let chain = chains.get(root)
123
+ if (chain === undefined) {
124
+ chain = { ids: [], memberRounds: [] }
125
+ chains.set(root, chain)
126
+ }
127
+ chain.ids.push(id)
128
+ chain.memberRounds.push(node.round)
129
+ }
130
+ }
131
+ return [...chains.values()]
132
+ }
133
+
134
+ function longestConsecutiveRun(rounds: readonly number[]): number {
135
+ const occupied = new Set(rounds)
136
+ let longest = 0
137
+ for (const round of occupied) {
138
+ if (occupied.has(round - 1)) continue
139
+ let length = 1
140
+ while (occupied.has(round + length)) length += 1
141
+ if (length > longest) longest = length
142
+ }
143
+ return longest
144
+ }
145
+
146
+ function tipDismissed(chain: ChainMembers, dismissedIds: ReadonlySet<string>): boolean {
147
+ const tipRound = Math.max(...chain.memberRounds)
148
+ for (let i = 0; i < chain.ids.length; i += 1) {
149
+ const id = chain.ids[i]
150
+ if (chain.memberRounds[i] === tipRound && id !== undefined && !dismissedIds.has(id)) {
151
+ return false
152
+ }
153
+ }
154
+ return true
155
+ }
@@ -0,0 +1,52 @@
1
+ import { verifyPathGlobError } from '../config/schema'
2
+
3
+ /**
4
+ * Pure verify-applicability rule. `always` is explicit rather than represented
5
+ * by an empty selector list, so mandatory-gate precedence cannot depend on
6
+ * truthiness or ordering.
7
+ */
8
+ export type VerifyApplicabilityRule =
9
+ | { kind: 'always' }
10
+ | { kind: 'paths'; step: string; paths: readonly string[] }
11
+
12
+ export type VerifyApplicabilityResult = { applies: true } | { applies: false; reason: string }
13
+
14
+ /** Stable, queryable reason used by the kernel-authored skipped outcome. */
15
+ export function pathExclusionReason(step: string, paths: readonly string[]): string {
16
+ return `excluded by [verify.${step}].paths: no changed path matched ${JSON.stringify(paths)}`
17
+ }
18
+
19
+ /**
20
+ * Evaluate one rule against Git's repository-relative, `/`-separated changed
21
+ * paths. Matching is case-sensitive and OR-shaped: one selector matching one
22
+ * changed path is enough. No I/O and no event writes occur here.
23
+ */
24
+ export function evaluateVerifyApplicability(
25
+ rule: VerifyApplicabilityRule,
26
+ changedPaths: readonly string[],
27
+ ): VerifyApplicabilityResult {
28
+ if (rule.kind === 'always') return { applies: true }
29
+
30
+ // Parsed Config is the normal caller and has already validated these. Keep
31
+ // this exported pure seam fail-closed for direct callers too: Bun.Glob is
32
+ // never asked to interpret syntax outside the documented subset.
33
+ for (const path of rule.paths) {
34
+ const error = verifyPathGlobError(path)
35
+ if (error !== undefined) {
36
+ throw new Error(
37
+ `invalid [verify.${rule.step}].paths selector ${JSON.stringify(path)}: ${error}`,
38
+ )
39
+ }
40
+ }
41
+
42
+ const globs = rule.paths.map((path) => new Bun.Glob(path))
43
+ for (const changedPath of changedPaths) {
44
+ for (const glob of globs) {
45
+ if (glob.match(changedPath)) return { applies: true }
46
+ }
47
+ }
48
+ return {
49
+ applies: false,
50
+ reason: pathExclusionReason(rule.step, rule.paths),
51
+ }
52
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Approximate Markdown reference extraction, shared by the checks that ask
3
+ * "does this link point at something that exists?".
4
+ *
5
+ * These are regexes, not a CommonMark parser. Reference-style targets
6
+ * (`![alt][ref]`) and targets containing parentheses are invisible to them.
7
+ * Both callers are link-existence checks, where an unseen reference fails
8
+ * toward a loud complaint rather than a silent pass, so the approximation is
9
+ * safe — but a false report is a signal to widen the parser, never to delete
10
+ * the thing it failed to see.
11
+ */
12
+
13
+ /**
14
+ * Blanks out fenced code blocks, preserving line count. A target inside a fence
15
+ * is sample text, not a rendered reference. Backtick and tilde fences are
16
+ * tracked separately so a `~~~` line inside a ``` block does not close it.
17
+ */
18
+ export function withoutFencedCode(markdown: string): string {
19
+ const lines = markdown.split('\n')
20
+ let fence: '`' | '~' | undefined
21
+ return lines
22
+ .map((line) => {
23
+ const opening = line.match(/^\s*(`{3,}|~{3,})/)
24
+ if (opening) {
25
+ const marker = opening[1]![0] as '`' | '~'
26
+ if (fence === undefined) fence = marker
27
+ else if (fence === marker) fence = undefined
28
+ return ''
29
+ }
30
+ return fence === undefined ? line : ''
31
+ })
32
+ .join('\n')
33
+ }
34
+
35
+ /**
36
+ * Inline link and image targets, in source order. Angle brackets are unwrapped
37
+ * and a title suffix is dropped, so `[a](<b.md> "T")` yields `b.md`.
38
+ */
39
+ export function markdownTargets(markdown: string): string[] {
40
+ const targets: string[] = []
41
+ for (const match of markdown.matchAll(/!?\[[^\]]*\]\(([^)]+)\)/g)) {
42
+ const inline = match[1]!.trim()
43
+ // An angle-bracketed destination runs to its closing `>`, and whatever
44
+ // follows is the title. The two must be read together: unwrapping first
45
+ // fails whenever a title is present (the text no longer ends in `>`), and
46
+ // splitting the title off first would truncate a bracketed destination that
47
+ // legitimately contains whitespace and a quote. A `<` with no closing `>`
48
+ // is malformed, and falls through to the unbracketed reading.
49
+ const closing = inline.startsWith('<') ? inline.indexOf('>') : -1
50
+ targets.push(closing === -1 ? inline.split(/\s+["']/u, 1)[0]! : inline.slice(1, closing))
51
+ }
52
+ return targets
53
+ }
54
+
55
+ /**
56
+ * `src` attributes of raw `<img>` tags, in source order. Kept separate from
57
+ * `markdownTargets` so callers that only care about Markdown syntax keep their
58
+ * existing behaviour: Markdown permits raw HTML, and this repository's README
59
+ * already mixes it in, so a centred hero image would otherwise be invisible.
60
+ */
61
+ export function htmlImageTargets(markdown: string): string[] {
62
+ const targets: string[] = []
63
+ for (const match of markdown.matchAll(/<img\b[^>]*?\ssrc\s*=\s*("([^"]*)"|'([^']*)')/giu)) {
64
+ targets.push((match[2] ?? match[3])!.trim())
65
+ }
66
+ return targets
67
+ }
@@ -0,0 +1,288 @@
1
+ /**
2
+ * The ontology (SPEC §4): one name, used everywhere; every noun lives in
3
+ * exactly one layer. Schemas are the source of truth — the `ab` CLI and the
4
+ * store validate deposits against them (D6), and the TypeScript types are
5
+ * inferred.
6
+ *
7
+ * Reserved-word discipline: **build** names the whole pipeline execution and
8
+ * nothing else; the coding phase is **implement**.
9
+ */
10
+ import { z } from 'zod'
11
+
12
+ // ── Phases (SPEC §5) ─────────────────────────────────────────────────────────
13
+
14
+ export const CORE_PHASES = [
15
+ 'plan',
16
+ 'plan-review',
17
+ 'implement',
18
+ 'code-review',
19
+ 'finalize',
20
+ 'reconcile',
21
+ ] as const
22
+ export type CorePhase = (typeof CORE_PHASES)[number]
23
+
24
+ /** Verify steps are config-defined — an open set (SPEC §15.2.1). */
25
+ export type VerifyPhase = `verify:${string}`
26
+ export type Phase = CorePhase | VerifyPhase
27
+
28
+ export function isCorePhase(value: string): value is CorePhase {
29
+ return (CORE_PHASES as readonly string[]).includes(value)
30
+ }
31
+
32
+ export function isVerifyPhase(value: string): value is VerifyPhase {
33
+ return value.startsWith('verify:') && value.length > 'verify:'.length
34
+ }
35
+
36
+ export function isPhase(value: string): value is Phase {
37
+ return isCorePhase(value) || isVerifyPhase(value)
38
+ }
39
+
40
+ /** `verify:e2e` → `e2e` */
41
+ export function verifyStep(phase: VerifyPhase): string {
42
+ return phase.slice('verify:'.length)
43
+ }
44
+
45
+ export function verifyPhase(step: string): VerifyPhase {
46
+ return `verify:${step}`
47
+ }
48
+
49
+ export const phaseSchema = z.custom<Phase>((value) => typeof value === 'string' && isPhase(value), {
50
+ message: 'not a phase: expected a core phase or "verify:<step>"',
51
+ })
52
+
53
+ // ── Findings (SPEC §15.4) ────────────────────────────────────────────────────
54
+
55
+ export const severitySchema = z.enum(['blocking', 'important', 'minor'])
56
+ export type Severity = z.infer<typeof severitySchema>
57
+
58
+ export const findingSchema = z.strictObject({
59
+ /** Kernel-assigned at deposit, stable for the build (e.g. `f_3a91`). */
60
+ id: z.string().min(1),
61
+ severity: severitySchema,
62
+ file: z.string().optional(),
63
+ lines: z.array(z.number().int()).optional(),
64
+ summary: z.string().min(1),
65
+ detail: z.string().optional(),
66
+ /** Reviewer-marked: ids of earlier findings this one continues (stall input). */
67
+ persists: z.array(z.string()).default([]),
68
+ })
69
+ export type Finding = z.infer<typeof findingSchema>
70
+
71
+ /** What an agent submits — ids are stamped at deposit, never self-assigned. */
72
+ export const findingDraftSchema = findingSchema.omit({ id: true })
73
+ export type FindingDraft = z.infer<typeof findingDraftSchema>
74
+
75
+ // ── Verdicts (SPEC §4, §10) ──────────────────────────────────────────────────
76
+
77
+ /** Review-phase vocabulary (`plan-review`, `code-review`). */
78
+ export const reviewVerdictKindSchema = z.enum(['approve', 'revise', 'escalate'])
79
+ export type ReviewVerdictKind = z.infer<typeof reviewVerdictKindSchema>
80
+
81
+ /** Durable outcome of any `verify.completed` fact. */
82
+ export const verifyOutcomeSchema = z.enum(['pass', 'fail', 'skipped'])
83
+ export type VerifyOutcome = z.infer<typeof verifyOutcomeSchema>
84
+
85
+ /** Agent-verify CLI vocabulary (`verify:*` steps of kind "agent"). */
86
+ export const verifyVerdictKindSchema = z.enum(['pass', 'fail', 'skip'])
87
+ export type VerifyVerdictKind = z.infer<typeof verifyVerdictKindSchema>
88
+
89
+ /** Structured outcome of a review, as consumed by `converge` (SPEC §10). */
90
+ export type Verdict =
91
+ | { verdict: 'approve' }
92
+ | { verdict: 'revise'; findings: Finding[] }
93
+ | { verdict: 'escalate'; reason: string }
94
+
95
+ // ── Observations (SPEC §4, §12) ──────────────────────────────────────────────
96
+
97
+ export const observationKindSchema = z.enum(['followup', 'refactor', 'latent-bug'])
98
+ export type ObservationKind = z.infer<typeof observationKindSchema>
99
+
100
+ // ── Escalations (SPEC §11, §15.3) ────────────────────────────────────────────
101
+
102
+ export const escalationSourceSchema = z.enum(['agent', 'stall', 'policy'])
103
+ export type EscalationSource = z.infer<typeof escalationSourceSchema>
104
+
105
+ /** Closed identity for every policy-authored escalation condition. `round`
106
+ * scopes an occurrence; it does not identify which policy guard raised it. */
107
+ export const policyEscalationCauseSchema = z.enum([
108
+ 'review-round-limit',
109
+ 'verify-failure-limit',
110
+ 'reconcile-no-progress',
111
+ 'setup-failure-limit',
112
+ 'infrastructure-failure-limit',
113
+ 'phase-attempt-limit',
114
+ 'non-retryable-phase-failure',
115
+ ])
116
+ export type PolicyEscalationCause = z.infer<typeof policyEscalationCauseSchema>
117
+
118
+ /** Setup may block a build through the escalation protocol without becoming a
119
+ * pipeline phase. Keeping this target separate preserves the fixed phase
120
+ * grammar while allowing the ordinary answer/retry machinery to re-arm setup. */
121
+ export type EscalationTarget = Phase | 'setup'
122
+ export const escalationTargetSchema = z.custom<EscalationTarget>(
123
+ (value) => value === 'setup' || (typeof value === 'string' && isPhase(value)),
124
+ { message: 'not an escalation target: expected "setup", a core phase, or "verify:<step>"' },
125
+ )
126
+
127
+ export const escalationResolutionSchema = z.enum([
128
+ 'guidance',
129
+ 'dismiss-finding',
130
+ 'revise-spec',
131
+ 'abort',
132
+ /** Bare re-attempt with no phase guidance: human UI, or policy-only startup. */
133
+ 'retry',
134
+ ])
135
+ export type EscalationResolution = z.infer<typeof escalationResolutionSchema>
136
+
137
+ // ── Tickets ──────────────────────────────────────────────────────────────────
138
+
139
+ export const ticketRefSchema = z.strictObject({
140
+ source: z.string().min(1),
141
+ id: z.string().min(1),
142
+ url: z.string().optional(),
143
+ title: z.string().optional(),
144
+ })
145
+ export type TicketRef = z.infer<typeof ticketRefSchema>
146
+
147
+ // ── Artifacts ────────────────────────────────────────────────────────────────
148
+
149
+ /**
150
+ * Revisions are 0-based per kind: the first deposit of a kind is rev 0
151
+ * (normative in SPEC §6.3 — the spec is "kind `spec`, revision 0"), and every
152
+ * later deposit of the same kind is max+1.
153
+ */
154
+ export const artifactRefSchema = z.strictObject({
155
+ kind: z.string().min(1),
156
+ rev: z.number().int().nonnegative(),
157
+ })
158
+ export type ArtifactRef = z.infer<typeof artifactRefSchema>
159
+
160
+ /** Closed set of core kinds; verify reports are per-step (open set). */
161
+ export const CORE_ARTIFACT_KINDS = [
162
+ 'spec',
163
+ 'plan',
164
+ 'plan-review',
165
+ 'implement-notes',
166
+ 'code-review',
167
+ 'pr-description',
168
+ 'reconcile-notes',
169
+ 'transcript',
170
+ ] as const
171
+ export type CoreArtifactKind = (typeof CORE_ARTIFACT_KINDS)[number]
172
+
173
+ /** `e2e` → `verify-report:e2e` */
174
+ export function verifyReportKind(step: string): string {
175
+ return `verify-report:${step}`
176
+ }
177
+
178
+ // ── Pull-request attachments ─────────────────────────────────────────────────
179
+
180
+ /** Deliberately narrower than GitHub's full repository-name policy: one
181
+ * non-blank, whitespace-free `owner/repo` pair is enough for deterministic
182
+ * routing without pretending to validate a remote identifier locally. */
183
+ export const githubRepositorySchema = z
184
+ .string()
185
+ .regex(
186
+ /^[^/\s]+\/[^/\s]+$/,
187
+ 'expected exactly one non-blank GitHub repository pair in "owner/repo" form',
188
+ )
189
+ export type GitHubRepository = z.infer<typeof githubRepositorySchema>
190
+
191
+ /** Optional, frozen destination for review-window image copies. */
192
+ export const prImageHostSchema = z.strictObject({
193
+ provider: z.literal('github-release'),
194
+ repository: githubRepositorySchema,
195
+ releaseId: z.number().int().positive(),
196
+ })
197
+ export type PrImageHostTarget = z.infer<typeof prImageHostSchema>
198
+
199
+ /** A basename safe to render and pass back to `ab artifact download`. */
200
+ export const prAttachmentFilenameSchema = z
201
+ .string()
202
+ .min(1)
203
+ .max(255)
204
+ .refine((value) => value.trim().length > 0, 'attachment filename must be non-blank')
205
+ .refine((value) => value !== '.' && value !== '..', 'attachment filename must name a file')
206
+ .refine(
207
+ (value) => !/[\\/\u0000-\u001f\u007f]/.test(value),
208
+ 'attachment filename must be a basename without path separators or control characters',
209
+ )
210
+
211
+ /** Normalized MIME media type: lowercase type/subtype with no parameters. */
212
+ export const mediaTypeSchema = z
213
+ .string()
214
+ .regex(
215
+ /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/,
216
+ 'expected a normalized lowercase media type without parameters',
217
+ )
218
+
219
+ /** One exact BuildStore artifact explicitly designated for PR projection. */
220
+ export const prAttachmentSchema = z.strictObject({
221
+ artifact: artifactRefSchema,
222
+ filename: prAttachmentFilenameSchema,
223
+ mediaType: mediaTypeSchema,
224
+ })
225
+ export type PrAttachment = z.infer<typeof prAttachmentSchema>
226
+
227
+ /** Durable deletion handle returned after one image attachment is hosted. */
228
+ export const hostedPrAttachmentAssetSchema = z.strictObject({
229
+ provider: z.literal('github-release'),
230
+ repository: githubRepositorySchema,
231
+ releaseId: z.number().int().positive(),
232
+ assetId: z.number().int().positive(),
233
+ url: z
234
+ .string()
235
+ .url()
236
+ .refine((url) => url.startsWith('https://'), 'hosted PR attachment URL must use HTTPS'),
237
+ })
238
+ export type HostedPrAttachmentAsset = z.infer<typeof hostedPrAttachmentAssetSchema>
239
+
240
+ // ── Builds ───────────────────────────────────────────────────────────────────
241
+
242
+ export const buildOutcomeSchema = z.enum(['merged', 'closed-unmerged', 'abandoned', 'discarded'])
243
+ export type BuildOutcome = z.infer<typeof buildOutcomeSchema>
244
+
245
+ /** Derived status — a reduction of events, never stored (SPEC §15.5). */
246
+ export const BUILD_STATUSES = ['queued', 'running', 'paused', 'blocked', 'done', 'aborted'] as const
247
+ export type BuildStatus = (typeof BUILD_STATUSES)[number]
248
+
249
+ export const commitRangeSchema = z.strictObject({
250
+ base: z.string().min(1),
251
+ head: z.string().min(1),
252
+ })
253
+ export type CommitRange = z.infer<typeof commitRangeSchema>
254
+
255
+ /** How a workspace's branch tip was selected at provision time. */
256
+ export const workspaceBaseSchema = z.discriminatedUnion('source', [
257
+ z.strictObject({
258
+ source: z.literal('remote'),
259
+ sha: z.string().min(1),
260
+ }),
261
+ z.strictObject({
262
+ source: z.literal('local'),
263
+ sha: z.string().min(1),
264
+ remoteError: z.string().min(1),
265
+ }),
266
+ z.strictObject({
267
+ source: z.literal('existing'),
268
+ sha: z.string().min(1),
269
+ }),
270
+ ])
271
+ export type WorkspaceBase = z.infer<typeof workspaceBaseSchema>
272
+
273
+ // ── Feedback (phase-occurrence input — SPEC §10, §15.3 started events) ─────
274
+
275
+ export const feedbackSchema = z.union([
276
+ /** Findings from a revise verdict, by id (payloads carry refs, not blobs). */
277
+ z.strictObject({ findings: z.array(z.string()) }),
278
+ /** A verify failure report routed back into the code loop (SPEC §5). */
279
+ z.strictObject({
280
+ verify: z.strictObject({ step: z.string(), report: artifactRefSchema }),
281
+ }),
282
+ /** A human escalation answer fed to a producer or agent verifier as
283
+ * authoritative feedback (§15.6-B). */
284
+ z.strictObject({
285
+ guidance: z.strictObject({ escalation: z.string(), answer: z.string() }),
286
+ }),
287
+ ])
288
+ export type Feedback = z.infer<typeof feedbackSchema>