paseo-room 0.5.0 → 0.6.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.
Files changed (54) hide show
  1. package/README.md +56 -3
  2. package/dist/index.js +1745 -197
  3. package/dist/index.js.map +1 -1
  4. package/dist/plugin-assets/paseo-plugin.json +1 -1
  5. package/dist/prompts/contract/peer.md +12 -0
  6. package/dist/runtime-plugin/client/data.ts +67 -0
  7. package/dist/runtime-plugin/client/views.tsx +176 -0
  8. package/dist/runtime-plugin/index.client.tsx +21 -0
  9. package/dist/runtime-plugin/index.server.ts +53 -0
  10. package/dist/runtime-plugin/package.json +5 -0
  11. package/dist/runtime-plugin/paseo-plugin.json +4 -0
  12. package/dist/runtime-plugin/server/bridge/bridge.mjs +146 -0
  13. package/dist/runtime-plugin/server/brief.ts +39 -0
  14. package/dist/runtime-plugin/server/capabilities.ts +45 -0
  15. package/dist/runtime-plugin/server/context.ts +80 -0
  16. package/dist/runtime-plugin/server/contracts/actions.ts +39 -0
  17. package/dist/runtime-plugin/server/contracts/assignment.ts +51 -0
  18. package/dist/runtime-plugin/server/contracts/envelope.ts +26 -0
  19. package/dist/runtime-plugin/server/contracts/peer.ts +112 -0
  20. package/dist/runtime-plugin/server/controller.ts +558 -0
  21. package/dist/runtime-plugin/server/correlations.ts +157 -0
  22. package/dist/runtime-plugin/server/domain/acceptance.ts +69 -0
  23. package/dist/runtime-plugin/server/domain/authorize.ts +37 -0
  24. package/dist/runtime-plugin/server/domain/receipts.ts +73 -0
  25. package/dist/runtime-plugin/server/domain/state.ts +413 -0
  26. package/dist/runtime-plugin/server/domain/validate.ts +69 -0
  27. package/dist/runtime-plugin/server/domain/views.ts +233 -0
  28. package/dist/runtime-plugin/server/events/schema.ts +214 -0
  29. package/dist/runtime-plugin/server/gate.ts +233 -0
  30. package/dist/runtime-plugin/server/generated/location.ts +11 -0
  31. package/dist/runtime-plugin/server/git.ts +158 -0
  32. package/dist/runtime-plugin/server/handlers/actions.ts +130 -0
  33. package/dist/runtime-plugin/server/handlers/peer.ts +183 -0
  34. package/dist/runtime-plugin/server/handlers/turns.ts +113 -0
  35. package/dist/runtime-plugin/server/hooks.ts +73 -0
  36. package/dist/runtime-plugin/server/lifecycle.ts +60 -0
  37. package/dist/runtime-plugin/server/notices.ts +103 -0
  38. package/dist/runtime-plugin/server/ownership.ts +63 -0
  39. package/dist/runtime-plugin/server/paseo-port.ts +194 -0
  40. package/dist/runtime-plugin/server/recognition.ts +80 -0
  41. package/dist/runtime-plugin/server/recovery.ts +166 -0
  42. package/dist/runtime-plugin/server/rpc.ts +169 -0
  43. package/dist/runtime-plugin/server/spool.ts +143 -0
  44. package/dist/runtime-plugin/server/store/project.ts +228 -0
  45. package/dist/runtime-plugin/server/store/publish.ts +142 -0
  46. package/dist/runtime-plugin/server/tools.ts +91 -0
  47. package/dist/runtime-plugin/shared/identity.ts +16 -0
  48. package/dist/runtime-plugin/shared/limits.ts +40 -0
  49. package/dist/runtime-plugin/shared/manifest.ts +51 -0
  50. package/dist/runtime-plugin/shared/policy.ts +61 -0
  51. package/dist/runtime-plugin/shared/rpc-contracts.ts +66 -0
  52. package/dist/runtime-plugin/shared/rpc.ts +34 -0
  53. package/dist/runtime-plugin/tsconfig.json +22 -0
  54. package/package.json +9 -4
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.5.0",
18
+ version: "0.6.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",
@@ -45,17 +45,17 @@ var init_package = __esm({
45
45
  "dist/"
46
46
  ],
47
47
  scripts: {
48
- typecheck: "tsc --noEmit",
48
+ typecheck: "tsc --noEmit && tsc -p src/runtime-plugin",
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'; for (const [source, target] of [['src/room/prompts', 'dist/prompts'], ['src/room/skills', 'dist/skills'], ['src/plugin-assets', 'dist/plugin-assets']]) { rmSync(target, { recursive: true, force: true }); cpSync(source, target, { 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/room/skills', 'dist/skills'], ['src/plugin-assets', 'dist/plugin-assets'], ['src/runtime-plugin', 'dist/runtime-plugin']]) { 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
  },
