@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,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,
@@ -94,10 +87,6 @@ import {
94
87
  NotarySourceRunError,
95
88
  resolveNotarySourceRunLocator,
96
89
  } from "../notary-source-run.ts";
97
- import {
98
- appendEngineSessionMaterial,
99
- type EngineSessionMaterial,
100
- } from "../package-resources/engine-material.ts";
101
90
  import { CliUsageError } from "./cli-errors.ts";
102
91
  import {
103
92
  REJECTED_PUBLIC_SPELLINGS,
@@ -114,16 +103,27 @@ import type { PublicThinkingLevel } from "./registry.ts";
114
103
 
115
104
  import { errorText, isRecord } from "../unknown-value.ts";
116
105
 
106
+ /** Caller-supplied attachment path recorded as-is (#1165). */
117
107
  export type FrozenAttachment = {
118
- /** Original caller path retained only as provenance. */
119
- readonly provenancePath: string;
120
- /** Absolute path of the admitted frozen snapshot bytes. */
121
- readonly frozenPath: string;
122
- readonly byteLength: number;
123
- readonly sha256: string;
124
- readonly mediaKind: "regular-file";
108
+ readonly path: string;
125
109
  };
126
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
+
127
127
  /** Shared admitted Role run identity (#106 common Invocation + #109 Coder). */
128
128
  export type AdmittedRoleInvocationBase = {
129
129
  readonly runId: string;
@@ -131,7 +131,7 @@ export type AdmittedRoleInvocationBase = {
131
131
  readonly projectRoot: string;
132
132
  /** Opaque instruction bytes as submitted. */
133
133
  readonly instruction: string;
134
- /** True when the caller supplied no nonblank instruction. */
134
+ /** True when the caller supplied a literally empty instruction (length 0). */
135
135
  readonly instructionEmpty: boolean;
136
136
  readonly attachments: readonly FrozenAttachment[];
137
137
  readonly runDirectory: string;
@@ -226,7 +226,6 @@ export type AdmittedCollectorInvocation = AdmittedRoleInvocationBase & {
226
226
  readonly prNumber?: number;
227
227
  readonly repository: CollectorRepository;
228
228
  readonly requestManifestPath?: string;
229
- readonly manifestDigest: string;
230
229
  };
231
230
 
232
231
  export type AdmittedDoctorInvocation = AdmittedRoleInvocationBase & {
@@ -626,19 +625,12 @@ export async function relocateAdmittedRunToTicket(
626
625
  "taskPath",
627
626
  "packetPath",
628
627
  "prerequisitesPath",
629
- "requestManifestPath",
630
628
  "mergerInputPath",
631
629
  ],
632
630
  oldRunDirectory,
633
631
  target.runDirectory,
634
632
  );
635
- for (const attachment of admitted.attachments) {
636
- (attachment as { frozenPath: string }).frozenPath = rewriteRunDirectoryPathValue(
637
- attachment.frozenPath,
638
- oldRunDirectory,
639
- target.runDirectory,
640
- ) as string;
641
- }
633
+ // #1165: caller --attach / --request-manifest paths stay as given; do not rewrite.
642
634
  (admitted as { principal: DurablePrincipal }).principal = principal;
643
635
 
644
636
  // current.json projects host.original from this run directory. Ownership is
@@ -846,7 +838,8 @@ function assignPublicSeatOption(
846
838
  const value = taken.value;
847
839
  switch (taken.def.id) {
848
840
  case "attach":
849
- 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));
850
843
  return;
851
844
  case "project":
852
845
  fields.project = requireOptionPath(taken.def.canonical, value);
@@ -892,7 +885,8 @@ function assignPublicSeatOption(
892
885
  fields.repo = parseRepoOption(value);
893
886
  return;
894
887
  case "request-manifest":
895
- 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);
896
890
  return;
897
891
  case "issue": {
898
892
  if (value === undefined || value.trim() === "") {
@@ -1076,6 +1070,20 @@ function requireOptionPath(
1076
1070
  return value;
1077
1071
  }
1078
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
+
1079
1087
  /**
1080
1088
  * Shared Skill-arg token rule for caller-controlled values projected into the
1081
1089
  * space-joined Skill invocation line. Rejects blank, whitespace (smuggles the
@@ -1170,184 +1178,6 @@ export function requireAuthorityRef(value: string | undefined): string {
1170
1178
  });
1171
1179
  }
1172
1180
 
1173
- async function readRegularFileAttachment(
1174
- sourcePath: string,
1175
- ): Promise<{ absolute: string; bytes: Buffer }> {
1176
- const absolute = isAbsolute(sourcePath) ? sourcePath : resolve(sourcePath);
1177
- let st;
1178
- try {
1179
- st = await lstat(absolute);
1180
- } catch (error) {
1181
- throw new CliUsageError(
1182
- `attachment is not a readable regular file: ${sourcePath}`,
1183
- { cause: error },
1184
- );
1185
- }
1186
- if (!st.isFile() || st.isSymbolicLink()) {
1187
- throw new CliUsageError(
1188
- `attachment must be a regular file (not a directory or symlink): ${sourcePath}`,
1189
- );
1190
- }
1191
- try {
1192
- return { absolute, bytes: await readFile(absolute) };
1193
- } catch (error) {
1194
- throw new CliUsageError(
1195
- `attachment is not a readable regular file: ${sourcePath}`,
1196
- { cause: error },
1197
- );
1198
- }
1199
- }
1200
-
1201
- export type PreparedAttachment = {
1202
- readonly absolute: string;
1203
- readonly snapshotPath: string;
1204
- };
1205
-
1206
- /**
1207
- * Snapshot deferred inputs sequentially before identity side effects. The staging
1208
- * files keep aggregate attachment bytes off heap until their final run is known.
1209
- */
1210
- async function removePreparedAttachmentDirectory(stagingDirectory: string): Promise<void> {
1211
- await rm(stagingDirectory, { recursive: true, force: true });
1212
- }
1213
-
1214
- /** Own the complete deferred-snapshot lifetime without masking either failure. */
1215
- export async function withPreparedAttachments<T>(
1216
- attachmentPaths: readonly string[],
1217
- use: (prepared: readonly PreparedAttachment[]) => Promise<T>,
1218
- ): Promise<T> {
1219
- if (attachmentPaths.length === 0) return await use([]);
1220
- const stagingDirectory = await mkdtemp(join(tmpdir(), "ak-role-attachments-"));
1221
- const prepared: PreparedAttachment[] = [];
1222
- let result: T;
1223
- try {
1224
- for (let index = 0; index < attachmentPaths.length; index += 1) {
1225
- const { absolute, bytes } = await readRegularFileAttachment(attachmentPaths[index]!);
1226
- const snapshotPath = join(stagingDirectory, String(index).padStart(6, "0"));
1227
- await writeFile(snapshotPath, bytes);
1228
- prepared.push({ absolute, snapshotPath });
1229
- }
1230
- result = await use(prepared);
1231
- } catch (primary) {
1232
- try {
1233
- await removePreparedAttachmentDirectory(stagingDirectory);
1234
- } catch (cleanup) {
1235
- throw new AggregateError(
1236
- [primary, cleanup],
1237
- "attachment snapshot operation failed and cleanup also failed",
1238
- { cause: primary },
1239
- );
1240
- }
1241
- throw primary;
1242
- }
1243
- await removePreparedAttachmentDirectory(stagingDirectory);
1244
- return result;
1245
- }
1246
-
1247
- async function freezeAttachmentBytes(
1248
- provenancePath: string,
1249
- bytes: Buffer,
1250
- destinationDir: string,
1251
- index: number,
1252
- ): Promise<FrozenAttachment> {
1253
- const frozenPath = join(
1254
- destinationDir,
1255
- `${String(index).padStart(2, "0")}-${basename(provenancePath)}`,
1256
- );
1257
- await writeFile(frozenPath, bytes);
1258
- return {
1259
- provenancePath,
1260
- frozenPath,
1261
- byteLength: bytes.byteLength,
1262
- sha256: sha256Hex(bytes),
1263
- mediaKind: "regular-file",
1264
- };
1265
- }
1266
-
1267
- async function freezePreparedAttachment(
1268
- prepared: PreparedAttachment,
1269
- destinationDir: string,
1270
- index: number,
1271
- ): Promise<FrozenAttachment> {
1272
- return freezeAttachmentBytes(
1273
- prepared.absolute,
1274
- await readFile(prepared.snapshotPath),
1275
- destinationDir,
1276
- index,
1277
- );
1278
- }
1279
-
1280
- async function freezeRegularFileAttachment(
1281
- sourcePath: string,
1282
- destinationDir: string,
1283
- index: number,
1284
- ): Promise<{ attachment: FrozenAttachment; body: Buffer }> {
1285
- const { absolute, bytes } = await readRegularFileAttachment(sourcePath);
1286
- return {
1287
- attachment: await freezeAttachmentBytes(absolute, bytes, destinationDir, index),
1288
- body: bytes,
1289
- };
1290
- }
1291
-
1292
- /** Freeze attachments only — ticket binding is the shared LLM seat path (#635). */
1293
- async function freezeAttachments(
1294
- ledgerHome: string,
1295
- attachmentPaths: readonly string[],
1296
- attachmentsDirectory: string,
1297
- ): Promise<readonly FrozenAttachment[]> {
1298
- const attachments: FrozenAttachment[] = [];
1299
- // attachments/ appears only when something is frozen; created through the
1300
- // ledger's symlink-rejecting tree walk, never a plain mkdir.
1301
- if (attachmentPaths.length > 0) {
1302
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1303
- }
1304
- for (let i = 0; i < attachmentPaths.length; i += 1) {
1305
- const frozen = await freezeRegularFileAttachment(
1306
- attachmentPaths[i]!,
1307
- attachmentsDirectory,
1308
- i,
1309
- );
1310
- attachments.push(frozen.attachment);
1311
- }
1312
- return attachments;
1313
- }
1314
-
1315
- /**
1316
- * Freeze summons attachments into an already-retained run (#637 same-ticket resume).
1317
- * Writes under attachments/summons-<key>/ so prior freeze names stay intact.
1318
- * Manual resume never calls this — birth attachments keep their original semantics.
1319
- */
1320
- export async function freezeAttachmentsIntoRun(
1321
- attachmentPaths: readonly string[],
1322
- runDirectory: string,
1323
- summonsKey: string = `s-${Date.now().toString(36)}`,
1324
- ): Promise<readonly FrozenAttachment[]> {
1325
- if (attachmentPaths.length === 0) return [];
1326
- const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(runDirectory));
1327
- const attachmentsDirectory = join(runDirectory, "attachments", summonsKey);
1328
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1329
- return freezeAttachments(ledgerHome, attachmentPaths, attachmentsDirectory);
1330
- }
1331
-
1332
- /** Freeze prepared bytes and metadata through one path for birth or retained runs. */
1333
- export async function freezePreparedAttachmentsIntoRun(
1334
- prepared: readonly PreparedAttachment[],
1335
- runDirectory: string,
1336
- summonsKey?: string,
1337
- ): Promise<readonly FrozenAttachment[]> {
1338
- if (prepared.length === 0) return [];
1339
- const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(runDirectory));
1340
- const attachmentsDirectory = summonsKey === undefined
1341
- ? join(runDirectory, "attachments")
1342
- : join(runDirectory, "attachments", summonsKey);
1343
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
1344
- const attachments: FrozenAttachment[] = [];
1345
- for (let index = 0; index < prepared.length; index += 1) {
1346
- attachments.push(await freezePreparedAttachment(prepared[index]!, attachmentsDirectory, index));
1347
- }
1348
- return attachments;
1349
- }
1350
-
1351
1181
  function ticketAdmissionFields(
1352
1182
  ticketNumber: number | undefined,
1353
1183
  ): { ticketNumber?: number } {
@@ -1577,38 +1407,21 @@ export async function admitPublicRole(
1577
1407
  } else {
1578
1408
  repository = resolveGitHubRemoteRepository(projectRoot);
1579
1409
  }
1580
- let manifest = emptyCollectorManifest();
1581
- let manifestCanonicalJson: string | undefined;
1582
- if (parsed.requestManifestPath !== undefined) {
1583
- try {
1584
- manifest = await loadCollectorManifest(parsed.requestManifestPath);
1585
- manifestCanonicalJson = manifest.canonicalJson;
1586
- } catch (error) {
1587
- throw new CliUsageError(
1588
- errorText(error),
1589
- { cause: error },
1590
- );
1591
- }
1592
- }
1593
- const manifestDigest = manifest.digest;
1410
+ // #1165: pass caller path as-is; package does not read, validate, or rewrite.
1594
1411
  const admittedCollector = await admitStandardMaterialInvocation("collector", {
1595
1412
  ...shared,
1596
1413
  instruction,
1597
1414
  attachmentPaths,
1598
1415
  ...project,
1599
- placedFields: async (placed) => {
1416
+ placedFields: async () => {
1600
1417
  // #1088: explicit --pr only. Unbound PR is located by the LLM via host CLI.
1601
- let requestManifestPath: string | undefined;
1602
- if (manifestCanonicalJson !== undefined) {
1603
- requestManifestPath = join(placed.runDirectory, "request-manifest.json");
1604
- await writeFile(requestManifestPath, manifestCanonicalJson, "utf8");
1605
- }
1606
1418
  return {
1607
1419
  ...(explicitPrNumber === undefined ? {} : { prNumber: explicitPrNumber }),
1608
1420
  repository: repository.canonical,
1609
1421
  repositoryDisplay: repository.display,
1610
- ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
1611
- manifestDigest,
1422
+ ...(parsed.requestManifestPath === undefined
1423
+ ? {}
1424
+ : { requestManifestPath: parsed.requestManifestPath }),
1612
1425
  };
1613
1426
  },
1614
1427
  });
@@ -1621,12 +1434,10 @@ export async function admitPublicRole(
1621
1434
  );
1622
1435
  }
1623
1436
  const issueNumber = parsed.issueNumber;
1624
- let frozenAttachments: readonly FrozenAttachment[] = [];
1625
- const admittedDoctor = await admitStandardMaterialInvocation("doctor", {
1437
+ return admitStandardMaterialInvocation("doctor", {
1626
1438
  ...shared,
1627
1439
  instruction,
1628
- attachmentPaths: [],
1629
- freezeAttachments: false,
1440
+ attachmentPaths,
1630
1441
  ...project,
1631
1442
  placedFields: async (placed) => {
1632
1443
  let caseRunsPath: string;
@@ -1667,16 +1478,13 @@ export async function admitPublicRole(
1667
1478
  { cause: error },
1668
1479
  );
1669
1480
  }
1670
- frozenAttachments = await freezeAttachments(placed.ledgerHome, attachmentPaths, placed.attachmentsDirectory);
1671
1481
  return {
1672
1482
  issueNumber,
1673
1483
  caseRunsPath,
1674
1484
  caseIdentity,
1675
- attachments: persistedAttachmentRefs(frozenAttachments),
1676
1485
  };
1677
1486
  },
1678
1487
  });
1679
- return { ...admittedDoctor, attachments: frozenAttachments };
1680
1488
  }
1681
1489
  case "source-locator": {
1682
1490
  const projectRoot = resolve(parsed.project ?? shared.cwd);
@@ -1706,7 +1514,7 @@ export async function admitPublicRole(
1706
1514
  instruction: "",
1707
1515
  attachmentPaths: [],
1708
1516
  ...project,
1709
- freezeAttachments: false,
1517
+ recordAttachments: false,
1710
1518
  admittedFields: {
1711
1519
  sourceRunPath: sourceRun.runDirectory,
1712
1520
  sourceRun,
@@ -1723,7 +1531,7 @@ export async function admitPublicRole(
1723
1531
  instruction,
1724
1532
  attachmentPaths: [],
1725
1533
  ...project,
1726
- freezeAttachments: false,
1534
+ recordAttachments: false,
1727
1535
  admittedFields: { baseRevision },
1728
1536
  });
1729
1537
  }
@@ -1872,8 +1680,8 @@ async function placeRoleAdmission(options: {
1872
1680
  readonly runId?: string;
1873
1681
  readonly assertedTicketNumber?: number;
1874
1682
  readonly attachmentPaths: readonly string[];
1875
- /** Gleaner-left admits no caller attachments. */
1876
- readonly freezeAttachments?: boolean;
1683
+ /** When false, leave attachments empty (deferred countersign / seats without attach). */
1684
+ readonly recordAttachments?: boolean;
1877
1685
  /** Countersign reserves coordinates, then materializes after identity lookup. */
1878
1686
  readonly materialize?: boolean;
1879
1687
  }): Promise<PlacedRoleAdmission> {
@@ -1896,9 +1704,9 @@ async function placeRoleAdmission(options: {
1896
1704
  home: options.home,
1897
1705
  ...(options.materialize === undefined ? {} : { materialize: options.materialize }),
1898
1706
  });
1899
- const attachments = options.freezeAttachments === false
1707
+ const attachments = options.recordAttachments === false
1900
1708
  ? []
1901
- : await freezeAttachments(ledgerHome, options.attachmentPaths, attachmentsDirectory);
1709
+ : admitCallerAttachments(options.attachmentPaths);
1902
1710
  return {
1903
1711
  runId,
1904
1712
  bookKey,
@@ -1916,20 +1724,8 @@ async function placeRoleAdmission(options: {
1916
1724
 
1917
1725
  function persistedAttachmentRefs(
1918
1726
  attachments: readonly FrozenAttachment[],
1919
- ): ReadonlyArray<{
1920
- provenancePath: string;
1921
- frozenPath: string;
1922
- byteLength: number;
1923
- sha256: string;
1924
- mediaKind: FrozenAttachment["mediaKind"];
1925
- }> {
1926
- return attachments.map((attachment) => ({
1927
- provenancePath: attachment.provenancePath,
1928
- frozenPath: attachment.frozenPath,
1929
- byteLength: attachment.byteLength,
1930
- sha256: attachment.sha256,
1931
- mediaKind: attachment.mediaKind,
1932
- }));
1727
+ ): ReadonlyArray<{ path: string }> {
1728
+ return attachments.map((attachment) => ({ path: attachment.path }));
1933
1729
  }
1934
1730
 
1935
1731
  async function persistPlacedAdmission(
@@ -1971,8 +1767,7 @@ async function persistPlacedAdmission(
1971
1767
  * before materializeCountersignInvocation writes the page.
1972
1768
  * Seats whose extra facts are known before placement pass them as admittedFields.
1973
1769
  * Seats whose extra facts depend on the placed run pass placedFields
1974
- * (coder writes task.md; fixer writes fix-packet.md; collector writes request-manifest.json;
1975
- * 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).
1976
1771
  */
1977
1772
  async function admitStandardMaterialInvocation<
1978
1773
  R extends PackagedRole,
@@ -1980,10 +1775,10 @@ async function admitStandardMaterialInvocation<
1980
1775
  >(
1981
1776
  role: R,
1982
1777
  options: AdmitInspectorInvocationOptions & {
1983
- /** Reserve coordinates; skip freeze, placement disk, and admitted section write. */
1778
+ /** Reserve coordinates; skip placement disk and admitted section write. */
1984
1779
  readonly deferPersistence?: boolean;
1985
- /** Skip attachment freeze. Gleaner-left admits no caller attachments. */
1986
- readonly freezeAttachments?: boolean;
1780
+ /** Leave attachments empty (gleaner-left / notary / deferred countersign). */
1781
+ readonly recordAttachments?: boolean;
1987
1782
  /** Seat facts already validated by admitPublicRole. */
1988
1783
  readonly admittedFields?: Extra;
1989
1784
  /** Seat facts that exist only after placement. Written before the admitted page. */
@@ -1991,14 +1786,14 @@ async function admitStandardMaterialInvocation<
1991
1786
  },
1992
1787
  ): Promise<AdmittedRoleInvocationBase & { readonly role: R } & Extra> {
1993
1788
  const defer = options.deferPersistence === true;
1994
- const skipFreeze = defer || options.freezeAttachments === false;
1789
+ const skipAttachments = defer || options.recordAttachments === false;
1995
1790
  const placed = await placeRoleAdmission({
1996
1791
  role,
1997
1792
  home: options.home,
1998
1793
  principalAuthority: options.principalAuthority,
1999
1794
  cwd: options.cwd,
2000
1795
  attachmentPaths: options.attachmentPaths,
2001
- ...(skipFreeze ? { freezeAttachments: false } : {}),
1796
+ ...(skipAttachments ? { recordAttachments: false } : {}),
2002
1797
  ...(defer ? { materialize: false } : {}),
2003
1798
  ...(options.project === undefined ? {} : { project: options.project }),
2004
1799
  ...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
@@ -2011,7 +1806,8 @@ async function admitStandardMaterialInvocation<
2011
1806
  ? {}
2012
1807
  : { correlationId: options.correlationId };
2013
1808
  const instruction = options.instruction;
2014
- const instructionEmpty = instruction.trim() === "";
1809
+ // Admitted emptiness is literal absence, not whitespace-only (#1165 J3).
1810
+ const instructionEmpty = instruction.length === 0;
2015
1811
  const placedExtra = options.placedFields === undefined ? undefined : await options.placedFields(placed);
2016
1812
  const admittedFields = {
2017
1813
  ...(options.admittedFields ?? ({} as Extra)),
@@ -2054,7 +1850,8 @@ type InstructionTransportSource = {
2054
1850
  readonly role?: string;
2055
1851
  readonly instruction: string;
2056
1852
  readonly instructionEmpty: boolean;
2057
- readonly attachments: readonly { readonly frozenPath: string }[];
1853
+ readonly attachments: readonly { readonly path: string }[];
1854
+ readonly requestManifestPath?: string;
2058
1855
  readonly baseRevision?: string;
2059
1856
  readonly lens?: ReviewerLens;
2060
1857
  readonly authorityRefs?: readonly string[];
@@ -2073,28 +1870,30 @@ function admittedTransportPromptKind(
2073
1870
  /**
2074
1871
  * One initial prompt transport. The registry `transportPrompt` leaf selects
2075
1872
  * a fixed kickoff, a bound baseline, or frozen skill args. Absent means the
2076
- * caller instruction plus frozen attachment paths.
1873
+ * caller instruction plus caller attachment paths as-is (#1165).
1874
+ * Engine / outsourcing material rides startup readingMaterial (#1167), not here.
2077
1875
  */
2078
1876
  export function buildInstructionTransportPrompt(
2079
1877
  admitted: InstructionTransportSource,
2080
- engineMaterial?: EngineSessionMaterial,
2081
1878
  ): string {
2082
1879
  const kind = admittedTransportPromptKind(admitted);
2083
1880
  if (kind === "fixed-kickoff") {
2084
1881
  if (admitted.sourceRunPath === undefined || admitted.sourceRunPath.trim() === "") {
2085
1882
  throw new Error("fixed-kickoff transport prompt is missing the source run pointer");
2086
1883
  }
2087
- return appendEngineSessionMaterial([admitted.sourceRunPath], engineMaterial).join("\n");
1884
+ return admitted.sourceRunPath;
2088
1885
  }
2089
1886
  if (kind === "baseline") {
2090
1887
  if (admitted.baseRevision === undefined) {
2091
1888
  throw new Error("baseline transport prompt is missing the bound revision");
2092
1889
  }
2093
1890
  const lines = [admitted.baseRevision];
2094
- 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) {
2095
1894
  lines.push("", admitted.instruction);
2096
1895
  }
2097
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
1896
+ return lines.join("\n");
2098
1897
  }
2099
1898
  if (kind === "skill-args") {
2100
1899
  if (
@@ -2109,20 +1908,28 @@ export function buildInstructionTransportPrompt(
2109
1908
  lens: admitted.lens,
2110
1909
  authorityRefs: admitted.authorityRefs,
2111
1910
  })];
2112
- if (!admitted.instructionEmpty && admitted.instruction.trim() !== "") {
1911
+ if (admitted.instruction.length > 0) {
2113
1912
  lines.push("", admitted.instruction);
2114
1913
  }
2115
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
1914
+ return lines.join("\n");
2116
1915
  }
2117
- const lines: string[] = [admitted.instructionEmpty ? "" : admitted.instruction];
2118
- 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) {
2119
1926
  lines.push("");
2120
- lines.push("已受理附件(冻结快照路径):");
2121
- for (const attachment of admitted.attachments) {
2122
- lines.push(`- ${attachment.frozenPath}`);
1927
+ lines.push("已受理附件:");
1928
+ for (const entry of flaggedPaths) {
1929
+ lines.push(`- ${entry}`);
2123
1930
  }
2124
1931
  }
2125
- return appendEngineSessionMaterial(lines, engineMaterial).join("\n");
1932
+ return lines.join("\n");
2126
1933
  }
2127
1934
 
2128
1935
  export type AdmitCountersignInvocationOptions = {
@@ -2147,7 +1954,7 @@ export type AdmitCountersignInvocationOptions = {
2147
1954
  export async function materializeCountersignInvocation(
2148
1955
  admitted: AdmittedCountersignInvocation,
2149
1956
  options: Pick<AdmitCountersignInvocationOptions, "home" | "principalAuthority" | "model"> & {
2150
- preparedAttachments: readonly PreparedAttachment[];
1957
+ attachmentPaths: readonly string[];
2151
1958
  /** Typed ticket known before the deferred run is written. */
2152
1959
  ticketNumber?: number;
2153
1960
  },
@@ -2157,8 +1964,7 @@ export async function materializeCountersignInvocation(
2157
1964
  home: options.home,
2158
1965
  principalAuthority: options.principalAuthority,
2159
1966
  cwd: admitted.projectRoot,
2160
- attachmentPaths: [],
2161
- freezeAttachments: false,
1967
+ attachmentPaths: options.attachmentPaths,
2162
1968
  runId: admitted.runId,
2163
1969
  ...(options.ticketNumber === undefined ? {} : { assertedTicketNumber: options.ticketNumber }),
2164
1970
  });
@@ -2169,11 +1975,7 @@ export async function materializeCountersignInvocation(
2169
1975
  if (ticketFields.ticketNumber !== undefined) {
2170
1976
  (admitted as { ticketNumber?: number }).ticketNumber = ticketFields.ticketNumber;
2171
1977
  }
2172
- const attachments = await freezePreparedAttachmentsIntoRun(
2173
- options.preparedAttachments,
2174
- admitted.runDirectory,
2175
- );
2176
- (admitted as { attachments: readonly FrozenAttachment[] }).attachments = attachments;
1978
+ (admitted as { attachments: readonly FrozenAttachment[] }).attachments = placement.attachments;
2177
1979
  writeAdmittedRequestPersistence(admitted.runDirectory, admitted, {
2178
1980
  sessionDirectory: placement.sessionDirectory,
2179
1981
  sessionFile: placement.sessionFile,
@@ -2200,10 +2002,13 @@ export async function loadAdmittedJudgeRequest(
2200
2002
  if (typeof record.instruction !== "string") return undefined;
2201
2003
  if (typeof record.instructionEmpty !== "boolean") return undefined;
2202
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);
2203
2008
  return {
2204
2009
  instruction: record.instruction,
2205
2010
  instructionEmpty: record.instructionEmpty,
2206
- attachments: record.attachments as FrozenAttachment[],
2011
+ attachments,
2207
2012
  };
2208
2013
  } catch {
2209
2014
  return undefined;
@@ -2600,7 +2405,7 @@ export function parseAnalystArgv(args: readonly string[]): ParseAnalystArgvResul
2600
2405
  if (taken.def.id === "attach") {
2601
2406
  pushValue(
2602
2407
  "attach",
2603
- requireOptionPath(taken.def.canonical, taken.value),
2408
+ requireProvidedOptionValue(taken.def.canonical, taken.value),
2604
2409
  );
2605
2410
  return;
2606
2411
  }