@akagilnc/pi-workflow-roles 0.1.5583 → 0.1.5598

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/dist/acp-host/production-host.js +393 -854
  4. package/dist/collector-config.js +0 -44
  5. package/dist/headless-host/production-host.js +361 -822
  6. package/dist/merger-contracts.js +6 -2
  7. package/dist/migrate-book-topology.js +4855 -10472
  8. package/dist/package-contracts/worker-output.js +1 -6
  9. package/dist/packaged-role-registry.js +5 -4
  10. package/dist/public-cli/instruction-seat-run.js +8 -8
  11. package/dist/public-cli/invocation.js +93 -236
  12. package/dist/public-cli/main.js +18409 -18786
  13. package/dist/public-cli/option-definitions.js +7 -7
  14. package/dist/public-cli/post-admission.js +11 -40
  15. package/dist/public-cli/run-lifecycle.js +11 -43
  16. package/dist/public-role-summons.js +1 -1
  17. package/dist/role-run-relocation.js +14 -23
  18. package/package.json +1 -1
  19. package/scripts/build-package.mjs +0 -1
  20. package/src/collector-config.ts +0 -42
  21. package/src/collector-role.ts +10 -20
  22. package/src/host-contracts.ts +0 -3
  23. package/src/merger-contracts.ts +6 -2
  24. package/src/merger-role.ts +11 -1
  25. package/src/navigator-attendance.ts +2 -2
  26. package/src/package-contracts/worker-output.ts +0 -8
  27. package/src/packaged-role-registry.ts +4 -3
  28. package/src/public-cli/instruction-seat-run.ts +7 -8
  29. package/src/public-cli/invocation.ts +113 -351
  30. package/src/public-cli/option-definitions.ts +7 -7
  31. package/src/public-cli/post-admission.ts +12 -56
  32. package/src/public-cli/run-lifecycle.ts +11 -55
  33. package/src/public-role-summons.ts +1 -1
  34. package/src/role-run-placement.ts +0 -1
  35. package/src/role-run-relocation.ts +14 -25
  36. package/src/role-runtime-dependencies.ts +0 -2
  37. package/src/role-runtime.ts +0 -16
  38. package/src/worker-role.ts +11 -78
  39. package/dist/package-contracts/fixer-packet.js +0 -77
  40. package/packets/fixer-prerequisites.json +0 -6
  41. package/src/package-contracts/fixer-packet.ts +0 -91
@@ -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,18 +60,11 @@ 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,
73
66
  } from "../collector-config.ts";
74
67
  import { ownerRepoFromGitHubRemoteUrl } from "./github-remote.ts";
75
- import {
76
- FixerPacketValidationError,
77
- parseFixerPrerequisites,
78
- type FixerPrerequisite,
79
- } from "../package-contracts/fixer-packet.ts";
80
68
  import type { FixerPhase } from "../package-contracts/fixer-output.ts";
81
69
  import { createProductionMergerGitState } from "../merger-git-state.ts";
82
70
  import type { MergerGitState } from "../merger-git-state.ts";
@@ -110,16 +98,27 @@ import type { PublicThinkingLevel } from "./registry.ts";
110
98
 
111
99
  import { errorText, isRecord } from "../unknown-value.ts";
112
100
 
101
+ /** Caller-supplied attachment path recorded as-is (#1165). */
113
102
  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";
103
+ readonly path: string;
121
104
  };
122
105
 
106
+ /** Normalize admitted attachment records (caller path only; #1165). */
107
+ export function normalizeAdmittedAttachment(raw: unknown): FrozenAttachment | undefined {
108
+ if (typeof raw !== "object" || raw === null) return undefined;
109
+ const record = raw as Record<string, unknown>;
110
+ const path = record.path;
111
+ // Opaque caller path — keep "" / whitespace; only a non-string is absent.
112
+ if (typeof path === "string") return { path };
113
+ return undefined;
114
+ }
115
+
116
+ function admitCallerAttachments(
117
+ attachmentPaths: readonly string[],
118
+ ): readonly FrozenAttachment[] {
119
+ return attachmentPaths.map((path) => ({ path }));
120
+ }
121
+
123
122
  /** Shared admitted Role run identity (#106 common Invocation + #109 Coder). */
