@akagilnc/pi-workflow-roles 0.1.5577 → 0.1.5593

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.
@@ -1,21 +1,20 @@
1
1
  /**
2
- * Public Invocation request admission: optional opaque instruction, frozen
3
- * Attachments, project default/override (ADR 0052 / #106).
2
+ * Public Invocation request admission: optional opaque instruction, caller
3
+ * attachment paths as-is, project default/override (ADR 0052 / #106 / #1165).
4
4
  */
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { existsSync } from "node:fs";
7
- import { lstat, mkdtemp, readFile, realpath, rename, rm, writeFile, } from "node:fs/promises";
8
- import { tmpdir } from "node:os";
9
- import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
7
+ import { readFile, realpath, rename, writeFile, } from "node:fs/promises";
8
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
10
9
  import { activationBookDirectory, ensureRealDirectoryTree, homeFromRunDirectory, pathContainedIn, resolveActivationLedgerHome, } from "../activation-ledger-topology.js";
11
10
  import { resolveBookKeyFromGit } from "../activation-ledger-git.js";
12
11
  import { ensureRoleRunDirectory, ensureRoleRunPlacement, formatRunLeaf, isUnboundRunDirectory, listBookRunDirectories, roleRunPlacement, } from "../role-run-placement.js";
13
12
  import { packagedAdmittedSubject, packagedArgvResult, packagedBareToken, packagedBaseIsRevision, packagedEmitsAuthorityRefs, packagedPublicInstructionSubject, packagedRoleMetadata, packagedStoresSourceRunRaw, packagedSubjectChoices, } from "../packaged-role-registry.js";
14
13
  import { isSafePositiveTicketNumber, parseTicketNumber, readBoardTicketNumber, requireSafePositiveTicketNumber, } from "../run-ticket-number.js";
15
- import { rewriteRunDirectoryPathFields, rewriteRunDirectoryPathValue, } from "../role-run-relocation.js";
14
+ import { rewriteRunDirectoryPathFields, } from "../role-run-relocation.js";
16
15
  import { readPageSync, renderCurrentSync, updateSectionSync, writeSectionSync } from "../run-dossier.js";
17
16
  import { loadDoctorCase, } from "../doctor-evidence.js";
18
- import { emptyCollectorManifest, loadCollectorManifest, parseCollectorPrNumber, parseCollectorRepository, } from "../collector-config.js";
17
+ import { parseCollectorPrNumber, parseCollectorRepository, } from "../collector-config.js";
19
18
  import { ownerRepoFromGitHubRemoteUrl } from "./github-remote.js";
20
19
  import { FixerPacketValidationError, parseFixerPrerequisites, } from "../package-contracts/fixer-packet.js";
21
20
  import { createProductionMergerGitState } from "../merger-git-state.js";
@@ -25,10 +24,23 @@ import { rehomeUnboundTicketProvenance } from "../ticket-provenance.js";
25
24
  import { uuidv7 } from "../uuidv7.js";
26
25
  import {} from "../notary-contracts.js";
27
26
  import { NotarySourceRunError, resolveNotarySourceRunLocator, } from "../notary-source-run.js";
28
- import { appendEngineSessionMaterial, } from "../package-resources/engine-material.js";
29
27
  import { CliUsageError } from "./cli-errors.js";
30
28
  import { REJECTED_PUBLIC_SPELLINGS, createTypedOptionConsumer, evaluateAnalystModeOptionContract, optionsForOwner, resolveAnalystMode, } from "./option-definitions.js";
31
29
  import { errorText, isRecord } from "../unknown-value.js";
