@akagilnc/pi-workflow-roles 0.1.2135 → 0.1.2146

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 (39) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/compliance-transport.js +19 -24
  4. package/dist/doctor-contracts.js +10 -2
  5. package/dist/engine-detour-tool.js +5 -15
  6. package/dist/engine-detour.js +3 -11
  7. package/dist/engine-labor-fallback.js +129 -114
  8. package/dist/evidence-child-executor.js +3 -4
  9. package/dist/merger-contracts.js +9 -2
  10. package/dist/navigator-attendance.js +11 -4
  11. package/dist/package-contracts/judge-output.js +8 -1
  12. package/dist/package-contracts/reviewer-output.js +9 -1
  13. package/dist/package-contracts/terminating-tools.js +14 -5
  14. package/dist/public-cli/main.js +123 -125
  15. package/package.json +1 -1
  16. package/resources/engines/codex.md +3 -4
  17. package/resources/engines/grok-4.6.md +5 -6
  18. package/resources/engines/kimi.md +2 -2
  19. package/resources/engines/opus.md +2 -2
  20. package/scripts/build-package.mjs +2 -0
  21. package/src/collector-role.ts +5 -33
  22. package/src/compliance-transport.ts +19 -24
  23. package/src/doctor-contracts.ts +19 -3
  24. package/src/engine-detour-tool.ts +6 -21
  25. package/src/engine-detour.ts +3 -15
  26. package/src/engine-labor-fallback.ts +100 -2
  27. package/src/evidence-child-executor.ts +3 -4
  28. package/src/factory-board.ts +6 -2
  29. package/src/merger-contracts.ts +18 -3
  30. package/src/navigator-attendance.ts +18 -4
  31. package/src/package-contracts/judge-output.ts +25 -2
  32. package/src/package-contracts/reviewer-output.ts +23 -2
  33. package/src/package-contracts/terminating-tools.ts +22 -6
  34. package/src/package-contracts/worker-output.ts +10 -2
  35. package/src/public-cli/settlement.ts +51 -17
  36. package/src/role-runtime.ts +8 -4
  37. package/src/taishi-metric-families/acceptance-success-rework.ts +6 -3
  38. package/dist/package-owned-tool-idle.js +0 -223
  39. package/src/package-owned-tool-idle.ts +0 -257
@@ -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,8 +27,9 @@ import {
27
27
  createEngineLaborFallbackLatch,
28
28
  installActivationEngineLaborFallbackLatch,
29
29
  restoreEngineLaborFallbackFromSessionEntries,
30
+ seatFallbackBaseStatus,
31
+ seatFallbackStatusHasLawfulEvidence,
30
32
  } from "./engine-labor-fallback.ts";
31
- import { installPackageOwnedToolRegistration } from "./package-owned-tool-idle.ts";
32
33
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
33
34
  import { createOAuthKeepalive, type OAuthKeepaliveOptions } from "./oauth-keepalive.ts";
34
35
 
@@ -565,7 +566,12 @@ export function publicNavigatorSettlement(role: string, phase: NavigatorPhase, e
565
566
  const status = typeof details.status === "string"
566
567
  ? details.status
567
568
  : typeof details.judgeStatus === "string" ? details.judgeStatus : undefined;
568
- 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") {
569
575
  return { kind: "human_decision", role, phase, status };
570
576
  }
571
577
  return { kind: "accepted", role, phase, ...(status === undefined ? {} : { status }) };
