@akagilnc/pi-workflow-roles 0.1.1883 → 0.1.1918

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.
@@ -21,6 +21,12 @@ import {
21
21
  type ReviewerPreflightViolation,
22
22
  } from "../reviewer-dispatch.ts";
23
23
  import type { ControlledFailureCause } from "./terminal.ts";
24
+ import {
25
+ hasUpstreamErrorTestimony,
26
+ projectConfirmedRemotePayload,
27
+ } from "../upstream-error-testimony.ts";
28
+
29
+ export { hasUpstreamErrorTestimony } from "../upstream-error-testimony.ts";
24
30
 
25
31
  /** Durable Reviewer-rejection child→parent page under AK_ROLE_RUN_DIR. */
26
32
  const REVIEWER_DISPATCH_REJECTION_FILE = "typed-known-failure.json";
@@ -147,34 +153,70 @@ export type ExplicitInternalKnownFailure = {
147
153
  readonly details?: Readonly<Record<string, unknown>>;
148
154
  };
149
155
 
156
+ function nonEmptyString(value: unknown): string | undefined {
157
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
158
+ }
159
+
160
+ function sessionStopDetails(input: {
161
+ readonly errorMessage?: string | null;
162
+ readonly provider?: string;
163
+ readonly model?: string;
164
+ readonly api?: string;
165
+ readonly rawStopReason?: string;
166
+ readonly httpStatus?: number;
167
+ readonly diagnostics?: unknown;
168
+ readonly body?: unknown;
169
+ readonly code?: unknown;
170
+ readonly errno?: unknown;
171
+ }): Record<string, unknown> {
172
+ const details: Record<string, unknown> = {};
173
+ const errorMessage = nonEmptyString(input.errorMessage);
174
+ if (errorMessage !== undefined) details.errorMessage = errorMessage;
175
+ const api = nonEmptyString(input.api);
176
+ if (api !== undefined) details.api = api;
177
+ const rawStopReason = nonEmptyString(input.rawStopReason);
178
+ if (rawStopReason !== undefined) details.rawStopReason = rawStopReason;
179
+ const testimony = hasUpstreamErrorTestimony(input);
180
+ if (testimony) {
181
+ const provider = nonEmptyString(input.provider);
182
+ if (provider !== undefined) details.provider = provider;
183
+ const model = nonEmptyString(input.model);
184
+ if (model !== undefined) details.model = model;
185
+ }
186
+ if (typeof input.httpStatus === "number") details.httpStatus = input.httpStatus;
187
+ if (input.diagnostics !== undefined) details.diagnostics = input.diagnostics;
188
+ // SDK structured payload fields: project only when held on a confirmed-remote node.
189
+ if (testimony) Object.assign(details, projectConfirmedRemotePayload(input));
190
+ return details;
191
+ }
192
+
150
193
  /**
151
- * Produce a typed provider knownFailure from a native session assistant stop.
152
- * Source fields are session-typed (stopReason / errorMessage / provider) — never
153
- * child stderr prose. Used by the public classifier after a real Pi child exits.
194
+ * Project a native session assistant stop onto the existing knownFailure chain.
195
+ * Classification follows two-way testimony: typed HTTP status or SDK structure
196
+ * keeps provider; stopReason, configured provider/model, or errorMessage prose
197
+ * alone is the existing unrecognized value. Present upstream payload is preserved
198
+ * in details without rewriting; missing fields are omitted.
154
199
  */
