@afokapu/atdd-flow 0.3.14 → 0.3.16
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/extensions/pi/index.ts +70 -10
- package/package.json +1 -1
- package/src/cli.ts +2 -2
- package/src/multiplexer.ts +102 -42
- package/src/seats.ts +9 -4
package/extensions/pi/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
4
4
|
import { parse, stringify } from "yaml";
|
|
5
5
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { clearStaleRuntimeState, clearRuntimeState, heartbeatRuntimeState, ownsRuntimeState, registerRuntimeState, runtimeOwnerToken, withOwnedRuntimeState } from "../../src/runtime-state";
|
|
7
|
+
import { paths, readYaml, type Seat } from "../../src/core";
|
|
7
8
|
|
|
8
9
|
type Thread = { id?: unknown; participants?: unknown; subject?: unknown };
|
|
9
10
|
export type Mail = { id?: unknown; from?: unknown; to?: unknown; subject?: unknown; created_at?: unknown };
|
|
@@ -40,12 +41,27 @@ export type PiRuntimeOptions = {
|
|
|
40
41
|
staleAfterMs?: number;
|
|
41
42
|
deliver: InboxOptions["deliver"];
|
|
42
43
|
createTransport?: (options: InboxOptions) => FlowInboxTransport;
|
|
44
|
+
/** Flow-launched Pi instances register and heartbeat before their verified launch activates inbox delivery. */
|
|
45
|
+
deferActivation?: boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type LaunchActivationOptions = {
|
|
49
|
+
root: string;
|
|
50
|
+
seat: string;
|
|
51
|
+
piSession: string;
|
|
52
|
+
herdrSession: string;
|
|
53
|
+
pane: string;
|
|
54
|
+
activate: () => Promise<void>;
|
|
55
|
+
intervalMs?: number;
|
|
56
|
+
timeoutMs?: number;
|
|
43
57
|
};
|
|
44
58
|
|
|
45
59
|
const defaultBatchSize = 32;
|
|
46
60
|
const defaultIntervalMs = 30_000;
|
|
47
61
|
const defaultHeartbeatMs = 15_000;
|
|
48
62
|
const defaultStaleAfterMs = 60_000;
|
|
63
|
+
const defaultActivationIntervalMs = 25;
|
|
64
|
+
const defaultActivationTimeoutMs = 60_000;
|
|
49
65
|
|
|
50
66
|
function participants(thread: Thread) {
|
|
51
67
|
return Array.isArray(thread.participants) && thread.participants.every((entry) => typeof entry === "string") ? thread.participants as string[] : [];
|
|
@@ -216,11 +232,12 @@ export const createInboxReconciler = createInboxTransport;
|
|
|
216
232
|
*/
|
|
217
233
|
export function createPiRuntime({
|
|
218
234
|
root, seat, pid = process.pid, model = process.env.PI_MODEL ?? "pi", cwd = process.cwd(),
|
|
219
|
-
heartbeatMs = defaultHeartbeatMs, staleAfterMs = defaultStaleAfterMs, deliver, createTransport = createInboxTransport,
|
|
235
|
+
heartbeatMs = defaultHeartbeatMs, staleAfterMs = defaultStaleAfterMs, deliver, createTransport = createInboxTransport, deferActivation = false,
|
|
220
236
|
}: PiRuntimeOptions) {
|
|
221
237
|
const ownerToken = runtimeOwnerToken();
|
|
222
238
|
let transport: FlowInboxTransport | undefined;
|
|
223
239
|
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
240
|
+
let activated = false;
|
|
224
241
|
|
|
225
242
|
const owned = () => ownsRuntimeState(root, seat, ownerToken);
|
|
226
243
|
const stop = async () => {
|
|
@@ -234,11 +251,9 @@ export function createPiRuntime({
|
|
|
234
251
|
if (!transport || !await owned()) return;
|
|
235
252
|
await transport.reconcile();
|
|
236
253
|
};
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
await clearStaleRuntimeState(root, seat, staleAfterMs);
|
|
241
|
-
await registerRuntimeState(root, { seat, owner_token: ownerToken, pid, model, cwd });
|
|
254
|
+
const activate = async () => {
|
|
255
|
+
if (!await owned()) return false;
|
|
256
|
+
if (activated) return true;
|
|
242
257
|
transport = createTransport({
|
|
243
258
|
root, seat, batchSize: defaultBatchSize, intervalMs: defaultIntervalMs,
|
|
244
259
|
deliver: async (mail, thread, path) => {
|
|
@@ -257,13 +272,47 @@ export function createPiRuntime({
|
|
|
257
272
|
},
|
|
258
273
|
});
|
|
259
274
|
await transport.start();
|
|
275
|
+
activated = true;
|
|
276
|
+
return true;
|
|
277
|
+
};
|
|
278
|
+
const start = async () => {
|
|
279
|
+
// This removes only an expired local observation. Registration itself is
|
|
280
|
+
// replacement-safe and does not alter the authoritative Desk seat.
|
|
281
|
+
await clearStaleRuntimeState(root, seat, staleAfterMs);
|
|
282
|
+
await registerRuntimeState(root, { seat, owner_token: ownerToken, pid, model, cwd });
|
|
260
283
|
heartbeat = setInterval(() => {
|
|
261
284
|
void heartbeatRuntimeState(root, seat, ownerToken).then((state) => {
|
|
262
285
|
if (!state) void stop();
|
|
263
286
|
});
|
|
264
287
|
}, heartbeatMs);
|
|
288
|
+
if (!deferActivation) await activate();
|
|
265
289
|
};
|
|
266
|
-
return { ownerToken, start, stop, reconcile, get transport() { return transport; } };
|
|
290
|
+
return { ownerToken, start, activate, stop, reconcile, get transport() { return transport; } };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
type LaunchReceipt = { seat?: string; pi_session?: string; pi_session_path?: string; herdr_session?: string; pane?: string };
|
|
294
|
+
|
|
295
|
+
/** Wait only for the launcher's already-verified receipt and exact Desk binding; this never creates either record. */
|
|
296
|
+
export async function awaitLaunchActivation({ root, seat, piSession, herdrSession, pane, activate, intervalMs = defaultActivationIntervalMs, timeoutMs = defaultActivationTimeoutMs }: LaunchActivationOptions) {
|
|
297
|
+
const deadline = Date.now() + timeoutMs;
|
|
298
|
+
while (Date.now() <= deadline) {
|
|
299
|
+
try {
|
|
300
|
+
const record = await readYaml<Seat>(paths(root).seatFile(seat));
|
|
301
|
+
const runtime = record.runtime;
|
|
302
|
+
const locator = runtime?.addresses.herdr;
|
|
303
|
+
if (runtime?.application === "herdr" && typeof locator !== "string" && locator?.session === herdrSession && locator.pane === pane
|
|
304
|
+
&& runtime.pi_session === piSession && runtime.pi_session_path && runtime.launch_receipt) {
|
|
305
|
+
const receipt = await readYaml<LaunchReceipt>(runtime.launch_receipt);
|
|
306
|
+
if (receipt.seat === seat && receipt.pi_session === piSession && receipt.pi_session_path === runtime.pi_session_path
|
|
307
|
+
&& receipt.herdr_session === herdrSession && receipt.pane === pane) {
|
|
308
|
+
await activate();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch { /* Binding and receipt are written after Herdr verification; wait within this finite window. */ }
|
|
313
|
+
await pause(intervalMs);
|
|
314
|
+
}
|
|
315
|
+
throw new Error("Timed out waiting for verified Flow launch receipt and binding.");
|
|
267
316
|
}
|
|
268
317
|
|
|
269
318
|
/** Desk mail is authoritative; the queue is a bounded delivery index and fs.watch only shortens latency. */
|
|
@@ -273,7 +322,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
273
322
|
if (!root || !seat) return;
|
|
274
323
|
let runtime: ReturnType<typeof createPiRuntime> | undefined;
|
|
275
324
|
pi.on("session_start", async (_event, ctx) => {
|
|
276
|
-
runtime = createPiRuntime({ root, seat, deliver: async (mail, thread, path, sendIfOwned) => {
|
|
325
|
+
runtime = createPiRuntime({ root, seat, deferActivation: true, deliver: async (mail, thread, path, sendIfOwned) => {
|
|
277
326
|
if (!sendIfOwned) return false;
|
|
278
327
|
return sendIfOwned(() => {
|
|
279
328
|
pi.sendMessage({
|
|
@@ -284,8 +333,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
284
333
|
});
|
|
285
334
|
} });
|
|
286
335
|
await runtime.start();
|
|
287
|
-
|
|
288
|
-
|
|
336
|
+
const piSession = process.env.ATDD_FLOW_PI_SESSION;
|
|
337
|
+
const herdrSession = process.env.ATDD_FLOW_HERDR_SESSION;
|
|
338
|
+
const pane = process.env.ATDD_FLOW_HERDR_PANE;
|
|
339
|
+
if (!piSession || !herdrSession || !pane) return;
|
|
340
|
+
const current = runtime;
|
|
341
|
+
void awaitLaunchActivation({
|
|
342
|
+
root, seat, piSession, herdrSession, pane,
|
|
343
|
+
activate: async () => {
|
|
344
|
+
if (!await current.activate()) return;
|
|
345
|
+
pi.sendMessage({ customType: "atdd-flow-start", content: `SYSTEM: you are ${seat}. Read your durable seat and assigned work with: atdd-flow open ${seat}. Continue assigned in_progress work until it is review-ready or explicitly blocked.`, display: true, details: { seat, root } }, { triggerTurn: true, deliverAs: "followUp" });
|
|
346
|
+
if (ctx.hasUI) ctx.ui.notify(`ATDD Flow native mail active for ${seat}`, "info");
|
|
347
|
+
},
|
|
348
|
+
}).catch(() => undefined);
|
|
289
349
|
});
|
|
290
350
|
pi.on("session_shutdown", () => { void runtime?.stop(); });
|
|
291
351
|
}
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -25,7 +25,7 @@ Usage:
|
|
|
25
25
|
atdd-flow bind <address> [--application <application>] --address <native-address> [--session <name>] [--agent <executable>] [--worktree <path>] [--wake host|native]
|
|
26
26
|
atdd-flow attach <address> [--application <application>] [--wake host|native]
|
|
27
27
|
atdd-flow pi extension-path
|
|
28
|
-
atdd-flow pi runtime launch <address> --pane <
|
|
28
|
+
atdd-flow pi runtime launch <address> [--pane <asserted-herdr-pane>] --herdr-session <session> [--resume] [--dry-run]
|
|
29
29
|
atdd-flow application use <address> <application>
|
|
30
30
|
atdd-flow multiplexer status|apply herdr [--session <name>]
|
|
31
31
|
atdd-flow describe <address> --purpose <one-line responsibility>
|
|
@@ -92,7 +92,7 @@ async function main() {
|
|
|
92
92
|
else console.log(`Launched Pi runtime for ${required(rest[2], "address")} with ${plan.candidate}.`);
|
|
93
93
|
return;
|
|
94
94
|
}
|
|
95
|
-
throw new Error("Use `pi extension-path` or `pi runtime launch <address> --pane <pane> --herdr-session <session> [--resume] [--dry-run]`.");
|
|
95
|
+
throw new Error("Use `pi extension-path` or `pi runtime launch <address> [--pane <asserted-pane>] --herdr-session <session> [--resume] [--dry-run]`.");
|
|
96
96
|
},
|
|
97
97
|
application: async () => {
|
|
98
98
|
if (rest[0] === "use") return useApplication(root, required(rest[1], "address"), required(rest[2], "application"));
|
package/src/multiplexer.ts
CHANGED
|
@@ -28,6 +28,17 @@ type Workspace = { workspace_id: string; label?: string; worktree?: { checkout_p
|
|
|
28
28
|
type Tab = { tab_id: string; workspace_id: string; label?: string };
|
|
29
29
|
type Pane = { pane_id: string; tab_id: string; label?: string };
|
|
30
30
|
type Target = { project: string; worktree: string; workspaceLabel: string; seat?: Seat };
|
|
31
|
+
type WorkspaceState = { id: string; label: string; worktree: string; rootTab?: string; rootPane?: string };
|
|
32
|
+
type HerdrCommand = (command: string[]) => Promise<string>;
|
|
33
|
+
|
|
34
|
+
export type HerdrSeatProjection = {
|
|
35
|
+
session: string;
|
|
36
|
+
workspace: string;
|
|
37
|
+
tab: string;
|
|
38
|
+
pane: string;
|
|
39
|
+
worktree: string;
|
|
40
|
+
label: string;
|
|
41
|
+
};
|
|
31
42
|
|
|
32
43
|
const policyFile = () => join(import.meta.dir, "..", "multiplexer", "herdr.yaml");
|
|
33
44
|
const selectedSession = (args: string[], environment: Record<string, string | undefined> = process.env) => {
|
|
@@ -87,11 +98,8 @@ async function targets(root: string): Promise<Target[]> {
|
|
|
87
98
|
let config: Project;
|
|
88
99
|
try { config = await project(root, name); }
|
|
89
100
|
catch { continue; }
|
|
90
|
-
// A repository is the Desk's explicit primary-checkout declaration. Do not infer one.
|
|
91
101
|
if (!config.repository) continue;
|
|
92
102
|
const primary = resolve(config.repository);
|
|
93
|
-
// The primary checkout is explicitly declared by the project even if no
|
|
94
|
-
// primary-worktree seat currently occupies it.
|
|
95
103
|
result.push({ project: name, worktree: primary, workspaceLabel: name });
|
|
96
104
|
for (const entry of await projectSeats(root, name)) {
|
|
97
105
|
if ((entry.role === "main" || entry.role === "coordinator") && resolve(entry.worktree) === primary) {
|
|
@@ -111,68 +119,110 @@ async function targets(root: string): Promise<Target[]> {
|
|
|
111
119
|
return result;
|
|
112
120
|
}
|
|
113
121
|
|
|
114
|
-
|
|
115
|
-
return runOutput(["herdr", "--session", session, ...args]);
|
|
116
|
-
}
|
|
117
|
-
|
|
122
|
+
const herdr: HerdrCommand = (args) => runOutput(args);
|
|
118
123
|
function json(output: string): any {
|
|
119
124
|
try { return JSON.parse(output).result; }
|
|
120
125
|
catch { throw new Error("Herdr returned invalid JSON; projection did not guess a target."); }
|
|
121
126
|
}
|
|
122
|
-
|
|
123
127
|
function workspaceId(value: any) {
|
|
124
128
|
const found = value?.workspace?.workspace_id ?? value?.workspace_id;
|
|
125
129
|
if (typeof found !== "string") throw new Error("Herdr did not return a workspace id.");
|
|
126
130
|
return found;
|
|
127
131
|
}
|
|
128
|
-
|
|
132
|
+
function tabId(value: any) {
|
|
133
|
+
const found = value?.root_tab?.tab_id ?? value?.tab?.tab_id;
|
|
134
|
+
return typeof found === "string" ? found : undefined;
|
|
135
|
+
}
|
|
129
136
|
function paneId(value: any) {
|
|
130
137
|
const found = value?.root_pane?.pane_id ?? value?.pane?.pane_id;
|
|
131
|
-
|
|
132
|
-
return found;
|
|
138
|
+
return typeof found === "string" ? found : undefined;
|
|
133
139
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const value = json(await command(session, ["workspace", "list"]));
|
|
140
|
+
async function liveWorkspaces(session: string, command: HerdrCommand) {
|
|
141
|
+
const value = json(await command(["herdr", "--session", session, "workspace", "list"]));
|
|
137
142
|
if (!Array.isArray(value?.workspaces)) throw new Error("Herdr workspace list response is invalid.");
|
|
138
143
|
return value.workspaces as Workspace[];
|
|
139
144
|
}
|
|
140
145
|
|
|
141
|
-
async function ensureWorkspace(session: string, target: Target, workspaces: Workspace[]) {
|
|
146
|
+
async function ensureWorkspace(session: string, target: Target, workspaces: Workspace[], command: HerdrCommand, primary?: WorkspaceState) {
|
|
142
147
|
let current = workspaces.find((entry) => resolve(entry.worktree?.checkout_path ?? "") === target.worktree);
|
|
143
148
|
if (!current) {
|
|
144
|
-
const
|
|
145
|
-
?
|
|
146
|
-
:
|
|
149
|
+
const args = target.workspaceLabel === target.project
|
|
150
|
+
? ["workspace", "create", "--cwd", target.worktree, "--label", target.workspaceLabel, "--no-focus"]
|
|
151
|
+
: ["worktree", "open", "--workspace", primary?.id ?? "", "--cwd", primary?.worktree ?? "", "--path", target.worktree, "--label", target.workspaceLabel, "--no-focus"];
|
|
152
|
+
if (args.includes("")) throw new Error(`Project primary workspace is required to open linked worktree ${target.worktree}.`);
|
|
153
|
+
const created = json(await command(["herdr", "--session", session, ...args]));
|
|
147
154
|
current = { workspace_id: workspaceId(created), label: target.workspaceLabel, worktree: { checkout_path: target.worktree } };
|
|
148
155
|
workspaces.push(current);
|
|
149
|
-
|
|
150
|
-
|
|
156
|
+
return { id: current.workspace_id, label: target.workspaceLabel, worktree: target.worktree, ...(tabId(created) ? { rootTab: tabId(created) } : {}), ...(paneId(created) ? { rootPane: paneId(created) } : {}) };
|
|
157
|
+
}
|
|
158
|
+
if (current.label !== target.workspaceLabel) {
|
|
159
|
+
await command(["herdr", "--session", session, "workspace", "rename", current.workspace_id, target.workspaceLabel]);
|
|
151
160
|
current.label = target.workspaceLabel;
|
|
152
161
|
}
|
|
153
|
-
return current.workspace_id;
|
|
162
|
+
return { id: current.workspace_id, label: target.workspaceLabel, worktree: target.worktree };
|
|
154
163
|
}
|
|
155
164
|
|
|
156
|
-
async function
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
165
|
+
async function tabs(session: string, workspace: string, command: HerdrCommand) {
|
|
166
|
+
const result = json(await command(["herdr", "--session", session, "tab", "list", "--workspace", workspace]));
|
|
167
|
+
if (!Array.isArray(result?.tabs)) throw new Error("Herdr tab list response is invalid.");
|
|
168
|
+
return result.tabs as Tab[];
|
|
169
|
+
}
|
|
170
|
+
async function panes(session: string, workspace: string, command: HerdrCommand) {
|
|
171
|
+
const result = json(await command(["herdr", "--session", session, "pane", "list", "--workspace", workspace]));
|
|
172
|
+
if (!Array.isArray(result?.panes)) throw new Error("Herdr pane list response is invalid.");
|
|
173
|
+
return result.panes as Pane[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function ensureSeatTab(session: string, workspace: WorkspaceState, target: Target & { seat: Seat }, command: HerdrCommand): Promise<HerdrSeatProjection> {
|
|
177
|
+
let entries = await tabs(session, workspace.id, command);
|
|
178
|
+
let tab = entries.find((entry) => entry.label === target.seat.address);
|
|
161
179
|
let pane: string | undefined;
|
|
180
|
+
if (!tab && workspace.rootTab) {
|
|
181
|
+
tab = entries.find((entry) => entry.tab_id === workspace.rootTab) ?? { tab_id: workspace.rootTab, workspace_id: workspace.id };
|
|
182
|
+
await command(["herdr", "--session", session, "tab", "rename", tab.tab_id, target.seat.address]);
|
|
183
|
+
tab.label = target.seat.address;
|
|
184
|
+
}
|
|
162
185
|
if (!tab) {
|
|
163
|
-
const created = json(await command(session,
|
|
164
|
-
|
|
186
|
+
const created = json(await command(["herdr", "--session", session, "tab", "create", "--workspace", workspace.id, "--cwd", target.worktree, "--label", target.seat.address, "--no-focus"]));
|
|
187
|
+
const id = tabId(created);
|
|
188
|
+
if (!id) throw new Error("Herdr did not return a tab id.");
|
|
189
|
+
tab = { tab_id: id, workspace_id: workspace.id, label: target.seat.address };
|
|
165
190
|
pane = paneId(created);
|
|
166
|
-
if (!tab.tab_id) throw new Error("Herdr did not return a tab id.");
|
|
167
191
|
}
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
171
|
-
const
|
|
172
|
-
if (labelled) return;
|
|
173
|
-
const first = pane ?? panes.find((entry) => entry.tab_id === tab!.tab_id)?.pane_id;
|
|
192
|
+
const entriesPanes = await panes(session, workspace.id, command);
|
|
193
|
+
const labelled = entriesPanes.find((entry) => entry.tab_id === tab!.tab_id && entry.label === target.seat.address);
|
|
194
|
+
if (labelled) return { session, workspace: workspace.id, tab: tab.tab_id, pane: labelled.pane_id, worktree: target.worktree, label: target.seat.address };
|
|
195
|
+
const first = pane ?? workspace.rootPane ?? entriesPanes.find((entry) => entry.tab_id === tab!.tab_id)?.pane_id;
|
|
174
196
|
if (!first) throw new Error(`Herdr tab ${tab.tab_id} has no pane to label.`);
|
|
175
|
-
await command(session,
|
|
197
|
+
await command(["herdr", "--session", session, "pane", "rename", first, target.seat.address]);
|
|
198
|
+
return { session, workspace: workspace.id, tab: tab.tab_id, pane: first, worktree: target.worktree, label: target.seat.address };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function ensureTarget(session: string, target: Target, workspaces: Workspace[], command: HerdrCommand, primary: WorkspaceState) {
|
|
202
|
+
const workspace = target.workspaceLabel === target.project ? primary : await ensureWorkspace(session, target, workspaces, command, primary);
|
|
203
|
+
return target.seat ? ensureSeatTab(session, workspace, target as Target & { seat: Seat }, command) : undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Reconciles exactly one Desk seat into a labeled Herdr workspace/tab/pane. */
|
|
207
|
+
export async function projectHerdrSeat(root: string, address: string, session: string, command: HerdrCommand = herdr): Promise<HerdrSeatProjection> {
|
|
208
|
+
await herdrPolicy();
|
|
209
|
+
const desired = await targets(root);
|
|
210
|
+
const target = desired.find((entry) => entry.seat?.address === address);
|
|
211
|
+
if (!target?.seat) throw new Error(`No active Desk projection target exists for ${address}.`);
|
|
212
|
+
const primaryTarget = desired.find((entry) => entry.project === target.project && entry.workspaceLabel === target.project && !entry.seat);
|
|
213
|
+
if (!primaryTarget) throw new Error(`Project ${target.project} has no declared primary workspace.`);
|
|
214
|
+
const workspaces = await liveWorkspaces(session, command);
|
|
215
|
+
const primary = await ensureWorkspace(session, primaryTarget, workspaces, command);
|
|
216
|
+
return (await ensureTarget(session, target, workspaces, command, primary))!;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function topologyCompliant(session: string, target: Target, workspace: Workspace, command: HerdrCommand) {
|
|
220
|
+
if (workspace.label !== target.workspaceLabel) return false;
|
|
221
|
+
if (!target.seat) return true;
|
|
222
|
+
const entries = await tabs(session, workspace.workspace_id, command);
|
|
223
|
+
const tab = entries.find((entry) => entry.label === target.seat!.address);
|
|
224
|
+
if (!tab) return false;
|
|
225
|
+
return (await panes(session, workspace.workspace_id, command)).some((entry) => entry.tab_id === tab.tab_id && entry.label === target.seat!.address);
|
|
176
226
|
}
|
|
177
227
|
|
|
178
228
|
export async function multiplexer(root: string, args: string[]) {
|
|
@@ -188,17 +238,27 @@ export async function multiplexer(root: string, args: string[]) {
|
|
|
188
238
|
return;
|
|
189
239
|
}
|
|
190
240
|
const desired = await targets(root);
|
|
241
|
+
const command: HerdrCommand = herdr;
|
|
242
|
+
const workspaces = await liveWorkspaces(session, command);
|
|
191
243
|
if (action === "status") {
|
|
192
|
-
// Status deliberately has no reconciliation side effects.
|
|
193
|
-
const workspaces = await liveWorkspaces(session);
|
|
194
244
|
const present = desired.filter((target) => workspaces.some((entry) => resolve(entry.worktree?.checkout_path ?? "") === target.worktree));
|
|
195
|
-
|
|
245
|
+
const topology = await Promise.all(desired.map(async (target) => {
|
|
246
|
+
const workspace = workspaces.find((entry) => resolve(entry.worktree?.checkout_path ?? "") === target.worktree);
|
|
247
|
+
return workspace ? topologyCompliant(session, target, workspace, command) : false;
|
|
248
|
+
}));
|
|
249
|
+
console.log(JSON.stringify({ schema: "atdd-workflow/multiplexer-status/v1", application: "herdr", session, desired: desired.length, present: present.length, topology_compliant: topology.filter(Boolean).length }, null, 2));
|
|
196
250
|
return;
|
|
197
251
|
}
|
|
198
|
-
const
|
|
252
|
+
const primary = new Map<string, WorkspaceState>();
|
|
199
253
|
for (const target of desired) {
|
|
200
|
-
|
|
201
|
-
if (
|
|
254
|
+
let projectPrimary = primary.get(target.project);
|
|
255
|
+
if (!projectPrimary) {
|
|
256
|
+
const primaryTarget = desired.find((entry) => entry.project === target.project && entry.workspaceLabel === target.project && !entry.seat);
|
|
257
|
+
if (!primaryTarget) throw new Error(`Project ${target.project} has no declared primary workspace.`);
|
|
258
|
+
projectPrimary = await ensureWorkspace(session, primaryTarget, workspaces, command);
|
|
259
|
+
primary.set(target.project, projectPrimary);
|
|
260
|
+
}
|
|
261
|
+
await ensureTarget(session, target, workspaces, command, projectPrimary);
|
|
202
262
|
}
|
|
203
263
|
console.log(`Projected ${desired.length} Desk seat worktree(s) into Herdr session ${session}.`);
|
|
204
264
|
}
|
package/src/seats.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "./core";
|
|
10
10
|
import { type ModelSelectionInput, type ModelSelectionResponse, selectModel } from "./judgment";
|
|
11
11
|
import { seatTasks } from "./tasks";
|
|
12
|
+
import { projectHerdrSeat } from "./multiplexer";
|
|
12
13
|
|
|
13
14
|
const defaultRoles = (dynamicModels = true): Record<string, Role> => ({
|
|
14
15
|
coordinator: {
|
|
@@ -246,7 +247,7 @@ function agentReport(value: unknown) {
|
|
|
246
247
|
const name = text(agent?.name);
|
|
247
248
|
const pane = text(agent?.pane_id);
|
|
248
249
|
if (!name || !pane) throw new Error("Herdr agent get did not report agent name and pane id.");
|
|
249
|
-
return { name, pane, status: text(agent?.status) ?? text(agent?.state) };
|
|
250
|
+
return { name, pane, status: text(agent?.agent_status) ?? text(agent?.status) ?? text(agent?.state) };
|
|
250
251
|
}
|
|
251
252
|
function piProcess(processes: ProcessReport["processes"]) {
|
|
252
253
|
return processes.find((entry) => entry.name === "pi" || entry.argv0?.endsWith("/pi") || entry.argv0 === "pi" || /(^|\/)pi(?:\s|$)/.test(entry.cmdline ?? ""));
|
|
@@ -271,17 +272,21 @@ export async function launchPiRuntime(root: string, address: string, args: strin
|
|
|
271
272
|
const resolved = await canonicalAddress(root, address);
|
|
272
273
|
const record = await seat(root, resolved);
|
|
273
274
|
const config = await desk(root);
|
|
274
|
-
const
|
|
275
|
+
const requestedPane = words(args, "--pane");
|
|
275
276
|
const herdrSession = required(words(args, "--herdr-session"), "--herdr-session");
|
|
276
277
|
const resume = args.includes("--resume");
|
|
277
278
|
const dryRun = args.includes("--dry-run");
|
|
278
|
-
|
|
279
|
+
const allowed = ["--herdr-session", herdrSession, "--resume", "--dry-run", ...(requestedPane ? ["--pane", requestedPane] : [])];
|
|
280
|
+
if (args.some((argument) => !allowed.includes(argument))) throw new Error("Use `pi runtime launch <seat> [--pane <asserted-pane>] --herdr-session <session> [--resume] [--dry-run]`.");
|
|
279
281
|
const active = await launchEligibleTasks(root, record);
|
|
280
282
|
if (!active.length) throw new Error(`${resolved} has no launch-eligible task; refusing runtime launch.`);
|
|
281
283
|
const selection = await chooseLaunchModel(root, config, record, required(await modelPortfolio(root), "models.yaml for Pi runtime launch"), dependencies.select);
|
|
282
284
|
const model = resolveModelCommand(config, selection.candidate);
|
|
283
285
|
if (basename(model.agent) !== "pi") throw new Error(`Pi runtime launch requires a Pi candidate, received ${selection.candidate.id}.`);
|
|
284
286
|
const command = dependencies.command ?? runOutput;
|
|
287
|
+
const projection = await projectHerdrSeat(root, resolved, herdrSession, command);
|
|
288
|
+
if (requestedPane && requestedPane !== projection.pane) throw new Error("The asserted --pane does not match the seat-scoped Herdr projection.");
|
|
289
|
+
const pane = projection.pane;
|
|
285
290
|
const [paneRaw, processRaw] = await Promise.all([command(herdr(herdrSession, "pane", "get", pane)), command(herdr(herdrSession, "pane", "process-info", "--pane", pane))]);
|
|
286
291
|
const currentPane = paneReport(JSON.parse(paneRaw));
|
|
287
292
|
const currentProcess = processReport(JSON.parse(processRaw));
|
|
@@ -301,7 +306,7 @@ export async function launchPiRuntime(root: string, address: string, args: strin
|
|
|
301
306
|
const request: HerdrRequest = { session: herdrSession, pane, seat: resolved, root, piSession, ...(priorSessionPath ? { sessionPath: priorSessionPath } : {}), resume, args: [...model.args, "--extension", piExtensionPath()] };
|
|
302
307
|
const plan = { candidate: selection.candidate.id, piSession, pane, herdrSession, receipt: receiptPath(root, piSession), command: startCommand(request), ...(dryRun ? { dryRun: true } : {}) };
|
|
303
308
|
if (dryRun) return plan;
|
|
304
|
-
await command(herdr(herdrSession, "pane", "run", pane, `export ATDD_WORKFLOW_ROOT=${shellQuote(root)} ATDD_WORKFLOW_SEAT=${shellQuote(resolved)}`));
|
|
309
|
+
await command(herdr(herdrSession, "pane", "run", pane, `export ATDD_WORKFLOW_ROOT=${shellQuote(root)} ATDD_WORKFLOW_SEAT=${shellQuote(resolved)} ATDD_FLOW_PI_SESSION=${shellQuote(piSession)} ATDD_FLOW_HERDR_SESSION=${shellQuote(herdrSession)} ATDD_FLOW_HERDR_PANE=${shellQuote(pane)}`));
|
|
305
310
|
await command(startCommand(request));
|
|
306
311
|
const [agentRaw, verifiedPaneRaw, verifiedProcessRaw] = await Promise.all([command(herdr(herdrSession, "agent", "get", agentName(resolved))), command(herdr(herdrSession, "pane", "get", pane)), command(herdr(herdrSession, "pane", "process-info", "--pane", pane))]);
|
|
307
312
|
const agent = agentReport(JSON.parse(agentRaw));
|