@akagilnc/pi-workflow-roles 0.1.2139 → 0.1.2148

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.
@@ -8,6 +8,7 @@ import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
8
8
  import { Type, type Static } from "typebox";
9
9
  import { Value } from "typebox/value";
10
10
 
11
+ import { seatFallbackBaseStatus } from "./engine-labor-fallback.ts";
11
12
  import {
12
13
  NAVIGATOR_INVOCATION_ENTRY,
13
14
  mintNavigatorInvocationId,
@@ -461,6 +462,18 @@ export type NavigatorCandidateSelection = {
461
462
  readonly matchedToSettlement: boolean;
462
463
  };
463
464
 
465
+ /** Match route-playbook statuses against settlement, treating seat-fallback taint as base-equivalent. */
466
+ function statusListMatchesSettlement(
467
+ candidateStatuses: readonly string[],
468
+ settlementStatus: string,
469
+ ): boolean {
470
+ if (candidateStatuses.includes(settlementStatus)) return true;
471
+ const settlementBase = seatFallbackBaseStatus(settlementStatus);
472
+ return candidateStatuses.some(
473
+ (status) => seatFallbackBaseStatus(status) === settlementBase,
474
+ );
475
+ }
476
+
464
477
  export function selectNavigatorCandidate(
465
478
  candidates: readonly NavigatorCandidate[],
466
479
  settlement: NavigatorSettlement,
@@ -477,7 +490,9 @@ export function selectNavigatorCandidate(
477
490
  if (rolePhaseMatched.length > 0) {
478
491
  if (settlement.status !== undefined) {
479
492
  const statusSpecific = rolePhaseMatched.find(
480
- (candidate) => candidate.matches?.statuses?.includes(settlement.status!) === true,
493
+ (candidate) =>
494
+ candidate.matches?.statuses !== undefined &&
495
+ statusListMatchesSettlement(candidate.matches.statuses, settlement.status!),
481
496
  );
482
497
  if (statusSpecific !== undefined) {
483
498
  return { candidate: statusSpecific, matchedToSettlement: true };
@@ -1,5 +1,12 @@
1
1
  /** Package-owned Judge output leaf — no role registration surface. */
2
2
 
3
+ import {
4
+ seatFallbackBaseStatus,
5
+ seatFallbackStatusHasLawfulEvidence,
6
+ type SeatFallbackTaintedStatus,
7
+ type WithEngineLaborFallback,
8
+ } from "../engine-labor-fallback.ts";
9
+
3
10
  export const JUDGE_OUTPUT_TOOL_NAME = "ak_judge_output";
4
11
  export const JUDGE_ACCEPTED_TEXT = "Judge verdict accepted";
5
12
 
@@ -10,7 +17,7 @@ export type JudgeClass = {
10
17
  disposition: string;
11
18
  };
12
19
 
13
- export type JudgeVerdict =
20
+ type JudgeVerdictClean =
14
21
  | { judgeStatus: "converged"; note?: string; evidence?: unknown }
15
22
  | {
16
23
  judgeStatus: "continue";
@@ -26,6 +33,11 @@ export type JudgeVerdict =
26
33
  evidence?: unknown;
27
34
  };
28
35
 
36
+ /** Clean submission shape or seat-fallback tainted accepted receipt (ADR 0071). */
37
+ export type JudgeVerdict =
38
+ | JudgeVerdictClean
39
+ | WithEngineLaborFallback<JudgeVerdictClean>;
40
+
29
41
  export function validateAcceptedJudgeDetails(verdict: unknown): JudgeVerdict {
30
42
  if (verdict === null || typeof verdict !== "object" || Array.isArray(verdict)) throw new Error("Judge verdict has no execution discriminator");
31
43
  let judgeStatus: unknown;
@@ -34,6 +46,17 @@ export function validateAcceptedJudgeDetails(verdict: unknown): JudgeVerdict {
34
46
  } catch {
35
47
  throw new Error("Judge verdict has no execution discriminator");
36
48
  }
37
- if (["converged", "continue", "escalate"].includes(String(judgeStatus))) return verdict as JudgeVerdict;
49
+ if (typeof judgeStatus !== "string") {
50
+ throw new Error("Judge verdict has no execution discriminator");
51
+ }
52
+ const base = seatFallbackBaseStatus(judgeStatus) as SeatFallbackTaintedStatus<
53
+ "converged" | "continue" | "escalate"
54
+ >;
55
+ if (
56
+ ["converged", "continue", "escalate"].includes(base) &&
57
+ seatFallbackStatusHasLawfulEvidence(judgeStatus, verdict)
58
+ ) {
59
+ return verdict as JudgeVerdict;
60
+ }
38
61
  throw new Error("Judge verdict has no execution discriminator");
39
62
  }
@@ -1,5 +1,11 @@
1
1
  /** Package-owned Reviewer intent and runtime-receipt leaves — no role registration surface. */
2
2
 
3
+ import {
4
+ seatFallbackBaseStatus,
5
+ seatFallbackStatusHasLawfulEvidence,
6
+ type SeatFallbackTaintedStatus,
7
+ type WithEngineLaborFallback,
8
+ } from "../engine-labor-fallback.ts";
3
9
  import type { ReviewerAcceptedEvidence, ReviewerFailureClassification, ReviewerWorkspaceDisposition } from "../reviewer-execution-ledger.ts";
4
10
 
5
11
  export const REVIEWER_OUTPUT_TOOL_NAME = "ak_reviewer_output";
@@ -28,7 +34,7 @@ export type RuntimeReviewerAcceptedBatch = Readonly<{
28
34
  }>;
29
35
  /** Honest Spec-child disposition on the receipt face. */
30
36
  export type RuntimeReviewerSpecDisposition = "launched" | "skipped-missing";
31
- export type RuntimeReviewerReceiptV2 = Readonly<{
37
+ type RuntimeReviewerReceiptV2Clean = Readonly<{
32
38
  version: 2;
33
39
  status: "completed" | "refused";
34
40
  diagnostic?: string;
@@ -45,6 +51,14 @@ export type RuntimeReviewerReceiptV2 = Readonly<{
45
51
  target?: ReviewerAcceptedEvidence["target"];
46
52
  }>;
47
53
  }>;
54
+ /** Clean runtime receipt or seat-fallback tainted accepted receipt (ADR 0071). */
55
+ export type RuntimeReviewerReceiptV2 =
56
+ | RuntimeReviewerReceiptV2Clean
57
+ | WithEngineLaborFallback<
58
+ Omit<RuntimeReviewerReceiptV2Clean, "status"> & {
59
+ status: SeatFallbackTaintedStatus<"completed" | "refused">;
60
+ }
61
+ >;
48
62
 
49
63
  function isRecord(value: unknown): value is Record<string, unknown> {
50
64
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -63,6 +77,11 @@ export function validateReviewerIntent(output: unknown): ReviewerIntent {
63
77
 
64
78
  /** Validate runtime-owned facts at their real identity seams (target pins + plain text). */
65
79
  export function validateRuntimeReviewerReceipt(output: unknown): RuntimeReviewerReceiptV2 {
80
+ const status = read(output, "status");
81
+ // ADR 0071: tainted top-level status requires latch-shaped engineLaborFallback evidence.
82
+ if (typeof status === "string" && !seatFallbackStatusHasLawfulEvidence(status, output)) {
83
+ throw new Error("Reviewer receipt has no recognized execution discriminator");
84
+ }
66
85
  const acceptedBatch = read(output, "acceptedBatch");
67
86
  const identities = read(output, "identities");
68
87
  const construction = read(identities, "construction");
@@ -129,7 +148,9 @@ export function validateRuntimeReviewerReceipt(output: unknown): RuntimeReviewer
129
148
  export function projectReviewerIntentToReceipt(intentValue: unknown, receiptValue: unknown): RuntimeReviewerReceiptV2 {
130
149
  const intent = validateReviewerIntent(intentValue);
131
150
  const receipt = validateRuntimeReviewerReceipt(receiptValue);
132
- if (receipt.status !== intent.status || (intent.status === "completed" ? receipt.diagnostic !== undefined : receipt.diagnostic !== intent.diagnostic)) {
151
+ const receiptStatus = String(receipt.status);
152
+ const receiptBase = seatFallbackBaseStatus(receiptStatus);
153
+ if (receiptBase !== intent.status || (intent.status === "completed" ? receipt.diagnostic !== undefined : receipt.diagnostic !== intent.diagnostic)) {
133
154
  throw new Error("Reviewer intent and runtime receipt disagree");
134
155
  }
135
156
  return receipt;
@@ -25,6 +25,10 @@ import {
25
25
  type RuntimeReviewerReceiptV2,
26
26
  } from "./reviewer-output.ts";
27
27
  import { isAuditEscalationResult } from "../audit-escalation.ts";
28
+ import {
29
+ seatFallbackBaseStatus,
30
+ seatFallbackStatusHasLawfulEvidence,
31
+ } from "../engine-labor-fallback.ts";
28
32
  import { DOCTOR_OUTPUT_TOOL_NAME, validateDoctorSubmissionShape, validateRecordedDoctorOutput, type DoctorOutput, type DoctorSubmission } from "../doctor-contracts.ts";
29
33
  import { MERGER_ACCEPTED_TEXT, MERGER_OUTPUT_TOOL_NAME, validateMergerOutput, type MergerOutput } from "../merger-contracts.ts";
30
34
  import {
@@ -159,10 +163,19 @@ export function validateAcceptedDetails(
159
163
  [MERGER_OUTPUT_TOOL_NAME]: ["completed", "escalate"],
160
164
  };
161
165
  const collectorDiscriminator = toolName === COLLECTOR_OUTPUT_TOOL && Array.isArray(candidate?.groups);
166
+ const baseDiscriminator = typeof discriminator === "string" ? seatFallbackBaseStatus(discriminator) : discriminator;
167
+ // ADR 0071: `-by-fallback` is not independently lawful — require latch-shaped evidence.
168
+ const taintedWithoutEvidence =
169
+ typeof discriminator === "string" &&
170
+ !seatFallbackStatusHasLawfulEvidence(discriminator, details);
162
171
  const runtimeBindingMissing =
163
- (toolName === DOCTOR_OUTPUT_TOOL_NAME && discriminator === "completed" && !(candidate?.cost !== null && typeof candidate?.cost === "object")) ||
172
+ (toolName === DOCTOR_OUTPUT_TOOL_NAME && baseDiscriminator === "completed" && !(candidate?.cost !== null && typeof candidate?.cost === "object")) ||
164
173
  (toolName === REVIEWER_OUTPUT_TOOL_NAME && candidate?.version !== 2);
165
- if (runtimeBindingMissing || (!collectorDiscriminator && (typeof discriminator !== "string" || !lawfulStatuses[toolName].includes(discriminator)))) {
174
+ if (
175
+ taintedWithoutEvidence ||
176
+ runtimeBindingMissing ||
177
+ (!collectorDiscriminator && (typeof discriminator !== "string" || !lawfulStatuses[toolName].includes(baseDiscriminator as string)))
178
+ ) {
166
179
  throw new AcceptedDetailsContractError("terminating receipt has no recognized execution discriminator");
167
180
  }
168
181
  try {
@@ -196,12 +209,14 @@ export function validateAcceptedLifecycle(
196
209
  const details = validateAcceptedDetails(toolName, detailsValue);
197
210
  if (toolName === DOCTOR_OUTPUT_TOOL_NAME) {
198
211
  const testimony = validateDoctorSubmissionShape(argumentsValue);
199
- if (testimony.status === "refused") {
212
+ if (seatFallbackBaseStatus(String(testimony.status)) === "refused") {
200
213
  if (!deepEqual(testimony, details)) throw new Error("accepted tool lifecycle details mismatch");
201
214
  return details;
202
215
  }
203
- const receipt = details as DoctorOutput;
204
- if (receipt.status !== "completed") throw new Error("accepted tool lifecycle details mismatch");
216
+ const receipt = details as DoctorOutput & { cost?: unknown };
217
+ if (seatFallbackBaseStatus(String(receipt.status)) !== "completed") {
218
+ throw new Error("accepted tool lifecycle details mismatch");
219
+ }
205
220
  const { cost: _runtimeCost, ...projected } = receipt;
206
221
  if (!deepEqual(testimony, projected)) throw new Error("accepted tool lifecycle details mismatch");
207
222
  return details;
@@ -226,7 +241,8 @@ export function acceptedFacts(toolName: TerminatingToolName, details: AcceptedDe
226
241
  case JUDGE_OUTPUT_TOOL_NAME: return { status: (details as { judgeStatus: string }).judgeStatus };
227
242
  case MERGER_OUTPUT_TOOL_NAME: {
228
243
  const output = details as unknown as Record<string, unknown>;
229
- return { status: output.status as string, ...(output.status === "completed" && typeof output.mergeCommitId === "string" ? { commit: output.mergeCommitId } : {}) };
244
+ const status = output.status as string;
245
+ return { status, ...(seatFallbackBaseStatus(status) === "completed" && typeof output.mergeCommitId === "string" ? { commit: output.mergeCommitId } : {}) };
230
246
  }
231
247
  case COLLECTOR_OUTPUT_TOOL:
232
248
  return { status: "collected" };
@@ -15,16 +15,24 @@ export type {
15
15
  } from "./fixer-output.ts";
16
16
  export { fixerPrerequisiteSchema, fixerPrerequisitesSchema, parseFixerPrerequisites, validateFixerPrerequisites } from "./fixer-packet.ts";
17
17
  export type { FixerInvocationInput, FixerPrerequisite } from "./fixer-packet.ts";
18
+ import type { WithEngineLaborFallback } from "../engine-labor-fallback.ts";
18
19
  import { validateFixerOutput, type FixerOutput } from "./fixer-output.ts";
19
20
 
20
21
  export const CODER_OUTPUT_TOOL_NAME = "ak_coder_output";
21
22
  export const CODER_ACCEPTED_TEXT = "Coder report accepted";
22
23
  export type WorkerRoleLabel = "Coder" | "Fixer";
23
- export type CoderOutput =
24
+ type CoderOutputClean =
24
25
  | { status: "planned"; report: string }
25
26
  | { status: "completed" | "refused"; report: string }
26
27
  | { status: "unfinished"; report: string; remainingScope: string; reason?: string };
27
- export type WorkerOutput = CoderOutput | FixerOutput;
28
+ /** Clean submission shape or seat-fallback tainted accepted receipt (ADR 0071). */
29
+ export type CoderOutput =
30
+ | CoderOutputClean
31
+ | WithEngineLaborFallback<CoderOutputClean>;
32
+ export type WorkerOutput =
33
+ | CoderOutput
34
+ | FixerOutput
35
+ | WithEngineLaborFallback<FixerOutput>;
28
36
 
29
37
  export function validateAcceptedCoderDetails(output: unknown): CoderOutput {
30
38
  return output as CoderOutput;
@@ -34,7 +34,11 @@ import {
34
34
  COLLECTOR_WAIT_TOOL,
35
35
  } from "../collector-ledger.ts";
36
36
  import { ENGINE_DETOUR_TOOL_NAME } from "../engine-detour.ts";
37
- import { readEngineLaborFallbackFieldFrom } from "../engine-labor-fallback.ts";
37
+ import {
38
+ readEngineLaborFallbackFieldFrom,
39
+ seatFallbackBaseStatus,
40
+ seatFallbackStatusHasLawfulEvidence,
41
+ } from "../engine-labor-fallback.ts";
38
42
  import {
39
43
  JUDGE_OUTPUT_TOOL_NAME,
40
44
  type JudgeVerdict,
@@ -1059,7 +1063,7 @@ function auditNoReceiptDecisiveFact(candidate: object): Record<string, unknown>
1059
1063
 
1060
1064
  function judgeDecisiveFacts(
1061
1065
  verdict: object,
1062
- judgeStatus: JudgeVerdict["judgeStatus"],
1066
+ judgeStatus: string,
1063
1067
  ): Record<string, unknown> {
1064
1068
  const facts: Record<string, unknown> = {
1065
1069
  judgeStatus,
@@ -1067,7 +1071,8 @@ function judgeDecisiveFacts(
1067
1071
  // #380: spread sole-built field; do not re-key here (S1).
1068
1072
  ...readEngineLaborFallbackFieldFrom(verdict),
1069
1073
  };
1070
- if (judgeStatus === "continue") {
1074
+ const statusBase = seatFallbackBaseStatus(judgeStatus);
1075
+ if (statusBase === "continue") {
1071
1076
  const fix = safelyRead(verdict, "fix");
1072
1077
  if (fix.readable && isRecord(fix.value)) {
1073
1078
  const summary = safelyRead(fix.value, "summary");
@@ -1093,7 +1098,7 @@ function judgeDecisiveFacts(
1093
1098
  }
1094
1099
  }
1095
1100
  }
1096
- if (judgeStatus === "escalate") {
1101
+ if (statusBase === "escalate") {
1097
1102
  const gate = safelyRead(verdict, "decisionGate");
1098
1103
  if (gate.readable && isRecord(gate.value)) {
1099
1104
  const question = safelyRead(gate.value, "question");
@@ -1118,10 +1123,14 @@ function coderDecisiveFacts(output: CoderOutput): Record<string, unknown> {
1118
1123
  const status = safelyRead(candidate, "status");
1119
1124
  const facts: Record<string, unknown> = {};
1120
1125
  if (status.readable && typeof status.value === "string") facts.coderStatus = status.value;
1126
+ const statusBase =
1127
+ status.readable && typeof status.value === "string"
1128
+ ? seatFallbackBaseStatus(status.value)
1129
+ : undefined;
1121
1130
  const remainingScope = safelyRead(candidate, "remainingScope");
1122
- if (status.readable && status.value === "unfinished" && remainingScope.readable && typeof remainingScope.value === "string") facts.remainingScope = remainingScope.value;
1131
+ if (statusBase === "unfinished" && remainingScope.readable && typeof remainingScope.value === "string") facts.remainingScope = remainingScope.value;
1123
1132
  const reason = safelyRead(candidate, "reason");
1124
- if (status.readable && status.value === "unfinished" && reason.readable && typeof reason.value === "string" && reason.value.trim().length > 0) {
1133
+ if (statusBase === "unfinished" && reason.readable && typeof reason.value === "string" && reason.value.trim().length > 0) {
1125
1134
  facts.reason = reason.value;
1126
1135
  }
1127
1136
  const report = safelyRead(candidate, "report");
@@ -1134,14 +1143,18 @@ function fixerDecisiveFacts(output: FixerOutput): Record<string, unknown> {
1134
1143
  const status = safelyRead(candidate, "status");
1135
1144
  const facts: Record<string, unknown> = {};
1136
1145
  if (status.readable && typeof status.value === "string") facts.fixerStatus = status.value;
1146
+ const statusBase =
1147
+ status.readable && typeof status.value === "string"
1148
+ ? seatFallbackBaseStatus(status.value)
1149
+ : undefined;
1137
1150
  const remainingScope = safelyRead(candidate, "remainingScope");
1138
- if (status.readable && (status.value === "unfinished" || status.value === "refused") && remainingScope.readable && typeof remainingScope.value === "string") facts.remainingScope = remainingScope.value;
1151
+ if ((statusBase === "unfinished" || statusBase === "refused") && remainingScope.readable && typeof remainingScope.value === "string") facts.remainingScope = remainingScope.value;
1139
1152
  const reason = safelyRead(candidate, "reason");
1140
- if (status.readable && status.value === "unfinished" && reason.readable && typeof reason.value === "string" && reason.value.trim().length > 0) {
1153
+ if (statusBase === "unfinished" && reason.readable && typeof reason.value === "string" && reason.value.trim().length > 0) {
1141
1154
  facts.reason = reason.value;
1142
1155
  }
1143
1156
  const blockerRead = safelyRead(candidate, "blocker");
1144
- if (status.readable && status.value === "refused" && blockerRead.readable && isRecord(blockerRead.value)) {
1157
+ if (statusBase === "refused" && blockerRead.readable && isRecord(blockerRead.value)) {
1145
1158
  const cause = safelyRead(blockerRead.value, "cause");
1146
1159
  if (cause.readable && typeof cause.value === "string") facts.blockerCause = cause.value;
1147
1160
  const prerequisiteId = safelyRead(blockerRead.value, "prerequisiteId");
@@ -1223,7 +1236,11 @@ function doctorDecisiveFacts(output: DoctorOutput): Record<string, unknown> {
1223
1236
  const status = safelyRead(candidate, "status");
1224
1237
  const facts: Record<string, unknown> = { ...auditNoReceiptDecisiveFact(candidate) };
1225
1238
  if (status.readable && typeof status.value === "string") facts.doctorStatus = status.value;
1226
- if (status.readable && status.value === "refused") {
1239
+ const statusBase =
1240
+ status.readable && typeof status.value === "string"
1241
+ ? seatFallbackBaseStatus(status.value)
1242
+ : undefined;
1243
+ if (statusBase === "refused") {
1227
1244
  const reason = safelyRead(candidate, "reason");
1228
1245
  if (reason.readable && reason.value !== undefined) facts.reason = reason.value;
1229
1246
  const missing = safelyRead(candidate, "missingEvidence");
@@ -1277,7 +1294,11 @@ function reviewerDecisiveFacts(
1277
1294
  facts.specDisposition = specDisposition.value;
1278
1295
  }
1279
1296
  const diagnostic = safelyRead(candidate, "diagnostic");
1280
- if (status.readable && status.value === "refused" && diagnostic.readable) {
1297
+ const statusBase =
1298
+ status.readable && typeof status.value === "string"
1299
+ ? seatFallbackBaseStatus(status.value)
1300
+ : undefined;
1301
+ if (statusBase === "refused" && diagnostic.readable) {
1281
1302
  facts.diagnosticPresent = typeof diagnostic.value === "string" && diagnostic.value.trim().length > 0;
1282
1303
  }
1283
1304
  return facts;
@@ -1980,9 +2001,12 @@ export function extractJudgeRoleOutcome(
1980
2001
  // must not become a second verdict-shape gate (ADR 0040).
1981
2002
  if (!isRecord(details)) continue;
1982
2003
  const statusRead = safelyRead(details, "judgeStatus");
1983
- if (!statusRead.readable) continue;
2004
+ if (!statusRead.readable || typeof statusRead.value !== "string") continue;
1984
2005
  const judgeStatus = statusRead.value;
1985
- if (judgeStatus !== "converged" && judgeStatus !== "continue" && judgeStatus !== "escalate") continue;
2006
+ const statusBase = seatFallbackBaseStatus(judgeStatus);
2007
+ if (statusBase !== "converged" && statusBase !== "continue" && statusBase !== "escalate") continue;
2008
+ // ADR 0071: `-by-fallback` without latch-shaped evidence is not a lawful terminal.
2009
+ if (!seatFallbackStatusHasLawfulEvidence(judgeStatus, details)) continue;
1986
2010
  return {
1987
2011
  kind: "accepted",
1988
2012
  role: "judge",
@@ -2985,13 +3009,19 @@ async function settleLawfulDoctorTerminalResult(
2985
3009
  const extracted = extractDoctorRoleOutcome(entries);
2986
3010
  if (extracted === undefined) return undefined;
2987
3011
  // Bind completed receipt case identity to the admitted Issue evidence case.
3012
+ // Seat-fallback may taint status; base semantics still require case binding.
2988
3013
  if (
2989
3014
  extracted.output !== undefined &&
2990
- extracted.output.status === "completed"
3015
+ seatFallbackBaseStatus(String(extracted.output.status)) === "completed"
2991
3016
  ) {
3017
+ const completedCase = (
3018
+ extracted.output as {
3019
+ case: { issueNumber: number; runsPath: string };
3020
+ }
3021
+ ).case;
2992
3022
  if (
2993
- extracted.output.case.issueNumber !== admitted.caseIdentity.issueNumber ||
2994
- extracted.output.case.runsPath !== admitted.caseIdentity.runsPath
3023
+ completedCase.issueNumber !== admitted.caseIdentity.issueNumber ||
3024
+ completedCase.runsPath !== admitted.caseIdentity.runsPath
2995
3025
  ) {
2996
3026
  const error = new Error(
2997
3027
  "Doctor receipt case identity does not match admitted case identity",
@@ -3331,7 +3361,11 @@ function mergerDecisiveFacts(output: MergerOutput): Record<string, unknown> {
3331
3361
  const attemptId = safelyRead(candidate, "attemptId");
3332
3362
  if (status.readable && typeof status.value === "string") facts.mergerStatus = status.value;
3333
3363
  if (attemptId.readable && attemptId.value !== undefined) facts.attemptId = attemptId.value;
3334
- const decisiveKey = status.readable && status.value === "completed" ? "mergeCommitId" : "diagnosis";
3364
+ const statusBase =
3365
+ status.readable && typeof status.value === "string"
3366
+ ? seatFallbackBaseStatus(status.value)
3367
+ : undefined;
3368
+ const decisiveKey = statusBase === "completed" ? "mergeCommitId" : "diagnosis";
3335
3369
  const decisive = safelyRead(candidate, decisiveKey);
3336
3370
  if (decisive.readable && decisive.value !== undefined) facts[decisiveKey] = decisive.value;
3337
3371
  return facts;
@@ -27,6 +27,8 @@ import {
27
27
  createEngineLaborFallbackLatch,
28
28
  installActivationEngineLaborFallbackLatch,
29
29
  restoreEngineLaborFallbackFromSessionEntries,
30
+ seatFallbackBaseStatus,
31
+ seatFallbackStatusHasLawfulEvidence,
30
32
  } from "./engine-labor-fallback.ts";
31
33
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
32
34
  import { createOAuthKeepalive, type OAuthKeepaliveOptions } from "./oauth-keepalive.ts";
@@ -564,7 +566,12 @@ export function publicNavigatorSettlement(role: string, phase: NavigatorPhase, e
564
566
  const status = typeof details.status === "string"
565
567
  ? details.status
566
568
  : typeof details.judgeStatus === "string" ? details.judgeStatus : undefined;
567
- if (status === "escalate") {
569
+ // ADR 0071: `-by-fallback` without latch-shaped evidence is not a lawful terminal.
570
+ if (status !== undefined && !seatFallbackStatusHasLawfulEvidence(status, event.details)) {
571
+ return undefined;
572
+ }
573
+ // Seat-fallback taint keeps escalate semantics (base) while preserving the polluted token.
574
+ if (status !== undefined && seatFallbackBaseStatus(status) === "escalate") {
568
575
  return { kind: "human_decision", role, phase, status };
569
576
  }
570
577
  return { kind: "accepted", role, phase, ...(status === undefined ? {} : { status }) };
@@ -9,6 +9,7 @@
9
9
  * - success rate den = success-eligible accepted legs (no-receipt out; planned out)
10
10
  * - planned = plan-duty acceptance; never success numerator or denominator
11
11
  */
12
+ import { seatFallbackBaseStatus } from "../engine-labor-fallback.ts";
12
13
  import type { TaishiReadableRunFacts, TaishiRunTerminalFace } from "../taishi-ledger.ts";
13
14
  import { medianNumber } from "../taishi-median.ts";
14
15
  import type { TaishiMetricFamilyModule } from "../taishi-metric-family.ts";
@@ -185,8 +186,10 @@ function mapTerminal(
185
186
  };
186
187
  }
187
188
 
189
+ // Seat-fallback taint is visible on the label; acceptance/success use base semantics.
190
+ const statusBase = seatFallbackBaseStatus(status);
188
191
  const acceptedSet = ACCEPTED_STATUS[role];
189
- if (acceptedSet === undefined || !acceptedSet.has(status)) {
192
+ if (acceptedSet === undefined || !acceptedSet.has(statusBase)) {
190
193
  return {
191
194
  terminalLabel: status,
192
195
  accepted: false,
@@ -196,9 +199,9 @@ function mapTerminal(
196
199
  };
197
200
  }
198
201
 
199
- const plannedDuty = WORKER_ROLES.has(role) && status === "planned";
202
+ const plannedDuty = WORKER_ROLES.has(role) && statusBase === "planned";
200
203
  const successSet = SUCCESS_STATUS[role] ?? new Set<string>();
201
- const success = !plannedDuty && successSet.has(status);
204
+ const success = !plannedDuty && successSet.has(statusBase);
202
205
  const successEligible = !plannedDuty;
203
206
 
204
207
  return {