@akagilnc/pi-workflow-roles 0.1.5365 → 0.1.5368

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.
@@ -18,7 +18,7 @@ import {
18
18
  type SameTicketSummonsMaterials,
19
19
  } from "./run-lifecycle.ts";
20
20
  import { CliUsageError } from "./cli-errors.ts";
21
- import type { RoleTurnRequestProjectionOptions } from "./turn-request.ts";
21
+ import { projectPublicTurnAxes, type RoleTurnRequestProjectionOptions } from "./turn-request.ts";
22
22
  import {
23
23
  bindAdmittedTicketNumber,
24
24
  buildInstructionTransportPrompt,
@@ -119,6 +119,7 @@ import {
119
119
  persistReturnedRunState,
120
120
  runWithAutoResumeLoop,
121
121
  TurnDispatchedFailure,
122
+ writeHardenedArtifactFile,
122
123
  } from "./auto-resume.ts";
123
124
 
124
125
  function projectRelocatedTurnIdentity(
@@ -268,11 +269,10 @@ async function recordBestEffortPostDispatchDiagnostic<A extends AdmittedRoleInvo
268
269
  } catch (appendError) {
269
270
  try {
270
271
  const artifactsDir = await ensureRealArtifactsDirectory(admitted.runDirectory);
271
- await writeFile(
272
- join(artifactsDir, `post-admission-diagnostic-${randomUUID()}.json`),
273
- `${JSON.stringify({ version: 1, ...payload }, null, 2)}\n`,
274
- { encoding: "utf8", flag: "wx" },
275
- );
272
+ await writeHardenedArtifactFile(artifactsDir, "post-admission-diagnostic", {
273
+ version: 1,
274
+ ...payload,
275
+ });
276
276
  } catch (artifactError) {
277
277
  retentionFailure =
278
278
  `post-dispatch diagnostic durable retention failed on both channels (best-effort continue): dossier=${describeErrorIdentity(appendError)}; artifact=${describeErrorIdentity(artifactError)}`;
@@ -1318,13 +1318,8 @@ export function resumeTurnRequestProjectionOptions(
1318
1318
  prompt = "";
1319
1319
  }
1320
1320
  return {
1321
- packageRoot: env.packageRoot,
1322
- home: env.home,
1321
+ ...projectPublicTurnAxes(env),
1323
1322
  ...(env.host === undefined ? {} : { host: env.host }),
1324
- agentDir: env.agentDir,
1325
- ...(env.model === undefined ? {} : { model: env.model }),
1326
- ...pickEngineAxis(env),
1327
- ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
1328
1323
  ...(env.correlationId === undefined && admitted.correlationId === undefined
1329
1324
  ? {}
1330
1325
  : { correlationId: env.correlationId ?? admitted.correlationId }),
@@ -1345,13 +1340,8 @@ export function roleTurnOptions(
1345
1340
  ): RoleTurnRequestProjectionOptions {
1346
1341
  const correlationId = env.correlationId ?? admitted.correlationId;
1347
1342
  return {
1348
- packageRoot: env.packageRoot,
1349
- home: env.home,
1343
+ ...projectPublicTurnAxes(env),
1350
1344
  ...(env.host === undefined ? {} : { host: env.host }),
1351
- agentDir: env.agentDir,
1352
- ...(env.model === undefined ? {} : { model: env.model }),
1353
- ...pickEngineAxis(env),
1354
- ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
1355
1345
  ...(correlationId === undefined || correlationId.trim() === ""
1356
1346
  ? {}
1357
1347
  : { correlationId }),
@@ -1383,6 +1373,66 @@ async function dispatchAfterWriterLease<T>(input: {
1383
1373
  }
1384
1374
  }
1385
1375
 
1376
+ /**
1377
+ * One auto-resume dispatch: deferred run-state persist settles on the same
1378
+ * path for station-child and ordinary in-call retries. The caller owns how
1379
+ * the turn request is built and which correlation id the env carries.
1380
+ */
1381
+ async function runSettledAutoResumeLoop<
1382
+ A extends AdmittedRoleInvocation,
1383
+ T extends TerminalResult,
1384
+ P,
1385
+ >(input: {
1386
+ admitted: A;
1387
+ env: PostAdmissionEnv;
1388
+ io: CliIo;
1389
+ adapters: PostAdmissionAdapters<A, T>;
1390
+ effectiveEngine?: string;
1391
+ dispatchEnv?: PostAdmissionEnv;
1392
+ buildInitialPayload: () => P;
1393
+ buildResumePayload: () => P;
1394
+ toRequest: (payload: P) => Promise<RoleTurnRequest>;
1395
+ }): Promise<{ exitCode: number; admitted?: A; terminal?: T }> {
1396
+ const adapters = withOnceSuccessfulBeforeDispatch(input.adapters);
1397
+ const dispatchEnv = input.dispatchEnv ?? input.env;
1398
+ return runWithAutoResumeLoop({
1399
+ admitted: input.admitted,
1400
+ principalAuthority: input.env.principalAuthority,
1401
+ io: input.io,
1402
+ sessionAppender: input.env.sessionAppender,
1403
+ autoResumeLimit: input.env.autoResumeLimit,
1404
+ ...(input.env.signal === undefined ? {} : { signal: input.env.signal }),
1405
+ buildInitialPayload: input.buildInitialPayload,
1406
+ buildResumePayload: input.buildResumePayload,
1407
+ dispatch: async (payload, lease, _isFirst, attemptIo) =>
1408
+ dispatchAfterWriterLease({
1409
+ ...(lease === undefined ? {} : { lease }),
1410
+ build: () => input.toRequest(payload),
1411
+ dispatch: async (request) => {
1412
+ const result = await dispatchPostAdmissionTurn({
1413
+ admitted: input.admitted,
1414
+ env: dispatchEnv,
1415
+ io: attemptIo,
1416
+ request,
1417
+ ...(lease === undefined ? {} : { lease }),
1418
+ adapters,
1419
+ persistRunState: false,
1420
+ ...(input.effectiveEngine === undefined
1421
+ ? {}
1422
+ : { effectiveEngine: input.effectiveEngine }),
1423
+ });
1424
+ return settleDeferredPersist(
1425
+ input.admitted,
1426
+ input.env.principalAuthority,
1427
+ adapters,
1428
+ attemptIo,
1429
+ result,
1430
+ );
1431
+ },
1432
+ }),
1433
+ });
1434
+ }
1435
+
1386
1436
  function isAlreadyFrozenSummonsAttachment(
1387
1437
  runDirectory: string,
1388
1438
  attachmentPath: string,
@@ -1601,7 +1651,6 @@ export async function runPostAdmissionSeatResume<
1601
1651
  try {
1602
1652
  if (env.stationChild === true) {
1603
1653
  let firstTurn: RoleTurnRequest | undefined;
1604
- const stationAdapters = withOnceSuccessfulBeforeDispatch(adapters);
1605
1654
  type StationChildAttempt = { readonly resumeTurn: boolean };
1606
1655
  // One public call → one detour scope across in-place auto-resume dispatches.
1607
1656
  const invocationScopeId = mintEngineDetourInvocationScope({
@@ -1609,68 +1658,42 @@ export async function runPostAdmissionSeatResume<
1609
1658
  ? {}
1610
1659
  : { effectiveEngine: input.effectiveEngine }),
1611
1660
  });
1612
- return await runWithAutoResumeLoop({
1661
+ return await runSettledAutoResumeLoop({
1613
1662
  admitted: loaded.admitted,
1614
- principalAuthority: env.principalAuthority,
1663
+ env,
1615
1664
  io: input.io,
1616
- sessionAppender: env.sessionAppender,
1617
- autoResumeLimit: env.autoResumeLimit,
1618
- ...(env.signal === undefined ? {} : { signal: env.signal }),
1665
+ adapters,
1666
+ ...(input.effectiveEngine === undefined
1667
+ ? {}
1668
+ : { effectiveEngine: input.effectiveEngine }),
1619
1669
  buildInitialPayload: (): StationChildAttempt => ({ resumeTurn: false }),
1620
1670
  buildResumePayload: (): StationChildAttempt => ({ resumeTurn: true }),
1621
- // Same as public manual resume: prior-court sealed acceptance is not a
1622
- // redispatch brake (#833). New-court station-child turns still auto-resume.
1623
- dispatch: async (payload, lease, _isFirst, attemptIo) =>
1624
- dispatchAfterWriterLease({
1625
- ...(lease === undefined ? {} : { lease }),
1626
- build: async () => {
1627
- // #840 r8 判词 class 2: this call-local retry must keep this
1628
- // court's frozen summons / 交卷 body / attachments verbatim
1629
- // (same object as firstTurn) and project only the minimal
1630
- // host-needed resume trigger — never engine handbook material,
1631
- // which would replace a 审核循环 same-ticket continuation with a bare outsourcing
1632
- // 「重新读」 envelope (#755 contract, resumeTurnRequestProjectionOptions
1633
- // above). RESUME_TRANSPORT_ENVELOPE is the same package-owned,
1634
- // non-semantic trigger that projection already uses for a
1635
- // same-ticket summons carrying no instruction/attachments.
1636
- if (payload.resumeTurn && firstTurn !== undefined) {
1637
- return {
1638
- ...firstTurn,
1639
- continuation: {
1640
- kind: "resume",
1641
- prompt: RESUME_TRANSPORT_ENVELOPE,
1642
- },
1643
- };
1644
- }
1645
- const turnRequest = withEngineDetourInvocationScope(
1646
- await buildRequestAfterLease(),
1647
- invocationScopeId,
1648
- );
1649
- firstTurn = turnRequest;
1650
- return turnRequest;
1651
- },
1652
- dispatch: async (turnRequest) => {
1653
- const result = await dispatchPostAdmissionTurn({
1654
- admitted: loaded.admitted,
1655
- env,
1656
- io: attemptIo,
1657
- request: turnRequest,
1658
- ...(lease === undefined ? {} : { lease }),
1659
- adapters: stationAdapters,
1660
- persistRunState: false,
1661
- ...(input.effectiveEngine === undefined
1662
- ? {}
1663
- : { effectiveEngine: input.effectiveEngine }),
1664
- });
1665
- return settleDeferredPersist(
1666
- loaded.admitted,
1667
- env.principalAuthority,
1668
- stationAdapters,
1669
- attemptIo,
1670
- result,
1671
- );
1672
- },
1673
- }),
1671
+ toRequest: async (payload) => {
1672
+ // #840 r8 判词 class 2: this call-local retry must keep this
1673
+ // court's frozen summons / 交卷 body / attachments verbatim
1674
+ // (same object as firstTurn) and project only the minimal
1675
+ // host-needed resume trigger — never engine handbook material,
1676
+ // which would replace a 审核循环 same-ticket continuation with a bare outsourcing
1677
+ // 「重新读」 envelope (#755 contract, resumeTurnRequestProjectionOptions
1678
+ // above). RESUME_TRANSPORT_ENVELOPE is the same package-owned,
1679
+ // non-semantic trigger that projection already uses for a
1680
+ // same-ticket summons carrying no instruction/attachments.
1681
+ if (payload.resumeTurn && firstTurn !== undefined) {
1682
+ return {
1683
+ ...firstTurn,
1684
+ continuation: {
1685
+ kind: "resume",
1686
+ prompt: RESUME_TRANSPORT_ENVELOPE,
1687
+ },
1688
+ };
1689
+ }
1690
+ const turnRequest = withEngineDetourInvocationScope(
1691
+ await buildRequestAfterLease(),
1692
+ invocationScopeId,
1693
+ );
1694
+ firstTurn = turnRequest;
1695
+ return turnRequest;
1696
+ },
1674
1697
  });
1675
1698
  }
1676
1699
  const resumed = await runPostAdmissionManualResume({
@@ -1873,7 +1896,6 @@ export async function runPostAdmissionResumable<
1873
1896
  terminal?: T;
1874
1897
  }> {
1875
1898
  const { admitted, env, io, buildInitialRequest, buildResumeRequest, effectiveEngine } = input;
1876
- const adapters = withOnceSuccessfulBeforeDispatch(input.adapters);
1877
1899
 
1878
1900
  // One public call → one detour scope across in-place auto-resume dispatches.
1879
1901
  const invocationScopeId = mintEngineDetourInvocationScope({
@@ -1884,32 +1906,20 @@ export async function runPostAdmissionResumable<
1884
1906
  const buildScopedResume = (): RoleTurnRequest =>
1885
1907
  withEngineDetourInvocationScope(buildResumeRequest(), invocationScopeId);
1886
1908
 
1887
- return runWithAutoResumeLoop({
1909
+ return runSettledAutoResumeLoop({
1888
1910
  admitted,
1889
- principalAuthority: env.principalAuthority,
1911
+ env,
1890
1912
  io,
1891
- sessionAppender: env.sessionAppender,
1892
- autoResumeLimit: env.autoResumeLimit,
1893
- ...(env.signal === undefined ? {} : { signal: env.signal }),
1913
+ adapters: input.adapters,
1914
+ ...(effectiveEngine === undefined ? {} : { effectiveEngine }),
1915
+ // Ordinary in-call retry keeps the admitted correlation when one was stored.
1916
+ dispatchEnv: {
1917
+ ...env,
1918
+ ...(admitted.correlationId === undefined ? {} : { correlationId: admitted.correlationId }),
1919
+ },
1894
1920
  buildInitialPayload: buildScopedInitial,
1895
1921
  buildResumePayload: buildScopedResume,
1896
- dispatch: async (request, lease, _isFirst, attemptIo) => {
1897
- const result = await dispatchPostAdmissionTurn({
1898
- admitted,
1899
- env: {
1900
- ...env,
1901
- ...(admitted.correlationId === undefined ? {} : { correlationId: admitted.correlationId }),
1902
- },
1903
- io: attemptIo,
1904
- request,
1905
- ...(lease === undefined ? {} : { lease }),
1906
- adapters,
1907
- persistRunState: false,
1908
- // #600: every attempt (initial + auto-resume) writes seat engine when present.
1909
- ...(effectiveEngine === undefined ? {} : { effectiveEngine }),
1910
- });
1911
- return settleDeferredPersist(admitted, env.principalAuthority, adapters, attemptIo, result);
1912
- },
1922
+ toRequest: async (request) => request,
1913
1923
  });
1914
1924
  }
1915
1925
 
@@ -0,0 +1,72 @@
1
+ /**
2
+ * One seat-runtime assembly for the public CLI and nested summons.
3
+ * Order is host selection, the caller's argv parse, the missing-model fact,
4
+ * then host-facing provider projection. Callers own how a missing model is shown.
5
+ */
6
+ import { pickEngineAxis } from "../package-resources/engine-material.ts";
7
+ import type { DurablePrincipalAuthority, RoleTurnHost } from "../host-contracts.ts";
8
+ import {
9
+ resolvedSeatWithModel,
10
+ type EffectiveSeat,
11
+ type SeatModelConfig,
12
+ } from "./config.ts";
13
+ import {
14
+ loadHostProvidersTable,
15
+ projectHostFacingProvider,
16
+ } from "./host-providers.ts";
17
+ import {
18
+ composeRoleTurnHostAdapters,
19
+ selectRoleTurnHost,
20
+ type NamedRoleTurnHostAdapter,
21
+ type RoleTurnHostResolutionInput,
22
+ } from "./role-turn-host-resolution.ts";
23
+ import type { PublicCallableRole } from "./registry.ts";
24
+
25
+ export type OpenedRoleSeatRuntime =
26
+ | { readonly ok: false }
27
+ | {
28
+ readonly ok: true;
29
+ readonly roleTurnHost: RoleTurnHost;
30
+ readonly hostAdapters: readonly NamedRoleTurnHostAdapter[];
31
+ readonly model?: SeatModelConfig;
32
+ readonly engine?: string;
33
+ readonly engineModel?: string;
34
+ readonly host: string;
35
+ };
36
+
37
+ export function openRoleSeatRuntime(input: {
38
+ readonly resolution: RoleTurnHostResolutionInput;
39
+ readonly principalAuthority: DurablePrincipalAuthority;
40
+ readonly role: PublicCallableRole;
41
+ readonly seat: EffectiveSeat;
42
+ readonly home: string;
43
+ readonly afterHost?: () => void;
44
+ /** Already host-facing. A second projection would rename the provider again. */
45
+ readonly hostFacingModel?: SeatModelConfig;
46
+ }): OpenedRoleSeatRuntime {
47
+ const hostAdapters = composeRoleTurnHostAdapters(
48
+ input.resolution,
49
+ input.principalAuthority,
50
+ );
51
+ const roleTurnHost = selectRoleTurnHost(hostAdapters, {
52
+ role: input.role,
53
+ seat: input.seat,
54
+ });
55
+ input.afterHost?.();
56
+ const seatWithModel = resolvedSeatWithModel(input.seat);
57
+ if (seatWithModel === undefined) return { ok: false };
58
+ const projected = input.hostFacingModel ?? projectHostFacingProvider(
59
+ seatWithModel.selection,
60
+ seatWithModel.host,
61
+ loadHostProvidersTable(input.home),
62
+ input.home,
63
+ );
64
+ return {
65
+ ok: true,
66
+ roleTurnHost,
67
+ hostAdapters,
68
+ ...(projected === undefined ? {} : { model: projected }),
69
+ ...pickEngineAxis(seatWithModel),
70
+ host: seatWithModel.host,
71
+ };
72
+ }
@@ -49,6 +49,7 @@ import {
49
49
  type PackagedRole,
50
50
  } from "../packaged-role-registry.ts";
51
51
  import {
52
+ admittedReviewerBaseFault,
52
53
  recordEffectiveInvocationModel,
53
54
  requireAuthorityRef,
54
55
  isReviewerLens,
@@ -1586,25 +1587,7 @@ function seatLoadedResult<R extends AdmittedRoleInvocation>(
1586
1587
  export async function peekRoleRunRole(
1587
1588
  home: string,
1588
1589
  runId: string,
1589
- ): Promise<
1590
- | "judge"
1591
- | "coder"
1592
- | "fixer"
1593
- | "collector"
1594
- | "doctor"
1595
- | "reviewer"
1596
- | "merger"
1597
- | "notary"
1598
- | "countersign"
1599
- | "gleaner-left"
1600
- | "inspector"
1601
- | "gatekeeper"
1602
- | "navigator"
1603
- | "auditor"
1604
- | "diarist"
1605
- | "secretariat"
1606
- | undefined
1607
- > {
1590
+ ): Promise<PackagedRole | undefined> {
1608
1591
  const runDirectory = await findRunDirectoryById(home, runId);
1609
1592
  if (runDirectory === undefined) return undefined;
1610
1593
  const run = await readRoleRunIdentity(runDirectory);
@@ -1725,14 +1708,12 @@ function admitResumedRole(loaded: {
1725
1708
  }
1726
1709
  case "review-basis": {
1727
1710
  const rawBase = fields.baseRevision;
1728
- if (rawBase === undefined || rawBase.trim() === "") {
1729
- throw new CliUsageError(
1730
- `role run admitted reviewer base revision is missing: ${runId}`,
1731
- );
1732
- }
1733
- if (/\s/.test(rawBase) || rawBase.startsWith("-")) {
1711
+ const baseFault = admittedReviewerBaseFault(rawBase);
1712
+ if (baseFault !== undefined || rawBase === undefined) {
1734
1713
  throw new CliUsageError(
1735
- `role run admitted reviewer base revision is damaged: ${runId}`,
1714
+ baseFault === "damaged"
1715
+ ? `role run admitted reviewer base revision is damaged: ${runId}`
1716
+ : `role run admitted reviewer base revision is missing: ${runId}`,
1736
1717
  );
1737
1718
  }
1738
1719
  const lens = fields.lens;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import type { NoReceiptLifecycleFacts } from "../receipt-delivery-policy.ts";
9
9
  import type { ControlledFailureCause } from "../host-contracts.ts";
10
+ import type { PackagedRole } from "../packaged-role-registry.ts";
10
11
 
11
12
  export type { ControlledFailureCause } from "../host-contracts.ts";
12
13
 
@@ -22,23 +23,7 @@ export type TerminalArtifactRef = {
22
23
  };
23
24
 
24
25
  /** Public callable roles that currently produce Terminal outcomes. */
25
- export type TerminalRoleName =
26
- | "judge"
27
- | "coder"
28
- | "fixer"
29
- | "collector"
30
- | "doctor"
31
- | "reviewer"
32
- | "merger"
33
- | "notary"
34
- | "countersign"
35
- | "gleaner-left"
36
- | "inspector"
37
- | "gatekeeper"
38
- | "navigator"
39
- | "auditor"
40
- | "diarist"
41
- | "secretariat";
26
+ export type TerminalRoleName = PackagedRole;
42
27
 
43
28
  export type NoReceiptTerminalOutcome = NoReceiptLifecycleFacts & {
44
29
  kind: "no_receipt";
@@ -27,6 +27,32 @@ export type ResumeModelConfig = {
27
27
  readonly thinking?: PublicThinkingLevel;
28
28
  };
29
29
 
30
+ /**
31
+ * Env axes shared by every public turn projection.
32
+ * Callers add correlation, continuation, host, and station-only fields.
33
+ */
34
+ export function projectPublicTurnAxes(env: {
35
+ readonly packageRoot: string;
36
+ readonly home: string;
37
+ readonly agentDir: string;
38
+ readonly model?: SeatModelConfig;
39
+ readonly engine?: string;
40
+ readonly engineModel?: string;
41
+ readonly timeoutMs?: number;
42
+ }): Pick<
43
+ RoleTurnRequestProjectionOptions,
44
+ "packageRoot" | "home" | "agentDir" | "model" | "engine" | "engineModel" | "timeoutMs"
45
+ > {
46
+ return {
47
+ packageRoot: env.packageRoot,
48
+ home: env.home,
49
+ agentDir: env.agentDir,
50
+ ...(env.model === undefined ? {} : { model: env.model }),
51
+ ...pickEngineAxis(env),
52
+ ...(env.timeoutMs === undefined ? {} : { timeoutMs: env.timeoutMs }),
53
+ };
54
+ }
55
+
30
56
  export type RoleTurnRequestProjectionOptions = {
31
57
  packageRoot: string;
32
58
  home: string;
@@ -25,7 +25,6 @@ import type {
25
25
  } from "./host-contracts.ts";
26
26
  import type { TerminalResult } from "./public-cli/terminal.ts";
27
27
  import type { HostSelectionFailure, NamedRoleTurnHostAdapter } from "./public-cli/role-turn-host-resolution.ts";
28
- import { pickEngineAxis } from "./package-resources/engine-material.ts";
29
28
 
30
29
  /** Env published by the parent activation so nested summons never re-derive root. */
31
30
  export const AK_ROLE_PACKAGE_ROOT_ENV = "AK_ROLE_PACKAGE_ROOT" as const;
@@ -210,18 +209,6 @@ async function resolveSummonHome(options: PublicSummonRequest): Promise<string>
210
209
  return packageMachineHome();
211
210
  }
212
211
 
213
- /** Seat axes only — no parent-env fallback (#675 / #617 DK-3 / #883). */
214
- function projectSeatEngine(seat: EffectiveSeat): {
215
- engine?: string;
216
- engineModel?: string;
217
- } {
218
- return pickEngineAxis(seat);
219
- }
220
-
221
- function projectSeatHost(seat: EffectiveSeat): { host?: string } {
222
- return seat.host === undefined ? {} : { host: seat.host };
223
- }
224
-
225
212
  function hostSelectionFailureFromUnknown(error: unknown): HostSelectionFailure | undefined {
226
213
  if (typeof error !== "object" || error === null) return undefined;
227
214
  if ((error as { name?: unknown }).name !== "HostSelectionError") return undefined;
@@ -273,18 +260,17 @@ async function createSummonEnv(
273
260
  const [
274
261
  { piDurablePrincipalAuthority },
275
262
  { appendPiSessionCustomEntry },
276
- { resolveRoleTurnHost },
277
- { missingResolvedSeatModelMessage, resolvedSeatWithModel },
263
+ { openRoleSeatRuntime },
264
+ { missingResolvedSeatModelMessage },
278
265
  ] = await Promise.all([
279
266
  import("./pi/durable-principal.ts"),
280
267
  import("./pi/role-turn-host.ts"),
281
- import("./public-cli/role-turn-host-resolution.ts"),
268
+ import("./public-cli/role-seat-runtime.ts"),
282
269
  import("./public-cli/config.ts"),
283
270
  ]);
284
271
  const principalAuthority = options.principalAuthority ?? piDurablePrincipalAuthority;
285
- // Host first → argv (afterHost) → missing-model → provider projection (#617/#178/#840).
286
- const roleTurnHost = resolveRoleTurnHost(
287
- {
272
+ const opened = openRoleSeatRuntime({
273
+ resolution: {
288
274
  packageRoot: options.packageRoot,
289
275
  ...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
290
276
  ...(options.hostAdapters === undefined ? {} : { hostAdapters: options.hostAdapters }),
@@ -293,29 +279,19 @@ async function createSummonEnv(
293
279
  : { extraPiArgs: options.extraPiArgs }),
294
280
  ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
295
281
  },
296
- { role: options.role, seat: options.seat, principalAuthority },
297
- );
298
- afterHost?.();
299
- const seatWithModel = resolvedSeatWithModel(options.seat);
300
- if (seatWithModel === undefined) {
282
+ principalAuthority,
283
+ role: options.role,
284
+ seat: options.seat,
285
+ home: options.home,
286
+ ...(afterHost === undefined ? {} : { afterHost }),
287
+ ...(options.hostFacingModel === undefined ? {} : { hostFacingModel: options.hostFacingModel }),
288
+ });
289
+ if (!opened.ok) {
301
290
  return {
302
291
  ok: false,
303
292
  missingModelMessage: missingResolvedSeatModelMessage(options.role),
304
293
  };
305
294
  }
306
- const hostName = seatWithModel.host ?? "pi";
307
- let hostFacingSelection: RoleTurnModelConfig | undefined = options.hostFacingModel;
308
- if (hostFacingSelection === undefined) {
309
- const { loadHostProvidersTable, projectHostFacingProvider } = await import(
310
- "./public-cli/host-providers.ts"
311
- );
312
- hostFacingSelection = projectHostFacingProvider(
313
- seatWithModel.selection,
314
- hostName,
315
- loadHostProvidersTable(options.home),
316
- options.home,
317
- );
318
- }
319
295
  return {
320
296
  ok: true,
321
297
  env: {
@@ -324,12 +300,13 @@ async function createSummonEnv(
324
300
  agentDir: options.agentDir,
325
301
  sessionAppender: appendPiSessionCustomEntry,
326
302
  packageRoot: options.packageRoot,
327
- roleTurnHost,
303
+ roleTurnHost: opened.roleTurnHost,
328
304
  cwd: options.cwd,
329
305
  credentials: options.credentials,
330
- ...(hostFacingSelection === undefined ? {} : { model: hostFacingSelection }),
331
- ...projectSeatEngine(seatWithModel),
332
- ...projectSeatHost(seatWithModel),
306
+ ...(opened.model === undefined ? {} : { model: opened.model }),
307
+ ...(opened.engine === undefined ? {} : { engine: opened.engine }),
308
+ ...(opened.engineModel === undefined ? {} : { engineModel: opened.engineModel }),
309
+ host: opened.host,
333
310
  ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
334
311
  },
335
312
  };
@@ -344,16 +321,14 @@ export async function summonPublicRole(
344
321
  ): Promise<PublicSummonResult> {
345
322
  const packageRoot = resolveSummonsPackageRoot(options.packageRoot);
346
323
  const home = await resolveSummonHome(options);
347
- const agentDir =
348
- options.agentDir
349
- ?? process.env.PI_CODING_AGENT_DIR
350
- ?? join(home, ".pi", "agent");
351
324
  const {
352
325
  loadCredentialProviders,
353
326
  loadPublicCliConfig,
354
327
  resolveEffectiveSeat,
328
+ resolvePublicAgentDir,
355
329
  validatePublicCliConfigAxes,
356
330
  } = await import("./public-cli/config.ts");
331
+ const agentDir = resolvePublicAgentDir(options.agentDir, home);
357
332
  const credentials =
358
333
  options.credentials ?? (await loadCredentialProviders(agentDir));
359
334
  const config = await loadPublicCliConfig(home);
@@ -107,6 +107,11 @@ export function roleRunArtifactsDirectory(runDirectory: string): string {
107
107
  return join(runDirectory, "artifacts");
108
108
  }
109
109
 
110
+ /** Canonical unbound placement: a run directory under `<book>/unbound/runs/`. */
111
+ export function isUnboundRunDirectory(runDirectory: string): boolean {
112
+ return runDirectory.replaceAll("\\", "/").includes("/unbound/runs/");
113
+ }
114
+
110
115
  /** The placement seam owns creation for both new runs and resumed legacy runs. */
111
116
  export function ensureRoleRunDirectory(
112
117
  ledgerHome: string,