30
+ /** Normalize admitted attachment records (caller path only; #1165). */
31
+ export function normalizeAdmittedAttachment(raw) {
32
+ if (typeof raw !== "object" || raw === null)
33
+ return undefined;
34
+ const record = raw;
35
+ const path = record.path;
36
+ // Opaque caller path — keep "" / whitespace; only a non-string is absent.
37
+ if (typeof path === "string")
38
+ return { path };
39
+ return undefined;
40
+ }
41
+ function admitCallerAttachments(attachmentPaths) {
42
+ return attachmentPaths.map((path) => ({ path }));
43
+ }
32
44
  /** Issue a principal from the single authoritative role-run placement. */
33
45
  export function issueAdmissionPlacement(authority, request) {
34
46
  const ledgerHome = resolveActivationLedgerHome(request.home);
@@ -262,12 +274,9 @@ export async function relocateAdmittedRunToTicket(admitted, authority, heldLease
262
274
  "taskPath",
263
275
  "packetPath",
264
276
  "prerequisitesPath",
265
- "requestManifestPath",
266
277
  "mergerInputPath",
267
278
  ], oldRunDirectory, target.runDirectory);
268
- for (const attachment of admitted.attachments) {
269
- attachment.frozenPath = rewriteRunDirectoryPathValue(attachment.frozenPath, oldRunDirectory, target.runDirectory);
270
- }
279
+ // #1165: caller --attach / --request-manifest paths stay as given; do not rewrite.
271
280
  admitted.principal = principal;
272
281
  // current.json projects host.original from this run directory. Ownership is
273
282
  // already transferred; a refuse still carries the true cause to the caller
