@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
@@ -61,10 +61,6 @@ const requestSchema = collectorRequestArgsSchema;
61
61
  const waitSchema = collectorWaitArgsSchema;
62
62
  const outputSchema = collectorOutputArgsSchema;
63
63
 
64
- // Well below the fixed 183s package-tool silence clock while avoiding noisy UI
65
- // churn during Collector's legal five-minute wait.
66
- const COLLECTOR_WAIT_PROGRESS_INTERVAL_MS = 60_000;
67
-
68
64
  type RequestParams = Static<typeof requestSchema>;
69
65
  type WaitParams = Static<typeof waitSchema>;
70
66
  type OutputParams = Static<typeof outputSchema>;
@@ -377,40 +373,18 @@ export function createCollectorRoleRuntime(
377
373
  "Call ak_collector_wait with a positive durationMs; runtime caps each wait to five minutes and to remaining eligibility.",
378
374
  ],
379
375
  parameters: waitSchema,
380
- async execute(toolCallId, params: WaitParams, signal, onUpdate, ctx) {
376
+ async execute(toolCallId, params: WaitParams, signal, _onUpdate, ctx) {
381
377
  if (activation === undefined) {
382
378
  throw new Error("Collector is not activated");
383
379
  }
384
- const currentActivation = activation;
385
- let progressTimer: ReturnType<typeof setTimeout> | undefined;
386
380
  try {
387
- currentActivation.ledger.beginOperational(COLLECTOR_WAIT_TOOL, toolCallId);
388
- const waitStartedMono = currentActivation.clock.monoNow();
389
- let lastElapsedMs = 0;
390
- const scheduleProgress = (): void => {
391
- if (onUpdate === undefined) return;
392
- progressTimer = setTimeout(() => {
393
- const elapsedMs = Math.max(
394
- 0,
395
- Math.floor(currentActivation.clock.monoNow() - waitStartedMono),
396
- );
397
- if (elapsedMs > lastElapsedMs) {
398
- lastElapsedMs = elapsedMs;
399
- onUpdate({
400
- content: [],
401
- details: { kind: "collector_wait_progress", elapsedMs },
402
- });
403
- }
404
- scheduleProgress();
405
- }, COLLECTOR_WAIT_PROGRESS_INTERVAL_MS);
406
- };
407
- scheduleProgress();
408
- const details = await currentActivation.ledger.wait(
381
+ activation.ledger.beginOperational(COLLECTOR_WAIT_TOOL, toolCallId);
382
+ const details = await activation.ledger.wait(
409
383
  params,
410
- currentActivation.clock,
384
+ activation.clock,
411
385
  signal,
412
386
  );
413
- currentActivation.ledger.completeOperational(toolCallId);
387
+ activation.ledger.completeOperational(toolCallId);
414
388
  return {
415
389
  content: [{
416
390
  type: "text" as const,
@@ -420,8 +394,6 @@ export function createCollectorRoleRuntime(
420
394
  };
421
395
  } catch (error) {
422
396
  hostActions.failInfrastructure(error, ctx);
423
- } finally {
424
- if (progressTimer !== undefined) clearTimeout(progressTimer);
425
397
  }
426
398
  },
427
399
  });
@@ -7,7 +7,6 @@ import {
7
7
  } from "./evidence-child-executor.ts";
8
8
  import { createAuditorDossierTool } from "./auditor-dossier-tool.ts";
9
9
  import type { DossierObservation } from "./dossier-resolution.ts";
10
- import { withPackageOwnedToolIdleSuspended } from "./package-owned-tool-idle.ts";
11
10
  import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
12
11
 
13
12
  export type ComplianceCompletion = AuditorCompletion;
@@ -117,28 +116,24 @@ export type RunComplianceAuditOptions = {
117
116
  };
118
117
 
119
118
  export async function runComplianceAudit(options: RunComplianceAuditOptions): Promise<ComplianceDecision> {
120
- // #339: only the real compliance-audit await leaves the outer package-owned
121
- // idle owner. Pre/post-audit work stays under the single outer backstop.
122
- return withPackageOwnedToolIdleSuspended(async () => {
123
- const prompt = options.serializedInput ?? AUDITOR_DOSSIER_PROMPT;
124
- const receipt = await executeAuditorChild({
125
- tool: options.tool,
126
- dossierTool: createAuditorDossierTool(options.runDirectory),
127
- systemPrompt: options.systemPrompt,
128
- prompt,
129
- roleLabel: options.roleLabel,
130
- context: options.context,
131
- retainResponse: (response) => retainComplianceResponse(options.context, response),
132
- ...(options.runCompletion === undefined ? {} : { runCompletion: options.runCompletion }),
133
- ...(options.signal === undefined ? {} : { signal: options.signal }),
134
- });
135
- if (receipt.noReceiptLifecycle !== undefined) {
136
- return {
137
- status: "no-receipt",
138
- ...receipt.noReceiptLifecycle,
139
- ...(receipt.response.usage === undefined ? {} : { usage: receipt.response.usage }),
140
- };
141
- }
142
- return readComplianceCandidate(receipt.decision, receipt.response.usage);
119
+ const prompt = options.serializedInput ?? AUDITOR_DOSSIER_PROMPT;
120
+ const receipt = await executeAuditorChild({
121
+ tool: options.tool,
122
+ dossierTool: createAuditorDossierTool(options.runDirectory),
123
+ systemPrompt: options.systemPrompt,
124
+ prompt,
125
+ roleLabel: options.roleLabel,
126
+ context: options.context,
127
+ retainResponse: (response) => retainComplianceResponse(options.context, response),
128
+ ...(options.runCompletion === undefined ? {} : { runCompletion: options.runCompletion }),
129
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
143
130
  });
131
+ if (receipt.noReceiptLifecycle !== undefined) {
132
+ return {
133
+ status: "no-receipt",
134
+ ...receipt.noReceiptLifecycle,
135
+ ...(receipt.response.usage === undefined ? {} : { usage: receipt.response.usage }),
136
+ };
137
+ }
138
+ return readComplianceCandidate(receipt.decision, receipt.response.usage);
144
139
  }
@@ -1,5 +1,10 @@
1
1
  import { Type } from "typebox";
2
2
  import { canonicalJson } from "./canonical-json.ts";
3
+ import {
4
+ seatFallbackBaseStatus,
5
+ seatFallbackStatusHasLawfulEvidence,
6
+ type WithEngineLaborFallback,
7
+ } from "./engine-labor-fallback.ts";
3
8
  import { openToolObjectFromUnion } from "./open-tool-schema.ts";
4
9
 
5
10
  export const DOCTOR_EVIDENCE_TOOL_NAME = "ak_doctor_evidence";
@@ -37,9 +42,13 @@ export type DoctorFinding =
37
42
  export type DoctorSubmission =
38
43
  | { status: "completed"; case: DoctorCaseIdentity; findings: DoctorFinding[] }
39
44
  | { status: "refused"; reason: string; missingEvidence: Array<{ need: string; targetKeys: string[] }> };
40
- export type DoctorOutput =
45
+ type DoctorOutputClean =
41
46
  | { status: "completed"; case: DoctorCaseIdentity; findings: DoctorFinding[]; cost: DoctorCaseCost }
42
47
  | Extract<DoctorSubmission, { status: "refused" }>;
48
+ /** Clean recorded shape or seat-fallback tainted accepted receipt (ADR 0071). */
49
+ export type DoctorOutput =
50
+ | DoctorOutputClean
51
+ | WithEngineLaborFallback<DoctorOutputClean>;
43
52
  export type DoctorEvidenceEntry = { id: string; kind: "session" | "stderr"; byteLength: number; contentLength: number; sha256: string; content: string };
44
53
  export type DoctorCase = { version: 1; identity: DoctorCaseIdentity; evidence: DoctorEvidenceEntry[]; cost: DoctorCaseCost };
45
54
 
@@ -96,12 +105,19 @@ function isRecord(value: unknown): value is Record<string, unknown> { return typ
96
105
  function read(value: unknown, key: string): unknown { if (!isRecord(value)) return undefined; try { return value[key]; } catch { return undefined; } }
97
106
  export function validateDoctorSubmissionShape(value: unknown): DoctorSubmission {
98
107
  const status = read(value, "status");
99
- if (status !== "completed" && status !== "refused") throw new DoctorSubmissionContractError("Doctor submission has no recognized execution status");
108
+ const base = typeof status === "string" ? seatFallbackBaseStatus(status) : status;
109
+ if (base !== "completed" && base !== "refused") throw new DoctorSubmissionContractError("Doctor submission has no recognized execution status");
110
+ // ADR 0071: tainted status requires latch-shaped engineLaborFallback evidence.
111
+ if (typeof status === "string" && !seatFallbackStatusHasLawfulEvidence(status, value)) {
112
+ throw new DoctorSubmissionContractError("Doctor submission has no recognized execution status");
113
+ }
100
114
  return value as DoctorSubmission;
101
115
  }
102
116
  export function validateRecordedDoctorOutput(value: unknown): DoctorOutput {
103
117
  const output = validateDoctorSubmissionShape(value);
104
- if (read(output, "status") === "completed" && read(output, "cost") === undefined) throw new Error("Completed Doctor receipt has no runtime-owned cost testimony");
118
+ const status = read(output, "status");
119
+ const base = typeof status === "string" ? seatFallbackBaseStatus(status) : status;
120
+ if (base === "completed" && read(output, "cost") === undefined) throw new Error("Completed Doctor receipt has no runtime-owned cost testimony");
105
121
  return output as DoctorOutput;
106
122
  }
107
123
 
@@ -4,7 +4,7 @@
4
4
  * Evidence-child legs install the same definition via customTools (no spawn in role modules).
5
5
  * #380: engine process failure soft-returns so the seat rejoins the main road; declaration
6
6
  * is recorded once via engine-labor-fallback (no fail-closed reject-leg).
7
- * Caller AbortSignal cancel propagates without fallback; package-owned idle abort soft-fails.
7
+ * Caller AbortSignal cancel propagates without fallback.
8
8
  */
9
9
  import type {
10
10
  AgentToolResult,
@@ -27,11 +27,6 @@ import {
27
27
  recordEngineLaborFallback,
28
28
  type EngineLaborFallbackField,
29
29
  } from "./engine-labor-fallback.ts";
30
- import {
31
- isPackageOwnedToolIdleTimeoutError,
32
- pokePackageOwnedToolIdle,
33
- wrapPackageOwnedToolDefinition,
34
- } from "./package-owned-tool-idle.ts";
35
30
 
36
31
  const engineDetourArgsSchema = Type.Object(
37
32
  {
@@ -81,18 +76,11 @@ function seatFallbackToolResult(
81
76
  };
82
77
  }
83
78
 
84
- /** Caller/upper-layer cancel must propagate; idle backstop is seat-fallback, not cancel. */
79
+ /** Caller/upper-layer cancel must propagate; process failure is seat-fallback, not cancel. */
85
80
  function isCallerCancellation(
86
81
  error: unknown,
87
82
  signal: AbortSignal | undefined,
88
83
  ): boolean {
89
- if (isPackageOwnedToolIdleTimeoutError(error)) return false;
90
- if (
91
- signal !== undefined &&
92
- isPackageOwnedToolIdleTimeoutError(signal.reason)
93
- ) {
94
- return false;
95
- }
96
84
  if (signal?.aborted === true) return true;
97
85
  if (
98
86
  typeof error === "object" &&
@@ -108,7 +96,7 @@ function isCallerCancellation(
108
96
  * Build one once-latch detour tool definition for a configured engine name.
109
97
  * `latch` is shared so parent registration can reset between activations.
110
98
  * `fail` owns host abort (parent) vs throw (evidence child) for tool misuse only.
111
- * Engine process failure (nonzero/empty/spawn/idle-timeout) soft-returns seat fallback (#380).
99
+ * Engine process failure (nonzero/empty/spawn) soft-returns seat fallback (#380).
112
100
  * Caller AbortSignal cancel propagates without writing fallback.
113
101
  */
114
102
  export function createEngineDetourToolDefinition(input: {
@@ -118,7 +106,7 @@ export function createEngineDetourToolDefinition(input: {
118
106
  }): ToolDefinition {
119
107
  const latch = input.latch ?? { used: false };
120
108
  const engineName = input.engineName;
121
- return wrapPackageOwnedToolDefinition({
109
+ return {
122
110
  name: ENGINE_DETOUR_TOOL_NAME,
123
111
  label: "Engine Detour",
124
112
  description:
@@ -170,16 +158,13 @@ export function createEngineDetourToolDefinition(input: {
170
158
 
171
159
  let result: Awaited<ReturnType<typeof runEngineDetourOnce>>;
172
160
  try {
173
- // Byte activity on stdout/stderr touches the outer package-owned idle clock
174
- // (183s silence law unchanged). True hangs still die; slow streaming engines live.
175
161
  result = await runEngineDetourOnce({
176
162
  argv,
177
163
  cwd: ctx.cwd,
178
164
  ...(signal === undefined ? {} : { signal }),
179
- onOutputActivity: pokePackageOwnedToolIdle,
180
165
  });
181
166
  } catch (error) {
182
- // Caller cancel: propagate. Idle backstop + spawn/engine failure: seat fallback.
167
+ // Caller cancel: propagate. Spawn/engine failure: seat fallback.
183
168
  if (isCallerCancellation(error, signal)) {
184
169
  throw error;
185
170
  }
@@ -200,7 +185,7 @@ export function createEngineDetourToolDefinition(input: {
200
185
  },
201
186
  };
202
187
  },
203
- }) as ToolDefinition;
188
+ } as ToolDefinition;
204
189
  }
205
190
 
206
191
  /**
@@ -44,11 +44,6 @@ export type EngineDetourRunInput = Readonly<{
44
44
  cwd: string;
45
45
  env?: NodeJS.ProcessEnv;
46
46
  signal?: AbortSignal;
47
- /**
48
- * Called when the child emits at least one byte on stdout or stderr.
49
- * Host wires this to the package-owned idle clock (activity = touch); no timer here.
50
- */
51
- onOutputActivity?: () => void;
52
47
  }>;
53
48
 
54
49
  function abortReasonError(signal: AbortSignal): Error {
@@ -78,8 +73,8 @@ export async function runEngineDetourOnce(
78
73
  return await new Promise<EngineDetourResult>((resolve, reject) => {
79
74
  let settled = false;
80
75
  const signal = input.signal;
81
- // Own abort→kill explicitly so rejection preserves signal.reason (caller cancel
82
- // vs package-owned idle). Do not pass `signal` to spawn (Node replaces reason).
76
+ // Own abort→kill explicitly so rejection preserves signal.reason (caller cancel).
77
+ // Do not pass `signal` to spawn (Node replaces reason).
83
78
  const child = spawn(command, args, {
84
79
  cwd: input.cwd,
85
80
  env: input.env ?? process.env,
@@ -87,17 +82,11 @@ export async function runEngineDetourOnce(
87
82
  });
88
83
  let stdout = "";
89
84
  let stderr = "";
90
- const noteActivity = (chunk: string): void => {
91
- if (chunk.length === 0) return;
92
- input.onOutputActivity?.();
93
- };
94
85
  child.stdout.setEncoding("utf8").on("data", (chunk: string) => {
95
86
  stdout += chunk;
96
- noteActivity(chunk);
97
87
  });
98
88
  child.stderr.setEncoding("utf8").on("data", (chunk: string) => {
99
89
  stderr += chunk;
100
- noteActivity(chunk);
101
90
  });
102
91
  const fail = (error: unknown): void => {
103
92
  if (settled) return;
@@ -116,8 +105,7 @@ export async function runEngineDetourOnce(
116
105
  resolve(result);
117
106
  };
118
107
  const onAbort = (): void => {
119
- // Fail synchronously so cooperative idle/cancel paths can soft-settle
120
- // before the outer package-owned idle hard-reject drain.
108
+ // Fail synchronously so caller-cancel soft-settle preserves signal.reason.
121
109
  fail(signal !== undefined ? abortReasonError(signal) : new Error("aborted"));
122
110
  try {
123
111
  child.kill("SIGTERM");
@@ -32,6 +32,87 @@ export type EngineLaborFallbackField = ReturnType<
32
32
  typeof buildEngineLaborFallbackField
33
33
  >;
34
34
 
35
+ /**
36
+ * Seat-fallback status taint suffix.
37
+ * When engineLaborFallback is declared, typed status discriminators
38
+ * (judgeStatus / status) must not remain clean values like converged/completed.
39
+ * Mechanical form: `${base}-by-fallback` — readable on the status line alone.
40
+ */
41
+ export const SEAT_FALLBACK_STATUS_SUFFIX = "-by-fallback" as const;
42
+
43
+ export function isSeatFallbackTaintedStatus(status: string): boolean {
44
+ return status.endsWith(SEAT_FALLBACK_STATUS_SUFFIX);
45
+ }
46
+
47
+ /** Strip seat-fallback taint for semantic routing / acceptance matching. */
48
+ export function seatFallbackBaseStatus(status: string): string {
49
+ return isSeatFallbackTaintedStatus(status)
50
+ ? status.slice(0, -SEAT_FALLBACK_STATUS_SUFFIX.length)
51
+ : status;
52
+ }
53
+
54
+ /**
55
+ * ADR 0071: `-by-fallback` is lawful only with a valid `engineLaborFallback`
56
+ * declaration (latch-shaped triple). Clean statuses do not need the field.
57
+ */
58
+ export function seatFallbackStatusHasLawfulEvidence(
59
+ status: string,
60
+ source: unknown,
61
+ ): boolean {
62
+ if (!isSeatFallbackTaintedStatus(status)) return true;
63
+ return readEngineLaborFallbackFieldFrom(source) !== undefined;
64
+ }
65
+
66
+ /** Idempotent taint: clean → `${clean}-by-fallback`; already tainted stays. */
67
+ export function taintStatusForSeatFallback(status: string): string {
68
+ if (status.length === 0 || isSeatFallbackTaintedStatus(status)) return status;
69
+ return `${status}${SEAT_FALLBACK_STATUS_SUFFIX}`;
70
+ }
71
+
72
+ /** Clean status literal or its seat-fallback tainted form. */
73
+ export type SeatFallbackTaintedStatus<S extends string = string> =
74
+ | S
75
+ | `${S}${typeof SEAT_FALLBACK_STATUS_SUFFIX}`;
76
+
77
+ type TaintDiscriminatorFields<T> = {
78
+ [K in keyof T]: K extends "status" | "judgeStatus"
79
+ ? T[K] extends string
80
+ ? SeatFallbackTaintedStatus<Extract<T[K], string>>
81
+ : T[K]
82
+ : T[K];
83
+ };
84
+
85
+ /** Receipt shape after sole-built field attach + status taint (runtime truth). */
86
+ export type WithEngineLaborFallback<T extends object> = T extends unknown
87
+ ? TaintDiscriminatorFields<T> & EngineLaborFallbackField
88
+ : never;
89
+
90
+ const STATUS_DISCRIMINATOR_KEYS = ["judgeStatus", "status"] as const;
91
+
92
+ /** Rewrite known status discriminators on a receipt when seat fallback is declared. */
93
+ function taintReceiptStatusDiscriminators<T extends object>(
94
+ receipt: T,
95
+ ): TaintDiscriminatorFields<T> {
96
+ let next: Record<string, unknown> | undefined;
97
+ for (const key of STATUS_DISCRIMINATOR_KEYS) {
98
+ if (!Object.prototype.hasOwnProperty.call(receipt, key)) continue;
99
+ let value: unknown;
100
+ try {
101
+ value = (receipt as Record<string, unknown>)[key];
102
+ } catch {
103
+ continue;
104
+ }
105
+ if (typeof value !== "string" || value.length === 0) continue;
106
+ const tainted = taintStatusForSeatFallback(value);
107
+ if (tainted === value) continue;
108
+ if (next === undefined) {
109
+ next = { ...(receipt as object as Record<string, unknown>) };
110
+ }
111
+ next[key] = tainted;
112
+ }
113
+ return (next === undefined ? receipt : next) as TaintDiscriminatorFields<T>;
114
+ }
115
+
35
116
  export type EngineLaborFallbackLatch = {
36
117
  field: EngineLaborFallbackField | undefined;
37
118
  };
@@ -62,14 +143,31 @@ export function readEngineLaborFallbackField(
62
143
  /**
63
144
  * Merge sole-built field into typed receipt details.
64
145
  * Spread only — must not construct the field key again (S1).
146
+ * When the field is present, also taint typed status discriminators so a
147
+ * status-line-only reader cannot mistake seat labor for clean engine labor.
65
148
  * Without a mechanical latch, strip any model-injected reserved key (no forged declaration).
149
+ * Return type widens when field is attached: discriminators may carry `-by-fallback`
150
+ * and the sole-built field is present (callers must not assume clean-only unions).
66
151
  */
152
+ export function withEngineLaborFallbackField<T extends object>(
153
+ receipt: T,
154
+ field: EngineLaborFallbackField,
155
+ ): WithEngineLaborFallback<T>;
156
+ export function withEngineLaborFallbackField<T extends object>(
157
+ receipt: T,
158
+ field: undefined,
159
+ ): T;
160
+ export function withEngineLaborFallbackField<T extends object>(
161
+ receipt: T,
162
+ field: EngineLaborFallbackField | undefined,
163
+ ): T | WithEngineLaborFallback<T>;
67
164
  export function withEngineLaborFallbackField<T extends object>(
68
165
  receipt: T,
69
166
  field: EngineLaborFallbackField | undefined,
70
- ): T {
167
+ ): T | WithEngineLaborFallback<T> {
71
168
  if (field !== undefined) {
72
- return { ...receipt, ...field };
169
+ const taintedReceipt = taintReceiptStatusDiscriminators(receipt);
170
+ return { ...taintedReceipt, ...field } as WithEngineLaborFallback<T>;
73
171
  }
74
172
  if (
75
173
  !Object.prototype.hasOwnProperty.call(receipt, "engineLaborFallback")
@@ -36,7 +36,6 @@ import {
36
36
  engineSessionMaterialFromOptions,
37
37
  type EngineSessionMaterial,
38
38
  } from "./package-resources/engine-material.ts";
39
- import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
40
39
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT, type NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
41
40
  import { REVIEWER_VERIFICATION_BOUNDARY } from "./reviewer-construction.ts";
42
41
  import type { ReviewerPromptText } from "./reviewer-prompt-identity.ts";
@@ -750,7 +749,7 @@ export async function executeAuditorChild(
750
749
  let decisionToolFailure: unknown;
751
750
  const decisionToolFailures = new Map<string, unknown>();
752
751
  const delivery = createReceiptDeliveryPolicy();
753
- const tool = wrapPackageOwnedToolDefinition({
752
+ const tool = {
754
753
  ...options.tool,
755
754
  label: options.roleLabel,
756
755
  async execute(...args: any[]) {
@@ -775,7 +774,7 @@ export async function executeAuditorChild(
775
774
  throw error;
776
775
  }
777
776
  },
778
- });
777
+ };
779
778
 
780
779
  const parentSessionManager = options.context.sessionManager;
781
780
  const parentHeader = parentSessionManager?.getHeader?.();
@@ -796,7 +795,7 @@ export async function executeAuditorChild(
796
795
  thinkingLevel: options.context.thinkingLevel ?? "off",
797
796
  modelRuntime: inherited.runtime,
798
797
  systemPrompt: options.systemPrompt,
799
- customTools: [wrapPackageOwnedToolDefinition({ ...options.dossierTool, label: options.roleLabel }), tool],
798
+ customTools: [{ ...options.dossierTool, label: options.roleLabel }, tool],
800
799
  sessionManager: auditorSessionManager,
801
800
  });
802
801
 
@@ -20,6 +20,7 @@ import { randomUUID } from "node:crypto";
20
20
  import { lstat, mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
21
21
  import { basename, dirname, join, relative, resolve, sep } from "node:path";
22
22
 
23
+ import { seatFallbackBaseStatus } from "./engine-labor-fallback.ts";
23
24
  import {
24
25
  formatDurationZh,
25
26
  formatLocalDateTime,
@@ -176,7 +177,8 @@ export function decideTicketCurrentState(input: {
176
177
  const latest = sortRunsByStart(input.runs).at(-1)!;
177
178
  if (!latest.hasResult) return unacceptedBand(latest.mtimeMs, input.now.getTime());
178
179
  // Escalate is an awaiting overlay: same placement and sort band, distinct state.
179
- if (latest.resultStatus === "escalate") return "escalate-awaiting";
180
+ // Seat-fallback taint (`escalate-by-fallback`) keeps escalate base semantics.
181
+ if (seatFallbackBaseStatus(latest.resultStatus) === "escalate") return "escalate-awaiting";
180
182
  return "accepted-awaiting";
181
183
  }
182
184
 
@@ -843,7 +845,9 @@ function buildBreadcrumbSteps(runs: readonly TicketTrajectoryRun[]): BreadcrumbS
843
845
  const isReturn = seen.has(group.station);
844
846
  seen.add(group.station);
845
847
  const isRejected = group.runs.some(
846
- (run) => run.hasResult && REJECTED_RESULT_STATUSES.has(run.resultStatus),
848
+ (run) =>
849
+ run.hasResult &&
850
+ REJECTED_RESULT_STATUSES.has(seatFallbackBaseStatus(run.resultStatus)),
847
851
  );
848
852
  steps.push({
849
853
  station: group.station,
@@ -1,6 +1,11 @@
1
1
  import { Type, type Static } from "typebox";
2
2
  import { isFullGitObjectId } from "./git-object-id.ts";
3
3
  import { exactUtf8 } from "./exact-utf8.ts";
4
+ import {
5
+ seatFallbackBaseStatus,
6
+ seatFallbackStatusHasLawfulEvidence,
7
+ type WithEngineLaborFallback,
8
+ } from "./engine-labor-fallback.ts";
4
9
  import { sha256Hex } from "./sha256.ts";
5
10
  import { openToolObjectFromUnion } from "./open-tool-schema.ts";
6
11
 
@@ -24,9 +29,13 @@ export const mergerOutputSchema = openToolObjectFromUnion(mergerOutputVariants);
24
29
  export type DeepReadonly<T> = T extends (...args: never[]) => unknown ? T : T extends readonly (infer U)[] ? readonly DeepReadonly<U>[] : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T;
25
30
  export type MergerMaterial = DeepReadonly<Static<typeof materialSchema>>;
26
31
  export type MergerInput = DeepReadonly<Static<typeof mergerInputSchema>>;
27
- export type MergerOutput =
32
+ type MergerOutputClean =
28
33
  | { status: "completed"; attemptId: string; report: string; mergeCommitId: string }
29
34
  | { status: "escalate"; attemptId: string; diagnosis: string; report: string };
35
+ /** Clean submission shape or seat-fallback tainted accepted receipt (ADR 0071). */
36
+ export type MergerOutput =
37
+ | MergerOutputClean
38
+ | WithEngineLaborFallback<MergerOutputClean>;
30
39
  export const MERGER_OUTPUT_TOOL_NAME = "ak_merger_output";
31
40
  export const MERGER_ACCEPTED_TEXT = "Merger output accepted";
32
41
 
@@ -65,7 +74,13 @@ export function validateMergerInput(value: unknown): MergerInput {
65
74
 
66
75
  export function validateMergerOutput(value: unknown, expectedAttemptId?: string): MergerOutput {
67
76
  if (!record(value) || (expectedAttemptId !== undefined && value.attemptId !== expectedAttemptId)) throw new Error("Merger output attempt mismatch");
68
- if (value.status === "completed" && isFullGitObjectId(value.mergeCommitId)) return structuredClone(value) as MergerOutput;
69
- if (value.status === "escalate") return structuredClone(value) as MergerOutput;
77
+ const status = typeof value.status === "string" ? value.status : undefined;
78
+ const statusBase = status !== undefined ? seatFallbackBaseStatus(status) : undefined;
79
+ // ADR 0071: tainted status requires latch-shaped engineLaborFallback evidence.
80
+ if (status !== undefined && !seatFallbackStatusHasLawfulEvidence(status, value)) {
81
+ throw new Error("Merger output has no recognized execution discriminator");
82
+ }
83
+ if (statusBase === "completed" && isFullGitObjectId(value.mergeCommitId)) return structuredClone(value) as MergerOutput;
84
+ if (statusBase === "escalate") return structuredClone(value) as MergerOutput;
70
85
  throw new Error("Merger output has no recognized execution discriminator");
71
86
  }
@@ -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,
@@ -16,7 +17,6 @@ import { PACKAGED_ROLE_REGISTRY, type PackagedRole, packagedRoleMetadata } from
16
17
  import { openInProcessAgentSession } from "./in-process-session.ts";
17
18
  import { renderPublicAkRoleCommand } from "./public-command-renderer.ts";
18
19
  import { issueRoot, subjectPath } from "./work-subject-identity.ts";
19
- import { wrapPackageOwnedToolDefinition } from "./package-owned-tool-idle.ts";
20
20
  import { createReceiptDeliveryPolicy, NO_RECEIPT_LIFECYCLE_ENTRY_TYPE, RECEIPT_DELIVERY_PROMPT } from "./receipt-delivery-policy.ts";
21
21
  import { recordTypedProviderHttpStatus } from "./typed-provider-http.ts";
22
22
  import {
@@ -437,7 +437,7 @@ export function parseNavigatorModelSetting(value: string): { provider: string; m
437
437
  }
438
438
 
439
439
  export function createNavigatorPrepareTool(onOutput: (value: PrepareOutput) => void): ToolDefinition {
440
- return wrapPackageOwnedToolDefinition({
440
+ return {
441
441
  name: NAVIGATOR_PREPARE_TOOL_NAME,
442
442
  label: "Navigator preparation",
443
443
  description: "Submit Navigator direction advice. Provide candidates with next.role (phase when meaningful). route/matches/reason/command are optional context, not acceptance gates.",
@@ -448,7 +448,7 @@ export function createNavigatorPrepareTool(onOutput: (value: PrepareOutput) => v
448
448
  onOutput(value as PrepareOutput);
449
449
  return { content: [{ type: "text" as const, text: "Navigator preparation accepted" }], details: value, terminate: true as const };
450
450
  },
451
- });
451
+ };
452
452
  }
453
453
 
454
454
  /**
@@ -462,6 +462,18 @@ export type NavigatorCandidateSelection = {
462
462
  readonly matchedToSettlement: boolean;
463
463
  };
464
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
+
465
477
  export function selectNavigatorCandidate(
466
478
  candidates: readonly NavigatorCandidate[],
467
479
  settlement: NavigatorSettlement,
@@ -478,7 +490,9 @@ export function selectNavigatorCandidate(
478
490
  if (rolePhaseMatched.length > 0) {
479
491
  if (settlement.status !== undefined) {
480
492
  const statusSpecific = rolePhaseMatched.find(
481
- (candidate) => candidate.matches?.statuses?.includes(settlement.status!) === true,
493
+ (candidate) =>
494
+ candidate.matches?.statuses !== undefined &&
495
+ statusListMatchesSettlement(candidate.matches.statuses, settlement.status!),
482
496
  );
483
497
  if (statusSpecific !== undefined) {
484
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
  }