@akagilnc/pi-workflow-roles 0.1.4673 → 0.1.4698

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.
@@ -4,7 +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 { appendFile, lstat, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
7
+ import { appendFile, lstat, readFile, readdir, rm, unlink, writeFile } from "node:fs/promises";
8
8
  import { dirname, join } from "node:path";
9
9
  import { readAnalystGateCyclesFromAuditorRoles, } from "../analyst-gate-cycles-read.js";
10
10
  import { readSitianRecords, resolveSitianRecordPath, sitianReport } from "../sitian-facade.js";
@@ -41,6 +41,7 @@ import { observePackagedMethodSkillInvocation, } from "../package-resources/meth
41
41
  import { classifyPackagedRoleTerminalResult, findLatestDurablePackagedRoleTerminal, hasNavigatorInfrastructureFailureBase, isAcceptedPackagedRoleTerminalResult, isReceiptSettlementBindingClear, NAVIGATOR_INVOCATION_ENTRY, parseInvocationMarkerIdentity, } from "../navigator-invocation-identity.js";
42
42
  import { NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_TURN_LIMIT, noReceiptLifecycleFacts, parseNoReceiptLifecycleFacts, } from "../receipt-delivery-policy.js";
43
43
  import { roleRunArtifactsDirectory } from "../role-run-placement.js";
44
+ import { listSeamOwnedUniqueErrorFacePaths, RUN_TERMINAL_ARTIFACT_FILES, RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS, } from "../run-terminal-artifacts.js";
44
45
  import { ensureRunArtifactsDir, homeFromRunDirectory, } from "./invocation.js";
45
46
  /** Ledger reads use the run's machine home — not ambient process HOME (child write vs parent settle). */
46
47
  function sealedLedgerHome(admitted) {
@@ -107,13 +108,20 @@ export function withSubmissions(terminal, submissions) {
107
108
  if (submissions.length === 0)
108
109
  return terminal;
109
110
  const roleOutcome = terminal.roleOutcome;
110
- // #879 keeps an already scoped this-court result; #881 full run history is
111
- // presented independently on terminal.submissions. Only fill a missing result payload.
111
+ // #881 / #836: full run history always lands on terminal.submissions.
112
+ // #879: accepted/audit_escalation may still fill a missing this-court
113
+ // payloads face from that history. #953: failure must not — prior receipts
114
+ // stay on the historical carrier only, never as the current failure's
115
+ // ordinary payloads/receipt face.
116
+ if (roleOutcome.kind === "failure") {
117
+ return { ...terminal, submissions };
118
+ }
112
119
  const withPayloads = roleOutcome.kind === "accepted" || roleOutcome.kind === "audit_escalation"
113
- ? { ...roleOutcome, payloads: (roleOutcome.payloads ?? []).length > 0 ? roleOutcome.payloads : submissions }
114
- : roleOutcome.kind === "failure"
115
- ? { ...roleOutcome, payloads: roleOutcome.payloads ?? submissions }
116
- : roleOutcome;
120
+ ? {
121
+ ...roleOutcome,
122
+ payloads: coalesceSubmissionRows(roleOutcome.payloads, submissions),
123
+ }
124
+ : roleOutcome;
117
125
  return { ...terminal, roleOutcome: withPayloads, submissions };
118
126
  }
119
127
  /** Attach full ledger submissions onto any settled terminal (#836). */
@@ -127,6 +135,7 @@ export async function attachRecordedSubmissions(admitted, terminal, scope) {
127
135
  /**
128
136
  * Host ended with no recorded submission — lawful no_receipt (exit 0).
129
137
  * Does not invent an output failure for an empty ledger.
138
+ * #953: empty public terminal face — clear every reader-adoptable prior face.
130
139
  */
131
140
  export async function settleHostEndedNoReceipt(admitted, authority, scope) {
132
141
  const facts = noReceiptLifecycleFacts({
@@ -137,6 +146,7 @@ export async function settleHostEndedNoReceipt(admitted, authority, scope) {
137
146
  attemptPointer: `current:${admitted.runDirectory}`,
138
147
  });
139
148
  const coordinates = coordinatesFromAdmitted(authority, admitted);
149
+ await clearOppositeTerminalArtifactFace(admitted.runDirectory);
140
150
  return withOptionalGateProjection({
141
151
  roleOutcome: {
142
152
  kind: "no_receipt",
@@ -157,7 +167,7 @@ async function closedLedgerOutcome(admitted, role, scope) {
157
167
  function coordinatesFromAdmitted(authority, admitted) {
158
168
  return authority.decode(admitted.principal);
159
169
  }
160
- import { exitCodeForTerminalOutcome, formatTerminalResult, isLawfulTypedTerminalOutcome, adviceNavigatorFact, } from "./terminal.js";
170
+ import { coalesceSubmissionRows, exitCodeForTerminalOutcome, formatTerminalResult, isLawfulTypedTerminalOutcome, adviceNavigatorFact, } from "./terminal.js";
161
171
  export { exitCodeForTerminalOutcome, formatTerminalResult, isLawfulTypedTerminalOutcome, };
162
172
  /**
163
173
  * Host stderr as recorded — full bytes, no flood filter, no char clip (#836).
@@ -470,6 +480,13 @@ function isMissingPathError(error) {
470
480
  "code" in error &&
471
481
  error.code === "ENOENT");
472
482
  }
483
+ /** Face clear only: path not enterable as a face (ENOENT or file mid-path). */
484
+ function isAbsentFacePathError(error) {
485
+ return (error instanceof Error &&
486
+ "code" in error &&
487
+ (error.code === "ENOENT" ||
488
+ error.code === "ENOTDIR"));
489
+ }
473
490
  /**
474
491
  * Preserve session-read failure identity as a typed session cause.
475
492
  * SyntaxError keeps its name so durable settlement does not wash malformed JSONL
@@ -1644,72 +1661,131 @@ async function extractNavigatorFactFromAdmittedSession(sessionFile) {
1644
1661
  };
1645
1662
  }
1646
1663
  }
1647
- export async function publishJudgeArtifacts(admitted, roleOutcome, coordinates) {
1664
+ /**
1665
+ * Remove one face path (file or directory collision plant).
1666
+ * Only missing target is silent — other errno stay loud (失败诚实).
1667
+ * recursive: publication may have occupied a face name as a directory (EISDIR plant).
1668
+ */
1669
+ async function removeFaceIfPresent(path) {
1670
+ try {
1671
+ await rm(path, { recursive: true, force: true });
1672
+ }
1673
+ catch (error) {
1674
+ // force:true already ignores ENOENT; ENOTDIR = face path not enterable
1675
+ // (artifacts-as-file mid-path) — same absent-face semantics as the reader.
1676
+ // Other errno stay loud (失败诚实).
1677
+ if (isAbsentFacePathError(error))
1678
+ return;
1679
+ throw error;
1680
+ }
1681
+ }
1682
+ /**
1683
+ * #953: artifact face reflects the current terminal only.
1684
+ * Reader contract owns the full adoptable set (conventional trio + fixed
1685
+ * fallbacks + seam-owned unique). Clear every seam-owned face before the
1686
+ * new current publishes — including the conventional name about to be
1687
+ * rewritten. Retaining that name left directory collision plants in place
1688
+ * (supported publish input); writer then fell back while the reader stopped
1689
+ * at EISDIR on the stale conventional path and never adopted the fallback.
1690
+ * report / error / empty share one method (no per-branch face list).
1691
+ * empty publishes nothing; does not create a no_receipt-shaped public artifact.
1692
+ * Non-terminal materials stay. Face names may be directory collision plants —
1693
+ * remove recursively. Unique ownership is listSeamOwnedUniqueErrorFacePaths:
1694
+ * parent runs/ faces clear only when body.runId binds; unparseable runId → none.
1695
+ * Path absence (ENOENT/ENOTDIR) is silent; other delete failures stay loud.
1696
+ */
1697
+ async function clearOppositeTerminalArtifactFace(runDirectory) {
1698
+ const artifactsDir = roleRunArtifactsDirectory(runDirectory);
1699
+ for (const file of RUN_TERMINAL_ARTIFACT_FILES) {
1700
+ await removeFaceIfPresent(join(artifactsDir, file));
1701
+ }
1702
+ // Fixed fallbacks + unique clear too; subsequent publish writes the new
1703
+ // durable path after this clear (conventional first when writable).
1704
+ for (const relative of RUN_TERMINAL_ERROR_FALLBACK_RELATIVE_PATHS) {
1705
+ await removeFaceIfPresent(join(runDirectory, relative));
1706
+ }
1707
+ for (const path of await listSeamOwnedUniqueErrorFacePaths(runDirectory)) {
1708
+ await removeFaceIfPresent(path);
1709
+ }
1710
+ }
1711
+ async function ensureTerminalArtifactFace(runDirectory) {
1712
+ const artifactsDir = await ensureRunArtifactsDir(runDirectory);
1713
+ await clearOppositeTerminalArtifactFace(runDirectory);
1714
+ return artifactsDir;
1715
+ }
1716
+ function acceptedArtifactAttachmentRefs(attachments) {
1717
+ return attachments.map((a) => ({
1718
+ provenancePath: a.provenancePath,
1719
+ frozenPath: a.frozenPath,
1720
+ sha256: a.sha256,
1721
+ byteLength: a.byteLength,
1722
+ }));
1723
+ }
1724
+ /**
1725
+ * Sole success-terminal artifact publisher (#953): append attempt history,
1726
+ * refresh the public terminal face, write report/evidence, return refs.
1727
+ * Seat-specific structured fields stay in callers; do not fork this flow.
1728
+ */
1729
+ async function publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, bodies) {
1648
1730
  // #419: history first — report/evidence stay last-write-wins views only
1649
1731
  // because every attempt's complete result has already been appended.
1650
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
1651
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
1652
- const reportPath = join(artifactsDir, "report.json");
1653
- const evidencePath = join(artifactsDir, "evidence.json");
1654
- await writeFile(reportPath, `${JSON.stringify({
1655
- role: "judge",
1656
- runId: admitted.runId,
1657
- outcome: roleOutcome,
1658
- }, null, 2)}\n`, "utf8");
1659
- await writeFile(evidencePath, `${JSON.stringify({
1732
+ await appendRunAttemptHistory({
1733
+ role: admitted.role,
1660
1734
  runId: admitted.runId,
1661
- sessionDirectory: coordinates.sessionDirectory,
1662
1735
  sessionFile: coordinates.sessionFile,
1663
- admittedRequestPath: admitted.admittedRequestPath,
1664
- attachments: admitted.attachments.map((a) => ({
1665
- provenancePath: a.provenancePath,
1666
- frozenPath: a.frozenPath,
1667
- sha256: a.sha256,
1668
- byteLength: a.byteLength,
1669
- })),
1670
- }, null, 2)}\n`, "utf8");
1736
+ }, roleOutcome);
1737
+ const artifactsDir = await ensureTerminalArtifactFace(admitted.runDirectory);
1738
+ const reportPath = join(artifactsDir, "report.json");
1739
+ const evidencePath = join(artifactsDir, "evidence.json");
1740
+ await writeFile(reportPath, `${JSON.stringify(bodies.report, null, 2)}\n`, "utf8");
1741
+ await writeFile(evidencePath, `${JSON.stringify(bodies.evidence, null, 2)}\n`, "utf8");
1671
1742
  return [
1672
1743
  { kind: "report", path: reportPath },
1673
1744
  { kind: "evidence", path: evidencePath },
1674
1745
  ];
1675
1746
  }
1747
+ export async function publishJudgeArtifacts(admitted, roleOutcome, coordinates) {
1748
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
1749
+ report: {
1750
+ role: "judge",
1751
+ runId: admitted.runId,
1752
+ outcome: roleOutcome,
1753
+ },
1754
+ evidence: {
1755
+ runId: admitted.runId,
1756
+ sessionDirectory: coordinates.sessionDirectory,
1757
+ sessionFile: coordinates.sessionFile,
1758
+ admittedRequestPath: admitted.admittedRequestPath,
1759
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
1760
+ },
1761
+ });
1762
+ }
1676
1763
  /**
1677
1764
  * Publish lawful Coder success Artifacts on the shared #106 success interface.
1678
1765
  * Evidence records package method provenance without ambient home Skill paths.
1679
1766
  */
1680
1767
  export async function publishCoderArtifacts(admitted, roleOutcome, coordinates, options = {}) {
1681
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
1682
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
1683
- const reportPath = join(artifactsDir, "report.json");
1684
- const evidencePath = join(artifactsDir, "evidence.json");
1685
- await writeFile(reportPath, `${JSON.stringify({
1686
- role: "coder",
1687
- runId: admitted.runId,
1688
- phase: admitted.phase,
1689
- outcome: roleOutcome,
1690
- }, null, 2)}\n`, "utf8");
1691
- await writeFile(evidencePath, `${JSON.stringify({
1692
- runId: admitted.runId,
1693
- role: "coder",
1694
- phase: admitted.phase,
1695
- sessionDirectory: coordinates.sessionDirectory,
1696
- sessionFile: coordinates.sessionFile,
1697
- admittedRequestPath: admitted.admittedRequestPath,
1698
- taskPath: admitted.taskPath,
1699
- attachments: admitted.attachments.map((a) => ({
1700
- provenancePath: a.provenancePath,
1701
- frozenPath: a.frozenPath,
1702
- sha256: a.sha256,
1703
- byteLength: a.byteLength,
1704
- })),
1705
- ...(options.methodProvenance === undefined
1706
- ? {}
1707
- : { methodProvenance: options.methodProvenance }),
1708
- }, null, 2)}\n`, "utf8");
1709
- return [
1710
- { kind: "report", path: reportPath },
1711
- { kind: "evidence", path: evidencePath },
1712
- ];
1768
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
1769
+ report: {
1770
+ role: "coder",
1771
+ runId: admitted.runId,
1772
+ phase: admitted.phase,
1773
+ outcome: roleOutcome,
1774
+ },
1775
+ evidence: {
1776
+ runId: admitted.runId,
1777
+ role: "coder",
1778
+ phase: admitted.phase,
1779
+ sessionDirectory: coordinates.sessionDirectory,
1780
+ sessionFile: coordinates.sessionFile,
1781
+ admittedRequestPath: admitted.admittedRequestPath,
1782
+ taskPath: admitted.taskPath,
1783
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
1784
+ ...(options.methodProvenance === undefined
1785
+ ? {}
1786
+ : { methodProvenance: options.methodProvenance }),
1787
+ },
1788
+ });
1713
1789
  }
1714
1790
  /**
1715
1791
  * Read session entries for lawful settlement. Missing path → undefined (absence).
@@ -1861,43 +1937,32 @@ export function extractFixerMethodInvocations(entries, options) {
1861
1937
  * Evidence records package diagnosis provenance and optional observed invocation.
1862
1938
  */
1863
1939
  export async function publishFixerArtifacts(admitted, roleOutcome, coordinates, options) {
1864
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
1865
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
1866
- const reportPath = join(artifactsDir, "report.json");
1867
- const evidencePath = join(artifactsDir, "evidence.json");
1868
- await writeFile(reportPath, `${JSON.stringify({
1869
- role: "fixer",
1870
- runId: admitted.runId,
1871
- phase: admitted.phase,
1872
- outcome: roleOutcome,
1873
- }, null, 2)}\n`, "utf8");
1874
- await writeFile(evidencePath, `${JSON.stringify({
1875
- runId: admitted.runId,
1876
- role: "fixer",
1877
- phase: admitted.phase,
1878
- sessionDirectory: coordinates.sessionDirectory,
1879
- sessionFile: coordinates.sessionFile,
1880
- admittedRequestPath: admitted.admittedRequestPath,
1881
- packetPath: admitted.packetPath,
1882
- ...(admitted.prerequisitesPath === undefined
1883
- ? {}
1884
- : { prerequisitesPath: admitted.prerequisitesPath }),
1885
- prerequisites: admitted.prerequisites,
1886
- attachments: admitted.attachments.map((a) => ({
1887
- provenancePath: a.provenancePath,
1888
- frozenPath: a.frozenPath,
1889
- sha256: a.sha256,
1890
- byteLength: a.byteLength,
1891
- })),
1892
- methodProvenance: options.methodProvenance,
1893
- // Optional diagnosis: availability is package-bound; invocation only when observed.
1894
- methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
1895
- methodInvocations: options.methodInvocations ?? [],
1896
- }, null, 2)}\n`, "utf8");
1897
- return [
1898
- { kind: "report", path: reportPath },
1899
- { kind: "evidence", path: evidencePath },
1900
- ];
1940
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
1941
+ report: {
1942
+ role: "fixer",
1943
+ runId: admitted.runId,
1944
+ phase: admitted.phase,
1945
+ outcome: roleOutcome,
1946
+ },
1947
+ evidence: {
1948
+ runId: admitted.runId,
1949
+ role: "fixer",
1950
+ phase: admitted.phase,
1951
+ sessionDirectory: coordinates.sessionDirectory,
1952
+ sessionFile: coordinates.sessionFile,
1953
+ admittedRequestPath: admitted.admittedRequestPath,
1954
+ packetPath: admitted.packetPath,
1955
+ ...(admitted.prerequisitesPath === undefined
1956
+ ? {}
1957
+ : { prerequisitesPath: admitted.prerequisitesPath }),
1958
+ prerequisites: admitted.prerequisites,
1959
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
1960
+ methodProvenance: options.methodProvenance,
1961
+ // Optional diagnosis: availability is package-bound; invocation only when observed.
1962
+ methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
1963
+ methodInvocations: options.methodInvocations ?? [],
1964
+ },
1965
+ });
1901
1966
  }