@@ -575,8 +581,6 @@ export function createRoleRuntimeExtension(
575
581
  dependencies: RoleRuntimeDependencies,
576
582
  ): (pi: ExtensionAPI) => void {
577
583
  return (pi) => {
578
- // #102: one shared package-owned tool registration surface for all role runtimes.
579
- installPackageOwnedToolRegistration(pi);
580
584
  pi.registerFlag(ROLE_FLAG.name, ROLE_FLAG.definition);
581
585
  // Reviewer transport flags: shared envelope owns registration (ADR 0018).
582
586
  for (const flag of REVIEWER_TRANSPORT_FLAGS) {
@@ -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 {
@@ -1,223 +0,0 @@
1
- /**
2
- * #102 package-owned tool idle backstop.
3
- *
4
- * Fixed 183000ms silence clock on package-owned tool execute only.
5
- * Real producing onUpdate resets; final resolve/reject clears; timeout throws so
6
- * Pi settles the current call as an LLM-visible isError tool result. No retry,
7
- * role failure, process termination, config, or Pi built-in coverage.
8
- *
9
- * #339: do not name-exempt whole terminating tools. Outer idle stays armed for
10
- * pre/post-audit work. Only the real compliance-audit await suspends this single
11
- * layer (ADR 0059 owns that interval); resume re-arms the same outer backstop.
12
- *
13
- * #380: on idle, abort a derived tool signal (not the caller signal) so detour
14
- * spawn cleanup and softFail can run through the existing AbortSignal path.
15
- * Cooperative soft-settle may win; non-listeners still hard-reject after a
16
- * microtask drain (no second timer).
17
- */
18
- import { AsyncLocalStorage } from "node:async_hooks";
19
- import { DEFAULT_STREAM_IDLE_TIMEOUT_MS, createStreamIdleGuard, } from "./stream-idle-guard.js";
20
- import { isProducingToolUpdate } from "./tool-execution-observation.js";
21
- export const PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS = DEFAULT_STREAM_IDLE_TIMEOUT_MS;
22
- export const PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE = "AK_PACKAGE_OWNED_TOOL_IDLE_TIMEOUT";
23
- const WRAPPED = Symbol.for("ak.packageOwnedToolIdleWrapped");
24
- /**
25
- * Active outer package-owned execute idle, if any. Nested tool executes install
26
- * their own store; compliance audit only suspends the store visible at await time.
27
- */
28
- const packageOwnedToolIdleScope = new AsyncLocalStorage();
29
- export class PackageOwnedToolIdleTimeoutError extends Error {
30
- code = PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE;
31
- idleTimeoutMs = PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS;
32
- constructor() {
33
- super(`package-owned tool idle timeout after ${PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS}ms`);
34
- this.name = "PackageOwnedToolIdleTimeoutError";
35
- }
36
- }
37
- export function isPackageOwnedToolIdleTimeoutError(value) {
38
- return (value instanceof PackageOwnedToolIdleTimeoutError ||
39
- (typeof value === "object" &&
40
- value !== null &&
41
- value.name === "PackageOwnedToolIdleTimeoutError" &&
42
- value.code === PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_CODE));
43
- }
44
- /**
45
- * Package-tool activity includes content production and host-only details
46
- * progress. Keep the observation-plane oracle separate: its stderr heartbeat
47
- * contract remains content-driven. Pi's known execute-entry placeholder
48
- * (`content: [], details: undefined`) is not activity.
49
- */
50
- function isPackageOwnedToolActivityUpdate(partialResult) {
51
- if (isProducingToolUpdate(partialResult))
52
- return true;
53
- if (typeof partialResult !== "object" || partialResult === null)
54
- return false;
55
- const details = partialResult.details;
56
- if (details === undefined || details === null)
57
- return false;
58
- if (typeof details === "string")
59
- return details.length > 0;
60
- if (Array.isArray(details))
61
- return details.length > 0;
62
- if (typeof details === "object")
63
- return Reflect.ownKeys(details).length > 0;
64
- return true;
65
- }
66
- /**
67
- * #339: suspend the active package-owned execute idle for one real compliance
68
- * audit await. Single-layer only — production has one runComplianceAudit seam.
69
- * No-op outside a wrapped package-owned execute. No second timeout or retry.
70
- */
71
- export async function withPackageOwnedToolIdleSuspended(run) {
72
- const scope = packageOwnedToolIdleScope.getStore();
73
- if (scope === undefined)
74
- return run();
75
- scope.suspend();
76
- try {
77
- return await run();
78
- }
79
- finally {
80
- scope.resume();
81
- }
82
- }
83
- /**
84
- * Touch the active package-owned execute idle clock (reuse stream-idle-guard poke).
85
- * No-op outside a wrapped package-owned execute or while that layer is suspended.
86
- * Used by long-running tools (engine detour) that see real subprocess output bytes.
87
- */
88
- export function pokePackageOwnedToolIdle() {
89
- packageOwnedToolIdleScope.getStore()?.poke();
90
- }
91
- /**
92
- * Single shared execute wrapper for package-owned tool definitions.
93
- * Idempotent: wrapping twice returns the same protected definition.
94
- */
95
- export function wrapPackageOwnedToolDefinition(tool) {
96
- // Mark the execute function, not the tool object — callers may spread tool fields
97
- // onto a new definition with a different execute (e.g. auditor customTools).
98
- if (tool.execute[WRAPPED] === true)
99
- return tool;
100
- const originalExecute = tool.execute.bind(tool);
101
- const wrappedExecute = function packageOwnedToolIdleExecute(...args) {
102
- const parentSignal = args[2];
103
- const onUpdate = args[3];
104
- return new Promise((resolve, reject) => {
105
- let settled = false;
106
- let suspended = false;
107
- let idle = createStreamIdleGuard({
108
- idleTimeoutMs: PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS,
109
- });
110
- // Derived tool signal: caller cancel forwards; idle aborts this only (not parent).
111
- const toolController = new AbortController();
112
- const onParentAbort = () => {
113
- if (toolController.signal.aborted)
114
- return;
115
- toolController.abort(parentSignal?.reason);
116
- };
117
- if (parentSignal !== undefined) {
118
- if (parentSignal.aborted) {
119
- toolController.abort(parentSignal.reason);
120
- }
121
- else {
122
- parentSignal.addEventListener("abort", onParentAbort);
123
- }
124
- }
125
- const cleanup = () => {
126
- idle.signal.removeEventListener("abort", onIdle);
127
- idle.dispose();
128
- parentSignal?.removeEventListener("abort", onParentAbort);
129
- };
130
- const settle = (deliver) => {
131
- if (settled)
132
- return;
133
- settled = true;
134
- cleanup();
135
- deliver();
136
- };
137
- const onIdle = () => {
138
- const idleError = new PackageOwnedToolIdleTimeoutError();
139
- if (!toolController.signal.aborted) {
140
- toolController.abort(idleError);
141
- }
142
- // Cooperative tools (detour) soft-settle via the aborted signal in the
143
- // next microtasks. Non-listeners still hard-reject after a short drain
144
- // of those microtasks — no second wall-clock timer.
145
- void Promise.resolve()
146
- .then(() => { })
147
- .then(() => { })
148
- .then(() => {
149
- settle(() => reject(idleError));
150
- });
151
- };
152
- idle.signal.addEventListener("abort", onIdle, { once: true });
153
- const suspension = {
154
- suspend() {
155
- if (settled || suspended)
156
- return;
157
- suspended = true;
158
- // ADR 0059 owns the audit interval — drop this layer until audit returns.
159
- idle.signal.removeEventListener("abort", onIdle);
160
- idle.dispose();
161
- },
162
- resume() {
163
- if (settled || !suspended)
164
- return;
165
- suspended = false;
166
- // Fresh single-layer silence window for post-audit work (e.g. cleanup).
167
- idle = createStreamIdleGuard({
168
- idleTimeoutMs: PACKAGE_OWNED_TOOL_IDLE_TIMEOUT_MS,
169
- });
170
- idle.signal.addEventListener("abort", onIdle, { once: true });
171
- },
172
- poke() {
173
- if (settled || suspended)
174
- return;
175
- idle.poke();
176
- },
177
- };
178
- const guardedOnUpdate = onUpdate === undefined
179
- ? undefined
180
- : (partialResult) => {
181
- if (settled)
182
- return;
183
- if (isPackageOwnedToolActivityUpdate(partialResult))
184
- idle.poke();
185
- onUpdate(partialResult);
186
- };
187
- const callArgs = args.slice();
188
- // Pass derived signal so idle/cancel can clean up subprocesses without
189
- // aborting the caller/parent agent signal identity.
190
- callArgs[2] = toolController.signal;
191
- callArgs[3] = guardedOnUpdate;
192
- void packageOwnedToolIdleScope.run(suspension, async () => {
193
- try {
194
- const result = await originalExecute(...callArgs);
195
- settle(() => resolve(result));
196
- }
197
- catch (error) {
198
- settle(() => reject(error));
199
- }
200
- });
201
- });
202
- };
203
- wrappedExecute[WRAPPED] = true;
204
- return {
205
- ...tool,
206
- execute: wrappedExecute,
207
- };
208
- }
209
- /**
210
- * Install the shared registration surface on an ExtensionAPI once.
211
- * All subsequent pi.registerTool calls for package-owned tools are wrapped.
212
- */
213
- export function installPackageOwnedToolRegistration(pi) {
214
- const current = pi.registerTool;
215
- if (current[WRAPPED] === true)
216
- return;
217
- const original = current.bind(pi);
218
- const installed = ((tool) => {
219
- original(wrapPackageOwnedToolDefinition(tool));
220
- });
221
- installed[WRAPPED] = true;
222
- pi.registerTool = installed;
223
- }