@akagilnc/pi-workflow-roles 0.1.4782 → 0.1.4802

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.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +104 -215
  4. package/dist/headless-host/production-host.js +149 -229
  5. package/dist/migrate-book-topology.js +1 -1
  6. package/dist/pi/role-turn-host.js +1 -1
  7. package/dist/public-cli/auto-resume.js +9 -15
  8. package/dist/public-cli/countersign-run.js +75 -87
  9. package/dist/public-cli/diarist-run.js +6 -26
  10. package/dist/public-cli/inspector-run.js +1 -1
  11. package/dist/public-cli/main.js +64 -177
  12. package/dist/public-cli/notary-run.js +0 -3
  13. package/dist/public-cli/option-definitions.js +1 -1
  14. package/dist/public-cli/post-admission.js +35 -65
  15. package/dist/public-cli/reviewer-run.js +1 -1
  16. package/dist/public-cli/run-lifecycle.js +14 -46
  17. package/dist/public-cli/seat-ticket-binding.js +11 -25
  18. package/dist/public-role-summons.js +2 -0
  19. package/package.json +1 -1
  20. package/souls/coder.md +1 -2
  21. package/src/headless-host/role-turn-host.ts +49 -12
  22. package/src/pi/role-turn-host.ts +1 -1
  23. package/src/public-cli/auto-resume.ts +19 -18
  24. package/src/public-cli/countersign-run.ts +76 -129
  25. package/src/public-cli/diarist-run.ts +6 -33
  26. package/src/public-cli/inspector-run.ts +1 -1
  27. package/src/public-cli/invocation.ts +5 -0
  28. package/src/public-cli/notary-run.ts +3 -7
  29. package/src/public-cli/option-definitions.ts +1 -1
  30. package/src/public-cli/post-admission.ts +37 -69
  31. package/src/public-cli/reviewer-run.ts +1 -1
  32. package/src/public-cli/run-lifecycle.ts +18 -66
  33. package/src/public-cli/seat-ticket-binding.ts +12 -28
  34. package/src/public-role-summons.ts +15 -6
  35. package/src/role-runtime.ts +9 -0
@@ -11,7 +11,6 @@ import { isAbsolute, join, resolve } from "node:path";
11
11
 
12
12
  import {
13
13
  buildAutoResumeContinuationPrompt,
14
- buildResumeContinuationPrompt,
15
14
  RESUME_TRANSPORT_ENVELOPE,
16
15
  type PublicResumeRequest,
17
16
  type SameTicketSummonsMaterials,
@@ -1103,15 +1102,12 @@ export async function dispatchPostAdmissionTurn<
1103
1102
  // Settlement already sealed accepted — a cleanup failure here must
1104
1103
  // not erase that fact or make the caller replay this court's
1105
1104
  // summons over already-delivered work (#840 已交劳动只整理终局不重做).
1106
- // A later bare resume self-heals: buildRequestAfterLease finds the
1107
- // open court already sealed and clears it then (documented
1108
- // continue-under-failure contract, not a swallow — 失败诚实宪法 真因
1109
- // 必须落痕) — durably, since every auto-resume attempt's io here is
1110
- // dummyIo (#840 r9 判词 class 1).
1105
+ // Preserve the cleanup failure as a post-dispatch diagnostic; the
1106
+ // accepted settlement remains authoritative (#840 r9 判词 class 1).
1111
1107
  await recordBestEffortPostDispatchDiagnostic(
1112
1108
  admitted,
1113
1109
  env,
1114
- `current-court cleanup failed after accepted settlement (best-effort continue, self-heals on next resume): ${describeErrorIdentity(error)}`,
1110
+ `current-court cleanup failed after accepted settlement (best-effort continue): ${describeErrorIdentity(error)}`,
1115
1111
  io,
1116
1112
  );
1117
1113
  }
