@akagilnc/pi-workflow-roles 0.1.3652 → 0.1.3663

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.
@@ -1,10 +1,18 @@
1
+ /**
2
+ * Public Notary Role run: admit source-run locator → shared post-admission coordinator
3
+ * → settle Terminal result (#448 / #517). Zero caller prompt/attachment. Lifecycle is
4
+ * the shared post-admission seam; this module keeps only Notary adapters.
5
+ * #637 / #747: same-parent (--source-run) re-summons resume the seat's previous run.
6
+ */
7
+ import { existsSync } from "node:fs";
8
+ import { gateSubmissionCandidatePath } from "../auditor-dossier-tool.js";
1
9
  import { NotarySourceRunError, resolveNotarySourceRunLocator, } from "../notary-source-run.js";
2
10
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.js";
3
11
  import { CliUsageError } from "./cli-errors.js";
4
12
  import { admitNotaryInvocation, buildNotaryTransportPrompt, } from "./invocation.js";
5
13
  import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.js";
6
14
  import { runPostAdmissionOneShot, runPostAdmissionSeatResume, resumeTurnRequestProjectionOptions, } from "./post-admission.js";
7
- import { loadResumableNotaryRun, markRunAdmitted, } from "./run-lifecycle.js";
15
+ import { appendResumeMaterialReread, loadResumableNotaryRun, markRunAdmitted, } from "./run-lifecycle.js";
8
16
  import { presentStructuralRejection, trySettleNotaryTerminalResult, } from "./settlement.js";
9
17
  import { projectRoleTurnRequest, } from "./turn-request.js";
10
18
  /** Project admitted invocation onto the host-neutral turn request. */
@@ -153,10 +161,17 @@ export async function runPublicNotaryResume(request, env, io) {
153
161
  }
154
162
  return loaded;
155
163
  },
156
- buildTurnRequest: (admitted, effective) =>
157
- // #755: review continuation is plain dialogue / resume envelope — no
158
- //「重新读」candidate line, no engine handbook packaging.
159
- buildNotaryTurnRequest(admitted, resumeTurnRequestProjectionOptions(admitted, effective, env)),
164
+ buildTurnRequest: (admitted, effective) => {
165
+ const projection = resumeTurnRequestProjectionOptions(admitted, effective, env);
166
+ const candidatePath = gateSubmissionCandidatePath(admitted.sourceRunPath);
167
+ const prompt = existsSync(candidatePath)
168
+ ? appendResumeMaterialReread(projection.continuation.prompt, candidatePath)
169
+ : projection.continuation.prompt;
170
+ return buildNotaryTurnRequest(admitted, {
171
+ ...projection,
172
+ continuation: { kind: "resume", prompt },
173
+ });
174
+ },
160
175
  adapters: notaryAdapters(),
161
176
  ...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
162
177
  });
@@ -8,10 +8,19 @@
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { readFile, writeFile } from "node:fs/promises";
10
10
  import { isAbsolute, join, resolve } from "node:path";