1902
1967
  async function settleLawfulFixerTerminalResult(admitted, authority, options, scope) {
1903
1968
  const ledgerOutcome = await closedLedgerOutcome(admitted, "fixer", scope);
@@ -1934,35 +1999,24 @@ export async function settleFixerTerminalResult(admitted, authority, options, sc
1934
1999
  return settled;
1935
2000
  }
1936
2001
  export async function publishCollectorArtifacts(admitted, roleOutcome, coordinates) {
1937
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
1938
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
1939
- const reportPath = join(artifactsDir, "report.json");
1940
- const evidencePath = join(artifactsDir, "evidence.json");
1941
- await writeFile(reportPath, `${JSON.stringify({
1942
- role: "collector",
1943
- runId: admitted.runId,
1944
- outcome: roleOutcome,
1945
- }, null, 2)}\n`, "utf8");
1946
- await writeFile(evidencePath, `${JSON.stringify({
1947
- runId: admitted.runId,
1948
- role: "collector",
1949
- ...(admitted.prNumber === undefined ? {} : { prNumber: admitted.prNumber }),
1950
- repository: admitted.repository.canonical,
1951
- manifestDigest: admitted.manifestDigest,
1952
- sessionDirectory: coordinates.sessionDirectory,
1953
- sessionFile: coordinates.sessionFile,
1954
- admittedRequestPath: admitted.admittedRequestPath,
1955
- attachments: admitted.attachments.map((a) => ({
1956
- provenancePath: a.provenancePath,
1957
- frozenPath: a.frozenPath,
1958
- sha256: a.sha256,
1959
- byteLength: a.byteLength,
1960
- })),
1961
- }, null, 2)}\n`, "utf8");
1962
- return [
1963
- { kind: "report", path: reportPath },
1964
- { kind: "evidence", path: evidencePath },
1965
- ];
2002
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
2003
+ report: {
2004
+ role: "collector",
2005
+ runId: admitted.runId,
2006
+ outcome: roleOutcome,
2007
+ },
2008
+ evidence: {
2009
+ runId: admitted.runId,
2010
+ role: "collector",
2011
+ ...(admitted.prNumber === undefined ? {} : { prNumber: admitted.prNumber }),
2012
+ repository: admitted.repository.canonical,
2013
+ manifestDigest: admitted.manifestDigest,
2014
+ sessionDirectory: coordinates.sessionDirectory,
2015
+ sessionFile: coordinates.sessionFile,
2016
+ admittedRequestPath: admitted.admittedRequestPath,
2017
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
2018
+ },
2019
+ });
1966
2020
  }