@@ -1310,8 +1306,7 @@ export async function dispatchPostAdmissionTurn<
1310
1306
  /**
1311
1307
  * Shared resume continuation projection (#471 / #600 / #633 / #637 / #755 / #879):
1312
1308
  * seat-table model/engine/timeout axes, restored correlation, and either
1313
- * - manual resume (no same-ticket summons): package envelope / optional caller
1314
- * message, with engine-axis handbook via buildResumeContinuationPrompt, or
1309
+ * - manual resume (no same-ticket summons): caller message bytes, or
1315
1310
  * - same-ticket summons (审核循环续话): caller/peer words + optional frozen
1316
1311
  * attachment paths only — no「请重读」、no code-authored content substitute,
1317
1312
  * no engine handbook packaging (#750/#755/#879).
@@ -1351,12 +1346,7 @@ export function resumeTurnRequestProjectionOptions(
1351
1346
  // #755: same-ticket summons without prepared materials — caller words only.
1352
1347
  prompt = request.message;
1353
1348
  } else {
1354
- // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
1355
- prompt = buildResumeContinuationPrompt({
1356
- packageRoot: env.packageRoot,
1357
- ...pickEngineAxis(env),
1358
- message: request.message,
1359
- });
1349
+ prompt = request.message;
1360
1350
  }
1361
1351
  } else if (summonsPrepared !== undefined) {
1362
1352
  // #879 station-child officer: instruction bytes === peer body/reask (no wrap).
@@ -1369,11 +1359,7 @@ export function resumeTurnRequestProjectionOptions(
1369
1359
  // only). Pointer is activation/sourceRun material — not dialogue content.
1370
1360
  prompt = "";
1371
1361
  } else {
1372
- // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
1373
- prompt = buildResumeContinuationPrompt({
1374
- packageRoot: env.packageRoot,
1375
- ...pickEngineAxis(env),
1376
- });
1362
+ prompt = "";
1377
1363
  }
1378
1364
  return {
1379
1365
  packageRoot: env.packageRoot,
@@ -1395,12 +1381,13 @@ export function resumeTurnRequestProjectionOptions(
1395
1381
  }
1396
1382
 
1397
1383
  /**
1398
- * Hold the writer lease through after-lease build, then hand off to dispatch.
1399
- * Builder (or any throw before dispatch) must release here — dispatch's finally
1400
- * only runs after this handoff (manual resume and station-child auto-resume).
1384
+ * Build the turn request, then hand off to dispatch. When a writer lease is
1385
+ * held, release it if build throws before handoff — dispatch's finally only
1386
+ * runs after this handoff (manual resume and station-child auto-resume).
1387
+ * #987: lease is optional; auto-resume no longer pre-acquires before host CLI.
1401
1388
  */
