@akagilnc/pi-workflow-roles 0.1.3629 → 0.1.3635

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.
@@ -3207,6 +3207,7 @@ function notaryDecisiveFacts(output) {
3207
3207
  if (status === "pass" || status === "bounce") {
3208
3208
  const findings = output.findings;
3209
3209
  facts.findingsCount = Array.isArray(findings) ? findings.length : 0;
3210
+ facts.findings = Array.isArray(findings) ? findings : [];
3210
3211
  }
3211
3212
  if (status === "bounce") {
3212
3213
  facts.disposition = "rewrite";
@@ -4706,6 +4707,33 @@ async function findRunDirectoryById(home, runId) {
4706
4707
  }
4707
4708
  return void 0;
4708
4709
  }
4710
+ function parentRunPathFromGatePointerInstruction(instruction) {
4711
+ if (!instruction.startsWith(GATE_DOSSIER_POINTER_PREFIX)) return void 0;
4712
+ const path = instruction.slice(GATE_DOSSIER_POINTER_PREFIX.length).trim();
4713
+ return path === "" ? void 0 : path;
4714
+ }
4715
+ async function readRunParentPath(runDirectory) {
4716
+ let raw;
4717
+ try {
4718
+ raw = JSON.parse(
4719
+ await readFile8(join11(runDirectory, "admitted-request.json"), "utf8")
4720
+ );
4721
+ } catch (error) {
4722
+ if (errorCodeOf2(error) === "ENOENT") return void 0;
4723
+ throw error;
4724
+ }
4725
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
4726
+ return void 0;
4727
+ }
4728
+ const record4 = raw;
4729
+ if (typeof record4.sourceRunPath === "string" && record4.sourceRunPath.trim() !== "") {
4730
+ return record4.sourceRunPath;
4731
+ }
4732
+ if (typeof record4.instruction === "string") {
4733
+ return parentRunPathFromGatePointerInstruction(record4.instruction);
4734
+ }
4735
+ return void 0;
4736
+ }
4709
4737
  async function findLatestRunIdForSeatTicket(input) {
4710
4738
  const ledgerHome = resolveActivationLedgerHome(input.home);
4711
4739
  const runsDir = join11(
@@ -4725,8 +4753,16 @@ async function findLatestRunIdForSeatTicket(input) {
4725
4753
  if (!entry.endsWith(suffix)) continue;
4726
4754
  const runId = entry.slice(0, entry.length - suffix.length);
4727
4755
  if (runId.length === 0) continue;
4728
- const ticketNumber = await readRunTicketNumber(join11(runsDir, entry));
4729
- if (ticketNumber !== input.ticketNumber) continue;
4756
+ const runDirectory = join11(runsDir, entry);
4757
+ if (input.parentRunPath !== void 0) {
4758
+ const parentPath = await readRunParentPath(runDirectory);
4759
+ if (parentPath !== input.parentRunPath) continue;
4760
+ } else if (input.ticketNumber !== void 0) {
4761
+ const ticketNumber = await readRunTicketNumber(runDirectory);
4762
+ if (ticketNumber !== input.ticketNumber) continue;
4763
+ } else {
4764
+ continue;
4765
+ }
4730
4766
  if (best === void 0 || runId > best) best = runId;
4731
4767
  }
4732
4768
  return best;
@@ -5073,7 +5109,7 @@ async function loadResumableInspectorRun(home, runId, authority) {
5073
5109
  };
5074
5110
  return seatLoadedResult(loaded, admitted);
5075
5111
  }
5076
- var V1_RESUMABLE_PROVIDERS, AUTO_RESUME_LIMIT, RESUME_TRANSPORT_ENVELOPE, RUN_STATE_FILE, WRITER_LOCK_FILE, RunWriterLeaseHeldError, WRITER_LEASE_RECLAIM_ROUNDS;
5112
+ var V1_RESUMABLE_PROVIDERS, AUTO_RESUME_LIMIT, RESUME_TRANSPORT_ENVELOPE, RUN_STATE_FILE, WRITER_LOCK_FILE, RunWriterLeaseHeldError, WRITER_LEASE_RECLAIM_ROUNDS, GATE_DOSSIER_POINTER_PREFIX;
5077
5113
  var init_run_lifecycle = __esm({
5078
5114
  "src/public-cli/run-lifecycle.ts"() {
5079
5115
  "use strict";
@@ -5098,6 +5134,7 @@ var init_run_lifecycle = __esm({
5098
5134
  }
5099
5135
  };
5100
5136
  WRITER_LEASE_RECLAIM_ROUNDS = 3;
5137
+ GATE_DOSSIER_POINTER_PREFIX = "\u5377\u5B97\u6307\u9488\uFF1A";
5101
5138
  }
5102
5139
  });
5103
5140
 
@@ -6287,6 +6324,7 @@ __export(invocation_exports, {
6287
6324
  parseNotaryArgv: () => parseNotaryArgv,
6288
6325
  parsePositiveTicketNumber: () => parsePositiveTicketNumber,
6289
6326
  parseReviewerArgv: () => parseReviewerArgv,
6327
+ persistAdmittedSourceRunPath: () => persistAdmittedSourceRunPath,
6290
6328
  recordEffectiveInvocationModel: () => recordEffectiveInvocationModel,
6291
6329
  recordLaunchedPiIdentity: () => recordLaunchedPiIdentity,
6292
6330
  recordLaunchedRolePackageIdentity: () => recordLaunchedRolePackageIdentity,
@@ -6402,6 +6440,25 @@ async function mergeInvocationIdentityPage(runDirectory, fields) {
6402
6440
  "utf8"
6403
6441
  );
6404
6442
  }
6443
+ async function persistAdmittedSourceRunPath(admitted, sourceRunPath) {
6444
+ if (sourceRunPath.trim() === "") {
6445
+ throw new Error("persistAdmittedSourceRunPath requires a non-empty sourceRunPath");
6446
+ }
6447
+ const admittedPath = admitted.admittedRequestPath;
6448
+ const current = JSON.parse(await readFile9(admittedPath, "utf8"));
6449
+ if (typeof current.sourceRunPath === "string") {
6450
+ if (current.sourceRunPath === sourceRunPath) return;
6451
+ throw new Error(
6452
+ `persistAdmittedSourceRunPath refuses to replace ${current.sourceRunPath} with ${sourceRunPath}`
6453
+ );
6454
+ }
6455
+ await writeFile4(
6456
+ admittedPath,
6457
+ `${JSON.stringify({ ...current, sourceRunPath }, null, 2)}
6458
+ `,
6459
+ "utf8"
6460
+ );
6461
+ }
6405
6462
  async function bindAdmittedTicketNumber(admitted, ticketNumber) {
6406
6463
  if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
6407
6464
  throw new Error(`bindAdmittedTicketNumber requires a safe positive integer, got ${String(ticketNumber)}`);
@@ -10536,11 +10593,15 @@ async function resolveSeatTicketBinding(admitted, env = {}) {
10536
10593
  }
10537
10594
  async function tryResumeSameTicketSeatRun(input) {
10538
10595
  if (input.freshSummons === true) return void 0;
10596
+ if (input.parentRunPath === void 0 && input.ticketNumber === void 0) {
10597
+ return void 0;
10598
+ }
10539
10599
  const previousRunId = await findLatestRunIdForSeatTicket({
10540
10600
  home: input.home,
10541
10601
  bookKey: resolveBookKeyFromGit(input.projectRoot),
10542
10602
  role: input.role,
10543
- ticketNumber: input.ticketNumber
10603
+ ...input.parentRunPath === void 0 ? {} : { parentRunPath: input.parentRunPath },
10604
+ ...input.ticketNumber === void 0 ? {} : { ticketNumber: input.ticketNumber }
10544
10605
  });
10545
10606
  if (previousRunId === void 0) return void 0;
10546
10607
  return await input.resume(previousRunId, input.summons);
@@ -10659,7 +10720,7 @@ function failureReason(error) {
10659
10720
  }
10660
10721
  function asStringArray3(value) {
10661
10722
  if (!Array.isArray(value)) return [];
10662
- return value.filter((item) => typeof item === "string");
10723
+ return value.map((item) => typeof item === "string" ? item : JSON.stringify(item));
10663
10724
  }
10664
10725
  function isRecord14(value) {
10665
10726
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -16961,8 +17022,7 @@ async function runPublicNotary(argv, env, io, parseNotaryArgv2) {
16961
17022
  }
16962
17023
  throw error;
16963
17024
  }
16964
- const ticketNumber = await readRunTicketNumber(source.runDirectory);
16965
- if (ticketNumber !== void 0) {
17025
+ {
16966
17026
  const summons = {
16967
17027
  sourceRunPath: source.runDirectory,
16968
17028
  sourceRun: source
@@ -16971,7 +17031,7 @@ async function runPublicNotary(argv, env, io, parseNotaryArgv2) {
16971
17031
  home: env.home,
16972
17032
  projectRoot,
16973
17033
  role: "notary",
16974
- ticketNumber,
17034
+ parentRunPath: source.runDirectory,
16975
17035
  freshSummons: env.freshSummons,
16976
17036
  summons,
16977
17037
  resume: (runId, materials) => runPublicNotaryResume(
@@ -17086,7 +17146,6 @@ var init_notary_run = __esm({
17086
17146
  init_auditor_dossier_tool();
17087
17147
  init_notary_source_run();
17088
17148
  init_engine_material();
17089
- init_run_ticket_number();
17090
17149
  init_cli_errors();
17091
17150
  init_invocation();
17092
17151
  init_seat_ticket_binding();
@@ -17132,8 +17191,8 @@ async function runPublicInspector(argv, env, io, parseInspectorArgv2) {
17132
17191
  projectRoot,
17133
17192
  env
17134
17193
  );
17135
- const probedTicketNumber = ticketNumberFromProbe(ticketProbe);
17136
- if (probedTicketNumber !== void 0) {
17194
+ const parentRunPath = parentRunPathFromGatePointerInstruction(parsed.instruction);
17195
+ if (parentRunPath !== void 0) {
17137
17196
  const summons = {
17138
17197
  instruction: parsed.instruction,
17139
17198
  instructionEmpty: parsed.instruction.trim() === "",
@@ -17143,7 +17202,7 @@ async function runPublicInspector(argv, env, io, parseInspectorArgv2) {
17143
17202
  home: env.home,
17144
17203
  projectRoot,
17145
17204
  role: "inspector",
17146
- ticketNumber: probedTicketNumber,
17205
+ parentRunPath,
17147
17206
  freshSummons: env.freshSummons,
17148
17207
  summons,
17149
17208
  resume: (runId, materials) => runPublicInspectorResume(
@@ -17428,21 +17487,20 @@ async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
17428
17487
  env
17429
17488
  );
17430
17489
  }
17431
- const probedTicketNumber = auditorSourceTicket ?? (ticketProbe === void 0 ? void 0 : ticketNumberFromProbe(ticketProbe));
17432
- if (probedTicketNumber !== void 0) {
17433
- const summons = {
17434
- instruction: parsed.instruction,
17435
- instructionEmpty: parsed.instruction.trim() === "",
17436
- attachmentPaths: parsed.attachmentPaths
17437
- };
17490
+ const summons = {
17491
+ instruction: parsed.instruction,
17492
+ instructionEmpty: parsed.instruction.trim() === "",
17493
+ attachmentPaths: parsed.attachmentPaths
17494
+ };
17495
+ if (role === "auditor" && auditorSourceRun !== void 0) {
17438
17496
  const resumed = await withAuditorSoulEnv({
17439
17497
  ...auditorSubject === void 0 ? {} : { subject: auditorSubject },
17440
- ...auditorSourceRun === void 0 ? {} : { sourceRunDirectory: auditorSourceRun },
17498
+ sourceRunDirectory: auditorSourceRun,
17441
17499
  run: () => tryResumeSameTicketSeatRun({
17442
17500
  home: env.home,
17443
17501
  projectRoot,
17444
17502
  role,
17445
- ticketNumber: probedTicketNumber,
17503
+ parentRunPath: auditorSourceRun,
17446
17504
  freshSummons: env.freshSummons,
17447
17505
  summons,
17448
17506
  resume: (runId, materials) => runPublicInstructionSeatResume(
@@ -17453,6 +17511,24 @@ async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
17453
17511
  })
17454
17512
  });
17455
17513
  if (resumed !== void 0) return resumed;
17514
+ } else {
17515
+ const probedTicketNumber = ticketProbe === void 0 ? void 0 : ticketNumberFromProbe(ticketProbe);
17516
+ if (probedTicketNumber !== void 0) {
17517
+ const resumed = await tryResumeSameTicketSeatRun({
17518
+ home: env.home,
17519
+ projectRoot,
17520
+ role,
17521
+ ticketNumber: probedTicketNumber,
17522
+ freshSummons: env.freshSummons,
17523
+ summons,
17524
+ resume: (runId, materials) => runPublicInstructionSeatResume(
17525
+ { runId, ...materials === void 0 ? {} : { summons: materials } },
17526
+ env,
17527
+ io
17528
+ )
17529
+ });
17530
+ if (resumed !== void 0) return resumed;
17531
+ }
17456
17532
  }
17457
17533
  let admitted;
17458
17534
  try {
@@ -17474,6 +17550,9 @@ async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
17474
17550
  }
17475
17551
  throw error;
17476
17552
  }
17553
+ if (role === "auditor" && auditorSourceRun !== void 0) {
17554
+ await persistAdmittedSourceRunPath(admitted, auditorSourceRun);
17555
+ }
17477
17556
  return await withAuditorSoulEnv({
17478
17557
  ...auditorSubject === void 0 ? {} : { subject: auditorSubject },
17479
17558
  ...auditorSourceRun === void 0 ? {} : { sourceRunDirectory: auditorSourceRun },
@@ -24667,13 +24746,11 @@ async function prepareAcpRoleEnvelope(options) {
24667
24746
  });
24668
24747
  reply(socket, rpc.id, {
24669
24748
  content: projected.content,
24670
- structuredContent: projected.details,
24671
24749
  isError: true
24672
24750
  });
24673
24751
  } catch {
24674
24752
  reply(socket, rpc.id, {
24675
24753
  content: declared.content,
24676
- structuredContent: declared.details,
24677
24754
  isError: true
24678
24755
  });
24679
24756
  }
@@ -24686,7 +24763,7 @@ async function prepareAcpRoleEnvelope(options) {
24686
24763
  details: result.details,
24687
24764
  isError: false
24688
24765
  });
24689
- reply(socket, rpc.id, { content: projected.content, structuredContent: projected.details, ...projected.isError ? { isError: true } : {} });
24766
+ reply(socket, rpc.id, { content: projected.content, ...projected.isError ? { isError: true } : {} });
24690
24767
  } catch (error) {
24691
24768
  let content;
24692
24769
  let details;
@@ -24710,7 +24787,7 @@ async function prepareAcpRoleEnvelope(options) {
24710
24787
  details,
24711
24788
  isError: true
24712
24789
  });
24713
- reply(socket, rpc.id, { content: projected.content, structuredContent: projected.details, ...projected.isError ? { isError: true } : {} });
24790
+ reply(socket, rpc.id, { content: projected.content, ...projected.isError ? { isError: true } : {} });
24714
24791
  }
24715
24792
  } catch (error) {
24716
24793
  reply(socket, rpc.id, void 0, error);
@@ -51,7 +51,9 @@ function failureReason(error) {
51
51
  function asStringArray(value) {
52
52
  if (!Array.isArray(value))
53
53
  return [];
54
- return value.filter((item) => typeof item === "string");
54
+ // Officer findings may be structured objects (category/law/evidence); the
55
+ // parent role must see them verbatim, not an empty list (#750 evidence).
56
+ return value.map((item) => (typeof item === "string" ? item : JSON.stringify(item)));
55
57
  }
56
58
  /** Serializable stand-in when the child tool call had no arguments object. */
57
59
  export const MISSING_ARGUMENTS_SUBMISSION = Object.freeze({ missing: "arguments" });
@@ -80,6 +80,7 @@ function notaryDecisiveFacts(output) {
80
80
  if (status === "pass" || status === "bounce") {
81
81
  const findings = output.findings;
82
82
  facts.findingsCount = Array.isArray(findings) ? findings.length : 0;
83
+ facts.findings = Array.isArray(findings) ? findings : [];
83
84
  }
84
85
  if (status === "bounce") {
85
86
  facts.disposition = "rewrite";
@@ -1,9 +1,9 @@
1
1
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.js";
2
2
  import { CliUsageError } from "./cli-errors.js";
3
- import { applyInstructionTicketProbe, probeInstructionTicket, ticketNumberFromProbe, tryResumeSameTicketSeatRun, } from "./seat-ticket-binding.js";
3
+ import { applyInstructionTicketProbe, probeInstructionTicket, tryResumeSameTicketSeatRun, } from "./seat-ticket-binding.js";
4
4
  import { admitInspectorInvocation, buildInspectorTransportPrompt, } from "./invocation.js";
5
5
  import { prepareSummonsResumeMaterials, runPostAdmissionOneShot, runPostAdmissionSeatResume, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
6
- import { loadResumableInspectorRun, markRunAdmitted, } from "./run-lifecycle.js";
6
+ import { loadResumableInspectorRun, markRunAdmitted, parentRunPathFromGatePointerInstruction, } from "./run-lifecycle.js";
7
7
  import { presentStructuralRejection, trySettleInspectorTerminalResult, } from "./settlement.js";
8
8
  import { projectRoleTurnRequest, } from "./turn-request.js";
9
9
  /** Project admitted invocation onto the host-neutral turn request. */
@@ -26,14 +26,14 @@ export async function runPublicInspector(argv, env, io, parseInspectorArgv) {
26
26
  }
27
27
  throw error;
28
28
  }
29
- // #637: same ticket → resume prior inspector run with this summons' materials.
29
+ // #747: same parent (卷宗指针) → resume prior inspector run with this summons' materials.
30
30
  // Probe captures DiaristTicketResolutionError so admit+beforeDispatch can settle
31
31
  // controlled failure (bare pre-admit throw skips terminal settlement).
32
32
  // No bare catch→fresh: lookup/resume failures surface; only true absence mints new.
33
33
  const projectRoot = parsed.project ?? env.cwd;
34
34
  const ticketProbe = await probeInstructionTicket(parsed.instruction, projectRoot, env);
35
- const probedTicketNumber = ticketNumberFromProbe(ticketProbe);
36
- if (probedTicketNumber !== undefined) {
35
+ const parentRunPath = parentRunPathFromGatePointerInstruction(parsed.instruction);
36
+ if (parentRunPath !== undefined) {
37
37
  const summons = {
38
38
  instruction: parsed.instruction,
39
39
  instructionEmpty: parsed.instruction.trim() === "",
@@ -43,7 +43,7 @@ export async function runPublicInspector(argv, env, io, parseInspectorArgv) {
43
43
  home: env.home,
44
44
  projectRoot,
45
45
  role: "inspector",
46
- ticketNumber: probedTicketNumber,
46
+ parentRunPath,
47
47
  freshSummons: env.freshSummons,
48
48
  summons,
49
49
  resume: (runId, materials) => runPublicInspectorResume({ runId, ...(materials === undefined ? {} : { summons: materials }) }, env, io),
@@ -1,7 +1,7 @@
1
1
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.js";
2
2
  import { readRunTicketNumber } from "../run-ticket-number.js";
3
3
  import { CliUsageError } from "./cli-errors.js";
4
- import { admitAuditorInvocation, admitEvidenceChildInvocation, admitGatekeeperInvocation, admitNavigatorInvocation, bindAdmittedTicketNumber, buildInstructionTransportPrompt, } from "./invocation.js";
4
+ import { admitAuditorInvocation, admitEvidenceChildInvocation, admitGatekeeperInvocation, admitNavigatorInvocation, bindAdmittedTicketNumber, buildInstructionTransportPrompt, persistAdmittedSourceRunPath, } from "./invocation.js";
5
5
  import { prepareSummonsResumeMaterials, runPostAdmissionOneShot, runPostAdmissionSeatResume, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
6
6
  import { loadResumableInstructionSeatRun, markRunAdmitted, } from "./run-lifecycle.js";
7
7
  import { applyInstructionTicketProbe, probeInstructionTicket, ticketNumberFromProbe, tryResumeSameTicketSeatRun, } from "./seat-ticket-binding.js";
@@ -124,10 +124,9 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
124
124
  let auditorSourceRun;
125
125
  let auditorSourceTicket;
126
126
  let ticketProbe;
127
- // #637: same ticket → resume prior seat run with this summons' materials.
128
- // Auditor inherits ticket from source-run (notary face); other instruction seats
129
- // probe instruction (inspector face). No bare catch→fresh: lookup/resume failures
130
- // surface; only true absence of a prior run mints new.
127
+ // #637 / #747: resume prior seat run with this summons' materials.
128
+ // Auditor (#747): parent --source-run path is the lookup key.
129
+ // Other instruction seats: ticket probe (inspector face). No bare catch→fresh.
131
130
  const projectRoot = parsed.project ?? env.cwd;
132
131
  if (role === "auditor") {
133
132
  if (parsed.subject !== "judge" && parsed.subject !== "doctor") {
@@ -159,24 +158,20 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
159
158
  // Inspector face: unconditional probe (same seam as inspector-run.ts).
160
159
  ticketProbe = await probeInstructionTicket(parsed.instruction, projectRoot, env);
161
160
  }
162
- const probedTicketNumber = auditorSourceTicket
163
- ?? (ticketProbe === undefined ? undefined : ticketNumberFromProbe(ticketProbe));
164
- if (probedTicketNumber !== undefined) {
165
- const summons = {
166
- instruction: parsed.instruction,
167
- instructionEmpty: parsed.instruction.trim() === "",
168
- attachmentPaths: parsed.attachmentPaths,
169
- };
161
+ const summons = {
162
+ instruction: parsed.instruction,
163
+ instructionEmpty: parsed.instruction.trim() === "",
164
+ attachmentPaths: parsed.attachmentPaths,
165
+ };
166
+ if (role === "auditor" && auditorSourceRun !== undefined) {
170
167
  const resumed = await withAuditorSoulEnv({
171
168
  ...(auditorSubject === undefined ? {} : { subject: auditorSubject }),
172
- ...(auditorSourceRun === undefined
173
- ? {}
174
- : { sourceRunDirectory: auditorSourceRun }),
169
+ sourceRunDirectory: auditorSourceRun,
175
170
  run: () => tryResumeSameTicketSeatRun({
176
171
  home: env.home,
177
172
  projectRoot,
178
173
  role,
179
- ticketNumber: probedTicketNumber,
174
+ parentRunPath: auditorSourceRun,
180
175
  freshSummons: env.freshSummons,
181
176
  summons,
182
177
  resume: (runId, materials) => runPublicInstructionSeatResume({ runId, ...(materials === undefined ? {} : { summons: materials }) }, env, io),
@@ -185,6 +180,22 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
185
180
  if (resumed !== undefined)
186
181
  return resumed;
187
182
  }
183
+ else {
184
+ const probedTicketNumber = ticketProbe === undefined ? undefined : ticketNumberFromProbe(ticketProbe);
185
+ if (probedTicketNumber !== undefined) {
186
+ const resumed = await tryResumeSameTicketSeatRun({
187
+ home: env.home,
188
+ projectRoot,
189
+ role,
190
+ ticketNumber: probedTicketNumber,
191
+ freshSummons: env.freshSummons,
192
+ summons,
193
+ resume: (runId, materials) => runPublicInstructionSeatResume({ runId, ...(materials === undefined ? {} : { summons: materials }) }, env, io),
194
+ });
195
+ if (resumed !== undefined)
196
+ return resumed;
197
+ }
198
+ }
188
199
  let admitted;
189
200
  try {
190
201
  admitted = await admitInstructionSeat(role, {
@@ -206,6 +217,9 @@ export async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
206
217
  }
207
218
  throw error;
208
219
  }
220
+ if (role === "auditor" && auditorSourceRun !== undefined) {
221
+ await persistAdmittedSourceRunPath(admitted, auditorSourceRun);
222
+ }
209
223
  return await withAuditorSoulEnv({
210
224
  ...(auditorSubject === undefined ? {} : { subject: auditorSubject }),
211
225
  ...(auditorSourceRun === undefined
@@ -126,6 +126,23 @@ async function mergeInvocationIdentityPage(runDirectory, fields) {
126
126
  ...fields,
127
127
  }, null, 2)}\n`, "utf8");
128
128
  }
129
+ /**
130
+ * Persist parent --source-run path onto admitted-request for officer resume lookup (#747).
131
+ * Reuses the existing notary sourceRunPath key; does not invent a new field name.
132
+ */
133
+ export async function persistAdmittedSourceRunPath(admitted, sourceRunPath) {
134
+ if (sourceRunPath.trim() === "") {
135
+ throw new Error("persistAdmittedSourceRunPath requires a non-empty sourceRunPath");
136
+ }
137
+ const admittedPath = admitted.admittedRequestPath;
138
+ const current = JSON.parse(await readFile(admittedPath, "utf8"));
139
+ if (typeof current.sourceRunPath === "string") {
140
+ if (current.sourceRunPath === sourceRunPath)
141
+ return;
142
+ throw new Error(`persistAdmittedSourceRunPath refuses to replace ${current.sourceRunPath} with ${sourceRunPath}`);
143
+ }
144
+ await writeFile(admittedPath, `${JSON.stringify({ ...current, sourceRunPath }, null, 2)}\n`, "utf8");
145
+ }
129
146
  /**
130
147
  * Bind a post-admission resolved ticketNumber onto the in-memory admitted
131
148
  * object and both durable pages (invocation.json + admitted-request.json).
@@ -14902,6 +14902,7 @@ function notaryDecisiveFacts(output) {
14902
14902
  if (status === "pass" || status === "bounce") {
14903
14903
  const findings = output.findings;
14904
14904
  facts.findingsCount = Array.isArray(findings) ? findings.length : 0;
14905
+ facts.findings = Array.isArray(findings) ? findings : [];
14905
14906
  }
14906
14907
  if (status === "bounce") {
14907
14908
  facts.disposition = "rewrite";
@@ -18000,6 +18001,33 @@ async function findRunDirectoryById(home, runId) {
18000
18001
  }
18001
18002
  return void 0;
18002
18003
  }
18004
+ function parentRunPathFromGatePointerInstruction(instruction) {
18005
+ if (!instruction.startsWith(GATE_DOSSIER_POINTER_PREFIX)) return void 0;
18006
+ const path = instruction.slice(GATE_DOSSIER_POINTER_PREFIX.length).trim();
18007
+ return path === "" ? void 0 : path;
18008
+ }
18009
+ async function readRunParentPath(runDirectory) {
18010
+ let raw;
18011
+ try {
18012
+ raw = JSON.parse(
18013
+ await readFile8(join12(runDirectory, "admitted-request.json"), "utf8")
18014
+ );
18015
+ } catch (error) {
18016
+ if (errorCodeOf2(error) === "ENOENT") return void 0;
18017
+ throw error;
18018
+ }
18019
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
18020
+ return void 0;
18021
+ }
18022
+ const record4 = raw;
18023
+ if (typeof record4.sourceRunPath === "string" && record4.sourceRunPath.trim() !== "") {
18024
+ return record4.sourceRunPath;
18025
+ }
18026
+ if (typeof record4.instruction === "string") {
18027
+ return parentRunPathFromGatePointerInstruction(record4.instruction);
18028
+ }
18029
+ return void 0;
18030
+ }
18003
18031
  async function findLatestRunIdForSeatTicket(input) {
18004
18032
  const ledgerHome = resolveActivationLedgerHome(input.home);
18005
18033
  const runsDir = join12(
@@ -18019,8 +18047,16 @@ async function findLatestRunIdForSeatTicket(input) {
18019
18047
  if (!entry.endsWith(suffix)) continue;
18020
18048
  const runId = entry.slice(0, entry.length - suffix.length);
18021
18049
  if (runId.length === 0) continue;
18022
- const ticketNumber = await readRunTicketNumber(join12(runsDir, entry));
18023
- if (ticketNumber !== input.ticketNumber) continue;
18050
+ const runDirectory = join12(runsDir, entry);
18051
+ if (input.parentRunPath !== void 0) {
18052
+ const parentPath = await readRunParentPath(runDirectory);
18053
+ if (parentPath !== input.parentRunPath) continue;
18054
+ } else if (input.ticketNumber !== void 0) {
18055
+ const ticketNumber = await readRunTicketNumber(runDirectory);
18056
+ if (ticketNumber !== input.ticketNumber) continue;
18057
+ } else {
18058
+ continue;
18059
+ }
18024
18060
  if (best === void 0 || runId > best) best = runId;
18025
18061
  }
18026
18062
  return best;
@@ -18577,7 +18613,7 @@ async function peekRoleRunRole(home, runId) {
18577
18613
  const run = await readRoleRunIdentity(runDirectory);
18578
18614
  return run?.role;
18579
18615
  }
18580
- var V1_RESUMABLE_PROVIDERS, AUTO_RESUME_LIMIT, RESUME_TRANSPORT_ENVELOPE, RUN_STATE_FILE, WRITER_LOCK_FILE, RunWriterLeaseHeldError, WRITER_LEASE_RECLAIM_ROUNDS;
18616
+ var V1_RESUMABLE_PROVIDERS, AUTO_RESUME_LIMIT, RESUME_TRANSPORT_ENVELOPE, RUN_STATE_FILE, WRITER_LOCK_FILE, RunWriterLeaseHeldError, WRITER_LEASE_RECLAIM_ROUNDS, GATE_DOSSIER_POINTER_PREFIX;
18581
18617
  var init_run_lifecycle = __esm({
18582
18618
  "src/public-cli/run-lifecycle.ts"() {
18583
18619
  "use strict";
@@ -18602,6 +18638,7 @@ var init_run_lifecycle = __esm({
18602
18638
  }
18603
18639
  };
18604
18640
  WRITER_LEASE_RECLAIM_ROUNDS = 3;
18641
+ GATE_DOSSIER_POINTER_PREFIX = "\u5377\u5B97\u6307\u9488\uFF1A";
18605
18642
  }
18606
18643
  });
18607
18644
 
@@ -19898,6 +19935,25 @@ async function mergeInvocationIdentityPage(runDirectory, fields) {
19898
19935
  "utf8"
19899
19936
  );
19900
19937
  }
19938
+ async function persistAdmittedSourceRunPath(admitted, sourceRunPath) {
19939
+ if (sourceRunPath.trim() === "") {
19940
+ throw new Error("persistAdmittedSourceRunPath requires a non-empty sourceRunPath");
19941
+ }
19942
+ const admittedPath = admitted.admittedRequestPath;
19943
+ const current = JSON.parse(await readFile9(admittedPath, "utf8"));
19944
+ if (typeof current.sourceRunPath === "string") {
19945
+ if (current.sourceRunPath === sourceRunPath) return;
19946
+ throw new Error(
19947
+ `persistAdmittedSourceRunPath refuses to replace ${current.sourceRunPath} with ${sourceRunPath}`
19948
+ );
19949
+ }
19950
+ await writeFile5(
19951
+ admittedPath,
19952
+ `${JSON.stringify({ ...current, sourceRunPath }, null, 2)}
19953
+ `,
19954
+ "utf8"
19955
+ );
19956
+ }
19901
19957
  async function bindAdmittedTicketNumber(admitted, ticketNumber) {
19902
19958
  if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
19903
19959
  throw new Error(`bindAdmittedTicketNumber requires a safe positive integer, got ${String(ticketNumber)}`);
@@ -23782,11 +23838,15 @@ async function resolveSeatTicketBinding(admitted, env = {}) {
23782
23838
  }
23783
23839
  async function tryResumeSameTicketSeatRun(input) {
23784
23840
  if (input.freshSummons === true) return void 0;
23841
+ if (input.parentRunPath === void 0 && input.ticketNumber === void 0) {
23842
+ return void 0;
23843
+ }
23785
23844
  const previousRunId = await findLatestRunIdForSeatTicket({
23786
23845
  home: input.home,
23787
23846
  bookKey: resolveBookKeyFromGit(input.projectRoot),
23788
23847
  role: input.role,
23789
- ticketNumber: input.ticketNumber
23848
+ ...input.parentRunPath === void 0 ? {} : { parentRunPath: input.parentRunPath },
23849
+ ...input.ticketNumber === void 0 ? {} : { ticketNumber: input.ticketNumber }
23790
23850
  });
23791
23851
  if (previousRunId === void 0) return void 0;
23792
23852
  return await input.resume(previousRunId, input.summons);
@@ -29436,21 +29496,20 @@ async function runPublicInstructionSeat(argv, env, io, role, parseArgv2) {
29436
29496
  env
29437
29497
  );
29438
29498
  }
29439
- const probedTicketNumber = auditorSourceTicket ?? (ticketProbe === void 0 ? void 0 : ticketNumberFromProbe(ticketProbe));
29440
- if (probedTicketNumber !== void 0) {
29441
- const summons = {
29442
- instruction: parsed.instruction,
29443
- instructionEmpty: parsed.instruction.trim() === "",
29444
- attachmentPaths: parsed.attachmentPaths
29445
- };
29499
+ const summons = {
29500
+ instruction: parsed.instruction,
29501
+ instructionEmpty: parsed.instruction.trim() === "",
29502
+ attachmentPaths: parsed.attachmentPaths
29503
+ };
29504
+ if (role === "auditor" && auditorSourceRun !== void 0) {
29446
29505
  const resumed = await withAuditorSoulEnv({
29447
29506
  ...auditorSubject === void 0 ? {} : { subject: auditorSubject },
29448
- ...auditorSourceRun === void 0 ? {} : { sourceRunDirectory: auditorSourceRun },
29507
+ sourceRunDirectory: auditorSourceRun,
29449
29508
  run: () => tryResumeSameTicketSeatRun({
29450
29509
  home: env.home,
29451
29510
  projectRoot,
29452
29511
  role,
29453
- ticketNumber: probedTicketNumber,
29512
+ parentRunPath: auditorSourceRun,
29454
29513
  freshSummons: env.freshSummons,
29455
29514
  summons,
29456
29515
  resume: (runId, materials) => runPublicInstructionSeatResume(
@@ -29461,6 +29520,24 @@ async function runPublicInstructionSeat(argv, env, io, role, parseArgv2) {
29461
29520
  })
29462
29521
  });
29463
29522
  if (resumed !== void 0) return resumed;
29523
+ } else {
29524
+ const probedTicketNumber = ticketProbe === void 0 ? void 0 : ticketNumberFromProbe(ticketProbe);
29525
+ if (probedTicketNumber !== void 0) {
29526
+ const resumed = await tryResumeSameTicketSeatRun({
29527
+ home: env.home,
29528
+ projectRoot,
29529
+ role,
29530
+ ticketNumber: probedTicketNumber,
29531
+ freshSummons: env.freshSummons,
29532
+ summons,
29533
+ resume: (runId, materials) => runPublicInstructionSeatResume(
29534
+ { runId, ...materials === void 0 ? {} : { summons: materials } },
29535
+ env,
29536
+ io
29537
+ )
29538
+ });
29539
+ if (resumed !== void 0) return resumed;
29540
+ }
29464
29541
  }
29465
29542
  let admitted;
29466
29543
  try {
@@ -29482,6 +29559,9 @@ async function runPublicInstructionSeat(argv, env, io, role, parseArgv2) {
29482
29559
  }
29483
29560
  throw error;
29484
29561
  }
29562
+ if (role === "auditor" && auditorSourceRun !== void 0) {
29563
+ await persistAdmittedSourceRunPath(admitted, auditorSourceRun);
29564
+ }
29485
29565
  return await withAuditorSoulEnv({
29486
29566
  ...auditorSubject === void 0 ? {} : { subject: auditorSubject },
29487
29567
  ...auditorSourceRun === void 0 ? {} : { sourceRunDirectory: auditorSourceRun },
@@ -30427,8 +30507,7 @@ async function runPublicNotary(argv, env, io, parseNotaryArgv2) {
30427
30507
  }
30428
30508
  throw error;
30429
30509
  }
30430
- const ticketNumber = await readRunTicketNumber(source.runDirectory);
30431
- if (ticketNumber !== void 0) {
30510
+ {
30432
30511
  const summons = {
30433
30512
  sourceRunPath: source.runDirectory,
30434
30513
  sourceRun: source
@@ -30437,7 +30516,7 @@ async function runPublicNotary(argv, env, io, parseNotaryArgv2) {
30437
30516
  home: env.home,
30438
30517
  projectRoot,
30439
30518
  role: "notary",
30440
- ticketNumber,
30519
+ parentRunPath: source.runDirectory,
30441
30520
  freshSummons: env.freshSummons,
30442
30521
  summons,
30443
30522
  resume: (runId, materials) => runPublicNotaryResume(
@@ -30552,7 +30631,6 @@ var init_notary_run = __esm({
30552
30631
  init_auditor_dossier_tool();
30553
30632
  init_notary_source_run();
30554
30633
  init_engine_material();
30555
- init_run_ticket_number();
30556
30634
  init_cli_errors();
30557
30635
  init_invocation();
30558
30636
  init_seat_ticket_binding();
@@ -30592,8 +30670,8 @@ async function runPublicInspector(argv, env, io, parseInspectorArgv2) {
30592
30670
  projectRoot,
30593
30671
  env
30594
30672
  );
30595
- const probedTicketNumber = ticketNumberFromProbe(ticketProbe);
30596
- if (probedTicketNumber !== void 0) {
30673
+ const parentRunPath = parentRunPathFromGatePointerInstruction(parsed.instruction);
30674
+ if (parentRunPath !== void 0) {
30597
30675
  const summons = {
30598
30676
  instruction: parsed.instruction,
30599
30677
  instructionEmpty: parsed.instruction.trim() === "",
@@ -30603,7 +30681,7 @@ async function runPublicInspector(argv, env, io, parseInspectorArgv2) {
30603
30681
  home: env.home,
30604
30682
  projectRoot,
30605
30683
  role: "inspector",
30606
- ticketNumber: probedTicketNumber,
30684
+ parentRunPath,
30607
30685
  freshSummons: env.freshSummons,
30608
30686
  summons,
30609
30687
  resume: (runId, materials) => runPublicInspectorResume(
@@ -2,13 +2,12 @@
2
2
  * Public Notary Role run: admit source-run locator → shared post-admission coordinator
3
3
  * → settle Terminal result (#448 / #517). Zero caller prompt/attachment. Lifecycle is
4
4
  * the shared post-admission seam; this module keeps only Notary adapters.
5
- * #637: same-ticket re-summons resume the seat's previous run (no new run).
5
+ * #637 / #747: same-parent (--source-run) re-summons resume the seat's previous run.
6
6
  */
7
7
  import { existsSync } from "node:fs";
8
8
  import { gateSubmissionCandidatePath } from "../auditor-dossier-tool.js";
9
9
  import { NotarySourceRunError, resolveNotarySourceRunLocator, } from "../notary-source-run.js";
10
10
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.js";
11
- import { readRunTicketNumber } from "../run-ticket-number.js";
12
11
  import { CliUsageError } from "./cli-errors.js";
13
12
  import { admitNotaryInvocation, buildNotaryTransportPrompt, } from "./invocation.js";
14
13
  import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.js";
@@ -60,8 +59,8 @@ export async function runPublicNotary(argv, env, io, parseNotaryArgv) {
60
59
  }
61
60
  throw error;
62
61
  }
63
- const ticketNumber = await readRunTicketNumber(source.runDirectory);
64
- if (ticketNumber !== undefined) {
62
+ // #747: officer resume key is this parent source-run path (not ticket number).
63
+ {
65
64
  const summons = {
66
65
  sourceRunPath: source.runDirectory,
67
66
  sourceRun: source,
@@ -70,7 +69,7 @@ export async function runPublicNotary(argv, env, io, parseNotaryArgv) {
70
69
  home: env.home,
71
70
  projectRoot,
72
71
  role: "notary",
73
- ticketNumber,
72
+ parentRunPath: source.runDirectory,
74
73
  freshSummons: env.freshSummons,
75
74
  summons,
76
75
  resume: (runId, materials) => runPublicNotaryResume({ runId, ...(materials === undefined ? {} : { summons: materials }) }, env, io),
@@ -746,9 +746,46 @@ export async function findRunDirectoryById(home, runId) {
746
746
  }
747
747
  return undefined;
748
748
  }
749
+ /** Code-owned gate inspector summons prefix (public-role-summons / #747). */
750
+ export const GATE_DOSSIER_POINTER_PREFIX = "卷宗指针:";
751
+ /** Parent path from a code-owned inspector 卷宗指针 instruction; else undefined. */
752
+ export function parentRunPathFromGatePointerInstruction(instruction) {
753
+ if (!instruction.startsWith(GATE_DOSSIER_POINTER_PREFIX))
754
+ return undefined;
755
+ const path = instruction.slice(GATE_DOSSIER_POINTER_PREFIX.length).trim();
756
+ return path === "" ? undefined : path;
757
+ }
749
758
  /**
750
- * Locate the latest retained run for one seat+ticket under a book (#637).
751
- * Same walk surface as findRunDirectoryById; ticket identity from durable pages.
759
+ * Parent-run binding on a retained officer run (#747).
760
+ * Notary/auditor: typed sourceRunPath. Inspector: exact code-owned 卷宗指针 instruction.
761
+ * Missing page → undefined; damage / non-ENOENT IO propagates.
762
+ */
763
+ export async function readRunParentPath(runDirectory) {
764
+ let raw;
765
+ try {
766
+ raw = JSON.parse(await readFile(join(runDirectory, "admitted-request.json"), "utf8"));
767
+ }
768
+ catch (error) {
769
+ if (errorCodeOf(error) === "ENOENT")
770
+ return undefined;
771
+ throw error;
772
+ }
773
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
774
+ return undefined;
775
+ }
776
+ const record = raw;
777
+ if (typeof record.sourceRunPath === "string" && record.sourceRunPath.trim() !== "") {
778
+ return record.sourceRunPath;
779
+ }
780
+ if (typeof record.instruction === "string") {
781
+ return parentRunPathFromGatePointerInstruction(record.instruction);
782
+ }
783
+ return undefined;
784
+ }
785
+ /**
786
+ * Locate the latest retained run for one seat under a book (#637 / #747).
787
+ * Same walk surface as findRunDirectoryById. Match by parent run path (officer
788
+ * seats, #747) or by ticket number (countersign / diarist principal).
752
789
  * runId is UUIDv7 — lexicographic max is latest. No parallel index.
753
790
  * Only a truly missing runs directory means no history; damage/permission errors propagate.
754
791
  */
@@ -772,9 +809,20 @@ export async function findLatestRunIdForSeatTicket(input) {
772
809
  const runId = entry.slice(0, entry.length - suffix.length);
773
810
  if (runId.length === 0)
774
811
  continue;
775
- const ticketNumber = await readRunTicketNumber(join(runsDir, entry));
776
- if (ticketNumber !== input.ticketNumber)
812
+ const runDirectory = join(runsDir, entry);
813
+ if (input.parentRunPath !== undefined) {
814
+ const parentPath = await readRunParentPath(runDirectory);
815
+ if (parentPath !== input.parentRunPath)
816
+ continue;
817
+ }
818
+ else if (input.ticketNumber !== undefined) {
819
+ const ticketNumber = await readRunTicketNumber(runDirectory);
820
+ if (ticketNumber !== input.ticketNumber)
821
+ continue;
822
+ }
823
+ else {
777
824
  continue;
825
+ }
778
826
  if (best === undefined || runId > best)
779
827
  best = runId;
780
828
  }
@@ -91,21 +91,30 @@ export async function resolveSeatTicketBinding(admitted, env = {}) {
91
91
  return resolution;
92
92
  }
93
93
  /**
94
- * Sole same-ticket → resume decision (#637 / #724).
95
- * Looks up the latest retained run for seat+ticket; when found, runs resume with
96
- * this summons' materials. Lookup/resume failures propagate (失败诚实) — never
97
- * wash into a fresh mint. Returns undefined when the caller declared an explicit
94
+ * Sole same-seat → resume decision (#637 / #724 / #747).
95
+ * Officer seats (notary/inspector/auditor) look up by parent run path; countersign
96
+ * / diarist keep ticket-number principal. When found, runs resume with this
97
+ * summons' materials. Lookup/resume failures propagate (失败诚实) — never wash
98
+ * into a fresh mint. Returns undefined when the caller declared an explicit
98
99
  * fresh summons (`ak-role new`) or when no prior run exists; both mint new.
99
100
  * freshSummons is required so no seat can drift back into its own skip branch.
100
101
  */
101
102
  export async function tryResumeSameTicketSeatRun(input) {
102
103
  if (input.freshSummons === true)
103
104
  return undefined;
105
+ if (input.parentRunPath === undefined && input.ticketNumber === undefined) {
106
+ return undefined;
107
+ }
104
108
  const previousRunId = await findLatestRunIdForSeatTicket({
105
109
  home: input.home,
106
110
  bookKey: resolveBookKeyFromGit(input.projectRoot),
107
111
  role: input.role,
108
- ticketNumber: input.ticketNumber,
112
+ ...(input.parentRunPath === undefined
113
+ ? {}
114
+ : { parentRunPath: input.parentRunPath }),
115
+ ...(input.ticketNumber === undefined
116
+ ? {}
117
+ : { ticketNumber: input.ticketNumber }),
109
118
  });
110
119
  if (previousRunId === undefined)
111
120
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3629",
3
+ "version": "0.1.3635",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -474,14 +474,12 @@ export async function prepareAcpRoleEnvelope(options: {
474
474
  });
475
475
  reply(socket, rpc.id, {
476
476
  content: projected.content,
477
- structuredContent: projected.details,
478
477
  isError: true,
479
478
  });
480
479
  } catch {
481
480
  // Slot already filled; durable projection may have partially failed.
482
481
  reply(socket, rpc.id, {
483
482
  content: declared.content,
484
- structuredContent: declared.details,
485
483
  isError: true,
486
484
  });
487
485
  }
@@ -497,7 +495,7 @@ export async function prepareAcpRoleEnvelope(options: {
497
495
  // Candidate only: do not emit turn_end here. Seal waits for the typed ACP
498
496
  // round boundary (closeRound after session/prompt), so delayed siblings stay
499
497
  // in the same round instead of becoming silent post-seal anomalies.
500
- reply(socket, rpc.id, { content: projected.content, structuredContent: projected.details, ...(projected.isError ? { isError: true } : {}) });
498
+ reply(socket, rpc.id, { content: projected.content, ...(projected.isError ? { isError: true } : {}) });
501
499
  } catch (error) {
502
500
  let content: ContentPart[];
503
501
  let details: Record<string, unknown>;
@@ -529,7 +527,7 @@ export async function prepareAcpRoleEnvelope(options: {
529
527
  details,
530
528
  isError: true,
531
529
  });
532
- reply(socket, rpc.id, { content: projected.content, structuredContent: projected.details, ...(projected.isError ? { isError: true } : {}) });
530
+ reply(socket, rpc.id, { content: projected.content, ...(projected.isError ? { isError: true } : {}) });
533
531
  }
534
532
  } catch (error) { reply(socket, rpc.id, undefined, error); }
535
533
  })();
@@ -146,7 +146,9 @@ function failureReason(error: unknown): string {
146
146
 
147
147
  function asStringArray(value: unknown): readonly string[] {
148
148
  if (!Array.isArray(value)) return [];
149
- return value.filter((item): item is string => typeof item === "string");
149
+ // Officer findings may be structured objects (category/law/evidence); the
150
+ // parent role must see them verbatim, not an empty list (#750 evidence).
151
+ return value.map((item) => (typeof item === "string" ? item : JSON.stringify(item)));
150
152
  }
151
153
 
152
154
  /** Serializable stand-in when the child tool call had no arguments object. */
@@ -128,6 +128,8 @@ export function notaryDecisiveFacts(output: NotaryOutput): Record<string, unknow
128
128
  if (status === "pass" || status === "bounce") {
129
129
  const findings = (output as { findings?: unknown }).findings;
130
130
  facts.findingsCount = Array.isArray(findings) ? findings.length : 0;
131
+ // The parent role reads the findings themselves; the count alone dropped them (#750).
132
+ facts.findings = Array.isArray(findings) ? findings : [];
131
133
  }
132
134
  if (status === "bounce") {
133
135
  facts.disposition = "rewrite";
@@ -3,7 +3,7 @@
3
3
  * coordinator → settle Terminal result (#568 / ADR 0074). Lawful releases:
4
4
  * pass/bounce/escalate. #633: manual resume continues the exact session. Dual path
5
5
  * with gate-province dispatch; this module is the direct command face.
6
- * #637: same-ticket re-summons resume the seat's previous run (no new run).
6
+ * #637 / #747: same-parent (卷宗指针) re-summons resume the seat's previous run.
7
7
  */
8
8
  import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
9
9
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
@@ -11,7 +11,6 @@ import { CliUsageError } from "./cli-errors.ts";
11
11
  import {
12
12
  applyInstructionTicketProbe,
13
13
  probeInstructionTicket,
14
- ticketNumberFromProbe,
15
14
  tryResumeSameTicketSeatRun,
16
15
  } from "./seat-ticket-binding.ts";
17
16
  import {
@@ -31,6 +30,7 @@ import {
31
30
  import {
32
31
  loadResumableInspectorRun,
33
32
  markRunAdmitted,
33
+ parentRunPathFromGatePointerInstruction,
34
34
  type PublicResumeRequest,
35
35
  type SameTicketSummonsMaterials,
36
36
  } from "./run-lifecycle.ts";
@@ -87,7 +87,7 @@ export async function runPublicInspector(
87
87
  throw error;
88
88
  }
89
89
 
90
- // #637: same ticket → resume prior inspector run with this summons' materials.
90
+ // #747: same parent (卷宗指针) → resume prior inspector run with this summons' materials.
91
91
  // Probe captures DiaristTicketResolutionError so admit+beforeDispatch can settle
92
92
  // controlled failure (bare pre-admit throw skips terminal settlement).
93
93
  // No bare catch→fresh: lookup/resume failures surface; only true absence mints new.
@@ -97,8 +97,8 @@ export async function runPublicInspector(
97
97
  projectRoot,
98
98
  env,
99
99
  );
100
- const probedTicketNumber = ticketNumberFromProbe(ticketProbe);
101
- if (probedTicketNumber !== undefined) {
100
+ const parentRunPath = parentRunPathFromGatePointerInstruction(parsed.instruction);
101
+ if (parentRunPath !== undefined) {
102
102
  const summons: SameTicketSummonsMaterials = {
103
103
  instruction: parsed.instruction,
104
104
  instructionEmpty: parsed.instruction.trim() === "",
@@ -108,7 +108,7 @@ export async function runPublicInspector(
108
108
  home: env.home,
109
109
  projectRoot,
110
110
  role: "inspector",
111
- ticketNumber: probedTicketNumber,
111
+ parentRunPath,
112
112
  freshSummons: env.freshSummons,
113
113
  summons,
114
114
  resume: (runId, materials) =>
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Shared instruction-seat run (#639 / #675 / #637): gatekeeper, navigator, auditor,
3
3
  * and evidence-child share admit → turn-request → post-admission → settle.
4
- * Same-ticket re-summons resume the seat's previous run via the shared public
5
- * tryResumeSameTicketSeatRun seam (inspector/notary/countersign face) — no
6
- * independent run/rebind/nest path.
4
+ * Auditor same-parent (--source-run) re-summons resume via tryResumeSameTicketSeatRun
5
+ * (#747); other instruction seats keep ticket-number principal — no independent
6
+ * run/rebind/nest path.
7
7
  */
8
8
  import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
9
9
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
@@ -16,6 +16,7 @@ import {
16
16
  admitNavigatorInvocation,
17
17
  bindAdmittedTicketNumber,
18
18
  buildInstructionTransportPrompt,
19
+ persistAdmittedSourceRunPath,
19
20
  type AdmittedAuditorInvocation,
20
21
  type AdmittedEvidenceChildInvocation,
21
22
  type AdmittedGatekeeperInvocation,
@@ -258,10 +259,9 @@ export async function runPublicInstructionSeat(
258
259
  let auditorSourceTicket: number | undefined;
259
260
  let ticketProbe: InstructionTicketProbe | undefined;
260
261
 
261
- // #637: same ticket → resume prior seat run with this summons' materials.
262
- // Auditor inherits ticket from source-run (notary face); other instruction seats
263
- // probe instruction (inspector face). No bare catch→fresh: lookup/resume failures
264
- // surface; only true absence of a prior run mints new.
262
+ // #637 / #747: resume prior seat run with this summons' materials.
263
+ // Auditor (#747): parent --source-run path is the lookup key.
264
+ // Other instruction seats: ticket probe (inspector face). No bare catch→fresh.
265
265
  const projectRoot = parsed.project ?? env.cwd;
266
266
  if (role === "auditor") {
267
267
  if (parsed.subject !== "judge" && parsed.subject !== "doctor") {
@@ -305,27 +305,21 @@ export async function runPublicInstructionSeat(
305
305
  );
306
306
  }
307
307
 
308
- const probedTicketNumber =
309
- auditorSourceTicket
310
- ?? (ticketProbe === undefined ? undefined : ticketNumberFromProbe(ticketProbe));
311
-
312
- if (probedTicketNumber !== undefined) {
313
- const summons: SameTicketSummonsMaterials = {
314
- instruction: parsed.instruction,
315
- instructionEmpty: parsed.instruction.trim() === "",
316
- attachmentPaths: parsed.attachmentPaths,
317
- };
308
+ const summons: SameTicketSummonsMaterials = {
309
+ instruction: parsed.instruction,
310
+ instructionEmpty: parsed.instruction.trim() === "",
311
+ attachmentPaths: parsed.attachmentPaths,
312
+ };
313
+ if (role === "auditor" && auditorSourceRun !== undefined) {
318
314
  const resumed = await withAuditorSoulEnv({
319
315
  ...(auditorSubject === undefined ? {} : { subject: auditorSubject }),
320
- ...(auditorSourceRun === undefined
321
- ? {}
322
- : { sourceRunDirectory: auditorSourceRun }),
316
+ sourceRunDirectory: auditorSourceRun,
323
317
  run: () =>
324
318
  tryResumeSameTicketSeatRun({
325
319
  home: env.home,
326
320
  projectRoot,
327
321
  role,
328
- ticketNumber: probedTicketNumber,
322
+ parentRunPath: auditorSourceRun,
329
323
  freshSummons: env.freshSummons,
330
324
  summons,
331
325
  resume: (runId, materials) =>
@@ -337,6 +331,26 @@ export async function runPublicInstructionSeat(
337
331
  }),
338
332
  });
339
333
  if (resumed !== undefined) return resumed;
334
+ } else {
335
+ const probedTicketNumber =
336
+ ticketProbe === undefined ? undefined : ticketNumberFromProbe(ticketProbe);
337
+ if (probedTicketNumber !== undefined) {
338
+ const resumed = await tryResumeSameTicketSeatRun({
339
+ home: env.home,
340
+ projectRoot,
341
+ role,
342
+ ticketNumber: probedTicketNumber,
343
+ freshSummons: env.freshSummons,
344
+ summons,
345
+ resume: (runId, materials) =>
346
+ runPublicInstructionSeatResume(
347
+ { runId, ...(materials === undefined ? {} : { summons: materials }) },
348
+ env,
349
+ io,
350
+ ),
351
+ });
352
+ if (resumed !== undefined) return resumed;
353
+ }
340
354
  }
341
355
 
342
356
  let admitted: AdmittedInstructionSeatInvocation;
@@ -360,6 +374,10 @@ export async function runPublicInstructionSeat(
360
374
  throw error;
361
375
  }
362
376
 
377
+ if (role === "auditor" && auditorSourceRun !== undefined) {
378
+ await persistAdmittedSourceRunPath(admitted, auditorSourceRun);
379
+ }
380
+
363
381
  return await withAuditorSoulEnv({
364
382
  ...(auditorSubject === undefined ? {} : { subject: auditorSubject }),
365
383
  ...(auditorSourceRun === undefined
@@ -437,6 +437,35 @@ async function mergeInvocationIdentityPage(
437
437
  );
438
438
  }
439
439
 
440
+ /**
441
+ * Persist parent --source-run path onto admitted-request for officer resume lookup (#747).
442
+ * Reuses the existing notary sourceRunPath key; does not invent a new field name.
443
+ */
444
+ export async function persistAdmittedSourceRunPath(
445
+ admitted: AdmittedRoleInvocation,
446
+ sourceRunPath: string,
447
+ ): Promise<void> {
448
+ if (sourceRunPath.trim() === "") {
449
+ throw new Error("persistAdmittedSourceRunPath requires a non-empty sourceRunPath");
450
+ }
451
+ const admittedPath = admitted.admittedRequestPath;
452
+ const current = JSON.parse(await readFile(admittedPath, "utf8")) as Record<
453
+ string,
454
+ unknown
455
+ >;
456
+ if (typeof current.sourceRunPath === "string") {
457
+ if (current.sourceRunPath === sourceRunPath) return;
458
+ throw new Error(
459
+ `persistAdmittedSourceRunPath refuses to replace ${current.sourceRunPath} with ${sourceRunPath}`,
460
+ );
461
+ }
462
+ await writeFile(
463
+ admittedPath,
464
+ `${JSON.stringify({ ...current, sourceRunPath }, null, 2)}\n`,
465
+ "utf8",
466
+ );
467
+ }
468
+
440
469
  /**
441
470
  * Bind a post-admission resolved ticketNumber onto the in-memory admitted
442
471
  * object and both durable pages (invocation.json + admitted-request.json).
@@ -2,7 +2,7 @@
2
2
  * Public Notary Role run: admit source-run locator → shared post-admission coordinator
3
3
  * → settle Terminal result (#448 / #517). Zero caller prompt/attachment. Lifecycle is
4
4
  * the shared post-admission seam; this module keeps only Notary adapters.
5
- * #637: same-ticket re-summons resume the seat's previous run (no new run).
5
+ * #637 / #747: same-parent (--source-run) re-summons resume the seat's previous run.
6
6
  */
7
7
  import { existsSync } from "node:fs";
8
8
 
@@ -13,7 +13,6 @@ import {
13
13
  resolveNotarySourceRunLocator,
14
14
  } from "../notary-source-run.ts";
15
15
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
16
- import { readRunTicketNumber } from "../run-ticket-number.ts";
17
16
  import { CliUsageError } from "./cli-errors.ts";
18
17
  import {
19
18
  admitNotaryInvocation,
@@ -112,8 +111,8 @@ export async function runPublicNotary(
112
111
  }
113
112
  throw error;
114
113
  }
115
- const ticketNumber = await readRunTicketNumber(source.runDirectory);
116
- if (ticketNumber !== undefined) {
114
+ // #747: officer resume key is this parent source-run path (not ticket number).
115
+ {
117
116
  const summons: SameTicketSummonsMaterials = {
118
117
  sourceRunPath: source.runDirectory,
119
118
  sourceRun: source,
@@ -122,7 +121,7 @@ export async function runPublicNotary(
122
121
  home: env.home,
123
122
  projectRoot,
124
123
  role: "notary",
125
- ticketNumber,
124
+ parentRunPath: source.runDirectory,
126
125
  freshSummons: env.freshSummons,
127
126
  summons,
128
127
  resume: (runId, materials) =>
@@ -1052,9 +1052,52 @@ export async function findRunDirectoryById(
1052
1052
  return undefined;
1053
1053
  }
1054
1054
 
1055
+ /** Code-owned gate inspector summons prefix (public-role-summons / #747). */
1056
+ export const GATE_DOSSIER_POINTER_PREFIX = "卷宗指针:" as const;
1057
+
1058
+ /** Parent path from a code-owned inspector 卷宗指针 instruction; else undefined. */
1059
+ export function parentRunPathFromGatePointerInstruction(
1060
+ instruction: string,
1061
+ ): string | undefined {
1062
+ if (!instruction.startsWith(GATE_DOSSIER_POINTER_PREFIX)) return undefined;
1063
+ const path = instruction.slice(GATE_DOSSIER_POINTER_PREFIX.length).trim();
1064
+ return path === "" ? undefined : path;
1065
+ }
1066
+
1055
1067
  /**
1056
- * Locate the latest retained run for one seat+ticket under a book (#637).
1057
- * Same walk surface as findRunDirectoryById; ticket identity from durable pages.
1068
+ * Parent-run binding on a retained officer run (#747).
1069
+ * Notary/auditor: typed sourceRunPath. Inspector: exact code-owned 卷宗指针 instruction.
1070
+ * Missing page → undefined; damage / non-ENOENT IO propagates.
1071
+ */
1072
+ export async function readRunParentPath(
1073
+ runDirectory: string,
1074
+ ): Promise<string | undefined> {
1075
+ let raw: unknown;
1076
+ try {
1077
+ raw = JSON.parse(
1078
+ await readFile(join(runDirectory, "admitted-request.json"), "utf8"),
1079
+ );
1080
+ } catch (error) {
1081
+ if (errorCodeOf(error) === "ENOENT") return undefined;
1082
+ throw error;
1083
+ }
1084
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
1085
+ return undefined;
1086
+ }
1087
+ const record = raw as Record<string, unknown>;
1088
+ if (typeof record.sourceRunPath === "string" && record.sourceRunPath.trim() !== "") {
1089
+ return record.sourceRunPath;
1090
+ }
1091
+ if (typeof record.instruction === "string") {
1092
+ return parentRunPathFromGatePointerInstruction(record.instruction);
1093
+ }
1094
+ return undefined;
1095
+ }
1096
+
1097
+ /**
1098
+ * Locate the latest retained run for one seat under a book (#637 / #747).
1099
+ * Same walk surface as findRunDirectoryById. Match by parent run path (officer
1100
+ * seats, #747) or by ticket number (countersign / diarist principal).
1058
1101
  * runId is UUIDv7 — lexicographic max is latest. No parallel index.
1059
1102
  * Only a truly missing runs directory means no history; damage/permission errors propagate.
1060
1103
  */
@@ -1062,7 +1105,8 @@ export async function findLatestRunIdForSeatTicket(input: {
1062
1105
  readonly home: string;
1063
1106
  readonly bookKey: string;
1064
1107
  readonly role: RoleRunRecord["role"];
1065
- readonly ticketNumber: number;
1108
+ readonly ticketNumber?: number;
1109
+ readonly parentRunPath?: string;
1066
1110
  }): Promise<string | undefined> {
1067
1111
  const ledgerHome = resolveActivationLedgerHome(input.home);
1068
1112
  const runsDir = join(
@@ -1082,8 +1126,16 @@ export async function findLatestRunIdForSeatTicket(input: {
1082
1126
  if (!entry.endsWith(suffix)) continue;
1083
1127
  const runId = entry.slice(0, entry.length - suffix.length);
1084
1128
  if (runId.length === 0) continue;
1085
- const ticketNumber = await readRunTicketNumber(join(runsDir, entry));
1086
- if (ticketNumber !== input.ticketNumber) continue;
1129
+ const runDirectory = join(runsDir, entry);
1130
+ if (input.parentRunPath !== undefined) {
1131
+ const parentPath = await readRunParentPath(runDirectory);
1132
+ if (parentPath !== input.parentRunPath) continue;
1133
+ } else if (input.ticketNumber !== undefined) {
1134
+ const ticketNumber = await readRunTicketNumber(runDirectory);
1135
+ if (ticketNumber !== input.ticketNumber) continue;
1136
+ } else {
1137
+ continue;
1138
+ }
1087
1139
  if (best === undefined || runId > best) best = runId;
1088
1140
  }
1089
1141
  return best;
@@ -149,10 +149,11 @@ export async function resolveSeatTicketBinding(
149
149
  }
150
150
 
151
151
  /**
152
- * Sole same-ticket → resume decision (#637 / #724).
153
- * Looks up the latest retained run for seat+ticket; when found, runs resume with
154
- * this summons' materials. Lookup/resume failures propagate (失败诚实) — never
155
- * wash into a fresh mint. Returns undefined when the caller declared an explicit
152
+ * Sole same-seat → resume decision (#637 / #724 / #747).
153
+ * Officer seats (notary/inspector/auditor) look up by parent run path; countersign
154
+ * / diarist keep ticket-number principal. When found, runs resume with this
155
+ * summons' materials. Lookup/resume failures propagate (失败诚实) — never wash
156
+ * into a fresh mint. Returns undefined when the caller declared an explicit
156
157
  * fresh summons (`ak-role new`) or when no prior run exists; both mint new.
157
158
  * freshSummons is required so no seat can drift back into its own skip branch.
158
159
  */
@@ -160,7 +161,8 @@ export async function tryResumeSameTicketSeatRun<T>(input: {
160
161
  readonly home: string;
161
162
  readonly projectRoot: string;
162
163
  readonly role: RoleRunRecord["role"];
163
- readonly ticketNumber: number;
164
+ readonly ticketNumber?: number;
165
+ readonly parentRunPath?: string;
164
166
  readonly freshSummons: true | undefined;
165
167
  readonly summons?: SameTicketSummonsMaterials;
166
168
  readonly resume: (
@@ -169,11 +171,19 @@ export async function tryResumeSameTicketSeatRun<T>(input: {
169
171
  ) => Promise<T>;
170
172
  }): Promise<T | undefined> {
171
173
  if (input.freshSummons === true) return undefined;
174
+ if (input.parentRunPath === undefined && input.ticketNumber === undefined) {
175
+ return undefined;
176
+ }
172
177
  const previousRunId = await findLatestRunIdForSeatTicket({
173
178
  home: input.home,
174
179
  bookKey: resolveBookKeyFromGit(input.projectRoot),
175
180
  role: input.role,
176
- ticketNumber: input.ticketNumber,
181
+ ...(input.parentRunPath === undefined
182
+ ? {}
183
+ : { parentRunPath: input.parentRunPath }),
184
+ ...(input.ticketNumber === undefined
185
+ ? {}
186
+ : { ticketNumber: input.ticketNumber }),
177
187
  });
178
188
  if (previousRunId === undefined) return undefined;
179
189
  return await input.resume(previousRunId, input.summons);