@akagilnc/pi-workflow-roles 0.1.4745 → 0.1.4772
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/dist/acp-host/production-host.js +278 -590
- package/dist/headless-host/production-host.js +284 -596
- package/dist/migrate-book-topology.js +1255 -1087
- package/dist/notary-source-run.js +12 -2
- package/dist/public-cli/invocation.js +11 -8
- package/dist/public-cli/main.js +250 -563
- package/dist/public-cli/post-admission.js +96 -131
- package/dist/public-cli/run-lifecycle.js +25 -4
- package/dist/role-run-relocation.js +19 -18
- package/package.json +1 -1
- package/src/book-topology-migration-placement.ts +9 -1
- package/src/book-topology-runs-migrator.ts +156 -4
- package/src/notary-source-run.ts +18 -2
- package/src/public-cli/cli.ts +0 -5
- package/src/public-cli/config.ts +0 -1
- package/src/public-cli/countersign-run.ts +1 -1
- package/src/public-cli/diarist-run.ts +1 -1
- package/src/public-cli/invocation.ts +13 -9
- package/src/public-cli/post-admission.ts +124 -187
- package/src/public-cli/run-lifecycle.ts +42 -4
- package/src/role-run-relocation.ts +27 -30
- package/dist/public-cli/public-run-credentials.js +0 -49
- package/src/public-cli/public-run-credentials.ts +0 -71
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* #865 T9: migrate retained runs into ticket-scoped or unbound placement.
|
|
3
3
|
* Inventory covers legacy flat `runs/`, existing `<ticket>/runs/`, and
|
|
4
4
|
* `unbound/runs/`. Attribution reuses the shared migrating-run helper;
|
|
5
|
-
* already-canonical trees copy as-is.
|
|
5
|
+
* already-canonical ticket trees copy as-is. Unbound leaves that already
|
|
6
|
+
* hold a board typed ticketNumber place under that ticket (#863 stock).
|
|
6
7
|
*/
|
|
7
|
-
import { cp, mkdir, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { cp, mkdir, rename, readdir, stat, writeFile } from "node:fs/promises";
|
|
8
9
|
import { basename, dirname, join, relative, sep } from "node:path";
|
|
9
10
|
|
|
10
11
|
import {
|
|
@@ -16,16 +17,21 @@ import {
|
|
|
16
17
|
resolveMigratingRunTicket,
|
|
17
18
|
} from "./book-topology-migration-placement.ts";
|
|
18
19
|
import {
|
|
20
|
+
assertBookTopologyMigrationPrerequisites,
|
|
19
21
|
reconcileMigrationPartition,
|
|
20
22
|
type BookTopologyMigrationContext,
|
|
21
23
|
type BookTopologyPartitionMigrator,
|
|
22
24
|
type MigrationItemOutcome,
|
|
23
25
|
} from "./book-topology-migration.ts";
|
|
26
|
+
import { listBookRunDirectories } from "./role-run-placement.ts";
|
|
24
27
|
import {
|
|
25
28
|
rewriteRoleRunDurablePages,
|
|
26
29
|
type RunDirectoryPathRewrite,
|
|
27
30
|
} from "./role-run-relocation.ts";
|
|
28
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
MIGRATION_TICKET_DERIVATION_PAGE,
|
|
33
|
+
readBoardTicketNumber,
|
|
34
|
+
} from "./run-ticket-number.ts";
|
|
29
35
|
|
|
30
36
|
const RUNS_PARTITION = "runs";
|
|
31
37
|
|
|
@@ -141,9 +147,38 @@ async function planBookMoves(
|
|
|
141
147
|
historicalRunDirectory = join(booksDirectory, bookKey, leaf.relativePath);
|
|
142
148
|
}
|
|
143
149
|
derivation = undefined;
|
|
150
|
+
} else if (leaf.layout === "unbound") {
|
|
151
|
+
// #863: board typed ticket on an unbound leaf → place under that ticket.
|
|
152
|
+
// No board ticket → stay unbound. Never invent from prose or derivation.
|
|
153
|
+
const boardTicket = leaf.isDirectory
|
|
154
|
+
? await readBoardTicketNumber(leaf.sourcePath)
|
|
155
|
+
: undefined;
|
|
156
|
+
if (parsed !== undefined && boardTicket !== undefined) {
|
|
157
|
+
targetPath = destinationRunDirectory(
|
|
158
|
+
booksDirectory,
|
|
159
|
+
bookKey,
|
|
160
|
+
boardTicket,
|
|
161
|
+
parsed.runId,
|
|
162
|
+
parsed.role,
|
|
163
|
+
);
|
|
164
|
+
disposition = "placed";
|
|
165
|
+
historicalRunDirectory = join(
|
|
166
|
+
booksDirectory,
|
|
167
|
+
bookKey,
|
|
168
|
+
"unbound",
|
|
169
|
+
"runs",
|
|
170
|
+
leaf.leafName,
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
targetPath = join(booksDirectory, bookKey, leaf.relativePath);
|
|
174
|
+
disposition = "unbound";
|
|
175
|
+
historicalRunDirectory = undefined;
|
|
176
|
+
}
|
|
177
|
+
derivation = undefined;
|
|
144
178
|
} else if (leaf.layout !== "flat") {
|
|
179
|
+
// Already-canonical `<ticket>/runs/` — copy as-is.
|
|
145
180
|
targetPath = join(booksDirectory, bookKey, leaf.relativePath);
|
|
146
|
-
disposition =
|
|
181
|
+
disposition = "placed";
|
|
147
182
|
historicalRunDirectory = undefined;
|
|
148
183
|
derivation = undefined;
|
|
149
184
|
} else if (parsed === undefined) {
|
|
@@ -242,3 +277,120 @@ export const bookTopologyRunsMigrator: BookTopologyPartitionMigrator = {
|
|
|
242
277
|
return reconcileMigrationPartition(RUNS_PARTITION, "entries", outcomes);
|
|
243
278
|
},
|
|
244
279
|
};
|
|
280
|
+
|
|
281
|
+
export type BoardBoundUnboundRelocation = {
|
|
282
|
+
readonly from: string;
|
|
283
|
+
readonly to: string;
|
|
284
|
+
readonly ticketNumber: number;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
type PlannedBoardBoundMove = {
|
|
288
|
+
readonly sourcePath: string;
|
|
289
|
+
readonly targetPath: string;
|
|
290
|
+
readonly ticketNumber: number;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Live in-place repair for already-migrated trees (#863 stock): plan the full
|
|
295
|
+
* board-bound unbound→ticket batch first, refuse any overwrite before mutation,
|
|
296
|
+
* rewrite durable pages (including batch `crossRunRewrites`) while sources still
|
|
297
|
+
* sit at unbound, then rename. Parse/write failures therefore leave sources in
|
|
298
|
+
* place so a retry can finish the same closure; rename-only retries still find
|
|
299
|
+
* remaining unbound sources. Leaves without a board ticket stay unbound.
|
|
300
|
+
*/
|
|
301
|
+
export async function relocateBoardBoundUnboundRunsInBook(
|
|
302
|
+
bookDirectory: string,
|
|
303
|
+
): Promise<readonly BoardBoundUnboundRelocation[]> {
|
|
304
|
+
const bookKey = basename(bookDirectory);
|
|
305
|
+
const booksDirectory = dirname(bookDirectory);
|
|
306
|
+
const unboundRuns = join(bookDirectory, "unbound", "runs");
|
|
307
|
+
let entries;
|
|
308
|
+
try {
|
|
309
|
+
entries = await readdir(unboundRuns, { withFileTypes: true });
|
|
310
|
+
} catch (error) {
|
|
311
|
+
if (isMigrationEnoent(error)) return [];
|
|
312
|
+
throw error;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const planned: PlannedBoardBoundMove[] = [];
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
if (!entry.isDirectory()) continue;
|
|
318
|
+
const parsed = parseRunLeaf(entry.name);
|
|
319
|
+
if (parsed === undefined) continue;
|
|
320
|
+
const sourcePath = join(unboundRuns, entry.name);
|
|
321
|
+
const boardTicket = await readBoardTicketNumber(sourcePath);
|
|
322
|
+
if (boardTicket === undefined) continue;
|
|
323
|
+
const targetPath = destinationRunDirectory(
|
|
324
|
+
booksDirectory,
|
|
325
|
+
bookKey,
|
|
326
|
+
boardTicket,
|
|
327
|
+
parsed.runId,
|
|
328
|
+
parsed.role,
|
|
329
|
+
);
|
|
330
|
+
planned.push({
|
|
331
|
+
sourcePath,
|
|
332
|
+
targetPath,
|
|
333
|
+
ticketNumber: boardTicket,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (planned.length === 0) return [];
|
|
338
|
+
|
|
339
|
+
for (const move of planned) {
|
|
340
|
+
if (await pathExists(move.targetPath)) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`board-bound unbound relocate refuses to overwrite ${move.targetPath} with ${move.sourcePath}`,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const crossRunRewrites: RunDirectoryPathRewrite[] = planned.map((move) => ({
|
|
348
|
+
oldRunDirectory: move.sourcePath,
|
|
349
|
+
newRunDirectory: move.targetPath,
|
|
350
|
+
}));
|
|
351
|
+
const relocatedBySource = new Map(
|
|
352
|
+
planned.map((move) => [move.sourcePath, move] as const),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// Rewrite before any rename so durable-page failures remain retryable from
|
|
356
|
+
// the same unbound sources (failure-honesty: no relocatedCount=0 whitewash).
|
|
357
|
+
for (const runDir of await listBookRunDirectories(bookDirectory)) {
|
|
358
|
+
const move = relocatedBySource.get(runDir);
|
|
359
|
+
await rewriteRoleRunDurablePages({
|
|
360
|
+
pagesDirectory: runDir,
|
|
361
|
+
oldRunDirectory: move?.sourcePath ?? runDir,
|
|
362
|
+
newRunDirectory: move?.targetPath ?? runDir,
|
|
363
|
+
crossRunRewrites,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
for (const move of planned) {
|
|
368
|
+
await mkdir(dirname(move.targetPath), { recursive: true });
|
|
369
|
+
await rename(move.sourcePath, move.targetPath);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return planned.map((move) => ({
|
|
373
|
+
from: move.sourcePath,
|
|
374
|
+
to: move.targetPath,
|
|
375
|
+
ticketNumber: move.ticketNumber,
|
|
376
|
+
}));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Walk every book under `booksDirectory` and relocate board-bound unbound
|
|
381
|
+
* runs in place. Requires zero live writer locks (same gate as topology migrate).
|
|
382
|
+
*/
|
|
383
|
+
export async function relocateBoardBoundUnboundRunsInBooks(
|
|
384
|
+
booksDirectory: string,
|
|
385
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
386
|
+
): Promise<readonly BoardBoundUnboundRelocation[]> {
|
|
387
|
+
await assertBookTopologyMigrationPrerequisites(booksDirectory, env);
|
|
388
|
+
const relocated: BoardBoundUnboundRelocation[] = [];
|
|
389
|
+
for (const bookKey of await listMigrationBookKeys(booksDirectory)) {
|
|
390
|
+
const batch = await relocateBoardBoundUnboundRunsInBook(
|
|
391
|
+
join(booksDirectory, bookKey),
|
|
392
|
+
);
|
|
393
|
+
relocated.push(...batch);
|
|
394
|
+
}
|
|
395
|
+
return relocated;
|
|
396
|
+
}
|
package/src/notary-source-run.ts
CHANGED
|
@@ -87,7 +87,8 @@ export async function resolveNotarySourceRunLocator(options: {
|
|
|
87
87
|
|
|
88
88
|
const ledgerHome = resolveActivationLedgerHome(options.home);
|
|
89
89
|
const bookKey = resolveBookKeyFromGit(options.projectRoot);
|
|
90
|
-
const
|
|
90
|
+
const bookDirectory = activationBookDirectory(ledgerHome, bookKey);
|
|
91
|
+
const bookRunsRoot = join(bookDirectory, "runs");
|
|
91
92
|
|
|
92
93
|
let candidate: string;
|
|
93
94
|
const bare = parseRunDirectoryName(raw);
|
|
@@ -96,12 +97,27 @@ export async function resolveNotarySourceRunLocator(options: {
|
|
|
96
97
|
?? join(bookRunsRoot, `${bare.runId}@${bare.role}`);
|
|
97
98
|
} else {
|
|
98
99
|
candidate = isAbsolute(raw) ? raw : resolve(options.projectRoot, raw);
|
|
100
|
+
const identity = parseRunDirectoryName(basename(candidate));
|
|
101
|
+
const subjectDirectory = dirname(dirname(candidate));
|
|
102
|
+
const isLegacyUnboundLocator =
|
|
103
|
+
identity !== undefined &&
|
|
104
|
+
basename(dirname(candidate)) === "runs" &&
|
|
105
|
+
basename(subjectDirectory) === "unbound" &&
|
|
106
|
+
resolve(dirname(subjectDirectory)) === resolve(bookDirectory);
|
|
107
|
+
if (isLegacyUnboundLocator) {
|
|
108
|
+
candidate = (await findRunDirectoryById(
|
|
109
|
+
options.home,
|
|
110
|
+
identity.runId,
|
|
111
|
+
bookKey,
|
|
112
|
+
identity.role,
|
|
113
|
+
)) ?? candidate;
|
|
114
|
+
}
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
const real = await requireRunDirectory(candidate, raw);
|
|
102
118
|
const identity = parseRunDirectoryName(basename(real))!;
|
|
103
119
|
|
|
104
|
-
const bookIdentity = physicalPathIdentity(
|
|
120
|
+
const bookIdentity = physicalPathIdentity(bookDirectory);
|
|
105
121
|
const parentIdentity = physicalPathIdentity(dirname(real));
|
|
106
122
|
const subjectBookIdentity = physicalPathIdentity(dirname(dirname(dirname(real))));
|
|
107
123
|
if (
|
package/src/public-cli/cli.ts
CHANGED
|
@@ -124,7 +124,6 @@ const RESUME_SEAT_DISPATCH: Record<
|
|
|
124
124
|
) => Promise<{
|
|
125
125
|
exitCode: number;
|
|
126
126
|
terminal?: TerminalResult;
|
|
127
|
-
staleWriterLeaseReclaimed?: true;
|
|
128
127
|
}>;
|
|
129
128
|
}
|
|
130
129
|
> = {
|
|
@@ -473,19 +472,15 @@ export type CliResult = {
|
|
|
473
472
|
/** Settled Terminal when an admitted Role run produced one (programmatic/tests). */
|
|
474
473
|
terminal?: TerminalResult;
|
|
475
474
|
hostFailure?: HostSelectionFailure;
|
|
476
|
-
/** Typed #556 fact from public resume/dispatch when a dead holder lock was unlinked. */
|
|
477
|
-
staleWriterLeaseReclaimed?: true;
|
|
478
475
|
};
|
|
479
476
|
|
|
480
477
|
function cliResultFromRoleRun(result: {
|
|
481
478
|
exitCode: number;
|
|
482
479
|
terminal?: TerminalResult;
|
|
483
|
-
staleWriterLeaseReclaimed?: true;
|
|
484
480
|
}): CliResult {
|
|
485
481
|
return {
|
|
486
482
|
exitCode: result.exitCode,
|
|
487
483
|
...(result.terminal === undefined ? {} : { terminal: result.terminal }),
|
|
488
|
-
...(result.staleWriterLeaseReclaimed === true ? { staleWriterLeaseReclaimed: true as const } : {}),
|
|
489
484
|
};
|
|
490
485
|
}
|
|
491
486
|
|
package/src/public-cli/config.ts
CHANGED
|
@@ -813,7 +813,6 @@ export function listRolesForDisplay(
|
|
|
813
813
|
|
|
814
814
|
/**
|
|
815
815
|
* Read configured credential presence from a Pi auth.json document.
|
|
816
|
-
* Presence drives the selected-provider fail-closed seam (public-run-credentials).
|
|
817
816
|
*/
|
|
818
817
|
export function credentialProvidersFromAuthData(
|
|
819
818
|
data: unknown,
|
|
@@ -785,7 +785,7 @@ export async function runPublicCountersign(
|
|
|
785
785
|
function countersignAdapters(options?: {
|
|
786
786
|
beforeDispatch?: (
|
|
787
787
|
admitted: AdmittedCountersignInvocation,
|
|
788
|
-
lease
|
|
788
|
+
lease?: RunWriterLease,
|
|
789
789
|
) => void | Promise<void>;
|
|
790
790
|
}) {
|
|
791
791
|
return {
|
|
@@ -76,7 +76,7 @@ export function buildDiaristTurnRequest(
|
|
|
76
76
|
async function bindAndRelocateDiaristIfBoardBound(
|
|
77
77
|
admitted: AdmittedDiaristInvocation,
|
|
78
78
|
authority: DiaristRunEnv["principalAuthority"],
|
|
79
|
-
lease
|
|
79
|
+
lease?: RunWriterLease,
|
|
80
80
|
): Promise<void> {
|
|
81
81
|
const boardTicket = await readBoardTicketNumber(admitted.runDirectory);
|
|
82
82
|
if (boardTicket === undefined) return;
|
|
@@ -41,7 +41,6 @@ import {
|
|
|
41
41
|
requireSafePositiveTicketNumber,
|
|
42
42
|
} from "../run-ticket-number.ts";
|
|
43
43
|
import {
|
|
44
|
-
rewriteRoleRunDurablePages,
|
|
45
44
|
rewriteRunDirectoryPathFields,
|
|
46
45
|
rewriteRunDirectoryPathValue,
|
|
47
46
|
} from "../role-run-relocation.ts";
|
|
@@ -628,8 +627,8 @@ export async function relocateAdmittedRunToTicket(
|
|
|
628
627
|
admitted: AdmittedRoleInvocation,
|
|
629
628
|
authority: DurablePrincipalAuthority,
|
|
630
629
|
heldLease?: { relocate(runDirectory: string): void },
|
|
631
|
-
): Promise<
|
|
632
|
-
if (admitted.ticketNumber === undefined || !admitted.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) return;
|
|
630
|
+
): Promise<{ oldRunDirectory: string; newRunDirectory: string } | undefined> {
|
|
631
|
+
if (admitted.ticketNumber === undefined || !admitted.runDirectory.includes(`${sep}unbound${sep}runs${sep}`)) return undefined;
|
|
633
632
|
const oldRunDirectory = admitted.runDirectory;
|
|
634
633
|
const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(oldRunDirectory));
|
|
635
634
|
const target = roleRunPlacement(ledgerHome, {
|
|
@@ -639,7 +638,17 @@ export async function relocateAdmittedRunToTicket(
|
|
|
639
638
|
role: admitted.role,
|
|
640
639
|
});
|
|
641
640
|
ensureRoleRunDirectory(ledgerHome, dirname(target.runDirectory));
|
|
641
|
+
// Host sealing is pure identity projection, but may reject the coordinates.
|
|
642
|
+
// Keep that failure before the filesystem commit point.
|
|
643
|
+
const principal = authority.seal(target);
|
|
644
|
+
|
|
645
|
+
// Rename is the only durable commit. Persisted paths are resolved from typed
|
|
646
|
+
// run identity on read, so online relocation never writes unleased peers or
|
|
647
|
+
// pretends a directory rename plus page rewrites form one transaction.
|
|
642
648
|
await rename(oldRunDirectory, target.runDirectory);
|
|
649
|
+
|
|
650
|
+
// rename moved the open lock inode with the directory. Transfer cleanup
|
|
651
|
+
// ownership immediately after the commit.
|
|
643
652
|
heldLease?.relocate(target.runDirectory);
|
|
644
653
|
|
|
645
654
|
const admittedRecord = admitted as unknown as Record<string, unknown>;
|
|
@@ -664,14 +673,9 @@ export async function relocateAdmittedRunToTicket(
|
|
|
664
673
|
target.runDirectory,
|
|
665
674
|
) as string;
|
|
666
675
|
}
|
|
667
|
-
const principal = authority.seal(target);
|
|
668
676
|
(admitted as { principal: DurablePrincipal }).principal = principal;
|
|
669
677
|
|
|
670
|
-
|
|
671
|
-
pagesDirectory: target.runDirectory,
|
|
672
|
-
oldRunDirectory,
|
|
673
|
-
newRunDirectory: target.runDirectory,
|
|
674
|
-
});
|
|
678
|
+
return { oldRunDirectory, newRunDirectory: target.runDirectory };
|
|
675
679
|
}
|
|
676
680
|
|
|
677
681
|
/**
|