@akagilnc/pi-workflow-roles 0.1.3631 → 0.1.3637

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.
@@ -4707,6 +4707,33 @@ async function findRunDirectoryById(home, runId) {
4707
4707
  }
4708
4708
  return void 0;
4709
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
+ }
4710
4737
  async function findLatestRunIdForSeatTicket(input) {
4711
4738
  const ledgerHome = resolveActivationLedgerHome(input.home);
4712
4739
  const runsDir = join11(
@@ -4726,8 +4753,16 @@ async function findLatestRunIdForSeatTicket(input) {
4726
4753
  if (!entry.endsWith(suffix)) continue;
4727
4754
  const runId = entry.slice(0, entry.length - suffix.length);
4728
4755
  if (runId.length === 0) continue;
4729
- const ticketNumber = await readRunTicketNumber(join11(runsDir, entry));
4730
- 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
+ }
4731
4766
  if (best === void 0 || runId > best) best = runId;
4732
4767
  }
4733
4768
  return best;
@@ -5074,7 +5109,7 @@ async function loadResumableInspectorRun(home, runId, authority) {
5074
5109
  };
5075
5110
  return seatLoadedResult(loaded, admitted);
5076
5111
  }
5077
- 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;
5078
5113
  var init_run_lifecycle = __esm({
5079
5114
  "src/public-cli/run-lifecycle.ts"() {
5080
5115
  "use strict";
@@ -5099,6 +5134,7 @@ var init_run_lifecycle = __esm({
5099
5134
  }
5100
5135
  };
5101
5136
  WRITER_LEASE_RECLAIM_ROUNDS = 3;
5137
+ GATE_DOSSIER_POINTER_PREFIX = "\u5377\u5B97\u6307\u9488\uFF1A";
5102
5138
  }
5103
5139
  });
5104
5140
 
