paseo-room 0.3.0 → 0.4.1

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
@@ -45,6 +45,7 @@ interactive stdin/stdout/stderr, and targets exactly one selected agent and role
45
45
  | `--agent <codex\|claude\|pi>` | `codex` | Which coding agent to seat. Repeat the flag to combine agents. |
46
46
  | `--apply` | off | Actually write setup/remove changes; `auth login` does not use it. |
47
47
  | `--json` | off | One machine-readable document instead of text. |
48
+ | `--no-claude-memory-contract` | off | Omit the role contract from Claude role `CLAUDE.md` files, leaving the room plugin as the only Claude carrier. Your global memory is still carried. `setup` only. |
48
49
  | `--room-home <path>` | `~/.paseo-room` | Where role homes are written. |
49
50
  | `--codex-home <path>` | `~/.codex` | Source Codex configuration. |
50
51
  | `--claude-home <path>` | `~/.claude` | Source Claude Code configuration. |
@@ -99,7 +100,7 @@ starts, its exit code is preserved; a signal is returned using the conventional
99
100
  auth.json # created and owned by Codex after role login, if file-backed
100
101
  AGENTS.md, skills, plugins, hooks.json → symlinks into ~/.codex when present (Peer: see below)
101
102
  roles/claude/<role>/
102
- CLAUDE.md # your global memory + role instructions
103
+ CLAUDE.md # your global memory + role instructions (contract omitted with --no-claude-memory-contract)
103
104
  settings.json # your settings.json + PASEO_ROOM_ROLE
104
105
  .claude.json # seeded once from yours, then owned by Claude
105
106
  .credentials.json # created and owned by Claude after role login, if file-backed
@@ -258,6 +259,14 @@ the plugin is absent, disabled, failed, registered from another path, or drifted
258
259
  silently missing carrier is not a guarantee. The hook runs for newly created sessions;
259
260
  recreate an existing Claude session after setup or an update.
260
261
 
262
+ `setup --no-claude-memory-contract` leaves the plugin as the only Claude carrier: each role
263
+ `CLAUDE.md` then holds your global memory alone, and the file is not written at all when you
264
+ have none. Only the contract is dropped, never your own memory, and an earlier generation of it
265
+ is removed rather than left behind. The choice is recorded in `room.json`, so `verify` compares
266
+ against it and rejects the flag itself; `setup` without the flag restores the fallback. Keep the
267
+ fallback unless you have a reason not to: the plugin hook is verified, but whether a *resumed*
268
+ session re-enters it is unproven, and `CLAUDE.md` is what covers that case.
269
+
261
270
  Pi providers use a strict command tail:
262
271
 
