@akagilnc/pi-workflow-roles 0.1.5478 → 0.1.5504

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 (79) hide show
  1. package/dist/acp-host/production-host.js +4945 -4980
  2. package/dist/analyst-gate-cycles-read.js +11 -29
  3. package/dist/analyst-ledger.js +10 -12
  4. package/dist/archivist-record-entry.js +4 -3
  5. package/dist/archivist-record-pointer.js +59 -32
  6. package/dist/atomic-write.js +2 -2
  7. package/dist/auditor-dossier-tool.js +8 -4
  8. package/dist/auditor-soul.js +2 -1
  9. package/dist/collector-config.js +6 -5
  10. package/dist/doctor-evidence.js +5 -1
  11. package/dist/engine-detour-usage.js +4 -10
  12. package/dist/headless-host/description.js +251 -0
  13. package/dist/headless-host/production-host.js +2787 -2658
  14. package/dist/ledger-session-read.js +5 -0
  15. package/dist/migrate-book-topology.js +129 -100
  16. package/dist/navigator-public-session.js +13 -17
  17. package/dist/notary-source-run.js +7 -15
  18. package/dist/packaged-role-registry.js +0 -12
  19. package/dist/pi/durable-principal.js +2 -3
  20. package/dist/pi/role-turn-host.js +27 -13
  21. package/dist/public-cli/instruction-seat-run.js +4 -4
  22. package/dist/public-cli/invocation.js +17 -45
  23. package/dist/public-cli/main.js +1074 -1085
  24. package/dist/public-cli/run-lifecycle.js +17 -47
  25. package/dist/public-cli/settlement.js +28 -128
  26. package/dist/public-cli/terminal.js +0 -12
  27. package/dist/role-run-placement.js +52 -5
  28. package/dist/role-run-relocation.js +39 -2
  29. package/dist/run-terminal-artifacts.js +2 -6
  30. package/dist/run-ticket-number.js +33 -4
  31. package/dist/session-assistant-usage.js +9 -18
  32. package/dist/session-identity.js +3 -4
  33. package/dist/sitian-appender.js +20 -6
  34. package/dist/submission-ledger.js +4 -4
  35. package/dist/ticket-provenance-contracts.js +2 -8
  36. package/dist/ticket-provenance.js +5 -4
  37. package/package.json +1 -1
  38. package/src/acp-host/role-turn-host.ts +2 -2
  39. package/src/analyst-gate-cycles-read.ts +11 -34
  40. package/src/analyst-ledger.ts +14 -14
  41. package/src/archivist-record-entry.ts +4 -3
  42. package/src/archivist-record-pointer.ts +70 -34
  43. package/src/atomic-write.ts +3 -1
  44. package/src/auditor-dossier-tool.ts +8 -4
  45. package/src/auditor-soul.ts +2 -1
  46. package/src/collector-config.ts +5 -3
  47. package/src/doctor-evidence.ts +5 -1
  48. package/src/engine-detour-usage.ts +10 -13
  49. package/src/factory-board.ts +16 -81
  50. package/src/headless-host/description.ts +248 -1
  51. package/src/headless-host/production-host.ts +1 -2
  52. package/src/headless-host/role-turn-host.ts +10 -9
  53. package/src/ledger-session-read.ts +6 -0
  54. package/src/navigator-public-session.ts +13 -18
  55. package/src/navigator-work-context.ts +3 -1
  56. package/src/notary-role.ts +2 -1
  57. package/src/notary-source-run.ts +7 -17
  58. package/src/packaged-role-registry.ts +0 -15
  59. package/src/pi/durable-principal.ts +2 -4
  60. package/src/pi/role-turn-host.ts +35 -12
  61. package/src/public-cli/instruction-seat-run.ts +4 -4
  62. package/src/public-cli/invocation.ts +29 -57
  63. package/src/public-cli/run-lifecycle.ts +20 -43
  64. package/src/public-cli/settlement.ts +30 -173
  65. package/src/public-cli/terminal.ts +0 -60
  66. package/src/role-envelope.ts +4 -2
  67. package/src/role-run-placement.ts +63 -4
  68. package/src/role-run-relocation.ts +39 -2
  69. package/src/role-runtime.ts +40 -59
  70. package/src/run-terminal-artifacts.ts +3 -5
  71. package/src/run-ticket-number.ts +41 -3
  72. package/src/session-assistant-usage.ts +8 -20
  73. package/src/session-identity.ts +3 -4
  74. package/src/sitian-appender.ts +20 -12
  75. package/src/submission-ledger.ts +4 -4
  76. package/src/ticket-provenance-contracts.ts +3 -6
  77. package/src/ticket-provenance.ts +5 -6
  78. package/src/ticket-snapshot.ts +3 -2
  79. package/src/ticket-trajectory.ts +85 -99
@@ -8,11 +8,11 @@
8
8
  import { chmod, lstat, open, readdir, readFile, unlink, writeFile } from "node:fs/promises";
9
9
  import { basename, isAbsolute, join } from "node:path";
10
10
  import { activationBookDirectory, resolveActivationLedgerHome, } from "../activation-ledger-topology.js";
11
- import { listBookRunDirectories } from "../role-run-placement.js";
11
+ import { findRoleRunDirectory, listBookRunDirectories, parseRunLeaf, sessionFileIn } from "../role-run-placement.js";
12
12
  import { isSafePositiveTicketNumber } from "../run-ticket-number.js";
13
13
  import { CliUsageError } from "./cli-errors.js";
