paseo-room 0.4.1 → 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 (87) hide show
  1. package/README.md +178 -73
  2. package/dist/index.js +2069 -400
  3. package/dist/index.js.map +1 -1
  4. package/dist/plugin-assets/paseo-plugin.json +1 -1
  5. package/dist/prompts/contract/lead.md +152 -0
  6. package/dist/prompts/contract/peer.md +74 -0
  7. package/dist/prompts/contract/shared-authority.md +33 -0
  8. package/dist/prompts/contract/shared-seat-identity.md +18 -0
  9. package/dist/prompts/contract/supervisor.md +89 -0
  10. package/dist/runtime-plugin/client/data.ts +67 -0
  11. package/dist/runtime-plugin/client/views.tsx +176 -0
  12. package/dist/runtime-plugin/index.client.tsx +21 -0
  13. package/dist/runtime-plugin/index.server.ts +53 -0
  14. package/dist/runtime-plugin/package.json +5 -0
  15. package/dist/runtime-plugin/paseo-plugin.json +4 -0
  16. package/dist/runtime-plugin/server/bridge/bridge.mjs +146 -0
  17. package/dist/runtime-plugin/server/brief.ts +39 -0
  18. package/dist/runtime-plugin/server/capabilities.ts +45 -0
  19. package/dist/runtime-plugin/server/context.ts +80 -0
  20. package/dist/runtime-plugin/server/contracts/actions.ts +39 -0
  21. package/dist/runtime-plugin/server/contracts/assignment.ts +51 -0
  22. package/dist/runtime-plugin/server/contracts/envelope.ts +26 -0
  23. package/dist/runtime-plugin/server/contracts/peer.ts +112 -0
  24. package/dist/runtime-plugin/server/controller.ts +558 -0
  25. package/dist/runtime-plugin/server/correlations.ts +157 -0
  26. package/dist/runtime-plugin/server/domain/acceptance.ts +69 -0
  27. package/dist/runtime-plugin/server/domain/authorize.ts +37 -0
  28. package/dist/runtime-plugin/server/domain/receipts.ts +73 -0
  29. package/dist/runtime-plugin/server/domain/state.ts +413 -0
  30. package/dist/runtime-plugin/server/domain/validate.ts +69 -0
  31. package/dist/runtime-plugin/server/domain/views.ts +233 -0
  32. package/dist/runtime-plugin/server/events/schema.ts +214 -0
  33. package/dist/runtime-plugin/server/gate.ts +233 -0
  34. package/dist/runtime-plugin/server/generated/location.ts +11 -0
  35. package/dist/runtime-plugin/server/git.ts +158 -0
  36. package/dist/runtime-plugin/server/handlers/actions.ts +130 -0
  37. package/dist/runtime-plugin/server/handlers/peer.ts +183 -0
  38. package/dist/runtime-plugin/server/handlers/turns.ts +113 -0
  39. package/dist/runtime-plugin/server/hooks.ts +73 -0
  40. package/dist/runtime-plugin/server/lifecycle.ts +60 -0
  41. package/dist/runtime-plugin/server/notices.ts +103 -0
  42. package/dist/runtime-plugin/server/ownership.ts +63 -0
  43. package/dist/runtime-plugin/server/paseo-port.ts +194 -0
  44. package/dist/runtime-plugin/server/recognition.ts +80 -0
  45. package/dist/runtime-plugin/server/recovery.ts +166 -0
  46. package/dist/runtime-plugin/server/rpc.ts +169 -0
  47. package/dist/runtime-plugin/server/spool.ts +143 -0
  48. package/dist/runtime-plugin/server/store/project.ts +228 -0
  49. package/dist/runtime-plugin/server/store/publish.ts +142 -0
  50. package/dist/runtime-plugin/server/tools.ts +91 -0
  51. package/dist/runtime-plugin/shared/identity.ts +16 -0
  52. package/dist/runtime-plugin/shared/limits.ts +40 -0
  53. package/dist/runtime-plugin/shared/manifest.ts +51 -0
  54. package/dist/runtime-plugin/shared/policy.ts +61 -0
  55. package/dist/runtime-plugin/shared/rpc-contracts.ts +66 -0
  56. package/dist/runtime-plugin/shared/rpc.ts +34 -0
  57. package/dist/runtime-plugin/tsconfig.json +22 -0
  58. package/dist/skills/paseo-project-onboarding/SKILL.md +122 -0
  59. package/dist/skills/paseo-project-onboarding/references/workspace-protocol-template.md +56 -0
  60. package/package.json +9 -4
  61. package/dist/prompts/contract/lead/complete-peer-brief.md +0 -32
  62. package/dist/prompts/contract/lead/independent-review.md +0 -9
  63. package/dist/prompts/contract/lead/moving-write-ownership.md +0 -13
  64. package/dist/prompts/contract/lead/peer-seat-lifecycle.md +0 -39
  65. package/dist/prompts/contract/lead/project-technical-ownership.md +0 -12
  66. package/dist/prompts/contract/lead/technical-acceptance.md +0 -6
  67. package/dist/prompts/contract/peer/assignment-scope.md +0 -10
  68. package/dist/prompts/contract/peer/bounded-outcome.md +0 -13
  69. package/dist/prompts/contract/peer/independent-judgment.md +0 -11
  70. package/dist/prompts/contract/peer/no-orchestration.md +0 -5
  71. package/dist/prompts/contract/peer/no-self-acceptance.md +0 -5
  72. package/dist/prompts/contract/peer/reproducible-handoff.md +0 -17
  73. package/dist/prompts/contract/shared/evidence-and-event-waiting.md +0 -7
  74. package/dist/prompts/contract/shared/human-authority.md +0 -8
  75. package/dist/prompts/contract/shared/scope-and-unrelated-work.md +0 -4
  76. package/dist/prompts/contract/shared-supervisor-lead/workspace-protocol-precedence.md +0 -18
  77. package/dist/prompts/contract/supervisor/directive-integrity.md +0 -5
  78. package/dist/prompts/contract/supervisor/escalation-boundaries.md +0 -5
  79. package/dist/prompts/contract/supervisor/lead-discovery-and-recovery.md +0 -52
  80. package/dist/prompts/contract/supervisor/observation-and-advice.md +0 -13
  81. package/dist/prompts/contract/supervisor/technical-non-interference.md +0 -8
  82. package/dist/prompts/documents/workspace.md +0 -6
  83. package/dist/prompts/workspace/repository-conventions.md +0 -7
  84. package/dist/prompts/workspace/review.md +0 -4
  85. package/dist/prompts/workspace/topology.md +0 -13
  86. package/dist/prompts/workspace/verification.md +0 -10
  87. /package/dist/prompts/contract/{shared-lead-peer/challenge-signals.md → challenge-signals.md} +0 -0
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.4.1",
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/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,15 +163,9 @@ function preservedCredentialCheck(input) {
158
163
  function presentNames(names, supported) {
159
164
  return supported.filter((name) => names.has(name));
160
165
  }
161
- function configuredByNamesCheck(id, agent, role, names) {
166
+ function ambientNamesCheck(id2, agent, role, names) {
162
167
  return warn(
163
- `${id}.configured-structurally`,
164
- `${agent} ${role} auth: configured structurally; ${names.join(", ")} ${names.length === 1 ? "is" : "are"} present by name. Values, token validity, and freshness were not checked.`
165
- );
166
- }
167
- function ambientNamesCheck(id, agent, role, names) {
168
- return warn(
169
- `${id}.ambient-auth-unverifiable`,
168
+ `${id2}.ambient-auth-unverifiable`,
170
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.`
171
170
  );
172
171
  }
@@ -220,13 +219,13 @@ function layoutChecks(layout, agents) {
220
219
  "Point --room-home at a dedicated directory, by default ~/.paseo-room."
221
220
  ));
222
221
  }
223
- for (const id of agents) {
224
- const home = layout.agentHome[id];
222
+ for (const id2 of agents) {
223
+ const home = layout.agentHome[id2];
225
224
  if (contains(layout.roomHome, home) || contains(home, layout.roomHome)) {
226
225
  checks.push(fail(
227
- `${id}.home`,
228
- `The ${id} home ${home} overlaps the room home ${layout.roomHome}.`,
229
- `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).`
230
229
  ));
231
230
  }
232
231
  }
@@ -450,13 +449,14 @@ function providerLabel(agent, role) {
450
449
  const title2 = (value) => value.charAt(0).toUpperCase() + value.slice(1);
451
450
  return `${title2(agent)} ${title2(role)}`;
452
451
  }
453
- 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;
454
453
  var init_roles = __esm({
455
454
  "src/roles.ts"() {
456
455
  "use strict";
457
456
  ROLES = ["supervisor", "lead", "peer"];
458
457
  AGENT_IDS = ["codex", "claude", "pi"];
459
458
  ROLE_PASEO_TOOLS = { supervisor: true, lead: true, peer: false };
459
+ ROLE_PEER_REPORTING = { supervisor: false, lead: false, peer: true };
460
460
  ROLE_THINKING = { supervisor: "low", lead: "high", peer: "high" };
461
461
  DELEGATING_THINKING = ["ultra", "ultracode"];
462
462
  ROLE_ICON = { supervisor: "eye", lead: "compass", peer: "code" };
@@ -472,14 +472,15 @@ var init_roles = __esm({
472
472
  // src/room.ts
473
473
  import { join as join3 } from "path";
474
474
  import { z } from "zod";
475
- function renderMarker(version, agents, roles, contract, claudeMemoryContract) {
475
+ function renderMarker(version, agents, roles, contract, claudeMemoryContract, runtime) {
476
476
  return JSON.stringify({
477
477
  version,
478
478
  agents: [...agents],
479
479
  roles: [...roles],
480
480
  ...contract === void 0 ? {} : { contract },
481
481
  // Written only when it differs from the default, so an unchanged room's marker is unchanged.
482
- ...claudeMemoryContract === false ? { claudeMemoryContract: false } : {}
482
+ ...claudeMemoryContract === false ? { claudeMemoryContract: false } : {},
483
+ ...runtime === void 0 ? {} : { runtime }
483
484
  }, null, 2) + "\n";
484
485
  }
485
486
  async function readMarker(layout) {
@@ -513,7 +514,12 @@ var init_room = __esm({
513
514
  * leaving the plugin as its only Claude carrier. Optional and defaulted to true so a room
514
515
  * written before the option existed keeps parsing and keeps its current behaviour.
515
516
  */
516
- 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()
517
523
  });
518
524
  }
519
525
  });
@@ -683,12 +689,12 @@ async function loginRole(agentValue, roleValue, options, output) {
683
689
  }
684
690
  const childEnv = { ...env, ...invocation.env };
685
691
  const launch = options.spawn ?? spawnLogin;
686
- return new Promise((resolve4) => {
692
+ return new Promise((resolve5) => {
687
693
  let settled = false;
688
- const finish = (code) => {
694
+ const finish = (code2) => {
689
695
  if (settled) return;
690
696
  settled = true;
691
- resolve4(code);
697
+ resolve5(code2);
692
698
  };
693
699
  let child;
694
700
  try {
@@ -710,9 +716,9 @@ async function loginRole(agentValue, roleValue, options, output) {
710
716
  `);
711
717
  finish(1);
712
718
  });
