@defrex/autobuild 0.8.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/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,1399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ab` — the only channel between agents and the store, and the enforcement
|
|
3
|
+
* point of the entire ontology (SPEC §8). This module is pure routing: every
|
|
4
|
+
* dependency is injected via CliDeps, so the whole surface is testable over
|
|
5
|
+
* fakes; wiring real deps (resolved env, resolved store, real forge, real
|
|
6
|
+
* exec, random ids) is `src/cli/binary.ts`'s job.
|
|
7
|
+
*
|
|
8
|
+
* Errors go to stderr with exit code 1, formatted as agent feedback (D6):
|
|
9
|
+
* what was wrong and what would be accepted, so the in-session correction
|
|
10
|
+
* loop is immediate and cheap.
|
|
11
|
+
*/
|
|
12
|
+
import { readFile } from 'node:fs/promises'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import type { TicketRef } from '../ontology'
|
|
15
|
+
import type { Clock } from '../store/types'
|
|
16
|
+
import type { BuildStore } from '../store/types'
|
|
17
|
+
import { textContent } from '../store/types'
|
|
18
|
+
import type { Forge } from '../ports/types'
|
|
19
|
+
import type { Exec } from '../ports/workspace/git-worktree'
|
|
20
|
+
import type { IdSource } from '../ids'
|
|
21
|
+
import { reduceBuild } from '../kernel/reducer'
|
|
22
|
+
import { artifactDownload, artifactGet, artifactPut } from './artifact'
|
|
23
|
+
import { multiFlag, parseArgs, stringFlag, type ParsedArgs } from './args'
|
|
24
|
+
import { abBuildControl, type BuildControlAction, type BuildControlResult } from './build-control'
|
|
25
|
+
import {
|
|
26
|
+
abBulkControl,
|
|
27
|
+
bulkControlLines,
|
|
28
|
+
bulkFailureLines,
|
|
29
|
+
BulkWalkError,
|
|
30
|
+
type BulkDirection,
|
|
31
|
+
} from './bulk-control'
|
|
32
|
+
import { buildContext, type ContextManifest } from './context'
|
|
33
|
+
import { abDispatch } from './dispatch'
|
|
34
|
+
import type { DashboardRendererResolver } from './dashboard/render'
|
|
35
|
+
import type { TerminalInput, TerminalOut } from './terminal'
|
|
36
|
+
import type { CliEnv, HarvestCliEnv } from './env'
|
|
37
|
+
import { abInit } from './init'
|
|
38
|
+
import { validateInitReadiness } from './init-validation'
|
|
39
|
+
import { loadDotEnv } from './dotenv'
|
|
40
|
+
import type { RuntimeRegistry } from '../ports/runner/runtime'
|
|
41
|
+
import type { SetupAgentLauncher } from './init-agent'
|
|
42
|
+
import { abModels } from './models'
|
|
43
|
+
import { recognizeHelpRequest, renderCommandHelp, renderTopLevelHelp } from './help'
|
|
44
|
+
import { observe } from './observe'
|
|
45
|
+
import { abPlugin, type PluginContractSubprocess } from './plugin'
|
|
46
|
+
import { preparePrAttachments } from './pr-attachments'
|
|
47
|
+
import { renderPrSummary } from './pr-summary'
|
|
48
|
+
import { abBuilds, abBuildStatus } from './status'
|
|
49
|
+
import { abRepositoryStatus } from './repository-status'
|
|
50
|
+
import { abWatch, WATCH_USAGE } from './watch'
|
|
51
|
+
import { abWait, WAIT_USAGE } from './wait'
|
|
52
|
+
import { abMcp, AB_MCP_USAGE } from './mcp'
|
|
53
|
+
import type { StoreOpener } from './store-opening'
|
|
54
|
+
import { done, escalate, verdict } from './terminals'
|
|
55
|
+
import { abTicket, openTicketSource } from './ticket'
|
|
56
|
+
import { abUpgrade, type ResolveConflict } from './upgrade'
|
|
57
|
+
import { withUpgradeProgress } from './upgrade-progress'
|
|
58
|
+
import {
|
|
59
|
+
cleanupUpgradeCommitContext,
|
|
60
|
+
createUpgradeCommitContext,
|
|
61
|
+
finishUpgradeCommit,
|
|
62
|
+
loadUpgradeCommitContext,
|
|
63
|
+
UPGRADE_COMMIT_CONTEXT_ENV,
|
|
64
|
+
type UpgradeCommitContext,
|
|
65
|
+
} from './upgrade-commit'
|
|
66
|
+
import { formatInstalledVersion, readDistributionIdentity } from './installation'
|
|
67
|
+
import {
|
|
68
|
+
selfUpdate,
|
|
69
|
+
SELF_UPDATE_HANDOFF_ENV,
|
|
70
|
+
type SelfUpdateCommand,
|
|
71
|
+
type SelfUpdateOptions,
|
|
72
|
+
type SelfUpdateResult,
|
|
73
|
+
} from './self-update'
|
|
74
|
+
import {
|
|
75
|
+
abHarvestStatus,
|
|
76
|
+
buildHarvestContext,
|
|
77
|
+
submitHarvestProposals,
|
|
78
|
+
submitHarvestVerdict,
|
|
79
|
+
type HarvestCliDeps,
|
|
80
|
+
} from './harvest'
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Commands that run OUTSIDE phase sessions (§16.3, §8.8, §3.3). Repository
|
|
84
|
+
* commands resolve the cwd; durable controls additionally take a target build
|
|
85
|
+
* slug. None requires the ambient AB_* phase tuple.
|
|
86
|
+
*
|
|
87
|
+
* `src/cli/binary.ts` routes through `isSessionlessInvocation` below. This set
|
|
88
|
+
* owns flat command names; that helper additionally owns the few mixed nested
|
|
89
|
+
* namespaces (`artifact download`, `harvest status`). The `builds`, `build
|
|
90
|
+
* status`, and `artifact download` shells remain on this operator-sessionless
|
|
91
|
+
* route but use their dedicated Store opener to honor a complete ambient phase
|
|
92
|
+
* identity and reject malformed identity. Everything else first attempts
|
|
93
|
+
* strict ambient resolution in `binary.ts`. Keeping classification beside the
|
|
94
|
+
* switch makes it unit-testable.
|
|
95
|
+
*/
|
|
96
|
+
export const SESSIONLESS_COMMANDS = new Set([
|
|
97
|
+
'init',
|
|
98
|
+
'upgrade',
|
|
99
|
+
'update',
|
|
100
|
+
'ticket',
|
|
101
|
+
'dispatch',
|
|
102
|
+
'builds',
|
|
103
|
+
'build',
|
|
104
|
+
'watch',
|
|
105
|
+
'wait',
|
|
106
|
+
'mcp',
|
|
107
|
+
'repository',
|
|
108
|
+
'pause',
|
|
109
|
+
'resume',
|
|
110
|
+
'auto-merge',
|
|
111
|
+
'answer',
|
|
112
|
+
'abort',
|
|
113
|
+
'models',
|
|
114
|
+
'plugin',
|
|
115
|
+
'help',
|
|
116
|
+
'--help',
|
|
117
|
+
'-h',
|
|
118
|
+
'--version',
|
|
119
|
+
])
|
|
120
|
+
|
|
121
|
+
/** Nested namespaces can mix operator and phase forms. `artifact download`
|
|
122
|
+
* uses operator-sessionless routing plus ambient-aware read authority; put/get
|
|
123
|
+
* remain phase-only. */
|
|
124
|
+
export function isSessionlessInvocation(argv: readonly string[]): boolean {
|
|
125
|
+
const command = argv[0]
|
|
126
|
+
return (
|
|
127
|
+
command === undefined ||
|
|
128
|
+
recognizeHelpRequest(argv) !== undefined ||
|
|
129
|
+
SESSIONLESS_COMMANDS.has(command) ||
|
|
130
|
+
(command === 'artifact' && argv[1] === 'download') ||
|
|
131
|
+
(command === 'harvest' && argv[1] === 'status')
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface CliDeps {
|
|
136
|
+
store: BuildStore
|
|
137
|
+
/** Resolved ambient auth (D8) — see resolveCliEnv (src/cli/env.ts). */
|
|
138
|
+
env: CliEnv
|
|
139
|
+
workspacePath: string
|
|
140
|
+
forge: Forge
|
|
141
|
+
exec: Exec
|
|
142
|
+
ids: IdSource
|
|
143
|
+
clock: Clock
|
|
144
|
+
stdout: (line: string) => void
|
|
145
|
+
stderr: (line: string) => void
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* What `runCli` minimally needs. `ab init` and `ab upgrade` run OUTSIDE
|
|
150
|
+
* build sessions (§16.3) — they take a repo path, not a build, and must
|
|
151
|
+
* work with no AB_* environment — so the session surface is optional here.
|
|
152
|
+
* The binary also uses this narrow shape when ambient phase context is absent,
|
|
153
|
+
* allowing requireSession/requireHarvestSession to return namespace-aware
|
|
154
|
+
* guidance without constructing a store. Full CliDeps satisfies this
|
|
155
|
+
* structurally, so wired-up callers are unchanged.
|
|
156
|
+
*/
|
|
157
|
+
export interface SessionlessCliDeps {
|
|
158
|
+
/** For init/upgrade this is just the cwd — the default target repo. */
|
|
159
|
+
workspacePath: string
|
|
160
|
+
stdout: (line: string) => void
|
|
161
|
+
stderr: (line: string) => void
|
|
162
|
+
/** Raw process environment for sessionless store selection, operator
|
|
163
|
+
* provenance/self-control checks, and adapter secrets; distinct from `env`,
|
|
164
|
+
* the resolved phase-session tuple. */
|
|
165
|
+
processEnv?: Record<string, string | undefined>
|
|
166
|
+
/** Stop signal for long-running sessionless commands (`ab dispatch`'s watch
|
|
167
|
+
* loop); the binary aborts it on SIGINT. */
|
|
168
|
+
signal?: AbortSignal
|
|
169
|
+
/** Interactive output seam for live dispatch and upgrade rendering. Absent ⇒
|
|
170
|
+
* non-interactive ⇒ plain, line-oriented output. */
|
|
171
|
+
terminal?: TerminalOut
|
|
172
|
+
/** Raw keyboard seam for interactive dispatch and upgrade controls. */
|
|
173
|
+
input?: TerminalInput
|
|
174
|
+
/** True only when both stdin and stdout are interactive terminals. */
|
|
175
|
+
initInteractive?: boolean
|
|
176
|
+
/** Injectable direct interactive child-process seam for init tests. */
|
|
177
|
+
initLauncher?: SetupAgentLauncher
|
|
178
|
+
/** Injectable init detection registry; production uses shipped registrations. */
|
|
179
|
+
initRuntimes?: RuntimeRegistry
|
|
180
|
+
/** Complete readiness seam for CLI routing tests. */
|
|
181
|
+
initValidation?: typeof validateInitReadiness
|
|
182
|
+
/** Optional per-paint presentation lookup used only by the repo-local dev
|
|
183
|
+
* entry. The published binary never supplies it. */
|
|
184
|
+
resolveDashboardRenderer?: DashboardRendererResolver
|
|
185
|
+
/** Distribution root overrides are test-only; production resolves relative
|
|
186
|
+
* to the running module, independent of the current repository. */
|
|
187
|
+
distributionRoot?: string
|
|
188
|
+
/** Self-update seams keep release/package-manager orchestration testable. */
|
|
189
|
+
selfUpdateCommand?: SelfUpdateCommand
|
|
190
|
+
selfUpdate?: (options: SelfUpdateOptions) => Promise<SelfUpdateResult>
|
|
191
|
+
/** Production supplies the tool-free upgrade agent; tests inject a fake.
|
|
192
|
+
* Construction is deferred until a skill actually conflicts. */
|
|
193
|
+
upgradeResolverFactory?: (opts: {
|
|
194
|
+
targetRepo: string
|
|
195
|
+
env: Record<string, string | undefined>
|
|
196
|
+
}) => ResolveConflict
|
|
197
|
+
/** Injectable child-process seam for plugin contract CLI tests. */
|
|
198
|
+
pluginSubprocess?: PluginContractSubprocess
|
|
199
|
+
/** Injectable store-adapter seam for operator-sessionless commands, including
|
|
200
|
+
* ambient-authority-aware reads; production composes the real adapter. */
|
|
201
|
+
openStore?: StoreOpener
|
|
202
|
+
store?: BuildStore
|
|
203
|
+
env?: CliEnv
|
|
204
|
+
harvestEnv?: HarvestCliEnv
|
|
205
|
+
forge?: Forge
|
|
206
|
+
exec?: Exec
|
|
207
|
+
ids?: IdSource
|
|
208
|
+
clock?: Clock
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Narrow to full session deps, or fail with agent feedback (D6). */
|
|
212
|
+
function requireHarvestSession(command: string, deps: SessionlessCliDeps): HarvestCliDeps {
|
|
213
|
+
const { store, harvestEnv: env, ids } = deps
|
|
214
|
+
if (store === undefined || env === undefined || ids === undefined) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`'ab harvest ${command}' runs inside a harvest agent session — the ` +
|
|
217
|
+
'runner sets AB_STORE, AB_REPO, AB_HARVEST, AB_PHASE, and AB_SESSION.',
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
return { store, env, ids, workspacePath: deps.workspacePath }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function requireSession(command: string, deps: SessionlessCliDeps): CliDeps {
|
|
224
|
+
const { store, env, forge, exec, ids, clock } = deps
|
|
225
|
+
if (
|
|
226
|
+
store === undefined ||
|
|
227
|
+
env === undefined ||
|
|
228
|
+
forge === undefined ||
|
|
229
|
+
exec === undefined ||
|
|
230
|
+
ids === undefined ||
|
|
231
|
+
clock === undefined
|
|
232
|
+
) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`'ab ${command}' runs inside a build session — the runner sets AB_STORE, ` +
|
|
235
|
+
'AB_BUILD, AB_PHASE, AB_SESSION for every session (SPEC §8.1, D8). ' +
|
|
236
|
+
'Use `ab help` for commands that run sessionless.',
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
return { ...deps, store, env, forge, exec, ids, clock }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function buildControlConfirmation(result: BuildControlResult): string {
|
|
243
|
+
switch (result.kind) {
|
|
244
|
+
case 'command': {
|
|
245
|
+
const outcome: Record<typeof result.command, string> = {
|
|
246
|
+
pause: 'pause requested',
|
|
247
|
+
resume: 'resume requested',
|
|
248
|
+
abort: 'abort requested',
|
|
249
|
+
discard: 'discard requested',
|
|
250
|
+
'auto-merge-on': 'auto-merge requested',
|
|
251
|
+
'auto-merge-off': 'auto-merge cancelled',
|
|
252
|
+
}
|
|
253
|
+
return `build ${result.slug}: ${outcome[result.command]}`
|
|
254
|
+
}
|
|
255
|
+
case 'answered': {
|
|
256
|
+
const plural = result.count === 1 ? '' : 's'
|
|
257
|
+
const terminal = result.terminalSignal
|
|
258
|
+
if (terminal !== undefined) {
|
|
259
|
+
const ending =
|
|
260
|
+
terminal.kind === 'terminal-status'
|
|
261
|
+
? `the build ${terminal.status === 'done' ? 'completed' : 'aborted'} while the answer was recorded${
|
|
262
|
+
terminal.outcome !== undefined ? ` (${terminal.outcome})` : ''
|
|
263
|
+
}; it will not restart`
|
|
264
|
+
: terminal.kind === 'abort-requested'
|
|
265
|
+
? 'abort was requested during the action; the build will be aborted and will not restart'
|
|
266
|
+
: `the PR ${terminal.state}; build completion is underway and it will not restart`
|
|
267
|
+
return `build ${result.slug}: answered ${result.count} escalation${plural} with ${result.resolution}; ${ending}`
|
|
268
|
+
}
|
|
269
|
+
if (result.reviewRoundCeiling !== undefined) {
|
|
270
|
+
const ceiling = result.reviewRoundCeiling
|
|
271
|
+
return `build ${result.slug}: answered ${result.count} open escalation${plural} with ${result.resolution}; ${ceiling.loop} review round ceiling set to ${ceiling.value}${
|
|
272
|
+
result.resumed ? '; resume requested' : ''
|
|
273
|
+
}`
|
|
274
|
+
}
|
|
275
|
+
if (result.resolution === 'revise-spec') {
|
|
276
|
+
const revision = result.specRev === undefined ? '' : ` (spec rev ${result.specRev})`
|
|
277
|
+
if (result.authorizedEarlier === true) {
|
|
278
|
+
return `build ${result.slug}: completed the spec revision recorded earlier${revision}; the body supplied now was not used${
|
|
279
|
+
result.resumed ? '; resume requested' : ''
|
|
280
|
+
}`
|
|
281
|
+
}
|
|
282
|
+
return `build ${result.slug}: answered ${result.count} escalation${plural} with revise-spec${revision}; restarting from plan${
|
|
283
|
+
result.resumed ? '; resume requested' : ''
|
|
284
|
+
}`
|
|
285
|
+
}
|
|
286
|
+
if (result.resolution === 'dismiss-finding' && (result.remainingOpen ?? 0) > 0) {
|
|
287
|
+
const remaining = result.remainingOpen ?? 0
|
|
288
|
+
return `build ${result.slug}: dismissed ${result.count} escalation${plural}; ${remaining} blocker${
|
|
289
|
+
remaining === 1 ? '' : 's'
|
|
290
|
+
} remains (cites no review findings) — answer with guidance or an empty answer to retry`
|
|
291
|
+
}
|
|
292
|
+
return `build ${result.slug}: answered ${result.count} open escalation${plural} with ${result.resolution}${
|
|
293
|
+
result.resumed ? '; resume requested' : ''
|
|
294
|
+
}`
|
|
295
|
+
}
|
|
296
|
+
case 'answer-required':
|
|
297
|
+
throw new Error('build-control requested interactive feedback for an explicit CLI command')
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function runBuildControl(
|
|
302
|
+
deps: SessionlessCliDeps,
|
|
303
|
+
slug: string,
|
|
304
|
+
action: BuildControlAction,
|
|
305
|
+
storeRef?: string,
|
|
306
|
+
readTicketBody?: (ref: TicketRef) => Promise<string>,
|
|
307
|
+
): Promise<void> {
|
|
308
|
+
if (deps.exec === undefined) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
'build-control commands need an exec seam — this is a wiring bug in the ab binary',
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
const result = await abBuildControl({
|
|
314
|
+
targetRepo: deps.workspacePath,
|
|
315
|
+
env: deps.processEnv ?? {},
|
|
316
|
+
exec: deps.exec,
|
|
317
|
+
slug,
|
|
318
|
+
action,
|
|
319
|
+
...(storeRef !== undefined ? { storeRef } : {}),
|
|
320
|
+
...(readTicketBody !== undefined ? { readTicketBody } : {}),
|
|
321
|
+
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
322
|
+
})
|
|
323
|
+
deps.stdout(buildControlConfirmation(result))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The repository-wide form. Success renders the walk's summary; a partial walk
|
|
328
|
+
* is the one failure this route owns rather than delegates — it reports what
|
|
329
|
+
* did complete on stderr and exits 1, and nothing reaches stdout. Every other
|
|
330
|
+
* error propagates to `runCli`'s stderr+1 handler.
|
|
331
|
+
*/
|
|
332
|
+
async function runBulkControl(
|
|
333
|
+
deps: SessionlessCliDeps,
|
|
334
|
+
direction: BulkDirection,
|
|
335
|
+
opts: { json: boolean; storeRef?: string },
|
|
336
|
+
): Promise<number> {
|
|
337
|
+
if (deps.exec === undefined) {
|
|
338
|
+
throw new Error(
|
|
339
|
+
'build-control commands need an exec seam — this is a wiring bug in the ab binary',
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const summary = await abBulkControl({
|
|
344
|
+
targetRepo: deps.workspacePath,
|
|
345
|
+
env: deps.processEnv ?? {},
|
|
346
|
+
exec: deps.exec,
|
|
347
|
+
direction,
|
|
348
|
+
...(opts.storeRef !== undefined ? { storeRef: opts.storeRef } : {}),
|
|
349
|
+
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
350
|
+
})
|
|
351
|
+
if (opts.json) deps.stdout(JSON.stringify(summary, null, 2))
|
|
352
|
+
else for (const line of bulkControlLines(summary)) deps.stdout(line)
|
|
353
|
+
return 0
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (!(error instanceof BulkWalkError)) throw error
|
|
356
|
+
if (opts.json) deps.stderr(JSON.stringify({ ...error.progress, error: error.message }, null, 2))
|
|
357
|
+
else for (const line of bulkFailureLines(error)) deps.stderr(line)
|
|
358
|
+
return 1
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function listFlag(parsed: ParsedArgs, name: string): string[] | undefined {
|
|
363
|
+
const value = stringFlag(parsed, name)
|
|
364
|
+
if (value === undefined) return undefined
|
|
365
|
+
return value
|
|
366
|
+
.split(',')
|
|
367
|
+
.map((entry) => entry.trim())
|
|
368
|
+
.filter((entry) => entry !== '')
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function renderManifest(manifest: ContextManifest): string[] {
|
|
372
|
+
const lines = [
|
|
373
|
+
`context materialized for ${manifest.build} — ${manifest.phase}@${manifest.round}`,
|
|
374
|
+
`required deposits: ${manifest.required.join(', ') || '(none)'}`,
|
|
375
|
+
`allowed terminals: ${manifest.allowedTerminals.join(', ')}`,
|
|
376
|
+
`manifest: ${manifest.manifestPath}`,
|
|
377
|
+
]
|
|
378
|
+
if (manifest.notesPath !== undefined) lines.push(`notes path: ${manifest.notesPath}`)
|
|
379
|
+
if (manifest.commitRange !== undefined) {
|
|
380
|
+
lines.push(`commit range: ${manifest.commitRange.base}..${manifest.commitRange.head}`)
|
|
381
|
+
}
|
|
382
|
+
if (manifest.conflict !== undefined) {
|
|
383
|
+
lines.push(`conflict baseSha: ${manifest.conflict.baseSha}`)
|
|
384
|
+
}
|
|
385
|
+
if (manifest.step !== undefined) {
|
|
386
|
+
lines.push(`verify step: ${manifest.step.name} (kind ${manifest.step.config.kind})`)
|
|
387
|
+
}
|
|
388
|
+
lines.push('.ab/ files:')
|
|
389
|
+
const entries = Object.entries(manifest.materialized)
|
|
390
|
+
if (entries.length === 0) {
|
|
391
|
+
lines.push(' (none)')
|
|
392
|
+
}
|
|
393
|
+
for (const [relPath, source] of entries) {
|
|
394
|
+
lines.push(
|
|
395
|
+
` ${relPath} — ${source === 'derived' ? 'derived' : `${source.kind}@${source.rev}`}`,
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
return lines
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export async function runCli(argv: string[], deps: SessionlessCliDeps): Promise<number> {
|
|
402
|
+
try {
|
|
403
|
+
return await dispatch(argv, deps)
|
|
404
|
+
} catch (error) {
|
|
405
|
+
deps.stderr(error instanceof Error ? error.message : String(error))
|
|
406
|
+
return 1
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<number> {
|
|
411
|
+
const [command, ...rest] = argv
|
|
412
|
+
const { stdout, stderr } = deps
|
|
413
|
+
const helpRequest = recognizeHelpRequest(argv)
|
|
414
|
+
|
|
415
|
+
// Help is resolved before every command handler. This keeps both detailed
|
|
416
|
+
// forms byte-identical and prevents config, store, plugin, or session wiring
|
|
417
|
+
// from becoming a prerequisite for documentation.
|
|
418
|
+
if (helpRequest?.kind === 'overview') {
|
|
419
|
+
stdout(renderTopLevelHelp())
|
|
420
|
+
return 0
|
|
421
|
+
}
|
|
422
|
+
if (helpRequest?.kind === 'command') {
|
|
423
|
+
stdout(renderCommandHelp(helpRequest.command))
|
|
424
|
+
return 0
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
switch (command) {
|
|
428
|
+
case undefined: {
|
|
429
|
+
stderr(renderTopLevelHelp())
|
|
430
|
+
return 1
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
case 'help':
|
|
434
|
+
case '--help':
|
|
435
|
+
case '-h': {
|
|
436
|
+
throw new Error('usage: ab help [command]\n ab <command> --help')
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
case '--version': {
|
|
440
|
+
if (rest.length > 0) throw new Error('usage: ab --version')
|
|
441
|
+
stdout(formatInstalledVersion(await readDistributionIdentity(deps.distributionRoot)))
|
|
442
|
+
return 0
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// init and upgrade run OUTSIDE build sessions (§16.3): they operate on a
|
|
446
|
+
// repo, not a build, so they route before any store/env requirement.
|
|
447
|
+
case 'init': {
|
|
448
|
+
const usage = 'usage: ab init [target] [--force | --validate] (§16.3)'
|
|
449
|
+
const removed = [
|
|
450
|
+
'--forge',
|
|
451
|
+
'--ticket-source',
|
|
452
|
+
'--workspace-provider',
|
|
453
|
+
'--role-profile',
|
|
454
|
+
'--no-interactive',
|
|
455
|
+
].find((flag) => rest.some((arg) => arg === flag || arg.startsWith(`${flag}=`)))
|
|
456
|
+
if (removed !== undefined) {
|
|
457
|
+
throw new Error(
|
|
458
|
+
`${removed} was removed — ab init now installs a neutral skeleton and hands setup to a coding agent. ${usage}`,
|
|
459
|
+
)
|
|
460
|
+
}
|
|
461
|
+
const parsed = parseArgs(rest, { force: 'boolean', validate: 'boolean' }, usage)
|
|
462
|
+
if (parsed.positionals.length > 1) throw new Error(usage)
|
|
463
|
+
if (parsed.flags.has('force') && parsed.flags.has('validate')) {
|
|
464
|
+
throw new Error(`--force and --validate cannot be combined — ${usage}`)
|
|
465
|
+
}
|
|
466
|
+
const targetRepo = parsed.positionals[0] ?? deps.workspacePath
|
|
467
|
+
if (parsed.flags.has('validate')) {
|
|
468
|
+
const env = { ...(deps.processEnv ?? process.env) }
|
|
469
|
+
loadDotEnv(join(targetRepo, '.env'), env)
|
|
470
|
+
if (
|
|
471
|
+
env.VERCEL_OIDC_TOKEN !== undefined &&
|
|
472
|
+
(deps.processEnv ?? process.env).VERCEL_OIDC_TOKEN === undefined
|
|
473
|
+
) {
|
|
474
|
+
throw new Error(
|
|
475
|
+
'VERCEL_OIDC_TOKEN loaded only from the target .env is unavailable to the Vercel SDK; export it in the launcher environment or configure VERCEL_TOKEN, VERCEL_TEAM_ID, and VERCEL_PROJECT_ID',
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
const report = await (deps.initValidation ?? validateInitReadiness)({
|
|
479
|
+
targetRepo,
|
|
480
|
+
env,
|
|
481
|
+
stdout,
|
|
482
|
+
...(deps.exec !== undefined ? { exec: deps.exec } : {}),
|
|
483
|
+
...(deps.initRuntimes !== undefined ? { runtimes: deps.initRuntimes } : {}),
|
|
484
|
+
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
485
|
+
...(deps.signal !== undefined ? { signal: deps.signal } : {}),
|
|
486
|
+
})
|
|
487
|
+
return report.exitCode
|
|
488
|
+
}
|
|
489
|
+
const report = await abInit({
|
|
490
|
+
targetRepo,
|
|
491
|
+
force: parsed.flags.has('force'),
|
|
492
|
+
stdout,
|
|
493
|
+
interactive: deps.initInteractive === true,
|
|
494
|
+
...(deps.initLauncher === undefined ? {} : { launcher: deps.initLauncher }),
|
|
495
|
+
...(deps.initRuntimes !== undefined ? { runtimes: deps.initRuntimes } : {}),
|
|
496
|
+
env: deps.processEnv ?? process.env,
|
|
497
|
+
...(deps.signal === undefined ? {} : { signal: deps.signal }),
|
|
498
|
+
})
|
|
499
|
+
return report.exitCode
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
case 'upgrade':
|
|
503
|
+
case 'update': {
|
|
504
|
+
const usage =
|
|
505
|
+
'usage: ab upgrade [target] [--no-self-update | --version <semver>] [--no-commit] (§16.3)'
|
|
506
|
+
const parsed = parseArgs(
|
|
507
|
+
rest,
|
|
508
|
+
{ 'no-self-update': 'boolean', 'no-commit': 'boolean', version: 'value' },
|
|
509
|
+
usage,
|
|
510
|
+
)
|
|
511
|
+
if (parsed.positionals.length > 1) throw new Error(usage)
|
|
512
|
+
if (parsed.flags.has('no-self-update') && parsed.flags.has('version')) {
|
|
513
|
+
throw new Error(`--no-self-update and --version cannot be combined — ${usage}`)
|
|
514
|
+
}
|
|
515
|
+
const targetRepo = parsed.positionals[0] ?? deps.workspacePath
|
|
516
|
+
const noCommit = parsed.flags.has('no-commit')
|
|
517
|
+
const handoff = deps.processEnv?.[SELF_UPDATE_HANDOFF_ENV] === '1'
|
|
518
|
+
let commitContext: UpgradeCommitContext | undefined
|
|
519
|
+
if (!noCommit) {
|
|
520
|
+
const handoffPath = handoff ? deps.processEnv?.[UPGRADE_COMMIT_CONTEXT_ENV] : undefined
|
|
521
|
+
if (handoff && handoffPath === undefined) {
|
|
522
|
+
stderr(
|
|
523
|
+
'ab upgrade did not commit: automatic commit suppressed for cross-version compatibility because the parent binary did not provide a pre-self-update Git baseline; all upgrade-owned changes remain uncommitted for the operator',
|
|
524
|
+
)
|
|
525
|
+
} else {
|
|
526
|
+
try {
|
|
527
|
+
commitContext =
|
|
528
|
+
handoffPath === undefined
|
|
529
|
+
? await createUpgradeCommitContext(targetRepo)
|
|
530
|
+
: await loadUpgradeCommitContext(handoffPath, targetRepo)
|
|
531
|
+
} catch (error) {
|
|
532
|
+
stderr(
|
|
533
|
+
`ab upgrade did not commit: could not capture or restore the pre-upgrade Git baseline: ${
|
|
534
|
+
error instanceof Error ? error.message : String(error)
|
|
535
|
+
}`,
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
if (!parsed.flags.has('no-self-update') && !handoff) {
|
|
543
|
+
const update = await (deps.selfUpdate ?? selfUpdate)({
|
|
544
|
+
targetRepo,
|
|
545
|
+
stdout,
|
|
546
|
+
stderr,
|
|
547
|
+
env: deps.processEnv ?? {},
|
|
548
|
+
noCommit: noCommit || commitContext === undefined,
|
|
549
|
+
...(commitContext === undefined
|
|
550
|
+
? {}
|
|
551
|
+
: { upgradeCommitContextPath: commitContext.path }),
|
|
552
|
+
...(deps.distributionRoot === undefined ? {} : { distRoot: deps.distributionRoot }),
|
|
553
|
+
...(deps.selfUpdateCommand === undefined ? {} : { command: deps.selfUpdateCommand }),
|
|
554
|
+
...(stringFlag(parsed, 'version') === undefined
|
|
555
|
+
? {}
|
|
556
|
+
: { version: stringFlag(parsed, 'version') }),
|
|
557
|
+
})
|
|
558
|
+
if (update.kind === 'handoff') return update.exitCode
|
|
559
|
+
if (update.kind === 'failed') return 1
|
|
560
|
+
}
|
|
561
|
+
const resolverFactory = deps.upgradeResolverFactory
|
|
562
|
+
let resolver: ResolveConflict | undefined
|
|
563
|
+
const resolveConflict: ResolveConflict | undefined =
|
|
564
|
+
resolverFactory === undefined
|
|
565
|
+
? undefined
|
|
566
|
+
: async (input, options) => {
|
|
567
|
+
// Factory/config/runtime work is lazy: a clean upgrade never
|
|
568
|
+
// needs agent infrastructure. Throws are caught per skill by
|
|
569
|
+
// abUpgrade and become the byte-preserving conflicted outcome.
|
|
570
|
+
resolver ??= resolverFactory({
|
|
571
|
+
targetRepo,
|
|
572
|
+
env: deps.processEnv ?? {},
|
|
573
|
+
})
|
|
574
|
+
return resolver(input, options)
|
|
575
|
+
}
|
|
576
|
+
const progressResolver =
|
|
577
|
+
resolveConflict === undefined || deps.terminal === undefined
|
|
578
|
+
? resolveConflict
|
|
579
|
+
: withUpgradeProgress(resolveConflict, {
|
|
580
|
+
terminal: deps.terminal,
|
|
581
|
+
input: deps.input ?? { start: () => () => {} },
|
|
582
|
+
})
|
|
583
|
+
const report = await abUpgrade({
|
|
584
|
+
targetRepo,
|
|
585
|
+
stdout,
|
|
586
|
+
...(deps.distributionRoot !== undefined ? { distRoot: deps.distributionRoot } : {}),
|
|
587
|
+
...(deps.exec !== undefined ? { exec: deps.exec } : {}),
|
|
588
|
+
...(progressResolver !== undefined ? { resolveConflict: progressResolver } : {}),
|
|
589
|
+
})
|
|
590
|
+
if (commitContext !== undefined) {
|
|
591
|
+
await finishUpgradeCommit(commitContext, report, { stdout, stderr })
|
|
592
|
+
}
|
|
593
|
+
return report.exitCode
|
|
594
|
+
} finally {
|
|
595
|
+
if (commitContext !== undefined) await cleanupUpgradeCommitContext(commitContext)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Ticket grooming runs OUTSIDE build sessions (§8.8): command-scoped flag
|
|
600
|
+
// declarations and one configured-source seam own every pre-build operation.
|
|
601
|
+
case 'ticket': {
|
|
602
|
+
if (deps.exec === undefined) {
|
|
603
|
+
throw new Error("'ab ticket' needs an exec seam — this is a wiring bug in the ab binary")
|
|
604
|
+
}
|
|
605
|
+
await abTicket(rest, {
|
|
606
|
+
targetRepo: deps.workspacePath,
|
|
607
|
+
env: deps.processEnv ?? {},
|
|
608
|
+
exec: deps.exec,
|
|
609
|
+
stdout,
|
|
610
|
+
stderr,
|
|
611
|
+
})
|
|
612
|
+
return 0
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// dispatch runs OUTSIDE build sessions (§3.3, §12): it serves a repo, not
|
|
616
|
+
// a build, so it routes before any store/env requirement and does its own
|
|
617
|
+
// heavy wiring (like ticket create). One dispatcher per repo (§12).
|
|
618
|
+
case 'dispatch': {
|
|
619
|
+
const usage =
|
|
620
|
+
'usage: ab dispatch [--once] [--interval <seconds>] [--store <ref>] [--repository <origin>] [--plain] [--intake | --no-intake] [--auto-merge | --no-auto-merge] (§3.3)'
|
|
621
|
+
const parsed = parseArgs(
|
|
622
|
+
rest,
|
|
623
|
+
{
|
|
624
|
+
once: 'boolean',
|
|
625
|
+
interval: 'value',
|
|
626
|
+
store: 'value',
|
|
627
|
+
repository: 'value',
|
|
628
|
+
plain: 'boolean',
|
|
629
|
+
intake: 'boolean',
|
|
630
|
+
'no-intake': 'boolean',
|
|
631
|
+
'auto-merge': 'boolean',
|
|
632
|
+
'no-auto-merge': 'boolean',
|
|
633
|
+
},
|
|
634
|
+
usage,
|
|
635
|
+
)
|
|
636
|
+
if (parsed.positionals.length > 0) throw new Error(usage)
|
|
637
|
+
|
|
638
|
+
const once = parsed.flags.has('once')
|
|
639
|
+
const plain = parsed.flags.has('plain')
|
|
640
|
+
const sawIntake = parsed.flags.has('intake')
|
|
641
|
+
const sawNoIntake = parsed.flags.has('no-intake')
|
|
642
|
+
const sawAutoMerge = parsed.flags.has('auto-merge')
|
|
643
|
+
const sawNoAutoMerge = parsed.flags.has('no-auto-merge')
|
|
644
|
+
if (sawIntake && sawNoIntake) {
|
|
645
|
+
throw new Error(`--intake and --no-intake cannot be combined — ${usage}`)
|
|
646
|
+
}
|
|
647
|
+
if (sawAutoMerge && sawNoAutoMerge) {
|
|
648
|
+
throw new Error(`--auto-merge and --no-auto-merge cannot be combined — ${usage}`)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const interval = stringFlag(parsed, 'interval')
|
|
652
|
+
let intervalMs: number | undefined
|
|
653
|
+
if (interval !== undefined) {
|
|
654
|
+
const seconds = Number(interval)
|
|
655
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
656
|
+
throw new Error(`--interval requires a positive number of seconds — ${usage}`)
|
|
657
|
+
}
|
|
658
|
+
intervalMs = Math.round(seconds * 1000)
|
|
659
|
+
}
|
|
660
|
+
const storeRef = stringFlag(parsed, 'store')
|
|
661
|
+
const repository = stringFlag(parsed, 'repository') ?? deps.processEnv?.AB_REPOSITORY
|
|
662
|
+
const intake = sawIntake ? true : sawNoIntake ? false : undefined
|
|
663
|
+
const defaultAutoMerge = sawAutoMerge ? true : sawNoAutoMerge ? false : undefined
|
|
664
|
+
if (deps.exec === undefined) {
|
|
665
|
+
throw new Error("'ab dispatch' needs an exec seam — this is a wiring bug in the ab binary")
|
|
666
|
+
}
|
|
667
|
+
await abDispatch({
|
|
668
|
+
targetRepo: deps.workspacePath,
|
|
669
|
+
env: deps.processEnv ?? {},
|
|
670
|
+
exec: deps.exec,
|
|
671
|
+
stdout,
|
|
672
|
+
stderr,
|
|
673
|
+
once,
|
|
674
|
+
plain,
|
|
675
|
+
...(intake !== undefined ? { intake } : {}),
|
|
676
|
+
...(defaultAutoMerge !== undefined ? { defaultAutoMerge } : {}),
|
|
677
|
+
...(intervalMs !== undefined ? { intervalMs } : {}),
|
|
678
|
+
...(storeRef !== undefined ? { storeRef } : {}),
|
|
679
|
+
...(repository !== undefined && repository !== '' ? { repository } : {}),
|
|
680
|
+
...(deps.signal !== undefined ? { signal: deps.signal } : {}),
|
|
681
|
+
...(deps.terminal !== undefined ? { terminal: deps.terminal } : {}),
|
|
682
|
+
...(deps.input !== undefined ? { input: deps.input } : {}),
|
|
683
|
+
...(deps.resolveDashboardRenderer !== undefined
|
|
684
|
+
? { resolveDashboardRenderer: deps.resolveDashboardRenderer }
|
|
685
|
+
: {}),
|
|
686
|
+
})
|
|
687
|
+
return 0
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
case 'plugin': {
|
|
691
|
+
if (deps.exec === undefined) {
|
|
692
|
+
throw new Error("'ab plugin' needs an exec seam — this is a wiring bug in the ab binary")
|
|
693
|
+
}
|
|
694
|
+
return await abPlugin(rest, {
|
|
695
|
+
targetRepo: deps.workspacePath,
|
|
696
|
+
env: deps.processEnv ?? {},
|
|
697
|
+
exec: deps.exec,
|
|
698
|
+
stdout,
|
|
699
|
+
stderr,
|
|
700
|
+
...(deps.pluginSubprocess !== undefined ? { subprocess: deps.pluginSubprocess } : {}),
|
|
701
|
+
})
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// models runs OUTSIDE build sessions (§9): it lists Pi's model catalog so a
|
|
705
|
+
// human can find the provider-qualified id to paste into autobuild.toml. No
|
|
706
|
+
// store needed — the local Pi CLI is behind an injectable catalog seam.
|
|
707
|
+
case 'models': {
|
|
708
|
+
const usage = 'usage: ab models [query] [--available] (§9)'
|
|
709
|
+
const parsed = parseArgs(rest, { available: 'boolean' }, usage)
|
|
710
|
+
await abModels({
|
|
711
|
+
...(parsed.positionals.length > 0 ? { query: parsed.positionals.join(' ') } : {}),
|
|
712
|
+
availableOnly: parsed.flags.has('available'),
|
|
713
|
+
stdout,
|
|
714
|
+
})
|
|
715
|
+
return 0
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Read-only status commands run OUTSIDE build sessions (§16.3) like
|
|
719
|
+
// dispatch: they resolve their own store (--store > AB_STORE > default).
|
|
720
|
+
case 'repository': {
|
|
721
|
+
const usage = 'usage: ab repository status [--json] [--store <ref>] (§8.2)'
|
|
722
|
+
const [sub, ...more] = rest
|
|
723
|
+
if (sub !== 'status') throw new Error(usage)
|
|
724
|
+
const parsed = parseArgs(more, { json: 'boolean', store: 'value' }, usage)
|
|
725
|
+
if (parsed.positionals.length > 0) throw new Error(usage)
|
|
726
|
+
if (deps.exec === undefined) {
|
|
727
|
+
throw new Error(
|
|
728
|
+
"'ab repository status' needs an exec seam — this is a wiring bug in the ab binary",
|
|
729
|
+
)
|
|
730
|
+
}
|
|
731
|
+
const storeRef = stringFlag(parsed, 'store')
|
|
732
|
+
await abRepositoryStatus({
|
|
733
|
+
targetRepo: deps.workspacePath,
|
|
734
|
+
env: deps.processEnv ?? {},
|
|
735
|
+
exec: deps.exec,
|
|
736
|
+
stdout,
|
|
737
|
+
json: parsed.flags.has('json'),
|
|
738
|
+
...(storeRef !== undefined ? { storeRef } : {}),
|
|
739
|
+
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
740
|
+
})
|
|
741
|
+
return 0
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
case 'builds': {
|
|
745
|
+
const usage = 'usage: ab builds [--queued] [--all] [--json] [--store <ref>] (§8.2)'
|
|
746
|
+
const parsed = parseArgs(
|
|
747
|
+
rest,
|
|
748
|
+
{ queued: 'boolean', all: 'boolean', json: 'boolean', store: 'value' },
|
|
749
|
+
usage,
|
|
750
|
+
)
|
|
751
|
+
if (parsed.positionals.length > 0) throw new Error(usage)
|
|
752
|
+
const storeRef = stringFlag(parsed, 'store')
|
|
753
|
+
if (deps.exec === undefined) {
|
|
754
|
+
throw new Error("'ab builds' needs an exec seam — this is a wiring bug in the ab binary")
|
|
755
|
+
}
|
|
756
|
+
await abBuilds({
|
|
757
|
+
targetRepo: deps.workspacePath,
|
|
758
|
+
env: deps.processEnv ?? {},
|
|
759
|
+
exec: deps.exec,
|
|
760
|
+
stdout,
|
|
761
|
+
queued: parsed.flags.has('queued'),
|
|
762
|
+
all: parsed.flags.has('all'),
|
|
763
|
+
json: parsed.flags.has('json'),
|
|
764
|
+
...(storeRef !== undefined ? { storeRef } : {}),
|
|
765
|
+
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
766
|
+
...(deps.clock !== undefined ? { now: deps.clock } : {}),
|
|
767
|
+
})
|
|
768
|
+
return 0
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
case 'mcp': {
|
|
772
|
+
const parsed = parseArgs(rest, { store: 'value', repo: 'value' }, AB_MCP_USAGE)
|
|
773
|
+
if (parsed.positionals.length > 0) throw new Error(AB_MCP_USAGE)
|
|
774
|
+
if (deps.exec === undefined) {
|
|
775
|
+
throw new Error("'ab mcp' needs an exec seam — this is a wiring bug in the ab binary")
|
|
776
|
+
}
|
|
777
|
+
const storeRef = stringFlag(parsed, 'store')
|
|
778
|
+
const repo = stringFlag(parsed, 'repo')
|
|
779
|
+
return await abMcp({
|
|
780
|
+
targetRepo: deps.workspacePath,
|
|
781
|
+
env: deps.processEnv ?? {},
|
|
782
|
+
exec: deps.exec,
|
|
783
|
+
stdout,
|
|
784
|
+
stderr,
|
|
785
|
+
...(storeRef !== undefined ? { storeRef } : {}),
|
|
786
|
+
...(repo !== undefined ? { repo } : {}),
|
|
787
|
+
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
788
|
+
...(deps.clock !== undefined ? { clock: deps.clock } : {}),
|
|
789
|
+
})
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
case 'build': {
|
|
793
|
+
const usage = 'usage: ab build status <slug> [--events <n>] [--json] [--store <ref>] (§8.2)'
|
|
794
|
+
const [sub, ...more] = rest
|
|
795
|
+
// Only `status` today; the subcommand shape keeps room for `ab build <verb>`.
|
|
796
|
+
if (sub !== 'status') {
|
|
797
|
+
throw new Error(usage)
|
|
798
|
+
}
|
|
799
|
+
const parsed = parseArgs(more, { events: 'value', json: 'boolean', store: 'value' }, usage)
|
|
800
|
+
const [slug] = parsed.positionals
|
|
801
|
+
if (slug === undefined || parsed.positionals.length !== 1) {
|
|
802
|
+
throw new Error(usage)
|
|
803
|
+
}
|
|
804
|
+
const eventCount = stringFlag(parsed, 'events')
|
|
805
|
+
let events: number | undefined
|
|
806
|
+
if (eventCount !== undefined) {
|
|
807
|
+
const count = Number(eventCount)
|
|
808
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
809
|
+
throw new Error(`--events requires a positive integer, got "${eventCount}" — ${usage}`)
|
|
810
|
+
}
|
|
811
|
+
events = count
|
|
812
|
+
}
|
|
813
|
+
const storeRef = stringFlag(parsed, 'store')
|
|
814
|
+
if (deps.exec === undefined) {
|
|
815
|
+
throw new Error("'ab build' needs an exec seam — this is a wiring bug in the ab binary")
|
|
816
|
+
}
|
|
817
|
+
await abBuildStatus({
|
|
818
|
+
targetRepo: deps.workspacePath,
|
|
819
|
+
env: deps.processEnv ?? {},
|
|
820
|
+
exec: deps.exec,
|
|
821
|
+
stdout,
|
|
822
|
+
slug,
|
|
823
|
+
json: parsed.flags.has('json'),
|
|
824
|
+
...(events !== undefined ? { events } : {}),
|
|
825
|
+
...(storeRef !== undefined ? { storeRef } : {}),
|
|
826
|
+
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
827
|
+
...(deps.clock !== undefined ? { now: deps.clock } : {}),
|
|
828
|
+
})
|
|
829
|
+
return 0
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Read-only streaming runs OUTSIDE build sessions (§16.3) like the other
|
|
833
|
+
// status commands: it resolves its own store (--store > AB_STORE > default)
|
|
834
|
+
// and honors a complete ambient identity as a scope restriction (watch.ts).
|
|
835
|
+
case 'watch': {
|
|
836
|
+
const parsed = parseArgs(
|
|
837
|
+
rest,
|
|
838
|
+
{
|
|
839
|
+
repository: 'boolean',
|
|
840
|
+
event: 'multi',
|
|
841
|
+
since: 'value',
|
|
842
|
+
timeout: 'value',
|
|
843
|
+
interval: 'value',
|
|
844
|
+
count: 'value',
|
|
845
|
+
json: 'boolean',
|
|
846
|
+
store: 'value',
|
|
847
|
+
},
|
|
848
|
+
WATCH_USAGE,
|
|
849
|
+
)
|
|
850
|
+
const countFlag = stringFlag(parsed, 'count')
|
|
851
|
+
let count: number | undefined
|
|
852
|
+
if (countFlag !== undefined) {
|
|
853
|
+
const parsedCount = Number(countFlag)
|
|
854
|
+
if (!Number.isInteger(parsedCount) || parsedCount <= 0) {
|
|
855
|
+
throw new Error(
|
|
856
|
+
`--count requires a positive integer, got "${countFlag}" — ${WATCH_USAGE}`,
|
|
857
|
+
)
|
|
858
|
+
}
|
|
859
|
+
count = parsedCount
|
|
860
|
+
}
|
|
861
|
+
const storeRef = stringFlag(parsed, 'store')
|
|
862
|
+
if (deps.exec === undefined) {
|
|
863
|
+
throw new Error("'ab watch' needs an exec seam — this is a wiring bug in the ab binary")
|
|
864
|
+
}
|
|
865
|
+
await abWatch({
|
|
866
|
+
targetRepo: deps.workspacePath,
|
|
867
|
+
env: deps.processEnv ?? {},
|
|
868
|
+
exec: deps.exec,
|
|
869
|
+
stdout,
|
|
870
|
+
stderr,
|
|
871
|
+
slugs: parsed.positionals,
|
|
872
|
+
repository: parsed.flags.has('repository'),
|
|
873
|
+
events: multiFlag(parsed, 'event'),
|
|
874
|
+
since: stringFlag(parsed, 'since'),
|
|
875
|
+
timeout: stringFlag(parsed, 'timeout'),
|
|
876
|
+
interval: stringFlag(parsed, 'interval'),
|
|
877
|
+
...(count !== undefined ? { count } : {}),
|
|
878
|
+
json: parsed.flags.has('json'),
|
|
879
|
+
...(storeRef !== undefined ? { storeRef } : {}),
|
|
880
|
+
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
881
|
+
...(deps.clock !== undefined ? { now: deps.clock } : {}),
|
|
882
|
+
...(deps.signal !== undefined ? { signal: deps.signal } : {}),
|
|
883
|
+
})
|
|
884
|
+
return 0
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Read-only blocking runs OUTSIDE build sessions (§16.3) exactly like
|
|
888
|
+
// `ab watch`: it resolves its own store and honors a complete ambient
|
|
889
|
+
// identity as a scope restriction. Exit codes 0/2/3/4 are the command's
|
|
890
|
+
// contract (wait.ts); thrown usage errors exit 1 via runCli.
|
|
891
|
+
case 'wait': {
|
|
892
|
+
const parsed = parseArgs(
|
|
893
|
+
rest,
|
|
894
|
+
{
|
|
895
|
+
for: 'multi',
|
|
896
|
+
event: 'multi',
|
|
897
|
+
since: 'value',
|
|
898
|
+
timeout: 'value',
|
|
899
|
+
interval: 'value',
|
|
900
|
+
json: 'boolean',
|
|
901
|
+
store: 'value',
|
|
902
|
+
},
|
|
903
|
+
WAIT_USAGE,
|
|
904
|
+
)
|
|
905
|
+
if (multiFlag(parsed, 'for').length === 0 && multiFlag(parsed, 'event').length === 0) {
|
|
906
|
+
throw new Error(WAIT_USAGE)
|
|
907
|
+
}
|
|
908
|
+
const storeRef = stringFlag(parsed, 'store')
|
|
909
|
+
if (deps.exec === undefined) {
|
|
910
|
+
throw new Error("'ab wait' needs an exec seam — this is a wiring bug in the ab binary")
|
|
911
|
+
}
|
|
912
|
+
return await abWait({
|
|
913
|
+
targetRepo: deps.workspacePath,
|
|
914
|
+
env: deps.processEnv ?? {},
|
|
915
|
+
exec: deps.exec,
|
|
916
|
+
stdout,
|
|
917
|
+
stderr,
|
|
918
|
+
slugs: parsed.positionals,
|
|
919
|
+
forValues: multiFlag(parsed, 'for'),
|
|
920
|
+
events: multiFlag(parsed, 'event'),
|
|
921
|
+
since: stringFlag(parsed, 'since'),
|
|
922
|
+
timeout: stringFlag(parsed, 'timeout'),
|
|
923
|
+
interval: stringFlag(parsed, 'interval'),
|
|
924
|
+
json: parsed.flags.has('json'),
|
|
925
|
+
...(storeRef !== undefined ? { storeRef } : {}),
|
|
926
|
+
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
927
|
+
...(deps.clock !== undefined ? { now: deps.clock } : {}),
|
|
928
|
+
...(deps.signal !== undefined ? { signal: deps.signal } : {}),
|
|
929
|
+
})
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
case 'abort': {
|
|
933
|
+
const usage = 'usage: ab abort <slug> [--store <ref>]'
|
|
934
|
+
const parsed = parseArgs(rest, { store: 'value' }, usage)
|
|
935
|
+
if (parsed.positionals.length !== 1) throw new Error(usage)
|
|
936
|
+
await runBuildControl(
|
|
937
|
+
deps,
|
|
938
|
+
parsed.positionals[0]!,
|
|
939
|
+
{ kind: 'abort' },
|
|
940
|
+
stringFlag(parsed, 'store'),
|
|
941
|
+
)
|
|
942
|
+
return 0
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
case 'pause':
|
|
946
|
+
case 'resume': {
|
|
947
|
+
const usage = `usage: ab ${command} <slug> | --all [--store <ref>] [--json]`
|
|
948
|
+
const parsed = parseArgs(rest, { store: 'value', all: 'boolean', json: 'boolean' }, usage)
|
|
949
|
+
const all = parsed.flags.has('all')
|
|
950
|
+
const json = parsed.flags.has('json')
|
|
951
|
+
const storeRef = stringFlag(parsed, 'store')
|
|
952
|
+
if (json && !all) throw new Error(`--json requires --all — ${usage}`)
|
|
953
|
+
if (all && parsed.positionals.length > 0) {
|
|
954
|
+
throw new Error(`--all takes no build slug — ${usage}`)
|
|
955
|
+
}
|
|
956
|
+
if (all) {
|
|
957
|
+
return runBulkControl(deps, command, {
|
|
958
|
+
json,
|
|
959
|
+
...(storeRef !== undefined ? { storeRef } : {}),
|
|
960
|
+
})
|
|
961
|
+
}
|
|
962
|
+
if (parsed.positionals.length !== 1) throw new Error(usage)
|
|
963
|
+
await runBuildControl(deps, parsed.positionals[0]!, { kind: command }, storeRef)
|
|
964
|
+
return 0
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
case 'auto-merge': {
|
|
968
|
+
const usage = 'usage: ab auto-merge <slug> <on|off> [--store <ref>]'
|
|
969
|
+
const parsed = parseArgs(rest, { store: 'value' }, usage)
|
|
970
|
+
const [slug, setting] = parsed.positionals
|
|
971
|
+
if (
|
|
972
|
+
parsed.positionals.length !== 2 ||
|
|
973
|
+
slug === undefined ||
|
|
974
|
+
(setting !== 'on' && setting !== 'off')
|
|
975
|
+
) {
|
|
976
|
+
throw new Error(usage)
|
|
977
|
+
}
|
|
978
|
+
await runBuildControl(
|
|
979
|
+
deps,
|
|
980
|
+
slug,
|
|
981
|
+
{ kind: setting === 'on' ? 'auto-merge-on' : 'auto-merge-off' },
|
|
982
|
+
stringFlag(parsed, 'store'),
|
|
983
|
+
)
|
|
984
|
+
return 0
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
case 'answer': {
|
|
988
|
+
const usage =
|
|
989
|
+
'usage: ab answer <slug> [<text>] [--review-round-ceiling <n> | --dismiss | --revise-spec <file> | --revise-spec-from-ticket] [--store <ref>]'
|
|
990
|
+
const parsed = parseArgs(
|
|
991
|
+
rest,
|
|
992
|
+
{
|
|
993
|
+
store: 'value',
|
|
994
|
+
dismiss: 'boolean',
|
|
995
|
+
'revise-spec': 'value',
|
|
996
|
+
'revise-spec-from-ticket': 'boolean',
|
|
997
|
+
'review-round-ceiling': 'value',
|
|
998
|
+
},
|
|
999
|
+
usage,
|
|
1000
|
+
)
|
|
1001
|
+
const [slug, ...text] = parsed.positionals
|
|
1002
|
+
if (slug === undefined) throw new Error(usage)
|
|
1003
|
+
const resolutionFlags = [
|
|
1004
|
+
parsed.flags.has('dismiss'),
|
|
1005
|
+
parsed.flags.has('revise-spec'),
|
|
1006
|
+
parsed.flags.has('revise-spec-from-ticket'),
|
|
1007
|
+
].filter(Boolean).length
|
|
1008
|
+
if (resolutionFlags > 1) throw new Error(`resolution flags are mutually exclusive — ${usage}`)
|
|
1009
|
+
const answer = text.join(' ')
|
|
1010
|
+
const storeRef = stringFlag(parsed, 'store')
|
|
1011
|
+
const specPath = stringFlag(parsed, 'revise-spec')
|
|
1012
|
+
const ceilingFlag = stringFlag(parsed, 'review-round-ceiling')
|
|
1013
|
+
let reviewRoundCeiling: number | undefined
|
|
1014
|
+
if (ceilingFlag !== undefined) {
|
|
1015
|
+
const parsedCeiling = Number(ceilingFlag)
|
|
1016
|
+
if (!Number.isInteger(parsedCeiling) || parsedCeiling <= 0) {
|
|
1017
|
+
throw new Error(
|
|
1018
|
+
`--review-round-ceiling requires a positive integer, got "${ceilingFlag}" — ${usage}`,
|
|
1019
|
+
)
|
|
1020
|
+
}
|
|
1021
|
+
if (specPath !== undefined || parsed.flags.has('revise-spec-from-ticket')) {
|
|
1022
|
+
throw new Error(
|
|
1023
|
+
'cannot combine --review-round-ceiling with a spec revision: revision resets the loop round budget on its own',
|
|
1024
|
+
)
|
|
1025
|
+
}
|
|
1026
|
+
if (parsed.flags.has('dismiss')) {
|
|
1027
|
+
throw new Error(
|
|
1028
|
+
'cannot combine --review-round-ceiling with --dismiss; use guidance or a bare retry to answer the review-round-limit escalation',
|
|
1029
|
+
)
|
|
1030
|
+
}
|
|
1031
|
+
reviewRoundCeiling = parsedCeiling
|
|
1032
|
+
}
|
|
1033
|
+
const resolve = parsed.flags.has('dismiss')
|
|
1034
|
+
? ({ kind: 'dismiss-finding' } as const)
|
|
1035
|
+
: specPath !== undefined
|
|
1036
|
+
? ({
|
|
1037
|
+
kind: 'revise-spec',
|
|
1038
|
+
body: {
|
|
1039
|
+
kind: 'supplied',
|
|
1040
|
+
origin: `--revise-spec ${specPath}`,
|
|
1041
|
+
read: async (): Promise<string> => {
|
|
1042
|
+
try {
|
|
1043
|
+
return await readFile(specPath, 'utf8')
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
1046
|
+
throw new Error(
|
|
1047
|
+
`--revise-spec ${specPath}: file not found — expected a file holding the replacement spec body`,
|
|
1048
|
+
)
|
|
1049
|
+
}
|
|
1050
|
+
throw error
|
|
1051
|
+
}
|
|
1052
|
+
},
|
|
1053
|
+
},
|
|
1054
|
+
} as const)
|
|
1055
|
+
: parsed.flags.has('revise-spec-from-ticket')
|
|
1056
|
+
? ({ kind: 'revise-spec', body: { kind: 'ticket' } } as const)
|
|
1057
|
+
: undefined
|
|
1058
|
+
const readTicketBody =
|
|
1059
|
+
resolve?.kind === 'revise-spec' && resolve.body.kind === 'ticket'
|
|
1060
|
+
? async (ref: TicketRef): Promise<string> => {
|
|
1061
|
+
const { source } = await openTicketSource(
|
|
1062
|
+
{
|
|
1063
|
+
targetRepo: deps.workspacePath,
|
|
1064
|
+
env: deps.processEnv ?? {},
|
|
1065
|
+
...(deps.exec !== undefined ? { exec: deps.exec } : {}),
|
|
1066
|
+
stdout,
|
|
1067
|
+
...(storeRef !== undefined ? { storeRef } : {}),
|
|
1068
|
+
},
|
|
1069
|
+
'ab answer --revise-spec-from-ticket',
|
|
1070
|
+
)
|
|
1071
|
+
if (source.name !== ref.source) {
|
|
1072
|
+
throw new Error(
|
|
1073
|
+
`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`,
|
|
1074
|
+
)
|
|
1075
|
+
}
|
|
1076
|
+
const ticket = await source.get(ref.id)
|
|
1077
|
+
if (ticket === null) {
|
|
1078
|
+
throw new Error(
|
|
1079
|
+
`ticket "${ref.id}" was not found in the configured ${source.name} source`,
|
|
1080
|
+
)
|
|
1081
|
+
}
|
|
1082
|
+
if (ticket.ref.source !== ref.source || ticket.ref.id !== ref.id) {
|
|
1083
|
+
throw new Error(
|
|
1084
|
+
`ticket adapter returned ${ticket.ref.source}:${ticket.ref.id}, expected ${ref.source}:${ref.id}`,
|
|
1085
|
+
)
|
|
1086
|
+
}
|
|
1087
|
+
return ticket.body
|
|
1088
|
+
}
|
|
1089
|
+
: undefined
|
|
1090
|
+
await runBuildControl(
|
|
1091
|
+
deps,
|
|
1092
|
+
slug,
|
|
1093
|
+
{
|
|
1094
|
+
kind: 'answer',
|
|
1095
|
+
...(answer !== '' ? { text: answer } : {}),
|
|
1096
|
+
...(resolve !== undefined ? { resolve } : {}),
|
|
1097
|
+
...(reviewRoundCeiling !== undefined ? { reviewRoundCeiling } : {}),
|
|
1098
|
+
},
|
|
1099
|
+
storeRef,
|
|
1100
|
+
readTicketBody,
|
|
1101
|
+
)
|
|
1102
|
+
return 0
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
case 'harvest': {
|
|
1106
|
+
const [sub, ...more] = rest
|
|
1107
|
+
if (sub === 'status') {
|
|
1108
|
+
const usage = 'usage: ab harvest status [--events <n>] [--json] [--store <ref>]'
|
|
1109
|
+
const parsed = parseArgs(more, { events: 'value', json: 'boolean', store: 'value' }, usage)
|
|
1110
|
+
if (parsed.positionals.length > 0) throw new Error(usage)
|
|
1111
|
+
const eventCount = stringFlag(parsed, 'events')
|
|
1112
|
+
let events: number | undefined
|
|
1113
|
+
if (eventCount !== undefined) {
|
|
1114
|
+
const count = Number(eventCount)
|
|
1115
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
1116
|
+
throw new Error(`--events requires a positive integer — ${usage}`)
|
|
1117
|
+
}
|
|
1118
|
+
events = count
|
|
1119
|
+
}
|
|
1120
|
+
const storeRef = stringFlag(parsed, 'store')
|
|
1121
|
+
if (deps.exec === undefined) {
|
|
1122
|
+
throw new Error(
|
|
1123
|
+
"'ab harvest status' needs an exec seam — this is a wiring bug in the ab binary",
|
|
1124
|
+
)
|
|
1125
|
+
}
|
|
1126
|
+
await abHarvestStatus({
|
|
1127
|
+
repo: deps.workspacePath,
|
|
1128
|
+
env: deps.processEnv ?? {},
|
|
1129
|
+
exec: deps.exec,
|
|
1130
|
+
stdout,
|
|
1131
|
+
json: parsed.flags.has('json'),
|
|
1132
|
+
...(events !== undefined ? { events } : {}),
|
|
1133
|
+
...(storeRef !== undefined ? { storeRef } : {}),
|
|
1134
|
+
})
|
|
1135
|
+
return 0
|
|
1136
|
+
}
|
|
1137
|
+
if (sub === 'context') {
|
|
1138
|
+
const usage = 'usage: ab harvest context [--json]'
|
|
1139
|
+
const parsed = parseArgs(more, { json: 'boolean' }, usage)
|
|
1140
|
+
if (parsed.positionals.length > 0) throw new Error(usage)
|
|
1141
|
+
const manifest = await buildHarvestContext(requireHarvestSession('context', deps))
|
|
1142
|
+
if (parsed.flags.get('json') === true) {
|
|
1143
|
+
stdout(JSON.stringify(manifest, null, 2))
|
|
1144
|
+
} else {
|
|
1145
|
+
stdout(
|
|
1146
|
+
`harvest context materialized for ${manifest.run} — ${manifest.phase}@${manifest.round}`,
|
|
1147
|
+
)
|
|
1148
|
+
stdout(`required deposit: ${manifest.required.join(', ')}`)
|
|
1149
|
+
for (const file of manifest.materialized) stdout(` .ab/${file}`)
|
|
1150
|
+
}
|
|
1151
|
+
return 0
|
|
1152
|
+
}
|
|
1153
|
+
if (sub === 'submit') {
|
|
1154
|
+
const usage = 'usage: ab harvest submit <proposals.json>'
|
|
1155
|
+
const parsed = parseArgs(more, {}, usage)
|
|
1156
|
+
const [file] = parsed.positionals
|
|
1157
|
+
if (file === undefined || parsed.positionals.length !== 1) {
|
|
1158
|
+
throw new Error(usage)
|
|
1159
|
+
}
|
|
1160
|
+
const event = await submitHarvestProposals(requireHarvestSession('submit', deps), file)
|
|
1161
|
+
stdout(`${event.type} recorded (repo seq ${event.seq})`)
|
|
1162
|
+
return 0
|
|
1163
|
+
}
|
|
1164
|
+
if (sub === 'verdict') {
|
|
1165
|
+
const usage =
|
|
1166
|
+
'usage: ab harvest verdict <approve|revise|escalate> --notes <file> ' +
|
|
1167
|
+
'[--findings <json>] [--reason <text>]'
|
|
1168
|
+
const parsed = parseArgs(
|
|
1169
|
+
more,
|
|
1170
|
+
{ notes: 'value', findings: 'value', reason: 'value' },
|
|
1171
|
+
usage,
|
|
1172
|
+
)
|
|
1173
|
+
const [kind] = parsed.positionals
|
|
1174
|
+
const notes = stringFlag(parsed, 'notes')
|
|
1175
|
+
if (kind === undefined || parsed.positionals.length !== 1 || notes === undefined) {
|
|
1176
|
+
throw new Error(usage)
|
|
1177
|
+
}
|
|
1178
|
+
const event = await submitHarvestVerdict(requireHarvestSession('verdict', deps), {
|
|
1179
|
+
verdict: kind,
|
|
1180
|
+
notes,
|
|
1181
|
+
...(stringFlag(parsed, 'findings') !== undefined
|
|
1182
|
+
? { findings: stringFlag(parsed, 'findings')! }
|
|
1183
|
+
: {}),
|
|
1184
|
+
...(stringFlag(parsed, 'reason') !== undefined
|
|
1185
|
+
? { reason: stringFlag(parsed, 'reason')! }
|
|
1186
|
+
: {}),
|
|
1187
|
+
})
|
|
1188
|
+
stdout(`${event.type} recorded (repo seq ${event.seq})`)
|
|
1189
|
+
return 0
|
|
1190
|
+
}
|
|
1191
|
+
throw new Error('usage: ab harvest <status|context|submit|verdict> …')
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
case 'context': {
|
|
1195
|
+
const session = requireSession(command, deps)
|
|
1196
|
+
const usage = 'usage: ab context [--json]'
|
|
1197
|
+
const parsed = parseArgs(rest, { json: 'boolean' }, usage)
|
|
1198
|
+
if (parsed.positionals.length > 0) throw new Error(usage)
|
|
1199
|
+
const manifest = await buildContext(session)
|
|
1200
|
+
if (parsed.flags.get('json') === true) {
|
|
1201
|
+
stdout(JSON.stringify(manifest, null, 2))
|
|
1202
|
+
} else {
|
|
1203
|
+
for (const line of renderManifest(manifest)) stdout(line)
|
|
1204
|
+
}
|
|
1205
|
+
return 0
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
case 'artifact': {
|
|
1209
|
+
const [sub, ...more] = rest
|
|
1210
|
+
if (sub === 'download') {
|
|
1211
|
+
const usage =
|
|
1212
|
+
'usage: ab artifact download <build> <kind>[@rev] --output <file> [--store <ref>] (§8.2)'
|
|
1213
|
+
const parsed = parseArgs(more, { output: 'value', store: 'value' }, usage)
|
|
1214
|
+
const [build, spec] = parsed.positionals
|
|
1215
|
+
const outputPath = stringFlag(parsed, 'output')
|
|
1216
|
+
if (
|
|
1217
|
+
build === undefined ||
|
|
1218
|
+
spec === undefined ||
|
|
1219
|
+
parsed.positionals.length !== 2 ||
|
|
1220
|
+
outputPath === undefined
|
|
1221
|
+
) {
|
|
1222
|
+
throw new Error(usage)
|
|
1223
|
+
}
|
|
1224
|
+
const storeRef = stringFlag(parsed, 'store')
|
|
1225
|
+
if (deps.exec === undefined) {
|
|
1226
|
+
throw new Error(
|
|
1227
|
+
"'ab artifact download' needs an exec seam — this is a wiring bug in the ab binary",
|
|
1228
|
+
)
|
|
1229
|
+
}
|
|
1230
|
+
const downloaded = await artifactDownload({
|
|
1231
|
+
targetRepo: deps.workspacePath,
|
|
1232
|
+
env: deps.processEnv ?? {},
|
|
1233
|
+
exec: deps.exec,
|
|
1234
|
+
build,
|
|
1235
|
+
spec,
|
|
1236
|
+
outputPath,
|
|
1237
|
+
...(storeRef !== undefined ? { storeRef } : {}),
|
|
1238
|
+
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
1239
|
+
})
|
|
1240
|
+
stdout(
|
|
1241
|
+
`downloaded ${downloaded.artifact.meta.kind}@${downloaded.artifact.meta.revision} to ${downloaded.outputPath}`,
|
|
1242
|
+
)
|
|
1243
|
+
return 0
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const session = requireSession(command, deps)
|
|
1247
|
+
if (sub === 'put') {
|
|
1248
|
+
const usage = 'usage: ab artifact put <kind> <file> [--attach] (§8.2)'
|
|
1249
|
+
const parsed = parseArgs(more, { attach: 'boolean' }, usage)
|
|
1250
|
+
const [kind, file] = parsed.positionals
|
|
1251
|
+
if (kind === undefined || file === undefined || parsed.positionals.length !== 2) {
|
|
1252
|
+
throw new Error(usage)
|
|
1253
|
+
}
|
|
1254
|
+
const attach = parsed.flags.has('attach')
|
|
1255
|
+
const meta = await artifactPut(session, kind, file, { attach })
|
|
1256
|
+
|
|
1257
|
+
// A designation made after the PR exists (verify after reconcile or a
|
|
1258
|
+
// finalize post-step) republishes the same complete projection. Every
|
|
1259
|
+
// external side effect is best-effort after the atomic designation.
|
|
1260
|
+
if (attach) {
|
|
1261
|
+
try {
|
|
1262
|
+
const events = await session.store.getEvents(session.env.build)
|
|
1263
|
+
const pr = reduceBuild(events).pr
|
|
1264
|
+
if (pr !== undefined) {
|
|
1265
|
+
try {
|
|
1266
|
+
await preparePrAttachments(session, events, pr.url)
|
|
1267
|
+
} catch {
|
|
1268
|
+
// A post-upload hosted-fact failure records the exact public
|
|
1269
|
+
// asset identity before throwing. Keep going so its hosting
|
|
1270
|
+
// failure cannot suppress the complete text projection.
|
|
1271
|
+
}
|
|
1272
|
+
try {
|
|
1273
|
+
await session.forge.commentOnPr(
|
|
1274
|
+
session.workspacePath,
|
|
1275
|
+
pr.number,
|
|
1276
|
+
renderPrSummary(session.env, await session.store.getEvents(session.env.build)),
|
|
1277
|
+
)
|
|
1278
|
+
} catch {
|
|
1279
|
+
// The exact artifact and designation are already durable. A
|
|
1280
|
+
// comment refresh cannot turn the agent's deposit into failure.
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
} catch {
|
|
1284
|
+
// Reading the optional PR projection is best-effort too.
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// The assigned rev is the command's one output (§8.2).
|
|
1289
|
+
stdout(String(meta.revision))
|
|
1290
|
+
return 0
|
|
1291
|
+
}
|
|
1292
|
+
if (sub === 'get') {
|
|
1293
|
+
const usage = 'usage: ab artifact get <kind>[@rev] (§8.2)'
|
|
1294
|
+
const parsed = parseArgs(more, {}, usage)
|
|
1295
|
+
const [spec] = parsed.positionals
|
|
1296
|
+
if (spec === undefined || parsed.positionals.length !== 1) {
|
|
1297
|
+
throw new Error(usage)
|
|
1298
|
+
}
|
|
1299
|
+
const artifact = await artifactGet(session, spec)
|
|
1300
|
+
stdout(textContent(artifact))
|
|
1301
|
+
return 0
|
|
1302
|
+
}
|
|
1303
|
+
throw new Error('usage: ab artifact <put|get|download> … (§8.2)')
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
case 'observe': {
|
|
1307
|
+
const usage =
|
|
1308
|
+
'usage: ab observe --kind <followup|refactor|latent-bug> [--files a,b] [--refs x,y] <summary> (§8.2)'
|
|
1309
|
+
const parsed = parseArgs(rest, { kind: 'value', files: 'value', refs: 'value' }, usage)
|
|
1310
|
+
const kind = stringFlag(parsed, 'kind')
|
|
1311
|
+
if (kind === undefined) {
|
|
1312
|
+
throw new Error("'ab observe' requires --kind <followup|refactor|latent-bug> (§8.2)")
|
|
1313
|
+
}
|
|
1314
|
+
const session = requireSession(command, deps)
|
|
1315
|
+
const summary = parsed.positionals.join(' ')
|
|
1316
|
+
const files = listFlag(parsed, 'files')
|
|
1317
|
+
const refs = listFlag(parsed, 'refs')
|
|
1318
|
+
const event = await observe(session, {
|
|
1319
|
+
kind,
|
|
1320
|
+
summary,
|
|
1321
|
+
...(files !== undefined ? { files } : {}),
|
|
1322
|
+
...(refs !== undefined ? { refs } : {}),
|
|
1323
|
+
})
|
|
1324
|
+
stdout(`observation recorded: ${event.payload.id}`)
|
|
1325
|
+
return 0
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
case 'done': {
|
|
1329
|
+
const session = requireSession(command, deps)
|
|
1330
|
+
const usage = 'usage: ab done [--notes <file>] (§8.2)'
|
|
1331
|
+
const parsed = parseArgs(rest, { notes: 'value' }, usage)
|
|
1332
|
+
if (parsed.positionals.length > 0) throw new Error(usage)
|
|
1333
|
+
const notes = stringFlag(parsed, 'notes')
|
|
1334
|
+
const event = await done(session, notes !== undefined ? { notes } : {})
|
|
1335
|
+
stdout(
|
|
1336
|
+
event.type === 'publication.requested'
|
|
1337
|
+
? `${event.type} recorded (seq ${event.seq}); remote publication is pending — do not run ab done again`
|
|
1338
|
+
: `${event.type} recorded (seq ${event.seq})`,
|
|
1339
|
+
)
|
|
1340
|
+
return 0
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
case 'verdict': {
|
|
1344
|
+
const usage =
|
|
1345
|
+
'usage: ab verdict <approve|revise|escalate|pass|fail|skip> ' +
|
|
1346
|
+
'[--findings <json>] [--notes <file>] [--reason <text>] [--report <file>] (§8.2)'
|
|
1347
|
+
const parsed = parseArgs(
|
|
1348
|
+
rest,
|
|
1349
|
+
{
|
|
1350
|
+
findings: 'value',
|
|
1351
|
+
notes: 'value',
|
|
1352
|
+
reason: 'value',
|
|
1353
|
+
report: 'value',
|
|
1354
|
+
},
|
|
1355
|
+
usage,
|
|
1356
|
+
)
|
|
1357
|
+
const [kind] = parsed.positionals
|
|
1358
|
+
if (kind === undefined || parsed.positionals.length !== 1) {
|
|
1359
|
+
throw new Error(usage)
|
|
1360
|
+
}
|
|
1361
|
+
const session = requireSession(command, deps)
|
|
1362
|
+
const notes = stringFlag(parsed, 'notes')
|
|
1363
|
+
const findings = stringFlag(parsed, 'findings')
|
|
1364
|
+
const reason = stringFlag(parsed, 'reason')
|
|
1365
|
+
const report = stringFlag(parsed, 'report')
|
|
1366
|
+
const events = await verdict(session, {
|
|
1367
|
+
verdict: kind,
|
|
1368
|
+
...(notes !== undefined ? { notes } : {}),
|
|
1369
|
+
...(findings !== undefined ? { findings } : {}),
|
|
1370
|
+
...(reason !== undefined ? { reason } : {}),
|
|
1371
|
+
...(report !== undefined ? { report } : {}),
|
|
1372
|
+
})
|
|
1373
|
+
for (const event of events) stdout(`${event.type} recorded (seq ${event.seq})`)
|
|
1374
|
+
return 0
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
case 'escalate': {
|
|
1378
|
+
const usage = 'usage: ab escalate <question> [--refs a,b] (§8.2)'
|
|
1379
|
+
const parsed = parseArgs(rest, { refs: 'value' }, usage)
|
|
1380
|
+
const question = parsed.positionals.join(' ')
|
|
1381
|
+
if (question === '') {
|
|
1382
|
+
throw new Error('usage: ab escalate <question> [--refs a,b] (§8.2)')
|
|
1383
|
+
}
|
|
1384
|
+
const session = requireSession(command, deps)
|
|
1385
|
+
const refs = listFlag(parsed, 'refs')
|
|
1386
|
+
const event = await escalate(session, {
|
|
1387
|
+
question,
|
|
1388
|
+
...(refs !== undefined ? { refs } : {}),
|
|
1389
|
+
})
|
|
1390
|
+
stdout(`escalation raised: ${event.payload.id}`)
|
|
1391
|
+
return 0
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
default: {
|
|
1395
|
+
stderr(`unknown command "${command}"\n\n${renderTopLevelHelp()}`)
|
|
1396
|
+
return 1
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|