paseo-room 0.2.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. |
@@ -88,7 +89,7 @@ starts, its exit code is preserved; a signal is returned using the conventional
88
89
  ~/.paseo-room/
89
90
  room.json # what this CLI created; verify and remove read it
90
91
  AUTHENTICATION.md # exact per-role login commands; contains no secrets
91
- room/WORKSPACE_PROTOCOL.md # the default protocol every seat carries, as one readable file
92
+ room/WORKSPACE_PROTOCOL.md # the default protocol Lead and Supervisor carry, as one readable file
92
93
  plugin/ # Claude only: trusted creation-time system-prompt append carrier
93
94
  paseo-plugin.json # accepts Paseo >=0.8.0 <0.9.0
94
95
  index.server.ts, server/ # exact provider map + generated role contracts
@@ -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
@@ -348,7 +357,8 @@ Supervisor reads the exact current `room-<agent>-lead` profile from `list_profil
348
357
  materializes every field it defines (agent creation takes no profile id), uses `list_agents(cwd)`
349
358
  only to find candidates, then inspects each one's full status for the profile's provider, the
350
359
  intended workspace and its mode. For `room-<agent>-peer`, Lead copies provider, mode and
351
- features exactly, treats model and thinking as protocol-governed defaults, and additionally
360
+ features exactly, keeps the profile's model unless the root workspace protocol routes models,
361
+ chooses the thinking effort for the brief, and additionally
352
362
  requires the live seat's daemon-added `paseo.parent-agent-id` to name itself. Ownership must be
353
363
  corroborated by parentage or known Human-opened history; an ambiguous candidate goes to
354
364
  duplicate recovery and Human escalation instead of being adopted.
@@ -449,10 +459,12 @@ The exact model-facing wording every seat reads lives in the canonical Markdown
449
459
  that settles it rather than pre-solving the work; any plan or file list in it is provisional.
450
460
  One moving write scope has exactly one owner, and at most one Peer is writable at a time.
451
461
  Room tools: on.
452
- - **Peer** owns one bounded outcome, forms its own technical position from the code and its own
453
- verification, may challenge a failed premise with
454
- `REOPEN_REQUEST` / `DEPENDENCY_REQUEST` / `BLOCKED`, hands back a reproducible candidate,
455
- and never accepts its own difficult change. Room tools: off.
462
+ - **Peer** owns one bounded assignment — writable inside an assigned scope, or read-only
463
+ against a named candidate, question or area — under exactly one disposition Lead names in the
464
+ brief (Engineer, Architect, Reviewer or Scout), forms its own technical position from the code
465
+ and its own verification, may challenge a failed premise with
466
+ `REOPEN_REQUEST` / `DEPENDENCY_REQUEST` / `BLOCKED`, hands back reproducible evidence either
467
+ way, and never accepts its own difficult change. Room tools: off.
456
468
 
457
469
  Human keeps product goals, priority, material cost, external effects and irreversible risk.
458
470
 
@@ -473,18 +485,24 @@ Two further limits are deliberately conservative. **One writable Peer per projec
473
485
  per moving scope: the room gives you no writer isolation, so separate scopes are not evidence
474
486
  of separate working trees, and no workspace protocol relaxes the limit. Concurrent writable
475
487
  Peers in isolated worktrees are a deferred decision, not an oversight. And a seat's **model and
476
- reasoning effort are defaults**: Lead varies them for a brief only where the repository's
477
- workspace protocol supplies an explicit task-risk policy, and never up to a tier advertising
478
- automatic delegation. Provider, mode, workspace, parent and feature values are eligibility
479
- evidence and copied exactly.
480
-
481
- Every seat also carries a **default workspace protocol** — topology by difficulty,
482
- verification, review, repository conventions — so a project has that layer without doing
483
- anything. Each seat gets the sections that bear on its own work; topology goes to Lead and
484
- Supervisor, not to Peer. A repository that needs different rules writes
485
- `docs/WORKSPACE_PROTOCOL.md`, which wins wherever it speaks while the default holds
486
- wherever it is silent. `~/.paseo-room/room/WORKSPACE_PROTOCOL.md` is the whole default as
487
- one file, so you can read what is in force and start from it.
488
+ reasoning effort are not one knob**: the model stays the profile's default unless the root
489
+ `WORKSPACE_PROTOCOL.md` explicitly supplies model routing, while the thinking effort is Lead's
490
+ per-brief choice on task risk, uncertainty, context size and verification burden — lowest that
491
+ reliably answers the task, higher for architecture-sensitive or weakly observable work, only an
492
+ option the live Paseo and provider context establishes as supported, and never up to a tier
493
+ advertising automatic delegation. Provider, mode, workspace, parent and feature values are
494
+ eligibility evidence and copied exactly.
495
+
496
+ Every seat that owns workflow also carries a **default workspace protocol** — topology by
497
+ difficulty, verification, review, repository conventions — so a project has that layer without
498
+ doing anything. It goes to Lead and Supervisor, not to Peer: Lead reads the protocol and quotes
499
+ what bears on an assignment into the brief, including the exact verification command, so a Peer
500
+ spends its attention on one brief rather than on deciding which repository rules apply. A
501
+ repository that needs different rules writes `WORKSPACE_PROTOCOL.md` at its root, which wins
502
+ wherever it speaks while the default holds wherever it is silent — the root, because it is
503
+ agent guidance rather than project documentation. `~/.paseo-room/room/WORKSPACE_PROTOCOL.md` is
504
+ the whole default as one file, so you can read what is in force and start from it. The CLI
505
+ never writes into a repository.
488
506
 
489
507
  ## Working the room
490
508
 
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.2.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
  });
@@ -799,7 +818,7 @@ var init_prompts = __esm({
799
818
  },
800
819
  contract: {
801
820
  humanAuthority: { path: "contract/shared/human-authority.md", kind: "section" },
802
- workspaceProtocolPrecedence: { path: "contract/shared/workspace-protocol-precedence.md", kind: "section" },
821
+ workspaceProtocolPrecedence: { path: "contract/shared-supervisor-lead/workspace-protocol-precedence.md", kind: "section" },
803
822
  evidenceAndEventWaiting: { path: "contract/shared/evidence-and-event-waiting.md", kind: "section" },
804
823
  scopeAndUnrelatedWork: { path: "contract/shared/scope-and-unrelated-work.md", kind: "section" },
805
824
  directiveIntegrity: { path: "contract/supervisor/directive-integrity.md", kind: "section" },
@@ -816,7 +835,7 @@ var init_prompts = __esm({
816
835
  peerSeatLifecycle: { path: "contract/lead/peer-seat-lifecycle.md", kind: "section" },
817
836
  boundedOutcome: { path: "contract/peer/bounded-outcome.md", kind: "section" },
818
837
  independentJudgment: { path: "contract/peer/independent-judgment.md", kind: "section" },
819
- writingAndReviewScope: { path: "contract/peer/writing-and-review-scope.md", kind: "section" },
838
+ assignmentScope: { path: "contract/peer/assignment-scope.md", kind: "section" },
820
839
  noOrchestration: { path: "contract/peer/no-orchestration.md", kind: "section" },
821
840
  reproducibleHandoff: { path: "contract/peer/reproducible-handoff.md", kind: "section" },
822
841
  noSelfAcceptance: { path: "contract/peer/no-self-acceptance.md", kind: "section" }
@@ -849,12 +868,13 @@ var init_prompts = __esm({
849
868
  // src/room/instructions.ts
850
869
  import { createHash } from "crypto";
851
870
  function instructionKeys(role) {
852
- return [...SHARED_KEYS, ...ROLE_KEYS[role]];
871
+ return [...SHARED_KEYS[role], ...ROLE_KEYS[role]];
853
872
  }
854
873
  function protocolKeys(role) {
855
874
  return PROTOCOL_KEYS[role];
856
875
  }
857
876
  function protocol(keys) {
877
+ if (keys.length === 0) return [];
858
878
  return [
859
879
  loadPromptAsset("documents", "workspace"),
860
880
  ...keys.map((key) => loadPromptAsset("workspace", key))
@@ -873,18 +893,28 @@ function contractDigest() {
873
893
  for (const kind of INSTRUCTION_KINDS) hash.update(`${kind}\0${renderInstructions(kind)}\0`);
874
894
  return `sha256:${hash.digest("hex").slice(0, 16)}`;
875
895
  }
876
- var SHARED_KEYS, ROLE_KEYS, ALL_PROTOCOL_KEYS, PROTOCOL_KEYS, INSTRUCTION_KINDS;
896
+ var AUTHORITY_KEYS, PROTOCOL_READER_KEYS, SHARED_KEYS, ROLE_KEYS, ALL_PROTOCOL_KEYS, PROTOCOL_KEYS, INSTRUCTION_KINDS;
877
897
  var init_instructions = __esm({
878
898
  "src/room/instructions.ts"() {
879
899
  "use strict";
880
900
  init_roles();
881
901
  init_prompts();
882
- SHARED_KEYS = [
902
+ AUTHORITY_KEYS = [
903
+ "humanAuthority",
904
+ "evidenceAndEventWaiting",
905
+ "scopeAndUnrelatedWork"
906
+ ];
907
+ PROTOCOL_READER_KEYS = [
883
908
  "humanAuthority",
884
909
  "workspaceProtocolPrecedence",
885
910
  "evidenceAndEventWaiting",
886
911
  "scopeAndUnrelatedWork"
887
912
  ];
913
+ SHARED_KEYS = {
914
+ supervisor: PROTOCOL_READER_KEYS,
915
+ lead: PROTOCOL_READER_KEYS,
916
+ peer: AUTHORITY_KEYS
917
+ };
888
918
  ROLE_KEYS = {
889
919
  supervisor: [
890
920
  "directiveIntegrity",
@@ -907,7 +937,7 @@ var init_instructions = __esm({
907
937
  "challengeSignals",
908
938
  "boundedOutcome",
909
939
  "independentJudgment",
910
- "writingAndReviewScope",
940
+ "assignmentScope",
911
941
  "noOrchestration",
912
942
  "reproducibleHandoff",
913
943
  "noSelfAcceptance"
@@ -922,7 +952,7 @@ var init_instructions = __esm({
922
952
  PROTOCOL_KEYS = {
923
953
  supervisor: ALL_PROTOCOL_KEYS,
924
954
  lead: ALL_PROTOCOL_KEYS,
925
- peer: ["verification", "review", "repositoryConventions"]
955
+ peer: []
926
956
  };
927
957
  INSTRUCTION_KINDS = [...ROLES, "workspace"];
928
958
  }
@@ -1087,8 +1117,10 @@ function renderRoleState(source) {
1087
1117
  for (const key of SEEDED_KEYS) if (key in root) state[key] = root[key];
1088
1118
  return JSON.stringify(state, null, 2) + "\n";
1089
1119
  }
1090
- function renderRoleMemory(source, role) {
1120
+ function renderRoleMemory(source, role, contract = true) {
1091
1121
  const operator = source?.trim();
1122
+ if (!contract) return operator ? `${operator}
1123
+ ` : void 0;
1092
1124
  return operator ? `${operator}