@@ -383,7 +392,8 @@ function assignPublicSeatOption(owner, taken, fields) {
383
392
  const value = taken.value;
384
393
  switch (taken.def.id) {
385
394
  case "attach":
386
- fields.attachmentPaths.push(requireOptionPath(taken.def.canonical, value));
395
+ // #1165: file-flag values are opaque; only a missing argument is a usage error.
396
+ fields.attachmentPaths.push(requireProvidedOptionValue(taken.def.canonical, value));
387
397
  return;
388
398
  case "project":
389
399
  fields.project = requireOptionPath(taken.def.canonical, value);
@@ -427,7 +437,8 @@ function assignPublicSeatOption(owner, taken, fields) {
427
437
  fields.repo = parseRepoOption(value);
428
438
  return;
429
439
  case "request-manifest":
430
- fields.requestManifestPath = requireOptionPath(taken.def.canonical, value);
440
+ // #1165: file-flag values are opaque; only a missing argument is a usage error.
441
+ fields.requestManifestPath = requireProvidedOptionValue(taken.def.canonical, value);
431
442
  return;
432
443
  case "issue": {
433
444
  if (value === undefined || value.trim() === "") {
@@ -540,6 +551,16 @@ function requireOptionPath(flag, value) {
540
551
  }
541
552
  return value;
542
553
  }
554
+ /**
555
+ * #1165 file flags (--attach / --request-manifest): keep the provided string
556
+ * opaque (including spaces and ""). Only a missing argument is a usage error.
557
+ */
558
+ function requireProvidedOptionValue(flag, value) {
559
+ if (value === undefined) {
560
+ throw new CliUsageError(`${flag} requires a path`);
561
+ }
562
+ return value;
563
+ }
543
564
  /**
544
565
  * Shared Skill-arg token rule for caller-controlled values projected into the
545
566
  * space-joined Skill invocation line. Rejects blank, whitespace (smuggles the
@@ -625,123 +646,6 @@ export function requireAuthorityRef(value) {
625
646
  optionLike: "--authority-ref requires a durable reference, not inline Spec prose",
626
647
  });
627
648
  }
628
- async function readRegularFileAttachment(sourcePath) {
629
- const absolute = isAbsolute(sourcePath) ? sourcePath : resolve(sourcePath);
630
- let st;
631
- try {
632
- st = await lstat(absolute);
633
- }
634
- catch (error) {
635
- throw new CliUsageError(`attachment is not a readable regular file: ${sourcePath}`, { cause: error });
636
- }
637
- if (!st.isFile() || st.isSymbolicLink()) {
638
- throw new CliUsageError(`attachment must be a regular file (not a directory or symlink): ${sourcePath}`);
639
- }
640
- try {
641
- return { absolute, bytes: await readFile(absolute) };
642
- }
643
- catch (error) {
644
- throw new CliUsageError(`attachment is not a readable regular file: ${sourcePath}`, { cause: error });
645
- }
646
- }
647
- /**
648
- * Snapshot deferred inputs sequentially before identity side effects. The staging
649
- * files keep aggregate attachment bytes off heap until their final run is known.
650
- */
651
- async function removePreparedAttachmentDirectory(stagingDirectory) {
652
- await rm(stagingDirectory, { recursive: true, force: true });
653
- }
654
- /** Own the complete deferred-snapshot lifetime without masking either failure. */
655
- export async function withPreparedAttachments(attachmentPaths, use) {
656
- if (attachmentPaths.length === 0)
657
- return await use([]);
658
- const stagingDirectory = await mkdtemp(join(tmpdir(), "ak-role-attachments-"));
659
- const prepared = [];
660
- let result;
661
- try {
662
- for (let index = 0; index < attachmentPaths.length; index += 1) {
663
- const { absolute, bytes } = await readRegularFileAttachment(attachmentPaths[index]);
664
- const snapshotPath = join(stagingDirectory, String(index).padStart(6, "0"));
665
- await writeFile(snapshotPath, bytes);
666
- prepared.push({ absolute, snapshotPath });
667
- }
668
- result = await use(prepared);
669
- }
670
- catch (primary) {
671
- try {
672
- await removePreparedAttachmentDirectory(stagingDirectory);
673
- }
674
- catch (cleanup) {
675
- throw new AggregateError([primary, cleanup], "attachment snapshot operation failed and cleanup also failed", { cause: primary });
676
- }
677
- throw primary;
678
- }
679
- await removePreparedAttachmentDirectory(stagingDirectory);
680
- return result;
681
- }
682
- async function freezeAttachmentBytes(provenancePath, bytes, destinationDir, index) {
683
- const frozenPath = join(destinationDir, `${String(index).padStart(2, "0")}-${basename(provenancePath)}`);
684
- await writeFile(frozenPath, bytes);
685
- return {
686
- provenancePath,
687
- frozenPath,
688
- byteLength: bytes.byteLength,
689
- sha256: sha256Hex(bytes),
690
- mediaKind: "regular-file",
691
- };
692
- }
693
- async function freezePreparedAttachment(prepared, destinationDir, index) {
694
- return freezeAttachmentBytes(prepared.absolute, await readFile(prepared.snapshotPath), destinationDir, index);
695
- }
696
- async function freezeRegularFileAttachment(sourcePath, destinationDir, index) {
697
- const { absolute, bytes } = await readRegularFileAttachment(sourcePath);
698
- return {
699
- attachment: await freezeAttachmentBytes(absolute, bytes, destinationDir, index),
700
- body: bytes,
701
- };
702
- }
703
- /** Freeze attachments only — ticket binding is the shared LLM seat path (#635). */
704
- async function freezeAttachments(ledgerHome, attachmentPaths, attachmentsDirectory) {
705
- const attachments = [];
706
- // attachments/ appears only when something is frozen; created through the
707
- // ledger's symlink-rejecting tree walk, never a plain mkdir.
708
- if (attachmentPaths.length > 0) {
709
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
710
- }
711
- for (let i = 0; i < attachmentPaths.length; i += 1) {
712
- const frozen = await freezeRegularFileAttachment(attachmentPaths[i], attachmentsDirectory, i);
713
- attachments.push(frozen.attachment);
714
- }
715
- return attachments;
716
- }
717
- /**
718
- * Freeze summons attachments into an already-retained run (#637 same-ticket resume).
719
- * Writes under attachments/summons-<key>/ so prior freeze names stay intact.
720
- * Manual resume never calls this — birth attachments keep their original semantics.
721
- */
722
- export async function freezeAttachmentsIntoRun(attachmentPaths, runDirectory, summonsKey = `s-${Date.now().toString(36)}`) {
723
- if (attachmentPaths.length === 0)
724
- return [];
725
- const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(runDirectory));
726
- const attachmentsDirectory = join(runDirectory, "attachments", summonsKey);
727
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
728
- return freezeAttachments(ledgerHome, attachmentPaths, attachmentsDirectory);
729
- }
730
- /** Freeze prepared bytes and metadata through one path for birth or retained runs. */
731
- export async function freezePreparedAttachmentsIntoRun(prepared, runDirectory, summonsKey) {
732
- if (prepared.length === 0)
733
- return [];
734
- const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(runDirectory));
735
- const attachmentsDirectory = summonsKey === undefined
736
- ? join(runDirectory, "attachments")
737
- : join(runDirectory, "attachments", summonsKey);
738
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
739
- const attachments = [];
740
- for (let index = 0; index < prepared.length; index += 1) {
741
- attachments.push(await freezePreparedAttachment(prepared[index], attachmentsDirectory, index));
742
- }
743
- return attachments;
744
- }
745
649
  function ticketAdmissionFields(ticketNumber) {
746
650
  if (ticketNumber === undefined)
747
651
  return {};
@@ -892,36 +796,21 @@ export async function admitPublicRole(role, parsed, env, override) {
892
796
  else {
893
797
  repository = resolveGitHubRemoteRepository(projectRoot);
894
798
  }
895
- let manifest = emptyCollectorManifest();
896
- let manifestCanonicalJson;
897
- if (parsed.requestManifestPath !== undefined) {
898
- try {
899
- manifest = await loadCollectorManifest(parsed.requestManifestPath);
900
- manifestCanonicalJson = manifest.canonicalJson;
901
- }
902
- catch (error) {
903
- throw new CliUsageError(errorText(error), { cause: error });
904
- }
905
- }
906
- const manifestDigest = manifest.digest;
799
+ // #1165: pass caller path as-is; package does not read, validate, or rewrite.
907
800
  const admittedCollector = await admitStandardMaterialInvocation("collector", {
908
801
  ...shared,
909
802
  instruction,
910
803
  attachmentPaths,
911
804
  ...project,
912
- placedFields: async (placed) => {
805
+ placedFields: async () => {
913
806
  // #1088: explicit --pr only. Unbound PR is located by the LLM via host CLI.
914
- let requestManifestPath;
915
- if (manifestCanonicalJson !== undefined) {
916
- requestManifestPath = join(placed.runDirectory, "request-manifest.json");
917
- await writeFile(requestManifestPath, manifestCanonicalJson, "utf8");
918
- }
919
807
  return {
920
808
  ...(explicitPrNumber === undefined ? {} : { prNumber: explicitPrNumber }),
921
809
  repository: repository.canonical,
922
810
  repositoryDisplay: repository.display,
923
- ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
924
- manifestDigest,
811
+ ...(parsed.requestManifestPath === undefined
812
+ ? {}
813
+ : { requestManifestPath: parsed.requestManifestPath }),
925
814
  };
926
815
  },
927
816
  });
@@ -932,12 +821,10 @@ export async function admitPublicRole(role, parsed, env, override) {
932
821
  throw new CliUsageError(`doctor --issue must be a positive integer, got ${Number.NaN}`);
933
822
  }
934
823
  const issueNumber = parsed.issueNumber;
935
- let frozenAttachments = [];
936
- const admittedDoctor = await admitStandardMaterialInvocation("doctor", {
824
+ return admitStandardMaterialInvocation("doctor", {
937
825
  ...shared,
938
826
  instruction,
939
- attachmentPaths: [],
940
- freezeAttachments: false,
827
+ attachmentPaths,
941
828
  ...project,
942
829
  placedFields: async (placed) => {
943
830
  let caseRunsPath;
@@ -974,16 +861,13 @@ export async function admitPublicRole(role, parsed, env, override) {
974
861
  const detail = errorText(error);
975
862
  throw new CliUsageError(`doctor case could not be constructed from retained evidence: ${detail}`, { cause: error });
976
863
  }
977
- frozenAttachments = await freezeAttachments(placed.ledgerHome, attachmentPaths, placed.attachmentsDirectory);
978
864
  return {
979
865
  issueNumber,
980
866
  caseRunsPath,
981
867
  caseIdentity,
982
- attachments: persistedAttachmentRefs(frozenAttachments),
983
868
  };
984
869
  },
985
870
  });
986
- return { ...admittedDoctor, attachments: frozenAttachments };
987
871
  }
988
872
  case "source-locator": {
989
873
  const projectRoot = resolve(parsed.project ?? shared.cwd);
@@ -1014,7 +898,7 @@ export async function admitPublicRole(role, parsed, env, override) {
1014
898
  instruction: "",
1015
899
  attachmentPaths: [],
1016
900
  ...project,
1017
- freezeAttachments: false,
901
+ recordAttachments: false,
1018
902
  admittedFields: {
1019
903
  sourceRunPath: sourceRun.runDirectory,
1020
904
  sourceRun,
@@ -1031,7 +915,7 @@ export async function admitPublicRole(role, parsed, env, override) {
1031
915
  instruction,
1032
916
  attachmentPaths: [],
1033
917
  ...project,
1034
- freezeAttachments: false,
918
+ recordAttachments: false,
1035
919
  admittedFields: { baseRevision },
1036
920
  });
1037
921
  }
@@ -1129,9 +1013,9 @@ async function placeRoleAdmission(options) {
1129
1013
  home: options.home,
1130
1014
  ...(options.materialize === undefined ? {} : { materialize: options.materialize }),
1131
1015
  });
1132
- const attachments = options.freezeAttachments === false
1016
+ const attachments = options.recordAttachments === false
1133
1017
  ? []
1134
- : await freezeAttachments(ledgerHome, options.attachmentPaths, attachmentsDirectory);
1018
+ : admitCallerAttachments(options.attachmentPaths);
1135
1019
  return {
1136
1020
  runId,
1137
1021
  bookKey,
@@ -1147,13 +1031,7 @@ async function placeRoleAdmission(options) {
1147
1031
  };
1148
1032
  }
1149
1033
  function persistedAttachmentRefs(attachments) {
1150
- return attachments.map((attachment) => ({
1151
- provenancePath: attachment.provenancePath,
1152
- frozenPath: attachment.frozenPath,
1153
- byteLength: attachment.byteLength,
1154
- sha256: attachment.sha256,
1155
- mediaKind: attachment.mediaKind,
1156
- }));
1034
+ return attachments.map((attachment) => ({ path: attachment.path }));
1157
1035
  }
1158
1036
  async function persistPlacedAdmission(admitted, placed, model) {
1159
1037
  writeAdmittedRequestPersistence(placed.runDirectory, admitted, {
@@ -1173,19 +1051,18 @@ async function persistPlacedAdmission(admitted, placed, model) {
1173
1051
  * before materializeCountersignInvocation writes the page.
1174
1052
  * Seats whose extra facts are known before placement pass them as admittedFields.
1175
1053
  * Seats whose extra facts depend on the placed run pass placedFields
1176
- * (coder writes task.md; fixer writes fix-packet.md; collector writes request-manifest.json;
1177
- * doctor resolves the case, then freezes attachments; merger writes merger-input.json).
1054
+ * (coder writes task.md; fixer writes fix-packet.md; merger writes merger-input.json).
1178
1055
  */
1179
1056
  async function admitStandardMaterialInvocation(role, options) {
1180
1057
  const defer = options.deferPersistence === true;
1181
- const skipFreeze = defer || options.freezeAttachments === false;
1058
+ const skipAttachments = defer || options.recordAttachments === false;
1182
1059
  const placed = await placeRoleAdmission({
1183
1060
  role,
1184
1061
  home: options.home,
1185
1062
  principalAuthority: options.principalAuthority,
1186
1063
  cwd: options.cwd,
1187
1064
  attachmentPaths: options.attachmentPaths,
1188
- ...(skipFreeze ? { freezeAttachments: false } : {}),
1065
+ ...(skipAttachments ? { recordAttachments: false } : {}),
1189
1066
  ...(defer ? { materialize: false } : {}),
1190
1067
  ...(options.project === undefined ? {} : { project: options.project }),
1191
1068
  ...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
@@ -1197,7 +1074,8 @@ async function admitStandardMaterialInvocation(role, options) {
1197
1074
  ? {}
1198
1075
  : { correlationId: options.correlationId };
1199
1076
  const instruction = options.instruction;
1200
- const instructionEmpty = instruction.trim() === "";
1077
+ // Admitted emptiness is literal absence, not whitespace-only (#1165 J3).
1078
+ const instructionEmpty = instruction.length === 0;
1201
1079
  const placedExtra = options.placedFields === undefined ? undefined : await options.placedFields(placed);
1202
1080
  const admittedFields = {
1203
1081
  ...(options.admittedFields ?? {}),
@@ -1245,25 +1123,28 @@ function admittedTransportPromptKind(admitted) {
1245
1123
  /**
1246
1124
  * One initial prompt transport. The registry `transportPrompt` leaf selects
1247
1125
  * a fixed kickoff, a bound baseline, or frozen skill args. Absent means the
1248
- * caller instruction plus frozen attachment paths.
1126
+ * caller instruction plus caller attachment paths as-is (#1165).
1127
+ * Engine / outsourcing material rides startup readingMaterial (#1167), not here.
1249
1128
  */
1250
- export function buildInstructionTransportPrompt(admitted, engineMaterial) {
1129
+ export function buildInstructionTransportPrompt(admitted) {
1251
1130
  const kind = admittedTransportPromptKind(admitted);
1252
1131
  if (kind === "fixed-kickoff") {
1253
1132
  if (admitted.sourceRunPath === undefined || admitted.sourceRunPath.trim() === "") {
1254
1133
  throw new Error("fixed-kickoff transport prompt is missing the source run pointer");
1255
1134
  }
1256
- return appendEngineSessionMaterial([admitted.sourceRunPath], engineMaterial).join("\n");
1135
+ return admitted.sourceRunPath;
1257
1136
  }
1258
1137
  if (kind === "baseline") {
1259
1138
  if (admitted.baseRevision === undefined) {
1260
1139
  throw new Error("baseline transport prompt is missing the bound revision");
1261
1140
  }
1262
1141
  const lines = [admitted.baseRevision];
1263
- if (!admitted.instructionEmpty) {
1142
+ // Use raw admitted.instruction — do not gate on instructionEmpty metadata
1143
+ // (old pages may set that flag wrong while still holding whitespace text).
1144
+ if (admitted.instruction.length > 0) {
1264
1145
  lines.push("", admitted.instruction);
1265
1146
  }
1266
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
1147
+ return lines.join("\n");
1267
1148
  }
1268
1149
  if (kind === "skill-args") {
1269
1150
  if (admitted.baseRevision === undefined
@@ -1276,20 +1157,28 @@ export function buildInstructionTransportPrompt(admitted, engineMaterial) {
1276
1157
  lens: admitted.lens,
1277
1158
  authorityRefs: admitted.authorityRefs,
1278
1159
  })];
1279
- if (!admitted.instructionEmpty && admitted.instruction.trim() !== "") {
1160
+ if (admitted.instruction.length > 0) {
1280
1161
  lines.push("", admitted.instruction);
1281
1162
  }
1282
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
1163
+ return lines.join("\n");
1283
1164
  }
1284
- const lines = [admitted.instructionEmpty ? "" : admitted.instruction];
1285
- if (admitted.attachments.length > 0) {
1165
+ // #1165 J3: assemble from the admitted instruction bytes themselves.
1166
+ const lines = [admitted.instruction];
1167
+ // #1164/#1165: each caller path keeps its file-flag provenance.
1168
+ const flaggedPaths = [
1169
+ ...admitted.attachments.map((attachment) => `--attach ${attachment.path}`),
1170
+ ...(admitted.requestManifestPath === undefined
1171
+ ? []
1172
+ : [`--request-manifest ${admitted.requestManifestPath}`]),
1173
+ ];
1174
+ if (flaggedPaths.length > 0) {
1286
1175
  lines.push("");
1287
- lines.push("已受理附件(冻结快照路径):");
1288
- for (const attachment of admitted.attachments) {
1289
- lines.push(`- ${attachment.frozenPath}`);
1176
+ lines.push("已受理附件:");
1177
+ for (const entry of flaggedPaths) {
1178
+ lines.push(`- ${entry}`);
1290
1179
  }
1291
1180
  }
1292
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
1181
+ return lines.join("\n");
1293
1182
  }
1294
1183
  /** Persist a deferred Countersign admission after same-ticket lookup found no retained run. */
1295
1184
  export async function materializeCountersignInvocation(admitted, options) {
@@ -1298,8 +1187,7 @@ export async function materializeCountersignInvocation(admitted, options) {
1298
1187
  home: options.home,
1299
1188
  principalAuthority: options.principalAuthority,
1300
1189
  cwd: admitted.projectRoot,
1301
- attachmentPaths: [],
1302
- freezeAttachments: false,
1190
+ attachmentPaths: options.attachmentPaths,
1303
1191
  runId: admitted.runId,
1304
1192
  ...(options.ticketNumber === undefined ? {} : { assertedTicketNumber: options.ticketNumber }),
1305
1193
  });
@@ -1310,8 +1198,7 @@ export async function materializeCountersignInvocation(admitted, options) {
1310
1198
  if (ticketFields.ticketNumber !== undefined) {
1311
1199
  admitted.ticketNumber = ticketFields.ticketNumber;
1312
1200
  }
1313
- const attachments = await freezePreparedAttachmentsIntoRun(options.preparedAttachments, admitted.runDirectory);
1314
- admitted.attachments = attachments;
1201
+ admitted.attachments = placement.attachments;
1315
1202
  writeAdmittedRequestPersistence(admitted.runDirectory, admitted, {
1316
1203
  sessionDirectory: placement.sessionDirectory,
1317
1204
  sessionFile: placement.sessionFile,
@@ -1332,10 +1219,13 @@ export async function loadAdmittedJudgeRequest(runDirectory) {
1332
1219
  return undefined;
1333
1220
  if (!Array.isArray(record.attachments))
1334
1221
  return undefined;
1222
+ const attachments = record.attachments
1223
+ .map((item) => normalizeAdmittedAttachment(item))
1224
+ .filter((item) => item !== undefined);
1335
1225
  return {
1336
1226
  instruction: record.instruction,
1337
1227
  instructionEmpty: record.instructionEmpty,
1338
- attachments: record.attachments,
1228
+ attachments,
1339
1229
  };
1340
1230
  }
1341
1231
  catch {
@@ -1597,7 +1487,7 @@ export function parseAnalystArgv(args) {
1597
1487
  return;
1598
1488
  }
1599
1489
  if (taken.def.id === "attach") {
1600
- pushValue("attach", requireOptionPath(taken.def.canonical, taken.value));
1490
+ pushValue("attach", requireProvidedOptionValue(taken.def.canonical, taken.value));
1601
1491
  return;
1602
1492
  }
1603
1493
  if (taken.def.id === "group-a-label" || taken.def.id === "group-b-label") {