@akagilnc/pi-workflow-roles 0.1.2383 → 0.1.2402

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 (57) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/audit-escalation.js +0 -13
  4. package/dist/compliance-transport.js +28 -5
  5. package/dist/doctor-contracts.js +2 -9
  6. package/dist/dossier-resolution.js +21 -2
  7. package/dist/engine-detour-tool.js +10 -35
  8. package/dist/evidence-child-executor.js +12 -4
  9. package/dist/merger-contracts.js +2 -8
  10. package/dist/navigator-attendance.js +1 -9
  11. package/dist/navigator-invocation-identity.js +16 -4
  12. package/dist/notary-contracts.js +7 -25
  13. package/dist/package-contracts/judge-output.js +1 -4
  14. package/dist/package-contracts/reviewer-output.js +1 -7
  15. package/dist/package-contracts/terminating-tools.js +6 -11
  16. package/dist/public-cli/main.js +125 -475
  17. package/package.json +1 -1
  18. package/scripts/build-package.mjs +1 -2
  19. package/src/analyst-gate-cycles-read.ts +5 -8
  20. package/src/analyst-metric-families/acceptance-success-rework.ts +3 -6
  21. package/src/audit-escalation.ts +0 -25
  22. package/src/collector-role.ts +1 -8
  23. package/src/compliance-transport.ts +38 -6
  24. package/src/doctor-auditor.ts +3 -3
  25. package/src/doctor-contracts.ts +3 -18
  26. package/src/doctor-role.ts +1 -5
  27. package/src/dossier-resolution.ts +25 -4
  28. package/src/engine-detour-tool.ts +16 -51
  29. package/src/evidence-child-executor.ts +12 -4
  30. package/src/factory-board.ts +2 -4
  31. package/src/gatekeeper-role.ts +33 -43
  32. package/src/judge-auditor.ts +3 -3
  33. package/src/judge-role.ts +1 -8
  34. package/src/merger-contracts.ts +3 -17
  35. package/src/merger-role.ts +1 -8
  36. package/src/navigator-attendance.ts +1 -14
  37. package/src/navigator-invocation-identity.ts +34 -13
  38. package/src/notary-contracts.ts +9 -43
  39. package/src/notary-role.ts +3 -5
  40. package/src/package-contracts/judge-output.ts +2 -17
  41. package/src/package-contracts/reviewer-output.ts +2 -21
  42. package/src/package-contracts/terminating-tools.ts +5 -14
  43. package/src/package-contracts/worker-output.ts +2 -10
  44. package/src/public-cli/doctor-run.ts +0 -2
  45. package/src/public-cli/fixer-run.ts +0 -15
  46. package/src/public-cli/judge-run.ts +0 -16
  47. package/src/public-cli/notary-run.ts +1 -1
  48. package/src/public-cli/one-shot-dispatch.ts +1 -1
  49. package/src/public-cli/reviewer-run.ts +0 -15
  50. package/src/public-cli/settlement.ts +110 -368
  51. package/src/public-cli/terminal.ts +3 -71
  52. package/src/reviewer-auditor.ts +3 -3
  53. package/src/reviewer-role.ts +2 -11
  54. package/src/role-runtime.ts +43 -37
  55. package/src/worker-role.ts +2 -14
  56. package/dist/engine-labor-fallback.js +0 -136
  57. package/src/engine-labor-fallback.ts +0 -270
@@ -4,8 +4,7 @@
4
4
  * Controlled failures and audit human decisions settle here without washing causes.
5
5
  */
6
6
  import { randomUUID } from "node:crypto";