14
14
  import { parseCollectorRepository } from "../collector-config.js";
15
- import { rewriteAdmittedRoleRunPage, rewriteRunDirectoryPathValue, } from "../role-run-relocation.js";
15
+ import { retainedRunPathsMatch, rewriteAdmittedRoleRunPage, rewriteRunDirectoryPathValue, } from "../role-run-relocation.js";
16
16
  import { appendEngineSessionMaterial, engineSessionMaterialFromOptions, pickEngineAxis, } from "../package-resources/engine-material.js";
17
17
  import { packagedResumeSourcePath, packagedRoleMetadata, } from "../packaged-role-registry.js";
18
18
  import { admittedReviewerBaseFault, recordEffectiveInvocationModel, requireAuthorityRef, isReviewerLens, } from "./invocation.js";
@@ -645,33 +645,9 @@ export async function findRunDirectoryById(home, runId, onlyBookKey, onlyRole) {
645
645
  return undefined;
646
646
  throw error;
647
647
  }
648
- const matches = [];
649
- for (const bookKey of bookKeys) {
650
- if (onlyBookKey !== undefined && bookKey !== onlyBookKey)
651
- continue;
652
- const bookDir = activationBookDirectory(ledgerHome, bookKey);
653
- let runDirectories;
654
- try {
655
- runDirectories = await listBookRunDirectories(bookDir);
656
- }
657
- catch (error) {
658
- if (errorCodeOf(error) === "ENOENT")
659
- continue;
660
- throw error;
661
- }
662
- for (const runDirectory of runDirectories) {
663
- const entry = basename(runDirectory);
664
- if ((onlyRole === undefined && (entry === `${runId}@judge` || entry.startsWith(`${runId}@`))) ||
665
- entry === `${runId}@${onlyRole}`) {
666
- matches.push(runDirectory);
667
- }
668
- }
669
- }
670
- if (matches.length === 0)
671
- return undefined;
672
- if (matches.length === 1)
673
- return matches[0];
674
- throw new Error(`ambiguous role run id ${runId}: ${matches.join(", ")}`);
648
+ return findRoleRunDirectory(bookKeys
649
+ .filter((bookKey) => onlyBookKey === undefined || bookKey === onlyBookKey)
650
+ .map((bookKey) => activationBookDirectory(ledgerHome, bookKey)), runId, onlyRole);
675
651
  }
676
652
  /** Code-owned gate inspector summons prefix (public-role-summons / #747). */
677
653
  export const GATE_DOSSIER_POINTER_PREFIX = "卷宗指针:";