11
- import { buildResumeContinuationPrompt, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
11
+ import { buildResumeContinuationPrompt, instructResumeHandbookRead, } from "./run-lifecycle.js";
12
12
  import { CliUsageError } from "./cli-errors.js";
13
13
  import { buildInstructionTransportPrompt, freezeAttachmentsIntoRun, } from "./invocation.js";
14
14
  import { pathContainedIn } from "../activation-ledger-topology.js";
15
+ import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.js";
16
+ import { projectCaseDossierPointerSection } from "./case-dossier-delivery.js";
17
+ /** Append one system section to a continuation prompt, keeping its kind. */
18
+ function appendContinuationSection(continuation, section) {
19
+ const prompt = `${continuation.prompt}\n\n${section}`;
20
+ return continuation.kind === "initial"
21
+ ? { kind: "initial", prompt }
22
+ : { kind: "resume", prompt };
23
+ }
15
24
  import { projectHostTransitionPriorNative } from "../host-transition-prior-native.js";
16
25
  import { missingCredentialPreDispatchFailure, postRunMissingCredentialFailure, } from "./public-run-credentials.js";
17
26
  import { acquireRunWriterLease, clearCurrentCourt, clearTypedProviderHttpObservation, markRunResumable, markRunRunning, markRunTerminal, readCurrentCourt, recordCurrentCourt, renderResumeCommand, RunWriterLeaseHeldError, } from "./run-lifecycle.js";
@@ -115,20 +124,17 @@ export async function dispatchPostAdmissionTurn(input) {
115
124
  const principalCoordinates = admitted.principal === undefined
116
125
  ? undefined
117
126
  : env.principalAuthority.decode(admitted.principal);
118
- let turnRequest;
127
+ let hostTransition;
119
128
  try {
120
129
  previousHost = await readInvocationHost(admitted.runDirectory);
121
- const hostTransition = previousHost !== undefined && liveHost !== undefined && principalCoordinates !== undefined
122
- ? await projectHostTransitionPriorNative({
123
- previousHost,
124
- liveHost,
125
- piSessionFile: principalCoordinates.sessionFile,
126
- })
127
- : undefined;
128
- turnRequest = env.signal === undefined ? request : { ...request, signal: env.signal };
129
- if (hostTransition !== undefined) {
130
- turnRequest = { ...turnRequest, hostTransition };
131
- }
130
+ hostTransition =
131
+ previousHost !== undefined && liveHost !== undefined && principalCoordinates !== undefined
132
+ ? await projectHostTransitionPriorNative({
133
+ previousHost,
134
+ liveHost,
135
+ piSessionFile: principalCoordinates.sessionFile,
136
+ })
137
+ : undefined;
132
138
  }
133
139
  catch (error) {
134
140
  // prior-native IO is on the public one-shot path — controlled failure, not bare throw.
@@ -156,6 +162,27 @@ export async function dispatchPostAdmissionTurn(input) {
156
162
  }, adapters, env.principalAuthority, io));
157
163
  }
158
164
  }
165
+ // Turn request is assembled after beforeDispatch so this turn sees whatever it
166
+ // settled — the seat's ticket bind re-projection. Case dossier delivery
167
+ // (ADR 0081) rides here for every public entry: first call, same-ticket
168
+ // re-summons and manual resume alike. System refs append their own neutral
169
+ // section; caller frozen attachments and the seat's own prompt bytes are
170
+ // never rewritten.
171
+ let turnRequest = env.signal === undefined ? request : { ...request, signal: env.signal };
172
+ if (hostTransition !== undefined) {
173
+ turnRequest = { ...turnRequest, hostTransition };
174
+ }
175
+ const dossierSection = await projectCaseDossierPointerSection({
176
+ ticketNumber: admitted.ticketNumber,
177
+ projectRoot: admitted.projectRoot,
178
+ home: env.home,
179
+ });
180
+ if (dossierSection !== undefined) {
181
+ turnRequest = {
182
+ ...turnRequest,
183
+ continuation: appendContinuationSection(turnRequest.continuation, dossierSection),
184
+ };
185
+ }
159
186
  let result;
160
187
  try {
161
188
  result = await env.roleTurnHost.executeTurn(turnRequest);
@@ -232,37 +259,36 @@ export async function dispatchPostAdmissionTurn(input) {
232
259
  }
233
260
  }
234
261
  /**
235
- * Shared resume continuation projection (#471 / #600 / #633 / #637 / #755):
236
- * seat-table model/engine/timeout axes, restored correlation, and either
237
- * - manual resume (no same-ticket summons): package envelope / optional caller
238
- * message, with engine-axis handbook via buildResumeContinuationPrompt, or
239
- * - same-ticket summons (审核循环续话): caller/peer words + optional frozen
240
- * attachment paths only — no「重新读」、no engine handbook packaging
241
- * (#750/#755), whether or not attachments are present.
242
- * Caller message wins as prompt base when supplied (bytes unchanged, including
243
- * blank/whitespace); else summons instruction. Attachment projection must not
244
- * re-interpret the caller message as instructionEmpty.
262
+ * Shared resume continuation projection (#471 / #600 / #633 / #637): seat-table
263
+ * model/engine/timeout axes, restored correlation, and either
264
+ * - manual resume: package envelope / optional caller message (unchanged), or
265
+ * - same-ticket summons: this turn's instruction + frozen attachment paths.
266
+ * Caller message and summons materials each keep their place: a resume message
267
+ * keeps manual-resume prompt semantics while open-court frozen attachments still
268
+ * ride; summons instruction is only the prompt base when no caller message.
245
269
  * Seats add only their activation projection. Call prepareSummonsResumeMaterials
246
- * first when request.summons carries instruction or attachment paths.
270
+ * first when request.summons carries attachment paths.
247
271
  */
