@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,23 +1,21 @@
1
1
  /**
2
- * Public Invocation request admission: optional opaque instruction, frozen
3
- * Attachments, project default/override (ADR 0052 / #106).
2
+ * Public Invocation request admission: optional opaque instruction, caller
3
+ * attachment paths as-is, project default/override (ADR 0052 / #106 / #1165).
4
4
  */
5
5
  import { execFileSync } from "node:child_process";
6
6
  import { existsSync } from "node:fs";
7
- import { lstat, mkdtemp, readFile, realpath, rename, rm, writeFile, } from "node:fs/promises";
8
- import { tmpdir } from "node:os";
9
- import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
7
+ import { readFile, realpath, rename, writeFile, } from "node:fs/promises";
8
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
10
9
  import { activationBookDirectory, ensureRealDirectoryTree, homeFromRunDirectory, pathContainedIn, resolveActivationLedgerHome, } from "../activation-ledger-topology.js";
11
10
  import { resolveBookKeyFromGit } from "../activation-ledger-git.js";
12
11
  import { ensureRoleRunDirectory, ensureRoleRunPlacement, formatRunLeaf, isUnboundRunDirectory, listBookRunDirectories, roleRunPlacement, } from "../role-run-placement.js";
13
12
  import { packagedAdmittedSubject, packagedArgvResult, packagedBareToken, packagedBaseIsRevision, packagedEmitsAuthorityRefs, packagedPublicInstructionSubject, packagedRoleMetadata, packagedStoresSourceRunRaw, packagedSubjectChoices, } from "../packaged-role-registry.js";
14
13
  import { isSafePositiveTicketNumber, parseTicketNumber, readBoardTicketNumber, requireSafePositiveTicketNumber, } from "../run-ticket-number.js";
15
- import { rewriteRunDirectoryPathFields, rewriteRunDirectoryPathValue, } from "../role-run-relocation.js";
14
+ import { rewriteRunDirectoryPathFields, } from "../role-run-relocation.js";
16
15
  import { readPageSync, renderCurrentSync, updateSectionSync, writeSectionSync } from "../run-dossier.js";
17
16
  import { loadDoctorCase, } from "../doctor-evidence.js";
18
- import { emptyCollectorManifest, loadCollectorManifest, parseCollectorPrNumber, parseCollectorRepository, } from "../collector-config.js";
17
+ import { parseCollectorPrNumber, parseCollectorRepository, } from "../collector-config.js";
19
18
  import { ownerRepoFromGitHubRemoteUrl } from "./github-remote.js";
20
- import { FixerPacketValidationError, parseFixerPrerequisites, } from "../package-contracts/fixer-packet.js";
21
19
  import { createProductionMergerGitState } from "../merger-git-state.js";
22
20
  import { validateMergerInput, } from "../merger-contracts.js";
23
21
  import { sha256Hex } from "../sha256.js";
@@ -28,6 +26,20 @@ import { NotarySourceRunError, resolveNotarySourceRunLocator, } from "../notary-
28
26
  import { CliUsageError } from "./cli-errors.js";
29
27
  import { REJECTED_PUBLIC_SPELLINGS, createTypedOptionConsumer, evaluateAnalystModeOptionContract, optionsForOwner, resolveAnalystMode, } from "./option-definitions.js";
30
28
  import { errorText, isRecord } from "../unknown-value.js";
29
+ /** Normalize admitted attachment records (caller path only; #1165). */
30
+ export function normalizeAdmittedAttachment(raw) {
31
+ if (typeof raw !== "object" || raw === null)
32
+ return undefined;
33
+ const record = raw;
34
+ const path = record.path;
35
+ // Opaque caller path — keep "" / whitespace; only a non-string is absent.
36
+ if (typeof path === "string")
37
+ return { path };
38
+ return undefined;
39
+ }
40
+ function admitCallerAttachments(attachmentPaths) {
41
+ return attachmentPaths.map((path) => ({ path }));
42
+ }
31
43
  /** Issue a principal from the single authoritative role-run placement. */