@@ -723,7 +699,7 @@ async function runHasFormedSessionPrincipal(runDirectory) {
723
699
  const sessionFile = typeof disk.principalWire.sessionFile === "string" &&
724
700
  disk.principalWire.sessionFile.trim() !== ""
725
701
  ? disk.principalWire.sessionFile
726
- : join(disk.principalWire.sessionDirectory, "session.jsonl");
702
+ : sessionFileIn(disk.principalWire.sessionDirectory);
727
703
  try {
728
704
  const stat = await lstat(sessionFile);
729
705
  return stat.isFile() && !stat.isSymbolicLink();
@@ -759,19 +735,15 @@ export async function findLatestRunIdForSeatTicket(input) {
759
735
  return undefined;
760
736
  throw error;
761
737
  }
762
- const suffix = `@${input.role}`;
763
738
  let best;
764
739
  for (const runDirectory of runDirectories) {
765
- const entry = basename(runDirectory);
766
- if (!entry.endsWith(suffix))
767
- continue;
768
- const runId = entry.slice(0, entry.length - suffix.length);
769
- if (runId.length === 0)
740
+ const parsed = parseRunLeaf(basename(runDirectory));
741
+ if (parsed === undefined || parsed.role !== input.role)
770
742
  continue;
743
+ const runId = parsed.runId;
771
744
  const parentPath = await readRunParentPath(runDirectory);
772
- // The parent's ticket can change after this child was admitted. Within one
773
- // book the run leaf is its stable identity; directory placement is not.
774
- if (parentPath !== input.parentRunPath && basename(parentPath ?? "") !== basename(input.parentRunPath))
745
+ // Follow the parent's retained placement without collapsing distinct book/subject bindings.
746
+ if (!await retainedRunPathsMatch(parentPath, input.parentRunPath, input.parentRunPath))
775
747
  continue;
776
748
  // Durable fact: never resume-select a provisional that never formed principal.
777
749
  if (!(await runHasFormedSessionPrincipal(runDirectory)))
@@ -888,7 +860,7 @@ async function loadResumableRunRecord(home, runId, authority) {
888
860
  lens = record.lens;
889
861
  }
890
862
  // Collector — admitted repository/PR identity (#633 resume).
891
- if (typeof record.prNumber === "number" && Number.isSafeInteger(record.prNumber) && record.prNumber >= 1) {
863
+ if (isSafePositiveTicketNumber(record.prNumber)) {
892
864
  prNumber = record.prNumber;
893
865
  }
894
866
  if (typeof record.repository === "string" && record.repository.trim() !== "") {
@@ -907,7 +879,7 @@ async function loadResumableRunRecord(home, runId, authority) {
907
879
  waitWindowMs = record.waitWindowMs;
908
880
  }
909
881
  // Doctor — admitted single-case identity (#633 resume).
910
- if (typeof record.issueNumber === "number" && Number.isSafeInteger(record.issueNumber) && record.issueNumber >= 1) {
882
+ if (isSafePositiveTicketNumber(record.issueNumber)) {
911
883
  issueNumber = record.issueNumber;
912
884
  }
913
885
  if (typeof record.caseRunsPath === "string" && record.caseRunsPath.trim() !== "") {
@@ -915,9 +887,7 @@ async function loadResumableRunRecord(home, runId, authority) {
915
887
  }
916
888
  if (isRecord(record.caseIdentity)) {
917
889
  const ci = record.caseIdentity;
918
- if (typeof ci.issueNumber === "number" &&
919
- Number.isSafeInteger(ci.issueNumber) &&
920
- ci.issueNumber >= 1 &&
890
+ if (isSafePositiveTicketNumber(ci.issueNumber) &&
921
891
  typeof ci.runsPath === "string" &&
922
892
  ci.runsPath.trim() !== "") {
923
893
  caseIdentity = { issueNumber: ci.issueNumber, runsPath: ci.runsPath };
@@ -1013,9 +983,9 @@ async function loadResumableRunRecord(home, runId, authority) {
1013
983
  throw new CliUsageError(`role run invocation identity is unreadable: ${runId}`, { cause: error });
1014
984
  }
1015
985
  }
1016
- const sourceRunLeaf = basename(sourceRunPath ?? "").split("@");
1017
- const referencedRunId = sourceRun?.runId ?? sourceRunLeaf[0];
1018
- const referencedRole = sourceRun?.role ?? sourceRunLeaf[1];
986
+ const sourceRunLeaf = parseRunLeaf(basename(sourceRunPath ?? ""));
987
+ const referencedRunId = sourceRun?.runId ?? sourceRunLeaf?.runId;
988
+ const referencedRole = sourceRun?.role ?? sourceRunLeaf?.role;
1019
989
  if (referencedRunId !== undefined && referencedRunId !== "") {
1020
990
  const currentSourceRunDirectory = await findRunDirectoryById(home, referencedRunId, run.bookKey, referencedRole);
1021
991
  if (currentSourceRunDirectory !== undefined) {
@@ -6,12 +6,13 @@
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { dirname, join } from "node:path";
8
8
  import { lstat, mkdir, readFile, rm, writeFile } from "node:fs/promises";
9
+ import { sessionFileOf } from "../role-run-placement.js";
9
10
  import { writeHardenedArtifactFile } from "./auto-resume.js";
10
- import { readAnalystGateCyclesFromAuditorRoles, } from "../analyst-gate-cycles-read.js";
11
11
  import { sitianReport } from "../sitian-facade.js";
12
12
  import { readAttemptScopedSubmissionRows, readRecordedSubmissionRows, readRecordedSubmissions, } from "../submission-ledger.js";
13
13
  import { CONTROLLED_FAILURE_CAUSES } from "../host-contracts.js";
14
14
  import { childSignalDeathDiagnostic, processCancelDiagnostic, } from "./process-cancel.js";
15
+ import { readStrictPiSessionJsonl } from "../ledger-session-read.js";
15
16
  import { serializeThrownValue } from "../serialize-thrown-value.js";
16
17
  import { describeErrorIdentity } from "./run-lifecycle.js";
17
18
  import { readEngineDetourToolUsage, readInvocationEngineMounted, runDirectoryFromSessionDirectory, sessionFileFromSessionDirectory, withEngineDetourToolUsageFact, } from "../engine-detour-usage.js";
@@ -19,7 +20,7 @@ import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "../dossier-resolution.js";
19
20
  import { ensureRunArtifactsDir, homeFromRunDirectory, } from "./invocation.js";
20
21
  import { SECRETARIAT_COUNTERSIGN_TERMINAL_FACT_KEY, SECRETARIAT_GATE_OFFICER_ENTRY_TYPE, } from "../secretariat-contracts.js";
21
22
  import { findLatestDurablePackagedRoleTerminal, NAVIGATOR_ROUTE_PLAYBOOK_FAILURE_ENTRY, } from "../navigator-invocation-identity.js";
22
- import { packagedDurableOfficerEntry, packagedRoleAcceptedOutputTool, packagedRoleMetadata, packagedSkipsGateOnInfrastructureStage, } from "../packaged-role-registry.js";
23
+ import { packagedDurableOfficerEntry, packagedRoleAcceptedOutputTool, packagedRoleMetadata, } from "../packaged-role-registry.js";
23
24
  import { NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, noReceiptLifecycleFacts, parseNoReceiptLifecycleFacts, priorReceiptContinuation, receiptAttemptPointer, } from "../receipt-delivery-policy.js";
24
25
  import { roleRunArtifactsDirectory } from "../role-run-placement.js";
25
26
  import { listSeamOwnedUniqueErrorFacePaths, uniqueErrorFallbackName, UNIQUE_ERROR_FALLBACK_STEM, RUN_TERMINAL_ARTIFACT_FILES, RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS, RUN_TERMINAL_REPORT_FILE, RUN_TERMINAL_ERROR_FILE, RUN_TERMINAL_EVIDENCE_FILE, RUN_TERMINAL_ERROR_SETTLEMENT_FILE, } from "../run-terminal-artifacts.js";
@@ -123,7 +124,7 @@ export async function noteSettlementFault(runDirectory, scope, diagnostic) {
123
124
  export function ledgerReadScope(admitted, scope) {
124
125
  return {
125
126
  home: sealedLedgerHome(admitted),
126
- sessionParent: join(admitted.runDirectory, "session", "session.jsonl"),
127
+ sessionParent: sessionFileOf(admitted.runDirectory),
127
128
  ...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
128
129
  ? {}
129
130
  : { attemptId: scope.courtAttemptId }),
@@ -297,12 +298,12 @@ async function settleNoReceiptTerminal(admitted, authority, scope, facts) {
297
298
  }
298
299
  if (scope?.previewOnly !== true)
299
300
  await clearOppositeTerminalArtifactFace(admitted.runDirectory);
300
- return withOptionalGateProjection({
301
+ return attachEngineDetourToolUsage({
301
302
  roleOutcome,
302
303
  navigator: await extractNavigatorFactFromAdmittedSession(coordinates.sessionFile, admitted.runDirectory, scope),
303
304
  artifacts: [],
304
305
  runId: admitted.runId,
305
- }, coordinates.sessionDirectory, detourGateContext(admitted, scope));
306
+ }, coordinates.sessionDirectory, detourUsageContext(admitted, scope));
306
307
  }
307
308
  /** Transitional host-session reads remain only for non-sealed failure and audit evidence. */
308
309
  function coordinatesFromAdmitted(authority, admitted) {
@@ -638,12 +639,7 @@ export const NAVIGATOR_POST_ROLE_GRACE_MS = 10_000;
638
639
  * terminal note it; they do not relabel it as a host session cause.
639
640
  */
640
641
  export async function readBoundSessionEntries(sessionFile) {
641
- const text = await readFile(sessionFile, "utf8");
642
- const entries = [];
643
- for (const line of text.trim().split("\n").filter(Boolean)) {
644
- entries.push(JSON.parse(line));
645
- }
646
- return entries;
642
+ return await readStrictPiSessionJsonl(sessionFile);
647
643
  }
648
644
  /**
649
645
  * #419 per-attempt process history. 史必追加,指针可覆盖;指针可以覆盖的前提是史已落。
@@ -737,61 +733,6 @@ function parseNavigatorAttendanceDetails(details) {
737
733
  reason: "Navigator attendance disposition is unparseable",
738
734
  };
739
735
  }
740
- /**
741
- * Project direct and historical paired gate rounds onto the public Terminal.
742
- * actualSeats derive only from accepted receipts, never expected/missing seats.
743
- */
744
- export function projectTerminalGateFact(rounds) {
745
- if (rounds.length === 0)
746
- return undefined;
747
- const seen = new Set();
748
- for (const round of rounds) {
749
- if (round.origin.kind === "historical_dispatch")
750
- seen.add("gatekeeper");
751
- seen.add(round.officer);
752
- }
753
- const actualSeats = ["gatekeeper", "inspector", "notary"].filter((seat) => seen.has(seat));
754
- return {
755
- actualSeats,
756
- rounds: rounds.map((round) => ({
757
- roundIndex: round.roundIndex,
758
- dispatch: round.origin.kind === "direct"
759
- ? { kind: "direct", officer: round.officer }
760
- : {
761
- kind: "historical_dispatch",
762
- officer: round.officer,
763
- ...(round.origin.reason === undefined
764
- ? {}
765
- : { reason: round.origin.reason }),
766
- },
767
- officer: {
768
- seat: round.officer,
769
- status: round.status,
770
- findings: round.findings,
771
- },
772
- })),
773
- };
774
- }
775
- /**
776
- * Read gate facts from the run's session/auditor-roles nest (#446/#478).
777
- * Missing directories → undefined (no-gate zero change).
778
- * Damaged discovered volumes propagate — never wash to "no gate".
779
- */
780
- export async function extractGateFactFromSessionDirectory(sessionDirectory, options = {}) {
781
- const directories = [join(sessionDirectory, "auditor-roles")];
782
- const parentSessionFile = options.parentSessionFile ?? join(sessionDirectory, "session.jsonl");
783
- const rounds = await readAnalystGateCyclesFromAuditorRoles(directories, {
784
- parentSessionFile,
785
- });
786
- return projectTerminalGateFact(rounds);
787
- }
788
- /**
789
- * Attach optional gate projection onto a settled Terminal base.
790
- * Shared by every settle path so auditor-roles is scanned once here only.
791
- * `runId` is optional only for a batch projection that has no parent run.
792
- * Gate read damage is a package note beside the host terminal. It does not
793
- * replace that terminal and it does not omit the gate without a trace.
794
- */
795
736
  /**
796
737
  * #537: project this-invocation ak_engine_detour usage onto decisiveFacts.
797
738
  * Absent when engine is not mounted; callCount 0 when mounted with zero calls.
@@ -800,27 +741,27 @@ export async function extractGateFactFromSessionDirectory(sessionDirectory, opti
800
741
  * courtAttemptId and never Pi session.jsonl toolResult join. Session damage
801
742
  * therefore cannot replace an already-formed roleOutcome (no_receipt / failure).
802
743
  */
803
- async function attachEngineDetourToolUsage(base, sessionDirectory, gateContext = {}) {
804
- const runDirectory = typeof gateContext.runDirectory === "string" && gateContext.runDirectory.length > 0
805
- ? gateContext.runDirectory
744
+ async function attachEngineDetourToolUsage(base, sessionDirectory, detourContext = {}) {
745
+ const runDirectory = typeof detourContext.runDirectory === "string" && detourContext.runDirectory.length > 0
746
+ ? detourContext.runDirectory
806
747
  : runDirectoryFromSessionDirectory(sessionDirectory);
807
748
  let engineMounted = false;
808
749
  try {
809
750
  engineMounted = await readInvocationEngineMounted(runDirectory);
810
751
  }
811
752
  catch (error) {
812
- await noteSettlementFault(runDirectory, gateContext.notePackageFault === undefined
753
+ await noteSettlementFault(runDirectory, detourContext.notePackageFault === undefined
813
754
  ? undefined
814
- : { notePackageFault: gateContext.notePackageFault }, `engine mount read failed beside host terminal: ${describeErrorIdentity(error)}`);
755
+ : { notePackageFault: detourContext.notePackageFault }, `engine mount read failed beside host terminal: ${describeErrorIdentity(error)}`);
815
756
  return base;
816
757
  }
817
758
  if (!engineMounted)
818
759
  return base;
819
760
  // Public-invocation scope from the shared Host envelope (settlement scope), never
820
761
  // courtAttemptId and never a detour sidecar file.
821
- const invocationScopeId = typeof gateContext.invocationScopeId === "string" &&
822
- gateContext.invocationScopeId.length > 0
823
- ? gateContext.invocationScopeId
762
+ const invocationScopeId = typeof detourContext.invocationScopeId === "string" &&
763
+ detourContext.invocationScopeId.length > 0
764
+ ? detourContext.invocationScopeId
824
765
  : undefined;
825
766
  const sessionFile = sessionFileFromSessionDirectory(sessionDirectory);
826
767
  let usage;
@@ -833,9 +774,9 @@ async function attachEngineDetourToolUsage(base, sessionDirectory, gateContext =
833
774
  });
834
775
  }
835
776
  catch (error) {
836
- await noteSettlementFault(runDirectory, gateContext.notePackageFault === undefined
777
+ await noteSettlementFault(runDirectory, detourContext.notePackageFault === undefined
837
778
  ? undefined
838
- : { notePackageFault: gateContext.notePackageFault }, `engine detour read failed beside host terminal: ${describeErrorIdentity(error)}`);
779
+ : { notePackageFault: detourContext.notePackageFault }, `engine detour read failed beside host terminal: ${describeErrorIdentity(error)}`);
839
780
  return base;
840
781
  }
841
782
  return {
@@ -843,45 +784,16 @@ async function attachEngineDetourToolUsage(base, sessionDirectory, gateContext =
843
784
  roleOutcome: withEngineDetourToolUsageFact(base.roleOutcome, usage),
844
785
  };
845
786
  }
846
- /** Gate + detour projection context from admitted run + settlement scope. */
847
- function detourGateContext(admitted, scope) {
787
+ /** Engine-detour usage context from admitted run + settlement scope. */
788
+ function detourUsageContext(admitted, scope) {
848
789
  return {
849
790
  runDirectory: admitted.runDirectory,
850
- ...(scope?.courtAttemptId === undefined || scope.courtAttemptId.length === 0
851
- ? {}
852
- : { courtAttemptId: scope.courtAttemptId }),
853
791
  ...(scope?.invocationScopeId === undefined || scope.invocationScopeId.length === 0
854
792
  ? {}
855
793
  : { invocationScopeId: scope.invocationScopeId }),
856
794
  ...(scope?.notePackageFault === undefined ? {} : { notePackageFault: scope.notePackageFault }),
857
795
  };
858
796
  }
859
- async function withOptionalGateProjection(base, sessionDirectory, gateContext = {}) {
860
- // A gate transport failure is already represented by typed evidence and has no
861
- // accepted gate cycle to project. Re-reading that rejected receipt as an
862
- // accepted cycle would replace the original failure with a projection error.
863
- const secondaryEvidence = base.roleOutcome.kind === "failure"
864
- ? base.roleOutcome.decisiveFacts.secondaryEvidence
865
- : undefined;
866
- const skipGate = isRecord(secondaryEvidence)
867
- && secondaryEvidence.kind === "role_infrastructure_failure"
868
- && packagedSkipsGateOnInfrastructureStage(secondaryEvidence.stage);
869
- let next = base;
870
- if (!skipGate) {
871
- try {
872
- const gate = await extractGateFactFromSessionDirectory(sessionDirectory, gateContext);
873
- if (gate !== undefined)
874
- next = { ...base, gate };
875
- }
876
- catch (error) {
877
- const runDirectory = gateContext.runDirectory ?? runDirectoryFromSessionDirectory(sessionDirectory);
878
- await noteSettlementFault(runDirectory, gateContext.notePackageFault === undefined
879
- ? undefined
880
- : { notePackageFault: gateContext.notePackageFault }, `gate read failed beside host terminal: ${describeErrorIdentity(error)}`);
881
- }
882
- }
883
- return attachEngineDetourToolUsage(next, sessionDirectory, gateContext);
884
- }
885
797
  function routePlaybookFailureMessage(entries) {
886
798
  for (let i = entries.length - 1; i >= 0; i -= 1) {
887
799
  const entry = entries[i];
@@ -1071,12 +983,12 @@ async function finishLawfulSeat(admitted, coordinates, entries, roleOutcome, sco
1071
983
  const artifacts = scope?.previewOnly === true
1072
984
  ? []
1073
985
  : await publishDeclaredSeatArtifacts(admitted, roleOutcome, coordinates, entries, scope?.recordAttemptHistory === true);
1074
- const terminal = await withOptionalGateProjection({
986
+ const terminal = await attachEngineDetourToolUsage({
1075
987
  roleOutcome,
1076
988
  navigator: extractNavigatorFact(entries),
1077
989
  artifacts,
1078
990
  runId: admitted.runId,
1079
- }, coordinates.sessionDirectory, detourGateContext(admitted, scope));
991
+ }, coordinates.sessionDirectory, detourUsageContext(admitted, scope));
1080
992
  return attachRecordedSubmissions(admitted, terminal, scope);
1081
993
  }
1082
994
  /**
@@ -1317,26 +1229,14 @@ async function applySecretariatCountersignTerminal(admitted, authority, settled,
1317
1229
  return settled;
1318
1230
  }
1319
1231
  /**
1320
- * After the audit gate returns, attach gate rounds and the secretariat officer
1321
- * fact onto the terminal this turn already settled. Does not publish again —
1322
- * a second publish would append another attempt-history row for the same attempt.
1232
+ * After the audit gate returns, attach the secretariat officer fact onto the
1233
+ * terminal this turn already settled. Does not publish again — a second publish
1234
+ * would append another attempt-history row for the same attempt.
1323
1235
  */
1324
- export async function attachPostAuditProjection(admitted, authority, terminal, scope) {
1325
- const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
1326
- let gate;
1327
- try {
1328
- gate = await extractGateFactFromSessionDirectory(sessionDirectory, {
1329
- runDirectory: admitted.runDirectory,
1330
- parentSessionFile: sessionFile,
1331
- });
1332
- }
1333
- catch (error) {
1334
- await noteSettlementFault(admitted.runDirectory, scope, `gate read failed beside host terminal: ${describeErrorIdentity(error)}`);
1335
- }
1336
- const withGate = gate === undefined ? terminal : { ...terminal, gate };
1236
+ export async function attachPostAuditCountersignFact(admitted, authority, terminal, scope) {
1337
1237
  if (admitted.role !== "secretariat")
1338
- return withGate;
1339
- return applySecretariatCountersignTerminal(admitted, authority, withGate, scope);
1238
+ return terminal;
1239
+ return applySecretariatCountersignTerminal(admitted, authority, terminal, scope);
1340
1240
  }
1341
1241
  function publicationAttemptFromError(path, error) {
1342
1242
  if (error instanceof Error) {
@@ -1538,7 +1438,7 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
1538
1438
  diagnostic: failure.diagnostic,
1539
1439
  decisiveFacts,
1540
1440
  };
1541
- return withOptionalGateProjection({ roleOutcome, navigator, artifacts, runId: admitted.runId }, sessionDirectory, detourGateContext(admitted, options));
1441
+ return attachEngineDetourToolUsage({ roleOutcome, navigator, artifacts, runId: admitted.runId }, sessionDirectory, detourUsageContext(admitted, options));
1542
1442
  }
1543
1443
  /**
1544
1444
  * Emit one complete failure Terminal on stdout and one concise stderr diagnostic.
@@ -85,18 +85,6 @@ export function formatTerminalResult(result) {
85
85
  for (const artifact of result.artifacts) {
86
86
  lines.push(`artifact\t${artifact.kind}\t${encodeTerminalField(artifact.path)}`);
87
87
  }
88
- // Gate region is unfrozen presentation of typed facts (ADR 0052) — machines
89
- // read result.gate, never these row labels.
90
- if (result.gate !== undefined) {
91
- lines.push(`gate\t${encodeTerminalField(result.gate.actualSeats.join(","))}\t${result.gate.rounds.length}`);
92
- for (const round of result.gate.rounds) {
93
- const reason = round.dispatch.kind === "historical_dispatch"
94
- && round.dispatch.reason !== undefined
95
- ? encodeTerminalField(round.dispatch.reason)
96
- : "";
97
- lines.push(`gate-round\t${round.roundIndex}\t${round.dispatch.kind}\t${round.dispatch.officer}\t${reason}\t${round.officer.seat}\t${encodeTerminalField(round.officer.status)}\t${encodeTerminalField(JSON.stringify(round.officer.findings))}`);
98
- }
99
- }
100
88
  if (result.reviewerChildOutcomes !== undefined) {
101
89
  for (const axis of ["completeness", "correctness"]) {
102
90
  const child = result.reviewerChildOutcomes[axis];
@@ -1,11 +1,35 @@
1
1
  import { readdir } from "node:fs/promises";
2
- import { join } from "node:path";
2
+ import { basename, dirname, join } from "node:path";
3
3
  import {
4
4
  activationBookDirectory,
5
5
  ensureRealDirectoryTree
6
6
  } from "./activation-ledger-topology.js";
7
7
  import { requireSafePositiveTicketNumber } from "./run-ticket-number.js";
8
8
  import { isEnoent } from "./unknown-value.js";
9
+ function formatRunLeaf(runId, role) {
10
+ return `${runId}@${role}`;
11
+ }
12
+ function parseRunLeaf(name) {
13
+ const at = name.lastIndexOf("@");
14
+ if (at <= 0 || at === name.length - 1) return void 0;
15
+ return { runId: name.slice(0, at), role: name.slice(at + 1) };
16
+ }
17
+ function sessionDirectoryOf(runDirectory) {
18
+ return join(runDirectory, "session");
19
+ }
20
+ const ROLE_RUN_SESSION_FILENAME = "session.jsonl";
21
+ function sessionFileIn(sessionDirectory) {
22
+ return join(sessionDirectory, ROLE_RUN_SESSION_FILENAME);
23
+ }
24
+ function sessionFileOf(runDirectory) {
25
+ return sessionFileIn(sessionDirectoryOf(runDirectory));
26
+ }
27
+ function runDirectoryOfSessionFile(sessionFile) {
28
+ return dirname(dirname(sessionFile));
29
+ }
30
+ function runDirectoryFromSessionDirectory(sessionDirectory) {
31
+ return dirname(sessionDirectory);
32
+ }
9
33
  async function listBookRunDirectories(bookDir) {
10
34
  const out = [];
11
35
  const seen = /* @__PURE__ */ new Set();
@@ -39,6 +63,20 @@ async function listBookRunDirectories(bookDir) {
39
63
  }
40
64
  return out.sort();
41
65
  }
66
+ async function findRoleRunDirectory(bookDirectories, runId, onlyRole) {
67
+ const matches = [];
68
+ for (const bookDirectory of bookDirectories) {
69
+ for (const runDirectory of await listBookRunDirectories(bookDirectory)) {
70
+ const parsed = parseRunLeaf(basename(runDirectory));
71
+ if (parsed === void 0 || parsed.runId !== runId) continue;
72
+ if (onlyRole !== void 0 && parsed.role !== onlyRole) continue;
73
+ matches.push(runDirectory);
74
+ }
75
+ }
76
+ if (matches.length === 0) return void 0;
77
+ if (matches.length === 1) return matches[0];
78
+ throw new Error(`ambiguous role run id ${runId}: ${matches.join(", ")}`);
79
+ }
42
80
  function roleRunPlacement(ledgerHome, input) {
43
81
  const subjectDirectory = "ticketNumber" in input.subject ? String(requireSafePositiveTicketNumber(
44
82
  input.subject.ticketNumber,
@@ -48,13 +86,13 @@ function roleRunPlacement(ledgerHome, input) {
48
86
  activationBookDirectory(ledgerHome, input.bookKey),
49
87
  subjectDirectory,
50
88
  "runs",
51
- `${input.runId}@${input.role}`
89
+ formatRunLeaf(input.runId, input.role)
52
90
  );
53
- const sessionDirectory = join(runDirectory, "session");
91
+ const sessionDirectory = sessionDirectoryOf(runDirectory);
54
92
  return {
55
93
  runDirectory,
56
94
  sessionDirectory,
57
- sessionFile: join(sessionDirectory, "session.jsonl"),
95
+ sessionFile: sessionFileIn(sessionDirectory),
58
96
  artifactsDirectory: roleRunArtifactsDirectory(runDirectory),
59
97
  attachmentsDirectory: join(runDirectory, "attachments")
60
98
  };
@@ -77,10 +115,19 @@ function ensureRoleRunPlacement(ledgerHome, placement) {
77
115
  }
78
116
  }
79
117
  export {
118
+ ROLE_RUN_SESSION_FILENAME,
80
119
  ensureRoleRunDirectory,
81
120
  ensureRoleRunPlacement,
121
+ findRoleRunDirectory,
122
+ formatRunLeaf,
82
123
  isUnboundRunDirectory,
83
124
  listBookRunDirectories,
125
+ parseRunLeaf,
84
126
  roleRunArtifactsDirectory,
85
- roleRunPlacement
127
+ roleRunPlacement,
128
+ runDirectoryFromSessionDirectory,
129
+ runDirectoryOfSessionFile,
130
+ sessionDirectoryOf,
131
+ sessionFileIn,
132
+ sessionFileOf
86
133
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Single authority for rewriting durable run pages after a run directory moves.
3
- * Live unbound→ticket relocate and book-topology migration both call this.
3
+ * Live callers project typed fields; only historical migrators walk durable pages.
4
4
  *
5
5
  * Scope is typed machine-consumed path fields only — never free text, never
6
6
  * frozen attachment/artifact bytes. Nested walk is confined to package-owned
@@ -8,7 +8,10 @@
8
8
  */
9
9
  import { existsSync } from "node:fs";
10
10
  import { readdir, readFile, writeFile } from "node:fs/promises";
11
- import { join, sep } from "node:path";
11
+ import { dirname, join, sep } from "node:path";
12
+ import { activationBookDirectory, physicalPathIdentity } from "./activation-ledger-topology.js";
13
+ import { readRoleRunIdentity } from "./public-cli/run-lifecycle.js";
14
+ import { findRoleRunDirectory, roleRunPlacement } from "./role-run-placement.js";
12
15
  import { AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE } from "./compliance-transport.js";
13
16
  import { isRecord, isEnoent } from "./unknown-value.js";
14
17
  const ADMITTED_PAGE_FIELDS = [
@@ -52,6 +55,40 @@ export function rewriteRunDirectoryPathValue(value, oldRunDirectory, newRunDirec
52
55
  }
53
56
  return value;
54
57
  }
58
+ /** Match physical aliases or a normal unbound→ticket bind, never historical migration layouts. */
59
+ export async function retainedRunPathsMatch(recordedPath, currentPath, currentRunDirectory) {
60
+ if (recordedPath === currentPath)
61
+ return true;
62
+ if (typeof recordedPath !== "string")
63
+ return false;
64
+ const recordedPathIdentity = physicalPathIdentity(recordedPath);
65
+ const currentPathIdentity = physicalPathIdentity(currentPath);
66
+ if (recordedPathIdentity === currentPathIdentity)
67
+ return true;
68
+ if (currentRunDirectory === undefined)
69
+ return false;
70
+ const runDirectory = physicalPathIdentity(currentRunDirectory);
71
+ const identity = await readRoleRunIdentity(runDirectory);
72
+ if (identity === undefined)
73
+ return false;
74
+ for (let directory = runDirectory; dirname(directory) !== directory; directory = dirname(directory)) {
75
+ const ledgerHome = dirname(dirname(directory));
76
+ if (directory !== activationBookDirectory(ledgerHome, identity.bookKey))
77
+ continue;
78
+ const unbound = roleRunPlacement(ledgerHome, {
79
+ bookKey: identity.bookKey,
80
+ subject: { unbound: true },
81
+ runId: identity.runId,
82
+ role: identity.role,
83
+ });
84
+ if (rewriteRunDirectoryPathValue(recordedPathIdentity, unbound.runDirectory, runDirectory) !== currentPathIdentity) {
85
+ return false;
86
+ }
87
+ const placed = await findRoleRunDirectory([directory], identity.runId, identity.role);
88
+ return placed !== undefined && physicalPathIdentity(placed) === runDirectory;
89
+ }
90
+ return false;
91
+ }
55
92
  /**
56
93
  * Apply the longest matching oldRunDirectory rewrite. Longer keys win so a
57
94
  * nested run path is not partially claimed by a shorter sibling prefix.
@@ -9,7 +9,7 @@
9
9
  import { randomUUID } from "node:crypto";
10
10
  import { readdir, readFile } from "node:fs/promises";
11
11
  import { basename, dirname, join } from "node:path";
12
- import { roleRunArtifactsDirectory } from "./role-run-placement.js";
12
+ import { parseRunLeaf, roleRunArtifactsDirectory } from "./role-run-placement.js";
13
13
  import { isRecord, errorText, isMissingPathError } from "./unknown-value.js";
14
14
  export const RUN_TERMINAL_REPORT_FILE = "report.json";
15
15
  export const RUN_TERMINAL_ERROR_FILE = "error.json";
@@ -126,11 +126,7 @@ async function listUniqueErrorFallbackPaths(directories) {
126
126
  * Sole authority for runDirectory → runId (last `@` split).
127
127
  */
128
128
  export function runIdFromRunDirectory(runDirectory) {
129
- const name = basename(runDirectory);
130
- const at = name.lastIndexOf("@");
131
- if (at <= 0 || at === name.length - 1)
132
- return undefined;
133
- return name.slice(0, at);
129
+ return parseRunLeaf(basename(runDirectory))?.runId;
134
130
  }
135
131
  /**
136
132
  * Shared parent-directory unique fallback may be adopted only when the
@@ -2,8 +2,33 @@ import { readFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { isEnoent, isRecord } from "./unknown-value.js";
4
4
  const MIGRATION_TICKET_DERIVATION_PAGE = "migration-ticket-derivation.json";
5
+ function isPositiveTicketNumber(value) {
6
+ return typeof value === "number" && Number.isInteger(value) && value >= 1;
7
+ }
5
8
  function isSafePositiveTicketNumber(value) {
6
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
9
+ return isPositiveTicketNumber(value) && Number.isSafeInteger(value);
10
+ }
11
+ function isTicketNumberString(value) {
12
+ return typeof value === "string" && /^[1-9][0-9]*$/.test(value);
13
+ }
14
+ function parseTicketNumber(value) {
15
+ if (isSafePositiveTicketNumber(value)) return value;
16
+ if (!isTicketNumberString(value)) return void 0;
17
+ const parsed = Number(value);
18
+ return isSafePositiveTicketNumber(parsed) ? parsed : void 0;
19
+ }
20
+ function sitianSubjectTicketNumber(subject) {
21
+ if (typeof subject === "string" || typeof subject === "number") {
22
+ return parseTicketNumber(subject);
23
+ }
24
+ if (subject !== null && typeof subject === "object" && !Array.isArray(subject)) {
25
+ return parseTicketNumber(subject.ticketNumber);
26
+ }
27
+ return void 0;
28
+ }
29
+ function ticketNumberFromSitianSubject(subject) {
30
+ const parsed = sitianSubjectTicketNumber(subject);
31
+ return parsed === void 0 ? void 0 : String(parsed);
7
32
  }
8
33
  function readDeclaredTicketNumber(value) {
9
34
  if (isSafePositiveTicketNumber(value)) return value;
@@ -11,8 +36,7 @@ function readDeclaredTicketNumber(value) {
11
36
  const trimmed = value.trim();
12
37
  const match = /^#?([1-9]\d*)(?!\.\d)(?:\b|$)/.exec(trimmed);
13
38
  if (match === null) return void 0;
14
- const ticketNumber = Number(match[1]);
15
- return isSafePositiveTicketNumber(ticketNumber) ? ticketNumber : void 0;
39
+ return parseTicketNumber(match[1]);
16
40
  }
17
41
  function requireSafePositiveTicketNumber(ticketNumber, context = "ticketNumber") {
18
42
  if (!isSafePositiveTicketNumber(ticketNumber)) {
@@ -58,10 +82,15 @@ async function readRunTicketNumber(runDirectory) {
58
82
  }
59
83
  export {
60
84
  MIGRATION_TICKET_DERIVATION_PAGE,
85
+ isPositiveTicketNumber,
61
86
  isSafePositiveTicketNumber,
87
+ isTicketNumberString,
88
+ parseTicketNumber,
62
89
  readBoardTicketNumber,
63
90
  readDeclaredTicketNumber,
64
91
  readMigrationDerivedTicketNumber,
65
92
  readRunTicketNumber,
66
- requireSafePositiveTicketNumber
93
+ requireSafePositiveTicketNumber,
94
+ sitianSubjectTicketNumber,
95
+ ticketNumberFromSitianSubject
67
96
  };