paseo-room 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -85,8 +85,11 @@ starts, its exit code is preserved; a signal is returned using the conventional
85
85
  query Keychain state, and reports older-runtime Keychain isolation as unverifiable. See
86
86
  Anthropic's [credential-management reference](https://code.claude.com/docs/en/authentication#credential-management).
87
87
  - An initialised Pi home (`~/.pi/agent`) and an operator-installed global
88
- `pi-mcp-adapter` package under its `npm/node_modules`. The package version is diagnostic,
89
- not a compatibility constraint. The room never installs or upgrades Pi or the adapter.
88
+ `pi-mcp-adapter` package under its `npm/node_modules` (`pi install npm:pi-mcp-adapter`). The
89
+ package version is diagnostic, not a compatibility constraint. Pi 1.0's built-in MCP extension
90
+ does not replace it: Paseo gives a Pi seat its MCP servers only through the adapter, which it
91
+ recognises by name and configures with the adapter's own `--mcp-config` flag. The room never
92
+ installs or upgrades Pi or the adapter; setup stops with that install command instead.
90
93
 
91
94
  ## What gets created
92
95
 
@@ -285,17 +288,35 @@ but what the model actually reads is not proven, and `CLAUDE.md` is what covers
285
288
  Pi providers use a strict command tail:
286
289
 
287
290
  ```text
288
- --no-extensions --extension <canonical pi-mcp-adapter entry> --no-approve
291
+ --no-extensions --extension <canonical pi-mcp-adapter entry>
292
+ [--extension <canonical entry of each selected package>] --no-approve
289
293
  --append-system-prompt <role APPEND_SYSTEM.md>
290
294
  ```
291
295
 
296
+ Each Pi provider also pins `PASEO_ROOM_ROLE=<role>`, so an extension can tell it runs as a
297
+ seat. To load more of your own installed Pi packages in the seats, select them at setup:
298
+
299
+ ```bash
300
+ paseo-room setup --agent pi --pi-extension pi-blackbytes --pi-extension pi-provider-kiro=supervisor,lead,peer --apply
301
+ ```
302
+
303
+ Roles default to `supervisor,lead`; Peer gets an extension only when named, with a warning.
304
+ Packages load only from your Pi home's `npm/node_modules`, with the same containment checks
305
+ as the adapter; package prompts, skills and themes are not loaded. A package that is not
306
+ installed is a warning with its `pi install npm:<package>` command and the seat starts without
307
+ it; the room never installs one. Selecting a package states that it opens no second
308
+ multi-agent path in a seat (`pi-blackbytes` 3.1.0 turns its sub-agents off when
309
+ `PASEO_ROOM_ROLE` is set). The choice is recorded in `room.json`; a later `setup` without the
310
+ flag removes the extensions and warns which ones. See
311
+ [docs/design/pi-seat-extensions.md](docs/design/pi-seat-extensions.md).
312
+
292
313
  This is Paseo's core `pi` provider, not an OMP provider or compatibility layer.
293
314
  Paseo appends and owns its own generated temporary integration extension; `paseo-room` does
294
315
  not resolve, create or include it. Before planning any writes, the room validates the exact
295
316
  global package identity and canonical entry containment, then runs one bounded offline Pi RPC
296
- `get_commands` probe with `PI_CODING_AGENT_DIR=/dev/null`,
297
- `PI_MCP_CONFIG_MODE=exclusive`, and `HOME` set to a fresh temporary working directory that is
298
- removed afterwards. This prevents adapter startup from selecting operator-global or caller
317
+ `get_commands` probe that loads the adapter and every selected extension, with
318
+ `PI_MCP_CONFIG_MODE=exclusive`, `PASEO_ROOM_ROLE=lead`, `HOME` set to a fresh temporary working
319
+ directory that is removed afterwards, and `PI_CODING_AGENT_DIR` an empty directory inside it. This prevents adapter startup from selecting operator-global or caller
299
320
  project MCP configs during the probe. The correlated response must
300
321
  attribute `/mcp` to `source: "extension"` at that same canonical path. Missing, malformed,
301
322
  path-escaped or wrongly attributed adapters fail closed. The probe and setup do not write the
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "paseo-room",
18
- version: "0.17.0",
18
+ version: "0.18.0",
19
19
  description: "One CLI that configures Codex, Claude Code, and Pi role homes in $HOME and registers them with your local Paseo daemon.",
