@defrex/autobuild 0.11.0 → 0.13.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 +3 -1
- package/SPEC.md +335 -31
- package/docs/architecture.md +125 -7
- package/docs/configuration.md +142 -21
- package/docs/hosted-dispatcher.md +17 -2
- package/docs/hosted-service.md +240 -0
- package/docs/mcp.md +8 -2
- package/docs/operator-api.md +97 -7
- package/docs/remote-store-protocol.md +199 -144
- package/docs/remote-ticket-protocol.md +249 -19
- package/docs/setup.md +199 -16
- package/docs/ticket-dependencies.md +1 -1
- package/docs/ticket-migration.md +64 -0
- package/package.json +7 -6
- package/packages/core/src/cli/attention.ts +209 -0
- package/packages/core/src/cli/build-control.ts +12 -1
- package/packages/core/src/cli/configured-store.ts +17 -0
- package/packages/core/src/cli/connect.ts +364 -0
- package/packages/core/src/cli/context.ts +7 -1
- package/packages/core/src/cli/dashboard/model.ts +4 -5
- package/packages/core/src/cli/dashboard/poll.ts +1 -1
- package/packages/core/src/cli/dispatch.ts +336 -41
- package/packages/core/src/cli/harvest.ts +13 -3
- package/packages/core/src/cli/help.ts +51 -16
- package/packages/core/src/cli/init-validation.ts +27 -2
- package/packages/core/src/cli/installation.ts +67 -13
- package/packages/core/src/cli/main.ts +60 -16
- package/packages/core/src/cli/mcp.ts +9 -2
- package/packages/core/src/cli/pr-summary-publication.ts +220 -0
- package/packages/core/src/cli/pr-summary.ts +2 -0
- package/packages/core/src/cli/project-credentials.ts +132 -0
- package/packages/core/src/cli/repo-state.ts +15 -2
- package/packages/core/src/cli/self-update.ts +15 -1
- package/packages/core/src/cli/store-opening.ts +22 -14
- package/packages/core/src/cli/terminals.ts +0 -10
- 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 +172 -35
- package/packages/core/src/cli/upgrade-compatibility.ts +80 -0
- package/packages/core/src/cli/wait.ts +84 -17
- package/packages/core/src/cli/watch.ts +197 -60
- package/packages/core/src/config/live.ts +3 -0
- package/packages/core/src/config/schema.ts +90 -11
- package/packages/core/src/connection.ts +86 -0
- package/packages/core/src/events/catalog.ts +11 -2
- package/packages/core/src/events/payloads.ts +50 -0
- package/packages/core/src/events/repository.ts +13 -0
- package/packages/core/src/integration/harness.ts +37 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +145 -83
- package/packages/core/src/kernel/engine.ts +24 -0
- package/packages/core/src/kernel/generators/build-log.ts +11 -1
- package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
- package/packages/core/src/kernel/log-index.ts +17 -0
- package/packages/core/src/kernel/pr-summary.ts +89 -0
- package/packages/core/src/kernel/reducer-registry.ts +10 -0
- package/packages/core/src/kernel/reducer.ts +8 -0
- package/packages/core/src/ontology.ts +2 -0
- package/packages/core/src/operator/annotations.ts +12 -0
- package/packages/core/src/operator/index.ts +12 -0
- package/packages/core/src/operator/protocol.ts +19 -2
- package/packages/core/src/operator/query.ts +38 -16
- package/packages/core/src/operator/registry.ts +61 -8
- package/packages/core/src/operator/ticket-source-backend.ts +17 -20
- package/packages/core/src/operator/tickets.ts +136 -28
- package/packages/core/src/plugin-sdk/index.ts +7 -0
- package/packages/core/src/plugins/manifest.ts +5 -3
- package/packages/core/src/plugins/runtimes.ts +7 -1
- package/packages/core/src/ports/forge/fake.ts +13 -1
- package/packages/core/src/ports/forge/github-transport.ts +36 -1
- package/packages/core/src/ports/forge/github.ts +29 -5
- package/packages/core/src/ports/forge/local-git.ts +12 -4
- package/packages/core/src/ports/forge/summary-marker.ts +8 -0
- package/packages/core/src/ports/runner/session-env.ts +3 -0
- package/packages/core/src/ports/tickets/contract.ts +66 -1
- package/packages/core/src/ports/tickets/create.ts +54 -10
- 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 -3
- 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 +96 -0
- package/packages/core/src/ports/tickets/remote.ts +61 -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 +6 -1
- package/packages/core/src/ports/workspace/create.ts +15 -2
- package/packages/core/src/ports/workspace/distribution-archive.ts +3 -5
- package/packages/core/src/ports/workspace/harvest-execution.ts +3 -1
- package/packages/core/src/ports/workspace/local-build-execution.ts +8 -1
- package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +51 -19
- package/packages/core/src/processes/build-execution-state.ts +1 -1
- package/packages/core/src/processes/build-runner.ts +87 -26
- package/packages/core/src/processes/dispatch-failure-budget.ts +83 -0
- package/packages/core/src/processes/dispatcher.ts +168 -41
- package/packages/core/src/processes/execution-no-progress.ts +176 -0
- package/packages/core/src/processes/harvest-child.ts +31 -4
- package/packages/core/src/processes/harvest-execution-state.ts +19 -2
- package/packages/core/src/processes/harvest-runner.ts +166 -104
- package/packages/core/src/processes/harvest.ts +34 -14
- package/packages/core/src/processes/phase-failure-budget.ts +2 -0
- package/packages/core/src/processes/publication-loss.ts +18 -1
- package/packages/core/src/processes/repo-view.ts +35 -0
- package/packages/core/src/store/contract.ts +32 -1
- package/packages/core/src/store/local/store.ts +1 -1
- package/packages/core/src/store/remote/client.ts +1 -1
- package/packages/core/src/store/remote/compatibility.ts +90 -0
- package/packages/core/src/store/remote/credentials.ts +28 -0
- package/packages/core/src/store/remote/index.ts +14 -1
- package/packages/core/src/store/remote/protocol.ts +25 -0
- package/packages/core/src/store/remote/token.ts +74 -13
- 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 +1 -1
- package/packages/core/src/testing/index.ts +7 -0
- package/skills/guide/SKILL.md +232 -34
- package/skills/guide/references/operator-api.md +142 -13
- package/skills/guide/references/plugin-authoring.md +10 -2
- package/skills/guide/references/remote-store-protocol.md +199 -144
- package/skills/guide/references/remote-ticket-protocol.md +253 -18
- package/skills/guide/references/setup.md +199 -16
- package/skills/guide/references/ticket-dependencies.md +1 -1
- package/skills/guide/references/ticket-migration.md +64 -0
- package/skills/operate/SKILL.md +126 -36
- package/skills/tickets/SKILL.md +71 -0
- package/templates/hosted-service/package.json +13 -0
- package/templates/hosted-service/vercel.json +7 -0
|
@@ -69,6 +69,20 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
|
|
|
69
69
|
'locally. --validate cannot be combined with --force.',
|
|
70
70
|
]),
|
|
71
71
|
},
|
|
72
|
+
{
|
|
73
|
+
name: 'connect',
|
|
74
|
+
audience: 'human',
|
|
75
|
+
summary: 'Connect this project to a hosted service with browser approval.',
|
|
76
|
+
detail:
|
|
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
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'disconnect',
|
|
81
|
+
audience: 'human',
|
|
82
|
+
summary: 'Revoke and remove this project’s saved credential.',
|
|
83
|
+
detail:
|
|
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
|
+
},
|
|
72
86
|
{
|
|
73
87
|
name: 'upgrade',
|
|
74
88
|
audience: 'human',
|
|
@@ -159,20 +173,26 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
|
|
|
159
173
|
detail: page([
|
|
160
174
|
'Usage:',
|
|
161
175
|
' ab ticket create <title> --body <file> [--state <state>] [--labels a,b] [--blocked-by id,id] [--json]',
|
|
162
|
-
' ab ticket
|
|
163
|
-
' ab ticket
|
|
164
|
-
' 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]',
|
|
165
182
|
' ab ticket list [--state <state>] [--labels a,b] [--json]',
|
|
166
183
|
' ab ticket show <id> [--store <ref>] [--json]',
|
|
167
|
-
' ab ticket move <id> <state> [--json]',
|
|
184
|
+
' ab ticket move <id> <state> [--expected-revision <opaque>] [--json]',
|
|
168
185
|
' ab ticket states [--json]',
|
|
169
186
|
' ab ticket state create <name> [--about <text>] [--json]',
|
|
170
187
|
' ab ticket attach <id> <kind> <path> [--name <name>] [--store <ref>] [--json]',
|
|
171
188
|
' ab ticket asset get <id> <kind> <name> <dest> [--rev <n>] [--store <ref>] [--json]',
|
|
172
189
|
' ab ticket asset rm <id> <kind> <name> [--store <ref>] [--json]',
|
|
173
190
|
'',
|
|
174
|
-
'Operate on the configured [tickets] source (§8.8);
|
|
175
|
-
'
|
|
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.',
|
|
176
196
|
'--state overrides [tickets].createState (or the provider default) for that',
|
|
177
197
|
'create only; it is passed through unchanged and validated by the source.',
|
|
178
198
|
'update partially replaces editable fields: omitted fields survive, --labels',
|
|
@@ -311,12 +331,23 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
|
|
|
311
331
|
' flushed as soon as it is written.',
|
|
312
332
|
' --store <ref> Override the BuildStore path or URL.',
|
|
313
333
|
'',
|
|
314
|
-
'Default filter —
|
|
315
|
-
'
|
|
316
|
-
'
|
|
317
|
-
'
|
|
318
|
-
'dispatcher.
|
|
319
|
-
'dispatcher.
|
|
334
|
+
'Default filter — actionable attention only. Builds: escalation.raised and',
|
|
335
|
+
'build.merge-decision (finalized open PR, auto-merge off or currently deferred).',
|
|
336
|
+
'Armed consent awaiting application is silent; one merge decision per build/PR.',
|
|
337
|
+
'Repository: harvest.escalated, harvest.recovery-exhausted, dispatcher.config-rejected,',
|
|
338
|
+
'dispatcher.config-reloaded with restartRequired, abnormal dispatcher.run-stopped,',
|
|
339
|
+
'and dispatcher.lost. dispatcher.ticket-source-persistent reports degradation past',
|
|
340
|
+
'resetAt after a tick started at or after reset completes or fails while still degraded,',
|
|
341
|
+
'or 15 minutes without a reset; dispatcher.tick-failure-persistent reports',
|
|
342
|
+
'five minutes of continuous tick failure outside source grace or cooldown. Each episode',
|
|
343
|
+
'reports once; recovery ends it. Cooldown failures do not start the tick-failure clock.',
|
|
344
|
+
'--event replaces defaults with exactly matching catalog events (or dispatcher.lost).',
|
|
345
|
+
'Routine failures, finalize and terminal outcomes require explicit --event globs.',
|
|
346
|
+
'Derived records carry observation instead of event, with observedAt and PR/reason',
|
|
347
|
+
'or source/cause/message/resetAt or run/firstFailure/error plus episodeSeq.',
|
|
348
|
+
'Cursors suppress delivered observations, including timer-only records. Old v1',
|
|
349
|
+
'cursors remain valid. Fresh watches baseline history and retain repository timers;',
|
|
350
|
+
'reconcile existing build attention through status after arming.',
|
|
320
351
|
'',
|
|
321
352
|
'Record shape (--json): build (slug, or null for a repository event), repo (repository',
|
|
322
353
|
'id, present only for repository events), event (the complete durable envelope: build,',
|
|
@@ -364,19 +395,23 @@ export const HELP_CATALOG: readonly HelpEntry[] = [
|
|
|
364
395
|
'Conditions (--for, repeatable): blocked, paused, running, done, aborted, terminal (done',
|
|
365
396
|
'or aborted), pr=open, pr=merged, pr=closed, pr=conflicted, phase=<phase> (a core phase —',
|
|
366
397
|
'plan, plan-review, implement, code-review, finalize, reconcile — or verify:<step>), and',
|
|
367
|
-
'attention (
|
|
398
|
+
'attention (an open escalation or a build waiting for a merge decision). Merge attention',
|
|
399
|
+
'requires a finalized open PR with auto-merge off or a current actionable gate deferral;',
|
|
400
|
+
'armed pending consent, routine outcomes and self-healing failures stay silent.',
|
|
401
|
+
'An unknown condition value is rejected before',
|
|
368
402
|
'any store access. Repeatable --event globs follow the ab watch rules. Multiple --for',
|
|
369
403
|
'values and --event globs are alternatives: the first one satisfied ends the wait — all',
|
|
370
404
|
'--for values in supply order first (each checked against the state after an event and',
|
|
371
|
-
'the
|
|
405
|
+
'the actionable build projection), then all --event globs in supply order.',
|
|
372
406
|
'',
|
|
373
407
|
'A status, PR-lifecycle, or phase condition is satisfied when a targeted build’s reduction',
|
|
374
408
|
'has that status, PR state, or current phase. If any targeted build already satisfies',
|
|
375
409
|
'such a condition when the command starts, the command returns immediately — in the',
|
|
376
410
|
'manner of kubectl wait — with that build’s latest event; --since does not suppress this.',
|
|
377
411
|
'Otherwise the condition is satisfied by the first later event whose reduction produces',
|
|
378
|
-
'it. attention
|
|
379
|
-
'
|
|
412
|
+
'it. attention also recognizes existing conditions immediately, even with --since.',
|
|
413
|
+
'--event conditions match only events appended after the command started, or a',
|
|
414
|
+
'cursor-resumed backlog after --since; use --event to opt into other event types.',
|
|
380
415
|
'',
|
|
381
416
|
'Record shape (--json): the ab watch record — build, event (the complete durable',
|
|
382
417
|
'envelope), state (the reduction after this event: status, phase, round, openEscalations,',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { projectToken } from './project-credentials'
|
|
2
|
+
import { resolveRepoOrigin } from './repo-state'
|
|
1
3
|
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
2
4
|
import { tmpdir } from 'node:os'
|
|
3
5
|
import { join, resolve } from 'node:path'
|
|
@@ -390,7 +392,31 @@ export async function validateInitReadiness(opts: {
|
|
|
390
392
|
const configPath = join(repo, 'autobuild.toml')
|
|
391
393
|
const configBytes = await readFile(configPath, 'utf8')
|
|
392
394
|
const config = await loadConfig(configPath)
|
|
393
|
-
|
|
395
|
+
hostPreflight(config, opts.env)
|
|
396
|
+
const selectedRemote = opts.env.AB_STORE?.trim() || config.store?.url
|
|
397
|
+
const identity =
|
|
398
|
+
selectedRemote && isRemoteStoreRef(selectedRemote) && !opts.env.AB_TOKEN
|
|
399
|
+
? ((await resolveRepoOrigin(repo, exec)) ?? repo)
|
|
400
|
+
: repo
|
|
401
|
+
const state = resolveRepoStatePaths({
|
|
402
|
+
repo: identity,
|
|
403
|
+
checkout: repo,
|
|
404
|
+
envStore: opts.env.AB_STORE,
|
|
405
|
+
configStore: config.store?.url,
|
|
406
|
+
})
|
|
407
|
+
opts = {
|
|
408
|
+
...opts,
|
|
409
|
+
env: {
|
|
410
|
+
...opts.env,
|
|
411
|
+
AB_STORE: state.storeRef,
|
|
412
|
+
...(isRemoteStoreRef(state.storeRef)
|
|
413
|
+
? { AB_TOKEN: projectToken(state.storeRef, identity, opts.env) }
|
|
414
|
+
: {}),
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
if (config.store?.url === state.storeRef && !opts.env.AB_TOKEN) {
|
|
418
|
+
throw new Error('No project credential for this service; run ab connect')
|
|
419
|
+
}
|
|
394
420
|
// Pre-registry parse and redaction come from the builtin capability table;
|
|
395
421
|
// after plugin load the registration's own declarations take over (AUT-516).
|
|
396
422
|
const builtinCaps = builtinWorkspaceProviderCapabilities(config.workspace.provider)
|
|
@@ -405,7 +431,6 @@ export async function validateInitReadiness(opts: {
|
|
|
405
431
|
let caps: WorkspaceProviderCapabilities | undefined
|
|
406
432
|
let assigneeLine: string | undefined
|
|
407
433
|
try {
|
|
408
|
-
hostPreflight(config, opts.env)
|
|
409
434
|
// Ticket acquisition is a host responsibility for both workspace providers.
|
|
410
435
|
// Exercise its read surface before allocating disposable infrastructure.
|
|
411
436
|
const hostPlugins = await loadPlugins(config.plugins, repo, { packageRoot: repo })
|
|
@@ -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 { preparePinnedAssets } from '../store/ticket-assets'
|
|
2
|
+
import type { PinnedAsset } from '../events/payloads'
|
|
3
|
+
import { abConnect, abDisconnect } from './connect'
|
|
1
4
|
/**
|
|
2
5
|
* `ab` — the only channel between agents and the store, and the enforcement
|
|
3
6
|
* point of the entire ontology (SPEC §8). This module is pure routing: every
|
|
@@ -16,7 +19,7 @@ import type { Clock } from '../store/types'
|
|
|
16
19
|
import type { BuildStore } from '../store/types'
|
|
17
20
|
import { textContent } from '../store/types'
|
|
18
21
|
import type { Forge } from '../ports/types'
|
|
19
|
-
import type
|
|
22
|
+
import { spawnExec, type Exec } from '../ports/workspace/git-worktree'
|
|
20
23
|
import type { IdSource } from '../ids'
|
|
21
24
|
import { reduceBuild } from '../kernel/reducer'
|
|
22
25
|
import { artifactDownload, artifactDownloadStream, artifactGet, artifactPut } from './artifact'
|
|
@@ -44,7 +47,6 @@ import { recognizeHelpRequest, renderCommandHelp, renderTopLevelHelp } from './h
|
|
|
44
47
|
import { observe } from './observe'
|
|
45
48
|
import { abPlugin, type PluginContractSubprocess } from './plugin'
|
|
46
49
|
import { preparePrAttachments } from './pr-attachments'
|
|
47
|
-
import { renderPrSummary } from './pr-summary'
|
|
48
50
|
import { abBuildAssetGet, abBuilds, abBuildStatus } from './status'
|
|
49
51
|
import { abRepositoryStatus } from './repository-status'
|
|
50
52
|
import { abWatch, WATCH_USAGE } from './watch'
|
|
@@ -53,6 +55,7 @@ import { abMcp, AB_MCP_USAGE } from './mcp'
|
|
|
53
55
|
import type { StoreOpener } from './store-opening'
|
|
54
56
|
import { done, escalate, verdict } from './terminals'
|
|
55
57
|
import { abTicket, openTicketSource } from './ticket'
|
|
58
|
+
import { checkUpgradeCompatibility, type CompatibilityFetch } from './upgrade-compatibility'
|
|
56
59
|
import { abUpgrade, type ResolveConflict } from './upgrade'
|
|
57
60
|
import { withUpgradeProgress } from './upgrade-progress'
|
|
58
61
|
import {
|
|
@@ -95,6 +98,8 @@ import {
|
|
|
95
98
|
*/
|
|
96
99
|
export const SESSIONLESS_COMMANDS = new Set([
|
|
97
100
|
'init',
|
|
101
|
+
'connect',
|
|
102
|
+
'disconnect',
|
|
98
103
|
'upgrade',
|
|
99
104
|
'update',
|
|
100
105
|
'ticket',
|
|
@@ -186,6 +191,7 @@ export interface SessionlessCliDeps {
|
|
|
186
191
|
* to the running module, independent of the current repository. */
|
|
187
192
|
distributionRoot?: string
|
|
188
193
|
/** Self-update seams keep release/package-manager orchestration testable. */
|
|
194
|
+
compatibilityFetch?: CompatibilityFetch
|
|
189
195
|
selfUpdateCommand?: SelfUpdateCommand
|
|
190
196
|
selfUpdate?: (options: SelfUpdateOptions) => Promise<SelfUpdateResult>
|
|
191
197
|
/** Production supplies the tool-free upgrade agent; tests inject a fake.
|
|
@@ -304,6 +310,7 @@ async function runBuildControl(
|
|
|
304
310
|
action: BuildControlAction,
|
|
305
311
|
storeRef?: string,
|
|
306
312
|
readTicketBody?: (ref: TicketRef) => Promise<string>,
|
|
313
|
+
prepareTicketAssets?: (ref: TicketRef, store: BuildStore, repo: string) => Promise<PinnedAsset[]>,
|
|
307
314
|
): Promise<void> {
|
|
308
315
|
if (deps.exec === undefined) {
|
|
309
316
|
throw new Error(
|
|
@@ -318,6 +325,7 @@ async function runBuildControl(
|
|
|
318
325
|
action,
|
|
319
326
|
...(storeRef !== undefined ? { storeRef } : {}),
|
|
320
327
|
...(readTicketBody !== undefined ? { readTicketBody } : {}),
|
|
328
|
+
...(prepareTicketAssets ? { prepareTicketAssets } : {}),
|
|
321
329
|
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
322
330
|
})
|
|
323
331
|
deps.stdout(buildControlConfirmation(result))
|
|
@@ -444,6 +452,24 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
444
452
|
|
|
445
453
|
// init and upgrade run OUTSIDE build sessions (§16.3): they operate on a
|
|
446
454
|
// repo, not a build, so they route before any store/env requirement.
|
|
455
|
+
case 'connect':
|
|
456
|
+
case 'disconnect': {
|
|
457
|
+
const usage = command === 'connect' ? 'usage: ab connect [url]' : 'usage: ab disconnect'
|
|
458
|
+
const parsed = parseArgs(rest, { tickets: 'boolean', builds: 'boolean' }, usage)
|
|
459
|
+
if (parsed.positionals.length > (command === 'connect' ? 1 : 0)) throw new Error(usage)
|
|
460
|
+
const options = {
|
|
461
|
+
tickets: parsed.flags.get('tickets') === true,
|
|
462
|
+
builds: parsed.flags.get('builds') === true,
|
|
463
|
+
cwd: deps.workspacePath,
|
|
464
|
+
env: deps.processEnv ?? process.env,
|
|
465
|
+
exec: deps.exec ?? spawnExec,
|
|
466
|
+
print: stdout,
|
|
467
|
+
...(deps.signal ? { signal: deps.signal } : {}),
|
|
468
|
+
}
|
|
469
|
+
if (command === 'connect') await abConnect(parsed.positionals[0], options)
|
|
470
|
+
else await abDisconnect(options)
|
|
471
|
+
return 0
|
|
472
|
+
}
|
|
447
473
|
case 'init': {
|
|
448
474
|
const usage = 'usage: ab init [target] [--force | --validate] (§16.3)'
|
|
449
475
|
const removed = [
|
|
@@ -515,6 +541,14 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
515
541
|
}
|
|
516
542
|
const targetRepo = parsed.positionals[0] ?? deps.workspacePath
|
|
517
543
|
const noCommit = parsed.flags.has('no-commit')
|
|
544
|
+
const compatibilityWarnings = new Set<string>()
|
|
545
|
+
const upgradeStderr = (line: string): void => {
|
|
546
|
+
if (line.startsWith('warning:')) {
|
|
547
|
+
if (compatibilityWarnings.has(line)) return
|
|
548
|
+
compatibilityWarnings.add(line)
|
|
549
|
+
}
|
|
550
|
+
stderr(line)
|
|
551
|
+
}
|
|
518
552
|
const handoff = deps.processEnv?.[SELF_UPDATE_HANDOFF_ENV] === '1'
|
|
519
553
|
let commitContext: UpgradeCommitContext | undefined
|
|
520
554
|
if (!noCommit) {
|
|
@@ -543,9 +577,10 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
543
577
|
if (!parsed.flags.has('no-self-update') && !handoff) {
|
|
544
578
|
const update = await (deps.selfUpdate ?? selfUpdate)({
|
|
545
579
|
targetRepo,
|
|
580
|
+
compatibilityFetch: deps.compatibilityFetch,
|
|
546
581
|
stdout,
|
|
547
|
-
stderr,
|
|
548
|
-
env: deps.processEnv ??
|
|
582
|
+
stderr: upgradeStderr,
|
|
583
|
+
env: deps.processEnv ?? process.env,
|
|
549
584
|
noCommit: noCommit || commitContext === undefined,
|
|
550
585
|
...(commitContext === undefined
|
|
551
586
|
? {}
|
|
@@ -559,6 +594,13 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
559
594
|
if (update.kind === 'handoff') return update.exitCode
|
|
560
595
|
if (update.kind === 'failed') return 1
|
|
561
596
|
}
|
|
597
|
+
await checkUpgradeCompatibility({
|
|
598
|
+
targetRepo,
|
|
599
|
+
distRoot: deps.distributionRoot,
|
|
600
|
+
env: deps.processEnv,
|
|
601
|
+
stderr: upgradeStderr,
|
|
602
|
+
fetch: deps.compatibilityFetch,
|
|
603
|
+
})
|
|
562
604
|
const resolverFactory = deps.upgradeResolverFactory
|
|
563
605
|
let resolver: ResolveConflict | undefined
|
|
564
606
|
const resolveConflict: ResolveConflict | undefined =
|
|
@@ -1091,6 +1133,7 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1091
1133
|
: parsed.flags.has('revise-spec-from-ticket')
|
|
1092
1134
|
? ({ kind: 'revise-spec', body: { kind: 'ticket' } } as const)
|
|
1093
1135
|
: undefined
|
|
1136
|
+
let revisionTicketSource: import('../ports/types').TicketSource | undefined
|
|
1094
1137
|
const readTicketBody =
|
|
1095
1138
|
resolve?.kind === 'revise-spec' && resolve.body.kind === 'ticket'
|
|
1096
1139
|
? async (ref: TicketRef): Promise<string> => {
|
|
@@ -1104,6 +1147,7 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1104
1147
|
},
|
|
1105
1148
|
'ab answer --revise-spec-from-ticket',
|
|
1106
1149
|
)
|
|
1150
|
+
revisionTicketSource = source
|
|
1107
1151
|
if (source.name !== ref.source) {
|
|
1108
1152
|
throw new Error(
|
|
1109
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`,
|
|
@@ -1115,7 +1159,10 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1115
1159
|
`ticket "${ref.id}" was not found in the configured ${source.name} source`,
|
|
1116
1160
|
)
|
|
1117
1161
|
}
|
|
1118
|
-
if (
|
|
1162
|
+
if (
|
|
1163
|
+
ticket.ref.source !== ref.source ||
|
|
1164
|
+
(source.name !== 'hosted' && ticket.ref.id !== ref.id)
|
|
1165
|
+
) {
|
|
1119
1166
|
throw new Error(
|
|
1120
1167
|
`ticket adapter returned ${ticket.ref.source}:${ticket.ref.id}, expected ${ref.source}:${ref.id}`,
|
|
1121
1168
|
)
|
|
@@ -1134,6 +1181,13 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1134
1181
|
},
|
|
1135
1182
|
storeRef,
|
|
1136
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,
|
|
1137
1191
|
)
|
|
1138
1192
|
return 0
|
|
1139
1193
|
}
|
|
@@ -1314,7 +1368,7 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1314
1368
|
const meta = await artifactPut(session, kind, file, { attach })
|
|
1315
1369
|
|
|
1316
1370
|
// A designation made after the PR exists (verify after reconcile or a
|
|
1317
|
-
// finalize post-step)
|
|
1371
|
+
// finalize post-step) prepares hosted attachments. Every
|
|
1318
1372
|
// external side effect is best-effort after the atomic designation.
|
|
1319
1373
|
if (attach) {
|
|
1320
1374
|
try {
|
|
@@ -1328,16 +1382,6 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1328
1382
|
// asset identity before throwing. Keep going so its hosting
|
|
1329
1383
|
// failure cannot suppress the complete text projection.
|
|
1330
1384
|
}
|
|
1331
|
-
try {
|
|
1332
|
-
await session.forge.commentOnPr(
|
|
1333
|
-
session.workspacePath,
|
|
1334
|
-
pr.number,
|
|
1335
|
-
renderPrSummary(session.env, await session.store.getEvents(session.env.build)),
|
|
1336
|
-
)
|
|
1337
|
-
} catch {
|
|
1338
|
-
// The exact artifact and designation are already durable. A
|
|
1339
|
-
// comment refresh cannot turn the agent's deposit into failure.
|
|
1340
|
-
}
|
|
1341
1385
|
}
|
|
1342
1386
|
} catch {
|
|
1343
1387
|
// Reading the optional PR projection is best-effort too.
|
|
@@ -107,19 +107,21 @@ export async function buildMcpTicketBackend(
|
|
|
107
107
|
config.tickets.readyState,
|
|
108
108
|
config.tickets.claimedState,
|
|
109
109
|
config.tickets.createState,
|
|
110
|
+
config.tickets.doneState,
|
|
110
111
|
].filter((state): state is string => state !== undefined),
|
|
111
112
|
),
|
|
112
113
|
]
|
|
113
114
|
return {
|
|
114
115
|
sourceFor: async (context) => {
|
|
115
|
-
if (config.tickets.teamKey !== context.teamKey) {
|
|
116
|
+
if ((config.tickets.teamKey ?? '') !== context.teamKey) {
|
|
116
117
|
throw new Error(
|
|
117
118
|
`the repository's effective ticket config names team "${context.teamKey}", but this checkout's autobuild.toml names team "${String(config.tickets.teamKey)}"; refusing to serve tickets across that drift — run ab mcp from the checkout dispatch uses`,
|
|
118
119
|
)
|
|
119
120
|
}
|
|
120
121
|
return source
|
|
121
122
|
},
|
|
122
|
-
statesFor: async () =>
|
|
123
|
+
statesFor: async (_context, source) =>
|
|
124
|
+
source.listStates ? (await source.listStates()).map((s) => s.name) : states,
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
|
|
@@ -200,6 +202,11 @@ export async function abMcp(opts: AbMcpOpts): Promise<number> {
|
|
|
200
202
|
try {
|
|
201
203
|
const backend = await buildMcpTicketBackend({
|
|
202
204
|
...opts,
|
|
205
|
+
env: {
|
|
206
|
+
...opts.env,
|
|
207
|
+
AB_STORE: context.storeRef,
|
|
208
|
+
...(context.token !== undefined ? { AB_TOKEN: context.token } : {}),
|
|
209
|
+
},
|
|
203
210
|
checkout: context.checkout,
|
|
204
211
|
localStateRoot: context.localStateRoot,
|
|
205
212
|
})
|