Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/runtime-live-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
16 changes: 16 additions & 0 deletions docs/runtime-live-ci-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions internal/dispatch/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -898,8 +898,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"
}
Expand Down
63 changes: 63 additions & 0 deletions internal/dispatch/build_stage_report_protocol_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 (
"os"
"path/filepath"
"strings"
"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) {
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)
}
}
}
59 changes: 59 additions & 0 deletions internal/ensigncycle/pi_nonself_describing_build_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ensigncycle

import (
"path/filepath"
"testing"
)

// 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"
}
170 changes: 170 additions & 0 deletions internal/ensigncycle/pi_nonself_describing_live_test.go
Original file line number Diff line number Diff line change
@@ -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"))
}
}