248
272
  export function resumeTurnRequestProjectionOptions(admitted, request, env, summonsPrepared) {
273
+ const engineMaterial = engineSessionMaterialFromOptions({
274
+ ...(env.engine === undefined ? {} : { engine: env.engine }),
275
+ packageRoot: env.packageRoot,
276
+ });
249
277
  let prompt;
250
278
  if (request.message !== undefined) {
251
- if (summonsPrepared !== undefined) {
252
- // #755: same-ticket review / open-court — caller words + optional paths.
253
- // Attachments are not a gate: message-only summons must stay plain too.
254
- prompt = buildInstructionTransportPrompt({
279
+ // Manual resume caller-message semantics stay authoritative for the prompt:
280
+ // message present → base bytes unchanged (including blank/whitespace).
281
+ // Open-court frozen attachments still continue when present; attachment
282
+ // projection must not re-interpret the caller message as instructionEmpty.
283
+ if (summonsPrepared !== undefined &&
284
+ summonsPrepared.attachments.length > 0) {
285
+ prompt = instructResumeHandbookRead(buildInstructionTransportPrompt({
255
286
  instruction: request.message,
256
287
  instructionEmpty: false,
257
288
  attachments: summonsPrepared.attachments,
258
- });
259
- }
260
- else if (request.summons !== undefined) {
261
- // #755: same-ticket summons without prepared materials — caller words only.
262
- prompt = request.message;
289
+ }, engineMaterial), engineMaterial);
263
290
  }
264
291
  else {
265
- // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
266
292
  prompt = buildResumeContinuationPrompt({
267
293
  packageRoot: env.packageRoot,
268
294
  ...(env.engine === undefined ? {} : { engine: env.engine }),
@@ -271,16 +297,9 @@ export function resumeTurnRequestProjectionOptions(admitted, request, env, summo
271
297
  }
272
298
  }
273
299
  else if (summonsPrepared !== undefined) {
274
- // #755: same-ticket review summons — instruction/attachments only.
275
- prompt = buildInstructionTransportPrompt(summonsPrepared);
276
- }
277
- else if (request.summons !== undefined) {
278
- // #755: same-ticket summons with no instruction/attachments (e.g. notary
279
- // source-run pointer) — plain resume envelope, no handbook / 重新读.
280
- prompt = RESUME_TRANSPORT_ENVELOPE;
300
+ prompt = instructResumeHandbookRead(buildInstructionTransportPrompt(summonsPrepared, engineMaterial), engineMaterial);
281
301
  }
282
302
  else {
283
- // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
284
303
  prompt = buildResumeContinuationPrompt({
285
304
  packageRoot: env.packageRoot,
286
305
  ...(env.engine === undefined ? {} : { engine: env.engine }),
@@ -32,10 +32,11 @@ function resumeRereadInstruction(materialPath, handbook) {
32
32
  : `重新读 ${materialPath}`;
33
33
  }
34
34
  /**
35
- * Engine-axis resume-only material lines (#736 / ADR 0069·0071).
36
- * #755 withdrew this rewrite from 审核循环续话; review same-ticket resume stays
37
- * plain dialogue via resumeTurnRequestProjectionOptions. Outsourcing resume
38
- * still rewrites neutral handbook/path pointers into 重新读 instructions.
35
+ * Resume-only material lines (#736 ticket exemption to ADR 0073 §3).
36
+ * Neutral `- <absolute path>` pointers already on the continuation
37
+ * (handbook from appendEngineSessionMaterial; frozen attachments from
38
+ * instruction-seat transport) become reread instructions.
39
+ * Handbook keeps the argv suffix; other materials are path-only.
39
40
  * First-round delivery is unchanged; never pastes material body.
40
41
  */
41
42
  export function instructResumeHandbookRead(prompt, engineMaterial) {
@@ -55,9 +56,16 @@ export function instructResumeHandbookRead(prompt, engineMaterial) {
55
56
  })
56
57
  .join("\n");
57
58
  }
59
+ /** Append a live-path reread instruction when the pointer is not already on the continuation. */
60
+ export function appendResumeMaterialReread(prompt, materialPath) {
61
+ const instruction = resumeRereadInstruction(materialPath, false);
62
+ const lines = prompt.split("\n");
63
+ if (lines.includes(instruction))
64
+ return prompt;
65
+ return prompt.length === 0 ? instruction : `${prompt}\n${instruction}`;
66
+ }
58
67
  /**
59
- * Unique continuation-prompt selector for manual/auto engine-axis resume
60
- * (#471 / #600 / #736). Not used for 审核循环 same-ticket summons (#755).
68
+ * Unique continuation-prompt selector for manual/auto resume (#471 / #600 / #736).
61
69
  * Message present → base bytes unchanged; absent → package transport envelope.
62
70
  * When engine material is present, append structured engine coordinates then
63
71
  * rewrite absolute material pointer lines into 重新读 instructions. Zero parse,
@@ -819,6 +827,30 @@ export async function findLatestRunIdForSeatTicket(input) {
819
827
  }
820
828
  return best;
821
829
  }
830
+ /**
831
+ * Ticket identities this book's retained runs already record (#709).
832
+ * Read-only reuse surface for seat ticket binding — no ticket number is minted here.
833
+ */
834
+ export async function collectBookRunTicketNumbers(input) {
835
+ const ledgerHome = resolveActivationLedgerHome(input.home);
836
+ const runsDir = join(activationBookDirectory(ledgerHome, input.bookKey), "runs");
837
+ let entries;
838
+ try {
839
+ entries = await readdir(runsDir);
840
+ }
841
+ catch (error) {
842
+ if (errorCodeOf(error) === "ENOENT")
843
+ return new Set();
844
+ throw error;
845
+ }
846
+ const known = new Set();
847
+ for (const entry of entries) {
848
+ const ticketNumber = await readRunTicketNumber(join(runsDir, entry));
849
+ if (ticketNumber !== undefined)
850
+ known.add(ticketNumber);
851
+ }
852
+ return known;
853
+ }
822
854
  /** Restore optional correlation + typed ticket identity from a durable admitted page. */
823
855
  function parsePersistedTicketIdentity(record) {
824
856
  const correlationId = typeof record.correlationId === "string" && record.correlationId.trim() !== ""
@@ -829,20 +861,15 @@ function parsePersistedTicketIdentity(record) {
829
861
  record.ticketNumber >= 1
830
862
  ? record.ticketNumber
831
863
  : undefined;
832
- const ticketResolution = record.ticketResolution === "true-unbound" ? "true-unbound" : undefined;
833
864
  return {
834
865
  ...(correlationId === undefined ? {} : { correlationId }),
835
866
  ...(ticketNumber === undefined ? {} : { ticketNumber }),
836
- ...(ticketResolution === undefined ? {} : { ticketResolution }),
837
867
  };
838
868
  }
839
869
  function restoredTicketFields(fields) {
840
870
  return {
841
871
  ...(fields.correlationId === undefined ? {} : { correlationId: fields.correlationId }),
842
872
  ...(fields.ticketNumber === undefined ? {} : { ticketNumber: fields.ticketNumber }),
843
- ...(fields.ticketResolution === undefined
844
- ? {}
845
- : { ticketResolution: fields.ticketResolution }),
846
873
  };
847
874
  }
848
875
  async function loadResumableRunRecord(home, runId, authority) {
@@ -880,7 +907,6 @@ async function loadResumableRunRecord(home, runId, authority) {
880
907
  let derived;
881
908
  let correlationId;
882
909
  let ticketNumber;
883
- let ticketResolution;
884
910
  let prNumber;
885
911
  let repository;
886
912
  let repositoryDisplay;
@@ -1016,7 +1042,6 @@ async function loadResumableRunRecord(home, runId, authority) {
1016
1042
  const fromAdmitted = parsePersistedTicketIdentity(record);
1017
1043
  correlationId = fromAdmitted.correlationId;
1018
1044
  ticketNumber = fromAdmitted.ticketNumber;
1019
- ticketResolution = fromAdmitted.ticketResolution;
1020
1045
  }
1021
1046
  }
1022
1047
  catch (error) {
@@ -1041,17 +1066,12 @@ async function loadResumableRunRecord(home, runId, authority) {
1041
1066
  : {}),
1042
1067
  };
1043
1068
  }
1044
- if (correlationId === undefined ||
1045
- ticketNumber === undefined ||
1046
- ticketResolution === undefined) {
1069
+ if (correlationId === undefined || ticketNumber === undefined) {
1047
1070
  const fromInvocation = parsePersistedTicketIdentity(rec);
1048
1071
  if (correlationId === undefined)
1049
1072
  correlationId = fromInvocation.correlationId;
1050
1073
  if (ticketNumber === undefined)
1051
1074
  ticketNumber = fromInvocation.ticketNumber;
1052
- if (ticketResolution === undefined) {
1053
- ticketResolution = fromInvocation.ticketResolution;
1054
- }
1055
1075
  }
1056
1076
  }
1057
1077
  }
@@ -1081,7 +1101,6 @@ async function loadResumableRunRecord(home, runId, authority) {
1081
1101
  ...(derived === undefined ? {} : { derived }),
1082
1102
  ...(correlationId === undefined ? {} : { correlationId }),
1083
1103
  ...(ticketNumber === undefined ? {} : { ticketNumber }),
1084
- ...(ticketResolution === undefined ? {} : { ticketResolution }),
1085
1104
  ...(prNumber === undefined ? {} : { prNumber }),
1086
1105
  ...(repository === undefined ? {} : { repository }),
1087
1106
  ...(repositoryDisplay === undefined ? {} : { repositoryDisplay }),
@@ -1,94 +1,117 @@
1
1
  /**
2
- * Shared LLM ticket binding for public court seats (#635).
3
- * One path: unbound admission → diarist instruction assertion → bindAdmittedTicketNumber.
2
+ * Shared ticket identity seam for public court seats (#635 / #637 / #709 / #747).
3
+ * One path: unbound admission → reuse a ticket number this book already records
4
+ * (retained run pages + 起居录 volumes) → bindAdmittedTicketNumber.
5
+ * ADR 0081 `reuse-case-ticket-without-extra-llm`: no seat-side model call, no
6
+ * second ticket-number source of truth, no minting a number from human titles.
7
+ * Nothing matched is not 真无票 — the run simply stays unbound, which is lawful.
4
8
  * No CLI --ticket and no attachment frontmatter binding.
5
- * #637: same-ticket prior-run lookup → resume decision also lives here (shared seam).
9
+ * #747: officer same-parent resume also lives here (shared seam).
6
10
  */
7
- import { resolveBookKeyFromGit } from "../activation-ledger-git.js";
8
- import { createGhTicketExistenceChecker, createHermesDiaristTicketResolver, DiaristTicketResolutionError, resolveDiaristTicketFromInstruction, } from "../diarist-ticket-resolution.js";
9
- import { resolveDiaristGithubOrigin } from "../diarist.js";
10
- import { bindAdmittedTicketNumber, recordTrueUnboundTicketResolution, } from "./invocation.js";
11
- import { findLatestRunIdForSeatTicket, } from "./run-lifecycle.js";
11
+ import { existsSync } from "node:fs";
12
+ import { ActivationGitRepositoryRequiredError, resolveBookKeyFromGit, } from "../activation-ledger-git.js";
13
+ import { resolveTicketProvenanceVolume } from "../ticket-provenance.js";
14
+ import { bindAdmittedTicketNumber, } from "./invocation.js";
15
+ import { collectBookRunTicketNumbers, findLatestRunIdForSeatTicket, } from "./run-lifecycle.js";
12
16
  /**
13
- * Instruction → ticket resolution without admission (#635 / #637).
14
- * Used to locate a prior same-ticket run before minting a new one.
15
- * Throws on resolution failure — prefer probeInstructionTicket at seat entry.
17
+ * Complete decimal ticket tokens in an instruction.
18
+ * A digit run is one token: `82` inside `#582` is not 82, and a leading zero
19
+ * makes the run a different literal than the ticket number it would parse to.
16
20
  */
17
- export async function resolveInstructionTicket(instruction, projectRoot, env = {}) {
18
- const resolver = createHermesDiaristTicketResolver({
19
- ...(env.packageRoot === undefined ? {} : { packageRoot: env.packageRoot }),
20
- cwd: projectRoot,
21
- });
22
- const checkExistence = createGhTicketExistenceChecker();
23
- const origin = resolveDiaristGithubOrigin(projectRoot);
24
- return resolveDiaristTicketFromInstruction({
25
- instruction,
26
- origin,
27
- resolver,
28
- checkExistence,
29
- });
21
+ export function instructionTicketTokens(instruction) {
22
+ const tokens = new Set();
23
+ for (const run of instruction.match(/\d+/g) ?? []) {
24
+ if (run.startsWith("0"))
25
+ continue;
26
+ const parsed = Number(run);
27
+ if (!Number.isSafeInteger(parsed) || parsed < 1)
28
+ continue;
29
+ tokens.add(parsed);
30
+ }
31
+ return [...tokens];
30
32
  }
31
33
  /**
32
- * Pre-admit probe: resolution success or captured DiaristTicketResolutionError.
33
- * Other errors still propagate (launch/infrastructure outside this class).
34
+ * Ticket identity this summons reuses, or undefined when none is unambiguous.
35
+ * Known identities come only from records this book already holds: 起居录 volume
36
+ * partitions and ticket numbers on retained run pages. Zero or several known
37
+ * tokens in the instruction leave the run unbound — never guess, never fail.
34
38
  */
35
- export async function probeInstructionTicket(instruction, projectRoot, env = {}) {
36
- try {
37
- const resolution = await resolveInstructionTicket(instruction, projectRoot, env);
38
- return { kind: "resolved", resolution };
39
- }
40
- catch (error) {
41
- if (error instanceof DiaristTicketResolutionError) {
42
- return { kind: "failed", error };
39
+ export async function resolveKnownTicketNumber(input) {
40
+ const tokens = instructionTicketTokens(input.instruction);
41
+ if (tokens.length === 0)
42
+ return undefined;
43
+ const known = [];
44
+ let runTickets;
45
+ for (const token of tokens) {
46
+ const volume = resolveTicketProvenanceVolume(token, input.projectRoot, input.home);
47
+ if (existsSync(volume.volumeDir)) {
48
+ known.push(token);
49
+ continue;
43
50
  }
44
- throw error;
51
+ if (runTickets === undefined) {
52
+ runTickets = await readBookRunTickets(input);
53
+ }
54
+ if (runTickets.has(token))
55
+ known.push(token);
45
56
  }
57
+ return known.length === 1 ? known[0] : undefined;
46
58
  }
47
- /** Ticket number from a successful ticket probe; undefined for unbound/failed. */
48
- export function ticketNumberFromProbe(probe) {
49
- if (probe.kind !== "resolved")
50
- return undefined;
51
- if (probe.resolution.kind !== "ticket")
52
- return undefined;
53
- return probe.resolution.ticketNumber;
59
+ /**
60
+ * Diarist first-summons identity (ADR 0081 `initial-court-ticket-supplied`).
61
+ * Prefer a book-known reuse; when the book has no record yet, the sole complete
62
+ * decimal token in the caller's dispatch is the supplied ticket — the working
63
+ * 起居郎 round establishes the volume. Several tokens or none stay unbound.
64
+ * Not a second ticket-number source of truth and not an extra recognizer call.
65
+ */
66
+ export async function resolveDiaristSummonsTicketNumber(input) {
67
+ const known = await resolveKnownTicketNumber(input);
68
+ if (known !== undefined)
69
+ return known;
70
+ const tokens = instructionTicketTokens(input.instruction);
71
+ return tokens.length === 1 ? tokens[0] : undefined;
54
72
  }
55
73
  /**
56
- * Sole disposition of a resolved ticket onto an unbound admission (#635 / #637).
57
- * Countersign / inspector / resolveSeatTicketBinding share this — no parallel branches.
74
+ * Ticket numbers on this book's retained runs.
75
+ * A directory with no book (not a git repository) simply holds no run history —
76
+ * admission owns that rejection face, this lookup does not pre-empt it.
58
77
  */
59
- export async function applyTicketResolution(admitted, resolution) {
60
- if (resolution.kind === "ticket") {
61
- await bindAdmittedTicketNumber(admitted, resolution.ticketNumber);
78
+ async function readBookRunTickets(input) {
79
+ let bookKey;
80
+ try {
81
+ bookKey = input.bookKey ?? resolveBookKeyFromGit(input.projectRoot);
62
82
  }
63
- else {
64
- await recordTrueUnboundTicketResolution(admitted);
83
+ catch (error) {
84
+ if (error instanceof ActivationGitRepositoryRequiredError)
85
+ return new Set();
86
+ throw error;
65
87
  }
88
+ return await collectBookRunTicketNumbers({ home: input.home, bookKey });
66
89
  }
67
- /**
68
- * Apply a pre-admit probe onto an unbound admission.
69
- * Failed probes rethrow — caller must be inside the controlled-failure boundary
70
- * (post-admission beforeDispatch), never before admit/markRunning.
71
- */
72
- export async function applyInstructionTicketProbe(admitted, probe) {
73
- if (probe.kind === "failed")
74
- throw probe.error;
75
- await applyTicketResolution(admitted, probe.resolution);
90
+ /** Bind a reused ticket number onto an admission that is still unbound. */
91
+ export async function bindReusedTicketNumber(admitted, ticketNumber) {
92
+ if (ticketNumber === undefined)
93
+ return;
94
+ if (admitted.ticketNumber !== undefined)
95
+ return;
96
+ await bindAdmittedTicketNumber(admitted, ticketNumber);
76
97
  }
77
98
  /**
78
- * Bind ticketNumber onto an unbound admitted seat via LLM instruction recognition.
79
- * Already-settled admissions (ticketNumber or durable true-unbound) short-circuit —
80
- * no re-entry into hermes on auto-resume attempts or manual resume (#635).
81
- * true-unbound is persisted once; failed verification throws.
99
+ * Sole seat disposition for an unbound admission (#635 / #709).
100
+ * Already-bound admissions short-circuit — resume keeps the identity it has.
101
+ * Used by seats that do not pre-resolve for same-ticket resume; the seats that
102
+ * do (countersign / inspector / diarist) reuse the number they already resolved.
82
103
  */
83
- export async function resolveSeatTicketBinding(admitted, env = {}) {
104
+ export async function resolveSeatTicketBinding(admitted, env) {
84
105
  if (admitted.ticketNumber !== undefined)
85
- return undefined;
86
- if (admitted.ticketResolution === "true-unbound") {
87
- return { kind: "true-unbound" };
88
- }
89
- const resolution = await resolveInstructionTicket(admitted.instruction, admitted.projectRoot, env);
90
- await applyTicketResolution(admitted, resolution);
91
- return resolution;
106
+ return admitted.ticketNumber;
107
+ const ticketNumber = await resolveKnownTicketNumber({
108
+ instruction: admitted.instruction,
109
+ projectRoot: admitted.projectRoot,
110
+ home: env.home,
111
+ bookKey: admitted.bookKey,
112
+ });
113
+ await bindReusedTicketNumber(admitted, ticketNumber);
114
+ return ticketNumber;
92
115
  }
93
116
  /**
94
117
  * Sole same-seat → resume decision (#637 / #724 / #747).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3652",
3
+ "version": "0.1.3663",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 系统随案递送本票起居录的中立指针段(ADR 0081 `automatic-case-material`;
3
+ * 指针输入沿 ADR 0079 `summons-pointer-input`,不把卷宗正文塞进提示词)。
4
+ * 只读已有案卷:不刷新、不生成、不校验内容、不新增拒收或停工条件。
5
+ * 机器文本仅中立标识材料(ADR 0073),用途说明归角色材料所有。
6
+ */
7
+ import { stat } from "node:fs/promises";
8
+
9
+ import { resolveTicketProvenanceVolume } from "../ticket-provenance.ts";
10
+
11
+ /** Section heading of the system-delivered dossier pointer. */
12
+ export const CASE_DOSSIER_SECTION_HEADING = "## 本票起居录(系统随案提供)" as const;
13
+
14
+ /** Honest one-line state of one dossier file: present, absent, or unreadable. */
15
+ async function describeDossierFile(path: string): Promise<string> {
16
+ try {
17
+ const stats = await stat(path);
18
+ if (!stats.isFile()) return `不可读(非普通文件):${path}`;
19
+ return path;
20
+ } catch (error) {
21
+ const code = (error as NodeJS.ErrnoException).code;
22
+ if (code === "ENOENT") return `尚未生成:${path}`;
23
+ return `不可读(${code ?? "未知错误"}):${path}`;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Pointer section for a bound ticket's existing 起居录, or undefined when the
29
+ * run carries no ticket identity (unbound calls stay legal and get no dossier).
30
+ * A bound ticket whose volume is missing or unreadable is stated as such —
31
+ * the section never claims a dossier that is not there.
32
+ */
33
+ export async function projectCaseDossierPointerSection(input: {
34
+ readonly ticketNumber: number | undefined;
35
+ readonly projectRoot: string;
36
+ readonly home: string;
37
+ }): Promise<string | undefined> {
38
+ if (input.ticketNumber === undefined) return undefined;
39
+ const volume = resolveTicketProvenanceVolume(
40
+ input.ticketNumber,
41
+ input.projectRoot,
42
+ input.home,
43
+ );
44
+ return [
45
+ CASE_DOSSIER_SECTION_HEADING,
46
+ "",
47
+ `票号:#${input.ticketNumber}`,
48
+ `人读视图:${await describeDossierFile(volume.humanViewFile)}`,
49
+ `记录卷宗:${await describeDossierFile(volume.recordFile)}`,
50
+ ].join("\n");
51
+ }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Public Countersign Role run: admit ticket materials → shared post-admission
3
3
  * coordinator → settle Terminal result (#572 / ADR 0074). #599: manual resume
4
- * continues the exact session. Unbound admission resolves its ticket via the
5
- * shared seat LLM bind (#635). Who runs 起居郎, and when, is the caller's
6
- * composition (ADR 0010 / ADR 0075 `no-call-rule`) — not this seat's business.
4
+ * continues the exact session. Unbound admission reuses a known ticket identity
5
+ * (#709). Who runs 起居郎, and when, is the caller's composition
6
+ * (ADR 0010 / ADR 0075 `no-call-rule`) — not this seat's business.
7
7
  */
8
8
  import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
9
9
  import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
@@ -15,9 +15,8 @@ import {
15
15
  type ParseCountersignArgvResult,
16
16
  } from "./invocation.ts";
17
17
  import {
18
- applyInstructionTicketProbe,
19
- probeInstructionTicket,
20
- ticketNumberFromProbe,
18
+ bindReusedTicketNumber,
19
+ resolveKnownTicketNumber,
21
20
  tryResumeSameTicketSeatRun,
22
21
  } from "./seat-ticket-binding.ts";
23
22
  import {
@@ -91,17 +90,15 @@ export async function runPublicCountersign(
91
90
  }
92
91
 
93
92
  // #637: same ticket → resume prior countersign run with this summons' materials.
94
- // Probe captures DiaristTicketResolutionError so admit+beforeDispatch can settle
95
- // controlled failure (bare pre-admit throw skips terminal settlement).
93
+ // #709: identity is reused from records this book already holds — no seat model call.
96
94
  // No bare catch→fresh: lookup/resume failures surface; only true absence mints new.
97
95
  const projectRoot = parsed.project ?? env.cwd;
98
- const ticketProbe = await probeInstructionTicket(
99
- parsed.instruction,
96
+ const reusedTicketNumber = await resolveKnownTicketNumber({
97
+ instruction: parsed.instruction,
100
98
  projectRoot,
101
- env,
102
- );
103
- const probedTicketNumber = ticketNumberFromProbe(ticketProbe);
104
- if (probedTicketNumber !== undefined) {
99
+ home: env.home,
100
+ });
101
+ if (reusedTicketNumber !== undefined) {
105
102
  const summons: SameTicketSummonsMaterials = {
106
103
  instruction: parsed.instruction,
107
104
  instructionEmpty: parsed.instruction.trim() === "",
@@ -111,7 +108,7 @@ export async function runPublicCountersign(
111
108
  home: env.home,
112
109
  projectRoot,
113
110
  role: "countersign",
114
- ticketNumber: probedTicketNumber,
111
+ ticketNumber: reusedTicketNumber,
115
112
  freshSummons: env.freshSummons,
116
113
  summons,
117
114
  resume: (runId, materials) =>
@@ -178,8 +175,8 @@ export async function runPublicCountersign(
178
175
  request: turnRequest,
179
176
  adapters: countersignAdapters({
180
177
  beforeDispatch: async (admitted) => {
181
- // #635/#637: apply pre-admit probe inside controlled-failure boundary.
182
- await applyInstructionTicketProbe(admitted, ticketProbe);
178
+ // #635/#709: bind the reused identity inside the controlled-failure boundary.
179
+ await bindReusedTicketNumber(admitted, reusedTicketNumber);
183
180
  Object.assign(
184
181
  turnRequest,
185
182
  buildCountersignTurnRequest(admitted, turnProjection),