124
123
  export type AdmittedRoleInvocationBase = {
125
124
  readonly runId: string;
@@ -127,7 +126,7 @@ export type AdmittedRoleInvocationBase = {
127
126
  readonly projectRoot: string;
128
127
  /** Opaque instruction bytes as submitted. */
129
128
  readonly instruction: string;
130
- /** True when the caller supplied no nonblank instruction. */
129
+ /** True when the caller supplied a literally empty instruction (length 0). */
131
130
  readonly instructionEmpty: boolean;
132
131
  readonly attachments: readonly FrozenAttachment[];
133
132
  readonly runDirectory: string;
@@ -200,20 +199,14 @@ export type AdmittedCoderInvocation = AdmittedRoleInvocationBase & {
200
199
  readonly role: "coder";
201
200
  /** Explicit plan or default apply — preserved through admission and continuation. */
202
201
  readonly phase: CoderPhase;
203
- /** Durable task file path consumed by internal --ak-coder-task. */
204
- readonly taskPath: string;
205
202
  };
206
203
 
207
204
  export type AdmittedFixerInvocation = AdmittedRoleInvocationBase & {
208
205
  readonly role: "fixer";
209
206
  /** Explicit plan or default apply — preserved through admission and continuation. */
210
207
  readonly phase: FixerPhase;
211
- /** Durable opaque instruction path consumed by internal --ak-fix-packet. */
212
- readonly packetPath: string;
213
- /** Optional durable prerequisites JSON path for --ak-fixer-prerequisites. */
208
+ /** Optional caller --prerequisites path as-is (#1168; package does not read it). */
214
209
  readonly prerequisitesPath?: string;
215
- /** Structurally validated prerequisite declarations frozen at admission. */
216
- readonly prerequisites: readonly FixerPrerequisite[];
217
210
  };
218
211
 
219
212
  export type AdmittedCollectorInvocation = AdmittedRoleInvocationBase & {
@@ -222,7 +215,6 @@ export type AdmittedCollectorInvocation = AdmittedRoleInvocationBase & {
222
215
  readonly prNumber?: number;
223
216
  readonly repository: CollectorRepository;
224
217
  readonly requestManifestPath?: string;
225
- readonly manifestDigest: string;
226
218
  };
227
219
 
228
220
  export type AdmittedDoctorInvocation = AdmittedRoleInvocationBase & {
@@ -619,22 +611,13 @@ export async function relocateAdmittedRunToTicket(
619
611
  admittedRecord,
620
612
  [
621
613
  "runDirectory",
622
- "taskPath",
623
- "packetPath",
624
- "prerequisitesPath",
625
- "requestManifestPath",
626
614
  "mergerInputPath",
627
615
  ],
628
616
  oldRunDirectory,
629
617
  target.runDirectory,
630
618
  );
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
- }
619
+ // #1165/#1168: caller file-flag paths stay as given. Obsolete copy fields
620
+ // (taskPath/packetPath) are not rewritten — stock volumes keep their bytes.
638
621
  (admitted as { principal: DurablePrincipal }).principal = principal;
639
622
 
640
623
  // current.json projects host.original from this run directory. Ownership is
@@ -842,7 +825,8 @@ function assignPublicSeatOption(
842
825
  const value = taken.value;
843
826
  switch (taken.def.id) {
844
827
  case "attach":
845
- fields.attachmentPaths.push(requireOptionPath(taken.def.canonical, value));
828
+ // #1165: file-flag values are opaque; only a missing argument is a usage error.
829
+ fields.attachmentPaths.push(requireProvidedOptionValue(taken.def.canonical, value));
846
830
  return;
847
831
  case "project":
848
832
  fields.project = requireOptionPath(taken.def.canonical, value);
@@ -879,7 +863,8 @@ function assignPublicSeatOption(
879
863
  fields.authorityRefs.push(requireAuthorityRef(value));
880
864
  return;
881
865
  case "prerequisites":
882
- fields.prerequisitesPath = requireOptionPath(taken.def.canonical, value);
866
+ // #1168: same opaque file-flag rule as --attach.
867
+ fields.prerequisitesPath = requireProvidedOptionValue(taken.def.canonical, value);
883
868
  return;
884
869
  case "pr":
885
870
  fields.prNumber = parsePositivePrOption(value);
@@ -888,7 +873,8 @@ function assignPublicSeatOption(
888
873
  fields.repo = parseRepoOption(value);
889
874
  return;
890
875
  case "request-manifest":
891
- fields.requestManifestPath = requireOptionPath(taken.def.canonical, value);
876
+ // #1165: file-flag values are opaque; only a missing argument is a usage error.
877
+ fields.requestManifestPath = requireProvidedOptionValue(taken.def.canonical, value);
892
878
  return;
893
879
  case "issue": {
894
880
  if (value === undefined || value.trim() === "") {
@@ -1072,6 +1058,20 @@ function requireOptionPath(
1072
1058
  return value;
1073
1059
  }
1074
1060
 
1061
+ /**
1062
+ * #1165 file flags (--attach / --request-manifest): keep the provided string
1063
+ * opaque (including spaces and ""). Only a missing argument is a usage error.
1064
+ */
1065
+ function requireProvidedOptionValue(
1066
+ flag: string,
1067
+ value: string | undefined,
1068
+ ): string {
1069
+ if (value === undefined) {
1070
+ throw new CliUsageError(`${flag} requires a path`);
1071
+ }
1072
+ return value;
1073
+ }
1074
+
1075
1075
  /**
1076
1076
  * Shared Skill-arg token rule for caller-controlled values projected into the
1077
1077
  * space-joined Skill invocation line. Rejects blank, whitespace (smuggles the
@@ -1166,184 +1166,6 @@ export function requireAuthorityRef(value: string | undefined): string {
1166
1166
  });
1167
1167
  }
1168
1168
 
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
1169
  function ticketAdmissionFields(
1348
1170
  ticketNumber: number | undefined,
1349
1171
  ): { ticketNumber?: number } {
@@ -1490,16 +1312,13 @@ export async function admitPublicRole(
1490
1312
  if (!record.phases.some((item) => item === phase)) {
1491
1313
  throw new CliUsageError("coder phase must be plan or apply");
1492
1314
  }
1315
+ // #1168: dispatch text is the first message only — no task.md copy.
1493
1316
  return admitStandardMaterialInvocation("coder", {
1494
1317
  ...shared,
1495
1318
  instruction,
1496
1319
  attachmentPaths,
1497
1320
  ...project,
1498
- placedFields: async (placed) => {
1499
- const taskPath = join(placed.runDirectory, "task.md");
1500
- await writeFile(taskPath, instruction, "utf8");
1501
- return { phase, taskPath };
1502
- },
1321
+ placedFields: async () => ({ phase }),
1503
1322
  });
1504
1323
  }
1505
1324
  case "worker-packet": {
@@ -1510,53 +1329,18 @@ export async function admitPublicRole(
1510
1329
  if (!record.phases.some((item) => item === phase)) {
1511
1330
  throw new CliUsageError("fixer phase must be plan or apply");
1512
1331
  }
1513
- let prerequisites: readonly FixerPrerequisite[] = Object.freeze([]);
1514
- let prerequisitesSource: string | undefined;
1515
- if (parsed.prerequisitesPath !== undefined) {
1516
- const absolutePrereq = isAbsolute(parsed.prerequisitesPath)
1517
- ? parsed.prerequisitesPath
1518
- : resolve(parsed.prerequisitesPath);
1519
- try {
1520
- prerequisitesSource = await readFile(absolutePrereq, "utf8");
1521
- } catch (error) {
1522
- throw new CliUsageError(
1523
- `fixer prerequisites path is unreadable: ${parsed.prerequisitesPath}`,
1524
- { cause: error },
1525
- );
1526
- }
1527
- try {
1528
- prerequisites = parseFixerPrerequisites(prerequisitesSource);
1529
- } catch (error) {
1530
- if (error instanceof FixerPacketValidationError) {
1531
- throw new CliUsageError(error.message, { cause: error });
1532
- }
1533
- throw error;
1534
- }
1535
- }
1332
+ // #1168: dispatch once as first message; --prerequisites path as-is.
1536
1333
  return admitStandardMaterialInvocation("fixer", {
1537
1334
  ...shared,
1538
1335
  instruction,
1539
1336
  attachmentPaths,
1540
1337
  ...project,
1541
- placedFields: async (placed) => {
1542
- let prerequisitesPath: string | undefined;
1543
- if (prerequisitesSource !== undefined) {
1544
- prerequisitesPath = join(placed.runDirectory, "prerequisites.json");
1545
- await writeFile(
1546
- prerequisitesPath,
1547
- `${JSON.stringify(prerequisites, null, 2)}\n`,
1548
- "utf8",
1549
- );
1550
- }
1551
- const packetPath = join(placed.runDirectory, "fix-packet.md");
1552
- await writeFile(packetPath, instruction, "utf8");
1553
- return {
1554
- phase,
1555
- packetPath,
1556
- prerequisites,
1557
- ...(prerequisitesPath === undefined ? {} : { prerequisitesPath }),
1558
- };
1559
- },
1338
+ placedFields: async () => ({
1339
+ phase,
1340
+ ...(parsed.prerequisitesPath === undefined
1341
+ ? {}
1342
+ : { prerequisitesPath: parsed.prerequisitesPath }),
1343
+ }),
1560
1344
  });
1561
1345
  }
1562
1346
  case "collect-target": {
@@ -1573,38 +1357,21 @@ export async function admitPublicRole(
1573
1357
  } else {
1574
1358
  repository = resolveGitHubRemoteRepository(projectRoot);
1575
1359
  }
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;
1360
+ // #1165: pass caller path as-is; package does not read, validate, or rewrite.
1590
1361
  const admittedCollector = await admitStandardMaterialInvocation("collector", {
1591
1362
  ...shared,
1592
1363
  instruction,
1593
1364
  attachmentPaths,
1594
1365
  ...project,
1595
- placedFields: async (placed) => {
1366
+ placedFields: async () => {
1596
1367
  // #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
1368
  return {
1603
1369
  ...(explicitPrNumber === undefined ? {} : { prNumber: explicitPrNumber }),
1604
1370
  repository: repository.canonical,
1605
1371
  repositoryDisplay: repository.display,
1606
- ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
1607
- manifestDigest,
1372
+ ...(parsed.requestManifestPath === undefined
1373
+ ? {}
1374
+ : { requestManifestPath: parsed.requestManifestPath }),
1608
1375
  };
1609
1376
  },
1610
1377
  });
@@ -1617,12 +1384,10 @@ export async function admitPublicRole(
1617
1384
  );
1618
1385
  }
1619
1386
  const issueNumber = parsed.issueNumber;
1620
- let frozenAttachments: readonly FrozenAttachment[] = [];
1621
- const admittedDoctor = await admitStandardMaterialInvocation("doctor", {
1387
+ return admitStandardMaterialInvocation("doctor", {
1622
1388
  ...shared,
1623
1389
  instruction,
1624
- attachmentPaths: [],
1625
- freezeAttachments: false,
1390
+ attachmentPaths,
1626
1391
  ...project,
1627
1392
  placedFields: async (placed) => {
1628
1393
  let caseRunsPath: string;
@@ -1663,16 +1428,13 @@ export async function admitPublicRole(
1663
1428
  { cause: error },
1664
1429
  );
1665
1430
  }
1666
- frozenAttachments = await freezeAttachments(placed.ledgerHome, attachmentPaths, placed.attachmentsDirectory);
1667
1431
  return {
1668
1432
  issueNumber,
1669
1433
  caseRunsPath,
1670
1434
  caseIdentity,
1671
- attachments: persistedAttachmentRefs(frozenAttachments),
1672
1435
  };
1673
1436
  },
1674
1437
  });
1675
- return { ...admittedDoctor, attachments: frozenAttachments };
1676
1438
  }
1677
1439
  case "source-locator": {
1678
1440
  const projectRoot = resolve(parsed.project ?? shared.cwd);
@@ -1702,7 +1464,7 @@ export async function admitPublicRole(
1702
1464
  instruction: "",
1703
1465
  attachmentPaths: [],
1704
1466
  ...project,
1705
- freezeAttachments: false,
1467
+ recordAttachments: false,
1706
1468
  admittedFields: {
1707
1469
  sourceRunPath: sourceRun.runDirectory,
1708
1470
  sourceRun,
@@ -1719,7 +1481,7 @@ export async function admitPublicRole(
1719
1481
  instruction,
1720
1482
  attachmentPaths: [],
1721
1483
  ...project,
1722
- freezeAttachments: false,
1484
+ recordAttachments: false,
1723
1485
  admittedFields: { baseRevision },
1724
1486
  });
1725
1487
  }
@@ -1767,13 +1529,12 @@ export async function admitPublicRole(
1767
1529
  placedFields: async (placed) => {
1768
1530
  const targetLabel = derived.targetObjectId === "" ? "(none observed)" : derived.targetObjectId;
1769
1531
  const sourceLabel = derived.sourceObjectId === "" ? "(none observed)" : derived.sourceObjectId;
1532
+ // #1168: git facts stay; do not encode dispatch text as task/authority copies.
1770
1533
  const mergerInput = validateMergerInput({
1771
1534
  attemptId: placed.runId,
1772
1535
  targetObjectId: derived.targetObjectId,
1773
1536
  sourceObjectId: derived.sourceObjectId,
1774
1537
  materials: {
1775
- task: mergerMaterialFromUtf8(instruction),
1776
- authority: mergerMaterialFromUtf8(instruction),
1777
1538
  targetIntent: mergerMaterialFromUtf8(
1778
1539
  `Investigate primary sources for target parent ${targetLabel}. Do not invent intent.`,
1779
1540
  ),
@@ -1868,8 +1629,8 @@ async function placeRoleAdmission(options: {
1868
1629
  readonly runId?: string;
1869
1630
  readonly assertedTicketNumber?: number;
1870
1631
  readonly attachmentPaths: readonly string[];
1871
- /** Gleaner-left admits no caller attachments. */
1872
- readonly freezeAttachments?: boolean;
1632
+ /** When false, leave attachments empty (deferred countersign / seats without attach). */
1633
+ readonly recordAttachments?: boolean;
1873
1634
  /** Countersign reserves coordinates, then materializes after identity lookup. */
1874
1635
  readonly materialize?: boolean;
1875
1636
  }): Promise<PlacedRoleAdmission> {
@@ -1892,9 +1653,9 @@ async function placeRoleAdmission(options: {
1892
1653
  home: options.home,
1893
1654
  ...(options.materialize === undefined ? {} : { materialize: options.materialize }),
1894
1655
  });
1895
- const attachments = options.freezeAttachments === false
1656
+ const attachments = options.recordAttachments === false
1896
1657
  ? []
1897
- : await freezeAttachments(ledgerHome, options.attachmentPaths, attachmentsDirectory);
1658
+ : admitCallerAttachments(options.attachmentPaths);
1898
1659
  return {
1899
1660
  runId,
1900
1661
  bookKey,
@@ -1912,20 +1673,8 @@ async function placeRoleAdmission(options: {
1912
1673
 
1913
1674
  function persistedAttachmentRefs(
1914
1675
  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
- }));
1676
+ ): ReadonlyArray<{ path: string }> {
1677
+ return attachments.map((attachment) => ({ path: attachment.path }));
1929
1678
  }
1930
1679
 
1931
1680
  async function persistPlacedAdmission(
@@ -1967,8 +1716,7 @@ async function persistPlacedAdmission(
1967
1716
  * before materializeCountersignInvocation writes the page.
1968
1717
  * Seats whose extra facts are known before placement pass them as admittedFields.
1969
1718
  * 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).
1719
+ * (merger writes merger-input.json with git facts; coder/fixer dispatch is first-message only, #1168).
1972
1720
  */
1973
1721
  async function admitStandardMaterialInvocation<
1974
1722
  R extends PackagedRole,
@@ -1976,10 +1724,10 @@ async function admitStandardMaterialInvocation<
1976
1724
  >(
1977
1725
  role: R,
1978
1726
  options: AdmitInspectorInvocationOptions & {
1979
- /** Reserve coordinates; skip freeze, placement disk, and admitted section write. */
1727
+ /** Reserve coordinates; skip placement disk and admitted section write. */
1980
1728
  readonly deferPersistence?: boolean;
1981
- /** Skip attachment freeze. Gleaner-left admits no caller attachments. */
1982
- readonly freezeAttachments?: boolean;
1729
+ /** Leave attachments empty (gleaner-left / notary / deferred countersign). */
1730
+ readonly recordAttachments?: boolean;
1983
1731
  /** Seat facts already validated by admitPublicRole. */
1984
1732
  readonly admittedFields?: Extra;
1985
1733
  /** Seat facts that exist only after placement. Written before the admitted page. */
@@ -1987,14 +1735,14 @@ async function admitStandardMaterialInvocation<
1987
1735
  },
1988
1736
  ): Promise<AdmittedRoleInvocationBase & { readonly role: R } & Extra> {
1989
1737
  const defer = options.deferPersistence === true;
1990
- const skipFreeze = defer || options.freezeAttachments === false;
1738
+ const skipAttachments = defer || options.recordAttachments === false;
1991
1739
  const placed = await placeRoleAdmission({
1992
1740
  role,
1993
1741
  home: options.home,
1994
1742
  principalAuthority: options.principalAuthority,
1995
1743
  cwd: options.cwd,
1996
1744
  attachmentPaths: options.attachmentPaths,
1997
- ...(skipFreeze ? { freezeAttachments: false } : {}),
1745
+ ...(skipAttachments ? { recordAttachments: false } : {}),
1998
1746
  ...(defer ? { materialize: false } : {}),
1999
1747
  ...(options.project === undefined ? {} : { project: options.project }),
2000
1748
  ...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
@@ -2007,7 +1755,8 @@ async function admitStandardMaterialInvocation<
2007
1755
  ? {}
2008
1756
  : { correlationId: options.correlationId };
2009
1757
  const instruction = options.instruction;
2010
- const instructionEmpty = instruction.trim() === "";
1758
+ // Admitted emptiness is literal absence, not whitespace-only (#1165 J3).
1759
+ const instructionEmpty = instruction.length === 0;
2011
1760
  const placedExtra = options.placedFields === undefined ? undefined : await options.placedFields(placed);
2012
1761
  const admittedFields = {
2013
1762
  ...(options.admittedFields ?? ({} as Extra)),
@@ -2049,8 +1798,10 @@ export type AdmitAuditorInvocationOptions = AdmitInspectorInvocationOptions;
2049
1798
  type InstructionTransportSource = {
2050
1799
  readonly role?: string;
2051
1800
  readonly instruction: string;
2052
- readonly instructionEmpty: boolean;
2053
- readonly attachments: readonly { readonly frozenPath: string }[];
1801
+ readonly instructionEmpty?: boolean;
1802
+ readonly attachments: readonly { readonly path: string }[];
1803
+ readonly requestManifestPath?: string;
1804
+ readonly prerequisitesPath?: string;
2054
1805
  readonly baseRevision?: string;
2055
1806
  readonly lens?: ReviewerLens;
2056
1807
  readonly authorityRefs?: readonly string[];
@@ -2069,7 +1820,7 @@ function admittedTransportPromptKind(
2069
1820
  /**
2070
1821
  * One initial prompt transport. The registry `transportPrompt` leaf selects
2071
1822
  * a fixed kickoff, a bound baseline, or frozen skill args. Absent means the
2072
- * caller instruction plus frozen attachment paths.
1823
+ * caller instruction plus caller attachment paths as-is (#1165).
2073
1824
  * Engine / outsourcing material rides startup readingMaterial (#1167), not here.
2074
1825
  */
2075
1826
  export function buildInstructionTransportPrompt(
@@ -2087,7 +1838,9 @@ export function buildInstructionTransportPrompt(
2087
1838
  throw new Error("baseline transport prompt is missing the bound revision");
2088
1839
  }
2089
1840
  const lines = [admitted.baseRevision];
2090
- if (!admitted.instructionEmpty) {
1841
+ // Use raw admitted.instruction — do not gate on instructionEmpty metadata
1842
+ // (old pages may set that flag wrong while still holding whitespace text).
1843
+ if (admitted.instruction.length > 0) {
2091
1844
  lines.push("", admitted.instruction);
2092
1845
  }
2093
1846
  return lines.join("\n");
@@ -2105,17 +1858,28 @@ export function buildInstructionTransportPrompt(
2105
1858
  lens: admitted.lens,
2106
1859
  authorityRefs: admitted.authorityRefs,
2107
1860
  })];
2108
- if (!admitted.instructionEmpty && admitted.instruction.trim() !== "") {
1861
+ if (admitted.instruction.length > 0) {
2109
1862
  lines.push("", admitted.instruction);
2110
1863
  }
2111
1864
  return lines.join("\n");
2112
1865
  }
2113
- const lines: string[] = [admitted.instructionEmpty ? "" : admitted.instruction];
2114
- if (admitted.attachments.length > 0) {
1866
+ // #1165 J3 / #1168: assemble from the admitted instruction bytes themselves.
1867
+ const lines: string[] = [admitted.instruction];
1868
+ // #1164/#1165/#1168: each caller path keeps its file-flag provenance.
1869
+ const flaggedPaths: string[] = [
1870
+ ...admitted.attachments.map((attachment) => `--attach ${attachment.path}`),
1871
+ ...(admitted.requestManifestPath === undefined
1872
+ ? []
1873
+ : [`--request-manifest ${admitted.requestManifestPath}`]),
1874
+ ...(admitted.prerequisitesPath === undefined
1875
+ ? []
1876
+ : [`--prerequisites ${admitted.prerequisitesPath}`]),
1877
+ ];
1878
+ if (flaggedPaths.length > 0) {
2115
1879
  lines.push("");
2116
- lines.push("已受理附件(冻结快照路径):");
2117
- for (const attachment of admitted.attachments) {
2118
- lines.push(`- ${attachment.frozenPath}`);
1880
+ lines.push("已受理附件:");
1881
+ for (const entry of flaggedPaths) {
1882
+ lines.push(`- ${entry}`);
2119
1883
  }
2120
1884
  }
2121
1885
  return lines.join("\n");
@@ -2143,7 +1907,7 @@ export type AdmitCountersignInvocationOptions = {
2143
1907
  export async function materializeCountersignInvocation(
2144
1908
  admitted: AdmittedCountersignInvocation,
2145
1909
  options: Pick<AdmitCountersignInvocationOptions, "home" | "principalAuthority" | "model"> & {
2146
- preparedAttachments: readonly PreparedAttachment[];
1910
+ attachmentPaths: readonly string[];
2147
1911
  /** Typed ticket known before the deferred run is written. */
2148
1912
  ticketNumber?: number;
2149
1913
  },
@@ -2153,8 +1917,7 @@ export async function materializeCountersignInvocation(
2153
1917
  home: options.home,
2154
1918
  principalAuthority: options.principalAuthority,
2155
1919
  cwd: admitted.projectRoot,
2156
- attachmentPaths: [],
2157
- freezeAttachments: false,
1920
+ attachmentPaths: options.attachmentPaths,
2158
1921
  runId: admitted.runId,
2159
1922
  ...(options.ticketNumber === undefined ? {} : { assertedTicketNumber: options.ticketNumber }),
2160
1923
  });
@@ -2165,11 +1928,7 @@ export async function materializeCountersignInvocation(
2165
1928
  if (ticketFields.ticketNumber !== undefined) {
2166
1929
  (admitted as { ticketNumber?: number }).ticketNumber = ticketFields.ticketNumber;
2167
1930
  }
2168
- const attachments = await freezePreparedAttachmentsIntoRun(
2169
- options.preparedAttachments,
2170
- admitted.runDirectory,
2171
- );
2172
- (admitted as { attachments: readonly FrozenAttachment[] }).attachments = attachments;
1931
+ (admitted as { attachments: readonly FrozenAttachment[] }).attachments = placement.attachments;
2173
1932
  writeAdmittedRequestPersistence(admitted.runDirectory, admitted, {
2174
1933
  sessionDirectory: placement.sessionDirectory,
2175
1934
  sessionFile: placement.sessionFile,
@@ -2196,10 +1955,13 @@ export async function loadAdmittedJudgeRequest(
2196
1955
  if (typeof record.instruction !== "string") return undefined;
2197
1956
  if (typeof record.instructionEmpty !== "boolean") return undefined;
2198
1957
  if (!Array.isArray(record.attachments)) return undefined;
1958
+ const attachments = record.attachments
1959
+ .map((item) => normalizeAdmittedAttachment(item))
1960
+ .filter((item): item is FrozenAttachment => item !== undefined);
2199
1961
  return {
2200
1962
  instruction: record.instruction,
2201
1963
  instructionEmpty: record.instructionEmpty,
2202
- attachments: record.attachments as FrozenAttachment[],
1964
+ attachments,
2203
1965
  };
2204
1966
  } catch {
2205
1967
  return undefined;
@@ -2213,7 +1975,7 @@ export type AdmitFixerInvocationOptions = {
2213
1975
  phase: FixerPhase;
2214
1976
  instruction: string;
2215
1977
  attachmentPaths: readonly string[];
2216
- /** Optional caller path to prerequisite JSON array; malformed grammar rejects here. */
1978
+ /** Optional caller --prerequisites path as-is (#1168). */
2217
1979
  prerequisitesPath?: string;
2218
1980
  project?: string;
2219
1981
  createRunId?: () => string;
@@ -2389,7 +2151,7 @@ export function buildReviewerSkillArgProjection(
2389
2151
  ].join(" ");
2390
2152
  }
2391
2153
 
2392
- function mergerMaterialFromUtf8(text: string): MergerInput["materials"]["task"] {
2154
+ function mergerMaterialFromUtf8(text: string): MergerInput["materials"]["targetIntent"] {
2393
2155
  const bytes = Buffer.from(text, "utf8");
2394
2156
  return Object.freeze({
2395
2157
  bytesBase64: bytes.toString("base64"),
@@ -2596,7 +2358,7 @@ export function parseAnalystArgv(args: readonly string[]): ParseAnalystArgvResul
2596
2358
  if (taken.def.id === "attach") {
2597
2359
  pushValue(
2598
2360
  "attach",
2599
- requireOptionPath(taken.def.canonical, taken.value),
2361
+ requireProvidedOptionValue(taken.def.canonical, taken.value),
2600
2362
  );
2601
2363
  return;
2602
2364
  }