Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .github/workflows/runtime-live-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,10 @@ jobs:
node -e "const p=require('$pi_npm_root/node_modules/pi-intercom/package.json'); if (p.name !== 'pi-intercom') throw new Error('unexpected pi-intercom package name '+p.name); console.log('verified '+p.name+'@'+p.version)"
test -f "$pi_npm_root/node_modules/pi-subagents/src/extension/index.ts"
test -f "$pi_npm_root/node_modules/pi-subagents/skills/pi-subagents/SKILL.md"
test -f "$pi_npm_root/node_modules/pi-subagents/src/intercom/intercom-bridge.ts"
test -f "$pi_npm_root/node_modules/pi-intercom/skills/pi-intercom/SKILL.md"
echo "PI_SUBAGENTS_PACKAGE_ROOT=$pi_npm_root/node_modules/pi-subagents" >> "$GITHUB_ENV"
echo "PI_INTERCOM_PACKAGE_ROOT=$pi_npm_root/node_modules/pi-intercom" >> "$GITHUB_ENV"

- name: Build spacedock binary
run: |
Expand All @@ -390,6 +393,8 @@ jobs:
test -f "$GITHUB_WORKSPACE/skills/ensign/references/pi-ensign-runtime.md"
test -f "$PI_SUBAGENTS_PACKAGE_ROOT/src/extension/index.ts"
test -f "$PI_SUBAGENTS_PACKAGE_ROOT/skills/pi-subagents/SKILL.md"
test -f "$PI_SUBAGENTS_PACKAGE_ROOT/src/intercom/intercom-bridge.ts"
test -f "$PI_INTERCOM_PACKAGE_ROOT/skills/pi-intercom/SKILL.md"