32
44
  export function issueAdmissionPlacement(authority, request) {
33
45
  const ledgerHome = resolveActivationLedgerHome(request.home);
@@ -258,15 +270,10 @@ export async function relocateAdmittedRunToTicket(admitted, authority, heldLease
258
270
  const admittedRecord = admitted;
259
271
  rewriteRunDirectoryPathFields(admittedRecord, [
260
272
  "runDirectory",
261
- "taskPath",
262
- "packetPath",
263
- "prerequisitesPath",
264
- "requestManifestPath",
265
273
  "mergerInputPath",
266
274
  ], oldRunDirectory, target.runDirectory);
267
- for (const attachment of admitted.attachments) {
268
- attachment.frozenPath = rewriteRunDirectoryPathValue(attachment.frozenPath, oldRunDirectory, target.runDirectory);
269
- }
275
+ // #1165/#1168: caller file-flag paths stay as given. Obsolete copy fields
276
+ // (taskPath/packetPath) are not rewritten — stock volumes keep their bytes.
270
277
  admitted.principal = principal;
271
278
  // current.json projects host.original from this run directory. Ownership is
272
279
  // already transferred; a refuse still carries the true cause to the caller
@@ -382,7 +389,8 @@ function assignPublicSeatOption(owner, taken, fields) {
382
389
  const value = taken.value;
383
390
  switch (taken.def.id) {
384
391
  case "attach":
385
- fields.attachmentPaths.push(requireOptionPath(taken.def.canonical, value));
392
+ // #1165: file-flag values are opaque; only a missing argument is a usage error.
393
+ fields.attachmentPaths.push(requireProvidedOptionValue(taken.def.canonical, value));
386
394
  return;
387
395
  case "project":
388
396
  fields.project = requireOptionPath(taken.def.canonical, value);
@@ -417,7 +425,8 @@ function assignPublicSeatOption(owner, taken, fields) {
417
425
  fields.authorityRefs.push(requireAuthorityRef(value));
418
426
  return;
419
427
  case "prerequisites":
420
- fields.prerequisitesPath = requireOptionPath(taken.def.canonical, value);
428
+ // #1168: same opaque file-flag rule as --attach.
429
+ fields.prerequisitesPath = requireProvidedOptionValue(taken.def.canonical, value);
421
430
  return;
422
431
  case "pr":
423
432
  fields.prNumber = parsePositivePrOption(value);
@@ -426,7 +435,8 @@ function assignPublicSeatOption(owner, taken, fields) {
426
435
  fields.repo = parseRepoOption(value);
427
436
  return;
428
437
  case "request-manifest":
429
- fields.requestManifestPath = requireOptionPath(taken.def.canonical, value);
438
+ // #1165: file-flag values are opaque; only a missing argument is a usage error.
439
+ fields.requestManifestPath = requireProvidedOptionValue(taken.def.canonical, value);
430
440
  return;
431
441
  case "issue": {
432
442
  if (value === undefined || value.trim() === "") {
@@ -539,6 +549,16 @@ function requireOptionPath(flag, value) {
539
549
  }
540
550
  return value;
541
551
  }
552
+ /**
553
+ * #1165 file flags (--attach / --request-manifest): keep the provided string
554
+ * opaque (including spaces and ""). Only a missing argument is a usage error.
555
+ */
556
+ function requireProvidedOptionValue(flag, value) {
557
+ if (value === undefined) {
558
+ throw new CliUsageError(`${flag} requires a path`);
559
+ }
560
+ return value;
561
+ }
542
562
  /**
543
563
  * Shared Skill-arg token rule for caller-controlled values projected into the
544
564
  * space-joined Skill invocation line. Rejects blank, whitespace (smuggles the
@@ -624,123 +644,6 @@ export function requireAuthorityRef(value) {
624
644
  optionLike: "--authority-ref requires a durable reference, not inline Spec prose",
625
645
  });
626
646
  }
627
- async function readRegularFileAttachment(sourcePath) {
628
- const absolute = isAbsolute(sourcePath) ? sourcePath : resolve(sourcePath);
629
- let st;
630
- try {
631
- st = await lstat(absolute);
632
- }
633
- catch (error) {
634
- throw new CliUsageError(`attachment is not a readable regular file: ${sourcePath}`, { cause: error });
635
- }
636
- if (!st.isFile() || st.isSymbolicLink()) {
637
- throw new CliUsageError(`attachment must be a regular file (not a directory or symlink): ${sourcePath}`);
638
- }
639
- try {
640
- return { absolute, bytes: await readFile(absolute) };
641
- }
642
- catch (error) {
643
- throw new CliUsageError(`attachment is not a readable regular file: ${sourcePath}`, { cause: error });
644
- }
645
- }
646
- /**
647
- * Snapshot deferred inputs sequentially before identity side effects. The staging
648
- * files keep aggregate attachment bytes off heap until their final run is known.
649
- */
650
- async function removePreparedAttachmentDirectory(stagingDirectory) {
651
- await rm(stagingDirectory, { recursive: true, force: true });
652
- }
653
- /** Own the complete deferred-snapshot lifetime without masking either failure. */
654
- export async function withPreparedAttachments(attachmentPaths, use) {
655
- if (attachmentPaths.length === 0)
656
- return await use([]);
657
- const stagingDirectory = await mkdtemp(join(tmpdir(), "ak-role-attachments-"));
658
- const prepared = [];
659
- let result;
660
- try {
661
- for (let index = 0; index < attachmentPaths.length; index += 1) {
662
- const { absolute, bytes } = await readRegularFileAttachment(attachmentPaths[index]);
663
- const snapshotPath = join(stagingDirectory, String(index).padStart(6, "0"));
664
- await writeFile(snapshotPath, bytes);
665
- prepared.push({ absolute, snapshotPath });
666
- }
667
- result = await use(prepared);
668
- }
669
- catch (primary) {
670
- try {
671
- await removePreparedAttachmentDirectory(stagingDirectory);
672
- }
673
- catch (cleanup) {
674
- throw new AggregateError([primary, cleanup], "attachment snapshot operation failed and cleanup also failed", { cause: primary });
675
- }
676
- throw primary;
677
- }
678
- await removePreparedAttachmentDirectory(stagingDirectory);
679
- return result;
680
- }
681
- async function freezeAttachmentBytes(provenancePath, bytes, destinationDir, index) {
682
- const frozenPath = join(destinationDir, `${String(index).padStart(2, "0")}-${basename(provenancePath)}`);
683
- await writeFile(frozenPath, bytes);
684
- return {
685
- provenancePath,
686
- frozenPath,
687
- byteLength: bytes.byteLength,
688
- sha256: sha256Hex(bytes),
689
- mediaKind: "regular-file",
690
- };
691
- }
692
- async function freezePreparedAttachment(prepared, destinationDir, index) {
693
- return freezeAttachmentBytes(prepared.absolute, await readFile(prepared.snapshotPath), destinationDir, index);
694
- }
695
- async function freezeRegularFileAttachment(sourcePath, destinationDir, index) {
696
- const { absolute, bytes } = await readRegularFileAttachment(sourcePath);
697
- return {
698
- attachment: await freezeAttachmentBytes(absolute, bytes, destinationDir, index),
699
- body: bytes,
700
- };
701
- }
702
- /** Freeze attachments only — ticket binding is the shared LLM seat path (#635). */
703
- async function freezeAttachments(ledgerHome, attachmentPaths, attachmentsDirectory) {
704
- const attachments = [];
705
- // attachments/ appears only when something is frozen; created through the
706
- // ledger's symlink-rejecting tree walk, never a plain mkdir.
707
- if (attachmentPaths.length > 0) {
708
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
709
- }
710
- for (let i = 0; i < attachmentPaths.length; i += 1) {
711
- const frozen = await freezeRegularFileAttachment(attachmentPaths[i], attachmentsDirectory, i);
712
- attachments.push(frozen.attachment);
713
- }
714
- return attachments;
715
- }
716
- /**
717
- * Freeze summons attachments into an already-retained run (#637 same-ticket resume).
718
- * Writes under attachments/summons-<key>/ so prior freeze names stay intact.
719
- * Manual resume never calls this — birth attachments keep their original semantics.
720
- */
721
- export async function freezeAttachmentsIntoRun(attachmentPaths, runDirectory, summonsKey = `s-${Date.now().toString(36)}`) {
722
- if (attachmentPaths.length === 0)
723
- return [];
724
- const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(runDirectory));
725
- const attachmentsDirectory = join(runDirectory, "attachments", summonsKey);
726
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
727
- return freezeAttachments(ledgerHome, attachmentPaths, attachmentsDirectory);
728
- }
729
- /** Freeze prepared bytes and metadata through one path for birth or retained runs. */
730
- export async function freezePreparedAttachmentsIntoRun(prepared, runDirectory, summonsKey) {
731
- if (prepared.length === 0)
732
- return [];
733
- const ledgerHome = resolveActivationLedgerHome(homeFromRunDirectory(runDirectory));
734
- const attachmentsDirectory = summonsKey === undefined
735
- ? join(runDirectory, "attachments")
736
- : join(runDirectory, "attachments", summonsKey);
737
- ensureRealDirectoryTree(ledgerHome, attachmentsDirectory);
738
- const attachments = [];
739
- for (let index = 0; index < prepared.length; index += 1) {
740
- attachments.push(await freezePreparedAttachment(prepared[index], attachmentsDirectory, index));
741
- }
742
- return attachments;
743
- }
744
647
  function ticketAdmissionFields(ticketNumber) {
745
648
  if (ticketNumber === undefined)
746
649
  return {};
@@ -811,16 +714,13 @@ export async function admitPublicRole(role, parsed, env, override) {
811
714
  if (!record.phases.some((item) => item === phase)) {
812
715
  throw new CliUsageError("coder phase must be plan or apply");
813
716
  }
717
+ // #1168: dispatch text is the first message only — no task.md copy.
814
718
  return admitStandardMaterialInvocation("coder", {
815
719
  ...shared,
816
720
  instruction,
817
721
  attachmentPaths,
818
722
  ...project,
819
- placedFields: async (placed) => {
820
- const taskPath = join(placed.runDirectory, "task.md");
821
- await writeFile(taskPath, instruction, "utf8");
822
- return { phase, taskPath };
823
- },
723
+ placedFields: async () => ({ phase }),
824
724
  });
825
725
  }
826
726
  case "worker-packet": {
@@ -831,48 +731,18 @@ export async function admitPublicRole(role, parsed, env, override) {
831
731
  if (!record.phases.some((item) => item === phase)) {
832
732
  throw new CliUsageError("fixer phase must be plan or apply");
833
733
  }
834
- let prerequisites = Object.freeze([]);
835
- let prerequisitesSource;
836
- if (parsed.prerequisitesPath !== undefined) {
837
- const absolutePrereq = isAbsolute(parsed.prerequisitesPath)
838
- ? parsed.prerequisitesPath
839
- : resolve(parsed.prerequisitesPath);
840
- try {
841
- prerequisitesSource = await readFile(absolutePrereq, "utf8");
842
- }
843
- catch (error) {
844
- throw new CliUsageError(`fixer prerequisites path is unreadable: ${parsed.prerequisitesPath}`, { cause: error });
845
- }
846
- try {
847
- prerequisites = parseFixerPrerequisites(prerequisitesSource);
848
- }
849
- catch (error) {
850
- if (error instanceof FixerPacketValidationError) {
851
- throw new CliUsageError(error.message, { cause: error });
852
- }
853
- throw error;
854
- }
855
- }
734
+ // #1168: dispatch once as first message; --prerequisites path as-is.
856
735
  return admitStandardMaterialInvocation("fixer", {
857
736
  ...shared,
858
737
  instruction,
859
738
  attachmentPaths,
860
739
  ...project,
861
- placedFields: async (placed) => {
862
- let prerequisitesPath;
863
- if (prerequisitesSource !== undefined) {
864
- prerequisitesPath = join(placed.runDirectory, "prerequisites.json");
865
- await writeFile(prerequisitesPath, `${JSON.stringify(prerequisites, null, 2)}\n`, "utf8");
866
- }
867
- const packetPath = join(placed.runDirectory, "fix-packet.md");
868
- await writeFile(packetPath, instruction, "utf8");
869
- return {
870
- phase,
871
- packetPath,
872
- prerequisites,
873
- ...(prerequisitesPath === undefined ? {} : { prerequisitesPath }),
874
- };
875
- },
740
+ placedFields: async () => ({
741
+ phase,
742
+ ...(parsed.prerequisitesPath === undefined
743
+ ? {}
744
+ : { prerequisitesPath: parsed.prerequisitesPath }),
745
+ }),
876
746
  });
877
747
  }
878
748
  case "collect-target": {
@@ -891,36 +761,21 @@ export async function admitPublicRole(role, parsed, env, override) {
891
761
  else {
892
762
  repository = resolveGitHubRemoteRepository(projectRoot);
893
763
  }
894
- let manifest = emptyCollectorManifest();
895
- let manifestCanonicalJson;
896
- if (parsed.requestManifestPath !== undefined) {
897
- try {
898
- manifest = await loadCollectorManifest(parsed.requestManifestPath);
899
- manifestCanonicalJson = manifest.canonicalJson;
900
- }
901
- catch (error) {
902
- throw new CliUsageError(errorText(error), { cause: error });
903
- }
904
- }
905
- const manifestDigest = manifest.digest;
764
+ // #1165: pass caller path as-is; package does not read, validate, or rewrite.
906
765
  const admittedCollector = await admitStandardMaterialInvocation("collector", {
907
766
  ...shared,
908
767
  instruction,
909
768
  attachmentPaths,
910
769
  ...project,
911
- placedFields: async (placed) => {
770
+ placedFields: async () => {
912
771
  // #1088: explicit --pr only. Unbound PR is located by the LLM via host CLI.
913
- let requestManifestPath;
914
- if (manifestCanonicalJson !== undefined) {
915
- requestManifestPath = join(placed.runDirectory, "request-manifest.json");
916
- await writeFile(requestManifestPath, manifestCanonicalJson, "utf8");
917
- }
918
772
  return {
919
773
  ...(explicitPrNumber === undefined ? {} : { prNumber: explicitPrNumber }),
920
774
  repository: repository.canonical,
921
775
  repositoryDisplay: repository.display,
922
- ...(requestManifestPath === undefined ? {} : { requestManifestPath }),
923
- manifestDigest,
776
+ ...(parsed.requestManifestPath === undefined
777
+ ? {}
778
+ : { requestManifestPath: parsed.requestManifestPath }),
924
779
  };
925
780
  },
926
781
  });
@@ -931,12 +786,10 @@ export async function admitPublicRole(role, parsed, env, override) {
931
786
  throw new CliUsageError(`doctor --issue must be a positive integer, got ${Number.NaN}`);
932
787
  }
933
788
  const issueNumber = parsed.issueNumber;
934
- let frozenAttachments = [];
935
- const admittedDoctor = await admitStandardMaterialInvocation("doctor", {
789
+ return admitStandardMaterialInvocation("doctor", {
936
790
  ...shared,
937
791
  instruction,
938
- attachmentPaths: [],
939
- freezeAttachments: false,
792
+ attachmentPaths,
940
793
  ...project,
941
794
  placedFields: async (placed) => {
942
795
  let caseRunsPath;
@@ -973,16 +826,13 @@ export async function admitPublicRole(role, parsed, env, override) {
973
826
  const detail = errorText(error);
974
827
  throw new CliUsageError(`doctor case could not be constructed from retained evidence: ${detail}`, { cause: error });
975
828
  }
976
- frozenAttachments = await freezeAttachments(placed.ledgerHome, attachmentPaths, placed.attachmentsDirectory);
977
829
  return {
978
830
  issueNumber,
979
831
  caseRunsPath,
980
832
  caseIdentity,
981
- attachments: persistedAttachmentRefs(frozenAttachments),
982
833
  };
983
834
  },
984
835
  });
985
- return { ...admittedDoctor, attachments: frozenAttachments };
986
836
  }
987
837
  case "source-locator": {
988
838
  const projectRoot = resolve(parsed.project ?? shared.cwd);
@@ -1013,7 +863,7 @@ export async function admitPublicRole(role, parsed, env, override) {
1013
863
  instruction: "",
1014
864
  attachmentPaths: [],
1015
865
  ...project,
1016
- freezeAttachments: false,
866
+ recordAttachments: false,
1017
867
  admittedFields: {
1018
868
  sourceRunPath: sourceRun.runDirectory,
1019
869
  sourceRun,
@@ -1030,7 +880,7 @@ export async function admitPublicRole(role, parsed, env, override) {
1030
880
  instruction,
1031
881
  attachmentPaths: [],
1032
882
  ...project,
1033
- freezeAttachments: false,
883
+ recordAttachments: false,
1034
884
  admittedFields: { baseRevision },
1035
885
  });
1036
886
  }
@@ -1075,13 +925,12 @@ export async function admitPublicRole(role, parsed, env, override) {
1075
925
  placedFields: async (placed) => {
1076
926
  const targetLabel = derived.targetObjectId === "" ? "(none observed)" : derived.targetObjectId;
1077
927
  const sourceLabel = derived.sourceObjectId === "" ? "(none observed)" : derived.sourceObjectId;
928
+ // #1168: git facts stay; do not encode dispatch text as task/authority copies.
1078
929
  const mergerInput = validateMergerInput({
1079
930
  attemptId: placed.runId,
1080
931
  targetObjectId: derived.targetObjectId,
1081
932
  sourceObjectId: derived.sourceObjectId,
1082
933
  materials: {
1083
- task: mergerMaterialFromUtf8(instruction),
1084
- authority: mergerMaterialFromUtf8(instruction),
1085
934
  targetIntent: mergerMaterialFromUtf8(`Investigate primary sources for target parent ${targetLabel}. Do not invent intent.`),
1086
935
  sourceIntent: mergerMaterialFromUtf8(`Investigate primary sources for source parent ${sourceLabel}. Do not invent intent.`),
1087
936
  },
@@ -1128,9 +977,9 @@ async function placeRoleAdmission(options) {
1128
977
  home: options.home,
1129
978
  ...(options.materialize === undefined ? {} : { materialize: options.materialize }),
1130
979
  });
1131
- const attachments = options.freezeAttachments === false
980
+ const attachments = options.recordAttachments === false
1132
981
  ? []
1133
- : await freezeAttachments(ledgerHome, options.attachmentPaths, attachmentsDirectory);
982
+ : admitCallerAttachments(options.attachmentPaths);
1134
983
  return {
1135
984
  runId,
1136
985
  bookKey,
@@ -1146,13 +995,7 @@ async function placeRoleAdmission(options) {
1146
995
  };
1147
996
  }
1148
997
  function persistedAttachmentRefs(attachments) {
1149
- return attachments.map((attachment) => ({
1150
- provenancePath: attachment.provenancePath,
1151
- frozenPath: attachment.frozenPath,
1152
- byteLength: attachment.byteLength,
1153
- sha256: attachment.sha256,
1154
- mediaKind: attachment.mediaKind,
1155
- }));
998
+ return attachments.map((attachment) => ({ path: attachment.path }));
1156
999
  }
1157
1000
  async function persistPlacedAdmission(admitted, placed, model) {
1158
1001
  writeAdmittedRequestPersistence(placed.runDirectory, admitted, {
@@ -1172,19 +1015,18 @@ async function persistPlacedAdmission(admitted, placed, model) {
1172
1015
  * before materializeCountersignInvocation writes the page.
1173
1016
  * Seats whose extra facts are known before placement pass them as admittedFields.
1174
1017
  * Seats whose extra facts depend on the placed run pass placedFields
1175
- * (coder writes task.md; fixer writes fix-packet.md; collector writes request-manifest.json;
1176
- * doctor resolves the case, then freezes attachments; merger writes merger-input.json).
1018
+ * (merger writes merger-input.json with git facts; coder/fixer dispatch is first-message only, #1168).
1177
1019
  */
1178
1020
  async function admitStandardMaterialInvocation(role, options) {
1179
1021
  const defer = options.deferPersistence === true;
1180
- const skipFreeze = defer || options.freezeAttachments === false;
1022
+ const skipAttachments = defer || options.recordAttachments === false;
1181
1023
  const placed = await placeRoleAdmission({
1182
1024
  role,
1183
1025
  home: options.home,
1184
1026
  principalAuthority: options.principalAuthority,
1185
1027
  cwd: options.cwd,
1186
1028
  attachmentPaths: options.attachmentPaths,
1187
- ...(skipFreeze ? { freezeAttachments: false } : {}),
1029
+ ...(skipAttachments ? { recordAttachments: false } : {}),
1188
1030
  ...(defer ? { materialize: false } : {}),
1189
1031
  ...(options.project === undefined ? {} : { project: options.project }),
1190
1032
  ...(options.createRunId === undefined ? {} : { createRunId: options.createRunId }),
@@ -1196,7 +1038,8 @@ async function admitStandardMaterialInvocation(role, options) {
1196
1038
  ? {}
1197
1039
  : { correlationId: options.correlationId };
1198
1040
  const instruction = options.instruction;
1199
- const instructionEmpty = instruction.trim() === "";
1041
+ // Admitted emptiness is literal absence, not whitespace-only (#1165 J3).
1042
+ const instructionEmpty = instruction.length === 0;
1200
1043
  const placedExtra = options.placedFields === undefined ? undefined : await options.placedFields(placed);
1201
1044
  const admittedFields = {
1202
1045
  ...(options.admittedFields ?? {}),
@@ -1244,7 +1087,7 @@ function admittedTransportPromptKind(admitted) {
1244
1087
  /**
1245
1088
  * One initial prompt transport. The registry `transportPrompt` leaf selects
1246
1089
  * a fixed kickoff, a bound baseline, or frozen skill args. Absent means the
1247
- * caller instruction plus frozen attachment paths.
1090
+ * caller instruction plus caller attachment paths as-is (#1165).
1248
1091
  * Engine / outsourcing material rides startup readingMaterial (#1167), not here.
1249
1092
  */
1250
1093
  export function buildInstructionTransportPrompt(admitted) {
@@ -1260,7 +1103,9 @@ export function buildInstructionTransportPrompt(admitted) {
1260
1103
  throw new Error("baseline transport prompt is missing the bound revision");
1261
1104
  }
1262
1105
  const lines = [admitted.baseRevision];
1263
- if (!admitted.instructionEmpty) {
1106
+ // Use raw admitted.instruction — do not gate on instructionEmpty metadata
1107
+ // (old pages may set that flag wrong while still holding whitespace text).
1108
+ if (admitted.instruction.length > 0) {
1264
1109
  lines.push("", admitted.instruction);
1265
1110
  }
1266
1111
  return lines.join("\n");
@@ -1276,17 +1121,28 @@ export function buildInstructionTransportPrompt(admitted) {
1276
1121
  lens: admitted.lens,
1277
1122
  authorityRefs: admitted.authorityRefs,
1278
1123
  })];
1279
- if (!admitted.instructionEmpty && admitted.instruction.trim() !== "") {
1124
+ if (admitted.instruction.length > 0) {
1280
1125
  lines.push("", admitted.instruction);
1281
1126
  }
1282
1127
  return lines.join("\n");
1283
1128
  }
1284
- const lines = [admitted.instructionEmpty ? "" : admitted.instruction];
1285
- if (admitted.attachments.length > 0) {
1129
+ // #1165 J3 / #1168: assemble from the admitted instruction bytes themselves.
1130
+ const lines = [admitted.instruction];
1131
+ // #1164/#1165/#1168: each caller path keeps its file-flag provenance.
1132
+ const flaggedPaths = [
1133
+ ...admitted.attachments.map((attachment) => `--attach ${attachment.path}`),
1134
+ ...(admitted.requestManifestPath === undefined
1135
+ ? []
1136
+ : [`--request-manifest ${admitted.requestManifestPath}`]),
1137
+ ...(admitted.prerequisitesPath === undefined
1138
+ ? []
1139
+ : [`--prerequisites ${admitted.prerequisitesPath}`]),
1140
+ ];
1141
+ if (flaggedPaths.length > 0) {
1286
1142
  lines.push("");
1287
- lines.push("已受理附件(冻结快照路径):");
1288
- for (const attachment of admitted.attachments) {
1289
- lines.push(`- ${attachment.frozenPath}`);
1143
+ lines.push("已受理附件:");
1144
+ for (const entry of flaggedPaths) {
1145
+ lines.push(`- ${entry}`);
1290
1146
  }
1291
1147
  }
1292
1148
  return lines.join("\n");
@@ -1298,8 +1154,7 @@ export async function materializeCountersignInvocation(admitted, options) {
1298
1154
  home: options.home,
1299
1155
  principalAuthority: options.principalAuthority,
1300
1156
  cwd: admitted.projectRoot,
1301
- attachmentPaths: [],
1302
- freezeAttachments: false,
1157
+ attachmentPaths: options.attachmentPaths,
1303
1158
  runId: admitted.runId,
1304
1159
  ...(options.ticketNumber === undefined ? {} : { assertedTicketNumber: options.ticketNumber }),
1305
1160
  });
@@ -1310,8 +1165,7 @@ export async function materializeCountersignInvocation(admitted, options) {
1310
1165
  if (ticketFields.ticketNumber !== undefined) {
1311
1166
  admitted.ticketNumber = ticketFields.ticketNumber;
1312
1167
  }
1313
- const attachments = await freezePreparedAttachmentsIntoRun(options.preparedAttachments, admitted.runDirectory);
1314
- admitted.attachments = attachments;
1168
+ admitted.attachments = placement.attachments;
1315
1169
  writeAdmittedRequestPersistence(admitted.runDirectory, admitted, {
1316
1170
  sessionDirectory: placement.sessionDirectory,
1317
1171
  sessionFile: placement.sessionFile,
@@ -1332,10 +1186,13 @@ export async function loadAdmittedJudgeRequest(runDirectory) {
1332
1186
  return undefined;
1333
1187
  if (!Array.isArray(record.attachments))
1334
1188
  return undefined;
1189
+ const attachments = record.attachments
1190
+ .map((item) => normalizeAdmittedAttachment(item))
1191
+ .filter((item) => item !== undefined);
1335
1192
  return {
1336
1193
  instruction: record.instruction,
1337
1194
  instructionEmpty: record.instructionEmpty,
1338
- attachments: record.attachments,
1195
+ attachments,
1339
1196
  };
1340
1197
  }
1341
1198
  catch {
@@ -1597,7 +1454,7 @@ export function parseAnalystArgv(args) {
1597
1454
  return;
1598
1455
  }
1599
1456
  if (taken.def.id === "attach") {
1600
- pushValue("attach", requireOptionPath(taken.def.canonical, taken.value));
1457
+ pushValue("attach", requireProvidedOptionValue(taken.def.canonical, taken.value));
1601
1458
  return;
1602
1459
  }
1603
1460
  if (taken.def.id === "group-a-label" || taken.def.id === "group-b-label") {