@akagilnc/pi-workflow-roles 0.1.3671 → 0.1.3682
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/dist/acp-host/production-host.js +272 -166
- package/dist/diarist-contracts.js +4 -4
- package/dist/diarist-role.js +10 -4
- package/dist/diarist.js +121 -17
- package/dist/package-contracts/terminating-tools.js +1 -1
- package/dist/public-cli/inspector-run.js +3 -13
- package/dist/public-cli/instruction-seat-run.js +6 -31
- package/dist/public-cli/invocation.js +32 -0
- package/dist/public-cli/judge-run.js +1 -7
- package/dist/public-cli/main.js +721 -754
- package/dist/public-cli/run-lifecycle.js +0 -28
- package/dist/public-cli/seat-ticket-binding.js +16 -113
- package/dist/role-runtime.js +89 -10
- package/package.json +1 -1
- package/resources/diarist-collect.md +10 -0
- package/src/diarist-contracts.ts +21 -9
- package/src/diarist-role.ts +15 -4
- package/src/diarist.ts +192 -22
- package/src/package-contracts/terminating-tools.ts +1 -1
- package/src/public-cli/coder-run.ts +1 -7
- package/src/public-cli/countersign-run.ts +238 -86
- package/src/public-cli/diarist-run.ts +83 -28
- package/src/public-cli/fixer-run.ts +1 -7
- package/src/public-cli/inspector-run.ts +3 -17
- package/src/public-cli/instruction-seat-run.ts +6 -36
- package/src/public-cli/invocation.ts +52 -0
- package/src/public-cli/judge-run.ts +1 -7
- package/src/public-cli/run-lifecycle.ts +0 -31
- package/src/public-cli/seat-ticket-binding.ts +15 -146
- package/src/role-runtime.ts +107 -9
|
@@ -1,34 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Public Countersign Role run: admit ticket materials → court-pipeline prior
|
|
3
3
|
* station (起居郎) → shared post-admission coordinator → settle Terminal result
|
|
4
|
-
* (#572 / ADR 0074 / ADR 0075 / #742). #599: manual resume continues the
|
|
5
|
-
* session.
|
|
6
|
-
*
|
|
4
|
+
* (#572 / ADR 0074 / ADR 0075 / #742 / #771). #599: manual resume continues the
|
|
5
|
+
* exact session. ADR 0079: same-ticket re-summons resume the seat's previous run
|
|
6
|
+
* (`ticket-seat-memory-countersign-principal`); explicit `ak-role new` mints fresh
|
|
7
|
+
* (`explicit-fresh-summons`).
|
|
7
8
|
*
|
|
8
|
-
* Court admission auto-runs 起居郎
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 起居录 path delivery is owned once by
|
|
9
|
+
* Court admission auto-runs 起居郎 so the 起居郎 LLM asserts the court target;
|
|
10
|
+
* mechanical layer only verifies; countersign reuses that typed identity for bind
|
|
11
|
+
* and same-ticket resume lookup (ADR 0075 / 0081 / 0079). Code never matches
|
|
12
|
+
* instruction text against book-known numbers. Who may call 起居郎 and in what
|
|
13
|
+
* order is not written into law (ADR 0075 `no-call-rule`); the present admission
|
|
14
|
+
* effect is what this seat currently does. 起居录 path delivery is owned once by
|
|
15
|
+
* post-admission (#709 / ADR 0081).
|
|
16
|
+
*
|
|
17
|
+
* Wiring (#771): admit first so the countersign run exists; resolve typed identity
|
|
18
|
+
* from 起居郎 after admit; same-ticket resume uses that typed key (abandon the
|
|
19
|
+
* unused mint when resuming). 起居郎 escalate (认不出) and typed failure terminals
|
|
20
|
+
* (incl. verification failure) settle as countersign controlled failure — never
|
|
21
|
+
* wash into 真无票. Only a true missing lawful typed terminal stays unbound and
|
|
22
|
+
* continues the body (r5 unbound-continue). Bound refresh hands the typed key to
|
|
23
|
+
* 起居郎 so freeze loads issue face (`refresh-every-court` / typed handoff).
|
|
14
24
|
*/
|
|
15
25
|
import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
|
|
16
26
|
import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
|
|
17
27
|
import { CliUsageError } from "./cli-errors.ts";
|
|
18
28
|
import {
|
|
19
29
|
admitCountersignInvocation,
|
|
30
|
+
bindAdmittedTicketNumber,
|
|
20
31
|
buildCountersignTransportPrompt,
|
|
21
32
|
parseDiaristArgv,
|
|
22
33
|
type AdmittedCountersignInvocation,
|
|
23
34
|
type ParseCountersignArgvResult,
|
|
24
35
|
} from "./invocation.ts";
|
|
25
|
-
import {
|
|
26
|
-
bindReusedTicketNumber,
|
|
27
|
-
resolveKnownTicketNumber,
|
|
28
|
-
tryResumeSameTicketSeatRun,
|
|
29
|
-
} from "./seat-ticket-binding.ts";
|
|
30
36
|
import {
|
|
31
37
|
prepareSummonsResumeMaterials,
|
|
38
|
+
presentControlledFailure,
|
|
32
39
|
runPostAdmissionOneShot,
|
|
33
40
|
type PostAdmissionEnv,
|
|
34
41
|
runPostAdmissionSeatResume,
|
|
@@ -37,9 +44,11 @@ import {
|
|
|
37
44
|
import {
|
|
38
45
|
loadResumableCountersignRun,
|
|
39
46
|
markRunAdmitted,
|
|
47
|
+
markRunTerminal,
|
|
40
48
|
type PublicResumeRequest,
|
|
41
49
|
type SameTicketSummonsMaterials,
|
|
42
50
|
} from "./run-lifecycle.ts";
|
|
51
|
+
import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.ts";
|
|
43
52
|
import {
|
|
44
53
|
presentStructuralRejection,
|
|
45
54
|
trySettleCountersignTerminalResult,
|
|
@@ -83,28 +92,31 @@ export function buildCountersignTurnRequest(
|
|
|
83
92
|
);
|
|
84
93
|
}
|
|
85
94
|
|
|
95
|
+
/** 起居郎 identity outcome — escalate stays distinct from missing terminal. */
|
|
96
|
+
type CourtDiaristIdentity =
|
|
97
|
+
| { readonly kind: "ticket"; readonly ticketNumber: number }
|
|
98
|
+
| { readonly kind: "unbound" }
|
|
99
|
+
| { readonly kind: "escalate"; readonly reason: string };
|
|
100
|
+
|
|
86
101
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* the countersign body. Station failure propagates (失败诚实 — no wash).
|
|
95
|
-
* Path delivery onto materials is not this station's job — post-admission owns it.
|
|
102
|
+
* Invoke public 起居郎 under the court-pipeline quiet face.
|
|
103
|
+
* Returns typed identity: ticket assertion, true-unbound, or escalate.
|
|
104
|
+
* Non-zero exit without escalate status is not rethrown here — caller decides
|
|
105
|
+
* whether refresh must fail or first-entry settles controlled failure.
|
|
106
|
+
* When `boundTicketNumber` is set (typed handoff from countersign), diarist
|
|
107
|
+
* freezes under that key so issue face enters the catalog — never mechanical
|
|
108
|
+
* recognition from prose.
|
|
96
109
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
async function invokeCourtDiarist(input: {
|
|
111
|
+
readonly instruction: string;
|
|
112
|
+
readonly projectRoot: string;
|
|
113
|
+
readonly failureLabel: string;
|
|
114
|
+
/** Already-verified typed key from countersign (refresh / post-assert handoff). */
|
|
115
|
+
readonly boundTicketNumber?: number;
|
|
116
|
+
}, env: CountersignRunEnv, io: CliIo): Promise<{
|
|
117
|
+
readonly identity: CourtDiaristIdentity;
|
|
118
|
+
readonly failedWithoutEscalate?: { readonly diagnostic: string };
|
|
119
|
+
}> {
|
|
108
120
|
// Quiet face: the countersign caller must not see diarist CLI chatter.
|
|
109
121
|
const quietIo: CliIo = {
|
|
110
122
|
stdout() {},
|
|
@@ -116,11 +128,7 @@ export async function runCountersignCourtDiaristStation(
|
|
|
116
128
|
|
|
117
129
|
const { runPublicDiarist } = await import("./diarist-run.ts");
|
|
118
130
|
const result = await runPublicDiarist(
|
|
119
|
-
[
|
|
120
|
-
"--project",
|
|
121
|
-
admitted.projectRoot,
|
|
122
|
-
`整理 #${admitted.ticketNumber} 的本案依据。`,
|
|
123
|
-
],
|
|
131
|
+
["--project", input.projectRoot, input.instruction],
|
|
124
132
|
{
|
|
125
133
|
home: env.home,
|
|
126
134
|
agentDir: env.agentDir,
|
|
@@ -136,20 +144,95 @@ export async function runCountersignCourtDiaristStation(
|
|
|
136
144
|
...(env.credentials === undefined ? {} : { credentials: env.credentials }),
|
|
137
145
|
...(env.correlationId === undefined ? {} : { correlationId: env.correlationId }),
|
|
138
146
|
...(env.signal === undefined ? {} : { signal: env.signal }),
|
|
147
|
+
// Typed handoff only — never derived from summons prose.
|
|
148
|
+
...(input.boundTicketNumber === undefined
|
|
149
|
+
? {}
|
|
150
|
+
: { boundTicketNumber: input.boundTicketNumber }),
|
|
139
151
|
},
|
|
140
152
|
quietIo,
|
|
141
153
|
parseDiaristArgv,
|
|
142
154
|
);
|
|
143
155
|
|
|
156
|
+
const roleOutcome = result.terminal?.roleOutcome;
|
|
157
|
+
// Discriminated union: only non-failure arms carry status (failure uses cause).
|
|
158
|
+
if (roleOutcome !== undefined && roleOutcome.kind !== "failure") {
|
|
159
|
+
if (roleOutcome.status === "escalate") {
|
|
160
|
+
const reason =
|
|
161
|
+
typeof roleOutcome.decisiveFacts.reason === "string"
|
|
162
|
+
? roleOutcome.decisiveFacts.reason
|
|
163
|
+
: "diarist escalated without reason";
|
|
164
|
+
return { identity: { kind: "escalate", reason } };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
144
168
|
if (result.exitCode !== 0) {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
?
|
|
148
|
-
:
|
|
169
|
+
const diagnostic =
|
|
170
|
+
roleOutcome?.kind === "failure"
|
|
171
|
+
? roleOutcome.diagnostic
|
|
172
|
+
: `exit ${result.exitCode}`;
|
|
173
|
+
return {
|
|
174
|
+
identity: { kind: "unbound" },
|
|
175
|
+
failedWithoutEscalate: {
|
|
176
|
+
diagnostic: `court diarist station failed for ${input.failureLabel}: ${diagnostic}`,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const asserted = result.admitted?.ticketNumber;
|
|
182
|
+
if (
|
|
183
|
+
typeof asserted === "number" &&
|
|
184
|
+
Number.isSafeInteger(asserted) &&
|
|
185
|
+
asserted >= 1
|
|
186
|
+
) {
|
|
187
|
+
return { identity: { kind: "ticket", ticketNumber: asserted } };
|
|
188
|
+
}
|
|
189
|
+
return { identity: { kind: "unbound" } };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Court-pipeline prior station: refresh this ticket's 起居录 before the
|
|
194
|
+
* countersign body turn when already bound (ADR 0075 `refresh-every-court`).
|
|
195
|
+
* Caller-invisible — no diarist argv on the countersign command line.
|
|
196
|
+
*
|
|
197
|
+
* Missing ticketNumber (true-unbound / identity deferred) skips the refresh
|
|
198
|
+
* station — no diary is minted for a true-unbound run. First-entry identity
|
|
199
|
+
* lives on `runPublicCountersign` (typed 起居郎 key for bind + same-ticket
|
|
200
|
+
* resume). Bound refresh: 起居郎 failure propagates (失败诚实).
|
|
201
|
+
* Path delivery onto materials is not this station's job — post-admission owns it.
|
|
202
|
+
*/
|
|
203
|
+
export async function runCountersignCourtDiaristStation(
|
|
204
|
+
admitted: AdmittedCountersignInvocation,
|
|
205
|
+
env: CountersignRunEnv,
|
|
206
|
+
io: CliIo,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
if (env.runCourtDiaristStation !== undefined) {
|
|
209
|
+
await env.runCourtDiaristStation(admitted);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Production refresh-every-court only under a known ticket identity.
|
|
213
|
+
// First-entry unbound identity is owned by runPublicCountersign.
|
|
214
|
+
if (admitted.ticketNumber === undefined) return;
|
|
215
|
+
|
|
216
|
+
const outcome = await invokeCourtDiarist(
|
|
217
|
+
{
|
|
218
|
+
instruction: `整理 #${admitted.ticketNumber} 的本案依据。`,
|
|
219
|
+
projectRoot: admitted.projectRoot,
|
|
220
|
+
failureLabel: `ticket #${admitted.ticketNumber}`,
|
|
221
|
+
// Refresh holds a typed key — hand it off so freeze loads issue face.
|
|
222
|
+
boundTicketNumber: admitted.ticketNumber,
|
|
223
|
+
},
|
|
224
|
+
env,
|
|
225
|
+
io,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
if (outcome.identity.kind === "escalate") {
|
|
149
229
|
throw new Error(
|
|
150
|
-
`court diarist station
|
|
230
|
+
`court diarist station escalated (cannot identify court target): ${outcome.identity.reason}`,
|
|
151
231
|
);
|
|
152
232
|
}
|
|
233
|
+
if (outcome.failedWithoutEscalate !== undefined) {
|
|
234
|
+
throw new Error(outcome.failedWithoutEscalate.diagnostic);
|
|
235
|
+
}
|
|
153
236
|
}
|
|
154
237
|
|
|
155
238
|
export async function runPublicCountersign(
|
|
@@ -173,38 +256,6 @@ export async function runPublicCountersign(
|
|
|
173
256
|
throw error;
|
|
174
257
|
}
|
|
175
258
|
|
|
176
|
-
// #637: same ticket → resume prior countersign run with this summons' materials.
|
|
177
|
-
// #709: identity is reused from records this book already holds — no seat model call.
|
|
178
|
-
// No bare catch→fresh: lookup/resume failures surface; only true absence mints new.
|
|
179
|
-
const projectRoot = parsed.project ?? env.cwd;
|
|
180
|
-
const reusedTicketNumber = await resolveKnownTicketNumber({
|
|
181
|
-
instruction: parsed.instruction,
|
|
182
|
-
projectRoot,
|
|
183
|
-
home: env.home,
|
|
184
|
-
});
|
|
185
|
-
if (reusedTicketNumber !== undefined) {
|
|
186
|
-
const summons: SameTicketSummonsMaterials = {
|
|
187
|
-
instruction: parsed.instruction,
|
|
188
|
-
instructionEmpty: parsed.instruction.trim() === "",
|
|
189
|
-
attachmentPaths: parsed.attachmentPaths,
|
|
190
|
-
};
|
|
191
|
-
const resumed = await tryResumeSameTicketSeatRun({
|
|
192
|
-
home: env.home,
|
|
193
|
-
projectRoot,
|
|
194
|
-
role: "countersign",
|
|
195
|
-
ticketNumber: reusedTicketNumber,
|
|
196
|
-
freshSummons: env.freshSummons,
|
|
197
|
-
summons,
|
|
198
|
-
resume: (runId, materials) =>
|
|
199
|
-
runPublicCountersignResume(
|
|
200
|
-
{ runId, ...(materials === undefined ? {} : { summons: materials }) },
|
|
201
|
-
env,
|
|
202
|
-
io,
|
|
203
|
-
),
|
|
204
|
-
});
|
|
205
|
-
if (resumed !== undefined) return resumed;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
259
|
let admitted: AdmittedCountersignInvocation;
|
|
209
260
|
try {
|
|
210
261
|
admitted = await admitCountersignInvocation({
|
|
@@ -228,6 +279,101 @@ export async function runPublicCountersign(
|
|
|
228
279
|
|
|
229
280
|
await markRunAdmitted(admitted, env.principalAuthority);
|
|
230
281
|
|
|
282
|
+
// #637 / #771 / ADR 0079: ticket identity is the 起居郎 LLM typed assertion
|
|
283
|
+
// (never mechanical matching of summons text). Resolve that typed key after
|
|
284
|
+
// admit so the countersign run page exists first; same-ticket re-summons
|
|
285
|
+
// resume the prior run on the typed key (unused mint is abandoned). The test
|
|
286
|
+
// seam `runCourtDiaristStation` defers identity to beforeDispatch and
|
|
287
|
+
// therefore skips auto-resume under the seam alone.
|
|
288
|
+
let typedTicket: number | undefined;
|
|
289
|
+
let identityDiaristRan = false;
|
|
290
|
+
|
|
291
|
+
if (env.runCourtDiaristStation === undefined) {
|
|
292
|
+
const outcome = await invokeCourtDiarist(
|
|
293
|
+
{
|
|
294
|
+
instruction: parsed.instruction,
|
|
295
|
+
projectRoot: admitted.projectRoot,
|
|
296
|
+
failureLabel: "unbound summons",
|
|
297
|
+
},
|
|
298
|
+
env,
|
|
299
|
+
io,
|
|
300
|
+
);
|
|
301
|
+
identityDiaristRan = true;
|
|
302
|
+
|
|
303
|
+
if (outcome.identity.kind === "escalate") {
|
|
304
|
+
// 御批: 识别不了就上抛 — settle on the admitted countersign run.
|
|
305
|
+
return await presentControlledFailure(
|
|
306
|
+
admitted,
|
|
307
|
+
{
|
|
308
|
+
timedOut: false,
|
|
309
|
+
code: null,
|
|
310
|
+
stderr: "",
|
|
311
|
+
thrown: new Error(
|
|
312
|
+
`court diarist station escalated (cannot identify court target): ${outcome.identity.reason}`,
|
|
313
|
+
),
|
|
314
|
+
},
|
|
315
|
+
countersignAdapters(),
|
|
316
|
+
env.principalAuthority,
|
|
317
|
+
io,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Typed failure terminal (verification / infra / non-zero without escalate)
|
|
322
|
+
// is not 真无票 — settle controlled failure on the admitted run (失败诚实).
|
|
323
|
+
// Only a true missing lawful typed terminal keeps the r5 unbound-continue.
|
|
324
|
+
if (outcome.failedWithoutEscalate !== undefined) {
|
|
325
|
+
return await presentControlledFailure(
|
|
326
|
+
admitted,
|
|
327
|
+
{
|
|
328
|
+
timedOut: false,
|
|
329
|
+
code: null,
|
|
330
|
+
stderr: "",
|
|
331
|
+
thrown: new Error(outcome.failedWithoutEscalate.diagnostic),
|
|
332
|
+
},
|
|
333
|
+
countersignAdapters(),
|
|
334
|
+
env.principalAuthority,
|
|
335
|
+
io,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Missing lawful 起居郎 terminal is not countersign body failure: leave
|
|
340
|
+
// unbound and continue (true-unbound face). Escalate / typed failure above;
|
|
341
|
+
// bound refresh still fails honest via runCountersignCourtDiaristStation.
|
|
342
|
+
if (outcome.identity.kind === "ticket") {
|
|
343
|
+
typedTicket = outcome.identity.ticketNumber;
|
|
344
|
+
const summons: SameTicketSummonsMaterials = {
|
|
345
|
+
instruction: parsed.instruction,
|
|
346
|
+
instructionEmpty: parsed.instruction.trim() === "",
|
|
347
|
+
attachmentPaths: parsed.attachmentPaths,
|
|
348
|
+
};
|
|
349
|
+
const resumed = await tryResumeSameTicketSeatRun({
|
|
350
|
+
home: env.home,
|
|
351
|
+
projectRoot: admitted.projectRoot,
|
|
352
|
+
role: "countersign",
|
|
353
|
+
ticketNumber: typedTicket,
|
|
354
|
+
freshSummons: env.freshSummons,
|
|
355
|
+
summons,
|
|
356
|
+
resume: async (runId, materials) => {
|
|
357
|
+
// Resume selected: abandon mint first so a throw cannot leave it admitted.
|
|
358
|
+
await markRunTerminal(admitted.runDirectory);
|
|
359
|
+
// Identity 起居郎 asserted unbound (no issue face). Resume still runs
|
|
360
|
+
// the bound refresh station under the typed key (refresh-every-court).
|
|
361
|
+
return await runPublicCountersignResume(
|
|
362
|
+
{
|
|
363
|
+
runId,
|
|
364
|
+
...(materials === undefined ? {} : { summons: materials }),
|
|
365
|
+
},
|
|
366
|
+
env,
|
|
367
|
+
io,
|
|
368
|
+
);
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
if (resumed !== undefined) {
|
|
372
|
+
return resumed;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
231
377
|
const turnProjection: RoleTurnRequestProjectionOptions = {
|
|
232
378
|
packageRoot: env.packageRoot,
|
|
233
379
|
home: env.home,
|
|
@@ -259,15 +405,20 @@ export async function runPublicCountersign(
|
|
|
259
405
|
request: turnRequest,
|
|
260
406
|
adapters: countersignAdapters({
|
|
261
407
|
beforeDispatch: async (admittedSeat) => {
|
|
262
|
-
//
|
|
263
|
-
|
|
408
|
+
// Dossier pointer delivery rides post-admission after this hook (#709).
|
|
409
|
+
if (!identityDiaristRan) {
|
|
410
|
+
// Test seam (or any deferred identity): station owns assert + bind.
|
|
411
|
+
await runCountersignCourtDiaristStation(admittedSeat, env, io);
|
|
412
|
+
} else if (typedTicket !== undefined) {
|
|
413
|
+
// Production identity asserted unbound; bind typed key, then bound
|
|
414
|
+
// refresh so freeze loads issue face (typed handoff, not prose match).
|
|
415
|
+
await bindAdmittedTicketNumber(admittedSeat, typedTicket);
|
|
416
|
+
await runCountersignCourtDiaristStation(admittedSeat, env, io);
|
|
417
|
+
}
|
|
264
418
|
Object.assign(
|
|
265
419
|
turnRequest,
|
|
266
420
|
buildCountersignTurnRequest(admittedSeat, turnProjection),
|
|
267
421
|
);
|
|
268
|
-
// Court station after bind so the diarist round sees the ticket.
|
|
269
|
-
// Dossier pointer delivery rides post-admission after this hook (#709).
|
|
270
|
-
await runCountersignCourtDiaristStation(admittedSeat, env, io);
|
|
271
422
|
},
|
|
272
423
|
}),
|
|
273
424
|
...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
|
|
@@ -295,11 +446,12 @@ function countersignAdapters(options?: {
|
|
|
295
446
|
|
|
296
447
|
/**
|
|
297
448
|
* Resume a previously admitted Countersign run (#599 / DK-3 / #637).
|
|
298
|
-
* Restores role/ticket/session identity.
|
|
299
|
-
* station first (ADR 0075 `refresh-every-court`)
|
|
300
|
-
* turn's instruction + frozen attachments on
|
|
301
|
-
* keeps package-envelope / caller-message
|
|
302
|
-
* 起居录 path delivery remains post-admission's
|
|
449
|
+
* Restores role/ticket/session identity. Bound court re-entry runs the diarist
|
|
450
|
+
* refresh station first (ADR 0075 `refresh-every-court`); unbound skips refresh.
|
|
451
|
+
* Same-ticket summons deliver this turn's instruction + frozen attachments on
|
|
452
|
+
* the resume prompt; manual resume keeps package-envelope / caller-message
|
|
453
|
+
* semantics and birth attachments. 起居录 path delivery remains post-admission's
|
|
454
|
+
* single mount (#709).
|
|
303
455
|
*/
|
|
304
456
|
export async function runPublicCountersignResume(
|
|
305
457
|
request: PublicResumeRequest,
|
|
@@ -19,15 +19,11 @@ import { engineSessionMaterialFromOptions } from "../package-resources/engine-ma
|
|
|
19
19
|
import { CliUsageError } from "./cli-errors.ts";
|
|
20
20
|
import {
|
|
21
21
|
admitDiaristInvocation,
|
|
22
|
+
bindAdmittedTicketNumber,
|
|
22
23
|
buildInstructionTransportPrompt,
|
|
23
24
|
type AdmittedDiaristInvocation,
|
|
24
25
|
type ParseDiaristArgvResult,
|
|
25
26
|
} from "./invocation.ts";
|
|
26
|
-
import {
|
|
27
|
-
bindReusedTicketNumber,
|
|
28
|
-
resolveDiaristSummonsTicketNumber,
|
|
29
|
-
tryResumeSameTicketSeatRun,
|
|
30
|
-
} from "./seat-ticket-binding.ts";
|
|
31
27
|
import {
|
|
32
28
|
prepareSummonsResumeMaterials,
|
|
33
29
|
runPostAdmissionOneShot,
|
|
@@ -43,6 +39,7 @@ import {
|
|
|
43
39
|
type PublicResumeRequest,
|
|
44
40
|
type SameTicketSummonsMaterials,
|
|
45
41
|
} from "./run-lifecycle.ts";
|
|
42
|
+
import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.ts";
|
|
46
43
|
import {
|
|
47
44
|
presentStructuralRejection,
|
|
48
45
|
trySettleDiaristTerminalResult,
|
|
@@ -57,6 +54,13 @@ import {
|
|
|
57
54
|
export type DiaristRunEnv = PostAdmissionEnv & {
|
|
58
55
|
principalAuthority: DurablePrincipalAuthority;
|
|
59
56
|
createRunId?: () => string;
|
|
57
|
+
/**
|
|
58
|
+
* Typed handoff from a caller that already holds a verified ticket key
|
|
59
|
+
* (countersign refresh / post-assert). Never derived from summons prose.
|
|
60
|
+
* When set: same-ticket resume under that key, else bind-before-freeze so
|
|
61
|
+
* issue face enters the catalog (ADR 0075 / 0081).
|
|
62
|
+
*/
|
|
63
|
+
boundTicketNumber?: number;
|
|
60
64
|
};
|
|
61
65
|
|
|
62
66
|
/** Frozen catalog filename inside the run dossier (durable material, not argv). */
|
|
@@ -97,9 +101,12 @@ export function buildDiaristTurnRequest(
|
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
/**
|
|
100
|
-
* Mechanical source enumeration
|
|
101
|
-
*
|
|
102
|
-
*
|
|
104
|
+
* Mechanical source enumeration into a frozen catalog for this turn.
|
|
105
|
+
* Bound summons: establish the per-ticket volume and load the issue face.
|
|
106
|
+
* First-summons (unbound): freeze session candidates + summons text so the LLM
|
|
107
|
+
* turn can assert ticketNumber; accept verifies and mints the volume (ADR 0075).
|
|
108
|
+
* True-unbound is decided by the LLM assertion (null/absent ticketNumber), not
|
|
109
|
+
* by skipping the freeze here.
|
|
103
110
|
*
|
|
104
111
|
* A run that has not settled yet (crash-resume, open-court continuation) keeps
|
|
105
112
|
* the catalog its candidateIndexes were minted against — re-enumerating there
|
|
@@ -109,23 +116,30 @@ export function buildDiaristTurnRequest(
|
|
|
109
116
|
async function freezeDiaristSourceCatalog(
|
|
110
117
|
admitted: AdmittedDiaristInvocation,
|
|
111
118
|
env: Pick<DiaristRunEnv, "cwd" | "home">,
|
|
112
|
-
): Promise<string
|
|
113
|
-
if (admitted.ticketNumber === undefined) return undefined;
|
|
119
|
+
): Promise<string> {
|
|
114
120
|
const path = join(admitted.runDirectory, DIARIST_SOURCE_CATALOG_FILE);
|
|
115
121
|
if (existsSync(path)) {
|
|
116
122
|
const identity = await readRoleRunIdentity(admitted.runDirectory);
|
|
117
123
|
if (identity !== undefined && identity.state !== "terminal") return path;
|
|
118
124
|
}
|
|
119
|
-
const issueFace =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
const issueFace =
|
|
126
|
+
admitted.ticketNumber === undefined
|
|
127
|
+
? undefined
|
|
128
|
+
: await loadDiaristIssueFace({
|
|
129
|
+
ticketNumber: admitted.ticketNumber,
|
|
130
|
+
projectRoot: admitted.projectRoot,
|
|
131
|
+
home: env.home,
|
|
132
|
+
});
|
|
124
133
|
const catalog = await prepareDiaristSourceCatalog({
|
|
125
|
-
|
|
134
|
+
...(admitted.ticketNumber === undefined
|
|
135
|
+
? {}
|
|
136
|
+
: { ticketNumber: admitted.ticketNumber }),
|
|
137
|
+
instruction: admitted.instruction,
|
|
138
|
+
runDirectory: admitted.runDirectory,
|
|
139
|
+
projectRoot: admitted.projectRoot,
|
|
126
140
|
cwd: admitted.projectRoot,
|
|
127
141
|
home: env.home,
|
|
128
|
-
issueFace,
|
|
142
|
+
...(issueFace === undefined ? {} : { issueFace }),
|
|
129
143
|
sessionCwds: [admitted.projectRoot, env.cwd],
|
|
130
144
|
});
|
|
131
145
|
await writeFile(path, serializeDiaristSourceCatalog(catalog), "utf8");
|
|
@@ -168,16 +182,19 @@ export async function runPublicDiarist(
|
|
|
168
182
|
throw error;
|
|
169
183
|
}
|
|
170
184
|
|
|
171
|
-
// #637:
|
|
172
|
-
//
|
|
173
|
-
//
|
|
185
|
+
// #637 / #771 / ADR 0075: ticket identity is the LLM's typed assertion on this
|
|
186
|
+
// turn (mechanical verify on accept), OR a typed handoff key already held by
|
|
187
|
+
// the caller (countersign refresh). Code never pre-judges the summons text
|
|
188
|
+
// against book-known numbers. First summons without handoff stays unbound
|
|
189
|
+
// until assert.
|
|
190
|
+
|
|
174
191
|
const projectRoot = parsed.project ?? env.cwd;
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
192
|
+
const handoffTicket = env.boundTicketNumber;
|
|
193
|
+
if (
|
|
194
|
+
typeof handoffTicket === "number" &&
|
|
195
|
+
Number.isSafeInteger(handoffTicket) &&
|
|
196
|
+
handoffTicket >= 1
|
|
197
|
+
) {
|
|
181
198
|
const summons: SameTicketSummonsMaterials = {
|
|
182
199
|
instruction: parsed.instruction,
|
|
183
200
|
instructionEmpty: parsed.instruction.trim() === "",
|
|
@@ -187,7 +204,7 @@ export async function runPublicDiarist(
|
|
|
187
204
|
home: env.home,
|
|
188
205
|
projectRoot,
|
|
189
206
|
role: "diarist",
|
|
190
|
-
ticketNumber:
|
|
207
|
+
ticketNumber: handoffTicket,
|
|
191
208
|
freshSummons: env.freshSummons,
|
|
192
209
|
summons,
|
|
193
210
|
resume: (runId, materials) =>
|
|
@@ -223,6 +240,15 @@ export async function runPublicDiarist(
|
|
|
223
240
|
|
|
224
241
|
await markRunAdmitted(admitted, env.principalAuthority);
|
|
225
242
|
|
|
243
|
+
// Typed handoff: bind before freeze so issue face enters the catalog.
|
|
244
|
+
if (
|
|
245
|
+
typeof handoffTicket === "number" &&
|
|
246
|
+
Number.isSafeInteger(handoffTicket) &&
|
|
247
|
+
handoffTicket >= 1
|
|
248
|
+
) {
|
|
249
|
+
await bindAdmittedTicketNumber(admitted, handoffTicket);
|
|
250
|
+
}
|
|
251
|
+
|
|
226
252
|
const turnProjection: RoleTurnRequestProjectionOptions = {
|
|
227
253
|
packageRoot: env.packageRoot,
|
|
228
254
|
home: env.home,
|
|
@@ -254,7 +280,6 @@ export async function runPublicDiarist(
|
|
|
254
280
|
request: turnRequest,
|
|
255
281
|
adapters: diaristAdapters({
|
|
256
282
|
beforeDispatch: async (admittedSeat) => {
|
|
257
|
-
await bindReusedTicketNumber(admittedSeat, reusedTicketNumber);
|
|
258
283
|
const sourcesPath = await freezeDiaristSourceCatalog(admittedSeat, env);
|
|
259
284
|
Object.assign(
|
|
260
285
|
turnRequest,
|
|
@@ -263,6 +288,36 @@ export async function runPublicDiarist(
|
|
|
263
288
|
},
|
|
264
289
|
}),
|
|
265
290
|
...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
|
|
291
|
+
}).then(async (result) => {
|
|
292
|
+
// Accept may have bound ticket onto durable pages after LLM assertion.
|
|
293
|
+
// Mirror only lawful non-escalate accepted terminals — escalate must not
|
|
294
|
+
// leak an unverified ticketNumber into the caller-visible typed key.
|
|
295
|
+
if (admitted.ticketNumber === undefined && result.admitted !== undefined) {
|
|
296
|
+
const roleOutcome = result.terminal?.roleOutcome;
|
|
297
|
+
if (
|
|
298
|
+
roleOutcome !== undefined &&
|
|
299
|
+
roleOutcome.kind === "accepted" &&
|
|
300
|
+
roleOutcome.status !== "escalate"
|
|
301
|
+
) {
|
|
302
|
+
const facts = roleOutcome.decisiveFacts as {
|
|
303
|
+
ticketNumber?: unknown;
|
|
304
|
+
sitian?: { ticketNumber?: unknown };
|
|
305
|
+
};
|
|
306
|
+
const raw =
|
|
307
|
+
typeof facts.ticketNumber === "number"
|
|
308
|
+
? facts.ticketNumber
|
|
309
|
+
: typeof facts.sitian?.ticketNumber === "number"
|
|
310
|
+
? facts.sitian.ticketNumber
|
|
311
|
+
: undefined;
|
|
312
|
+
if (typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 1) {
|
|
313
|
+
(admitted as { ticketNumber?: number }).ticketNumber = raw;
|
|
314
|
+
if (result.admitted.ticketNumber === undefined) {
|
|
315
|
+
(result.admitted as { ticketNumber?: number }).ticketNumber = raw;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
266
321
|
});
|
|
267
322
|
}
|
|
268
323
|
|
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
buildFixerTransportPrompt,
|
|
25
25
|
type AdmittedFixerInvocation,
|
|
26
26
|
} from "./invocation.ts";
|
|
27
|
-
import { resolveSeatTicketBinding } from "./seat-ticket-binding.ts";
|
|
28
27
|
import {
|
|
29
28
|
buildResumeContinuationPrompt,
|
|
30
29
|
loadResumableFixerRun,
|
|
@@ -214,12 +213,7 @@ export async function runPublicFixer(
|
|
|
214
213
|
}),
|
|
215
214
|
},
|
|
216
215
|
}),
|
|
217
|
-
adapters:
|
|
218
|
-
...fixerAdapters(env.packageRoot, methodMaterial),
|
|
219
|
-
beforeDispatch: async (admittedSeat) => {
|
|
220
|
-
await resolveSeatTicketBinding(admittedSeat, env);
|
|
221
|
-
},
|
|
222
|
-
},
|
|
216
|
+
adapters: fixerAdapters(env.packageRoot, methodMaterial),
|
|
223
217
|
...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
|
|
224
218
|
});
|
|
225
219
|
}
|
|
@@ -8,11 +8,7 @@
|
|
|
8
8
|
import type { DurablePrincipalAuthority, RoleTurnRequest } from "../host-contracts.ts";
|
|
9
9
|
import { engineSessionMaterialFromOptions } from "../package-resources/engine-material.ts";
|
|
10
10
|
import { CliUsageError } from "./cli-errors.ts";
|
|
11
|
-
import {
|
|
12
|
-
bindReusedTicketNumber,
|
|
13
|
-
resolveKnownTicketNumber,
|
|
14
|
-
tryResumeSameTicketSeatRun,
|
|
15
|
-
} from "./seat-ticket-binding.ts";
|
|
11
|
+
import { tryResumeSameTicketSeatRun } from "./seat-ticket-binding.ts";
|
|
16
12
|
import {
|
|
17
13
|
admitInspectorInvocation,
|
|
18
14
|
buildInspectorTransportPrompt,
|
|
@@ -88,14 +84,9 @@ export async function runPublicInspector(
|
|
|
88
84
|
}
|
|
89
85
|
|
|
90
86
|
// #747: same parent (卷宗指针) → resume prior inspector run with this summons' materials.
|
|
91
|
-
// #
|
|
87
|
+
// #771: no mechanical ticket match against instruction text; parent-run path only.
|
|
92
88
|
// No bare catch→fresh: lookup/resume failures surface; only true absence mints new.
|
|
93
89
|
const projectRoot = parsed.project ?? env.cwd;
|
|
94
|
-
const reusedTicketNumber = await resolveKnownTicketNumber({
|
|
95
|
-
instruction: parsed.instruction,
|
|
96
|
-
projectRoot,
|
|
97
|
-
home: env.home,
|
|
98
|
-
});
|
|
99
90
|
const parentRunPath = parentRunPathFromGatePointerInstruction(parsed.instruction);
|
|
100
91
|
if (parentRunPath !== undefined) {
|
|
101
92
|
const summons: SameTicketSummonsMaterials = {
|
|
@@ -168,12 +159,7 @@ export async function runPublicInspector(
|
|
|
168
159
|
env,
|
|
169
160
|
io,
|
|
170
161
|
request: turnRequest,
|
|
171
|
-
adapters: inspectorAdapters(
|
|
172
|
-
beforeDispatch: async (admittedSeat) => {
|
|
173
|
-
// #635/#709: bind the reused identity inside the controlled-failure boundary.
|
|
174
|
-
await bindReusedTicketNumber(admittedSeat, reusedTicketNumber);
|
|
175
|
-
},
|
|
176
|
-
}),
|
|
162
|
+
adapters: inspectorAdapters(),
|
|
177
163
|
...(env.engine === undefined ? {} : { effectiveEngine: env.engine }),
|
|
178
164
|
});
|
|
179
165
|
}
|