7
- import { constants as fsConstants } from "node:fs";
8
- import { appendFile, lstat, mkdir, open, readFile, readdir, writeFile } from "node:fs/promises";
7
+ import { appendFile, lstat, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
9
8
  import { dirname, join } from "node:path";
10
9
 
11
10
  import {
@@ -32,7 +31,6 @@ import {
32
31
  AUDITOR_PARENT_ATTEMPT_BINDING_ENTRY_TYPE,
33
32
  COMPLIANCE_RESPONSE_ENTRY_TYPE,
34
33
  readComplianceCandidate,
35
- type ComplianceAuditIncomplete,
36
34
  type ComplianceDecision,
37
35
  } from "../compliance-transport.ts";
38
36
  import {
@@ -41,11 +39,6 @@ import {
41
39
  COLLECTOR_WAIT_TOOL,
42
40
  } from "../collector-ledger.ts";
43
41
  import { ENGINE_DETOUR_TOOL_NAME } from "../engine-detour.ts";
44
- import {
45
- readEngineLaborFallbackFieldFrom,
46
- seatFallbackBaseStatus,
47
- seatFallbackStatusHasLawfulEvidence,
48
- } from "../engine-labor-fallback.ts";
49
42
  import {
50
43
  JUDGE_OUTPUT_TOOL_NAME,
51
44
  type JudgeVerdict,
@@ -118,10 +111,8 @@ import {
118
111
  formatTerminalResult,
119
112
  isLawfulTypedTerminalOutcome,
120
113
  recommendationNavigatorFact,
121
- buildAuditIncompleteTerminalOutcome,
122
114
  buildResidualIncompleteTerminalOutcome,
123
115
  redactExactRunId,
124
- type AuditIncompleteResidual,
125
116
  type ControlledFailureCause,
126
117
  type TerminalArtifactRef,
127
118
  type TerminalMenxiaFact,
@@ -728,10 +719,16 @@ export async function readBoundEvidenceChildKnownFailure(
728
719
  return undefined;
729
720
  }
730
721
 
731
- /** Recover a provider stop from the auditor child bound to the current parent attempt. */
732
- export async function readBoundAuditorKnownFailure(
722
+ type BoundAuditorVolume = {
723
+ readonly entries: SessionEntry[];
724
+ readonly attemptEntryId?: string;
725
+ readonly parentId: string;
726
+ readonly sessionFile: string;
727
+ };
728
+
729
+ async function loadBoundAuditorVolumes(
733
730
  sessionFile: string,
734
- ): Promise<ExplicitInternalKnownFailure | undefined> {
731
+ ): Promise<readonly BoundAuditorVolume[] | undefined> {
735
732
  let parentEntries: SessionEntry[];
736
733
  try {
737
734
  parentEntries = await readBoundSessionEntries(sessionFile);
@@ -773,7 +770,7 @@ export async function readBoundAuditorKnownFailure(
773
770
  // first attempt's child after resume advanced latest, losing retentionFailure
774
771
  // when retry had no compliance entry. Fix: ignore envelope for staleness and
775
772
  // prefer any valid compliance failure before falling back to primary.
776
- const validAuditorFiles: Array<{ file: string; entries: SessionEntry[]; attemptEntryId?: string }> = [];
773
+ const valid: BoundAuditorVolume[] = [];
777
774
  for (const file of names.filter((name) => name.endsWith(".jsonl")).sort().reverse()) {
778
775
  let entries: SessionEntry[];
779
776
  try {
@@ -788,10 +785,20 @@ export async function readBoundAuditorKnownFailure(
788
785
  const attemptEntryId = typeof bindingParent?.attemptEntryId === "string" ? bindingParent.attemptEntryId : undefined;
789
786
  const attemptEntryIndex = attemptEntryId === undefined ? -1 : parentEntries.findIndex((entry) => entry.id === attemptEntryId);
790
787
  if (bindingParent?.sessionId !== parentId || bindingParent.sessionFile !== sessionFile || attemptEntryIndex < latestParentUserIndex) continue;
791
- validAuditorFiles.push({ file, entries, ...(attemptEntryId === undefined ? {} : { attemptEntryId }) });
788
+ valid.push({
789
+ entries,
790
+ parentId,
791
+ sessionFile,
792
+ ...(attemptEntryId === undefined ? {} : { attemptEntryId }),
793
+ });
792
794
  }
793
- // Prefer compliance failure (retention) from any valid attempt, newest first.
794
- for (const { entries, attemptEntryId } of validAuditorFiles) {
795
+ return valid;
796
+ }
797
+
798
+ function complianceFailureFromAuditorVolumes(
799
+ volumes: readonly BoundAuditorVolume[],
800
+ ): ExplicitInternalKnownFailure | undefined {
801
+ for (const { entries, attemptEntryId, parentId, sessionFile } of volumes) {
795
802
  const stop = extractSessionProviderStop(entries);
796
803
  if (stop === undefined) continue;
797
804
  for (let i = entries.length - 1; i >= 0; i -= 1) {
@@ -812,8 +819,13 @@ export async function readBoundAuditorKnownFailure(
812
819
  };
813
820
  }
814
821
  }
815
- // No compliance failure: fall back to most recent provider stop (auto-resume latest attempt).
816
- for (const { entries } of validAuditorFiles) {
822
+ return undefined;
823
+ }
824
+
825
+ function providerStopFallbackFromAuditorVolumes(
826
+ volumes: readonly BoundAuditorVolume[],
827
+ ): ExplicitInternalKnownFailure | undefined {
828
+ for (const { entries } of volumes) {
817
829
  const stop = extractSessionProviderStop(entries);
818
830
  if (stop === undefined) continue;
819
831
  const primary = knownFailureFromProviderStop(stop)!;
@@ -828,6 +840,34 @@ export async function readBoundAuditorKnownFailure(
828
840
  return undefined;
829
841
  }
830
842
 
843
+ /** Recover a provider stop from the auditor child bound to the current parent attempt. */
844
+ export async function readBoundAuditorKnownFailure(
845
+ sessionFile: string,
846
+ ): Promise<ExplicitInternalKnownFailure | undefined> {
847
+ const volumes = await loadBoundAuditorVolumes(sessionFile);
848
+ if (volumes === undefined) return undefined;
849
+ return complianceFailureFromAuditorVolumes(volumes)
850
+ ?? providerStopFallbackFromAuditorVolumes(volumes);
851
+ }
852
+
853
+ /** Strong auditor tier only — retained compliance-failure entries, no provider-stop fallback. */
854
+ async function readBoundAuditorComplianceFailure(
855
+ sessionFile: string,
856
+ ): Promise<ExplicitInternalKnownFailure | undefined> {
857
+ const volumes = await loadBoundAuditorVolumes(sessionFile);
858
+ if (volumes === undefined) return undefined;
859
+ return complianceFailureFromAuditorVolumes(volumes);
860
+ }
861
+
862
+ /** Weaker auditor tier: provider stop without a retained compliance-failure entry. */
863
+ async function readBoundAuditorProviderStopFallback(
864
+ sessionFile: string,
865
+ ): Promise<ExplicitInternalKnownFailure | undefined> {
866
+ const volumes = await loadBoundAuditorVolumes(sessionFile);
867
+ if (volumes === undefined) return undefined;
868
+ return providerStopFallbackFromAuditorVolumes(volumes);
869
+ }
870
+
831
871
  function typedFailedTerminatingToolKnownFailure(
832
872
  entries: readonly SessionEntry[],
833
873
  ): ExplicitInternalKnownFailure | undefined {
@@ -850,11 +890,14 @@ function typedFailedTerminatingToolKnownFailure(
850
890
  ? message.content.find((part) => isRecord(part) && part.type === "text" && typeof part.text === "string")
851
891
  : undefined;
852
892
  const diagnostic = isRecord(textPart) ? textPart.text : undefined;
893
+ // Durable details already carry fact + typed evidence from envelope one-shot projection (#475).
894
+ // Do not re-parse retained compliance responses here.
895
+ const details = isRecord(message.details) ? message.details : classification.fact;
853
896
  return {
854
897
  cause: "output",
855
898
  identity: { name: message.toolName, code: message.toolCallId },
856
899
  ...(typeof diagnostic === "string" && diagnostic.trim() !== "" ? { diagnostic } : {}),
857
- details: classification.fact,
900
+ details,
858
901
  };
859
902
  }
860
903
  return undefined;
@@ -913,12 +956,13 @@ export async function resolveAuditedRunnerFailureResolution(input: {
913
956
  });
914
957
  }
915
958
  }
916
- // Bound auditor evidence outranks a parent failure that the auditor path itself
917
- // caused (retention EISDIR race). A typed terminating-tool host failure is next:
918
- // it outranks provider/credential and nonzero fallbacks, but not its recorded cause.
959
+ // Bound auditor compliance-failure retention outranks a parent failure that the
960
+ // auditor path itself caused (retention EISDIR race). A typed terminating-tool
961
+ // host failure is next — it outranks weaker auditor provider-stop fallback so
962
+ // parent failInfrastructure abort pollution cannot wash a real diagnostic (#475).
919
963
  try {
920
- const auditorFailure = await readBoundAuditorKnownFailure(input.sessionFile);
921
- if (auditorFailure !== undefined) return resolutionOf(auditorFailure);
964
+ const auditorCompliance = await readBoundAuditorComplianceFailure(input.sessionFile);
965
+ if (auditorCompliance !== undefined) return resolutionOf(auditorCompliance);
922
966
  } catch (error) {
923
967
  const failure = sessionReadFailure(error, "failed to recover bound auditor failure");
924
968
  return resolutionOf({
@@ -942,6 +986,17 @@ export async function resolveAuditedRunnerFailureResolution(input: {
942
986
  });
943
987
  }
944
988
  }
989
+ try {
990
+ const auditorStop = await readBoundAuditorProviderStopFallback(input.sessionFile);
991
+ if (auditorStop !== undefined) return resolutionOf(auditorStop);
992
+ } catch (error) {
993
+ const failure = sessionReadFailure(error, "failed to recover bound auditor provider stop");
994
+ return resolutionOf({
995
+ cause: "session",
996
+ identity: thrownIdentity(failure),
997
+ diagnostic: failure.message || failure.name,
998
+ });
999
+ }
945
1000
  // Reviewer axis evidence-children are next: fixed two-axis dispatch fails
946
1001
  // during activation with only child stops durable. Parent stop remains the
947
1002
  // fallback; credential is last.
@@ -1110,10 +1165,8 @@ function judgeDecisiveFacts(
1110
1165
  const facts: Record<string, unknown> = {
1111
1166
  judgeStatus,
1112
1167
  ...auditNoReceiptDecisiveFact(verdict),
1113
- // #380: spread sole-built field; do not re-key here (S1).
1114
- ...readEngineLaborFallbackFieldFrom(verdict),
1115
1168
  };
1116
- const statusBase = seatFallbackBaseStatus(judgeStatus);
1169
+ const statusBase = judgeStatus;
1117
1170
  if (statusBase === "continue") {
1118
1171
  const fix = safelyRead(verdict, "fix");
1119
1172
  if (fix.readable && isRecord(fix.value)) {
@@ -1167,7 +1220,7 @@ function coderDecisiveFacts(output: CoderOutput): Record<string, unknown> {
1167
1220
  if (status.readable && typeof status.value === "string") facts.coderStatus = status.value;
1168
1221
  const statusBase =
1169
1222
  status.readable && typeof status.value === "string"
1170
- ? seatFallbackBaseStatus(status.value)
1223
+ ? (status.value)
1171
1224
  : undefined;
1172
1225
  const remainingScope = safelyRead(candidate, "remainingScope");
1173
1226
  if (statusBase === "unfinished" && remainingScope.readable && typeof remainingScope.value === "string") facts.remainingScope = remainingScope.value;
@@ -1187,7 +1240,7 @@ function fixerDecisiveFacts(output: FixerOutput): Record<string, unknown> {
1187
1240
  if (status.readable && typeof status.value === "string") facts.fixerStatus = status.value;
1188
1241
  const statusBase =
1189
1242
  status.readable && typeof status.value === "string"
1190
- ? seatFallbackBaseStatus(status.value)
1243
+ ? (status.value)
1191
1244
  : undefined;
1192
1245
  const remainingScope = safelyRead(candidate, "remainingScope");
1193
1246
  if ((statusBase === "unfinished" || statusBase === "refused") && remainingScope.readable && typeof remainingScope.value === "string") facts.remainingScope = remainingScope.value;
@@ -1280,7 +1333,7 @@ function doctorDecisiveFacts(output: DoctorOutput): Record<string, unknown> {
1280
1333
  if (status.readable && typeof status.value === "string") facts.doctorStatus = status.value;
1281
1334
  const statusBase =
1282
1335
  status.readable && typeof status.value === "string"
1283
- ? seatFallbackBaseStatus(status.value)
1336
+ ? (status.value)
1284
1337
  : undefined;
1285
1338
  if (statusBase === "refused") {
1286
1339
  const reason = safelyRead(candidate, "reason");
@@ -1329,8 +1382,6 @@ function reviewerDecisiveFacts(
1329
1382
  amendmentAxes,
1330
1383
  acceptedBatchPresent: acceptedBatch.readable && acceptedBatch.value !== undefined,
1331
1384
  ...auditNoReceiptDecisiveFact(candidate),
1332
- // #380: spread sole-built field; do not re-key here (S1).
1333
- ...readEngineLaborFallbackFieldFrom(candidate),
1334
1385
  };
1335
1386
  if (status.readable && typeof status.value === "string") facts.reviewerStatus = status.value;
1336
1387
  if (
@@ -1342,7 +1393,7 @@ function reviewerDecisiveFacts(
1342
1393
  const diagnostic = safelyRead(candidate, "diagnostic");
1343
1394
  const statusBase =
1344
1395
  status.readable && typeof status.value === "string"
1345
- ? seatFallbackBaseStatus(status.value)
1396
+ ? (status.value)
1346
1397
  : undefined;
1347
1398
  if (statusBase === "refused" && diagnostic.readable) {
1348
1399
  facts.diagnosticPresent = typeof diagnostic.value === "string" && diagnostic.value.trim().length > 0;
@@ -1550,35 +1601,6 @@ export function assertCollectorReceiptMatchesAdmitted(
1550
1601
  }
1551
1602
  }
1552
1603
 
1553
- /**
1554
- * Shared audit-incomplete extraction for the four roles with Soul auditors.
1555
- * The role submission and retained auditor response are separate evidence faces;
1556
- * neither is converted into an accepted Receipt.
1557
- */
1558
- function isComplianceAuditIncomplete(value: unknown): value is ComplianceAuditIncomplete {
1559
- if (!isRecord(value) || value.status !== "audit-incomplete") return false;
1560
- const observation = value.observation;
1561
- if (!isRecord(observation)) return false;
1562
- if (observation.kind === "missing-dossier") return true;
1563
- if (observation.kind === "missing-subject") {
1564
- return typeof observation.subject === "string" && observation.subject.length > 0;
1565
- }
1566
- if (observation.kind === "object-status-unreadable") {
1567
- return observation.status === "missing" || observation.status === "unknown";
1568
- }
1569
- return observation.kind === "non-object-arguments" && [
1570
- "null",
1571
- "array",
1572
- "undefined",
1573
- "string",
1574
- "number",
1575
- "boolean",
1576
- "bigint",
1577
- "symbol",
1578
- "function",
1579
- ].includes(observation.type as string);
1580
- }
1581
-
1582
1604
  function auditToolNameForRole(
1583
1605
  role: (typeof AUDITOR_SOUL_ROLES)[number],
1584
1606
  ): string {
@@ -1592,19 +1614,6 @@ function auditToolNameForRole(
1592
1614
  }
1593
1615
  }
1594
1616
 
1595
- function outputToolNameForAuditedRole(
1596
- role: (typeof AUDITOR_SOUL_ROLES)[number],
1597
- ): string {
1598
- switch (role) {
1599
- case "judge":
1600
- return JUDGE_OUTPUT_TOOL_NAME;
1601
- case "reviewer":
1602
- return REVIEWER_OUTPUT_TOOL_NAME;
1603
- case "doctor":
1604
- return DOCTOR_OUTPUT_TOOL_NAME;
1605
- }
1606
- }
1607
-
1608
1617
  type BoundRoleToolCall = {
1609
1618
  callIndex: number;
1610
1619
  candidate: unknown;
@@ -1757,13 +1766,6 @@ function isUnboundAuditEscalationFace(details: unknown): boolean {
1757
1766
  return kind.readable && kind.value === "audit_escalation";
1758
1767
  }
1759
1768
 
1760
- function auditIncompleteFromCandidate(
1761
- candidate: unknown,
1762
- ): ComplianceAuditIncomplete | undefined {
1763
- const decision = readComplianceCandidate(candidate);
1764
- return decision.status === "audit-incomplete" ? decision : undefined;
1765
- }
1766
-
1767
1769
  function boundRetainedAuditResponse(
1768
1770
  entries: readonly SessionEntry[],
1769
1771
  callIndex: number,
@@ -1791,113 +1793,6 @@ function boundRetainedAuditResponse(
1791
1793
  return matches.length === 1 ? matches[0] : undefined;
1792
1794
  }
1793
1795
 
1794
- export function extractComplianceAuditIncompleteRoleOutcome(
1795
- entries: readonly SessionEntry[],
1796
- role: (typeof AUDITOR_SOUL_ROLES)[number],
1797
- outputToolName: string,
1798
- ): { outcome: ReturnType<typeof buildAuditIncompleteTerminalOutcome> } | undefined {
1799
- if (outputToolName !== outputToolNameForAuditedRole(role)) return undefined;
1800
- const auditToolName = auditToolNameForRole(role);
1801
- for (let index = entries.length - 1; index >= 0; index -= 1) {
1802
- const message = entries[index]?.message;
1803
- if (
1804
- entries[index]?.type !== "message" ||
1805
- message?.role !== "toolResult" ||
1806
- message.toolName !== outputToolName ||
1807
- message.isError === true ||
1808
- !isComplianceAuditIncomplete(message.details)
1809
- ) {
1810
- continue;
1811
- }
1812
- const roleCall = boundRoleToolCallForResult(
1813
- entries,
1814
- index,
1815
- message,
1816
- outputToolName,
1817
- );
1818
- if (roleCall === undefined) continue;
1819
- // Preflight missing-dossier / missing-subject never contacts the provider, so
1820
- // there is no retained auditor response — the role tool details are the audit.
1821
- // details already narrowed by isComplianceAuditIncomplete at the loop gate.
1822
- const details = message.details;
1823
- if (
1824
- details.observation.kind === "missing-dossier"
1825
- || details.observation.kind === "missing-subject"
1826
- ) {
1827
- return {
1828
- outcome: buildAuditIncompleteTerminalOutcome({
1829
- role,
1830
- roleCandidate: roleCall.candidate,
1831
- audit: details,
1832
- }),
1833
- };
1834
- }
1835
- const retained = boundRetainedAuditResponse(
1836
- entries,
1837
- roleCall.callIndex,
1838
- index,
1839
- auditToolName,
1840
- );
1841
- if (retained === undefined) continue;
1842
- const audit = auditIncompleteFromCandidate(retained.candidate);
1843
- if (audit === undefined) continue;
1844
- return {
1845
- outcome: buildAuditIncompleteTerminalOutcome({
1846
- role,
1847
- roleCandidate: roleCall.candidate,
1848
- audit,
1849
- }),
1850
- };
1851
- }
1852
- return undefined;
1853
- }
1854
-
1855
- function auditArtifactPublicationError(message: string, code: string): Error & {
1856
- code: string;
1857
- } {
1858
- const error = new Error(message) as Error & { code: string };
1859
- error.name = "ArtifactPublicationError";
1860
- error.code = code;
1861
- return error;
1862
- }
1863
-
1864
- async function ensureAuditEvidenceDirectory(runDirectory: string): Promise<string> {
1865
- const artifactsDir = join(runDirectory, "artifacts");
1866
- const runStat = await lstat(runDirectory);
1867
- if (runStat.isSymbolicLink() || !runStat.isDirectory()) {
1868
- throw auditArtifactPublicationError(
1869
- "audit evidence run directory is not a real directory",
1870
- "ELOOP",
1871
- );
1872
- }
1873
- try {
1874
- const existing = await lstat(artifactsDir);
1875
- if (existing.isSymbolicLink()) {
1876
- throw auditArtifactPublicationError(
1877
- "audit evidence artifacts directory is a symlink",
1878
- "ELOOP",
1879
- );
1880
- }
1881
- if (!existing.isDirectory()) {
1882
- throw auditArtifactPublicationError(
1883
- "audit evidence artifacts path is not a directory",
1884
- "EEXIST",
1885
- );
1886
- }
1887
- } catch (error) {
1888
- if (!isMissingPathError(error)) throw error;
1889
- await mkdir(artifactsDir, { recursive: true });
1890
- const created = await lstat(artifactsDir);
1891
- if (created.isSymbolicLink() || !created.isDirectory()) {
1892
- throw auditArtifactPublicationError(
1893
- "audit evidence artifacts directory is not a real directory",
1894
- "ELOOP",
1895
- );
1896
- }
1897
- }
1898
- return artifactsDir;
1899
- }
1900
-
1901
1796
  /**
1902
1797
  * #419 per-attempt process history. 史必追加,指针可覆盖;指针可以覆盖的前提是史已落。
1903
1798
  * Reuses the run session principal's append-only JSONL custom-entry shape
@@ -1957,167 +1852,6 @@ export async function appendRunAttemptHistory(
1957
1852
  await appendFile(admitted.sessionFile, line, "utf8");
1958
1853
  }
1959
1854
 
1960
- /**
1961
- * Publish the retained residual with complete-write semantics (#419: the
1962
- * previous attempt's pointer file is a rebuildable view once this attempt's
1963
- * complete result is in the appended history; planted symlinks/directories
1964
- * still fail loudly with their #182-A identities).
1965
- */
1966
- export async function publishComplianceAuditIncompleteEvidence(
1967
- admitted: AdmittedRoleInvocation,
1968
- outcome: ReturnType<typeof buildAuditIncompleteTerminalOutcome>,
1969
- ): Promise<TerminalArtifactRef> {
1970
- await appendRunAttemptHistory(admitted, outcome);
1971
- if (
1972
- typeof fsConstants.O_NOFOLLOW !== "number" ||
1973
- typeof fsConstants.O_NONBLOCK !== "number"
1974
- ) {
1975
- // #418 fail-closed: on platforms without O_NOFOLLOW (e.g. Windows,
1976
- // nodejs/node#41590) the JS bitwise-or below would silently drop the flag
1977
- // and the #182-A anti-symlink protection would vanish. Refuse loudly via
1978
- // the existing publication-failure channel instead of publishing
1979
- // unprotected; the complete attempt result above stays in appended history.
1980
- throw auditArtifactPublicationError(
1981
- "audit evidence publication requires O_NOFOLLOW|O_NONBLOCK open-flag support (anti-symlink/anti-planted protection must not be silently dropped); refusing to publish",
1982
- "ENOSYS",
1983
- );
1984
- }
1985
- const artifactsDir = await ensureAuditEvidenceDirectory(admitted.runDirectory);
1986
- const evidencePath = join(artifactsDir, "audit-incomplete.json");
1987
- let existing: Awaited<ReturnType<typeof lstat>> | undefined;
1988
- try {
1989
- existing = await lstat(evidencePath);
1990
- } catch (error) {
1991
- if (!isMissingPathError(error)) throw error;
1992
- }
1993
- if (existing?.isSymbolicLink()) {
1994
- throw auditArtifactPublicationError(
1995
- "audit evidence destination is a symlink",
1996
- "ELOOP",
1997
- );
1998
- }
1999
- if (existing && !existing.isFile()) {
2000
- throw auditArtifactPublicationError(
2001
- "audit evidence destination is not a regular file",
2002
- "EEXIST",
2003
- );
2004
- }
2005
- // #182-A protection restored atomically at this open seam: O_NOFOLLOW makes
2006
- // the open itself refuse symlinks (ELOOP), so one swapped in after the lstat
2007
- // above can never be followed or written through; O_NONBLOCK keeps a
2008
- // race-planted FIFO from blocking this open forever. The fstat below
2009
- // backstops any non-regular object that wins the window.
2010
- const handle = await open(
2011
- evidencePath,
2012
- fsConstants.O_WRONLY |
2013
- fsConstants.O_CREAT |
2014
- fsConstants.O_TRUNC |
2015
- fsConstants.O_NOFOLLOW |
2016
- fsConstants.O_NONBLOCK,
2017
- 0o600,
2018
- );
2019
- try {
2020
- if (!(await handle.stat()).isFile()) {
2021
- throw auditArtifactPublicationError(
2022
- "audit evidence destination is not a regular file",
2023
- "EEXIST",
2024
- );
2025
- }
2026
- await handle.writeFile(`${JSON.stringify(outcome, null, 2)}\n`, "utf8");
2027
- await handle.sync();
2028
- } finally {
2029
- await handle.close();
2030
- }
2031
- return { kind: "evidence", path: evidencePath };
2032
- }
2033
-
2034
- function auditPublicationFailureTerminal(
2035
- admitted: AdmittedRoleInvocation,
2036
- entries: readonly SessionEntry[],
2037
- outcome: ReturnType<typeof buildAuditIncompleteTerminalOutcome>,
2038
- error: unknown,
2039
- ): TerminalResult {
2040
- const attempt = publicationAttemptFromError(
2041
- join(admitted.runDirectory, "artifacts", "audit-incomplete.json"),
2042
- error,
2043
- );
2044
- const diagnostic = `audit-incomplete evidence publication failed: ${attempt.diagnostic}`;
2045
- const decisiveFacts: Record<string, unknown> = {
2046
- ...outcome.decisiveFacts,
2047
- cause: "unrecognized",
2048
- diagnostic,
2049
- publicationFailure: attempt,
2050
- };
2051
- if (attempt.identity?.name !== undefined) decisiveFacts.errorName = attempt.identity.name;
2052
- if (attempt.identity?.code !== undefined) decisiveFacts.errorCode = attempt.identity.code;
2053
- const auditResidual: AuditIncompleteResidual = {
2054
- roleCandidate: outcome.roleCandidate,
2055
- audit: outcome.audit,
2056
- acceptedReceipt: false,
2057
- };
2058
- return {
2059
- roleOutcome: {
2060
- kind: "failure",
2061
- role: admitted.role,
2062
- cause: "unrecognized",
2063
- diagnostic,
2064
- decisiveFacts,
2065
- auditResidual,
2066
- },
2067
- navigator: extractNavigatorFact(entries),
2068
- artifacts: [],
2069
- runId: admitted.runId,
2070
- };
2071
- }
2072
-
2073
- /**
2074
- * Settle the shared audit-incomplete Terminal for Judge/Fixer/Reviewer/Doctor.
2075
- * Callers invoke this only after their ordinary lawful extractor found no result,
2076
- * which preserves the no-other-lawful-result invariant without a second validator.
2077
- */
2078
- export async function trySettleComplianceAuditIncompleteTerminalResult(
2079
- admitted: AdmittedRoleInvocation,
2080
- ): Promise<TerminalResult | undefined> {
2081
- if (!(AUDITOR_SOUL_ROLES as readonly string[]).includes(admitted.role)) {
2082
- return undefined;
2083
- }
2084
- const outputToolName =
2085
- admitted.role === "judge"
2086
- ? JUDGE_OUTPUT_TOOL_NAME
2087
- : admitted.role === "reviewer"
2088
- ? REVIEWER_OUTPUT_TOOL_NAME
2089
- : DOCTOR_OUTPUT_TOOL_NAME;
2090
- const entries = await readLawfulSettlementEntries(admitted);
2091
- if (entries === undefined) return undefined;
2092
- const extracted = extractComplianceAuditIncompleteRoleOutcome(
2093
- entries,
2094
- admitted.role as (typeof AUDITOR_SOUL_ROLES)[number],
2095
- outputToolName,
2096
- );
2097
- if (extracted === undefined) return undefined;
2098
- // Catch covers publication only — menxia read errors keep their session/JSONL identity
2099
- // and must not be washed into the publication-failure label.
2100
- let evidence: TerminalArtifactRef;
2101
- try {
2102
- evidence = await publishComplianceAuditIncompleteEvidence(
2103
- admitted,
2104
- extracted.outcome,
2105
- );
2106
- } catch (error) {
2107
- // Publication failure is a non-lawful terminal, never an accepted audit result.
2108
- return auditPublicationFailureTerminal(admitted, entries, extracted.outcome, error);
2109
- }
2110
- return withOptionalMenxiaProjection(
2111
- {
2112
- roleOutcome: extracted.outcome,
2113
- navigator: extractNavigatorFact(entries),
2114
- artifacts: [evidence],
2115
- runId: admitted.runId,
2116
- },
2117
- admitted.sessionDirectory,
2118
- );
2119
- }
2120
-
2121
1855
  /** Lawful Judge outcomes extracted from session (never a fabricated failure Receipt). */
2122
1856
  export type LawfulJudgeRoleOutcome = Extract<
2123
1857
  TerminalRoleOutcome,
@@ -2160,10 +1894,8 @@ export function extractJudgeRoleOutcome(
2160
1894
  const statusRead = safelyRead(details, "judgeStatus");
2161
1895
  if (!statusRead.readable || typeof statusRead.value !== "string") continue;
2162
1896
  const judgeStatus = statusRead.value;
2163
- const statusBase = seatFallbackBaseStatus(judgeStatus);
1897
+ const statusBase = judgeStatus;
2164
1898
  if (statusBase !== "converged" && statusBase !== "continue" && statusBase !== "escalate") continue;
2165
- // ADR 0071: `-by-fallback` without latch-shaped evidence is not a lawful terminal.
2166
- if (!seatFallbackStatusHasLawfulEvidence(judgeStatus, details)) continue;
2167
1899
  return {
2168
1900
  kind: "accepted",
2169
1901
  role: "judge",
@@ -2327,6 +2059,21 @@ async function withOptionalMenxiaProjection<
2327
2059
  artifacts: readonly TerminalArtifactRef[];
2328
2060
  },
2329
2061
  >(base: T, sessionDirectory: string): Promise<T & { menxia?: TerminalMenxiaFact }> {
2062
+ // A gate transport failure is already represented by typed evidence and has no
2063
+ // accepted gate cycle to project. Re-reading that rejected receipt as an
2064
+ // accepted cycle would replace the original failure with a projection error.
2065
+ const secondaryEvidence = base.roleOutcome.kind === "failure"
2066
+ ? base.roleOutcome.decisiveFacts.secondaryEvidence
2067
+ : undefined;
2068
+ if (
2069
+ isRecord(secondaryEvidence)
2070
+ && secondaryEvidence.kind === "role_infrastructure_failure"
2071
+ && (
2072
+ secondaryEvidence.stage === "gatekeeper"
2073
+ || secondaryEvidence.stage === "inspector"
2074
+ || secondaryEvidence.stage === "notary"
2075
+ )
2076
+ ) return base;
2330
2077
  const menxia = await extractMenxiaFactFromSessionDirectory(sessionDirectory);
2331
2078
  return menxia === undefined ? base : { ...base, menxia };
2332
2079
  }
@@ -3254,10 +3001,9 @@ async function settleLawfulDoctorTerminalResult(
3254
3001
  const extracted = extractDoctorRoleOutcome(entries);
3255
3002
  if (extracted === undefined) return undefined;
3256
3003
  // Bind completed receipt case identity to the admitted Issue evidence case.
3257
- // Seat-fallback may taint status; base semantics still require case binding.
3258
3004
  if (
3259
3005
  extracted.output !== undefined &&
3260
- seatFallbackBaseStatus(String(extracted.output.status)) === "completed"
3006
+ (String(extracted.output.status)) === "completed"
3261
3007
  ) {
3262
3008
  const completedCase = (
3263
3009
  extracted.output as {
@@ -3314,7 +3060,7 @@ export async function trySettleDoctorTerminalResult(
3314
3060
  return settleLawfulDoctorTerminalResult(admitted);
3315
3061
  }
3316
3062
 
3317
- /** Lawful Notary accepted outcome (pass/bounce/incomplete-with-reason). */
3063
+ /** Lawful Notary accepted outcome (pass/bounce). */
3318
3064
  export type LawfulNotaryRoleOutcome = {
3319
3065
  kind: "accepted";
3320
3066
  role: "notary";
@@ -3356,6 +3102,7 @@ async function settleLawfulNotaryTerminalResult(
3356
3102
  if (entries === undefined) return undefined;
3357
3103
  const extracted = extractNotaryRoleOutcome(entries);
3358
3104
  if (extracted === undefined) {
3105
+ // No usable Notary release → existing non-zero failure channel with candidate (#475).
3359
3106
  for (let index = entries.length - 1; index >= 0; index -= 1) {
3360
3107
  const message = entries[index]?.message;
3361
3108
  if (message?.role !== "toolResult") continue;
@@ -3366,16 +3113,11 @@ async function settleLawfulNotaryTerminalResult(
3366
3113
  NOTARY_OUTPUT_TOOL_NAME,
3367
3114
  );
3368
3115
  if (residual === undefined) continue;
3369
- return {
3370
- roleOutcome: buildResidualIncompleteTerminalOutcome({
3371
- role: "notary",
3372
- candidate: residual.candidate,
3373
- diagnostic: residual.diagnostic,
3374
- }),
3375
- navigator: extractNavigatorFact(entries),
3376
- artifacts: [],
3377
- runId: admitted.runId,
3378
- };
3116
+ return settleFailureTerminalResult(admitted, {
3117
+ cause: "output",
3118
+ diagnostic: residual.diagnostic,
3119
+ details: { candidate: residual.candidate, acceptedReceipt: false },
3120
+ });
3379
3121
  }
3380
3122
  return undefined;
3381
3123
  }
@@ -3712,7 +3454,7 @@ function mergerDecisiveFacts(output: MergerOutput): Record<string, unknown> {
3712
3454
  if (attemptId.readable && attemptId.value !== undefined) facts.attemptId = attemptId.value;
3713
3455
  const statusBase =
3714
3456
  status.readable && typeof status.value === "string"
3715
- ? seatFallbackBaseStatus(status.value)
3457
+ ? (status.value)
3716
3458
  : undefined;
3717
3459
  const decisiveKey = statusBase === "completed" ? "mergeCommitId" : "diagnosis";
3718
3460
  const decisive = safelyRead(candidate, decisiveKey);