155
200
  export function knownFailureFromProviderStop(input: {
156
201
  readonly stopReason?: string;
157
202
  readonly errorMessage?: string | null;
158
203
  readonly provider?: string;
159
204
  readonly model?: string;
205
+ readonly api?: string;
206
+ readonly rawStopReason?: string;
207
+ readonly httpStatus?: number;
208
+ readonly diagnostics?: unknown;
209
+ readonly body?: unknown;
210
+ readonly code?: unknown;
211
+ readonly errno?: unknown;
160
212
  }): ExplicitInternalKnownFailure | undefined {
161
- if (input.stopReason !== "error") return undefined;
162
- const diagnostic =
163
- typeof input.errorMessage === "string" && input.errorMessage.trim() !== ""
164
- ? input.errorMessage.trim()
165
- : "provider failure";
166
- const identity: { name: string; code?: string } = {
167
- name: "ProviderStopError",
168
- };
169
- if (typeof input.provider === "string" && input.provider.trim() !== "") {
170
- identity.code = input.provider;
171
- } else if (typeof input.model === "string" && input.model.trim() !== "") {
172
- identity.code = input.model;
173
- }
213
+ if (input.stopReason !== "error" && input.stopReason !== "aborted") return undefined;
214
+ const diagnostic = nonEmptyString(input.errorMessage);
215
+ const details = sessionStopDetails(input);
174
216
  return {
175
- cause: "provider",
176
- identity,
177
- diagnostic,
217
+ cause: hasUpstreamErrorTestimony(input) ? "provider" : "unrecognized",
218
+ ...(diagnostic === undefined ? {} : { diagnostic }),
219
+ ...(Object.keys(details).length === 0 ? {} : { details }),
178
220
  };
179
221
  }
180
222
 
@@ -47,7 +47,7 @@ import {
47
47
  RESUME_TRANSPORT_ENVELOPE,
48
48
  RunWriterLeaseHeldError,
49
49
  type RunWriterLease,
50
- readTypedHttp429Observation,
50
+ type TypedProviderHttpObservation,
51
51
  } from "./run-lifecycle.ts";
