paseo-room 0.1.0 → 0.3.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/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.1.0",
18
+ version: "0.3.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",
@@ -49,7 +49,7 @@ var init_package = __esm({
49
49
  lint: "eslint . --max-warnings 0",
50
50
  test: "vitest run",
51
51
  "test:package": "vitest run --config vitest.package.config.ts",
52
- build: `tsup && node --input-type=module -e "import { cpSync, rmSync } from 'node:fs'; rmSync('dist/prompts', { recursive: true, force: true }); cpSync('src/room/prompts', 'dist/prompts', { recursive: true });"`,
52
+ build: `tsup && node --input-type=module -e "import { cpSync, rmSync } from 'node:fs'; for (const [source, target] of [['src/room/prompts', 'dist/prompts'], ['src/plugin-assets', 'dist/plugin-assets']]) { rmSync(target, { recursive: true, force: true }); cpSync(source, target, { recursive: true }); }"`,
53
53
  prepack: "npm run build",
54
54
  verify: "npm run typecheck && npm run lint && npm test && npm run build && npm run test:package"
55
55
  },
@@ -799,7 +799,7 @@ var init_prompts = __esm({
799
799
  },
800
800
  contract: {
801
801
  humanAuthority: { path: "contract/shared/human-authority.md", kind: "section" },
802
- workspaceProtocolPrecedence: { path: "contract/shared/workspace-protocol-precedence.md", kind: "section" },
802
+ workspaceProtocolPrecedence: { path: "contract/shared-supervisor-lead/workspace-protocol-precedence.md", kind: "section" },
803
803
  evidenceAndEventWaiting: { path: "contract/shared/evidence-and-event-waiting.md", kind: "section" },
804
804
  scopeAndUnrelatedWork: { path: "contract/shared/scope-and-unrelated-work.md", kind: "section" },
805
805
  directiveIntegrity: { path: "contract/supervisor/directive-integrity.md", kind: "section" },
@@ -816,7 +816,7 @@ var init_prompts = __esm({
816
816
  peerSeatLifecycle: { path: "contract/lead/peer-seat-lifecycle.md", kind: "section" },
817
817
  boundedOutcome: { path: "contract/peer/bounded-outcome.md", kind: "section" },
818
818
  independentJudgment: { path: "contract/peer/independent-judgment.md", kind: "section" },
819
- writingAndReviewScope: { path: "contract/peer/writing-and-review-scope.md", kind: "section" },
819
+ assignmentScope: { path: "contract/peer/assignment-scope.md", kind: "section" },
820
820
  noOrchestration: { path: "contract/peer/no-orchestration.md", kind: "section" },
821
821
  reproducibleHandoff: { path: "contract/peer/reproducible-handoff.md", kind: "section" },
822
822
  noSelfAcceptance: { path: "contract/peer/no-self-acceptance.md", kind: "section" }
@@ -849,12 +849,13 @@ var init_prompts = __esm({
849
849
  // src/room/instructions.ts
850
850
  import { createHash } from "crypto";
851
851
  function instructionKeys(role) {
852
- return [...SHARED_KEYS, ...ROLE_KEYS[role]];
852
+ return [...SHARED_KEYS[role], ...ROLE_KEYS[role]];
853
853
  }
854
854
  function protocolKeys(role) {
855
855
  return PROTOCOL_KEYS[role];
856
856
  }
857
857
  function protocol(keys) {
858
+ if (keys.length === 0) return [];
858
859
  return [
859
860
  loadPromptAsset("documents", "workspace"),
860
861
  ...keys.map((key) => loadPromptAsset("workspace", key))
@@ -873,18 +874,28 @@ function contractDigest() {
873
874
  for (const kind of INSTRUCTION_KINDS) hash.update(`${kind}\0${renderInstructions(kind)}\0`);
874
875
  return `sha256:${hash.digest("hex").slice(0, 16)}`;
875
876
  }
876
- var SHARED_KEYS, ROLE_KEYS, ALL_PROTOCOL_KEYS, PROTOCOL_KEYS, INSTRUCTION_KINDS;
877
+ var AUTHORITY_KEYS, PROTOCOL_READER_KEYS, SHARED_KEYS, ROLE_KEYS, ALL_PROTOCOL_KEYS, PROTOCOL_KEYS, INSTRUCTION_KINDS;
877
878
  var init_instructions = __esm({
878
879
  "src/room/instructions.ts"() {
879
880
  "use strict";
880
881
  init_roles();
881
882
  init_prompts();
882
- SHARED_KEYS = [
883
+ AUTHORITY_KEYS = [
884
+ "humanAuthority",
885
+ "evidenceAndEventWaiting",
886
+ "scopeAndUnrelatedWork"
887
+ ];
888
+ PROTOCOL_READER_KEYS = [
883
889
  "humanAuthority",
884
890
  "workspaceProtocolPrecedence",
885
891
  "evidenceAndEventWaiting",
886
892
  "scopeAndUnrelatedWork"
887
893
  ];
894
+ SHARED_KEYS = {
895
+ supervisor: PROTOCOL_READER_KEYS,
896
+ lead: PROTOCOL_READER_KEYS,
897
+ peer: AUTHORITY_KEYS
898
+ };
888
899
  ROLE_KEYS = {
889
900
  supervisor: [
890
901
  "directiveIntegrity",
@@ -907,7 +918,7 @@ var init_instructions = __esm({
907
918
  "challengeSignals",
908
919
  "boundedOutcome",
909
920
  "independentJudgment",
910
- "writingAndReviewScope",
921
+ "assignmentScope",
911
922
  "noOrchestration",
912
923
  "reproducibleHandoff",
913
924
  "noSelfAcceptance"
@@ -922,7 +933,7 @@ var init_instructions = __esm({
922
933
  PROTOCOL_KEYS = {
923
934
  supervisor: ALL_PROTOCOL_KEYS,
924
935
  lead: ALL_PROTOCOL_KEYS,
925
- peer: ["verification", "review", "repositoryConventions"]
936
+ peer: []
926
937
  };
927
938
  INSTRUCTION_KINDS = [...ROLES, "workspace"];
928
939
  }
@@ -1868,7 +1879,7 @@ var init_pi = __esm({
1868
1879
  import { gte, valid } from "semver";
1869
1880
  import { z as z2 } from "zod";
1870
1881
  function minimumPaseoVersion(agents) {
1871
- return agents.includes("pi") ? PI_MINIMUM_VERSION : MINIMUM_VERSION;
1882
+ return agents.includes("pi") || agents.includes("claude") ? PI_MINIMUM_VERSION : MINIMUM_VERSION;
1872
1883
  }
1873
1884
  function normalizeUrl(listen) {
1874
1885
  const match = /^(?:ws:\/\/)?([^:/]+|\[[^\]]+\]):(\d+)$/.exec(listen.trim());
@@ -1929,15 +1940,38 @@ async function withSession(daemon, run2, options = {}) {
1929
1940
  }, warn() {
1930
1941
  }, error() {
1931
1942
  } };
1932
- const factory = options.factory ?? (await import("@getpaseo/client")).createPaseoClient;
1933
- const client = factory({
1943
+ const clientConfig = {
1934
1944
  url: `${daemon.url}/ws`,
1935
1945
  ...options.password === void 0 ? {} : { password: options.password },
1936
1946
  appVersion: MINIMUM_VERSION,
1937
1947
  connectTimeoutMs: 1e4,
1938
1948
  reconnect: { enabled: false },
1939
1949
  logger: quiet
1940
- });
1950
+ };
1951
+ let client;
1952
+ if (options.factory !== void 0) {
1953
+ client = options.factory(clientConfig);
1954
+ } else {
1955
+ const [{ createPaseoApi }, { DaemonClient }] = await Promise.all([
1956
+ import("@getpaseo/client"),
1957
+ import("@getpaseo/client/internal/daemon-client")
1958
+ ]);
1959
+ const raw = new DaemonClient({ ...clientConfig, clientId: `paseo-room-${String(process.pid)}`, clientType: "cli" });
1960
+ client = {
1961
+ ...createPaseoApi(raw),
1962
+ connect: () => raw.connect(),
1963
+ close: () => raw.close(),
1964
+ ensureConnected: () => {
1965
+ raw.ensureConnected();
1966
+ },
1967
+ getConnectionState: () => raw.getConnectionState(),
1968
+ listPlugins: () => raw.listPlugins(),
1969
+ installDirectoryPlugin: (path, id) => raw.installDirectoryPlugin(path, id),
1970
+ reloadPlugin: (id) => raw.reloadPlugin(id),
1971
+ enablePlugin: (id) => raw.enablePlugin(id),
1972
+ removePlugin: (id) => raw.removePlugin(id)
1973
+ };
1974
+ }
1941
1975
  try {
1942
1976
  await client.connect();
1943
1977
  return await run2({
@@ -1958,6 +1992,25 @@ async function withSession(daemon, run2, options = {}) {
1958
1992
  async writeProfiles(profiles) {
1959
1993
  await client.config.patch({ agentProfiles: [...profiles] });
1960
1994
  },
1995
+ async pluginsEnabled() {
1996
+ const response = configSchema.parse(await client.config.get());
1997
+ return response.config.pluginsEnabled === true;
1998
+ },
1999
+ async listPlugins() {
2000
+ return await client.listPlugins();
2001
+ },
2002
+ async installPlugin(path, id) {
2003
+ return await client.installDirectoryPlugin(path, id);
2004
+ },
2005
+ async reloadPlugin(id) {
2006
+ return await client.reloadPlugin(id);
2007
+ },
2008
+ async enablePlugin(id) {
2009
+ return await client.enablePlugin(id);
2010
+ },
2011
+ async removePlugin(id) {
2012
+ await client.removePlugin(id);
2013
+ },
1961
2014
  async refresh(ids) {
1962
2015
  await client.providers.refresh({ providers: [...ids] });
1963
2016
  }
@@ -2024,14 +2077,59 @@ var init_paseo = __esm({
2024
2077
  config: z2.object({
2025
2078
  providers: z2.record(z2.string(), z2.unknown()).optional(),
2026
2079
  // Read loosely: entries carry operator fields the room neither knows nor touches.
2027
- agentProfiles: z2.array(z2.record(z2.string(), z2.unknown())).optional()
2080
+ agentProfiles: z2.array(z2.record(z2.string(), z2.unknown())).optional(),
2081
+ pluginsEnabled: z2.boolean().optional()
2028
2082
  })
2029
2083
  });
2030
2084
  }
2031
2085
  });
2032
2086
 
2087
+ // src/plugin.ts
2088
+ import { readFileSync as readFileSync2 } from "fs";
2089
+ import { join as join10 } from "path";
2090
+ function loadAsset(path) {
2091
+ return readFileSync2(new URL(`./plugin-assets/${path}`, import.meta.url), "utf8");
2092
+ }
2093
+ function claudeCarrierPluginDir(layout) {
2094
+ return join10(layout.roomHome, "plugin");
2095
+ }
2096
+ function renderClaudeCarrierContract(roles) {
2097
+ const contracts = Object.fromEntries(roles.map((role) => [providerId("claude", role), renderInstructions(role)]));
2098
+ return [
2099
+ "/** Generated by paseo-room. The Markdown prompt assets remain authoritative. */",
2100
+ `export const GENERATION = ${JSON.stringify(contractDigest())};`,
2101
+ `export const CONTRACT: Readonly<Record<string, string>> = ${JSON.stringify(contracts, null, 2)};`,
2102
+ ""
2103
+ ].join("\n");
2104
+ }
2105
+ function claudeCarrierEntries(layout, roles) {
2106
+ const root = claudeCarrierPluginDir(layout);
2107
+ return [
2108
+ { kind: "dir", path: root },
2109
+ { kind: "dir", path: join10(root, "server") },
2110
+ ...STATIC_ASSETS.map((path) => ({ kind: "file", path: join10(root, path), content: loadAsset(path) })),
2111
+ { kind: "file", path: join10(root, "server", "contract.ts"), content: renderClaudeCarrierContract(roles) }
2112
+ ];
2113
+ }
2114
+ var CLAUDE_CARRIER_PLUGIN_ID, STATIC_ASSETS;
2115
+ var init_plugin = __esm({
2116
+ "src/plugin.ts"() {
2117
+ "use strict";
2118
+ init_roles();
2119
+ init_instructions();
2120
+ CLAUDE_CARRIER_PLUGIN_ID = "paseo-room-claude-carrier";
2121
+ STATIC_ASSETS = [
2122
+ "paseo-plugin.json",
2123
+ "package.json",
2124
+ "tsconfig.json",
2125
+ "index.server.ts",
2126
+ "server/carrier.ts"
2127
+ ];
2128
+ }
2129
+ });
2130
+
2033
2131
  // src/commands.ts
2034
- import { join as join10, sep as sep2 } from "path";
2132
+ import { join as join11, sep as sep2 } from "path";
2035
2133
  import { lstat as lstat4, rm as rm3 } from "fs/promises";
2036
2134
  function sessionOptions(options) {
2037
2135
  const password = (options.env ?? process.env).PASEO_PASSWORD;
@@ -2063,7 +2161,7 @@ async function buildDesired(layout, agents, roles) {
2063
2161
  const entries = [
2064
2162
  { kind: "dir", path: layout.roomHome },
2065
2163
  { kind: "dir", path: sharedRoom(layout) },
2066
- { kind: "file", path: join10(sharedRoom(layout), "WORKSPACE_PROTOCOL.md"), content: renderInstructions("workspace") }
2164
+ { kind: "file", path: join11(sharedRoom(layout), "WORKSPACE_PROTOCOL.md"), content: renderInstructions("workspace") }
2067
2165
  ];
2068
2166
  const providers = {};
2069
2167
  const profiles = [];
@@ -2097,17 +2195,19 @@ async function buildDesired(layout, agents, roles) {
2097
2195
  if (agents.every((id) => binaries[id] !== void 0)) {
2098
2196
  entries.push({
2099
2197
  kind: "file",
2100
- path: join10(layout.roomHome, AUTHENTICATION_GUIDE),
2198
+ path: join11(layout.roomHome, AUTHENTICATION_GUIDE),
2101
2199
  content: renderAuthenticationGuide(layout.roomHome, binaries, agents, roles)
2102
2200
  });
2103
2201
  checks.push(warn(
2104
2202
  "room.authentication-not-validated",
2105
- `Role authentication was not validated. Login instructions are managed at ${join10(layout.roomHome, AUTHENTICATION_GUIDE)}.`,
2203
+ `Role authentication was not validated. Login instructions are managed at ${join11(layout.roomHome, AUTHENTICATION_GUIDE)}.`,
2106
2204
  "After setup --apply, follow that guide or run: paseo-room auth login <agent> <role>"
2107
2205
  ));
2108
2206
  }
2109
- entries.push({ kind: "file", path: join10(layout.roomHome, MARKER), content: renderMarker(package_default.version, agents, roles, contractDigest()) });
2110
- return { entries, providers, profiles, checks };
2207
+ const pluginPath = agents.includes("claude") ? claudeCarrierPluginDir(layout) : void 0;
2208
+ if (pluginPath !== void 0) entries.push(...claudeCarrierEntries(layout, roles));
2209
+ entries.push({ kind: "file", path: join11(layout.roomHome, MARKER), content: renderMarker(package_default.version, agents, roles, contractDigest()) });
2210
+ return { entries, providers, profiles, checks, ...pluginPath === void 0 ? {} : { pluginPath } };
2111
2211
  }
2112
2212
  function contractProvenance(previous) {
2113
2213
  const digest = contractDigest();
@@ -2162,10 +2262,10 @@ async function roomPathSafety(layout, agents, roles) {
2162
2262
  const checks = [];
2163
2263
  if (!await safeDirectory(layout.roomHome, checks)) return checks;
2164
2264
  await safeDirectory(sharedRoom(layout), checks);
2165
- const rolesHome = join10(layout.roomHome, "roles");
2265
+ const rolesHome = join11(layout.roomHome, "roles");
2166
2266
  if (!await safeDirectory(rolesHome, checks)) return checks;
2167
2267
  for (const agent of agents) {
2168
- const agentHome = join10(rolesHome, agent);
2268
+ const agentHome = join11(rolesHome, agent);
2169
2269
  if (!await safeDirectory(agentHome, checks)) continue;
2170
2270
  for (const role of roles) await safeDirectory(roleHome(layout, agent, role), checks);
2171
2271
  }
@@ -2186,14 +2286,54 @@ async function staleFrom(layout, previous, agents, roles) {
2186
2286
  if (await exists(path)) retainedDirectories.push(path);
2187
2287
  }
2188
2288
  }
2189
- return { providerIds, profileIds, retainedDirectories };
2289
+ return {
2290
+ providerIds,
2291
+ profileIds,
2292
+ removePlugin: previous.agents.includes("claude") && !agents.includes("claude"),
2293
+ retainedDirectories
2294
+ };
2295
+ }
2296
+ function pluginPlan(desired, stale, plugins, expectedPath) {
2297
+ const livePlugin = plugins.find((plugin) => plugin.id === CLAUDE_CARRIER_PLUGIN_ID);
2298
+ const shouldManage = desired.pluginPath !== void 0 || stale.removePlugin;
2299
+ if (!shouldManage) return { checks: [] };
2300
+ if (livePlugin !== void 0 && livePlugin.path !== expectedPath) {
2301
+ return {
2302
+ livePlugin,
2303
+ checks: [fail(
2304
+ "claude.plugin.path",
2305
+ `Plugin ${CLAUDE_CARRIER_PLUGIN_ID} is registered from ${livePlugin.path}, not the room-owned path ${expectedPath}.`,
2306
+ "Move or remove that conflicting plugin registration manually, then run setup again."
2307
+ )]
2308
+ };
2309
+ }
2310
+ if (desired.pluginPath !== void 0) {
2311
+ const healthy = livePlugin?.enabled === true && livePlugin.status === "running" && livePlugin.error === void 0;
2312
+ return {
2313
+ ...livePlugin === void 0 ? {} : { livePlugin },
2314
+ operation: { action: livePlugin === void 0 ? "create" : healthy ? "noop" : "update", kind: "plugin", target: CLAUDE_CARRIER_PLUGIN_ID },
2315
+ checks: []
2316
+ };
2317
+ }
2318
+ return {
2319
+ ...livePlugin === void 0 ? {} : { livePlugin },
2320
+ operation: { action: livePlugin === void 0 ? "noop" : "remove", kind: "plugin", target: CLAUDE_CARRIER_PLUGIN_ID },
2321
+ checks: []
2322
+ };
2190
2323
  }
2191
- async function planRoom(session, desired, stale) {
2192
- const live = await session.readProviders();
2193
- const liveProfiles = await session.readProfiles();
2324
+ async function planRoom(session, desired, stale, expectedPluginPath) {
2325
+ const inspectPlugins = desired.pluginPath !== void 0 || stale.removePlugin;
2326
+ const [live, liveProfiles, plugins] = await Promise.all([
2327
+ session.readProviders(),
2328
+ session.readProfiles(),
2329
+ inspectPlugins ? session.listPlugins() : Promise.resolve([])
2330
+ ]);
2194
2331
  const byId = new Map(liveProfiles.map((entry) => [String(entry.id), entry]));
2332
+ const plugin = pluginPlan(desired, stale, plugins, expectedPluginPath);
2195
2333
  return {
2196
2334
  liveProfiles,
2335
+ ...plugin.livePlugin === void 0 ? {} : { livePlugin: plugin.livePlugin },
2336
+ pluginChecks: plugin.checks,
2197
2337
  operations: [
2198
2338
  ...await planEntries(desired.entries),
2199
2339
  ...Object.entries(desired.providers).map(([id, provider]) => ({
@@ -2207,10 +2347,53 @@ async function planRoom(session, desired, stale) {
2207
2347
  target: profile.id
2208
2348
  })),
2209
2349
  ...stale.providerIds.filter((id) => id in live).map((id) => ({ action: "remove", kind: "provider", target: id })),
2210
- ...stale.profileIds.filter((id) => byId.has(id)).map((id) => ({ action: "remove", kind: "profile", target: id }))
2350
+ ...stale.profileIds.filter((id) => byId.has(id)).map((id) => ({ action: "remove", kind: "profile", target: id })),
2351
+ ...plugin.operation === void 0 ? [] : [plugin.operation]
2211
2352
  ]
2212
2353
  };
2213
2354
  }
2355
+ function pluginsEnabledCheck(enabled) {
2356
+ return enabled ? pass("claude.plugin.enabled", "Paseo plugins are enabled for the required Claude contract carrier.") : fail(
2357
+ "claude.plugin.enabled",
2358
+ "Claude rooms require the paseo-room trusted server plugin, but Paseo plugins are disabled. Plugins are trusted, unsandboxed code with access to the daemon machine.",
2359
+ "Review that trust boundary, enable plugins in Paseo Settings, reload Paseo, then run setup again."
2360
+ );
2361
+ }
2362
+ function pluginRuntimeCheck(plugin, expectedPath) {
2363
+ if (plugin === void 0) {
2364
+ return fail("claude.plugin.runtime", `Plugin ${CLAUDE_CARRIER_PLUGIN_ID} is not registered.`, "Run: paseo-room setup --apply");
2365
+ }
2366
+ if (plugin.path !== expectedPath) {
2367
+ return fail(
2368
+ "claude.plugin.runtime",
2369
+ `Plugin ${CLAUDE_CARRIER_PLUGIN_ID} is registered from ${plugin.path}, not ${expectedPath}.`,
2370
+ "Move or remove that conflicting plugin registration manually, then run setup again."
2371
+ );
2372
+ }
2373
+ if (!plugin.enabled || plugin.status !== "running" || plugin.error !== void 0) {
2374
+ const detail = plugin.error === void 0 ? `status ${plugin.status}` : `${plugin.status}: ${plugin.error}`;
2375
+ return fail("claude.plugin.runtime", `Plugin ${CLAUDE_CARRIER_PLUGIN_ID} is not running (${detail}).`, "Run: paseo-room setup --apply");
2376
+ }
2377
+ return pass("claude.plugin.runtime", `Claude contract carrier plugin is running from ${expectedPath}.`);
2378
+ }
2379
+ async function reconcilePlugin(session, desired, plan, pending, expectedPath) {
2380
+ if (desired.pluginPath === void 0) {
2381
+ if (plan.livePlugin !== void 0) await session.removePlugin(CLAUDE_CARRIER_PLUGIN_ID);
2382
+ return [];
2383
+ }
2384
+ let plugin = plan.livePlugin;
2385
+ if (plugin === void 0) {
2386
+ plugin = await session.installPlugin(expectedPath, CLAUDE_CARRIER_PLUGIN_ID);
2387
+ } else {
2388
+ if (!plugin.enabled) plugin = await session.enablePlugin(CLAUDE_CARRIER_PLUGIN_ID);
2389
+ const filesChanged = pending.some(
2390
+ (operation) => (operation.kind === "file" || operation.kind === "dir") && operation.target.startsWith(`${expectedPath}${sep2}`)
2391
+ );
2392
+ const needsReload = filesChanged || plugin.status !== "running" || plugin.error !== void 0;
2393
+ if (needsReload) plugin = await session.reloadPlugin(CLAUDE_CARRIER_PLUGIN_ID);
2394
+ }
2395
+ return [pluginRuntimeCheck(plugin, expectedPath)];
2396
+ }
2214
2397
  async function setup(options = {}) {
2215
2398
  const layout = resolveLayout(options, options.env);
2216
2399
  const agents = options.agents ?? ["codex"];
@@ -2222,7 +2405,8 @@ async function setup(options = {}) {
2222
2405
  const safetyAgents = [.../* @__PURE__ */ new Set([...agents, ...previous?.agents ?? []])];
2223
2406
  const pathSafety = await roomPathSafety(layout, safetyAgents, ROLES);
2224
2407
  if (hasFailure(pathSafety)) return failed("setup", pathSafety);
2225
- const daemon = await checkDaemon(layout, options.env, minimumPaseoVersion(agents));
2408
+ const compatibilityAgents = [.../* @__PURE__ */ new Set([...agents, ...previous?.agents ?? []])];
2409
+ const daemon = await checkDaemon(layout, options.env, minimumPaseoVersion(compatibilityAgents));
2226
2410
  if (!daemon.daemon) return failed("setup", daemon.checks);
2227
2411
  const desired = await buildDesired(layout, agents, ROLES);
2228
2412
  const stale = await staleFrom(layout, previous, agents, ROLES);
@@ -2234,19 +2418,24 @@ async function setup(options = {}) {
2234
2418
  const checks = [...daemon.checks, ...desired.checks, ...retained];
2235
2419
  if (hasFailure(checks)) return failed("setup", checks);
2236
2420
  return withSession(daemon.daemon, async (session) => {
2237
- const { operations, liveProfiles } = await planRoom(session, desired, stale);
2421
+ const expectedPluginPath = claudeCarrierPluginDir(layout);
2422
+ const plan = await planRoom(session, desired, stale, expectedPluginPath);
2423
+ const { operations, liveProfiles } = plan;
2238
2424
  const pending = operations.filter((operation) => operation.action !== "noop");
2239
- const preApplyDiagnostics = [...checks, ...contractProvenance(previous), ...thinkingDiagnostics(liveProfiles, desired.profiles)];
2240
- if (!options.apply) {
2425
+ const pluginPreconditions = desired.pluginPath === void 0 ? plan.pluginChecks : [pluginsEnabledCheck(await session.pluginsEnabled()), ...plan.pluginChecks];
2426
+ const preApplyDiagnostics = [...checks, ...pluginPreconditions, ...contractProvenance(previous), ...thinkingDiagnostics(liveProfiles, desired.profiles)];
2427
+ if (hasFailure(preApplyDiagnostics) || !options.apply) {
2241
2428
  return {
2242
2429
  command: "setup",
2243
- outcome: pending.length > 0 ? "changes-planned" : "ok",
2430
+ outcome: hasFailure(preApplyDiagnostics) ? "failed" : pending.length > 0 ? "changes-planned" : "ok",
2244
2431
  changed: false,
2245
2432
  checks: preApplyDiagnostics,
2246
2433
  operations
2247
2434
  };
2248
2435
  }
2249
- await applyEntries(desired.entries);
2436
+ const markerPath = join11(layout.roomHome, MARKER);
2437
+ const markerEntries = desired.entries.filter((entry) => entry.path === markerPath);
2438
+ await applyEntries(desired.entries.filter((entry) => entry.path !== markerPath));
2250
2439
  const ids = Object.keys(desired.providers);
2251
2440
  await session.writeProviders(desired.providers);
2252
2441
  if (stale.providerIds.length > 0) await session.removeProviders(stale.providerIds);
@@ -2256,24 +2445,27 @@ async function setup(options = {}) {
2256
2445
  await session.writeProfiles(appliedProfiles);
2257
2446
  }
2258
2447
  await session.refresh(ids);
2448
+ const pluginChecks = await reconcilePlugin(session, desired, plan, pending, expectedPluginPath);
2449
+ await applyEntries(markerEntries);
2259
2450
  const appliedMarker = {
2260
2451
  version: package_default.version,
2261
2452
  agents: [...agents],
2262
2453
  roles: [...ROLES],
2263
2454
  contract: contractDigest()
2264
2455
  };
2456
+ const appliedChecks = [
2457
+ ...checks,
2458
+ ...pluginPreconditions,
2459
+ ...pluginChecks,
2460
+ pass("room.applied", `Room ready at ${layout.roomHome} with ${String(ids.length)} Paseo providers.`),
2461
+ ...contractProvenance(appliedMarker),
2462
+ ...thinkingDiagnostics(appliedProfiles, desired.profiles)
2463
+ ];
2265
2464
  return {
2266
2465
  command: "setup",
2267
- outcome: "ok",
2466
+ outcome: hasFailure(appliedChecks) ? "failed" : "ok",
2268
2467
  changed: pending.length > 0,
2269
- // Report success before non-blocking diagnostics so it cannot read as if a later pass
2270
- // cleared a warning. `appliedProfiles` is the exact array written above when one changed.
2271
- checks: [
2272
- ...checks,
2273
- pass("room.applied", `Room ready at ${layout.roomHome} with ${String(ids.length)} Paseo providers.`),
2274
- ...contractProvenance(appliedMarker),
2275
- ...thinkingDiagnostics(appliedProfiles, desired.profiles)
2276
- ],
2468
+ checks: appliedChecks,
2277
2469
  operations
2278
2470
  };
2279
2471
  }, sessionOptions(options));
@@ -2296,18 +2488,26 @@ async function verify(options = {}) {
2296
2488
  const checks = [...daemon.checks, ...desired.checks];
2297
2489
  if (hasFailure(checks)) return failed("verify", checks);
2298
2490
  return withSession(daemon.daemon, async (session) => {
2299
- const { operations, liveProfiles } = await planRoom(session, desired, NOTHING_STALE);
2491
+ const expectedPluginPath = claudeCarrierPluginDir(layout);
2492
+ const plan = await planRoom(session, desired, NOTHING_STALE, expectedPluginPath);
2493
+ const { operations, liveProfiles } = plan;
2300
2494
  const drifted = operations.filter((operation) => operation.action !== "noop");
2301
2495
  const count = (kind) => drifted.filter((operation) => operation.kind === kind).length;
2302
2496
  const providers = count("provider");
2303
2497
  const profiles = count("profile");
2304
- const driftedFiles = drifted.filter((operation) => operation.kind !== "provider" && operation.kind !== "profile");
2498
+ const driftedFiles = drifted.filter((operation) => !["provider", "profile", "plugin"].includes(operation.kind));
2305
2499
  const files = driftedFiles.length;
2500
+ const pluginChecks = desired.pluginPath === void 0 ? [] : [
2501
+ pluginsEnabledCheck(await session.pluginsEnabled()),
2502
+ ...plan.pluginChecks,
2503
+ pluginRuntimeCheck(plan.livePlugin, expectedPluginPath)
2504
+ ];
2306
2505
  const all = [
2307
2506
  ...checks,
2308
2507
  files === 0 ? pass("room.files", "Every managed role file matches the current definition.") : fail("room.files", `${String(files)} managed role files are missing or outdated.${fileDriftDetail(driftedFiles.map((operation) => operation.target))}`, "Run: paseo-room setup --apply"),
2309
2508
  providers === 0 ? pass("room.providers", `All ${String(Object.keys(desired.providers).length)} Paseo providers are registered as expected.`) : fail("room.providers", `${String(providers)} Paseo providers are missing or differ.`, "Run: paseo-room setup --apply"),
2310
2509
  profiles === 0 ? pass("room.profiles", `All ${String(desired.profiles.length)} Paseo agent profiles are registered as expected.`) : fail("room.profiles", `${String(profiles)} Paseo agent profiles are missing or differ.`, "Run: paseo-room setup --apply"),
2510
+ ...pluginChecks,
2311
2511
  // Diagnostics last: a warning never precedes the failure it might be mistaken for.
2312
2512
  ...contractProvenance(marker),
2313
2513
  ...thinkingDiagnostics(liveProfiles, desired.profiles)
@@ -2333,9 +2533,11 @@ async function remove(options = {}) {
2333
2533
  if (invalid.length > 0) return failed("remove", invalid);
2334
2534
  const ids = marker.agents.flatMap((agent) => marker.roles.map((role) => providerId(agent, role)));
2335
2535
  const profiles = new Set(marker.agents.flatMap((agent) => marker.roles.map((role) => profileId(agent, role))));
2536
+ const hasClaude = marker.agents.includes("claude");
2336
2537
  const operations = [
2337
2538
  ...ids.map((id) => ({ action: "remove", kind: "provider", target: id })),
2338
2539
  ...[...profiles].map((id) => ({ action: "remove", kind: "profile", target: id })),
2540
+ ...hasClaude ? [{ action: "remove", kind: "plugin", target: CLAUDE_CARRIER_PLUGIN_ID }] : [],
2339
2541
  { action: "remove", kind: "dir", target: layout.roomHome }
2340
2542
  ];
2341
2543
  const credentialWarning = warn(
@@ -2360,6 +2562,14 @@ async function remove(options = {}) {
2360
2562
  }
2361
2563
  try {
2362
2564
  await withSession(daemon.daemon, async (session) => {
2565
+ if (hasClaude) {
2566
+ const plugin = (await session.listPlugins()).find((entry) => entry.id === CLAUDE_CARRIER_PLUGIN_ID);
2567
+ const expectedPath = claudeCarrierPluginDir(layout);
2568
+ if (plugin !== void 0 && plugin.path !== expectedPath) {
2569
+ throw new Error(`refusing to remove foreign plugin path ${plugin.path}`);
2570
+ }
2571
+ if (plugin !== void 0) await session.removePlugin(CLAUDE_CARRIER_PLUGIN_ID);
2572
+ }
2363
2573
  await session.removeProviders(ids);
2364
2574
  const live = await session.readProfiles();
2365
2575
  const kept = live.filter((entry) => !profiles.has(String(entry.id)));
@@ -2403,12 +2613,13 @@ var init_commands = __esm({
2403
2613
  init_fsops();
2404
2614
  init_layout();
2405
2615
  init_paseo();
2616
+ init_plugin();
2406
2617
  init_result();
2407
2618
  init_instructions();
2408
2619
  init_room();
2409
2620
  init_roles();
2410
2621
  AGENTS = { codex: codexAgent, claude: claudeAgent, pi: piAgent };
2411
- NOTHING_STALE = { providerIds: [], profileIds: [], retainedDirectories: [] };
2622
+ NOTHING_STALE = { providerIds: [], profileIds: [], removePlugin: false, retainedDirectories: [] };
2412
2623
  }
2413
2624
  });
2414
2625