paseo-room 0.6.0 → 0.7.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 (61) hide show
  1. package/README.md +118 -13
  2. package/dist/index.js +452 -60
  3. package/dist/index.js.map +1 -1
  4. package/dist/prompts/contract/lead.md +13 -4
  5. package/dist/prompts/contract/shared-authority.md +2 -2
  6. package/dist/prompts/contract/supervisor.md +17 -0
  7. package/dist/runtime-plugin/client/attention-settings.tsx +173 -0
  8. package/dist/runtime-plugin/client/data.ts +14 -2
  9. package/dist/runtime-plugin/client/forms.tsx +326 -0
  10. package/dist/runtime-plugin/client/host.ts +17 -0
  11. package/dist/runtime-plugin/client/kit.tsx +298 -0
  12. package/dist/runtime-plugin/client/model.ts +108 -0
  13. package/dist/runtime-plugin/client/pills.ts +58 -0
  14. package/dist/runtime-plugin/client/record.tsx +241 -0
  15. package/dist/runtime-plugin/client/role-pills.tsx +96 -0
  16. package/dist/runtime-plugin/client/room.tsx +291 -0
  17. package/dist/runtime-plugin/client/tone.ts +2 -0
  18. package/dist/runtime-plugin/client/views.tsx +161 -150
  19. package/dist/runtime-plugin/index.client.tsx +10 -2
  20. package/dist/runtime-plugin/index.server.ts +37 -3
  21. package/dist/runtime-plugin/server/attention/delivery.ts +193 -0
  22. package/dist/runtime-plugin/server/attention/engine.ts +465 -0
  23. package/dist/runtime-plugin/server/attention/key.ts +44 -0
  24. package/dist/runtime-plugin/server/attention/log.ts +54 -0
  25. package/dist/runtime-plugin/server/attention/mask.ts +44 -0
  26. package/dist/runtime-plugin/server/attention/observer.ts +313 -0
  27. package/dist/runtime-plugin/server/attention/portfolio.ts +77 -0
  28. package/dist/runtime-plugin/server/attention/questions.ts +80 -0
  29. package/dist/runtime-plugin/server/attention/seat-starter.ts +166 -0
  30. package/dist/runtime-plugin/server/attention/sensor.ts +207 -0
  31. package/dist/runtime-plugin/server/attention/signals.ts +230 -0
  32. package/dist/runtime-plugin/server/attention/triage.ts +68 -0
  33. package/dist/runtime-plugin/server/brief.ts +13 -1
  34. package/dist/runtime-plugin/server/context.ts +26 -2
  35. package/dist/runtime-plugin/server/contracts/actions.ts +18 -3
  36. package/dist/runtime-plugin/server/controller.ts +361 -58
  37. package/dist/runtime-plugin/server/correlations.ts +7 -3
  38. package/dist/runtime-plugin/server/domain/acceptance.ts +7 -2
  39. package/dist/runtime-plugin/server/domain/scope.ts +191 -0
  40. package/dist/runtime-plugin/server/domain/state.ts +281 -8
  41. package/dist/runtime-plugin/server/domain/views.ts +141 -7
  42. package/dist/runtime-plugin/server/events/schema.ts +33 -2
  43. package/dist/runtime-plugin/server/git.ts +93 -1
  44. package/dist/runtime-plugin/server/handlers/actions.ts +58 -12
  45. package/dist/runtime-plugin/server/handlers/peer.ts +22 -3
  46. package/dist/runtime-plugin/server/handlers/turns.ts +28 -21
  47. package/dist/runtime-plugin/server/host.ts +26 -0
  48. package/dist/runtime-plugin/server/lifecycle.ts +14 -3
  49. package/dist/runtime-plugin/server/notices.ts +26 -3
  50. package/dist/runtime-plugin/server/ownership.ts +1 -1
  51. package/dist/runtime-plugin/server/paseo-port.ts +299 -18
  52. package/dist/runtime-plugin/server/recovery.ts +124 -7
  53. package/dist/runtime-plugin/server/rpc.ts +198 -9
  54. package/dist/runtime-plugin/server/seats.ts +122 -0
  55. package/dist/runtime-plugin/server/tools.ts +9 -6
  56. package/dist/runtime-plugin/shared/attention.ts +70 -0
  57. package/dist/runtime-plugin/shared/identity.ts +8 -0
  58. package/dist/runtime-plugin/shared/limits.ts +6 -0
  59. package/dist/runtime-plugin/shared/policy.ts +2 -2
  60. package/dist/runtime-plugin/shared/rpc-contracts.ts +99 -1
  61. package/package.json +3 -2
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.6.0",
18
+ version: "0.7.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",
@@ -51,7 +51,8 @@ var init_package = __esm({
51
51
  "test:package": "vitest run --config vitest.package.config.ts",
52
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
- verify: "npm run typecheck && npm run lint && npm test && npm run build && npm run test:package"
54
+ verify: "npm run typecheck && npm run lint && npm test && npm run build && npm run test:package",
55
+ "attention:eval": "node --experimental-transform-types --no-warnings --import ./scripts/ts-resolve.mjs scripts/attention-eval.ts"
55
56
  },
56
57
  dependencies: {
57
58
  "@clack/prompts": "^1.8.0",
@@ -1546,9 +1547,9 @@ var init_codex = __esm({
1546
1547
  let catalogSource;
1547
1548
  let evidence2;
1548
1549
  try {
1549
- const parsed = JSON.parse(models.stdout);
1550
- evidence2 = catalogEvidence(parsed);
1551
- catalogSource = renderCatalog(parsed);
1550
+ const parsed2 = JSON.parse(models.stdout);
1551
+ evidence2 = catalogEvidence(parsed2);
1552
+ catalogSource = renderCatalog(parsed2);
1552
1553
  } catch {
1553
1554
  return { entries: [], checks: [...checks, fail(
1554
1555
  "codex.catalog",
@@ -1605,8 +1606,8 @@ function asObject2(value) {
1605
1606
  return value !== null && typeof value === "object" && !Array.isArray(value) ? { ...value } : void 0;
1606
1607
  }
1607
1608
  function renderPiSettings(source) {
1608
- const parsed = source === void 0 ? {} : JSON.parse(source);
1609
- const settings = asObject2(parsed);
1609
+ const parsed2 = source === void 0 ? {} : JSON.parse(source);
1610
+ const settings = asObject2(parsed2);
1610
1611
  if (!settings) throw new Error("Pi settings must be a JSON object.");
1611
1612
  delete settings.packages;
1612
1613
  delete settings.extensions;
@@ -1772,13 +1773,13 @@ async function probePiMcp(executable, adapterEntry, env, options = {}) {
1772
1773
  }
1773
1774
  const correlated = [];
1774
1775
  for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
1775
- let parsed;
1776
+ let parsed2;
1776
1777
  try {
1777
- parsed = JSON.parse(line);
1778
+ parsed2 = JSON.parse(line);
1778
1779
  } catch {
1779
1780
  return probeFailure("Pi capability probe returned malformed JSONL.");
1780
1781
  }
1781
- const event = asObject2(parsed);
1782
+ const event = asObject2(parsed2);
1782
1783
  if (!event) return probeFailure("Pi capability probe returned malformed JSONL.");
1783
1784
  if (event.type === "response" && event.id === PROBE_ID) correlated.push(event);
1784
1785
  }
@@ -2017,11 +2018,11 @@ function normalizeUrl(listen) {
2017
2018
  return `ws://${local}:${port}`;
2018
2019
  }
2019
2020
  function assessStatus(raw, minimumVersion = MINIMUM_VERSION, cliVersion) {
2020
- const parsed = statusSchema.safeParse(raw);
2021
- if (!parsed.success) {
2021
+ const parsed2 = statusSchema.safeParse(raw);
2022
+ if (!parsed2.success) {
2022
2023
  return { checks: [fail("paseo.status", "Paseo status output was not understood.", "Upgrade Paseo, then run: paseo daemon status --json")] };
2023
2024
  }
2024
- const status = parsed.data;
2025
+ const status = parsed2.data;
2025
2026
  if (status.localDaemon !== "running") {
2026
2027
  return { checks: [fail("paseo.daemon", `Paseo daemon is ${status.localDaemon}.`, "Start it with: paseo daemon start")] };
2027
2028
  }
@@ -2289,7 +2290,7 @@ var init_policy = __esm({
2289
2290
  "use strict";
2290
2291
  RUNTIME_ROLES = ["supervisor", "lead", "peer"];
2291
2292
  RUNTIME_AGENTS = ["codex", "claude", "pi"];
2292
- SUPERVISOR_OPERATIONS = ["room_status", "runtime_findings", "message_lead"];
2293
+ SUPERVISOR_OPERATIONS = ["room_status", "runtime_findings", "message_lead", "attention_feedback"];
2293
2294
  LEAD_OPERATIONS = [
2294
2295
  "assignment_create",
2295
2296
  "assignment_dispatch",
@@ -2300,7 +2301,9 @@ var init_policy = __esm({
2300
2301
  "assignment_abandon",
2301
2302
  "assignment_close",
2302
2303
  "assignment_status",
2303
- "gate_run"
2304
+ "gate_run",
2305
+ "workspace_close",
2306
+ "lease_reclaim"
2304
2307
  ];
2305
2308
  PEER_REPORTING_TOOLS = ["ask", "handoff"];
2306
2309
  RUNTIME_CAPABILITIES = [
@@ -2464,12 +2467,191 @@ var init_runtime = __esm({
2464
2467
  }
2465
2468
  });
2466
2469
 
2470
+ // src/runtime-plugin/shared/limits.ts
2471
+ import { z as z4 } from "zod";
2472
+ function utf8Bytes(value) {
2473
+ return encoder.encode(value).length;
2474
+ }
2475
+ function boundedString(max = MAX_STRING_BYTES) {
2476
+ return z4.string().refine((value) => {
2477
+ const bytes = utf8Bytes(value);
2478
+ return bytes >= 1 && bytes <= max;
2479
+ }, { message: `must be 1 byte to ${String(max)} bytes of UTF-8` });
2480
+ }
2481
+ function boundedArray(item) {
2482
+ return z4.array(item).max(MAX_ARRAY_ITEMS);
2483
+ }
2484
+ var MAX_AGGREGATE_BYTES, MAX_ARRAY_ITEMS, MAX_STRING_BYTES, MAX_COMMAND_BYTES, MAX_GATE_TIMEOUT_SECONDS, encoder, MAX_WORKTREE_LEASES;
2485
+ var init_limits = __esm({
2486
+ "src/runtime-plugin/shared/limits.ts"() {
2487
+ "use strict";
2488
+ MAX_AGGREGATE_BYTES = 64 * 1024;
2489
+ MAX_ARRAY_ITEMS = 64;
2490
+ MAX_STRING_BYTES = 8 * 1024;
2491
+ MAX_COMMAND_BYTES = 16 * 1024;
2492
+ MAX_GATE_TIMEOUT_SECONDS = 3600;
2493
+ encoder = new TextEncoder();
2494
+ MAX_WORKTREE_LEASES = 3;
2495
+ }
2496
+ });
2497
+
2498
+ // src/runtime-plugin/server/domain/scope.ts
2499
+ function normalizeScope(raw) {
2500
+ return raw.normalize("NFC").replace(/\/{2,}/g, "/").replace(/\/+$/, "");
2501
+ }
2502
+ function refusal(raw, text3) {
2503
+ if (utf8Bytes(raw) > MAX_STRING_BYTES) return `longer than ${String(MAX_STRING_BYTES)} bytes`;
2504
+ if (text3 === "") return "empty";
2505
+ if (text3.includes("\\")) return "contains a backslash";
2506
+ if (text3.startsWith("/")) return "is absolute";
2507
+ if (text3.startsWith("!")) return "is a negation";
2508
+ if (/[{}]/.test(text3)) return "uses brace expansion";
2509
+ if (/[[\]]/.test(text3)) return "uses a character class";
2510
+ if (text3.split("/").some((segment) => segment === "." || segment === "..")) return "has a . or .. segment";
2511
+ return void 0;
2512
+ }
2513
+ function parseScope(raw) {
2514
+ const text3 = normalizeScope(raw);
2515
+ const reason3 = refusal(raw, text3);
2516
+ if (reason3 !== void 0) return { ok: false, item: raw, reason: reason3 };
2517
+ const segments = text3.split("/").map((segment) => /^\*+$/.test(segment) && segment.length > 1 ? GLOBSTAR : segment.replace(/\*{2,}/g, "*"));
2518
+ if (segments.at(-1) !== GLOBSTAR) segments.push(GLOBSTAR);
2519
+ return { ok: true, entry: { text: text3, segments } };
2520
+ }
2521
+ function parseScopes(raws) {
2522
+ if (raws.length === 0) return { ok: true, entries: [WHOLE_REPOSITORY] };
2523
+ const entries = [];
2524
+ for (const raw of raws) {
2525
+ const parsed2 = parseScope(raw);
2526
+ if (!parsed2.ok) return parsed2;
2527
+ entries.push(parsed2.entry);
2528
+ }
2529
+ return { ok: true, entries };
2530
+ }
2531
+ function fold(value) {
2532
+ return codePoints(value).map((point) => {
2533
+ const lower = point.toLowerCase();
2534
+ return codePoints(lower).length === 1 ? lower : point;
2535
+ });
2536
+ }
2537
+ function segmentMatches(pattern, literal) {
2538
+ let row = new Array(literal.length + 1).fill(false);
2539
+ row[literal.length] = true;
2540
+ for (let i = pattern.length - 1; i >= 0; i--) {
2541
+ const next = new Array(literal.length + 1).fill(false);
2542
+ for (let j = literal.length; j >= 0; j--) {
2543
+ const token = pattern[i];
2544
+ if (token === "*") next[j] = (row[j] ?? false) || j < literal.length && (next[j + 1] ?? false);
2545
+ else next[j] = j < literal.length && (token === "?" || token === literal[j]) && (row[j + 1] ?? false);
2546
+ }
2547
+ row = next;
2548
+ }
2549
+ return row[0] ?? false;
2550
+ }
2551
+ function fixedEnds(pattern) {
2552
+ const wildcards = pattern.flatMap((point, index) => point === "*" || point === "?" ? [index] : []);
2553
+ return { prefix: pattern.slice(0, wildcards[0]), suffix: pattern.slice((wildcards.at(-1) ?? pattern.length) + 1) };
2554
+ }
2555
+ function segmentsIntersect(a, b) {
2556
+ const left = fold(a);
2557
+ const right = fold(b);
2558
+ const leftPattern = isPattern(a);
2559
+ const rightPattern = isPattern(b);
2560
+ if (!leftPattern && !rightPattern) return left.join("") === right.join("");
2561
+ if (!rightPattern) return segmentMatches(left, right);
2562
+ if (!leftPattern) return segmentMatches(right, left);
2563
+ const l = fixedEnds(left);
2564
+ const r = fixedEnds(right);
2565
+ const suffixesAgree = agree([...l.suffix].reverse(), [...r.suffix].reverse());
2566
+ return agree(l.prefix, r.prefix) && suffixesAgree;
2567
+ }
2568
+ function sequencesIntersect(a, b) {
2569
+ const width = b.length + 1;
2570
+ const table2 = new Uint8Array((a.length + 1) * width);
2571
+ const at = (i, j) => table2[i * width + j] === 1;
2572
+ for (let i = a.length; i >= 0; i--) {
2573
+ for (let j = b.length; j >= 0; j--) {
2574
+ let value;
2575
+ if (i === a.length && j === b.length) value = true;
2576
+ else if (i === a.length) value = b[j] === GLOBSTAR && at(i, j + 1);
2577
+ else if (j === b.length) value = a[i] === GLOBSTAR && at(i + 1, j);
2578
+ else if (a[i] === GLOBSTAR || b[j] === GLOBSTAR) value = at(i + 1, j) || at(i, j + 1);
2579
+ else value = segmentsIntersect(a[i] ?? "", b[j] ?? "") && at(i + 1, j + 1);
2580
+ table2[i * width + j] = value ? 1 : 0;
2581
+ }
2582
+ }
2583
+ return at(0, 0);
2584
+ }
2585
+ function overlaps(a, b) {
2586
+ return sequencesIntersect(a.segments, b.segments);
2587
+ }
2588
+ function firstOverlap(as, bs) {
2589
+ for (const a of as) for (const b of bs) if (overlaps(a, b)) return [a, b];
2590
+ return void 0;
2591
+ }
2592
+ function reaches(scope, serialPath) {
2593
+ return overlaps(scope, serialPath);
2594
+ }
2595
+ var GLOBSTAR, WHOLE_REPOSITORY, codePoints, isPattern, agree;
2596
+ var init_scope = __esm({
2597
+ "src/runtime-plugin/server/domain/scope.ts"() {
2598
+ "use strict";
2599
+ init_limits();
2600
+ GLOBSTAR = "**";
2601
+ WHOLE_REPOSITORY = { text: GLOBSTAR, segments: [GLOBSTAR] };
2602
+ codePoints = (value) => Array.from(value);
2603
+ isPattern = (segment) => segment.includes("*") || segment.includes("?");
2604
+ agree = (a, b) => a.every((point, index) => index >= b.length || point === b[index]);
2605
+ }
2606
+ });
2607
+
2467
2608
  // src/runtime-plugin/server/domain/state.ts
2468
2609
  function emptyProjectState(projectId) {
2469
- return { projectId, assignments: /* @__PURE__ */ new Map(), ownership: /* @__PURE__ */ new Map(), notices: /* @__PURE__ */ new Map(), lastSequence: 0 };
2610
+ return { projectId, assignments: /* @__PURE__ */ new Map(), ownership: /* @__PURE__ */ new Map(), workspaces: /* @__PURE__ */ new Map(), notices: /* @__PURE__ */ new Map(), lastSequence: 0 };
2611
+ }
2612
+ function activeWriter(state, except) {
2613
+ for (const owner of state.ownership.values()) if (owner.state !== "released" && owner.assignmentId !== except) return owner;
2614
+ return void 0;
2615
+ }
2616
+ function leadWorkspaceWriter(state, except) {
2617
+ for (const owner of state.ownership.values()) {
2618
+ if (owner.state !== "released" && owner.lease === void 0 && owner.assignmentId !== except) return owner;
2619
+ }
2620
+ return void 0;
2470
2621
  }
2471
- function activeWriter(state) {
2472
- for (const owner of state.ownership.values()) if (owner.state !== "released") return owner;
2622
+ function activeLeases(state, except) {
2623
+ return [...state.ownership.values()].filter((owner) => owner.lease !== void 0 && owner.state !== "released" && owner.assignmentId !== except);
2624
+ }
2625
+ function parsed(raws, empty) {
2626
+ if (raws.length === 0 && empty === "none") return [];
2627
+ const result = parseScopes(raws);
2628
+ return result.ok ? result.entries : `${result.item} ${result.reason}`;
2629
+ }
2630
+ function leaseCollision(state, assignmentId, request) {
2631
+ const exclusive = leadWorkspaceWriter(state, assignmentId);
2632
+ if (exclusive !== void 0) return { code: "writer_exclusive", message: `Assignment ${exclusive.assignmentId} writes in Lead's workspace (${exclusive.state}); it excludes every other writer.` };
2633
+ const leases = activeLeases(state, assignmentId);
2634
+ const uncertain = leases.find((lease) => lease.state === "uncertain");
2635
+ if (uncertain !== void 0) return { code: "writer_uncertain", message: `The writer of ${uncertain.assignmentId} is uncertain; no new writer starts until it is proven stopped.` };
2636
+ if (leases.length >= MAX_WORKTREE_LEASES) return { code: "lease_cap", message: `${String(MAX_WORKTREE_LEASES)} worktree leases are already held in this project.` };
2637
+ const mine = parsed(request.scopes, "whole");
2638
+ if (typeof mine === "string") return { code: "scope_not_canonical", message: `writeScope item ${mine}.` };
2639
+ const serial = parsed(request.serialOnly, "none");
2640
+ if (typeof serial === "string") return { code: "scope_not_canonical", message: `serialOnly item ${serial}.` };
2641
+ const others = leases.map((lease) => ({ lease, scopes: parsed(lease.lease.scopes, "whole"), serial: parsed(lease.lease.serialOnly, "none") }));
2642
+ for (const other of others) {
2643
+ if (typeof other.scopes === "string" || typeof other.serial === "string") {
2644
+ return { code: "scope_overlap", message: `The scopes of ${other.lease.assignmentId} cannot be compared.` };
2645
+ }
2646
+ const overlap = firstOverlap(mine, other.scopes);
2647
+ if (overlap !== void 0) return { code: "scope_overlap", message: `${overlap[0].text} overlaps ${overlap[1].text} held by ${other.lease.assignmentId}.` };
2648
+ }
2649
+ const union = [...serial, ...others.flatMap((other) => typeof other.serial === "string" ? [] : other.serial)];
2650
+ for (const path of union) {
2651
+ if (!mine.some((scope) => reaches(scope, path))) continue;
2652
+ const holder = others.find((other) => typeof other.scopes !== "string" && other.scopes.some((scope) => reaches(scope, path)));
2653
+ if (holder !== void 0) return { code: "serial_path", message: `${path.text} is serial-only and ${holder.lease.assignmentId} already writes under it.` };
2654
+ }
2473
2655
  return void 0;
2474
2656
  }
2475
2657
  function need(condition, message) {
@@ -2511,11 +2693,13 @@ function checkEvent(state, event) {
2511
2693
  const writable = assignment2.input.mode === "writable";
2512
2694
  switch (event.type) {
2513
2695
  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.");
2696
+ return inState(assignment2, ["draft"], "dispatch") ?? need(state.ownershipConflict === void 0, "Dispatch is paused by a Lead ownership conflict.") ?? need(!writable || leadWorkspaceWriter(state) === void 0, "Another writer still owns this project.");
2515
2697
  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.");
2698
+ return need(writable, "A read-only assignment reserves no writer ownership.") ?? inState(assignment2, ["dispatching"], "reserve ownership") ?? need(leadWorkspaceWriter(state) === void 0, "Another writer still owns this project.");
2699
+ case "agent.create-requested": {
2700
+ const lease = owner?.lease;
2701
+ 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.") ?? need(!writable || lease !== void 0 || activeWriter(state, assignment2.id) === void 0, "Another writer still owns this project.") ?? need(lease === void 0 || state.workspaces.get(assignment2.id)?.create === "succeeded" && event.data.workspaceId === lease.workspaceId, "A leased Peer is created only in its proven worktree.");
2702
+ }
2519
2703
  case "agent.create-succeeded":
2520
2704
  case "agent.create-failed":
2521
2705
  case "agent.create-uncertain":
@@ -2580,6 +2764,38 @@ function checkEvent(state, event) {
2580
2764
  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
2765
  case "ownership.uncertain":
2582
2766
  return need(owner !== void 0 && owner.state !== "released", "No unreleased ownership to mark uncertain.");
2767
+ case "lease.reserved": {
2768
+ const collision = leaseCollision(state, assignment2.id, event.data);
2769
+ return need(writable, "A read-only assignment holds no lease.") ?? inState(assignment2, ["dispatching"], "reserve a lease") ?? need(owner?.state === "reserved" && owner.lease === void 0 && owner.agentId === void 0, "A lease extends a fresh reservation.") ?? need(owner?.workspaceId === event.data.workspaceId && owner.baseCommit === event.data.baseCommit, "The lease names a different workspace or base than the reservation.") ?? (collision === void 0 ? void 0 : `${collision.code}: ${collision.message}`);
2770
+ }
2771
+ case "workspace.create-requested":
2772
+ return inState(assignment2, ["dispatching"], "request a worktree") ?? need(owner?.state === "reserved" && owner.lease?.workspaceId === event.data.workspaceId && owner.lease.baseCommit === event.data.baseCommit, "A worktree is requested only for its reserved lease.") ?? need(!state.workspaces.has(assignment2.id), "This assignment already requested its worktree.");
2773
+ case "workspace.create-succeeded":
2774
+ case "workspace.create-failed":
2775
+ case "workspace.create-uncertain":
2776
+ case "workspace.create-refused": {
2777
+ const record = state.workspaces.get(assignment2.id);
2778
+ const from = event.type === "workspace.create-uncertain" ? ["requested"] : ["requested", "uncertain"];
2779
+ return need(record !== void 0 && record.createIntentId === event.data.intentId && from.includes(record.create), "No unresolved worktree create matches.") ?? need(!("workspaceId" in event.data) || event.data.workspaceId === record?.workspaceId, "The result names a different workspace.");
2780
+ }
2781
+ case "lease.reclaimed": {
2782
+ const check = reclaimCheck(state, assignment2.id);
2783
+ if (!check.ok) return check.message;
2784
+ return need(event.data.fromEpoch === check.value.lease.epoch && event.data.toEpoch === event.data.fromEpoch + 1, "A reclaim moves the lease to exactly the next epoch.") ?? need(event.data.priorAgentId === check.value.prior, "The reclaim names a different writer than the lease holder.");
2785
+ }
2786
+ case "scope.exceeded":
2787
+ return need(owner?.lease !== void 0, "Only a leased candidate has scopes to exceed.") ?? need(assignment2.candidate?.commit === event.data.candidateCommit, "The evidence names a different candidate.");
2788
+ case "workspace.close-requested": {
2789
+ const record = state.workspaces.get(assignment2.id);
2790
+ return need(record?.workspaceId === event.data.workspaceId && (record.create === "succeeded" || record.create === "refused"), "Only a created worktree is closed.") ?? need(record?.close === "open" || record?.close === "failed", "The worktree is already closing or closed.") ?? need(owner?.state === "released", "A worktree closes only after its writer is proven released.") ?? need(!event.data.discardUncommitted || event.data.reason !== void 0, "Discarding uncommitted work needs a reason.");
2791
+ }
2792
+ case "workspace.close-succeeded":
2793
+ case "workspace.close-failed":
2794
+ case "workspace.close-uncertain": {
2795
+ const record = state.workspaces.get(assignment2.id);
2796
+ const from = event.type === "workspace.close-uncertain" ? ["requested"] : ["requested", "uncertain"];
2797
+ return need(record !== void 0 && record.closeIntentId === event.data.intentId && from.includes(record.close) && record.workspaceId === event.data.workspaceId, "No unresolved worktree close matches.");
2798
+ }
2583
2799
  case "gate.requested":
2584
2800
  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
2801
  case "gate.finished":
@@ -2612,6 +2828,21 @@ function setOwner(state, id2, change) {
2612
2828
  const current2 = state.ownership.get(id2);
2613
2829
  if (current2 !== void 0) state.ownership.set(id2, { ...current2, ...change });
2614
2830
  }
2831
+ function setLease(state, id2, change) {
2832
+ const current2 = state.ownership.get(id2);
2833
+ if (current2?.lease !== void 0) state.ownership.set(id2, { ...current2, lease: { ...current2.lease, ...change } });
2834
+ }
2835
+ function setWorkspace(state, id2, change, event) {
2836
+ const current2 = state.workspaces.get(id2);
2837
+ if (current2 !== void 0) state.workspaces.set(id2, { ...current2, ...change, eventIds: [...current2.eventIds, event.id] });
2838
+ }
2839
+ function reclaimed(view, event) {
2840
+ const copy = { ...withoutPermission(view) };
2841
+ delete copy.peerAgentId;
2842
+ delete copy.observedProviderId;
2843
+ delete copy.observedModel;
2844
+ return { ...copy, state: "dispatching", reportingState: "consumed", eventIds: [...view.eventIds, event.id] };
2845
+ }
2615
2846
  function applyReport(state, view, event) {
2616
2847
  const report = {
2617
2848
  generation: event.data.generation,
@@ -2629,6 +2860,12 @@ function applyReport(state, view, event) {
2629
2860
  ...event.data.candidate === void 0 ? {} : { candidate: event.data.candidate },
2630
2861
  ...event.data.inspectedCommit === void 0 ? {} : { inspectedCommit: event.data.inspectedCommit }
2631
2862
  }, event);
2863
+ const next = state.assignments.get(view.id);
2864
+ if (next?.scopeExceeded !== void 0 && event.data.candidate !== void 0 && event.data.candidate.commit !== next.scopeExceeded.candidateCommit) {
2865
+ const copy = { ...next };
2866
+ delete copy.scopeExceeded;
2867
+ state.assignments.set(view.id, copy);
2868
+ }
2632
2869
  }
2633
2870
  function applyEvent(state, event) {
2634
2871
  state.lastSequence = Math.max(state.lastSequence, event.sequence);
@@ -2792,6 +3029,80 @@ function applyEvent(state, event) {
2792
3029
  setOwner(state, id2, { state: "uncertain" });
2793
3030
  update(state, id2, {}, event);
2794
3031
  return;
3032
+ case "lease.reserved":
3033
+ setOwner(state, id2, {
3034
+ lease: {
3035
+ workspaceId: event.data.workspaceId,
3036
+ branch: event.data.branch,
3037
+ baseCommit: event.data.baseCommit,
3038
+ scopes: event.data.scopes,
3039
+ serialOnly: event.data.serialOnly,
3040
+ epoch: event.data.epoch,
3041
+ priorAgentIds: []
3042
+ }
3043
+ });
3044
+ update(state, id2, {}, event);
3045
+ return;
3046
+ case "workspace.create-requested":
3047
+ state.workspaces.set(id2, {
3048
+ assignmentId: id2,
3049
+ workspaceId: event.data.workspaceId,
3050
+ idempotencyKey: event.data.idempotencyKey,
3051
+ baseCommit: event.data.baseCommit,
3052
+ branchName: event.data.branchName,
3053
+ worktreeSlug: event.data.worktreeSlug,
3054
+ create: "requested",
3055
+ createIntentId: event.data.intentId,
3056
+ close: "open",
3057
+ eventIds: [event.id]
3058
+ });
3059
+ update(state, id2, { openIntents: withIntent(view, event.data.intentId, event.type) }, event);
3060
+ return;
3061
+ case "workspace.create-succeeded":
3062
+ setWorkspace(state, id2, { create: "succeeded", worktreePath: event.data.worktreePath, branch: event.data.branch, headCommit: event.data.headCommit }, event);
3063
+ setLease(state, id2, { worktreePath: event.data.worktreePath, branch: event.data.branch });
3064
+ update(state, id2, { openIntents: withIntent(view, event.data.intentId, void 0), ...view.state === "uncertain" ? { state: "dispatching" } : {} }, event);
3065
+ return;
3066
+ case "workspace.create-failed":
3067
+ case "workspace.create-refused":
3068
+ setWorkspace(state, id2, { create: event.type === "workspace.create-failed" ? "failed" : "refused" }, event);
3069
+ setOwner(state, id2, { state: "released" });
3070
+ update(state, id2, { state: "blocked", openIntents: withIntent(view, event.data.intentId, void 0) }, event);
3071
+ return;
3072
+ case "workspace.create-uncertain":
3073
+ setWorkspace(state, id2, { create: "uncertain" }, event);
3074
+ setOwner(state, id2, { state: "uncertain" });
3075
+ update(state, id2, { state: "uncertain" }, event);
3076
+ return;
3077
+ case "lease.reclaimed": {
3078
+ const owner = state.ownership.get(id2);
3079
+ const lease = owner?.lease;
3080
+ if (owner === void 0 || lease === void 0) return;
3081
+ const next = { ...owner, state: "reserved", lease: { ...lease, epoch: event.data.toEpoch, priorAgentIds: [...lease.priorAgentIds, event.data.priorAgentId] } };
3082
+ delete next.agentId;
3083
+ state.ownership.set(id2, next);
3084
+ state.assignments.set(id2, reclaimed(view, event));
3085
+ return;
3086
+ }
3087
+ case "scope.exceeded":
3088
+ update(state, id2, { scopeExceeded: { candidateCommit: event.data.candidateCommit, paths: event.data.paths, eventId: event.id } }, event);
3089
+ return;
3090
+ case "workspace.close-requested":
3091
+ setWorkspace(state, id2, { close: "requested", closeIntentId: event.data.intentId, discardUncommitted: event.data.discardUncommitted }, event);
3092
+ update(state, id2, { openIntents: withIntent(view, event.data.intentId, event.type) }, event);
3093
+ return;
3094
+ case "workspace.close-succeeded":
3095
+ setWorkspace(state, id2, { close: "succeeded", archivedAt: event.data.archivedAt, directoryRemoved: event.data.directoryRemoved }, event);
3096
+ update(state, id2, { openIntents: withIntent(view, event.data.intentId, void 0) }, event);
3097
+ return;
3098
+ case "workspace.close-failed":
3099
+ setWorkspace(state, id2, { close: "failed" }, event);
3100
+ update(state, id2, { openIntents: withIntent(view, event.data.intentId, void 0) }, event);
3101
+ return;
3102
+ case "workspace.close-uncertain":
3103
+ setWorkspace(state, id2, { close: "uncertain" }, event);
3104
+ update(state, id2, {}, event);
3105
+ return;
2795
3106
  case "gate.requested":
2796
3107
  update(state, id2, { gates: [...view.gates, { gateRunId: event.data.gateRunId, candidate: event.data.candidate, command: event.data.command, status: "running" }] }, event);
2797
3108
  return;
@@ -2814,10 +3125,29 @@ function project(projectId, events) {
2814
3125
  }
2815
3126
  return { state, violations: [] };
2816
3127
  }
3128
+ function reclaimCheck(state, assignmentId) {
3129
+ const refused = (code2, message) => ({ ok: false, code: code2, message });
3130
+ const view = state.assignments.get(assignmentId);
3131
+ const owner = state.ownership.get(assignmentId);
3132
+ const lease = owner?.lease;
3133
+ const record = state.workspaces.get(assignmentId);
3134
+ if (view === void 0 || owner === void 0 || lease === void 0 || record === void 0) return refused("lease_missing", `Assignment ${assignmentId} holds no worktree lease.`);
3135
+ if (owner.state === "released" || owner.state === "reserved") return refused("lease_state", "Only a held or uncertain lease is reclaimed.");
3136
+ const prior = owner.agentId;
3137
+ if (prior === void 0 || view.peerAgentId !== prior) return refused("lease_state", "The reclaim names a different writer than the lease holder.");
3138
+ if (TERMINAL_STATES.includes(view.state) || view.state === "draft" || view.state === "dispatching") return refused("assignment_state", `Cannot reclaim while the assignment is ${view.state}.`);
3139
+ if (view.closure !== "open") return refused("assignment_state", "A closing assignment is not reclaimed.");
3140
+ if (Object.keys(view.openIntents).length > 0) return refused("effect_unresolved", "Settle every unresolved effect before a reclaim.");
3141
+ if (record.create !== "succeeded" || record.close !== "open" || record.worktreePath === void 0) return refused("worktree_unavailable", "The lease's worktree is not open.");
3142
+ if (view.peerProviderId === void 0) return refused("lease_state", "The assignment names no Peer provider.");
3143
+ return { ok: true, value: { view, lease, worktreePath: record.worktreePath, prior, provider: view.peerProviderId } };
3144
+ }
2817
3145
  var TERMINAL_STATES;
2818
3146
  var init_state = __esm({
2819
3147
  "src/runtime-plugin/server/domain/state.ts"() {
2820
3148
  "use strict";
3149
+ init_limits();
3150
+ init_scope();
2821
3151
  TERMINAL_STATES = ["accepted", "rejected", "abandoned"];
2822
3152
  }
2823
3153
  });
@@ -2831,6 +3161,17 @@ var init_receipts = __esm({
2831
3161
  });
2832
3162
 
2833
3163
  // src/runtime-plugin/server/domain/views.ts
3164
+ function worktreeDisposition(state, record, present = () => true) {
3165
+ if (record.create === "requested" || record.create === "uncertain" || record.close === "requested" || record.close === "uncertain") return "unresolved";
3166
+ if (record.create === "failed") return "gone";
3167
+ if (record.close === "succeeded") {
3168
+ return record.directoryRemoved === false && (record.worktreePath === void 0 || present(record.worktreePath)) ? "leftover" : "gone";
3169
+ }
3170
+ return state.ownership.get(record.assignmentId)?.state === "released" ? "retained" : "active";
3171
+ }
3172
+ function dispositions(state, present) {
3173
+ return new Map([...state.workspaces.values()].map((record) => [record, worktreeDisposition(state, record, present)]));
3174
+ }
2834
3175
  function quiescence(state) {
2835
3176
  const blockers = [];
2836
3177
  for (const view of state.assignments.values()) {
@@ -2844,13 +3185,23 @@ function quiescence(state) {
2844
3185
  for (const [intent, type] of Object.entries(view.openIntents)) blockers.push({ kind: "intent", id: intent, detail: `${type} has no result` });
2845
3186
  }
2846
3187
  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}` });
3188
+ if (owner.state === "released") continue;
3189
+ blockers.push(owner.lease === void 0 ? { kind: "ownership", id: owner.assignmentId, detail: `writer ownership is ${owner.state}` } : { kind: "lease", id: owner.assignmentId, detail: `worktree lease (epoch ${String(owner.lease.epoch)}) is ${owner.state}` });
3190
+ }
3191
+ for (const record of state.workspaces.values()) {
3192
+ if (worktreeDisposition(state, record) === "unresolved") {
3193
+ blockers.push({ kind: "worktree", id: record.workspaceId, detail: `worktree of ${record.assignmentId} is unresolved (create ${record.create}, close ${record.close})` });
3194
+ }
2848
3195
  }
2849
3196
  for (const notice of state.notices.values()) {
2850
3197
  if (notice.state === "pending" || notice.state === "uncertain") blockers.push({ kind: "delivery", id: notice.noticeId, detail: `notice is ${notice.state}` });
2851
3198
  }
2852
3199
  return { quiescent: blockers.length === 0, blockers };
2853
3200
  }
3201
+ function worktreesOnDisk(state, present) {
3202
+ const values = [...dispositions(state, present).values()];
3203
+ return { retained: values.filter((value) => value === "retained").length, leftover: values.filter((value) => value === "leftover").length };
3204
+ }
2854
3205
  var init_views = __esm({
2855
3206
  "src/runtime-plugin/server/domain/views.ts"() {
2856
3207
  "use strict";
@@ -2859,33 +3210,6 @@ var init_views = __esm({
2859
3210
  }
2860
3211
  });
2861
3212
 
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
3213
  // src/runtime-plugin/server/contracts/assignment.ts
2890
3214
  import { z as z5 } from "zod";
2891
3215
  var ASSIGNMENT_MODES, ASSIGNMENT_KINDS, commitSchema, gateSpecSchema, items, assignmentCreateSchema, candidateRefSchema;
@@ -2942,12 +3266,23 @@ var init_actions = __esm({
2942
3266
  SUPERVISOR_ACTION_SCHEMAS = {
2943
3267
  room_status: z6.strictObject({}),
2944
3268
  runtime_findings: z6.strictObject({}),
2945
- message_lead: z6.strictObject({ message: boundedString() })
3269
+ // `project` names a project in the caller's portfolio: a runtime project id, the project key or
3270
+ // the repository's name. Omitted, it is the caller's own working project, or its only project.
3271
+ message_lead: z6.strictObject({ message: boundedString(), project: z6.string().min(1).max(4096).optional() }),
3272
+ attention_feedback: z6.strictObject({ id: z6.string().regex(/^att_[A-Za-z0-9_-]{8,32}$/), verdict: z6.enum(["useful", "noise", "unknown"]) })
2946
3273
  };
2947
3274
  LEAD_ACTION_SCHEMAS = {
2948
3275
  assignment_create: assignmentCreateSchema,
2949
3276
  // 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) }),
3277
+ // `worktree` asks the runtime for an isolated worktree; the default is Phase 1's shared
3278
+ // workspace. `serialOnly` names paths that admit one writer at a time, quoted from the
3279
+ // repository's own protocol (docs/design/runtime-coordination-phase2.md P2-D1, §5.3).
3280
+ assignment_dispatch: z6.strictObject({
3281
+ ...assignment,
3282
+ peerProvider: z6.string().min(1).max(128),
3283
+ isolation: z6.enum(["lead-workspace", "worktree"]).optional(),
3284
+ serialOnly: boundedArray(boundedString()).optional()
3285
+ }),
2951
3286
  assignment_answer: z6.strictObject({ ...assignment, answer: boundedString() }),
2952
3287
  assignment_rework: z6.strictObject({ ...assignment, instructions: boundedString() }),
2953
3288
  assignment_accept: z6.strictObject({
@@ -2961,7 +3296,10 @@ var init_actions = __esm({
2961
3296
  assignment_abandon: z6.strictObject({ ...assignment, reason }),
2962
3297
  assignment_close: z6.strictObject(assignment),
2963
3298
  assignment_status: z6.strictObject({ assignmentId: assignmentIdSchema.optional() }),
2964
- gate_run: z6.strictObject(assignment)
3299
+ gate_run: z6.strictObject(assignment),
3300
+ // Discarding a retained worktree's uncommitted work is Lead's explicit decision, with a reason.
3301
+ workspace_close: z6.strictObject({ ...assignment, discardUncommitted: z6.literal(true).optional(), reason: reason.optional() }).refine((input) => input.discardUncommitted !== true || input.reason !== void 0, { message: "discardUncommitted needs a reason", path: ["reason"] }),
3302
+ lease_reclaim: z6.strictObject({ ...assignment, reason })
2965
3303
  };
2966
3304
  }
2967
3305
  });
@@ -3061,10 +3399,10 @@ function readEvent(value) {
3061
3399
  if (typeof payloadVersion !== "number" || !SUPPORTED_PAYLOAD_VERSIONS[type].includes(payloadVersion)) {
3062
3400
  return { ok: false, reason: "unsupported-version", detail: `Unsupported payload version ${String(payloadVersion)} for ${type}.` };
3063
3401
  }
3064
- const parsed = readerSchema.safeParse(value);
3065
- return parsed.success ? { ok: true, event: parsed.data } : { ok: false, reason: "invalid", detail: z8.prettifyError(parsed.error) };
3402
+ const parsed2 = readerSchema.safeParse(value);
3403
+ return parsed2.success ? { ok: true, event: parsed2.data } : { ok: false, reason: "invalid", detail: z8.prettifyError(parsed2.error) };
3066
3404
  }
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;
3405
+ var EVENT_SCHEMA, id, text2, reason2, generation2, digest2, timestamp, workspaceIdSchema, branch, epoch, scopes, gateResultSchema, NOTICE_DISPOSITIONS, NOTICE_CLASSES, EVENT_PAYLOADS, EVENT_TYPES, SUPPORTED_PAYLOAD_VERSIONS, actorSchema, envelope, firstWriter, restWriters, firstReader, restReaders, writerSchema, readerSchema;
3068
3406
  var init_schema = __esm({
3069
3407
  "src/runtime-plugin/server/events/schema.ts"() {
3070
3408
  "use strict";
@@ -3080,6 +3418,10 @@ var init_schema = __esm({
3080
3418
  generation2 = z8.number().int().min(1);
3081
3419
  digest2 = z8.string().regex(/^sha256:[0-9a-f]{64}$/);
3082
3420
  timestamp = z8.iso.datetime({ offset: true });
3421
+ workspaceIdSchema = z8.string().regex(/^wks_[a-f0-9]{16}$/);
3422
+ branch = z8.string().min(1).max(256);
3423
+ epoch = z8.number().int().min(1);
3424
+ scopes = z8.array(boundedString()).max(64);
3083
3425
  gateResultSchema = z8.strictObject({
3084
3426
  id,
3085
3427
  assignmentId: assignmentIdSchema,
@@ -3127,7 +3469,17 @@ var init_schema = __esm({
3127
3469
  "ownership.released": { agentId: id, archivedAt: timestamp },
3128
3470
  "ownership.uncertain": { reason: reason2 },
3129
3471
  // External effects: agent creation, turn delivery and archive (§6).
3130
- "agent.create-requested": { intentId: id, peerProviderId: id, workspaceId: id, parentAgentId: id, label: id },
3472
+ // `agentId` and `idempotencyKey` are chosen before the call, so recovery reissues the identical
3473
+ // request (Phase 2 delta P2-D3). Optional and additive: a Phase 1 reader ignores them.
3474
+ "agent.create-requested": {
3475
+ intentId: id,
3476
+ peerProviderId: id,
3477
+ workspaceId: id,
3478
+ parentAgentId: id,
3479
+ label: id,
3480
+ agentId: z8.uuid().optional(),
3481
+ idempotencyKey: z8.string().min(1).max(512).optional()
3482
+ },
3131
3483
  "agent.create-succeeded": { intentId: id, agentId: id },
3132
3484
  "agent.create-failed": { intentId: id, reason: reason2 },
3133
3485
  "agent.create-uncertain": { intentId: id, reason: reason2 },
@@ -3178,6 +3530,30 @@ var init_schema = __esm({
3178
3530
  },
3179
3531
  "gate.finished": { result: gateResultSchema },
3180
3532
  "gate.uncertain": { gateRunId: id, reason: reason2 },
3533
+ // Phase 2 worktree concurrency (docs/design/runtime-coordination-phase2.md §4). A writer lease
3534
+ // extends the assignment's ownership with its own worktree, scopes and epoch.
3535
+ "lease.reserved": { workspaceId: workspaceIdSchema, branch, baseCommit: commitSchema, scopes, serialOnly: scopes, epoch: z8.literal(1) },
3536
+ "workspace.create-requested": {
3537
+ intentId: id,
3538
+ workspaceId: workspaceIdSchema,
3539
+ idempotencyKey: z8.string().min(1).max(512),
3540
+ baseCommit: commitSchema,
3541
+ branchName: branch,
3542
+ worktreeSlug: z8.string().min(1).max(128)
3543
+ },
3544
+ // Written only after the Git proof (P2-D4); `branch` is the name Paseo resolved.
3545
+ "workspace.create-succeeded": { intentId: id, workspaceId: workspaceIdSchema, worktreePath: z8.string().min(1).max(4096), branch, headCommit: commitSchema },
3546
+ "workspace.create-failed": { intentId: id, reason: reason2 },
3547
+ "workspace.create-uncertain": { intentId: id, reason: reason2 },
3548
+ // Created but failed its proof: no Peer is ever placed there, and a close follows.
3549
+ "workspace.create-refused": { intentId: id, workspaceId: workspaceIdSchema, reason: reason2 },
3550
+ "lease.reclaimed": { fromEpoch: epoch, toEpoch: epoch, priorAgentId: id, decidedBy: z8.enum(["lead", "human"]), reason: reason2 },
3551
+ // Handoff evidence, not containment (§5.4).
3552
+ "scope.exceeded": { candidateCommit: commitSchema, paths: z8.array(z8.string().min(1)).min(1) },
3553
+ "workspace.close-requested": { intentId: id, workspaceId: workspaceIdSchema, discardUncommitted: z8.boolean(), reason: reason2.optional() },
3554
+ "workspace.close-succeeded": { intentId: id, workspaceId: workspaceIdSchema, archivedAt: timestamp, directoryRemoved: z8.boolean() },
3555
+ "workspace.close-failed": { intentId: id, workspaceId: workspaceIdSchema, reason: reason2 },
3556
+ "workspace.close-uncertain": { intentId: id, workspaceId: workspaceIdSchema, reason: reason2 },
3181
3557
  // Deterministic notices (D9). Delivery is at least once with a stable id.
3182
3558
  "notice.pending": {
3183
3559
  noticeId: id,
@@ -3540,6 +3916,7 @@ var init_project = __esm({
3540
3916
  });
3541
3917
 
3542
3918
  // src/runtime-state.ts
3919
+ import { existsSync } from "fs";
3543
3920
  import { readdir as readdir5 } from "fs/promises";
3544
3921
  import { join as join15 } from "path";
3545
3922
  async function inspectRuntimeState(roomHome) {
@@ -3547,6 +3924,8 @@ async function inspectRuntimeState(roomHome) {
3547
3924
  const names = await readdir5(join15(root, "projects")).catch(() => []);
3548
3925
  const blockers = [];
3549
3926
  const stores = [];
3927
+ let retained = 0;
3928
+ let leftover = 0;
3550
3929
  for (const name of names.sort()) {
3551
3930
  try {
3552
3931
  stores.push(await ProjectStore.open(join15(root, "projects", name)));
@@ -3562,8 +3941,11 @@ async function inspectRuntimeState(roomHome) {
3562
3941
  continue;
3563
3942
  }
3564
3943
  for (const blocker of quiescence(projection.state).blockers) blockers.push({ ...blocker, project: store.meta.canonicalRoot });
3944
+ const onDisk = worktreesOnDisk(projection.state, existsSync);
3945
+ retained += onDisk.retained;
3946
+ leftover += onDisk.leftover;
3565
3947
  }
3566
- return { root, projects: names.length, blockers };
3948
+ return { root, projects: names.length, blockers, retainedWorktrees: retained, leftoverDirectories: leftover };
3567
3949
  }
3568
3950
  function describeBlockers(summary, limit = 5) {
3569
3951
  const shown = summary.blockers.slice(0, limit).map((blocker) => `${blocker.project}: ${blocker.kind} ${blocker.id} ${blocker.detail}`);
@@ -3931,17 +4313,27 @@ async function runtimeDeselectionCheck(layout) {
3931
4313
  }
3932
4314
  return [warn(
3933
4315
  "runtime.state-retained",
3934
- `Runtime coordination is quiet; its recorded state for ${String(summary.projects)} project(s) stays at ${summary.root}.`,
4316
+ `Runtime coordination is quiet; its recorded state for ${String(summary.projects)} project(s) stays at ${summary.root}.${worktreeNote(summary, "gone")}`,
3935
4317
  "Export it with: paseo-room export --out <dir>, or keep it for a later --runtime."
3936
4318
  )];
3937
4319
  }
4320
+ function worktreeNote(summary, panel) {
4321
+ const parts = [];
4322
+ if (summary.retainedWorktrees > 0) {
4323
+ parts.push(` ${String(summary.retainedWorktrees)} runtime worktree(s) remain open in Paseo; the CLI does not close them \u2014 ${panel === "available" ? "close them from the runtime panel first" : "archive them in Paseo, or re-enable --runtime and close them from the runtime panel"}.`);
4324
+ }
4325
+ if (summary.leftoverDirectories > 0) {
4326
+ parts.push(` ${String(summary.leftoverDirectories)} closed worktree director(ies) were left on disk by a failed teardown; remove them by hand once nothing in them is needed.`);
4327
+ }
4328
+ return parts.join("");
4329
+ }
3938
4330
  async function runtimeRemovalWarning(layout) {
3939
4331
  const summary = await inspectRuntimeState(layout.roomHome).catch(() => void 0);
3940
4332
  if (summary === void 0 || summary.projects === 0) return [];
3941
4333
  const active = summary.blockers.length > 0 ? ` It still has active or uncertain work: ${describeBlockers(summary)}.` : "";
3942
4334
  return [warn(
3943
4335
  "room.remove.runtime-state",
3944
- `Deleting ${layout.roomHome} also deletes runtime coordination history for ${String(summary.projects)} project(s).${active}`,
4336
+ `Deleting ${layout.roomHome} also deletes runtime coordination history for ${String(summary.projects)} project(s).${active}${worktreeNote(summary, "available")}`,
3945
4337
  "Export it first with: paseo-room export --out <dir>"
3946
4338
  )];
3947
4339
  }