paseo-room 0.3.0 → 0.4.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
@@ -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.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",
@@ -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
  });
@@ -1098,8 +1117,10 @@ function renderRoleState(source) {
1098
1117
  for (const key of SEEDED_KEYS) if (key in root) state[key] = root[key];
1099
1118
  return JSON.stringify(state, null, 2) + "\n";
1100
1119
  }
1101
- function renderRoleMemory(source, role) {
1120
+ function renderRoleMemory(source, role, contract = true) {
1102
1121
  const operator = source?.trim();
1122
+ if (!contract) return operator ? `${operator}
1123
+ ` : void 0;
1103
1124
  return operator ? `${operator}
1104
1125
 
1105
1126
  ${renderInstructions(role)}` : renderInstructions(role);
@@ -1219,7 +1240,7 @@ var init_claude = __esm({
1219
1240
  "CronList"
1220
1241
  ]
1221
1242
  },
1222
- async build(layout, roles) {
1243
+ async build(layout, roles, options = {}) {
1223
1244
  const home = layout.agentHome.claude;
1224
1245
  const binary = await which(layout.bin.claude, layout.searchPath);
1225
1246
  if (!binary) {
@@ -1272,7 +1293,9 @@ var init_claude = __esm({
1272
1293
  if (divergence) drift.push(mcpDriftCheck(role, target, statePath, divergence));
1273
1294
  }
1274
1295
  entries.push({ kind: "dir", path: target });
1275
- entries.push({ kind: "file", path: join7(target, "CLAUDE.md"), content: renderRoleMemory(memorySource, role) });
1296
+ const memoryPath = join7(target, "CLAUDE.md");
1297
+ const memory = renderRoleMemory(memorySource, role, options.memoryContract ?? true);
1298
+ entries.push(memory === void 0 ? { kind: "absent", path: memoryPath } : { kind: "file", path: memoryPath, content: memory });
1276
1299
  entries.push({ kind: "file", path: join7(target, "settings.json"), content: renderRoleSettings(settingsSource, role, target) });
1277
1300
  entries.push({ kind: "file", path: statePath, content: renderRoleState(stateSource), once: true });
1278
1301
  entries.push(...await roleResourceEntries({ role, target, home, names: SHARED, shared, executable: EXECUTABLE }));
@@ -2157,7 +2180,7 @@ function roleProfiles(agent, roles) {
2157
2180
  notes: ROLE_NOTES[role]
2158
2181
  }));
2159
2182
  }
2160
- async function buildDesired(layout, agents, roles) {
2183
+ async function buildDesired(layout, agents, roles, build = {}) {
2161
2184
  const entries = [
2162
2185
  { kind: "dir", path: layout.roomHome },
2163
2186
  { kind: "dir", path: sharedRoom(layout) },
@@ -2169,7 +2192,7 @@ async function buildDesired(layout, agents, roles) {
2169
2192
  const binaries = {};
2170
2193
  for (const id of agents) {
2171
2194
  const agent = AGENTS[id];
2172
- const plan = await agent.build(layout, roles);
2195
+ const plan = await agent.build(layout, roles, build);
2173
2196
  entries.push(...plan.entries);
2174
2197
  checks.push(...plan.checks);
2175
2198
  const credentials = plan.credentials ?? [];
@@ -2206,7 +2229,7 @@ async function buildDesired(layout, agents, roles) {
2206
2229
  }
2207
2230
  const pluginPath = agents.includes("claude") ? claudeCarrierPluginDir(layout) : void 0;
2208
2231
  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()) });
2232
+ entries.push({ kind: "file", path: join11(layout.roomHome, MARKER), content: renderMarker(package_default.version, agents, roles, contractDigest(), markerMemoryContract(agents, build.memoryContract ?? true)) });
2210
2233
  return { entries, providers, profiles, checks, ...pluginPath === void 0 ? {} : { pluginPath } };
2211
2234
  }
2212
2235
  function contractProvenance(previous) {
@@ -2232,6 +2255,14 @@ function thinkingDiagnostics(live, desired) {
2232
2255
  `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
2256
  )];
2234
2257
  }
2258
+ function fileDriftSummary(drifted) {
2259
+ const removals = drifted.filter((operation) => operation.action === "remove").length;
2260
+ const count = String(drifted.length);
2261
+ const subject = drifted.length === 1 ? "managed role file is" : "managed role files are";
2262
+ if (removals === 0) return `${count} ${subject} missing or outdated.`;
2263
+ if (removals === drifted.length) return `${count} ${subject} present but suppressed by this room.`;
2264
+ return `${count} ${subject} missing or outdated, and ${String(removals)} of them are present but suppressed by this room.`;
2265
+ }
2235
2266
  function fileDriftDetail(targets) {
2236
2267
  const catalogs = targets.filter((target) => target.endsWith(`${sep2}model-catalog.json`));
2237
2268
  if (catalogs.length === 0) return "";
@@ -2394,9 +2425,21 @@ async function reconcilePlugin(session, desired, plan, pending, expectedPath) {
2394
2425
  }
2395
2426
  return [pluginRuntimeCheck(plugin, expectedPath)];
2396
2427
  }
2428
+ function markerMemoryContract(agents, memoryContract) {
2429
+ return agents.includes("claude") ? memoryContract : void 0;
2430
+ }
2431
+ function memoryContractDiagnostic(agents, memoryContract) {
2432
+ if (memoryContract || !agents.includes("claude")) return [];
2433
+ return [warn(
2434
+ "claude.memory-contract",
2435
+ "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.",
2436
+ "Drop --no-claude-memory-contract and run setup --apply to restore the file fallback."
2437
+ )];
2438
+ }
2397
2439
  async function setup(options = {}) {
2398
2440
  const layout = resolveLayout(options, options.env);
2399
2441
  const agents = options.agents ?? ["codex"];
2442
+ const memoryContract = options.claudeMemoryContract ?? true;
2400
2443
  const invalid = layoutChecks(layout, agents);
2401
2444
  if (invalid.length > 0) return failed("setup", invalid);
2402
2445
  const rootSafety = await roomPathSafety(layout, [], []);
@@ -2408,7 +2451,7 @@ async function setup(options = {}) {
2408
2451
  const compatibilityAgents = [.../* @__PURE__ */ new Set([...agents, ...previous?.agents ?? []])];
2409
2452
  const daemon = await checkDaemon(layout, options.env, minimumPaseoVersion(compatibilityAgents));
2410
2453
  if (!daemon.daemon) return failed("setup", daemon.checks);
2411
- const desired = await buildDesired(layout, agents, ROLES);
2454
+ const desired = await buildDesired(layout, agents, ROLES, { memoryContract });
2412
2455
  const stale = await staleFrom(layout, previous, agents, ROLES);
2413
2456
  const retained = stale.retainedDirectories.length === 0 ? [] : [warn(
2414
2457
  "room.stale-role-homes-preserved",
@@ -2423,7 +2466,7 @@ async function setup(options = {}) {
2423
2466
  const { operations, liveProfiles } = plan;
2424
2467
  const pending = operations.filter((operation) => operation.action !== "noop");
2425
2468
  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)];
2469
+ const preApplyDiagnostics = [...checks, ...pluginPreconditions, ...memoryContractDiagnostic(agents, memoryContract), ...contractProvenance(previous), ...thinkingDiagnostics(liveProfiles, desired.profiles)];
2427
2470
  if (hasFailure(preApplyDiagnostics) || !options.apply) {
2428
2471
  return {
2429
2472
  command: "setup",
@@ -2451,13 +2494,15 @@ async function setup(options = {}) {
2451
2494
  version: package_default.version,
2452
2495
  agents: [...agents],
2453
2496
  roles: [...ROLES],
2454
- contract: contractDigest()
2497
+ contract: contractDigest(),
2498
+ ...markerMemoryContract(agents, memoryContract) === false ? { claudeMemoryContract: false } : {}
2455
2499
  };
2456
2500
  const appliedChecks = [
2457
2501
  ...checks,
2458
2502
  ...pluginPreconditions,
2459
2503
  ...pluginChecks,
2460
2504
  pass("room.applied", `Room ready at ${layout.roomHome} with ${String(ids.length)} Paseo providers.`),
2505
+ ...memoryContractDiagnostic(agents, memoryContract),
2461
2506
  ...contractProvenance(appliedMarker),
2462
2507
  ...thinkingDiagnostics(appliedProfiles, desired.profiles)
2463
2508
  ];
@@ -2484,7 +2529,10 @@ async function verify(options = {}) {
2484
2529
  if (hasFailure(pathSafety)) return failed("verify", pathSafety);
2485
2530
  const daemon = await checkDaemon(layout, options.env, minimumPaseoVersion(marker.agents));
2486
2531
  if (!daemon.daemon) return failed("verify", daemon.checks);
2487
- const desired = await buildDesired(layout, marker.agents, marker.roles);
2532
+ const desired = await buildDesired(layout, marker.agents, marker.roles, {
2533
+ // The room's own recorded choice, so verify compares against what setup wrote.
2534
+ memoryContract: marker.claudeMemoryContract ?? true
2535
+ });
2488
2536
  const checks = [...daemon.checks, ...desired.checks];
2489
2537
  if (hasFailure(checks)) return failed("verify", checks);
2490
2538
  return withSession(daemon.daemon, async (session) => {
@@ -2504,11 +2552,12 @@ async function verify(options = {}) {
2504
2552
  ];
2505
2553
  const all = [
2506
2554
  ...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"),
2555
+ 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
2556
  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
2557
  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
2558
  ...pluginChecks,
2511
2559
  // Diagnostics last: a warning never precedes the failure it might be mistaken for.
2560
+ ...memoryContractDiagnostic(marker.agents, marker.claudeMemoryContract ?? true),
2512
2561
  ...contractProvenance(marker),
2513
2562
  ...thinkingDiagnostics(liveProfiles, desired.profiles)
2514
2563
  ];
@@ -2658,12 +2707,25 @@ async function runWizard(prompts, emit, write, options = {}, onAction) {
2658
2707
  options: AGENT_IDS.map((value) => ({ value, label: AGENTS[value].label }))
2659
2708
  });
2660
2709
  if (typeof agents === "symbol" || agents.length === 0) return cancelled();
2661
- const preview = await setup({ ...options, agents });
2710
+ let claudeMemoryContract = true;
2711
+ if (agents.includes("claude")) {
2712
+ const carrier = await prompts.select({
2713
+ message: "Claude role contract carrier: the room plugin is always used. Also write the contract into each role CLAUDE.md?",
2714
+ options: [
2715
+ { value: "both", label: "Yes \u2014 keep it as a fallback (recommended; resume behaviour is unproven)" },
2716
+ { value: "plugin", label: "No \u2014 plugin only; CLAUDE.md keeps your global memory alone" }
2717
+ ]
2718
+ });
2719
+ if (typeof carrier === "symbol") return cancelled();
2720
+ claudeMemoryContract = carrier === "both";
2721
+ }
2722
+ const setupOptions = { ...options, agents, claudeMemoryContract };
2723
+ const preview = await setup(setupOptions);
2662
2724
  const status = emit(preview);
2663
2725
  if (preview.outcome !== "changes-planned") return status;
2664
2726
  const approved = await prompts.confirm({ message: "Apply these changes? Authentication will not be validated or started." });
2665
2727
  if (approved !== true) return cancelled();
2666
- return emit(await setup({ ...options, agents, apply: true }));
2728
+ return emit(await setup({ ...setupOptions, apply: true }));
2667
2729
  }
2668
2730
  var terminalPrompts;
2669
2731
  var init_wizard = __esm({
@@ -2729,7 +2791,14 @@ function optionsFrom(raw, base) {
2729
2791
  if (typeof value === "string" && value.trim()) paths[flag] = value.trim();
2730
2792
  }
2731
2793
  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 } : {} };
2794
+ return {
2795
+ ...base,
2796
+ ...paths,
2797
+ ...agents ? { agents } : {},
2798
+ ...raw.apply === true ? { apply: true } : {},
2799
+ // Commander defaults a --no- flag to true, so only an explicit opt-out is carried.
2800
+ ...raw.claudeMemoryContract === false ? { claudeMemoryContract: false } : {}
2801
+ };
2733
2802
  }
2734
2803
  function failedFromThrown(command, error) {
2735
2804
  if (error instanceof PromptAssetError) {
@@ -2779,7 +2848,7 @@ async function runCli(argv, output, context = {}) {
2779
2848
  return emit(failedFromThrown(wizardCommand, error));
2780
2849
  }
2781
2850
  }
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) => {
2851
+ 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
2852
  output.stdout(text);
2784
2853
  }, writeErr: (text) => {
2785
2854
  output.stderr(text);
@@ -2800,7 +2869,16 @@ async function runCli(argv, output, context = {}) {
2800
2869
  return 2;
2801
2870
  }
2802
2871
  try {
2872
+ if (raw.claudeMemoryContract === false && (command === "verify" || command === "remove")) {
2873
+ output.stderr(`paseo-room ${command}: --no-claude-memory-contract is a setup choice recorded in the room marker; ${command} reads it from there.
2874
+ `);
2875
+ return 2;
2876
+ }
2803
2877
  if (command === "auth") {
2878
+ if (raw.claudeMemoryContract === false) {
2879
+ output.stderr("paseo-room auth login only authenticates a role and does not use --no-claude-memory-contract.\n");
2880
+ return 2;
2881
+ }
2804
2882
  if (raw.apply === true) {
2805
2883
  output.stderr("paseo-room auth login is already explicit and does not use --apply.\n");
2806
2884
  return 2;