263
272
  ```text
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.3.0",
18
+ version: "0.4.1",
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",
@@ -270,7 +270,7 @@ async function lstatOrAbsent(path) {
270
270
  }
271
271
  }
272
272
  async function staleChildren(entry) {
273
- const declared = new Set(entry.children);
273
+ const declared = /* @__PURE__ */ new Set([...entry.children, ...entry.reserved ?? []]);
274
274
  let names;
275
275
  try {
276
276
  names = await readdir(entry.path);
@@ -282,6 +282,7 @@ async function staleChildren(entry) {
282
282
  }
283
283
  async function current(entry) {
284
284
  const stat2 = await lstatOrAbsent(entry.path);
285
+ if (entry.kind === "absent") return stat2 ? "different" : "same";
285
286
  if (!stat2) return "absent";
286
287
  if (entry.kind === "dir") return stat2.isDirectory() ? "same" : "different";
287
288
  if (entry.kind === "managed-dir") {
@@ -301,9 +302,9 @@ async function planEntries(entries) {
301
302
  for (const entry of entries) {
302
303
  const state = await current(entry);
303
304
  operations.push({
304
- action: state === "same" ? "noop" : state === "absent" ? "create" : "update",
305
+ action: state === "same" ? "noop" : entry.kind === "absent" ? "remove" : state === "absent" ? "create" : "update",
305
306
  // A managed directory is still a directory to everything that reports operations.
306
- kind: entry.kind === "managed-dir" ? "dir" : entry.kind,
307
+ kind: entry.kind === "managed-dir" ? "dir" : entry.kind === "absent" ? "file" : entry.kind,
307
308
  target: entry.path
308
309
  });
309
310
  }
@@ -372,6 +373,12 @@ async function applyManagedDirectory(entry) {
372
373
  await rm(child, { force: true });
373
374
  }
374
375
  }
376
+ async function removeManagedFile(path) {
377
+ const stat2 = await lstatOrAbsent(path);
378
+ if (!stat2) return;
379
+ if (!stat2.isFile()) refuse(path, "a regular file this room generated", stat2);
380
+ await rm(path, { force: true });
381
+ }
375
382
  async function applyEntries(entries) {
376
383
  for (const entry of entries) {
377
384
  if (await current(entry) === "same") continue;
@@ -383,6 +390,10 @@ async function applyEntries(entries) {
383
390
  await applyManagedDirectory(entry);
384
391
  continue;
385
392
  }
393
+ if (entry.kind === "absent") {
394
+ await removeManagedFile(entry.path);
395
+ continue;
396
+ }
386
397
  await mkdir(dirname(entry.path), { recursive: true, mode: DIR_MODE });
387
398
  await assertReplaceable(entry);
388
399
  await replaceAtomically(entry);
@@ -461,12 +472,14 @@ var init_roles = __esm({
461
472
  // src/room.ts
462
473
  import { join as join3 } from "path";
463
474
  import { z } from "zod";
464
- function renderMarker(version, agents, roles, contract) {
475
+ function renderMarker(version, agents, roles, contract, claudeMemoryContract) {
465
476
  return JSON.stringify({
466
477
  version,
467
478
  agents: [...agents],
468
479
  roles: [...roles],
469
- ...contract === void 0 ? {} : { contract }
480
+ ...contract === void 0 ? {} : { contract },
481
+ // Written only when it differs from the default, so an unchanged room's marker is unchanged.
482
+ ...claudeMemoryContract === false ? { claudeMemoryContract: false } : {}
470
483
  }, null, 2) + "\n";
471
484
  }
472
485
  async function readMarker(layout) {
@@ -494,7 +507,13 @@ var init_room = __esm({
494
507
  * written before contract provenance existed has no digest, and such a marker must still
495
508
  * parse rather than look like a foreign file.
496
509
  */
497
- contract: z.string().min(1).optional()
510
+ contract: z.string().min(1).optional(),
511
+ /**
512
+ * False when this room deliberately suppressed the role contract in Claude's `CLAUDE.md`,
513
+ * leaving the plugin as its only Claude carrier. Optional and defaulted to true so a room
514
+ * written before the option existed keeps parsing and keeps its current behaviour.
515
+ */
516
+ claudeMemoryContract: z.boolean().optional()
498
517
  });
499
518
  }
500
519
  });
@@ -1021,9 +1040,9 @@ function isPaseoSkill(name) {
1021
1040
  function receivesExecutableResources(role) {
1022
1041
  return role !== "peer";
1023
1042
  }
1024
- async function skillNames(path) {
1043
+ async function skillNames(path, reserved) {
1025
1044
  try {
1026
- return (await readdir2(path)).filter((name) => !isPaseoSkill(name)).sort();
1045
+ return (await readdir2(path)).filter((name) => !isPaseoSkill(name) && !reserved.has(name)).sort();
1027
1046
  } catch (error) {
1028
1047
  if (error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT") return [];
1029
1048
  throw error;
@@ -1032,6 +1051,7 @@ async function skillNames(path) {
1032
1051
  async function roleResourceEntries(input) {
1033
1052
  const present = new Set(input.shared);
1034
1053
  const executable = new Set(input.executable);
1054
+ const reserved = new Set(input.reservedSkills ?? []);
1035
1055
  const entries = [];
1036
1056
  for (const name of input.names) {
1037
1057
  const path = join6(input.home, name);
@@ -1039,8 +1059,14 @@ async function roleResourceEntries(input) {
1039
1059
  if (!receivesExecutableResources(input.role)) {
1040
1060
  if (executable.has(name)) continue;
1041
1061
  if (name === SKILLS) {
1042
- const children = present.has(path) ? await skillNames(path) : [];
1043
- entries.push({ kind: "managed-dir", path: alias, children, legacyLink: path });
1062
+ const children = present.has(path) ? await skillNames(path, reserved) : [];
1063
+ entries.push({
1064
+ kind: "managed-dir",
1065
+ path: alias,
1066
+ children,
1067
+ legacyLink: path,
1068
+ ...reserved.size === 0 ? {} : { reserved: [...reserved] }
1069
+ });
1044
1070
  for (const child of children) entries.push({ kind: "link", path: join6(alias, child), target: join6(path, child) });
1045
1071
  continue;
1046
1072
  }
@@ -1098,8 +1124,10 @@ function renderRoleState(source) {
1098
1124
  for (const key of SEEDED_KEYS) if (key in root) state[key] = root[key];
1099
1125
  return JSON.stringify(state, null, 2) + "\n";
1100
1126
  }
1101
- function renderRoleMemory(source, role) {
1127
+ function renderRoleMemory(source, role, contract = true) {
1102
1128
  const operator = source?.trim();
1129
+ if (!contract) return operator ? `${operator}
1130
+ ` : void 0;
1103
1131
  return operator ? `${operator}