1093
1125
 
1094
1126
  ${renderInstructions(role)}` : renderInstructions(role);
@@ -1208,7 +1240,7 @@ var init_claude = __esm({
1208
1240
  "CronList"
1209
1241
  ]
1210
1242
  },
1211
- async build(layout, roles) {
1243
+ async build(layout, roles, options = {}) {
1212
1244
  const home = layout.agentHome.claude;
1213
1245
  const binary = await which(layout.bin.claude, layout.searchPath);
1214
1246
  if (!binary) {
@@ -1261,7 +1293,9 @@ var init_claude = __esm({
1261
1293
  if (divergence) drift.push(mcpDriftCheck(role, target, statePath, divergence));
1262
1294
  }
1263
1295
  entries.push({ kind: "dir", path: target });
1264
- 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 });
1265
1299
  entries.push({ kind: "file", path: join7(target, "settings.json"), content: renderRoleSettings(settingsSource, role, target) });
1266
1300
  entries.push({ kind: "file", path: statePath, content: renderRoleState(stateSource), once: true });
1267
1301
  entries.push(...await roleResourceEntries({ role, target, home, names: SHARED, shared, executable: EXECUTABLE }));
@@ -2146,7 +2180,7 @@ function roleProfiles(agent, roles) {
2146
2180
  notes: ROLE_NOTES[role]
2147
2181
  }));
2148
2182
  }
2149
- async function buildDesired(layout, agents, roles) {
2183
+ async function buildDesired(layout, agents, roles, build = {}) {
2150
2184
  const entries = [
2151
2185
  { kind: "dir", path: layout.roomHome },
2152
2186
  { kind: "dir", path: sharedRoom(layout) },
@@ -2158,7 +2192,7 @@ async function buildDesired(layout, agents, roles) {
2158
2192
  const binaries = {};
2159
2193
  for (const id of agents) {
2160
2194
  const agent = AGENTS[id];
2161
- const plan = await agent.build(layout, roles);
2195
+ const plan = await agent.build(layout, roles, build);
2162
2196
  entries.push(...plan.entries);
2163
2197
  checks.push(...plan.checks);
2164
2198
  const credentials = plan.credentials ?? [];
@@ -2195,7 +2229,7 @@ async function buildDesired(layout, agents, roles) {
2195
2229
  }
2196
2230
  const pluginPath = agents.includes("claude") ? claudeCarrierPluginDir(layout) : void 0;
2197
2231
  if (pluginPath !== void 0) entries.push(...claudeCarrierEntries(layout, roles));
2198
- 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)) });
2199
2233
  return { entries, providers, profiles, checks, ...pluginPath === void 0 ? {} : { pluginPath } };
2200
2234
  }
2201
2235
  function contractProvenance(previous) {
@@ -2221,6 +2255,14 @@ function thinkingDiagnostics(live, desired) {
2221
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).`
2222
2256
  )];
