@defrex/autobuild 0.12.0 → 0.14.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/README.md +1 -1
- package/SPEC.md +138 -16
- package/bin/ab-dispatch-kernel.ts +4 -6
- package/docs/architecture.md +155 -73
- package/docs/configuration.md +41 -9
- package/docs/hosted-service.md +31 -5
- package/docs/operator-api.md +8 -1
- package/docs/remote-store-protocol.md +94 -2
- package/docs/remote-ticket-protocol.md +151 -9
- package/docs/setup.md +33 -7
- package/docs/ticket-migration.md +64 -0
- package/package.json +4 -3
- package/packages/core/src/cli/attention.ts +18 -164
- package/packages/core/src/cli/build-control.ts +23 -16
- package/packages/core/src/cli/bulk-control.ts +3 -2
- package/packages/core/src/cli/connect.ts +141 -38
- package/packages/core/src/cli/context.ts +23 -79
- package/packages/core/src/cli/dashboard/model.ts +36 -22
- package/packages/core/src/cli/dashboard/poll.ts +1 -1
- package/packages/core/src/cli/dispatch-frontend.ts +24 -67
- package/packages/core/src/cli/dispatch-process.ts +14 -7
- package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
- package/packages/core/src/cli/dispatch.ts +304 -82
- package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
- package/packages/core/src/cli/harvest.ts +84 -42
- package/packages/core/src/cli/help.ts +14 -8
- package/packages/core/src/cli/historical-build-state.ts +27 -0
- package/packages/core/src/cli/installation.ts +67 -13
- package/packages/core/src/cli/main.ts +23 -5
- package/packages/core/src/cli/mcp.ts +2 -1
- package/packages/core/src/cli/pr-attachments.ts +10 -13
- package/packages/core/src/cli/pr-summary-publication.ts +27 -60
- package/packages/core/src/cli/pr-summary.ts +43 -13
- package/packages/core/src/cli/publication-settlement.ts +10 -20
- package/packages/core/src/cli/repository-status.ts +38 -2
- package/packages/core/src/cli/self-update.ts +5 -1
- package/packages/core/src/cli/status.ts +62 -32
- package/packages/core/src/cli/terminals.ts +25 -142
- package/packages/core/src/cli/ticket-assets.ts +7 -2
- package/packages/core/src/cli/ticket-editor.ts +82 -0
- package/packages/core/src/cli/ticket.ts +166 -36
- package/packages/core/src/cli/wait.ts +91 -84
- package/packages/core/src/cli/watch.ts +60 -97
- package/packages/core/src/config/pipeline-source.ts +4 -3
- package/packages/core/src/config/schema.ts +35 -3
- package/packages/core/src/connection.ts +3 -3
- package/packages/core/src/events/catalog.ts +3 -2
- package/packages/core/src/events/payloads.ts +6 -0
- package/packages/core/src/events/repository.ts +7 -3
- package/packages/core/src/integration/harness.ts +38 -4
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2444 -9
- package/packages/core/src/kernel/auto-merge.ts +19 -3
- package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
- package/packages/core/src/kernel/generators/build-log.ts +1 -1
- package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
- package/packages/core/src/kernel/harvest.ts +25 -23
- package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
- package/packages/core/src/kernel/log-index-value.ts +38 -0
- package/packages/core/src/kernel/read-facts.ts +637 -0
- package/packages/core/src/kernel/reducer-registry.ts +139 -3
- package/packages/core/src/kernel/repository-attention.ts +179 -0
- package/packages/core/src/kernel/service-facts.ts +135 -0
- package/packages/core/src/kernel/terminal-guard.ts +133 -0
- package/packages/core/src/ontology.ts +1 -0
- package/packages/core/src/operator/annotations.ts +12 -0
- package/packages/core/src/operator/control.ts +5 -6
- package/packages/core/src/operator/index.ts +7 -0
- package/packages/core/src/operator/protocol.ts +19 -2
- package/packages/core/src/operator/query.ts +37 -20
- package/packages/core/src/operator/read-view.ts +4 -13
- package/packages/core/src/operator/registry.ts +61 -8
- package/packages/core/src/operator/requests.ts +2 -14
- package/packages/core/src/operator/sandbox.ts +10 -9
- package/packages/core/src/operator/ticket-source-backend.ts +17 -21
- package/packages/core/src/operator/tickets.ts +132 -22
- package/packages/core/src/plugin-sdk/index.ts +19 -0
- package/packages/core/src/plugins/runtimes.ts +5 -1
- package/packages/core/src/ports/runner/session-env.ts +2 -0
- package/packages/core/src/ports/tickets/contract.ts +66 -1
- package/packages/core/src/ports/tickets/create.ts +48 -12
- package/packages/core/src/ports/tickets/fake.ts +30 -1
- package/packages/core/src/ports/tickets/file-assets.ts +159 -0
- package/packages/core/src/ports/tickets/file-codec.ts +90 -0
- package/packages/core/src/ports/tickets/file.ts +114 -120
- package/packages/core/src/ports/tickets/grants.ts +47 -0
- package/packages/core/src/ports/tickets/hosted-index.ts +17 -0
- package/packages/core/src/ports/tickets/identity.ts +20 -0
- package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
- package/packages/core/src/ports/tickets/linear.ts +74 -9
- package/packages/core/src/ports/tickets/migration.ts +336 -0
- package/packages/core/src/ports/tickets/operations.ts +38 -7
- package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +95 -0
- package/packages/core/src/ports/tickets/remote.ts +60 -3
- package/packages/core/src/ports/tickets/requested-id.ts +46 -0
- package/packages/core/src/ports/tickets/revision.ts +62 -0
- package/packages/core/src/ports/tickets/states.ts +61 -0
- package/packages/core/src/ports/types.ts +40 -3
- package/packages/core/src/ports/workspace/build-execution.ts +1 -0
- package/packages/core/src/ports/workspace/create.ts +5 -1
- package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +8 -4
- package/packages/core/src/processes/build-child.ts +6 -6
- package/packages/core/src/processes/build-runner.ts +62 -70
- package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
- package/packages/core/src/processes/dispatcher.ts +223 -205
- package/packages/core/src/processes/execution-no-progress.ts +176 -0
- package/packages/core/src/processes/harvest-child.ts +30 -4
- package/packages/core/src/processes/harvest-execution-state.ts +15 -9
- package/packages/core/src/processes/harvest-runner.ts +58 -74
- package/packages/core/src/processes/harvest.ts +72 -29
- package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
- package/packages/core/src/processes/orchestrator-tick.ts +2 -2
- package/packages/core/src/processes/phase-failure-budget.ts +1 -0
- package/packages/core/src/processes/publication-loss.ts +35 -21
- package/packages/core/src/processes/repo-view.ts +229 -1099
- package/packages/core/src/processes/tick-publication.ts +110 -0
- package/packages/core/src/store/adapter.ts +23 -0
- package/packages/core/src/store/build-scope.ts +17 -0
- package/packages/core/src/store/contract.ts +283 -1
- package/packages/core/src/store/local/schema.ts +1 -0
- package/packages/core/src/store/local/store.ts +180 -55
- package/packages/core/src/store/memory.ts +119 -29
- package/packages/core/src/store/phase-session.ts +17 -0
- package/packages/core/src/store/reduced-reads.ts +239 -0
- package/packages/core/src/store/reducer-value-shapes.json +14056 -0
- package/packages/core/src/store/reducer-value-validation.ts +96 -0
- package/packages/core/src/store/reducer-values.ts +145 -0
- package/packages/core/src/store/remote/client.ts +41 -0
- package/packages/core/src/store/remote/compatibility.ts +2 -0
- package/packages/core/src/store/remote/index.ts +19 -0
- package/packages/core/src/store/remote/protocol.ts +16 -0
- package/packages/core/src/store/remote/version.ts +3 -1
- package/packages/core/src/store/repo-state-events.ts +1 -1
- package/packages/core/src/store/repo-tick.ts +72 -0
- package/packages/core/src/store/session-handle.ts +19 -0
- package/packages/core/src/store/snapshots.ts +10 -0
- package/packages/core/src/store/ticket-asset-snapshots.ts +192 -0
- package/packages/core/src/store/ticket-assets.ts +86 -8
- package/packages/core/src/store/types.ts +40 -3
- package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
- package/packages/core/src/testing/index.ts +8 -0
- package/packages/core/src/testing/reduced-read-budget.ts +462 -0
- package/skills/guide/SKILL.md +97 -12
- package/skills/guide/references/operator-api.md +8 -1
- package/skills/guide/references/remote-store-protocol.md +94 -2
- package/skills/guide/references/remote-ticket-protocol.md +151 -9
- package/skills/guide/references/setup.md +33 -7
- package/skills/guide/references/ticket-migration.md +64 -0
- package/skills/tickets/SKILL.md +71 -0
- package/templates/hosted-service/package.json +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readBuildFacts, readTerminalGuard, type CurrentBuildFacts } from '../store/reduced-reads'
|
|
1
2
|
/**
|
|
2
3
|
* The three terminal commands — `ab done`, `ab verdict`, `ab escalate`
|
|
3
4
|
* (SPEC §8.4 D5, §8.5 D6, §8.6 D7).
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
import { join } from 'node:path'
|
|
19
20
|
import { z } from 'zod'
|
|
20
21
|
import { loadConfig } from '../config/load'
|
|
21
|
-
import type {
|
|
22
|
+
import type { EventEnvelope } from '../events/catalog'
|
|
22
23
|
import { agentActor, KERNEL } from '../events/envelope'
|
|
23
24
|
import { isRemoteWorkspace } from '../events/workspace-remote'
|
|
24
25
|
import type { IdSource } from '../ids'
|
|
@@ -29,7 +30,6 @@ import {
|
|
|
29
30
|
} from '../kernel/auto-merge'
|
|
30
31
|
import { phaseSpecFor } from '../kernel/phases'
|
|
31
32
|
import { resolvePlanVerifySteps } from '../kernel/plan-verify-selection'
|
|
32
|
-
import { reduceBuild } from '../kernel/reducer'
|
|
33
33
|
import {
|
|
34
34
|
findingDraftSchema,
|
|
35
35
|
isVerifyPhase,
|
|
@@ -75,114 +75,9 @@ export interface TerminalDeps {
|
|
|
75
75
|
* round, so a still-in-flight CLI process from the failed session must not
|
|
76
76
|
* land a terminal the runner will misattribute to the retry).
|
|
77
77
|
*/
|
|
78
|
-
function assertNoPriorTerminal(
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
// across a spec.revised (decideNext ignores pre-restart results and
|
|
82
|
-
// re-runs the pipeline from plan), so a pre-restart verify.completed or
|
|
83
|
-
// finalize.completed must not shadow the rebuilt pipeline's terminal.
|
|
84
|
-
// Loop-phase rounds continue monotonically across restarts (engine
|
|
85
|
-
// LoopIndex.maxRoundEver) and never collide, so they match over the full
|
|
86
|
-
// log; the reconcile epilogue is restart-orthogonal (§15.7).
|
|
87
|
-
let restartSeq = 0
|
|
88
|
-
for (const event of events) {
|
|
89
|
-
if (event.type === 'spec.revised') restartSeq = event.seq
|
|
90
|
-
}
|
|
91
|
-
// reconcile.completed carries no attempt (§15.3); pair it with the latest
|
|
92
|
-
// reconcile.started seen before it.
|
|
93
|
-
let reconcileAttempt = 0
|
|
94
|
-
let sessionStartedSeq: number | undefined
|
|
95
|
-
for (const event of events) {
|
|
96
|
-
if (event.type === 'reconcile.started') reconcileAttempt = event.payload.attempt
|
|
97
|
-
if (event.type === 'session.started' && event.payload.session === env.session) {
|
|
98
|
-
sessionStartedSeq = event.seq
|
|
99
|
-
}
|
|
100
|
-
if (event.type === 'session.ended' && event.payload.session === env.session) {
|
|
101
|
-
throw new Error(
|
|
102
|
-
`terminal rejected (D5): session "${env.session}" already ended ` +
|
|
103
|
-
`(session.ended at seq ${event.seq}) — a session that ended without ` +
|
|
104
|
-
'a terminal was failed and retried; only the live retry may complete ' +
|
|
105
|
-
'this phase round (§8.4).',
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
if (
|
|
109
|
-
sessionStartedSeq !== undefined &&
|
|
110
|
-
event.type === 'phase.failed' &&
|
|
111
|
-
event.payload.phase === env.phase &&
|
|
112
|
-
event.payload.round === env.round &&
|
|
113
|
-
event.seq > sessionStartedSeq
|
|
114
|
-
) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
`terminal rejected (D5): ${env.phase}@${env.round} already failed after ` +
|
|
117
|
-
`this session started (phase.failed at seq ${event.seq}) — the retry ` +
|
|
118
|
-
'session owns this round now (§8.4).',
|
|
119
|
-
)
|
|
120
|
-
}
|
|
121
|
-
if (
|
|
122
|
-
event.type === 'escalation.raised' &&
|
|
123
|
-
event.actor.kind === 'agent' &&
|
|
124
|
-
event.actor.session === env.session
|
|
125
|
-
) {
|
|
126
|
-
throw new Error(
|
|
127
|
-
`second terminal call rejected (D5): this session already escalated — ` +
|
|
128
|
-
`escalation.raised "${event.payload.id}" at seq ${event.seq}. ` +
|
|
129
|
-
'Every phase ends with exactly one terminal command (§8.4).',
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
if (
|
|
133
|
-
event.type === 'publication.requested' &&
|
|
134
|
-
event.actor.kind === 'agent' &&
|
|
135
|
-
event.actor.session === env.session &&
|
|
136
|
-
((env.phase === 'implement' &&
|
|
137
|
-
event.payload.operation === 'implement' &&
|
|
138
|
-
event.payload.round === env.round) ||
|
|
139
|
-
(env.phase === 'reconcile' && event.payload.operation === 'reconcile') ||
|
|
140
|
-
(env.phase === 'finalize' && event.payload.operation === 'finalize'))
|
|
141
|
-
) {
|
|
142
|
-
throw new Error(
|
|
143
|
-
`second terminal call rejected (D5): ${event.type} for ` +
|
|
144
|
-
`${env.phase}@${env.round} already recorded at seq ${event.seq}. ` +
|
|
145
|
-
'Remote publication is pending; do not run ab done again (§8.4).',
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
if (event.type !== spec.terminalEvent) continue
|
|
149
|
-
if (
|
|
150
|
-
(event.type === 'verify.completed' || event.type === 'finalize.completed') &&
|
|
151
|
-
event.seq <= restartSeq
|
|
152
|
-
) {
|
|
153
|
-
continue
|
|
154
|
-
}
|
|
155
|
-
if (!terminalMatchesRound(event, env, reconcileAttempt)) continue
|
|
156
|
-
throw new Error(
|
|
157
|
-
`second terminal call rejected (D5): ${event.type} for ` +
|
|
158
|
-
`${env.phase}@${env.round} already recorded at seq ${event.seq}. ` +
|
|
159
|
-
'Every phase ends with exactly one terminal command (§8.4).',
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function terminalMatchesRound(event: AbEvent, env: CliEnv, reconcileAttempt: number): boolean {
|
|
165
|
-
switch (event.type) {
|
|
166
|
-
case 'plan.completed':
|
|
167
|
-
case 'plan-review.verdict':
|
|
168
|
-
case 'implement.completed':
|
|
169
|
-
case 'code-review.verdict':
|
|
170
|
-
return event.payload.round === env.round
|
|
171
|
-
case 'verify.completed':
|
|
172
|
-
return (
|
|
173
|
-
isVerifyPhase(env.phase) &&
|
|
174
|
-
event.payload.step === verifyStep(env.phase) &&
|
|
175
|
-
event.payload.attempt === env.round
|
|
176
|
-
)
|
|
177
|
-
case 'finalize.completed':
|
|
178
|
-
// finalize runs once per build (§5) — any prior completion counts.
|
|
179
|
-
return true
|
|
180
|
-
case 'reconcile.completed':
|
|
181
|
-
// No started event before it → conservative match (total over any log).
|
|
182
|
-
return reconcileAttempt === env.round || reconcileAttempt === 0
|
|
183
|
-
default:
|
|
184
|
-
return false
|
|
185
|
-
}
|
|
78
|
+
async function assertNoPriorTerminal(store: BuildStore, env: CliEnv): Promise<void> {
|
|
79
|
+
const result = await readTerminalGuard(store, env.build, env.phase, env.round, env.session)
|
|
80
|
+
if (result.diagnostic) throw new Error(result.diagnostic)
|
|
186
81
|
}
|
|
187
82
|
|
|
188
83
|
// ── Small helpers ────────────────────────────────────────────────────────────
|
|
@@ -240,28 +135,21 @@ async function assertCleanWorktree(deps: TerminalDeps): Promise<void> {
|
|
|
240
135
|
}
|
|
241
136
|
}
|
|
242
137
|
|
|
243
|
-
function baseBranchOf(events:
|
|
244
|
-
|
|
245
|
-
if (event.type === 'build.created') return event.payload.baseBranch
|
|
246
|
-
}
|
|
138
|
+
function baseBranchOf(events: CurrentBuildFacts): string {
|
|
139
|
+
if (events.created) return events.created.payload.baseBranch
|
|
247
140
|
throw new Error('this build has no build.created event — cannot resolve its baseBranch (§15.3)')
|
|
248
141
|
}
|
|
249
142
|
|
|
250
|
-
function requireImplementationProvisioning(events:
|
|
251
|
-
if (events.
|
|
143
|
+
function requireImplementationProvisioning(events: CurrentBuildFacts): void {
|
|
144
|
+
if (events.workspaceGenerations > 0) return
|
|
252
145
|
throw new Error(
|
|
253
146
|
"implement's 'ab done' requires workspace.provisioned provenance — " +
|
|
254
147
|
'this build has no workspace.provisioned event (§7.4, §15.3)',
|
|
255
148
|
)
|
|
256
149
|
}
|
|
257
150
|
|
|
258
|
-
function remotePublication(events:
|
|
259
|
-
|
|
260
|
-
for (const event of events) {
|
|
261
|
-
if (event.type === 'workspace.provisioned') remote = isRemoteWorkspace(event.payload)
|
|
262
|
-
else if (event.type === 'workspace.released') remote = false
|
|
263
|
-
}
|
|
264
|
-
return remote
|
|
151
|
+
function remotePublication(events: CurrentBuildFacts): boolean {
|
|
152
|
+
return events.workspace !== null && isRemoteWorkspace(events.workspace)
|
|
265
153
|
}
|
|
266
154
|
|
|
267
155
|
async function buildBranch(deps: TerminalDeps): Promise<string> {
|
|
@@ -281,7 +169,7 @@ async function buildBranch(deps: TerminalDeps): Promise<string> {
|
|
|
281
169
|
*/
|
|
282
170
|
async function implementationReviewBoundary(
|
|
283
171
|
deps: TerminalDeps,
|
|
284
|
-
events:
|
|
172
|
+
events: CurrentBuildFacts,
|
|
285
173
|
): Promise<{ branch: string; base: string; head: string }> {
|
|
286
174
|
const diagnostic = 'cannot establish implementation review boundary'
|
|
287
175
|
try {
|
|
@@ -384,7 +272,7 @@ export interface FinalizePrInput {
|
|
|
384
272
|
* requested head before entering this idempotent PR-side sequence. */
|
|
385
273
|
export async function completeFinalizePr(
|
|
386
274
|
deps: TerminalDeps,
|
|
387
|
-
events:
|
|
275
|
+
events: CurrentBuildFacts,
|
|
388
276
|
input: FinalizePrInput,
|
|
389
277
|
): Promise<EventEnvelope> {
|
|
390
278
|
const { env, store } = deps
|
|
@@ -404,8 +292,8 @@ export async function completeFinalizePr(
|
|
|
404
292
|
|
|
405
293
|
await preparePrAttachments(deps, events, pr.url)
|
|
406
294
|
|
|
407
|
-
const latest = await store
|
|
408
|
-
const autoMerge = pendingAutoMerge(
|
|
295
|
+
const latest = (await readBuildFacts(store, env.build)).value
|
|
296
|
+
const autoMerge = pendingAutoMerge(latest.state)
|
|
409
297
|
const autoMergeResult =
|
|
410
298
|
autoMerge === undefined
|
|
411
299
|
? undefined
|
|
@@ -452,8 +340,8 @@ export async function done(deps: TerminalDeps, opts: DoneOpts = {}): Promise<Eve
|
|
|
452
340
|
`${spec.kind} phase; use 'ab verdict' (§8.2)`,
|
|
453
341
|
)
|
|
454
342
|
}
|
|
455
|
-
const events = await store
|
|
456
|
-
assertNoPriorTerminal(
|
|
343
|
+
const events = (await readBuildFacts(store, env.build)).value
|
|
344
|
+
await assertNoPriorTerminal(store, env)
|
|
457
345
|
const actor = agentActor(env.phase, env.session)
|
|
458
346
|
|
|
459
347
|
switch (spec.name) {
|
|
@@ -466,12 +354,7 @@ export async function done(deps: TerminalDeps, opts: DoneOpts = {}): Promise<Eve
|
|
|
466
354
|
// still the already-reviewed plan, re-entering review as if revised.
|
|
467
355
|
const revs = await store.listArtifacts(env.build, 'plan')
|
|
468
356
|
const latest = revs[revs.length - 1]
|
|
469
|
-
|
|
470
|
-
for (const event of events) {
|
|
471
|
-
if (event.type === 'plan.completed') {
|
|
472
|
-
lastCited = Math.max(lastCited, event.payload.artifact.rev)
|
|
473
|
-
}
|
|
474
|
-
}
|
|
357
|
+
const lastCited = events.maxPlanRevision
|
|
475
358
|
if (latest === undefined || latest.revision <= lastCited) {
|
|
476
359
|
throw new Error(
|
|
477
360
|
`plan@${env.round} 'ab done' requires this round's plan deposit: found ` +
|
|
@@ -704,8 +587,8 @@ export async function verdict(deps: TerminalDeps, opts: VerdictOpts): Promise<Ev
|
|
|
704
587
|
`accept pass|fail|skip. ${env.phase} accepts: ${vocabulary.join('|')}`,
|
|
705
588
|
)
|
|
706
589
|
}
|
|
707
|
-
const events = await store
|
|
708
|
-
assertNoPriorTerminal(
|
|
590
|
+
const events = (await readBuildFacts(store, env.build)).value
|
|
591
|
+
await assertNoPriorTerminal(store, env)
|
|
709
592
|
const actor = agentActor(env.phase, env.session)
|
|
710
593
|
|
|
711
594
|
if (spec.kind === 'agent-verify') {
|
|
@@ -717,7 +600,7 @@ export async function verdict(deps: TerminalDeps, opts: VerdictOpts): Promise<Ev
|
|
|
717
600
|
async function reviewVerdict(
|
|
718
601
|
deps: TerminalDeps,
|
|
719
602
|
opts: VerdictOpts,
|
|
720
|
-
events:
|
|
603
|
+
events: CurrentBuildFacts,
|
|
721
604
|
actor: ReturnType<typeof agentActor>,
|
|
722
605
|
): Promise<EventEnvelope[]> {
|
|
723
606
|
const { env, store } = deps
|
|
@@ -816,7 +699,7 @@ export function renderFindingsIssues(issues: readonly z.core.$ZodIssue[]): strin
|
|
|
816
699
|
async function parseFindings(
|
|
817
700
|
deps: TerminalDeps,
|
|
818
701
|
path: string,
|
|
819
|
-
events:
|
|
702
|
+
events: CurrentBuildFacts,
|
|
820
703
|
): Promise<Finding[]> {
|
|
821
704
|
const raw = await readTextFile(path, '--findings')
|
|
822
705
|
let json: unknown
|
|
@@ -841,7 +724,7 @@ async function parseFindings(
|
|
|
841
724
|
// `persists` may only reference ids from prior rounds of THIS loop (§15.4;
|
|
842
725
|
// ids are deposit-stamped, so a reviewer can only ever see rounds it was
|
|
843
726
|
// shown — §8.3).
|
|
844
|
-
const state =
|
|
727
|
+
const state = events.state
|
|
845
728
|
const loop =
|
|
846
729
|
deps.env.phase === 'plan-review'
|
|
847
730
|
? state.reviewFindings.planReview
|
|
@@ -969,8 +852,8 @@ export async function escalate(
|
|
|
969
852
|
if (opts.question.trim() === '') {
|
|
970
853
|
throw new Error("'ab escalate' requires a question — it is what the human answers (§11)")
|
|
971
854
|
}
|
|
972
|
-
const
|
|
973
|
-
assertNoPriorTerminal(
|
|
855
|
+
const _events = (await readBuildFacts(store, env.build)).value
|
|
856
|
+
await assertNoPriorTerminal(store, env)
|
|
974
857
|
return store.append(env.build, {
|
|
975
858
|
actor: agentActor(env.phase, env.session),
|
|
976
859
|
type: 'escalation.raised',
|
|
@@ -42,6 +42,7 @@ export function ticketAssetNote(
|
|
|
42
42
|
event: TicketAssetNoteEvent,
|
|
43
43
|
meta: TicketAssetMeta,
|
|
44
44
|
cli = 'ab',
|
|
45
|
+
hosted = false,
|
|
45
46
|
): string {
|
|
46
47
|
const fileCount = meta.entries.filter((entry) => entry.type === 'file').length
|
|
47
48
|
const label = `\`${meta.kind}/${meta.name}\``
|
|
@@ -52,7 +53,9 @@ export function ticketAssetNote(
|
|
|
52
53
|
return [
|
|
53
54
|
`Ticket asset attached: ${label} (revision ${meta.revision}, ${contents}).`,
|
|
54
55
|
'',
|
|
55
|
-
|
|
56
|
+
hosted
|
|
57
|
+
? 'The contents live in the ticket service storage.'
|
|
58
|
+
: 'The contents live in the Autobuild store, not in this ticket.',
|
|
56
59
|
`Download the latest revision: \`${get}\``,
|
|
57
60
|
`Download this exact revision: \`${get} --rev ${meta.revision}\``,
|
|
58
61
|
].join('\n')
|
|
@@ -60,7 +63,9 @@ export function ticketAssetNote(
|
|
|
60
63
|
return [
|
|
61
64
|
`Ticket asset replaced: ${label} is now revision ${meta.revision} (${contents}).`,
|
|
62
65
|
'',
|
|
63
|
-
|
|
66
|
+
hosted
|
|
67
|
+
? 'The contents live in the ticket service storage. Earlier revisions stay retrievable.'
|
|
68
|
+
: 'The contents live in the Autobuild store, not in this ticket. Earlier revisions stay retrievable.',
|
|
64
69
|
`Download the latest revision: \`${get}\``,
|
|
65
70
|
`Download this exact revision: \`${get} --rev ${meta.revision}\``,
|
|
66
71
|
].join('\n')
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { mkdtemp, readFile, writeFile, rm } from 'node:fs/promises'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import type { TicketSource, TicketUpdate } from '../ports/types'
|
|
5
|
+
import { parseTicketFile, serializeTicketFile } from '../ports/tickets/file-codec'
|
|
6
|
+
import { TicketRevisionUnsupported } from '../ports/tickets/revision'
|
|
7
|
+
import { requireTicket, updateTicket } from '../ports/tickets/operations'
|
|
8
|
+
|
|
9
|
+
export type TicketEditorRunner = (command: string, path: string) => Promise<number>
|
|
10
|
+
|
|
11
|
+
/** The configured editor is a shell command; only our generated path is quoted. */
|
|
12
|
+
export const runTicketEditor: TicketEditorRunner = async (command, path) => {
|
|
13
|
+
const quoted = `'${path.replaceAll("'", "'\\''")}'`
|
|
14
|
+
return Bun.spawn(['sh', '-c', `${command} ${quoted}`], {
|
|
15
|
+
stdin: 'inherit',
|
|
16
|
+
stdout: 'inherit',
|
|
17
|
+
stderr: 'inherit',
|
|
18
|
+
}).exited
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function editTicket(
|
|
22
|
+
source: TicketSource,
|
|
23
|
+
id: string,
|
|
24
|
+
env: Record<string, string | undefined>,
|
|
25
|
+
stdout: (line: string) => void,
|
|
26
|
+
runner: TicketEditorRunner = runTicketEditor,
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
if (!source.mutateIfRevision) throw new TicketRevisionUnsupported(source.name)
|
|
29
|
+
const command = env.VISUAL?.trim() || env.EDITOR?.trim()
|
|
30
|
+
if (!command) throw new Error('set VISUAL or EDITOR to edit a ticket')
|
|
31
|
+
const ticket = await requireTicket(source, id)
|
|
32
|
+
const front = {
|
|
33
|
+
id,
|
|
34
|
+
title: ticket.title,
|
|
35
|
+
labels: [...ticket.labels],
|
|
36
|
+
blockedBy: [...(ticket.blockedBy ?? [])],
|
|
37
|
+
...(ticket.creationKey !== undefined ? { idempotencyKey: ticket.creationKey } : {}),
|
|
38
|
+
}
|
|
39
|
+
// Reference comments remain part of the frontmatter, outside the editable keys.
|
|
40
|
+
const reference = `# Read-only reference\n# state = ${JSON.stringify(ticket.state ?? null)}\n# revision = ${JSON.stringify(ticket.revision)}\n`
|
|
41
|
+
const serialized = serializeTicketFile(front, ticket.body)
|
|
42
|
+
const original = serialized.replace('+++\n', `+++\n${reference}`)
|
|
43
|
+
const directory = await mkdtemp(join(tmpdir(), 'ab-ticket-edit-'))
|
|
44
|
+
const path = join(directory, 'ticket.md')
|
|
45
|
+
await writeFile(path, original, { mode: 0o600 })
|
|
46
|
+
try {
|
|
47
|
+
const code = await runner(command, path)
|
|
48
|
+
if (code !== 0) throw new Error(`editor exited with status ${code}`)
|
|
49
|
+
const edited = await readFile(path, 'utf8')
|
|
50
|
+
if (edited !== original) {
|
|
51
|
+
const parsed = parseTicketFile(path, edited)
|
|
52
|
+
const close = edited.indexOf('\n+++\n', 4)
|
|
53
|
+
const comments = edited
|
|
54
|
+
.slice(4, close)
|
|
55
|
+
.split('\n')
|
|
56
|
+
.filter((line) => line.trimStart().startsWith('#'))
|
|
57
|
+
.join('\n')
|
|
58
|
+
if (comments !== reference.trimEnd())
|
|
59
|
+
throw new Error('read-only reference comments cannot change')
|
|
60
|
+
if (
|
|
61
|
+
parsed.front.id !== id ||
|
|
62
|
+
parsed.front.idempotencyKey !== front.idempotencyKey ||
|
|
63
|
+
JSON.stringify(parsed.front.blockedBy ?? []) !== JSON.stringify(front.blockedBy)
|
|
64
|
+
)
|
|
65
|
+
throw new Error('id, idempotencyKey and blockedBy are read-only')
|
|
66
|
+
const patch: TicketUpdate = {}
|
|
67
|
+
if (parsed.front.title !== ticket.title) patch.title = parsed.front.title
|
|
68
|
+
if (parsed.body !== ticket.body) patch.body = parsed.body
|
|
69
|
+
if (JSON.stringify(parsed.front.labels) !== JSON.stringify(ticket.labels))
|
|
70
|
+
patch.labels = parsed.front.labels
|
|
71
|
+
if (Object.keys(patch).length > 0)
|
|
72
|
+
await updateTicket(source, id, patch, false, ticket.revision)
|
|
73
|
+
}
|
|
74
|
+
await rm(directory, { recursive: true })
|
|
75
|
+
stdout(`ticket edit complete: ${source.name}:${id}`)
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`${error instanceof Error ? error.message : String(error)}; edited text kept at ${path}`,
|
|
79
|
+
{ cause: error },
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|