20
20
  keywords: [
21
21
  "paseo",
@@ -473,7 +473,7 @@ var init_roles = __esm({
473
473
  // src/room.ts
474
474
  import { join as join3 } from "path";
475
475
  import { z } from "zod";
476
- function renderMarker(version, agents, roles, contract, claudeMemoryContract, runtime) {
476
+ function renderMarker(version, agents, roles, contract, claudeMemoryContract, runtime, piExtensions) {
477
477
  return JSON.stringify({
478
478
  version,
479
479
  agents: [...agents],
@@ -481,7 +481,10 @@ function renderMarker(version, agents, roles, contract, claudeMemoryContract, ru
481
481
  ...contract === void 0 ? {} : { contract },
482
482
  // Written only when it differs from the default, so an unchanged room's marker is unchanged.
483
483
  ...claudeMemoryContract === false ? { claudeMemoryContract: false } : {},
484
- ...runtime === void 0 ? {} : { runtime }
484
+ ...runtime === void 0 ? {} : { runtime },
485
+ ...piExtensions === void 0 || piExtensions.length === 0 ? {} : {
486
+ piExtensions: piExtensions.map((choice) => ({ package: choice.package, roles: [...choice.roles] }))
487
+ }
485
488
  }, null, 2) + "\n";
486
489
  }
487
490
  async function readMarker(layout) {
@@ -520,7 +523,12 @@ var init_room = __esm({
520
523
  * Present only when this room opted in to runtime coordination. Missing means disabled, so a
521
524
  * marker written before runtime existed keeps parsing and keeps its behaviour.
522
525
  */
523
- runtime: z.strictObject({ enabled: z.literal(true), generation: z.string().min(1), schema: z.literal(1) }).optional()
526
+ runtime: z.strictObject({ enabled: z.literal(true), generation: z.string().min(1), schema: z.literal(1) }).optional(),
527
+ /**
528
+ * Pi packages this room loads as explicit seat extensions (docs/design/pi-seat-extensions.md).
529
+ * Missing means none, so a marker written before the option existed keeps its behaviour.
530
+ */
531
+ piExtensions: z.array(z.strictObject({ package: z.string().min(1), roles: z.array(z.enum(ROLES)).min(1) })).min(1).optional()
524
532
  });
525
533
  }
526
534
  });
@@ -1608,7 +1616,7 @@ var init_codex = __esm({
1608
1616
 
1609
1617
  // src/agents/pi.ts
1610
1618
  import { spawn as spawn2 } from "child_process";
1611
- import { mkdtemp, readFile as readFile3, realpath, rm as rm2, stat } from "fs/promises";
1619
+ import { mkdir as mkdir2, mkdtemp, readFile as readFile3, realpath, rm as rm2, stat } from "fs/promises";
1612
1620
  import { tmpdir } from "os";
1613
1621
  import { isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "path";
1614
1622
  function asObject2(value) {
@@ -1630,16 +1638,116 @@ function renderPiAppend(source, role) {
1630
1638
  renderInstructions(role)
1631
1639
  ].filter((part) => Boolean(part)).join("\n\n");
1632
1640
  }
1641
+ function parsePiExtensionSpec(spec) {
1642
+ const [name = "", roleList, ...rest] = spec.trim().split("=");
1643
+ if (rest.length > 0 || !NPM_NAME.test(name)) {
1644
+ throw new Error(`Not a Pi package selection: ${spec}. Use <npm package>[=<role>,<role>].`);
1645
+ }
1646
+ if (name === PACKAGE_NAME) throw new Error(`${PACKAGE_NAME} is always loaded and cannot be selected.`);
1647
+ if (roleList === void 0) return { package: name, roles: DEFAULT_EXTENSION_ROLES };
1648
+ const roles = roleList.split(",").map((role) => role.trim());
1649
+ const unknown = roles.filter((role) => !ROLES.some((known) => known === role));
1650
+ if (unknown.length > 0 || roles.length === 0) {
1651
+ throw new Error(`Unknown role in ${spec}: ${unknown.join(", ") || "(none)"}. Roles are ${ROLES.join(", ")}.`);
1652
+ }
1653
+ return { package: name, roles: ROLES.filter((role) => roles.includes(role)) };
1654
+ }
1655
+ function normalizePiExtensions(choices) {
1656
+ const merged = /* @__PURE__ */ new Map();
1657
+ for (const choice of choices) {
1658
+ const roles = merged.get(choice.package) ?? /* @__PURE__ */ new Set();
1659
+ for (const role of choice.roles) roles.add(role);
1660
+ merged.set(choice.package, roles);
1661
+ }
1662
+ return [...merged.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([name, roles]) => ({ package: name, roles: ROLES.filter((role) => roles.has(role)) }));
1663
+ }
1664
+ async function resolvePiPackage(home, name) {
1665
+ const packageRoot = join10(home, "npm", "node_modules", name);
1666
+ const manifestPath = join10(packageRoot, "package.json");
1667
+ try {
1668
+ await stat(packageRoot);
1669
+ } catch (error) {
1670
+ if (error.code === "ENOENT") return { kind: "missing" };
1671
+ return { kind: "unsafe", detail: `Could not inspect ${packageRoot}.` };
1672
+ }
1673
+ try {
1674
+ const [canonicalRoot, manifestStat, raw] = await Promise.all([
1675
+ realpath(packageRoot),
1676
+ stat(manifestPath),
1677
+ readFile3(manifestPath, "utf8")
1678
+ ]);
1679
+ if (!manifestStat.isFile()) return { kind: "unsafe", detail: `${manifestPath} is not a regular file.` };
1680
+ const manifest = asObject2(JSON.parse(raw));
1681
+ if (!manifest || manifest.name !== name) return { kind: "unsafe", detail: `${manifestPath} does not identify ${name}.` };
1682
+ const declared = asObject2(manifest.pi)?.extensions;
1683
+ if (!Array.isArray(declared) || declared.length === 0 || !declared.every((item) => typeof item === "string" && item.trim() !== "")) {
1684
+ return { kind: "unsafe", detail: `${manifestPath} declares no Pi extension entry.` };
1685
+ }
1686
+ const entries = [];
1687
+ for (const declaration of declared) {
1688
+ if (isAbsolute2(declaration)) return { kind: "unsafe", detail: `${manifestPath} declares an unsafe absolute extension path.` };
1689
+ const candidate = resolve3(packageRoot, declaration);
1690
+ const [canonicalEntry, entryStat] = await Promise.all([realpath(candidate), stat(candidate)]);
1691
+ if (!contains(canonicalRoot, canonicalEntry) || canonicalEntry === canonicalRoot || !entryStat.isFile()) {
1692
+ return { kind: "unsafe", detail: `A declared ${name} extension escapes its package or is not a regular file.` };
1693
+ }
1694
+ entries.push(canonicalEntry);
1695
+ }
1696
+ const version = typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : "unknown";
1697
+ return { kind: "resolved", entries, version };
1698
+ } catch {
1699
+ return { kind: "unsafe", detail: `Could not safely resolve ${name} from ${manifestPath}.` };
1700
+ }
1701
+ }
1702
+ async function resolvePiExtensions(home, choices, roles) {
1703
+ const byRole = {};
1704
+ const checks = [];
1705
+ for (const choice of normalizePiExtensions(choices)) {
1706
+ const seated = choice.roles.filter((role) => roles.includes(role));
1707
+ if (seated.length === 0) continue;
1708
+ const id2 = `pi.extension.${choice.package}`;
1709
+ const resolution = await resolvePiPackage(home, choice.package);
1710
+ if (resolution.kind === "missing") {
1711
+ checks.push(warn(
1712
+ `${id2}.missing`,
1713
+ `Pi package ${choice.package} is selected for ${seated.join(", ")} but is not installed in ${join10(home, "npm")}; those seats start without it.`,
1714
+ `Install it yourself with: pi install npm:${choice.package}, then run setup again. The room never installs Pi packages.`
1715
+ ));
1716
+ continue;
1717
+ }
1718
+ if (resolution.kind === "unsafe") {
1719
+ checks.push(fail(id2, resolution.detail, `Reinstall ${choice.package} with Pi, or drop --pi-extension ${choice.package}, then run setup again.`));
1720
+ continue;
1721
+ }
1722
+ for (const role of seated) byRole[role] = [...byRole[role] ?? [], ...resolution.entries];
1723
+ checks.push(pass(id2, `${choice.package} ${resolution.version} loads for ${seated.join(", ")} from ${resolution.entries.join(", ")}.`));
1724
+ if (seated.includes("peer")) {
1725
+ checks.push(warn(
1726
+ `${id2}.peer`,
1727
+ `Peer runs ${choice.package}'s extension code. The room cannot prove it opens no second multi-agent path; selecting it states that it does not, or that it closes that path when ${SEAT_ROLE_ENV} is set.`,
1728
+ `Drop peer from --pi-extension ${choice.package}=<roles> if that is not true.`
1729
+ ));
1730
+ }
1731
+ }
1732
+ return { byRole, checks };
1733
+ }
1633
1734
  function adapterFailure(home, detail) {
1634
1735
  return fail(
1635
1736
  "pi.adapter",
1636
1737
  detail,
1637
- `Install ${PACKAGE_NAME} in ${join10(home, "npm")} with Pi yourself, then run setup again.`
1738
+ `Install ${PACKAGE_NAME} into ${join10(home, "npm")} yourself with: pi install npm:${PACKAGE_NAME}. Pi's built-in MCP extension is not a substitute: Paseo hands a Pi seat its tools only through ${PACKAGE_NAME}. The room never installs Pi packages. Then run setup again.`
1638
1739
  );
1639
1740
  }
1640
1741
  async function resolvePiAdapter(home) {
1641
1742
  const packageRoot = join10(home, "npm", "node_modules", PACKAGE_NAME);
1642
1743
  const manifestPath = join10(packageRoot, "package.json");
1744
+ try {
1745
+ await stat(packageRoot);
1746
+ } catch (error) {
1747
+ if (error.code === "ENOENT") {
1748
+ return { check: adapterFailure(home, `${PACKAGE_NAME} is not installed in ${join10(home, "npm")}.`) };
1749
+ }
1750
+ }
1643
1751
  try {
1644
1752
  const [canonicalRoot, manifestStat, raw] = await Promise.all([
1645
1753
  realpath(packageRoot),
@@ -1692,7 +1800,16 @@ function terminateProbe(child) {
1692
1800
  function runProbeProcess(executable, adapterEntry, env, options, cwd) {
1693
1801
  const timeoutMs = options.timeoutMs ?? PROBE_TIMEOUT_MS;
1694
1802
  const outputLimit = options.outputLimit ?? PROBE_OUTPUT_LIMIT;
1695
- const args = ["--mode", "rpc", "--no-session", "--no-extensions", "--extension", adapterEntry, "--no-approve"];
1803
+ const args = [
1804
+ "--mode",
1805
+ "rpc",
1806
+ "--no-session",
1807
+ "--no-extensions",
1808
+ "--extension",
1809
+ adapterEntry,
1810
+ ...(options.extensions ?? []).flatMap((entry) => ["--extension", entry]),
1811
+ "--no-approve"
1812
+ ];
1696
1813
  return new Promise((resolveResult) => {
1697
1814
  const child = spawn2(executable, args, { cwd, detached: true, env, stdio: ["pipe", "pipe", "pipe"] });
1698
1815
  const stdout = [];
@@ -1750,12 +1867,15 @@ async function runProbe(executable, adapterEntry, env, options) {
1750
1867
  const createdCwd = await mkdtemp(join10(tmpdir(), "paseo-room-pi-probe-"));
1751
1868
  const cwd = await realpath(createdCwd);
1752
1869
  try {
1870
+ const agentDir = join10(cwd, "agent");
1871
+ await mkdir2(agentDir);
1753
1872
  return await runProbeProcess(executable, adapterEntry, {
1754
1873
  ...env,
1755
1874
  HOME: cwd,
1756
- PI_CODING_AGENT_DIR: "/dev/null",
1875
+ PI_CODING_AGENT_DIR: agentDir,
1757
1876
  PI_MCP_CONFIG_MODE: "exclusive",
1758
- PI_OFFLINE: "1"
1877
+ PI_OFFLINE: "1",
1878
+ [SEAT_ROLE_ENV]: "lead"
1759
1879
  }, options, cwd);
1760
1880
  } finally {
1761
1881
  await rm2(createdCwd, { force: true, recursive: true });
@@ -1850,7 +1970,7 @@ async function piCredentialDiagnostic(layout, role, binary = "pi") {
1850
1970
  fix: `Start a minimal, non-room-equivalent interactive login session with: ${launch}. Run /login, then exit Pi.`
1851
1971
  }] };
1852
1972
  }
1853
- var PACKAGE_NAME, PROBE_ID, PROBE_TIMEOUT_MS, PROBE_OUTPUT_LIMIT, SHARED3, EXECUTABLE3, MCP_FILE, MCP_KEYS, AUTH_ENV2, piAgent;
1973
+ var PACKAGE_NAME, PROBE_ID, PROBE_TIMEOUT_MS, PROBE_OUTPUT_LIMIT, SHARED3, EXECUTABLE3, MCP_FILE, MCP_KEYS, AUTH_ENV2, DEFAULT_EXTENSION_ROLES, NPM_NAME, SEAT_ROLE_ENV, piAgent;
1854
1974
  var init_pi = __esm({
1855
1975
  "src/agents/pi.ts"() {
1856
1976
  "use strict";
@@ -1859,6 +1979,7 @@ var init_pi = __esm({
1859
1979
  init_fsops();
1860
1980
  init_layout();
1861
1981
  init_result();
1982
+ init_roles();
1862
1983
  init_instructions();
1863
1984
  init_prompts();
1864
1985
  init_which();
@@ -1895,13 +2016,16 @@ var init_pi = __esm({
1895
2016
  "KIMI_API_KEY",
1896
2017
  "COPILOT_GITHUB_TOKEN"
1897
2018
  ];
2019
+ DEFAULT_EXTENSION_ROLES = ["supervisor", "lead"];
2020
+ NPM_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
2021
+ SEAT_ROLE_ENV = "PASEO_ROOM_ROLE";
1898
2022
  piAgent = {
1899
2023
  id: "pi",
1900
2024
  label: "Pi",
1901
2025
  homeEnv: "PI_CODING_AGENT_DIR",
1902
2026
  providerEnv: { PI_MCP_CONFIG_MODE: "exclusive" },
1903
2027
  pins: {},
1904
- async build(layout, roles) {
2028
+ async build(layout, roles, options) {
1905
2029
  const home = layout.agentHome.pi;
1906
2030
  const binary = await which(layout.bin.pi, layout.searchPath);
1907
2031
  if (!binary) {
@@ -1914,13 +2038,22 @@ var init_pi = __esm({
1914
2038
  }
1915
2039
  const resolved = await resolvePiAdapter(home);
1916
2040
  if (!resolved.adapter) return { entries: [], checks: [resolved.check] };
2041
+ const extensions = await resolvePiExtensions(home, options?.piExtensions ?? [], roles);
2042
+ if (extensions.checks.some((check) => check.status === "fail")) {
2043
+ return { entries: [], checks: [resolved.check, ...extensions.checks] };
2044
+ }
2045
+ const selected = [...new Set(Object.values(extensions.byRole).flat())];
1917
2046
  const capability = await probePiMcp(binary, resolved.adapter.entry, {
1918
2047
  HOME: layout.home,
1919
2048
  PATH: layout.searchPath,
1920
- PI_CODING_AGENT_DIR: "/dev/null",
1921
2049
  PI_OFFLINE: "1"
1922
- });
1923
- if (capability.status === "fail") return { entries: [], checks: [resolved.check, capability] };
2050
+ }, { extensions: selected });
2051
+ if (capability.status === "fail") {
2052
+ return { entries: [], checks: [resolved.check, ...extensions.checks, selected.length === 0 ? capability : {
2053
+ ...capability,
2054
+ fix: `${capability.fix ?? ""} A selected Pi extension may also be what stops Pi from starting; retry without --pi-extension to tell.`.trim()
2055
+ }] };
2056
+ }
1924
2057
  const settingsPath = join10(home, "settings.json");
1925
2058
  let settings;
1926
2059
  try {
@@ -1981,6 +2114,7 @@ var init_pi = __esm({
1981
2114
  const entries = [];
1982
2115
  const credentials = [];
1983
2116
  const argv = {};
2117
+ const providerEnv = {};
1984
2118
  const roomSkill = { name: ROOM_SKILL_NAME, source: roomSkillSource(layout) };
1985
2119
  for (const role of roles) {
1986
2120
  const target = roleHome(layout, "pi", role);
@@ -1999,14 +2133,24 @@ var init_pi = __esm({
1999
2133
  leadSkillProjection: leadSkillProjection(layout, "pi")
2000
2134
  }));
2001
2135
  credentials.push(await piCredentialDiagnostic(layout, role, binary));
2002
- argv[role] = ["--no-extensions", "--extension", resolved.adapter.entry, "--no-approve", "--append-system-prompt", appendPath];
2136
+ argv[role] = [
2137
+ "--no-extensions",
2138
+ "--extension",
2139
+ resolved.adapter.entry,
2140
+ ...(extensions.byRole[role] ?? []).flatMap((entry) => ["--extension", entry]),
2141
+ "--no-approve",
2142
+ "--append-system-prompt",
2143
+ appendPath
2144
+ ];
2145
+ providerEnv[role] = { [SEAT_ROLE_ENV]: role };
2003
2146
  }
2004
2147
  return {
2005
2148
  entries,
2006
2149
  credentials,
2007
- checks: [pass("pi.home", `Pi found at ${binary} using ${home}.`), resolved.check, capability],
2150
+ checks: [pass("pi.home", `Pi found at ${binary} using ${home}.`), resolved.check, ...extensions.checks, capability],
2008
2151
  binary,
2009
- argv
2152
+ argv,
2153
+ providerEnv
2010
2154
  };
2011
2155
  }
2012
2156
  };
@@ -3785,13 +3929,13 @@ var init_schema = __esm({
3785
3929
 
3786
3930
  // src/runtime-plugin/server/store/publish.ts
3787
3931
  import { randomBytes as randomBytes2 } from "crypto";
3788
- import { chmod as chmod2, link, lstat as lstat4, mkdir as mkdir2, open, unlink as unlink2 } from "fs/promises";
3932
+ import { chmod as chmod2, link, lstat as lstat4, mkdir as mkdir3, open, unlink as unlink2 } from "fs/promises";
3789
3933
  import { join as join13 } from "path";
3790
3934
  function code(error) {
3791
3935
  return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
3792
3936
  }
3793
3937
  async function ensurePrivateDirectory(path) {
3794
- await mkdir2(path, { recursive: true, mode: PRIVATE_DIR_MODE });
3938
+ await mkdir3(path, { recursive: true, mode: PRIVATE_DIR_MODE });
3795
3939
  const stat2 = await lstat4(path);
3796
3940
  if (!stat2.isDirectory()) throw new Error(`${path} is not a directory.`);
3797
3941
  if ((stat2.mode & 511) !== PRIVATE_DIR_MODE) await chmod2(path, PRIVATE_DIR_MODE);
@@ -3880,7 +4024,7 @@ var init_publish = __esm({
3880
4024
 
3881
4025
  // src/runtime-plugin/server/store/project.ts
3882
4026
  import { randomBytes as randomBytes3, randomUUID } from "crypto";
3883
- import { mkdir as mkdir3, readdir as readdir3, readFile as readFile4, rename as rename2, rm as rm3, writeFile as writeFile2 } from "fs/promises";
4027
+ import { mkdir as mkdir4, readdir as readdir3, readFile as readFile4, rename as rename2, rm as rm3, writeFile as writeFile2 } from "fs/promises";
3884
4028
  import { basename as basename2, join as join14 } from "path";
3885
4029
  import { z as z12 } from "zod";
3886
4030
  function runtimeRoot(roomHome) {
@@ -4097,7 +4241,7 @@ var init_project = __esm({
4097
4241
  }
4098
4242
  /** Writes one disposable derived view. Cache files may be replaced; they are never authority. */
4099
4243
  async writeCache(name, content) {
4100
- await mkdir3(this.cacheDirectory, { recursive: true, mode: 448 });
4244
+ await mkdir4(this.cacheDirectory, { recursive: true, mode: 448 });
4101
4245
  const temporary = join14(this.cacheDirectory, `.tmp-${String(process.pid)}-${randomBytes3(6).toString("hex")}`);
4102
4246
  await writeFile2(temporary, content, { mode: PRIVATE_FILE_MODE });
4103
4247
  await rename2(temporary, join14(this.cacheDirectory, name));
@@ -4280,7 +4424,7 @@ async function buildDesired(layout, agents, roles, build = {}) {
4280
4424
  entries.push(...runtimePluginEntries(layout, agents, roles));
4281
4425
  runtime = { enabled: true, generation: renderRuntimeManifest(agents, roles).roomGeneration, schema: 1 };
4282
4426
  }
4283
- entries.push({ kind: "file", path: join16(layout.roomHome, MARKER), content: renderMarker(package_default.version, agents, roles, contractDigest(), markerMemoryContract(agents, build.memoryContract ?? true), runtime) });
4427
+ entries.push({ kind: "file", path: join16(layout.roomHome, MARKER), content: renderMarker(package_default.version, agents, roles, contractDigest(), markerMemoryContract(agents, build.memoryContract ?? true), runtime, markerPiExtensions(agents, build.piExtensions)) });
4284
4428
  return { entries, providers, profiles, checks, plugins, ...runtime === void 0 ? {} : { runtime } };
4285
4429
  }
4286
4430
  function contractProvenance(previous) {
@@ -4536,6 +4680,25 @@ async function runtimeRemovalWarning(layout) {
4536
4680
  "Export it first with: paseo-room export --out <dir>"
4537
4681
  )];
4538
4682
  }
4683
+ function markerPiExtensions(agents, choices) {
4684
+ if (!agents.includes("pi") || choices === void 0 || choices.length === 0) return void 0;
4685
+ return normalizePiExtensions(choices);
4686
+ }
4687
+ function piExtensionChanges(previous, agents, choices) {
4688
+ const before = previous?.agents.includes("pi") === true ? previous.piExtensions ?? [] : [];
4689
+ const now = markerPiExtensions(agents, choices) ?? [];
4690
+ const removed = before.flatMap((choice) => {
4691
+ const kept = now.find((item) => item.package === choice.package)?.roles ?? [];
4692
+ const dropped = choice.roles.filter((role) => !kept.includes(role));
4693
+ return dropped.length === 0 ? [] : [`${choice.package} (${dropped.join(", ")})`];
4694
+ });
4695
+ const keep = before.map((choice) => `--pi-extension ${choice.package}=${choice.roles.join(",")}`).join(" ");
4696
+ return removed.length === 0 ? [] : [warn(
4697
+ "pi.extensions-removed",
4698
+ `This setup removes Pi extensions the room loaded before: ${removed.join("; ")}.`,
4699
+ `To keep them, run setup again with: ${keep}`
4700
+ )];
4701
+ }
4539
4702
  function markerMemoryContract(agents, memoryContract) {
4540
4703
  return agents.includes("claude") ? memoryContract : void 0;
4541
4704
  }
@@ -4551,8 +4714,12 @@ async function setup(options = {}) {
4551
4714
  const layout = resolveLayout(options, options.env);
4552
4715
  const agents = options.agents ?? ["codex"];
4553
4716
  const memoryContract = options.claudeMemoryContract ?? true;
4717
+ const piExtensions = options.piExtensions ?? [];
4554
4718
  const invalid = layoutChecks(layout, agents);
4555
4719
  if (invalid.length > 0) return failed("setup", invalid);
4720
+ if (piExtensions.length > 0 && !agents.includes("pi")) {
4721
+ return failed("setup", [fail("pi.extension-without-pi", "--pi-extension selects Pi packages, but this setup does not seat Pi.", "Add --agent pi, or drop --pi-extension.")]);
4722
+ }
4556
4723
  const rootSafety = await roomPathSafety(layout, [], []);
4557
4724
  if (hasFailure(rootSafety)) return failed("setup", rootSafety);
4558
4725
  const previous = await readMarker(layout);
@@ -4563,7 +4730,7 @@ async function setup(options = {}) {
4563
4730
  const runtime = options.runtime === true;
4564
4731
  const daemon = await checkDaemon(layout, options.env, minimumPaseoVersion(compatibilityAgents, runtime || previous?.runtime?.enabled === true));
4565
4732
  if (!daemon.daemon) return failed("setup", daemon.checks);
4566
- const desired = await buildDesired(layout, agents, ROLES, { memoryContract, runtime });
4733
+ const desired = await buildDesired(layout, agents, ROLES, { memoryContract, runtime, piExtensions });
4567
4734
  const stale = await staleFrom(layout, previous, agents, ROLES, runtime);
4568
4735
  const runtimeChecks = stale.removePlugins.some((spec) => spec.id === RUNTIME_PLUGIN_ID) ? await runtimeDeselectionCheck(layout) : [];
4569
4736
  const retained = stale.retainedDirectories.length === 0 ? [] : [warn(
@@ -4571,7 +4738,7 @@ async function setup(options = {}) {
4571
4738
  `Preserved ${String(stale.retainedDirectories.length)} deselected role homes because they may contain role-owned credentials or runtime state.`,
4572
4739
  `Use paseo-room remove --apply to delete the entire room after reviewing its credential warning, or remove deselected homes manually: ${stale.retainedDirectories.join(", ")}`
4573
4740
  )];
4574
- const checks = [...daemon.checks, ...runtimeChecks, ...desired.checks, ...retained];
4741
+ const checks = [...daemon.checks, ...runtimeChecks, ...desired.checks, ...retained, ...piExtensionChanges(previous, agents, piExtensions)];
4575
4742
  if (hasFailure(checks)) return failed("setup", checks);
4576
4743
  return withSession(daemon.daemon, async (session) => {
4577
4744
  const plan = await planRoom(session, desired, stale);
@@ -4603,13 +4770,15 @@ async function setup(options = {}) {
4603
4770
  await session.refresh(ids);
4604
4771
  const pluginChecks = await reconcilePlugins(session, desired, stale, plan, pending);
4605
4772
  await applyEntries(markerEntries);
4773
+ const appliedPiExtensions = markerPiExtensions(agents, piExtensions);
4606
4774
  const appliedMarker = {
4607
4775
  version: package_default.version,
4608
4776
  agents: [...agents],
4609
4777
  roles: [...ROLES],
4610
4778
  contract: contractDigest(),
4611
4779
  ...markerMemoryContract(agents, memoryContract) === false ? { claudeMemoryContract: false } : {},
4612
- ...desired.runtime === void 0 ? {} : { runtime: desired.runtime }
4780
+ ...desired.runtime === void 0 ? {} : { runtime: desired.runtime },
4781
+ ...appliedPiExtensions === void 0 ? {} : { piExtensions: appliedPiExtensions.map((choice) => ({ package: choice.package, roles: [...choice.roles] })) }
4613
4782
  };
4614
4783
  const appliedChecks = [
4615
4784
  ...checks,
@@ -4647,7 +4816,8 @@ async function verify(options = {}) {
4647
4816
  const desired = await buildDesired(layout, marker.agents, marker.roles, {
4648
4817
  // The room's own recorded choices, so verify compares against what setup wrote.
4649
4818
  memoryContract: marker.claudeMemoryContract ?? true,
4650
- runtime
4819
+ runtime,
4820
+ ...marker.piExtensions === void 0 ? {} : { piExtensions: marker.piExtensions }
4651
4821
  });
4652
4822
  const runtimeChecks = runtime ? [marker.runtime?.generation === desired.runtime?.generation ? pass("runtime.generation", `Runtime manifest generation ${String(desired.runtime?.generation)} matches this package.`) : fail("runtime.generation", `This room was set up with runtime generation ${String(marker.runtime?.generation)}; this package generates ${String(desired.runtime?.generation)}.`, "Run: paseo-room setup --runtime --apply")] : [];
4653
4823
  const checks = [...daemon.checks, ...runtimeChecks, ...desired.checks];
@@ -4845,6 +5015,20 @@ async function runWizard(prompts, emit, write, options = {}, onAction) {
4845
5015
  if (typeof carrier === "symbol") return cancelled();
4846
5016
  claudeMemoryContract = carrier === "both";
4847
5017
  }
5018
+ let piExtensions = options.piExtensions;
5019
+ const recorded = agents.includes("pi") && piExtensions === void 0 ? (await readMarker(resolveLayout(options, options.env)))?.piExtensions ?? [] : [];
5020
+ if (recorded.length > 0) {
5021
+ const listed = recorded.map((choice) => `${choice.package} (${choice.roles.join(", ")})`).join("; ");
5022
+ const keep = await prompts.select({
5023
+ message: `Pi seats load these extensions: ${listed}. Keep them?`,
5024
+ options: [
5025
+ { value: "keep", label: "Yes \u2014 keep loading them (recommended)" },
5026
+ { value: "drop", label: "No \u2014 start Pi seats with the MCP adapter only" }
5027
+ ]
5028
+ });
5029
+ if (typeof keep === "symbol") return cancelled();
5030
+ if (keep === "keep") piExtensions = recorded;
5031
+ }
4848
5032
  const coordination = await prompts.select({
4849
5033
  message: "Runtime coordination (preview): track assignments, Peer reports and writer ownership through a trusted Paseo plugin?",
4850
5034
  options: [
@@ -4853,7 +5037,13 @@ async function runWizard(prompts, emit, write, options = {}, onAction) {
4853
5037
  ]
4854
5038
  });
4855
5039
  if (typeof coordination === "symbol") return cancelled();
4856
- const setupOptions = { ...options, agents, claudeMemoryContract, ...coordination === "runtime" ? { runtime: true } : {} };
5040
+ const setupOptions = {
5041
+ ...options,
5042
+ agents,
5043
+ claudeMemoryContract,
5044
+ ...coordination === "runtime" ? { runtime: true } : {},
5045
+ ...piExtensions === void 0 ? {} : { piExtensions }
5046
+ };
4857
5047
  const preview = await setup(setupOptions);
4858
5048
  const status = emit(preview);
4859
5049
  if (preview.outcome !== "changes-planned") return status;
@@ -4866,6 +5056,8 @@ var init_wizard = __esm({
4866
5056
  "src/wizard.ts"() {
4867
5057
  "use strict";
4868
5058
  init_commands();
5059
+ init_layout();
5060
+ init_room();
4869
5061
  init_roles();
4870
5062
  terminalPrompts = {
4871
5063
  select: (options) => clack.select({ message: options.message, options: [...options.options] }),
@@ -4887,7 +5079,7 @@ init_layout();
4887
5079
  init_result();
4888
5080
  init_schema();
4889
5081
  init_project();
4890
- import { copyFile, mkdir as mkdir4, readdir as readdir5, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
5082
+ import { copyFile, mkdir as mkdir5, readdir as readdir5, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
4891
5083
  import { join as join17, resolve as resolve4 } from "path";
4892
5084
  var LIMITATION = "Briefs, answers, reports and gate commands are stored as written and cannot be proven secret-free; review the export before sharing it.";
4893
5085
  async function isEmptyOrAbsent(path) {
@@ -4958,9 +5150,9 @@ async function exportRuntime(options = {}) {
4958
5150
  ...summary.filter((entry) => entry.omitted.length > 0).map((entry) => warn("export.omitted", `Project ${entry.projectId}: ${String(entry.omitted.length)} unreadable event file(s) were left out by name: ${entry.omitted.join(", ")}.`))
4959
5151
  ];
4960
5152
  if (!options.apply) return { command: "export", outcome: "changes-planned", changed: false, checks, operations };
4961
- await mkdir4(destination, { recursive: true, mode: 448 });
5153
+ await mkdir5(destination, { recursive: true, mode: 448 });
4962
5154
  for (const [from, to] of copies) {
4963
- await mkdir4(join17(to, ".."), { recursive: true, mode: 448 });
5155
+ await mkdir5(join17(to, ".."), { recursive: true, mode: 448 });
4964
5156
  await copyFile(from, to);
4965
5157
  }
4966
5158
  await writeFile3(join17(destination, "summary.json"), `${JSON.stringify({
@@ -5006,6 +5198,7 @@ function renderHuman(result) {
5006
5198
 
5007
5199
  // src/cli.ts
5008
5200
  init_result();
5201
+ init_pi();
5009
5202
  init_roles();
5010
5203
  init_prompts();
5011
5204
  init_skills();
@@ -5015,6 +5208,13 @@ function collectAgent(value, previous) {
5015
5208
  const seated = previous ?? [];
5016
5209
  return seated.includes(agent) ? seated : [...seated, agent];
5017
5210
  }
5211
+ function collectPiExtension(value, previous) {
5212
+ try {
5213
+ return [...previous ?? [], parsePiExtensionSpec(value)];
5214
+ } catch (error) {
5215
+ throw new InvalidArgumentError(error instanceof Error ? error.message : String(error));
5216
+ }
5217
+ }
5018
5218
  var PATH_FLAGS = ["roomHome", "codexHome", "claudeHome", "piHome", "codexBin", "claudeBin", "piBin", "paseoBin"];
5019
5219
  function optionsFrom(raw, base) {
5020
5220
  const paths = {};
@@ -5030,7 +5230,8 @@ function optionsFrom(raw, base) {
5030
5230
  ...raw.apply === true ? { apply: true } : {},
5031
5231
  // Commander defaults a --no- flag to true, so only an explicit opt-out is carried.
5032
5232
  ...raw.claudeMemoryContract === false ? { claudeMemoryContract: false } : {},
5033
- ...raw.runtime === true ? { runtime: true } : {}
5233
+ ...raw.runtime === true ? { runtime: true } : {},
5234
+ ...Array.isArray(raw.piExtension) && raw.piExtension.length > 0 ? { piExtensions: raw.piExtension } : {}
5034
5235
  };
5035
5236
  }
5036
5237
  function failedFromThrown(command, error) {
@@ -5091,7 +5292,7 @@ async function runCli(argv, output, context = {}) {
5091
5292
  return emit(failedFromThrown(wizardCommand, error));
5092
5293
  }
5093
5294
  }
5094
- const program = new Command().name("paseo-room").description("Configure Codex/Claude/Pi role homes in $HOME and register them with your local Paseo daemon.").version(package_default.version).usage("<setup|verify|remove|export> [options]\n auth login <codex|claude|pi> <supervisor|lead|peer> [options]").argument("<command>", "setup | verify | remove | export | auth").argument("[command-arguments...]", "auth login <agent> <role>").option("--agent <agent>", "codex, claude, or pi; repeat to combine (default: codex)", collectAgent).option("--apply", "actually make the changes (default: dry run)").option("--json", "machine-readable output").option("--no-claude-memory-contract", "omit the role contract from Claude role CLAUDE.md files, leaving the room plugin as the only Claude carrier").option("--runtime", "opt in to runtime coordination (preview): installs the trusted paseo-room-runtime plugin; omit it to deselect").option("--out <dir>", "export: destination directory (default: a new directory under the room's runtime exports)").option("--include-gate-output", "export: also copy bounded gate output tails (best-effort masked)").option("--room-home <path>", "where role homes are written (default: ~/.paseo-room)").option("--codex-home <path>", "source Codex config (default: ~/.codex)").option("--claude-home <path>", "source Claude Code config (default: ~/.claude)").option("--pi-home <path>", "source Pi config (default: ~/.pi/agent)").option("--codex-bin <path>", "Codex executable (default: found on PATH)").option("--claude-bin <path>", "Claude Code executable (default: found on PATH)").option("--pi-bin <path>", "Pi executable (default: found on PATH)").option("--paseo-bin <path>", "Paseo executable (default: found on PATH)").configureOutput({ writeOut: (text3) => {
5295
+ const program = new Command().name("paseo-room").description("Configure Codex/Claude/Pi role homes in $HOME and register them with your local Paseo daemon.").version(package_default.version).usage("<setup|verify|remove|export> [options]\n auth login <codex|claude|pi> <supervisor|lead|peer> [options]").argument("<command>", "setup | verify | remove | export | auth").argument("[command-arguments...]", "auth login <agent> <role>").option("--agent <agent>", "codex, claude, or pi; repeat to combine (default: codex)", collectAgent).option("--apply", "actually make the changes (default: dry run)").option("--json", "machine-readable output").option("--no-claude-memory-contract", "omit the role contract from Claude role CLAUDE.md files, leaving the room plugin as the only Claude carrier").option("--runtime", "opt in to runtime coordination (preview): installs the trusted paseo-room-runtime plugin; omit it to deselect").option("--pi-extension <package[=roles]>", "load an operator-installed Pi package in Pi seats; roles default to supervisor,lead (peer only when named); repeat for more", collectPiExtension).option("--out <dir>", "export: destination directory (default: a new directory under the room's runtime exports)").option("--include-gate-output", "export: also copy bounded gate output tails (best-effort masked)").option("--room-home <path>", "where role homes are written (default: ~/.paseo-room)").option("--codex-home <path>", "source Codex config (default: ~/.codex)").option("--claude-home <path>", "source Claude Code config (default: ~/.claude)").option("--pi-home <path>", "source Pi config (default: ~/.pi/agent)").option("--codex-bin <path>", "Codex executable (default: found on PATH)").option("--claude-bin <path>", "Claude Code executable (default: found on PATH)").option("--pi-bin <path>", "Pi executable (default: found on PATH)").option("--paseo-bin <path>", "Paseo executable (default: found on PATH)").configureOutput({ writeOut: (text3) => {
5095
5296
  output.stdout(text3);
5096
5297
  }, writeErr: (text3) => {
5097
5298
  output.stderr(text3);
@@ -5124,6 +5325,11 @@ async function runCli(argv, output, context = {}) {
5124
5325
  }
5125
5326
  if (raw.runtime === true && (command === "verify" || command === "remove" || command === "export")) {
5126
5327
  output.stderr(`paseo-room ${command}: --runtime is a setup choice recorded in the room marker; ${command} reads it from there.
5328
+ `);
5329
+ return 2;
5330
+ }
5331
+ if (raw.piExtension !== void 0 && command !== "setup") {
5332
+ output.stderr(`paseo-room ${command}: --pi-extension is a setup choice recorded in the room marker; ${command} reads it from there.
5127
5333
  `);
5128
5334
  return 2;
5129
5335
  }