1402
1389
  async function dispatchAfterWriterLease<T>(input: {
1403
- lease: RunWriterLease;
1390
+ lease?: RunWriterLease;
1404
1391
  build: () => Promise<RoleTurnRequest>;
1405
1392
  dispatch: (request: RoleTurnRequest) => Promise<T>;
1406
1393
  }): Promise<T> {
@@ -1410,7 +1397,7 @@ async function dispatchAfterWriterLease<T>(input: {
1410
1397
  handedOffToDispatch = true;
1411
1398
  return await input.dispatch(request);
1412
1399
  } finally {
1413
- if (!handedOffToDispatch) {
1400
+ if (!handedOffToDispatch && input.lease !== undefined) {
1414
1401
  await input.lease.release();
1415
1402
  }
1416
1403
  }
@@ -1430,7 +1417,7 @@ function isAlreadyFrozenSummonsAttachment(
1430
1417
  * Freeze same-ticket summons attachments into the retained run directory (#637).
1431
1418
  * No-op materials (no paths / instruction-only) skip the freeze.
1432
1419
  * Paths already under this run's attachments/ are the accepted freeze identity —
1433
- * reuse them; do not re-freeze from external originals on bare resume.
1420
+ * reuse them for the same internal re-summons flow.
1434
1421
  * Manual resume never calls this — old attachment semantics stay intact.
1435
1422
  */
1436
1423
  export async function prepareSummonsResumeMaterials(
@@ -1464,16 +1451,15 @@ export async function prepareSummonsResumeMaterials(
1464
1451
  }
1465
1452
 
1466
1453
  /**
1467
- * Shared manual-resume orchestration for seats whose continuation is the
1468
- * package resume envelope (#599 / #633): load once → structural rejection →
1454
+ * Shared manual-resume orchestration for seats (#599 / #633):
1455
+ * load once → structural rejection →
1469
1456
  * optional seat afterAdmittedLoad (method material / controlled failure) →
1470
1457
  * seat turn projection → station-child auto-resume or public manual resume.
1471
1458
  * Seat-owned loader
1472
1459
  * validation, turn builder, and adapters stay on the seat.
1473
1460
  *
1474
- * Court open/recovery transaction (#637): under the existing writer lease,
1475
- * read currentCourt, judge seal, clear (bound to the judged court id), freeze,
1476
- * and record. No pre-lease clear or stale court-snapshot consumption.
1461
+ * Court handling (#637): public manual resume reads only the open court's
1462
+ * settlement identity; internal re-summons may freeze and record its materials.
1477
1463
  */
1478
1464
  export async function runPostAdmissionSeatResume<
1479
1465
  A extends AdmittedRoleInvocation,
@@ -1482,7 +1468,7 @@ export async function runPostAdmissionSeatResume<
1482
1468
  request: PublicResumeRequest;
1483
1469
  env: PostAdmissionEnv;
1484
1470
  io: CliIo;
1485
- /** Load admitted state; receives the effective resume request (may carry rehydrated summons). */
1471
+ /** Load admitted state from the caller's resume request. */
1486
1472
  load: (request: PublicResumeRequest) => Promise<{ admitted: A }>;
1487
1473
  /** Build turn from admitted + effective request (summons ride existing projection). */
1488
1474
  buildTurnRequest: (
@@ -1494,8 +1480,7 @@ export async function runPostAdmissionSeatResume<
1494
1480
  * After the single pre-lease load. Factory seats resolve method-material
1495
1481
  * adapters here via resolveResumeMethodMaterialAdapters (or short-circuit
1496
1482
  * with the same controlled-failure face as initial). Must not re-load the
1497
- * same admitted; under-lease summons rehydrate remains the only second load,
1498
- * and only when materials change.
1483
+ * same admitted.
1499
1484
  */
1500
1485
  afterAdmittedLoad?: (
1501
1486
  admitted: A,
@@ -1517,7 +1502,7 @@ export async function runPostAdmissionSeatResume<
1517
1502
  // Load once for runDirectory / structural rejection / afterAdmittedLoad.
1518
1503
  // Court identity for public manual resume is judged in the turn builder
1519
1504
  // (#987: no package writer-lease gate before host CLI resume). Station-child
1520
- // auto-resume still acquires the shared lease in runWithAutoResumeLoop.
1505
+ // auto-resume shares that rule via runWithAutoResumeLoop (no pre-acquire).
1521
1506
  let loaded;
1522
1507
  try {
1523
1508
  loaded = await input.load(request);
@@ -1548,34 +1533,19 @@ export async function runPostAdmissionSeatResume<
1548
1533
 
1549
1534
  const buildRequestAfterLease = async (): Promise<RoleTurnRequest> => {
1550
1535
  let openCourtAttemptId: string | undefined;
1551
- // Build uses the admitted for this resume (rehydrated when open court
1552
- // materials ride). Settlement identity stays on the outer admitted.
1553
- // Name kept for station-child callers that still build under lease.
1554
- let admittedForBuild = loaded.admitted;
1536
+ // Settlement identity stays on the outer admitted.
1537
+ const admittedForBuild = loaded.admitted;
1555
1538
 
1556
- // Bare resume: open-court pointer is the continue signal (not ledger seal).
1539
+ // Bare resume keeps the open court's settlement identity, but public resume
1540
+ // never re-delivers the prior summons or its attachments (#987).
1557
1541
  if (request.summons === undefined) {
1558
1542
  const openCourt = await readCurrentCourt(admittedForBuild.runDirectory);
1559
1543
  if (openCourt !== undefined) {
1560
1544
  openCourtAttemptId = openCourt.courtAttemptId;
1561
- request = {
1562
- runId: request.runId,
1563
- ...(request.message === undefined
1564
- ? {}
1565
- : { message: request.message }),
1566
- ...(openCourt.summons === undefined
1567
- ? {}
1568
- : { summons: openCourt.summons }),
1569
- };
1570
- if (openCourt.summons !== undefined) {
1571
- const reloaded = await input.load(request);
1572
- admittedForBuild = reloaded.admitted;
1573
- }
1574
1545
  }
1575
1546
  }
1576
1547
 
1577
- // Freeze external paths once; rewrite summons to the frozen identity so
1578
- // currentCourt + later bare resume reuse the accepted snapshot.
1548
+ // Internal re-summons freezes external paths once and records that identity.
1579
1549
  if (request.summons !== undefined) {
1580
1550
  const prepared = await prepareSummonsResumeMaterials(
1581
1551
  admittedForBuild.runDirectory,
@@ -1626,10 +1596,10 @@ export async function runPostAdmissionSeatResume<
1626
1596
  return turnRequest;
1627
1597
  };
1628
1598
 
1629
- // Court recovery / open, then dispatch. Public manual resume does not take a
1630
- // package writer lease before the host CLI (#987 / ADR 0080 one-shot).
1631
- // Station-child same-ticket/same-parent resume is call-local auto-resume
1632
- // (#840 / #416) and still acquires the shared lease in its loop.
1599
+ // Court recovery / open, then dispatch. Public manual resume and station-child
1600
+ // auto-resume both pass through to the host CLI without a package writer-lease
1601
+ // pre-gate (#987 / ADR 0080). Station-child same-ticket/same-parent resume is
1602
+ // still call-local auto-resume (#840 / #416).
1633
1603
  // afterAdmittedPrepare runs inside this try so mint failure and cleanup share one finally.
1634
1604
  try {
1635
1605
  if (input.afterAdmittedPrepare !== undefined) {
@@ -1664,14 +1634,13 @@ export async function runPostAdmissionSeatResume<
1664
1634
  // redispatch brake (#833). New-court station-child turns still auto-resume.
1665
1635
  dispatch: async (payload, lease, _isFirst, attemptIo) =>
1666
1636
  dispatchAfterWriterLease({
1667
- lease,
1637
+ ...(lease === undefined ? {} : { lease }),
1668
1638
  build: async () => {
1669
1639
  // #840 r8 判词 class 2: this call-local retry must keep this
1670
1640
  // court's frozen summons / 交卷 body / attachments verbatim
1671
1641
  // (same object as firstTurn) and project only the minimal
1672
- // host-needed resume trigger — never the manual-resume engine
1673
- // handbook (buildResumeContinuationPrompt), which would replace
1674
- // a 审核循环 same-ticket continuation with a bare outsourcing
1642
+ // host-needed resume trigger — never engine handbook material,
1643
+ // which would replace a 审核循环 same-ticket continuation with a bare outsourcing
1675
1644
  // 「重新读」 envelope (#755 contract, resumeTurnRequestProjectionOptions
1676
1645
  // above). RESUME_TRANSPORT_ENVELOPE is the same package-owned,
1677
1646
  // non-semantic trigger that projection already uses for a
@@ -1698,7 +1667,7 @@ export async function runPostAdmissionSeatResume<
1698
1667
  env,
1699
1668
  io: attemptIo,
1700
1669
  request: turnRequest,
1701
- lease,
1670
+ ...(lease === undefined ? {} : { lease }),
1702
1671
  adapters: stationAdapters,
1703
1672
  persistRunState: false,
1704
1673
  ...(input.effectiveEngine === undefined
@@ -1727,8 +1696,7 @@ export async function runPostAdmissionSeatResume<
1727
1696
  buildRequestAfterLease,
1728
1697
  });
1729
1698
  } catch (error) {
1730
- // Open-court rehydrate load under lease may still surface seat structural
1731
- // rejection (e.g. notary rejects caller message) — same exit face as pre-lease.
1699
+ // Seat preparation may still surface structural rejection.
1732
1700
  if (error instanceof CliUsageError) {
1733
1701
  presentStructuralRejection(error, input.io);
1734
1702
  return { exitCode: 2 };
@@ -1836,7 +1804,7 @@ export async function runPostAdmissionResumable<
1836
1804
  },
1837
1805
  io: attemptIo,
1838
1806
  request,
1839
- lease,
1807
+ ...(lease === undefined ? {} : { lease }),
1840
1808
  adapters,
1841
1809
  persistRunState: false,
1842
1810
  // #600: every attempt (initial + auto-resume) writes seat engine when present.
@@ -1851,8 +1819,8 @@ export async function runPostAdmissionResumable<
1851
1819
  * Manual resume: pass-through to the host CLI resume — no package writer-lease
1852
1820
  * pre-gate (#987), no sealed-accepted short-circuit (#833 / #416). Court open
1853
1821
  * (summons / message / open court) is built when using buildRequestAfterLease;
1854
- * sole-final stays per-attempt. Station-child auto-resume keeps shared lease
1855
- * acquire in runWithAutoResumeLoop + dispatchAfterWriterLease.
1822
+ * sole-final stays per-attempt. Station-child auto-resume shares the same
1823
+ * no-pre-gate rule via runWithAutoResumeLoop + dispatchAfterWriterLease.
1856
1824
  */
1857
1825
  export async function runPostAdmissionManualResume<
1858
1826
  A extends AdmittedRoleInvocation,
@@ -440,7 +440,7 @@ export async function runPublicReviewerResume(
440
440
  ...(sandbox.executionCwd === undefined ? {} : { cwd: sandbox.executionCwd }),
441
441
  continuation: {
442
442
  kind: "resume",
443
- prompt: reviewerResumePrompt(activeEnv, effective.message),
443
+ prompt: effective.message ?? "",
444
444
  },
445
445
  });
446
446
  },
@@ -17,10 +17,7 @@ import {
17
17
  resolveActivationLedgerHome,
18
18
  } from "../activation-ledger-topology.ts";
19
19
  import { listBookRunDirectories } from "../role-run-placement.ts";
20
- import {
21
- isSafePositiveTicketNumber,
22
- readBoardTicketNumber,
23
- } from "../run-ticket-number.ts";
20
+ import { isSafePositiveTicketNumber } from "../run-ticket-number.ts";
24
21
  import { CliUsageError } from "./cli-errors.ts";
25
22
  import {
26
23
  readLatestTypedProviderHttpObservation,
@@ -48,7 +45,6 @@ import {
48
45
  appendEngineSessionMaterial,
49
46
  engineSessionMaterialFromOptions,
50
47
  pickEngineAxis,
51
- type EngineSessionMaterial,
52
48
  } from "../package-resources/engine-material.ts";
53
49
  import type { PublicThinkingLevel } from "./registry.ts";
54
50
  import {
@@ -135,11 +131,8 @@ export type RoleRunRecord = {
135
131
  * Package-owned non-empty Chinese neutral resume transport (#959 / ADR 0073).
136
132
  * Used by:
137
133
  * - auto-resume (all seats via buildAutoResumeContinuationPrompt) — required so
138
- * hosts that reject empty stdin (codex) still receive a prompt;
139
- * - reviewer seat manual resume (reviewerResumePrompt) — same non-empty need on
140
- * that seat's own manual entry.
141
- * Generic bare `ak-role resume` stays empty-capable via selectResumeContinuationPrompt
142
- * / buildResumeContinuationPrompt — ADR 0080 keeps auto and generic-manual entries separate.
134
+ * hosts that reject empty stdin (codex) still receive a prompt.
135
+ * Public manual resume forwards only the caller's bytes (#987).
143
136
  */
144
137
  export const RESUME_TRANSPORT_ENVELOPE = "继续。" as const;
145
138
 
@@ -151,15 +144,15 @@ export type PublicResumeRequest = {
151
144
  /**
152
145
  * Same-ticket re-summons materials (#637). Present only when a public seat
153
146
  * re-enters via the summons face — never from `ak-role resume`.
154
- * Manual resume keeps package envelope / caller message semantics unchanged.
147
+ * Manual resume forwards only caller-supplied bytes and never re-delivers them.
155
148
  */
156
149
  readonly summons?: SameTicketSummonsMaterials;
157
150
  };
158
151
 
159
152
  /**
160
153
  * Open court turn on a retained run (#637).
161
- * courtAttemptId + the same summons materials shape already used by re-summons.
162
- * Bare resume rehydrates request.summons and rides existing load/buildTurnRequest.
154
+ * courtAttemptId identifies settlement across later public manual resume calls.
155
+ * Summons materials belong only to the internal re-summons face.
163
156
  * Cleared when this courtAttemptId seals.
164
157
  */
165
158
  export type CurrentCourtState = {
@@ -181,40 +174,6 @@ export type SameTicketSummonsMaterials = {
181
174
  readonly sourceRun?: NotarySourceRunLocator;
182
175
  };
183
176
 
184
- /**
185
- * Manual resume continuation selector (#471 / #600 / #736 / ADR 0080).
186
- * Message present → those bytes; absent → engine pointers only (may be empty).
187
- * Caller message (including blank) still wins verbatim when supplied.
188
- * Auto-resume must use buildAutoResumeContinuationPrompt — do not fold the
189
- * non-empty Chinese envelope into this shared manual selector (#959).
190
- */
191
- export function selectResumeContinuationPrompt(
192
- message?: string,
193
- engineMaterial?: EngineSessionMaterial,
194
- ): string {
195
- const lines = message !== undefined ? [message] : [];
196
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
197
- }
198
-
199
- /**
200
- * Manual resume continuation with engine material from the seat env (#600).
201
- * Bare manual resume stays empty-prompt-capable; auto-resume is a separate entry.
202
- */
203
- export function buildResumeContinuationPrompt(options: {
204
- packageRoot: string;
205
- engine?: string;
206
- engineModel?: string;
207
- message?: string;
208
- }): string {
209
- return selectResumeContinuationPrompt(
210
- options.message,
211
- engineSessionMaterialFromOptions({
212
- packageRoot: options.packageRoot,
213
- ...pickEngineAxis(options),
214
- }),
215
- );
216
- }
217
-
218
177
  /**
219
178
  * Auto-resume continuation only (#959 / ADR 0080).
220
179
  * Always non-empty: Chinese neutral envelope plus optional engine pointers.
@@ -225,13 +184,13 @@ export function buildAutoResumeContinuationPrompt(options: {
225
184
  engine?: string;
226
185
  engineModel?: string;
227
186
  }): string {
228
- return selectResumeContinuationPrompt(
229
- RESUME_TRANSPORT_ENVELOPE,
187
+ return appendEngineSessionMaterial(
188
+ [RESUME_TRANSPORT_ENVELOPE],
230
189
  engineSessionMaterialFromOptions({
231
190
  packageRoot: options.packageRoot,
232
191
  ...pickEngineAxis(options),
233
192
  }),
234
- );
193
+ ).join("\n");
235
194
  }
236
195
 
237
196
  const RUN_STATE_FILE = "run-state.json";
@@ -672,8 +631,7 @@ export async function markRunTerminal(runDirectory: string): Promise<void> {
672
631
  if (current === undefined) {
673
632
  throw new Error("cannot mark terminal: run state missing");
674
633
  }
675
- // Preserve open currentCourt: terminal after a failed/incomplete court must still
676
- // let bare resume continue that court (#637).
634
+ // Preserve the open court's settlement identity after a failed/incomplete turn (#637).
677
635
  await writeRoleRunStateDisk(runDirectory, {
678
636
  runId: current.runId,
679
637
  role: current.role,
@@ -1183,19 +1141,19 @@ async function runHasFormedSessionPrincipal(runDirectory: string): Promise<boole
1183
1141
  /**
1184
1142
  * Locate the latest retained run for one seat under a book (#637 / #747).
1185
1143
  * Same walk surface as findRunDirectoryById (listBookRunDirectories). Match by
1186
- * parent run path (officer seats, #747) or by ticket number (countersign /
1187
- * diarist principal). runId is UUIDv7 — lexicographic max is latest among runs
1188
- * that formed a session principal. No parallel index.
1144
+ * parent run path (officer / gate seats, #747 / #987). runId is UUIDv7 —
1145
+ * lexicographic max is latest among runs that formed a session principal. No
1146
+ * parallel index. Public ticket-number selection of a prior run was deleted
1147
+ * (#987 Result 7); callers use explicit `ak-role resume <runId>`.
1189
1148
  * Only a truly missing book directory means no history; damage/permission errors propagate.
1190
1149
  */
1191
1150
  export async function findLatestRunIdForSeatTicket(input: {
1192
1151
  readonly home: string;
1193
1152
  readonly bookKey: string;
1194
1153
  readonly role: RoleRunRecord["role"];
1195
- readonly ticketNumber?: number;
1196
- readonly parentRunPath?: string;
1154
+ readonly parentRunPath: string;
1197
1155
  }): Promise<string | undefined> {
1198
- if (input.ticketNumber === undefined && input.parentRunPath === undefined) {
1156
+ if (input.parentRunPath.trim() === "") {
1199
1157
  return undefined;
1200
1158
  }
1201
1159
  const ledgerHome = resolveActivationLedgerHome(input.home);
@@ -1214,14 +1172,8 @@ export async function findLatestRunIdForSeatTicket(input: {
1214
1172
  if (!entry.endsWith(suffix)) continue;
1215
1173
  const runId = entry.slice(0, entry.length - suffix.length);
1216
1174
  if (runId.length === 0) continue;
1217
- if (input.parentRunPath !== undefined) {
1218
- const parentPath = await readRunParentPath(runDirectory);
1219
- if (parentPath !== input.parentRunPath) continue;
1220
- } else if (input.ticketNumber !== undefined) {
1221
- // Same-ticket resume is board identity only — never migration-derived.
1222
- const ticketNumber = await readBoardTicketNumber(runDirectory);
1223
- if (ticketNumber !== input.ticketNumber) continue;
1224
- }
1175
+ const parentPath = await readRunParentPath(runDirectory);
1176
+ if (parentPath !== input.parentRunPath) continue;
1225
1177
  // Durable fact: never resume-select a provisional that never formed principal.
1226
1178
  if (!(await runHasFormedSessionPrincipal(runDirectory))) continue;
1227
1179
  if (best === undefined || runId > best) best = runId;
@@ -1,15 +1,13 @@
1
1
  /**
2
- * Shared ticket identity seam for public court seats (#635 / #637 / #709 / #747 / #771).
2
+ * Shared same-parent resume seam for gate / officer seats (#635 / #637 / #747 / #987).
3
3
  *
4
- * ADR 0075(起居郎 LLM 自行认票): the diarist LLM recognizes the
5
- * court target; recognized ticket → provenance, truly unbound → no provenance,
6
- * or escalate when it cannot recognize one. Code does not re-judge the ticket
7
- * (锚定宪法; owner 2026-09-08: 代码不准做判断). Other seats reuse a typed
8
- * identity already handed over (起居郎 assertion / source-run / already-bound
9
- * resume) — they do not re-recognize from instruction. No CLI --ticket, no
10
- * attachment frontmatter. #747: officer same-parent resume also lives here.
11
- *
12
- * This module owns same-ticket / same-parent resume only.
4
+ * Lookup key is parent run path only (#747 / #987 Result 7). Public seats no
5
+ * longer select a prior run by ticket number — callers use explicit
6
+ * `ak-role resume <runId>`. Officer and gate countersign same-parent re-summons
7
+ * keep this seam. Lookup/resume failures propagate (失败诚实) — never wash into
8
+ * a fresh mint. Returns undefined when the caller declared an explicit fresh
9
+ * summons (`ak-role new`) or when no prior run exists; both mint new.
10
+ * freshSummons is required so no seat can drift back into its own skip branch.
13
11
  */
14
12
  import { resolveBookKeyFromGit } from "../activation-ledger-git.ts";
15
13
  import {
@@ -19,20 +17,13 @@ import {
19
17
  } from "./run-lifecycle.ts";
20
18
 
21
19
  /**
22
- * Sole same-seat → resume decision (#637 / #724 / #747).
23
- * Officer seats (notary/inspector/auditor) look up by parent run path; countersign
24
- * / diarist keep ticket-number principal. When found, runs resume with this
25
- * summons' materials. Lookup/resume failures propagate (失败诚实) — never wash
26
- * into a fresh mint. Returns undefined when the caller declared an explicit
27
- * fresh summons (`ak-role new`) or when no prior run exists; both mint new.
28
- * freshSummons is required so no seat can drift back into its own skip branch.
20
+ * Sole same-seat → resume decision by parent run path (#637 / #724 / #747 / #987).
29
21
  */
30
22
  export async function tryResumeSameTicketSeatRun<T>(input: {
31
23
  readonly home: string;
32
24
  readonly projectRoot: string;
33
25
  readonly role: RoleRunRecord["role"];
34
- readonly ticketNumber?: number;
35
- readonly parentRunPath?: string;
26
+ readonly parentRunPath: string;
36
27
  readonly freshSummons: true | undefined;
37
28
  readonly summons?: SameTicketSummonsMaterials;
38
29
  readonly resume: (
@@ -41,19 +32,12 @@ export async function tryResumeSameTicketSeatRun<T>(input: {
41
32
  ) => Promise<T>;
42
33
  }): Promise<T | undefined> {
43
34
  if (input.freshSummons === true) return undefined;
44
- if (input.parentRunPath === undefined && input.ticketNumber === undefined) {
45
- return undefined;
46
- }
35
+ if (input.parentRunPath.trim() === "") return undefined;
47
36
  const previousRunId = await findLatestRunIdForSeatTicket({
48
37
  home: input.home,
49
38
  bookKey: resolveBookKeyFromGit(input.projectRoot),
50
39
  role: input.role,
51
- ...(input.parentRunPath === undefined
52
- ? {}
53
- : { parentRunPath: input.parentRunPath }),
54
- ...(input.ticketNumber === undefined
55
- ? {}
56
- : { ticketNumber: input.ticketNumber }),
40
+ parentRunPath: input.parentRunPath,
57
41
  });
58
42
  if (previousRunId === undefined) return undefined;
59
43
  return await input.resume(previousRunId, input.summons);
@@ -99,8 +99,13 @@ export type PublicSummonRequest = {
99
99
  readonly hostAdapters?: readonly NamedRoleTurnHostAdapter[];
100
100
  /** Offline test inject for deterministic run ids (same face as public CLI). */
101
101
  readonly createRunId?: () => string;
102
- /** Typed ticket number handoff for diarist child run (#840). */
102
+ /** Typed ticket number handoff for diarist child run (#840). Bind only — not a resume key (#987). */
103
103
  readonly boundTicketNumber?: number;
104
+ /**
105
+ * Gate / officer same-parent resume key (#747 / #987). Countersign gate re-ask
106
+ * looks up prior 给事中 by this parent run directory, never by ticket number.
107
+ */
108
+ readonly parentRunPath?: string;
104
109
  /** Caller correlation id for nested leg ledger (ADR 0010 / #924). */
105
110
  readonly correlationId?: string;
106
111
  /**
@@ -436,6 +441,9 @@ export async function summonPublicRole(
436
441
  ...(options.boundTicketNumber === undefined
437
442
  ? {}
438
443
  : { boundTicketNumber: options.boundTicketNumber }),
444
+ ...(options.parentRunPath === undefined || options.parentRunPath.trim() === ""
445
+ ? {}
446
+ : { parentRunPath: options.parentRunPath }),
439
447
  ...(options.correlationId === undefined || options.correlationId.trim() === ""
440
448
  ? {}
441
449
  : { correlationId: options.correlationId }),
@@ -1040,7 +1048,7 @@ export async function summonParallelReviewerLenses(options: {
1040
1048
  return results;
1041
1049
  }
1042
1050
 
1043
- /** Gate officer summons: notary/auditor via --source-run; inspector via pointer; countersign via ticket (#969). */
1051
+ /** Gate officer summons: notary/auditor via --source-run; inspector via pointer; countersign via parentRunPath (#969 / #987). */
1044
1052
  export async function summonGateOfficer(options: {
1045
1053
  readonly officer: "inspector" | "notary" | "auditor" | "countersign";
1046
1054
  readonly sourceRunDirectory: string;
@@ -1122,11 +1130,11 @@ export async function summonGateOfficer(options: {
1122
1130
  });
1123
1131
  }
1124
1132
  if (options.officer === "countersign") {
1125
- // #969: Secretariat submission gate → 给事中.
1133
+ // #969 / #987: Secretariat submission gate → 给事中.
1126
1134
  // Dialogue / identity instruction = parent typed payload 原话 (ADR 0079) or reask;
1127
- // no code-authored summons prose (#924). Ticket identity = parent run durable
1128
- // board binding only (readBoardTicketNumber) — receipt ticketNumber is payload
1129
- // content, never the sole identity source; parent true-unbound stays unbound.
1135
+ // no code-authored summons prose (#924). Gate resume key = parentRunPath
1136
+ // (sourceRunDirectory); board ticket handoff is bind-only, never a resume
1137
+ // lookup (#987 Result 7). Receipt ticketNumber stays payload content.
1130
1138
  // correlationId only on this officer — notary/auditor/inspector stay untouched.
1131
1139
  const { runIdFromRunDirectory } = await import("./run-terminal-artifacts.ts");
1132
1140
  const correlationId = runIdFromRunDirectory(options.sourceRunDirectory);
@@ -1144,6 +1152,7 @@ export async function summonGateOfficer(options: {
1144
1152
  argv: ["--project", options.cwd, "--", instruction],
1145
1153
  ...common,
1146
1154
  correlationId,
1155
+ parentRunPath: options.sourceRunDirectory,
1147
1156
  ...(parentTicket === undefined ? {} : { boundTicketNumber: parentTicket }),
1148
1157
  });
1149
1158
  }
@@ -1199,13 +1199,22 @@ export function createSecretariatRoleRuntime(
1199
1199
  const summon = dependencies.summonCountersign;
1200
1200
  const summoned = summon === undefined
1201
1201
  ? await (async () => {
1202
+ // #987 Result 7 / #747: mid-turn summon resumes by parentRunPath
1203
+ // (secretariat run dir), same key as summonGateOfficer countersign.
1204
+ // Board ticket is bind-only — never the resume lookup.
1202
1205
  const coordinates = readRoleRunCoordinates(ctx, "secretariat summons");
1203
1206
  const { summonPublicRole } = await import("./public-role-summons.ts");
1207
+ const { readBoardTicketNumber } = await import("./run-ticket-number.ts");
1208
+ const parentTicket = await readBoardTicketNumber(coordinates.runDirectory);
1204
1209
  return summonPublicRole({
1205
1210
  role: "countersign",
1206
1211
  argv: ["--project", coordinates.projectRoot, "--", instruction],
1207
1212
  cwd: coordinates.projectRoot,
1208
1213
  home: coordinates.home,
1214
+ parentRunPath: coordinates.runDirectory,
1215
+ ...(parentTicket === undefined
1216
+ ? {}
1217
+ : { boundTicketNumber: parentTicket }),
1209
1218
  ...(signal === undefined ? {} : { signal }),
1210
1219
  ...(correlationId === undefined ? {} : { correlationId }),
1211
1220
  ...(dependencies.packageRoot === undefined