52
52
  import {
53
53
  classifyPostAdmissionFailure,
@@ -59,7 +59,9 @@ import {
59
59
  isLawfulTypedTerminalOutcome,
60
60
  presentFailureTerminal,
61
61
  presentStructuralRejection,
62
- resolveAuditedRunnerKnownFailure,
62
+ resolveAuditedRunnerFailureResolution,
63
+ resolveControlledFailureResumeObservation,
64
+ controlledFailureInputFromResolution,
63
65
  explicitInternalKnownFailureClassificationInput,
64
66
  settleFailureTerminalResult,
65
67
  trySettleFixerTerminalResult,
@@ -205,6 +207,8 @@ async function presentControlledFailure(
205
207
  stderr: string;
206
208
  thrown?: unknown;
207
209
  knownFailure?: ExplicitInternalKnownFailure;
210
+ typedHttpObservationSettled?: true;
211
+ typedHttpObservation?: TypedProviderHttpObservation;
208
212
  },
209
213
  io: CliIo,
210
214
  ): Promise<{
@@ -213,10 +217,23 @@ async function presentControlledFailure(
213
217
  terminal: TerminalResult;
214
218
  }> {
215
219
  const hasThrown = Object.hasOwn(failureInput, "thrown");
220
+ const resumeObservation = await resolveControlledFailureResumeObservation({
221
+ runDirectory: admitted.runDirectory,
222
+ ...(failureInput.typedHttpObservationSettled === true
223
+ ? {
224
+ typedHttpObservationSettled: true as const,
225
+ ...(failureInput.typedHttpObservation === undefined
226
+ ? {}
227
+ : { typedHttpObservation: failureInput.typedHttpObservation }),
228
+ }
229
+ : {}),
230
+ });
231
+ const knownFailure =
232
+ failureInput.knownFailure ?? resumeObservation.observationReadFailure;
216
233
  const session =
217
234
  !hasThrown &&
218
235
  !failureInput.timedOut &&
219
- failureInput.knownFailure === undefined
236
+ knownFailure === undefined
220
237
  ? await inspectJudgeSession(admitted.sessionFile)
221
238
  : undefined;
222
239
  const failure = classifyPostAdmissionFailure({
@@ -224,12 +241,12 @@ async function presentControlledFailure(
224
241
  code: failureInput.code,
225
242
  stderr: failureInput.stderr,
226
243
  ...(hasThrown ? { thrown: failureInput.thrown } : {}),
227
- ...explicitInternalKnownFailureClassificationInput(failureInput.knownFailure),
244
+ ...explicitInternalKnownFailureClassificationInput(knownFailure),
228
245
  ...(session === undefined ? {} : { session }),
229
246
  });
230
247
 
231
248
  const hasLawfulTerminalResult = await hasLawfulFixerTerminalResult(admitted);
232
- const typedHttp429 = await readTypedHttp429Observation(admitted.runDirectory);
249
+ const typedHttp429 = resumeObservation.typedHttp429;
233
250
  const sessionPrincipalAvailable = await isSessionPrincipalAvailable(
234
251
  admitted.sessionFile,
235
252
  );
@@ -295,8 +312,9 @@ async function dispatchAdmittedFixer(input: {
295
312
  PI_CODING_AGENT_DIR: env.agentDir,
296
313
  AK_ROLE_RUN_DIR: admitted.runDirectory,
297
314
  };
298
- if (env.correlationId !== undefined && env.correlationId.trim() !== "") {
299
- childEnv.AK_CORRELATION_ID = env.correlationId;
315
+ const correlationId = admitted.correlationId ?? env.correlationId;
316
+ if (correlationId !== undefined && correlationId.trim() !== "") {
317
+ childEnv.AK_CORRELATION_ID = correlationId;
300
318
  }
301
319
 
302
320
  let result: ExplicitInternalPiResult;
@@ -386,10 +404,11 @@ async function dispatchAdmittedFixer(input: {
386
404
  env.model,
387
405
  env.credentials,
388
406
  );
389
- const knownFailure = await resolveAuditedRunnerKnownFailure({
407
+ const resolution = await resolveAuditedRunnerFailureResolution({
390
408
  runner: result.knownFailure,
391
409
  sessionFile: admitted.sessionFile,
392
410
  credential: credentialFailure,
411
+ runDirectory: admitted.runDirectory,
393
412
  });
394
413
  return await presentControlledFailure(
395
414
  admitted,
@@ -397,7 +416,7 @@ async function dispatchAdmittedFixer(input: {
397
416
  timedOut: result.timedOut,
398
417
  code: result.code,
399
418
  stderr: result.stderr,
400
- ...(knownFailure === undefined ? {} : { knownFailure }),
419
+ ...controlledFailureInputFromResolution(resolution),
401
420
  },
402
421
  io,
403
422
  );
@@ -489,7 +508,10 @@ export async function runPublicFixer(
489
508
 
490
509
  return await dispatchAdmittedFixer({
491
510
  admitted,
492
- env,
511
+ env: {
512
+ ...env,
513
+ ...(admitted.correlationId === undefined ? {} : { correlationId: admitted.correlationId }),
514
+ },
493
515
  io,
494
516
  extraArgs,
495
517
  lease,
@@ -575,7 +597,10 @@ export async function runPublicFixerResume(
575
597
 
576
598
  return await dispatchAdmittedFixer({
577
599
  admitted,
578
- env,
600
+ env: {
601
+ ...env,
602
+ ...(admitted.correlationId === undefined ? {} : { correlationId: admitted.correlationId }),
603
+ },
579
604
  io,
580
605
  extraArgs,
581
606
  lease,
@@ -20,6 +20,7 @@ import {
20
20
  } from "../activation-ledger-topology.ts";
21
21
  import { resolveBookKeyFromGit } from "../activation-ledger-git.ts";
22
22
  import { roleRunSessionCoordinates } from "../sitian-role-run-coordinates.ts";
23
+ import { resolveTicketNumberFromAttachmentBodies } from "../ticket-frontmatter.ts";
23
24
  import {
24
25
  loadDoctorCase,
25
26
  } from "../doctor-evidence.ts";
@@ -73,6 +74,16 @@ export type AdmittedRoleInvocationBase = {
73
74
  /** Exact Pi session file principal (bound at admission; reopened on resume). */
74
75
  readonly sessionFile: string;
75
76
  readonly admittedRequestPath: string;
77
+ /**
78
+ * Optional opaque invocation correlation restored from a prior admitted page
79
+ * (ADR 0049 host channel). Admission does not mint ticket-binding ids.
80
+ */
81
+ readonly correlationId?: string;
82
+ /**
83
+ * Typed ticket face from frozen attachment frontmatter (`ticketNumber`).
84
+ * Absent when no attachment carries a valid contract field (unbound).
85
+ */
86
+ readonly ticketNumber?: number;
76
87
  };
77
88
 
78
89
  export type AdmittedJudgeInvocation = AdmittedRoleInvocationBase & {
@@ -153,7 +164,7 @@ export type AdmittedRoleInvocation =
153
164
 
154
165
  type RoleInvocationLedgerSource = Pick<
155
166
  AdmittedRoleInvocationBase,
156
- "runId" | "bookKey" | "projectRoot" | "runDirectory" | "sessionDirectory" | "sessionFile"
167
+ "runId" | "bookKey" | "projectRoot" | "runDirectory" | "sessionDirectory" | "sessionFile" | "correlationId" | "ticketNumber"
157
168
  >;
158
169
 
159
170
  /**
@@ -173,6 +184,8 @@ async function writeRoleInvocationLedger(
173
184
  runDirectory: source.runDirectory,
174
185
  sessionDirectory: source.sessionDirectory,
175
186
  sessionFile: source.sessionFile,
187
+ ...(source.correlationId === undefined ? {} : { correlationId: source.correlationId }),
188
+ ...(source.ticketNumber === undefined ? {} : { ticketNumber: source.ticketNumber }),
176
189
  };
177
190
  await writeFile(
178
191
  join(source.runDirectory, "invocation.json"),
@@ -525,7 +538,7 @@ async function freezeRegularFileAttachment(
525
538
  sourcePath: string,
526
539
  destinationDir: string,
527
540
  index: number,
528
- ): Promise<FrozenAttachment> {
541
+ ): Promise<{ attachment: FrozenAttachment; body: Buffer }> {
529
542
  const absolute = isAbsolute(sourcePath) ? sourcePath : resolve(sourcePath);
530
543
  let st;
531
544
  try {
@@ -546,14 +559,49 @@ async function freezeRegularFileAttachment(
546
559
  const frozenPath = join(destinationDir, name);
547
560
  await writeFile(frozenPath, bytes);
548
561
  return {
549
- provenancePath: absolute,
550
- frozenPath,
551
- byteLength: bytes.byteLength,
552
- sha256: sha256Hex(bytes),
553
- mediaKind: "regular-file",
562
+ attachment: {
563
+ provenancePath: absolute,
564
+ frozenPath,
565
+ byteLength: bytes.byteLength,
566
+ sha256: sha256Hex(bytes),
567
+ mediaKind: "regular-file",
568
+ },
569
+ body: bytes,
554
570
  };
555
571
  }
556
572
 
573
+ /** Freeze attachments and resolve typed ticketNumber from frozen bodies (bind-if-present). */
574
+ async function freezeAttachmentsWithTicketNumber(
575
+ attachmentPaths: readonly string[],
576
+ attachmentsDirectory: string,
577
+ ): Promise<{
578
+ readonly attachments: FrozenAttachment[];
579
+ readonly ticketNumber?: number;
580
+ }> {
581
+ const attachments: FrozenAttachment[] = [];
582
+ const bodies: Buffer[] = [];
583
+ for (let i = 0; i < attachmentPaths.length; i += 1) {
584
+ const frozen = await freezeRegularFileAttachment(
585
+ attachmentPaths[i]!,
586
+ attachmentsDirectory,
587
+ i,
588
+ );
589
+ attachments.push(frozen.attachment);
590
+ bodies.push(frozen.body);
591
+ }
592
+ const ticketNumber = resolveTicketNumberFromAttachmentBodies(bodies);
593
+ return {
594
+ attachments,
595
+ ...(ticketNumber === undefined ? {} : { ticketNumber }),
596
+ };
597
+ }
598
+
599
+ function ticketAdmissionFields(
600
+ ticketNumber: number | undefined,
601
+ ): { ticketNumber?: number } {
602
+ return ticketNumber === undefined ? {} : { ticketNumber };
603
+ }
604
+
557
605
  export type AdmitJudgeInvocationOptions = {
558
606
  home: string;
559
607
  cwd: string;
@@ -583,16 +631,11 @@ export async function admitJudgeInvocation(
583
631
  ensureRealDirectoryTree(ledgerHome, sessionDirectory);
584
632
  ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
585
633
 
586
- const attachments: FrozenAttachment[] = [];
587
- for (let i = 0; i < options.attachmentPaths.length; i += 1) {
588
- attachments.push(
589
- await freezeRegularFileAttachment(
590
- options.attachmentPaths[i]!,
591
- attachmentsDirectory,
592
- i,
593
- ),
594
- );
595
- }
634
+ const { attachments, ticketNumber } = await freezeAttachmentsWithTicketNumber(
635
+ options.attachmentPaths,
636
+ attachmentsDirectory,
637
+ );
638
+ const ticketFields = ticketAdmissionFields(ticketNumber);
596
639
 
597
640
  const instruction = options.instruction;
598
641
  const instructionEmpty = instruction.trim() === "";
@@ -604,6 +647,7 @@ export async function admitJudgeInvocation(
604
647
  runDirectory,
605
648
  sessionDirectory,
606
649
  sessionFile,
650
+ ...ticketFields,
607
651
  instruction,
608
652
  instructionEmpty,
609
653
  attachments: attachments.map((a) => ({
@@ -630,6 +674,7 @@ export async function admitJudgeInvocation(
630
674
  sessionDirectory,
631
675
  sessionFile,
632
676
  admittedRequestPath,
677
+ ...ticketFields,
633
678
  };
634
679
  }
635
680
 
@@ -721,16 +766,11 @@ export async function admitCoderInvocation(
721
766
  ensureRealDirectoryTree(ledgerHome, sessionDirectory);
722
767
  ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
723
768
 
724
- const attachments: FrozenAttachment[] = [];
725
- for (let i = 0; i < options.attachmentPaths.length; i += 1) {
726
- attachments.push(
727
- await freezeRegularFileAttachment(
728
- options.attachmentPaths[i]!,
729
- attachmentsDirectory,
730
- i,
731
- ),
732
- );
733
- }
769
+ const { attachments, ticketNumber } = await freezeAttachmentsWithTicketNumber(
770
+ options.attachmentPaths,
771
+ attachmentsDirectory,
772
+ );
773
+ const ticketFields = ticketAdmissionFields(ticketNumber);
734
774
 
735
775
  const taskPath = join(runDirectory, "task.md");
736
776
  await writeFile(taskPath, instruction, "utf8");
@@ -744,6 +784,7 @@ export async function admitCoderInvocation(
744
784
  runDirectory,
745
785
  sessionDirectory,
746
786
  sessionFile,
787
+ ...ticketFields,
747
788
  instruction,
748
789
  instructionEmpty: false,
749
790
  taskPath,
@@ -773,6 +814,7 @@ export async function admitCoderInvocation(
773
814
  sessionFile,
774
815
  admittedRequestPath,
775
816
  taskPath,
817
+ ...ticketFields,
776
818
  };
777
819
  }
778
820
 
@@ -828,34 +870,15 @@ export async function admitFixerInvocation(
828
870
  throw new CliUsageError("fixer phase must be plan or apply");
829
871
  }
830
872
 
831
- const projectRoot = resolve(options.project ?? options.cwd);
832
- const runId = (options.createRunId ?? uuidv7)();
833
- const { ledgerHome, bookKey, runDirectory, sessionDirectory, sessionFile } =
834
- roleRunSessionCoordinates({ cwd: projectRoot, runId, role: "fixer", home: options.home });
835
- const attachmentsDirectory = join(runDirectory, "attachments");
836
- ensureRealDirectoryTree(ledgerHome, sessionDirectory);
837
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
838
-
839
- const attachments: FrozenAttachment[] = [];
840
- for (let i = 0; i < options.attachmentPaths.length; i += 1) {
841
- attachments.push(
842
- await freezeRegularFileAttachment(
843
- options.attachmentPaths[i]!,
844
- attachmentsDirectory,
845
- i,
846
- ),
847
- );
848
- }
849
-
873
+ // Validate/read prerequisites before freezing request materials.
850
874
  let prerequisites: readonly FixerPrerequisite[] = Object.freeze([]);
851
- let prerequisitesPath: string | undefined;
875
+ let prerequisitesSource: string | undefined;
852
876
  if (options.prerequisitesPath !== undefined) {
853
877
  const absolutePrereq = isAbsolute(options.prerequisitesPath)
854
878
  ? options.prerequisitesPath
855
879
  : resolve(options.prerequisitesPath);
856
- let source: string;
857
880
  try {
858
- source = await readFile(absolutePrereq, "utf8");
881
+ prerequisitesSource = await readFile(absolutePrereq, "utf8");
859
882
  } catch (error) {
860
883
  throw new CliUsageError(
861
884
  `fixer prerequisites path is unreadable: ${options.prerequisitesPath}`,
@@ -863,13 +886,31 @@ export async function admitFixerInvocation(
863
886
  );
864
887
  }
865
888
  try {
866
- prerequisites = parseFixerPrerequisites(source);
889
+ prerequisites = parseFixerPrerequisites(prerequisitesSource);
867
890
  } catch (error) {
868
891
  if (error instanceof FixerPacketValidationError) {
869
892
  throw new CliUsageError(error.message, { cause: error });
870
893
  }
871
894
  throw error;
872
895
  }
896
+ }
897
+
898
+ const projectRoot = resolve(options.project ?? options.cwd);
899
+ const runId = (options.createRunId ?? uuidv7)();
900
+ const { ledgerHome, bookKey, runDirectory, sessionDirectory, sessionFile } =
901
+ roleRunSessionCoordinates({ cwd: projectRoot, runId, role: "fixer", home: options.home });
902
+ const attachmentsDirectory = join(runDirectory, "attachments");
903
+ ensureRealDirectoryTree(ledgerHome, sessionDirectory);
904
+ ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
905
+
906
+ const { attachments, ticketNumber } = await freezeAttachmentsWithTicketNumber(
907
+ options.attachmentPaths,
908
+ attachmentsDirectory,
909
+ );
910
+ const ticketFields = ticketAdmissionFields(ticketNumber);
911
+
912
+ let prerequisitesPath: string | undefined;
913
+ if (prerequisitesSource !== undefined) {
873
914
  prerequisitesPath = join(runDirectory, "prerequisites.json");
874
915
  await writeFile(
875
916
  prerequisitesPath,
@@ -890,6 +931,7 @@ export async function admitFixerInvocation(
890
931
  runDirectory,
891
932
  sessionDirectory,
892
933
  sessionFile,
934
+ ...ticketFields,
893
935
  instruction,
894
936
  instructionEmpty: false,
895
937
  packetPath,
@@ -926,6 +968,7 @@ export async function admitFixerInvocation(
926
968
  packetPath,
927
969
  ...(prerequisitesPath === undefined ? {} : { prerequisitesPath }),
928
970
  prerequisites,
971
+ ...ticketFields,
929
972
  };
930
973
  }
931
974
 
@@ -1103,6 +1146,19 @@ export async function admitCollectorInvocation(
1103
1146
  repository = resolveGitHubRemoteRepository(projectRoot);
1104
1147
  }
1105
1148
 
1149
+ // Validate optional request-manifest before freezing request materials.
1150
+ let manifest = emptyCollectorManifest();
1151
+ let manifestCanonicalJson: string | undefined;
1152
+ if (options.requestManifestPath !== undefined) {
1153
+ try {
1154
+ manifest = await loadCollectorManifest(options.requestManifestPath);
1155
+ manifestCanonicalJson = manifest.canonicalJson;
1156
+ } catch (error) {
1157
+ throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
1158
+ }
1159
+ }
1160
+ const manifestDigest = manifest.digest;
1161
+
1106
1162
  const runId = (options.createRunId ?? uuidv7)();
1107
1163
  const { ledgerHome, bookKey, runDirectory, sessionDirectory, sessionFile } =
1108
1164
  roleRunSessionCoordinates({ cwd: projectRoot, runId, role: "collector", home: options.home });
@@ -1110,30 +1166,17 @@ export async function admitCollectorInvocation(
1110
1166
  ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1111
1167
  ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1112
1168
 
1113
- const attachments: FrozenAttachment[] = [];
1114
- const attachmentPaths = options.attachmentPaths ?? [];
1115
- for (let i = 0; i < attachmentPaths.length; i += 1) {
1116
- attachments.push(
1117
- await freezeRegularFileAttachment(
1118
- attachmentPaths[i]!,
1119
- attachmentsDirectory,
1120
- i,
1121
- ),
1122
- );
1123
- }
1169
+ const { attachments, ticketNumber } = await freezeAttachmentsWithTicketNumber(
1170
+ options.attachmentPaths ?? [],
1171
+ attachmentsDirectory,
1172
+ );
1173
+ const ticketFields = ticketAdmissionFields(ticketNumber);
1124
1174
 
1125
- let manifest = emptyCollectorManifest();
1126
1175
  let requestManifestPath: string | undefined;
1127
- if (options.requestManifestPath !== undefined) {
1128
- try {
1129
- manifest = await loadCollectorManifest(options.requestManifestPath);
1130
- } catch (error) {
1131
- throw new CliUsageError(error instanceof Error ? error.message : String(error), { cause: error });
1132
- }
1176
+ if (manifestCanonicalJson !== undefined) {
1133
1177
  requestManifestPath = join(runDirectory, "request-manifest.json");
1134
- await writeFile(requestManifestPath, manifest.canonicalJson, "utf8");
1178
+ await writeFile(requestManifestPath, manifestCanonicalJson, "utf8");
1135
1179
  }
1136
- const manifestDigest = manifest.digest;
1137
1180
 
1138
1181
  const instruction = options.instruction ?? "";
1139
1182
  const instructionEmpty = instruction.trim() === "";
@@ -1145,6 +1188,7 @@ export async function admitCollectorInvocation(
1145
1188
  runDirectory,
1146
1189
  sessionDirectory,
1147
1190
  sessionFile,
1191
+ ...ticketFields,
1148
1192
  instruction,
1149
1193
  instructionEmpty,
1150
1194
  prNumber,
@@ -1184,6 +1228,7 @@ export async function admitCollectorInvocation(
1184
1228
  repository,
1185
1229
  ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
1186
1230
  manifestDigest,
1231
+ ...ticketFields,
1187
1232
  };
1188
1233
  }
1189
1234
 
@@ -1462,17 +1507,11 @@ export async function admitDoctorInvocation(
1462
1507
  ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1463
1508
  ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1464
1509
 
1465
- const attachments: FrozenAttachment[] = [];
1466
- const attachmentPaths = options.attachmentPaths ?? [];
1467
- for (let i = 0; i < attachmentPaths.length; i += 1) {
1468
- attachments.push(
1469
- await freezeRegularFileAttachment(
1470
- attachmentPaths[i]!,
1471
- attachmentsDirectory,
1472
- i,
1473
- ),
1474
- );
1475
- }
1510
+ const { attachments, ticketNumber } = await freezeAttachmentsWithTicketNumber(
1511
+ options.attachmentPaths ?? [],
1512
+ attachmentsDirectory,
1513
+ );
1514
+ const ticketFields = ticketAdmissionFields(ticketNumber);
1476
1515
 
1477
1516
  const instruction = options.instruction ?? "";
1478
1517
  const instructionEmpty = instruction.trim() === "";
@@ -1484,6 +1523,7 @@ export async function admitDoctorInvocation(
1484
1523
  runDirectory,
1485
1524
  sessionDirectory,
1486
1525
  sessionFile,
1526
+ ...ticketFields,
1487
1527
  instruction,
1488
1528
  instructionEmpty,
1489
1529
  issueNumber: options.issueNumber,
@@ -1520,6 +1560,7 @@ export async function admitDoctorInvocation(
1520
1560
  issueNumber: options.issueNumber,
1521
1561
  caseRunsPath,
1522
1562
  caseIdentity,
1563
+ ...ticketFields,
1523
1564
  };
1524
1565
  }
1525
1566
 
@@ -1625,16 +1666,11 @@ export async function admitReviewerInvocation(
1625
1666
  ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1626
1667
 
1627
1668
  // Public parse already rejects attachments; keep freeze loop for structural symmetry.
1628
- const attachments: FrozenAttachment[] = [];
1629
- for (let i = 0; i < options.attachmentPaths.length; i += 1) {
1630
- attachments.push(
1631
- await freezeRegularFileAttachment(
1632
- options.attachmentPaths[i]!,
1633
- attachmentsDirectory,
1634
- i,
1635
- ),
1636
- );
1637
- }
1669
+ const { attachments, ticketNumber } = await freezeAttachmentsWithTicketNumber(
1670
+ options.attachmentPaths,
1671
+ attachmentsDirectory,
1672
+ );
1673
+ const ticketFields = ticketAdmissionFields(ticketNumber);
1638
1674
 
1639
1675
  const instruction = options.instruction;
1640
1676
  const instructionEmpty = instruction.trim() === "";
@@ -1646,6 +1682,7 @@ export async function admitReviewerInvocation(
1646
1682
  runDirectory,
1647
1683
  sessionDirectory,
1648
1684
  sessionFile,
1685
+ ...ticketFields,
1649
1686
  instruction,
1650
1687
  instructionEmpty,
1651
1688
  baseRevision: options.baseRevision,
@@ -1678,6 +1715,7 @@ export async function admitReviewerInvocation(
1678
1715
  sessionFile,
1679
1716
  admittedRequestPath,
1680
1717
  baseRevision: options.baseRevision,
1718
+ ...ticketFields,
1681
1719
  };
1682
1720
  }
1683
1721
 
@@ -1845,16 +1883,11 @@ export async function admitMergerInvocation(
1845
1883
  ensureRealDirectoryTree(ledgerHome, sessionDirectory);
1846
1884
  ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1847
1885
 
1848
- const attachments: FrozenAttachment[] = [];
1849
- for (let i = 0; i < options.attachmentPaths.length; i += 1) {
1850
- attachments.push(
1851
- await freezeRegularFileAttachment(
1852
- options.attachmentPaths[i]!,
1853
- attachmentsDirectory,
1854
- i,
1855
- ),
1856
- );
1857
- }
1886
+ const { attachments, ticketNumber } = await freezeAttachmentsWithTicketNumber(
1887
+ options.attachmentPaths,
1888
+ attachmentsDirectory,
1889
+ );
1890
+ const ticketFields = ticketAdmissionFields(ticketNumber);
1858
1891
 
1859
1892
  // Intent materials seed primary-source investigation; the method owns the work.
1860
1893
  const targetIntent = mergerMaterialFromUtf8(
@@ -1866,6 +1899,7 @@ export async function admitMergerInvocation(
1866
1899
  const taskMaterial = mergerMaterialFromUtf8(instruction);
1867
1900
  const authorityMaterial = mergerMaterialFromUtf8(instruction);
1868
1901
 
1902
+ // Validate merger envelope before placing admitted identity.
1869
1903
  const mergerInput = validateMergerInput({
1870
1904
  version: 1,
1871
1905
  attemptId: runId,
@@ -1898,6 +1932,7 @@ export async function admitMergerInvocation(
1898
1932
  runDirectory,
1899
1933
  sessionDirectory,
1900
1934
  sessionFile,
1935
+ ...ticketFields,
1901
1936
  instruction,
1902
1937
  instructionEmpty: false,
1903
1938
  mergerInputPath,
@@ -1938,6 +1973,7 @@ export async function admitMergerInvocation(
1938
1973
  admittedRequestPath,
1939
1974
  mergerInputPath,
1940
1975
  derived: admitted.derived,
1976
+ ...ticketFields,
1941
1977
  };
1942
1978
  }
1943
1979