1104
1132
 
1105
1133
  ${renderInstructions(role)}` : renderInstructions(role);
@@ -1153,7 +1181,7 @@ function mcpDriftCheck(role, home, path, divergence) {
1153
1181
  `If this role should have the same servers, add them with: CLAUDE_CONFIG_DIR=${home} claude mcp add ... \u2014 or delete ${path} and run setup --apply to reseed it from your current Claude state, which discards that role's other runtime state.`
1154
1182
  );
1155
1183
  }
1156
- var SHARED, EXECUTABLE, MCP_KEY, AUTH_ENV, CONTROL_PLANE_ENV, SECURE_STORAGE_ENV, SEEDED_KEYS, claudeAgent;
1184
+ var SHARED, EXECUTABLE, RUNTIME_SKILLS, MCP_KEY, AUTH_ENV, CONTROL_PLANE_ENV, SECURE_STORAGE_ENV, SEEDED_KEYS, claudeAgent;
1157
1185
  var init_claude = __esm({
1158
1186
  "src/agents/claude.ts"() {
1159
1187
  "use strict";
@@ -1176,6 +1204,7 @@ var init_claude = __esm({
1176
1204
  "themes"
1177
1205
  ];
1178
1206
  EXECUTABLE = ["plugins", "commands", "hooks"];
1207
+ RUNTIME_SKILLS = ["synced"];
1179
1208
  MCP_KEY = "mcpServers";
1180
1209
  AUTH_ENV = [
1181
1210
  "ANTHROPIC_AUTH_TOKEN",
@@ -1219,7 +1248,7 @@ var init_claude = __esm({
1219
1248
  "CronList"
1220
1249
  ]
1221
1250
  },
1222
- async build(layout, roles) {
1251
+ async build(layout, roles, options = {}) {
1223
1252
  const home = layout.agentHome.claude;
1224
1253
  const binary = await which(layout.bin.claude, layout.searchPath);
1225
1254
  if (!binary) {
@@ -1272,10 +1301,12 @@ var init_claude = __esm({
1272
1301
  if (divergence) drift.push(mcpDriftCheck(role, target, statePath, divergence));
1273
1302
  }
1274
1303
  entries.push({ kind: "dir", path: target });
1275
- entries.push({ kind: "file", path: join7(target, "CLAUDE.md"), content: renderRoleMemory(memorySource, role) });
1304
+ const memoryPath = join7(target, "CLAUDE.md");
1305
+ const memory = renderRoleMemory(memorySource, role, options.memoryContract ?? true);
1306
+ entries.push(memory === void 0 ? { kind: "absent", path: memoryPath } : { kind: "file", path: memoryPath, content: memory });
1276
1307
  entries.push({ kind: "file", path: join7(target, "settings.json"), content: renderRoleSettings(settingsSource, role, target) });
1277
1308
  entries.push({ kind: "file", path: statePath, content: renderRoleState(stateSource), once: true });
1278
- entries.push(...await roleResourceEntries({ role, target, home, names: SHARED, shared, executable: EXECUTABLE }));
1309
+ entries.push(...await roleResourceEntries({ role, target, home, names: SHARED, shared, executable: EXECUTABLE, reservedSkills: RUNTIME_SKILLS }));
1279
1310
  credentials.push(await claudeCredentialDiagnostic(layout, role, settingsSource, process.platform, binary));
1280
1311
  providerEnv[role] = { [SECURE_STORAGE_ENV]: target };
1281
1312
  }
@@ -2157,7 +2188,7 @@ function roleProfiles(agent, roles) {
2157
2188
  notes: ROLE_NOTES[role]
2158
2189
  }));
2159
2190
  }
2160
- async function buildDesired(layout, agents, roles) {
2191
+ async function buildDesired(layout, agents, roles, build = {}) {
2161
2192
  const entries = [
2162
2193
  { kind: "dir", path: layout.roomHome },
2163
2194
  { kind: "dir", path: sharedRoom(layout) },
@@ -2169,7 +2200,7 @@ async function buildDesired(layout, agents, roles) {
2169
2200
  const binaries = {};
2170
2201
  for (const id of agents) {
2171
2202
  const agent = AGENTS[id];
2172
- const plan = await agent.build(layout, roles);
2203
+ const plan = await agent.build(layout, roles, build);
2173
2204
  entries.push(...plan.entries);
2174
2205
  checks.push(...plan.checks);
2175
2206
  const credentials = plan.credentials ?? [];
@@ -2206,7 +2237,7 @@ async function buildDesired(layout, agents, roles) {
2206
2237
  }
2207
2238
  const pluginPath = agents.includes("claude") ? claudeCarrierPluginDir(layout) : void 0;
2208
2239
  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()) });
2240
+ entries.push({ kind: "file", path: join11(layout.roomHome, MARKER), content: renderMarker(package_default.version, agents, roles, contractDigest(), markerMemoryContract(agents, build.memoryContract ?? true)) });
2210
2241
  return { entries, providers, profiles, checks, ...pluginPath === void 0 ? {} : { pluginPath } };
2211
2242
  }
2212
2243
  function contractProvenance(previous) {
@@ -2232,6 +2263,14 @@ function thinkingDiagnostics(live, desired) {
2232
2263
  `Choose a lower reasoning option for those seats in Paseo if you want the room's own defaults (${ROLE_THINKING.supervisor} for Supervisor, ${ROLE_THINKING.lead} for Lead and Peer).`
2233
2264
  )];
2234
2265
  }
2266
+ function fileDriftSummary(drifted) {
2267
+ const removals = drifted.filter((operation) => operation.action === "remove").length;
2268
+ const count = String(drifted.length);
2269
+ const subject = drifted.length === 1 ? "managed role file is" : "managed role files are";
2270
+ if (removals === 0) return `${count} ${subject} missing or outdated.`;
2271
+ if (removals === drifted.length) return `${count} ${subject} present but suppressed by this room.`;
2272
+ return `${count} ${subject} missing or outdated, and ${String(removals)} of them are present but suppressed by this room.`;
2273
+ }
2235
2274
  function fileDriftDetail(targets) {
2236
2275
  const catalogs = targets.filter((target) => target.endsWith(`${sep2}model-catalog.json`));
2237
2276
  if (catalogs.length === 0) return "";
@@ -2394,9 +2433,21 @@ async function reconcilePlugin(session, desired, plan, pending, expectedPath) {
2394
2433
  }
2395
2434
  return [pluginRuntimeCheck(plugin, expectedPath)];
2396
2435
  }
2436
+ function markerMemoryContract(agents, memoryContract) {
2437
+ return agents.includes("claude") ? memoryContract : void 0;
2438
+ }
2439
+ function memoryContractDiagnostic(agents, memoryContract) {
2440
+ if (memoryContract || !agents.includes("claude")) return [];
2441
+ return [warn(
2442
+ "claude.memory-contract",
2443
+ "Claude role CLAUDE.md files carry your global memory only: the role contract was not written to them, so the trusted server plugin is this room's only Claude carrier. Only a newly created agent passes through its creation hook, and whether a resumed session re-enters that hook is unproven.",
2444
+ "Drop --no-claude-memory-contract and run setup --apply to restore the file fallback."
2445
+ )];
2446
+ }
2397
2447
  async function setup(options = {}) {
2398
2448
  const layout = resolveLayout(options, options.env);
2399
2449
  const agents = options.agents ?? ["codex"];
2450
+ const memoryContract = options.claudeMemoryContract ?? true;
2400
2451
  const invalid = layoutChecks(layout, agents);
2401
2452
  if (invalid.length > 0) return failed("setup", invalid);
2402
2453
  const rootSafety = await roomPathSafety(layout, [], []);
@@ -2408,7 +2459,7 @@ async function setup(options = {}) {
2408
2459
  const compatibilityAgents = [.../* @__PURE__ */ new Set([...agents, ...previous?.agents ?? []])];
2409
2460
  const daemon = await checkDaemon(layout, options.env, minimumPaseoVersion(compatibilityAgents));
2410
2461
  if (!daemon.daemon) return failed("setup", daemon.checks);
2411
- const desired = await buildDesired(layout, agents, ROLES);
2462
+ const desired = await buildDesired(layout, agents, ROLES, { memoryContract });
2412
2463
  const stale = await staleFrom(layout, previous, agents, ROLES);
2413
2464
  const retained = stale.retainedDirectories.length === 0 ? [] : [warn(
2414
2465
  "room.stale-role-homes-preserved",
@@ -2423,7 +2474,7 @@ async function setup(options = {}) {
2423
2474
  const { operations, liveProfiles } = plan;
2424
2475
  const pending = operations.filter((operation) => operation.action !== "noop");
2425
2476
  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)];
2477
+ const preApplyDiagnostics = [...checks, ...pluginPreconditions, ...memoryContractDiagnostic(agents, memoryContract), ...contractProvenance(previous), ...thinkingDiagnostics(liveProfiles, desired.profiles)];
2427
2478
  if (hasFailure(preApplyDiagnostics) || !options.apply) {
2428
2479
  return {
2429
2480
  command: "setup",
@@ -2451,13 +2502,15 @@ async function setup(options = {}) {
2451
2502
  version: package_default.version,
2452
2503
  agents: [...agents],
2453
2504
  roles: [...ROLES],
2454
- contract: contractDigest()
2505
+ contract: contractDigest(),
2506
+ ...markerMemoryContract(agents, memoryContract) === false ? { claudeMemoryContract: false } : {}
2455
2507
  };
2456
2508
  const appliedChecks = [
2457
2509
  ...checks,
2458
2510
  ...pluginPreconditions,
2459
2511
  ...pluginChecks,
2460
2512
  pass("room.applied", `Room ready at ${layout.roomHome} with ${String(ids.length)} Paseo providers.`),
2513
+ ...memoryContractDiagnostic(agents, memoryContract),
2461
2514
  ...contractProvenance(appliedMarker),
2462
2515
  ...thinkingDiagnostics(appliedProfiles, desired.profiles)
2463
2516
  ];
@@ -2484,7 +2537,10 @@ async function verify(options = {}) {
2484
2537
  if (hasFailure(pathSafety)) return failed("verify", pathSafety);
2485
2538
  const daemon = await checkDaemon(layout, options.env, minimumPaseoVersion(marker.agents));
2486
2539
  if (!daemon.daemon) return failed("verify", daemon.checks);
2487
- const desired = await buildDesired(layout, marker.agents, marker.roles);
2540
+ const desired = await buildDesired(layout, marker.agents, marker.roles, {
2541
+ // The room's own recorded choice, so verify compares against what setup wrote.
2542
+ memoryContract: marker.claudeMemoryContract ?? true
2543
+ });
2488
2544
  const checks = [...daemon.checks, ...desired.checks];
2489
2545
  if (hasFailure(checks)) return failed("verify", checks);
2490
2546
  return withSession(daemon.daemon, async (session) => {
@@ -2504,11 +2560,12 @@ async function verify(options = {}) {
2504
2560
  ];
2505
2561
  const all = [
2506
2562
  ...checks,
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"),
2563
+ files === 0 ? pass("room.files", "Every managed role file matches the current definition.") : fail("room.files", `${fileDriftSummary(driftedFiles)}${fileDriftDetail(driftedFiles.map((operation) => operation.target))}`, "Run: paseo-room setup --apply"),
2508
2564
  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"),
2509
2565
  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
2566
  ...pluginChecks,
2511
2567
  // Diagnostics last: a warning never precedes the failure it might be mistaken for.
2568
+ ...memoryContractDiagnostic(marker.agents, marker.claudeMemoryContract ?? true),
2512
2569
  ...contractProvenance(marker),
2513
2570
  ...thinkingDiagnostics(liveProfiles, desired.profiles)
2514
2571
  ];
@@ -2658,12 +2715,25 @@ async function runWizard(prompts, emit, write, options = {}, onAction) {
2658
2715
  options: AGENT_IDS.map((value) => ({ value, label: AGENTS[value].label }))
2659
2716
  });
2660
2717
  if (typeof agents === "symbol" || agents.length === 0) return cancelled();
2661
- const preview = await setup({ ...options, agents });
2718
+ let claudeMemoryContract = true;
2719
+ if (agents.includes("claude")) {
2720
+ const carrier = await prompts.select({
2721
+ message: "Claude role contract carrier: the room plugin is always used. Also write the contract into each role CLAUDE.md?",
2722
+ options: [
2723
+ { value: "both", label: "Yes \u2014 keep it as a fallback (recommended; resume behaviour is unproven)" },
2724
+ { value: "plugin", label: "No \u2014 plugin only; CLAUDE.md keeps your global memory alone" }
2725
+ ]
2726
+ });
2727
+ if (typeof carrier === "symbol") return cancelled();
2728
+ claudeMemoryContract = carrier === "both";
2729
+ }
2730
+ const setupOptions = { ...options, agents, claudeMemoryContract };
2731
+ const preview = await setup(setupOptions);
2662
2732
  const status = emit(preview);
2663
2733
  if (preview.outcome !== "changes-planned") return status;
2664
2734
  const approved = await prompts.confirm({ message: "Apply these changes? Authentication will not be validated or started." });
2665
2735
  if (approved !== true) return cancelled();
2666
- return emit(await setup({ ...options, agents, apply: true }));
2736
+ return emit(await setup({ ...setupOptions, apply: true }));
2667
2737
  }
2668
2738
  var terminalPrompts;
2669
2739
  var init_wizard = __esm({
@@ -2683,6 +2753,7 @@ var init_wizard = __esm({
2683
2753
  init_package();
2684
2754
  init_auth();
2685
2755
  init_commands();
2756
+ init_fsops();
2686
2757
  import { Command, CommanderError, InvalidArgumentError } from "commander";
2687
2758
 
2688
2759
  // src/render.ts
@@ -2729,7 +2800,14 @@ function optionsFrom(raw, base) {
2729
2800
  if (typeof value === "string" && value.trim()) paths[flag] = value.trim();
2730
2801
  }
2731
2802
  const agents = Array.isArray(raw.agent) && raw.agent.length > 0 ? raw.agent : void 0;
2732
- return { ...base, ...paths, ...agents ? { agents } : {}, ...raw.apply === true ? { apply: true } : {} };
2803
+ return {
2804
+ ...base,
2805
+ ...paths,
2806
+ ...agents ? { agents } : {},
2807
+ ...raw.apply === true ? { apply: true } : {},
2808
+ // Commander defaults a --no- flag to true, so only an explicit opt-out is carried.
2809
+ ...raw.claudeMemoryContract === false ? { claudeMemoryContract: false } : {}
2810
+ };
2733
2811
  }
2734
2812
  function failedFromThrown(command, error) {
2735
2813
  if (error instanceof PromptAssetError) {
@@ -2739,6 +2817,9 @@ function failedFromThrown(command, error) {
2739
2817
  "Reinstall paseo-room, then try again."
2740
2818
  )]);
2741
2819
  }
2820
+ if (error instanceof ManagedPathError) {
2821
+ return failed(command, [fail(`${command}.managed-path`, error.message, error.fix)]);
2822
+ }
2742
2823
  const detail = error instanceof Error ? error.message : "unknown error";
2743
2824
  return failed(command, [fail(
2744
2825
  `${command}.error`,
@@ -2779,7 +2860,7 @@ async function runCli(argv, output, context = {}) {
2779
2860
  return emit(failedFromThrown(wizardCommand, error));
2780
2861
  }
2781
2862
  }
2782
- 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> [options]\n auth login <codex|claude|pi> <supervisor|lead|peer> [options]").argument("<command>", "setup | verify | remove | 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("--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: (text) => {
2863
+ 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> [options]\n auth login <codex|claude|pi> <supervisor|lead|peer> [options]").argument("<command>", "setup | verify | remove | 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("--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: (text) => {
2783
2864
  output.stdout(text);
2784
2865
  }, writeErr: (text) => {
2785
2866
  output.stderr(text);
@@ -2800,7 +2881,16 @@ async function runCli(argv, output, context = {}) {
2800
2881
  return 2;
2801
2882
  }
2802
2883
  try {
2884
+ if (raw.claudeMemoryContract === false && (command === "verify" || command === "remove")) {
2885
+ output.stderr(`paseo-room ${command}: --no-claude-memory-contract is a setup choice recorded in the room marker; ${command} reads it from there.
2886
+ `);
2887
+ return 2;
2888
+ }
2803
2889
  if (command === "auth") {
2890
+ if (raw.claudeMemoryContract === false) {
2891
+ output.stderr("paseo-room auth login only authenticates a role and does not use --no-claude-memory-contract.\n");
2892
+ return 2;
2893
+ }
2804
2894
  if (raw.apply === true) {
2805
2895
  output.stderr("paseo-room auth login is already explicit and does not use --apply.\n");
2806
2896
  return 2;