713
- child.once("close", (code, signal) => {
714
- if (code !== null) {
715
- finish(code);
719
+ child.once("close", (code2, signal) => {
720
+ if (code2 !== null) {
721
+ finish(code2);
716
722
  return;
717
723
  }
718
724
  if (signal) {
@@ -765,18 +771,11 @@ function validateHeadOrCapsule(source, kind) {
765
771
  }
766
772
  return trimmed;
767
773
  }
768
- function validateSection(source) {
769
- const trimmed = source.trimEnd();
770
- if (!trimmed.trim()) throw new Error("is empty.");
771
- const lines = trimmed.split("\n");
772
- const heading = lines.shift() ?? "";
773
- if (!/^## \S.*$/.test(heading) || headingCount(trimmed, 2) !== 1) {
774
- throw new Error("section must begin with exactly one H2 heading.");
775
- }
776
- if (lines.some((line) => /^#{1,6}\s/.test(line))) {
774
+ function renderSection(heading, bodyLines) {
775
+ if (bodyLines.some((line) => /^#{1,6}\s/.test(line))) {
777
776
  throw new Error("section must not contain additional Markdown headings.");
778
777
  }
779
- const body = lines.join("\n").trim();
778
+ const body = bodyLines.join("\n").trim();
780
779
  if (!body) throw new Error("section must contain at least one non-empty statement.");
781
780
  const statements = body.split(/\n\s*\n/).map(
782
781
  (statement) => statement.split("\n").map((line) => line.trim()).join(" ")
@@ -787,6 +786,40 @@ function validateSection(source) {
787
786
  return `${heading}
788
787
  ${statements.map((statement) => `- ${statement}`).join("\n")}`;
789
788
  }
789
+ function validateSection(source) {
790
+ const trimmed = source.trimEnd();
791
+ if (!trimmed.trim()) throw new Error("is empty.");
792
+ const lines = trimmed.split("\n");
793
+ const heading = lines.shift() ?? "";
794
+ if (!/^## \S.*$/.test(heading) || headingCount(trimmed, 2) !== 1) {
795
+ throw new Error("section must begin with exactly one H2 heading.");
796
+ }
797
+ return renderSection(heading, lines);
798
+ }
799
+ function splitSections(lines) {
800
+ const preface = [];
801
+ const sections = [];
802
+ for (const line of lines) {
803
+ if (/^## \S.*$/.test(line)) {
804
+ sections.push({ heading: line, lines: [] });
805
+ continue;
806
+ }
807
+ const current2 = sections.at(-1);
808
+ if (current2 === void 0) preface.push(line);
809
+ else current2.lines.push(line);
810
+ }
811
+ return { preface, sections };
812
+ }
813
+ function validateBody(source) {
814
+ const trimmed = source.trimEnd();
815
+ if (!trimmed.trim()) throw new Error("is empty.");
816
+ const { preface, sections } = splitSections(trimmed.split("\n"));
817
+ if (sections.length === 0) throw new Error("role body must contain at least one H2 section.");
818
+ if (preface.some((line) => line.trim())) {
819
+ throw new Error("role body must not contain content above its first H2 heading.");
820
+ }
821
+ return sections.map((section) => renderSection(section.heading, section.lines)).join("\n\n");
822
+ }
790
823
  function loadPromptAsset(group, key) {
791
824
  const logicalAsset = `${group}.${String(key)}`;
792
825
  const cached = cache.get(logicalAsset);
@@ -796,7 +829,7 @@ function loadPromptAsset(group, key) {
796
829
  try {
797
830
  const source = readFileSync(assetUrl, "utf8");
798
831
  if (source.includes("\uFFFD")) throw new Error("is not valid UTF-8 text.");
799
- const validated = asset.kind === "section" ? validateSection(source) : validateHeadOrCapsule(source, asset.kind);
832
+ const validated = validate(source, asset.kind);
800
833
  cache.set(logicalAsset, validated);
801
834
  return validated;
802
835
  } catch (error) {
@@ -805,6 +838,16 @@ function loadPromptAsset(group, key) {
805
838
  throw new PromptAssetError(logicalAsset, assetUrl, detail, { cause: error });
806
839
  }
807
840
  }
841
+ function validate(source, kind) {
842
+ switch (kind) {
843
+ case "section":
844
+ return validateSection(source);
845
+ case "body":
846
+ return validateBody(source);
847
+ default:
848
+ return validateHeadOrCapsule(source, kind);
849
+ }
850
+ }
808
851
  var PROMPT_ASSETS, PromptAssetError, cache;
809
852
  var init_prompts = __esm({
810
853
  "src/room/prompts.ts"() {
@@ -813,38 +856,15 @@ var init_prompts = __esm({
813
856
  documents: {
814
857
  supervisor: { path: "documents/supervisor.md", kind: "head" },
815
858
  lead: { path: "documents/lead.md", kind: "head" },
816
- peer: { path: "documents/peer.md", kind: "head" },
817
- workspace: { path: "documents/workspace.md", kind: "head" }
859
+ peer: { path: "documents/peer.md", kind: "head" }
818
860
  },
819
861
  contract: {
820
- humanAuthority: { path: "contract/shared/human-authority.md", kind: "section" },
821
- workspaceProtocolPrecedence: { path: "contract/shared-supervisor-lead/workspace-protocol-precedence.md", kind: "section" },
822
- evidenceAndEventWaiting: { path: "contract/shared/evidence-and-event-waiting.md", kind: "section" },
823
- scopeAndUnrelatedWork: { path: "contract/shared/scope-and-unrelated-work.md", kind: "section" },
824
- directiveIntegrity: { path: "contract/supervisor/directive-integrity.md", kind: "section" },
825
- technicalNonInterference: { path: "contract/supervisor/technical-non-interference.md", kind: "section" },
826
- leadDiscoveryAndRecovery: { path: "contract/supervisor/lead-discovery-and-recovery.md", kind: "section" },
827
- observationAndAdvice: { path: "contract/supervisor/observation-and-advice.md", kind: "section" },
828
- escalationBoundaries: { path: "contract/supervisor/escalation-boundaries.md", kind: "section" },
829
- projectTechnicalOwnership: { path: "contract/lead/project-technical-ownership.md", kind: "section" },
830
- movingWriteOwnership: { path: "contract/lead/moving-write-ownership.md", kind: "section" },
831
- completePeerBrief: { path: "contract/lead/complete-peer-brief.md", kind: "section" },
832
- challengeSignals: { path: "contract/shared-lead-peer/challenge-signals.md", kind: "section" },
833
- technicalAcceptance: { path: "contract/lead/technical-acceptance.md", kind: "section" },
834
- independentReview: { path: "contract/lead/independent-review.md", kind: "section" },
835
- peerSeatLifecycle: { path: "contract/lead/peer-seat-lifecycle.md", kind: "section" },
836
- boundedOutcome: { path: "contract/peer/bounded-outcome.md", kind: "section" },
837
- independentJudgment: { path: "contract/peer/independent-judgment.md", kind: "section" },
838
- assignmentScope: { path: "contract/peer/assignment-scope.md", kind: "section" },
839
- noOrchestration: { path: "contract/peer/no-orchestration.md", kind: "section" },
840
- reproducibleHandoff: { path: "contract/peer/reproducible-handoff.md", kind: "section" },
841
- noSelfAcceptance: { path: "contract/peer/no-self-acceptance.md", kind: "section" }
842
- },
843
- workspace: {
844
- topology: { path: "workspace/topology.md", kind: "section" },
845
- verification: { path: "workspace/verification.md", kind: "section" },
846
- review: { path: "workspace/review.md", kind: "section" },
847
- repositoryConventions: { path: "workspace/repository-conventions.md", kind: "section" }
862
+ sharedAuthority: { path: "contract/shared-authority.md", kind: "body" },
863
+ sharedSeatIdentity: { path: "contract/shared-seat-identity.md", kind: "section" },
864
+ challengeSignals: { path: "contract/challenge-signals.md", kind: "section" },
865
+ supervisor: { path: "contract/supervisor.md", kind: "body" },
866
+ lead: { path: "contract/lead.md", kind: "body" },
867
+ peer: { path: "contract/peer.md", kind: "body" }
848
868
  },
849
869
  pi: {
850
870
  communicationStyle: { path: "pi/communication-style.md", kind: "capsule" },
@@ -868,24 +888,12 @@ var init_prompts = __esm({
868
888
  // src/room/instructions.ts
869
889
  import { createHash } from "crypto";
870
890
  function instructionKeys(role) {
871
- return [...SHARED_KEYS[role], ...ROLE_KEYS[role]];
872
- }
873
- function protocolKeys(role) {
874
- return PROTOCOL_KEYS[role];
875
- }
876
- function protocol(keys) {
877
- if (keys.length === 0) return [];
878
- return [
879
- loadPromptAsset("documents", "workspace"),
880
- ...keys.map((key) => loadPromptAsset("workspace", key))
881
- ];
891
+ return [...SHARED_KEYS[role], ROLE_KEYS[role]];
882
892
  }
883
893
  function renderInstructions(kind) {
884
- if (kind === "workspace") return protocol(ALL_PROTOCOL_KEYS).join("\n\n") + "\n";
885
894
  return [
886
895
  loadPromptAsset("documents", kind),
887
- ...instructionKeys(kind).map((key) => loadPromptAsset("contract", key)),
888
- ...protocol(protocolKeys(kind))
896
+ ...instructionKeys(kind).map((key) => loadPromptAsset("contract", key))
889
897
  ].join("\n\n") + "\n";
890
898
  }
891
899
  function contractDigest() {
@@ -893,68 +901,23 @@ function contractDigest() {
893
901
  for (const kind of INSTRUCTION_KINDS) hash.update(`${kind}\0${renderInstructions(kind)}\0`);
894
902
  return `sha256:${hash.digest("hex").slice(0, 16)}`;
895
903
  }
896
- var AUTHORITY_KEYS, PROTOCOL_READER_KEYS, SHARED_KEYS, ROLE_KEYS, ALL_PROTOCOL_KEYS, PROTOCOL_KEYS, INSTRUCTION_KINDS;
904
+ var SHARED_KEYS, ROLE_KEYS, INSTRUCTION_KINDS;
897
905
  var init_instructions = __esm({
898
906
  "src/room/instructions.ts"() {
899
907
  "use strict";
900
908
  init_roles();
901
909
  init_prompts();
902
- AUTHORITY_KEYS = [
903
- "humanAuthority",
904
- "evidenceAndEventWaiting",
905
- "scopeAndUnrelatedWork"
906
- ];
907
- PROTOCOL_READER_KEYS = [
908
- "humanAuthority",
909
- "workspaceProtocolPrecedence",
910
- "evidenceAndEventWaiting",
911
- "scopeAndUnrelatedWork"
912
- ];
913
910
  SHARED_KEYS = {
914
- supervisor: PROTOCOL_READER_KEYS,
915
- lead: PROTOCOL_READER_KEYS,
916
- peer: AUTHORITY_KEYS
911
+ supervisor: ["sharedAuthority", "sharedSeatIdentity"],
912
+ lead: ["sharedAuthority", "sharedSeatIdentity", "challengeSignals"],
913
+ peer: ["sharedAuthority", "challengeSignals"]
917
914
  };
918
915
  ROLE_KEYS = {
919
- supervisor: [
920
- "directiveIntegrity",
921
- "technicalNonInterference",
922
- "leadDiscoveryAndRecovery",
923
- "observationAndAdvice",
924
- "escalationBoundaries"
925
- ],
926
- lead: [
927
- "projectTechnicalOwnership",
928
- "movingWriteOwnership",
929
- "completePeerBrief",
930
- "challengeSignals",
931
- "technicalAcceptance",
932
- "independentReview",
933
- "peerSeatLifecycle"
934
- ],
935
- // Peer needs Challenge Signals as well as its own obligations.
936
- peer: [
937
- "challengeSignals",
938
- "boundedOutcome",
939
- "independentJudgment",
940
- "assignmentScope",
941
- "noOrchestration",
942
- "reproducibleHandoff",
943
- "noSelfAcceptance"
944
- ]
945
- };
946
- ALL_PROTOCOL_KEYS = [
947
- "topology",
948
- "verification",
949
- "review",
950
- "repositoryConventions"
951
- ];
952
- PROTOCOL_KEYS = {
953
- supervisor: ALL_PROTOCOL_KEYS,
954
- lead: ALL_PROTOCOL_KEYS,
955
- peer: []
916
+ supervisor: "supervisor",
917
+ lead: "lead",
918
+ peer: "peer"
956
919
  };
957
- INSTRUCTION_KINDS = [...ROLES, "workspace"];
920
+ INSTRUCTION_KINDS = [...ROLES];
958
921
  }
959
922
  });
960
923
 
@@ -1011,11 +974,11 @@ function detectPaseoServers(servers) {
1011
974
  function evidence(match) {
1012
975
  return match.field === "name" ? `${match.server} (server name)` : `${match.server} (${match.field}: ${match.value})`;
1013
976
  }
1014
- function paseoMcpCheck(id, path, servers) {
977
+ function paseoMcpCheck(id2, path, servers) {
1015
978
  const matches = detectPaseoServers(servers);
1016
979
  if (matches.length === 0) return void 0;
1017
980
  return fail(
1018
- id,
981
+ id2,
1019
982
  `${path} declares MCP servers that are recognizably Paseo-related: ${matches.map(evidence).join("; ")}.`,
1020
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.`
1021
984
  );
@@ -1040,14 +1003,39 @@ function isPaseoSkill(name) {
1040
1003
  function receivesExecutableResources(role) {
1041
1004
  return role !== "peer";
1042
1005
  }
1043
- async function skillNames(path, reserved) {
1006
+ function receivesRoomSkill(role) {
1007
+ return role === "lead";
1008
+ }
1009
+ async function skillNames(path, reserved, keep) {
1044
1010
  try {
1045
- return (await readdir2(path)).filter((name) => !isPaseoSkill(name) && !reserved.has(name)).sort();
1011
+ return (await readdir2(path)).filter((name) => keep(name) && !reserved.has(name.toLowerCase())).sort();
1046
1012
  } catch (error) {
1047
1013
  if (error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT") return [];
1048
1014
  throw error;
1049
1015
  }
1050
1016
  }
1017
+ async function skillsProjection(input, path, alias, present, reserved) {
1018
+ const roomSkill = receivesRoomSkill(input.role) ? input.roomSkill : void 0;
1019
+ const projectsChildren = !receivesExecutableResources(input.role) || roomSkill !== void 0;
1020
+ if (!projectsChildren) return void 0;
1021
+ const reservedNames = new Set([...reserved].map((name) => name.toLowerCase()));
1022
+ const keep = roomSkill === void 0 ? (name) => !isPaseoSkill(name) : (name) => name.toLowerCase() !== roomSkill.name.toLowerCase();
1023
+ const operator = present.has(path) ? await skillNames(path, reservedNames, keep) : [];
1024
+ const children = roomSkill === void 0 ? operator : [...operator, roomSkill.name];
1025
+ const projection = input.role === "lead" ? input.leadSkillProjection : alias;
1026
+ return [
1027
+ {
1028
+ kind: "managed-dir",
1029
+ path: projection,
1030
+ children,
1031
+ ...input.role === "peer" ? { legacyLink: path } : {},
1032
+ ...reserved.size === 0 ? {} : { reserved: [...reserved] }
1033
+ },
1034
+ ...operator.map((child) => ({ kind: "link", path: join6(projection, child), target: join6(path, child) })),
1035
+ ...roomSkill === void 0 ? [] : [{ kind: "link", path: join6(projection, roomSkill.name), target: roomSkill.source }],
1036
+ ...input.role === "lead" ? [{ kind: "link", path: alias, target: projection }] : []
1037
+ ];
1038
+ }
1051
1039
  async function roleResourceEntries(input) {
1052
1040
  const present = new Set(input.shared);
1053
1041
  const executable = new Set(input.executable);
@@ -1056,21 +1044,14 @@ async function roleResourceEntries(input) {
1056
1044
  for (const name of input.names) {
1057
1045
  const path = join6(input.home, name);
1058
1046
  const alias = join6(input.target, name);
1059
- if (!receivesExecutableResources(input.role)) {
1060
- if (executable.has(name)) continue;
1061
- if (name === SKILLS) {
1062
- const children = present.has(path) ? await skillNames(path, reserved) : [];
1063
- entries.push({
1064
- kind: "managed-dir",
1065
- path: alias,
1066
- children,
1067
- legacyLink: path,
1068
- ...reserved.size === 0 ? {} : { reserved: [...reserved] }
1069
- });
1070
- for (const child of children) entries.push({ kind: "link", path: join6(alias, child), target: join6(path, child) });
1047
+ if (name === SKILLS) {
1048
+ const projection = await skillsProjection(input, path, alias, present, reserved);
1049
+ if (projection !== void 0) {
1050
+ entries.push(...projection);
1071
1051
  continue;
1072
1052
  }
1073
1053
  }
1054
+ if (!receivesExecutableResources(input.role) && executable.has(name)) continue;
1074
1055
  if (!present.has(path)) continue;
1075
1056
  entries.push({ kind: "link", path: alias, target: path });
1076
1057
  }
@@ -1084,9 +1065,99 @@ var init_resources = __esm({
1084
1065
  }
1085
1066
  });
1086
1067
 
1068
+ // src/room/skills.ts
1069
+ import { readFileSync as readFileSync2 } from "fs";
1070
+ import { join as join7 } from "path";
1071
+ function validateSkillAsset(path, source) {
1072
+ if (source.includes("\uFFFD")) throw new Error("is not valid UTF-8 text.");
1073
+ if (!source.trim()) throw new Error("is empty.");
1074
+ if (path === "SKILL.md") {
1075
+ const frontmatter = /^---\n([\s\S]*?)\n---\n/.exec(source)?.[1];
1076
+ if (frontmatter === void 0) throw new Error("must begin with YAML frontmatter.");
1077
+ const declaredName = /^name: (.+)$/m.exec(frontmatter)?.[1];
1078
+ if (declaredName !== ROOM_SKILL_NAME) {
1079
+ throw new Error(`frontmatter name must be ${ROOM_SKILL_NAME}.`);
1080
+ }
1081
+ if (!/^description: \S.+$/m.test(frontmatter)) {
1082
+ throw new Error("frontmatter must contain a non-empty single-line description.");
1083
+ }
1084
+ }
1085
+ if (path === "references/workspace-protocol-template.md" && !source.startsWith("# Workspace protocol template (scaffold)\n")) {
1086
+ throw new Error("must begin with the workspace protocol scaffold heading.");
1087
+ }
1088
+ return source;
1089
+ }
1090
+ function loadSkillAsset(path) {
1091
+ const cached = cache2.get(path);
1092
+ if (cached !== void 0) return cached;
1093
+ const logicalAsset = `${ROOM_SKILL_NAME}/${path}`;
1094
+ const assetUrl = new URL(`./skills/${ROOM_SKILL_NAME}/${path}`, import.meta.url);
1095
+ try {
1096
+ const validated = validateSkillAsset(path, readFileSync2(assetUrl, "utf8"));
1097
+ cache2.set(path, validated);
1098
+ return validated;
1099
+ } catch (error) {
1100
+ if (error instanceof SkillAssetError) throw error;
1101
+ const detail = error instanceof Error ? error.message : "could not be read.";
1102
+ throw new SkillAssetError(logicalAsset, assetUrl, detail, { cause: error });
1103
+ }
1104
+ }
1105
+ function roomSkillSource(layout) {
1106
+ return join7(sharedRoom(layout), "skills", ROOM_SKILL_NAME);
1107
+ }
1108
+ function skillProjectionRoot(layout) {
1109
+ return join7(sharedRoom(layout), "skill-projections");
1110
+ }
1111
+ function agentSkillProjectionRoot(layout, agent) {
1112
+ return join7(skillProjectionRoot(layout), agent);
1113
+ }
1114
+ function leadSkillProjection(layout, agent) {
1115
+ return join7(agentSkillProjectionRoot(layout, agent), "lead");
1116
+ }
1117
+ function roomSkillEntries(layout, agents) {
1118
+ const root = roomSkillSource(layout);
1119
+ return [
1120
+ { kind: "dir", path: join7(sharedRoom(layout), "skills") },
1121
+ { kind: "dir", path: skillProjectionRoot(layout) },
1122
+ ...agents.map((agent) => ({ kind: "dir", path: agentSkillProjectionRoot(layout, agent) })),
1123
+ { kind: "managed-dir", path: root, children: ["SKILL.md", ...SKILL_DIRECTORIES] },
1124
+ {
1125
+ kind: "managed-dir",
1126
+ path: join7(root, "references"),
1127
+ children: SKILL_FILES.filter((path) => path.startsWith("references/")).map((path) => path.slice("references/".length))
1128
+ },
1129
+ ...SKILL_FILES.map((path) => ({
1130
+ kind: "file",
1131
+ path: join7(root, path),
1132
+ content: loadSkillAsset(path)
1133
+ }))
1134
+ ];
1135
+ }
1136
+ var ROOM_SKILL_NAME, SKILL_DIRECTORIES, SKILL_FILES, SkillAssetError, cache2;
1137
+ var init_skills = __esm({
1138
+ "src/room/skills.ts"() {
1139
+ "use strict";
1140
+ init_layout();
1141
+ ROOM_SKILL_NAME = "paseo-project-onboarding";
1142
+ SKILL_DIRECTORIES = ["references"];
1143
+ SKILL_FILES = ["SKILL.md", "references/workspace-protocol-template.md"];
1144
+ SkillAssetError = class extends Error {
1145
+ logicalAsset;
1146
+ assetUrl;
1147
+ constructor(logicalAsset, assetUrl, detail, options) {
1148
+ super(`Skill asset ${logicalAsset} at ${assetUrl.pathname} ${detail} Reinstall paseo-room and try again.`, options);
1149
+ this.name = "SkillAssetError";
1150
+ this.logicalAsset = logicalAsset;
1151
+ this.assetUrl = assetUrl;
1152
+ }
1153
+ };
1154
+ cache2 = /* @__PURE__ */ new Map();
1155
+ }
1156
+ });
1157
+
1087
1158
  // src/agents/claude.ts
1088
1159
  import { readFile as readFile2 } from "fs/promises";
1089
- import { join as join7 } from "path";
1160
+ import { join as join8 } from "path";
1090
1161
  function asObject(value) {
1091
1162
  return value !== null && typeof value === "object" && !Array.isArray(value) ? { ...value } : {};
1092
1163
  }
@@ -1105,17 +1176,21 @@ async function readStateIfPresent(path) {
1105
1176
  throw error;
1106
1177
  }
1107
1178
  }
1108
- function renderRoleSettings(source, role, secureStorageDir) {
1109
- const settings = readObject(source);
1110
- settings.env = {
1111
- ...asObject(settings.env),
1112
- ...CONTROL_PLANE_ENV,
1113
- PASEO_ROOM_ROLE: role,
1114
- ...secureStorageDir === void 0 ? {} : { [SECURE_STORAGE_ENV]: secureStorageDir }
1179
+ function renderRoleSettings(role, secureStorageDir) {
1180
+ const settings = {
1181
+ // Claude applies settings.env after the launch environment, so repeat provider closures here.
1182
+ env: {
1183
+ ...CONTROL_PLANE_ENV,
1184
+ PASEO_ROOM_ROLE: role,
1185
+ ...secureStorageDir === void 0 ? {} : { [SECURE_STORAGE_ENV]: secureStorageDir }
1186
+ },
1187
+ // Provider disallowedTools is not sufficient alone; the isolated home carries the same list.
1188
+ permissions: { deny: DISALLOWED_TOOLS },
1189
+ // These restrictive settings cannot be weakened by another settings scope.
1190
+ disableAgentView: true,
1191
+ disableWorkflows: true,
1192
+ crossSessionInbound: "refuse"
1115
1193
  };
1116
- settings.disableAgentView = true;
1117
- settings.disableWorkflows = true;
1118
- settings.crossSessionInbound = "refuse";
1119
1194
  return JSON.stringify(settings, null, 2) + "\n";
1120
1195
  }
1121
1196
  function renderRoleState(source) {
@@ -1132,41 +1207,31 @@ function renderRoleMemory(source, role, contract = true) {
1132
1207
 
1133
1208
  ${renderInstructions(role)}` : renderInstructions(role);
1134
1209
  }
1135
- function claudeAuthMethodNames(settingsSource, envNames) {
1136
- const settings = readObject(settingsSource);
1137
- const settingsEnv = asObject(settings.env);
1138
- const names = new Set(presentNames(envNames, AUTH_ENV));
1139
- for (const name of AUTH_ENV) if (Object.hasOwn(settingsEnv, name)) names.add(name);
1140
- if (Object.hasOwn(settings, "apiKeyHelper")) names.add("apiKeyHelper");
1141
- return [...names];
1142
- }
1143
- async function claudeCredentialDiagnostic(layout, role, settingsSource, platform = process.platform, binary = "claude") {
1210
+ async function claudeCredentialDiagnostic(layout, role, platform = process.platform, binary = "claude") {
1144
1211
  const home = roleHome(layout, "claude", role);
1145
- const path = join7(home, ".credentials.json");
1212
+ const path = join8(home, ".credentials.json");
1146
1213
  const roleEnvironment = { CLAUDE_CONFIG_DIR: home, [SECURE_STORAGE_ENV]: home };
1147
1214
  const login = roleCommand(roleEnvironment, binary, ["auth", "login"]);
1148
1215
  const status = roleCommand(roleEnvironment, binary, ["auth", "status"]);
1149
1216
  const state = await inspectCredentialPath(path);
1150
- const id = `claude.auth.${role}`;
1217
+ const id2 = `claude.auth.${role}`;
1151
1218
  if (state.kind !== "missing") {
1152
- 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 })] };
1153
1220
  }
1154
- const configured = claudeAuthMethodNames(settingsSource, /* @__PURE__ */ new Set());
1155
- if (configured.length > 0) return { path, checks: [configuredByNamesCheck(id, "Claude", role, configured)] };
1156
1221
  const ambient = presentNames(layout.envNames, AUTH_ENV);
1157
- 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)] };
1158
1223
  if (platform === "darwin") {
1159
1224
  return { path, checks: [{
1160
- id: `${id}.native-keyring-unverifiable`,
1225
+ id: `${id2}.native-keyring-unverifiable`,
1161
1226
  status: "warn",
1162
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.`,
1163
1228
  fix: `Authenticate this role with: ${login}. If that subcommand is unavailable, launch ${roleCommand(roleEnvironment, binary)} and run /login. Check status with: ${status}.`
1164
1229
  }] };
1165
1230
  }
1166
1231
  return { path, checks: [{
1167
- id: `${id}.login-required`,
1232
+ id: `${id2}.login-required`,
1168
1233
  status: "warn",
1169
- message: `Claude ${role} auth: login-required; no role-owned .credentials.json or configured environment/static auth method was detected. Authentication was not attempted.`,
1234
+ message: `Claude ${role} auth: login-required; no role-owned .credentials.json or supported ambient auth environment name was detected. Authentication was not attempted.`,
1170
1235
  fix: `Authenticate this role with: ${login}. If that subcommand is unavailable, launch ${roleCommand(roleEnvironment, binary)} and run /login. Check status with: ${status}.`
1171
1236
  }] };
1172
1237
  }
@@ -1181,7 +1246,7 @@ function mcpDriftCheck(role, home, path, divergence) {
1181
1246
  `If this role should have the same servers, add them with: CLAUDE_CONFIG_DIR=${home} claude mcp add ... \u2014 or delete ${path} and run setup --apply to reseed it from your current Claude state, which discards that role's other runtime state.`
1182
1247
  );
1183
1248
  }
1184
- var SHARED, EXECUTABLE, RUNTIME_SKILLS, MCP_KEY, AUTH_ENV, CONTROL_PLANE_ENV, SECURE_STORAGE_ENV, SEEDED_KEYS, claudeAgent;
1249
+ var SHARED, EXECUTABLE, RUNTIME_SKILLS, MCP_KEY, AUTH_ENV, CONTROL_PLANE_ENV, DISALLOWED_TOOLS, SECURE_STORAGE_ENV, SEEDED_KEYS, claudeAgent;
1185
1250
  var init_claude = __esm({
1186
1251
  "src/agents/claude.ts"() {
1187
1252
  "use strict";
@@ -1193,6 +1258,7 @@ var init_claude = __esm({
1193
1258
  init_which();
1194
1259
  init_mcp();
1195
1260
  init_resources();
1261
+ init_skills();
1196
1262
  SHARED = [
1197
1263
  "skills",
1198
1264
  "plugins",
@@ -1218,6 +1284,22 @@ var init_claude = __esm({
1218
1284
  CLAUDE_CODE_DISABLE_AGENT_VIEW: "1",
1219
1285
  CLAUDE_CODE_DISABLE_WORKFLOWS: "1"
1220
1286
  };
1287
+ DISALLOWED_TOOLS = [
1288
+ "Task",
1289
+ "Agent",
1290
+ "Workflow",
1291
+ "ListAgents",
1292
+ "SendMessage",
1293
+ "TeamCreate",
1294
+ "TeamDelete",
1295
+ "TaskCreate",
1296
+ "TaskGet",
1297
+ "TaskList",
1298
+ "TaskUpdate",
1299
+ "CronCreate",
1300
+ "CronDelete",
1301
+ "CronList"
1302
+ ];
1221
1303
  SECURE_STORAGE_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
1222
1304
  SEEDED_KEYS = ["hasCompletedOnboarding", "theme", "installMethod", "userID", MCP_KEY];
1223
1305
  claudeAgent = {
@@ -1230,24 +1312,7 @@ var init_claude = __esm({
1230
1312
  // Keep the legacy Task name and block current native orchestration,
1231
1313
  // shared task/cron coordination, and cross-session paths. Paseo alone owns
1232
1314
  // agent lifecycle and coordination.
1233
- pins: {
1234
- disallowedTools: [
1235
- "Task",
1236
- "Agent",
1237
- "Workflow",
1238
- "ListAgents",
1239
- "SendMessage",
1240
- "TeamCreate",
1241
- "TeamDelete",
1242
- "TaskCreate",
1243
- "TaskGet",
1244
- "TaskList",
1245
- "TaskUpdate",
1246
- "CronCreate",
1247
- "CronDelete",
1248
- "CronList"
1249
- ]
1250
- },
1315
+ pins: { disallowedTools: DISALLOWED_TOOLS },
1251
1316
  async build(layout, roles, options = {}) {
1252
1317
  const home = layout.agentHome.claude;
1253
1318
  const binary = await which(layout.bin.claude, layout.searchPath);
@@ -1258,8 +1323,7 @@ var init_claude = __esm({
1258
1323
  return { entries: [], checks: [fail("claude.home", `No Claude config directory at ${home}.`, "Run claude once to initialise it, or pass --claude-home.")] };
1259
1324
  }
1260
1325
  const checks = [pass("claude.home", `Claude Code found at ${binary} using ${home}.`)];
1261
- const settingsSource = await readIfPresent(join7(home, "settings.json"));
1262
- const memorySource = await readIfPresent(join7(home, "CLAUDE.md"));
1326
+ const memorySource = await readIfPresent(join8(home, "CLAUDE.md"));
1263
1327
  let stateSource;
1264
1328
  let operatorServers;
1265
1329
  try {
@@ -1279,9 +1343,10 @@ var init_claude = __esm({
1279
1343
  const credentials = [];
1280
1344
  const providerEnv = {};
1281
1345
  const drift = [];
1346
+ const roomSkill = { name: ROOM_SKILL_NAME, source: roomSkillSource(layout) };
1282
1347
  for (const role of roles) {
1283
1348
  const target = roleHome(layout, "claude", role);
1284
- const statePath = join7(target, ".claude.json");
1349
+ const statePath = join8(target, ".claude.json");
1285
1350
  let roleStateSource;
1286
1351
  let roleServers;
1287
1352
  try {
@@ -1301,13 +1366,23 @@ var init_claude = __esm({
1301
1366
  if (divergence) drift.push(mcpDriftCheck(role, target, statePath, divergence));
1302
1367
  }
1303
1368
  entries.push({ kind: "dir", path: target });
1304
- const memoryPath = join7(target, "CLAUDE.md");
1369
+ const memoryPath = join8(target, "CLAUDE.md");
1305
1370
  const memory = renderRoleMemory(memorySource, role, options.memoryContract ?? true);
1306
1371
  entries.push(memory === void 0 ? { kind: "absent", path: memoryPath } : { kind: "file", path: memoryPath, content: memory });
1307
- entries.push({ kind: "file", path: join7(target, "settings.json"), content: renderRoleSettings(settingsSource, role, target) });
1372
+ entries.push({ kind: "file", path: join8(target, "settings.json"), content: renderRoleSettings(role, target) });
1308
1373
  entries.push({ kind: "file", path: statePath, content: renderRoleState(stateSource), once: true });
1309
- entries.push(...await roleResourceEntries({ role, target, home, names: SHARED, shared, executable: EXECUTABLE, reservedSkills: RUNTIME_SKILLS }));
1310
- credentials.push(await claudeCredentialDiagnostic(layout, role, settingsSource, process.platform, binary));
1374
+ entries.push(...await roleResourceEntries({
1375
+ role,
1376
+ target,
1377
+ home,
1378
+ names: SHARED,
1379
+ shared,
1380
+ executable: EXECUTABLE,
1381
+ reservedSkills: RUNTIME_SKILLS,
1382
+ roomSkill,
1383
+ leadSkillProjection: leadSkillProjection(layout, "claude")
1384
+ }));
1385
+ credentials.push(await claudeCredentialDiagnostic(layout, role, process.platform, binary));
1311
1386
  providerEnv[role] = { [SECURE_STORAGE_ENV]: target };
1312
1387
  }
1313
1388
  return { entries, credentials, checks: [...checks, ...drift], binary, providerEnv };
@@ -1317,7 +1392,7 @@ var init_claude = __esm({
1317
1392
  });
1318
1393
 
1319
1394
  // src/agents/codex.ts
1320
- import { join as join8 } from "path";
1395
+ import { join as join9 } from "path";
1321
1396
  import { parse, stringify } from "smol-toml";
1322
1397
  function table(parent, key) {
1323
1398
  const value = parent[key];
@@ -1374,18 +1449,18 @@ function codexCredentialStore(config) {
1374
1449
  }
1375
1450
  async function credentialDiagnostic(layout, role, store, binary) {
1376
1451
  const home = roleHome(layout, "codex", role);
1377
- const path = join8(home, "auth.json");
1452
+ const path = join9(home, "auth.json");
1378
1453
  const login = roleCommand({ CODEX_HOME: home }, binary, ["login"]);
1379
1454
  const status = roleCommand({ CODEX_HOME: home }, binary, ["login", "status"]);
1380
1455
  const state = await inspectCredentialPath(path);
1381
- const id = `codex.auth.${role}`;
1456
+ const id2 = `codex.auth.${role}`;
1382
1457
  const checks = [];
1383
1458
  if (state.kind !== "missing") {
1384
- checks.push(preservedCredentialCheck({ id, agent: "Codex", role, path, state, login, status }));
1459
+ checks.push(preservedCredentialCheck({ id: id2, agent: "Codex", role, path, state, login, status }));
1385
1460
  } else {
1386
1461
  if (store === "keyring" || store === "auto") {
1387
1462
  checks.push({
1388
- id: `${id}.native-keyring-unverifiable`,
1463
+ id: `${id2}.native-keyring-unverifiable`,
1389
1464
  status: "warn",
1390
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.`,
1391
1466
  fix: `Authenticate this role with: ${login}. Check status with: ${status}.`
@@ -1394,7 +1469,7 @@ async function credentialDiagnostic(layout, role, store, binary) {
1394
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." : "";
1395
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" : ""}`;
1396
1471
  checks.push({
1397
- id: `${id}.login-required`,
1472
+ id: `${id2}.login-required`,
1398
1473
  status: "warn",
1399
1474
  message: `Codex ${role} auth: login-required; ${detail}.${apiKey} Authentication was not attempted.`,
1400
1475
  fix: `Authenticate this role with: ${login}. Check status with: ${status}.`
@@ -1403,7 +1478,7 @@ async function credentialDiagnostic(layout, role, store, binary) {
1403
1478
  }
1404
1479
  if (state.kind !== "missing" && (store === "keyring" || store === "auto")) {
1405
1480
  checks.push({
1406
- id: `${id}.native-keyring-unverifiable`,
1481
+ id: `${id2}.native-keyring-unverifiable`,
1407
1482
  status: "warn",
1408
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.`,
1409
1484
  fix: `Check status without exposing credentials: ${status}.`
@@ -1423,6 +1498,7 @@ var init_codex = __esm({
1423
1498
  init_which();
1424
1499
  init_mcp();
1425
1500
  init_resources();
1501
+ init_skills();
1426
1502
  SHARED2 = ["AGENTS.md", "skills", "plugins", "hooks.json"];
1427
1503
  EXECUTABLE2 = ["plugins", "hooks.json"];
1428
1504
  CATALOG = "model-catalog.json";
@@ -1441,7 +1517,7 @@ var init_codex = __esm({
1441
1517
  if (!binary) {
1442
1518
  return { entries: [], checks: [fail("codex.bin", "Codex executable not found.", "Install Codex, or pass --codex-bin /path/to/codex.")] };
1443
1519
  }
1444
- const configPath = join8(home, "config.toml");
1520
+ const configPath = join9(home, "config.toml");
1445
1521
  const raw = await readIfPresent(configPath);
1446
1522
  if (raw === void 0) {
1447
1523
  return { entries: [], checks: [fail("codex.home", `No config.toml in ${home}.`, "Run codex once to initialise it, or pass --codex-home.")] };
@@ -1489,19 +1565,29 @@ var init_codex = __esm({
1489
1565
  }
1490
1566
  checks.push(pass("codex.catalog", evidence2.markers === 0 ? `Codex model catalog captured from ${capture}; it declared no multi_agent_version field, and each role home gets that generated copy instead of Codex's built-in catalog.` : `Codex model catalog captured from ${capture}; ${String(evidence2.markers)} multi_agent_version field(s) are nulled in the generated copy in each role home.`));
1491
1567
  const shared = await existingPaths(home, SHARED2);
1568
+ const roomSkill = { name: ROOM_SKILL_NAME, source: roomSkillSource(layout) };
1492
1569
  for (const role of roles) {
1493
1570
  const target = roleHome(layout, "codex", role);
1494
- const catalogPath = join8(target, CATALOG);
1571
+ const catalogPath = join9(target, CATALOG);
1495
1572
  const roleDocument = renderInstructions(role);
1496
1573
  entries.push({ kind: "dir", path: target });
1497
- entries.push({ kind: "file", path: join8(target, "role-instructions.md"), content: roleDocument });
1574
+ entries.push({ kind: "file", path: join9(target, "role-instructions.md"), content: roleDocument });
1498
1575
  entries.push({
1499
1576
  kind: "file",
1500
- path: join8(target, "config.toml"),
1577
+ path: join9(target, "config.toml"),
1501
1578
  content: renderRoleConfig(source, { roleDocument, catalogPath })
1502
1579
  });
1503
1580
  entries.push({ kind: "file", path: catalogPath, content: catalogSource });
1504
- entries.push(...await roleResourceEntries({ role, target, home, names: SHARED2, shared, executable: EXECUTABLE2 }));
1581
+ entries.push(...await roleResourceEntries({
1582
+ role,
1583
+ target,
1584
+ home,
1585
+ names: SHARED2,
1586
+ shared,
1587
+ executable: EXECUTABLE2,
1588
+ roomSkill,
1589
+ leadSkillProjection: leadSkillProjection(layout, "codex")
1590
+ }));
1505
1591
  credentials.push(await credentialDiagnostic(layout, role, store, binary));
1506
1592
  }
1507
1593
  return { entries, credentials, checks, binary };
@@ -1514,7 +1600,7 @@ var init_codex = __esm({
1514
1600
  import { spawn as spawn2 } from "child_process";
1515
1601
  import { mkdtemp, readFile as readFile3, realpath, rm as rm2, stat } from "fs/promises";
1516
1602
  import { tmpdir } from "os";
1517
- import { isAbsolute as isAbsolute2, join as join9, resolve as resolve3 } from "path";
1603
+ import { isAbsolute as isAbsolute2, join as join10, resolve as resolve3 } from "path";
1518
1604
  function asObject2(value) {
1519
1605
  return value !== null && typeof value === "object" && !Array.isArray(value) ? { ...value } : void 0;
1520
1606
  }
@@ -1538,12 +1624,12 @@ function adapterFailure(home, detail) {
1538
1624
  return fail(
1539
1625
  "pi.adapter",
1540
1626
  detail,
1541
- `Install ${PACKAGE_NAME} in ${join9(home, "npm")} with Pi yourself, then run setup again.`
1627
+ `Install ${PACKAGE_NAME} in ${join10(home, "npm")} with Pi yourself, then run setup again.`
1542
1628
  );
1543
1629
  }
1544
1630
  async function resolvePiAdapter(home) {
1545
- const packageRoot = join9(home, "npm", "node_modules", PACKAGE_NAME);
1546
- const manifestPath = join9(packageRoot, "package.json");
1631
+ const packageRoot = join10(home, "npm", "node_modules", PACKAGE_NAME);
1632
+ const manifestPath = join10(packageRoot, "package.json");
1547
1633
  try {
1548
1634
  const [canonicalRoot, manifestStat, raw] = await Promise.all([
1549
1635
  realpath(packageRoot),
@@ -1608,10 +1694,10 @@ function runProbeProcess(executable, adapterEntry, env, options, cwd) {
1608
1694
  clearTimeout(timer);
1609
1695
  resolveResult(result);
1610
1696
  };
1611
- const abort = (reason) => {
1697
+ const abort = (reason3) => {
1612
1698
  if (settled) return;
1613
1699
  terminateProbe(child);
1614
- finish({ ok: false, stdout: Buffer.concat(stdout).toString("utf8"), reason });
1700
+ finish({ ok: false, stdout: Buffer.concat(stdout).toString("utf8"), reason: reason3 });
1615
1701
  };
1616
1702
  const capture = (chunk, keep) => {
1617
1703
  if (settled) return;
@@ -1640,9 +1726,9 @@ function runProbeProcess(executable, adapterEntry, env, options, cwd) {
1640
1726
  child.on("error", () => {
1641
1727
  abort("failure");
1642
1728
  });
1643
- child.on("close", (code) => {
1729
+ child.on("close", (code2) => {
1644
1730
  const output = Buffer.concat(stdout).toString("utf8");
1645
- 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" });
1646
1732
  });
1647
1733
  child.stdin.on("error", () => {
1648
1734
  });
@@ -1651,7 +1737,7 @@ function runProbeProcess(executable, adapterEntry, env, options, cwd) {
1651
1737
  });
1652
1738
  }
1653
1739
  async function runProbe(executable, adapterEntry, env, options) {
1654
- const createdCwd = await mkdtemp(join9(tmpdir(), "paseo-room-pi-probe-"));
1740
+ const createdCwd = await mkdtemp(join10(tmpdir(), "paseo-room-pi-probe-"));
1655
1741
  const cwd = await realpath(createdCwd);
1656
1742
  try {
1657
1743
  return await runProbeProcess(executable, adapterEntry, {
@@ -1728,15 +1814,15 @@ async function readPiOptional(path) {
1728
1814
  }
1729
1815
  async function piCredentialDiagnostic(layout, role, binary = "pi") {
1730
1816
  const home = roleHome(layout, "pi", role);
1731
- const path = join9(home, "auth.json");
1817
+ const path = join10(home, "auth.json");
1732
1818
  const launch = piLoginCommand(home, binary);
1733
1819
  const state = await inspectCredentialPath(path);
1734
- const id = `pi.auth.${role}`;
1820
+ const id2 = `pi.auth.${role}`;
1735
1821
  if (state.kind !== "missing") {
1736
1822
  return {
1737
1823
  path,
1738
1824
  checks: [preservedCredentialCheck({
1739
- id,
1825
+ id: id2,
1740
1826
  agent: "Pi",
1741
1827
  role,
1742
1828
  path,
@@ -1746,9 +1832,9 @@ async function piCredentialDiagnostic(layout, role, binary = "pi") {
1746
1832
  };
1747
1833
  }
1748
1834
  const configured = presentNames(layout.envNames, AUTH_ENV2);
1749
- 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)] };
1750
1836
  return { path, checks: [{
1751
- id: `${id}.login-required`,
1837
+ id: `${id2}.login-required`,
1752
1838
  status: "warn",
1753
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.`,
1754
1840
  fix: `Start a minimal, non-room-equivalent interactive login session with: ${launch}. Run /login, then exit Pi.`
@@ -1768,6 +1854,7 @@ var init_pi = __esm({
1768
1854
  init_which();
1769
1855
  init_mcp();
1770
1856
  init_resources();
1857
+ init_skills();
1771
1858
  PACKAGE_NAME = "pi-mcp-adapter";
1772
1859
  PROBE_ID = "paseo-room-pi-mcp-probe";
1773
1860
  PROBE_TIMEOUT_MS = 1e4;
@@ -1824,7 +1911,7 @@ var init_pi = __esm({
1824
1911
  PI_OFFLINE: "1"
1825
1912
  });
1826
1913
  if (capability.status === "fail") return { entries: [], checks: [resolved.check, capability] };
1827
- const settingsPath = join9(home, "settings.json");
1914
+ const settingsPath = join10(home, "settings.json");
1828
1915
  let settings;
1829
1916
  try {
1830
1917
  settings = renderPiSettings(await readPiOptional(settingsPath));
@@ -1842,7 +1929,7 @@ var init_pi = __esm({
1842
1929
  ]
1843
1930
  };
1844
1931
  }
1845
- const operatorAppendPath = join9(home, "APPEND_SYSTEM.md");
1932
+ const operatorAppendPath = join10(home, "APPEND_SYSTEM.md");
1846
1933
  let operatorAppend;
1847
1934
  try {
1848
1935
  operatorAppend = await readPiOptional(operatorAppendPath);
@@ -1861,7 +1948,7 @@ var init_pi = __esm({
1861
1948
  };
1862
1949
  }
1863
1950
  const shared = await existingPaths(home, SHARED3);
1864
- const mcpPath = join9(home, MCP_FILE);
1951
+ const mcpPath = join10(home, MCP_FILE);
1865
1952
  let servers;
1866
1953
  try {
1867
1954
  servers = jsonServerTable(await readPiOptional(mcpPath), MCP_KEYS);
@@ -1884,13 +1971,23 @@ var init_pi = __esm({
1884
1971
  const entries = [];
1885
1972
  const credentials = [];
1886
1973
  const argv = {};
1974
+ const roomSkill = { name: ROOM_SKILL_NAME, source: roomSkillSource(layout) };
1887
1975
  for (const role of roles) {
1888
1976
  const target = roleHome(layout, "pi", role);
1889
- const appendPath = join9(target, "APPEND_SYSTEM.md");
1977
+ const appendPath = join10(target, "APPEND_SYSTEM.md");
1890
1978
  entries.push({ kind: "dir", path: target });
1891
- entries.push({ kind: "file", path: join9(target, "settings.json"), content: settings });
1979
+ entries.push({ kind: "file", path: join10(target, "settings.json"), content: settings });
1892
1980
  entries.push({ kind: "file", path: appendPath, content: renderPiAppend(operatorAppend, role) });
1893
- entries.push(...await roleResourceEntries({ role, target, home, names: SHARED3, shared, executable: EXECUTABLE3 }));
1981
+ entries.push(...await roleResourceEntries({
1982
+ role,
1983
+ target,
1984
+ home,
1985
+ names: SHARED3,
1986
+ shared,
1987
+ executable: EXECUTABLE3,
1988
+ roomSkill,
1989
+ leadSkillProjection: leadSkillProjection(layout, "pi")
1990
+ }));
1894
1991
  credentials.push(await piCredentialDiagnostic(layout, role, binary));
1895
1992
  argv[role] = ["--no-extensions", "--extension", resolved.adapter.entry, "--no-approve", "--append-system-prompt", appendPath];
1896
1993
  }
@@ -1909,8 +2006,8 @@ var init_pi = __esm({
1909
2006
  // src/paseo.ts
1910
2007
  import { gte, valid } from "semver";
1911
2008
  import { z as z2 } from "zod";
1912
- function minimumPaseoVersion(agents) {
1913
- 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;
1914
2011
  }
1915
2012
  function normalizeUrl(listen) {
1916
2013
  const match = /^(?:ws:\/\/)?([^:/]+|\[[^\]]+\]):(\d+)$/.exec(listen.trim());
@@ -1919,7 +2016,7 @@ function normalizeUrl(listen) {
1919
2016
  const local = ["localhost", "127.0.0.1", "0.0.0.0", "[::1]", "[::]"].includes(host) ? "127.0.0.1" : host;
1920
2017
  return `ws://${local}:${port}`;
1921
2018
  }
1922
- function assessStatus(raw, minimumVersion = MINIMUM_VERSION) {
2019
+ function assessStatus(raw, minimumVersion = MINIMUM_VERSION, cliVersion) {
1923
2020
  const parsed = statusSchema.safeParse(raw);
1924
2021
  if (!parsed.success) {
1925
2022
  return { checks: [fail("paseo.status", "Paseo status output was not understood.", "Upgrade Paseo, then run: paseo daemon status --json")] };
@@ -1928,8 +2025,8 @@ function assessStatus(raw, minimumVersion = MINIMUM_VERSION) {
1928
2025
  if (status.localDaemon !== "running") {
1929
2026
  return { checks: [fail("paseo.daemon", `Paseo daemon is ${status.localDaemon}.`, "Start it with: paseo daemon start")] };
1930
2027
  }
1931
- const cli = valid(status.cliVersion);
1932
- 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);
1933
2030
  if (!cli || !daemon) {
1934
2031
  return { checks: [fail("paseo.version", "Paseo did not report a usable version.", "Upgrade Paseo to a release that reports semver versions.")] };
1935
2032
  }
@@ -1959,11 +2056,16 @@ async function checkDaemon(layout, env = process.env, minimumVersion = MINIMUM_V
1959
2056
  if (!output.ok) {
1960
2057
  return { checks: [fail("paseo.status", "Could not read Paseo daemon status.", "Run: paseo daemon status --json")] };
1961
2058
  }
2059
+ let status;
1962
2060
  try {
1963
- return assessStatus(JSON.parse(output.stdout), minimumVersion);
2061
+ status = JSON.parse(output.stdout);
1964
2062
  } catch {
1965
2063
  return assessStatus(void 0, minimumVersion);
1966
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);
1967
2069
  }
1968
2070
  async function withSession(daemon, run2, options = {}) {
1969
2071
  const quiet = { debug() {
@@ -1987,7 +2089,13 @@ async function withSession(daemon, run2, options = {}) {
1987
2089
  import("@getpaseo/client"),
1988
2090
  import("@getpaseo/client/internal/daemon-client")
1989
2091
  ]);
1990
- 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
+ });
1991
2099
  client = {
1992
2100
  ...createPaseoApi(raw),
1993
2101
  connect: () => raw.connect(),
@@ -1997,10 +2105,10 @@ async function withSession(daemon, run2, options = {}) {
1997
2105
  },
1998
2106
  getConnectionState: () => raw.getConnectionState(),
1999
2107
  listPlugins: () => raw.listPlugins(),
2000
- installDirectoryPlugin: (path, id) => raw.installDirectoryPlugin(path, id),
2001
- reloadPlugin: (id) => raw.reloadPlugin(id),
2002
- enablePlugin: (id) => raw.enablePlugin(id),
2003
- 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)
2004
2112
  };
2005
2113
  }
2006
2114
  try {
@@ -2030,17 +2138,17 @@ async function withSession(daemon, run2, options = {}) {
2030
2138
  async listPlugins() {
2031
2139
  return await client.listPlugins();
2032
2140
  },
2033
- async installPlugin(path, id) {
2034
- return await client.installDirectoryPlugin(path, id);
2141
+ async installPlugin(path, id2) {
2142
+ return await client.installDirectoryPlugin(path, id2);
2035
2143
  },
2036
- async reloadPlugin(id) {
2037
- return await client.reloadPlugin(id);
2144
+ async reloadPlugin(id2) {
2145
+ return await client.reloadPlugin(id2);
2038
2146
  },
2039
- async enablePlugin(id) {
2040
- return await client.enablePlugin(id);
2147
+ async enablePlugin(id2) {
2148
+ return await client.enablePlugin(id2);
2041
2149
  },
2042
- async removePlugin(id) {
2043
- await client.removePlugin(id);
2150
+ async removePlugin(id2) {
2151
+ await client.removePlugin(id2);
2044
2152
  },
2045
2153
  async refresh(ids) {
2046
2154
  await client.providers.refresh({ providers: [...ids] });
@@ -2100,9 +2208,9 @@ var init_paseo = __esm({
2100
2208
  PI_MINIMUM_VERSION = "0.8.0";
2101
2209
  statusSchema = z2.object({
2102
2210
  listen: z2.string().min(1),
2103
- localDaemon: z2.enum(["running", "stopped", "stale_pid", "unresponsive"]),
2104
- cliVersion: z2.string(),
2105
- daemonVersion: z2.string().nullable()
2211
+ localDaemon: z2.string().min(1),
2212
+ cliVersion: z2.string().optional(),
2213
+ daemonVersion: z2.string().nullish()
2106
2214
  });
2107
2215
  configSchema = z2.object({
2108
2216
  config: z2.object({
@@ -2116,20 +2224,29 @@ var init_paseo = __esm({
2116
2224
  });
2117
2225
 
2118
2226
  // src/plugin.ts
2119
- import { readFileSync as readFileSync2 } from "fs";
2120
- import { join as join10 } from "path";
2227
+ import { readFileSync as readFileSync3 } from "fs";
2228
+ import { join as join11 } from "path";
2121
2229
  function loadAsset(path) {
2122
- return readFileSync2(new URL(`./plugin-assets/${path}`, import.meta.url), "utf8");
2230
+ return readFileSync3(new URL(`./plugin-assets/${path}`, import.meta.url), "utf8");
2123
2231
  }
2124
2232
  function claudeCarrierPluginDir(layout) {
2125
- return join10(layout.roomHome, "plugin");
2233
+ return join11(layout.roomHome, "plugin");
2234
+ }
2235
+ function typescriptTemplateLiteral(value) {
2236
+ const escaped = value.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${");
2237
+ return `\`${escaped}\``;
2126
2238
  }
2127
2239
  function renderClaudeCarrierContract(roles) {
2128
- const contracts = Object.fromEntries(roles.map((role) => [providerId("claude", role), renderInstructions(role)]));
2240
+ const entries = roles.map((role) => {
2241
+ const id2 = JSON.stringify(providerId("claude", role));
2242
+ return ` ${id2}: ${typescriptTemplateLiteral(renderInstructions(role))},`;
2243
+ });
2129
2244
  return [
2130
2245
  "/** Generated by paseo-room. The Markdown prompt assets remain authoritative. */",
2131
2246
  `export const GENERATION = ${JSON.stringify(contractDigest())};`,
2132
- `export const CONTRACT: Readonly<Record<string, string>> = ${JSON.stringify(contracts, null, 2)};`,
2247
+ "export const CONTRACT: Readonly<Record<string, string>> = {",
2248
+ ...entries,
2249
+ "};",
2133
2250
  ""
2134
2251
  ].join("\n");
2135
2252
  }
@@ -2137,18 +2254,19 @@ function claudeCarrierEntries(layout, roles) {
2137
2254
  const root = claudeCarrierPluginDir(layout);
2138
2255
  return [
2139
2256
  { kind: "dir", path: root },
2140
- { kind: "dir", path: join10(root, "server") },
2141
- ...STATIC_ASSETS.map((path) => ({ kind: "file", path: join10(root, path), content: loadAsset(path) })),
2142
- { kind: "file", path: join10(root, "server", "contract.ts"), content: renderClaudeCarrierContract(roles) }
2257
+ { kind: "dir", path: join11(root, "server") },
2258
+ ...STATIC_ASSETS.map((path) => ({ kind: "file", path: join11(root, path), content: loadAsset(path) })),
2259
+ { kind: "file", path: join11(root, "server", "contract.ts"), content: renderClaudeCarrierContract(roles) }
2143
2260
  ];
2144
2261
  }
2145
- var CLAUDE_CARRIER_PLUGIN_ID, STATIC_ASSETS;
2262
+ var CLAUDE_CARRIER_PLUGIN_ID, CLAUDE_CARRIER_PASEO_RANGE, STATIC_ASSETS;
2146
2263
  var init_plugin = __esm({
2147
2264
  "src/plugin.ts"() {
2148
2265
  "use strict";
2149
2266
  init_roles();
2150
2267
  init_instructions();
2151
2268
  CLAUDE_CARRIER_PLUGIN_ID = "paseo-room-claude-carrier";
2269
+ CLAUDE_CARRIER_PASEO_RANGE = ">=0.8.0 <0.10.0";
2152
2270
  STATIC_ASSETS = [
2153
2271
  "paseo-plugin.json",
2154
2272
  "package.json",
@@ -2159,9 +2277,1312 @@ var init_plugin = __esm({
2159
2277
  }
2160
2278
  });
2161
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
+
2162
3582
  // src/commands.ts
2163
- import { join as join11, sep as sep2 } from "path";
2164
- 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";
2165
3586
  function sessionOptions(options) {
2166
3587
  const password = (options.env ?? process.env).PASEO_PASSWORD;
2167
3588
  return { ...password === void 0 ? {} : { password }, ...options.factory ? { factory: options.factory } : {} };
@@ -2188,18 +3609,47 @@ function roleProfiles(agent, roles) {
2188
3609
  notes: ROLE_NOTES[role]
2189
3610
  }));
2190
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
+ }
2191
3638
  async function buildDesired(layout, agents, roles, build = {}) {
2192
3639
  const entries = [
2193
3640
  { kind: "dir", path: layout.roomHome },
2194
3641
  { kind: "dir", path: sharedRoom(layout) },
2195
- { kind: "file", path: join11(sharedRoom(layout), "WORKSPACE_PROTOCOL.md"), content: renderInstructions("workspace") }
3642
+ // Earlier versions generated a default workspace protocol template here. The room ships no
3643
+ // default protocol now, so the old generated regular file is declared absent and removed.
3644
+ { kind: "absent", path: join16(sharedRoom(layout), "WORKSPACE_PROTOCOL.md") },
3645
+ ...roomSkillEntries(layout, agents)
2196
3646
  ];
2197
3647
  const providers = {};
2198
3648
  const profiles = [];
2199
3649
  const checks = [];
2200
3650
  const binaries = {};
2201
- for (const id of agents) {
2202
- const agent = AGENTS[id];
3651
+ for (const id2 of agents) {
3652
+ const agent = AGENTS[id2];
2203
3653
  const plan = await agent.build(layout, roles, build);
2204
3654
  entries.push(...plan.entries);
2205
3655
  checks.push(...plan.checks);
@@ -2209,13 +3659,13 @@ async function buildDesired(layout, agents, roles, build = {}) {
2209
3659
  const collisions = plan.entries.filter((entry) => credentialPaths.has(entry.path));
2210
3660
  if (collisions.length > 0) {
2211
3661
  checks.push(fail(
2212
- `${id}.credentials`,
3662
+ `${id2}.credentials`,
2213
3663
  `${agent.label} produced managed entries for preserve-only credential paths: ${collisions.map((entry) => entry.path).join(", ")}.`,
2214
3664
  "This is an adapter defect; do not apply the plan."
2215
3665
  ));
2216
3666
  }
2217
3667
  if (plan.binary === void 0) continue;
2218
- binaries[id] = plan.binary;
3668
+ binaries[id2] = plan.binary;
2219
3669
  Object.assign(providers, roleProviders(layout, agent, {
2220
3670
  binary: plan.binary,
2221
3671
  ...plan.argv ? { argv: plan.argv } : {},
@@ -2223,33 +3673,42 @@ async function buildDesired(layout, agents, roles, build = {}) {
2223
3673
  }, roles));
2224
3674
  profiles.push(...roleProfiles(agent, roles));
2225
3675
  }
2226
- if (agents.every((id) => binaries[id] !== void 0)) {
3676
+ if (agents.every((id2) => binaries[id2] !== void 0)) {
2227
3677
  entries.push({
2228
3678
  kind: "file",
2229
- path: join11(layout.roomHome, AUTHENTICATION_GUIDE),
3679
+ path: join16(layout.roomHome, AUTHENTICATION_GUIDE),
2230
3680
  content: renderAuthenticationGuide(layout.roomHome, binaries, agents, roles)
2231
3681
  });
2232
3682
  checks.push(warn(
2233
3683
  "room.authentication-not-validated",
2234
- `Role authentication was not validated. Login instructions are managed at ${join11(layout.roomHome, AUTHENTICATION_GUIDE)}.`,
3684
+ `Role authentication was not validated. Login instructions are managed at ${join16(layout.roomHome, AUTHENTICATION_GUIDE)}.`,
2235
3685
  "After setup --apply, follow that guide or run: paseo-room auth login <agent> <role>"
2236
3686
  ));
2237
3687
  }
2238
- const pluginPath = agents.includes("claude") ? claudeCarrierPluginDir(layout) : void 0;
2239
- if (pluginPath !== void 0) entries.push(...claudeCarrierEntries(layout, roles));
2240
- entries.push({ kind: "file", path: join11(layout.roomHome, MARKER), content: renderMarker(package_default.version, agents, roles, contractDigest(), markerMemoryContract(agents, build.memoryContract ?? true)) });
2241
- 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 } };
2242
3701
  }
2243
3702
  function contractProvenance(previous) {
2244
- const digest = contractDigest();
3703
+ const digest3 = contractDigest();
2245
3704
  if (!previous) return [];
2246
- if (previous.contract === digest) {
2247
- 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.`)];
2248
3707
  }
2249
3708
  const held = previous.contract === void 0 ? `was installed before the room recorded its contract generation (package ${previous.version})` : `holds contract generation ${previous.contract}`;
2250
3709
  return [warn(
2251
3710
  "room.contract",
2252
- `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.`,
2253
3712
  "Run: paseo-room setup --apply, then restart the affected seats."
2254
3713
  )];
2255
3714
  }
@@ -2272,14 +3731,14 @@ function fileDriftSummary(drifted) {
2272
3731
  return `${count} ${subject} missing or outdated, and ${String(removals)} of them are present but suppressed by this room.`;
2273
3732
  }
2274
3733
  function fileDriftDetail(targets) {
2275
- const catalogs = targets.filter((target) => target.endsWith(`${sep2}model-catalog.json`));
3734
+ const catalogs = targets.filter((target) => target.endsWith(`${sep3}model-catalog.json`));
2276
3735
  if (catalogs.length === 0) return "";
2277
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`;
2278
3737
  return ` ${count}, so those seats currently lack the scrubbed catalog closure rather than only older contract text.`;
2279
3738
  }
2280
3739
  async function safeDirectory(path, checks) {
2281
3740
  try {
2282
- const stat2 = await lstat4(path);
3741
+ const stat2 = await lstat5(path);
2283
3742
  if (stat2.isDirectory()) return true;
2284
3743
  checks.push(fail(
2285
3744
  "room.path-safety",
@@ -2300,17 +3759,32 @@ async function safeDirectory(path, checks) {
2300
3759
  async function roomPathSafety(layout, agents, roles) {
2301
3760
  const checks = [];
2302
3761
  if (!await safeDirectory(layout.roomHome, checks)) return checks;
2303
- await safeDirectory(sharedRoom(layout), checks);
2304
- const rolesHome = join11(layout.roomHome, "roles");
3762
+ const room = sharedRoom(layout);
3763
+ const roomIsSafe = await safeDirectory(room, checks);
3764
+ if (roomIsSafe && agents.length > 0) {
3765
+ const skills = join16(room, "skills");
3766
+ if (await safeDirectory(skills, checks)) {
3767
+ const source = roomSkillSource(layout);
3768
+ if (await safeDirectory(source, checks)) await safeDirectory(join16(source, "references"), checks);
3769
+ }
3770
+ const projections = skillProjectionRoot(layout);
3771
+ if (await safeDirectory(projections, checks)) {
3772
+ for (const agent of agents) {
3773
+ const agentRoot = agentSkillProjectionRoot(layout, agent);
3774
+ if (await safeDirectory(agentRoot, checks)) await safeDirectory(leadSkillProjection(layout, agent), checks);
3775
+ }
3776
+ }
3777
+ }
3778
+ const rolesHome = join16(layout.roomHome, "roles");
2305
3779
  if (!await safeDirectory(rolesHome, checks)) return checks;
2306
3780
  for (const agent of agents) {
2307
- const agentHome = join11(rolesHome, agent);
3781
+ const agentHome = join16(rolesHome, agent);
2308
3782
  if (!await safeDirectory(agentHome, checks)) continue;
2309
3783
  for (const role of roles) await safeDirectory(roleHome(layout, agent, role), checks);
2310
3784
  }
2311
3785
  return checks;
2312
3786
  }
2313
- async function staleFrom(layout, previous, agents, roles) {
3787
+ async function staleFrom(layout, previous, agents, roles, runtime) {
2314
3788
  if (!previous) return NOTHING_STALE;
2315
3789
  const kept = new Set(agents.flatMap((agent) => roles.map((role) => providerId(agent, role))));
2316
3790
  const providerIds = [];
@@ -2328,110 +3802,148 @@ async function staleFrom(layout, previous, agents, roles) {
2328
3802
  return {
2329
3803
  providerIds,
2330
3804
  profileIds,
2331
- 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
+ ],
2332
3809
  retainedDirectories
2333
3810
  };
2334
3811
  }
2335
- function pluginPlan(desired, stale, plugins, expectedPath) {
2336
- const livePlugin = plugins.find((plugin) => plugin.id === CLAUDE_CARRIER_PLUGIN_ID);
2337
- const shouldManage = desired.pluginPath !== void 0 || stale.removePlugin;
2338
- if (!shouldManage) return { checks: [] };
2339
- 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) {
2340
3815
  return {
2341
3816
  livePlugin,
2342
3817
  checks: [fail(
2343
- "claude.plugin.path",
2344
- `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}.`,
2345
3820
  "Move or remove that conflicting plugin registration manually, then run setup again."
2346
3821
  )]
2347
3822
  };
2348
3823
  }
2349
- if (desired.pluginPath !== void 0) {
3824
+ if (wanted) {
2350
3825
  const healthy = livePlugin?.enabled === true && livePlugin.status === "running" && livePlugin.error === void 0;
2351
3826
  return {
2352
3827
  ...livePlugin === void 0 ? {} : { livePlugin },
2353
- 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 },
2354
3829
  checks: []
2355
3830
  };
2356
3831
  }
2357
3832
  return {
2358
3833
  ...livePlugin === void 0 ? {} : { livePlugin },
2359
- 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 },
2360
3835
  checks: []
2361
3836
  };
2362
3837
  }
2363
- async function planRoom(session, desired, stale, expectedPluginPath) {
2364
- 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;
2365
3840
  const [live, liveProfiles, plugins] = await Promise.all([
2366
3841
  session.readProviders(),
2367
3842
  session.readProfiles(),
2368
3843
  inspectPlugins ? session.listPlugins() : Promise.resolve([])
2369
3844
  ]);
2370
3845
  const byId = new Map(liveProfiles.map((entry) => [String(entry.id), entry]));
2371
- 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);
2372
3852
  return {
2373
3853
  liveProfiles,
2374
- ...plugin.livePlugin === void 0 ? {} : { livePlugin: plugin.livePlugin },
2375
- pluginChecks: plugin.checks,
3854
+ livePlugins,
3855
+ pluginChecks: planned.flatMap((plugin) => plugin.checks),
2376
3856
  operations: [
2377
3857
  ...await planEntries(desired.entries),
2378
- ...Object.entries(desired.providers).map(([id, provider]) => ({
2379
- 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",
2380
3860
  kind: "provider",
2381
- target: id
3861
+ target: id2
2382
3862
  })),
2383
3863
  ...desired.profiles.map((profile) => ({
2384
3864
  action: !byId.has(profile.id) ? "create" : profileMatches(profile, byId.get(profile.id)) ? "noop" : "update",
2385
3865
  kind: "profile",
2386
3866
  target: profile.id
2387
3867
  })),
2388
- ...stale.providerIds.filter((id) => id in live).map((id) => ({ action: "remove", kind: "provider", target: id })),
2389
- ...stale.profileIds.filter((id) => byId.has(id)).map((id) => ({ action: "remove", kind: "profile", target: id })),
2390
- ...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])
2391
3871
  ]
2392
3872
  };
2393
3873
  }
2394
- function pluginsEnabledCheck(enabled) {
2395
- return enabled ? pass("claude.plugin.enabled", "Paseo plugins are enabled for the required Claude contract carrier.") : fail(
2396
- "claude.plugin.enabled",
2397
- "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,
2398
3878
  "Review that trust boundary, enable plugins in Paseo Settings, reload Paseo, then run setup again."
2399
3879
  );
2400
3880
  }
2401
- function pluginRuntimeCheck(plugin, expectedPath) {
3881
+ function pluginRuntimeCheck(spec, plugin) {
2402
3882
  if (plugin === void 0) {
2403
- 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");
2404
3884
  }
2405
- if (plugin.path !== expectedPath) {
3885
+ if (plugin.path !== spec.path) {
2406
3886
  return fail(
2407
- "claude.plugin.runtime",
2408
- `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}.`,
2409
3889
  "Move or remove that conflicting plugin registration manually, then run setup again."
2410
3890
  );
2411
3891
  }
2412
3892
  if (!plugin.enabled || plugin.status !== "running" || plugin.error !== void 0) {
2413
3893
  const detail = plugin.error === void 0 ? `status ${plugin.status}` : `${plugin.status}: ${plugin.error}`;
2414
- 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");
2415
3895
  }
2416
- 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}.`);
2417
3897
  }
2418
- async function reconcilePlugin(session, desired, plan, pending, expectedPath) {
2419
- if (desired.pluginPath === void 0) {
2420
- if (plan.livePlugin !== void 0) await session.removePlugin(CLAUDE_CARRIER_PLUGIN_ID);
2421
- 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);
2422
3901
  }
2423
- let plugin = plan.livePlugin;
2424
- if (plugin === void 0) {
2425
- plugin = await session.installPlugin(expectedPath, CLAUDE_CARRIER_PLUGIN_ID);
2426
- } else {
2427
- if (!plugin.enabled) plugin = await session.enablePlugin(CLAUDE_CARRIER_PLUGIN_ID);
2428
- const filesChanged = pending.some(
2429
- (operation) => (operation.kind === "file" || operation.kind === "dir") && operation.target.startsWith(`${expectedPath}${sep2}`)
2430
- );
2431
- const needsReload = filesChanged || plugin.status !== "running" || plugin.error !== void 0;
2432
- 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));
2433
3916
  }
2434
- return [pluginRuntimeCheck(plugin, expectedPath)];
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
+ )];
3931
+ }
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
+ )];
2435
3947
  }
2436
3948
  function markerMemoryContract(agents, memoryContract) {
2437
3949
  return agents.includes("claude") ? memoryContract : void 0;
@@ -2457,23 +3969,29 @@ async function setup(options = {}) {
2457
3969
  const pathSafety = await roomPathSafety(layout, safetyAgents, ROLES);
2458
3970
  if (hasFailure(pathSafety)) return failed("setup", pathSafety);
2459
3971
  const compatibilityAgents = [.../* @__PURE__ */ new Set([...agents, ...previous?.agents ?? []])];
2460
- 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));
2461
3974
  if (!daemon.daemon) return failed("setup", daemon.checks);
2462
- const desired = await buildDesired(layout, agents, ROLES, { memoryContract });
2463
- 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
+ ];
2464
3982
  const retained = stale.retainedDirectories.length === 0 ? [] : [warn(
2465
3983
  "room.stale-role-homes-preserved",
2466
3984
  `Preserved ${String(stale.retainedDirectories.length)} deselected role homes because they may contain role-owned credentials or runtime state.`,
2467
3985
  `Use paseo-room remove --apply to delete the entire room after reviewing its credential warning, or remove deselected homes manually: ${stale.retainedDirectories.join(", ")}`
2468
3986
  )];
2469
- const checks = [...daemon.checks, ...desired.checks, ...retained];
3987
+ const checks = [...daemon.checks, ...runtimeChecks, ...desired.checks, ...retained];
2470
3988
  if (hasFailure(checks)) return failed("setup", checks);
2471
3989
  return withSession(daemon.daemon, async (session) => {
2472
- const expectedPluginPath = claudeCarrierPluginDir(layout);
2473
- const plan = await planRoom(session, desired, stale, expectedPluginPath);
3990
+ const plan = await planRoom(session, desired, stale);
2474
3991
  const { operations, liveProfiles } = plan;
2475
3992
  const pending = operations.filter((operation) => operation.action !== "noop");
2476
- 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];
2477
3995
  const preApplyDiagnostics = [...checks, ...pluginPreconditions, ...memoryContractDiagnostic(agents, memoryContract), ...contractProvenance(previous), ...thinkingDiagnostics(liveProfiles, desired.profiles)];
2478
3996
  if (hasFailure(preApplyDiagnostics) || !options.apply) {
2479
3997
  return {
@@ -2484,7 +4002,7 @@ async function setup(options = {}) {
2484
4002
  operations
2485
4003
  };
2486
4004
  }
2487
- const markerPath = join11(layout.roomHome, MARKER);
4005
+ const markerPath = join16(layout.roomHome, MARKER);
2488
4006
  const markerEntries = desired.entries.filter((entry) => entry.path === markerPath);
2489
4007
  await applyEntries(desired.entries.filter((entry) => entry.path !== markerPath));
2490
4008
  const ids = Object.keys(desired.providers);
@@ -2496,14 +4014,15 @@ async function setup(options = {}) {
2496
4014
  await session.writeProfiles(appliedProfiles);
2497
4015
  }
2498
4016
  await session.refresh(ids);
2499
- const pluginChecks = await reconcilePlugin(session, desired, plan, pending, expectedPluginPath);
4017
+ const pluginChecks = await reconcilePlugins(session, desired, stale, plan, pending);
2500
4018
  await applyEntries(markerEntries);
2501
4019
  const appliedMarker = {
2502
4020
  version: package_default.version,
2503
4021
  agents: [...agents],
2504
4022
  roles: [...ROLES],
2505
4023
  contract: contractDigest(),
2506
- ...markerMemoryContract(agents, memoryContract) === false ? { claudeMemoryContract: false } : {}
4024
+ ...markerMemoryContract(agents, memoryContract) === false ? { claudeMemoryContract: false } : {},
4025
+ ...desired.runtime === void 0 ? {} : { runtime: desired.runtime }
2507
4026
  };
2508
4027
  const appliedChecks = [
2509
4028
  ...checks,
@@ -2535,17 +4054,23 @@ async function verify(options = {}) {
2535
4054
  if (invalid.length > 0) return failed("verify", invalid);
2536
4055
  const pathSafety = await roomPathSafety(layout, marker.agents, marker.roles);
2537
4056
  if (hasFailure(pathSafety)) return failed("verify", pathSafety);
2538
- 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));
2539
4059
  if (!daemon.daemon) return failed("verify", daemon.checks);
4060
+ const version = daemon.daemon.version;
2540
4061
  const desired = await buildDesired(layout, marker.agents, marker.roles, {
2541
- // The room's own recorded choice, so verify compares against what setup wrote.
2542
- memoryContract: marker.claudeMemoryContract ?? true
4062
+ // The room's own recorded choices, so verify compares against what setup wrote.
4063
+ memoryContract: marker.claudeMemoryContract ?? true,
4064
+ runtime
2543
4065
  });
2544
- const checks = [...daemon.checks, ...desired.checks];
2545
- 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);
2546
4072
  return withSession(daemon.daemon, async (session) => {
2547
- const expectedPluginPath = claudeCarrierPluginDir(layout);
2548
- const plan = await planRoom(session, desired, NOTHING_STALE, expectedPluginPath);
4073
+ const plan = await planRoom(session, desired, NOTHING_STALE);
2549
4074
  const { operations, liveProfiles } = plan;
2550
4075
  const drifted = operations.filter((operation) => operation.action !== "noop");
2551
4076
  const count = (kind) => drifted.filter((operation) => operation.kind === kind).length;
@@ -2553,11 +4078,12 @@ async function verify(options = {}) {
2553
4078
  const profiles = count("profile");
2554
4079
  const driftedFiles = drifted.filter((operation) => !["provider", "profile", "plugin"].includes(operation.kind));
2555
4080
  const files = driftedFiles.length;
2556
- const pluginChecks = desired.pluginPath === void 0 ? [] : [
2557
- pluginsEnabledCheck(await session.pluginsEnabled()),
2558
- ...plan.pluginChecks,
2559
- pluginRuntimeCheck(plan.livePlugin, expectedPluginPath)
2560
- ];
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
+ ]);
2561
4087
  const all = [
2562
4088
  ...checks,
2563
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"),
@@ -2590,22 +4116,26 @@ async function remove(options = {}) {
2590
4116
  if (invalid.length > 0) return failed("remove", invalid);
2591
4117
  const ids = marker.agents.flatMap((agent) => marker.roles.map((role) => providerId(agent, role)));
2592
4118
  const profiles = new Set(marker.agents.flatMap((agent) => marker.roles.map((role) => profileId(agent, role))));
2593
- const hasClaude = marker.agents.includes("claude");
4119
+ const plugins = [
4120
+ ...marker.agents.includes("claude") ? [carrierSpec(layout)] : [],
4121
+ ...marker.runtime?.enabled === true ? [runtimeSpec(layout)] : []
4122
+ ];
2594
4123
  const operations = [
2595
- ...ids.map((id) => ({ action: "remove", kind: "provider", target: id })),
2596
- ...[...profiles].map((id) => ({ action: "remove", kind: "profile", target: id })),
2597
- ...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 })),
2598
4127
  { action: "remove", kind: "dir", target: layout.roomHome }
2599
4128
  ];
2600
4129
  const credentialWarning = warn(
2601
4130
  "room.remove.credentials",
2602
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.`
2603
4132
  );
4133
+ const runtimeWarnings = await runtimeRemovalWarning(layout);
2604
4134
  if (!options.apply) {
2605
- return { command: "remove", outcome: "changes-planned", changed: false, checks: [credentialWarning], operations };
4135
+ return { command: "remove", outcome: "changes-planned", changed: false, checks: [credentialWarning, ...runtimeWarnings], operations };
2606
4136
  }
2607
4137
  const daemon = await checkDaemon(layout, options.env);
2608
- const checks = [credentialWarning];
4138
+ const checks = [credentialWarning, ...runtimeWarnings];
2609
4139
  if (!daemon.daemon) {
2610
4140
  return failed("remove", [
2611
4141
  credentialWarning,
@@ -2619,13 +4149,15 @@ async function remove(options = {}) {
2619
4149
  }
2620
4150
  try {
2621
4151
  await withSession(daemon.daemon, async (session) => {
2622
- if (hasClaude) {
2623
- const plugin = (await session.listPlugins()).find((entry) => entry.id === CLAUDE_CARRIER_PLUGIN_ID);
2624
- const expectedPath = claudeCarrierPluginDir(layout);
2625
- if (plugin !== void 0 && plugin.path !== expectedPath) {
2626
- 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);
2627
4160
  }
2628
- if (plugin !== void 0) await session.removePlugin(CLAUDE_CARRIER_PLUGIN_ID);
2629
4161
  }
2630
4162
  await session.removeProviders(ids);
2631
4163
  const live = await session.readProfiles();
@@ -2644,7 +4176,7 @@ async function remove(options = {}) {
2644
4176
  ]);
2645
4177
  }
2646
4178
  try {
2647
- await rm3(layout.roomHome, { recursive: true, force: true });
4179
+ await rm4(layout.roomHome, { recursive: true, force: true });
2648
4180
  } catch (error) {
2649
4181
  const detail = error instanceof Error ? error.message : "unknown filesystem error";
2650
4182
  checks.push(fail(
@@ -2673,10 +4205,13 @@ var init_commands = __esm({
2673
4205
  init_plugin();
2674
4206
  init_result();
2675
4207
  init_instructions();
4208
+ init_skills();
2676
4209
  init_room();
4210
+ init_runtime();
4211
+ init_runtime_state();
2677
4212
  init_roles();
2678
4213
  AGENTS = { codex: codexAgent, claude: claudeAgent, pi: piAgent };
2679
- NOTHING_STALE = { providerIds: [], profileIds: [], removePlugin: false, retainedDirectories: [] };
4214
+ NOTHING_STALE = { providerIds: [], profileIds: [], removePlugins: [], retainedDirectories: [] };
2680
4215
  }
2681
4216
  });
2682
4217
 
@@ -2727,7 +4262,15 @@ async function runWizard(prompts, emit, write, options = {}, onAction) {
2727
4262
  if (typeof carrier === "symbol") return cancelled();
2728
4263
  claudeMemoryContract = carrier === "both";
2729
4264
  }
2730
- 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 } : {} };
2731
4274
  const preview = await setup(setupOptions);
2732
4275
  const status = emit(preview);
2733
4276
  if (preview.outcome !== "changes-planned") return status;
@@ -2753,9 +4296,105 @@ var init_wizard = __esm({
2753
4296
  init_package();
2754
4297
  init_auth();
2755
4298
  init_commands();
2756
- init_fsops();
2757
4299
  import { Command, CommanderError, InvalidArgumentError } from "commander";
2758
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
+
2759
4398
  // src/render.ts
2760
4399
  function renderJson(result) {
2761
4400
  return `${JSON.stringify({ schemaVersion: 2, ...result }, null, 2)}
@@ -2786,8 +4425,9 @@ function renderHuman(result) {
2786
4425
  init_result();
2787
4426
  init_roles();
2788
4427
  init_prompts();
4428
+ init_skills();
2789
4429
  function collectAgent(value, previous) {
2790
- const agent = AGENT_IDS.find((id) => id === value);
4430
+ const agent = AGENT_IDS.find((id2) => id2 === value);
2791
4431
  if (!agent) throw new InvalidArgumentError(`Unknown agent: ${value}`);
2792
4432
  const seated = previous ?? [];
2793
4433
  return seated.includes(agent) ? seated : [...seated, agent];
@@ -2806,7 +4446,8 @@ function optionsFrom(raw, base) {
2806
4446
  ...agents ? { agents } : {},
2807
4447
  ...raw.apply === true ? { apply: true } : {},
2808
4448
  // Commander defaults a --no- flag to true, so only an explicit opt-out is carried.
2809
- ...raw.claudeMemoryContract === false ? { claudeMemoryContract: false } : {}
4449
+ ...raw.claudeMemoryContract === false ? { claudeMemoryContract: false } : {},
4450
+ ...raw.runtime === true ? { runtime: true } : {}
2810
4451
  };
2811
4452
  }
2812
4453
  function failedFromThrown(command, error) {
@@ -2817,6 +4458,13 @@ function failedFromThrown(command, error) {
2817
4458
  "Reinstall paseo-room, then try again."
2818
4459
  )]);
2819
4460
  }
4461
+ if (error instanceof SkillAssetError) {
4462
+ return failed(command, [fail(
4463
+ `${command}.skill-asset`,
4464
+ error.message,
4465
+ "Reinstall paseo-room, then try again."
4466
+ )]);
4467
+ }
2820
4468
  if (error instanceof ManagedPathError) {
2821
4469
  return failed(command, [fail(`${command}.managed-path`, error.message, error.fix)]);
2822
4470
  }
@@ -2829,7 +4477,7 @@ function failedFromThrown(command, error) {
2829
4477
  }
2830
4478
  async function runCli(argv, output, context = {}) {
2831
4479
  const password = (context.options?.env ?? process.env).PASEO_PASSWORD;
2832
- const redact = (text) => password ? text.split(password).join("[redacted]") : text;
4480
+ const redact = (text3) => password ? text3.split(password).join("[redacted]") : text3;
2833
4481
  let json = false;
2834
4482
  const emit = (result) => {
2835
4483
  output.stdout(redact(json ? renderJson(result) : renderHuman(result)));
@@ -2848,8 +4496,8 @@ async function runCli(argv, output, context = {}) {
2848
4496
  return await runWizard2(
2849
4497
  context.prompts ?? terminalPrompts2,
2850
4498
  emit,
2851
- (text) => {
2852
- output.stdout(text);
4499
+ (text3) => {
4500
+ output.stdout(text3);
2853
4501
  },
2854
4502
  base,
2855
4503
  (action) => {
@@ -2860,10 +4508,10 @@ async function runCli(argv, output, context = {}) {
2860
4508
  return emit(failedFromThrown(wizardCommand, error));
2861
4509
  }
2862
4510
  }
2863
- const program = new Command().name("paseo-room").description("Configure Codex/Claude/Pi role homes in $HOME and register them with your local Paseo daemon.").version(package_default.version).usage("<setup|verify|remove> [options]\n auth login <codex|claude|pi> <supervisor|lead|peer> [options]").argument("<command>", "setup | verify | remove | auth").argument("[command-arguments...]", "auth login <agent> <role>").option("--agent <agent>", "codex, claude, or pi; repeat to combine (default: codex)", collectAgent).option("--apply", "actually make the changes (default: dry run)").option("--json", "machine-readable output").option("--no-claude-memory-contract", "omit the role contract from Claude role CLAUDE.md files, leaving the room plugin as the only Claude carrier").option("--room-home <path>", "where role homes are written (default: ~/.paseo-room)").option("--codex-home <path>", "source Codex config (default: ~/.codex)").option("--claude-home <path>", "source Claude Code config (default: ~/.claude)").option("--pi-home <path>", "source Pi config (default: ~/.pi/agent)").option("--codex-bin <path>", "Codex executable (default: found on PATH)").option("--claude-bin <path>", "Claude Code executable (default: found on PATH)").option("--pi-bin <path>", "Pi executable (default: found on PATH)").option("--paseo-bin <path>", "Paseo executable (default: found on PATH)").configureOutput({ writeOut: (text) => {
2864
- output.stdout(text);
2865
- }, writeErr: (text) => {
2866
- 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);
2867
4515
  } }).exitOverride();
2868
4516
  let command;
2869
4517
  let commandArguments;
@@ -2883,10 +4531,24 @@ async function runCli(argv, output, context = {}) {
2883
4531
  try {
2884
4532
  if (raw.claudeMemoryContract === false && (command === "verify" || command === "remove")) {
2885
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.
2886
4544
  `);
2887
4545
  return 2;
2888
4546
  }
2889
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
+ }
2890
4552
  if (raw.claudeMemoryContract === false) {
2891
4553
  output.stderr("paseo-room auth login only authenticates a role and does not use --no-claude-memory-contract.\n");
2892
4554
  return 2;
@@ -2921,21 +4583,28 @@ async function runCli(argv, output, context = {}) {
2921
4583
  if (command === "setup") return emit(await setup(options));
2922
4584
  if (command === "verify") return emit(await verify(options));
2923
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
+ }
2924
4593
  } catch (error) {
2925
4594
  return emit(failedFromThrown(command, error));
2926
4595
  }
2927
- 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.
2928
4597
  `);
2929
4598
  return 2;
2930
4599
  }
2931
4600
 
2932
4601
  // src/index.ts
2933
4602
  process.exitCode = await runCli(process.argv.slice(2), {
2934
- stdout: (text) => {
2935
- process.stdout.write(text);
4603
+ stdout: (text3) => {
4604
+ process.stdout.write(text3);
2936
4605
  },
2937
- stderr: (text) => {
2938
- process.stderr.write(text);
4606
+ stderr: (text3) => {
4607
+ process.stderr.write(text3);
2939
4608
  }
2940
4609
  });
2941
4610
  //# sourceMappingURL=index.js.map