@afokapu/atdd-flow 0.3.48 → 0.3.50
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 +33 -0
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/handover.ts +162 -41
package/extensions/pi/index.ts
CHANGED
|
@@ -318,6 +318,28 @@ export async function awaitLaunchActivation({ root, seat, piSession, herdrSessio
|
|
|
318
318
|
throw new Error("Timed out waiting for verified Flow launch receipt and binding.");
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Host-attached Pi has no launcher variables; it may activate only when Herdr's injected pane identity exactly
|
|
323
|
+
* matches the Desk seat's native scoped binding. This reads the seat and never writes or creates a binding.
|
|
324
|
+
*/
|
|
325
|
+
export async function hostAttachedActivation({ root, seat, herdrSession, pane, piSession, piSessionPath }: { root: string; seat: string; herdrSession?: string; pane?: string; piSession?: string; piSessionPath?: string }) {
|
|
326
|
+
if (!herdrSession || !pane) return false;
|
|
327
|
+
try {
|
|
328
|
+
const runtime = (await readYaml<Seat>(paths(root).seatFile(seat))).runtime;
|
|
329
|
+
return nativeScopedTo(runtime, herdrSession, pane) && sessionUnsetOrEqual(runtime, piSession, piSessionPath);
|
|
330
|
+
} catch { return false; }
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function nativeScopedTo(runtime: Seat["runtime"], herdrSession: string, pane: string) {
|
|
334
|
+
const locator = runtime?.addresses.herdr;
|
|
335
|
+
return runtime?.application === "herdr" && runtime.wake === "native" && !runtime.launch_receipt
|
|
336
|
+
&& typeof locator === "object" && locator.session === herdrSession && locator.pane === pane;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const unsetOrEqual = (recorded: string | undefined, live: string | undefined) => !recorded || recorded === live;
|
|
340
|
+
const sessionUnsetOrEqual = (runtime: Seat["runtime"], piSession?: string, piSessionPath?: string) =>
|
|
341
|
+
unsetOrEqual(runtime?.pi_session, piSession) && unsetOrEqual(runtime?.pi_session_path, piSessionPath);
|
|
342
|
+
|
|
321
343
|
/** Injects the current task-derived action; failure to derive never blocks mail or startup. */
|
|
322
344
|
async function sendTaskDirective(pi: ExtensionAPI, root: string, seat: string) {
|
|
323
345
|
try {
|
|
@@ -337,6 +359,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
337
359
|
if (!root || !seat) return;
|
|
338
360
|
let runtime: ReturnType<typeof createPiRuntime> | undefined;
|
|
339
361
|
let handoverPrompted = false;
|
|
362
|
+
// Host-attach only: mail older than this activation is consumed without a prompt, so a long backlog cannot trigger one turn per message.
|
|
363
|
+
let hostActivatedAt: number | undefined;
|
|
340
364
|
pi.on("turn_end", async (_event, ctx) => {
|
|
341
365
|
if (handoverPrompted) return;
|
|
342
366
|
const percent = ctx.getContextUsage()?.percent;
|
|
@@ -369,6 +393,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
369
393
|
pi.on("session_start", async (_event, ctx) => {
|
|
370
394
|
runtime = createPiRuntime({ root, seat, deferActivation: true, deliver: async (mail, thread, path, sendIfOwned) => {
|
|
371
395
|
if (!sendIfOwned) return false;
|
|
396
|
+
if (hostActivatedAt !== undefined && Date.parse(typeof mail.created_at === "string" ? mail.created_at : "") < hostActivatedAt) return sendIfOwned(async () => undefined);
|
|
372
397
|
return sendIfOwned(async () => {
|
|
373
398
|
// Task state is re-read at every wake, so queued mail cannot carry a stale action.
|
|
374
399
|
await sendTaskDirective(pi, root, seat);
|
|
@@ -383,6 +408,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
383
408
|
const piSession = process.env.ATDD_FLOW_PI_SESSION;
|
|
384
409
|
const herdrSession = process.env.ATDD_FLOW_HERDR_SESSION;
|
|
385
410
|
const pane = process.env.ATDD_FLOW_HERDR_PANE;
|
|
411
|
+
if (!piSession && !herdrSession && !pane) {
|
|
412
|
+
const sessionManager = (ctx as { sessionManager?: { getSessionId?: () => string; getSessionFile?: () => string | undefined } }).sessionManager;
|
|
413
|
+
if (await hostAttachedActivation({ root, seat, herdrSession: process.env.HERDR_SESSION, pane: process.env.HERDR_PANE_ID, piSession: sessionManager?.getSessionId?.(), piSessionPath: sessionManager?.getSessionFile?.() })) {
|
|
414
|
+
hostActivatedAt = Date.now();
|
|
415
|
+
if (await runtime.activate()) pi.sendMessage({ customType: "atdd-flow-start", content: `Native mail active for ${seat} (host-attached). Notices created before this session were not replayed; run atdd-flow status seat ${seat} for unanswered mail.`, display: true, details: { seat, root } }, { triggerTurn: true, deliverAs: "followUp" });
|
|
416
|
+
}
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
386
419
|
if (!piSession || !herdrSession || !pane) return;
|
|
387
420
|
const current = runtime;
|
|
388
421
|
void awaitLaunchActivation({
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -38,7 +38,7 @@ Usage:
|
|
|
38
38
|
atdd-flow checkpoint <address> --summary <text> --next <text> [--status active|standby|blocked|complete|unverified]
|
|
39
39
|
atdd-flow handover <address> --file <path> [--check]
|
|
40
40
|
atdd-flow handover show <address>
|
|
41
|
-
atdd-flow handover replace <address> --handover <id> --authorization <operator-record> --takeover-task <id> --source-pi-session <id> --source-pi-session-path <jsonl> --source-herdr-session <session> --source-pane <pane> --herdr-session <session> --target-pane <pane> --target-cwd <canonical-main-worktree>
|
|
41
|
+
atdd-flow handover replace <address> [--legacy-source-bootstrap] --handover <id> --authorization <operator-record> --takeover-task <id> --source-pi-session <id> --source-pi-session-path <jsonl> --source-herdr-session <session> --source-pane <pane> --herdr-session <session> --target-pane <pane> --target-cwd <canonical-main-worktree>
|
|
42
42
|
atdd-flow handover resumed <address>
|
|
43
43
|
atdd-flow task add <project> <task-id> --title <text> --coordinator <address> [--assignee <address>] [--body <text>] [--source <reference>] [--depends-on <task-id,...>] --done-when <text> [--done-when <text> ...]
|
|
44
44
|
atdd-flow task assign <project> <task-id> --assignee <address> --by <coordinator-address>
|
package/src/handover.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
-
import { type Runtime, type Seat, atomicYaml, canonicalAddress, desk, exists, has, now, paths, project, readYaml, required, seat, words, yaml } from "./core";
|
|
2
|
+
import { link, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { type Runtime, type Seat, atomicYaml, canonicalAddress, desk, exists, has, now, paths, project, readYaml, required, runOutput, seat, words, yaml } from "./core";
|
|
5
5
|
import { type Task, terminal } from "./tasks";
|
|
6
6
|
import { resolveExactSessionAdoptionAuthorization } from "./adoption-authorizations";
|
|
7
7
|
import { readRuntimeState } from "./runtime-state";
|
|
@@ -102,9 +102,25 @@ export type HandoverReplacementRequest = {
|
|
|
102
102
|
authorization: { id: string; hash: string };
|
|
103
103
|
source: { pi_session: string; pi_session_path: string; herdr_session: string; pane: string };
|
|
104
104
|
target: { herdr_session: string; pane: string; cwd: string };
|
|
105
|
+
legacy_source?: {
|
|
106
|
+
authorization_hash: string;
|
|
107
|
+
pi_session: string;
|
|
108
|
+
pi_session_path: string;
|
|
109
|
+
herdr_session: string;
|
|
110
|
+
pane: string;
|
|
111
|
+
cwd: string;
|
|
112
|
+
agent: string;
|
|
113
|
+
verified_at: string;
|
|
114
|
+
};
|
|
105
115
|
created_at: string;
|
|
106
116
|
};
|
|
107
117
|
|
|
118
|
+
export type HandoverReplacementDependencies = {
|
|
119
|
+
command?: (command: string[]) => Promise<string>;
|
|
120
|
+
at?: () => string;
|
|
121
|
+
publish?: (file: string, content: string) => Promise<void>;
|
|
122
|
+
};
|
|
123
|
+
|
|
108
124
|
export const handoverReplacementPath = (root: string, address: string) => join(root, ".atdd-flow", "handover-replacements", `${encodeURIComponent(address)}.yaml`);
|
|
109
125
|
const nonEmpty = (value: string | undefined, flag: string) => required(value, flag).trim();
|
|
110
126
|
|
|
@@ -121,10 +137,27 @@ export async function isExactMainSeat(root: string, record: Seat, address: strin
|
|
|
121
137
|
return Boolean(config.repository && resolve(record.worktree) === resolve(config.repository));
|
|
122
138
|
}
|
|
123
139
|
|
|
124
|
-
|
|
140
|
+
function unboundLegacyRuntime(runtime: Runtime | undefined, pane: string) {
|
|
125
141
|
const locator = runtime?.addresses.herdr;
|
|
126
|
-
return
|
|
127
|
-
&& runtime
|
|
142
|
+
return runtime?.application === "herdr" && typeof locator === "string" && locator === pane
|
|
143
|
+
&& !runtime.pi_session && !runtime.pi_session_path && !runtime.launch_receipt;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function snapshotMatchesRequest(snapshot: NonNullable<HandoverReplacementRequest["legacy_source"]>, request: HandoverReplacementRequest) {
|
|
147
|
+
return snapshot.authorization_hash === request.authorization.hash && snapshot.pi_session === request.source.pi_session
|
|
148
|
+
&& snapshot.pi_session_path === request.source.pi_session_path && snapshot.herdr_session === request.source.herdr_session
|
|
149
|
+
&& snapshot.pane === request.source.pane && snapshot.cwd === request.target.cwd && Boolean(snapshot.agent) && Boolean(snapshot.verified_at);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function canonicalSourceMatchesRuntime(runtime: Runtime | undefined, source: HandoverReplacementRequest["source"]) {
|
|
153
|
+
const locator = runtime?.addresses.herdr;
|
|
154
|
+
return typeof locator !== "string" && locator?.session === source.herdr_session && locator.pane === source.pane
|
|
155
|
+
&& runtime?.pi_session === source.pi_session && runtime.pi_session_path === source.pi_session_path;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function replacementMatchesRuntime(runtime: Runtime | undefined, request: HandoverReplacementRequest) {
|
|
159
|
+
if (!request.legacy_source) return canonicalSourceMatchesRuntime(runtime, request.source);
|
|
160
|
+
return unboundLegacyRuntime(runtime, request.source.pane) && snapshotMatchesRequest(request.legacy_source, request);
|
|
128
161
|
}
|
|
129
162
|
|
|
130
163
|
export async function mainLaunchEligible(root: string, record: Seat, address: string, task: Task) {
|
|
@@ -172,48 +205,136 @@ async function exactMainTakeoverTask(root: string, address: string, id: string)
|
|
|
172
205
|
return { record };
|
|
173
206
|
}
|
|
174
207
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
208
|
+
type LegacyPaneObservation = { pane: string; agent: string; sessionPath: string; cwd: string };
|
|
209
|
+
const object = (value: unknown): Record<string, unknown> | undefined => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
210
|
+
const text = (value: unknown) => typeof value === "string" ? value : undefined;
|
|
211
|
+
function result(value: unknown) { const root = object(value); return object(root?.result) ?? root; }
|
|
212
|
+
function herdr(session: string, ...args: string[]) { return ["herdr", "--session", session, ...args]; }
|
|
213
|
+
|
|
214
|
+
function legacyPaneObservation(raw: string): LegacyPaneObservation {
|
|
215
|
+
const value = JSON.parse(raw), pane = object(result(value)?.pane) ?? result(value), session = object(pane?.agent_session);
|
|
216
|
+
const observation = { pane: text(pane?.pane_id), agent: text(pane?.agent), sessionPath: session?.kind === "path" ? text(session.value) : undefined, cwd: text(pane?.cwd) };
|
|
217
|
+
if (!observation.pane || !observation.agent || !observation.sessionPath || !observation.cwd) throw new Error("Legacy source bootstrap requires complete live Herdr pane/session/path/cwd evidence.");
|
|
218
|
+
return observation as LegacyPaneObservation;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function assertLegacyPane(observed: LegacyPaneObservation, source: HandoverReplacementRequest["source"], cwd: string) {
|
|
222
|
+
const match = basename(observed.sessionPath).match(/_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i);
|
|
223
|
+
if (observed.pane !== source.pane || resolve(observed.sessionPath) !== source.pi_session_path || match?.[1] !== source.pi_session || resolve(observed.cwd) !== cwd) {
|
|
224
|
+
throw new Error("Legacy source bootstrap live pane/session/path/cwd does not match the authorized source tuple.");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function legacyAgentName(raw: string, source: HandoverReplacementRequest["source"], cwd: string) {
|
|
229
|
+
const value = JSON.parse(raw), agent = object(result(value)?.agent) ?? result(value), session = object(agent?.agent_session);
|
|
230
|
+
const name = text(agent?.name), sessionPath = session?.kind === "path" ? text(session.value) : undefined, agentCwd = text(agent?.cwd);
|
|
231
|
+
if (!name || text(agent?.pane_id) !== source.pane || text(agent?.agent) !== "pi" || !sessionPath || resolve(sessionPath) !== source.pi_session_path
|
|
232
|
+
|| !agentCwd || resolve(agentCwd) !== cwd) throw new Error("Legacy source bootstrap Pi agent does not match the authorized source pane/session/path/cwd.");
|
|
233
|
+
return name;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function assertSinglePiWriter(raw: string) {
|
|
237
|
+
const value = JSON.parse(raw), report = object(result(value)?.process_info) ?? result(value);
|
|
238
|
+
const processes = Array.isArray(report?.foreground_processes) ? report.foreground_processes.map(object).filter(Boolean) as Record<string, unknown>[] : [];
|
|
239
|
+
const pi = processes.filter((process) => text(process.name) === "pi" || text(process.argv0)?.endsWith("/pi") || text(process.argv0) === "pi" || /(^|\/)pi(?:\s|$)/.test(text(process.cmdline) ?? ""));
|
|
240
|
+
if (pi.length !== 1) throw new Error("Legacy source bootstrap requires exactly one live Pi writer on the authorized source pane.");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function verifyLegacySource(source: HandoverReplacementRequest["source"], target: HandoverReplacementRequest["target"], authorizationHash: string, dependencies: HandoverReplacementDependencies) {
|
|
244
|
+
const command = dependencies.command ?? runOutput;
|
|
245
|
+
const pane = legacyPaneObservation(await command(herdr(source.herdr_session, "pane", "get", source.pane)));
|
|
246
|
+
assertLegacyPane(pane, source, target.cwd);
|
|
247
|
+
const [agentRaw, processRaw] = await Promise.all([
|
|
248
|
+
command(herdr(source.herdr_session, "agent", "get", source.pane)),
|
|
249
|
+
command(herdr(source.herdr_session, "pane", "process-info", "--pane", source.pane)),
|
|
250
|
+
]);
|
|
251
|
+
const agent = legacyAgentName(agentRaw, source, target.cwd);
|
|
252
|
+
assertSinglePiWriter(processRaw);
|
|
253
|
+
return { authorization_hash: authorizationHash, ...source, cwd: target.cwd, agent, verified_at: dependencies.at?.() ?? now() };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function assertLegacySeat(record: Seat, source: HandoverReplacementRequest["source"]) {
|
|
257
|
+
const runtime = record.runtime, locator = runtime?.addresses.herdr;
|
|
258
|
+
if (!runtime || runtime.application !== "herdr" || typeof locator !== "string" || locator !== source.pane
|
|
259
|
+
|| runtime.pi_session || runtime.pi_session_path || runtime.launch_receipt) {
|
|
260
|
+
throw new Error("Legacy source bootstrap requires the unchanged scalar Herdr placement without canonical runtime binding.");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function publishCreateOnly(file: string, content: string) {
|
|
265
|
+
await mkdir(dirname(file), { recursive: true });
|
|
266
|
+
const temporary = `${file}.${crypto.randomUUID()}.tmp`;
|
|
267
|
+
try {
|
|
268
|
+
await writeFile(temporary, content, { encoding: "utf8", flag: "wx" });
|
|
269
|
+
await link(temporary, file);
|
|
270
|
+
} finally { await rm(temporary, { force: true }); }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const replacementFlags = ["--legacy-source-bootstrap", "--handover", "--authorization", "--takeover-task", "--source-pi-session", "--source-pi-session-path", "--source-herdr-session", "--source-pane", "--herdr-session", "--target-pane", "--target-cwd"];
|
|
274
|
+
function validateReplacementArgs(args: string[]) {
|
|
275
|
+
if (args.some((arg) => arg.startsWith("--") && !replacementFlags.includes(arg))) throw new Error("Use `handover replace <main-seat> [--legacy-source-bootstrap] --handover <id> --authorization <operator-record> --takeover-task <id> --source-pi-session <id> --source-pi-session-path <jsonl> --source-herdr-session <session> --source-pane <pane> --herdr-session <session> --target-pane <pane> --target-cwd <canonical-main-worktree>`.");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function currentReplacementHandover(root: string, resolved: string, args: string[]) {
|
|
279
|
+
const handover = await recorded(root, resolved), handoverId = nonEmpty(words(args, "--handover"), "--handover");
|
|
185
280
|
if (!handover || handover.handover_id !== handoverId) throw new Error(`Path-A replacement requires the current validated handover ${handoverId} for ${resolved}.`);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
281
|
+
return handover;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function replacementSource(args: string[]) {
|
|
285
|
+
return {
|
|
190
286
|
pi_session: nonEmpty(words(args, "--source-pi-session"), "--source-pi-session"), pi_session_path: resolve(nonEmpty(words(args, "--source-pi-session-path"), "--source-pi-session-path")),
|
|
191
287
|
herdr_session: nonEmpty(words(args, "--source-herdr-session"), "--source-herdr-session"), pane: nonEmpty(words(args, "--source-pane"), "--source-pane"),
|
|
192
288
|
};
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const target = { herdr_session: nonEmpty(words(args, "--herdr-session"), "--herdr-session"), pane: nonEmpty(words(args, "--target-pane"), "--target-pane"), cwd
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function replacementTarget(args: string[], record: Seat, source: HandoverReplacementRequest["source"]) {
|
|
292
|
+
const cwd = resolve(nonEmpty(words(args, "--target-cwd"), "--target-cwd"));
|
|
293
|
+
if (cwd !== resolve(record.worktree)) throw new Error("Path-A replacement target CWD must be the exact canonical main worktree.");
|
|
294
|
+
const target = { herdr_session: nonEmpty(words(args, "--herdr-session"), "--herdr-session"), pane: nonEmpty(words(args, "--target-pane"), "--target-pane"), cwd };
|
|
199
295
|
if (target.herdr_session !== source.herdr_session || target.pane !== source.pane) throw new Error("Path-A replacement target must be the exact released source pane.");
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
296
|
+
return target;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function assertReplacementSource(record: Seat, source: HandoverReplacementRequest["source"], legacy: boolean) {
|
|
300
|
+
if (legacy) return assertLegacySeat(record, source);
|
|
301
|
+
if (!canonicalSourceMatchesRuntime(record.runtime, source)) throw new Error("Path-A replacement source tuple must exactly match the recorded live runtime.");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function prepareReplacement(root: string, resolved: string, args: string[]) {
|
|
305
|
+
validateReplacementArgs(args);
|
|
306
|
+
const legacy = args.includes("--legacy-source-bootstrap"), handover = await currentReplacementHandover(root, resolved, args);
|
|
307
|
+
const task = nonEmpty(words(args, "--takeover-task"), "--takeover-task");
|
|
308
|
+
const { record } = await exactMainTakeoverTask(root, resolved, task);
|
|
309
|
+
const source = replacementSource(args), target = replacementTarget(args, record, source);
|
|
310
|
+
assertReplacementSource(record, source, legacy);
|
|
311
|
+
return { legacy, handover, task, source, target };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function replacementRequest(resolved: string, intent: Awaited<ReturnType<typeof prepareReplacement>>, authorization: { id: string; hash: string }, legacySource: HandoverReplacementRequest["legacy_source"], dependencies: HandoverReplacementDependencies): HandoverReplacementRequest {
|
|
315
|
+
return {
|
|
316
|
+
schema: "atdd-flow/context-handover-replacement-request/v1", seat: resolved, task: intent.task,
|
|
317
|
+
handover: { id: intent.handover.handover_id, sha256: createHash("sha256").update(intent.handover.text).digest("hex") },
|
|
318
|
+
authorization, source: intent.source, target: intent.target,
|
|
319
|
+
...(legacySource ? { legacy_source: legacySource } : {}), created_at: dependencies.at?.() ?? now(),
|
|
213
320
|
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Creates the single immutable intent that an old Pi may observe before it voluntarily exits.
|
|
325
|
+
* It deliberately does not stop, bind, attach, project, or launch any process.
|
|
326
|
+
*/
|
|
327
|
+
export async function requestHandoverReplacement(root: string, address: string, args: string[], dependencies: HandoverReplacementDependencies = {}) {
|
|
328
|
+
const resolved = await canonicalAddress(root, address), intent = await prepareReplacement(root, resolved, args);
|
|
214
329
|
const file = handoverReplacementPath(root, resolved);
|
|
215
|
-
await
|
|
216
|
-
await
|
|
330
|
+
if (intent.legacy && await exists(file)) throw new Error(`Path-A replacement request already exists for ${resolved}.`);
|
|
331
|
+
const authorization = await resolveExactSessionAdoptionAuthorization(root, nonEmpty(words(args, "--authorization"), "--authorization"), {
|
|
332
|
+
seat: resolved, pi_session: intent.source.pi_session, pi_session_path: intent.source.pi_session_path,
|
|
333
|
+
source: { herdr_session: intent.source.herdr_session, pane: intent.source.pane }, target: intent.target,
|
|
334
|
+
});
|
|
335
|
+
const legacySource = intent.legacy ? await verifyLegacySource(intent.source, intent.target, authorization.hash, dependencies) : undefined;
|
|
336
|
+
const request = replacementRequest(resolved, intent, authorization, legacySource, dependencies);
|
|
337
|
+
await (dependencies.publish ?? publishCreateOnly)(file, yaml.print(request));
|
|
217
338
|
console.log(`Recorded Path-A replacement request for ${resolved}.`);
|
|
218
339
|
}
|
|
219
340
|
|