@akagilnc/pi-workflow-roles 0.1.3452 → 0.1.3471
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/CLAUDE.md +2 -0
- package/dist/evidence-child-executor.js +2 -11
- package/dist/grok/production-host.js +196 -205
- package/dist/pi/in-process-session.js +4 -0
- package/dist/public-cli/main.js +998 -920
- package/package.json +1 -1
- package/scripts/build-package.mjs +1 -3
- package/src/analyst-gate-cycles-read.ts +3 -21
- package/src/analyst-ledger.ts +11 -42
- package/src/archivist-record-entry.ts +5 -6
- package/src/evidence-child-executor.ts +5 -15
- package/src/grok/production-host.ts +3 -32
- package/src/grok/role-envelope.ts +10 -4
- package/src/host-contracts.ts +4 -11
- package/src/host-transition-prior-native.ts +3 -7
- package/src/ledger-session-read.ts +1 -17
- package/src/pi/in-process-session.ts +6 -2
- package/src/pi/role-turn-host.ts +9 -0
- package/src/public-cli/collector-run.ts +4 -4
- package/src/public-cli/countersign-run.ts +90 -20
- package/src/public-cli/doctor-run.ts +4 -4
- package/src/public-cli/gleaner-left-run.ts +9 -6
- package/src/public-cli/inspector-run.ts +95 -32
- package/src/public-cli/instruction-seat-run.ts +7 -6
- package/src/public-cli/invocation.ts +14 -8
- package/src/public-cli/judge-run.ts +4 -4
- package/src/public-cli/notary-run.ts +91 -30
- package/src/public-cli/post-admission.ts +424 -360
- package/src/public-cli/run-lifecycle.ts +205 -0
- package/src/public-cli/seat-ticket-binding.ts +136 -16
- package/src/public-cli/settlement.ts +74 -52
- package/src/run-ticket-number.ts +53 -0
- package/src/submission-ledger.ts +86 -18
- package/dist/ledger-session-read.js +0 -241
- package/dist/ticket-seat-memory.js +0 -244
- package/src/ticket-seat-memory.ts +0 -454
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
activationBookDirectory,
|
|
17
17
|
resolveActivationLedgerHome,
|
|
18
18
|
} from "../activation-ledger-topology.ts";
|
|
19
|
+
import { readRunTicketNumber } from "../run-ticket-number.ts";
|
|
19
20
|
import { CliUsageError } from "./cli-errors.ts";
|
|
20
21
|
import {
|
|
21
22
|
readLatestTypedProviderHttpObservation,
|
|
@@ -119,6 +120,37 @@ export type PublicResumeRequest = {
|
|
|
119
120
|
readonly runId: string;
|
|
120
121
|
/** Present when the caller supplied the post-runId argv (including empty string). */
|
|
121
122
|
readonly message?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Same-ticket re-summons materials (#637). Present only when a public seat
|
|
125
|
+
* re-enters via the summons face — never from `ak-role resume`.
|
|
126
|
+
* Manual resume keeps package envelope / caller message semantics unchanged.
|
|
127
|
+
*/
|
|
128
|
+
readonly summons?: SameTicketSummonsMaterials;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Open court turn on a retained run (#637).
|
|
133
|
+
* courtAttemptId + the same summons materials shape already used by re-summons.
|
|
134
|
+
* Bare resume rehydrates request.summons and rides existing load/buildTurnRequest.
|
|
135
|
+
* Cleared when this courtAttemptId seals.
|
|
136
|
+
*/
|
|
137
|
+
export type CurrentCourtState = {
|
|
138
|
+
readonly courtAttemptId: string;
|
|
139
|
+
readonly summons?: SameTicketSummonsMaterials;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Materials delivered on same-ticket re-summons while reusing the same-run resume seam.
|
|
144
|
+
* Instruction seats freeze new attachments into the retained run and ride the transport prompt;
|
|
145
|
+
* notary overrides the source-run activation pointer for this turn only.
|
|
146
|
+
*/
|
|
147
|
+
export type SameTicketSummonsMaterials = {
|
|
148
|
+
readonly instruction?: string;
|
|
149
|
+
readonly instructionEmpty?: boolean;
|
|
150
|
+
readonly attachmentPaths?: readonly string[];
|
|
151
|
+
/** Notary: this summons' resolved source-run locator (activation pointer). */
|
|
152
|
+
readonly sourceRunPath?: string;
|
|
153
|
+
readonly sourceRun?: NotarySourceRunLocator;
|
|
122
154
|
};
|
|
123
155
|
|
|
124
156
|
/**
|
|
@@ -212,8 +244,81 @@ type RoleRunStateDisk = {
|
|
|
212
244
|
readonly principalWire: RoleRunPrincipalWire;
|
|
213
245
|
readonly phase?: CoderPhase | FixerPhase;
|
|
214
246
|
readonly resumable?: TypedHttp429Observation;
|
|
247
|
+
/** Open court turn (#637); omit when no unsealed current court. */
|
|
248
|
+
readonly currentCourt?: CurrentCourtState;
|
|
215
249
|
};
|
|
216
250
|
|
|
251
|
+
function parseSameTicketSummonsMaterials(
|
|
252
|
+
raw: unknown,
|
|
253
|
+
): SameTicketSummonsMaterials | undefined {
|
|
254
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
255
|
+
const record = raw as Record<string, unknown>;
|
|
256
|
+
const instruction =
|
|
257
|
+
typeof record.instruction === "string" ? record.instruction : undefined;
|
|
258
|
+
const instructionEmpty =
|
|
259
|
+
typeof record.instructionEmpty === "boolean" ? record.instructionEmpty : undefined;
|
|
260
|
+
const attachmentPaths = Array.isArray(record.attachmentPaths)
|
|
261
|
+
? record.attachmentPaths.filter((p): p is string => typeof p === "string" && p.length > 0)
|
|
262
|
+
: undefined;
|
|
263
|
+
const sourceRunPath =
|
|
264
|
+
typeof record.sourceRunPath === "string" && record.sourceRunPath.trim() !== ""
|
|
265
|
+
? record.sourceRunPath
|
|
266
|
+
: undefined;
|
|
267
|
+
let sourceRun: NotarySourceRunLocator | undefined;
|
|
268
|
+
if (
|
|
269
|
+
record.sourceRun !== null &&
|
|
270
|
+
typeof record.sourceRun === "object" &&
|
|
271
|
+
!Array.isArray(record.sourceRun)
|
|
272
|
+
) {
|
|
273
|
+
const sr = record.sourceRun as Record<string, unknown>;
|
|
274
|
+
if (
|
|
275
|
+
typeof sr.runId === "string" &&
|
|
276
|
+
sr.runId.trim() !== "" &&
|
|
277
|
+
typeof sr.role === "string" &&
|
|
278
|
+
sr.role.trim() !== "" &&
|
|
279
|
+
typeof sr.runDirectory === "string" &&
|
|
280
|
+
sr.runDirectory.trim() !== ""
|
|
281
|
+
) {
|
|
282
|
+
sourceRun = {
|
|
283
|
+
runId: sr.runId,
|
|
284
|
+
role: sr.role,
|
|
285
|
+
runDirectory: sr.runDirectory,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (
|
|
290
|
+
instruction === undefined &&
|
|
291
|
+
instructionEmpty === undefined &&
|
|
292
|
+
(attachmentPaths === undefined || attachmentPaths.length === 0) &&
|
|
293
|
+
sourceRunPath === undefined &&
|
|
294
|
+
sourceRun === undefined
|
|
295
|
+
) {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
...(instruction === undefined ? {} : { instruction }),
|
|
300
|
+
...(instructionEmpty === undefined ? {} : { instructionEmpty }),
|
|
301
|
+
...(attachmentPaths === undefined || attachmentPaths.length === 0
|
|
302
|
+
? {}
|
|
303
|
+
: { attachmentPaths }),
|
|
304
|
+
...(sourceRunPath === undefined ? {} : { sourceRunPath }),
|
|
305
|
+
...(sourceRun === undefined ? {} : { sourceRun }),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function parseCurrentCourtState(raw: unknown): CurrentCourtState | undefined {
|
|
310
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
|
311
|
+
const record = raw as Record<string, unknown>;
|
|
312
|
+
if (typeof record.courtAttemptId !== "string" || record.courtAttemptId.length === 0) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
const summons = parseSameTicketSummonsMaterials(record.summons);
|
|
316
|
+
return {
|
|
317
|
+
courtAttemptId: record.courtAttemptId,
|
|
318
|
+
...(summons === undefined ? {} : { summons }),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
217
322
|
async function readRoleRunStateDisk(
|
|
218
323
|
runDirectory: string,
|
|
219
324
|
): Promise<RoleRunStateDisk | undefined> {
|
|
@@ -290,6 +395,7 @@ async function readRoleRunStateDisk(
|
|
|
290
395
|
record.phase === "plan" || record.phase === "apply"
|
|
291
396
|
? record.phase
|
|
292
397
|
: undefined;
|
|
398
|
+
const currentCourt = parseCurrentCourtState(record.currentCourt);
|
|
293
399
|
return {
|
|
294
400
|
runId: record.runId,
|
|
295
401
|
role: record.role,
|
|
@@ -301,6 +407,7 @@ async function readRoleRunStateDisk(
|
|
|
301
407
|
principalWire,
|
|
302
408
|
...(phase === undefined ? {} : { phase }),
|
|
303
409
|
...(resumable === undefined ? {} : { resumable }),
|
|
410
|
+
...(currentCourt === undefined ? {} : { currentCourt }),
|
|
304
411
|
};
|
|
305
412
|
}
|
|
306
413
|
|
|
@@ -322,6 +429,7 @@ async function writeRoleRunStateDisk(
|
|
|
322
429
|
admittedRequestPath: disk.admittedRequestPath,
|
|
323
430
|
...(disk.phase === undefined ? {} : { phase: disk.phase }),
|
|
324
431
|
...(disk.resumable === undefined ? {} : { resumable: disk.resumable }),
|
|
432
|
+
...(disk.currentCourt === undefined ? {} : { currentCourt: disk.currentCourt }),
|
|
325
433
|
};
|
|
326
434
|
await writeFile(
|
|
327
435
|
join(runDirectory, RUN_STATE_FILE),
|
|
@@ -448,6 +556,7 @@ export async function markRunRunning(
|
|
|
448
556
|
throw new Error("cannot mark running: run state missing");
|
|
449
557
|
}
|
|
450
558
|
// Omit resumable while a writer is active. Principal wire is passed through uninterpreted.
|
|
559
|
+
// Preserve open currentCourt across running transitions (#637).
|
|
451
560
|
await writeRoleRunStateDisk(runDirectory, {
|
|
452
561
|
runId: current.runId,
|
|
453
562
|
role: current.role,
|
|
@@ -458,6 +567,7 @@ export async function markRunRunning(
|
|
|
458
567
|
admittedRequestPath: current.admittedRequestPath,
|
|
459
568
|
principalWire: current.principalWire,
|
|
460
569
|
...(current.phase === undefined ? {} : { phase: current.phase }),
|
|
570
|
+
...(current.currentCourt === undefined ? {} : { currentCourt: current.currentCourt }),
|
|
461
571
|
});
|
|
462
572
|
}
|
|
463
573
|
|
|
@@ -482,6 +592,8 @@ export async function markRunTerminal(runDirectory: string): Promise<void> {
|
|
|
482
592
|
if (current === undefined) {
|
|
483
593
|
throw new Error("cannot mark terminal: run state missing");
|
|
484
594
|
}
|
|
595
|
+
// Preserve open currentCourt: terminal after a failed/incomplete court must still
|
|
596
|
+
// let bare resume continue that court (#637).
|
|
485
597
|
await writeRoleRunStateDisk(runDirectory, {
|
|
486
598
|
runId: current.runId,
|
|
487
599
|
role: current.role,
|
|
@@ -492,6 +604,62 @@ export async function markRunTerminal(runDirectory: string): Promise<void> {
|
|
|
492
604
|
admittedRequestPath: current.admittedRequestPath,
|
|
493
605
|
principalWire: current.principalWire,
|
|
494
606
|
...(current.phase === undefined ? {} : { phase: current.phase }),
|
|
607
|
+
...(current.currentCourt === undefined ? {} : { currentCourt: current.currentCourt }),
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** Read the open court turn on a retained run, if any (#637). */
|
|
612
|
+
export async function readCurrentCourt(
|
|
613
|
+
runDirectory: string,
|
|
614
|
+
): Promise<CurrentCourtState | undefined> {
|
|
615
|
+
const current = await readRoleRunStateDisk(runDirectory);
|
|
616
|
+
return current?.currentCourt;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** Persist the open court turn identity + materials (#637). */
|
|
620
|
+
export async function recordCurrentCourt(
|
|
621
|
+
runDirectory: string,
|
|
622
|
+
court: CurrentCourtState,
|
|
623
|
+
): Promise<void> {
|
|
624
|
+
const current = await readRoleRunStateDisk(runDirectory);
|
|
625
|
+
if (current === undefined) {
|
|
626
|
+
throw new Error("cannot record current court: run state missing");
|
|
627
|
+
}
|
|
628
|
+
await writeRoleRunStateDisk(runDirectory, {
|
|
629
|
+
...current,
|
|
630
|
+
currentCourt: court,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Clear open court after this courtAttemptId seals, or when the open court is
|
|
636
|
+
* already sealed and bare resume returns to run-scoped idempotence (#637).
|
|
637
|
+
* When expectedCourtAttemptId is set, clear only if it still matches — never
|
|
638
|
+
* drop a different court recorded under the writer lease after our judgment.
|
|
639
|
+
*/
|
|
640
|
+
export async function clearCurrentCourt(
|
|
641
|
+
runDirectory: string,
|
|
642
|
+
expectedCourtAttemptId?: string,
|
|
643
|
+
): Promise<void> {
|
|
644
|
+
const current = await readRoleRunStateDisk(runDirectory);
|
|
645
|
+
if (current === undefined || current.currentCourt === undefined) return;
|
|
646
|
+
if (
|
|
647
|
+
expectedCourtAttemptId !== undefined &&
|
|
648
|
+
current.currentCourt.courtAttemptId !== expectedCourtAttemptId
|
|
649
|
+
) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
await writeRoleRunStateDisk(runDirectory, {
|
|
653
|
+
runId: current.runId,
|
|
654
|
+
role: current.role,
|
|
655
|
+
state: current.state,
|
|
656
|
+
bookKey: current.bookKey,
|
|
657
|
+
projectRoot: current.projectRoot,
|
|
658
|
+
runDirectory: current.runDirectory,
|
|
659
|
+
admittedRequestPath: current.admittedRequestPath,
|
|
660
|
+
principalWire: current.principalWire,
|
|
661
|
+
...(current.phase === undefined ? {} : { phase: current.phase }),
|
|
662
|
+
...(current.resumable === undefined ? {} : { resumable: current.resumable }),
|
|
495
663
|
});
|
|
496
664
|
}
|
|
497
665
|
|
|
@@ -829,6 +997,43 @@ export async function findRunDirectoryById(
|
|
|
829
997
|
return undefined;
|
|
830
998
|
}
|
|
831
999
|
|
|
1000
|
+
/**
|
|
1001
|
+
* Locate the latest retained run for one seat+ticket under a book (#637).
|
|
1002
|
+
* Same walk surface as findRunDirectoryById; ticket identity from durable pages.
|
|
1003
|
+
* runId is UUIDv7 — lexicographic max is latest. No parallel index.
|
|
1004
|
+
* Only a truly missing runs directory means no history; damage/permission errors propagate.
|
|
1005
|
+
*/
|
|
1006
|
+
export async function findLatestRunIdForSeatTicket(input: {
|
|
1007
|
+
readonly home: string;
|
|
1008
|
+
readonly bookKey: string;
|
|
1009
|
+
readonly role: RoleRunRecord["role"];
|
|
1010
|
+
readonly ticketNumber: number;
|
|
1011
|
+
}): Promise<string | undefined> {
|
|
1012
|
+
const ledgerHome = resolveActivationLedgerHome(input.home);
|
|
1013
|
+
const runsDir = join(
|
|
1014
|
+
activationBookDirectory(ledgerHome, input.bookKey),
|
|
1015
|
+
"runs",
|
|
1016
|
+
);
|
|
1017
|
+
let entries: string[];
|
|
1018
|
+
try {
|
|
1019
|
+
entries = await readdir(runsDir);
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
if (errorCodeOf(error) === "ENOENT") return undefined;
|
|
1022
|
+
throw error;
|
|
1023
|
+
}
|
|
1024
|
+
const suffix = `@${input.role}`;
|
|
1025
|
+
let best: string | undefined;
|
|
1026
|
+
for (const entry of entries) {
|
|
1027
|
+
if (!entry.endsWith(suffix)) continue;
|
|
1028
|
+
const runId = entry.slice(0, entry.length - suffix.length);
|
|
1029
|
+
if (runId.length === 0) continue;
|
|
1030
|
+
const ticketNumber = await readRunTicketNumber(join(runsDir, entry));
|
|
1031
|
+
if (ticketNumber !== input.ticketNumber) continue;
|
|
1032
|
+
if (best === undefined || runId > best) best = runId;
|
|
1033
|
+
}
|
|
1034
|
+
return best;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
832
1037
|
type LoadedAdmittedRequestFields = {
|
|
833
1038
|
readonly instruction: string;
|
|
834
1039
|
readonly instructionEmpty: boolean;
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
* Shared LLM ticket binding for public court seats (#635).
|
|
3
3
|
* One path: unbound admission → diarist instruction assertion → bindAdmittedTicketNumber.
|
|
4
4
|
* No CLI --ticket and no attachment frontmatter binding.
|
|
5
|
+
* #637: same-ticket prior-run lookup → resume decision also lives here (shared seam).
|
|
5
6
|
*/
|
|
7
|
+
import { resolveBookKeyFromGit } from "../activation-ledger-git.ts";
|
|
6
8
|
import {
|
|
7
9
|
createGhTicketExistenceChecker,
|
|
8
10
|
createHermesDiaristTicketResolver,
|
|
11
|
+
DiaristTicketResolutionError,
|
|
9
12
|
resolveDiaristTicketFromInstruction,
|
|
10
13
|
type DiaristTicketResolution,
|
|
11
14
|
} from "../diarist-ticket-resolution.ts";
|
|
@@ -15,42 +18,159 @@ import {
|
|
|
15
18
|
recordTrueUnboundTicketResolution,
|
|
16
19
|
type AdmittedRoleInvocation,
|
|
17
20
|
} from "./invocation.ts";
|
|
21
|
+
import {
|
|
22
|
+
findLatestRunIdForSeatTicket,
|
|
23
|
+
type RoleRunRecord,
|
|
24
|
+
type SameTicketSummonsMaterials,
|
|
25
|
+
} from "./run-lifecycle.ts";
|
|
18
26
|
|
|
19
27
|
export type SeatTicketBindingEnv = {
|
|
20
28
|
readonly packageRoot?: string;
|
|
21
29
|
};
|
|
22
30
|
|
|
23
31
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
32
|
+
* Pre-admit instruction ticket probe (#635 / #637).
|
|
33
|
+
* Success carries the resolution for same-ticket resume + post-admit bind.
|
|
34
|
+
* DiaristTicketResolutionError is captured (not thrown) so the seat can still
|
|
35
|
+
* admit and settle the failure inside the post-admission controlled path —
|
|
36
|
+
* bare throw before admit would skip terminal settlement (失败诚实, no wash).
|
|
28
37
|
*/
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
): Promise<DiaristTicketResolution | undefined> {
|
|
33
|
-
if (admitted.ticketNumber !== undefined) return undefined;
|
|
34
|
-
if (admitted.ticketResolution === "true-unbound") {
|
|
35
|
-
return { kind: "true-unbound" };
|
|
36
|
-
}
|
|
38
|
+
export type InstructionTicketProbe =
|
|
39
|
+
| { readonly kind: "resolved"; readonly resolution: DiaristTicketResolution }
|
|
40
|
+
| { readonly kind: "failed"; readonly error: DiaristTicketResolutionError };
|
|
37
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Instruction → ticket resolution without admission (#635 / #637).
|
|
44
|
+
* Used to locate a prior same-ticket run before minting a new one.
|
|
45
|
+
* Throws on resolution failure — prefer probeInstructionTicket at seat entry.
|
|
46
|
+
*/
|
|
47
|
+
export async function resolveInstructionTicket(
|
|
48
|
+
instruction: string,
|
|
49
|
+
projectRoot: string,
|
|
50
|
+
env: SeatTicketBindingEnv = {},
|
|
51
|
+
): Promise<DiaristTicketResolution> {
|
|
38
52
|
const resolver = createHermesDiaristTicketResolver({
|
|
39
53
|
...(env.packageRoot === undefined ? {} : { packageRoot: env.packageRoot }),
|
|
40
|
-
cwd:
|
|
54
|
+
cwd: projectRoot,
|
|
41
55
|
});
|
|
42
56
|
const checkExistence = createGhTicketExistenceChecker();
|
|
43
|
-
const origin = resolveDiaristGithubOrigin(
|
|
44
|
-
|
|
45
|
-
instruction
|
|
57
|
+
const origin = resolveDiaristGithubOrigin(projectRoot);
|
|
58
|
+
return resolveDiaristTicketFromInstruction({
|
|
59
|
+
instruction,
|
|
46
60
|
origin,
|
|
47
61
|
resolver,
|
|
48
62
|
checkExistence,
|
|
49
63
|
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Pre-admit probe: resolution success or captured DiaristTicketResolutionError.
|
|
68
|
+
* Other errors still propagate (launch/infrastructure outside this class).
|
|
69
|
+
*/
|
|
70
|
+
export async function probeInstructionTicket(
|
|
71
|
+
instruction: string,
|
|
72
|
+
projectRoot: string,
|
|
73
|
+
env: SeatTicketBindingEnv = {},
|
|
74
|
+
): Promise<InstructionTicketProbe> {
|
|
75
|
+
try {
|
|
76
|
+
const resolution = await resolveInstructionTicket(
|
|
77
|
+
instruction,
|
|
78
|
+
projectRoot,
|
|
79
|
+
env,
|
|
80
|
+
);
|
|
81
|
+
return { kind: "resolved", resolution };
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof DiaristTicketResolutionError) {
|
|
84
|
+
return { kind: "failed", error };
|
|
85
|
+
}
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Ticket number from a successful ticket probe; undefined for unbound/failed. */
|
|
91
|
+
export function ticketNumberFromProbe(
|
|
92
|
+
probe: InstructionTicketProbe,
|
|
93
|
+
): number | undefined {
|
|
94
|
+
if (probe.kind !== "resolved") return undefined;
|
|
95
|
+
if (probe.resolution.kind !== "ticket") return undefined;
|
|
96
|
+
return probe.resolution.ticketNumber;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Sole disposition of a resolved ticket onto an unbound admission (#635 / #637).
|
|
101
|
+
* Countersign / inspector / resolveSeatTicketBinding share this — no parallel branches.
|
|
102
|
+
*/
|
|
103
|
+
export async function applyTicketResolution(
|
|
104
|
+
admitted: AdmittedRoleInvocation,
|
|
105
|
+
resolution: DiaristTicketResolution,
|
|
106
|
+
): Promise<void> {
|
|
50
107
|
if (resolution.kind === "ticket") {
|
|
51
108
|
await bindAdmittedTicketNumber(admitted, resolution.ticketNumber);
|
|
52
109
|
} else {
|
|
53
110
|
await recordTrueUnboundTicketResolution(admitted);
|
|
54
111
|
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Apply a pre-admit probe onto an unbound admission.
|
|
116
|
+
* Failed probes rethrow — caller must be inside the controlled-failure boundary
|
|
117
|
+
* (post-admission beforeDispatch), never before admit/markRunning.
|
|
118
|
+
*/
|
|
119
|
+
export async function applyInstructionTicketProbe(
|
|
120
|
+
admitted: AdmittedRoleInvocation,
|
|
121
|
+
probe: InstructionTicketProbe,
|
|
122
|
+
): Promise<void> {
|
|
123
|
+
if (probe.kind === "failed") throw probe.error;
|
|
124
|
+
await applyTicketResolution(admitted, probe.resolution);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Bind ticketNumber onto an unbound admitted seat via LLM instruction recognition.
|
|
129
|
+
* Already-settled admissions (ticketNumber or durable true-unbound) short-circuit —
|
|
130
|
+
* no re-entry into hermes on auto-resume attempts or manual resume (#635).
|
|
131
|
+
* true-unbound is persisted once; failed verification throws.
|
|
132
|
+
*/
|
|
133
|
+
export async function resolveSeatTicketBinding(
|
|
134
|
+
admitted: AdmittedRoleInvocation,
|
|
135
|
+
env: SeatTicketBindingEnv = {},
|
|
136
|
+
): Promise<DiaristTicketResolution | undefined> {
|
|
137
|
+
if (admitted.ticketNumber !== undefined) return undefined;
|
|
138
|
+
if (admitted.ticketResolution === "true-unbound") {
|
|
139
|
+
return { kind: "true-unbound" };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const resolution = await resolveInstructionTicket(
|
|
143
|
+
admitted.instruction,
|
|
144
|
+
admitted.projectRoot,
|
|
145
|
+
env,
|
|
146
|
+
);
|
|
147
|
+
await applyTicketResolution(admitted, resolution);
|
|
55
148
|
return resolution;
|
|
56
149
|
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Shared same-ticket → resume decision (#637).
|
|
153
|
+
* Looks up the latest retained run for seat+ticket; when found, runs resume with
|
|
154
|
+
* this summons' materials. Lookup/resume failures propagate (失败诚实) — never
|
|
155
|
+
* wash into a fresh mint. Returns undefined only when no prior run exists.
|
|
156
|
+
*/
|
|
157
|
+
export async function tryResumeSameTicketSeatRun<T>(input: {
|
|
158
|
+
readonly home: string;
|
|
159
|
+
readonly projectRoot: string;
|
|
160
|
+
readonly role: RoleRunRecord["role"];
|
|
161
|
+
readonly ticketNumber: number;
|
|
162
|
+
readonly summons?: SameTicketSummonsMaterials;
|
|
163
|
+
readonly resume: (
|
|
164
|
+
runId: string,
|
|
165
|
+
summons: SameTicketSummonsMaterials | undefined,
|
|
166
|
+
) => Promise<T>;
|
|
167
|
+
}): Promise<T | undefined> {
|
|
168
|
+
const previousRunId = await findLatestRunIdForSeatTicket({
|
|
169
|
+
home: input.home,
|
|
170
|
+
bookKey: resolveBookKeyFromGit(input.projectRoot),
|
|
171
|
+
role: input.role,
|
|
172
|
+
ticketNumber: input.ticketNumber,
|
|
173
|
+
});
|
|
174
|
+
if (previousRunId === undefined) return undefined;
|
|
175
|
+
return await input.resume(previousRunId, input.summons);
|
|
176
|
+
}
|