1967
2021
  async function settleLawfulCollectorTerminalResult(admitted, authority, scope) {
1968
2022
  const coordinates = coordinatesFromAdmitted(authority, admitted);
@@ -2047,37 +2101,28 @@ function extractDoctorCandidateAuditNoReceiptFact(entries) {
2047
2101
  return undefined;
2048
2102
  }
2049
2103
  export async function publishDoctorArtifacts(admitted, roleOutcome, coordinates, options = {}) {
2050
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
2051
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
2052
- const reportPath = join(artifactsDir, "report.json");
2053
- const evidencePath = join(artifactsDir, "evidence.json");
2054
- await writeFile(reportPath, `${JSON.stringify({
2055
- role: "doctor",
2056
- runId: admitted.runId,
2057
- outcome: roleOutcome,
2058
- ...(options.cost === undefined ? {} : { cost: options.cost }),
2059
- ...(options.auditNoReceipt === undefined ? {} : { auditNoReceipt: options.auditNoReceipt }),
2060
- }, null, 2)}\n`, "utf8");
2061
- await writeFile(evidencePath, `${JSON.stringify({
2062
- runId: admitted.runId,
2063
- role: "doctor",
2064
- issueNumber: admitted.issueNumber,
2065
- caseRunsPath: admitted.caseRunsPath,
2066
- caseIdentity: admitted.caseIdentity,
2067
- sessionDirectory: coordinates.sessionDirectory,
2068
- sessionFile: coordinates.sessionFile,
2069
- admittedRequestPath: admitted.admittedRequestPath,
2070
- attachments: admitted.attachments.map((a) => ({
2071
- provenancePath: a.provenancePath,
2072
- frozenPath: a.frozenPath,
2073
- sha256: a.sha256,
2074
- byteLength: a.byteLength,
2075
- })),
2076
- }, null, 2)}\n`, "utf8");
2077
- return [
2078
- { kind: "report", path: reportPath },
2079
- { kind: "evidence", path: evidencePath },
2080
- ];
2104
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
2105
+ report: {
2106
+ role: "doctor",
2107
+ runId: admitted.runId,
2108
+ outcome: roleOutcome,
2109
+ ...(options.cost === undefined ? {} : { cost: options.cost }),
2110
+ ...(options.auditNoReceipt === undefined
2111
+ ? {}
2112
+ : { auditNoReceipt: options.auditNoReceipt }),
2113
+ },
2114
+ evidence: {
2115
+ runId: admitted.runId,
2116
+ role: "doctor",
2117
+ issueNumber: admitted.issueNumber,
2118
+ caseRunsPath: admitted.caseRunsPath,
2119
+ caseIdentity: admitted.caseIdentity,
2120
+ sessionDirectory: coordinates.sessionDirectory,
2121
+ sessionFile: coordinates.sessionFile,
2122
+ admittedRequestPath: admitted.admittedRequestPath,
2123
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
2124
+ },
2125
+ });
2081
2126
  }
