Skip to content

Commit 6c464b1

Browse files
clkaoclaude
andauthored
Extract host-neutral merge + dispatch contract — codex/pi name no merge reference and break at terminalization (#385)
* feat(fo): extract host-neutral merge + dispatch cores; codex/pi gain terminal contract Re-home the generic first-officer terminal-merge and dispatch ceremony out of the claude-named references into two host-neutral deferred cores, so codex and pi FOs reaching terminalization follow a resolving merge/dispatch reference instead of a dead-end pointer. - New fo-merge-core.md (Merge-and-Cleanup 1-9, Ship-Local, worktree-removal safety, Mod-Block Enforcement) + fo-dispatch-core.md (dispatch procedure, reuse contract, worker resolution, worktree ownership, dispatch adapter, event-loop skeleton). - Boot-resident core's deferred-module pointers name the two cores directly; each runtime adapter reduced to a thin seam naming only its host residue. - claude-fo-merge.md keeps the step-10 TERMINAL_TEARDOWN_BOUNDED teardown byte-identical (the per-host seam); claude-fo-dispatch.md keeps team creation, the Agent()/SendMessage handles, registry-desync (#36806), context-budget probe, reconcile sweep + Backstop. - codex gains a NEW authored teardown seam (cohort cooperative-shutdown via mailbox; no TeamDelete/marker); pi repoints step 10 to its existing ## Shutdown. - AC-1: extend boot_resident_closure_test.go to walk codex/pi and assert each host names a resolving merge + dispatch reference (RED for codex/pi before, GREEN after). - AC-2: new fo_core_single_source_test.go — each moved ceremony block lives in a core XOR in any adapter (copy-paste-into-a-host regression goes RED). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(fo): lean rework — single core-naming, strip duplicated adapter preambles Shared-core deferred-module pointers are the sole core-namer; strip the "host-neutral X lives in fo-X-core.md, read it at [load point]" preamble from every runtime adapter (claude/codex/pi). Each adapter keeps only its host-specific part. Drop "seam"/"host-neutral version" jargon for the contract's existing vocabulary (deferred module/reference, load point, runtime adapter). Claude lifecycle load 10804 -> 10278 (-526). Marker TERMINAL_TEARDOWN_BOUNDED byte-identical (a59f79db). Not yet <= 9779 baseline — see stage report. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(fo): reframe AC-1 to reachability-via-shared-core; drop AC-2 AC-1 no longer requires each adapter to re-name the cores (that forced the redundant preamble). Reachability rides on the single core-naming in first-officer-shared-core.md, which every host loads at boot. New TestHostNeutralCoresResolveAndCarryCeremony asserts the shared core names each core + each core resolves on disk + carries its four ceremony anchors. The closure walk (bootResidentBodies, incl. shared core) keeps the dangling-target guard. Remove fo_core_single_source_test.go (AC-2) — a transient migration assurance, not a durable invariant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(fo): dedup the Claude id_style/boot-json paragraph into the shared core The claude-fo-dispatch id_style/boot-json paragraph duplicated the boot-resident core's ## ID Styles + Startup step 5 + ## FO Write Scope. Replace it with a one-line pointer; repoint the claude-runtime id-filing reference to the shared core. The one residual genuine duplication the lean rework left — captain authorized this dedup; the broad Claude-verbosity trim stays the parallel token-cleanup proposal's scope. Claude lifecycle 10278 -> 10213 (-65). Residual +434 over the 9779 baseline is accepted intrinsic core/adapter split-overhead (per the entity body's token-budget note). Marker byte-identical (a59f79db); AC-3b fragment audit: 0 MUSTs dropped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Revert "refactor(fo): dedup the Claude id_style/boot-json paragraph into the shared core" This reverts commit 617a674. * feat(fo): headless gate-stop MUST author the full present-gate review before exit 7e-era contract gap (not a rework regression): Startup step-9's headless bullet only required "report each entity's stop reason," so a headless -p FO stopping at a gate:true stage could exit with a terse stop line instead of the reviewable gate content — model variance flaked TestLiveDefaultHeadlessStopsAtGate (live_gate_stop_test.go:219 scans for an FO-authored "Gate review:" + "Decision:"). Strengthen the step-9 headless bullet to an unambiguous MUST: at each gate:true stop the FO MUST author the FULL present-gate review (Skill(present-gate)'s Gate review:/chosen-direction/checklist/Decision: template, per ## Completion and Gates) before exiting — the human picks up the headless transcript and decides from that content. The FO still does NOT resolve the gate headless (only "given the conn" does). Claude lifecycle 10278 -> 10403 (+125 for the MUST). Marker byte-identical (a59f79db). Offline go test ./... + go vet -tags live green; the live behavior gate re-runs at PR CI (account 429 locally). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(live): isolate --plugin-dir from real docs/dev + strong force-team cue Two pre-existing live-suite reliability bugs the CI surfaced (not contract work). BUG 1 (wrong-root boot): every claude-live launch passed --plugin-dir=repoRoot, the real repo whose docs/dev sibling carries live entities. An FO that anchored git rev-parse --show-toplevel + status --discover on the plugin path (not its isolated cwd fixture) discovered and drove the REAL docs/dev for 27 min instead of the /tmp fixture. New livePluginDir() stages an isolated plugin copy (.claude-plugin/, skills/, agents/) into a temp dir with NO docs/dev sibling, then git-inits it so a rev-parse from the plugin path resolves to a workflow-free root; status --discover there yields zero, so the FO falls back to the fixture. Verified the staged dir has the scaffolding, no docs/dev, and discovers zero. Repointed all five claude-live --plugin-dir sites (shared runner, full cycle, gate-stop, residency, zero-discover) — hardens every claude-live scenario, incl. fixing a latent wrong-root in zero-discover. BUG 2 (residency/teardown force-team coin): both tests used a soft "run in team mode" cue that 7e's headless bare-mode determination could decline, so the FO drove bare (no team, no roster) and red the team-only oracles on correct behavior. New shared forceTeamModeCue is a strong unambiguous MUST (TeamCreate first, bare mode NOT acceptable, override the -p bare default) — same prose-lever pattern as the gate-stop MUST. Used in residency + teardown. Offline go test ./... + go vet -tags live green. Live behavior re-runs at PR CI (account 429 locally); BUG-1's fix is deterministic, BUG-2's is the strong lever. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ensigncycle: strengthen forceTeamModeCue to prohibit fake team name and override dispatch module skip rule The prior cue ("create a team (TeamCreate) before the first dispatch") was ambiguous: the FO read the dispatch module's "In single-entity mode, skip team creation" rule and generated a fake team name string instead of calling the TeamCreate tool. The new cue explicitly names and overrides that rule, prohibits inventing a fake team name, and requires calling the TeamCreate TOOL before any Agent() dispatch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci(live-e2e): default-pin claude-live to Claude Code 2.1.177 2.1.178 stopped surfacing the native TeamCreate/TeamDelete tools in a headless `claude -p` session even with CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 set (anthropics/claude-code#68721), RED-failing the team-forced live tests TestLiveStandingResidencyInjectsCommOfficer and TestLiveEnsignCycleTeamTeardown. 2.1.177 is the last version that surfaces them. CI installs Claude unpinned, so it picked up 2.1.178. Default CLAUDE_VERSION to 2.1.177 while letting a manual workflow_dispatch claude_version input still override. Unpin when the upstream regression is fixed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dispatch): spawn-standing-all Agent spec must carry description The Agent tool REQUIRES `description`, but spawnSpec emitted only subagent_type/name/team_name/model/prompt — so forwarding the spec verbatim failed with InputValidationError and the standing teammate (comm-officer) never spawned. This breaks comm-officer injection in interactive team mode too, so it is mode-independent. Add Description to spawnSpec (populated `standing teammate: {name}`), matching dispatch build's envelope field order (subagent_type, description, name, ...). Regenerated the two affected spec-emit goldens. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(live-e2e): revert claude-live 2.1.177 pin — run latest, go bare under -p Per the captain's 2026-06-16 determination (go bare under `-p`; team mode is the interactive path), headless CI installs latest Claude and runs bare. This reverts the 2.1.177 default pin (8a5f92b) that worked around 2.1.178 dropping TeamCreate/TeamDelete from headless sessions (anthropics/claude-code#68721) — that pin propped up the forced-team headless tests now retired. A manual workflow_dispatch claude_version input still overrides. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(live): retire forced-team headless tests — go bare under -p Headless `claude -p` cannot sustain team mode: Claude Code 2.1.178 dropped native TeamCreate/TeamDelete from headless sessions (anthropics/claude-code#68721), and the SDK session races to end_turn before teammates finish (anthropics/claude-code-action#1124). Forcing team mode in a headless live test fights the captain's go-bare determination, so retire the two forced-team tests: - TestLiveEnsignCycleTeamTeardown (live_teardown_test.go) - TestLiveStandingResidencyInjectsCommOfficer (live_standing_residency_test.go) Their bare-mode behavior survives: teardown's bare path == the team-agnostic TestLiveEnsignCycle; residency's mechanism == spawn_standing_all_test.go. The team-only live artifacts (live roster injection, live teardown marker) are deferred to the terminal/pty harness task m40mphxan8phr3t3tp03gk89. Offline marker coverage stays in teardown_grade_watcher_test.go over the sonnet_teamdelete_*.jsonl fixtures. Remove both names from the -run regex and delete the now-unused forceTeamModeCue. go test ./... and go vet -tags live ./... stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1aae098 commit 6c464b1

21 files changed

Lines changed: 431 additions & 541 deletions

.github/workflows/runtime-live-e2e.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,11 @@ jobs:
167167

168168
# TestLiveEnsignCycle is the full lifecycle canary, TEAM-AGNOSTIC: it gates on
169169
# dispatch-close + the on-disk terminal end-state, so it greens whether the
170-
# headless `-p` FO drove team or bare (no team-vs-bare coin). TestLiveEnsignCycleTeamTeardown
171-
# FORCES team mode and keeps the TERMINAL_TEARDOWN_BOUNDED teardown-marker
172-
# coverage (the team-only signal removed from the canary's default path).
170+
# headless `-p` FO drove team or bare (no team-vs-bare coin). Headless `-p` goes
171+
# bare per the captain's go-bare determination, so the live suite carries no
172+
# forced-team test; live team end-to-end (roster injection, the bounded teardown
173+
# marker) is deferred to the terminal/pty harness task
174+
# m40mphxan8phr3t3tp03gk89.
173175
# TestLiveDefaultHeadlessStopsAtGate is AC-a: default `-p` with NO conn drives to
174176
# a gate:true stage and EXITS reporting gate status, without resolving the gate.
175177
# TestLiveZeroDiscoverReportsAndStops is the cheap boot-only lean-boot guard (a
@@ -182,7 +184,7 @@ jobs:
182184
run: |
183185
set -o pipefail
184186
mkdir -p "$SPACEDOCK_LIVE_ARTIFACT_DIR"
185-
go test -tags live -count=1 -timeout 40m -run 'TestLiveEnsignCycle|TestLiveEnsignCycleTeamTeardown|TestLiveDefaultHeadlessStopsAtGate|TestLiveZeroDiscoverReportsAndStops|TestLiveStandingResidencyInjectsCommOfficer' ./internal/ensigncycle/ -v 2>&1 | tee live-e2e-transcript.txt
187+
go test -tags live -count=1 -timeout 40m -run 'TestLiveEnsignCycle|TestLiveDefaultHeadlessStopsAtGate|TestLiveZeroDiscoverReportsAndStops' ./internal/ensigncycle/ -v 2>&1 | tee live-e2e-transcript.txt
186188
187189
# The shared scenario suite: the Claude lane runs the SAME host-neutral
188190
# gate/rejection/merge-hook scenarios the codex-live lane runs, through the

internal/contractlint/boot_resident_closure_test.go

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,33 @@ import (
1010
"testing"
1111
)
1212

13-
// bootResidentBodies are the two contract bodies the FO loader inlines/reads at
14-
// boot: the slimmed shared core and the Claude runtime adapter. AC-4 walks these
15-
// (NOT a SKILL.md, which the existing TestUserSkillReferenceClosureResolves reads)
16-
// because the loader reads the bodies directly, and only the bodies name the
17-
// deferred load-points the boot core defers to (a sibling reference path, a bare
18-
// skill invocation, a canonical mod file).
13+
// bootResidentBodies are the contract bodies the FO loader inlines/reads at boot:
14+
// the slimmed shared core and each host's runtime adapter. AC-4 walks these (NOT a
15+
// SKILL.md, which the existing TestUserSkillReferenceClosureResolves reads) because
16+
// the loader reads the bodies directly, and only the bodies name the deferred
17+
// load-points the boot core defers to (a sibling reference path, a bare skill
18+
// invocation, a canonical mod file). The codex and pi adapters join the Claude one
19+
// so the closure walk catches a dead-end reference on every host, not only Claude.
1920
var bootResidentBodies = []string{
2021
filepath.Join("skills", "first-officer", "references", "first-officer-shared-core.md"),
2122
filepath.Join("skills", "first-officer", "references", "claude-first-officer-runtime.md"),
23+
filepath.Join("skills", "first-officer", "references", "codex-first-officer-runtime.md"),
24+
filepath.Join("skills", "first-officer", "references", "pi-first-officer-runtime.md"),
25+
}
26+
27+
// foReferenceCores are the two host-neutral cores the boot-resident shared core names
28+
// at its dispatch and merge load points. Because every host loads the shared core at
29+
// boot, naming the cores there is what makes the ceremony REACHABLE on codex and pi —
30+
// no per-host re-naming is required. Each core must exist on disk (the closure walk
31+
// over bootResidentBodies resolves them once shared-core names them) and carry its
32+
// ceremony anchors.
33+
var foReferenceCores = map[string][]string{
34+
filepath.Join("skills", "first-officer", "references", "fo-merge-core.md"): {
35+
"## Merge and Cleanup", "### Ship-Local Ceremony", "### Worktree removal safety", "## Mod-Block Enforcement",
36+
},
37+
filepath.Join("skills", "first-officer", "references", "fo-dispatch-core.md"): {
38+
"## Dispatch", "## Reuse and Fresh Dispatch", "## Dispatch Adapter", "## Event Loop",
39+
},
2240
}
2341

2442
// bodyReferenceRe matches a sibling reference read-path named in a contract body
@@ -155,3 +173,47 @@ func TestBootResidentDeferredLoadPointGuardFailsOnDanglingTarget(t *testing.T) {
155173
t.Fatal("control: the real load-point (using-claude-team) was not resolved — the discriminator has nothing to contrast the dangling case against")
156174
}
157175
}
176+
177+
// TestHostNeutralCoresResolveAndCarryCeremony is the AC-1 reachability guard: the two
178+
// host-neutral cores the boot-resident shared core names at its dispatch and merge
179+
// load points exist on disk AND carry their ceremony anchors. Reachability for codex
180+
// and pi rides on the SINGLE core-naming in first-officer-shared-core.md — every host
181+
// loads the shared core at boot, so a core it names there is reachable on every host;
182+
// no per-adapter re-naming is required (the per-adapter restatement only added
183+
// redundancy). The closure half — that the shared core's named references resolve to
184+
// real files — is TestBootResidentDeferredLoadPointsResolve walking bootResidentBodies
185+
// (which includes the shared core). This test adds the content half: each core carries
186+
// its four ceremony anchors, so "reachable" means "reaches a real ceremony," not an
187+
// empty file. The independent source is the filesystem + the anchor set; a core
188+
// renamed/emptied/dropped fails.
189+
func TestHostNeutralCoresResolveAndCarryCeremony(t *testing.T) {
190+
root := repoRoot(t)
191+
if len(foReferenceCores) == 0 {
192+
t.Fatal("no host-neutral cores declared — the reachability check would pass vacuously")
193+
}
194+
// The shared core must NAME each core at a load point — that single naming is what
195+
// makes the ceremony reachable on every host.
196+
sharedCore := filepath.Join("skills", "first-officer", "references", "first-officer-shared-core.md")
197+
sharedData, err := os.ReadFile(filepath.Join(root, sharedCore))
198+
if err != nil {
199+
t.Fatalf("read shared core %s: %v", sharedCore, err)
200+
}
201+
sharedBody := string(sharedData)
202+
for corePath, anchors := range foReferenceCores {
203+
base := filepath.Base(corePath)
204+
if !strings.Contains(sharedBody, base) {
205+
t.Errorf("%s does not name %s — the boot-resident core is the sole core-namer, so an unnamed core is unreachable on every host", sharedCore, base)
206+
}
207+
data, err := os.ReadFile(filepath.Join(root, corePath))
208+
if err != nil {
209+
t.Errorf("host-neutral core %s does not resolve on disk: %v", corePath, err)
210+
continue
211+
}
212+
body := string(data)
213+
for _, anchor := range anchors {
214+
if !strings.Contains(body, anchor) {
215+
t.Errorf("%s is missing ceremony anchor %q — the named core resolves but does not carry the ceremony, so reachability reaches an empty file", corePath, anchor)
216+
}
217+
}
218+
}
219+
}

internal/contractlint/structural_checks_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ func TestNoUnexpectedModHookOrPRMergeIntroduced(t *testing.T) {
259259
// example into the dispatch reference — both legitimately carry `## Hook:`.
260260
filepath.Join("skills", "first-officer", "references", "claude-fo-merge.md"): true,
261261
filepath.Join("skills", "first-officer", "references", "claude-fo-dispatch.md"): true,
262+
// The host-neutral extraction re-homed Mod-Block Enforcement (which names the
263+
// `## Hook: merge` mechanism surface) into the merge core; it legitimately
264+
// carries the `## Hook:` token the same way the claude-fo-merge seam did.
265+
filepath.Join("skills", "first-officer", "references", "fo-merge-core.md"): true,
262266
}
263267
allowedPRMergeFiles := map[string]bool{
264268
filepath.Join("mods", "pr-merge.md"): true,

internal/dispatch/spawn_standing_all_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
// values are sourced from the fixture mod, NOT the binary.
1313
type spawnAllSpec struct {
1414
SubagentType string `json:"subagent_type"`
15+
Description string `json:"description"`
1516
Name string `json:"name"`
1617
TeamName string `json:"team_name"`
1718
Model string `json:"model"`
@@ -55,6 +56,13 @@ func TestSpawnStandingAllEmitsAbsentMemberSpec(t *testing.T) {
5556
if got.SubagentType != "general-purpose" {
5657
t.Errorf("subagent_type = %q, want general-purpose", got.SubagentType)
5758
}
59+
// The Agent tool REQUIRES description; the spec must carry a non-empty one
60+
// (spawn-standing-all derives it from the member name) or the forwarded
61+
// Agent() call fails InputValidationError and the standing teammate never
62+
// spawns.
63+
if want := "standing teammate: comm-officer"; got.Description != want {
64+
t.Errorf("description = %q, want %q", got.Description, want)
65+
}
5866
if got.Name != "comm-officer" {
5967
t.Errorf("name = %q, want comm-officer", got.Name)
6068
}

internal/dispatch/standing.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ func buildSpawnSpec(home, modPath, teamName string) (spec spawnSpec, alreadyAliv
204204

205205
return spawnSpec{
206206
SubagentType: subagentType,
207+
Description: fmt.Sprintf("standing teammate: %s", declaredName),
207208
Name: declaredName,
208209
TeamName: teamName,
209210
Model: model,
@@ -223,10 +224,13 @@ func emitAlreadyAlive(stdout io.Writer, name string) {
223224
}
224225

225226
// spawnSpec is the Agent() call spec spawn-standing emits when the member is not
226-
// yet alive. Field order matches the oracle's dict: subagent_type, name,
227-
// team_name, model, prompt.
227+
// yet alive. The Agent tool REQUIRES description, so the spec carries one (else
228+
// the forwarded Agent() call fails InputValidationError and the teammate never
229+
// spawns). Field order matches dispatch build's envelope: subagent_type,
230+
// description, name, team_name, model, prompt.
228231
type spawnSpec struct {
229232
SubagentType string `json:"subagent_type"`
233+
Description string `json:"description"`
230234
Name string `json:"name"`
231235
TeamName string `json:"team_name"`
232236
Model string `json:"model"`

internal/dispatch/testdata/golden/spawn-standing-spec-nonascii.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
===== stdout =====
44
{
55
"subagent_type": "general-purpose",
6+
"description": "standing teammate: comm-officer",
67
"name": "comm-officer",
78
"team_name": "fixture-team",
89
"model": "sonnet",

internal/dispatch/testdata/golden/spawn-standing-spec.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
===== stdout =====
44
{
55
"subagent_type": "general-purpose",
6+
"description": "standing teammate: comm-officer",
67
"name": "comm-officer",
78
"team_name": "fixture-team",
89
"model": "sonnet",

internal/ensigncycle/claude_live_runner_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const antiShutdownOverride = "Do not shut down your team or prepare your final "
3838

3939
type claudeLiveRunner struct {
4040
binary string
41-
repoRoot string
41+
pluginDir string
4242
env []string
4343
model string
4444
artifactRoot string
@@ -138,7 +138,7 @@ func claudeScenarioRunners() map[string]func(*testing.T, claudeLiveRunner, share
138138
func newClaudeLiveRunner(t *testing.T) claudeLiveRunner {
139139
t.Helper()
140140
binary := spacedockBinary(t)
141-
repo := repoRoot(t)
141+
pluginDir := livePluginDir(t)
142142
model := envOr("SPACEDOCK_LIVE_MODEL", "sonnet")
143143

144144
// isolatedClaudeEnv resolves the credential (OAuth benchmark-token locally,
@@ -152,7 +152,7 @@ func newClaudeLiveRunner(t *testing.T) claudeLiveRunner {
152152
home, _ := envValue(env, "HOME")
153153
return claudeLiveRunner{
154154
binary: binary,
155-
repoRoot: repo,
155+
pluginDir: pluginDir,
156156
env: env,
157157
model: model,
158158
artifactRoot: claudeLiveArtifactDir(t, "claude-shared-scenarios"),
@@ -327,7 +327,7 @@ func (r claudeLiveRunner) run(t *testing.T, scenario sharedRuntimeScenario, work
327327
finalPath := filepath.Join(artifactDir, "claude-final-message.txt")
328328

329329
cmd := exec.Command(r.binary, "claude",
330-
"--plugin-dir", r.repoRoot,
330+
"--plugin-dir", r.pluginDir,
331331
"--skip-contract-check",
332332
"--",
333333
"-p", prompt+" "+antiShutdownOverride,

internal/ensigncycle/live_gate_stop_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ var (
9494
// and a distinct prompt (no conn) from TestLiveEnsignCycle's conn-cue drive.
9595
func TestLiveDefaultHeadlessStopsAtGate(t *testing.T) {
9696
binary := spacedockBinary(t)
97-
repoRoot := repoRoot(t)
97+
pluginDir := livePluginDir(t)
9898
model := envOr("SPACEDOCK_LIVE_MODEL", "sonnet")
9999

100100
childEnv := isolatedClaudeEnv(t, os.Getenv("HOME"))
@@ -117,7 +117,7 @@ func TestLiveDefaultHeadlessStopsAtGate(t *testing.T) {
117117
// drives-to-the-gate-and-stops is exactly the behavior under test.
118118
drivePrompt := "Drive the workflow. " + antiShutdownOverride
119119
cmd := exec.Command(binary, "claude",
120-
"--plugin-dir", repoRoot,
120+
"--plugin-dir", pluginDir,
121121
"--skip-contract-check",
122122
"--",
123123
"-p", drivePrompt,

0 commit comments

Comments
 (0)