@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.
- package/README.md +0 -2
- package/dist/acp-host/production-host.js +1100 -1256
- package/dist/public-cli/case-dossier-delivery.js +43 -0
- package/dist/public-cli/inspector-run.js +9 -6
- package/dist/public-cli/instruction-seat-run.js +24 -24
- package/dist/public-cli/invocation.js +2 -24
- package/dist/public-cli/main.js +1936 -2189
- package/dist/public-cli/notary-run.js +20 -5
- package/dist/public-cli/post-admission.js +61 -42
- package/dist/public-cli/run-lifecycle.js +39 -20
- package/dist/public-cli/seat-ticket-binding.js +93 -70
- package/package.json +1 -1
- package/src/public-cli/case-dossier-delivery.ts +51 -0
- package/src/public-cli/countersign-run.ts +14 -17
- package/src/public-cli/diarist-run.ts +11 -13
- package/src/public-cli/inspector-run.ts +9 -10
- package/src/public-cli/instruction-seat-run.ts +28 -35
- package/src/public-cli/invocation.ts +4 -46
- package/src/public-cli/notary-run.ts +18 -6
- package/src/public-cli/post-admission.ts +78 -37
- package/src/public-cli/run-lifecycle.ts +46 -27
- package/src/public-cli/seat-ticket-binding.ts +108 -104
- package/dist/diarist-ticket-resolution.js +0 -177
- package/resources/diarist-resolve-ticket.md +0 -39
- package/src/diarist-ticket-resolution.ts +0 -302
|
@@ -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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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,
|
|
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
|
|
127
|
+
let hostTransition;
|
|
119
128
|
try {
|
|
120
129
|
previousHost = await readInvocationHost(admitted.runDirectory);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
236
|
-
*
|
|
237
|
-
* - manual resume
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
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
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
|
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
|
|
3
|
-
* One path: unbound admission →
|
|
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
|
-
* #
|
|
9
|
+
* #747: officer same-parent resume also lives here (shared seam).
|
|
6
10
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { bindAdmittedTicketNumber,
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
*
|
|
33
|
-
*
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
*
|
|
57
|
-
*
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
async function readBookRunTickets(input) {
|
|
79
|
+
let bookKey;
|
|
80
|
+
try {
|
|
81
|
+
bookKey = input.bookKey ?? resolveBookKeyFromGit(input.projectRoot);
|
|
62
82
|
}
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
*
|
|
79
|
-
* Already-
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
@@ -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
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
//
|
|
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
|
|
99
|
-
parsed.instruction,
|
|
96
|
+
const reusedTicketNumber = await resolveKnownTicketNumber({
|
|
97
|
+
instruction: parsed.instruction,
|
|
100
98
|
projectRoot,
|
|
101
|
-
env,
|
|
102
|
-
);
|
|
103
|
-
|
|
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:
|
|
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/#
|
|
182
|
-
await
|
|
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),
|