@akagilnc/pi-workflow-roles 0.1.5583 → 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,20 +1,16 @@
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
7
  import {
8
- lstat,
9
- mkdtemp,
10
8
  readFile,
11
9
  realpath,
12
10
  rename,
13
- rm,
14
11
  writeFile,
15
12
  } from "node:fs/promises";
16
- import { tmpdir } from "node:os";
17
- import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
13
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
18
14
 
19
15
  import {
20
16
  activationBookDirectory,
@@ -57,7 +53,6 @@ import {
57
53
  } from "../run-ticket-number.ts";
58
54
  import {
59
55
  rewriteRunDirectoryPathFields,
60
- rewriteRunDirectoryPathValue,
61
56
  } from "../role-run-relocation.ts";
62
57
  import { readPageSync, renderCurrentSync, updateSectionSync, writeSectionSync } from "../run-dossier.ts";
63
58
  import {
@@ -65,8 +60,6 @@ import {
65
60
  } from "../doctor-evidence.ts";
66
61
  import type { DoctorCaseIdentity } from "../doctor-contracts.ts";
67
62
  import {
68
- emptyCollectorManifest,
69
- loadCollectorManifest,
70
63
  parseCollectorPrNumber,
71
64
  parseCollectorRepository,
72
65
  type CollectorRepository,
@@ -110,16 +103,27 @@ import type { PublicThinkingLevel } from "./registry.ts";
110
103
 
111
104
  import { errorText, isRecord } from "../unknown-value.ts";
112
105
 
106
+ /** Caller-supplied attachment path recorded as-is (#1165). */
113
107
  export type FrozenAttachment = {
114
- /** Original caller path retained only as provenance. */
115
- readonly provenancePath: string;
116
- /** Absolute path of the admitted frozen snapshot bytes. */
117
- readonly frozenPath: string;
118
- readonly byteLength: number;
119
- readonly sha256: string;
120
- readonly mediaKind: "regular-file";
108
+ readonly path: string;
121
109
  };
122
110
 
111
+ /** Normalize admitted attachment records (caller path only; #1165). */
112
+ export function normalizeAdmittedAttachment(raw: unknown): FrozenAttachment | undefined {
113
+ if (typeof raw !== "object" || raw === null) return undefined;
114
+ const record = raw as Record<string, unknown>;
115
+ const path = record.path;
116
+ // Opaque caller path — keep "" / whitespace; only a non-string is absent.
117
+ if (typeof path === "string") return { path };
118
+ return undefined;
119
+ }
120
+
121
+ function admitCallerAttachments(
122
+ attachmentPaths: readonly string[],
123
+ ): readonly FrozenAttachment[] {
124
+ return attachmentPaths.map((path) => ({ path }));
125
+ }
126
+
123
127
  /** Shared admitted Role run identity (#106 common Invocation + #109 Coder). */
124
128
  export type AdmittedRoleInvocationBase = {
125
129
  readonly runId: string;
@@ -127,7 +131,7 @@ export type AdmittedRoleInvocationBase = {
127
131
  readonly projectRoot: string;
128
132
  /** Opaque instruction bytes as submitted. */
129
133
  readonly instruction: string;
130
- /** True when the caller supplied no nonblank instruction. */
134
+ /** True when the caller supplied a literally empty instruction (length 0). */
131
135
  readonly instructionEmpty: boolean;
132
136
  readonly attachments: readonly FrozenAttachment[];
133
137
  readonly runDirectory: string;
@@ -222,7 +226,6 @@ export type AdmittedCollectorInvocation = AdmittedRoleInvocationBase & {
222
226
  readonly prNumber?: number;
223
227
  readonly repository: CollectorRepository;
224
228
  readonly requestManifestPath?: string;
225
- readonly manifestDigest: string;
226
229
  };
227
230
 
228
231
  export type AdmittedDoctorInvocation = AdmittedRoleInvocationBase & {
@@ -622,19 +625,12 @@ export async function relocateAdmittedRunToTicket(
622
625
  "taskPath",
623
626
  "packetPath",
624
627
  "prerequisitesPath",
625
- "requestManifestPath",
626
628
  "mergerInputPath",
627
629
  ],
628
630
  oldRunDirectory,
629
631
  target.runDirectory,
630
632
  );
631
- for (const attachment of admitted.attachments) {
632
- (attachment as { frozenPath: string }).frozenPath = rewriteRunDirectoryPathValue(
633
- attachment.frozenPath,
634
- oldRunDirectory,
635
- target.runDirectory,
636
- ) as string;
637
- }
633
+ // #1165: caller --attach / --request-manifest paths stay as given; do not rewrite.
638
634
  (admitted as { principal: DurablePrincipal }).principal = principal;
639
635
 
640
636
  // current.json projects host.original from this run directory. Ownership is
@@ -842,7 +838,8 @@ function assignPublicSeatOption(
842
838
  const value = taken.value;
843
839
  switch (taken.def.id) {
844
840
  case "attach":
845
- fields.attachmentPaths.push(requireOptionPath(taken.def.canonical, value));
841
+ // #1165: file-flag values are opaque; only a missing argument is a usage error.
842
+ fields.attachmentPaths.push(requireProvidedOptionValue(taken.def.canonical, value));
846
843
  return;
847
844
  case "project":
848
845
  fields.project = requireOptionPath(taken.def.canonical, value);
@@ -888,7 +885,8 @@ function assignPublicSeatOption(
888
885
  fields.repo = parseRepoOption(value);
889
886
  return;
890
887
  case "request-manifest":
891
- fields.requestManifestPath = requireOptionPath(taken.def.canonical, value);
888
+ // #1165: file-flag values are opaque; only a missing argument is a usage error.
889
+ fields.requestManifestPath = requireProvidedOptionValue(taken.def.canonical, value);
892
890
  return;
893
891
  case "issue": {
894
892
  if (value === undefined || value.trim() === "") {
@@ -1072,6 +1070,20 @@ function requireOptionPath(
1072
1070
  return value;
1073
1071
  }
1074
1072
 
1073
+ /**
1074
+ * #1165 file flags (--attach / --request-manifest): keep the provided string
1075
+ * opaque (including spaces and ""). Only a missing argument is a usage error.
1076
+ */
1077
+ function requireProvidedOptionValue(
1078
+ flag: string,
1079
+ value: string | undefined,
1080
+ ): string {
1081
+ if (value === undefined) {
1082
+ throw new CliUsageError(`${flag} requires a path`);
1083
+ }
1084
+ return value;
1085
+ }
1086
+
1075
1087
  /**
1076
1088
  * Shared Skill-arg token rule for caller-controlled values projected into the
1077
1089
  * space-joined Skill invocation line. Rejects blank, whitespace (smuggles the
@@ -1166,184 +1178,6 @@ export function requireAuthorityRef(value: string | undefined): string {
1166
1178
  });
1167
1179
  }
1168
1180
 
1169
- async function readRegularFileAttachment(
1170
- sourcePath: string,
1171
- ): Promise<{ absolute: string; bytes: Buffer }> {
1172
- const absolute = isAbsolute(sourcePath) ? sourcePath : resolve(sourcePath);
1173
- let st;
1174
- try {
1175
- st = await lstat(absolute);
1176
- } catch (error) {
1177
- throw new CliUsageError(
1178
- `attachment is not a readable regular file: ${sourcePath}`,
1179
- { cause: error },
1180
- );
1181
- }
1182
- if (!st.isFile() || st.isSymbolicLink()) {
1183
- throw new CliUsageError(
1184
- `attachment must be a regular file (not a directory or symlink): ${sourcePath}`,
1185
- );
1186
- }
1187
- try {
1188
- return { absolute, bytes: await readFile(absolute) };
1189
- } catch (error) {
1190
- throw new CliUsageError(
1191
- `attachment is not a readable regular file: ${sourcePath}`,
1192
- { cause: error },
1193
- );
1194
- }
1195
- }
1196
-
1197
- export type PreparedAttachment = {
1198
- readonly absolute: string;
1199
- readonly snapshotPath: string;
1200
- };
1201
-
1202
- /**
1203
- * Snapshot deferred inputs sequentially before identity side effects. The staging
1204
- * files keep aggregate attachment bytes off heap until their final run is known.
1205
- */
1206
- async function removePreparedAttachmentDirectory(stagingDirectory: string): Promise<void> {
1207
- await rm(stagingDirectory, { recursive: true, force: true });
1208
- }
1209
-
1210
- /** Own the complete deferred-snapshot lifetime without masking either failure. */
1211
- export async function withPreparedAttachments<T>(
1212
- attachmentPaths: readonly string[],
1213
- use: (prepared: readonly PreparedAttachment[]) => Promise<T>,
1214
- ): Promise<T> {
1215
- if (attachmentPaths.length === 0) return await use([]);
1216
- const stagingDirectory = await mkdtemp(join(tmpdir(), "ak-role-attachments-"));
1217
- const prepared: PreparedAttachment[] = [];
1218
- let result: T;
1219
- try {
1220
- for (let index = 0; index < attachmentPaths.length; index += 1) {
1221
- const { absolute, bytes } = await readRegularFileAttachment(attachmentPaths[index]!);
1222
- const snapshotPath = join(stagingDirectory, String(index).padStart(6, "0"));
1223
- await writeFile(snapshotPath, bytes);
1224
- prepared.push({ absolute, snapshotPath });
1225
- }
1226
- result = await use(prepared);
1227
- } catch (primary) {
1228
- try {
1229
- await removePreparedAttachmentDirectory(stagingDirectory);
1230
- } catch (cleanup) {
1231
- throw new AggregateError(
1232
- [primary, cleanup],
1233
- "attachment snapshot operation failed and cleanup also failed",
1234
- { cause: primary },
1235
- );
1236
- }
1237
- throw primary;
1238
- }
1239
- await removePreparedAttachmentDirectory(stagingDirectory);
1240
- return result;
1241
- }
1242
-
1243
- async function freezeAttachmentBytes(
1244
- provenancePath: string,
1245
- bytes: Buffer,
1246
- destinationDir: string,
1247
- index: number,
1248
- ): Promise<FrozenAttachment> {
1249
- const frozenPath = join(
1250
- destinationDir,
1251
- `${String(index).padStart(2, "0")}-${basename(provenancePath)}`,
1252
- );
1253
- await writeFile(frozenPath, bytes);
1254
- return {
1255
- provenancePath,
1256
- frozenPath,
1257
- byteLength: bytes.byteLength,
1258
- sha256: sha256Hex(bytes),
1259
- mediaKind: "regular-file",
1260
- };
1261
- }
1262
-
1263
- async function freezePreparedAttachment(
1264
- prepared: PreparedAttachment,
1265
- destinationDir: string,
1266
- index: number,
1267
- ): Promise<FrozenAttachment> {
1268
- return freezeAttachmentBytes(
1269
- prepared.absolute,
1270
- await readFile(prepared.snapshotPath),
1271
- destinationDir,
1272
- index,
1273
- );
1274
- }
1275
-
1276
- async function freezeRegularFileAttachment(
1277
- sourcePath: string,
1278
- destinationDir: string,
1279
- index: number,
1280
- ): Promise<{ attachment: FrozenAttachment; body: Buffer }> {
1281
- const { absolute, bytes } = await readRegularFileAttachment(sourcePath);
1282
- return {
1283
- attachment: await freezeAttachmentBytes(absolute, bytes, destinationDir, index),
1284
- body: bytes,
1285
- };
1286
- }
1287
-
1288
- /** Freeze attachments only — ticket binding is the shared LLM seat path (#635). */
1289
- async function freezeAttachments(
1290
- ledgerHome: string,
1291
- attachmentPaths: readonly string[],
1292
- attachmentsDirectory: string,
1293
- ): Promise<readonly FrozenAttachment[]> {
1294
- const attachments: FrozenAttachment[] = [];
1295
- // attachments/ appears only when something is frozen; created through the
1296
- // ledger's symlink-rejecting tree walk, never a plain mkdir.
1297
- if (attachmentPaths.length > 0) {
1298
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1299
- }
1300
- for (let i = 0; i < attachmentPaths.length; i += 1) {
1301
- const frozen = await freezeRegularFileAttachment(
1302
- attachmentPaths[i]!,
1303
- attachmentsDirectory,
1304
- i,
1305
- );
1306
- attachments.push(frozen.attachment);
1307
- }
1308
- return attachments;
1309
- }
1310
-
1311
- /**
1312
- * Freeze summons attachments into an already-retained run (#637 same-ticket resume).
1313
- * Writes under attachments/summons-<key>/ so prior freeze names stay intact.
1314
- * Manual resume never calls this — birth attachments keep their original semantics.
1315
- */
1316
- export async function freezeAttachmentsIntoRun(
1317
- attachmentPaths: readonly string[],
1318
- runDirectory: string,
1319
- summonsKey: string = `s-${Date.now().toString(36)}`,
1320
- ): Promise<readonly FrozenAttachment[]> {
1321
- if (attachmentPaths.length === 0) return [];
1322
- const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(runDirectory));
1323
- const attachmentsDirectory = join(runDirectory, "attachments", summonsKey);
1324
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1325
- return freezeAttachments(ledgerHome, attachmentPaths, attachmentsDirectory);
1326
- }
1327
-
1328
- /** Freeze prepared bytes and metadata through one path for birth or retained runs. */
1329
- export async function freezePreparedAttachmentsIntoRun(
1330
- prepared: readonly PreparedAttachment[],
1331
- runDirectory: string,
1332
- summonsKey?: string,
1333
- ): Promise<readonly FrozenAttachment[]> {
1334
- if (prepared.length === 0) return [];
1335
- const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(runDirectory));
1336
- const attachmentsDirectory = summonsKey === undefined
1337
- ? join(runDirectory, "attachments")
1338
- : join(runDirectory, "attachments", summonsKey);
1339
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1340
- const attachments: FrozenAttachment[] = [];
1341
- for (let index = 0; index < prepared.length; index += 1) {
1342
- attachments.push(await freezePreparedAttachment(prepared[index]!, attachmentsDirectory, index));
1343
- }
1344
- return attachments;
1345
- }
1346
-
1347
1181
  function ticketAdmissionFields(
1348
1182
  ticketNumber: number | undefined,
1349
1183
  ): { ticketNumber?: number } {
@@ -1573,38 +1407,21 @@ export async function admitPublicRole(
1573
1407
  } else {
1574
1408
  repository = resolveGitHubRemoteRepository(projectRoot);
1575
1409
  }
1576
- let manifest = emptyCollectorManifest();
1577
- let manifestCanonicalJson: string | undefined;
1578
- if (parsed.requestManifestPath !== undefined) {
1579
- try {
1580
- manifest = await loadCollectorManifest(parsed.requestManifestPath);
1581
- manifestCanonicalJson = manifest.canonicalJson;
1582
- } catch (error) {
1583
- throw new CliUsageError(
1584
- errorText(error),
1585
- { cause: error },
1586
- );
1587
- }
1588
- }
1589
- const manifestDigest = manifest.digest;
1410
+ // #1165: pass caller path as-is; package does not read, validate, or rewrite.
1590
1411
  const admittedCollector = await admitStandardMaterialInvocation("collector", {
1591
1412
  ...shared,
1592
1413
  instruction,
1593
1414
  attachmentPaths,
1594
1415
  ...project,
1595
- placedFields: async (placed) => {
1416
+ placedFields: async () => {
1596
1417
  // #1088: explicit --pr only. Unbound PR is located by the LLM via host CLI.
1597
- let requestManifestPath: string | undefined;
1598
- if (manifestCanonicalJson !== undefined) {
1599
- requestManifestPath = join(placed.runDirectory, "request-manifest.json");
1600
- await writeFile(requestManifestPath, manifestCanonicalJson, "utf8");
1601
- }
1602
1418
  return {
1603
1419
  ...(explicitPrNumber === undefined ? {} : { prNumber: explicitPrNumber }),
1604
1420
  repository: repository.canonical,
1605
1421
  repositoryDisplay: repository.display,
1606
- ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
1607
- manifestDigest,
1422
+ ...(parsed.requestManifestPath === undefined
1423
+ ? {}
1424
+ : { requestManifestPath: parsed.requestManifestPath }),
1608
1425
  };
1609
1426
  },
1610
1427
  });
@@ -1617,12 +1434,10 @@ export async function admitPublicRole(
1617
1434
  );
1618
1435
  }
1619
1436
  const issueNumber = parsed.issueNumber;
1620
- let frozenAttachments: readonly FrozenAttachment[] = [];
1621
- const admittedDoctor = await admitStandardMaterialInvocation("doctor", {
1437
+ return admitStandardMaterialInvocation("doctor", {
1622
1438
  ...shared,
1623
1439
  instruction,
1624
- attachmentPaths: [],
1625
- freezeAttachments: false,
1440
+ attachmentPaths,
1626
1441
  ...project,
1627
1442
  placedFields: async (placed) => {
1628
1443
  let caseRunsPath: string;
@@ -1663,16 +1478,13 @@ export async function admitPublicRole(
1663
1478
  { cause: error },
1664
1479
  );
1665
1480
  }
1666
- frozenAttachments = await freezeAttachments(placed.ledgerHome, attachmentPaths, placed.attachmentsDirectory);
1667
1481
  return {
1668
1482
  issueNumber,
1669
1483
  caseRunsPath,
1670
1484
  caseIdentity,
1671
- attachments: persistedAttachmentRefs(frozenAttachments),
1672
1485
  };
1673
1486
  },
1674
1487
  });
1675
- return { ...admittedDoctor, attachments: frozenAttachments };
1676
1488
  }
1677
1489
  case "source-locator": {
1678
1490
  const projectRoot = resolve(parsed.project ?? shared.cwd);
@@ -1702,7 +1514,7 @@ export async function admitPublicRole(
1702
1514
  instruction: "",
1703
1515
  attachmentPaths: [],
1704
1516
  ...project,
1705
- freezeAttachments: false,
1517
+ recordAttachments: false,
1706
1518
  admittedFields: {
1707
1519
  sourceRunPath: sourceRun.runDirectory,
1708
1520
  sourceRun,
@@ -1719,7 +1531,7 @@ export async function admitPublicRole(
1719
1531
  instruction,
1720
1532
  attachmentPaths: [],
1721
1533
  ...project,
1722
- freezeAttachments: false,
1534
+ recordAttachments: false,
1723
1535
  admittedFields: { baseRevision },
1724
1536
  });
1725
1537
  }
@@ -1868,8 +1680,8 @@ async function placeRoleAdmission(options: {
1868
1680
  readonly runId?: string;
1869
1681
  readonly assertedTicketNumber?: number;
1870
1682
  readonly attachmentPaths: readonly string[];
1871
- /** Gleaner-left admits no caller attachments. */
1872
- readonly freezeAttachments?: boolean;
1683
+ /** When false, leave attachments empty (deferred countersign / seats without attach). */
1684
+ readonly recordAttachments?: boolean;
1873
1685
  /** Countersign reserves coordinates, then materializes after identity lookup. */
1874
1686
  readonly materialize?: boolean;
1875
1687
  }): Promise<PlacedRoleAdmission> {
@@ -1892,9 +1704,9 @@ async function placeRoleAdmission(options: {
1892
1704
  home: options.home,
1893
1705
  ...(options.materialize === undefined ? {} : { materialize: options.materialize }),
1894
1706
  });
1895
- const attachments = options.freezeAttachments === false
1707
+ const attachments = options.recordAttachments === false
1896
1708
  ? []
1897
- : await freezeAttachments(ledgerHome, options.attachmentPaths, attachmentsDirectory);
1709
+ : admitCallerAttachments(options.attachmentPaths);
1898
1710
  return {
1899
1711
  runId,
1900
1712
  bookKey,
@@ -1912,20 +1724,8 @@ async function placeRoleAdmission(options: {
1912
1724
 
1913
1725
  function persistedAttachmentRefs(
1914
1726
  attachments: readonly FrozenAttachment[],
1915
- ): ReadonlyArray<{
1916
- provenancePath: string;
1917
- frozenPath: string;
1918
- byteLength: number;
1919
- sha256: string;
1920
- mediaKind: FrozenAttachment["mediaKind"];
1921
- }> {
1922
- return attachments.map((attachment) => ({
1923
- provenancePath: attachment.provenancePath,
1924
- frozenPath: attachment.frozenPath,
1925
- byteLength: attachment.byteLength,
1926
- sha256: attachment.sha256,
1927
- mediaKind: attachment.mediaKind,
1928
- }));
1727
+ ): ReadonlyArray<{ path: string }> {
1728
+ return attachments.map((attachment) => ({ path: attachment.path }));
1929
1729
  }
1930
1730
 
1931
1731
  async function persistPlacedAdmission(
@@ -1967,8 +1767,7 @@ async function persistPlacedAdmission(
1967
1767
  * before materializeCountersignInvocation writes the page.
1968
1768
  * Seats whose extra facts are known before placement pass them as admittedFields.
1969
1769
  * Seats whose extra facts depend on the placed run pass placedFields
1970
- * (coder writes task.md; fixer writes fix-packet.md; collector writes request-manifest.json;
1971
- * doctor resolves the case, then freezes attachments; merger writes merger-input.json).
1770
+ * (coder writes task.md; fixer writes fix-packet.md; merger writes merger-input.json).
1972
1771
  */
1973
1772
  async function admitStandardMaterialInvocation<
1974
1773
  R extends PackagedRole,
@@ -1976,10 +1775,10 @@ async function admitStandardMaterialInvocation<
1976
1775
  >(
1977
1776
  role: R,
1978
1777
  options: AdmitInspectorInvocationOptions & {
1979
- /** Reserve coordinates; skip freeze, placement disk, and admitted section write. */
1778
+ /** Reserve coordinates; skip placement disk and admitted section write. */
1980
1779
  readonly deferPersistence?: boolean;
1981
- /** Skip attachment freeze. Gleaner-left admits no caller attachments. */
1982
- readonly freezeAttachments?: boolean;
1780
+ /** Leave attachments empty (gleaner-left / notary / deferred countersign). */
1781
+ readonly recordAttachments?: boolean;
1983
1782
  /** Seat facts already validated by admitPublicRole. */
1984
1783
  readonly admittedFields?: Extra;
1985
1784
  /** Seat facts that exist only after placement. Written before the admitted page. */
@@ -1987,14 +1786,14 @@ async function admitStandardMaterialInvocation<
1987
1786
  },
1988
1787
  ): Promise<AdmittedRoleInvocationBase & { readonly role: R } & Extra> {
1989
1788
  const defer = options.deferPersistence === true;
1990
- const skipFreeze = defer || options.freezeAttachments === false;
1789
+ const skipAttachments = defer || options.recordAttachments === false;
1991
1790
  const placed = await placeRoleAdmission({
1992
1791
  role,
1993
1792
  home: options.home,
1994
1793
  principalAuthority: options.principalAuthority,
1995
1794
  cwd: options.cwd,
1996
1795
  attachmentPaths: options.attachmentPaths,
1997
- ...(skipFreeze ? { freezeAttachments: false } : {}),
1796
+ ...(skipAttachments ? { recordAttachments: false } : {}),
1998
1797
  ...(defer ? { materialize: false } : {}),
1999
1798
  ...(options.project === undefined ? {} : { project: options.project }),
2000
1799
  ...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
@@ -2007,7 +1806,8 @@ async function admitStandardMaterialInvocation<
2007
1806
  ? {}
2008
1807
  : { correlationId: options.correlationId };
2009
1808
  const instruction = options.instruction;
2010
- const instructionEmpty = instruction.trim() === "";
1809
+ // Admitted emptiness is literal absence, not whitespace-only (#1165 J3).
1810
+ const instructionEmpty = instruction.length === 0;
2011
1811
  const placedExtra = options.placedFields === undefined ? undefined : await options.placedFields(placed);
2012
1812
  const admittedFields = {
2013
1813
  ...(options.admittedFields ?? ({} as Extra)),
@@ -2050,7 +1850,8 @@ type InstructionTransportSource = {
2050
1850
  readonly role?: string;
2051
1851
  readonly instruction: string;
2052
1852
  readonly instructionEmpty: boolean;
2053
- readonly attachments: readonly { readonly frozenPath: string }[];
1853
+ readonly attachments: readonly { readonly path: string }[];
1854
+ readonly requestManifestPath?: string;
2054
1855
  readonly baseRevision?: string;
2055
1856
  readonly lens?: ReviewerLens;
2056
1857
  readonly authorityRefs?: readonly string[];
@@ -2069,7 +1870,7 @@ function admittedTransportPromptKind(
2069
1870
  /**
2070
1871
  * One initial prompt transport. The registry `transportPrompt` leaf selects
2071
1872
  * a fixed kickoff, a bound baseline, or frozen skill args. Absent means the
2072
- * caller instruction plus frozen attachment paths.
1873
+ * caller instruction plus caller attachment paths as-is (#1165).
2073
1874
  * Engine / outsourcing material rides startup readingMaterial (#1167), not here.
2074
1875
  */
2075
1876
  export function buildInstructionTransportPrompt(
@@ -2087,7 +1888,9 @@ export function buildInstructionTransportPrompt(
2087
1888
  throw new Error("baseline transport prompt is missing the bound revision");
2088
1889
  }
2089
1890
  const lines = [admitted.baseRevision];
2090
- if (!admitted.instructionEmpty) {
1891
+ // Use raw admitted.instruction — do not gate on instructionEmpty metadata
1892
+ // (old pages may set that flag wrong while still holding whitespace text).
1893
+ if (admitted.instruction.length > 0) {
2091
1894
  lines.push("", admitted.instruction);
2092
1895
  }
2093
1896
  return lines.join("\n");
@@ -2105,17 +1908,25 @@ export function buildInstructionTransportPrompt(
2105
1908
  lens: admitted.lens,
2106
1909
  authorityRefs: admitted.authorityRefs,
2107
1910
  })];
2108
- if (!admitted.instructionEmpty && admitted.instruction.trim() !== "") {
1911
+ if (admitted.instruction.length > 0) {
2109
1912
  lines.push("", admitted.instruction);
2110
1913
  }
2111
1914
  return lines.join("\n");
2112
1915
  }
2113
- const lines: string[] = [admitted.instructionEmpty ? "" : admitted.instruction];
2114
- if (admitted.attachments.length > 0) {
1916
+ // #1165 J3: assemble from the admitted instruction bytes themselves.
1917
+ const lines: string[] = [admitted.instruction];
1918
+ // #1164/#1165: each caller path keeps its file-flag provenance.
1919
+ const flaggedPaths: string[] = [
1920
+ ...admitted.attachments.map((attachment) => `--attach ${attachment.path}`),
1921
+ ...(admitted.requestManifestPath === undefined
1922
+ ? []
1923
+ : [`--request-manifest ${admitted.requestManifestPath}`]),
1924
+ ];
1925
+ if (flaggedPaths.length > 0) {
2115
1926
  lines.push("");
2116
- lines.push("已受理附件(冻结快照路径):");
2117
- for (const attachment of admitted.attachments) {
2118
- lines.push(`- ${attachment.frozenPath}`);
1927
+ lines.push("已受理附件:");
1928
+ for (const entry of flaggedPaths) {
1929
+ lines.push(`- ${entry}`);
2119
1930
  }
2120
1931
  }
2121
1932
  return lines.join("\n");
@@ -2143,7 +1954,7 @@ export type AdmitCountersignInvocationOptions = {
2143
1954
  export async function materializeCountersignInvocation(
2144
1955
  admitted: AdmittedCountersignInvocation,
2145
1956
  options: Pick<AdmitCountersignInvocationOptions, "home" | "principalAuthority" | "model"> & {
2146
- preparedAttachments: readonly PreparedAttachment[];
1957
+ attachmentPaths: readonly string[];
2147
1958
  /** Typed ticket known before the deferred run is written. */
2148
1959
  ticketNumber?: number;
2149
1960
  },
@@ -2153,8 +1964,7 @@ export async function materializeCountersignInvocation(
2153
1964
  home: options.home,
2154
1965
  principalAuthority: options.principalAuthority,
2155
1966
  cwd: admitted.projectRoot,
2156
- attachmentPaths: [],
2157
- freezeAttachments: false,
1967
+ attachmentPaths: options.attachmentPaths,
2158
1968
  runId: admitted.runId,
2159
1969
  ...(options.ticketNumber === undefined ? {} : { assertedTicketNumber: options.ticketNumber }),
2160
1970
  });
@@ -2165,11 +1975,7 @@ export async function materializeCountersignInvocation(
2165
1975
  if (ticketFields.ticketNumber !== undefined) {
2166
1976
  (admitted as { ticketNumber?: number }).ticketNumber = ticketFields.ticketNumber;
2167
1977
  }
2168
- const attachments = await freezePreparedAttachmentsIntoRun(
2169
- options.preparedAttachments,
2170
- admitted.runDirectory,
2171
- );
2172
- (admitted as { attachments: readonly FrozenAttachment[] }).attachments = attachments;
1978
+ (admitted as { attachments: readonly FrozenAttachment[] }).attachments = placement.attachments;
2173
1979
  writeAdmittedRequestPersistence(admitted.runDirectory, admitted, {
2174
1980
  sessionDirectory: placement.sessionDirectory,
2175
1981
  sessionFile: placement.sessionFile,
@@ -2196,10 +2002,13 @@ export async function loadAdmittedJudgeRequest(
2196
2002
  if (typeof record.instruction !== "string") return undefined;
2197
2003
  if (typeof record.instructionEmpty !== "boolean") return undefined;
2198
2004
  if (!Array.isArray(record.attachments)) return undefined;
2005
+ const attachments = record.attachments
2006
+ .map((item) => normalizeAdmittedAttachment(item))
2007
+ .filter((item): item is FrozenAttachment => item !== undefined);
2199
2008
  return {
2200
2009
  instruction: record.instruction,
2201
2010
  instructionEmpty: record.instructionEmpty,
2202
- attachments: record.attachments as FrozenAttachment[],
2011
+ attachments,
2203
2012
  };
2204
2013
  } catch {
2205
2014
  return undefined;
@@ -2596,7 +2405,7 @@ export function parseAnalystArgv(args: readonly string[]): ParseAnalystArgvResul
2596
2405
  if (taken.def.id === "attach") {
2597
2406
  pushValue(
2598
2407
  "attach",
2599
- requireOptionPath(taken.def.canonical, taken.value),
2408
+ requireProvidedOptionValue(taken.def.canonical, taken.value),
2600
2409
  );
2601
2410
  return;
2602
2411
  }
@@ -344,7 +344,7 @@ const SHARED_PROJECT_SEMANTICS = {
344
344
  } as const satisfies Omit<PublicOptionDefinition, "owner">;
345
345
 
346
346
  /**
347
- * Immutable shared semantics for the common frozen-file `--attach` face.
347
+ * Immutable shared semantics for the common `--attach` face (#1165).
348
348
  * Role rows bind owner only. Reviewer has no attach face; analyst sweep attach
349
349
  * keeps its own modes/selectsMode/description and must not use this binding.
350
350
  */
@@ -357,8 +357,8 @@ const SHARED_ATTACH_SEMANTICS = {
357
357
  repeatable: true,
358
358
  form: "option" as const,
359
359
  description: {
360
- en: "Attach a regular file; frozen at admission (repeatable).",
361
- zh: "附加普通文件;受理即冻结(可重复)。",
360
+ en: "Caller file path passed as-is to the role (repeatable). Package does not read, copy, or validate; use an absolute path when cwd differs from --project.",
361
+ zh: "调用方文件路径原样递给衙门(可重复)。包不读、不复制、不校验;调用方当前目录与 --project 不同时请给绝对路径。",
362
362
  },
363
363
  } as const satisfies Omit<PublicOptionDefinition, "owner">;
364
364
 
@@ -556,8 +556,8 @@ const COLLECTOR_OPTIONS = [
556
556
  repeatable: false,
557
557
  form: "option",
558
558
  description: {
559
- en: "Optional request manifest JSON path ({requests:[{id,body}]}). Parsed for semantics and passed to the Collector as materials — not a machine collection directive.",
560
- zh: "可选 request manifest JSON 路径({requests:[{id,body}]})。按语义解析后作为材料递给通进司,不是机器代收 finding 的指令。",
559
+ en: "Caller path to a request manifest JSON file, passed as-is. Package does not read, copy, or validate; use an absolute path when cwd differs from --project.",
560
+ zh: "调用方 request manifest JSON 路径,原样传递。包不读、不复制、不校验;调用方当前目录与 --project 不同时请给绝对路径。",
561
561
  },
562
562
  },
563
563
  ] as const satisfies readonly PublicOptionDefinition[];