2223
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
+ }
2224
2266
  function fileDriftDetail(targets) {
2225
2267
  const catalogs = targets.filter((target) => target.endsWith(`${sep2}model-catalog.json`));
2226
2268
  if (catalogs.length === 0) return "";
@@ -2383,9 +2425,21 @@ async function reconcilePlugin(session, desired, plan, pending, expectedPath) {
2383
2425
  }
2384
2426
  return [pluginRuntimeCheck(plugin, expectedPath)];
2385
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
+ }
2386
2439
  async function setup(options = {}) {
2387
2440
  const layout = resolveLayout(options, options.env);
2388
2441
  const agents = options.agents ?? ["codex"];
2442
+ const memoryContract = options.claudeMemoryContract ?? true;
2389
2443
  const invalid = layoutChecks(layout, agents);
2390
2444
  if (invalid.length > 0) return failed("setup", invalid);
2391
2445
  const rootSafety = await roomPathSafety(layout, [], []);
@@ -2397,7 +2451,7 @@ async function setup(options = {}) {
2397
2451
  const compatibilityAgents = [.../* @__PURE__ */ new Set([...agents, ...previous?.agents ?? []])];
2398
2452
  const daemon = await checkDaemon(layout, options.env, minimumPaseoVersion(compatibilityAgents));
2399
2453
  if (!daemon.daemon) return failed("setup", daemon.checks);
2400
- const desired = await buildDesired(layout, agents, ROLES);
2454
+ const desired = await buildDesired(layout, agents, ROLES, { memoryContract });
2401
2455
  const stale = await staleFrom(layout, previous, agents, ROLES);
2402
2456
  const retained = stale.retainedDirectories.length === 0 ? [] : [warn(
2403
2457
  "room.stale-role-homes-preserved",
@@ -2412,7 +2466,7 @@ async function setup(options = {}) {
2412
2466
  const { operations, liveProfiles } = plan;
2413
2467
  const pending = operations.filter((operation) => operation.action !== "noop");
2414
2468
  const pluginPreconditions = desired.pluginPath === void 0 ? plan.pluginChecks : [pluginsEnabledCheck(await session.pluginsEnabled()), ...plan.pluginChecks];
2415
- const preApplyDiagnostics = [...checks, ...pluginPreconditions, ...contractProvenance(previous), ...thinkingDiagnostics(liveProfiles, desired.profiles)];
2469
+ const preApplyDiagnostics = [...checks, ...pluginPreconditions, ...memoryContractDiagnostic(agents, memoryContract), ...contractProvenance(previous), ...thinkingDiagnostics(liveProfiles, desired.profiles)];
2416
2470
  if (hasFailure(preApplyDiagnostics) || !options.apply) {
2417
2471
  return {
2418
2472
  command: "setup",
@@ -2440,13 +2494,15 @@ async function setup(options = {}) {
2440
2494
  version: package_default.version,
2441
2495
  agents: [...agents],
2442
2496
  roles: [...ROLES],
2443
- contract: contractDigest()
2497
+ contract: contractDigest(),
2498
+ ...markerMemoryContract(agents, memoryContract) === false ? { claudeMemoryContract: false } : {}
2444
2499
  };
2445
2500
  const appliedChecks = [
2446
2501
  ...checks,
2447
2502
  ...pluginPreconditions,
2448
2503
  ...pluginChecks,
2449
2504
  pass("room.applied", `Room ready at ${layout.roomHome} with ${String(ids.length)} Paseo providers.`),
2505
+ ...memoryContractDiagnostic(agents, memoryContract),
2450
2506
  ...contractProvenance(appliedMarker),
2451
2507
  ...thinkingDiagnostics(appliedProfiles, desired.profiles)
2452
2508
  ];
@@ -2473,7 +2529,10 @@ async function verify(options = {}) {
2473
2529
  if (hasFailure(pathSafety)) return failed("verify", pathSafety);
2474
2530
  const daemon = await checkDaemon(layout, options.env, minimumPaseoVersion(marker.agents));
2475
2531
  if (!daemon.daemon) return failed("verify", daemon.checks);
2476
- 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
+ });
2477
2536
  const checks = [...daemon.checks, ...desired.checks];
2478
2537
  if (hasFailure(checks)) return failed("verify", checks);
2479
2538
  return withSession(daemon.daemon, async (session) => {
@@ -2493,11 +2552,12 @@ async function verify(options = {}) {
2493
2552
  ];
2494
2553
  const all = [
2495
2554
  ...checks,
2496
- 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"),
2497
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"),
2498
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"),
2499
2558
  ...pluginChecks,
2500
2559
  // Diagnostics last: a warning never precedes the failure it might be mistaken for.
2560
+ ...memoryContractDiagnostic(marker.agents, marker.claudeMemoryContract ?? true),
2501
2561
  ...contractProvenance(marker),
2502
2562
  ...thinkingDiagnostics(liveProfiles, desired.profiles)
2503
2563
  ];
@@ -2647,12 +2707,25 @@ async function runWizard(prompts, emit, write, options = {}, onAction) {
2647
2707
  options: AGENT_IDS.map((value) => ({ value, label: AGENTS[value].label }))
2648
2708
  });
2649
2709
  if (typeof agents === "symbol" || agents.length === 0) return cancelled();
2650
- 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);
2651
2724
  const status = emit(preview);
2652
2725
  if (preview.outcome !== "changes-planned") return status;
2653
2726
  const approved = await prompts.confirm({ message: "Apply these changes? Authentication will not be validated or started." });
2654
2727
  if (approved !== true) return cancelled();
2655
- return emit(await setup({ ...options, agents, apply: true }));
2728
+ return emit(await setup({ ...setupOptions, apply: true }));
2656
2729
  }
2657
2730
  var terminalPrompts;
2658
2731
  var init_wizard = __esm({
@@ -2718,7 +2791,14 @@ function optionsFrom(raw, base) {
2718
2791
  if (typeof value === "string" && value.trim()) paths[flag] = value.trim();
2719
2792
  }
2720
2793
  const agents = Array.isArray(raw.agent) && raw.agent.length > 0 ? raw.agent : void 0;
2721
- 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
+ };
2722
2802
  }
2723
2803
  function failedFromThrown(command, error) {
2724
2804
  if (error instanceof PromptAssetError) {
@@ -2768,7 +2848,7 @@ async function runCli(argv, output, context = {}) {
2768
2848
  return emit(failedFromThrown(wizardCommand, error));
2769
2849
  }
2770
2850
  }
2771
- 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) => {
2772
2852
  output.stdout(text);
2773
2853
  }, writeErr: (text) => {
2774
2854
  output.stderr(text);
@@ -2789,7 +2869,16 @@ async function runCli(argv, output, context = {}) {
2789
2869
  return 2;
2790
2870
  }
2791
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
+ }
2792
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
+ }
2793
2882
  if (raw.apply === true) {
2794
2883
  output.stderr("paseo-room auth login is already explicit and does not use --apply.\n");
2795
2884
  return 2;