@defrex/autobuild 0.8.0 → 0.9.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 +313 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +57 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +151 -25
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +169 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +10 -0
- package/package.json +43 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/model.ts +57 -7
- package/packages/core/src/cli/dashboard/poll.ts +69 -10
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch.ts +320 -46
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +47 -8
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +248 -222
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/status.ts +149 -27
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +307 -7
- package/packages/core/src/cli/wait.ts +147 -39
- package/packages/core/src/cli/watch.ts +128 -166
- package/packages/core/src/config/live.ts +22 -0
- package/packages/core/src/config/pipeline-source.ts +376 -0
- package/packages/core/src/config/schema.ts +230 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +2 -1
- package/packages/core/src/kernel/auto-merge-default.ts +123 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/reducer.ts +40 -1
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +208 -19
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/types.ts +17 -2
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +37 -0
- package/packages/core/src/processes/build-runner.ts +54 -3
- package/packages/core/src/processes/dispatcher.ts +247 -41
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/orchestrator-tick.ts +319 -0
- package/packages/core/src/processes/sandbox-state.ts +10 -0
- package/packages/core/src/store/adapter.ts +32 -0
- package/packages/core/src/store/build-scope.ts +64 -0
- package/packages/core/src/store/contract.ts +868 -0
- package/packages/core/src/store/digest.ts +49 -0
- package/packages/core/src/store/local/schema.ts +29 -2
- package/packages/core/src/store/local/store.ts +338 -4
- package/packages/core/src/store/memory.ts +179 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +71 -0
- package/packages/core/src/store/remote/client.ts +260 -1
- package/packages/core/src/store/remote/index.ts +12 -2
- package/packages/core/src/store/remote/protocol.ts +100 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +140 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +64 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +28 -9
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +425 -0
- package/packages/core/src/store/types.ts +122 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +126 -33
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +169 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -20,9 +20,20 @@ import {
|
|
|
20
20
|
updateTicket,
|
|
21
21
|
} from '../ports/tickets/operations'
|
|
22
22
|
import type { Ticket, TicketSource, TicketUpdate } from '../ports/types'
|
|
23
|
-
import
|
|
23
|
+
import { reduceBuild } from '../kernel/reducer'
|
|
24
|
+
import type { BuildStore } from '../store/types'
|
|
25
|
+
import { spawnExec, type Exec } from '../ports/workspace/git-worktree'
|
|
26
|
+
import type { TicketAssetSummary } from '../store/ticket-assets'
|
|
24
27
|
import { parseArgs, stringFlag } from './args'
|
|
25
28
|
import { resolveMainRepo, resolveRepoStatePaths } from './repo-state'
|
|
29
|
+
import { withSessionlessStore, type StoreOpener } from './store-opening'
|
|
30
|
+
import {
|
|
31
|
+
buildAssetInput,
|
|
32
|
+
formatAssetSize,
|
|
33
|
+
ticketAssetNote,
|
|
34
|
+
walkAssetPath,
|
|
35
|
+
writeAssetTo,
|
|
36
|
+
} from './ticket-assets'
|
|
26
37
|
|
|
27
38
|
export type TicketSourceFactory = (
|
|
28
39
|
config: TicketsConfig,
|
|
@@ -41,6 +52,11 @@ export interface TicketCommandOpts {
|
|
|
41
52
|
stdout: (line: string) => void
|
|
42
53
|
/** Injectable for tests; defaults to the real adapter factory. */
|
|
43
54
|
sourceFactory?: TicketSourceFactory
|
|
55
|
+
/** Store seam for the ticket-asset commands and `show`'s asset list. */
|
|
56
|
+
openStore?: StoreOpener
|
|
57
|
+
/** Explicit `--store` for the store-backed commands; precedence is applied
|
|
58
|
+
* by repo-state.ts. */
|
|
59
|
+
storeRef?: string
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
export interface TicketCreateOpts extends TicketCommandOpts {
|
|
@@ -88,6 +104,35 @@ export interface TicketShowOpts extends TicketCommandOpts {
|
|
|
88
104
|
json?: boolean
|
|
89
105
|
}
|
|
90
106
|
|
|
107
|
+
export interface TicketAttachOpts extends TicketCommandOpts {
|
|
108
|
+
id: string
|
|
109
|
+
kind: string
|
|
110
|
+
path: string
|
|
111
|
+
/** Defaults to the path's basename. */
|
|
112
|
+
name?: string
|
|
113
|
+
json?: boolean
|
|
114
|
+
/** Warning sink (active-build notice); stdout stays the command's result. */
|
|
115
|
+
stderr?: (line: string) => void
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface TicketAssetGetOpts extends TicketCommandOpts {
|
|
119
|
+
id: string
|
|
120
|
+
kind: string
|
|
121
|
+
name: string
|
|
122
|
+
dest: string
|
|
123
|
+
rev?: number
|
|
124
|
+
json?: boolean
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface TicketAssetRmOpts extends TicketCommandOpts {
|
|
128
|
+
id: string
|
|
129
|
+
kind: string
|
|
130
|
+
name: string
|
|
131
|
+
json?: boolean
|
|
132
|
+
/** Warning sink (active-build notice); stdout stays the command's result. */
|
|
133
|
+
stderr?: (line: string) => void
|
|
134
|
+
}
|
|
135
|
+
|
|
91
136
|
export interface TicketMoveOpts extends TicketCommandOpts {
|
|
92
137
|
id: string
|
|
93
138
|
/** Source-local workflow state; validation belongs to the adapter. */
|
|
@@ -95,7 +140,17 @@ export interface TicketMoveOpts extends TicketCommandOpts {
|
|
|
95
140
|
json?: boolean
|
|
96
141
|
}
|
|
97
142
|
|
|
98
|
-
type TicketCommandName =
|
|
143
|
+
type TicketCommandName =
|
|
144
|
+
| 'create'
|
|
145
|
+
| 'update'
|
|
146
|
+
| 'block'
|
|
147
|
+
| 'unblock'
|
|
148
|
+
| 'list'
|
|
149
|
+
| 'show'
|
|
150
|
+
| 'move'
|
|
151
|
+
| 'attach'
|
|
152
|
+
| 'asset get'
|
|
153
|
+
| 'asset rm'
|
|
99
154
|
|
|
100
155
|
export interface ResolvedTicketCommand {
|
|
101
156
|
config: Config
|
|
@@ -173,7 +228,11 @@ function ticketSummary(ticket: Ticket): string {
|
|
|
173
228
|
return fields.join(' ')
|
|
174
229
|
}
|
|
175
230
|
|
|
176
|
-
function
|
|
231
|
+
function assetLine(asset: TicketAssetSummary): string {
|
|
232
|
+
return ` ${asset.kind}/${asset.name} rev ${asset.revision} ${formatAssetSize(asset.size)}`
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function ticketDetail(ticket: Ticket, assets: TicketAssetSummary[] = []): string[] {
|
|
177
236
|
const lines = [
|
|
178
237
|
`ticket ${ticket.ref.source}:${ticket.ref.id}`,
|
|
179
238
|
` title: ${ticket.title}`,
|
|
@@ -184,6 +243,7 @@ function ticketDetail(ticket: Ticket): string[] {
|
|
|
184
243
|
lines.push(` blocked by: ${ticket.blockedBy.join(', ')}`)
|
|
185
244
|
}
|
|
186
245
|
if (ticket.ref.url !== undefined) lines.push(` url: ${ticket.ref.url}`)
|
|
246
|
+
if (assets.length > 0) lines.push(' assets:', ...assets.map(assetLine))
|
|
187
247
|
lines.push(' body:')
|
|
188
248
|
return lines
|
|
189
249
|
}
|
|
@@ -291,16 +351,169 @@ export async function abTicketList(opts: TicketListOpts): Promise<void> {
|
|
|
291
351
|
export async function abTicketShow(opts: TicketShowOpts): Promise<void> {
|
|
292
352
|
const { source } = await resolveTicketCommand(opts, 'show')
|
|
293
353
|
const ticket = await requireTicket(source, opts.id)
|
|
354
|
+
// An asset-listing failure fails the command rather than hiding the assets.
|
|
355
|
+
const assets = await withTicketStore(opts, ({ store, repo }) =>
|
|
356
|
+
store.listTicketAssets(repo, opts.id),
|
|
357
|
+
)
|
|
294
358
|
if (opts.json === true) {
|
|
295
|
-
opts.stdout(
|
|
359
|
+
opts.stdout(
|
|
360
|
+
JSON.stringify(
|
|
361
|
+
{
|
|
362
|
+
...ticket,
|
|
363
|
+
assets: assets.map(({ kind, name, revision, size, layout, fileCount }) => ({
|
|
364
|
+
kind,
|
|
365
|
+
name,
|
|
366
|
+
revision,
|
|
367
|
+
size,
|
|
368
|
+
layout,
|
|
369
|
+
fileCount,
|
|
370
|
+
})),
|
|
371
|
+
},
|
|
372
|
+
null,
|
|
373
|
+
2,
|
|
374
|
+
),
|
|
375
|
+
)
|
|
296
376
|
return
|
|
297
377
|
}
|
|
298
|
-
for (const line of ticketDetail(ticket)) opts.stdout(line)
|
|
378
|
+
for (const line of ticketDetail(ticket, assets)) opts.stdout(line)
|
|
299
379
|
// Keep the adapter-provided body untouched. It may itself be multiline and
|
|
300
380
|
// may intentionally end (or not end) with a newline.
|
|
301
381
|
opts.stdout(ticket.body)
|
|
302
382
|
}
|
|
303
383
|
|
|
384
|
+
function withTicketStore<T>(
|
|
385
|
+
opts: TicketCommandOpts,
|
|
386
|
+
use: (context: { store: BuildStore; repo: string }) => Promise<T>,
|
|
387
|
+
): Promise<T> {
|
|
388
|
+
return withSessionlessStore(
|
|
389
|
+
{
|
|
390
|
+
targetRepo: opts.targetRepo,
|
|
391
|
+
env: opts.env,
|
|
392
|
+
exec: opts.exec ?? spawnExec,
|
|
393
|
+
...(opts.storeRef !== undefined ? { storeRef: opts.storeRef } : {}),
|
|
394
|
+
...(opts.openStore !== undefined ? { openStore: opts.openStore } : {}),
|
|
395
|
+
},
|
|
396
|
+
use,
|
|
397
|
+
)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** Slugs of this repo's non-terminal builds for the ticket. A build freezes its
|
|
401
|
+
* ticket assets at the claim (SPEC §6.3), so a change to them does not reach
|
|
402
|
+
* it. The notice is advisory: a failed lookup yields none and never blocks. */
|
|
403
|
+
async function activeBuildsFor(store: BuildStore, repo: string, id: string): Promise<string[]> {
|
|
404
|
+
try {
|
|
405
|
+
const active: string[] = []
|
|
406
|
+
for (const record of await store.listBuilds()) {
|
|
407
|
+
if (record.repo !== repo || record.ticket?.id !== id) continue
|
|
408
|
+
const { status } = reduceBuild(await store.getEvents(record.slug))
|
|
409
|
+
if (status !== 'done' && status !== 'aborted') active.push(record.slug)
|
|
410
|
+
}
|
|
411
|
+
return active
|
|
412
|
+
} catch {
|
|
413
|
+
return []
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function warnActiveBuilds(
|
|
418
|
+
opts: { stderr?: (line: string) => void; id: string },
|
|
419
|
+
slugs: string[],
|
|
420
|
+
): void {
|
|
421
|
+
for (const slug of slugs) {
|
|
422
|
+
opts.stderr?.(
|
|
423
|
+
`warning: build ${slug} is active for ticket ${opts.id}; it keeps the assets it froze at ` +
|
|
424
|
+
`the claim unless its spec is revised from the ticket (ab answer ${slug} --revise-spec-from-ticket)`,
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** `ab ticket attach <id> <kind> <path>` — store a file or folder as a ticket
|
|
430
|
+
* asset, then leave a note on the ticket in its source. Order matters: the
|
|
431
|
+
* source is asked first, so an unknown id stores nothing; the note follows the
|
|
432
|
+
* store write, so it never points at nothing. */
|
|
433
|
+
export async function abTicketAttach(opts: TicketAttachOpts): Promise<void> {
|
|
434
|
+
const { source } = await resolveTicketCommand(opts, 'attach')
|
|
435
|
+
await requireTicket(source, opts.id)
|
|
436
|
+
const walk = await walkAssetPath(opts.path)
|
|
437
|
+
const name = opts.name ?? walk.basename
|
|
438
|
+
const stored = await withTicketStore(opts, async ({ store, repo }) => {
|
|
439
|
+
const limits = await store.ticketAssetLimits(repo)
|
|
440
|
+
const input = await buildAssetInput(walk, { kind: opts.kind, name }, limits)
|
|
441
|
+
const live = await store.listTicketAssets(repo, opts.id)
|
|
442
|
+
const replacing = live.some((asset) => asset.kind === opts.kind && asset.name === name)
|
|
443
|
+
const meta = await store.putTicketAsset(repo, opts.id, input)
|
|
444
|
+
return { meta, replacing, active: await activeBuildsFor(store, repo, opts.id) }
|
|
445
|
+
})
|
|
446
|
+
const { meta } = stored
|
|
447
|
+
warnActiveBuilds(opts, stored.active)
|
|
448
|
+
try {
|
|
449
|
+
await source.comment(opts.id, ticketAssetNote(stored.replacing ? 'replaced' : 'attached', meta))
|
|
450
|
+
} catch (error) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
`ticket asset ${meta.kind}/${meta.name} was stored as revision ${meta.revision}, but the note on ` +
|
|
453
|
+
`${source.name}:${opts.id} failed: ${error instanceof Error ? error.message : String(error)} ` +
|
|
454
|
+
'(re-running attach stores another revision)',
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
if (opts.json === true) {
|
|
458
|
+
opts.stdout(JSON.stringify(meta, null, 2))
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
opts.stdout(
|
|
462
|
+
`ticket asset attached: ${source.name}:${opts.id} ${meta.kind}/${meta.name} revision ${meta.revision} ` +
|
|
463
|
+
`(${formatAssetSize(meta.size)})`,
|
|
464
|
+
)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** `ab ticket asset get <id> <kind> <name> <dest>` — exact bytes at the
|
|
468
|
+
* latest or a named revision. */
|
|
469
|
+
export async function abTicketAssetGet(opts: TicketAssetGetOpts): Promise<void> {
|
|
470
|
+
const asset = await withTicketStore(opts, ({ store, repo }) =>
|
|
471
|
+
store.getTicketAsset(repo, opts.id, opts.kind, opts.name, opts.rev),
|
|
472
|
+
)
|
|
473
|
+
if (asset === null) {
|
|
474
|
+
const revision = opts.rev === undefined ? '' : ` revision ${opts.rev}`
|
|
475
|
+
throw new Error(
|
|
476
|
+
`ticket ${opts.id} has no asset ${opts.kind}/${opts.name}${revision}` +
|
|
477
|
+
(opts.rev === undefined ? ' (it may have been removed — try --rev <n>)' : ''),
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
const written = await writeAssetTo(asset, opts.dest)
|
|
481
|
+
if (opts.json === true) {
|
|
482
|
+
opts.stdout(JSON.stringify({ ...asset.meta, path: written }, null, 2))
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
opts.stdout(
|
|
486
|
+
`ticket asset downloaded: ${opts.kind}/${opts.name} revision ${asset.meta.revision} → ${written}`,
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** `ab ticket asset rm <id> <kind> <name>` — remove from the current assets
|
|
491
|
+
* (earlier revisions stay retrievable) and note the removal on the ticket. */
|
|
492
|
+
export async function abTicketAssetRm(opts: TicketAssetRmOpts): Promise<void> {
|
|
493
|
+
const { source } = await resolveTicketCommand(opts, 'asset rm')
|
|
494
|
+
const { removed, active } = await withTicketStore(opts, async ({ store, repo }) => ({
|
|
495
|
+
removed: await store.removeTicketAsset(repo, opts.id, opts.kind, opts.name),
|
|
496
|
+
active: await activeBuildsFor(store, repo, opts.id),
|
|
497
|
+
}))
|
|
498
|
+
if (removed === null) {
|
|
499
|
+
throw new Error(`ticket ${opts.id} has no current asset ${opts.kind}/${opts.name} to remove`)
|
|
500
|
+
}
|
|
501
|
+
warnActiveBuilds(opts, active)
|
|
502
|
+
try {
|
|
503
|
+
await source.comment(opts.id, ticketAssetNote('removed', removed))
|
|
504
|
+
} catch (error) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`ticket asset ${opts.kind}/${opts.name} was removed, but the note on ${source.name}:${opts.id} ` +
|
|
507
|
+
`failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
if (opts.json === true) {
|
|
511
|
+
opts.stdout(JSON.stringify(removed, null, 2))
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
opts.stdout(`ticket asset removed: ${source.name}:${opts.id} ${opts.kind}/${opts.name}`)
|
|
515
|
+
}
|
|
516
|
+
|
|
304
517
|
/** `ab ticket move <id> <state>` — adapter-owned state validation and move. */
|
|
305
518
|
export async function abTicketMove(opts: TicketMoveOpts): Promise<void> {
|
|
306
519
|
const { source } = await resolveTicketCommand(opts, 'move')
|
|
@@ -319,7 +532,13 @@ const UPDATE_USAGE =
|
|
|
319
532
|
const BLOCK_USAGE = 'usage: ab ticket block <id> <blocker-id[,blocker-id...]> [--json] (§8.8)'
|
|
320
533
|
const UNBLOCK_USAGE = 'usage: ab ticket unblock <id> <blocker-id[,blocker-id...]> [--json] (§8.8)'
|
|
321
534
|
const LIST_USAGE = 'usage: ab ticket list [--state <state>] [--labels a,b] [--json] (§8.8)'
|
|
322
|
-
const SHOW_USAGE = 'usage: ab ticket show <id> [--json] (§8.8)'
|
|
535
|
+
const SHOW_USAGE = 'usage: ab ticket show <id> [--store <ref>] [--json] (§8.8)'
|
|
536
|
+
const ATTACH_USAGE =
|
|
537
|
+
'usage: ab ticket attach <id> <kind> <path> [--name <name>] [--store <ref>] [--json] (§8.8)'
|
|
538
|
+
const ASSET_GET_USAGE =
|
|
539
|
+
'usage: ab ticket asset get <id> <kind> <name> <dest> [--rev <n>] [--store <ref>] [--json] (§8.8)'
|
|
540
|
+
const ASSET_RM_USAGE =
|
|
541
|
+
'usage: ab ticket asset rm <id> <kind> <name> [--store <ref>] [--json] (§8.8)'
|
|
323
542
|
const MOVE_USAGE = 'usage: ab ticket move <id> <state> [--json] (§8.8)'
|
|
324
543
|
export const TICKET_USAGE = [
|
|
325
544
|
CREATE_USAGE,
|
|
@@ -329,6 +548,9 @@ export const TICKET_USAGE = [
|
|
|
329
548
|
LIST_USAGE,
|
|
330
549
|
SHOW_USAGE,
|
|
331
550
|
MOVE_USAGE,
|
|
551
|
+
ATTACH_USAGE,
|
|
552
|
+
ASSET_GET_USAGE,
|
|
553
|
+
ASSET_RM_USAGE,
|
|
332
554
|
].join('\n')
|
|
333
555
|
|
|
334
556
|
function commaList(value: string): string[] {
|
|
@@ -444,19 +666,97 @@ export async function abTicket(argv: string[], opts: TicketCliOpts): Promise<voi
|
|
|
444
666
|
}
|
|
445
667
|
|
|
446
668
|
case 'show': {
|
|
447
|
-
const parsed = parseArgs(args, { json: 'boolean' }, TICKET_USAGE)
|
|
669
|
+
const parsed = parseArgs(args, { json: 'boolean', store: 'value' }, TICKET_USAGE)
|
|
448
670
|
const [id, ...extra] = parsed.positionals
|
|
449
671
|
if (id === undefined || id.trim() === '' || extra.length > 0) {
|
|
450
672
|
throw new Error(TICKET_USAGE)
|
|
451
673
|
}
|
|
674
|
+
const store = stringFlag(parsed, 'store')
|
|
452
675
|
await abTicketShow({
|
|
453
676
|
...opts,
|
|
677
|
+
...(store !== undefined ? { storeRef: store } : {}),
|
|
454
678
|
id,
|
|
455
679
|
json: parsed.flags.has('json'),
|
|
456
680
|
})
|
|
457
681
|
return
|
|
458
682
|
}
|
|
459
683
|
|
|
684
|
+
case 'attach': {
|
|
685
|
+
const parsed = parseArgs(
|
|
686
|
+
args,
|
|
687
|
+
{ name: 'value', store: 'value', json: 'boolean' },
|
|
688
|
+
TICKET_USAGE,
|
|
689
|
+
)
|
|
690
|
+
const [id, kind, path, ...extra] = parsed.positionals
|
|
691
|
+
if (!id?.trim() || !kind?.trim() || !path?.trim() || extra.length > 0) {
|
|
692
|
+
throw new Error(TICKET_USAGE)
|
|
693
|
+
}
|
|
694
|
+
const name = stringFlag(parsed, 'name')
|
|
695
|
+
if (name !== undefined && name.trim() === '') throw new Error(TICKET_USAGE)
|
|
696
|
+
const store = stringFlag(parsed, 'store')
|
|
697
|
+
await abTicketAttach({
|
|
698
|
+
...opts,
|
|
699
|
+
...(store !== undefined ? { storeRef: store } : {}),
|
|
700
|
+
id,
|
|
701
|
+
kind,
|
|
702
|
+
path,
|
|
703
|
+
...(name !== undefined ? { name } : {}),
|
|
704
|
+
json: parsed.flags.has('json'),
|
|
705
|
+
})
|
|
706
|
+
return
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
case 'asset': {
|
|
710
|
+
const [action, ...assetArgs] = args
|
|
711
|
+
if (action === 'get') {
|
|
712
|
+
const parsed = parseArgs(
|
|
713
|
+
assetArgs,
|
|
714
|
+
{ rev: 'value', store: 'value', json: 'boolean' },
|
|
715
|
+
TICKET_USAGE,
|
|
716
|
+
)
|
|
717
|
+
const [id, kind, name, dest, ...extra] = parsed.positionals
|
|
718
|
+
if (!id?.trim() || !kind?.trim() || !name?.trim() || !dest?.trim() || extra.length > 0) {
|
|
719
|
+
throw new Error(TICKET_USAGE)
|
|
720
|
+
}
|
|
721
|
+
const rawRev = stringFlag(parsed, 'rev')
|
|
722
|
+
if (rawRev !== undefined && !/^\d+$/.test(rawRev)) {
|
|
723
|
+
throw new Error(
|
|
724
|
+
`--rev must be a nonnegative integer, got "${rawRev}" — ${ASSET_GET_USAGE}`,
|
|
725
|
+
)
|
|
726
|
+
}
|
|
727
|
+
const store = stringFlag(parsed, 'store')
|
|
728
|
+
await abTicketAssetGet({
|
|
729
|
+
...opts,
|
|
730
|
+
...(store !== undefined ? { storeRef: store } : {}),
|
|
731
|
+
id,
|
|
732
|
+
kind,
|
|
733
|
+
name,
|
|
734
|
+
dest,
|
|
735
|
+
...(rawRev !== undefined ? { rev: Number(rawRev) } : {}),
|
|
736
|
+
json: parsed.flags.has('json'),
|
|
737
|
+
})
|
|
738
|
+
return
|
|
739
|
+
}
|
|
740
|
+
if (action === 'rm') {
|
|
741
|
+
const parsed = parseArgs(assetArgs, { store: 'value', json: 'boolean' }, TICKET_USAGE)
|
|
742
|
+
const [id, kind, name, ...extra] = parsed.positionals
|
|
743
|
+
if (!id?.trim() || !kind?.trim() || !name?.trim() || extra.length > 0) {
|
|
744
|
+
throw new Error(TICKET_USAGE)
|
|
745
|
+
}
|
|
746
|
+
const store = stringFlag(parsed, 'store')
|
|
747
|
+
await abTicketAssetRm({
|
|
748
|
+
...opts,
|
|
749
|
+
...(store !== undefined ? { storeRef: store } : {}),
|
|
750
|
+
id,
|
|
751
|
+
kind,
|
|
752
|
+
name,
|
|
753
|
+
json: parsed.flags.has('json'),
|
|
754
|
+
})
|
|
755
|
+
return
|
|
756
|
+
}
|
|
757
|
+
throw new Error(TICKET_USAGE)
|
|
758
|
+
}
|
|
759
|
+
|
|
460
760
|
case 'move': {
|
|
461
761
|
const parsed = parseArgs(args, { json: 'boolean' }, TICKET_USAGE)
|
|
462
762
|
const [id, state, ...extra] = parsed.positionals
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The command shares `ab watch`'s machinery — target selection, ambient
|
|
11
11
|
* scoping (via the extracted `assertReadScope`), the cursor codec, the record
|
|
12
|
-
* projection, the
|
|
12
|
+
* projection, the cadence, and the read-failure policy — but ends on the
|
|
13
13
|
* first satisfied condition instead of streaming. Conditions are derived from
|
|
14
14
|
* the same reducer that produces `ab build status` (`status`, `prState`,
|
|
15
15
|
* `phase`), so "blocked" means an open escalation exactly as it does
|
|
@@ -17,6 +17,17 @@
|
|
|
17
17
|
* wait`, a state condition that already holds when the command starts returns
|
|
18
18
|
* immediately with the build's latest event.
|
|
19
19
|
*
|
|
20
|
+
* Cadence follows the store kind, exactly as `ab watch` does since AUT-334
|
|
21
|
+
* (verified and corrected for `ab wait` in AUT-368). Against a local store
|
|
22
|
+
* the loop is the sequential interval tick. Against an `http(s)` store every
|
|
23
|
+
* tracked build long-polls instead: one held `getEvents` request per stream
|
|
24
|
+
* is in flight (tasks run concurrently), `--interval` is the floor between
|
|
25
|
+
* request starts (elapsed request time counts toward the gap), and a hold's
|
|
26
|
+
* window is the remote default (25 s, whole seconds, under the store's
|
|
27
|
+
* ceiling) capped at the remaining `--timeout` budget. Every stop — a
|
|
28
|
+
* satisfied condition, an all-terminal named set, the timeout, an interrupt —
|
|
29
|
+
* cancels the in-flight holds promptly instead of waiting out a full window.
|
|
30
|
+
*
|
|
20
31
|
* Read-only: it appends no event, claims or renews no lease, and creates no
|
|
21
32
|
* build or repository record. Output carries no ANSI, ever, in either form.
|
|
22
33
|
*/
|
|
@@ -26,7 +37,13 @@ import type { BuildState, PrLifecycle } from '../kernel/reducer'
|
|
|
26
37
|
import { reduceBuild } from '../kernel/reducer'
|
|
27
38
|
import { isPhase, type BuildStatus, type Phase } from '../ontology'
|
|
28
39
|
import type { Exec } from '../ports/workspace/git-worktree'
|
|
29
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
createRemotePollRunner,
|
|
42
|
+
defaultDelay,
|
|
43
|
+
makeFailureStreak,
|
|
44
|
+
type RemotePollReadOpts,
|
|
45
|
+
} from './remote-poll'
|
|
46
|
+
import { buildInRepository, isRemoteStoreRef, normalizeGitRemoteUrl } from './repo-state'
|
|
30
47
|
import type { BuildRecord } from '../store/types'
|
|
31
48
|
import { withAmbientReadStore, type StoreOpener } from './store-opening'
|
|
32
49
|
import {
|
|
@@ -245,23 +262,6 @@ interface WaitStream {
|
|
|
245
262
|
status: BuildStatus
|
|
246
263
|
}
|
|
247
264
|
|
|
248
|
-
function defaultDelay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
249
|
-
return new Promise((resolve) => {
|
|
250
|
-
if (signal?.aborted === true) {
|
|
251
|
-
resolve()
|
|
252
|
-
return
|
|
253
|
-
}
|
|
254
|
-
const finish = (): void => {
|
|
255
|
-
clearTimeout(timer)
|
|
256
|
-
signal?.removeEventListener('abort', onAbort)
|
|
257
|
-
resolve()
|
|
258
|
-
}
|
|
259
|
-
const timer = setTimeout(finish, ms)
|
|
260
|
-
const onAbort = (): void => finish()
|
|
261
|
-
signal?.addEventListener('abort', onAbort, { once: true })
|
|
262
|
-
})
|
|
263
|
-
}
|
|
264
|
-
|
|
265
265
|
/**
|
|
266
266
|
* `ab wait` — block until a targeted build satisfies a condition, then print
|
|
267
267
|
* one record and exit 0; exit 2 when every named build went terminal without
|
|
@@ -386,6 +386,12 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
386
386
|
if (streams.has(slug)) return
|
|
387
387
|
const record = await store.getBuild(slug)
|
|
388
388
|
if (record === null) return
|
|
389
|
+
// AUT-545: this full-log read is deliberate, accepted growth — the
|
|
390
|
+
// retained prefix is consumed at full replay depth by every later
|
|
391
|
+
// reduction (approvals, findings, answered escalations, PR facts,
|
|
392
|
+
// observations), so bounding it would need reducer-partition store
|
|
393
|
+
// machinery out of AUT-545's scope. Required for `--since` replay and
|
|
394
|
+
// the registration state check over the full reduction.
|
|
389
395
|
const events = await store.getEvents(slug)
|
|
390
396
|
const stream = registerStream(slug, events, slugs.includes(slug))
|
|
391
397
|
if (resumeSeq !== undefined) {
|
|
@@ -419,8 +425,24 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
419
425
|
* build to appear.
|
|
420
426
|
*/
|
|
421
427
|
const discoverBuilds = async (): Promise<void> => {
|
|
428
|
+
// AUT-545: the terminal filter is the AUT-487 digest, not a full-log read —
|
|
429
|
+
// one flat batch read replaces a full getEvents per candidate build, per
|
|
430
|
+
// discovery pass; `terminal` is reduceBuild's exact terminal rule
|
|
431
|
+
// (store/digest.ts, pinned by contract). The per-build full read below
|
|
432
|
+
// remains deliberate for builds the digest cannot retire: its retained
|
|
433
|
+
// prefix is consumed at full replay depth by every later reduction
|
|
434
|
+
// (approvals, findings, answered escalations, PR facts, observations), so
|
|
435
|
+
// bounding it would need reducer-partition store machinery out of
|
|
436
|
+
// AUT-545's scope. The reduceBuild re-check stays: it is the
|
|
437
|
+
// legacy-record fallback (a record whose `repo` predates origin
|
|
438
|
+
// normalization is in `mine` via `repoOrigin` but absent from the digest
|
|
439
|
+
// map, which keys on `record.repo`) and it keeps this filter unable to
|
|
440
|
+
// diverge from reduceBuild.
|
|
441
|
+
const digests = await store.getRepoBuildDigests(repo)
|
|
422
442
|
for (const record of (await store.listBuilds()).filter(mine)) {
|
|
423
443
|
if (streams.has(record.slug)) continue
|
|
444
|
+
const digest = digests.get(record.slug)
|
|
445
|
+
if (digest !== undefined && digest.terminal !== undefined) continue
|
|
424
446
|
const events = await store.getEvents(record.slug)
|
|
425
447
|
const status = reduceBuild(events).status
|
|
426
448
|
if (!NONTERMINAL_STATUSES.includes(status)) continue
|
|
@@ -429,8 +451,11 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
429
451
|
}
|
|
430
452
|
}
|
|
431
453
|
|
|
432
|
-
const pollBuild = async (
|
|
433
|
-
|
|
454
|
+
const pollBuild = async (
|
|
455
|
+
stream: WaitStream,
|
|
456
|
+
readOpts?: RemotePollReadOpts,
|
|
457
|
+
): Promise<void> => {
|
|
458
|
+
const fresh = await store.getEvents(stream.slug, stream.lastSeq, readOpts)
|
|
434
459
|
for (const event of fresh) {
|
|
435
460
|
stream.events.push(event)
|
|
436
461
|
stream.lastSeq = event.seq
|
|
@@ -482,13 +507,7 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
482
507
|
/** One poll cycle: discovery, then per-stream reads. A failed read is
|
|
483
508
|
* reported once per failure streak, advances nothing, and is retried at
|
|
484
509
|
* the next interval; a fully successful cycle re-arms the report. */
|
|
485
|
-
|
|
486
|
-
const reportReadFailure = (error: unknown): void => {
|
|
487
|
-
if (readFailureReported) return
|
|
488
|
-
readFailureReported = true
|
|
489
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
490
|
-
opts.stderr(`ab wait: a store read failed (${message}); retrying at the next interval`)
|
|
491
|
-
}
|
|
510
|
+
const tickStreak = makeFailureStreak('wait', opts.stderr)
|
|
492
511
|
|
|
493
512
|
const tick = async (): Promise<void> => {
|
|
494
513
|
let allReadsOk = true
|
|
@@ -497,7 +516,7 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
497
516
|
await discoverBuilds()
|
|
498
517
|
} catch (error) {
|
|
499
518
|
allReadsOk = false
|
|
500
|
-
|
|
519
|
+
tickStreak.onFailure(error)
|
|
501
520
|
}
|
|
502
521
|
}
|
|
503
522
|
for (const stream of [...streams.values()]) {
|
|
@@ -506,10 +525,10 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
506
525
|
await pollBuild(stream)
|
|
507
526
|
} catch (error) {
|
|
508
527
|
allReadsOk = false
|
|
509
|
-
|
|
528
|
+
tickStreak.onFailure(error)
|
|
510
529
|
}
|
|
511
530
|
}
|
|
512
|
-
if (allReadsOk)
|
|
531
|
+
if (allReadsOk) tickStreak.onSuccess()
|
|
513
532
|
}
|
|
514
533
|
|
|
515
534
|
const finishSatisfied = (): number =>
|
|
@@ -561,17 +580,106 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
561
580
|
const timeoutText = opts.timeout ?? '30m'
|
|
562
581
|
const deadline = timeoutMs === 0 ? Number.POSITIVE_INFINITY : now().getTime() + timeoutMs
|
|
563
582
|
const aborted = (): boolean => opts.signal?.aborted === true
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
583
|
+
|
|
584
|
+
if (!isRemoteStoreRef(storeRef)) {
|
|
585
|
+
// Local cadence: one sequential tick per interval.
|
|
586
|
+
while (!hasMatch()) {
|
|
587
|
+
if (aborted() || now().getTime() >= deadline) break
|
|
588
|
+
await sleep(interval, opts.signal)
|
|
589
|
+
if (aborted()) break
|
|
590
|
+
await tick()
|
|
591
|
+
if (!hasMatch() && namedAllTerminal()) {
|
|
592
|
+
const terminal = pickTerminalRecord()
|
|
593
|
+
return finish(null, terminal.event, terminal.state)
|
|
594
|
+
}
|
|
572
595
|
}
|
|
596
|
+
} else {
|
|
597
|
+
// Remote cadence (AUT-334, verified and corrected in AUT-368): one
|
|
598
|
+
// long-poll task per tracked stream, running concurrently — a quiet
|
|
599
|
+
// stream keeps one held `getEvents` request in flight, so an appended
|
|
600
|
+
// event is seen within about a second of its append instead of at the
|
|
601
|
+
// next interval tick. Gap-fill spaces each stream's request starts at
|
|
602
|
+
// least `interval` apart (elapsed request time counts toward the
|
|
603
|
+
// gap), so a server that answers immediately still sees today's
|
|
604
|
+
// request rate. Discovery keeps its interval cadence. The pre-launch
|
|
605
|
+
// guard mirrors the local loop's top-of-cycle checks: a satisfied
|
|
606
|
+
// match, an abort, an elapsed deadline, or an all-terminal named set
|
|
607
|
+
// ends the wait before any held request (an already-satisfied wait
|
|
608
|
+
// issues zero held reads).
|
|
609
|
+
// Every stop is prompt: a satisfied condition, an all-terminal named
|
|
610
|
+
// set, an external abort, or an elapsed --timeout cancels the
|
|
611
|
+
// in-flight held reads (via `requestStop`) and wakes the gap sleeps
|
|
612
|
+
// (via the runner's stop controller) instead of waiting out a full
|
|
613
|
+
// hold. The held read's own bound is additionally capped at the
|
|
614
|
+
// wait's remaining time budget, so a hold can never outlive the
|
|
615
|
+
// deadline.
|
|
616
|
+
const shouldStop =
|
|
617
|
+
hasMatch() || aborted() || now().getTime() >= deadline || namedAllTerminal()
|
|
618
|
+
const remoteRunner = createRemotePollRunner({
|
|
619
|
+
command: 'wait',
|
|
620
|
+
stderr: opts.stderr,
|
|
621
|
+
now,
|
|
622
|
+
sleep,
|
|
623
|
+
intervalMs: interval,
|
|
624
|
+
deadlineMs: deadline,
|
|
625
|
+
aborted,
|
|
626
|
+
shouldStop: () =>
|
|
627
|
+
hasMatch() || aborted() || now().getTime() >= deadline || namedAllTerminal(),
|
|
628
|
+
drain: 'quiesce',
|
|
629
|
+
})
|
|
630
|
+
const requestStop = (): void => remoteRunner.requestStop()
|
|
631
|
+
|
|
632
|
+
const launch = (stream: WaitStream): void => {
|
|
633
|
+
remoteRunner.launch(
|
|
634
|
+
stream.slug,
|
|
635
|
+
(readOpts) => pollBuild(stream, readOpts),
|
|
636
|
+
() => hasMatch() || namedAllTerminal(),
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// An external abort (SIGINT) is a stop like any other: it cancels
|
|
641
|
+
// the in-flight held reads and wakes the gap sleeps.
|
|
642
|
+
opts.signal?.addEventListener('abort', requestStop, { once: true })
|
|
643
|
+
|
|
644
|
+
if (!shouldStop) {
|
|
645
|
+
for (const stream of streams.values()) launch(stream)
|
|
646
|
+
|
|
647
|
+
if (slugs.length === 0) {
|
|
648
|
+
// Discovery keeps its interval cadence, launching a new per-
|
|
649
|
+
// stream task for each newly discovered nonterminal build. A
|
|
650
|
+
// discovered build that already satisfies ends the wait. The
|
|
651
|
+
// launch loop stays unconditional, after the catch — exactly
|
|
652
|
+
// where the inline loop ran: streams registered before a
|
|
653
|
+
// mid-discovery throw still get tasks whose polls then run
|
|
654
|
+
// independently.
|
|
655
|
+
void remoteRunner.runDiscovery({
|
|
656
|
+
step: async (): Promise<void> => {
|
|
657
|
+
await discoverBuilds()
|
|
658
|
+
},
|
|
659
|
+
launchPending: (): void => {
|
|
660
|
+
for (const stream of streams.values()) launch(stream)
|
|
661
|
+
},
|
|
662
|
+
endCheck: () => hasMatch(),
|
|
663
|
+
})
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
await remoteRunner.drain()
|
|
668
|
+
opts.signal?.removeEventListener('abort', requestStop)
|
|
573
669
|
}
|
|
670
|
+
|
|
574
671
|
if (hasMatch()) return finishSatisfied()
|
|
672
|
+
if (namedAllTerminal()) {
|
|
673
|
+
// Decision 5, loop form: every named build went terminal without
|
|
674
|
+
// satisfying anything. The local loop checks this after each tick;
|
|
675
|
+
// on the remote path the runner records only that a stop was
|
|
676
|
+
// requested — its `shouldStop`/`endCheck` callbacks re-derive the
|
|
677
|
+
// conditions live — so after the quiesce drain this decision re-reads
|
|
678
|
+
// the same live predicates (`hasMatch`, `namedAllTerminal`, `aborted`,
|
|
679
|
+
// the deadline) to pick the exit.
|
|
680
|
+
const terminal = pickTerminalRecord()
|
|
681
|
+
return finish(null, terminal.event, terminal.state)
|
|
682
|
+
}
|
|
575
683
|
if (aborted()) {
|
|
576
684
|
opts.stderr('ab wait: interrupted — no condition was satisfied before the signal')
|
|
577
685
|
if (json) opts.stdout(JSON.stringify({ cursor: encodeCurrent() }))
|