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.
- package/README.md +118 -13
- package/dist/index.js +452 -60
- package/dist/index.js.map +1 -1
- package/dist/prompts/contract/lead.md +13 -4
- package/dist/prompts/contract/shared-authority.md +2 -2
- package/dist/prompts/contract/supervisor.md +17 -0
- package/dist/runtime-plugin/client/attention-settings.tsx +173 -0
- package/dist/runtime-plugin/client/data.ts +14 -2
- package/dist/runtime-plugin/client/forms.tsx +326 -0
- package/dist/runtime-plugin/client/host.ts +17 -0
- package/dist/runtime-plugin/client/kit.tsx +298 -0
- package/dist/runtime-plugin/client/model.ts +108 -0
- package/dist/runtime-plugin/client/pills.ts +58 -0
- package/dist/runtime-plugin/client/record.tsx +241 -0
- package/dist/runtime-plugin/client/role-pills.tsx +96 -0
- package/dist/runtime-plugin/client/room.tsx +291 -0
- package/dist/runtime-plugin/client/tone.ts +2 -0
- package/dist/runtime-plugin/client/views.tsx +161 -150
- package/dist/runtime-plugin/index.client.tsx +10 -2
- package/dist/runtime-plugin/index.server.ts +37 -3
- package/dist/runtime-plugin/server/attention/delivery.ts +193 -0
- package/dist/runtime-plugin/server/attention/engine.ts +465 -0
- package/dist/runtime-plugin/server/attention/key.ts +44 -0
- package/dist/runtime-plugin/server/attention/log.ts +54 -0
- package/dist/runtime-plugin/server/attention/mask.ts +44 -0
- package/dist/runtime-plugin/server/attention/observer.ts +313 -0
- package/dist/runtime-plugin/server/attention/portfolio.ts +77 -0
- package/dist/runtime-plugin/server/attention/questions.ts +80 -0
- package/dist/runtime-plugin/server/attention/seat-starter.ts +166 -0
- package/dist/runtime-plugin/server/attention/sensor.ts +207 -0
- package/dist/runtime-plugin/server/attention/signals.ts +230 -0
- package/dist/runtime-plugin/server/attention/triage.ts +68 -0
- package/dist/runtime-plugin/server/brief.ts +13 -1
- package/dist/runtime-plugin/server/context.ts +26 -2
- package/dist/runtime-plugin/server/contracts/actions.ts +18 -3
- package/dist/runtime-plugin/server/controller.ts +361 -58
- package/dist/runtime-plugin/server/correlations.ts +7 -3
- package/dist/runtime-plugin/server/domain/acceptance.ts +7 -2
- package/dist/runtime-plugin/server/domain/scope.ts +191 -0
- package/dist/runtime-plugin/server/domain/state.ts +281 -8
- package/dist/runtime-plugin/server/domain/views.ts +141 -7
- package/dist/runtime-plugin/server/events/schema.ts +33 -2
- package/dist/runtime-plugin/server/git.ts +93 -1
- package/dist/runtime-plugin/server/handlers/actions.ts +58 -12
- package/dist/runtime-plugin/server/handlers/peer.ts +22 -3
- package/dist/runtime-plugin/server/handlers/turns.ts +28 -21
- package/dist/runtime-plugin/server/host.ts +26 -0
- package/dist/runtime-plugin/server/lifecycle.ts +14 -3
- package/dist/runtime-plugin/server/notices.ts +26 -3
- package/dist/runtime-plugin/server/ownership.ts +1 -1
- package/dist/runtime-plugin/server/paseo-port.ts +299 -18
- package/dist/runtime-plugin/server/recovery.ts +124 -7
- package/dist/runtime-plugin/server/rpc.ts +198 -9
- package/dist/runtime-plugin/server/seats.ts +122 -0
- package/dist/runtime-plugin/server/tools.ts +9 -6
- package/dist/runtime-plugin/shared/attention.ts +70 -0
- package/dist/runtime-plugin/shared/identity.ts +8 -0
- package/dist/runtime-plugin/shared/limits.ts +6 -0
- package/dist/runtime-plugin/shared/policy.ts +2 -2
- package/dist/runtime-plugin/shared/rpc-contracts.ts +99 -1
- 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.
|
|
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
|
|
1550
|
-
evidence2 = catalogEvidence(
|
|
1551
|
-
catalogSource = renderCatalog(
|
|
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
|
|
1609
|
-
const settings = asObject2(
|
|
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
|
|
1776
|
+
let parsed2;
|
|
1776
1777
|
try {
|
|
1777
|
-
|
|
1778
|
+
parsed2 = JSON.parse(line);
|
|
1778
1779
|
} catch {
|
|
1779
1780
|
return probeFailure("Pi capability probe returned malformed JSONL.");
|
|
1780
1781
|
}
|
|
1781
|
-
const event = asObject2(
|
|
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
|
|
2021
|
-
if (!
|
|
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 =
|
|
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
|
|
2472
|
-
|
|
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 ||
|
|
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(
|
|
2517
|
-
case "agent.create-requested":
|
|
2518
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3065
|
-
return
|
|
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
|
-
|
|
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
|
}
|