@defrex/autobuild 0.9.0 → 0.10.1

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 (85) hide show
  1. package/README.md +97 -113
  2. package/SPEC.md +17 -4
  3. package/docs/architecture.md +109 -0
  4. package/docs/configuration.md +26 -7
  5. package/docs/remote-store-protocol.md +35 -0
  6. package/docs/website.md +4 -0
  7. package/package.json +6 -4
  8. package/packages/core/src/cli/dashboard/facts.ts +215 -0
  9. package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
  10. package/packages/core/src/cli/dashboard/model.ts +85 -147
  11. package/packages/core/src/cli/dashboard/poll.ts +7 -2
  12. package/packages/core/src/cli/dispatch-process.ts +1 -13
  13. package/packages/core/src/cli/dispatch.ts +80 -37
  14. package/packages/core/src/cli/help.ts +15 -1
  15. package/packages/core/src/cli/init-validation.ts +18 -0
  16. package/packages/core/src/cli/init.ts +3 -2
  17. package/packages/core/src/cli/installation.ts +50 -8
  18. package/packages/core/src/cli/self-update.ts +77 -2
  19. package/packages/core/src/cli/status.ts +1 -1
  20. package/packages/core/src/cli/ticket.ts +146 -1
  21. package/packages/core/src/cli/wait.ts +10 -2
  22. package/packages/core/src/cli/watch.ts +10 -2
  23. package/packages/core/src/config/live.ts +1 -0
  24. package/packages/core/src/config/pipeline-source.ts +17 -5
  25. package/packages/core/src/config/schema.ts +21 -0
  26. package/packages/core/src/integration/harness.ts +113 -1
  27. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
  28. package/packages/core/src/kernel/auto-merge-default.ts +43 -9
  29. package/packages/core/src/kernel/auto-merge.ts +99 -22
  30. package/packages/core/src/kernel/dispatch-settings.ts +42 -17
  31. package/packages/core/src/kernel/dispatch-status.ts +23 -0
  32. package/packages/core/src/kernel/engine.ts +9 -438
  33. package/packages/core/src/kernel/generators/build-log.ts +259 -0
  34. package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
  35. package/packages/core/src/kernel/generators/session-log.ts +72 -0
  36. package/packages/core/src/kernel/harvest.ts +62 -18
  37. package/packages/core/src/kernel/incremental-contract.ts +75 -0
  38. package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
  39. package/packages/core/src/kernel/incremental.ts +53 -0
  40. package/packages/core/src/kernel/log-index.ts +620 -0
  41. package/packages/core/src/kernel/pr-attachments.ts +164 -46
  42. package/packages/core/src/kernel/reducer-registry.ts +164 -0
  43. package/packages/core/src/kernel/reducer.ts +216 -74
  44. package/packages/core/src/operator/query.ts +68 -28
  45. package/packages/core/src/operator/read-view.ts +98 -0
  46. package/packages/core/src/orchestrator/prompt.ts +3 -1
  47. package/packages/core/src/ports/tickets/fake.ts +5 -1
  48. package/packages/core/src/ports/tickets/file.ts +169 -48
  49. package/packages/core/src/ports/tickets/linear.ts +33 -1
  50. package/packages/core/src/ports/tickets/operations.ts +1 -1
  51. package/packages/core/src/ports/tickets/remote.ts +8 -1
  52. package/packages/core/src/ports/types.ts +32 -1
  53. package/packages/core/src/processes/build-execution-state.ts +31 -15
  54. package/packages/core/src/processes/build-runner.ts +184 -90
  55. package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
  56. package/packages/core/src/processes/dispatcher.ts +270 -176
  57. package/packages/core/src/processes/execution-settlement.ts +72 -39
  58. package/packages/core/src/processes/harvest-execution-state.ts +41 -29
  59. package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
  60. package/packages/core/src/processes/orchestrator-tick.ts +44 -3
  61. package/packages/core/src/processes/publication-state.ts +164 -52
  62. package/packages/core/src/processes/repo-view.ts +1436 -0
  63. package/packages/core/src/processes/sandbox-state.ts +26 -2
  64. package/packages/core/src/processes/tick-state.ts +33 -0
  65. package/packages/core/src/store/adapter.ts +10 -1
  66. package/packages/core/src/store/build-scope.ts +26 -0
  67. package/packages/core/src/store/contract.ts +337 -0
  68. package/packages/core/src/store/digest.ts +71 -18
  69. package/packages/core/src/store/local/schema.ts +17 -0
  70. package/packages/core/src/store/local/store.ts +111 -4
  71. package/packages/core/src/store/memory.ts +68 -1
  72. package/packages/core/src/store/phase-session.ts +23 -0
  73. package/packages/core/src/store/remote/client.ts +50 -1
  74. package/packages/core/src/store/remote/index.ts +2 -0
  75. package/packages/core/src/store/remote/protocol.ts +15 -0
  76. package/packages/core/src/store/repo-state-events.ts +72 -7
  77. package/packages/core/src/store/session-handle.ts +30 -0
  78. package/packages/core/src/store/session-reducer.ts +52 -11
  79. package/packages/core/src/store/snapshots.ts +88 -0
  80. package/packages/core/src/store/ticket-assets.ts +41 -8
  81. package/packages/core/src/store/types.ts +38 -0
  82. package/skills/guide/SKILL.md +38 -21
  83. package/skills/guide/references/remote-store-protocol.md +35 -0
  84. package/skills/operate/SKILL.md +146 -19
  85. package/skills/tickets/SKILL.md +57 -5
