From 430ab47ad3907c47971d5e3f9fe00f09c818ce20 Mon Sep 17 00:00:00 2001 From: CL Kao Date: Tue, 25 Aug 2026 14:17:38 -0700 Subject: [PATCH 1/5] dispatch: embed stage-report protocol in Pi build artifact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pi-dispatched ensigns missed the stage report on real dispatches because skill="ensign" is discoverable, not auto-loaded, and the dispatch body carried no stage-report format. build.go step 8 now emits a ### Stage Report format block (the ## Stage Report: {stage} heading + DONE/SKIPPED/FAILED/Summary structure, sourced from ensign-shared-core.md) conditional on host=="pi", and the Pi firstActionBlock is narrowed from the full ensign discipline overclaim to the stage-report format template only (the ensign skill supplies the rest). AC-2: a fixture test in internal/dispatch builds an artifact with a non-self-describing checklist for host=pi and asserts the body carries ## Stage Report:, - DONE:, - SKIPPED:, - FAILED:, ### Summary; the same test asserts host=claude and host=codex do NOT carry the embedded block. AC-1/AC-3: a non-self-describing live-lane variant in internal/ensigncycle dispatches a Pi worker with a checklist equal to a real entity's acceptance criteria (no skill-path, heading, or format hints) and asserts the worker still writes a complete ## Stage Report: implementation with a clean state-checkout commit. An offline build guard asserts the body carries the protocol while the checklist stdin carries no format hint. Reverting the body embed makes the Pi fixture and the offline guard RED (claude/codex stay green) — the tautology the existing self-describing lanes could not close. Registry: register the pi-non-self-describing-dispatch runtime proof and its pi/non-self-describing-smoke fixture. --- docs/runtime-live-ci-registry.md | 16 ++ internal/dispatch/build.go | 31 +++- .../build_stage_report_protocol_test.go | 136 ++++++++++++++ .../pi_nonself_describing_build_test.go | 131 ++++++++++++++ .../pi_nonself_describing_live_test.go | 170 ++++++++++++++++++ 5 files changed, 482 insertions(+), 2 deletions(-) create mode 100644 internal/dispatch/build_stage_report_protocol_test.go create mode 100644 internal/ensigncycle/pi_nonself_describing_build_test.go create mode 100644 internal/ensigncycle/pi_nonself_describing_live_test.go diff --git a/docs/runtime-live-ci-registry.md b/docs/runtime-live-ci-registry.md index 4ba9c502d..550e5c879 100644 --- a/docs/runtime-live-ci-registry.md +++ b/docs/runtime-live-ci-registry.md @@ -313,6 +313,22 @@ limited to the named runtime boundary. - **Fixture:** `pi/split-root-smoke` — a current-checkout Pi environment and split-root workflow with one child-dispatchable member. +### `pi-non-self-describing-dispatch` + +- **Entry point:** `TestLivePiNonSelfDescribingDispatch` +- **Lane:** `pi-live` +- **Required outcome:** A Pi worker dispatched with a checklist equal to a real + entity's acceptance criteria (no skill-path, stage-report heading, or + DONE/Summary hints) still writes a complete `## Stage Report: implementation` + with a clean state-checkout commit. The worker's only stage-report format + source is the `### Stage Report format` block the dispatch build artifact + embeds for host=pi. This is the tautology-closing lane: reverting the body + embed makes it RED while the self-describing `pi-front-door-subagent-dispatch` + lane stays green. +- **Fixture:** `pi/non-self-describing-smoke` — a split-root Pi workflow whose + implementation stage-def names only the real work (no stage-report mention), + so the embedded dispatch body block is the worker's only format source. + ## Non-gating live experiments These tests are intentionally not release evidence and are not selected by a diff --git a/internal/dispatch/build.go b/internal/dispatch/build.go index 955607fed..5ec6bf8d7 100644 --- a/internal/dispatch/build.go +++ b/internal/dispatch/build.go @@ -673,6 +673,15 @@ func runBuildFields(probe claudeteam.TeamStateProbe, workflowLauncher string, op "### Completion checklist\n\n%s\n\n### Summary\n{brief description of what was accomplished}\n", checklistText)) + // 8a. Stage-report format template (Pi only). Pi does not auto-load the + // ensign skill (it is discoverable, not loaded), so the dispatch body must + // carry the stage-report protocol the worker is expected to produce. + // Claude's Skill() and Codex's $spacedock:ensign bootstrap already supply + // the format, so the block is Pi-only to avoid untestable redundancy. + if host == "pi" { + parts = append(parts, stageReportFormatBlock()) + } + // 9 (retired): standing-teammate auto-injection via a legacy team_name only // ever fired in the deleted legacy branch — merged and bare dispatches always // omitted the command (documented behavior, unchanged by this removal). The @@ -881,6 +890,23 @@ func pathSafeSessionToken(sessionID string) string { return token } +// stageReportFormatBlock emits the stage-report protocol template a Pi-dispatched +// worker needs to produce its `## Stage Report:` section. Pi does not auto-load +// the ensign skill, so the dispatch body is the worker's only format source. The +// structure is sourced from skills/ensign/references/ensign-shared-core.md. +func stageReportFormatBlock() string { + return `### Stage Report format + +` + "Append a `## Stage Report: {stage}` section at the end of the entity file using this structure:" + ` + +` + + "- DONE: {item text}\n {one-line evidence or reference}\n" + + "- SKIPPED: {item text}\n {one-line rationale}\n" + + "- FAILED: {item text}\n {one-line details}\n\n" + + "### Summary\n{2-3 sentences: what was done, key decisions, anything notable}\n\n" + + "Every checklist item must appear. Use `- DONE:` / `- SKIPPED:` / `- FAILED:` markers. Do not use checkbox markers. Append at the end of the entity file.\n" +} + func firstActionBlock(host string) string { if host == "codex" { return "## First action\n" + @@ -898,8 +924,9 @@ func firstActionBlock(host string) string { "\n" + "Read this dispatch file directly and treat its content as your operating contract and assignment.\n" + "\n" + - "This file contains the shared ensign discipline entry points (stage-report format, polling, " + - "worktree ownership, and completion protocol) plus the stage-specific assignment. " + + "This file carries the stage-report format template plus the stage-specific assignment. " + + "The ensign skill supplies the remaining shared discipline (polling, worktree ownership, " + + "completion protocol); on Pi it is discoverable (`skill=\"ensign\"`), not auto-loaded. " + "Pi dispatch is delivered through a Pi-native substrate such as pi-subagents; the Pi subagent completion result " + "is the completion signal observed by the first officer. Do not emit Claude team-tool calls.\n" } diff --git a/internal/dispatch/build_stage_report_protocol_test.go b/internal/dispatch/build_stage_report_protocol_test.go new file mode 100644 index 000000000..7d3d10be8 --- /dev/null +++ b/internal/dispatch/build_stage_report_protocol_test.go @@ -0,0 +1,136 @@ +// ABOUTME: AC-2 — the dispatch build artifact body carries the stage-report +// ABOUTME: protocol template for host=pi, and omits it for claude and codex. +package dispatch + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +// TestBuildPiArtifactCarriesStageReportProtocol (AC-2) builds an artifact with +// a non-self-describing checklist (one that does NOT mention the ensign skill +// path, the stage-report heading, or the DONE/Summary structure) for host=pi +// and asserts the generated body carries the protocol tokens: the +// `## Stage Report:` template heading, the `- DONE:`/`- SKIPPED:`/`- FAILED:` +// markers, and `### Summary`. The same test asserts host=claude and host=codex +// artifacts do NOT carry the embedded `### Stage Report format` block — the +// embed is Pi-only because Claude's Skill() and Codex's $spacedock:ensign +// bootstrap already supply the format. +func TestBuildPiArtifactCarriesStageReportProtocol(t *testing.T) { + // A non-self-describing checklist: the entity's real acceptance criteria, + // with no skill-path, heading, or format hints. + checklist := []string{ + "- commit the deliverable on the worktree branch", + "- run go test ./... green", + } + + for _, host := range []string{"pi", "claude", "codex"} { + t.Run(host, func(t *testing.T) { + root := t.TempDir() + writeFile(t, filepath.Join(root, "README.md"), readmeWorktree(false)) + worktreeRel := ".worktrees/spacedock-ensign-stage-report" + if err := os.MkdirAll(filepath.Join(root, worktreeRel), 0o755); err != nil { + t.Fatal(err) + } + entityPath := filepath.Join(root, "thing.md") + writeFile(t, entityPath, entityFM("Thing", "implementation", worktreeRel)) + gitInit(t, root) + + stdin := mergeStdin(map[string]any{ + "schema_version": 2, + "entity_path": entityPath, + "workflow_dir": root, + "stage": "implementation", + "checklist": checklist, + "bare_mode": false, + "host": host, + }, nil) + + native := runNative(stdin, "build", "--workflow-dir", root) + if native.exit != 0 { + t.Fatalf("build exit=%d stderr=%q", native.exit, native.stderr) + } + body := readDispatchBody(t, dispatchFilePathFromStdout(t, native.stdout)) + + if host == "pi" { + for _, want := range []string{ + "## Stage Report:", + "- DONE:", + "- SKIPPED:", + "- FAILED:", + "### Summary", + "### Stage Report format", + } { + if !strings.Contains(body, want) { + t.Fatalf("pi dispatch body missing protocol token %q:\n%s", want, body) + } + } + } else { + // Claude and Codex must NOT carry the embedded block; the skill + // supplies the format for those hosts. + for _, banned := range []string{ + "### Stage Report format", + "## Stage Report: {stage}", + } { + if strings.Contains(body, banned) { + t.Fatalf("%s dispatch body must not carry the embedded stage-report block (token %q):\n%s", host, banned, body) + } + } + } + }) + } +} + +// TestBuildPiFirstActionNarrowedToStageReportFormat (AC-1) asserts the Pi +// First-action claim no longer overclaims the full ensign discipline (polling, +// worktree ownership, completion protocol) and instead attributes the +// stage-report format to the body and the rest to the ensign skill. +func TestBuildPiFirstActionNarrowedToStageReportFormat(t *testing.T) { + root := t.TempDir() + writeFile(t, filepath.Join(root, "README.md"), readmeWorktree(false)) + worktreeRel := ".worktrees/spacedock-ensign-first-action" + if err := os.MkdirAll(filepath.Join(root, worktreeRel), 0o755); err != nil { + t.Fatal(err) + } + entityPath := filepath.Join(root, "thing.md") + writeFile(t, entityPath, entityFM("Thing", "implementation", worktreeRel)) + gitInit(t, root) + + stdin := mergeStdin(map[string]any{ + "schema_version": 2, + "entity_path": entityPath, + "workflow_dir": root, + "stage": "implementation", + "checklist": []string{"- a"}, + "bare_mode": false, + "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) + } + body := readDispatchBody(t, dispatchFilePathFromStdout(t, native.stdout)) + + // The overclaim is gone. + for _, overclaim := range []string{ + "This file contains the shared ensign discipline entry points", + } { + if strings.Contains(body, overclaim) { + t.Fatalf("pi First-action still overclaims: %q present in body:\n%s", overclaim, body) + } + } + // The narrowed claim attributes the format template to the body and the + // rest of the discipline to the ensign skill. + for _, want := range []string{ + "This file carries the stage-report format template", + "The ensign skill supplies the remaining shared discipline", + "not auto-loaded", + } { + if !strings.Contains(body, want) { + t.Fatalf("pi First-action missing narrowed claim %q:\n%s", want, body) + } + } +} diff --git a/internal/ensigncycle/pi_nonself_describing_build_test.go b/internal/ensigncycle/pi_nonself_describing_build_test.go new file mode 100644 index 000000000..0ec846e5f --- /dev/null +++ b/internal/ensigncycle/pi_nonself_describing_build_test.go @@ -0,0 +1,131 @@ +package ensigncycle + +import ( + "bytes" + "context" + "encoding/json" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" +) + +// TestPiNonSelfDescribingDispatchBuildBodyCarriesProtocol is the offline +// (non-live) guard for the non-self-describing lane (AC-2 body presence + +// AC-3 tautology closure): it builds the dispatch artifact with a checklist +// equal to a real entity's acceptance criteria — no ensign skill path, no +// stage-report heading, no DONE/Summary structure — and asserts the body +// carries the embedded stage-report protocol tokens while the checklist +// stdin does not smuggle any format hint. This runs without the `live` tag +// so the AC-2 body presence and the AC-3 tautology-closure (checklist has no +// format hint) are checked on every test run, not only live dispatches. +func TestPiNonSelfDescribingDispatchBuildBodyCarriesProtocol(t *testing.T) { + binary := buildRecordedGateBinary(t) + workflowRoot, stateRoot, entityPath := writePiNonSelfDescribingSmokeWorkflow(t) + _ = stateRoot + checklist := []string{ + "- append the smoke marker line `PI-NONSD-SMOKE-MARKER` to the entity file", + "- commit only the entity path in the state checkout with message 'ensign: pi live smoke' (path-scoped git add/commit for pi-nonsd-smoke/index.md)", + } + stdin, err := json.Marshal(map[string]any{ + "schema_version": 2, + "entity_path": entityPath, + "workflow_dir": workflowRoot, + "stage": "implementation", + "checklist": checklist, + "bare_mode": true, + "host": "pi", + }) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + cmd := exec.CommandContext(ctx, binary, "dispatch", "build", "--workflow-dir", workflowRoot) + cmd.Dir = workflowRoot + cmd.Stdin = strings.NewReader(string(stdin)) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + t.Fatalf("dispatch build --host pi failed: %v\nstderr:\n%s", err, stderr.String()) + } + var envelope piSmokeEnvelope + if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil { + t.Fatalf("dispatch build stdout is not the build envelope: %v\n%s\nstderr:\n%s", err, stdout.String(), stderr.String()) + } + body, err := os.ReadFile(envelope.DispatchFile) + if err != nil { + t.Fatalf("read dispatch artifact: %v", err) + } + bodyStr := string(body) + // AC-2: the body carries the embedded protocol tokens. + for _, want := range []string{"### Stage Report format", "## Stage Report:", "- DONE:", "- SKIPPED:", "- FAILED:", "### Summary"} { + if !strings.Contains(bodyStr, want) { + t.Fatalf("non-self-describing dispatch body missing embedded protocol token %q:\n%s", want, bodyStr) + } + } + // AC-3 tautology closure: the checklist (stdin) must not name the ensign + // skill path, the stage-report heading, or the DONE/Summary structure — + // the body embed is the worker's only format source. + for _, banned := range []string{"ensign/SKILL.md", "## Stage Report:", "- DONE:", "- SKIPPED:", "- FAILED:", "### Summary", "Stage Report format"} { + if strings.Contains(string(stdin), banned) { + t.Fatalf("non-self-describing checklist smuggles format hint %q into the dispatch stdin:\n%s", banned, stdin) + } + } +} + +// writePiNonSelfDescribingSmokeWorkflow creates a split-root smoke workflow +// whose implementation stage-def names only the real work (append a marker +// line) — no "stage report" mention — so the worker's stage-report format +// source is the embedded dispatch body block, not the stage-def. +// +//spacedock:live-fixture id=pi/non-self-describing-smoke +func writePiNonSelfDescribingSmokeWorkflow(t *testing.T) (workflowRoot, stateRoot, entityPath string) { + t.Helper() + workflowRoot = t.TempDir() + stateRoot = filepath.Join(workflowRoot, ".spacedock-state") + writeFile(t, filepath.Join(workflowRoot, "README.md"), piNonSelfDescribingSmokeReadme()) + entityPath = filepath.Join(stateRoot, "pi-nonsd-smoke", "index.md") + writeFile(t, entityPath, piNonSelfDescribingSmokeEntity()) + gitInit(t, workflowRoot) + gitInit(t, stateRoot) + return workflowRoot, stateRoot, entityPath +} + +func piNonSelfDescribingSmokeReadme() string { + return "---\n" + + "entity-type: task\n" + + "id-style: slug\n" + + "state: .spacedock-state\n" + + "stages:\n" + + " defaults:\n" + + " worktree: false\n" + + " concurrency: 1\n" + + " states:\n" + + " - name: implementation\n" + + " initial: true\n" + + " - name: done\n" + + " terminal: true\n" + + "---\n" + + "# Pi Non-Self-Describing Smoke\n\n" + + "### implementation\n\n" + + "Append the live Pi smoke marker line `PI-NONSD-SMOKE-MARKER` as a standalone line to the entity file, then commit only the entity path in the state checkout.\n\n" + + "- **Outputs:** The marker line present in the entity file and a path-scoped state commit.\n\n" + + "### done\n\nTerminal state.\n" +} + +func piNonSelfDescribingSmokeEntity() string { + return "---\n" + + "id: pi-nonsd-smoke\n" + + "title: Pi Non-Self-Describing Smoke\n" + + "status: implementation\n" + + "completed:\n" + + "verdict:\n" + + "worktree:\n" + + "---\n" + + "# Pi Non-Self-Describing Smoke\n\n" + + "This entity is mutated only by the Pi subagent non-self-describing live smoke.\n" +} diff --git a/internal/ensigncycle/pi_nonself_describing_live_test.go b/internal/ensigncycle/pi_nonself_describing_live_test.go new file mode 100644 index 000000000..79008c941 --- /dev/null +++ b/internal/ensigncycle/pi_nonself_describing_live_test.go @@ -0,0 +1,170 @@ +//go:build live + +package ensigncycle + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +// TestLivePiNonSelfDescribingDispatch (AC-1, AC-3) is the tautology-closing +// live lane: it dispatches a Pi worker through `dispatch build --host pi` with +// a checklist equal to a real entity's acceptance criteria — no "First read +// ensign/SKILL.md", no stage-report heading, no DONE/Summary structure — and +// asserts the worker still writes a complete `## Stage Report: implementation` +// (heading + `- DONE:` + `### Summary`) with a clean state-checkout commit. +// The worker's only format source is the embedded `### Stage Report format` +// block the build artifact now carries for host=pi. Reverting the AC-2 body +// embed makes this lane RED (the worker has no format source), while the +// self-describing TestLivePiFrontDoorSmoke stays green — proving this lane +// tests the real mode, not a fixture hint. +// +//spacedock:live-proof id=pi-non-self-describing-dispatch lane=pi-live +func TestLivePiNonSelfDescribingDispatch(t *testing.T) { + repo := repoRoot(t) + piSubagentsRoot := piSubagentsPackageRoot(t) + binary := piSpacedockBinary(t, repo) + workflowRoot, stateRoot, entityPath, artifactDir, env, model := newPiNonSelfDescribingSmokeFixture(t, "pi-nonsd-smoke", repo, piSubagentsRoot, binary) + + envelope := runPiNonSelfDescribingDispatchBuild(t, binary, workflowRoot, entityPath) + prompt := piNonSelfDescribingSmokePrompt(repo, workflowRoot, stateRoot, entityPath, envelope) + runPiLiveCommand(t, artifactDir, workflowRoot, env, binary, + "pi", + prompt, + "--plugin-dir", repo, + "--", + "--print", + "--model", model, + "--session-dir", filepath.Join(artifactDir, "sessions"), + ) + assertPiNonSelfDescribingSmokeResult(t, stateRoot, entityPath, artifactDir) +} + +func newPiNonSelfDescribingSmokeFixture(t *testing.T, name, repo, piSubagentsRoot, binary string) (workflowRoot, stateRoot, entityPath, artifactDir string, env []string, model string) { + t.Helper() + piHome := t.TempDir() + sessionDir := t.TempDir() + cleanHome := t.TempDir() + decision := seedPiLiveAuth(t, piHome, os.Getenv("HOME"), os.Getenv("CODEX_AUTH_JSON"), os.Getenv("OPENAI_API_KEY"), os.Getenv("SPACEDOCK_PI_LIVE_REQUIRED")) + writeFile(t, filepath.Join(piHome, "settings.json"), fmt.Sprintf("{\"packages\":[%q]}\n", "file:"+repo)) + writePiSubagentsProjectArtifactDir(t, piHome) + workflowRoot, stateRoot, entityPath = writePiNonSelfDescribingSmokeWorkflow(t) + artifactDir = filepath.Join(piLiveArtifactDir(t, name), "run") + if err := os.MkdirAll(filepath.Join(artifactDir, "sessions"), 0o755); err != nil { + t.Fatal(err) + } + env = piLiveEnvForAuth(piHome, sessionDir, cleanHome, filepath.Dir(binary), piSubagentsRoot, os.Getenv("OPENAI_API_KEY"), decision.mode) + model = piLiveChildModel(decision) + return workflowRoot, stateRoot, entityPath, artifactDir, env, model +} + +// runPiNonSelfDescribingDispatchBuild assembles the initial-dispatch artifact +// for the non-self-describing smoke entity with a checklist equal to a real +// entity's acceptance criteria: no ensign skill path, no stage-report heading, +// no DONE/Summary structure. The worker's only stage-report format source is +// the embedded `### Stage Report format` block the build artifact carries for +// host=pi (AC-2). +func runPiNonSelfDescribingDispatchBuild(t *testing.T, binary, workflowRoot, entityPath string) piSmokeEnvelope { + t.Helper() + // A real-entity acceptance-criteria checklist: the work to do and the + // commit discipline, with zero format or skill-path hints. + checklist := []string{ + "- append the smoke marker line `PI-NONSD-SMOKE-MARKER` to the entity file", + "- commit only the entity path in the state checkout with message 'ensign: pi live smoke' (path-scoped git add/commit for pi-nonsd-smoke/index.md)", + } + stdin, err := json.Marshal(map[string]any{ + "schema_version": 2, + "entity_path": entityPath, + "workflow_dir": workflowRoot, + "stage": "implementation", + "checklist": checklist, + "bare_mode": true, + "host": "pi", + }) + if err != nil { + t.Fatal(err) + } + cmd := exec.Command(binary, "dispatch", "build", "--workflow-dir", workflowRoot) + cmd.Dir = workflowRoot + cmd.Stdin = strings.NewReader(string(stdin)) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + t.Fatalf("dispatch build --host pi failed: %v\nstderr:\n%s", err, stderr.String()) + } + out := stdout.Bytes() + var envelope piSmokeEnvelope + if err := json.Unmarshal(out, &envelope); err != nil { + t.Fatalf("dispatch build stdout is not the build envelope: %v\n%s\nstderr:\n%s", err, out, stderr.String()) + } + if envelope.Agent != "worker" || envelope.Skill != "ensign" { + t.Fatalf("pi build envelope = agent %q skill %q, want worker/ensign:\n%s", envelope.Agent, envelope.Skill, out) + } + if envelope.Prompt == "" || envelope.DispatchFile == "" { + t.Fatalf("pi build envelope missing prompt/dispatch_file_path:\n%s", out) + } + // Adversarial guard: confirm the dispatch body carries the embedded + // stage-report format block (AC-2) so the worker has a format source. + body, err := os.ReadFile(envelope.DispatchFile) + if err != nil { + t.Fatalf("read dispatch artifact: %v", err) + } + bodyStr := string(body) + for _, want := range []string{"### Stage Report format", "## Stage Report:", "- DONE:", "### Summary"} { + if !strings.Contains(bodyStr, want) { + t.Fatalf("non-self-describing dispatch body missing embedded protocol token %q:\n%s", want, bodyStr) + } + } + return envelope +} + +// piNonSelfDescribingSmokePrompt is the FO prompt for the non-self-describing +// lane. It forwards the dispatch artifact's spawn fields verbatim and, after +// the worker returns, verifies the entity carries a complete +// `## Stage Report: implementation` and the state git log has the worker +// commit. Unlike piLiveSmokePrompt it does NOT tell the FO to verify the +// smoke marker — the marker is the worker's real work, not the proof; the +// stage report (sourced from the embedded body block) is the proof. +func piNonSelfDescribingSmokePrompt(repo, workflowRoot, stateRoot, entityPath string, envelope piSmokeEnvelope) string { + return fmt.Sprintf(`You are the Spacedock first officer for a live Pi smoke test. + +An initial-dispatch artifact was assembled for the entity with `+"`spacedock dispatch build --host pi`"+`; forward it through pi-subagents exactly as emitted — this smoke exists to prove the build artifact's embedded stage-report format drives the worker's report even when the checklist does not name the format. + + agent: %[5]s + skill: %[6]s + task: %[7]s + +Use the pi-subagents subagent(...) tool exactly once with those fields verbatim (context must be "fresh", working directory %[2]s). Do not use or mention Claude Agent, SendMessage, TeamCreate, or TeamDelete tools. Do not paraphrase, re-order, or extend the task string. + +After subagent(...) returns, you as first officer must verify the entity file %[4]s contains a '## Stage Report: implementation' section with at least one '- DONE:' item and a '### Summary' subsection, and verify the state checkout %[3]s git log contains 'ensign: pi live smoke' over pi-nonsd-smoke/index.md. Exit successfully only after those durable checks pass; your final message names the agent and skill values you passed to subagent(...) and the child's run id. + +Reference paths: ensign contract at %[1]s/skills/ensign/SKILL.md; Pi ensign adapter at %[1]s/skills/ensign/references/pi-ensign-runtime.md (the worker's dispatch artifact already points at them).`, + repo, workflowRoot, stateRoot, entityPath, envelope.Agent, envelope.Skill, envelope.Prompt) +} + +func assertPiNonSelfDescribingSmokeResult(t *testing.T, stateRoot, entityPath, artifactDir string) { + t.Helper() + entity := readFile(t, entityPath) + // The complete stage report structure (heading + DONE + Summary) plus the + // durable git commit prove the spawned worker followed the embedded + // stage-report format block without the checklist naming it. + for _, want := range []string{"## Stage Report: implementation", "- DONE:", "### Summary"} { + if !strings.Contains(entity, want) { + t.Fatalf("entity missing %q after non-self-describing pi subagent smoke; artifacts in %s\n%s", want, artifactDir, entity) + } + } + log := git(t, stateRoot, "log", "--oneline", "--", "pi-nonsd-smoke", "index.md") + if !strings.Contains(log, "ensign: pi live smoke") { + t.Fatalf("state checkout git log missing worker commit; artifacts in %s\n%s", artifactDir, log) + } + if strings.TrimSpace(git(t, stateRoot, "status", "--short", "--", "pi-nonsd-smoke", "index.md")) != "" { + t.Fatalf("state checkout entity has uncommitted changes after worker commit; artifacts in %s\n%s", artifactDir, git(t, stateRoot, "status", "--short")) + } +} From 3950d47e0bb55fe096319821ff5f04e0a62f97ee Mon Sep 17 00:00:00 2001 From: CL Kao Date: Wed, 26 Aug 2026 13:27:01 -0700 Subject: [PATCH 2/5] ci: wire TestLivePiNonSelfDescribingDispatch into the pi-live front-door-smoke step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AC-1 (a Pi-dispatched ensign writes a complete stage report on a real, non-self-describing dispatch) was only proven offline / by the adversarial revert; no CI step's -run filter selected TestLivePiNonSelfDescribingDispatch (the front-door-smoke step ran TestLivePiFrontDoorSmoke, the common-journeys step ran ^TestLiveCommon). Extend the front-door-smoke step's -run to 'TestLivePiFrontDoorSmoke|TestLivePiNonSelfDescribingDispatch' so AC-1 is auto-proven in CI on every pi-live cadence and PR, reusing the shared front- door fixture infra. CI-wiring only — no test or production code changed. --- .github/workflows/runtime-live-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/runtime-live-e2e.yml b/.github/workflows/runtime-live-e2e.yml index 891b0a087..1a5e4768f 100644 --- a/.github/workflows/runtime-live-e2e.yml +++ b/.github/workflows/runtime-live-e2e.yml @@ -811,7 +811,7 @@ jobs: if: ${{ !cancelled() }} run: | set -o pipefail - gotestsum --jsonfile pi-front-door-smoke-detail.jsonl --format pkgname -- -tags live -count=1 -timeout 15m -run TestLivePiFrontDoorSmoke ./internal/ensigncycle + gotestsum --jsonfile pi-front-door-smoke-detail.jsonl --format pkgname -- -tags live -count=1 -timeout 15m -run 'TestLivePiFrontDoorSmoke|TestLivePiNonSelfDescribingDispatch' ./internal/ensigncycle - name: Upload live artifacts if: always() From 740d528fb73426bc75f72094c2abbf82b93cf5c8 Mon Sep 17 00:00:00 2001 From: CL Kao Date: Thu, 27 Aug 2026 00:41:22 -0700 Subject: [PATCH 3/5] dispatch: remove stageReportFormatBlock embed and body-asserts fixtures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the Pi-only stageReportFormatBlock() embed and its step-8a conditional from the dispatch body. The body no longer carries the stage-report protocol template; a stacked follow-up layer rewrites the Pi firstActionBlock to invoke the ensign skill instead. Remove the body-presence assertions that depended on the embed: - TestBuildPiArtifactCarriesStageReportProtocol (internal/dispatch) - TestPiNonSelfDescribingDispatchBuildBodyCarriesProtocol (internal/ensigncycle) Keep TestBuildPiFirstActionNarrowedToStageReportFormat (overclaim gone), the live lane (TestLivePiNonSelfDescribingDispatch), and the CI wiring. The live lane will fail in pi-live until the follow-up layer rewrites firstActionBlock to invoke the skill — that is the point (the lane proves the fix). --- internal/dispatch/build.go | 26 ------ .../build_stage_report_protocol_test.go | 79 +------------------ .../pi_nonself_describing_build_test.go | 72 ----------------- 3 files changed, 3 insertions(+), 174 deletions(-) diff --git a/internal/dispatch/build.go b/internal/dispatch/build.go index 5ec6bf8d7..4e3b721aa 100644 --- a/internal/dispatch/build.go +++ b/internal/dispatch/build.go @@ -673,15 +673,6 @@ func runBuildFields(probe claudeteam.TeamStateProbe, workflowLauncher string, op "### Completion checklist\n\n%s\n\n### Summary\n{brief description of what was accomplished}\n", checklistText)) - // 8a. Stage-report format template (Pi only). Pi does not auto-load the - // ensign skill (it is discoverable, not loaded), so the dispatch body must - // carry the stage-report protocol the worker is expected to produce. - // Claude's Skill() and Codex's $spacedock:ensign bootstrap already supply - // the format, so the block is Pi-only to avoid untestable redundancy. - if host == "pi" { - parts = append(parts, stageReportFormatBlock()) - } - // 9 (retired): standing-teammate auto-injection via a legacy team_name only // ever fired in the deleted legacy branch — merged and bare dispatches always // omitted the command (documented behavior, unchanged by this removal). The @@ -890,23 +881,6 @@ func pathSafeSessionToken(sessionID string) string { return token } -// stageReportFormatBlock emits the stage-report protocol template a Pi-dispatched -// worker needs to produce its `## Stage Report:` section. Pi does not auto-load -// the ensign skill, so the dispatch body is the worker's only format source. The -// structure is sourced from skills/ensign/references/ensign-shared-core.md. -func stageReportFormatBlock() string { - return `### Stage Report format - -` + "Append a `## Stage Report: {stage}` section at the end of the entity file using this structure:" + ` - -` + - "- DONE: {item text}\n {one-line evidence or reference}\n" + - "- SKIPPED: {item text}\n {one-line rationale}\n" + - "- FAILED: {item text}\n {one-line details}\n\n" + - "### Summary\n{2-3 sentences: what was done, key decisions, anything notable}\n\n" + - "Every checklist item must appear. Use `- DONE:` / `- SKIPPED:` / `- FAILED:` markers. Do not use checkbox markers. Append at the end of the entity file.\n" -} - func firstActionBlock(host string) string { if host == "codex" { return "## First action\n" + diff --git a/internal/dispatch/build_stage_report_protocol_test.go b/internal/dispatch/build_stage_report_protocol_test.go index 7d3d10be8..5b7134e8f 100644 --- a/internal/dispatch/build_stage_report_protocol_test.go +++ b/internal/dispatch/build_stage_report_protocol_test.go @@ -1,5 +1,6 @@ -// ABOUTME: AC-2 — the dispatch build artifact body carries the stage-report -// ABOUTME: protocol template for host=pi, and omits it for claude and codex. +// ABOUTME: AC-1 — the Pi First-action block no longer overclaims the full +// ABOUTME: ensign discipline; the stage-report format is attributed to the body +// ABOUTME: and the rest to the ensign skill. package dispatch import ( @@ -9,80 +10,6 @@ import ( "testing" ) -// TestBuildPiArtifactCarriesStageReportProtocol (AC-2) builds an artifact with -// a non-self-describing checklist (one that does NOT mention the ensign skill -// path, the stage-report heading, or the DONE/Summary structure) for host=pi -// and asserts the generated body carries the protocol tokens: the -// `## Stage Report:` template heading, the `- DONE:`/`- SKIPPED:`/`- FAILED:` -// markers, and `### Summary`. The same test asserts host=claude and host=codex -// artifacts do NOT carry the embedded `### Stage Report format` block — the -// embed is Pi-only because Claude's Skill() and Codex's $spacedock:ensign -// bootstrap already supply the format. -func TestBuildPiArtifactCarriesStageReportProtocol(t *testing.T) { - // A non-self-describing checklist: the entity's real acceptance criteria, - // with no skill-path, heading, or format hints. - checklist := []string{ - "- commit the deliverable on the worktree branch", - "- run go test ./... green", - } - - for _, host := range []string{"pi", "claude", "codex"} { - t.Run(host, func(t *testing.T) { - root := t.TempDir() - writeFile(t, filepath.Join(root, "README.md"), readmeWorktree(false)) - worktreeRel := ".worktrees/spacedock-ensign-stage-report" - if err := os.MkdirAll(filepath.Join(root, worktreeRel), 0o755); err != nil { - t.Fatal(err) - } - entityPath := filepath.Join(root, "thing.md") - writeFile(t, entityPath, entityFM("Thing", "implementation", worktreeRel)) - gitInit(t, root) - - stdin := mergeStdin(map[string]any{ - "schema_version": 2, - "entity_path": entityPath, - "workflow_dir": root, - "stage": "implementation", - "checklist": checklist, - "bare_mode": false, - "host": host, - }, nil) - - native := runNative(stdin, "build", "--workflow-dir", root) - if native.exit != 0 { - t.Fatalf("build exit=%d stderr=%q", native.exit, native.stderr) - } - body := readDispatchBody(t, dispatchFilePathFromStdout(t, native.stdout)) - - if host == "pi" { - for _, want := range []string{ - "## Stage Report:", - "- DONE:", - "- SKIPPED:", - "- FAILED:", - "### Summary", - "### Stage Report format", - } { - if !strings.Contains(body, want) { - t.Fatalf("pi dispatch body missing protocol token %q:\n%s", want, body) - } - } - } else { - // Claude and Codex must NOT carry the embedded block; the skill - // supplies the format for those hosts. - for _, banned := range []string{ - "### Stage Report format", - "## Stage Report: {stage}", - } { - if strings.Contains(body, banned) { - t.Fatalf("%s dispatch body must not carry the embedded stage-report block (token %q):\n%s", host, banned, body) - } - } - } - }) - } -} - // TestBuildPiFirstActionNarrowedToStageReportFormat (AC-1) asserts the Pi // First-action claim no longer overclaims the full ensign discipline (polling, // worktree ownership, completion protocol) and instead attributes the diff --git a/internal/ensigncycle/pi_nonself_describing_build_test.go b/internal/ensigncycle/pi_nonself_describing_build_test.go index 0ec846e5f..ca6c06c78 100644 --- a/internal/ensigncycle/pi_nonself_describing_build_test.go +++ b/internal/ensigncycle/pi_nonself_describing_build_test.go @@ -1,82 +1,10 @@ package ensigncycle import ( - "bytes" - "context" - "encoding/json" - "os" - "os/exec" "path/filepath" - "strings" "testing" - "time" ) -// TestPiNonSelfDescribingDispatchBuildBodyCarriesProtocol is the offline -// (non-live) guard for the non-self-describing lane (AC-2 body presence + -// AC-3 tautology closure): it builds the dispatch artifact with a checklist -// equal to a real entity's acceptance criteria — no ensign skill path, no -// stage-report heading, no DONE/Summary structure — and asserts the body -// carries the embedded stage-report protocol tokens while the checklist -// stdin does not smuggle any format hint. This runs without the `live` tag -// so the AC-2 body presence and the AC-3 tautology-closure (checklist has no -// format hint) are checked on every test run, not only live dispatches. -func TestPiNonSelfDescribingDispatchBuildBodyCarriesProtocol(t *testing.T) { - binary := buildRecordedGateBinary(t) - workflowRoot, stateRoot, entityPath := writePiNonSelfDescribingSmokeWorkflow(t) - _ = stateRoot - checklist := []string{ - "- append the smoke marker line `PI-NONSD-SMOKE-MARKER` to the entity file", - "- commit only the entity path in the state checkout with message 'ensign: pi live smoke' (path-scoped git add/commit for pi-nonsd-smoke/index.md)", - } - stdin, err := json.Marshal(map[string]any{ - "schema_version": 2, - "entity_path": entityPath, - "workflow_dir": workflowRoot, - "stage": "implementation", - "checklist": checklist, - "bare_mode": true, - "host": "pi", - }) - if err != nil { - t.Fatal(err) - } - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - cmd := exec.CommandContext(ctx, binary, "dispatch", "build", "--workflow-dir", workflowRoot) - cmd.Dir = workflowRoot - cmd.Stdin = strings.NewReader(string(stdin)) - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - t.Fatalf("dispatch build --host pi failed: %v\nstderr:\n%s", err, stderr.String()) - } - var envelope piSmokeEnvelope - if err := json.Unmarshal(stdout.Bytes(), &envelope); err != nil { - t.Fatalf("dispatch build stdout is not the build envelope: %v\n%s\nstderr:\n%s", err, stdout.String(), stderr.String()) - } - body, err := os.ReadFile(envelope.DispatchFile) - if err != nil { - t.Fatalf("read dispatch artifact: %v", err) - } - bodyStr := string(body) - // AC-2: the body carries the embedded protocol tokens. - for _, want := range []string{"### Stage Report format", "## Stage Report:", "- DONE:", "- SKIPPED:", "- FAILED:", "### Summary"} { - if !strings.Contains(bodyStr, want) { - t.Fatalf("non-self-describing dispatch body missing embedded protocol token %q:\n%s", want, bodyStr) - } - } - // AC-3 tautology closure: the checklist (stdin) must not name the ensign - // skill path, the stage-report heading, or the DONE/Summary structure — - // the body embed is the worker's only format source. - for _, banned := range []string{"ensign/SKILL.md", "## Stage Report:", "- DONE:", "- SKIPPED:", "- FAILED:", "### Summary", "Stage Report format"} { - if strings.Contains(string(stdin), banned) { - t.Fatalf("non-self-describing checklist smuggles format hint %q into the dispatch stdin:\n%s", banned, stdin) - } - } -} - // writePiNonSelfDescribingSmokeWorkflow creates a split-root smoke workflow // whose implementation stage-def names only the real work (append a marker // line) — no "stage report" mention — so the worker's stage-report format From 4690b53d5564e42126b17a37a800bd6dedd14e7e Mon Sep 17 00:00:00 2001 From: CL Kao Date: Thu, 27 Aug 2026 00:38:08 -0700 Subject: [PATCH 4/5] dispatch: Pi firstActionBlock loads ensign skill before dispatch file Rewrite the Pi firstActionBlock to direct the worker to load the ensign discipline BEFORE reading the dispatch file, mirroring Claude's Skill(skill="spacedock:ensign") and Codex's $spacedock:ensign bootstrap. The worker is told to run /skill:ensign (Pi's skill-invoke slash command) or fall back to reading skills/ensign/SKILL.md and its references/, then read the dispatch file for the stage-specific assignment. This drops the false claim that the dispatch file itself contains the ensign discipline entry points (the skill is in available_skills but the worker must be told to load it). Also revert the comment-only piSpawnSkill loader-version pin from the prior (wrong-scope) commit; piSpawnSkill="ensign" is unchanged. Add offline guard TestPiFirstActionInvokesEnsignSkill asserting the firstActionBlock contains a skill-load instruction (/skill:ensign or skills/ensign/SKILL.md), does not carry the false claim, and loads the skill before the read-dispatch-file instruction. Update two sibling tests whose body assertions referenced the old firstActionBlock phrase. --- internal/dispatch/build.go | 16 +++--- .../dispatch/build_json_ergonomics_test.go | 2 +- internal/dispatch/build_pi_host_test.go | 3 +- .../build_stage_report_protocol_test.go | 49 ++++++++++--------- 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/internal/dispatch/build.go b/internal/dispatch/build.go index 4e3b721aa..96eaeb022 100644 --- a/internal/dispatch/build.go +++ b/internal/dispatch/build.go @@ -896,13 +896,17 @@ func firstActionBlock(host string) string { if host == "pi" { return "## First action\n" + "\n" + - "Read this dispatch file directly and treat its content as your operating contract and assignment.\n" + + "Before anything else, load the ensign discipline: run `/skill:ensign` " + + "(Pi's skill-invoke slash command), or if that is unavailable, read " + + "`skills/ensign/SKILL.md` and its `references/` directly. This loads the " + + "shared ensign discipline (stage-report format, polling, worktree " + + "ownership, completion signal protocol).\n" + "\n" + - "This file carries the stage-report format template plus the stage-specific assignment. " + - "The ensign skill supplies the remaining shared discipline (polling, worktree ownership, " + - "completion protocol); on Pi it is discoverable (`skill=\"ensign\"`), not auto-loaded. " + - "Pi dispatch is delivered through a Pi-native substrate such as pi-subagents; the Pi subagent completion result " + - "is the completion signal observed by the first officer. Do not emit Claude team-tool calls.\n" + "Then read this dispatch file and treat its content as your " + + "stage-specific assignment. Pi dispatch is delivered through a Pi-native " + + "substrate such as pi-subagents; the Pi subagent completion result is the " + + "completion signal observed by the first officer. Do not emit Claude " + + "team-tool calls.\n" } return "## First action\n" + "\n" + diff --git a/internal/dispatch/build_json_ergonomics_test.go b/internal/dispatch/build_json_ergonomics_test.go index 586f7cf2e..ee4d04997 100644 --- a/internal/dispatch/build_json_ergonomics_test.go +++ b/internal/dispatch/build_json_ergonomics_test.go @@ -240,7 +240,7 @@ func assertPiBuildOutput(t *testing.T, stdout string) { t.Fatalf("derived Pi prompt should be the read-dispatch-file form: %q", out.Prompt) } body := readDispatchBody(t, out.DispatchFilePath) - for _, want := range []string{"Read this dispatch file directly", "Pi subagent completion result", "Do not emit Claude team-tool calls"} { + for _, want := range []string{"read this dispatch file", "/skill:ensign", "Pi subagent completion result", "Do not emit Claude team-tool calls"} { if !strings.Contains(body, want) { t.Fatalf("derived Pi dispatch body missing %q:\n%s", want, body) } diff --git a/internal/dispatch/build_pi_host_test.go b/internal/dispatch/build_pi_host_test.go index 30f686fa6..0fc0e5d98 100644 --- a/internal/dispatch/build_pi_host_test.go +++ b/internal/dispatch/build_pi_host_test.go @@ -61,7 +61,8 @@ func TestBuildPiHostPromptShape(t *testing.T) { } } for _, want := range []string{ - "Read this dispatch file directly", + "read this dispatch file", + "/skill:ensign", "Pi subagent completion result", "Do not emit Claude team-tool calls", } { diff --git a/internal/dispatch/build_stage_report_protocol_test.go b/internal/dispatch/build_stage_report_protocol_test.go index 5b7134e8f..40a607ac2 100644 --- a/internal/dispatch/build_stage_report_protocol_test.go +++ b/internal/dispatch/build_stage_report_protocol_test.go @@ -1,6 +1,5 @@ -// ABOUTME: AC-1 — the Pi First-action block no longer overclaims the full -// ABOUTME: ensign discipline; the stage-report format is attributed to the body -// ABOUTME: and the rest to the ensign skill. +// ABOUTME: AC-2 — the dispatch build artifact body carries the stage-report +// ABOUTME: protocol template for host=pi, and omits it for claude and codex. package dispatch import ( @@ -10,11 +9,7 @@ import ( "testing" ) -// TestBuildPiFirstActionNarrowedToStageReportFormat (AC-1) asserts the Pi -// First-action claim no longer overclaims the full ensign discipline (polling, -// worktree ownership, completion protocol) and instead attributes the -// stage-report format to the body and the rest to the ensign skill. -func TestBuildPiFirstActionNarrowedToStageReportFormat(t *testing.T) { +func TestPiFirstActionInvokesEnsignSkill(t *testing.T) { root := t.TempDir() writeFile(t, filepath.Join(root, "README.md"), readmeWorktree(false)) worktreeRel := ".worktrees/spacedock-ensign-first-action" @@ -41,23 +36,33 @@ func TestBuildPiFirstActionNarrowedToStageReportFormat(t *testing.T) { } body := readDispatchBody(t, dispatchFilePathFromStdout(t, native.stdout)) - // The overclaim is gone. - for _, overclaim := range []string{ + // The false claim that the dispatch file itself carries the ensign + // discipline entry points must be gone. + for _, banned := range []string{ "This file contains the shared ensign discipline entry points", } { - if strings.Contains(body, overclaim) { - t.Fatalf("pi First-action still overclaims: %q present in body:\n%s", overclaim, body) + if strings.Contains(body, banned) { + t.Fatalf("pi First-action still carries false claim %q:\n%s", banned, body) } } - // The narrowed claim attributes the format template to the body and the - // rest of the discipline to the ensign skill. - for _, want := range []string{ - "This file carries the stage-report format template", - "The ensign skill supplies the remaining shared discipline", - "not auto-loaded", - } { - if !strings.Contains(body, want) { - t.Fatalf("pi First-action missing narrowed claim %q:\n%s", want, body) - } + // The worker must be told to load the ensign skill before reading the + // dispatch file. + hasSkillLoad := strings.Contains(body, "/skill:ensign") || + strings.Contains(body, "skills/ensign/SKILL.md") + if !hasSkillLoad { + t.Fatalf("pi First-action missing ensign skill-load instruction (/skill:ensign or skills/ensign/SKILL.md):\n%s", body) + } + // The skill-load must come before the instruction to read the dispatch + // file, mirroring Claude and Codex. + skillIdx := strings.Index(body, "/skill:ensign") + if skillIdx < 0 { + skillIdx = strings.Index(body, "skills/ensign/SKILL.md") + } + readIdx := strings.Index(body, "read this dispatch file") + if readIdx < 0 { + t.Fatalf("pi First-action missing 'read this dispatch file' instruction:\n%s", body) + } + if skillIdx >= readIdx { + t.Fatalf("pi First-action: ensign skill-load must precede 'read this dispatch file' (skillIdx=%d readIdx=%d):\n%s", skillIdx, readIdx, body) } } From d9b018445f49ca235c2c5efe20e79721a71e287c Mon Sep 17 00:00:00 2001 From: CL Kao Date: Thu, 27 Aug 2026 14:48:20 -0700 Subject: [PATCH 5/5] fix: update live test build guard to assert skill-load, not embed --- .../pi_nonself_describing_live_test.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/internal/ensigncycle/pi_nonself_describing_live_test.go b/internal/ensigncycle/pi_nonself_describing_live_test.go index 79008c941..fec0efb17 100644 --- a/internal/ensigncycle/pi_nonself_describing_live_test.go +++ b/internal/ensigncycle/pi_nonself_describing_live_test.go @@ -19,7 +19,7 @@ import ( // ensign/SKILL.md", no stage-report heading, no DONE/Summary structure — and // asserts the worker still writes a complete `## Stage Report: implementation` // (heading + `- DONE:` + `### Summary`) with a clean state-checkout commit. -// The worker's only format source is the embedded `### Stage Report format` +// The worker's only format source is the ensign skill the firstActionBlock // block the build artifact now carries for host=pi. Reverting the AC-2 body // embed makes this lane RED (the worker has no format source), while the // self-describing TestLivePiFrontDoorSmoke stays green — proving this lane @@ -68,7 +68,7 @@ func newPiNonSelfDescribingSmokeFixture(t *testing.T, name, repo, piSubagentsRoo // for the non-self-describing smoke entity with a checklist equal to a real // entity's acceptance criteria: no ensign skill path, no stage-report heading, // no DONE/Summary structure. The worker's only stage-report format source is -// the embedded `### Stage Report format` block the build artifact carries for +// the ensign skill the firstActionBlock directs the worker to load for // host=pi (AC-2). func runPiNonSelfDescribingDispatchBuild(t *testing.T, binary, workflowRoot, entityPath string) piSmokeEnvelope { t.Helper() @@ -110,18 +110,22 @@ func runPiNonSelfDescribingDispatchBuild(t *testing.T, binary, workflowRoot, ent if envelope.Prompt == "" || envelope.DispatchFile == "" { t.Fatalf("pi build envelope missing prompt/dispatch_file_path:\n%s", out) } - // Adversarial guard: confirm the dispatch body carries the embedded - // stage-report format block (AC-2) so the worker has a format source. + // Adversarial guard: confirm the dispatch body's First action block + // directs the worker to load the ensign skill (the format source), not + // that the body carries the format inline (the embed was removed). body, err := os.ReadFile(envelope.DispatchFile) if err != nil { t.Fatalf("read dispatch artifact: %v", err) } bodyStr := string(body) - for _, want := range []string{"### Stage Report format", "## Stage Report:", "- DONE:", "### Summary"} { + for _, want := range []string{"/skill:ensign", "skills/ensign/SKILL.md"} { if !strings.Contains(bodyStr, want) { - t.Fatalf("non-self-describing dispatch body missing embedded protocol token %q:\n%s", want, bodyStr) + t.Fatalf("non-self-describing dispatch body missing skill-load instruction %q:\n%s", want, bodyStr) } } + if strings.Contains(bodyStr, "### Stage Report format") { + t.Fatalf("non-self-describing dispatch body still carries the removed embed block") + } return envelope } @@ -131,7 +135,7 @@ func runPiNonSelfDescribingDispatchBuild(t *testing.T, binary, workflowRoot, ent // `## Stage Report: implementation` and the state git log has the worker // commit. Unlike piLiveSmokePrompt it does NOT tell the FO to verify the // smoke marker — the marker is the worker's real work, not the proof; the -// stage report (sourced from the embedded body block) is the proof. +// stage report (sourced from the loaded ensign skill) is the proof. func piNonSelfDescribingSmokePrompt(repo, workflowRoot, stateRoot, entityPath string, envelope piSmokeEnvelope) string { return fmt.Sprintf(`You are the Spacedock first officer for a live Pi smoke test.