From 4bc82513060182801d8b703768c8d5baecbe67be Mon Sep 17 00:00:00 2001 From: CL Kao Date: Thu, 4 Jun 2026 01:13:16 -0700 Subject: [PATCH 1/4] Ensure Pi dispatch wraps build artifacts --- internal/dispatch/build_pi_host_test.go | 83 +++++++++++++++++++ internal/piruntime/subagents.go | 25 ++++++ internal/piruntime/subagents_test.go | 32 +++++++ .../references/pi-first-officer-runtime.md | 6 +- skills/integration/skill_surface_test.go | 68 +++++++++++++++ 5 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 internal/piruntime/subagents.go create mode 100644 internal/piruntime/subagents_test.go diff --git a/internal/dispatch/build_pi_host_test.go b/internal/dispatch/build_pi_host_test.go index 54e8b3d8..ebcd731c 100644 --- a/internal/dispatch/build_pi_host_test.go +++ b/internal/dispatch/build_pi_host_test.go @@ -8,6 +8,8 @@ import ( "path/filepath" "strings" "testing" + + "github.com/spacedock-dev/spacedock/internal/piruntime" ) func TestBuildPiHostPromptShape(t *testing.T) { @@ -68,6 +70,87 @@ func TestBuildPiHostPromptShape(t *testing.T) { } } +func TestBuildPiHostArtifactCarriesCanonicalStageFactsThroughPiWrapper(t *testing.T) { + root := t.TempDir() + stateDir := filepath.Join(root, "state-checkout") + writeFile(t, filepath.Join(root, "README.md"), readmeWorktree(true)) + if err := os.MkdirAll(stateDir, 0o755); err != nil { + t.Fatal(err) + } + worktreeRel := ".worktrees/spacedock-ensign-canonical-stage" + worktreePath := filepath.Join(root, worktreeRel) + if err := os.MkdirAll(worktreePath, 0o755); err != nil { + t.Fatal(err) + } + entityPath := filepath.Join(stateDir, "canonical-stage", "index.md") + writeFile(t, entityPath, entityFM("Canonical Stage", "implementation", worktreeRel)) + gitInit(t, root) + + checklist := []string{"- keep the builder assignment", "- write the stage report"} + stdin := mergeStdin(map[string]any{ + "schema_version": 2, + "entity_path": entityPath, + "workflow_dir": root, + "stage": "implementation", + "checklist": checklist, + "bare_mode": true, + "host": "pi", + }, nil) + + native := runNative(stdin, "build", "--workflow-dir", root) + if native.exit != 0 { + t.Fatalf("build exit=%d stderr=%q", native.exit, native.stderr) + } + var out struct { + Name *string `json:"name"` + Description string `json:"description"` + DispatchFile string `json:"dispatch_file_path"` + Prompt string `json:"prompt"` + } + if err := json.Unmarshal([]byte(native.stdout), &out); err != nil { + t.Fatalf("stdout is not build JSON: %v\n%s", err, native.stdout) + } + if out.Name != nil { + t.Fatalf("bare Pi dispatch should not require team-style worker name, got %q", *out.Name) + } + if out.Description != "Canonical Stage: implementation" { + t.Fatalf("description = %q", out.Description) + } + if !strings.Contains(out.DispatchFile, "spacedock-ensign-canonical-stage-implementation.md") { + t.Fatalf("dispatch file path does not carry builder-derived slug/stage: %q", out.DispatchFile) + } + + wrapped := piruntime.SubagentStageDispatch(out.Prompt, "implementation", "canonical-stage implementation") + if wrapped.Context != "fresh" { + t.Fatalf("Pi wrapper context = %q, want fresh", wrapped.Context) + } + if wrapped.Task != out.Prompt { + t.Fatalf("Pi wrapper replaced the dispatch-build prompt:\nwant: %s\n got: %s", out.Prompt, wrapped.Task) + } + if strings.Contains(wrapped.Task, "acceptance") { + t.Fatalf("Pi stage wrapper task unexpectedly contains same-agent acceptance contract: %q", wrapped.Task) + } + + body := readDispatchBody(t, out.DispatchFile) + for _, want := range []string{ + "You are working on: Canonical Stage", + "Stage: implementation", + "Read the entity file at " + entityPath, + "spacedock dispatch show-stage-def --workflow-dir " + shlexQuote(root) + " --stage implementation", + "Your working directory for CODE is " + worktreePath, + "All CODE reads and writes MUST use paths under " + worktreePath, + "- keep the builder assignment", + "- write the stage report", + } { + if !strings.Contains(body, want) { + t.Fatalf("Pi dispatch artifact missing builder-derived fact %q:\n%s", want, body) + } + } + if !strings.Contains(wrapped.Task, out.DispatchFile) { + t.Fatalf("Pi wrapper does not forward dispatch file path %s in task %q", out.DispatchFile, wrapped.Task) + } +} + func TestBuildPiHostPreservesSplitRootEntityPath(t *testing.T) { root := t.TempDir() stateDir := filepath.Join(root, "state-checkout") diff --git a/internal/piruntime/subagents.go b/internal/piruntime/subagents.go new file mode 100644 index 00000000..e4fe56b9 --- /dev/null +++ b/internal/piruntime/subagents.go @@ -0,0 +1,25 @@ +// ABOUTME: Pi-subagents assignment wrappers for Spacedock stage dispatches. +// ABOUTME: Keeps Pi transport fields additive around dispatch-build artifacts. +package piruntime + +// SubagentDispatch is the Spacedock-owned subset of the pi-subagents stage +// dispatch wrapper. The canonical assignment remains the dispatch-build artifact; +// this wrapper only adds Pi transport metadata. +type SubagentDispatch struct { + Task string `json:"task"` + Context string `json:"context"` + Phase string `json:"phase,omitempty"` + Label string `json:"label,omitempty"` +} + +// SubagentStageDispatch wraps an already-built dispatch artifact prompt/content +// for pi-subagents. It intentionally has no acceptance field: stage acceptance is +// owned by the canonical assignment/checklist and independent Spacedock validation. +func SubagentStageDispatch(assignment, phase, label string) SubagentDispatch { + return SubagentDispatch{ + Task: assignment, + Context: "fresh", + Phase: phase, + Label: label, + } +} diff --git a/internal/piruntime/subagents_test.go b/internal/piruntime/subagents_test.go new file mode 100644 index 00000000..8a42b17a --- /dev/null +++ b/internal/piruntime/subagents_test.go @@ -0,0 +1,32 @@ +// ABOUTME: Pi-subagents stage dispatch wrapper invariants. +// ABOUTME: Proves the wrapper adds only transport fields around canonical assignments. +package piruntime + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestSubagentStageDispatchAddsOnlyPiTransportFields(t *testing.T) { + assignment := "Read /tmp/spacedock-dispatches/spacedock-ensign-fixture-implementation.md and treat its content as your assignment." + + wrapped := SubagentStageDispatch(assignment, "implementation", "fixture implementation") + if wrapped.Context != "fresh" { + t.Fatalf("context = %q, want fresh", wrapped.Context) + } + if wrapped.Task != assignment { + t.Fatalf("wrapper rewrote assignment:\nwant: %s\n got: %s", assignment, wrapped.Task) + } + if wrapped.Phase != "implementation" || wrapped.Label != "fixture implementation" { + t.Fatalf("phase/label not preserved: %#v", wrapped) + } + + payload, err := json.Marshal(wrapped) + if err != nil { + t.Fatal(err) + } + if strings.Contains(string(payload), "acceptance") { + t.Fatalf("stage wrapper must not contain same-agent acceptance contract: %s", payload) + } +} diff --git a/skills/first-officer/references/pi-first-officer-runtime.md b/skills/first-officer/references/pi-first-officer-runtime.md index 7909028d..32249233 100644 --- a/skills/first-officer/references/pi-first-officer-runtime.md +++ b/skills/first-officer/references/pi-first-officer-runtime.md @@ -13,9 +13,11 @@ The Spacedock contract talks in terms of dispatch, completion, follow-up, and sh ## Dispatch -Use `spacedock dispatch build` with `host: "pi"` in the input JSON. Forward the emitted dispatch file prompt to the Pi worker without rewriting it into Claude syntax. For the first slice, dispatch should normally run with `bare_mode: true`: the Pi subagent result is the completion signal, so no `team_name` is required. +Use `spacedock dispatch build` with `host: "pi"` in the input JSON. The build artifact is the assignment source of truth: it carries the entity slug/name, entity path, workflow directory, target stage, stage definition fetch command, worktree path when applicable, completion checklist, and completion-signal wording. Forward the emitted dispatch file prompt or dispatch file content to the Pi worker without composing a replacement assignment and without rewriting it into Claude syntax. For the first slice, dispatch should normally run with `bare_mode: true`: the Pi subagent result is the completion signal, so no `team_name` is required. -A Pi first officer may dispatch via `subagent(...)` when that tool is available. The task must include the emitted dispatch-file prompt or the dispatch file content, the workflow directory, the entity path, and the completion checklist. The child must load the Spacedock ensign skill and Pi ensign runtime adapter before working. +A Pi first officer may dispatch via `subagent(...)` when that tool is available. The wrapper may add only Pi transport metadata around the build artifact: `context: "fresh"` plus optional human-facing `phase` / `label` values. Those wrapper fields are additive; they must not redefine or replace the builder-derived slug/name, workflow directory, entity path, target stage, worktree path, completion checklist, or completion contract. The child must load the Spacedock ensign skill and Pi ensign runtime adapter before working. + +Manual Pi prompt composition is a break-glass fallback only when `spacedock dispatch build` is unavailable, exits non-zero, or the developer is explicitly debugging the dispatch builder. Any fallback must record the builder failure or unavailability reason and include the minimum canonical schema facts so the degraded dispatch is auditable. For Spacedock stage dispatches through `pi-subagents`, call `subagent(...)` with explicit `context: "fresh"`. Do not rely on the worker agent's default context; Spacedock stage workers must be seeded by the task prompt/dispatch content, workflow directory, entity file, and completion checklist rather than inherited first-officer transcript context. diff --git a/skills/integration/skill_surface_test.go b/skills/integration/skill_surface_test.go index 93b8e673..8d018325 100644 --- a/skills/integration/skill_surface_test.go +++ b/skills/integration/skill_surface_test.go @@ -138,6 +138,74 @@ func TestPiFirstOfficerRuntimeRequiresFreshSubagentContextForStages(t *testing.T } } +func TestPiFirstOfficerRuntimeRequiresDispatchBuildArtifactForStages(t *testing.T) { + markNonAC(t, "Pi dispatch builder behavior is exercised by internal/dispatch build_pi_host tests and the Pi live runner; this is the shipped FO instruction-surface lint that requires FO to consume the generated dispatch artifact") + root := skillsRoot(t) + path := filepath.Join(root, "first-officer", "references", "pi-first-officer-runtime.md") + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read Pi first-officer runtime: %v", err) + } + dispatch := sectionAfter(string(data), "## Dispatch") + if dispatch == "" { + t.Fatal("Pi first-officer runtime is missing the Dispatch section") + } + + required := []string{ + "spacedock dispatch build", + "host: \"pi\"", + "assignment source of truth", + "entity slug/name", + "entity path", + "workflow directory", + "target stage", + "worktree path", + "completion checklist", + "emitted dispatch file prompt or dispatch file content", + "without composing a replacement assignment", + "context: \"fresh\"", + "phase", + "label", + "additive", + "must not redefine or replace", + } + for _, want := range required { + if !strings.Contains(dispatch, want) { + t.Errorf("Pi first-officer Dispatch section does not contain required dispatch-build invariant %q", want) + } + } +} + +func TestPiFirstOfficerRuntimeLimitsManualPromptFallback(t *testing.T) { + markNonAC(t, "Pi dispatch builder behavior is exercised by internal/dispatch build_pi_host tests and the Pi live runner; this is the shipped FO instruction-surface lint limiting manual prompt composition to break-glass fallback") + root := skillsRoot(t) + path := filepath.Join(root, "first-officer", "references", "pi-first-officer-runtime.md") + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read Pi first-officer runtime: %v", err) + } + dispatch := sectionAfter(string(data), "## Dispatch") + if dispatch == "" { + t.Fatal("Pi first-officer runtime is missing the Dispatch section") + } + + required := []string{ + "Manual Pi prompt composition", + "break-glass fallback only", + "spacedock dispatch build", + "unavailable", + "exits non-zero", + "explicitly debugging the dispatch builder", + "record the builder failure or unavailability reason", + "minimum canonical schema facts", + "auditable", + } + for _, want := range required { + if !strings.Contains(dispatch, want) { + t.Errorf("Pi first-officer Dispatch section does not contain required manual-fallback invariant %q", want) + } + } +} func TestPiFirstOfficerRuntimeForbidsSubagentAcceptanceForStages(t *testing.T) { markNonAC(t, "Pi live runner (internal/ensigncycle TestLivePiSubagentEnsignSmoke exercises the Pi subagent dispatch path)") root := skillsRoot(t) From cef8fe9b16201e93fc265f8f89415221b539800f Mon Sep 17 00:00:00 2001 From: CL Kao Date: Thu, 4 Jun 2026 21:02:44 -0700 Subject: [PATCH 2/4] Add Pi stage dispatch smoke fixture --- internal/dispatch/build_pi_host_test.go | 182 ++++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/internal/dispatch/build_pi_host_test.go b/internal/dispatch/build_pi_host_test.go index ebcd731c..341ec129 100644 --- a/internal/dispatch/build_pi_host_test.go +++ b/internal/dispatch/build_pi_host_test.go @@ -4,7 +4,9 @@ package dispatch import ( "encoding/json" + "fmt" "os" + "os/exec" "path/filepath" "strings" "testing" @@ -151,6 +153,186 @@ func TestBuildPiHostArtifactCarriesCanonicalStageFactsThroughPiWrapper(t *testin } } +func TestPiStageDispatchSmokeUsesBuildArtifactThroughWrapper(t *testing.T) { + root := t.TempDir() + stateDir := filepath.Join(root, "state-checkout") + writeFile(t, filepath.Join(root, "README.md"), readmeWorktree(true)) + if err := os.MkdirAll(stateDir, 0o755); err != nil { + t.Fatal(err) + } + worktreeRel := ".worktrees/spacedock-ensign-smoke-stage" + worktreePath := filepath.Join(root, worktreeRel) + if err := os.MkdirAll(worktreePath, 0o755); err != nil { + t.Fatal(err) + } + entityPath := filepath.Join(stateDir, "smoke-stage", "index.md") + writeFile(t, entityPath, entityFM("Smoke Stage", "implementation", worktreeRel)) + gitInit(t, stateDir) + + checklist := []string{"- run the fixture Pi worker", "- append durable stage report"} + stdin := mergeStdin(map[string]any{ + "schema_version": 2, + "entity_path": entityPath, + "workflow_dir": root, + "stage": "implementation", + "checklist": checklist, + "bare_mode": true, + "host": "pi", + }, nil) + + native := runNative(stdin, "build", "--workflow-dir", root) + if native.exit != 0 { + t.Fatalf("build exit=%d stderr=%q", native.exit, native.stderr) + } + var out struct { + DispatchFile string `json:"dispatch_file_path"` + Prompt string `json:"prompt"` + } + if err := json.Unmarshal([]byte(native.stdout), &out); err != nil { + t.Fatalf("stdout is not build JSON: %v\n%s", err, native.stdout) + } + wrapped := piruntime.SubagentStageDispatch(out.Prompt, "implementation", "z6 implementation fixback") + payload, err := json.Marshal(wrapped) + if err != nil { + t.Fatal(err) + } + + cmd := exec.Command(os.Args[0], "-test.run=^TestPiStageDispatchSmokeFixtureWorker$", "-test.v") + cmd.Env = append(os.Environ(), + "SPACEDOCK_PI_FIXTURE_WORKER=1", + "SPACEDOCK_PI_WRAPPER_JSON="+string(payload), + "SPACEDOCK_PI_ENTITY_PATH="+entityPath, + "SPACEDOCK_PI_STATE_DIR="+stateDir, + "SPACEDOCK_PI_WORKFLOW_DIR="+root, + "SPACEDOCK_PI_WORKTREE_PATH="+worktreePath, + ) + workerOut, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("fixture Pi worker process failed: %v\n%s", err, workerOut) + } + + entityBody, err := os.ReadFile(entityPath) + if err != nil { + t.Fatal(err) + } + for _, want := range []string{ + "## Stage Report: implementation fixback smoke", + "- process: fixture Pi worker exited 0", + "- dispatch_file: " + out.DispatchFile, + "- wrapper_context: fresh", + "- checklist_seen: run the fixture Pi worker; append durable stage report", + } { + if !strings.Contains(string(entityBody), want) { + t.Fatalf("entity report missing durable evidence %q:\n%s", want, entityBody) + } + } + + logCmd := exec.Command("git", "-C", stateDir, "log", "--oneline", "--", entityPath) + logOut, err := logCmd.CombinedOutput() + if err != nil { + t.Fatalf("state git log failed: %v\n%s", err, logOut) + } + if !strings.Contains(string(logOut), "fixture Pi stage report") { + t.Fatalf("state git log missing fixture stage-report commit:\n%s", logOut) + } + statusCmd := exec.Command("git", "-C", stateDir, "status", "--short", "--", entityPath) + statusOut, err := statusCmd.CombinedOutput() + if err != nil { + t.Fatalf("state git status failed: %v\n%s", err, statusOut) + } + if strings.TrimSpace(string(statusOut)) != "" { + t.Fatalf("state entity should be clean after path-scoped commit:\n%s", statusOut) + } +} + +func TestPiStageDispatchSmokeFixtureWorker(t *testing.T) { + if os.Getenv("SPACEDOCK_PI_FIXTURE_WORKER") != "1" { + return + } + var wrapped piruntime.SubagentDispatch + if err := json.Unmarshal([]byte(os.Getenv("SPACEDOCK_PI_WRAPPER_JSON")), &wrapped); err != nil { + t.Fatalf("wrapper JSON: %v", err) + } + if wrapped.Context != "fresh" { + t.Fatalf("wrapper context = %q, want fresh", wrapped.Context) + } + if strings.Contains(os.Getenv("SPACEDOCK_PI_WRAPPER_JSON"), "acceptance") { + t.Fatalf("wrapper unexpectedly serialized same-agent acceptance: %s", os.Getenv("SPACEDOCK_PI_WRAPPER_JSON")) + } + + dispatchPath, err := piDispatchFileFromTask(wrapped.Task) + if err != nil { + t.Fatal(err) + } + dispatchBody, err := os.ReadFile(dispatchPath) + if err != nil { + t.Fatalf("read dispatch artifact: %v", err) + } + entityPath := os.Getenv("SPACEDOCK_PI_ENTITY_PATH") + workflowDir := os.Getenv("SPACEDOCK_PI_WORKFLOW_DIR") + worktreePath := os.Getenv("SPACEDOCK_PI_WORKTREE_PATH") + for _, want := range []string{ + "You are working on: Smoke Stage", + "Stage: implementation", + "Read the entity file at " + entityPath, + "Your working directory for CODE is " + worktreePath, + "spacedock dispatch show-stage-def --workflow-dir " + shlexQuote(workflowDir) + " --stage implementation", + "- run the fixture Pi worker", + "- append durable stage report", + } { + if !strings.Contains(string(dispatchBody), want) { + t.Fatalf("dispatch artifact missing %q:\n%s", want, dispatchBody) + } + } + + report := fmt.Sprintf(` +## Stage Report: implementation fixback smoke + +- process: fixture Pi worker exited 0 +- dispatch_file: %s +- wrapper_context: %s +- wrapper_phase: %s +- wrapper_label: %s +- checklist_seen: run the fixture Pi worker; append durable stage report +`, dispatchPath, wrapped.Context, wrapped.Phase, wrapped.Label) + f, err := os.OpenFile(entityPath, os.O_APPEND|os.O_WRONLY, 0) + if err != nil { + t.Fatalf("open entity report: %v", err) + } + if _, err := f.WriteString(report); err != nil { + _ = f.Close() + t.Fatalf("append entity report: %v", err) + } + if err := f.Close(); err != nil { + t.Fatalf("close entity report: %v", err) + } + + stateDir := os.Getenv("SPACEDOCK_PI_STATE_DIR") + for _, args := range [][]string{ + {"-c", "user.email=t@t", "-c", "user.name=t", "add", entityPath}, + {"-c", "user.email=t@t", "-c", "user.name=t", "commit", "-q", "-m", "fixture Pi stage report", "--", entityPath}, + } { + cmd := exec.Command("git", append([]string{"-C", stateDir}, args...)...) + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("git %v: %v\n%s", args, err, out) + } + } +} + +func piDispatchFileFromTask(task string) (string, error) { + const prefix = "Read " + if !strings.HasPrefix(task, prefix) { + return "", fmt.Errorf("Pi task does not start with %q: %q", prefix, task) + } + rest := strings.TrimPrefix(task, prefix) + marker := " and treat its content as your assignment" + idx := strings.Index(rest, marker) + if idx < 0 { + return "", fmt.Errorf("Pi task does not forward read-dispatch-file assignment: %q", task) + } + return rest[:idx], nil +} + func TestBuildPiHostPreservesSplitRootEntityPath(t *testing.T) { root := t.TempDir() stateDir := filepath.Join(root, "state-checkout") From 91da0952ec60e8f038ec21f10be9ebf5313486c4 Mon Sep 17 00:00:00 2001 From: CL Kao Date: Thu, 4 Jun 2026 21:32:18 -0700 Subject: [PATCH 3/4] Fix Pi dispatch stage-def wrapper assertions --- internal/dispatch/build_pi_host_test.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/internal/dispatch/build_pi_host_test.go b/internal/dispatch/build_pi_host_test.go index 341ec129..9a0e1c73 100644 --- a/internal/dispatch/build_pi_host_test.go +++ b/internal/dispatch/build_pi_host_test.go @@ -138,7 +138,6 @@ func TestBuildPiHostArtifactCarriesCanonicalStageFactsThroughPiWrapper(t *testin "You are working on: Canonical Stage", "Stage: implementation", "Read the entity file at " + entityPath, - "spacedock dispatch show-stage-def --workflow-dir " + shlexQuote(root) + " --stage implementation", "Your working directory for CODE is " + worktreePath, "All CODE reads and writes MUST use paths under " + worktreePath, "- keep the builder assignment", @@ -148,6 +147,7 @@ func TestBuildPiHostArtifactCarriesCanonicalStageFactsThroughPiWrapper(t *testin t.Fatalf("Pi dispatch artifact missing builder-derived fact %q:\n%s", want, body) } } + assertRenderedStageDefCommand(t, body, root, "implementation") if !strings.Contains(wrapped.Task, out.DispatchFile) { t.Fatalf("Pi wrapper does not forward dispatch file path %s in task %q", out.DispatchFile, wrapped.Task) } @@ -271,19 +271,20 @@ func TestPiStageDispatchSmokeFixtureWorker(t *testing.T) { entityPath := os.Getenv("SPACEDOCK_PI_ENTITY_PATH") workflowDir := os.Getenv("SPACEDOCK_PI_WORKFLOW_DIR") worktreePath := os.Getenv("SPACEDOCK_PI_WORKTREE_PATH") + dispatchText := string(dispatchBody) for _, want := range []string{ "You are working on: Smoke Stage", "Stage: implementation", "Read the entity file at " + entityPath, "Your working directory for CODE is " + worktreePath, - "spacedock dispatch show-stage-def --workflow-dir " + shlexQuote(workflowDir) + " --stage implementation", "- run the fixture Pi worker", "- append durable stage report", } { - if !strings.Contains(string(dispatchBody), want) { + if !strings.Contains(dispatchText, want) { t.Fatalf("dispatch artifact missing %q:\n%s", want, dispatchBody) } } + assertRenderedStageDefCommand(t, dispatchText, workflowDir, "implementation") report := fmt.Sprintf(` ## Stage Report: implementation fixback smoke @@ -319,6 +320,19 @@ func TestPiStageDispatchSmokeFixtureWorker(t *testing.T) { } } +func assertRenderedStageDefCommand(t *testing.T, body, workflowDir, stage string) { + t.Helper() + semantic := "dispatch show-stage-def --workflow-dir " + shlexQuote(workflowDir) + " --stage " + stage + for _, want := range []string{ + "spacedock_launcher() {", + "spacedock_launcher " + semantic, + } { + if !strings.Contains(body, want) { + t.Fatalf("dispatch artifact missing rendered stage-definition command %q:\n%s", want, body) + } + } +} + func piDispatchFileFromTask(task string) (string, error) { const prefix = "Read " if !strings.HasPrefix(task, prefix) { From ebf2836017878b4c1950b57abecb44fbb792675b Mon Sep 17 00:00:00 2001 From: CL Kao Date: Thu, 4 Jun 2026 01:13:16 -0700 Subject: [PATCH 4/4] Ensure Pi dispatch wraps build artifacts --- skills/integration/skill_surface_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/skills/integration/skill_surface_test.go b/skills/integration/skill_surface_test.go index 8d018325..68e447fe 100644 --- a/skills/integration/skill_surface_test.go +++ b/skills/integration/skill_surface_test.go @@ -206,6 +206,7 @@ func TestPiFirstOfficerRuntimeLimitsManualPromptFallback(t *testing.T) { } } } + func TestPiFirstOfficerRuntimeForbidsSubagentAcceptanceForStages(t *testing.T) { markNonAC(t, "Pi live runner (internal/ensigncycle TestLivePiSubagentEnsignSmoke exercises the Pi subagent dispatch path)") root := skillsRoot(t)