@@ -0,0 +1,215 @@
1
+ /**
2
+ * The facts a dashboard row reads from a build's event array, as an
3
+ * incremental reducer: the queued-row dispatch text, the session brackets, the
4
+ * per-phase wall-clock intervals, the pause timestamp, and the seqs of the four
5
+ * loop outputs. `projectBuildFromFacts` renders a row from these plus the
6
+ * reduced `BuildState`, so a row derives from a stored accumulator and a delta
7
+ * read with no event array, and `projectBuild(…, events, …)` is the same
8
+ * projection over `dashboardFactsReducer.reduce(events)` — one implementation,
9
+ * so the snapshot and replay paths agree by construction.
10
+ *
11
+ * The accumulator is plain JSON (see `kernel/incremental.ts`).
12
+ */
13
+ import type { AbEvent } from '../../events/catalog'
14
+ import { defineReducer } from '../../kernel/incremental'
15
+ import { verifyPhase } from '../../ontology'
16
+
17
+ export const DASHBOARD_FACTS_REDUCER_VERSION = 1
18
+
19
+ /** One phase occurrence's wall-clock span, keyed by phase. `startSeq` is the
20
+ * seq of the `*.started` event, so callers can scope by the same seq
21
+ * boundaries (`restartSince`/`cycleSince`) the step STATES already use. */
22
+ export interface PhaseInterval {
23
+ start: number
24
+ end: number
25
+ startSeq: number
26
+ }
27
+
28
+ export interface PhaseTiming {
29
+ closed: PhaseInterval[]
30
+ open?: { start: number; startSeq: number }
31
+ }
32
+
33
+ export interface DispatchFailureFact {
34
+ seq: number
35
+ stage: string
36
+ attempt: number
37
+ error: string
38
+ }
39
+
40
+ /** Max seq per round of one loop output event. */
41
+ export type RoundSeqs = Record<string, number>
42
+
43
+ type SessionEvent = Extract<AbEvent, { type: 'session.started' | 'session.ended' }>
44
+
45
+ export interface DashboardFacts {
46
+ /** Seq of the latest event of each queued-row kind (`undefined` until seen). */
47
+ created?: number
48
+ workspaceProvisioned?: number
49
+ workspaceReleased?: number
50
+ specLanded?: number
51
+ commentPosted?: number
52
+ latestFailure?: DispatchFailureFact
53
+ /** The session brackets, in log order; `projectSessions` pairs them. */
54
+ sessionEvents: SessionEvent[]
55
+ intervals: Record<string, PhaseTiming>
56
+ /** Timestamp of the latest `build.paused`. */
57
+ pausedAt?: string
58
+ produced: {
59
+ planCompleted: RoundSeqs
60
+ planReviewVerdict: RoundSeqs
61
+ implementCompleted: RoundSeqs
62
+ codeReviewVerdict: RoundSeqs
63
+ }
64
+ }
65
+
66
+ /** Whether a loop output of `round` landed after `since` (the restart boundary). */
67
+ export function producedAfter(seqs: RoundSeqs, round: number, since: number): boolean {
68
+ const seq = seqs[String(round)]
69
+ return seq !== undefined && seq > since
70
+ }
71
+
72
+ function raise(seqs: RoundSeqs, round: number, seq: number): void {
73
+ const key = String(round)
74
+ const current = seqs[key]
75
+ seqs[key] = current === undefined ? seq : Math.max(current, seq)
76
+ }
77
+
78
+ function foldInterval(intervals: Record<string, PhaseTiming>, ev: AbEvent): void {
79
+ const ms = Date.parse(ev.ts)
80
+ const get = (key: string): PhaseTiming => {
81
+ let t = intervals[key]
82
+ if (t === undefined) {
83
+ t = { closed: [] }
84
+ intervals[key] = t
85
+ }
86
+ return t
87
+ }
88
+ const open = (key: string): void => {
89
+ get(key).open = { start: ms, startSeq: ev.seq }
90
+ }
91
+ const close = (key: string): void => {
92
+ const t = get(key)
93
+ if (t.open === undefined) return
94
+ t.closed.push({ start: t.open.start, end: ms, startSeq: t.open.startSeq })
95
+ delete t.open
96
+ }
97
+ // Each `*.started` opens an interval for its phase key and its terminal event
98
+ // (`.completed`/`.verdict`) closes it. A second `*.started` while one is still
99
+ // open REPLACES the open start — a §15.6-C cross-sandbox re-run starts the
100
+ // phase afresh, so the crashed attempt contributes nothing. Finalize
101
+ // post-steps (`finalize.step-completed`) have no `.started` and no interval.
102
+ switch (ev.type) {
103
+ case 'plan.started':
104
+ open('plan')
105
+ break
106
+ case 'plan.completed':
107
+ close('plan')
108
+ break
109
+ case 'plan-review.started':
110
+ open('plan-review')
111
+ break
112
+ case 'plan-review.verdict':
113
+ close('plan-review')
114
+ break
115
+ case 'implement.started':
116
+ open('implement')
117
+ break
118
+ case 'implement.completed':
119
+ close('implement')
120
+ break
121
+ case 'code-review.started':
122
+ open('code-review')
123
+ break
124
+ case 'code-review.verdict':
125
+ close('code-review')
126
+ break
127
+ case 'verify.started':
128
+ open(verifyPhase(ev.payload.step))
129
+ break
130
+ case 'verify.completed':
131
+ close(verifyPhase(ev.payload.step))
132
+ break
133
+ case 'finalize.started':
134
+ open('finalize')
135
+ break
136
+ case 'finalize.completed':
137
+ close('finalize')
138
+ break
139
+ case 'reconcile.started':
140
+ open('reconcile')
141
+ break
142
+ case 'reconcile.completed':
143
+ close('reconcile')
144
+ break
145
+ default:
146
+ break
147
+ }
148
+ }
149
+
150
+ export const dashboardFactsReducer = defineReducer<DashboardFacts, AbEvent, DashboardFacts>({
151
+ version: DASHBOARD_FACTS_REDUCER_VERSION,
152
+ initial: () => ({
153
+ sessionEvents: [],
154
+ intervals: {},
155
+ produced: {
156
+ planCompleted: {},
157
+ planReviewVerdict: {},
158
+ implementCompleted: {},
159
+ codeReviewVerdict: {},
160
+ },
161
+ }),
162
+ fold(acc, events) {
163
+ for (const event of events) {
164
+ foldInterval(acc.intervals, event)
165
+ switch (event.type) {
166
+ case 'build.created':
167
+ acc.created = event.seq
168
+ break
169
+ case 'workspace.provisioned':
170
+ acc.workspaceProvisioned = event.seq
171
+ break
172
+ case 'workspace.released':
173
+ acc.workspaceReleased = event.seq
174
+ break
175
+ case 'spec.imported':
176
+ case 'spec.authored':
177
+ acc.specLanded = event.seq
178
+ break
179
+ case 'dispatch.comment-posted':
180
+ acc.commentPosted = event.seq
181
+ break
182
+ case 'dispatch.failed':
183
+ acc.latestFailure = {
184
+ seq: event.seq,
185
+ stage: event.payload.stage,
186
+ attempt: event.payload.attempt,
187
+ error: event.payload.error,
188
+ }
189
+ break
190
+ case 'session.started':
191
+ case 'session.ended':
192
+ acc.sessionEvents.push(event)
193
+ break
194
+ case 'build.paused':
195
+ acc.pausedAt = event.ts
196
+ break
197
+ case 'plan.completed':
198
+ raise(acc.produced.planCompleted, event.payload.round, event.seq)
199
+ break
200
+ case 'plan-review.verdict':
201
+ raise(acc.produced.planReviewVerdict, event.payload.round, event.seq)
202
+ break
203
+ case 'implement.completed':
204
+ raise(acc.produced.implementCompleted, event.payload.round, event.seq)
205
+ break
206
+ case 'code-review.verdict':
207
+ raise(acc.produced.codeReviewVerdict, event.payload.round, event.seq)
208
+ break
209
+ default:
210
+ break
211
+ }
212
+ }
213
+ },
214
+ finish: (acc) => structuredClone(acc),
215
+ })
@@ -0,0 +1,223 @@
1
+ import { graphemes } from './cells'
2
+
3
+ /**
4
+ * Deterministic parsing of a rendered dashboard frame.
5
+ *
6
+ * The dashboard deliberately emits a tiny terminal vocabulary: SGR reset,
7
+ * bold/dim and six named foreground colours, plus OSC 8 hyperlinks. This
8
+ * parser accepts exactly that vocabulary and nothing else. Unknown control
9
+ * traffic is an error rather than evidence that merely looks plausible after
10
+ * bytes were dropped. Every rendition of a frame — the PNG evidence, the
11
+ * website's HTML — derives from these same exact cells.
12
+ */
13
+
14
+ const ESC = '\x1b'
15
+ const BEL = '\x07'
16
+
17
+ export type Foreground = 'foreground' | 'red' | 'green' | 'yellow' | 'blue' | 'cyan'
18
+
19
+ export interface FrameStyle {
20
+ foreground: Foreground
21
+ bold: boolean
22
+ dim: boolean
23
+ href?: string
24
+ }
25
+
26
+ export interface FrameRun {
27
+ /** Zero-based terminal cell column the run starts at. */
28
+ column: number
29
+ /** Terminal cells the run occupies. */
30
+ cells: number
31
+ text: string
32
+ style: FrameStyle
33
+ }
34
+
35
+ export interface ParsedFrameLine {
36
+ /** The line with every escape removed. */
37
+ text: string
38
+ /** Terminal cells the whole line occupies. */
39
+ cells: number
40
+ runs: FrameRun[]
41
+ }
42
+
43
+ function cloneStyle(style: FrameStyle): FrameStyle {
44
+ return {
45
+ foreground: style.foreground,
46
+ bold: style.bold,
47
+ dim: style.dim,
48
+ ...(style.href !== undefined ? { href: style.href } : {}),
49
+ }
50
+ }
51
+
52
+ function sameStyle(left: FrameStyle, right: FrameStyle): boolean {
53
+ return (
54
+ left.foreground === right.foreground &&
55
+ left.bold === right.bold &&
56
+ left.dim === right.dim &&
57
+ left.href === right.href
58
+ )
59
+ }
60
+
61
+ function applySgr(style: FrameStyle, raw: string, line: number): void {
62
+ const codes = raw === '' ? [0] : raw.split(';').map((part) => Number(part))
63
+ if (codes.some((code) => !Number.isInteger(code))) {
64
+ throw new Error(`dashboard frame line ${line}: malformed SGR sequence ESC[${raw}m`)
65
+ }
66
+ for (const code of codes) {
67
+ switch (code) {
68
+ case 0:
69
+ style.foreground = 'foreground'
70
+ style.bold = false
71
+ style.dim = false
72
+ break
73
+ case 1:
74
+ style.bold = true
75
+ break
76
+ case 2:
77
+ style.dim = true
78
+ break
79
+ case 22:
80
+ style.bold = false
81
+ style.dim = false
82
+ break
83
+ case 31:
84
+ style.foreground = 'red'
85
+ break
86
+ case 32:
87
+ style.foreground = 'green'
88
+ break
89
+ case 33:
90
+ style.foreground = 'yellow'
91
+ break
92
+ case 34:
93
+ style.foreground = 'blue'
94
+ break
95
+ case 36:
96
+ style.foreground = 'cyan'
97
+ break
98
+ case 39:
99
+ style.foreground = 'foreground'
100
+ break
101
+ default:
102
+ throw new Error(`dashboard frame line ${line}: unsupported SGR code ${code} in ESC[${raw}m`)
103
+ }
104
+ }
105
+ }
106
+
107
+ /** Parse one rendered line into styled runs pinned to their terminal cells.
108
+ * `lineNumber` is one-based and only names the line in errors. */
109
+ export function parseFrameLine(value: string, lineNumber: number): ParsedFrameLine {
110
+ const style: FrameStyle = {
111
+ foreground: 'foreground',
112
+ bold: false,
113
+ dim: false,
114
+ }
115
+ const runs: FrameRun[] = []
116
+ let text = ''
117
+ let cells = 0
118
+
119
+ const append = (cluster: string, width: number): void => {
120
+ const previous = runs.at(-1)
121
+ // ASCII runs may coalesce. Unicode clusters stay independently pinned to
122
+ // their terminal cell columns so font fallback/advance cannot shift the
123
+ // run that follows a wide glyph.
124
+ if (
125
+ previous !== undefined &&
126
+ previous.column + previous.cells === cells &&
127
+ /^[\x20-\x7e]*$/.test(previous.text) &&
128
+ /^[\x20-\x7e]$/.test(cluster) &&
129
+ sameStyle(previous.style, style)
130
+ ) {
131
+ previous.text += cluster
132
+ previous.cells += width
133
+ } else {
134
+ runs.push({ column: cells, cells: width, text: cluster, style: cloneStyle(style) })
135
+ }
136
+ text += cluster
137
+ cells += width
138
+ }
139
+
140
+ for (let index = 0; index < value.length; ) {
141
+ if (value[index] === ESC) {
142
+ const family = value[index + 1]
143
+ if (family === '[') {
144
+ const rest = value.slice(index + 2)
145
+ const match = /^([0-9;]*)m/.exec(rest)
146
+ if (match === null) {
147
+ throw new Error(
148
+ `dashboard frame line ${lineNumber}: unsupported or unterminated CSI sequence`,
149
+ )
150
+ }
151
+ applySgr(style, match[1]!, lineNumber)
152
+ index += 2 + match[0].length
153
+ continue
154
+ }
155
+ if (family === ']') {
156
+ const end = value.indexOf(BEL, index + 2)
157
+ if (end === -1) {
158
+ throw new Error(`dashboard frame line ${lineNumber}: unterminated OSC sequence`)
159
+ }
160
+ const payload = value.slice(index + 2, end)
161
+ const match = /^8;;(.*)$/.exec(payload)
162
+ if (match === null) {
163
+ throw new Error(
164
+ `dashboard frame line ${lineNumber}: unsupported OSC sequence ${JSON.stringify(payload)}`,
165
+ )
166
+ }
167
+ const href = match[1]!
168
+ style.href = href === '' ? undefined : href
169
+ index = end + 1
170
+ continue
171
+ }
172
+ throw new Error(
173
+ `dashboard frame line ${lineNumber}: unsupported escape family ${JSON.stringify(family ?? '')}`,
174
+ )
175
+ }
176
+
177
+ const nextEscape = value.indexOf(ESC, index)
178
+ const stop = nextEscape === -1 ? value.length : nextEscape
179
+ const plain = value.slice(index, stop)
180
+ for (const character of plain) {
181
+ const code = character.codePointAt(0)!
182
+ if (code < 0x20 || code === 0x7f || (code >= 0x80 && code <= 0x9f)) {
183
+ throw new Error(
184
+ `dashboard frame line ${lineNumber}: unsupported control U+${code
185
+ .toString(16)
186
+ .toUpperCase()
187
+ .padStart(4, '0')}`,
188
+ )
189
+ }
190
+ }
191
+ for (const cluster of graphemes(plain)) append(cluster.text, cluster.width)
192
+ index = stop
193
+ }
194
+
195
+ if (style.href !== undefined) {
196
+ throw new Error(`dashboard frame line ${lineNumber}: OSC 8 hyperlink was not closed`)
197
+ }
198
+ if (style.foreground !== 'foreground' || style.bold || style.dim) {
199
+ throw new Error(
200
+ `dashboard frame line ${lineNumber}: SGR style was not reset before end of line`,
201
+ )
202
+ }
203
+ return { text, cells, runs }
204
+ }
205
+
206
+ /** Parse a whole frame, holding every line to the declared terminal width. */
207
+ export function parseFrame(lines: readonly string[], columns: number): ParsedFrameLine[] {
208
+ if (!Number.isInteger(columns) || columns <= 0) {
209
+ throw new Error(`dashboard frame columns must be a positive integer, got ${columns}`)
210
+ }
211
+ if (lines.length === 0) {
212
+ throw new Error('dashboard frame is empty')
213
+ }
214
+ const parsed = lines.map((line, index) => parseFrameLine(line, index + 1))
215
+ for (const [index, line] of parsed.entries()) {
216
+ if (line.cells > columns) {
217
+ throw new Error(
218
+ `dashboard frame line ${index + 1} is ${line.cells} cells wide, exceeding declared terminal width ${columns}`,
219
+ )
220
+ }
221
+ }
222
+ return parsed
223
+ }