@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,681 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end harness (SPEC §15.6 as LIVE scenarios, §8.7, §12, §15.7):
|
|
3
|
+
* real components everywhere the seam matters.
|
|
4
|
+
*
|
|
5
|
+
* - ONE MemoryBuildStore shared by dispatcher, build-runner, and every CLI
|
|
6
|
+
* invocation (steppingClock so store-assigned `ts` are deterministic).
|
|
7
|
+
* - A REAL temp git repo with a real bare `origin` remote and a committed
|
|
8
|
+
* autobuild.toml fixture (§16.1 D9) whose check command is controllable:
|
|
9
|
+
* `test -f ok.marker` fails until an implement round commits the marker.
|
|
10
|
+
* - REAL worktrees via GitWorktreeProvider for the dispatcher's provisioning.
|
|
11
|
+
* - A Dispatcher whose `launchRunner` constructs a REAL in-process
|
|
12
|
+
* BuildRunner over the SAME store and the provisioned workspace path.
|
|
13
|
+
* - THE POINT — scripted agents drive the REAL `ab` CLI: each script derives
|
|
14
|
+
* CliDeps from its ScriptContext (store = the shared store, env =
|
|
15
|
+
* resolveCliEnv(ctx.opts.env), workspacePath = ctx.opts.workspacePath,
|
|
16
|
+
* forge = the shared FakeForge, exec = spawnExec — real git) and invokes
|
|
17
|
+
* runCli(argv, deps) exactly like an agent typing `ab` commands (§8).
|
|
18
|
+
* A nonzero exit inside a script is a seam failure: it is recorded in
|
|
19
|
+
* `cliErrors` and thrown with the captured stderr.
|
|
20
|
+
*
|
|
21
|
+
* Only the world's edges are fakes, both journaled: FakeForge (D7 §8.6 —
|
|
22
|
+
* agents never touch the remote, so a journal IS the remote) and
|
|
23
|
+
* FakeTicketSource (§13 — projections flow outward only).
|
|
24
|
+
*/
|
|
25
|
+
import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
|
|
26
|
+
import { tmpdir } from 'node:os'
|
|
27
|
+
import { dirname, join } from 'node:path'
|
|
28
|
+
import type { DispatchWiring } from '../cli/dispatch'
|
|
29
|
+
import { resolveCliEnv, type CliEnv } from '../cli/env'
|
|
30
|
+
import { runCli } from '../cli/main'
|
|
31
|
+
import { parseConfig } from '../config/load'
|
|
32
|
+
import type { Config } from '../config/schema'
|
|
33
|
+
import type { AbEvent } from '../events/catalog'
|
|
34
|
+
import type { EventType } from '../events/payloads'
|
|
35
|
+
import { sequentialIds, sequentialUuids, type IdSource } from '../ids'
|
|
36
|
+
import type { BuildState } from '../kernel/reducer'
|
|
37
|
+
import { createPluginRegistry, type PluginRegistry } from '../plugins/registry'
|
|
38
|
+
import { materializePluginRuntimes } from '../plugins/runtimes'
|
|
39
|
+
import type { RuntimeRegistry } from '../ports/runner/runtime'
|
|
40
|
+
import { createForge } from '../ports/forge/create'
|
|
41
|
+
import { FakeForge } from '../ports/forge/fake'
|
|
42
|
+
import { LocalGitForge } from '../ports/forge/local-git'
|
|
43
|
+
import {
|
|
44
|
+
defaultTurnResult,
|
|
45
|
+
ScriptedAgentRunner,
|
|
46
|
+
type Script,
|
|
47
|
+
type ScriptContext,
|
|
48
|
+
} from '../ports/runner/fake'
|
|
49
|
+
import { FakeTicketSource } from '../ports/tickets/fake'
|
|
50
|
+
import type {
|
|
51
|
+
AgentTurnResult,
|
|
52
|
+
Forge,
|
|
53
|
+
Ticket,
|
|
54
|
+
TicketSource,
|
|
55
|
+
WorkspaceProvider,
|
|
56
|
+
} from '../ports/types'
|
|
57
|
+
import { GitWorktreeProvider, spawnExec } from '../ports/workspace/git-worktree'
|
|
58
|
+
import { InProcessBuildExecution } from '../ports/workspace/in-process-build-execution'
|
|
59
|
+
import { BuildRunner, LeaseHeldError, SetupFailureError } from '../processes/build-runner'
|
|
60
|
+
import { diagnosticArtifact } from '../processes/build-execution-state'
|
|
61
|
+
import { Dispatcher, type LaunchRunnerResult } from '../processes/dispatcher'
|
|
62
|
+
import { MemoryBuildStore } from '../store/memory'
|
|
63
|
+
import type { BuildStore } from '../store/types'
|
|
64
|
+
import { steppingClock } from '../testing/fixed'
|
|
65
|
+
|
|
66
|
+
// ── The committed autobuild.toml fixture (§16.1) ─────────────────────────────
|
|
67
|
+
//
|
|
68
|
+
// The check command is the harness's control knob (§15.6-A): `test -f
|
|
69
|
+
// ok.marker` fails until an implement round commits the marker file.
|
|
70
|
+
|
|
71
|
+
export const CONFIG_TOML = `
|
|
72
|
+
baseBranch = "main"
|
|
73
|
+
capacity = 2
|
|
74
|
+
|
|
75
|
+
[commands]
|
|
76
|
+
test = "test -f ok.marker"
|
|
77
|
+
|
|
78
|
+
[verify]
|
|
79
|
+
steps = ["unit"]
|
|
80
|
+
|
|
81
|
+
[verify.unit]
|
|
82
|
+
kind = "check"
|
|
83
|
+
command = "test"
|
|
84
|
+
|
|
85
|
+
[policy]
|
|
86
|
+
stallRounds = 3
|
|
87
|
+
|
|
88
|
+
[roles.default]
|
|
89
|
+
runtime = "scripted"
|
|
90
|
+
|
|
91
|
+
[tickets]
|
|
92
|
+
source = "file"
|
|
93
|
+
readyLabels = ["autobuild"]
|
|
94
|
+
readyState = "Ready"
|
|
95
|
+
`
|
|
96
|
+
|
|
97
|
+
/** Conforming per the dispatcher's exported heuristic (§6.3,
|
|
98
|
+
* docs/spec-standard.md): nonempty body, '## Acceptance criteria' with a
|
|
99
|
+
* list item, '## Out of scope'. */
|
|
100
|
+
export const CONFORMING_BODY = [
|
|
101
|
+
'Login attempts are currently unlimited; throttle repeated failures.',
|
|
102
|
+
'',
|
|
103
|
+
'## Acceptance criteria',
|
|
104
|
+
'',
|
|
105
|
+
'- a sixth failed login within five minutes returns 429',
|
|
106
|
+
'',
|
|
107
|
+
'## Out of scope',
|
|
108
|
+
'',
|
|
109
|
+
'- captcha',
|
|
110
|
+
'',
|
|
111
|
+
].join('\n')
|
|
112
|
+
|
|
113
|
+
export function readyTicket(id: string, over: Partial<Omit<Ticket, 'ref'>> = {}): Ticket {
|
|
114
|
+
const title = over.title ?? 'Add rate limiting'
|
|
115
|
+
return {
|
|
116
|
+
ref: { source: 'fake', id, title },
|
|
117
|
+
title,
|
|
118
|
+
body: over.body ?? CONFORMING_BODY,
|
|
119
|
+
state: over.state ?? 'Ready',
|
|
120
|
+
labels: over.labels ?? ['autobuild'],
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Real-git helpers ─────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
/** Identity/signing pinned per invocation so tests ignore user git config. */
|
|
127
|
+
export const GIT_ID = [
|
|
128
|
+
'-c',
|
|
129
|
+
'user.email=ab@e2e.invalid',
|
|
130
|
+
'-c',
|
|
131
|
+
'user.name=ab-e2e',
|
|
132
|
+
'-c',
|
|
133
|
+
'commit.gpgsign=false',
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
export async function git(args: string[], cwd: string): Promise<string> {
|
|
137
|
+
const result = await spawnExec(['git', ...args], { cwd })
|
|
138
|
+
if (result.exitCode !== 0) {
|
|
139
|
+
throw new Error(`git ${args.join(' ')} failed: ${result.stderr}`)
|
|
140
|
+
}
|
|
141
|
+
return result.stdout.trim()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Stage everything and commit; returns the commit sha. `.ab/` never dirties
|
|
145
|
+
* the tree (§7) because `ab context` writes a self-excluding
|
|
146
|
+
* `.ab/.gitignore` — the origin deliberately does NOT ignore it, exactly
|
|
147
|
+
* like a real repo, so these scenarios exercise that mechanism. */
|
|
148
|
+
export async function commitAll(ws: string, message: string): Promise<string> {
|
|
149
|
+
await git(['add', '-A'], ws)
|
|
150
|
+
await git([...GIT_ID, 'commit', '-q', '-m', message], ws)
|
|
151
|
+
return git(['rev-parse', 'HEAD'], ws)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function writeFileIn(dir: string, rel: string, content: string): Promise<string> {
|
|
155
|
+
const path = join(dir, rel)
|
|
156
|
+
await mkdir(dirname(path), { recursive: true })
|
|
157
|
+
await writeFile(path, content)
|
|
158
|
+
return path
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function initOrigin(dir: string, configToml: string, noRemote = false): Promise<string> {
|
|
162
|
+
// Reconcile fetches the current base from the conventional `origin` remote
|
|
163
|
+
// at phase startup. Keep that seam real in every integration scenario: the
|
|
164
|
+
// dispatcher works from `dir`, while a separate bare repo is its remote.
|
|
165
|
+
const remote = join(dirname(dir), 'remote.git')
|
|
166
|
+
if (!noRemote) {
|
|
167
|
+
await mkdir(remote, { recursive: true })
|
|
168
|
+
await git(['init', '--bare', '-q', '-b', 'main'], remote)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
await mkdir(dir, { recursive: true })
|
|
172
|
+
await git(['init', '-q', '-b', 'main'], dir)
|
|
173
|
+
if (!noRemote) await git(['remote', 'add', 'origin', remote], dir)
|
|
174
|
+
// No `.ab/` ignore here — see commitAll: the product establishes it.
|
|
175
|
+
await writeFile(join(dir, 'autobuild.toml'), configToml)
|
|
176
|
+
await writeFile(join(dir, 'README.md'), 'e2e origin\n')
|
|
177
|
+
await git(['add', '-A'], dir)
|
|
178
|
+
await git([...GIT_ID, 'commit', '-q', '-m', 'initial'], dir)
|
|
179
|
+
if (!noRemote) await git(['push', '-q', '-u', 'origin', 'main'], dir)
|
|
180
|
+
return remote
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Event helpers ────────────────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
export function typesOf(events: AbEvent[]): string[] {
|
|
186
|
+
return events.map((event) => event.type)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function ofType<T extends EventType>(
|
|
190
|
+
events: AbEvent[],
|
|
191
|
+
type: T,
|
|
192
|
+
): Extract<AbEvent, { type: T }>[] {
|
|
193
|
+
return events.filter((event): event is Extract<AbEvent, { type: T }> => event.type === type)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The agent session an event was emitted under (throws on non-agent actors
|
|
197
|
+
* so a wrong-actor event fails the test loudly). */
|
|
198
|
+
export function agentSession(event: AbEvent): string {
|
|
199
|
+
if (event.actor.kind !== 'agent') {
|
|
200
|
+
throw new Error(`${event.type} (seq ${event.seq}) has a ${event.actor.kind} actor, not agent`)
|
|
201
|
+
}
|
|
202
|
+
return event.actor.session
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Latest open workspace's local path, with provider ref fallback for logs
|
|
206
|
+
* written before workspace path evidence was added. */
|
|
207
|
+
export function openWorkspacePath(events: AbEvent[]): string | null {
|
|
208
|
+
let open: string | null = null
|
|
209
|
+
for (const event of events) {
|
|
210
|
+
if (event.type === 'workspace.provisioned') {
|
|
211
|
+
open = event.payload.path ?? event.payload.ref
|
|
212
|
+
} else if (event.type === 'workspace.released') open = null
|
|
213
|
+
}
|
|
214
|
+
return open
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Scripted agents over the real CLI ────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* What a skill handler gets per turn: the real `ab` CLI bound to this
|
|
221
|
+
* session's ambient auth (D8) and the real worktree.
|
|
222
|
+
*/
|
|
223
|
+
export interface Cli {
|
|
224
|
+
/** Invoke the REAL `ab` CLI (runCli); returns stdout lines. A nonzero exit
|
|
225
|
+
* is a seam failure — recorded in harness.cliErrors and thrown with the
|
|
226
|
+
* captured stderr. */
|
|
227
|
+
run(argv: string[]): Promise<string[]>
|
|
228
|
+
/** Absolute path of the real worktree (ctx.opts.workspacePath). */
|
|
229
|
+
ws: string
|
|
230
|
+
/** This turn's round, resolved from ambient AB_PHASE (D8) — the runner
|
|
231
|
+
* re-issues the env on every continued producer turn (§10). */
|
|
232
|
+
round: number
|
|
233
|
+
/** The ambient env this turn resolved (D8). */
|
|
234
|
+
env: CliEnv
|
|
235
|
+
ctx: ScriptContext
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export type SkillHandler = (
|
|
239
|
+
cli: Cli,
|
|
240
|
+
) => Promise<AgentTurnResult | undefined | void> | AgentTurnResult | undefined | void
|
|
241
|
+
|
|
242
|
+
/** Scripts are routed by ctx.opts.skill — one handler per phase skill. */
|
|
243
|
+
export type SkillHandlers = Record<string, SkillHandler>
|
|
244
|
+
|
|
245
|
+
// ── The harness ──────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
export interface E2eHarness {
|
|
248
|
+
tmp: string
|
|
249
|
+
/** The real local checkout (also the dispatcher's `repo`). */
|
|
250
|
+
origin: string
|
|
251
|
+
/** The bare `origin` remote used by the local checkout. */
|
|
252
|
+
remote: string
|
|
253
|
+
store: MemoryBuildStore
|
|
254
|
+
clock: ReturnType<typeof steppingClock>
|
|
255
|
+
ids: IdSource
|
|
256
|
+
forge: FakeForge
|
|
257
|
+
/** Forge actually threaded through CLI, runner, and dispatcher. */
|
|
258
|
+
forgeAdapter: Forge
|
|
259
|
+
tickets: FakeTicketSource
|
|
260
|
+
workspaces: WorkspaceProvider
|
|
261
|
+
agents: ScriptedAgentRunner
|
|
262
|
+
dispatcher: Dispatcher
|
|
263
|
+
/** The same fakes, store, clocks, ids, workspaces, and scripted runtime as a
|
|
264
|
+
* DispatchWiring so callers can drive the real `abDispatch` composition. */
|
|
265
|
+
wiring: DispatchWiring
|
|
266
|
+
config: Config
|
|
267
|
+
/** BuildRunners constructed by the dispatcher's launchRunner, in order.
|
|
268
|
+
* launchRunner only CONSTRUCTS (the dispatcher never awaits a pipeline);
|
|
269
|
+
* tests run them via runLatest(). */
|
|
270
|
+
launched: Array<{ slug: string; runner: BuildRunner; instance: string }>
|
|
271
|
+
/** Nonzero-exit `ab` invocations (message + stderr). Scenarios assert []. */
|
|
272
|
+
cliErrors: string[]
|
|
273
|
+
/** Advance bare origin through an independent checkout, deliberately
|
|
274
|
+
* leaving the dispatcher's local base ref stale. */
|
|
275
|
+
advanceRemote(changes: Record<string, string>, message: string): Promise<string>
|
|
276
|
+
/** Run the most recently launched BuildRunner to its park point (§11). */
|
|
277
|
+
runLatest(): Promise<BuildState>
|
|
278
|
+
events(slug: string): Promise<AbEvent[]>
|
|
279
|
+
cleanup(): Promise<void>
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function makeHarness(opts: {
|
|
283
|
+
handlers: SkillHandlers
|
|
284
|
+
tickets?: Ticket[]
|
|
285
|
+
/** Drive the dispatcher with a REAL TicketSource instead of the fake — e.g.
|
|
286
|
+
* FileTicketSource, to prove the source's own dependency representation and
|
|
287
|
+
* lifecycle end-to-end. `h.tickets` stays the (then unused) fake. */
|
|
288
|
+
ticketSource?: TicketSource
|
|
289
|
+
/** The committed autobuild.toml (§16.1) driving this build. Default
|
|
290
|
+
* CONFIG_TOML, so every existing scenario is untouched; a scenario proving
|
|
291
|
+
* two-axis routing (§9) supplies its own `[roles.default]`/phase roles. */
|
|
292
|
+
configToml?: string
|
|
293
|
+
/** Forge gate existence is independent from current PR mergeability. */
|
|
294
|
+
gatePresence?: 'present' | 'absent'
|
|
295
|
+
/** Optional adapter-level runtime composition for focused integration tests.
|
|
296
|
+
* The scripted agent remains available as the fake provider behind it. */
|
|
297
|
+
createRuntimeRegistry?: (agents: ScriptedAgentRunner) => RuntimeRegistry
|
|
298
|
+
/** Pass false to launch runners with `streamSessions: false` — the exact
|
|
299
|
+
* event log a build produces when session streaming is off. */
|
|
300
|
+
streamSessions?: boolean
|
|
301
|
+
/** Optional production-like workspace composition seam. The default remains
|
|
302
|
+
* the real git-worktree adapter used by existing scenarios. */
|
|
303
|
+
createWorkspaceProvider?: (context: {
|
|
304
|
+
config: Config
|
|
305
|
+
repoRoot: string
|
|
306
|
+
worktreeRoot: string
|
|
307
|
+
}) => WorkspaceProvider | Promise<WorkspaceProvider>
|
|
308
|
+
/** Resolve the config's forge name through a plugin registration instead of
|
|
309
|
+
* injecting FakeForge directly. The selected adapter still shares this one
|
|
310
|
+
* fake journal across CLI terminals, runner, dispatcher, and janitor. */
|
|
311
|
+
pluginForge?: { prAttachments?: boolean }
|
|
312
|
+
/** Exercise the shipped offline forge against a repository with no remote. */
|
|
313
|
+
localGitForge?: boolean
|
|
314
|
+
/** Optional transport adapter around the backing memory store. Hosted tests
|
|
315
|
+
* use this to force dispatcher, runner, and CLI traffic through HTTP. */
|
|
316
|
+
storeAdapter?: (backing: MemoryBuildStore) => Promise<{
|
|
317
|
+
store: BuildStore
|
|
318
|
+
storeRef: string
|
|
319
|
+
token?: string
|
|
320
|
+
cleanup?: () => Promise<void>
|
|
321
|
+
}>
|
|
322
|
+
/** Execute scripted phase commands through the private `ab` launcher on
|
|
323
|
+
* PATH, forcing production ambient-env parsing and Store reopening. */
|
|
324
|
+
processCli?: boolean
|
|
325
|
+
}): Promise<E2eHarness> {
|
|
326
|
+
const tmp = await mkdtemp(join(tmpdir(), 'ab-e2e-'))
|
|
327
|
+
const originPath = join(tmp, 'origin')
|
|
328
|
+
const configToml = opts.configToml ?? CONFIG_TOML
|
|
329
|
+
const remote = await initOrigin(originPath, configToml, opts.localGitForge === true)
|
|
330
|
+
// Git canonicalizes temporary paths on macOS (`/var` → `/private/var`).
|
|
331
|
+
// Records and every dispatch caller must use that same repository identity.
|
|
332
|
+
const origin = await realpath(originPath)
|
|
333
|
+
const remoteUpdater = join(tmp, 'remote-updater')
|
|
334
|
+
if (opts.localGitForge !== true) await git(['clone', '-q', remote, remoteUpdater], tmp)
|
|
335
|
+
|
|
336
|
+
const clock = steppingClock()
|
|
337
|
+
const ids = sequentialIds()
|
|
338
|
+
const uuids = sequentialUuids()
|
|
339
|
+
const store = new MemoryBuildStore({ clock })
|
|
340
|
+
const adapted = await opts.storeAdapter?.(store)
|
|
341
|
+
const activeStore = adapted?.store ?? store
|
|
342
|
+
const config = parseConfig(configToml, 'e2e autobuild.toml')
|
|
343
|
+
// Ad-hoc scenario configs predating mandatory runtime defaults stay explicit
|
|
344
|
+
// at the harness boundary rather than relying on production wiring.
|
|
345
|
+
config.roles.default ??= { runtime: 'scripted' }
|
|
346
|
+
const forge = new FakeForge({
|
|
347
|
+
gatePresence: opts.gatePresence ?? 'present',
|
|
348
|
+
...(opts.pluginForge?.prAttachments === true ? { prAttachments: true } : {}),
|
|
349
|
+
})
|
|
350
|
+
let selectedForge: Forge = opts.localGitForge === true ? new LocalGitForge() : forge
|
|
351
|
+
let plugins: PluginRegistry | undefined
|
|
352
|
+
if (opts.pluginForge !== undefined) {
|
|
353
|
+
plugins = createPluginRegistry()
|
|
354
|
+
plugins.register({
|
|
355
|
+
name: 'e2e-forge-plugin',
|
|
356
|
+
apiVersion: '^1.0.0',
|
|
357
|
+
forges: { [config.forge]: async () => forge },
|
|
358
|
+
})
|
|
359
|
+
selectedForge = await createForge({
|
|
360
|
+
name: config.forge,
|
|
361
|
+
registry: plugins,
|
|
362
|
+
env: process.env,
|
|
363
|
+
repoRoot: origin,
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
const tickets = new FakeTicketSource(opts.tickets ?? [])
|
|
367
|
+
const ticketSource: TicketSource = opts.ticketSource ?? tickets
|
|
368
|
+
const worktreeRoot = join(tmp, 'worktrees')
|
|
369
|
+
const workspaces =
|
|
370
|
+
opts.createWorkspaceProvider === undefined
|
|
371
|
+
? new GitWorktreeProvider({ root: worktreeRoot })
|
|
372
|
+
: await opts.createWorkspaceProvider({ config, repoRoot: origin, worktreeRoot })
|
|
373
|
+
|
|
374
|
+
const launched: Array<{ slug: string; runner: BuildRunner; instance: string }> = []
|
|
375
|
+
const cliErrors: string[] = []
|
|
376
|
+
|
|
377
|
+
// The script IS the agent (§9): route by skill, hand the handler the real
|
|
378
|
+
// CLI bound to this turn's ambient env (D8).
|
|
379
|
+
const script: Script = async (ctx) => {
|
|
380
|
+
const handler =
|
|
381
|
+
opts.handlers[ctx.opts.skill] ?? opts.handlers[ctx.opts.skill.replace(/^ab-/, '')]
|
|
382
|
+
if (handler === undefined) {
|
|
383
|
+
throw new Error(`no scripted handler for skill "${ctx.opts.skill}"`)
|
|
384
|
+
}
|
|
385
|
+
const result = await handler(makeCli(ctx))
|
|
386
|
+
return result ?? defaultTurnResult(`${ctx.opts.skill} finished`)
|
|
387
|
+
}
|
|
388
|
+
const agents = new ScriptedAgentRunner({ script })
|
|
389
|
+
|
|
390
|
+
function makeCli(ctx: ScriptContext): Cli {
|
|
391
|
+
// D8, VERBATIM: exactly what the real adapters do — the CLI resolves the
|
|
392
|
+
// env the turn was launched with, nothing rebuilt by the harness. The
|
|
393
|
+
// runner re-issues AB_PHASE/AB_SESSION on every continued producer turn
|
|
394
|
+
// (§10, build-runner.ts executeSession); a harness that hand-advanced
|
|
395
|
+
// the round here would mask a stale-env adapter (the continued round's
|
|
396
|
+
// `ab done` would resolve round 1 and be rejected as a D5 second call).
|
|
397
|
+
const env = resolveCliEnv(ctx.opts.env)
|
|
398
|
+
const ws = ctx.opts.workspacePath
|
|
399
|
+
|
|
400
|
+
const run = async (argv: string[]): Promise<string[]> => {
|
|
401
|
+
let code: number
|
|
402
|
+
let out: string[]
|
|
403
|
+
let err: string[]
|
|
404
|
+
if (opts.processCli === true) {
|
|
405
|
+
const child = Bun.spawn(['ab', ...argv], {
|
|
406
|
+
cwd: ws,
|
|
407
|
+
env: ctx.opts.env,
|
|
408
|
+
stdout: 'pipe',
|
|
409
|
+
stderr: 'pipe',
|
|
410
|
+
})
|
|
411
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
412
|
+
child.exited,
|
|
413
|
+
new Response(child.stdout).text(),
|
|
414
|
+
new Response(child.stderr).text(),
|
|
415
|
+
])
|
|
416
|
+
code = exitCode
|
|
417
|
+
out = stdout.trimEnd() === '' ? [] : stdout.trimEnd().split('\n')
|
|
418
|
+
err = stderr.trimEnd() === '' ? [] : stderr.trimEnd().split('\n')
|
|
419
|
+
} else {
|
|
420
|
+
out = []
|
|
421
|
+
err = []
|
|
422
|
+
code = await runCli(argv, {
|
|
423
|
+
store: activeStore,
|
|
424
|
+
env,
|
|
425
|
+
workspacePath: ws,
|
|
426
|
+
forge: selectedForge,
|
|
427
|
+
exec: spawnExec,
|
|
428
|
+
ids,
|
|
429
|
+
clock,
|
|
430
|
+
stdout: (line) => out.push(line),
|
|
431
|
+
stderr: (line) => err.push(line),
|
|
432
|
+
})
|
|
433
|
+
}
|
|
434
|
+
if (code !== 0) {
|
|
435
|
+
const message =
|
|
436
|
+
`ab ${argv.join(' ')} exited ${code} in ${env.phase}@${env.round}: ` +
|
|
437
|
+
`${err.join('\n') || '(no stderr)'}`
|
|
438
|
+
cliErrors.push(message)
|
|
439
|
+
throw new Error(message)
|
|
440
|
+
}
|
|
441
|
+
return out
|
|
442
|
+
}
|
|
443
|
+
return { run, ws, round: env.round, env, ctx }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Both direct harness runners and injected abDispatch wiring use this one
|
|
447
|
+
// registry, so capture scenarios cannot accidentally exercise a second fake
|
|
448
|
+
// pipeline implementation. `plugin-scripted` crosses the real plugin
|
|
449
|
+
// materialization boundary while reusing the contract-tested scripted fake.
|
|
450
|
+
const pluginRegistry = createPluginRegistry()
|
|
451
|
+
pluginRegistry.register({
|
|
452
|
+
name: 'e2e-runtime-plugin',
|
|
453
|
+
apiVersion: '^1.0.0',
|
|
454
|
+
agentRuntimes: {
|
|
455
|
+
'plugin-scripted': () => ({
|
|
456
|
+
runner: agents,
|
|
457
|
+
servesModels: ['plugin/'],
|
|
458
|
+
defaultModel: 'plugin/default',
|
|
459
|
+
}),
|
|
460
|
+
},
|
|
461
|
+
})
|
|
462
|
+
const baseRuntimes = opts.createRuntimeRegistry?.(agents) ?? {
|
|
463
|
+
scripted: { runner: agents, servesModels: [] },
|
|
464
|
+
claude: { runner: agents, servesModels: ['claude-'] },
|
|
465
|
+
pi: { runner: agents, servesModels: ['kimi-'] },
|
|
466
|
+
}
|
|
467
|
+
const runtimes = await materializePluginRuntimes(baseRuntimes, pluginRegistry, {
|
|
468
|
+
repoRoot: origin,
|
|
469
|
+
env: {},
|
|
470
|
+
})
|
|
471
|
+
// launchRunner (§3.3, §15.6-C, §15.7): construct a REAL BuildRunner over
|
|
472
|
+
// the SAME store and the provisioned workspace path; the dispatcher never
|
|
473
|
+
// runs agents itself.
|
|
474
|
+
let instances = 0
|
|
475
|
+
const makeRunner = async (
|
|
476
|
+
slug: string,
|
|
477
|
+
instance?: string,
|
|
478
|
+
sessionEnv?: Record<string, string>,
|
|
479
|
+
): Promise<{ runner: BuildRunner; instance: string }> => {
|
|
480
|
+
const record = await activeStore.getBuild(slug)
|
|
481
|
+
const workspacePath = openWorkspacePath(await activeStore.getEvents(slug))
|
|
482
|
+
if (record === null || workspacePath === null) {
|
|
483
|
+
throw new Error(`launchRunner("${slug}"): no build record or open workspace`)
|
|
484
|
+
}
|
|
485
|
+
instances += 1
|
|
486
|
+
const resolvedInstance = instance ?? `runner-${instances}`
|
|
487
|
+
return {
|
|
488
|
+
instance: resolvedInstance,
|
|
489
|
+
runner: new BuildRunner({
|
|
490
|
+
store: activeStore.scopeBuild(slug),
|
|
491
|
+
config,
|
|
492
|
+
// Two-axis registry (§9): every runtime is backed by the SAME scripted
|
|
493
|
+
// runner instance, so the `s_1…s_N` session numbering scenarios rely on
|
|
494
|
+
// is preserved regardless of which runtime a role selects. `scripted`
|
|
495
|
+
// is the configured default and runs only with its un-named built-in model; `pi`
|
|
496
|
+
// serves the Kimi family for exact configured-pair validation.
|
|
497
|
+
runtimes,
|
|
498
|
+
workspacePath,
|
|
499
|
+
branch: record.branch ?? `ab/${slug}`,
|
|
500
|
+
slug,
|
|
501
|
+
exec: spawnExec,
|
|
502
|
+
forge: selectedForge,
|
|
503
|
+
ids,
|
|
504
|
+
clock,
|
|
505
|
+
instance: resolvedInstance,
|
|
506
|
+
host: 'e2e-host',
|
|
507
|
+
...(sessionEnv !== undefined ? { sessionEnv } : {}),
|
|
508
|
+
// Long lease/heartbeat: liveness is driven by the shared stepping
|
|
509
|
+
// clock; scenarios advance it explicitly to expire a lease.
|
|
510
|
+
opts: {
|
|
511
|
+
heartbeatMs: 3_600_000,
|
|
512
|
+
leaseTtlMs: 3_600_000,
|
|
513
|
+
...(opts.streamSessions === false ? { streamSessions: false } : {}),
|
|
514
|
+
},
|
|
515
|
+
}),
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
const launchRunner = async (slug: string): Promise<LaunchRunnerResult> => {
|
|
519
|
+
launched.push({ slug, ...(await makeRunner(slug)) })
|
|
520
|
+
return 'scheduled'
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const dispatcher = new Dispatcher({
|
|
524
|
+
store: activeStore,
|
|
525
|
+
tickets: ticketSource,
|
|
526
|
+
workspaces,
|
|
527
|
+
forge: selectedForge,
|
|
528
|
+
config,
|
|
529
|
+
repo: origin,
|
|
530
|
+
exec: spawnExec,
|
|
531
|
+
launchRunner,
|
|
532
|
+
ids,
|
|
533
|
+
clock,
|
|
534
|
+
})
|
|
535
|
+
const wiring: DispatchWiring = {
|
|
536
|
+
store: activeStore,
|
|
537
|
+
tickets: ticketSource,
|
|
538
|
+
forge: selectedForge,
|
|
539
|
+
workspaces,
|
|
540
|
+
buildExecution: new InProcessBuildExecution(async (input) => {
|
|
541
|
+
const { runner } = await makeRunner(input.slug, input.instance, {
|
|
542
|
+
AB_STORE: input.storeRef,
|
|
543
|
+
...(adapted?.token !== undefined ? { AB_TOKEN: adapted.token } : {}),
|
|
544
|
+
})
|
|
545
|
+
await runner.run().catch(async (error) => {
|
|
546
|
+
await activeStore.putArtifact(
|
|
547
|
+
input.slug,
|
|
548
|
+
diagnosticArtifact({
|
|
549
|
+
instance: input.instance,
|
|
550
|
+
outcome:
|
|
551
|
+
error instanceof LeaseHeldError
|
|
552
|
+
? 'lease-held'
|
|
553
|
+
: error instanceof SetupFailureError
|
|
554
|
+
? 'setup-failed'
|
|
555
|
+
: 'failed',
|
|
556
|
+
error: error instanceof Error ? error.message : String(error),
|
|
557
|
+
}),
|
|
558
|
+
)
|
|
559
|
+
throw error
|
|
560
|
+
})
|
|
561
|
+
}),
|
|
562
|
+
runtimes,
|
|
563
|
+
// Scripted agents resolve this ambient value, then invoke runCli against
|
|
564
|
+
// the injected shared store. It is an identity label, never opened.
|
|
565
|
+
storeRef: adapted?.storeRef ?? 'memory://e2e',
|
|
566
|
+
...(adapted?.token !== undefined ? { token: adapted.token } : {}),
|
|
567
|
+
ids,
|
|
568
|
+
uuids,
|
|
569
|
+
clock,
|
|
570
|
+
...(plugins !== undefined ? { plugins } : {}),
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
tmp,
|
|
575
|
+
origin,
|
|
576
|
+
remote,
|
|
577
|
+
store,
|
|
578
|
+
clock,
|
|
579
|
+
ids,
|
|
580
|
+
forge,
|
|
581
|
+
forgeAdapter: selectedForge,
|
|
582
|
+
tickets,
|
|
583
|
+
workspaces,
|
|
584
|
+
agents,
|
|
585
|
+
dispatcher,
|
|
586
|
+
wiring,
|
|
587
|
+
config,
|
|
588
|
+
launched,
|
|
589
|
+
cliErrors,
|
|
590
|
+
async advanceRemote(changes: Record<string, string>, message: string): Promise<string> {
|
|
591
|
+
if (opts.localGitForge === true) {
|
|
592
|
+
throw new Error('advanceRemote is unavailable for a no-remote local-git harness')
|
|
593
|
+
}
|
|
594
|
+
for (const [path, content] of Object.entries(changes)) {
|
|
595
|
+
await writeFileIn(remoteUpdater, path, content)
|
|
596
|
+
}
|
|
597
|
+
const sha = await commitAll(remoteUpdater, message)
|
|
598
|
+
await git(['push', '-q', 'origin', 'main'], remoteUpdater)
|
|
599
|
+
return sha
|
|
600
|
+
},
|
|
601
|
+
async runLatest(): Promise<BuildState> {
|
|
602
|
+
const entry = launched.at(-1)
|
|
603
|
+
if (entry === undefined) throw new Error('runLatest: nothing launched')
|
|
604
|
+
try {
|
|
605
|
+
return await entry.runner.run()
|
|
606
|
+
} finally {
|
|
607
|
+
// Direct Dispatcher integration bypasses DispatchLoop's execution
|
|
608
|
+
// supervisor; mirror its exact post-completion lease release here.
|
|
609
|
+
await activeStore.releaseLease(entry.slug, entry.instance)
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
events: (slug) => store.getEvents(slug),
|
|
613
|
+
cleanup: async () => {
|
|
614
|
+
await activeStore.close()
|
|
615
|
+
if (activeStore !== store) await store.close()
|
|
616
|
+
await adapted?.cleanup?.()
|
|
617
|
+
await rm(tmp, { recursive: true, force: true })
|
|
618
|
+
},
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ── Default (§15.6 happy-path) handlers over the REAL CLI ────────────────────
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Every phase terminates the way §8.7 walks it: `ab context`, real work in
|
|
626
|
+
* the worktree (implement commits with real git), deposits via `ab artifact
|
|
627
|
+
* put`, exactly one terminal (`ab done` / `ab verdict`). Scenarios override
|
|
628
|
+
* individual entries.
|
|
629
|
+
*/
|
|
630
|
+
export function happyHandlers(): SkillHandlers {
|
|
631
|
+
return {
|
|
632
|
+
plan: async (cli) => {
|
|
633
|
+
await cli.run(['context'])
|
|
634
|
+
const plan = await writeFileIn(
|
|
635
|
+
cli.ws,
|
|
636
|
+
'.ab/plan.md',
|
|
637
|
+
`# Plan (round ${cli.round})\n\n1. Add rate-limit.txt and ok.marker.\n`,
|
|
638
|
+
)
|
|
639
|
+
await cli.run(['artifact', 'put', 'plan', plan])
|
|
640
|
+
await cli.run(['done'])
|
|
641
|
+
},
|
|
642
|
+
'plan-review': async (cli) => {
|
|
643
|
+
await cli.run(['context'])
|
|
644
|
+
const notes = await writeFileIn(cli.ws, '.ab/plan-review.md', 'Plan conforms to the spec.\n')
|
|
645
|
+
await cli.run(['verdict', 'approve', '--notes', notes])
|
|
646
|
+
},
|
|
647
|
+
implement: async (cli) => {
|
|
648
|
+
await cli.run(['context'])
|
|
649
|
+
await writeFileIn(cli.ws, 'rate-limit.txt', `throttle after 5 (r${cli.round})\n`)
|
|
650
|
+
await writeFileIn(cli.ws, 'ok.marker', 'ok\n')
|
|
651
|
+
await commitAll(cli.ws, `implement: rate limiting r${cli.round}`)
|
|
652
|
+
// §8.7: a mid-phase structured observation — never a terminal.
|
|
653
|
+
await cli.run(['observe', '--kind', 'refactor', 'extract limiter config into settings'])
|
|
654
|
+
const notes = await writeFileIn(
|
|
655
|
+
cli.ws,
|
|
656
|
+
'.ab/implement-notes.md',
|
|
657
|
+
`Added rate-limit.txt (round ${cli.round}).\n`,
|
|
658
|
+
)
|
|
659
|
+
await cli.run(['done', '--notes', notes])
|
|
660
|
+
},
|
|
661
|
+
'code-review': async (cli) => {
|
|
662
|
+
await cli.run(['context'])
|
|
663
|
+
const notes = await writeFileIn(
|
|
664
|
+
cli.ws,
|
|
665
|
+
'.ab/code-review.md',
|
|
666
|
+
'Diff matches the approved plan.\n',
|
|
667
|
+
)
|
|
668
|
+
await cli.run(['verdict', 'approve', '--notes', notes])
|
|
669
|
+
},
|
|
670
|
+
finalize: async (cli) => {
|
|
671
|
+
await cli.run(['context'])
|
|
672
|
+
const pr = await writeFileIn(
|
|
673
|
+
cli.ws,
|
|
674
|
+
'.ab/pr-description.md',
|
|
675
|
+
'Add login rate limiting\n\nThrottles repeated failed logins per the spec.\n',
|
|
676
|
+
)
|
|
677
|
+
await cli.run(['artifact', 'put', 'pr-description', pr])
|
|
678
|
+
await cli.run(['done'])
|
|
679
|
+
},
|
|
680
|
+
}
|
|
681
|
+
}
|