@defrex/autobuild 0.8.0 → 0.10.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 +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -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/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- 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-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- 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 +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- 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/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- 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 +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -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 +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- 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 +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- 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/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- 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 +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- 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 +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- 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 +204 -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/skills/tickets/SKILL.md +57 -5
- 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
|
@@ -21,13 +21,25 @@ export type InstallScope = 'local' | 'global'
|
|
|
21
21
|
* forge dependency, or a version range satisfied from the npm registry. */
|
|
22
22
|
export type InstallChannel = 'github' | 'npm'
|
|
23
23
|
|
|
24
|
+
/** The owning manifest sections a self-update target may be declared in. */
|
|
25
|
+
export type DependencySection = 'dependencies' | 'devDependencies'
|
|
26
|
+
|
|
27
|
+
/** Searched in this order; when a manifest declares the package in both, the
|
|
28
|
+
* first wins (the runtime declaration, which a plain `bun add` would rewrite). */
|
|
29
|
+
const DEPENDENCY_SECTIONS: readonly DependencySection[] = ['dependencies', 'devDependencies']
|
|
30
|
+
const SEARCHED_SECTIONS = DEPENDENCY_SECTIONS.join(' or ')
|
|
31
|
+
|
|
24
32
|
interface ManagedInstallationBase extends DistributionIdentity {
|
|
25
33
|
sourceCheckout: false
|
|
26
34
|
ownerRoot: string
|
|
27
35
|
ownerManifest: string
|
|
28
36
|
ownerLock: string
|
|
29
|
-
/** The owning manifest's direct dependency value
|
|
37
|
+
/** The owning manifest's direct dependency value in `section`, byte for byte. */
|
|
30
38
|
dependency: string
|
|
39
|
+
/** The manifest section the dependency was chosen from. */
|
|
40
|
+
section: DependencySection
|
|
41
|
+
/** The manifest also declares the package in the other section. */
|
|
42
|
+
dual: boolean
|
|
31
43
|
scope: InstallScope
|
|
32
44
|
}
|
|
33
45
|
|
|
@@ -180,9 +192,27 @@ async function parseJsonc(path: string): Promise<Record<string, unknown>> {
|
|
|
180
192
|
}
|
|
181
193
|
}
|
|
182
194
|
|
|
183
|
-
function lockDependency(
|
|
195
|
+
function lockDependency(
|
|
196
|
+
lock: Record<string, unknown>,
|
|
197
|
+
packageName: string,
|
|
198
|
+
section: DependencySection,
|
|
199
|
+
): unknown {
|
|
184
200
|
const workspace = object(object(lock.workspaces)?.[''])
|
|
185
|
-
return object(workspace?.
|
|
201
|
+
return object(workspace?.[section])?.[packageName]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Pick the section that declares the package: a pure function of the
|
|
205
|
+
* manifest, in fixed `DEPENDENCY_SECTIONS` order. */
|
|
206
|
+
function declaredSection(
|
|
207
|
+
manifest: Record<string, unknown>,
|
|
208
|
+
packageName: string,
|
|
209
|
+
): { section: DependencySection; value: string; dual: boolean } | undefined {
|
|
210
|
+
const declared = DEPENDENCY_SECTIONS.flatMap((section) => {
|
|
211
|
+
const value = object(manifest[section])?.[packageName]
|
|
212
|
+
return typeof value === 'string' ? [{ section, value }] : []
|
|
213
|
+
})
|
|
214
|
+
const first = declared[0]
|
|
215
|
+
return first === undefined ? undefined : { ...first, dual: declared.length > 1 }
|
|
186
216
|
}
|
|
187
217
|
|
|
188
218
|
function lockPackageRecord(
|
|
@@ -235,21 +265,29 @@ export async function inspectInstallation(options: {
|
|
|
235
265
|
try {
|
|
236
266
|
const manifest = await parseJson(ownerManifest)
|
|
237
267
|
const lock = await parseJsonc(ownerLock)
|
|
238
|
-
const
|
|
268
|
+
const declared = declaredSection(manifest, identity.packageName)
|
|
269
|
+
if (declared === undefined) {
|
|
270
|
+
return {
|
|
271
|
+
kind: 'unknown',
|
|
272
|
+
identity,
|
|
273
|
+
reason: `${ownerManifest} does not declare ${identity.packageName} under ${SEARCHED_SECTIONS} as a direct github: dependency or an npm registry version range satisfied by ${identity.version}`,
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const { section, value: dependency, dual } = declared
|
|
239
277
|
const repository = githubDependency(dependency)
|
|
240
278
|
const registry = repository === undefined && registryDependency(dependency, identity.version)
|
|
241
|
-
if (
|
|
279
|
+
if (repository === undefined && !registry) {
|
|
242
280
|
return {
|
|
243
281
|
kind: 'unknown',
|
|
244
282
|
identity,
|
|
245
|
-
reason: `${ownerManifest}
|
|
283
|
+
reason: `${ownerManifest} declares ${identity.packageName} under ${section} as "${dependency}", which is neither a direct github: dependency nor an npm registry version range satisfied by ${identity.version}`,
|
|
246
284
|
}
|
|
247
285
|
}
|
|
248
|
-
if (lockDependency(lock, identity.packageName) !== dependency) {
|
|
286
|
+
if (lockDependency(lock, identity.packageName, section) !== dependency) {
|
|
249
287
|
return {
|
|
250
288
|
kind: 'unknown',
|
|
251
289
|
identity,
|
|
252
|
-
reason: `${ownerLock} does not agree with the direct
|
|
290
|
+
reason: `${ownerLock} does not agree with the direct ${section} entry in ${ownerManifest}`,
|
|
253
291
|
}
|
|
254
292
|
}
|
|
255
293
|
const scope: InstallScope = (await samePath(join(options.globalBin, 'ab'), identity.binaryPath))
|
|
@@ -280,6 +318,8 @@ export async function inspectInstallation(options: {
|
|
|
280
318
|
ownerManifest,
|
|
281
319
|
ownerLock,
|
|
282
320
|
dependency,
|
|
321
|
+
section,
|
|
322
|
+
dual,
|
|
283
323
|
scope,
|
|
284
324
|
},
|
|
285
325
|
}
|
|
@@ -320,6 +360,8 @@ export async function inspectInstallation(options: {
|
|
|
320
360
|
owner: repository.owner,
|
|
321
361
|
repository: repository.repository,
|
|
322
362
|
dependency,
|
|
363
|
+
section,
|
|
364
|
+
dual,
|
|
323
365
|
scope,
|
|
324
366
|
},
|
|
325
367
|
}
|
|
@@ -19,7 +19,7 @@ import type { Forge } from '../ports/types'
|
|
|
19
19
|
import type { Exec } from '../ports/workspace/git-worktree'
|
|
20
20
|
import type { IdSource } from '../ids'
|
|
21
21
|
import { reduceBuild } from '../kernel/reducer'
|
|
22
|
-
import { artifactDownload, artifactGet, artifactPut } from './artifact'
|
|
22
|
+
import { artifactDownload, artifactDownloadStream, artifactGet, artifactPut } from './artifact'
|
|
23
23
|
import { multiFlag, parseArgs, stringFlag, type ParsedArgs } from './args'
|
|
24
24
|
import { abBuildControl, type BuildControlAction, type BuildControlResult } from './build-control'
|
|
25
25
|
import {
|
|
@@ -45,7 +45,7 @@ import { observe } from './observe'
|
|
|
45
45
|
import { abPlugin, type PluginContractSubprocess } from './plugin'
|
|
46
46
|
import { preparePrAttachments } from './pr-attachments'
|
|
47
47
|
import { renderPrSummary } from './pr-summary'
|
|
48
|
-
import { abBuilds, abBuildStatus } from './status'
|
|
48
|
+
import { abBuildAssetGet, abBuilds, abBuildStatus } from './status'
|
|
49
49
|
import { abRepositoryStatus } from './repository-status'
|
|
50
50
|
import { abWatch, WATCH_USAGE } from './watch'
|
|
51
51
|
import { abWait, WAIT_USAGE } from './wait'
|
|
@@ -478,6 +478,7 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
478
478
|
const report = await (deps.initValidation ?? validateInitReadiness)({
|
|
479
479
|
targetRepo,
|
|
480
480
|
env,
|
|
481
|
+
processEnv: deps.processEnv ?? process.env,
|
|
481
482
|
stdout,
|
|
482
483
|
...(deps.exec !== undefined ? { exec: deps.exec } : {}),
|
|
483
484
|
...(deps.initRuntimes !== undefined ? { runtimes: deps.initRuntimes } : {}),
|
|
@@ -790,9 +791,44 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
790
791
|
}
|
|
791
792
|
|
|
792
793
|
case 'build': {
|
|
793
|
-
const usage =
|
|
794
|
+
const usage =
|
|
795
|
+
'usage: ab build status <slug> [--events <n>] [--json] [--store <ref>] (§8.2)\n' +
|
|
796
|
+
' ab build asset get <slug> <kind> <name> <dest> [--rev <n>] [--json] [--store <ref>] (§8.8)'
|
|
794
797
|
const [sub, ...more] = rest
|
|
795
|
-
|
|
798
|
+
if (sub === 'asset') {
|
|
799
|
+
const [action, ...assetArgs] = more
|
|
800
|
+
if (action !== 'get') throw new Error(usage)
|
|
801
|
+
const assetParsed = parseArgs(
|
|
802
|
+
assetArgs,
|
|
803
|
+
{ rev: 'value', json: 'boolean', store: 'value' },
|
|
804
|
+
usage,
|
|
805
|
+
)
|
|
806
|
+
const [slug, kind, name, dest, ...extra] = assetParsed.positionals
|
|
807
|
+
if (!slug || !kind || !name || !dest || extra.length > 0) throw new Error(usage)
|
|
808
|
+
const rawRev = stringFlag(assetParsed, 'rev')
|
|
809
|
+
if (rawRev !== undefined && !/^\d+$/.test(rawRev)) {
|
|
810
|
+
throw new Error(`--rev must be a nonnegative integer, got "${rawRev}" — ${usage}`)
|
|
811
|
+
}
|
|
812
|
+
const assetStore = stringFlag(assetParsed, 'store')
|
|
813
|
+
if (deps.exec === undefined) {
|
|
814
|
+
throw new Error("'ab build' needs an exec seam — this is a wiring bug in the ab binary")
|
|
815
|
+
}
|
|
816
|
+
await abBuildAssetGet({
|
|
817
|
+
targetRepo: deps.workspacePath,
|
|
818
|
+
env: deps.processEnv ?? {},
|
|
819
|
+
exec: deps.exec,
|
|
820
|
+
stdout,
|
|
821
|
+
slug,
|
|
822
|
+
kind,
|
|
823
|
+
name,
|
|
824
|
+
dest,
|
|
825
|
+
json: assetParsed.flags.has('json'),
|
|
826
|
+
...(rawRev !== undefined ? { rev: Number(rawRev) } : {}),
|
|
827
|
+
...(assetStore !== undefined ? { storeRef: assetStore } : {}),
|
|
828
|
+
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
829
|
+
})
|
|
830
|
+
return 0
|
|
831
|
+
}
|
|
796
832
|
if (sub !== 'status') {
|
|
797
833
|
throw new Error(usage)
|
|
798
834
|
}
|
|
@@ -1211,8 +1247,37 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1211
1247
|
const usage =
|
|
1212
1248
|
'usage: ab artifact download <build> <kind>[@rev] --output <file> [--store <ref>] (§8.2)'
|
|
1213
1249
|
const parsed = parseArgs(more, { output: 'value', store: 'value' }, usage)
|
|
1214
|
-
const [
|
|
1250
|
+
const [first, second] = parsed.positionals
|
|
1215
1251
|
const outputPath = stringFlag(parsed, 'output')
|
|
1252
|
+
const storeRef = stringFlag(parsed, 'store')
|
|
1253
|
+
if (deps.exec === undefined) {
|
|
1254
|
+
throw new Error(
|
|
1255
|
+
"'ab artifact download' needs an exec seam — this is a wiring bug in the ab binary",
|
|
1256
|
+
)
|
|
1257
|
+
}
|
|
1258
|
+
// A stream-addressed first positional can never be a build slug: the
|
|
1259
|
+
// dispatcher's slug charset forbids ':'. Repo-scoped streams take the
|
|
1260
|
+
// one-positional operator form; everything else stays on the unchanged
|
|
1261
|
+
// build-scoped path.
|
|
1262
|
+
if (first?.startsWith('stream:')) {
|
|
1263
|
+
const streamUsage =
|
|
1264
|
+
'usage: ab artifact download stream:<id>[@rev] --output <file> [--store <ref>] (§8.2)'
|
|
1265
|
+
if (second !== undefined || outputPath === undefined) throw new Error(streamUsage)
|
|
1266
|
+
const downloaded = await artifactDownloadStream({
|
|
1267
|
+
targetRepo: deps.workspacePath,
|
|
1268
|
+
env: deps.processEnv ?? {},
|
|
1269
|
+
exec: deps.exec,
|
|
1270
|
+
spec: first,
|
|
1271
|
+
outputPath,
|
|
1272
|
+
...(storeRef !== undefined ? { storeRef } : {}),
|
|
1273
|
+
...(deps.openStore !== undefined ? { openStore: deps.openStore } : {}),
|
|
1274
|
+
})
|
|
1275
|
+
stdout(
|
|
1276
|
+
`downloaded ${downloaded.artifact.meta.kind}@${downloaded.artifact.meta.revision} to ${downloaded.outputPath}`,
|
|
1277
|
+
)
|
|
1278
|
+
return 0
|
|
1279
|
+
}
|
|
1280
|
+
const [build, spec] = parsed.positionals
|
|
1216
1281
|
if (
|
|
1217
1282
|
build === undefined ||
|
|
1218
1283
|
spec === undefined ||
|
|
@@ -1221,12 +1286,6 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1221
1286
|
) {
|
|
1222
1287
|
throw new Error(usage)
|
|
1223
1288
|
}
|
|
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
1289
|
const downloaded = await artifactDownload({
|
|
1231
1290
|
targetRepo: deps.workspacePath,
|
|
1232
1291
|
env: deps.processEnv ?? {},
|
|
@@ -26,6 +26,8 @@ import type { Config } from '../config/schema'
|
|
|
26
26
|
import type { Via } from '../events/envelope'
|
|
27
27
|
import { defaultTriageState } from '../processes/dispatcher'
|
|
28
28
|
import { createWorkspaceProvider } from '../ports/workspace/create'
|
|
29
|
+
import { createForge } from '../ports/forge/create'
|
|
30
|
+
import type { Forge } from '../ports/types'
|
|
29
31
|
import { createTicketSource } from '../ports/tickets/create'
|
|
30
32
|
import { loadPlugins } from '../plugins/load'
|
|
31
33
|
import { AUTOBUILD_VERSION } from '../store/remote/version'
|
|
@@ -221,8 +223,9 @@ export async function abMcp(opts: AbMcpOpts): Promise<number> {
|
|
|
221
223
|
throw error
|
|
222
224
|
}
|
|
223
225
|
if (mcpConfig.orchestrator.enabled) {
|
|
226
|
+
const plugins = await loadPlugins(mcpConfig.plugins, context.checkout)
|
|
224
227
|
const provider = await createWorkspaceProvider(mcpConfig.workspace, {
|
|
225
|
-
registry:
|
|
228
|
+
registry: plugins,
|
|
226
229
|
worktreeRoot: context.worktreeRoot,
|
|
227
230
|
repoRoot: context.checkout,
|
|
228
231
|
env: opts.env,
|
|
@@ -231,13 +234,32 @@ export async function abMcp(opts: AbMcpOpts): Promise<number> {
|
|
|
231
234
|
sandboxSetupCommand: mcpConfig.commands.setup,
|
|
232
235
|
sandboxRoot: join(context.localStateRoot, 'orchestrator-sandboxes'),
|
|
233
236
|
sandboxEnvironmentVariables: mcpConfig.orchestrator.sandbox.environmentVariables,
|
|
237
|
+
orchestratorSandboxEnabled: true,
|
|
234
238
|
})
|
|
239
|
+
// The forge is needed only for sandbox.publish; a construction
|
|
240
|
+
// failure must not drop every sandbox tool, so it is contained here
|
|
241
|
+
// and publication is silently omitted (canPublish stays false).
|
|
242
|
+
let forge: Forge | undefined
|
|
243
|
+
try {
|
|
244
|
+
forge = await createForge({
|
|
245
|
+
name: mcpConfig.forge,
|
|
246
|
+
registry: plugins,
|
|
247
|
+
env: opts.env,
|
|
248
|
+
repoRoot: context.checkout,
|
|
249
|
+
repository: context.repo,
|
|
250
|
+
})
|
|
251
|
+
} catch (forgeError) {
|
|
252
|
+
opts.stderr(
|
|
253
|
+
`ab mcp: forge unavailable, sandbox.publish omitted: ${forgeError instanceof Error ? forgeError.message : String(forgeError)}`,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
235
256
|
sandbox = await createOperatorSandboxService({
|
|
236
257
|
store: context.store,
|
|
237
258
|
repo: context.repo,
|
|
238
259
|
provider,
|
|
239
260
|
sandbox: mcpConfig.orchestrator.sandbox,
|
|
240
261
|
baseBranch: mcpConfig.baseBranch,
|
|
262
|
+
...(forge !== undefined ? { forge } : {}),
|
|
241
263
|
...(opts.clock !== undefined ? { clock: opts.clock } : {}),
|
|
242
264
|
})
|
|
243
265
|
}
|
|
@@ -77,6 +77,9 @@ function renderModuleReport(report: PluginDiagnosis['reports'][number]): string
|
|
|
77
77
|
report.api === undefined
|
|
78
78
|
? ''
|
|
79
79
|
: ` api=${report.api.declaredRange} host=${report.api.hostVersion} ${report.api.status}`
|
|
80
|
+
if (report.status === 'skipped') {
|
|
81
|
+
return `SKIP ${report.module} kind=${report.resolutionKind} stage=${report.stage}${location}${source}${plugin}${api}: ${report.notice}`
|
|
82
|
+
}
|
|
80
83
|
return report.status === 'loaded'
|
|
81
84
|
? `OK ${report.module} kind=${report.resolutionKind}${location}${source}${plugin}${api}`
|
|
82
85
|
: `FAIL ${report.module} kind=${report.resolutionKind} stage=${report.stage}${location}${source}${plugin}${api}: ${report.error}`
|
|
@@ -149,8 +152,8 @@ export async function abPlugin(argv: readonly string[], opts: PluginCliOpts): Pr
|
|
|
149
152
|
if (diagnosis.reports.length === 0) opts.stdout(' (none)')
|
|
150
153
|
for (const report of diagnosis.reports) {
|
|
151
154
|
const line = ` ${renderModuleReport(report)}`
|
|
152
|
-
if (report.status === '
|
|
153
|
-
else opts.
|
|
155
|
+
if (report.status === 'failed') opts.stderr(line)
|
|
156
|
+
else opts.stdout(line)
|
|
154
157
|
}
|
|
155
158
|
opts.stdout('adapters:')
|
|
156
159
|
for (const port of PLUGIN_PORTS) {
|
|
@@ -170,8 +173,8 @@ export async function abPlugin(argv: readonly string[], opts: PluginCliOpts): Pr
|
|
|
170
173
|
}
|
|
171
174
|
for (const report of diagnosis.reports) {
|
|
172
175
|
const line = renderModuleReport(report)
|
|
173
|
-
if (report.status === '
|
|
174
|
-
else opts.
|
|
176
|
+
if (report.status === 'failed') opts.stderr(line)
|
|
177
|
+
else opts.stdout(line)
|
|
175
178
|
}
|
|
176
179
|
return diagnosis.healthy ? 0 : 1
|
|
177
180
|
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared remote long-poll task machinery behind `ab watch` and `ab wait`
|
|
3
|
+
* (AUT-334, corrected for `ab wait` in AUT-368, extracted in AUT-403). One
|
|
4
|
+
* long-poll task per tracked stream runs concurrently against an `http(s)`
|
|
5
|
+
* store: a held `getEvents` request per stream is always in flight, gap-fill
|
|
6
|
+
* spaces request starts at least one interval apart, and every stop — a
|
|
7
|
+
* domain condition, an external abort, or an elapsed deadline — cancels the
|
|
8
|
+
* in-flight holds and wakes the gap sleeps instead of waiting out a full
|
|
9
|
+
* hold. A hold's own bound is capped at the remaining time budget.
|
|
10
|
+
*
|
|
11
|
+
* The two commands keep their different semantics through injected
|
|
12
|
+
* callbacks (`shouldStop`, per-task `endCheck`, the discovery step) and two
|
|
13
|
+
* explicit configuration points (`drain` mode, `deadlineMs`); everything
|
|
14
|
+
* mechanical lives here, so a fix to streak or stop handling cannot land in
|
|
15
|
+
* one command and silently miss the other.
|
|
16
|
+
*
|
|
17
|
+
* Timing is fully injected (`now`, `sleep`): the module never reads the wall
|
|
18
|
+
* clock and never schedules a real timer, so tests drive every wait
|
|
19
|
+
* deterministically.
|
|
20
|
+
*/
|
|
21
|
+
import { REMOTE_EVENT_WAIT_SECONDS } from '../store/remote/client'
|
|
22
|
+
|
|
23
|
+
/** The read options one held long-poll request carries. */
|
|
24
|
+
export interface RemotePollReadOpts {
|
|
25
|
+
waitSeconds?: number
|
|
26
|
+
signal?: AbortSignal
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The default abortable sleep: resolves after `ms`, or as soon as `signal`
|
|
31
|
+
* aborts. Shared verbatim by `ab watch` and `ab wait` as the `delay` default.
|
|
32
|
+
*/
|
|
33
|
+
export function defaultDelay(ms: number, signal?: AbortSignal): Promise<void> {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
if (signal?.aborted === true) {
|
|
36
|
+
resolve()
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
const finish = (): void => {
|
|
40
|
+
clearTimeout(timer)
|
|
41
|
+
signal?.removeEventListener('abort', onAbort)
|
|
42
|
+
resolve()
|
|
43
|
+
}
|
|
44
|
+
const timer = setTimeout(finish, ms)
|
|
45
|
+
const onAbort = (): void => finish()
|
|
46
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A failed read is reported once per failure streak, advances nothing, and
|
|
52
|
+
* is retried at the next interval (local) or next request (remote); a
|
|
53
|
+
* success on the same source re-arms the report. Each read source — per
|
|
54
|
+
* stream task, plus discovery — carries its own streak, so a persistent
|
|
55
|
+
* failure on one stream interleaved with successes elsewhere is still
|
|
56
|
+
* reported only once.
|
|
57
|
+
*/
|
|
58
|
+
export function makeFailureStreak(
|
|
59
|
+
command: string,
|
|
60
|
+
stderr: (line: string) => void,
|
|
61
|
+
): { onFailure: (error: unknown) => void; onSuccess: () => void } {
|
|
62
|
+
let reported = false
|
|
63
|
+
return {
|
|
64
|
+
onFailure: (error: unknown): void => {
|
|
65
|
+
if (reported) return
|
|
66
|
+
reported = true
|
|
67
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
68
|
+
stderr(`ab ${command}: a store read failed (${message}); retrying at the next interval`)
|
|
69
|
+
},
|
|
70
|
+
onSuccess: (): void => {
|
|
71
|
+
reported = false
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RemotePollRunnerOpts {
|
|
77
|
+
/** `'watch' | 'wait'` — the streak message's command prefix. */
|
|
78
|
+
command: string
|
|
79
|
+
stderr: (line: string) => void
|
|
80
|
+
/** Injected clock; never the wall clock directly. */
|
|
81
|
+
now: () => Date
|
|
82
|
+
/** Injected delay; every wait the runner makes goes through this. */
|
|
83
|
+
sleep: (ms: number, signal?: AbortSignal) => Promise<void>
|
|
84
|
+
/** The gap-fill floor between request starts. */
|
|
85
|
+
intervalMs: number
|
|
86
|
+
/** Epoch ms at which reads stop; `Number.POSITIVE_INFINITY` for an
|
|
87
|
+
* unbounded `--timeout 0`. */
|
|
88
|
+
deadlineMs: number
|
|
89
|
+
/** The external signal only (SIGINT) — never widened to the domain stop
|
|
90
|
+
* terms, so a failure observed as the deadline elapses is still reported. */
|
|
91
|
+
aborted: () => boolean
|
|
92
|
+
/** The loop-guard predicate: `aborted() || deadline elapsed || domain
|
|
93
|
+
* stop`. Evaluated at the top of every task cycle. */
|
|
94
|
+
shouldStop: () => boolean
|
|
95
|
+
/** Invoked on every runner stop — a task's endCheck, the discovery task's
|
|
96
|
+
* endCheck, or a caller-initiated `requestStop`. `ab watch` trips its
|
|
97
|
+
* shared `stop` flag here: `pollBuild`/`pollRepository` gate their batch
|
|
98
|
+
* loops on that flag (`if (stop) break`), so a sibling task whose held
|
|
99
|
+
* read has already resolved must stop delivering its batch the moment any
|
|
100
|
+
* task observes the terminal condition — exactly what the pre-extraction
|
|
101
|
+
* command-scoped `requestStop` did (f_42263f38). May fire more than once
|
|
102
|
+
* (every `requestStop` call fires it); the callback must be idempotent. */
|
|
103
|
+
onStop?: () => void
|
|
104
|
+
/** How `drain()` treats tasks launched while it is awaiting.
|
|
105
|
+
* `'snapshot'`: await only the tasks present when `drain()` is called
|
|
106
|
+
* (`ab watch`'s one-shot semantics). `'quiesce'`: keep awaiting until no
|
|
107
|
+
* task remains (`ab wait`'s semantics — the exit decision must observe
|
|
108
|
+
* every task settled). */
|
|
109
|
+
drain: 'snapshot' | 'quiesce'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface RemotePollRunner {
|
|
113
|
+
/** Whether a stop has been requested — by `requestStop` or a task's own
|
|
114
|
+
* `endCheck`. */
|
|
115
|
+
readonly stopped: boolean
|
|
116
|
+
/** Abort every in-flight held read, wake the gap sleeps, mark stopped. */
|
|
117
|
+
requestStop(): void
|
|
118
|
+
/** Launch one per-stream long-poll task; idempotent per key. `endCheck`
|
|
119
|
+
* runs after each successful poll; true ⇒ requestStop + return. */
|
|
120
|
+
launch(
|
|
121
|
+
key: string,
|
|
122
|
+
poll: (readOpts: RemotePollReadOpts) => Promise<unknown>,
|
|
123
|
+
endCheck?: () => boolean,
|
|
124
|
+
): void
|
|
125
|
+
/** The interval-cadence discovery task, self-tracked for drain. `step`
|
|
126
|
+
* (the discovery read) runs inside the try/catch; `launchPending` (the
|
|
127
|
+
* launch-everything loop) runs UNCONDITIONALLY after the catch, so streams
|
|
128
|
+
* registered before a mid-discovery throw are still launched; `endCheck`
|
|
129
|
+
* (wait only) runs after `launchPending`, throw or not. */
|
|
130
|
+
runDiscovery(opts: {
|
|
131
|
+
step: () => Promise<void>
|
|
132
|
+
launchPending: () => void
|
|
133
|
+
endCheck?: () => boolean
|
|
134
|
+
}): Promise<void>
|
|
135
|
+
/** Await launched tasks per the configured strategy. */
|
|
136
|
+
drain(): Promise<void>
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Build the per-command runner over the shared machinery. Every piece is
|
|
141
|
+
* moved verbatim from the command-local copies this module replaces; the
|
|
142
|
+
* commands' different stop and drain semantics enter only through `opts`.
|
|
143
|
+
*/
|
|
144
|
+
export function createRemotePollRunner(opts: RemotePollRunnerOpts): RemotePollRunner {
|
|
145
|
+
// Held reads in flight right now, each with its own controller. When the
|
|
146
|
+
// command stops for any reason, every in-flight read is cancelled —
|
|
147
|
+
// otherwise a stop during a quiet window would wait out a full wait
|
|
148
|
+
// window (up to the remote hold) before the command could exit.
|
|
149
|
+
const inFlightReads = new Set<AbortController>()
|
|
150
|
+
// Wakes stop-aware gap sleeps the moment the command stops.
|
|
151
|
+
const stopController = new AbortController()
|
|
152
|
+
let stopped = false
|
|
153
|
+
const requestStop = (): void => {
|
|
154
|
+
stopped = true
|
|
155
|
+
opts.onStop?.()
|
|
156
|
+
stopController.abort()
|
|
157
|
+
for (const controller of inFlightReads) controller.abort()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The wait bound for one held read: the remote default, capped at the
|
|
161
|
+
* command's remaining time budget. With an unbounded deadline the same
|
|
162
|
+
* expression yields exactly the remote default, so one form covers both. */
|
|
163
|
+
const readWaitSeconds = (atMs: number): number => {
|
|
164
|
+
const remaining = Math.floor((opts.deadlineMs - atMs) / 1000)
|
|
165
|
+
return Math.max(0, Math.min(REMOTE_EVENT_WAIT_SECONDS, remaining))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// One self-cleaning task set for both commands. Awaiting a snapshot that
|
|
169
|
+
// omits already-settled tasks is equivalent to awaiting the plain array
|
|
170
|
+
// the one-shot drain used before the extraction.
|
|
171
|
+
const tasks = new Set<Promise<void>>()
|
|
172
|
+
/** Track a task and keep the set accurate as tasks settle, so the quiesce
|
|
173
|
+
* drain also waits for tasks launched while it is awaiting. */
|
|
174
|
+
const track = (task: Promise<void>): void => {
|
|
175
|
+
tasks.add(task)
|
|
176
|
+
void task.then(
|
|
177
|
+
() => tasks.delete(task),
|
|
178
|
+
() => tasks.delete(task),
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const runStreamTask = async (
|
|
183
|
+
poll: (readOpts: RemotePollReadOpts) => Promise<unknown>,
|
|
184
|
+
endCheck?: () => boolean,
|
|
185
|
+
): Promise<void> => {
|
|
186
|
+
const streak = makeFailureStreak(opts.command, opts.stderr)
|
|
187
|
+
while (!stopped && !opts.shouldStop()) {
|
|
188
|
+
const started = opts.now().getTime()
|
|
189
|
+
const controller = new AbortController()
|
|
190
|
+
inFlightReads.add(controller)
|
|
191
|
+
try {
|
|
192
|
+
await poll({ waitSeconds: readWaitSeconds(started), signal: controller.signal })
|
|
193
|
+
streak.onSuccess()
|
|
194
|
+
if (endCheck?.()) {
|
|
195
|
+
requestStop()
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
} catch (error) {
|
|
199
|
+
// A cancelled held read is the command stopping, not a store
|
|
200
|
+
// failure — never report it. The check stays on the external signal
|
|
201
|
+
// (not the full guard), so a failure observed as the deadline
|
|
202
|
+
// elapses is still reported.
|
|
203
|
+
if (!stopped && !opts.aborted() && !controller.signal.aborted) streak.onFailure(error)
|
|
204
|
+
} finally {
|
|
205
|
+
inFlightReads.delete(controller)
|
|
206
|
+
}
|
|
207
|
+
if (stopped || opts.aborted()) return
|
|
208
|
+
const elapsed = opts.now().getTime() - started
|
|
209
|
+
if (elapsed < opts.intervalMs) {
|
|
210
|
+
await opts.sleep(opts.intervalMs - elapsed, stopController.signal)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const launched = new Set<string>()
|
|
216
|
+
const launch = (
|
|
217
|
+
key: string,
|
|
218
|
+
poll: (readOpts: RemotePollReadOpts) => Promise<unknown>,
|
|
219
|
+
endCheck?: () => boolean,
|
|
220
|
+
): void => {
|
|
221
|
+
if (launched.has(key)) return
|
|
222
|
+
launched.add(key)
|
|
223
|
+
track(runStreamTask(poll, endCheck))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const runDiscovery = (discovery: {
|
|
227
|
+
step: () => Promise<void>
|
|
228
|
+
launchPending: () => void
|
|
229
|
+
endCheck?: () => boolean
|
|
230
|
+
}): Promise<void> => {
|
|
231
|
+
const task = (async (): Promise<void> => {
|
|
232
|
+
const streak = makeFailureStreak(opts.command, opts.stderr)
|
|
233
|
+
while (!stopped && !opts.shouldStop()) {
|
|
234
|
+
try {
|
|
235
|
+
await discovery.step()
|
|
236
|
+
streak.onSuccess()
|
|
237
|
+
} catch (error) {
|
|
238
|
+
streak.onFailure(error)
|
|
239
|
+
}
|
|
240
|
+
// Unconditional — after the catch: streams registered before a
|
|
241
|
+
// mid-discovery throw still get tasks whose polls then run
|
|
242
|
+
// independently.
|
|
243
|
+
discovery.launchPending()
|
|
244
|
+
if (discovery.endCheck?.()) {
|
|
245
|
+
requestStop()
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
if (stopped || opts.aborted()) return
|
|
249
|
+
await opts.sleep(opts.intervalMs, stopController.signal)
|
|
250
|
+
}
|
|
251
|
+
})()
|
|
252
|
+
track(task)
|
|
253
|
+
return task
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const drain = async (): Promise<void> => {
|
|
257
|
+
if (opts.drain === 'quiesce') {
|
|
258
|
+
// Await every task — including ones launched while awaiting: the
|
|
259
|
+
// discovery task adds stream tasks mid-wait, and the caller's exit
|
|
260
|
+
// decision must observe every task settled.
|
|
261
|
+
while (tasks.size > 0) await Promise.all(tasks)
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
// One-shot snapshot: tasks launched during the await are NOT awaited.
|
|
265
|
+
await Promise.all([...tasks])
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
get stopped(): boolean {
|
|
270
|
+
return stopped
|
|
271
|
+
},
|
|
272
|
+
requestStop,
|
|
273
|
+
launch,
|
|
274
|
+
runDiscovery,
|
|
275
|
+
drain,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -190,6 +190,28 @@ export async function buildInRepository(
|
|
|
190
190
|
return normalizeGitRemoteUrl(record.repoOrigin) === identity
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Whether a repo-scoped stream's recorded scope belongs to `identity` — the
|
|
195
|
+
* same origin-aware first arm `buildInRepository` applies to builds, with the
|
|
196
|
+
* recorded side normalized like `record.repoOrigin` there so the guard never
|
|
197
|
+
* depends on the writer's normalizer vintage (an scp-like ssh-spelled scope
|
|
198
|
+
* equals the https identity of the same origin).
|
|
199
|
+
*
|
|
200
|
+
* Full parity with `buildInRepository` is impossible by construction:
|
|
201
|
+
* `StreamScope` carries no `repoOrigin`, so a legacy physical-path
|
|
202
|
+
* `scope.repo` (which `normalizeGitRemoteUrl` passes through unchanged) has no
|
|
203
|
+
* recorded origin to compare and can never match an origin identity — it stays
|
|
204
|
+
* rejected. That is the accepted AUT-314-class limitation, not something this
|
|
205
|
+
* guard can forgive.
|
|
206
|
+
*
|
|
207
|
+
* No `exec` parameter: the caller's identity is already resolved (the
|
|
208
|
+
* context's `repo` is `origin ?? resolved checkout` from `resolveRepoState`),
|
|
209
|
+
* so re-running git would be redundant.
|
|
210
|
+
*/
|
|
211
|
+
export function streamInRepository(scopeRepo: string, identity: string): boolean {
|
|
212
|
+
return normalizeGitRemoteUrl(scopeRepo) === identity
|
|
213
|
+
}
|
|
214
|
+
|
|
193
215
|
/** Resolve repository identity, then select all state paths from it.
|
|
194
216
|
*
|
|
195
217
|
* Identity is the resolved main checkout's normalized origin remote, falling
|