- name: Show tool versions
run: |
Expand Down
2 changes: 1 addition & 1 deletion docs/runtime-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,4 @@ For Pi, `spacedock pi` launches the proven front door by loading local resources
~/.pi/agent/npm/node_modules/pi-subagents/src/extension/index.ts
```

`spacedock install --host pi` is an idempotent readiness check and setup guide for that substrate; it does not install a Claude/Codex-style marketplace plugin and does not accept `--plugin-dir`. Resolve the local skill checkout by running it from the checkout or setting `SPACEDOCK_REPO_ROOT`. `spacedock doctor --host pi` reports the Pi CLI, auth file, `pi-subagents` extension/skill, and local Spacedock skill health; it still accepts `--plugin-dir <spacedock checkout>` for local skill checkout diagnostics. Live tests should not mutate global `~/.pi/agent`; they should keep using isolated Pi homes with copied auth.
`spacedock install --host pi` is an idempotent readiness check and setup guide for that substrate; it does not install a Claude/Codex-style marketplace plugin and does not accept `--plugin-dir`. Resolve the local skill checkout by running it from the checkout or setting `SPACEDOCK_REPO_ROOT`. `spacedock doctor --host pi` reports the Pi CLI, auth file, `pi-subagents` extension/skill, local Spacedock skill health, and supervisor-talkback setup prerequisites: the `pi-subagents` intercom bridge source, the resolved `PI_INTERCOM_PACKAGE_ROOT` package root, and the `pi-intercom` skill resource. Current `pi-subagents`/`pi-intercom` packages do not expose stable `pi-intercom` or `subagents-doctor` PATH commands, so readiness is based on package/resource paths instead of command shims. These doctor/install checks are necessary setup checks but insufficient to prove live supervisor talkback. Live proof still requires the cq-style `pi-intercom-supervisor-talkback` probe: progress update -> decision request -> supervisor reply -> child resume -> durable marker evidence. Live tests should not mutate global `~/.pi/agent`; they should keep using isolated Pi homes with copied auth.
141 changes: 97 additions & 44 deletions internal/cli/pi.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,39 @@ func (execPiRuntimeOps) Stat(path string) error { _, err := os.Sta
func (execPiRuntimeOps) Launch(argv []string) error { return execHost{}.Launch(argv, os.Environ()) }

type piRuntimeConfig struct {
repoRoot string
packageRoot string
extensionPath string
subagentsSkill string
firstOfficer string
ensign string
authPath string
openAIAPIKey string
pluginDirSource string
repoRoot string
packageRoot string
intercomPackageRoot string
extensionPath string
subagentsSkill string
firstOfficer string
ensign string
authPath string
openAIAPIKey string
sessionDir string
pluginDirSource string
packageRootSource string
intercomPackageSource string
authPathSource string
sessionDirSource string
}

type piCheckResult struct {
piBinOK bool
piBin string
authOK bool
extensionOK bool
subagentsSkillOK bool
firstOfficerOK bool
ensignOK bool
packageRoot string
repoRoot string
authPath string
piBinOK bool
piBin string
authOK bool
extensionOK bool
subagentsSkillOK bool
subagentsIntercomBridgeOK bool
intercomPackageOK bool
intercomSkillOK bool
firstOfficerOK bool
ensignOK bool
packageRoot string
intercomPackageRoot string
repoRoot string
authPath string
sessionDir string
}

func runPi(ctx context.Context, args []string, dir string, env []string, ops piRuntimeOps, stdout, stderr io.Writer) int {
Expand Down Expand Up @@ -92,21 +103,23 @@ func runInitWithPi(ctx context.Context, args []string, hostOps hostOps, piOps pi
}
cfg := piRuntimeConfigFromEnv(env, cwd(), pluginDir)
check := checkPiRuntime(piOps, cfg)
if piRuntimeLaunchReady(check) {
fmt.Fprintf(stdout, "Pi runtime ready.\n pi-subagents: %s\n Spacedock skills: %s\n", check.packageRoot, check.repoRoot)
return 0
}
if checkOnly {
printPiDoctorReport(stdout, check)
return piDoctorExit(check)
}
if piRuntimeLaunchReady(check) {
fmt.Fprintf(stdout, "Pi runtime ready.\n pi-subagents: %s\n pi-intercom: %s\n Spacedock skills: %s\n", check.packageRoot, check.intercomPackageRoot, check.repoRoot)
printPiSupervisorTalkbackBoundary(stdout)
return 0
}
fmt.Fprintf(stdout,
"Pi runtime setup incomplete.\n\n"+
"Required next steps:\n"+
" 1. Install Pi and authenticate so %s exists.\n"+
" 2. Install the subagent substrate, for example: pi install npm:pi-subagents\n"+
" 3. If pi-subagents is installed outside the default location, set PI_SUBAGENTS_PACKAGE_ROOT.\n"+
" 4. Re-run: spacedock doctor --host pi\n\n", check.authPath)
" 3. Install the supervisor-talkback substrate, for example: pi install npm:pi-intercom or npm install pi-intercom into the Pi npm root.\n"+
" 4. If pi-subagents or pi-intercom are installed outside the default locations, set PI_SUBAGENTS_PACKAGE_ROOT and PI_INTERCOM_PACKAGE_ROOT.\n"+
" 5. Re-run: spacedock doctor --host pi\n\n", check.authPath)
printPiDoctorReport(stdout, check)
return 0
}
Expand Down Expand Up @@ -196,6 +209,10 @@ func parsePiSetupArgs(command string, args []string, stderr io.Writer) (host str

func piRuntimeConfigFromEnv(env []string, dir, pluginDir string) piRuntimeConfig {
envMap := envMap(env)
home := envMap["HOME"]
if home == "" {
home = os.Getenv("HOME")
}
repo := pluginDir
pluginDirSource := "--plugin-dir"
if repo == "" {
Expand All @@ -207,50 +224,75 @@ func piRuntimeConfigFromEnv(env []string, dir, pluginDir string) piRuntimeConfig
pluginDirSource = "working directory"
}
pkg := envMap["PI_SUBAGENTS_PACKAGE_ROOT"]
pkgSource := "PI_SUBAGENTS_PACKAGE_ROOT"
if pkg == "" {
home := envMap["HOME"]
if home == "" {
home = os.Getenv("HOME")
}
pkg = filepath.Join(home, ".pi", "agent", "npm", "node_modules", "pi-subagents")
pkgSource = "default ~/.pi/agent/npm/node_modules/pi-subagents"
}
intercomPkg := envMap["PI_INTERCOM_PACKAGE_ROOT"]
intercomPkgSource := "PI_INTERCOM_PACKAGE_ROOT"
if intercomPkg == "" {
intercomPkg = filepath.Join(home, ".pi", "agent", "npm", "node_modules", "pi-intercom")
intercomPkgSource = "default ~/.pi/agent/npm/node_modules/pi-intercom"
}
authRoot := envMap["PI_CODING_AGENT_DIR"]
authPath := ""
authPathSource := "PI_CODING_AGENT_DIR"
if authRoot != "" {
authPath = filepath.Join(authRoot, "auth.json")
} else {
home := envMap["HOME"]
if home == "" {
home = os.Getenv("HOME")
}
authPath = filepath.Join(home, ".pi", "agent", "auth.json")
authPathSource = "default ~/.pi/agent/auth.json"
}
sessionDir := envMap["PI_CODING_AGENT_SESSION_DIR"]
sessionDirSource := "PI_CODING_AGENT_SESSION_DIR"
if sessionDir == "" {
sessionDir = filepath.Join(home, ".pi", "agent", "sessions")
sessionDirSource = "default ~/.pi/agent/sessions"
}
return piRuntimeConfig{
repoRoot: repo,
packageRoot: pkg,
extensionPath: filepath.Join(pkg, "src", "extension", "index.ts"),
subagentsSkill: filepath.Join(pkg, "skills", "pi-subagents"),
firstOfficer: filepath.Join(repo, "skills", "first-officer", "SKILL.md"),
ensign: filepath.Join(repo, "skills", "ensign", "SKILL.md"),
authPath: authPath,
openAIAPIKey: envMap["OPENAI_API_KEY"],
pluginDirSource: pluginDirSource,
repoRoot: repo,
packageRoot: pkg,
intercomPackageRoot: intercomPkg,
extensionPath: filepath.Join(pkg, "src", "extension", "index.ts"),
subagentsSkill: filepath.Join(pkg, "skills", "pi-subagents"),
firstOfficer: filepath.Join(repo, "skills", "first-officer", "SKILL.md"),
ensign: filepath.Join(repo, "skills", "ensign", "SKILL.md"),
authPath: authPath,
openAIAPIKey: envMap["OPENAI_API_KEY"],
sessionDir: sessionDir,
pluginDirSource: pluginDirSource,
packageRootSource: pkgSource,
intercomPackageSource: intercomPkgSource,
authPathSource: authPathSource,
sessionDirSource: sessionDirSource,
}
}

func checkPiRuntime(ops piRuntimeOps, cfg piRuntimeConfig) piCheckResult {
bin, err := ops.LookPath("pi")
res := piCheckResult{piBinOK: err == nil, piBin: bin, packageRoot: cfg.packageRoot, repoRoot: cfg.repoRoot, authPath: cfg.authPath}
res := piCheckResult{
piBinOK: err == nil,
piBin: bin,
packageRoot: cfg.packageRoot,
intercomPackageRoot: cfg.intercomPackageRoot,
repoRoot: cfg.repoRoot,
authPath: cfg.authPath,
sessionDir: cfg.sessionDir,
}
res.authOK = ops.Stat(cfg.authPath) == nil || strings.TrimSpace(cfg.openAIAPIKey) != ""
res.extensionOK = ops.Stat(cfg.extensionPath) == nil
res.subagentsSkillOK = ops.Stat(filepath.Join(cfg.subagentsSkill, "SKILL.md")) == nil
res.subagentsIntercomBridgeOK = ops.Stat(filepath.Join(cfg.packageRoot, "src", "intercom", "intercom-bridge.ts")) == nil
res.intercomPackageOK = ops.Stat(cfg.intercomPackageRoot) == nil
res.intercomSkillOK = ops.Stat(filepath.Join(cfg.intercomPackageRoot, "skills", "pi-intercom", "SKILL.md")) == nil
res.firstOfficerOK = ops.Stat(cfg.firstOfficer) == nil
res.ensignOK = ops.Stat(cfg.ensign) == nil
return res
}

func piRuntimeLaunchReady(c piCheckResult) bool {
return c.piBinOK && c.extensionOK && c.subagentsSkillOK && c.firstOfficerOK && c.ensignOK
return c.piBinOK && c.extensionOK && c.subagentsSkillOK && c.subagentsIntercomBridgeOK && c.intercomPackageOK && c.intercomSkillOK && c.firstOfficerOK && c.ensignOK
}

func piDoctorHealthy(c piCheckResult) bool {
Expand All @@ -270,8 +312,19 @@ func printPiDoctorReport(w io.Writer, c piCheckResult) {
printPiCheck(w, c.authOK, "Pi auth", c.authPath, "run `pi` login/auth flow; live tests copy this file into an isolated PI_CODING_AGENT_DIR")
printPiCheck(w, c.extensionOK, "pi-subagents extension", filepath.Join(c.packageRoot, "src", "extension", "index.ts"), "run `pi install npm:pi-subagents` or set PI_SUBAGENTS_PACKAGE_ROOT")
printPiCheck(w, c.subagentsSkillOK, "pi-subagents skill", filepath.Join(c.packageRoot, "skills", "pi-subagents"), "run `pi install npm:pi-subagents` or set PI_SUBAGENTS_PACKAGE_ROOT")
fmt.Fprintf(w, "INFO Pi auth/session dirs: auth=%s session=%s\n", c.authPath, c.sessionDir)
fmt.Fprintln(w, "Supervisor-talkback setup prerequisites")
printPiCheck(w, c.subagentsIntercomBridgeOK, "pi-subagents intercom bridge", filepath.Join(c.packageRoot, "src", "intercom", "intercom-bridge.ts"), "install/update pi-subagents or set PI_SUBAGENTS_PACKAGE_ROOT to a package root containing the intercom bridge")
printPiCheck(w, c.intercomPackageOK, "pi-intercom package root", c.intercomPackageRoot, "set PI_INTERCOM_PACKAGE_ROOT to the installed pi-intercom package root")
printPiCheck(w, c.intercomSkillOK, "pi-intercom skill", filepath.Join(c.intercomPackageRoot, "skills", "pi-intercom"), "install pi-intercom or set PI_INTERCOM_PACKAGE_ROOT to a package root containing skills/pi-intercom/SKILL.md")
printPiCheck(w, c.firstOfficerOK, "Spacedock first-officer skill", filepath.Join(c.repoRoot, "skills", "first-officer"), "pass --plugin-dir <spacedock checkout> or set SPACEDOCK_REPO_ROOT")
printPiCheck(w, c.ensignOK, "Spacedock ensign skill", filepath.Join(c.repoRoot, "skills", "ensign"), "pass --plugin-dir <spacedock checkout> or set SPACEDOCK_REPO_ROOT")
printPiSupervisorTalkbackBoundary(w)
}

func printPiSupervisorTalkbackBoundary(w io.Writer) {
fmt.Fprintln(w, "NOTE: These checks verify necessary supervisor-talkback setup prerequisites only; they are insufficient to prove live child talkback.")
fmt.Fprintln(w, "NOTE: Live proof still requires the cq-style progress -> decision -> supervisor reply -> child resume -> durable marker probe for pi-intercom-supervisor-talkback.")
}

func printPiCheck(w io.Writer, ok bool, label, path, remedy string) {
Expand Down
Loading
Loading