56
56
  dependencies: {
57
57
  "@clack/prompts": "^1.8.0",
58
- "@getpaseo/client": "0.8.0-beta.1",
58
+ "@getpaseo/client": "0.9.1",
59
59
  commander: "^14.0.3",
60
60
  semver: "^7.8.5",
61
61
  "smol-toml": "^1.8.0",
@@ -63,9 +63,14 @@ var init_package = __esm({
63
63
  },
64
64
  devDependencies: {
65
65
  "@eslint/js": "^10.0.1",
66
+ "@getpaseo/plugin": "0.9.1",
67
+ "@getpaseo/protocol": "0.9.1",
66
68
  "@types/node": "^22.0.0",
69
+ "@types/react": "19.2.2",
67
70
  "@types/semver": "^7.7.0",
68
71
  eslint: "^10.10.0",
72
+ react: "19.1.0",
73
+ "react-native": "0.81.5",
69
74
  tsup: "^8.5.1",
70
75
  typescript: "~5.9.3",
71
76
  "typescript-eslint": "^8.70.0",
@@ -76,14 +81,14 @@ var init_package = __esm({
76
81
  });
77
82
 
78
83
  // src/result.ts
79
- function pass(id, message) {
80
- return { id, status: "pass", message };
84
+ function pass(id2, message) {
85
+ return { id: id2, status: "pass", message };
81
86
  }
82
- function warn(id, message, fix) {
83
- return { id, status: "warn", message, ...fix === void 0 ? {} : { fix } };
87
+ function warn(id2, message, fix) {
88
+ return { id: id2, status: "warn", message, ...fix === void 0 ? {} : { fix } };
84
89
  }
85
- function fail(id, message, fix) {
86
- return { id, status: "fail", message, fix };
90
+ function fail(id2, message, fix) {
91
+ return { id: id2, status: "fail", message, fix };
87
92
  }
88
93
  function failed(command, checks) {
89
94
  return { command, outcome: "failed", changed: false, checks, operations: [] };
@@ -158,9 +163,9 @@ function preservedCredentialCheck(input) {
158
163
  function presentNames(names, supported) {
159
164
  return supported.filter((name) => names.has(name));
160
165
  }
161
- function ambientNamesCheck(id, agent, role, names) {
166
+ function ambientNamesCheck(id2, agent, role, names) {
162
167
  return warn(
163
- `${id}.ambient-auth-unverifiable`,
168
+ `${id2}.ambient-auth-unverifiable`,
164
169
  `${agent} ${role} auth: ambient auth unverifiable; ${names.join(", ")} ${names.length === 1 ? "is" : "are"} present by name in the setup process. paseo-room does not copy credential values into providers, so availability to the Paseo-launched role, token validity, and freshness were not checked.`
165
170
  );
166
171
  }
@@ -214,13 +219,13 @@ function layoutChecks(layout, agents) {
214
219
  "Point --room-home at a dedicated directory, by default ~/.paseo-room."
215
220
  ));
216
221
  }
217
- for (const id of agents) {
218
- const home = layout.agentHome[id];
222
+ for (const id2 of agents) {
223
+ const home = layout.agentHome[id2];
219
224
  if (contains(layout.roomHome, home) || contains(home, layout.roomHome)) {
220
225
  checks.push(fail(
221
- `${id}.home`,
222
- `The ${id} home ${home} overlaps the room home ${layout.roomHome}.`,
223
- `Pass --${id}-home pointing at your own ${id} configuration (this happens when setup runs inside a room seat).`
226
+ `${id2}.home`,
227
+ `The ${id2} home ${home} overlaps the room home ${layout.roomHome}.`,
228
+ `Pass --${id2}-home pointing at your own ${id2} configuration (this happens when setup runs inside a room seat).`
224
229
  ));
225
230
  }
226
231
  }
@@ -444,13 +449,14 @@ function providerLabel(agent, role) {
444
449
  const title2 = (value) => value.charAt(0).toUpperCase() + value.slice(1);
445
450
  return `${title2(agent)} ${title2(role)}`;
446
451
  }
447
- var ROLES, AGENT_IDS, ROLE_PASEO_TOOLS, ROLE_THINKING, DELEGATING_THINKING, ROLE_ICON, ROLE_COLOR, ROLE_NOTES;
452
+ var ROLES, AGENT_IDS, ROLE_PASEO_TOOLS, ROLE_PEER_REPORTING, ROLE_THINKING, DELEGATING_THINKING, ROLE_ICON, ROLE_COLOR, ROLE_NOTES;
448
453
  var init_roles = __esm({
449
454
  "src/roles.ts"() {
450
455
  "use strict";
451
456
  ROLES = ["supervisor", "lead", "peer"];
452
457
  AGENT_IDS = ["codex", "claude", "pi"];
453
458
  ROLE_PASEO_TOOLS = { supervisor: true, lead: true, peer: false };
459
+ ROLE_PEER_REPORTING = { supervisor: false, lead: false, peer: true };
454
460
  ROLE_THINKING = { supervisor: "low", lead: "high", peer: "high" };
455
461
  DELEGATING_THINKING = ["ultra", "ultracode"];
456
462
  ROLE_ICON = { supervisor: "eye", lead: "compass", peer: "code" };
@@ -466,14 +472,15 @@ var init_roles = __esm({
466
472
  // src/room.ts
467
473
  import { join as join3 } from "path";
468
474
  import { z } from "zod";
469
- function renderMarker(version, agents, roles, contract, claudeMemoryContract) {
475
+ function renderMarker(version, agents, roles, contract, claudeMemoryContract, runtime) {
470
476
  return JSON.stringify({
471
477
  version,
472
478
  agents: [...agents],
473
479
  roles: [...roles],
474
480
  ...contract === void 0 ? {} : { contract },
475
481
  // Written only when it differs from the default, so an unchanged room's marker is unchanged.
476
- ...claudeMemoryContract === false ? { claudeMemoryContract: false } : {}
482
+ ...claudeMemoryContract === false ? { claudeMemoryContract: false } : {},
483
+ ...runtime === void 0 ? {} : { runtime }
477
484
  }, null, 2) + "\n";
478
485
  }
479
486
  async function readMarker(layout) {
@@ -507,7 +514,12 @@ var init_room = __esm({
507
514
  * leaving the plugin as its only Claude carrier. Optional and defaulted to true so a room
508
515
  * written before the option existed keeps parsing and keeps its current behaviour.
509
516
  */
510
- claudeMemoryContract: z.boolean().optional()
517
+ claudeMemoryContract: z.boolean().optional(),
518
+ /**
519
+ * Present only when this room opted in to runtime coordination. Missing means disabled, so a
520
+ * marker written before runtime existed keeps parsing and keeps its behaviour.
521
+ */
522
+ runtime: z.strictObject({ enabled: z.literal(true), generation: z.string().min(1), schema: z.literal(1) }).optional()
511
523
  });
512
524
  }
513
525
  });
@@ -677,12 +689,12 @@ async function loginRole(agentValue, roleValue, options, output) {
677
689
  }
678
690
  const childEnv = { ...env, ...invocation.env };
679
691
  const launch = options.spawn ?? spawnLogin;
680
- return new Promise((resolve4) => {
692
+ return new Promise((resolve5) => {
681
693
  let settled = false;
682
- const finish = (code) => {
694
+ const finish = (code2) => {
683
695
  if (settled) return;
684
696
  settled = true;
685
- resolve4(code);
697
+ resolve5(code2);
686
698
  };
687
699
  let child;
688
700
  try {
@@ -704,9 +716,9 @@ async function loginRole(agentValue, roleValue, options, output) {
704
716
  `);
705
717
  finish(1);
706
718
  });
707
- child.once("close", (code, signal) => {
708
- if (code !== null) {
709
- finish(code);
719
+ child.once("close", (code2, signal) => {
720
+ if (code2 !== null) {
721
+ finish(code2);
710
722
  return;
711
723
  }
712
724
  if (signal) {
@@ -962,11 +974,11 @@ function detectPaseoServers(servers) {
962
974
  function evidence(match) {
963
975
  return match.field === "name" ? `${match.server} (server name)` : `${match.server} (${match.field}: ${match.value})`;
964
976
  }
965
- function paseoMcpCheck(id, path, servers) {
977
+ function paseoMcpCheck(id2, path, servers) {
966
978
  const matches = detectPaseoServers(servers);
967
979
  if (matches.length === 0) return void 0;
968
980
  return fail(
969
- id,
981
+ id2,
970
982
  `${path} declares MCP servers that are recognizably Paseo-related: ${matches.map(evidence).join("; ")}.`,
971
983
  `Remove or rename those servers in ${path} so no room seat reaches Paseo outside its own seat, then run setup again. paseo-room never edits your MCP configuration, and this name check is a bounded heuristic rather than a sandbox.`
972
984
  );
@@ -1202,22 +1214,22 @@ async function claudeCredentialDiagnostic(layout, role, platform = process.platf
1202
1214
  const login = roleCommand(roleEnvironment, binary, ["auth", "login"]);
1203
1215
  const status = roleCommand(roleEnvironment, binary, ["auth", "status"]);
1204
1216
  const state = await inspectCredentialPath(path);
1205
- const id = `claude.auth.${role}`;
1217
+ const id2 = `claude.auth.${role}`;
1206
1218
  if (state.kind !== "missing") {
1207
- return { path, checks: [preservedCredentialCheck({ id, agent: "Claude", role, path, state, login, status })] };
1219
+ return { path, checks: [preservedCredentialCheck({ id: id2, agent: "Claude", role, path, state, login, status })] };
1208
1220
  }
1209
1221
  const ambient = presentNames(layout.envNames, AUTH_ENV);
1210
- if (ambient.length > 0) return { path, checks: [ambientNamesCheck(id, "Claude", role, ambient)] };
1222
+ if (ambient.length > 0) return { path, checks: [ambientNamesCheck(id2, "Claude", role, ambient)] };
1211
1223
  if (platform === "darwin") {
1212
1224
  return { path, checks: [{
1213
- id: `${id}.native-keyring-unverifiable`,
1225
+ id: `${id2}.native-keyring-unverifiable`,
1214
1226
  status: "warn",
1215
1227
  message: `Claude ${role} auth: native-keyring unverifiable; the current runtime's secure-storage location is pinned to this role home, but no Keychain entry was queried and older runtime behavior was not assumed. Token validity and freshness were not checked.`,
1216
1228
  fix: `Authenticate this role with: ${login}. If that subcommand is unavailable, launch ${roleCommand(roleEnvironment, binary)} and run /login. Check status with: ${status}.`
1217
1229
  }] };
1218
1230
  }
1219
1231
  return { path, checks: [{
1220
- id: `${id}.login-required`,
1232
+ id: `${id2}.login-required`,
1221
1233
  status: "warn",
1222
1234
  message: `Claude ${role} auth: login-required; no role-owned .credentials.json or supported ambient auth environment name was detected. Authentication was not attempted.`,
1223
1235
  fix: `Authenticate this role with: ${login}. If that subcommand is unavailable, launch ${roleCommand(roleEnvironment, binary)} and run /login. Check status with: ${status}.`
@@ -1441,14 +1453,14 @@ async function credentialDiagnostic(layout, role, store, binary) {
1441
1453
  const login = roleCommand({ CODEX_HOME: home }, binary, ["login"]);
1442
1454
  const status = roleCommand({ CODEX_HOME: home }, binary, ["login", "status"]);
1443
1455
  const state = await inspectCredentialPath(path);
1444
- const id = `codex.auth.${role}`;
1456
+ const id2 = `codex.auth.${role}`;
1445
1457
  const checks = [];
1446
1458
  if (state.kind !== "missing") {
1447
- checks.push(preservedCredentialCheck({ id, agent: "Codex", role, path, state, login, status }));
1459
+ checks.push(preservedCredentialCheck({ id: id2, agent: "Codex", role, path, state, login, status }));
1448
1460
  } else {
1449
1461
  if (store === "keyring" || store === "auto") {
1450
1462
  checks.push({
1451
- id: `${id}.native-keyring-unverifiable`,
1463
+ id: `${id2}.native-keyring-unverifiable`,
1452
1464
  status: "warn",
1453
1465
  message: `Codex ${role} auth: native-keyring unverifiable; cli_auth_credentials_store is ${store}, and no keyring was queried. Token validity and freshness were not checked.`,
1454
1466
  fix: `Authenticate this role with: ${login}. Check status with: ${status}.`
@@ -1457,7 +1469,7 @@ async function credentialDiagnostic(layout, role, store, binary) {
1457
1469
  const apiKey = layout.envNames.has("OPENAI_API_KEY") ? " OPENAI_API_KEY is present only in the setup process; Codex has not stored it for this role and paseo-room does not copy it into the provider." : "";
1458
1470
  const detail = store === "ephemeral" ? "cli_auth_credentials_store is ephemeral, so no persistent credential file is expected" : `no role-owned auth.json exists${store === "file" ? " for the configured file store" : ""}`;
1459
1471
  checks.push({
1460
- id: `${id}.login-required`,
1472
+ id: `${id2}.login-required`,
1461
1473
  status: "warn",
1462
1474
  message: `Codex ${role} auth: login-required; ${detail}.${apiKey} Authentication was not attempted.`,
1463
1475
  fix: `Authenticate this role with: ${login}. Check status with: ${status}.`
@@ -1466,7 +1478,7 @@ async function credentialDiagnostic(layout, role, store, binary) {
1466
1478
  }
1467
1479
  if (state.kind !== "missing" && (store === "keyring" || store === "auto")) {
1468
1480
  checks.push({
1469
- id: `${id}.native-keyring-unverifiable`,
1481
+ id: `${id2}.native-keyring-unverifiable`,
1470
1482
  status: "warn",
1471
1483
  message: `Codex ${role} auth: native-keyring unverifiable; cli_auth_credentials_store is ${store}, and the preserved auth.json does not prove what the runtime will use. No keyring was queried.`,
1472
1484
  fix: `Check status without exposing credentials: ${status}.`
@@ -1682,10 +1694,10 @@ function runProbeProcess(executable, adapterEntry, env, options, cwd) {
1682
1694
  clearTimeout(timer);
1683
1695
  resolveResult(result);
1684
1696
  };
1685
- const abort = (reason) => {
1697
+ const abort = (reason3) => {
1686
1698
  if (settled) return;
1687
1699
  terminateProbe(child);
1688
- finish({ ok: false, stdout: Buffer.concat(stdout).toString("utf8"), reason });
1700
+ finish({ ok: false, stdout: Buffer.concat(stdout).toString("utf8"), reason: reason3 });
1689
1701
  };
1690
1702
  const capture = (chunk, keep) => {
1691
1703
  if (settled) return;
@@ -1714,9 +1726,9 @@ function runProbeProcess(executable, adapterEntry, env, options, cwd) {
1714
1726
  child.on("error", () => {
1715
1727
  abort("failure");
1716
1728
  });
1717
- child.on("close", (code) => {
1729
+ child.on("close", (code2) => {
1718
1730
  const output = Buffer.concat(stdout).toString("utf8");
1719
- finish(code === 0 ? { ok: true, stdout: output } : { ok: false, stdout: output, reason: "failure" });
1731
+ finish(code2 === 0 ? { ok: true, stdout: output } : { ok: false, stdout: output, reason: "failure" });
1720
1732
  });
1721
1733
  child.stdin.on("error", () => {
1722
1734
  });
@@ -1805,12 +1817,12 @@ async function piCredentialDiagnostic(layout, role, binary = "pi") {
1805
1817
  const path = join10(home, "auth.json");
1806
1818
  const launch = piLoginCommand(home, binary);
1807
1819
  const state = await inspectCredentialPath(path);
1808
- const id = `pi.auth.${role}`;
1820
+ const id2 = `pi.auth.${role}`;
1809
1821
  if (state.kind !== "missing") {
1810
1822
  return {
1811
1823
  path,
1812
1824
  checks: [preservedCredentialCheck({
1813
- id,
1825
+ id: id2,
1814
1826
  agent: "Pi",
1815
1827
  role,
1816
1828
  path,
@@ -1820,9 +1832,9 @@ async function piCredentialDiagnostic(layout, role, binary = "pi") {
1820
1832
  };
1821
1833
  }
1822
1834
  const configured = presentNames(layout.envNames, AUTH_ENV2);
1823
- if (configured.length > 0) return { path, checks: [ambientNamesCheck(id, "Pi", role, configured)] };
1835
+ if (configured.length > 0) return { path, checks: [ambientNamesCheck(id2, "Pi", role, configured)] };
1824
1836
  return { path, checks: [{
1825
- id: `${id}.login-required`,
1837
+ id: `${id2}.login-required`,
1826
1838
  status: "warn",
1827
1839
  message: `Pi ${role} auth: login-required; no role-owned auth.json or known provider environment name was detected. Ambient provider auth may exist, but it was not inspected or validated.`,
1828
1840
  fix: `Start a minimal, non-room-equivalent interactive login session with: ${launch}. Run /login, then exit Pi.`
@@ -1994,8 +2006,8 @@ var init_pi = __esm({
1994
2006
  // src/paseo.ts
1995
2007
  import { gte, valid } from "semver";
1996
2008
  import { z as z2 } from "zod";
1997
- function minimumPaseoVersion(agents) {
1998
- return agents.includes("pi") || agents.includes("claude") ? PI_MINIMUM_VERSION : MINIMUM_VERSION;
2009
+ function minimumPaseoVersion(agents, runtime = false) {
2010
+ return runtime || agents.includes("pi") || agents.includes("claude") ? PI_MINIMUM_VERSION : MINIMUM_VERSION;
1999
2011
  }
2000
2012
  function normalizeUrl(listen) {
2001
2013
  const match = /^(?:ws:\/\/)?([^:/]+|\[[^\]]+\]):(\d+)$/.exec(listen.trim());
@@ -2004,7 +2016,7 @@ function normalizeUrl(listen) {
2004
2016
  const local = ["localhost", "127.0.0.1", "0.0.0.0", "[::1]", "[::]"].includes(host) ? "127.0.0.1" : host;
2005
2017
  return `ws://${local}:${port}`;
2006
2018
  }
2007
- function assessStatus(raw, minimumVersion = MINIMUM_VERSION) {
2019
+ function assessStatus(raw, minimumVersion = MINIMUM_VERSION, cliVersion) {
2008
2020
  const parsed = statusSchema.safeParse(raw);
2009
2021
  if (!parsed.success) {
2010
2022
  return { checks: [fail("paseo.status", "Paseo status output was not understood.", "Upgrade Paseo, then run: paseo daemon status --json")] };
@@ -2013,8 +2025,8 @@ function assessStatus(raw, minimumVersion = MINIMUM_VERSION) {
2013
2025
  if (status.localDaemon !== "running") {
2014
2026
  return { checks: [fail("paseo.daemon", `Paseo daemon is ${status.localDaemon}.`, "Start it with: paseo daemon start")] };
2015
2027
  }
2016
- const cli = valid(status.cliVersion);
2017
- const daemon = status.daemonVersion === null ? null : valid(status.daemonVersion);
2028
+ const cli = valid(status.cliVersion ?? cliVersion);
2029
+ const daemon = status.daemonVersion == null ? null : valid(status.daemonVersion);
2018
2030
  if (!cli || !daemon) {
2019
2031
  return { checks: [fail("paseo.version", "Paseo did not report a usable version.", "Upgrade Paseo to a release that reports semver versions.")] };
2020
2032
  }
@@ -2044,11 +2056,16 @@ async function checkDaemon(layout, env = process.env, minimumVersion = MINIMUM_V
2044
2056
  if (!output.ok) {
2045
2057
  return { checks: [fail("paseo.status", "Could not read Paseo daemon status.", "Run: paseo daemon status --json")] };
2046
2058
  }
2059
+ let status;
2047
2060
  try {
2048
- return assessStatus(JSON.parse(output.stdout), minimumVersion);
2061
+ status = JSON.parse(output.stdout);
2049
2062
  } catch {
2050
2063
  return assessStatus(void 0, minimumVersion);
2051
2064
  }
2065
+ const reported = status?.cliVersion;
2066
+ if (typeof reported === "string") return assessStatus(status, minimumVersion);
2067
+ const version = await probe(binary, ["--version"], { HOME: layout.home, PATH: layout.searchPath });
2068
+ return assessStatus(status, minimumVersion, version.ok ? version.stdout.trim() : void 0);
2052
2069
  }
2053
2070
  async function withSession(daemon, run2, options = {}) {
2054
2071
  const quiet = { debug() {
@@ -2072,7 +2089,13 @@ async function withSession(daemon, run2, options = {}) {
2072
2089
  import("@getpaseo/client"),
2073
2090
  import("@getpaseo/client/internal/daemon-client")
2074
2091
  ]);
2075
- const raw = new DaemonClient({ ...clientConfig, clientId: `paseo-room-${String(process.pid)}`, clientType: "cli" });
2092
+ const { capabilities, ...config } = clientConfig;
2093
+ const raw = new DaemonClient({
2094
+ ...config,
2095
+ ...capabilities === void 0 ? {} : { capabilities },
2096
+ clientId: `paseo-room-${String(process.pid)}`,
2097
+ clientType: "cli"
2098
+ });
2076
2099
  client = {
2077
2100
  ...createPaseoApi(raw),
2078
2101
  connect: () => raw.connect(),
@@ -2082,10 +2105,10 @@ async function withSession(daemon, run2, options = {}) {
2082
2105
  },
2083
2106
  getConnectionState: () => raw.getConnectionState(),
2084
2107
  listPlugins: () => raw.listPlugins(),
2085
- installDirectoryPlugin: (path, id) => raw.installDirectoryPlugin(path, id),
2086
- reloadPlugin: (id) => raw.reloadPlugin(id),
2087
- enablePlugin: (id) => raw.enablePlugin(id),
2088
- removePlugin: (id) => raw.removePlugin(id)
2108
+ installDirectoryPlugin: (path, id2) => raw.installDirectoryPlugin(path, id2),
2109
+ reloadPlugin: (id2) => raw.reloadPlugin(id2),
2110
+ enablePlugin: (id2) => raw.enablePlugin(id2),
2111
+ removePlugin: (id2) => raw.removePlugin(id2)
2089
2112
  };
2090
2113
  }
2091
2114
  try {
@@ -2115,17 +2138,17 @@ async function withSession(daemon, run2, options = {}) {
2115
2138
  async listPlugins() {
2116
2139
  return await client.listPlugins();
2117
2140
  },
2118
- async installPlugin(path, id) {
2119
- return await client.installDirectoryPlugin(path, id);
2141
+ async installPlugin(path, id2) {
2142
+ return await client.installDirectoryPlugin(path, id2);
2120
2143
  },
2121
- async reloadPlugin(id) {
2122
- return await client.reloadPlugin(id);
2144
+ async reloadPlugin(id2) {
2145
+ return await client.reloadPlugin(id2);
2123
2146
  },
2124
- async enablePlugin(id) {
2125
- return await client.enablePlugin(id);
2147
+ async enablePlugin(id2) {
2148
+ return await client.enablePlugin(id2);
2126
2149
  },
2127
- async removePlugin(id) {
2128
- await client.removePlugin(id);
2150
+ async removePlugin(id2) {
2151
+ await client.removePlugin(id2);
2129
2152
  },
2130
2153
  async refresh(ids) {
2131
2154
  await client.providers.refresh({ providers: [...ids] });
@@ -2185,9 +2208,9 @@ var init_paseo = __esm({
2185
2208
  PI_MINIMUM_VERSION = "0.8.0";
2186
2209
  statusSchema = z2.object({
2187
2210
  listen: z2.string().min(1),
2188
- localDaemon: z2.enum(["running", "stopped", "stale_pid", "unresponsive"]),
2189
- cliVersion: z2.string(),
2190
- daemonVersion: z2.string().nullable()
2211
+ localDaemon: z2.string().min(1),
2212
+ cliVersion: z2.string().optional(),
2213
+ daemonVersion: z2.string().nullish()
2191
2214
  });
2192
2215
  configSchema = z2.object({
2193
2216
  config: z2.object({
@@ -2215,8 +2238,8 @@ function typescriptTemplateLiteral(value) {
2215
2238
  }
2216
2239
  function renderClaudeCarrierContract(roles) {
2217
2240
  const entries = roles.map((role) => {
2218
- const id = JSON.stringify(providerId("claude", role));
2219
- return ` ${id}: ${typescriptTemplateLiteral(renderInstructions(role))},`;
2241
+ const id2 = JSON.stringify(providerId("claude", role));
2242
+ return ` ${id2}: ${typescriptTemplateLiteral(renderInstructions(role))},`;
2220
2243
  });
2221
2244
  return [
2222
2245
  "/** Generated by paseo-room. The Markdown prompt assets remain authoritative. */",
@@ -2236,13 +2259,14 @@ function claudeCarrierEntries(layout, roles) {
2236
2259
  { kind: "file", path: join11(root, "server", "contract.ts"), content: renderClaudeCarrierContract(roles) }
2237
2260
  ];
2238
2261
  }
2239
- var CLAUDE_CARRIER_PLUGIN_ID, STATIC_ASSETS;
2262
+ var CLAUDE_CARRIER_PLUGIN_ID, CLAUDE_CARRIER_PASEO_RANGE, STATIC_ASSETS;
2240
2263
  var init_plugin = __esm({
2241
2264
  "src/plugin.ts"() {
2242
2265
  "use strict";
2243
2266
  init_roles();
2244
2267
  init_instructions();
2245
2268
  CLAUDE_CARRIER_PLUGIN_ID = "paseo-room-claude-carrier";
2269
+ CLAUDE_CARRIER_PASEO_RANGE = ">=0.8.0 <0.10.0";
2246
2270
  STATIC_ASSETS = [
2247
2271
  "paseo-plugin.json",
2248
2272
  "package.json",
@@ -2253,9 +2277,1312 @@ var init_plugin = __esm({
2253
2277
  }
2254
2278
  });
2255
2279
 
2280
+ // src/runtime-plugin/shared/policy.ts
2281
+ function runtimeRolePolicy(role, peerReportingEligible) {
2282
+ if (role === "supervisor") return { capabilities: [...SUPERVISOR_OPERATIONS] };
2283
+ if (role === "lead") return { capabilities: [...LEAD_OPERATIONS] };
2284
+ return peerReportingEligible ? { capabilities: [...PEER_REPORTING_TOOLS], peerReporting: PEER_REPORTING_DECLARATION } : { capabilities: [] };
2285
+ }
2286
+ var RUNTIME_ROLES, RUNTIME_AGENTS, SUPERVISOR_OPERATIONS, LEAD_OPERATIONS, PEER_REPORTING_TOOLS, RUNTIME_CAPABILITIES, PEER_REPORTING_DECLARATION;
2287
+ var init_policy = __esm({
2288
+ "src/runtime-plugin/shared/policy.ts"() {
2289
+ "use strict";
2290
+ RUNTIME_ROLES = ["supervisor", "lead", "peer"];
2291
+ RUNTIME_AGENTS = ["codex", "claude", "pi"];
2292
+ SUPERVISOR_OPERATIONS = ["room_status", "runtime_findings", "message_lead"];
2293
+ LEAD_OPERATIONS = [
2294
+ "assignment_create",
2295
+ "assignment_dispatch",
2296
+ "assignment_answer",
2297
+ "assignment_rework",
2298
+ "assignment_accept",
2299
+ "assignment_reject",
2300
+ "assignment_abandon",
2301
+ "assignment_close",
2302
+ "assignment_status",
2303
+ "gate_run"
2304
+ ];
2305
+ PEER_REPORTING_TOOLS = ["ask", "handoff"];
2306
+ RUNTIME_CAPABILITIES = [
2307
+ ...SUPERVISOR_OPERATIONS,
2308
+ ...LEAD_OPERATIONS,
2309
+ ...PEER_REPORTING_TOOLS
2310
+ ];
2311
+ PEER_REPORTING_DECLARATION = {
2312
+ protocol: 1,
2313
+ tools: ["ask", "handoff"],
2314
+ qualifiedVia: "exact-room-provider"
2315
+ };
2316
+ }
2317
+ });
2318
+
2319
+ // src/runtime-plugin/shared/manifest.ts
2320
+ import { z as z3 } from "zod";
2321
+ var MANIFEST_FILE, generation, peerReportingSchema, providerSchema, providerId2, runtimeRoomManifestSchema;
2322
+ var init_manifest = __esm({
2323
+ "src/runtime-plugin/shared/manifest.ts"() {
2324
+ "use strict";
2325
+ init_policy();
2326
+ MANIFEST_FILE = "generated/room-manifest.json";
2327
+ generation = z3.string().min(1).max(256);
2328
+ peerReportingSchema = z3.strictObject({
2329
+ protocol: z3.literal(1),
2330
+ tools: z3.tuple([z3.literal(PEER_REPORTING_TOOLS[0]), z3.literal(PEER_REPORTING_TOOLS[1])]),
2331
+ qualifiedVia: z3.literal("exact-room-provider")
2332
+ });
2333
+ providerSchema = z3.strictObject({
2334
+ agent: z3.enum(RUNTIME_AGENTS),
2335
+ role: z3.enum(RUNTIME_ROLES),
2336
+ capabilities: z3.array(z3.enum(RUNTIME_CAPABILITIES)),
2337
+ peerReporting: peerReportingSchema.optional()
2338
+ }).superRefine((entry, context) => {
2339
+ const eligible = entry.role === "peer" && entry.peerReporting !== void 0;
2340
+ const expected = runtimeRolePolicy(entry.role, eligible);
2341
+ if (JSON.stringify(entry.capabilities) !== JSON.stringify(expected.capabilities)) {
2342
+ context.addIssue({ code: "custom", message: `capabilities do not match the ${entry.role} runtime policy` });
2343
+ }
2344
+ if (entry.peerReporting !== void 0 && entry.role !== "peer") {
2345
+ context.addIssue({ code: "custom", message: "peerReporting is only valid on a Peer entry" });
2346
+ }
2347
+ });
2348
+ providerId2 = z3.string().regex(/^[a-z0-9][a-z0-9-]*$/).max(128);
2349
+ runtimeRoomManifestSchema = z3.strictObject({
2350
+ schema: z3.literal(1),
2351
+ roomGeneration: generation,
2352
+ contractGeneration: generation,
2353
+ reportingPolicyGeneration: generation,
2354
+ providers: z3.record(providerId2, providerSchema)
2355
+ });
2356
+ }
2357
+ });
2358
+
2359
+ // src/runtime-plugin/shared/identity.ts
2360
+ var RUNTIME_PLUGIN_ID, RUNTIME_PASEO_RANGE;
2361
+ var init_identity = __esm({
2362
+ "src/runtime-plugin/shared/identity.ts"() {
2363
+ "use strict";
2364
+ RUNTIME_PLUGIN_ID = "paseo-room-runtime";
2365
+ RUNTIME_PASEO_RANGE = ">=0.8.0 <0.10.0";
2366
+ }
2367
+ });
2368
+
2369
+ // src/runtime.ts
2370
+ import { createHash as createHash2 } from "crypto";
2371
+ import { readdirSync, readFileSync as readFileSync4 } from "fs";
2372
+ import { join as join12, relative, sep as sep2 } from "path";
2373
+ import { fileURLToPath } from "url";
2374
+ function runtimePluginDir(layout) {
2375
+ return join12(layout.roomHome, "runtime-plugin");
2376
+ }
2377
+ function digest(value) {
2378
+ return `sha256:${createHash2("sha256").update(JSON.stringify(value)).digest("hex").slice(0, 16)}`;
2379
+ }
2380
+ function reportingPolicy() {
2381
+ return {
2382
+ supervisor: runtimeRolePolicy("supervisor", ROLE_PEER_REPORTING.supervisor),
2383
+ lead: runtimeRolePolicy("lead", ROLE_PEER_REPORTING.lead),
2384
+ peer: runtimeRolePolicy("peer", ROLE_PEER_REPORTING.peer)
2385
+ };
2386
+ }
2387
+ function reportingPolicyGeneration() {
2388
+ return digest(reportingPolicy());
2389
+ }
2390
+ function renderRuntimeManifest(agents, roles) {
2391
+ const policy = reportingPolicy();
2392
+ const providers = {};
2393
+ for (const agent of agents) {
2394
+ for (const role of roles) {
2395
+ const { capabilities, peerReporting } = policy[role];
2396
+ providers[providerId(agent, role)] = {
2397
+ agent,
2398
+ role,
2399
+ capabilities: [...capabilities],
2400
+ ...peerReporting === void 0 ? {} : { peerReporting: { ...peerReporting, tools: ["ask", "handoff"] } }
2401
+ };
2402
+ }
2403
+ }
2404
+ const contractGeneration = contractDigest();
2405
+ return runtimeRoomManifestSchema.parse({
2406
+ schema: 1,
2407
+ roomGeneration: digest({ agents: [...agents], roles: [...roles], contractGeneration }),
2408
+ contractGeneration,
2409
+ reportingPolicyGeneration: reportingPolicyGeneration(),
2410
+ providers
2411
+ });
2412
+ }
2413
+ function renderRuntimeManifestFile(agents, roles) {
2414
+ return `${JSON.stringify(renderRuntimeManifest(agents, roles), null, 2)}
2415
+ `;
2416
+ }
2417
+ function renderRuntimeLocation(layout) {
2418
+ const location = { pluginDirectory: runtimePluginDir(layout), runtimeRoot: join12(layout.roomHome, "runtime", "v1") };
2419
+ return [
2420
+ "/** Generated by paseo-room setup for this room. */",
2421
+ "export interface RoomLocation {",
2422
+ " readonly pluginDirectory: string;",
2423
+ " readonly runtimeRoot: string;",
2424
+ "}",
2425
+ "",
2426
+ `export const ROOM_LOCATION: RoomLocation | undefined = ${JSON.stringify(location)};`,
2427
+ ""
2428
+ ].join("\n");
2429
+ }
2430
+ function bundleInventory() {
2431
+ const entries = readdirSync(BUNDLE, { recursive: true, withFileTypes: true });
2432
+ const toRelative = (entry) => relative(BUNDLE, join12(entry.parentPath, entry.name)).split(sep2).join("/");
2433
+ return {
2434
+ dirs: entries.filter((entry) => entry.isDirectory()).map(toRelative).sort(),
2435
+ files: entries.filter((entry) => entry.isFile()).map(toRelative).sort()
2436
+ };
2437
+ }
2438
+ function runtimePluginEntries(layout, agents, roles) {
2439
+ const root = runtimePluginDir(layout);
2440
+ const { dirs, files } = bundleInventory();
2441
+ const generatedDir = MANIFEST_FILE.split("/").slice(0, -1).join("/");
2442
+ return [
2443
+ { kind: "dir", path: root },
2444
+ ...[.../* @__PURE__ */ new Set([...dirs, generatedDir])].sort().map((path) => ({ kind: "dir", path: join12(root, path) })),
2445
+ ...files.map((path) => ({
2446
+ kind: "file",
2447
+ path: join12(root, path),
2448
+ content: path === LOCATION_MODULE ? renderRuntimeLocation(layout) : readFileSync4(join12(BUNDLE, path), "utf8")
2449
+ })),
2450
+ { kind: "file", path: join12(root, MANIFEST_FILE), content: renderRuntimeManifestFile(agents, roles) }
2451
+ ];
2452
+ }
2453
+ var BUNDLE, LOCATION_MODULE;
2454
+ var init_runtime = __esm({
2455
+ "src/runtime.ts"() {
2456
+ "use strict";
2457
+ init_roles();
2458
+ init_instructions();
2459
+ init_manifest();
2460
+ init_identity();
2461
+ init_policy();
2462
+ BUNDLE = fileURLToPath(new URL("./runtime-plugin/", import.meta.url));
2463
+ LOCATION_MODULE = "server/generated/location.ts";
2464
+ }
2465
+ });
2466
+
2467
+ // src/runtime-plugin/server/domain/state.ts
2468
+ function emptyProjectState(projectId) {
2469
+ return { projectId, assignments: /* @__PURE__ */ new Map(), ownership: /* @__PURE__ */ new Map(), notices: /* @__PURE__ */ new Map(), lastSequence: 0 };
2470
+ }
2471
+ function activeWriter(state) {
2472
+ for (const owner of state.ownership.values()) if (owner.state !== "released") return owner;
2473
+ return void 0;
2474
+ }
2475
+ function need(condition, message) {
2476
+ return condition ? void 0 : message;
2477
+ }
2478
+ function assignmentOf(state, event) {
2479
+ return event.assignmentId === void 0 ? void 0 : state.assignments.get(event.assignmentId);
2480
+ }
2481
+ function inState(assignment2, allowed, action) {
2482
+ return need(allowed.includes(assignment2.state), `Cannot ${action} while the assignment is ${assignment2.state}.`);
2483
+ }
2484
+ function checkEvent(state, event) {
2485
+ if (event.projectId !== state.projectId) return "Event belongs to another project.";
2486
+ switch (event.type) {
2487
+ case "project.bound":
2488
+ return need(state.binding === void 0, "The project is already bound.");
2489
+ case "project.rebound":
2490
+ return need(state.binding !== void 0, "Only a bound project can be rebound.");
2491
+ case "project.ownership-conflict":
2492
+ return void 0;
2493
+ case "project.ownership-resolved":
2494
+ return need(state.ownershipConflict !== void 0, "There is no ownership conflict to resolve.");
2495
+ case "notice.pending":
2496
+ return need(!state.notices.has(event.data.noticeId), `Notice ${event.data.noticeId} already exists.`);
2497
+ case "notice.sent":
2498
+ case "notice.failed":
2499
+ case "notice.uncertain":
2500
+ return need(state.notices.has(event.data.noticeId), `Unknown notice ${event.data.noticeId}.`);
2501
+ default:
2502
+ break;
2503
+ }
2504
+ if (event.type === "assignment.created") {
2505
+ if (event.assignmentId === void 0) return "An assignment event needs an assignment id.";
2506
+ return need(!state.assignments.has(event.assignmentId), `Assignment ${event.assignmentId} already exists.`);
2507
+ }
2508
+ const assignment2 = assignmentOf(state, event);
2509
+ if (assignment2 === void 0) return `Event ${event.type} names no known assignment.`;
2510
+ const owner = state.ownership.get(assignment2.id);
2511
+ const writable = assignment2.input.mode === "writable";
2512
+ switch (event.type) {
2513
+ case "assignment.dispatch-requested":
2514
+ return inState(assignment2, ["draft"], "dispatch") ?? need(state.ownershipConflict === void 0, "Dispatch is paused by a Lead ownership conflict.") ?? need(!writable || activeWriter(state) === void 0, "Another writer still owns this project.");
2515
+ case "ownership.reserved":
2516
+ return need(writable, "A read-only assignment reserves no writer ownership.") ?? inState(assignment2, ["dispatching"], "reserve ownership") ?? need(activeWriter(state) === void 0, "Another writer still owns this project.");
2517
+ case "agent.create-requested":
2518
+ return inState(assignment2, ["dispatching"], "create the Peer") ?? need(!writable || owner?.state === "reserved", "A writable Peer is created only after ownership is reserved.") ?? need(assignment2.peerAgentId === void 0, "This assignment already has a Peer.");
2519
+ case "agent.create-succeeded":
2520
+ case "agent.create-failed":
2521
+ case "agent.create-uncertain":
2522
+ return need(assignment2.openIntents[event.data.intentId] === "agent.create-requested" || assignment2.state === "uncertain" && event.type !== "agent.create-uncertain", "No unresolved create intent matches.");
2523
+ case "binding.refused":
2524
+ return inState(assignment2, ["dispatching"], "refuse the Peer binding") ?? need(assignment2.peerAgentId === event.data.agentId, "The refusal names a different agent than the one created.");
2525
+ case "binding.published":
2526
+ return inState(assignment2, ["dispatching"], "bind the Peer") ?? need(assignment2.peerAgentId === event.data.agentId, "The binding names a different agent than the one created.");
2527
+ case "ownership.held":
2528
+ return need(owner?.state === "reserved", "Only reserved ownership becomes held.") ?? need(assignment2.peerAgentId === event.data.agentId && assignment2.observedProviderId !== void 0, "Ownership is held only by the bound Peer.");
2529
+ case "reporting.generation-opened": {
2530
+ const from = {
2531
+ initial: ["dispatching"],
2532
+ answer: ["questioned"],
2533
+ "follow-up": ["blocked"],
2534
+ rework: ["rework"]
2535
+ };
2536
+ return inState(assignment2, from[event.data.turn], `open a ${event.data.turn} reporting generation`) ?? need(event.data.generation === assignment2.reportingGeneration + 1, "Reporting generations increase by exactly one.") ?? need(assignment2.reportingState === "closed" || assignment2.reportingState === "consumed", "The previous reporting generation is not terminal.") ?? need(assignment2.observedProviderId !== void 0, "No proven Peer binding exists.") ?? need(!writable || owner?.state === "held", "A writable Peer needs held ownership before any turn.");
2537
+ }
2538
+ case "run.requested":
2539
+ return need(event.data.generation === assignment2.reportingGeneration && assignment2.reportingState === "open", "A run must use the open reporting generation.") ?? need(assignment2.runGeneration < event.data.generation, "This reporting generation already has its run.") ?? need(!Object.values(assignment2.openIntents).includes("run.requested"), "Another run is still unresolved.");
2540
+ case "run.succeeded":
2541
+ case "run.failed":
2542
+ case "run.uncertain":
2543
+ return need(assignment2.openIntents[event.data.intentId] === "run.requested" || assignment2.state === "uncertain" && event.type !== "run.uncertain", "No unresolved run intent matches.");
2544
+ case "report.accepted":
2545
+ return need(event.data.generation === assignment2.reportingGeneration, "The report names a stale generation.") ?? need(assignment2.reportingState === "open" || assignment2.reportingState === "uncertain", "The reporting generation is not open.") ?? inState(assignment2, ["active", "awaiting-permission", "uncertain"], "accept a report");
2546
+ case "report.refused":
2547
+ return void 0;
2548
+ case "report.missing":
2549
+ return need(event.data.generation === assignment2.reportingGeneration, "Missing report names a stale generation.") ?? need(assignment2.reportingState === "open" || assignment2.reportingState === "uncertain", "The reporting generation is not open.") ?? inState(assignment2, ["active", "awaiting-permission", "uncertain"], "record a missing report");
2550
+ case "report.uncertain":
2551
+ return need(event.data.generation === assignment2.reportingGeneration, "Uncertain report names a stale generation.");
2552
+ case "permission.awaiting":
2553
+ return inState(assignment2, ["active"], "await a reporting permission") ?? need(event.data.generation === assignment2.reportingGeneration && assignment2.reportingState === "open", "Permission applies only to the open generation.");
2554
+ case "permission.resolved":
2555
+ return inState(assignment2, ["awaiting-permission"], "resolve a permission") ?? need(assignment2.awaitingPermissionId === event.data.permissionRequestId, "A different permission request is outstanding.");
2556
+ case "assignment.answered":
2557
+ return inState(assignment2, ["questioned", "blocked"], "answer");
2558
+ case "assignment.rework-requested":
2559
+ return inState(assignment2, ["handed-back"], "request rework");
2560
+ case "assignment.accepted":
2561
+ return inState(assignment2, ["handed-back"], "accept");
2562
+ case "assignment.rejected":
2563
+ return inState(assignment2, ["handed-back"], "reject");
2564
+ case "assignment.abandoned":
2565
+ if (assignment2.state === "uncertain") {
2566
+ return need(assignment2.reportingState !== "open" && assignment2.reportingState !== "uncertain", "Abandon an uncertain assignment only after its generation is terminal.") ?? need(assignment2.closure === "closing" || assignment2.closure === "uncertain", "Abandon an uncertain assignment only after archive is requested.");
2567
+ }
2568
+ return inState(assignment2, ["draft", "questioned", "blocked", "handed-back", "rework"], "abandon");
2569
+ case "assignment.close-requested":
2570
+ return need(TERMINAL_STATES.includes(assignment2.state) || assignment2.state === "uncertain", "Close an assignment only after a technical decision or while it is uncertain.") ?? need(assignment2.closure === "open" || assignment2.closure === "uncertain", "The assignment is already closing or closed.");
2571
+ case "archive.requested":
2572
+ return need(assignment2.closure === "closing" || assignment2.closure === "uncertain", "Archive follows a close request.") ?? need(assignment2.peerAgentId === event.data.agentId, "Archive names a different agent.") ?? need(!Object.values(assignment2.openIntents).includes("archive.requested"), "Another archive is still unresolved.");
2573
+ case "archive.succeeded":
2574
+ case "archive.failed":
2575
+ case "archive.uncertain":
2576
+ return need(assignment2.openIntents[event.data.intentId] === "archive.requested" || assignment2.closure === "uncertain" && event.type !== "archive.uncertain", "No unresolved archive intent matches.");
2577
+ case "ownership.releasing":
2578
+ return need(owner?.state === "held", "Only held ownership starts releasing.") ?? need(assignment2.closure === "closing", "Release starts only after a close request.");
2579
+ case "ownership.released":
2580
+ return need(owner?.state === "releasing" || owner?.state === "uncertain", "Only releasing or uncertain ownership is released.") ?? need(assignment2.closure === "closed", "Ownership is released only after archive is proven.");
2581
+ case "ownership.uncertain":
2582
+ return need(owner !== void 0 && owner.state !== "released", "No unreleased ownership to mark uncertain.");
2583
+ case "gate.requested":
2584
+ return inState(assignment2, ["handed-back"], "run a gate") ?? need(assignment2.candidate !== void 0 && assignment2.candidate.commit === event.data.candidate.commit, "A gate runs only against the projected candidate.") ?? need(!assignment2.gates.some((gate) => gate.status === "running"), "Another gate is still running.");
2585
+ case "gate.finished":
2586
+ return need(assignment2.gates.some((gate) => gate.gateRunId === event.data.result.id && gate.status !== "finished"), "No unsettled gate matches.");
2587
+ case "gate.uncertain":
2588
+ return need(assignment2.gates.some((gate) => gate.gateRunId === event.data.gateRunId && gate.status === "running"), "No running gate matches.");
2589
+ default: {
2590
+ const exhaustive = event;
2591
+ return `Unhandled event ${exhaustive.type}.`;
2592
+ }
2593
+ }
2594
+ }
2595
+ function update(state, id2, change, event) {
2596
+ const current2 = state.assignments.get(id2);
2597
+ if (current2 === void 0) return;
2598
+ state.assignments.set(id2, { ...current2, ...change, eventIds: [...current2.eventIds, event.id] });
2599
+ }
2600
+ function withIntent(view, intentId, type) {
2601
+ const next = { ...view.openIntents };
2602
+ if (type === void 0) Reflect.deleteProperty(next, intentId);
2603
+ else next[intentId] = type;
2604
+ return next;
2605
+ }
2606
+ function withoutPermission(view) {
2607
+ const copy = { ...view };
2608
+ delete copy.awaitingPermissionId;
2609
+ return copy;
2610
+ }
2611
+ function setOwner(state, id2, change) {
2612
+ const current2 = state.ownership.get(id2);
2613
+ if (current2 !== void 0) state.ownership.set(id2, { ...current2, ...change });
2614
+ }
2615
+ function applyReport(state, view, event) {
2616
+ const report = {
2617
+ generation: event.data.generation,
2618
+ tool: event.data.tool,
2619
+ requestId: event.data.requestId,
2620
+ fingerprint: event.data.fingerprint,
2621
+ receipt: event.data.receipt,
2622
+ report: event.data.report,
2623
+ eventId: event.id
2624
+ };
2625
+ update(state, view.id, {
2626
+ state: event.data.receipt.assignmentState,
2627
+ reportingState: "consumed",
2628
+ reports: [...view.reports, report],
2629
+ ...event.data.candidate === void 0 ? {} : { candidate: event.data.candidate },
2630
+ ...event.data.inspectedCommit === void 0 ? {} : { inspectedCommit: event.data.inspectedCommit }
2631
+ }, event);
2632
+ }
2633
+ function applyEvent(state, event) {
2634
+ state.lastSequence = Math.max(state.lastSequence, event.sequence);
2635
+ switch (event.type) {
2636
+ case "project.bound":
2637
+ case "project.rebound":
2638
+ state.binding = { canonicalRoot: event.data.canonicalRoot, gitCommonDir: event.data.gitCommonDir };
2639
+ return;
2640
+ case "project.ownership-conflict":
2641
+ state.ownershipConflict = { leadAgentIds: event.data.leadAgentIds, eventId: event.id };
2642
+ return;
2643
+ case "project.ownership-resolved":
2644
+ delete state.ownershipConflict;
2645
+ return;
2646
+ case "notice.pending":
2647
+ state.notices.set(event.data.noticeId, {
2648
+ noticeId: event.data.noticeId,
2649
+ kind: event.data.kind,
2650
+ class: event.data.class,
2651
+ state: "pending",
2652
+ eventId: event.id,
2653
+ ...event.assignmentId === void 0 ? {} : { assignmentId: event.assignmentId }
2654
+ });
2655
+ return;
2656
+ case "notice.sent":
2657
+ case "notice.failed":
2658
+ case "notice.uncertain": {
2659
+ const notice = state.notices.get(event.data.noticeId);
2660
+ if (notice !== void 0) state.notices.set(notice.noticeId, { ...notice, state: event.type === "notice.sent" ? "sent" : event.type === "notice.failed" ? "failed" : "uncertain" });
2661
+ return;
2662
+ }
2663
+ case "assignment.created":
2664
+ if (event.assignmentId === void 0) return;
2665
+ state.assignments.set(event.assignmentId, {
2666
+ id: event.assignmentId,
2667
+ input: event.data.input,
2668
+ leadAgentId: event.data.leadAgentId,
2669
+ leadProviderId: event.data.leadProviderId,
2670
+ state: "draft",
2671
+ closure: "open",
2672
+ reportingGeneration: 0,
2673
+ reportingState: "closed",
2674
+ runGeneration: 0,
2675
+ reports: [],
2676
+ gates: [],
2677
+ openIntents: {},
2678
+ eventIds: [event.id]
2679
+ });
2680
+ return;
2681
+ default:
2682
+ break;
2683
+ }
2684
+ const view = assignmentOf(state, event);
2685
+ if (view === void 0) return;
2686
+ const id2 = view.id;
2687
+ switch (event.type) {
2688
+ case "assignment.dispatch-requested":
2689
+ update(state, id2, { state: "dispatching", peerProviderId: event.data.peerProviderId, workspaceId: event.data.workspaceId }, event);
2690
+ return;
2691
+ case "ownership.reserved":
2692
+ state.ownership.set(id2, { assignmentId: id2, workspaceId: event.data.workspaceId, baseCommit: event.data.baseCommit, state: "reserved" });
2693
+ update(state, id2, {}, event);
2694
+ return;
2695
+ case "agent.create-requested":
2696
+ update(state, id2, { openIntents: withIntent(view, event.data.intentId, event.type) }, event);
2697
+ return;
2698
+ case "agent.create-succeeded":
2699
+ update(state, id2, { peerAgentId: event.data.agentId, openIntents: withIntent(view, event.data.intentId, void 0), ...view.state === "uncertain" ? { state: "dispatching" } : {} }, event);
2700
+ return;
2701
+ case "agent.create-failed": {
2702
+ const owner = state.ownership.get(id2);
2703
+ if (owner?.state === "reserved" || owner?.state === "uncertain" && owner.agentId === void 0) setOwner(state, id2, { state: "released" });
2704
+ update(state, id2, { state: "blocked", openIntents: withIntent(view, event.data.intentId, void 0) }, event);
2705
+ return;
2706
+ }
2707
+ case "agent.create-uncertain":
2708
+ setOwner(state, id2, { state: "uncertain" });
2709
+ update(state, id2, { state: "uncertain" }, event);
2710
+ return;
2711
+ case "binding.refused":
2712
+ setOwner(state, id2, { state: "uncertain", agentId: event.data.agentId });
2713
+ update(state, id2, { state: "uncertain" }, event);
2714
+ return;
2715
+ case "binding.published":
2716
+ update(state, id2, { observedProviderId: event.data.providerId, observedModel: event.data.model, workspaceId: event.data.workspaceId }, event);
2717
+ return;
2718
+ case "ownership.held":
2719
+ setOwner(state, id2, { state: "held", agentId: event.data.agentId });
2720
+ update(state, id2, {}, event);
2721
+ return;
2722
+ case "reporting.generation-opened":
2723
+ update(state, id2, { reportingGeneration: event.data.generation, reportingState: "open" }, event);
2724
+ return;
2725
+ case "run.requested":
2726
+ update(state, id2, { state: "active", runGeneration: event.data.generation, openIntents: withIntent(view, event.data.intentId, event.type) }, event);
2727
+ return;
2728
+ case "run.succeeded":
2729
+ update(state, id2, { openIntents: withIntent(view, event.data.intentId, void 0), ...view.state === "uncertain" ? { state: "active", reportingState: "open" } : {} }, event);
2730
+ return;
2731
+ case "run.failed":
2732
+ update(state, id2, { state: "blocked", reportingState: "consumed", openIntents: withIntent(view, event.data.intentId, void 0) }, event);
2733
+ return;
2734
+ case "run.uncertain":
2735
+ update(state, id2, { state: "uncertain", reportingState: "uncertain" }, event);
2736
+ return;
2737
+ case "report.accepted":
2738
+ applyReport(state, view, event);
2739
+ return;
2740
+ case "report.refused":
2741
+ update(state, id2, {}, event);
2742
+ return;
2743
+ case "report.missing":
2744
+ state.assignments.set(id2, { ...withoutPermission(view), state: "blocked", reportingState: "consumed", eventIds: [...view.eventIds, event.id] });
2745
+ return;
2746
+ case "report.uncertain":
2747
+ update(state, id2, { state: "uncertain", reportingState: "uncertain" }, event);
2748
+ return;
2749
+ case "permission.awaiting":
2750
+ update(state, id2, { state: "awaiting-permission", awaitingPermissionId: event.data.permissionRequestId }, event);
2751
+ return;
2752
+ case "permission.resolved":
2753
+ state.assignments.set(id2, { ...withoutPermission(view), state: "active", eventIds: [...view.eventIds, event.id] });
2754
+ return;
2755
+ case "assignment.answered":
2756
+ update(state, id2, {}, event);
2757
+ return;
2758
+ case "assignment.rework-requested":
2759
+ update(state, id2, { state: "rework" }, event);
2760
+ return;
2761
+ case "assignment.accepted":
2762
+ case "assignment.rejected":
2763
+ case "assignment.abandoned": {
2764
+ const type = event.type === "assignment.accepted" ? "accepted" : event.type === "assignment.rejected" ? "rejected" : "abandoned";
2765
+ update(state, id2, { state: type, decision: { type, eventId: event.id } }, event);
2766
+ return;
2767
+ }
2768
+ case "assignment.close-requested":
2769
+ update(state, id2, { closure: "closing" }, event);
2770
+ return;
2771
+ case "archive.requested":
2772
+ update(state, id2, { openIntents: withIntent(view, event.data.intentId, event.type) }, event);
2773
+ return;
2774
+ case "archive.succeeded":
2775
+ update(state, id2, { closure: "closed", openIntents: withIntent(view, event.data.intentId, void 0) }, event);
2776
+ return;
2777
+ case "archive.failed":
2778
+ update(state, id2, { closure: "uncertain", openIntents: withIntent(view, event.data.intentId, void 0) }, event);
2779
+ return;
2780
+ case "archive.uncertain":
2781
+ update(state, id2, { closure: "uncertain" }, event);
2782
+ return;
2783
+ case "ownership.releasing":
2784
+ setOwner(state, id2, { state: "releasing" });
2785
+ update(state, id2, {}, event);
2786
+ return;
2787
+ case "ownership.released":
2788
+ setOwner(state, id2, { state: "released" });
2789
+ update(state, id2, {}, event);
2790
+ return;
2791
+ case "ownership.uncertain":
2792
+ setOwner(state, id2, { state: "uncertain" });
2793
+ update(state, id2, {}, event);
2794
+ return;
2795
+ case "gate.requested":
2796
+ update(state, id2, { gates: [...view.gates, { gateRunId: event.data.gateRunId, candidate: event.data.candidate, command: event.data.command, status: "running" }] }, event);
2797
+ return;
2798
+ case "gate.finished":
2799
+ update(state, id2, { gates: view.gates.map((gate) => gate.gateRunId === event.data.result.id ? { ...gate, status: "finished", result: event.data.result } : gate) }, event);
2800
+ return;
2801
+ case "gate.uncertain":
2802
+ update(state, id2, { gates: view.gates.map((gate) => gate.gateRunId === event.data.gateRunId ? { ...gate, status: "uncertain" } : gate) }, event);
2803
+ return;
2804
+ default:
2805
+ return;
2806
+ }
2807
+ }
2808
+ function project(projectId, events) {
2809
+ const state = emptyProjectState(projectId);
2810
+ for (const event of events) {
2811
+ const problem = checkEvent(state, event);
2812
+ if (problem !== void 0) return { state, violations: [{ eventId: event.id, type: event.type, message: problem }] };
2813
+ applyEvent(state, event);
2814
+ }
2815
+ return { state, violations: [] };
2816
+ }
2817
+ var TERMINAL_STATES;
2818
+ var init_state = __esm({
2819
+ "src/runtime-plugin/server/domain/state.ts"() {
2820
+ "use strict";
2821
+ TERMINAL_STATES = ["accepted", "rejected", "abandoned"];
2822
+ }
2823
+ });
2824
+
2825
+ // src/runtime-plugin/server/domain/receipts.ts
2826
+ import { createHash as createHash3 } from "crypto";
2827
+ var init_receipts = __esm({
2828
+ "src/runtime-plugin/server/domain/receipts.ts"() {
2829
+ "use strict";
2830
+ }
2831
+ });
2832
+
2833
+ // src/runtime-plugin/server/domain/views.ts
2834
+ function quiescence(state) {
2835
+ const blockers = [];
2836
+ for (const view of state.assignments.values()) {
2837
+ if (view.state !== "draft" && !TERMINAL_STATES.includes(view.state)) blockers.push({ kind: "assignment", id: view.id, detail: `is ${view.state}` });
2838
+ if (view.peerAgentId !== void 0 && view.closure !== "closed") blockers.push({ kind: "archive", id: view.id, detail: `managed Peer ${view.peerAgentId} is not archived (${view.closure})` });
2839
+ for (const gate of view.gates) {
2840
+ if (gate.status === "running" || gate.status === "uncertain" && view.closure !== "closed") {
2841
+ blockers.push({ kind: "gate", id: gate.gateRunId, detail: `gate is ${gate.status}` });
2842
+ }
2843
+ }
2844
+ for (const [intent, type] of Object.entries(view.openIntents)) blockers.push({ kind: "intent", id: intent, detail: `${type} has no result` });
2845
+ }
2846
+ for (const owner of state.ownership.values()) {
2847
+ if (owner.state !== "released") blockers.push({ kind: "ownership", id: owner.assignmentId, detail: `writer ownership is ${owner.state}` });
2848
+ }
2849
+ for (const notice of state.notices.values()) {
2850
+ if (notice.state === "pending" || notice.state === "uncertain") blockers.push({ kind: "delivery", id: notice.noticeId, detail: `notice is ${notice.state}` });
2851
+ }
2852
+ return { quiescent: blockers.length === 0, blockers };
2853
+ }
2854
+ var init_views = __esm({
2855
+ "src/runtime-plugin/server/domain/views.ts"() {
2856
+ "use strict";
2857
+ init_receipts();
2858
+ init_state();
2859
+ }
2860
+ });
2861
+
2862
+ // src/runtime-plugin/shared/limits.ts
2863
+ import { z as z4 } from "zod";
2864
+ function utf8Bytes(value) {
2865
+ return encoder.encode(value).length;
2866
+ }
2867
+ function boundedString(max = MAX_STRING_BYTES) {
2868
+ return z4.string().refine((value) => {
2869
+ const bytes = utf8Bytes(value);
2870
+ return bytes >= 1 && bytes <= max;
2871
+ }, { message: `must be 1 byte to ${String(max)} bytes of UTF-8` });
2872
+ }
2873
+ function boundedArray(item) {
2874
+ return z4.array(item).max(MAX_ARRAY_ITEMS);
2875
+ }
2876
+ var MAX_AGGREGATE_BYTES, MAX_ARRAY_ITEMS, MAX_STRING_BYTES, MAX_COMMAND_BYTES, MAX_GATE_TIMEOUT_SECONDS, encoder;
2877
+ var init_limits = __esm({
2878
+ "src/runtime-plugin/shared/limits.ts"() {
2879
+ "use strict";
2880
+ MAX_AGGREGATE_BYTES = 64 * 1024;
2881
+ MAX_ARRAY_ITEMS = 64;
2882
+ MAX_STRING_BYTES = 8 * 1024;
2883
+ MAX_COMMAND_BYTES = 16 * 1024;
2884
+ MAX_GATE_TIMEOUT_SECONDS = 3600;
2885
+ encoder = new TextEncoder();
2886
+ }
2887
+ });
2888
+
2889
+ // src/runtime-plugin/server/contracts/assignment.ts
2890
+ import { z as z5 } from "zod";
2891
+ var ASSIGNMENT_MODES, ASSIGNMENT_KINDS, commitSchema, gateSpecSchema, items, assignmentCreateSchema, candidateRefSchema;
2892
+ var init_assignment = __esm({
2893
+ "src/runtime-plugin/server/contracts/assignment.ts"() {
2894
+ "use strict";
2895
+ init_limits();
2896
+ ASSIGNMENT_MODES = ["writable", "read-only"];
2897
+ ASSIGNMENT_KINDS = ["engineer", "architect", "reviewer", "scout"];
2898
+ commitSchema = z5.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/);
2899
+ gateSpecSchema = z5.strictObject({
2900
+ command: boundedString(MAX_COMMAND_BYTES),
2901
+ timeoutSeconds: z5.number().int().min(1).max(MAX_GATE_TIMEOUT_SECONDS),
2902
+ runtimeRerun: z5.enum(["none", "optional", "required"]),
2903
+ processContractVersion: z5.literal(1)
2904
+ });
2905
+ items = boundedArray(boundedString());
2906
+ assignmentCreateSchema = z5.strictObject({
2907
+ mode: z5.enum(ASSIGNMENT_MODES),
2908
+ kind: z5.enum(ASSIGNMENT_KINDS),
2909
+ outcome: boundedString(),
2910
+ prerequisites: items,
2911
+ writeScope: items,
2912
+ exclusions: items,
2913
+ invariants: items,
2914
+ acceptanceEvidence: items,
2915
+ gate: gateSpecSchema.optional(),
2916
+ expectedHandoff: items,
2917
+ reopenConditions: items,
2918
+ baseCommit: commitSchema
2919
+ });
2920
+ candidateRefSchema = z5.strictObject({
2921
+ kind: z5.literal("git-commit"),
2922
+ commit: commitSchema,
2923
+ baseCommit: commitSchema,
2924
+ changedPaths: z5.array(z5.string().min(1)),
2925
+ workspaceId: z5.string().min(1),
2926
+ branch: z5.string().min(1).optional()
2927
+ });
2928
+ }
2929
+ });
2930
+
2931
+ // src/runtime-plugin/server/contracts/actions.ts
2932
+ import { z as z6 } from "zod";
2933
+ var assignmentIdSchema, reason, assignment, SUPERVISOR_ACTION_SCHEMAS, LEAD_ACTION_SCHEMAS;
2934
+ var init_actions = __esm({
2935
+ "src/runtime-plugin/server/contracts/actions.ts"() {
2936
+ "use strict";
2937
+ init_limits();
2938
+ init_assignment();
2939
+ assignmentIdSchema = z6.string().regex(/^asg_[A-Za-z0-9_-]{8,64}$/);
2940
+ reason = boundedString();
2941
+ assignment = { assignmentId: assignmentIdSchema };
2942
+ SUPERVISOR_ACTION_SCHEMAS = {
2943
+ room_status: z6.strictObject({}),
2944
+ runtime_findings: z6.strictObject({}),
2945
+ message_lead: z6.strictObject({ message: boundedString() })
2946
+ };
2947
+ LEAD_ACTION_SCHEMAS = {
2948
+ assignment_create: assignmentCreateSchema,
2949
+ // An eligible exact Peer provider id from the room manifest; the model is never selectable.
2950
+ assignment_dispatch: z6.strictObject({ ...assignment, peerProvider: z6.string().min(1).max(128) }),
2951
+ assignment_answer: z6.strictObject({ ...assignment, answer: boundedString() }),
2952
+ assignment_rework: z6.strictObject({ ...assignment, instructions: boundedString() }),
2953
+ assignment_accept: z6.strictObject({
2954
+ ...assignment,
2955
+ reason,
2956
+ // Required by the domain when the Peer gate or a required rerun is red; never a waiver of
2957
+ // a missing gate.
2958
+ override: z6.strictObject({ reason, residualRiskAcknowledged: z6.literal(true) }).optional()
2959
+ }),
2960
+ assignment_reject: z6.strictObject({ ...assignment, reason }),
2961
+ assignment_abandon: z6.strictObject({ ...assignment, reason }),
2962
+ assignment_close: z6.strictObject(assignment),
2963
+ assignment_status: z6.strictObject({ assignmentId: assignmentIdSchema.optional() }),
2964
+ gate_run: z6.strictObject(assignment)
2965
+ };
2966
+ }
2967
+ });
2968
+
2969
+ // src/runtime-plugin/server/contracts/peer.ts
2970
+ import { z as z7 } from "zod";
2971
+ var text, texts, verificationReportSchema, askInputSchema, HANDOFF_DETAILS, handoffBase, PEER_REPORT_ERROR_CODES, peerReportReceiptSchema, peerReportErrorSchema;
2972
+ var init_peer = __esm({
2973
+ "src/runtime-plugin/server/contracts/peer.ts"() {
2974
+ "use strict";
2975
+ init_limits();
2976
+ text = boundedString();
2977
+ texts = boundedArray(text);
2978
+ verificationReportSchema = z7.strictObject({
2979
+ command: boundedString(MAX_COMMAND_BYTES),
2980
+ outcome: z7.enum(["passed", "failed", "not-run"]),
2981
+ note: text.optional()
2982
+ });
2983
+ askInputSchema = z7.strictObject({
2984
+ question: text,
2985
+ blockingContext: text,
2986
+ evidence: texts
2987
+ });
2988
+ HANDOFF_DETAILS = {
2989
+ engineer: z7.strictObject({ kind: z7.literal("engineer") }),
2990
+ architect: z7.strictObject({
2991
+ kind: z7.literal("architect"),
2992
+ alternatives: texts,
2993
+ strongestCounterargument: text,
2994
+ reversalConditions: texts,
2995
+ evidence: texts
2996
+ }),
2997
+ reviewer: z7.strictObject({
2998
+ kind: z7.literal("reviewer"),
2999
+ findings: boundedArray(z7.strictObject({ claim: text, evidence: texts }))
3000
+ }),
3001
+ scout: z7.strictObject({
3002
+ kind: z7.literal("scout"),
3003
+ evidence: texts,
3004
+ remainingUnknowns: texts,
3005
+ confidence: z7.enum(["low", "medium", "high"])
3006
+ })
3007
+ };
3008
+ handoffBase = {
3009
+ summary: text,
3010
+ deliverables: texts,
3011
+ verification: boundedArray(verificationReportSchema),
3012
+ residualRisks: texts,
3013
+ evidence: texts
3014
+ };
3015
+ PEER_REPORT_ERROR_CODES = [
3016
+ "report_malformed",
3017
+ "report_unauthorized",
3018
+ "report_stale",
3019
+ "report_conflict",
3020
+ "report_state",
3021
+ "report_precondition",
3022
+ "report_uncertain"
3023
+ ];
3024
+ peerReportReceiptSchema = z7.strictObject({
3025
+ schema: z7.literal(1),
3026
+ receipt: z7.string().min(1).max(128),
3027
+ tool: z7.enum(["ask", "handoff"]),
3028
+ status: z7.literal("accepted"),
3029
+ assignmentState: z7.enum(["questioned", "blocked", "handed-back"])
3030
+ });
3031
+ peerReportErrorSchema = z7.strictObject({
3032
+ schema: z7.literal(1),
3033
+ error: z7.strictObject({
3034
+ code: z7.enum(PEER_REPORT_ERROR_CODES),
3035
+ message: boundedString(1024),
3036
+ retryable: z7.boolean()
3037
+ })
3038
+ });
3039
+ }
3040
+ });
3041
+
3042
+ // src/runtime-plugin/server/events/schema.ts
3043
+ import { z as z8 } from "zod";
3044
+ function variants(strict) {
3045
+ return EVENT_TYPES.map((type) => z8.strictObject({
3046
+ ...envelope,
3047
+ type: z8.literal(type),
3048
+ payloadVersion: z8.literal(1),
3049
+ data: strict ? z8.strictObject(EVENT_PAYLOADS[type]) : z8.object(EVENT_PAYLOADS[type])
3050
+ }));
3051
+ }
3052
+ function validateForWrite(event) {
3053
+ return writerSchema.parse(event);
3054
+ }
3055
+ function readEvent(value) {
3056
+ if (typeof value !== "object" || value === null) return { ok: false, reason: "invalid", detail: "Event is not an object." };
3057
+ const { type, payloadVersion } = value;
3058
+ if (typeof type !== "string" || !Object.hasOwn(EVENT_PAYLOADS, type)) {
3059
+ return { ok: false, reason: "unknown-type", detail: `Unknown event type ${typeof type === "string" ? type : typeof type}.` };
3060
+ }
3061
+ if (typeof payloadVersion !== "number" || !SUPPORTED_PAYLOAD_VERSIONS[type].includes(payloadVersion)) {
3062
+ return { ok: false, reason: "unsupported-version", detail: `Unsupported payload version ${String(payloadVersion)} for ${type}.` };
3063
+ }
3064
+ const parsed = readerSchema.safeParse(value);
3065
+ return parsed.success ? { ok: true, event: parsed.data } : { ok: false, reason: "invalid", detail: z8.prettifyError(parsed.error) };
3066
+ }
3067
+ var EVENT_SCHEMA, id, text2, reason2, generation2, digest2, timestamp, gateResultSchema, NOTICE_DISPOSITIONS, NOTICE_CLASSES, EVENT_PAYLOADS, EVENT_TYPES, SUPPORTED_PAYLOAD_VERSIONS, actorSchema, envelope, firstWriter, restWriters, firstReader, restReaders, writerSchema, readerSchema;
3068
+ var init_schema = __esm({
3069
+ "src/runtime-plugin/server/events/schema.ts"() {
3070
+ "use strict";
3071
+ init_limits();
3072
+ init_policy();
3073
+ init_actions();
3074
+ init_assignment();
3075
+ init_peer();
3076
+ EVENT_SCHEMA = "paseo-room.runtime-event";
3077
+ id = z8.string().min(1).max(128);
3078
+ text2 = boundedString();
3079
+ reason2 = boundedString(1024);
3080
+ generation2 = z8.number().int().min(1);
3081
+ digest2 = z8.string().regex(/^sha256:[0-9a-f]{64}$/);
3082
+ timestamp = z8.iso.datetime({ offset: true });
3083
+ gateResultSchema = z8.strictObject({
3084
+ id,
3085
+ assignmentId: assignmentIdSchema,
3086
+ candidate: candidateRefSchema,
3087
+ command: boundedString(MAX_COMMAND_BYTES),
3088
+ startedAt: timestamp,
3089
+ finishedAt: timestamp.optional(),
3090
+ exitCode: z8.number().int().optional(),
3091
+ signal: z8.string().min(1).max(32).optional(),
3092
+ timedOut: z8.boolean(),
3093
+ termination: z8.enum(["exited", "signaled", "killed", "uncertain"]),
3094
+ processContractVersion: z8.literal(1),
3095
+ environmentPolicyVersion: z8.literal(1),
3096
+ outputDigest: digest2,
3097
+ outputTailAttachment: z8.string().min(1).max(256).optional(),
3098
+ workspaceMoved: z8.boolean()
3099
+ });
3100
+ NOTICE_DISPOSITIONS = ["record", "panel", "lead-now", "supervisor-digest", "supervisor-now", "operator-now", "human-required"];
3101
+ NOTICE_CLASSES = ["record", "owner", "operator", "page"];
3102
+ EVENT_PAYLOADS = {
3103
+ // Project binding and Lead ownership (§4.1, §5.2).
3104
+ "project.bound": { canonicalRoot: z8.string().min(1), gitCommonDir: z8.string().min(1) },
3105
+ "project.rebound": { canonicalRoot: z8.string().min(1), gitCommonDir: z8.string().min(1), previousRoot: z8.string().min(1) },
3106
+ "project.ownership-conflict": { leadAgentIds: z8.array(id).min(2), leadProviderIds: z8.array(id).min(1) },
3107
+ "project.ownership-resolved": { keptLeadAgentId: id, decidedBy: z8.enum(["evidence", "human"]) },
3108
+ // Assignment lifecycle decided by Lead or Human (§5.1).
3109
+ "assignment.created": { input: assignmentCreateSchema, leadAgentId: id, leadProviderId: id },
3110
+ "assignment.dispatch-requested": { peerProviderId: id, workspaceId: id },
3111
+ "assignment.answered": { answer: text2 },
3112
+ "assignment.rework-requested": { instructions: text2 },
3113
+ "assignment.accepted": {
3114
+ candidate: candidateRefSchema.optional(),
3115
+ inspectedCommit: commitSchema.optional(),
3116
+ reason: reason2,
3117
+ override: z8.strictObject({ reason: reason2, residualRiskAcknowledged: z8.literal(true) }).optional(),
3118
+ gateResultId: id.optional()
3119
+ },
3120
+ "assignment.rejected": { reason: reason2 },
3121
+ "assignment.abandoned": { reason: reason2 },
3122
+ "assignment.close-requested": {},
3123
+ // Writer ownership, independent of assignment state (§5.3).
3124
+ "ownership.reserved": { workspaceId: id, baseCommit: commitSchema },
3125
+ "ownership.held": { agentId: id },
3126
+ "ownership.releasing": { agentId: id },
3127
+ "ownership.released": { agentId: id, archivedAt: timestamp },
3128
+ "ownership.uncertain": { reason: reason2 },
3129
+ // External effects: agent creation, turn delivery and archive (§6).
3130
+ "agent.create-requested": { intentId: id, peerProviderId: id, workspaceId: id, parentAgentId: id, label: id },
3131
+ "agent.create-succeeded": { intentId: id, agentId: id },
3132
+ "agent.create-failed": { intentId: id, reason: reason2 },
3133
+ "agent.create-uncertain": { intentId: id, reason: reason2 },
3134
+ // A created Peer whose fresh snapshot did not prove provider, parent, workspace, idleness
3135
+ // and no prior prompt. It is archived, never adopted or prompted (§3.3, §5.3).
3136
+ "binding.refused": { agentId: id, reason: reason2 },
3137
+ "binding.published": {
3138
+ agentId: id,
3139
+ providerId: id,
3140
+ model: z8.string().min(1).max(256),
3141
+ parentAgentId: id,
3142
+ workspaceId: id,
3143
+ roomGeneration: id
3144
+ },
3145
+ "reporting.generation-opened": { generation: generation2, capabilityHash: digest2, turn: z8.enum(["initial", "answer", "rework", "follow-up"]) },
3146
+ "run.requested": { intentId: id, generation: generation2, promptDigest: digest2 },
3147
+ "run.succeeded": { intentId: id, generation: generation2 },
3148
+ "run.failed": { intentId: id, generation: generation2, reason: reason2 },
3149
+ "run.uncertain": { intentId: id, generation: generation2, reason: reason2 },
3150
+ "archive.requested": { intentId: id, agentId: id },
3151
+ "archive.succeeded": { intentId: id, agentId: id, archivedAt: timestamp, liveStatus: z8.literal("closed") },
3152
+ "archive.failed": { intentId: id, agentId: id, reason: reason2 },
3153
+ "archive.uncertain": { intentId: id, agentId: id, reason: reason2 },
3154
+ // Peer reporting (§3.4). Report content is evidence; identity and candidate are derived.
3155
+ "report.accepted": {
3156
+ generation: generation2,
3157
+ tool: z8.enum(PEER_REPORTING_TOOLS),
3158
+ requestId: id,
3159
+ fingerprint: digest2,
3160
+ receipt: peerReportReceiptSchema,
3161
+ report: z8.record(z8.string(), z8.unknown()),
3162
+ candidate: candidateRefSchema.optional(),
3163
+ inspectedCommit: commitSchema.optional()
3164
+ },
3165
+ "report.refused": { generation: generation2.optional(), tool: z8.string().min(1).max(64), requestId: id, code: z8.enum(PEER_REPORT_ERROR_CODES), reason: reason2 },
3166
+ "report.missing": { generation: generation2 },
3167
+ "report.uncertain": { generation: generation2, requestId: id.optional(), reason: reason2 },
3168
+ "permission.awaiting": { generation: generation2, permissionRequestId: id, tool: z8.enum(PEER_REPORTING_TOOLS) },
3169
+ "permission.resolved": { generation: generation2, permissionRequestId: id, outcome: z8.enum(["allowed", "denied", "other"]) },
3170
+ // Independent runtime gate (D6).
3171
+ "gate.requested": {
3172
+ gateRunId: id,
3173
+ candidate: candidateRefSchema,
3174
+ command: boundedString(MAX_COMMAND_BYTES),
3175
+ timeoutSeconds: gateSpecSchema.shape.timeoutSeconds,
3176
+ processContractVersion: z8.literal(1),
3177
+ environmentPolicyVersion: z8.literal(1)
3178
+ },
3179
+ "gate.finished": { result: gateResultSchema },
3180
+ "gate.uncertain": { gateRunId: id, reason: reason2 },
3181
+ // Deterministic notices (D9). Delivery is at least once with a stable id.
3182
+ "notice.pending": {
3183
+ noticeId: id,
3184
+ kind: z8.string().min(1).max(64),
3185
+ class: z8.enum(NOTICE_CLASSES),
3186
+ disposition: z8.enum(NOTICE_DISPOSITIONS),
3187
+ recipientAgentId: id.optional(),
3188
+ recipientRole: z8.enum(RUNTIME_ROLES).exclude(["peer"]).optional(),
3189
+ text: text2
3190
+ },
3191
+ "notice.sent": { noticeId: id },
3192
+ "notice.failed": { noticeId: id, reason: reason2 },
3193
+ "notice.uncertain": { noticeId: id, reason: reason2 }
3194
+ };
3195
+ EVENT_TYPES = Object.keys(EVENT_PAYLOADS);
3196
+ SUPPORTED_PAYLOAD_VERSIONS = Object.fromEntries(
3197
+ EVENT_TYPES.map((type) => [type, [1]])
3198
+ );
3199
+ actorSchema = z8.strictObject({
3200
+ source: z8.enum(["human", "plugin", "seat", "paseo"]),
3201
+ role: z8.enum(RUNTIME_ROLES).optional(),
3202
+ agentId: id.optional(),
3203
+ providerId: id.optional()
3204
+ });
3205
+ envelope = {
3206
+ schema: z8.literal(EVENT_SCHEMA),
3207
+ version: z8.literal(1),
3208
+ id: z8.string().regex(/^evt_[A-Za-z0-9_-]{8,64}$/),
3209
+ sequence: z8.number().int().min(1),
3210
+ projectId: z8.uuid(),
3211
+ assignmentId: assignmentIdSchema.optional(),
3212
+ actor: actorSchema,
3213
+ causationId: id.optional(),
3214
+ idempotencyKey: id.optional(),
3215
+ occurredAt: timestamp
3216
+ };
3217
+ [firstWriter, ...restWriters] = variants(true);
3218
+ [firstReader, ...restReaders] = variants(false);
3219
+ if (firstWriter === void 0 || firstReader === void 0) throw new Error("The runtime event union is empty.");
3220
+ writerSchema = z8.discriminatedUnion("type", [firstWriter, ...restWriters]);
3221
+ readerSchema = z8.discriminatedUnion("type", [firstReader, ...restReaders]);
3222
+ }
3223
+ });
3224
+
3225
+ // src/runtime-plugin/server/store/publish.ts
3226
+ import { randomBytes as randomBytes2 } from "crypto";
3227
+ import { chmod as chmod2, link, lstat as lstat4, mkdir as mkdir2, open, readdir as readdir3, unlink as unlink2 } from "fs/promises";
3228
+ import { join as join13 } from "path";
3229
+ function code(error) {
3230
+ return typeof error === "object" && error !== null && "code" in error ? String(error.code) : void 0;
3231
+ }
3232
+ async function ensurePrivateDirectory(path) {
3233
+ await mkdir2(path, { recursive: true, mode: PRIVATE_DIR_MODE });
3234
+ const stat2 = await lstat4(path);
3235
+ if (!stat2.isDirectory()) throw new Error(`${path} is not a directory.`);
3236
+ if ((stat2.mode & 511) !== PRIVATE_DIR_MODE) await chmod2(path, PRIVATE_DIR_MODE);
3237
+ }
3238
+ async function writeTemporary(directory, content, hook) {
3239
+ const temporary = join13(directory, `${TEMPORARY_PREFIX}${String(process.pid)}-${randomBytes2(8).toString("hex")}`);
3240
+ const handle = await open(temporary, "wx", PRIVATE_FILE_MODE);
3241
+ try {
3242
+ await handle.writeFile(content);
3243
+ hook?.("temporary-written");
3244
+ await handle.sync();
3245
+ hook?.("temporary-synced");
3246
+ } finally {
3247
+ await handle.close();
3248
+ }
3249
+ return temporary;
3250
+ }
3251
+ async function syncDirectory(directory) {
3252
+ let handle;
3253
+ try {
3254
+ handle = await open(directory, "r");
3255
+ await handle.sync();
3256
+ } catch (error) {
3257
+ if (!["EISDIR", "EINVAL", "EPERM", "EBADF", "ENOTSUP"].includes(code(error) ?? "")) throw error;
3258
+ } finally {
3259
+ await handle?.close();
3260
+ }
3261
+ }
3262
+ async function publishWith(directory, content, allocate, hook) {
3263
+ const temporary = await writeTemporary(directory, content, hook);
3264
+ let published;
3265
+ try {
3266
+ for (let attempt = 0; ; attempt += 1) {
3267
+ const name = allocate(attempt);
3268
+ if (name === void 0) break;
3269
+ const target = join13(directory, name);
3270
+ try {
3271
+ await link(temporary, target);
3272
+ published = target;
3273
+ break;
3274
+ } catch (error) {
3275
+ const reason3 = code(error);
3276
+ if (reason3 === "EEXIST") continue;
3277
+ if (reason3 !== void 0 && UNSUPPORTED.has(reason3)) throw new PublishUnsupportedError(directory, error);
3278
+ throw error;
3279
+ }
3280
+ }
3281
+ if (published !== void 0) hook?.("linked");
3282
+ } finally {
3283
+ await unlink2(temporary).catch(() => void 0);
3284
+ }
3285
+ if (published === void 0) return "";
3286
+ hook?.("temporary-removed");
3287
+ await syncDirectory(directory);
3288
+ return published;
3289
+ }
3290
+ async function publishOnce(directory, name, content, hook) {
3291
+ const path = await publishWith(directory, content, (attempt) => attempt === 0 ? name : void 0, hook);
3292
+ if (path === "") throw new AlreadyPublishedError(join13(directory, name));
3293
+ return path;
3294
+ }
3295
+ async function staleTemporaries(directory) {
3296
+ const names = await readdir3(directory);
3297
+ return names.filter((name) => name.startsWith(TEMPORARY_PREFIX)).sort();
3298
+ }
3299
+ var TEMPORARY_PREFIX, PRIVATE_DIR_MODE, PRIVATE_FILE_MODE, PublishUnsupportedError, AlreadyPublishedError, UNSUPPORTED;
3300
+ var init_publish = __esm({
3301
+ "src/runtime-plugin/server/store/publish.ts"() {
3302
+ "use strict";
3303
+ TEMPORARY_PREFIX = ".tmp-";
3304
+ PRIVATE_DIR_MODE = 448;
3305
+ PRIVATE_FILE_MODE = 384;
3306
+ PublishUnsupportedError = class extends Error {
3307
+ constructor(directory, cause) {
3308
+ super(`The filesystem at ${directory} cannot publish runtime state without the risk of overwriting it (hard links unavailable).`, { cause });
3309
+ this.name = "PublishUnsupportedError";
3310
+ }
3311
+ };
3312
+ AlreadyPublishedError = class extends Error {
3313
+ constructor(path) {
3314
+ super(`${path} is already published and is never replaced.`);
3315
+ this.path = path;
3316
+ this.name = "AlreadyPublishedError";
3317
+ }
3318
+ path;
3319
+ };
3320
+ UNSUPPORTED = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EXDEV", "EMLINK"]);
3321
+ }
3322
+ });
3323
+
3324
+ // src/runtime-plugin/server/store/project.ts
3325
+ import { randomBytes as randomBytes3, randomUUID } from "crypto";
3326
+ import { mkdir as mkdir3, readdir as readdir4, readFile as readFile4, rename as rename2, rm as rm3, writeFile as writeFile2 } from "fs/promises";
3327
+ import { basename as basename2, join as join14 } from "path";
3328
+ import { z as z9 } from "zod";
3329
+ function runtimeRoot(roomHome) {
3330
+ return join14(roomHome, "runtime", RUNTIME_LAYOUT_VERSION);
3331
+ }
3332
+ function eventName(sequence) {
3333
+ return `${String(sequence).padStart(12, "0")}.json`;
3334
+ }
3335
+ function slug(root) {
3336
+ const name = basename2(root).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
3337
+ return name === "" ? "project" : name;
3338
+ }
3339
+ var RUNTIME_LAYOUT_VERSION, EVENT_FILE, MAX_SEQUENCE_ATTEMPTS, metaSchema, ProjectStore;
3340
+ var init_project = __esm({
3341
+ "src/runtime-plugin/server/store/project.ts"() {
3342
+ "use strict";
3343
+ init_schema();
3344
+ init_publish();
3345
+ RUNTIME_LAYOUT_VERSION = "v1";
3346
+ EVENT_FILE = /^(\d{12})\.json$/;
3347
+ MAX_SEQUENCE_ATTEMPTS = 64;
3348
+ metaSchema = z9.strictObject({
3349
+ schema: z9.literal(1),
3350
+ projectId: z9.uuid(),
3351
+ createdAt: z9.iso.datetime({ offset: true }),
3352
+ canonicalRoot: z9.string().min(1),
3353
+ gitCommonDir: z9.string().min(1)
3354
+ });
3355
+ ProjectStore = class _ProjectStore {
3356
+ constructor(directory, meta, now) {
3357
+ this.directory = directory;
3358
+ this.meta = meta;
3359
+ this.now = now;
3360
+ }
3361
+ directory;
3362
+ meta;
3363
+ now;
3364
+ lastSequence = 0;
3365
+ get eventsDirectory() {
3366
+ return join14(this.directory, "events");
3367
+ }
3368
+ get cacheDirectory() {
3369
+ return join14(this.directory, "cache");
3370
+ }
3371
+ get gatesDirectory() {
3372
+ return join14(this.directory, "gates");
3373
+ }
3374
+ get quarantineDirectory() {
3375
+ return join14(this.directory, "quarantine");
3376
+ }
3377
+ /** Opens an existing project directory; refuses one whose `meta.json` is not exactly v1. */
3378
+ static async open(directory, now = () => /* @__PURE__ */ new Date()) {
3379
+ const meta = metaSchema.parse(JSON.parse(await readFile4(join14(directory, "meta.json"), "utf8")));
3380
+ return new _ProjectStore(directory, meta, now);
3381
+ }
3382
+ /**
3383
+ * Binds a new project to its canonical Git common directory. Identity is a minted UUID, never
3384
+ * a hash of a movable path; a moved repository is rebound only by an explicit operator action.
3385
+ */
3386
+ static async create(root, binding, now = () => /* @__PURE__ */ new Date()) {
3387
+ const projects = join14(root, "projects");
3388
+ await ensurePrivateDirectory(projects);
3389
+ const projectId = randomUUID();
3390
+ const directory = join14(projects, `${slug(binding.canonicalRoot)}-${projectId}`);
3391
+ await ensurePrivateDirectory(directory);
3392
+ for (const child of ["events", "cache", "gates", "quarantine"]) await ensurePrivateDirectory(join14(directory, child));
3393
+ const meta = { schema: 1, projectId, createdAt: now().toISOString(), ...binding };
3394
+ await publishOnce(directory, "meta.json", `${JSON.stringify(meta, null, 2)}
3395
+ `);
3396
+ const store = new _ProjectStore(directory, meta, now);
3397
+ await store.append({ type: "project.bound", payloadVersion: 1, actor: { source: "plugin" }, data: { ...binding } });
3398
+ return store;
3399
+ }
3400
+ /** Finds the project bound to this Git common directory. Never matches by name, remote or cwd. */
3401
+ static async find(root, gitCommonDir, now) {
3402
+ for (const store of await _ProjectStore.list(root, now)) {
3403
+ if (store.meta.gitCommonDir === gitCommonDir) return store;
3404
+ }
3405
+ return void 0;
3406
+ }
3407
+ static async list(root, now) {
3408
+ const projects = join14(root, "projects");
3409
+ let names;
3410
+ try {
3411
+ names = await readdir4(projects);
3412
+ } catch {
3413
+ return [];
3414
+ }
3415
+ const stores = [];
3416
+ for (const name of names.sort()) {
3417
+ try {
3418
+ stores.push(await _ProjectStore.open(join14(projects, name), now));
3419
+ } catch {
3420
+ }
3421
+ }
3422
+ return stores;
3423
+ }
3424
+ /** Reads every event strictly. Any unreadable file pauses the project; nothing is moved. */
3425
+ async replay() {
3426
+ const names = (await readdir4(this.eventsDirectory)).sort();
3427
+ const problems = [];
3428
+ const events = [];
3429
+ const ids = /* @__PURE__ */ new Map();
3430
+ for (const name of names) {
3431
+ if (name.startsWith(".tmp-")) continue;
3432
+ const match = EVENT_FILE.exec(name);
3433
+ if (!match) {
3434
+ problems.push({ file: name, reason: "unexpected-file", detail: "Not a runtime event file name." });
3435
+ continue;
3436
+ }
3437
+ let raw;
3438
+ let value;
3439
+ try {
3440
+ raw = await readFile4(join14(this.eventsDirectory, name), "utf8");
3441
+ value = JSON.parse(raw);
3442
+ } catch (error) {
3443
+ problems.push({ file: name, reason: "unreadable", detail: error instanceof Error ? error.message : String(error) });
3444
+ continue;
3445
+ }
3446
+ const read = readEvent(value);
3447
+ if (!read.ok) {
3448
+ problems.push({ file: name, reason: read.reason, detail: read.detail });
3449
+ continue;
3450
+ }
3451
+ const event = read.event;
3452
+ if (event.sequence !== Number(match[1])) {
3453
+ problems.push({ file: name, reason: "sequence-mismatch", detail: `File name does not match sequence ${String(event.sequence)}.` });
3454
+ continue;
3455
+ }
3456
+ if (event.projectId !== this.meta.projectId) {
3457
+ problems.push({ file: name, reason: "foreign-project", detail: `Event belongs to project ${event.projectId}.` });
3458
+ continue;
3459
+ }
3460
+ const previous = ids.get(event.id);
3461
+ if (previous !== void 0) {
3462
+ problems.push({ file: name, reason: "duplicate-id", detail: `Event id ${event.id} is also used by ${previous}.` });
3463
+ continue;
3464
+ }
3465
+ ids.set(event.id, name);
3466
+ events.push(event);
3467
+ }
3468
+ const gaps = [];
3469
+ let expected = 1;
3470
+ for (const event of events) {
3471
+ for (; expected < event.sequence; expected += 1) gaps.push(expected);
3472
+ expected = event.sequence + 1;
3473
+ }
3474
+ const highest = names.map((name) => EVENT_FILE.exec(name)?.[1]).filter((value) => value !== void 0).map(Number);
3475
+ this.lastSequence = Math.max(this.lastSequence, 0, ...highest);
3476
+ return {
3477
+ status: problems.length === 0 ? "ok" : "paused",
3478
+ events,
3479
+ problems,
3480
+ gaps,
3481
+ staleTemporaries: await staleTemporaries(this.eventsDirectory)
3482
+ };
3483
+ }
3484
+ /**
3485
+ * Validates and publishes one event at the next free sequence. A concurrent writer that took
3486
+ * that sequence causes reallocation, never replacement.
3487
+ */
3488
+ async append(event) {
3489
+ if (this.lastSequence === 0) await this.replay();
3490
+ const base = this.lastSequence;
3491
+ for (let attempt = 0; attempt < MAX_SEQUENCE_ATTEMPTS; attempt += 1) {
3492
+ const sequence = base + 1 + attempt;
3493
+ const full = validateForWrite({
3494
+ ...event,
3495
+ schema: EVENT_SCHEMA,
3496
+ version: 1,
3497
+ id: `evt_${randomBytes3(12).toString("hex")}`,
3498
+ sequence,
3499
+ projectId: this.meta.projectId,
3500
+ occurredAt: this.now().toISOString()
3501
+ });
3502
+ try {
3503
+ await publishOnce(this.eventsDirectory, eventName(sequence), `${JSON.stringify(full)}
3504
+ `);
3505
+ this.lastSequence = Math.max(this.lastSequence, sequence);
3506
+ return full;
3507
+ } catch (error) {
3508
+ if (!(error instanceof AlreadyPublishedError)) throw error;
3509
+ }
3510
+ }
3511
+ throw new Error(`Could not allocate an event sequence in ${this.eventsDirectory}.`);
3512
+ }
3513
+ /** Writes one disposable derived view. Cache files may be replaced; they are never authority. */
3514
+ async writeCache(name, content) {
3515
+ await mkdir3(this.cacheDirectory, { recursive: true, mode: 448 });
3516
+ const temporary = join14(this.cacheDirectory, `.tmp-${String(process.pid)}-${randomBytes3(6).toString("hex")}`);
3517
+ await writeFile2(temporary, content, { mode: PRIVATE_FILE_MODE });
3518
+ await rename2(temporary, join14(this.cacheDirectory, name));
3519
+ }
3520
+ async readCache(name) {
3521
+ try {
3522
+ return await readFile4(join14(this.cacheDirectory, name), "utf8");
3523
+ } catch {
3524
+ return void 0;
3525
+ }
3526
+ }
3527
+ /** Deletes every cached view; the next replay rebuilds them. */
3528
+ async clearCache() {
3529
+ await rm3(this.cacheDirectory, { recursive: true, force: true });
3530
+ await ensurePrivateDirectory(this.cacheDirectory);
3531
+ }
3532
+ /** Moves one named event file aside. Only an explicit operator action calls this. */
3533
+ async quarantine(file) {
3534
+ if (!EVENT_FILE.test(file) && !file.startsWith(".tmp-")) throw new Error(`${file} is not a runtime event file.`);
3535
+ await ensurePrivateDirectory(this.quarantineDirectory);
3536
+ await rename2(join14(this.eventsDirectory, file), join14(this.quarantineDirectory, file));
3537
+ }
3538
+ };
3539
+ }
3540
+ });
3541
+
3542
+ // src/runtime-state.ts
3543
+ import { readdir as readdir5 } from "fs/promises";
3544
+ import { join as join15 } from "path";
3545
+ async function inspectRuntimeState(roomHome) {
3546
+ const root = runtimeRoot(roomHome);
3547
+ const names = await readdir5(join15(root, "projects")).catch(() => []);
3548
+ const blockers = [];
3549
+ const stores = [];
3550
+ for (const name of names.sort()) {
3551
+ try {
3552
+ stores.push(await ProjectStore.open(join15(root, "projects", name)));
3553
+ } catch {
3554
+ blockers.push({ project: name, kind: "assignment", id: name, detail: "has unreadable project metadata" });
3555
+ }
3556
+ }
3557
+ for (const store of stores) {
3558
+ const replay = await store.replay();
3559
+ const projection = project(store.meta.projectId, replay.events);
3560
+ if (replay.status !== "ok" || projection.violations.length > 0) {
3561
+ blockers.push({ project: store.meta.canonicalRoot, kind: "assignment", id: store.meta.projectId, detail: "the project ledger is paused and cannot be proven quiet" });
3562
+ continue;
3563
+ }
3564
+ for (const blocker of quiescence(projection.state).blockers) blockers.push({ ...blocker, project: store.meta.canonicalRoot });
3565
+ }
3566
+ return { root, projects: names.length, blockers };
3567
+ }
3568
+ function describeBlockers(summary, limit = 5) {
3569
+ const shown = summary.blockers.slice(0, limit).map((blocker) => `${blocker.project}: ${blocker.kind} ${blocker.id} ${blocker.detail}`);
3570
+ const more = summary.blockers.length > limit ? `; and ${String(summary.blockers.length - limit)} more` : "";
3571
+ return `${shown.join("; ")}${more}`;
3572
+ }
3573
+ var init_runtime_state = __esm({
3574
+ "src/runtime-state.ts"() {
3575
+ "use strict";
3576
+ init_state();
3577
+ init_views();
3578
+ init_project();
3579
+ }
3580
+ });
3581
+
2256
3582
  // src/commands.ts
2257
- import { join as join12, sep as sep2 } from "path";
2258
- import { lstat as lstat4, rm as rm3 } from "fs/promises";
3583
+ import { join as join16, sep as sep3 } from "path";
3584
+ import { lstat as lstat5, rm as rm4 } from "fs/promises";
3585
+ import { satisfies } from "semver";
2259
3586
  function sessionOptions(options) {
2260
3587
  const password = (options.env ?? process.env).PASEO_PASSWORD;
2261
3588
  return { ...password === void 0 ? {} : { password }, ...options.factory ? { factory: options.factory } : {} };
@@ -2282,21 +3609,47 @@ function roleProfiles(agent, roles) {
2282
3609
  notes: ROLE_NOTES[role]
2283
3610
  }));
2284
3611
  }
3612
+ function carrierSpec(layout) {
3613
+ return {
3614
+ id: CLAUDE_CARRIER_PLUGIN_ID,
3615
+ path: claudeCarrierPluginDir(layout),
3616
+ check: "claude.plugin",
3617
+ running: "Claude contract carrier plugin",
3618
+ enabledPass: "Paseo plugins are enabled for the required Claude contract carrier.",
3619
+ enabledFail: "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.",
3620
+ paseoRange: CLAUDE_CARRIER_PASEO_RANGE,
3621
+ rangeCheck: "claude.paseo-range",
3622
+ rangeRemedy: "Use a Paseo release inside that range, or upgrade paseo-room, then run setup again."
3623
+ };
3624
+ }
3625
+ function runtimeSpec(layout) {
3626
+ return {
3627
+ id: RUNTIME_PLUGIN_ID,
3628
+ path: runtimePluginDir(layout),
3629
+ check: "runtime.plugin",
3630
+ running: "Runtime coordination plugin (preview)",
3631
+ enabledPass: "Paseo plugins are enabled for the runtime coordination plugin.",
3632
+ enabledFail: "Runtime coordination is a trusted server plugin, but Paseo plugins are disabled. Plugins are trusted, unsandboxed code with access to the daemon machine.",
3633
+ paseoRange: RUNTIME_PASEO_RANGE,
3634
+ rangeCheck: "runtime.paseo-range",
3635
+ rangeRemedy: "Use a Paseo release inside that range, or run setup without --runtime."
3636
+ };
3637
+ }
2285
3638
  async function buildDesired(layout, agents, roles, build = {}) {
2286
3639
  const entries = [
2287
3640
  { kind: "dir", path: layout.roomHome },
2288
3641
  { kind: "dir", path: sharedRoom(layout) },
2289
3642
  // Earlier versions generated a default workspace protocol template here. The room ships no
2290
3643
  // default protocol now, so the old generated regular file is declared absent and removed.
2291
- { kind: "absent", path: join12(sharedRoom(layout), "WORKSPACE_PROTOCOL.md") },
3644
+ { kind: "absent", path: join16(sharedRoom(layout), "WORKSPACE_PROTOCOL.md") },
2292
3645
  ...roomSkillEntries(layout, agents)
2293
3646
  ];
2294
3647
  const providers = {};
2295
3648
  const profiles = [];
2296
3649
  const checks = [];
2297
3650
  const binaries = {};
2298
- for (const id of agents) {
2299
- const agent = AGENTS[id];
3651
+ for (const id2 of agents) {
3652
+ const agent = AGENTS[id2];
2300
3653
  const plan = await agent.build(layout, roles, build);
2301
3654
  entries.push(...plan.entries);
2302
3655
  checks.push(...plan.checks);
@@ -2306,13 +3659,13 @@ async function buildDesired(layout, agents, roles, build = {}) {
2306
3659
  const collisions = plan.entries.filter((entry) => credentialPaths.has(entry.path));
2307
3660
  if (collisions.length > 0) {
2308
3661
  checks.push(fail(
2309
- `${id}.credentials`,
3662
+ `${id2}.credentials`,
2310
3663
  `${agent.label} produced managed entries for preserve-only credential paths: ${collisions.map((entry) => entry.path).join(", ")}.`,
2311
3664
  "This is an adapter defect; do not apply the plan."
2312
3665
  ));
2313
3666
  }
2314
3667
  if (plan.binary === void 0) continue;
2315
- binaries[id] = plan.binary;
3668
+ binaries[id2] = plan.binary;
2316
3669
  Object.assign(providers, roleProviders(layout, agent, {
2317
3670
  binary: plan.binary,
2318
3671
  ...plan.argv ? { argv: plan.argv } : {},
@@ -2320,33 +3673,42 @@ async function buildDesired(layout, agents, roles, build = {}) {
2320
3673
  }, roles));
2321
3674
  profiles.push(...roleProfiles(agent, roles));
2322
3675
  }
2323
- if (agents.every((id) => binaries[id] !== void 0)) {
3676
+ if (agents.every((id2) => binaries[id2] !== void 0)) {
2324
3677
  entries.push({
2325
3678
  kind: "file",
2326
- path: join12(layout.roomHome, AUTHENTICATION_GUIDE),
3679
+ path: join16(layout.roomHome, AUTHENTICATION_GUIDE),
2327
3680
  content: renderAuthenticationGuide(layout.roomHome, binaries, agents, roles)
2328
3681
  });
2329
3682
  checks.push(warn(
2330
3683
  "room.authentication-not-validated",
2331
- `Role authentication was not validated. Login instructions are managed at ${join12(layout.roomHome, AUTHENTICATION_GUIDE)}.`,
3684
+ `Role authentication was not validated. Login instructions are managed at ${join16(layout.roomHome, AUTHENTICATION_GUIDE)}.`,
2332
3685
  "After setup --apply, follow that guide or run: paseo-room auth login <agent> <role>"
2333
3686
  ));
2334
3687
  }
2335
- const pluginPath = agents.includes("claude") ? claudeCarrierPluginDir(layout) : void 0;
2336
- if (pluginPath !== void 0) entries.push(...claudeCarrierEntries(layout, roles));
2337
- entries.push({ kind: "file", path: join12(layout.roomHome, MARKER), content: renderMarker(package_default.version, agents, roles, contractDigest(), markerMemoryContract(agents, build.memoryContract ?? true)) });
2338
- return { entries, providers, profiles, checks, ...pluginPath === void 0 ? {} : { pluginPath } };
3688
+ const plugins = [];
3689
+ if (agents.includes("claude")) {
3690
+ plugins.push(carrierSpec(layout));
3691
+ entries.push(...claudeCarrierEntries(layout, roles));
3692
+ }
3693
+ let runtime;
3694
+ if (build.runtime === true) {
3695
+ plugins.push(runtimeSpec(layout));
3696
+ entries.push(...runtimePluginEntries(layout, agents, roles));
3697
+ runtime = { enabled: true, generation: renderRuntimeManifest(agents, roles).roomGeneration, schema: 1 };
3698
+ }
3699
+ entries.push({ kind: "file", path: join16(layout.roomHome, MARKER), content: renderMarker(package_default.version, agents, roles, contractDigest(), markerMemoryContract(agents, build.memoryContract ?? true), runtime) });
3700
+ return { entries, providers, profiles, checks, plugins, ...runtime === void 0 ? {} : { runtime } };
2339
3701
  }
2340
3702
  function contractProvenance(previous) {
2341
- const digest = contractDigest();
3703
+ const digest3 = contractDigest();
2342
3704
  if (!previous) return [];
2343
- if (previous.contract === digest) {
2344
- return [pass("room.contract", `Role contract generation ${digest} matches this package.`)];
3705
+ if (previous.contract === digest3) {
3706
+ return [pass("room.contract", `Role contract generation ${digest3} matches this package.`)];
2345
3707
  }
2346
3708
  const held = previous.contract === void 0 ? `was installed before the room recorded its contract generation (package ${previous.version})` : `holds contract generation ${previous.contract}`;
2347
3709
  return [warn(
2348
3710
  "room.contract",
2349
- `This room ${held}; this package renders ${digest}. Managed role documents are rewritten by setup, and a running seat keeps the text it started with.`,
3711
+ `This room ${held}; this package renders ${digest3}. Managed role documents are rewritten by setup, and a running seat keeps the text it started with.`,
2350
3712
  "Run: paseo-room setup --apply, then restart the affected seats."
2351
3713
  )];
2352
3714
  }
@@ -2369,14 +3731,14 @@ function fileDriftSummary(drifted) {
2369
3731
  return `${count} ${subject} missing or outdated, and ${String(removals)} of them are present but suppressed by this room.`;
2370
3732
  }
2371
3733
  function fileDriftDetail(targets) {
2372
- const catalogs = targets.filter((target) => target.endsWith(`${sep2}model-catalog.json`));
3734
+ const catalogs = targets.filter((target) => target.endsWith(`${sep3}model-catalog.json`));
2373
3735
  if (catalogs.length === 0) return "";
2374
3736
  const count = catalogs.length === 1 ? "One of them is a generated Codex model catalog" : `${String(catalogs.length)} of them are generated Codex model catalogs`;
2375
3737
  return ` ${count}, so those seats currently lack the scrubbed catalog closure rather than only older contract text.`;
2376
3738
  }
2377
3739
  async function safeDirectory(path, checks) {
2378
3740
  try {
2379
- const stat2 = await lstat4(path);
3741
+ const stat2 = await lstat5(path);
2380
3742
  if (stat2.isDirectory()) return true;
2381
3743
  checks.push(fail(
2382
3744
  "room.path-safety",
@@ -2400,10 +3762,10 @@ async function roomPathSafety(layout, agents, roles) {
2400
3762
  const room = sharedRoom(layout);
2401
3763
  const roomIsSafe = await safeDirectory(room, checks);
2402
3764
  if (roomIsSafe && agents.length > 0) {
2403
- const skills = join12(room, "skills");
3765
+ const skills = join16(room, "skills");
2404
3766
  if (await safeDirectory(skills, checks)) {
2405
3767
  const source = roomSkillSource(layout);
2406
- if (await safeDirectory(source, checks)) await safeDirectory(join12(source, "references"), checks);
3768
+ if (await safeDirectory(source, checks)) await safeDirectory(join16(source, "references"), checks);
2407
3769
  }
2408
3770
  const projections = skillProjectionRoot(layout);
2409
3771
  if (await safeDirectory(projections, checks)) {
@@ -2413,16 +3775,16 @@ async function roomPathSafety(layout, agents, roles) {
2413
3775
  }
2414
3776
  }
2415
3777
  }
2416
- const rolesHome = join12(layout.roomHome, "roles");
3778
+ const rolesHome = join16(layout.roomHome, "roles");
2417
3779
  if (!await safeDirectory(rolesHome, checks)) return checks;
2418
3780
  for (const agent of agents) {
2419
- const agentHome = join12(rolesHome, agent);
3781
+ const agentHome = join16(rolesHome, agent);
2420
3782
  if (!await safeDirectory(agentHome, checks)) continue;
2421
3783
  for (const role of roles) await safeDirectory(roleHome(layout, agent, role), checks);
2422
3784
  }
2423
3785
  return checks;
2424
3786
  }
2425
- async function staleFrom(layout, previous, agents, roles) {
3787
+ async function staleFrom(layout, previous, agents, roles, runtime) {
2426
3788
  if (!previous) return NOTHING_STALE;
2427
3789
  const kept = new Set(agents.flatMap((agent) => roles.map((role) => providerId(agent, role))));
2428
3790
  const providerIds = [];
@@ -2440,110 +3802,148 @@ async function staleFrom(layout, previous, agents, roles) {
2440
3802
  return {
2441
3803
  providerIds,
2442
3804
  profileIds,
2443
- removePlugin: previous.agents.includes("claude") && !agents.includes("claude"),
3805
+ removePlugins: [
3806
+ ...previous.agents.includes("claude") && !agents.includes("claude") ? [carrierSpec(layout)] : [],
3807
+ ...previous.runtime?.enabled === true && !runtime ? [runtimeSpec(layout)] : []
3808
+ ],
2444
3809
  retainedDirectories
2445
3810
  };
2446
3811
  }
2447
- function pluginPlan(desired, stale, plugins, expectedPath) {
2448
- const livePlugin = plugins.find((plugin) => plugin.id === CLAUDE_CARRIER_PLUGIN_ID);
2449
- const shouldManage = desired.pluginPath !== void 0 || stale.removePlugin;
2450
- if (!shouldManage) return { checks: [] };
2451
- if (livePlugin !== void 0 && livePlugin.path !== expectedPath) {
3812
+ function pluginPlan(spec, wanted, plugins) {
3813
+ const livePlugin = plugins.find((plugin) => plugin.id === spec.id);
3814
+ if (livePlugin !== void 0 && livePlugin.path !== spec.path) {
2452
3815
  return {
2453
3816
  livePlugin,
2454
3817
  checks: [fail(
2455
- "claude.plugin.path",
2456
- `Plugin ${CLAUDE_CARRIER_PLUGIN_ID} is registered from ${livePlugin.path}, not the room-owned path ${expectedPath}.`,
3818
+ `${spec.check}.path`,
3819
+ `Plugin ${spec.id} is registered from ${livePlugin.path}, not the room-owned path ${spec.path}.`,
2457
3820
  "Move or remove that conflicting plugin registration manually, then run setup again."
2458
3821
  )]
2459
3822
  };
2460
3823
  }
2461
- if (desired.pluginPath !== void 0) {
3824
+ if (wanted) {
2462
3825
  const healthy = livePlugin?.enabled === true && livePlugin.status === "running" && livePlugin.error === void 0;
2463
3826
  return {
2464
3827
  ...livePlugin === void 0 ? {} : { livePlugin },
2465
- operation: { action: livePlugin === void 0 ? "create" : healthy ? "noop" : "update", kind: "plugin", target: CLAUDE_CARRIER_PLUGIN_ID },
3828
+ operation: { action: livePlugin === void 0 ? "create" : healthy ? "noop" : "update", kind: "plugin", target: spec.id },
2466
3829
  checks: []
2467
3830
  };
2468
3831
  }
2469
3832
  return {
2470
3833
  ...livePlugin === void 0 ? {} : { livePlugin },
2471
- operation: { action: livePlugin === void 0 ? "noop" : "remove", kind: "plugin", target: CLAUDE_CARRIER_PLUGIN_ID },
3834
+ operation: { action: livePlugin === void 0 ? "noop" : "remove", kind: "plugin", target: spec.id },
2472
3835
  checks: []
2473
3836
  };
2474
3837
  }
2475
- async function planRoom(session, desired, stale, expectedPluginPath) {
2476
- const inspectPlugins = desired.pluginPath !== void 0 || stale.removePlugin;
3838
+ async function planRoom(session, desired, stale) {
3839
+ const inspectPlugins = desired.plugins.length > 0 || stale.removePlugins.length > 0;
2477
3840
  const [live, liveProfiles, plugins] = await Promise.all([
2478
3841
  session.readProviders(),
2479
3842
  session.readProfiles(),
2480
3843
  inspectPlugins ? session.listPlugins() : Promise.resolve([])
2481
3844
  ]);
2482
3845
  const byId = new Map(liveProfiles.map((entry) => [String(entry.id), entry]));
2483
- const plugin = pluginPlan(desired, stale, plugins, expectedPluginPath);
3846
+ const planned = [
3847
+ ...desired.plugins.map((spec) => pluginPlan(spec, true, plugins)),
3848
+ ...stale.removePlugins.map((spec) => pluginPlan(spec, false, plugins))
3849
+ ];
3850
+ const livePlugins = /* @__PURE__ */ new Map();
3851
+ for (const plugin of planned) if (plugin.livePlugin !== void 0) livePlugins.set(plugin.livePlugin.id, plugin.livePlugin);
2484
3852
  return {
2485
3853
  liveProfiles,
2486
- ...plugin.livePlugin === void 0 ? {} : { livePlugin: plugin.livePlugin },
2487
- pluginChecks: plugin.checks,
3854
+ livePlugins,
3855
+ pluginChecks: planned.flatMap((plugin) => plugin.checks),
2488
3856
  operations: [
2489
3857
  ...await planEntries(desired.entries),
2490
- ...Object.entries(desired.providers).map(([id, provider]) => ({
2491
- action: !(id in live) ? "create" : providerMatches(provider, live[id]) ? "noop" : "update",
3858
+ ...Object.entries(desired.providers).map(([id2, provider]) => ({
3859
+ action: !(id2 in live) ? "create" : providerMatches(provider, live[id2]) ? "noop" : "update",
2492
3860
  kind: "provider",
2493
- target: id
3861
+ target: id2
2494
3862
  })),
2495
3863
  ...desired.profiles.map((profile) => ({
2496
3864
  action: !byId.has(profile.id) ? "create" : profileMatches(profile, byId.get(profile.id)) ? "noop" : "update",
2497
3865
  kind: "profile",
2498
3866
  target: profile.id
2499
3867
  })),
2500
- ...stale.providerIds.filter((id) => id in live).map((id) => ({ action: "remove", kind: "provider", target: id })),
2501
- ...stale.profileIds.filter((id) => byId.has(id)).map((id) => ({ action: "remove", kind: "profile", target: id })),
2502
- ...plugin.operation === void 0 ? [] : [plugin.operation]
3868
+ ...stale.providerIds.filter((id2) => id2 in live).map((id2) => ({ action: "remove", kind: "provider", target: id2 })),
3869
+ ...stale.profileIds.filter((id2) => byId.has(id2)).map((id2) => ({ action: "remove", kind: "profile", target: id2 })),
3870
+ ...planned.flatMap((plugin) => plugin.operation === void 0 ? [] : [plugin.operation])
2503
3871
  ]
2504
3872
  };
2505
3873
  }
2506
- function pluginsEnabledCheck(enabled) {
2507
- return enabled ? pass("claude.plugin.enabled", "Paseo plugins are enabled for the required Claude contract carrier.") : fail(
2508
- "claude.plugin.enabled",
2509
- "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.",
3874
+ function pluginsEnabledCheck(spec, enabled) {
3875
+ return enabled ? pass(`${spec.check}.enabled`, spec.enabledPass) : fail(
3876
+ `${spec.check}.enabled`,
3877
+ spec.enabledFail,
2510
3878
  "Review that trust boundary, enable plugins in Paseo Settings, reload Paseo, then run setup again."
2511
3879
  );
2512
3880
  }
2513
- function pluginRuntimeCheck(plugin, expectedPath) {
3881
+ function pluginRuntimeCheck(spec, plugin) {
2514
3882
  if (plugin === void 0) {
2515
- return fail("claude.plugin.runtime", `Plugin ${CLAUDE_CARRIER_PLUGIN_ID} is not registered.`, "Run: paseo-room setup --apply");
3883
+ return fail(`${spec.check}.runtime`, `Plugin ${spec.id} is not registered.`, "Run: paseo-room setup --apply");
2516
3884
  }
2517
- if (plugin.path !== expectedPath) {
3885
+ if (plugin.path !== spec.path) {
2518
3886
  return fail(
2519
- "claude.plugin.runtime",
2520
- `Plugin ${CLAUDE_CARRIER_PLUGIN_ID} is registered from ${plugin.path}, not ${expectedPath}.`,
3887
+ `${spec.check}.runtime`,
3888
+ `Plugin ${spec.id} is registered from ${plugin.path}, not ${spec.path}.`,
2521
3889
  "Move or remove that conflicting plugin registration manually, then run setup again."
2522
3890
  );
2523
3891
  }
2524
3892
  if (!plugin.enabled || plugin.status !== "running" || plugin.error !== void 0) {
2525
3893
  const detail = plugin.error === void 0 ? `status ${plugin.status}` : `${plugin.status}: ${plugin.error}`;
2526
- return fail("claude.plugin.runtime", `Plugin ${CLAUDE_CARRIER_PLUGIN_ID} is not running (${detail}).`, "Run: paseo-room setup --apply");
3894
+ return fail(`${spec.check}.runtime`, `Plugin ${spec.id} is not running (${detail}).`, "Run: paseo-room setup --apply");
2527
3895
  }
2528
- return pass("claude.plugin.runtime", `Claude contract carrier plugin is running from ${expectedPath}.`);
3896
+ return pass(`${spec.check}.runtime`, `${spec.running} is running from ${spec.path}.`);
2529
3897
  }
2530
- async function reconcilePlugin(session, desired, plan, pending, expectedPath) {
2531
- if (desired.pluginPath === void 0) {
2532
- if (plan.livePlugin !== void 0) await session.removePlugin(CLAUDE_CARRIER_PLUGIN_ID);
2533
- return [];
3898
+ async function reconcilePlugins(session, desired, stale, plan, pending) {
3899
+ for (const spec of stale.removePlugins) {
3900
+ if (plan.livePlugins.has(spec.id)) await session.removePlugin(spec.id);
2534
3901
  }
2535
- let plugin = plan.livePlugin;
2536
- if (plugin === void 0) {
2537
- plugin = await session.installPlugin(expectedPath, CLAUDE_CARRIER_PLUGIN_ID);
2538
- } else {
2539
- if (!plugin.enabled) plugin = await session.enablePlugin(CLAUDE_CARRIER_PLUGIN_ID);
2540
- const filesChanged = pending.some(
2541
- (operation) => (operation.kind === "file" || operation.kind === "dir") && operation.target.startsWith(`${expectedPath}${sep2}`)
2542
- );
2543
- const needsReload = filesChanged || plugin.status !== "running" || plugin.error !== void 0;
2544
- if (needsReload) plugin = await session.reloadPlugin(CLAUDE_CARRIER_PLUGIN_ID);
3902
+ const checks = [];
3903
+ for (const spec of desired.plugins) {
3904
+ let plugin = plan.livePlugins.get(spec.id);
3905
+ if (plugin === void 0) {
3906
+ plugin = await session.installPlugin(spec.path, spec.id);
3907
+ } else {
3908
+ if (!plugin.enabled) plugin = await session.enablePlugin(spec.id);
3909
+ const filesChanged = pending.some(
3910
+ (operation) => (operation.kind === "file" || operation.kind === "dir") && operation.target.startsWith(`${spec.path}${sep3}`)
3911
+ );
3912
+ const needsReload = filesChanged || plugin.status !== "running" || plugin.error !== void 0;
3913
+ if (needsReload) plugin = await session.reloadPlugin(spec.id);
3914
+ }
3915
+ checks.push(pluginRuntimeCheck(spec, plugin));
3916
+ }
3917
+ return checks;
3918
+ }
3919
+ function pluginRangeCheck(spec, version) {
3920
+ return satisfies(version, spec.paseoRange) ? pass(spec.rangeCheck, `Paseo ${version} is inside the ${spec.running} range ${spec.paseoRange}.`) : fail(spec.rangeCheck, `${spec.running} supports Paseo ${spec.paseoRange}; the running daemon is ${version}.`, spec.rangeRemedy);
3921
+ }
3922
+ async function runtimeDeselectionCheck(layout) {
3923
+ const summary = await inspectRuntimeState(layout.roomHome);
3924
+ if (summary.projects === 0) return [];
3925
+ if (summary.blockers.length > 0) {
3926
+ return [fail(
3927
+ "runtime.deselect",
3928
+ `Runtime coordination still has active or uncertain work: ${describeBlockers(summary)}.`,
3929
+ "Keep --runtime and finish, close or abandon that work first; the runtime state is preserved either way."
3930
+ )];
2545
3931
  }
2546
- return [pluginRuntimeCheck(plugin, expectedPath)];
3932
+ return [warn(
3933
+ "runtime.state-retained",
3934
+ `Runtime coordination is quiet; its recorded state for ${String(summary.projects)} project(s) stays at ${summary.root}.`,
3935
+ "Export it with: paseo-room export --out <dir>, or keep it for a later --runtime."
3936
+ )];
3937
+ }
3938
+ async function runtimeRemovalWarning(layout) {
3939
+ const summary = await inspectRuntimeState(layout.roomHome).catch(() => void 0);
3940
+ if (summary === void 0 || summary.projects === 0) return [];
3941
+ const active = summary.blockers.length > 0 ? ` It still has active or uncertain work: ${describeBlockers(summary)}.` : "";
3942
+ return [warn(
3943
+ "room.remove.runtime-state",
3944
+ `Deleting ${layout.roomHome} also deletes runtime coordination history for ${String(summary.projects)} project(s).${active}`,
3945
+ "Export it first with: paseo-room export --out <dir>"
3946
+ )];
2547
3947
  }
2548
3948
  function markerMemoryContract(agents, memoryContract) {
2549
3949
  return agents.includes("claude") ? memoryContract : void 0;
@@ -2569,23 +3969,29 @@ async function setup(options = {}) {
2569
3969
  const pathSafety = await roomPathSafety(layout, safetyAgents, ROLES);
2570
3970
  if (hasFailure(pathSafety)) return failed("setup", pathSafety);
2571
3971
  const compatibilityAgents = [.../* @__PURE__ */ new Set([...agents, ...previous?.agents ?? []])];
2572
- const daemon = await checkDaemon(layout, options.env, minimumPaseoVersion(compatibilityAgents));
3972
+ const runtime = options.runtime === true;
3973
+ const daemon = await checkDaemon(layout, options.env, minimumPaseoVersion(compatibilityAgents, runtime || previous?.runtime?.enabled === true));
2573
3974
  if (!daemon.daemon) return failed("setup", daemon.checks);
2574
- const desired = await buildDesired(layout, agents, ROLES, { memoryContract });
2575
- const stale = await staleFrom(layout, previous, agents, ROLES);
3975
+ const version = daemon.daemon.version;
3976
+ const desired = await buildDesired(layout, agents, ROLES, { memoryContract, runtime });
3977
+ const stale = await staleFrom(layout, previous, agents, ROLES, runtime);
3978
+ const runtimeChecks = [
3979
+ ...desired.plugins.map((spec) => pluginRangeCheck(spec, version)),
3980
+ ...stale.removePlugins.some((spec) => spec.id === RUNTIME_PLUGIN_ID) ? await runtimeDeselectionCheck(layout) : []
3981
+ ];
2576
3982
  const retained = stale.retainedDirectories.length === 0 ? [] : [warn(
2577
3983
  "room.stale-role-homes-preserved",
2578
3984
  `Preserved ${String(stale.retainedDirectories.length)} deselected role homes because they may contain role-owned credentials or runtime state.`,
2579
3985
  `Use paseo-room remove --apply to delete the entire room after reviewing its credential warning, or remove deselected homes manually: ${stale.retainedDirectories.join(", ")}`
2580
3986
  )];
2581
- const checks = [...daemon.checks, ...desired.checks, ...retained];
3987
+ const checks = [...daemon.checks, ...runtimeChecks, ...desired.checks, ...retained];
2582
3988
  if (hasFailure(checks)) return failed("setup", checks);
2583
3989
  return withSession(daemon.daemon, async (session) => {
2584
- const expectedPluginPath = claudeCarrierPluginDir(layout);
2585
- const plan = await planRoom(session, desired, stale, expectedPluginPath);
3990
+ const plan = await planRoom(session, desired, stale);
2586
3991
  const { operations, liveProfiles } = plan;
2587
3992
  const pending = operations.filter((operation) => operation.action !== "noop");
2588
- const pluginPreconditions = desired.pluginPath === void 0 ? plan.pluginChecks : [pluginsEnabledCheck(await session.pluginsEnabled()), ...plan.pluginChecks];
3993
+ const enabled = desired.plugins.length === 0 ? true : await session.pluginsEnabled();
3994
+ const pluginPreconditions = [...desired.plugins.map((spec) => pluginsEnabledCheck(spec, enabled)), ...plan.pluginChecks];
2589
3995
  const preApplyDiagnostics = [...checks, ...pluginPreconditions, ...memoryContractDiagnostic(agents, memoryContract), ...contractProvenance(previous), ...thinkingDiagnostics(liveProfiles, desired.profiles)];
2590
3996
  if (hasFailure(preApplyDiagnostics) || !options.apply) {
2591
3997
  return {
@@ -2596,7 +4002,7 @@ async function setup(options = {}) {
2596
4002
  operations
2597
4003
  };
2598
4004
  }
2599
- const markerPath = join12(layout.roomHome, MARKER);
4005
+ const markerPath = join16(layout.roomHome, MARKER);
2600
4006
  const markerEntries = desired.entries.filter((entry) => entry.path === markerPath);
2601
4007
  await applyEntries(desired.entries.filter((entry) => entry.path !== markerPath));
2602
4008
  const ids = Object.keys(desired.providers);
@@ -2608,14 +4014,15 @@ async function setup(options = {}) {
2608
4014
  await session.writeProfiles(appliedProfiles);
2609
4015
  }
2610
4016
  await session.refresh(ids);
2611
- const pluginChecks = await reconcilePlugin(session, desired, plan, pending, expectedPluginPath);
4017
+ const pluginChecks = await reconcilePlugins(session, desired, stale, plan, pending);
2612
4018
  await applyEntries(markerEntries);
2613
4019
  const appliedMarker = {
2614
4020
  version: package_default.version,
2615
4021
  agents: [...agents],
2616
4022
  roles: [...ROLES],
2617
4023
  contract: contractDigest(),
2618
- ...markerMemoryContract(agents, memoryContract) === false ? { claudeMemoryContract: false } : {}
4024
+ ...markerMemoryContract(agents, memoryContract) === false ? { claudeMemoryContract: false } : {},
4025
+ ...desired.runtime === void 0 ? {} : { runtime: desired.runtime }
2619
4026
  };
2620
4027
  const appliedChecks = [
2621
4028
  ...checks,
@@ -2647,17 +4054,23 @@ async function verify(options = {}) {
2647
4054
  if (invalid.length > 0) return failed("verify", invalid);
2648
4055
  const pathSafety = await roomPathSafety(layout, marker.agents, marker.roles);
2649
4056
  if (hasFailure(pathSafety)) return failed("verify", pathSafety);
2650
- const daemon = await checkDaemon(layout, options.env, minimumPaseoVersion(marker.agents));
4057
+ const runtime = marker.runtime?.enabled === true;
4058
+ const daemon = await checkDaemon(layout, options.env, minimumPaseoVersion(marker.agents, runtime));
2651
4059
  if (!daemon.daemon) return failed("verify", daemon.checks);
4060
+ const version = daemon.daemon.version;
2652
4061
  const desired = await buildDesired(layout, marker.agents, marker.roles, {
2653
- // The room's own recorded choice, so verify compares against what setup wrote.
2654
- memoryContract: marker.claudeMemoryContract ?? true
4062
+ // The room's own recorded choices, so verify compares against what setup wrote.
4063
+ memoryContract: marker.claudeMemoryContract ?? true,
4064
+ runtime
2655
4065
  });
2656
- const checks = [...daemon.checks, ...desired.checks];
2657
- if (hasFailure(checks)) return failed("verify", checks);
4066
+ const runtimeChecks = [
4067
+ ...desired.plugins.map((spec) => pluginRangeCheck(spec, version)),
4068
+ ...runtime ? [marker.runtime?.generation === desired.runtime?.generation ? pass("runtime.generation", `Runtime manifest generation ${String(desired.runtime?.generation)} matches this package.`) : fail("runtime.generation", `This room was set up with runtime generation ${String(marker.runtime?.generation)}; this package generates ${String(desired.runtime?.generation)}.`, "Run: paseo-room setup --runtime --apply")] : []
4069
+ ];
4070
+ const checks = [...daemon.checks, ...runtimeChecks, ...desired.checks];
4071
+ if (hasFailure(daemon.checks) || hasFailure(desired.checks)) return failed("verify", checks);
2658
4072
  return withSession(daemon.daemon, async (session) => {
2659
- const expectedPluginPath = claudeCarrierPluginDir(layout);
2660
- const plan = await planRoom(session, desired, NOTHING_STALE, expectedPluginPath);
4073
+ const plan = await planRoom(session, desired, NOTHING_STALE);
2661
4074
  const { operations, liveProfiles } = plan;
2662
4075
  const drifted = operations.filter((operation) => operation.action !== "noop");
2663
4076
  const count = (kind) => drifted.filter((operation) => operation.kind === kind).length;
@@ -2665,11 +4078,12 @@ async function verify(options = {}) {
2665
4078
  const profiles = count("profile");
2666
4079
  const driftedFiles = drifted.filter((operation) => !["provider", "profile", "plugin"].includes(operation.kind));
2667
4080
  const files = driftedFiles.length;
2668
- const pluginChecks = desired.pluginPath === void 0 ? [] : [
2669
- pluginsEnabledCheck(await session.pluginsEnabled()),
2670
- ...plan.pluginChecks,
2671
- pluginRuntimeCheck(plan.livePlugin, expectedPluginPath)
2672
- ];
4081
+ const enabled = desired.plugins.length === 0 ? true : await session.pluginsEnabled();
4082
+ const pluginChecks = desired.plugins.flatMap((spec) => [
4083
+ pluginsEnabledCheck(spec, enabled),
4084
+ ...plan.pluginChecks.filter((check) => check.id.startsWith(`${spec.check}.`)),
4085
+ pluginRuntimeCheck(spec, plan.livePlugins.get(spec.id))
4086
+ ]);
2673
4087
  const all = [
2674
4088
  ...checks,
2675
4089
  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"),
@@ -2702,22 +4116,26 @@ async function remove(options = {}) {
2702
4116
  if (invalid.length > 0) return failed("remove", invalid);
2703
4117
  const ids = marker.agents.flatMap((agent) => marker.roles.map((role) => providerId(agent, role)));
2704
4118
  const profiles = new Set(marker.agents.flatMap((agent) => marker.roles.map((role) => profileId(agent, role))));
2705
- const hasClaude = marker.agents.includes("claude");
4119
+ const plugins = [
4120
+ ...marker.agents.includes("claude") ? [carrierSpec(layout)] : [],
4121
+ ...marker.runtime?.enabled === true ? [runtimeSpec(layout)] : []
4122
+ ];
2706
4123
  const operations = [
2707
- ...ids.map((id) => ({ action: "remove", kind: "provider", target: id })),
2708
- ...[...profiles].map((id) => ({ action: "remove", kind: "profile", target: id })),
2709
- ...hasClaude ? [{ action: "remove", kind: "plugin", target: CLAUDE_CARRIER_PLUGIN_ID }] : [],
4124
+ ...ids.map((id2) => ({ action: "remove", kind: "provider", target: id2 })),
4125
+ ...[...profiles].map((id2) => ({ action: "remove", kind: "profile", target: id2 })),
4126
+ ...plugins.map((spec) => ({ action: "remove", kind: "plugin", target: spec.id })),
2710
4127
  { action: "remove", kind: "dir", target: layout.roomHome }
2711
4128
  ];
2712
4129
  const credentialWarning = warn(
2713
4130
  "room.remove.credentials",
2714
4131
  `Deleting ${layout.roomHome} will delete role-owned credential files stored inside it. Native OS keyring entries are not inspected or deleted and may remain. Operator agent-home credentials are untouched.`
2715
4132
  );
4133
+ const runtimeWarnings = await runtimeRemovalWarning(layout);
2716
4134
  if (!options.apply) {
2717
- return { command: "remove", outcome: "changes-planned", changed: false, checks: [credentialWarning], operations };
4135
+ return { command: "remove", outcome: "changes-planned", changed: false, checks: [credentialWarning, ...runtimeWarnings], operations };
2718
4136
  }
2719
4137
  const daemon = await checkDaemon(layout, options.env);
2720
- const checks = [credentialWarning];
4138
+ const checks = [credentialWarning, ...runtimeWarnings];
2721
4139
  if (!daemon.daemon) {
2722
4140
  return failed("remove", [
2723
4141
  credentialWarning,
@@ -2731,13 +4149,15 @@ async function remove(options = {}) {
2731
4149
  }
2732
4150
  try {
2733
4151
  await withSession(daemon.daemon, async (session) => {
2734
- if (hasClaude) {
2735
- const plugin = (await session.listPlugins()).find((entry) => entry.id === CLAUDE_CARRIER_PLUGIN_ID);
2736
- const expectedPath = claudeCarrierPluginDir(layout);
2737
- if (plugin !== void 0 && plugin.path !== expectedPath) {
2738
- throw new Error(`refusing to remove foreign plugin path ${plugin.path}`);
4152
+ if (plugins.length > 0) {
4153
+ const live2 = await session.listPlugins();
4154
+ for (const spec of plugins) {
4155
+ const plugin = live2.find((entry) => entry.id === spec.id);
4156
+ if (plugin !== void 0 && plugin.path !== spec.path) throw new Error(`refusing to remove foreign plugin path ${plugin.path}`);
4157
+ }
4158
+ for (const spec of plugins) {
4159
+ if (live2.some((entry) => entry.id === spec.id)) await session.removePlugin(spec.id);
2739
4160
  }
2740
- if (plugin !== void 0) await session.removePlugin(CLAUDE_CARRIER_PLUGIN_ID);
2741
4161
  }
2742
4162
  await session.removeProviders(ids);
2743
4163
  const live = await session.readProfiles();
@@ -2756,7 +4176,7 @@ async function remove(options = {}) {
2756
4176
  ]);
2757
4177
  }
2758
4178
  try {
2759
- await rm3(layout.roomHome, { recursive: true, force: true });
4179
+ await rm4(layout.roomHome, { recursive: true, force: true });
2760
4180
  } catch (error) {
2761
4181
  const detail = error instanceof Error ? error.message : "unknown filesystem error";
2762
4182
  checks.push(fail(
@@ -2787,9 +4207,11 @@ var init_commands = __esm({
2787
4207
  init_instructions();
2788
4208
  init_skills();
2789
4209
  init_room();
4210
+ init_runtime();
4211
+ init_runtime_state();
2790
4212
  init_roles();
2791
4213
  AGENTS = { codex: codexAgent, claude: claudeAgent, pi: piAgent };
2792
- NOTHING_STALE = { providerIds: [], profileIds: [], removePlugin: false, retainedDirectories: [] };
4214
+ NOTHING_STALE = { providerIds: [], profileIds: [], removePlugins: [], retainedDirectories: [] };
2793
4215
  }
2794
4216
  });
2795
4217
 
@@ -2840,7 +4262,15 @@ async function runWizard(prompts, emit, write, options = {}, onAction) {
2840
4262
  if (typeof carrier === "symbol") return cancelled();
2841
4263
  claudeMemoryContract = carrier === "both";
2842
4264
  }
2843
- const setupOptions = { ...options, agents, claudeMemoryContract };
4265
+ const coordination = await prompts.select({
4266
+ message: "Runtime coordination (preview): track assignments, Peer reports and writer ownership through a trusted Paseo plugin?",
4267
+ options: [
4268
+ { value: "off", label: "No \u2014 keep the room as it is (default)" },
4269
+ { value: "runtime", label: "Yes \u2014 install the paseo-room-runtime plugin (trusted, unsandboxed code)" }
4270
+ ]
4271
+ });
4272
+ if (typeof coordination === "symbol") return cancelled();
4273
+ const setupOptions = { ...options, agents, claudeMemoryContract, ...coordination === "runtime" ? { runtime: true } : {} };
2844
4274
  const preview = await setup(setupOptions);
2845
4275
  const status = emit(preview);
2846
4276
  if (preview.outcome !== "changes-planned") return status;
@@ -2866,9 +4296,105 @@ var init_wizard = __esm({
2866
4296
  init_package();
2867
4297
  init_auth();
2868
4298
  init_commands();
2869
- init_fsops();
2870
4299
  import { Command, CommanderError, InvalidArgumentError } from "commander";
2871
4300
 
4301
+ // src/export.ts
4302
+ init_package();
4303
+ init_layout();
4304
+ init_result();
4305
+ init_schema();
4306
+ init_project();
4307
+ import { copyFile, mkdir as mkdir4, readdir as readdir6, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
4308
+ import { join as join17, resolve as resolve4 } from "path";
4309
+ var LIMITATION = "Briefs, answers, reports and gate commands are stored as written and cannot be proven secret-free; review the export before sharing it.";
4310
+ async function isEmptyOrAbsent(path) {
4311
+ try {
4312
+ return (await readdir6(path)).length === 0;
4313
+ } catch {
4314
+ return true;
4315
+ }
4316
+ }
4317
+ async function exportRuntime(options = {}) {
4318
+ const layout = resolveLayout(options, options.env);
4319
+ const root = runtimeRoot(layout.roomHome);
4320
+ let projects;
4321
+ try {
4322
+ projects = (await readdir6(join17(root, "projects"))).sort();
4323
+ } catch {
4324
+ projects = [];
4325
+ }
4326
+ if (projects.length === 0) {
4327
+ return failed("export", [fail("export.nothing", `No runtime state was found under ${root}.`, "Runtime coordination records state only after setup --runtime and a first assignment.")]);
4328
+ }
4329
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4330
+ const destination = resolve4(options.out ?? join17(root, "exports", stamp));
4331
+ if (!await isEmptyOrAbsent(destination)) {
4332
+ return failed("export", [fail("export.destination", `${destination} already exists and is not empty.`, "Choose a new or empty --out directory.")]);
4333
+ }
4334
+ const operations = [{ action: "create", kind: "dir", target: destination }];
4335
+ const summary = [];
4336
+ const copies = [];
4337
+ for (const name of projects) {
4338
+ const source = join17(root, "projects", name);
4339
+ const target = join17(destination, "projects", name);
4340
+ let meta;
4341
+ try {
4342
+ meta = JSON.parse(await readFile5(join17(source, "meta.json"), "utf8"));
4343
+ } catch {
4344
+ meta = {};
4345
+ }
4346
+ const entry = { projectId: meta.projectId ?? name, canonicalRoot: meta.canonicalRoot ?? "unknown", events: 0, omitted: [], gateOutputs: 0 };
4347
+ copies.push([join17(source, "meta.json"), join17(target, "meta.json")]);
4348
+ for (const file of (await readdir6(join17(source, "events")).catch(() => [])).sort()) {
4349
+ if (file.startsWith(".tmp-")) continue;
4350
+ let valid2;
4351
+ try {
4352
+ valid2 = readEvent(JSON.parse(await readFile5(join17(source, "events", file), "utf8"))).ok;
4353
+ } catch {
4354
+ valid2 = false;
4355
+ }
4356
+ if (valid2) {
4357
+ entry.events += 1;
4358
+ copies.push([join17(source, "events", file), join17(target, "events", file)]);
4359
+ } else entry.omitted.push(file);
4360
+ }
4361
+ for (const file of (await readdir6(join17(source, "gates")).catch(() => [])).sort()) {
4362
+ if (file.endsWith(".result.json")) copies.push([join17(source, "gates", file), join17(target, "gates", file)]);
4363
+ else if (file.endsWith(".log") && options.includeGateOutput === true) {
4364
+ entry.gateOutputs += 1;
4365
+ copies.push([join17(source, "gates", file), join17(target, "gates", file)]);
4366
+ }
4367
+ }
4368
+ summary.push(entry);
4369
+ }
4370
+ operations.push(...copies.map(([, target]) => ({ action: "create", kind: "file", target })));
4371
+ operations.push({ action: "create", kind: "file", target: join17(destination, "summary.json") });
4372
+ const checks = [
4373
+ warn("export.limitation", LIMITATION),
4374
+ ...options.includeGateOutput === true ? [warn("export.gate-output", "Gate output tails are included; they are best-effort masked only.")] : [],
4375
+ ...summary.filter((entry) => entry.omitted.length > 0).map((entry) => warn("export.omitted", `Project ${entry.projectId}: ${String(entry.omitted.length)} unreadable event file(s) were left out by name: ${entry.omitted.join(", ")}.`))
4376
+ ];
4377
+ if (!options.apply) return { command: "export", outcome: "changes-planned", changed: false, checks, operations };
4378
+ await mkdir4(destination, { recursive: true, mode: 448 });
4379
+ for (const [from, to] of copies) {
4380
+ await mkdir4(join17(to, ".."), { recursive: true, mode: 448 });
4381
+ await copyFile(from, to);
4382
+ }
4383
+ await writeFile3(join17(destination, "summary.json"), `${JSON.stringify({
4384
+ schema: 1,
4385
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
4386
+ packageVersion: package_default.version,
4387
+ gateOutputIncluded: options.includeGateOutput === true,
4388
+ limitation: LIMITATION,
4389
+ projects: summary
4390
+ }, null, 2)}
4391
+ `, { mode: 384 });
4392
+ return { command: "export", outcome: "ok", changed: true, checks: [...checks, pass("export.written", `Exported ${String(summary.length)} project(s) to ${destination}.`)], operations };
4393
+ }
4394
+
4395
+ // src/cli.ts
4396
+ init_fsops();
4397
+
2872
4398
  // src/render.ts
2873
4399
  function renderJson(result) {
2874
4400
  return `${JSON.stringify({ schemaVersion: 2, ...result }, null, 2)}
@@ -2901,7 +4427,7 @@ init_roles();
2901
4427
  init_prompts();
2902
4428
  init_skills();
2903
4429
  function collectAgent(value, previous) {
2904
- const agent = AGENT_IDS.find((id) => id === value);
4430
+ const agent = AGENT_IDS.find((id2) => id2 === value);
2905
4431
  if (!agent) throw new InvalidArgumentError(`Unknown agent: ${value}`);
2906
4432
  const seated = previous ?? [];
2907
4433
  return seated.includes(agent) ? seated : [...seated, agent];
@@ -2920,7 +4446,8 @@ function optionsFrom(raw, base) {
2920
4446
  ...agents ? { agents } : {},
2921
4447
  ...raw.apply === true ? { apply: true } : {},
2922
4448
  // Commander defaults a --no- flag to true, so only an explicit opt-out is carried.
2923
- ...raw.claudeMemoryContract === false ? { claudeMemoryContract: false } : {}
4449
+ ...raw.claudeMemoryContract === false ? { claudeMemoryContract: false } : {},
4450
+ ...raw.runtime === true ? { runtime: true } : {}
2924
4451
  };
2925
4452
  }
2926
4453
  function failedFromThrown(command, error) {
@@ -2950,7 +4477,7 @@ function failedFromThrown(command, error) {
2950
4477
  }
2951
4478
  async function runCli(argv, output, context = {}) {
2952
4479
  const password = (context.options?.env ?? process.env).PASEO_PASSWORD;
2953
- const redact = (text) => password ? text.split(password).join("[redacted]") : text;
4480
+ const redact = (text3) => password ? text3.split(password).join("[redacted]") : text3;
2954
4481
  let json = false;
2955
4482
  const emit = (result) => {
2956
4483
  output.stdout(redact(json ? renderJson(result) : renderHuman(result)));
@@ -2969,8 +4496,8 @@ async function runCli(argv, output, context = {}) {
2969
4496
  return await runWizard2(
2970
4497
  context.prompts ?? terminalPrompts2,
2971
4498
  emit,
2972
- (text) => {
2973
- output.stdout(text);
4499
+ (text3) => {
4500
+ output.stdout(text3);
2974
4501
  },
2975
4502
  base,
2976
4503
  (action) => {
@@ -2981,10 +4508,10 @@ async function runCli(argv, output, context = {}) {
2981
4508
  return emit(failedFromThrown(wizardCommand, error));
2982
4509
  }
2983
4510
  }
2984
- 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) => {
2985
- output.stdout(text);
2986
- }, writeErr: (text) => {
2987
- output.stderr(text);
4511
+ const program = new Command().name("paseo-room").description("Configure Codex/Claude/Pi role homes in $HOME and register them with your local Paseo daemon.").version(package_default.version).usage("<setup|verify|remove|export> [options]\n auth login <codex|claude|pi> <supervisor|lead|peer> [options]").argument("<command>", "setup | verify | remove | export | auth").argument("[command-arguments...]", "auth login <agent> <role>").option("--agent <agent>", "codex, claude, or pi; repeat to combine (default: codex)", collectAgent).option("--apply", "actually make the changes (default: dry run)").option("--json", "machine-readable output").option("--no-claude-memory-contract", "omit the role contract from Claude role CLAUDE.md files, leaving the room plugin as the only Claude carrier").option("--runtime", "opt in to runtime coordination (preview): installs the trusted paseo-room-runtime plugin; omit it to deselect").option("--out <dir>", "export: destination directory (default: a new directory under the room's runtime exports)").option("--include-gate-output", "export: also copy bounded gate output tails (best-effort masked)").option("--room-home <path>", "where role homes are written (default: ~/.paseo-room)").option("--codex-home <path>", "source Codex config (default: ~/.codex)").option("--claude-home <path>", "source Claude Code config (default: ~/.claude)").option("--pi-home <path>", "source Pi config (default: ~/.pi/agent)").option("--codex-bin <path>", "Codex executable (default: found on PATH)").option("--claude-bin <path>", "Claude Code executable (default: found on PATH)").option("--pi-bin <path>", "Pi executable (default: found on PATH)").option("--paseo-bin <path>", "Paseo executable (default: found on PATH)").configureOutput({ writeOut: (text3) => {
4512
+ output.stdout(text3);
4513
+ }, writeErr: (text3) => {
4514
+ output.stderr(text3);
2988
4515
  } }).exitOverride();
2989
4516
  let command;
2990
4517
  let commandArguments;
@@ -3004,10 +4531,24 @@ async function runCli(argv, output, context = {}) {
3004
4531
  try {
3005
4532
  if (raw.claudeMemoryContract === false && (command === "verify" || command === "remove")) {
3006
4533
  output.stderr(`paseo-room ${command}: --no-claude-memory-contract is a setup choice recorded in the room marker; ${command} reads it from there.
4534
+ `);
4535
+ return 2;
4536
+ }
4537
+ if ((raw.out !== void 0 || raw.includeGateOutput === true) && command !== "export") {
4538
+ output.stderr(`paseo-room ${command}: --out and --include-gate-output only apply to export.
4539
+ `);
4540
+ return 2;
4541
+ }
4542
+ if (raw.runtime === true && (command === "verify" || command === "remove" || command === "export")) {
4543
+ output.stderr(`paseo-room ${command}: --runtime is a setup choice recorded in the room marker; ${command} reads it from there.
3007
4544
  `);
3008
4545
  return 2;
3009
4546
  }
3010
4547
  if (command === "auth") {
4548
+ if (raw.runtime === true) {
4549
+ output.stderr("paseo-room auth login only authenticates a role and does not use --runtime.\n");
4550
+ return 2;
4551
+ }
3011
4552
  if (raw.claudeMemoryContract === false) {
3012
4553
  output.stderr("paseo-room auth login only authenticates a role and does not use --no-claude-memory-contract.\n");
3013
4554
  return 2;
@@ -3042,21 +4583,28 @@ async function runCli(argv, output, context = {}) {
3042
4583
  if (command === "setup") return emit(await setup(options));
3043
4584
  if (command === "verify") return emit(await verify(options));
3044
4585
  if (command === "remove") return emit(await remove(options));
4586
+ if (command === "export") {
4587
+ return emit(await exportRuntime({
4588
+ ...options,
4589
+ ...typeof raw.out === "string" && raw.out.trim() !== "" ? { out: raw.out.trim() } : {},
4590
+ ...raw.includeGateOutput === true ? { includeGateOutput: true } : {}
4591
+ }));
4592
+ }
3045
4593
  } catch (error) {
3046
4594
  return emit(failedFromThrown(command, error));
3047
4595
  }
3048
- output.stderr(`paseo-room: unknown command "${command}". Try: setup, verify, remove, or auth login.
4596
+ output.stderr(`paseo-room: unknown command "${command}". Try: setup, verify, remove, export, or auth login.
3049
4597
  `);
3050
4598
  return 2;
3051
4599
  }
3052
4600
 
3053
4601
  // src/index.ts
3054
4602
  process.exitCode = await runCli(process.argv.slice(2), {
3055
- stdout: (text) => {
3056
- process.stdout.write(text);
4603
+ stdout: (text3) => {
4604
+ process.stdout.write(text3);
3057
4605
  },
3058
- stderr: (text) => {
3059
- process.stderr.write(text);
4606
+ stderr: (text3) => {
4607
+ process.stderr.write(text3);
3060
4608
  }
3061
4609
  });
3062
4610
  //# sourceMappingURL=index.js.map