@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.
Files changed (36) hide show
  1. package/CLAUDE.md +2 -0
  2. package/dist/evidence-child-executor.js +2 -11
  3. package/dist/grok/production-host.js +196 -205
  4. package/dist/pi/in-process-session.js +4 -0
  5. package/dist/public-cli/main.js +998 -920
  6. package/package.json +1 -1
  7. package/scripts/build-package.mjs +1 -3
  8. package/src/analyst-gate-cycles-read.ts +3 -21
  9. package/src/analyst-ledger.ts +11 -42
  10. package/src/archivist-record-entry.ts +5 -6
  11. package/src/evidence-child-executor.ts +5 -15
  12. package/src/grok/production-host.ts +3 -32
  13. package/src/grok/role-envelope.ts +10 -4
  14. package/src/host-contracts.ts +4 -11
  15. package/src/host-transition-prior-native.ts +3 -7
  16. package/src/ledger-session-read.ts +1 -17
  17. package/src/pi/in-process-session.ts +6 -2
  18. package/src/pi/role-turn-host.ts +9 -0
  19. package/src/public-cli/collector-run.ts +4 -4
  20. package/src/public-cli/countersign-run.ts +90 -20
  21. package/src/public-cli/doctor-run.ts +4 -4
  22. package/src/public-cli/gleaner-left-run.ts +9 -6
  23. package/src/public-cli/inspector-run.ts +95 -32
  24. package/src/public-cli/instruction-seat-run.ts +7 -6
  25. package/src/public-cli/invocation.ts +14 -8
  26. package/src/public-cli/judge-run.ts +4 -4
  27. package/src/public-cli/notary-run.ts +91 -30
  28. package/src/public-cli/post-admission.ts +424 -360
  29. package/src/public-cli/run-lifecycle.ts +205 -0
  30. package/src/public-cli/seat-ticket-binding.ts +136 -16
  31. package/src/public-cli/settlement.ts +74 -52
  32. package/src/run-ticket-number.ts +53 -0
  33. package/src/submission-ledger.ts +86 -18
  34. package/dist/ledger-session-read.js +0 -241
  35. package/dist/ticket-seat-memory.js +0 -244
  36. 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
- * Bind ticketNumber onto an unbound admitted seat via LLM instruction recognition.
25
- * Already-settled admissions (ticketNumber or durable true-unbound) short-circuit —
26
- * no re-entry into hermes on auto-resume attempts or manual resume (#635).
27
- * true-unbound is persisted once; failed verification throws.
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 async function resolveSeatTicketBinding(
30
- admitted: AdmittedRoleInvocation,
31
- env: SeatTicketBindingEnv = {},
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: admitted.projectRoot,
54
+ cwd: projectRoot,
41
55
  });
42
56
  const checkExistence = createGhTicketExistenceChecker();
43
- const origin = resolveDiaristGithubOrigin(admitted.projectRoot);
44
- const resolution = await resolveDiaristTicketFromInstruction({
45
- instruction: admitted.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
+ }