@@ -6288,6 +6324,7 @@ __export(invocation_exports, {
6288
6324
  parseNotaryArgv: () => parseNotaryArgv,
6289
6325
  parsePositiveTicketNumber: () => parsePositiveTicketNumber,
6290
6326
  parseReviewerArgv: () => parseReviewerArgv,
6327
+ persistAdmittedSourceRunPath: () => persistAdmittedSourceRunPath,
6291
6328
  recordEffectiveInvocationModel: () => recordEffectiveInvocationModel,
6292
6329
  recordLaunchedPiIdentity: () => recordLaunchedPiIdentity,
6293
6330
  recordLaunchedRolePackageIdentity: () => recordLaunchedRolePackageIdentity,
@@ -6403,6 +6440,25 @@ async function mergeInvocationIdentityPage(runDirectory, fields) {
6403
6440
  "utf8"
6404
6441
  );
6405
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
+ }
6406
6462
  async function bindAdmittedTicketNumber(admitted, ticketNumber) {
6407
6463
  if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
6408
6464
  throw new Error(`bindAdmittedTicketNumber requires a safe positive integer, got ${String(ticketNumber)}`);
@@ -10537,11 +10593,15 @@ async function resolveSeatTicketBinding(admitted, env = {}) {
10537
10593
  }
10538
10594
  async function tryResumeSameTicketSeatRun(input) {
10539
10595
  if (input.freshSummons === true) return void 0;
10596
+ if (input.parentRunPath === void 0 && input.ticketNumber === void 0) {
10597
+ return void 0;
10598
+ }
10540
10599
  const previousRunId = await findLatestRunIdForSeatTicket({
10541
10600
  home: input.home,
10542
10601
  bookKey: resolveBookKeyFromGit(input.projectRoot),
10543
10602
  role: input.role,
10544
- ticketNumber: input.ticketNumber
10603
+ ...input.parentRunPath === void 0 ? {} : { parentRunPath: input.parentRunPath },
10604
+ ...input.ticketNumber === void 0 ? {} : { ticketNumber: input.ticketNumber }
10545
10605
  });
10546
10606
  if (previousRunId === void 0) return void 0;
10547
10607
  return await input.resume(previousRunId, input.summons);
@@ -11117,7 +11177,7 @@ function createSubmissionLedgerHost(host, outputTools, failInfrastructure2 = (er
11117
11177
  });
11118
11178
  rounds.set(attemptId, candidates2);
11119
11179
  return {
11120
- content: [],
11180
+ content: result.content,
11121
11181
  details: { submissionDisposition: "pending-round-closure" }
11122
11182
  };
11123
11183
  }
@@ -11133,7 +11193,7 @@ function createSubmissionLedgerHost(host, outputTools, failInfrastructure2 = (er
11133
11193
  candidates.push({ toolCallId, toolName: tool.name, role, result, context });
11134
11194
  rounds.set(attemptId, candidates);
11135
11195
  return {
11136
- content: [],
11196
+ content: result.content,
11137
11197
  details: { submissionDisposition: "pending-round-closure" }
11138
11198
  };
11139
11199
  }
@@ -16962,8 +17022,7 @@ async function runPublicNotary(argv, env, io, parseNotaryArgv2) {
16962
17022
  }
16963
17023
  throw error;
16964
17024
  }
16965
- const ticketNumber = await readRunTicketNumber(source.runDirectory);
16966
- if (ticketNumber !== void 0) {
17025
+ {
16967
17026
  const summons = {
16968
17027
  sourceRunPath: source.runDirectory,
16969
17028
  sourceRun: source
@@ -16972,7 +17031,7 @@ async function runPublicNotary(argv, env, io, parseNotaryArgv2) {
16972
17031
  home: env.home,
16973
17032
  projectRoot,
16974
17033
  role: "notary",
16975
- ticketNumber,
17034
+ parentRunPath: source.runDirectory,
16976
17035
  freshSummons: env.freshSummons,
16977
17036
  summons,
16978
17037
  resume: (runId, materials) => runPublicNotaryResume(
@@ -17087,7 +17146,6 @@ var init_notary_run = __esm({
17087
17146
  init_auditor_dossier_tool();
17088
17147
  init_notary_source_run();
17089
17148
  init_engine_material();
17090
- init_run_ticket_number();
17091
17149
  init_cli_errors();
17092
17150
  init_invocation();
17093
17151
  init_seat_ticket_binding();
@@ -17133,8 +17191,8 @@ async function runPublicInspector(argv, env, io, parseInspectorArgv2) {
17133
17191
  projectRoot,
17134
17192
  env
17135
17193
  );
17136
- const probedTicketNumber = ticketNumberFromProbe(ticketProbe);
17137
- if (probedTicketNumber !== void 0) {
17194
+ const parentRunPath = parentRunPathFromGatePointerInstruction(parsed.instruction);
17195
+ if (parentRunPath !== void 0) {
17138
17196
  const summons = {
17139
17197
  instruction: parsed.instruction,
17140
17198
  instructionEmpty: parsed.instruction.trim() === "",
@@ -17144,7 +17202,7 @@ async function runPublicInspector(argv, env, io, parseInspectorArgv2) {
17144
17202
  home: env.home,
17145
17203
  projectRoot,
17146
17204
  role: "inspector",
17147
- ticketNumber: probedTicketNumber,
17205
+ parentRunPath,
17148
17206
  freshSummons: env.freshSummons,
17149
17207
  summons,
17150
17208
  resume: (runId, materials) => runPublicInspectorResume(
@@ -17429,21 +17487,20 @@ async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
17429
17487
  env
17430
17488
  );
17431
17489
  }
17432
- const probedTicketNumber = auditorSourceTicket ?? (ticketProbe === void 0 ? void 0 : ticketNumberFromProbe(ticketProbe));
17433
- if (probedTicketNumber !== void 0) {
17434
- const summons = {
17435
- instruction: parsed.instruction,
17436
- instructionEmpty: parsed.instruction.trim() === "",
17437
- attachmentPaths: parsed.attachmentPaths
17438
- };
17490
+ const summons = {
17491
+ instruction: parsed.instruction,
17492
+ instructionEmpty: parsed.instruction.trim() === "",
17493
+ attachmentPaths: parsed.attachmentPaths
17494
+ };
17495
+ if (role === "auditor" && auditorSourceRun !== void 0) {
17439
17496
  const resumed = await withAuditorSoulEnv({
17440
17497
  ...auditorSubject === void 0 ? {} : { subject: auditorSubject },
17441
- ...auditorSourceRun === void 0 ? {} : { sourceRunDirectory: auditorSourceRun },
17498
+ sourceRunDirectory: auditorSourceRun,
17442
17499
  run: () => tryResumeSameTicketSeatRun({
17443
17500
  home: env.home,
17444
17501
  projectRoot,
17445
17502
  role,
17446
- ticketNumber: probedTicketNumber,
17503
+ parentRunPath: auditorSourceRun,
17447
17504
  freshSummons: env.freshSummons,
17448
17505
  summons,
17449
17506
  resume: (runId, materials) => runPublicInstructionSeatResume(
@@ -17454,6 +17511,24 @@ async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
17454
17511
  })
17455
17512
  });
17456
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
+ }
17457
17532
  }
17458
17533
  let admitted;
17459
17534
  try {
@@ -17475,6 +17550,9 @@ async function runPublicInstructionSeat(argv, env, io, role, parseArgv) {
17475
17550
  }
17476
17551
  throw error;
17477
17552
  }
17553
+ if (role === "auditor" && auditorSourceRun !== void 0) {
17554
+ await persistAdmittedSourceRunPath(admitted, auditorSourceRun);
17555
+ }
17478
17556
  return await withAuditorSoulEnv({
17479
17557
  ...auditorSubject === void 0 ? {} : { subject: auditorSubject },
17480
17558
  ...auditorSourceRun === void 0 ? {} : { sourceRunDirectory: auditorSourceRun },
@@ -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).
@@ -18001,6 +18001,33 @@ async function findRunDirectoryById(home, runId) {
18001
18001
  }
18002
18002
  return void 0;
18003
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
+ }
18004
18031
  async function findLatestRunIdForSeatTicket(input) {
18005
18032
  const ledgerHome = resolveActivationLedgerHome(input.home);
18006
18033
  const runsDir = join12(
@@ -18020,8 +18047,16 @@ async function findLatestRunIdForSeatTicket(input) {
18020
18047
  if (!entry.endsWith(suffix)) continue;
18021
18048
  const runId = entry.slice(0, entry.length - suffix.length);
18022
18049
  if (runId.length === 0) continue;
18023
- const ticketNumber = await readRunTicketNumber(join12(runsDir, entry));
18024
- 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
+ }
18025
18060
  if (best === void 0 || runId > best) best = runId;
18026
18061
  }
18027
18062
  return best;
@@ -18578,7 +18613,7 @@ async function peekRoleRunRole(home, runId) {
18578
18613
  const run = await readRoleRunIdentity(runDirectory);
18579
18614
  return run?.role;
18580
18615
  }
18581
- 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;
18582
18617
  var init_run_lifecycle = __esm({
18583
18618
  "src/public-cli/run-lifecycle.ts"() {
18584
18619
  "use strict";
@@ -18603,6 +18638,7 @@ var init_run_lifecycle = __esm({
18603
18638
  }
18604
18639
  };
18605
18640
  WRITER_LEASE_RECLAIM_ROUNDS = 3;
18641
+ GATE_DOSSIER_POINTER_PREFIX = "\u5377\u5B97\u6307\u9488\uFF1A";
18606
18642
  }
18607
18643
  });
18608
18644
 
@@ -19899,6 +19935,25 @@ async function mergeInvocationIdentityPage(runDirectory, fields) {
19899
19935
  "utf8"
19900
19936
  );
19901
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
+ }
19902
19957
  async function bindAdmittedTicketNumber(admitted, ticketNumber) {
19903
19958
  if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
19904
19959
  throw new Error(`bindAdmittedTicketNumber requires a safe positive integer, got ${String(ticketNumber)}`);
@@ -23783,11 +23838,15 @@ async function resolveSeatTicketBinding(admitted, env = {}) {
23783
23838
  }
23784
23839
  async function tryResumeSameTicketSeatRun(input) {
23785
23840
  if (input.freshSummons === true) return void 0;
23841
+ if (input.parentRunPath === void 0 && input.ticketNumber === void 0) {
23842
+ return void 0;
23843
+ }
23786
23844
  const previousRunId = await findLatestRunIdForSeatTicket({
23787
23845
  home: input.home,
23788
23846
  bookKey: resolveBookKeyFromGit(input.projectRoot),
23789
23847
  role: input.role,
23790
- ticketNumber: input.ticketNumber
23848
+ ...input.parentRunPath === void 0 ? {} : { parentRunPath: input.parentRunPath },
23849
+ ...input.ticketNumber === void 0 ? {} : { ticketNumber: input.ticketNumber }
23791
23850
  });
23792
23851
  if (previousRunId === void 0) return void 0;
23793
23852
  return await input.resume(previousRunId, input.summons);
@@ -29437,21 +29496,20 @@ async function runPublicInstructionSeat(argv, env, io, role, parseArgv2) {
29437
29496
  env
29438
29497
  );
29439
29498
  }
29440
- const probedTicketNumber = auditorSourceTicket ?? (ticketProbe === void 0 ? void 0 : ticketNumberFromProbe(ticketProbe));
29441
- if (probedTicketNumber !== void 0) {
29442
- const summons = {
29443
- instruction: parsed.instruction,
29444
- instructionEmpty: parsed.instruction.trim() === "",
29445
- attachmentPaths: parsed.attachmentPaths
29446
- };
29499
+ const summons = {
29500
+ instruction: parsed.instruction,
29501
+ instructionEmpty: parsed.instruction.trim() === "",
29502
+ attachmentPaths: parsed.attachmentPaths
29503
+ };
29504
+ if (role === "auditor" && auditorSourceRun !== void 0) {
29447
29505
  const resumed = await withAuditorSoulEnv({
29448
29506
  ...auditorSubject === void 0 ? {} : { subject: auditorSubject },
29449
- ...auditorSourceRun === void 0 ? {} : { sourceRunDirectory: auditorSourceRun },
29507
+ sourceRunDirectory: auditorSourceRun,
29450
29508
  run: () => tryResumeSameTicketSeatRun({
29451
29509
  home: env.home,
29452
29510
  projectRoot,
29453
29511
  role,
29454
- ticketNumber: probedTicketNumber,
29512
+ parentRunPath: auditorSourceRun,
29455
29513
  freshSummons: env.freshSummons,
29456
29514
  summons,
29457
29515
  resume: (runId, materials) => runPublicInstructionSeatResume(
@@ -29462,6 +29520,24 @@ async function runPublicInstructionSeat(argv, env, io, role, parseArgv2) {
29462
29520
  })
29463
29521
  });
29464
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
+ }
29465
29541
  }
29466
29542
  let admitted;
29467
29543
  try {
@@ -29483,6 +29559,9 @@ async function runPublicInstructionSeat(argv, env, io, role, parseArgv2) {
29483
29559
  }
29484
29560
  throw error;
29485
29561
  }
29562
+ if (role === "auditor" && auditorSourceRun !== void 0) {
29563
+ await persistAdmittedSourceRunPath(admitted, auditorSourceRun);
29564
+ }
29486
29565
  return await withAuditorSoulEnv({
29487
29566
  ...auditorSubject === void 0 ? {} : { subject: auditorSubject },
29488
29567
  ...auditorSourceRun === void 0 ? {} : { sourceRunDirectory: auditorSourceRun },
@@ -30428,8 +30507,7 @@ async function runPublicNotary(argv, env, io, parseNotaryArgv2) {
30428
30507
  }
30429
30508
  throw error;
30430
30509
  }
30431
- const ticketNumber = await readRunTicketNumber(source.runDirectory);
30432
- if (ticketNumber !== void 0) {
30510
+ {
30433
30511
  const summons = {
30434
30512
  sourceRunPath: source.runDirectory,
30435
30513
  sourceRun: source
@@ -30438,7 +30516,7 @@ async function runPublicNotary(argv, env, io, parseNotaryArgv2) {
30438
30516
  home: env.home,
30439
30517
  projectRoot,
30440
30518
  role: "notary",
30441
- ticketNumber,
30519
+ parentRunPath: source.runDirectory,
30442
30520
  freshSummons: env.freshSummons,
30443
30521
  summons,
30444
30522
  resume: (runId, materials) => runPublicNotaryResume(
@@ -30553,7 +30631,6 @@ var init_notary_run = __esm({
30553
30631
  init_auditor_dossier_tool();
30554
30632
  init_notary_source_run();
30555
30633
  init_engine_material();
30556
- init_run_ticket_number();
30557
30634
  init_cli_errors();
30558
30635
  init_invocation();
30559
30636
  init_seat_ticket_binding();
@@ -30593,8 +30670,8 @@ async function runPublicInspector(argv, env, io, parseInspectorArgv2) {
30593
30670
  projectRoot,
30594
30671
  env
30595
30672
  );
30596
- const probedTicketNumber = ticketNumberFromProbe(ticketProbe);
30597
- if (probedTicketNumber !== void 0) {
30673
+ const parentRunPath = parentRunPathFromGatePointerInstruction(parsed.instruction);
30674
+ if (parentRunPath !== void 0) {
30598
30675
  const summons = {
30599
30676
  instruction: parsed.instruction,
30600
30677
  instructionEmpty: parsed.instruction.trim() === "",
@@ -30604,7 +30681,7 @@ async function runPublicInspector(argv, env, io, parseInspectorArgv2) {
30604
30681
  home: env.home,
30605
30682
  projectRoot,
30606
30683
  role: "inspector",
30607
- ticketNumber: probedTicketNumber,
30684
+ parentRunPath,
30608
30685
  freshSummons: env.freshSummons,
30609
30686
  summons,
30610
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;
@@ -361,8 +361,11 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
361
361
  },
362
362
  });
363
363
  rounds.set(attemptId, candidates);
364
+ // The role's own acceptance text must reach the model: an empty
365
+ // result reads as "nothing happened" on hosts without terminate
366
+ // semantics (grok build kept resubmitting, #750 evidence).
364
367
  return {
365
- content: [],
368
+ content: result.content,
366
369
  details: { submissionDisposition: "pending-round-closure" },
367
370
  };
368
371
  }
@@ -378,7 +381,7 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
378
381
  candidates.push({ toolCallId, toolName: tool.name, role, result, context });
379
382
  rounds.set(attemptId, candidates);
380
383
  return {
381
- content: [],
384
+ content: result.content,
382
385
  details: { submissionDisposition: "pending-round-closure" },
383
386
  };
384
387
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3631",
3
+ "version": "0.1.3637",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -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);
@@ -439,8 +439,11 @@ export function createSubmissionLedgerHost(
439
439
  },
440
440
  });
441
441
  rounds.set(attemptId, candidates);
442
+ // The role's own acceptance text must reach the model: an empty
443
+ // result reads as "nothing happened" on hosts without terminate
444
+ // semantics (grok build kept resubmitting, #750 evidence).
442
445
  return {
443
- content: [],
446
+ content: result.content,
444
447
  details: { submissionDisposition: "pending-round-closure" },
445
448
  };
446
449
  }
@@ -456,7 +459,7 @@ export function createSubmissionLedgerHost(
456
459
  candidates.push({ toolCallId, toolName: tool.name, role, result, context });
457
460
  rounds.set(attemptId, candidates);
458
461
  return {
459
- content: [],
462
+ content: result.content,
460
463
  details: { submissionDisposition: "pending-round-closure" },
461
464
  };
462
465
  },