@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,30 +1,30 @@
|
|
|
1
|
+
import { readRepositoryFacts, readDispatcherRunFacts } from '../store/reduced-reads'
|
|
1
2
|
import type { RepositoryEvent } from '../events/repository'
|
|
2
3
|
import type { BuildStore, RepositoryRecord } from '../store/types'
|
|
3
4
|
import { formatDispatcherIdentity, projectDispatcherLiveness } from '../kernel/dispatcher-liveness'
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
* an earlier stop. Only this correlation query falls back to full history. */
|
|
6
|
+
/** Correlate only the latest, stopped and lease-holder runs through persisted values. */
|
|
7
7
|
export async function readDispatcherLiveness(
|
|
8
8
|
store: BuildStore,
|
|
9
9
|
repo: string,
|
|
10
10
|
now = new Date(),
|
|
11
|
-
|
|
11
|
+
_subset?: RepositoryEvent[],
|
|
12
12
|
) {
|
|
13
13
|
const record = await store.getRepo(repo)
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
const facts = record === null ? undefined : (await readRepositoryFacts(store, repo)).value
|
|
15
|
+
const runs = new Set(
|
|
16
|
+
[facts?.latestRun, facts?.latestStoppedRun, record?.lease?.holder].filter(
|
|
17
|
+
(run): run is string => run !== undefined,
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
const correlations = await Promise.all(
|
|
21
|
+
[...runs].map(async (run) => (await readDispatcherRunFacts(store, repo, run)).value),
|
|
22
|
+
)
|
|
23
|
+
const events = correlations.flatMap((run) =>
|
|
24
|
+
[run.start, run.latestStart, run.stop, run.tick].filter(
|
|
25
|
+
(event): event is RepositoryEvent => event !== undefined,
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
28
|
return { record, events, dispatcher: projectDispatcherLiveness(record, events, now) }
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -39,6 +39,10 @@ export class StandbyIdentityResolver {
|
|
|
39
39
|
constructor(
|
|
40
40
|
private readonly store: BuildStore,
|
|
41
41
|
private readonly repo: string,
|
|
42
|
+
private readonly history?: {
|
|
43
|
+
allowLookup: () => boolean
|
|
44
|
+
remember: (events: RepositoryEvent[], complete: boolean) => void
|
|
45
|
+
},
|
|
42
46
|
) {}
|
|
43
47
|
|
|
44
48
|
ingest(events: readonly RepositoryEvent[]): void {
|
|
@@ -51,11 +55,14 @@ export class StandbyIdentityResolver {
|
|
|
51
55
|
const pending = this.pending.get(holder)
|
|
52
56
|
if (pending !== undefined) return pending
|
|
53
57
|
if (this.starts.has(holder) || this.attempted.has(holder)) return
|
|
58
|
+
if (this.history && !this.history.allowLookup()) return
|
|
54
59
|
this.attempted.add(holder)
|
|
55
60
|
const lookup = (async () => {
|
|
56
61
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
const facts = (await readDispatcherRunFacts(this.store, this.repo, holder)).value
|
|
63
|
+
this.ingest(facts.start ? [facts.start] : [])
|
|
64
|
+
const complete = false // This lookup correlates one holder, not the complete journal.
|
|
65
|
+
this.history?.remember([...this.starts.values()], complete)
|
|
59
66
|
} catch {
|
|
60
67
|
// Identity is best effort; the sampled holder id remains sufficient.
|
|
61
68
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readRepositoryFacts, type RepositoryReadFacts } from '../store/reduced-reads'
|
|
1
2
|
/** Typed agent/operator surface for repository-scoped harvest workflows. Agent
|
|
2
3
|
* stdout is never interpreted: proposal sets and review verdicts land through
|
|
3
4
|
* this module as validated repository events and versioned artifacts. */
|
|
@@ -67,8 +68,8 @@ async function writeJson(path: string, value: unknown): Promise<void> {
|
|
|
67
68
|
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`)
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
function runForEnv(events:
|
|
71
|
-
const run =
|
|
71
|
+
function runForEnv(events: RepositoryReadFacts, env: HarvestCliEnv): HarvestRunState {
|
|
72
|
+
const run = events.harvest.runs.find((candidate) => candidate.run === env.run)
|
|
72
73
|
if (!run) throw new Error(`unknown harvest run "${env.run}" in repo "${env.repo}"`)
|
|
73
74
|
if (run.status !== 'running') {
|
|
74
75
|
throw new Error(
|
|
@@ -78,34 +79,44 @@ function runForEnv(events: RepositoryEvent[], env: HarvestCliEnv): HarvestRunSta
|
|
|
78
79
|
return run
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
function assertNoSessionTerminal(events:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
82
|
+
function assertNoSessionTerminal(events: RepositoryReadFacts, env: HarvestCliEnv): void {
|
|
83
|
+
const run = events.harvest.runs.find((run) => run.run === env.run)
|
|
84
|
+
const proposal =
|
|
85
|
+
env.phase === 'synthesize'
|
|
86
|
+
? run?.proposals.find((proposal) => proposal.round === env.round)
|
|
87
|
+
: undefined
|
|
88
|
+
const review =
|
|
89
|
+
env.phase === 'review'
|
|
90
|
+
? events.harvestVerdicts[JSON.stringify([env.run, env.round])]?.first
|
|
91
|
+
: undefined
|
|
92
|
+
const ended = events.harvestSessionEndings[JSON.stringify(env.session)]
|
|
93
|
+
const errors = [
|
|
94
|
+
...(proposal
|
|
95
|
+
? [
|
|
96
|
+
{
|
|
97
|
+
seq: proposal.seq,
|
|
98
|
+
message: `second harvest terminal rejected: proposals for ${env.run} synthesize@${env.round} already landed at repo seq ${proposal.seq}`,
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
: []),
|
|
102
|
+
...(review
|
|
103
|
+
? [
|
|
104
|
+
{
|
|
105
|
+
seq: review.seq,
|
|
106
|
+
message: `second harvest terminal rejected: verdict for ${env.run} review@${env.round} already landed at repo seq ${review.seq}`,
|
|
107
|
+
},
|
|
108
|
+
]
|
|
109
|
+
: []),
|
|
110
|
+
...(ended
|
|
111
|
+
? [
|
|
112
|
+
{
|
|
113
|
+
seq: ended,
|
|
114
|
+
message: `harvest terminal rejected: session "${env.session}" already ended at repo seq ${ended}`,
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
: []),
|
|
118
|
+
].sort((a, b) => a.seq - b.seq)
|
|
119
|
+
if (errors[0]) throw new Error(errors[0].message)
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
async function readArtifactJson(
|
|
@@ -122,7 +133,7 @@ async function readArtifactJson(
|
|
|
122
133
|
export async function buildHarvestContext(deps: HarvestCliDeps): Promise<HarvestContextManifest> {
|
|
123
134
|
const { store, env, workspacePath } = deps
|
|
124
135
|
// Bounded read (AUT-489): runForEnv reduces harvest facts only.
|
|
125
|
-
const events = await store
|
|
136
|
+
const events = (await readRepositoryFacts(store, env.repo)).value
|
|
126
137
|
const run = runForEnv(events, env)
|
|
127
138
|
const packet = await loadScanPacket(store, env.repo, run.scan)
|
|
128
139
|
const abDir = join(workspacePath, '.ab')
|
|
@@ -184,10 +195,20 @@ export async function buildHarvestContext(deps: HarvestCliDeps): Promise<Harvest
|
|
|
184
195
|
}
|
|
185
196
|
|
|
186
197
|
async function readJsonFile(path: string): Promise<unknown> {
|
|
187
|
-
|
|
188
|
-
if (!(await file.exists())) throw new Error(`file not found: ${path}`)
|
|
198
|
+
let text: string
|
|
189
199
|
try {
|
|
190
|
-
|
|
200
|
+
text = await Bun.file(path).text()
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
203
|
+
throw new Error(`file not found: ${path}`, { cause: error })
|
|
204
|
+
}
|
|
205
|
+
throw new Error(
|
|
206
|
+
`cannot read ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
207
|
+
{ cause: error },
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
return JSON.parse(text)
|
|
191
212
|
} catch (error) {
|
|
192
213
|
throw new Error(
|
|
193
214
|
`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -220,7 +241,7 @@ export async function submitHarvestProposals(
|
|
|
220
241
|
}
|
|
221
242
|
// Bounded read (AUT-489): runForEnv and assertNoSessionTerminal consume
|
|
222
243
|
// harvest facts only.
|
|
223
|
-
const events = await store
|
|
244
|
+
const events = (await readRepositoryFacts(store, env.repo)).value
|
|
224
245
|
const run = runForEnv(events, env)
|
|
225
246
|
assertNoSessionTerminal(events, env)
|
|
226
247
|
|
|
@@ -312,7 +333,7 @@ export async function submitHarvestVerdict(
|
|
|
312
333
|
}
|
|
313
334
|
// Bounded read (AUT-489): runForEnv and assertNoSessionTerminal consume
|
|
314
335
|
// harvest facts only.
|
|
315
|
-
const events = await store
|
|
336
|
+
const events = (await readRepositoryFacts(store, env.repo)).value
|
|
316
337
|
const run = runForEnv(events, env)
|
|
317
338
|
assertNoSessionTerminal(events, env)
|
|
318
339
|
if (proposalArtifactForRound(run, env.round) === undefined) {
|
|
@@ -592,19 +613,35 @@ function projectHarvestRunStatus(run: HarvestRunState): Omit<HarvestRunStatusVie
|
|
|
592
613
|
|
|
593
614
|
export function projectHarvestStatus(
|
|
594
615
|
repo: string,
|
|
595
|
-
events: RepositoryEvent[],
|
|
616
|
+
events: RepositoryEvent[] | RepositoryReadFacts,
|
|
596
617
|
newestEvents?: number,
|
|
597
618
|
streams?: readonly StreamRecord[],
|
|
598
619
|
): HarvestStatusView {
|
|
599
|
-
const state = reduceHarvest(events)
|
|
620
|
+
const state = Array.isArray(events) ? reduceHarvest(events) : events.harvest
|
|
600
621
|
const history =
|
|
601
|
-
newestEvents === undefined
|
|
622
|
+
newestEvents === undefined || !Array.isArray(events)
|
|
623
|
+
? undefined
|
|
624
|
+
: events.filter(isHarvestEvent).slice(-newestEvents)
|
|
602
625
|
const unresolved = [...parkedHarvestRuns(state), ...unresolvedHarvestAttentionRuns(state)]
|
|
603
626
|
const open = openHarvestRuns(state)
|
|
604
627
|
const included = new Set(
|
|
605
628
|
[...unresolved, ...open, ...(state.latest ? [state.latest] : [])].map((run) => run.run),
|
|
606
629
|
)
|
|
607
|
-
const sessionsByRun =
|
|
630
|
+
const sessionsByRun = Array.isArray(events)
|
|
631
|
+
? projectHarvestSessions(events, streams)
|
|
632
|
+
: new Map<string, HarvestSessionStatusView[]>()
|
|
633
|
+
if (!Array.isArray(events))
|
|
634
|
+
for (const { run, ...session } of events.harvestSessions) {
|
|
635
|
+
const record = streams?.find(
|
|
636
|
+
(record) => record.scope.kind === 'repo' && record.label === `session:${session.session}`,
|
|
637
|
+
)
|
|
638
|
+
const list = sessionsByRun.get(run) ?? []
|
|
639
|
+
list.push({
|
|
640
|
+
...session,
|
|
641
|
+
...(record ? { stream: record.id, streamStatus: record.status } : {}),
|
|
642
|
+
})
|
|
643
|
+
sessionsByRun.set(run, list)
|
|
644
|
+
}
|
|
608
645
|
const runs = state.runs
|
|
609
646
|
.filter((run) => included.has(run.run))
|
|
610
647
|
.map((run) => ({
|
|
@@ -792,7 +829,7 @@ export async function abHarvestStatus(opts: HarvestStatusOpts): Promise<void> {
|
|
|
792
829
|
async ({ store, repo }) => {
|
|
793
830
|
const record = await store.getRepo(repo)
|
|
794
831
|
// Bounded read (AUT-489): projectHarvestStatus reduces harvest facts only.
|
|
795
|
-
const events =
|
|
832
|
+
const events = (await readRepositoryFacts(store, repo)).value
|
|
796
833
|
// Best-effort stream enrichment for the session rows, mirroring the
|
|
797
834
|
// degrade-gracefully pattern of 'ab build status': a store that cannot
|
|
798
835
|
// list streams keeps the payload-derived projection instead of failing.
|
|
@@ -804,7 +841,12 @@ export async function abHarvestStatus(opts: HarvestStatusOpts): Promise<void> {
|
|
|
804
841
|
streams = undefined
|
|
805
842
|
}
|
|
806
843
|
}
|
|
807
|
-
const view = projectHarvestStatus(repo, events,
|
|
844
|
+
const view = projectHarvestStatus(repo, events, undefined, streams)
|
|
845
|
+
if (opts.events !== undefined)
|
|
846
|
+
view.events =
|
|
847
|
+
record === null
|
|
848
|
+
? []
|
|
849
|
+
: (await store.getRepoEvents(repo)).filter(isHarvestEvent).slice(-opts.events)
|
|
808
850
|
if (opts.json === true) opts.stdout(JSON.stringify(view, null, 2))
|
|
809
851
|
else for (const line of renderHarvestStatus(view)) opts.stdout(line)
|
|
810
852
|
},
|
|
@@ -74,14 +74,14 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
|
|
|
74
74
|
audience: 'human',
|
|
75
75
|
summary: 'Connect this project to a hosted service with browser approval.',
|
|
76
76
|
detail:
|
|
77
|
-
'Usage: ab connect [url]\nWith a URL, approve the printed URL and code in your browser. Stores the credential in user configuration and adds [store] url to autobuild.toml. Without a URL, reports
|
|
77
|
+
'Usage: ab connect [--tickets] [url]\nWith a URL, approve the printed URL and code in your browser. Stores the credential in user configuration and adds [store] url to autobuild.toml. Use --tickets to save [tickets] url and keep builds local. Without a URL, reports every service connection and its uses. Runs outside sessions.',
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
name: 'disconnect',
|
|
81
81
|
audience: 'human',
|
|
82
82
|
summary: 'Revoke and remove this project’s saved credential.',
|
|
83
83
|
detail:
|
|
84
|
-
'Usage: ab disconnect\nRevokes the saved credential remotely before removing it locally. Keeps project registration, data and [store] url. Runs outside sessions.',
|
|
84
|
+
'Usage: ab disconnect [--tickets | --builds]\nRevokes the saved credential remotely before removing it locally. Keeps project registration, data and [store] url. Runs outside sessions.',
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
name: 'upgrade',
|
|
@@ -173,20 +173,26 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
|
|
|
173
173
|
detail: page([
|
|
174
174
|
'Usage:',
|
|
175
175
|
' ab ticket create <title> --body <file> [--state <state>] [--labels a,b] [--blocked-by id,id] [--json]',
|
|
176
|
-
' ab ticket
|
|
177
|
-
' ab ticket
|
|
178
|
-
' ab ticket
|
|
176
|
+
' ab ticket edit <id>',
|
|
177
|
+
' ab ticket export <dir> [--store <ref>]',
|
|
178
|
+
' ab ticket import <dir> [--store <ref>]',
|
|
179
|
+
' ab ticket update <id> [--title <title>] [--body <file>] [--labels a,b] [--expected-revision <opaque>] [--json]',
|
|
180
|
+
' ab ticket block <id> <blocker-id[,blocker-id...]> [--expected-revision <opaque>] [--json]',
|
|
181
|
+
' ab ticket unblock <id> <blocker-id[,blocker-id...]> [--expected-revision <opaque>] [--json]',
|
|
179
182
|
' ab ticket list [--state <state>] [--labels a,b] [--json]',
|
|
180
183
|
' ab ticket show <id> [--store <ref>] [--json]',
|
|
181
|
-
' ab ticket move <id> <state> [--json]',
|
|
184
|
+
' ab ticket move <id> <state> [--expected-revision <opaque>] [--json]',
|
|
182
185
|
' ab ticket states [--json]',
|
|
183
186
|
' ab ticket state create <name> [--about <text>] [--json]',
|
|
184
187
|
' ab ticket attach <id> <kind> <path> [--name <name>] [--store <ref>] [--json]',
|
|
185
188
|
' ab ticket asset get <id> <kind> <name> <dest> [--rev <n>] [--store <ref>] [--json]',
|
|
186
189
|
' ab ticket asset rm <id> <kind> <name> [--store <ref>] [--json]',
|
|
187
190
|
'',
|
|
188
|
-
'Operate on the configured [tickets] source (§8.8);
|
|
189
|
-
'
|
|
191
|
+
'Operate on the configured [tickets] source (§8.8); commands run outside sessions.',
|
|
192
|
+
'Every JSON ticket read includes an opaque revision. update, move, block and',
|
|
193
|
+
'unblock accept --expected-revision to refuse stale writes. Linear refuses checks.',
|
|
194
|
+
'edit opens VISUAL or EDITOR, checks the opening revision, and preserves failed buffers.',
|
|
195
|
+
'create files a ticket and may establish initial blockers.',
|
|
190
196
|
'--state overrides [tickets].createState (or the provider default) for that',
|
|
191
197
|
'create only; it is passed through unchanged and validated by the source.',
|
|
192
198
|
'update partially replaces editable fields: omitted fields survive, --labels',
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BuildStore } from '../store/types'
|
|
2
|
+
import { buildStreamSeedReducer, type BuildStreamSeed } from '../kernel/read-facts'
|
|
3
|
+
|
|
4
|
+
/** Only explicit old --since cursors need historical reconstruction. Ordinary
|
|
5
|
+
* registration and the first event batch must use the persisted seed. */
|
|
6
|
+
export async function historicalBuildState(
|
|
7
|
+
store: BuildStore,
|
|
8
|
+
slug: string,
|
|
9
|
+
requested: number,
|
|
10
|
+
current: { cursor: number; value: BuildStreamSeed },
|
|
11
|
+
) {
|
|
12
|
+
if (requested >= current.cursor) return current.value
|
|
13
|
+
const events = await store.getEvents(slug)
|
|
14
|
+
return buildStreamSeedReducer.reduce(events.filter((event) => event.seq <= requested))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
import { repositoryAttentionReducer } from '../kernel/repository-attention'
|
|
18
|
+
export async function historicalRepositoryAttention(
|
|
19
|
+
store: BuildStore,
|
|
20
|
+
repo: string,
|
|
21
|
+
requested: number,
|
|
22
|
+
current: { cursor: number; value: ReturnType<typeof repositoryAttentionReducer.finish> },
|
|
23
|
+
) {
|
|
24
|
+
if (requested >= current.cursor) return current.value
|
|
25
|
+
const events = await store.getRepoEvents(repo)
|
|
26
|
+
return repositoryAttentionReducer.reduce(events.filter((event) => event.seq <= requested))
|
|
27
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stat, readFile, realpath } from 'node:fs/promises'
|
|
2
|
-
import { join, resolve, sep } from 'node:path'
|
|
2
|
+
import { dirname, join, resolve, sep } from 'node:path'
|
|
3
3
|
import semver from 'semver'
|
|
4
4
|
import { PLUGIN_API_VERSION } from '../plugins/manifest'
|
|
5
5
|
import { defaultDistRoot } from './init'
|
|
@@ -155,6 +155,57 @@ function ownerRootForPackage(identity: DistributionIdentity): string | undefined
|
|
|
155
155
|
return owner === '' ? sep : owner
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/** Store-internal fallback links cannot establish project ownership. */
|
|
159
|
+
function inBunStore(path: string): boolean {
|
|
160
|
+
return path
|
|
161
|
+
.split(sep)
|
|
162
|
+
.some((part, index, parts) => part === '.bun' && parts[index - 1] === 'node_modules')
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function ancestors(path: string): string[] {
|
|
166
|
+
const result: string[] = []
|
|
167
|
+
for (let current = resolve(path); ; current = dirname(current)) {
|
|
168
|
+
result.push(current)
|
|
169
|
+
if (dirname(current) === current) return result
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function discoverOwner(
|
|
174
|
+
identity: DistributionIdentity,
|
|
175
|
+
cwd: string,
|
|
176
|
+
): Promise<string | undefined> {
|
|
177
|
+
const direct = ownerRootForPackage(identity)
|
|
178
|
+
if (direct !== undefined && !inBunStore(direct)) return direct
|
|
179
|
+
// Reject invalid package names before constructing candidate public paths.
|
|
180
|
+
if (identity.packageName.split('/').some((part) => part === '' || part === '.' || part === '..'))
|
|
181
|
+
return undefined
|
|
182
|
+
const candidates = new Set([...ancestors(identity.root), ...ancestors(cwd)])
|
|
183
|
+
for (const candidate of candidates) {
|
|
184
|
+
if (inBunStore(candidate)) continue
|
|
185
|
+
if (!(await samePath(join(candidate, 'node_modules', identity.packageName), identity.root)))
|
|
186
|
+
continue
|
|
187
|
+
const manifestPath = join(candidate, 'package.json')
|
|
188
|
+
if (!(await exists(manifestPath))) continue
|
|
189
|
+
const manifest = await parseJson(manifestPath)
|
|
190
|
+
if (declaredSection(manifest, identity.packageName) !== undefined) return candidate
|
|
191
|
+
}
|
|
192
|
+
return undefined
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function readMetadata(path: string): Promise<string> {
|
|
196
|
+
try {
|
|
197
|
+
return await readFile(path, 'utf8')
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
200
|
+
throw new Error(`${path} does not exist`, { cause: error })
|
|
201
|
+
}
|
|
202
|
+
throw new Error(
|
|
203
|
+
`cannot read ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
204
|
+
{ cause: error },
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
158
209
|
function githubDependency(value: unknown): { owner: string; repository: string } | undefined {
|
|
159
210
|
if (typeof value !== 'string') return undefined
|
|
160
211
|
const match = /^github:([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(?:#([^\s]+))?$/.exec(value)
|
|
@@ -172,8 +223,9 @@ function registryDependency(value: unknown, installedVersion: string): boolean {
|
|
|
172
223
|
}
|
|
173
224
|
|
|
174
225
|
async function parseJson(path: string): Promise<Record<string, unknown>> {
|
|
226
|
+
const text = await readMetadata(path)
|
|
175
227
|
try {
|
|
176
|
-
return object(JSON.parse(
|
|
228
|
+
return object(JSON.parse(text)) ?? {}
|
|
177
229
|
} catch (error) {
|
|
178
230
|
throw new Error(
|
|
179
231
|
`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -182,8 +234,9 @@ async function parseJson(path: string): Promise<Record<string, unknown>> {
|
|
|
182
234
|
}
|
|
183
235
|
|
|
184
236
|
async function parseJsonc(path: string): Promise<Record<string, unknown>> {
|
|
237
|
+
const text = await readMetadata(path)
|
|
185
238
|
try {
|
|
186
|
-
const value = Bun.JSONC.parse(
|
|
239
|
+
const value = Bun.JSONC.parse(text)
|
|
187
240
|
return object(value) ?? {}
|
|
188
241
|
} catch (error) {
|
|
189
242
|
throw new Error(
|
|
@@ -241,6 +294,7 @@ async function samePath(left: string, right: string): Promise<boolean> {
|
|
|
241
294
|
export async function inspectInstallation(options: {
|
|
242
295
|
distRoot?: string
|
|
243
296
|
globalBin: string
|
|
297
|
+
cwd?: string
|
|
244
298
|
}): Promise<InstallationInspection> {
|
|
245
299
|
const identity = await readDistributionIdentity(options.distRoot)
|
|
246
300
|
if (identity.sourceCheckout) {
|
|
@@ -251,18 +305,18 @@ export async function inspectInstallation(options: {
|
|
|
251
305
|
}
|
|
252
306
|
}
|
|
253
307
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
308
|
+
try {
|
|
309
|
+
const ownerRoot = await discoverOwner(identity, options.cwd ?? process.cwd())
|
|
310
|
+
if (ownerRoot === undefined) {
|
|
311
|
+
return {
|
|
312
|
+
kind: 'unknown',
|
|
313
|
+
identity,
|
|
314
|
+
reason: 'the distribution is not a direct Bun node_modules package',
|
|
315
|
+
}
|
|
260
316
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const ownerLock = join(ownerRoot, 'bun.lock')
|
|
317
|
+
const ownerManifest = join(ownerRoot, 'package.json')
|
|
318
|
+
const ownerLock = join(ownerRoot, 'bun.lock')
|
|
264
319
|
|
|
265
|
-
try {
|
|
266
320
|
const manifest = await parseJson(ownerManifest)
|
|
267
321
|
const lock = await parseJsonc(ownerLock)
|
|
268
322
|
const declared = declaredSection(manifest, identity.packageName)
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readBuildFacts } from '../store/reduced-reads'
|
|
2
|
+
import { preparePinnedAssets } from '../store/ticket-assets'
|
|
3
|
+
import type { PinnedAsset } from '../events/payloads'
|
|
1
4
|
import { abConnect, abDisconnect } from './connect'
|
|
2
5
|
/**
|
|
3
6
|
* `ab` — the only channel between agents and the store, and the enforcement
|
|
@@ -19,7 +22,6 @@ import { textContent } from '../store/types'
|
|
|
19
22
|
import type { Forge } from '../ports/types'
|
|
20
23
|
import { spawnExec, type Exec } from '../ports/workspace/git-worktree'
|
|
21
24
|
import type { IdSource } from '../ids'
|
|
22
|
-
import { reduceBuild } from '../kernel/reducer'
|
|
23
25
|
import { artifactDownload, artifactDownloadStream, artifactGet, artifactPut } from './artifact'
|
|
24
26
|
import { multiFlag, parseArgs, stringFlag, type ParsedArgs } from './args'
|
|
25
27
|
import { abBuildControl, type BuildControlAction, type BuildControlResult } from './build-control'
|
|
@@ -308,6 +310,7 @@ async function runBuildControl(
|
|
|
308
310
|
action: BuildControlAction,
|
|
309
311
|
storeRef?: string,
|
|
310
312
|
readTicketBody?: (ref: TicketRef) => Promise<string>,
|
|
313
|
+
prepareTicketAssets?: (ref: TicketRef, store: BuildStore, repo: string) => Promise<PinnedAsset[]>,
|
|
311
314
|
): Promise<void> {
|
|
312
315
|
if (deps.exec === undefined) {
|
|
313
316
|
throw new Error(
|
|
@@ -322,6 +325,7 @@ async function runBuildControl(
|
|
|
322
325
|
action,
|
|
323
326
|
...(storeRef !== undefined ? { storeRef } : {}),
|
|
324
327
|
...(readTicketBody !== undefined ? { readTicketBody } : {}),
|
|
328
|
+
...(prepareTicketAssets ? { prepareTicketAssets } : {}),
|
|
325
329
|
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
326
330
|
})
|
|
327
331
|
deps.stdout(buildControlConfirmation(result))
|
|
@@ -451,9 +455,11 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
451
455
|
case 'connect':
|
|
452
456
|
case 'disconnect': {
|
|
453
457
|
const usage = command === 'connect' ? 'usage: ab connect [url]' : 'usage: ab disconnect'
|
|
454
|
-
const parsed = parseArgs(rest, {}, usage)
|
|
458
|
+
const parsed = parseArgs(rest, { tickets: 'boolean', builds: 'boolean' }, usage)
|
|
455
459
|
if (parsed.positionals.length > (command === 'connect' ? 1 : 0)) throw new Error(usage)
|
|
456
460
|
const options = {
|
|
461
|
+
tickets: parsed.flags.get('tickets') === true,
|
|
462
|
+
builds: parsed.flags.get('builds') === true,
|
|
457
463
|
cwd: deps.workspacePath,
|
|
458
464
|
env: deps.processEnv ?? process.env,
|
|
459
465
|
exec: deps.exec ?? spawnExec,
|
|
@@ -1127,6 +1133,7 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1127
1133
|
: parsed.flags.has('revise-spec-from-ticket')
|
|
1128
1134
|
? ({ kind: 'revise-spec', body: { kind: 'ticket' } } as const)
|
|
1129
1135
|
: undefined
|
|
1136
|
+
let revisionTicketSource: import('../ports/types').TicketSource | undefined
|
|
1130
1137
|
const readTicketBody =
|
|
1131
1138
|
resolve?.kind === 'revise-spec' && resolve.body.kind === 'ticket'
|
|
1132
1139
|
? async (ref: TicketRef): Promise<string> => {
|
|
@@ -1140,6 +1147,7 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1140
1147
|
},
|
|
1141
1148
|
'ab answer --revise-spec-from-ticket',
|
|
1142
1149
|
)
|
|
1150
|
+
revisionTicketSource = source
|
|
1143
1151
|
if (source.name !== ref.source) {
|
|
1144
1152
|
throw new Error(
|
|
1145
1153
|
`build ticket source is "${ref.source}", but this repository is configured for "${source.name}"; refusing to read a same-id ticket from a different source`,
|
|
@@ -1151,7 +1159,10 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1151
1159
|
`ticket "${ref.id}" was not found in the configured ${source.name} source`,
|
|
1152
1160
|
)
|
|
1153
1161
|
}
|
|
1154
|
-
if (
|
|
1162
|
+
if (
|
|
1163
|
+
ticket.ref.source !== ref.source ||
|
|
1164
|
+
(source.name !== 'hosted' && ticket.ref.id !== ref.id)
|
|
1165
|
+
) {
|
|
1155
1166
|
throw new Error(
|
|
1156
1167
|
`ticket adapter returned ${ticket.ref.source}:${ticket.ref.id}, expected ${ref.source}:${ref.id}`,
|
|
1157
1168
|
)
|
|
@@ -1170,6 +1181,13 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1170
1181
|
},
|
|
1171
1182
|
storeRef,
|
|
1172
1183
|
readTicketBody,
|
|
1184
|
+
readTicketBody
|
|
1185
|
+
? async (ref, store, repo) => {
|
|
1186
|
+
if (!revisionTicketSource)
|
|
1187
|
+
throw new Error('ticket body must be read before preparing assets')
|
|
1188
|
+
return preparePinnedAssets(revisionTicketSource, store, repo, ref.id)
|
|
1189
|
+
}
|
|
1190
|
+
: undefined,
|
|
1173
1191
|
)
|
|
1174
1192
|
return 0
|
|
1175
1193
|
}
|
|
@@ -1354,8 +1372,8 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1354
1372
|
// external side effect is best-effort after the atomic designation.
|
|
1355
1373
|
if (attach) {
|
|
1356
1374
|
try {
|
|
1357
|
-
const events = await session.store
|
|
1358
|
-
const pr =
|
|
1375
|
+
const events = (await readBuildFacts(session.store, session.env.build)).value
|
|
1376
|
+
const pr = events.state.pr
|
|
1359
1377
|
if (pr !== undefined) {
|
|
1360
1378
|
try {
|
|
1361
1379
|
await preparePrAttachments(session, events, pr.url)
|
|
@@ -120,7 +120,8 @@ export async function buildMcpTicketBackend(
|
|
|
120
120
|
}
|
|
121
121
|
return source
|
|
122
122
|
},
|
|
123
|
-
statesFor: async () =>
|
|
123
|
+
statesFor: async (_context, source) =>
|
|
124
|
+
source.listStates ? (await source.listStates()).map((s) => s.name) : states,
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CurrentBuildFacts } from '../store/reduced-reads'
|
|
2
|
+
import type { EventEnvelope } from '../events/catalog'
|
|
2
3
|
import { KERNEL } from '../events/envelope'
|
|
3
4
|
import type { IdSource } from '../ids'
|
|
4
|
-
import {
|
|
5
|
-
currentPrAttachments,
|
|
6
|
-
frozenPrImageHost,
|
|
7
|
-
hostedPrAttachments,
|
|
8
|
-
} from '../kernel/pr-attachments'
|
|
5
|
+
import type { currentPrAttachments } from '../kernel/pr-attachments'
|
|
9
6
|
import { hostedPrAttachmentAssetSchema, type HostedPrAttachmentAsset } from '../ontology'
|
|
10
7
|
import type { Forge } from '../ports/types'
|
|
11
8
|
import type { BuildStore } from '../store/types'
|
|
@@ -25,7 +22,7 @@ function errorMessage(error: unknown): string {
|
|
|
25
22
|
|
|
26
23
|
async function recordHostingFailure(
|
|
27
24
|
deps: PrAttachmentDeps,
|
|
28
|
-
events:
|
|
25
|
+
events: CurrentBuildFacts,
|
|
29
26
|
designation: ReturnType<typeof currentPrAttachments>[number],
|
|
30
27
|
error: unknown,
|
|
31
28
|
extraRefs: string[] = [],
|
|
@@ -44,7 +41,7 @@ async function recordHostingFailure(
|
|
|
44
41
|
refs: [`${artifact.kind}@${artifact.rev}`, ...extraRefs],
|
|
45
42
|
},
|
|
46
43
|
})
|
|
47
|
-
events.push(observed)
|
|
44
|
+
events.observations.push(observed.payload)
|
|
48
45
|
} catch {
|
|
49
46
|
// Hosting is a best-effort review affordance. A secondary failure to
|
|
50
47
|
// record its follow-up must not hide the complete text projection or turn
|
|
@@ -61,14 +58,14 @@ async function recordHostingFailure(
|
|
|
61
58
|
*/
|
|
62
59
|
export async function preparePrAttachments(
|
|
63
60
|
deps: PrAttachmentDeps,
|
|
64
|
-
events:
|
|
61
|
+
events: CurrentBuildFacts,
|
|
65
62
|
prUrl: string,
|
|
66
63
|
): Promise<void> {
|
|
67
|
-
const target =
|
|
64
|
+
const target = events.imageHost
|
|
68
65
|
if (target === undefined) return
|
|
69
66
|
|
|
70
|
-
const designations =
|
|
71
|
-
const hosted =
|
|
67
|
+
const designations = events.designations
|
|
68
|
+
const hosted = new Map(events.hostedImages)
|
|
72
69
|
for (const designation of designations) {
|
|
73
70
|
if (!designation.payload.mediaType.startsWith('image/')) continue
|
|
74
71
|
if (hosted.has(designation.seq)) continue
|
|
@@ -132,7 +129,7 @@ export async function preparePrAttachments(
|
|
|
132
129
|
)
|
|
133
130
|
throw error
|
|
134
131
|
}
|
|
135
|
-
events.push(event)
|
|
132
|
+
events.hostedImages.push([designation.seq, event])
|
|
136
133
|
hosted.set(designation.seq, event)
|
|
137
134
|
}
|
|
138
135
|
}
|