2082
2127
  async function settleLawfulDoctorTerminalResult(admitted, authority, scope) {
2083
2128
  const sealed = await sealedLedgerOutcome(admitted, "doctor", scope);
@@ -2132,6 +2177,25 @@ function currentAttemptStartIndex(entries) {
2132
2177
  }
2133
2178
  return 0;
2134
2179
  }
2180
+ /**
2181
+ * Filed-seat success face via the sole accepted publisher (#953).
2182
+ */
2183
+ async function publishSeatAcceptedArtifacts(admitted, roleOutcome, coordinates) {
2184
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
2185
+ report: {
2186
+ role: admitted.role,
2187
+ runId: admitted.runId,
2188
+ outcome: roleOutcome,
2189
+ },
2190
+ evidence: {
2191
+ runId: admitted.runId,
2192
+ sessionDirectory: coordinates.sessionDirectory,
2193
+ sessionFile: coordinates.sessionFile,
2194
+ admittedRequestPath: admitted.admittedRequestPath,
2195
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
2196
+ },
2197
+ });
2198
+ }
2135
2199
  async function settleLawfulSeatAcceptedTerminalResult(admitted, authority, spec, scope) {
2136
2200
  const coordinates = coordinatesFromAdmitted(authority, admitted);
2137
2201
  const { sessionDirectory, sessionFile } = coordinates;
@@ -2169,10 +2233,11 @@ async function settleLawfulSeatAcceptedTerminalResult(admitted, authority, spec,
2169
2233
  const roleOutcome = await closedLedgerOutcome(admitted, spec.role, scope);
2170
2234
  if (roleOutcome !== undefined && (thisAttemptHasSeatSuccess || residual === undefined)) {
2171
2235
  const navigator = extractNavigatorFact(entries);
2236
+ const artifacts = await publishSeatAcceptedArtifacts(admitted, roleOutcome, coordinates);
2172
2237
  return withSubmissions(await withOptionalGateProjection({
2173
2238
  roleOutcome,
2174
2239
  navigator,
2175
- artifacts: [],
2240
+ artifacts,
2176
2241
  runId: admitted.runId,
2177
2242
  }, sessionDirectory, detourGateContext(admitted, scope)), submissions);
2178
2243
  }
@@ -2433,42 +2498,31 @@ export function extractReviewerMethodInvocations(entries, options) {
2433
2498
  * observation without ambient home Skill paths.
2434
2499
  */
2435
2500
  export async function publishReviewerArtifacts(admitted, roleOutcome, coordinates, options) {
2436
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
2437
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
2438
- const reportPath = join(artifactsDir, "report.json");
2439
- const evidencePath = join(artifactsDir, "evidence.json");
2440
- await writeFile(reportPath, `${JSON.stringify({
2441
- role: "reviewer",
2442
- runId: admitted.runId,
2443
- outcome: roleOutcome,
2444
- }, null, 2)}\n`, "utf8");
2445
- await writeFile(evidencePath, `${JSON.stringify({
2446
- runId: admitted.runId,
2447
- role: "reviewer",
2448
- sessionDirectory: coordinates.sessionDirectory,
2449
- sessionFile: coordinates.sessionFile,
2450
- admittedRequestPath: admitted.admittedRequestPath,
2451
- baseRevision: admitted.baseRevision,
2452
- lens: admitted.lens,
2453
- authorityRefs: [...admitted.authorityRefs],
2454
- ...(admitted.instructionEmpty
2455
- ? {}
2456
- : { callerProvenance: admitted.instruction }),
2457
- attachments: admitted.attachments.map((a) => ({
2458
- provenancePath: a.provenancePath,
2459
- frozenPath: a.frozenPath,
2460
- sha256: a.sha256,
2461
- byteLength: a.byteLength,
2462
- })),
2463
- methodProvenance: options.methodProvenance,
2464
- // Forced package method: availability is package-bound; expansion only when observed.
2465
- methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
2466
- methodInvocations: options.methodInvocations ?? [],
2467
- }, null, 2)}\n`, "utf8");
2468
- return [
2469
- { kind: "report", path: reportPath },
2470
- { kind: "evidence", path: evidencePath },
2471
- ];
2501
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
2502
+ report: {
2503
+ role: "reviewer",
2504
+ runId: admitted.runId,
2505
+ outcome: roleOutcome,
2506
+ },
2507
+ evidence: {
2508
+ runId: admitted.runId,
2509
+ role: "reviewer",
2510
+ sessionDirectory: coordinates.sessionDirectory,
2511
+ sessionFile: coordinates.sessionFile,
2512
+ admittedRequestPath: admitted.admittedRequestPath,
2513
+ baseRevision: admitted.baseRevision,
2514
+ lens: admitted.lens,
2515
+ authorityRefs: [...admitted.authorityRefs],
2516
+ ...(admitted.instructionEmpty
2517
+ ? {}
2518
+ : { callerProvenance: admitted.instruction }),
2519
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
2520
+ methodProvenance: options.methodProvenance,
2521
+ // Forced package method: availability is package-bound; expansion only when observed.
2522
+ methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
2523
+ methodInvocations: options.methodInvocations ?? [],
2524
+ },
2525
+ });
2472
2526
  }
2473
2527
  async function settleLawfulReviewerTerminalResult(admitted, authority, options, scope) {
2474
2528
  const sealed = await sealedLedgerOutcome(admitted, "reviewer", scope);
@@ -2538,37 +2592,26 @@ export function extractMergerMethodInvocations(entries, options) {
2538
2592
  * adapter-derived mechanical envelope facts without ambient home Skill paths.
2539
2593
  */
2540
2594
  export async function publishMergerArtifacts(admitted, roleOutcome, coordinates, options) {
2541
- await appendRunAttemptHistory({ role: admitted.role, runId: admitted.runId, sessionFile: coordinates.sessionFile }, roleOutcome);
2542
- const artifactsDir = await ensureRunArtifactsDir(admitted.runDirectory);
2543
- const reportPath = join(artifactsDir, "report.json");
2544
- const evidencePath = join(artifactsDir, "evidence.json");
2545
- await writeFile(reportPath, `${JSON.stringify({
2546
- role: "merger",
2547
- runId: admitted.runId,
2548
- outcome: roleOutcome,
2549
- }, null, 2)}\n`, "utf8");
2550
- await writeFile(evidencePath, `${JSON.stringify({
2551
- runId: admitted.runId,
2552
- role: "merger",
2553
- sessionDirectory: coordinates.sessionDirectory,
2554
- sessionFile: coordinates.sessionFile,
2555
- admittedRequestPath: admitted.admittedRequestPath,
2556
- mergerInputPath: admitted.mergerInputPath,
2557
- derived: admitted.derived,
2558
- attachments: admitted.attachments.map((a) => ({
2559
- provenancePath: a.provenancePath,
2560
- frozenPath: a.frozenPath,
2561
- sha256: a.sha256,
2562
- byteLength: a.byteLength,
2563
- })),
2564
- methodProvenance: options.methodProvenance,
2565
- methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
2566
- methodInvocations: options.methodInvocations ?? [],
2567
- }, null, 2)}\n`, "utf8");
2568
- return [
2569
- { kind: "report", path: reportPath },
2570
- { kind: "evidence", path: evidencePath },
2571
- ];
2595
+ return publishAcceptedTerminalArtifacts(admitted, roleOutcome, coordinates, {
2596
+ report: {
2597
+ role: "merger",
2598
+ runId: admitted.runId,
2599
+ outcome: roleOutcome,
2600
+ },
2601
+ evidence: {
2602
+ runId: admitted.runId,
2603
+ role: "merger",
2604
+ sessionDirectory: coordinates.sessionDirectory,
2605
+ sessionFile: coordinates.sessionFile,
2606
+ admittedRequestPath: admitted.admittedRequestPath,
2607
+ mergerInputPath: admitted.mergerInputPath,
2608
+ derived: admitted.derived,
2609
+ attachments: acceptedArtifactAttachmentRefs(admitted.attachments),
2610
+ methodProvenance: options.methodProvenance,
2611
+ methodInvocationObserved: (options.methodInvocations ?? []).length > 0,
2612
+ methodInvocations: options.methodInvocations ?? [],
2613
+ },
2614
+ });
2572
2615
  }
2573
2616
  async function settleLawfulMergerTerminalResult(admitted, authority, options, scope) {
2574
2617
  const coordinates = coordinatesFromAdmitted(authority, admitted);
@@ -2710,6 +2753,17 @@ export async function publishFailureArtifacts(admitted, failure, authority) {
2710
2753
  const { sessionDirectory, sessionFile } = coordinatesFromAdmitted(authority, admitted);
2711
2754
  const { baseDir, attempt: baseAttempt } = await resolveFailureArtifactsBase(admitted.runDirectory);
2712
2755
  const priorIssues = baseAttempt === undefined ? [] : [baseAttempt];
2756
+ // #953: failure face must not leave a prior success report as the durable view.
2757
+ // Clear failure must not strand the original controlled failure outside
2758
+ // settlement — same as history failure below, it rides publicationIssues
2759
+ // and unique fallback still places the durable error (locked artifacts/ +
2760
+ // prior face must not abort publishFailureArtifacts).
2761
+ try {
2762
+ await clearOppositeTerminalArtifactFace(admitted.runDirectory);
2763
+ }
2764
+ catch (error) {
2765
+ priorIssues.push(publicationAttemptFromError(roleRunArtifactsDirectory(admitted.runDirectory), error));
2766
+ }
2713
2767
  // #419: each attempt's complete failure result joins the appended history
2714
2768
  // before any fixed-name artifact view is rewritten. History failure must not
2715
2769
  // strand the original controlled failure outside settlement — it rides
@@ -2803,32 +2857,47 @@ export async function settleFailureTerminalResult(admitted, failure, authority,
2803
2857
  const lifecycleEntry = entries.slice(attemptStart).reverse().find((entry) => entry.customType === NO_RECEIPT_LIFECYCLE_ENTRY_TYPE || entry.message?.customType === NO_RECEIPT_LIFECYCLE_ENTRY_TYPE);
2804
2858
  const raw = lifecycleEntry?.data ?? lifecycleEntry?.message?.details;
2805
2859
  if (raw !== undefined) {
2860
+ // Catch only covers lifecycle-byte parse. Clear / navigator / gate I/O
2861
+ // after a valid parse must keep their real failure identity (#953).
2862
+ let facts;
2806
2863
  try {
2807
- const facts = parseNoReceiptLifecycleFacts(raw);
2808
- if (facts.runPointer === admitted.runDirectory && facts.attemptPointer === `current:${admitted.runDirectory}`) {
2809
- let decisiveFacts = facts;
2810
- // #676 D1/J3: project durable bind-target rejection onto public no_receipt facts.
2811
- if (admitted.role === "collector") {
2812
- const bindRejection = extractCollectorTargetBindRejection(entries.slice(attemptStart));
2813
- if (bindRejection !== undefined) {
2814
- decisiveFacts = {
2815
- ...facts,
2816
- targetBindRejected: true,
2817
- targetBindDiagnostic: bindRejection.diagnostic,
2818
- ...(bindRejection.code === undefined ? {} : { targetBindCode: bindRejection.code }),
2819
- };
2820
- }
2864
+ facts = parseNoReceiptLifecycleFacts(raw);
2865
+ }
2866
+ catch {
2867
+ /* malformed lifecycle bytes remain the existing nonzero output failure */
2868
+ }
2869
+ if (facts !== undefined &&
2870
+ facts.runPointer === admitted.runDirectory &&
2871
+ facts.attemptPointer === `current:${admitted.runDirectory}`) {
2872
+ let decisiveFacts = facts;
2873
+ // #676 D1/J3: project durable bind-target rejection onto public no_receipt facts.
2874
+ if (admitted.role === "collector") {
2875
+ const bindRejection = extractCollectorTargetBindRejection(entries.slice(attemptStart));
2876
+ if (bindRejection !== undefined) {
2877
+ decisiveFacts = {
2878
+ ...facts,
2879
+ targetBindRejected: true,
2880
+ targetBindDiagnostic: bindRejection.diagnostic,
2881
+ ...(bindRejection.code === undefined ? {} : { targetBindCode: bindRejection.code }),
2882
+ };
2821
2883
  }
2822
- // #478: no_receipt is still a public Terminal — project accepted gate facts.
2823
- return withOptionalGateProjection({
2824
- roleOutcome: { kind: "no_receipt", role: admitted.role, status: "no-accepted-receipt", ...facts, decisiveFacts },
2825
- navigator: await extractNavigatorFactFromAdmittedSession(sessionFile),
2826
- artifacts: [],
2827
- runId: admitted.runId,
2828
- }, sessionDirectory, detourGateContext(admitted, options));
2829
2884
  }
2885
+ // #478: no_receipt is still a public Terminal — project accepted gate facts.
2886
+ // #953: empty public terminal face — clear every reader-adoptable prior face.
2887
+ await clearOppositeTerminalArtifactFace(admitted.runDirectory);
2888
+ return withOptionalGateProjection({
2889
+ roleOutcome: {
2890
+ kind: "no_receipt",
2891
+ role: admitted.role,
2892
+ status: "no-accepted-receipt",
2893
+ ...facts,
2894
+ decisiveFacts,
2895
+ },
2896
+ navigator: await extractNavigatorFactFromAdmittedSession(sessionFile),
2897
+ artifacts: [],
2898
+ runId: admitted.runId,
2899
+ }, sessionDirectory, detourGateContext(admitted, options));
2830
2900
  }
2831
- catch { /* malformed lifecycle bytes remain the existing nonzero output failure */ }
2832
2901
  }
2833
2902
  }
2834
2903
  }