@akagilnc/pi-workflow-roles 0.1.4021 → 0.1.4062

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 (74) hide show
  1. package/dist/acp-host/production-host.js +682 -355
  2. package/dist/doctor-auditor.js +1 -1
  3. package/dist/dossier-resolution.js +21 -8
  4. package/dist/gatekeeper-role.js +34 -27
  5. package/dist/headless-host/description.js +7 -8
  6. package/dist/headless-host/production-host.js +709 -369
  7. package/dist/host-contracts.js +16 -0
  8. package/dist/inspector-contracts.js +8 -0
  9. package/dist/navigator-attendance.js +2 -0
  10. package/dist/navigator-public-session.js +18 -34
  11. package/dist/navigator-session-contracts.js +19 -0
  12. package/dist/packaged-role-registry.js +1 -1
  13. package/dist/pi/known-failure.js +3 -4
  14. package/dist/pi/role-turn-host.js +43 -13
  15. package/dist/public-cli/auto-resume.js +20 -7
  16. package/dist/public-cli/case-dossier-delivery.js +60 -1
  17. package/dist/public-cli/diarist-run.js +9 -23
  18. package/dist/public-cli/inspector-run.js +17 -3
  19. package/dist/public-cli/instruction-seat-run.js +12 -7
  20. package/dist/public-cli/judge-run.js +3 -1
  21. package/dist/public-cli/main.js +440 -222
  22. package/dist/public-cli/notary-run.js +7 -4
  23. package/dist/public-cli/post-admission.js +66 -59
  24. package/dist/public-cli/run-lifecycle.js +3 -0
  25. package/dist/public-cli/settlement.js +78 -58
  26. package/dist/public-cli/terminal.js +1 -11
  27. package/dist/public-role-summons.js +8 -0
  28. package/dist/submission-ledger.js +139 -19
  29. package/dist/user-dialogue-stdin.js +33 -0
  30. package/extensions/role-runtime.ts +1 -0
  31. package/package.json +1 -1
  32. package/resources/836-deleted-machine-instruction-inventory.md +1 -1
  33. package/src/doctor-auditor.ts +1 -1
  34. package/src/dossier-resolution.ts +26 -8
  35. package/src/external-host-turn-loop.ts +9 -0
  36. package/src/gatekeeper-pass-envelope.ts +6 -0
  37. package/src/gatekeeper-role.ts +47 -28
  38. package/src/headless-host/description.ts +8 -11
  39. package/src/headless-host/role-turn-host.ts +20 -5
  40. package/src/host-contracts.ts +30 -7
  41. package/src/inspector-contracts.ts +7 -0
  42. package/src/judge-role.ts +4 -0
  43. package/src/navigator-attendance.ts +2 -0
  44. package/src/navigator-public-session.ts +19 -55
  45. package/src/navigator-session-contracts.ts +39 -0
  46. package/src/navigator-work-context.ts +3 -4
  47. package/src/notary-role.ts +1 -0
  48. package/src/packaged-role-registry.ts +1 -1
  49. package/src/pi/adapter.ts +19 -4
  50. package/src/pi/known-failure.ts +3 -4
  51. package/src/pi/role-turn-host.ts +48 -14
  52. package/src/public-cli/auto-resume.ts +57 -20
  53. package/src/public-cli/case-dossier-delivery.ts +86 -1
  54. package/src/public-cli/cli.ts +2 -2
  55. package/src/public-cli/collector-run.ts +3 -1
  56. package/src/public-cli/countersign-run.ts +34 -27
  57. package/src/public-cli/diarist-run.ts +10 -27
  58. package/src/public-cli/inspector-run.ts +26 -2
  59. package/src/public-cli/instruction-seat-run.ts +20 -10
  60. package/src/public-cli/invocation.ts +2 -0
  61. package/src/public-cli/judge-run.ts +3 -1
  62. package/src/public-cli/notary-run.ts +13 -4
  63. package/src/public-cli/post-admission.ts +74 -65
  64. package/src/public-cli/reviewer-run.ts +3 -1
  65. package/src/public-cli/run-lifecycle.ts +3 -0
  66. package/src/public-cli/settlement.ts +105 -80
  67. package/src/public-cli/terminal.ts +7 -16
  68. package/src/public-role-summons.ts +31 -7
  69. package/src/role-envelope.ts +9 -30
  70. package/src/role-runtime-dependencies.ts +1 -0
  71. package/src/role-runtime.ts +95 -19
  72. package/src/submission-ledger.ts +195 -26
  73. package/src/user-dialogue-stdin.ts +37 -0
  74. package/src/worker-role.ts +4 -0
@@ -22,7 +22,7 @@ export type ClaudePrintHostDescription = HeadlessHostBase & Readonly<{
22
22
  * are composed by the adapter from the turn request — not listed here.
23
23
  */
24
24
  fixedArgs: readonly string[];
25
- /** Print-mode flag that takes the user prompt as its value (e.g. `-p`). */
25
+ /** Print-mode flag (e.g. `-p`); user prompt rides stdin, not this flag's value (#879). */
26
26
  promptFlag: string;
27
27
  /** CLI flag for the seat model (e.g. `--model`). */
28
28
  modelFlag: string;
@@ -76,11 +76,11 @@ export function resolveHeadlessBinary(
76
76
 
77
77
  /**
78
78
  * Build one Claude print-mode argv for a single process turn.
79
- * Shape: `<promptFlag> <prompt> <fixedArgs…> <system/schema/mcp/model/effort/session…>`.
79
+ * Shape: `<promptFlag> <fixedArgs…> <system/schema/mcp/model/effort/session…>`.
80
+ * User dialogue rides stdin, not an argv element (#879 / ARG_MAX).
80
81
  */
81
82
  export function headlessTurnArgs(options: {
82
83
  readonly description: ClaudePrintHostDescription;
83
- readonly prompt: string;
84
84
  /** Absolute path written by the adapter; paired with `systemPromptFlag`. */
85
85
  readonly systemPromptPath: string;
86
86
  readonly jsonSchema: Readonly<Record<string, unknown>>;
@@ -94,7 +94,6 @@ export function headlessTurnArgs(options: {
94
94
  const { description } = options;
95
95
  const args: string[] = [
96
96
  description.promptFlag,
97
- options.prompt,
98
97
  ...description.fixedArgs,
99
98
  description.systemPromptFlag,
100
99
  options.systemPromptPath,
@@ -388,13 +387,12 @@ function codexMcpConfigArgs(
388
387
 
389
388
  /**
390
389
  * Build one `codex exec` / `codex exec resume` argv (#646).
391
- * New: `exec --approve-for-me … prompt`.
392
- * Resume: `exec --approve-for-me resume <thread_id> … prompt`.
390
+ * New: `exec --approve-for-me … -- -` (prompt on stdin).
391
+ * Resume: `exec --approve-for-me resume <thread_id> … -- -`.
393
392
  * Approval stays on the parent command so new and resumed turns use Codex's
394
393
  * automatic review with its workspace-write sandbox.
395
394
  */
396
395
  export function codexTurnArgs(options: {
397
- readonly prompt: string;
398
396
  /** Absolute path for `-c model_instructions_file=…`. */
399
397
  readonly systemPromptPath: string;
400
398
  /** Absolute path for `--output-schema` (closed transport schema). */
@@ -455,10 +453,9 @@ export function codexTurnArgs(options: {
455
453
  args.push("--skip-git-repo-check");
456
454
  }
457
455
 
458
- // Prompt last as positional. `--` stops option parsing so a leading `-`
459
- // (markdown lists, pasted flags, rulings) is not eaten by clap (codex tip:
460
- // "use '-- -s'"). Same for new and resume — both end here.
461
- args.push("--", options.prompt);
456
+ // `-` after `--` is Codex's stdin prompt sentinel. `--` still stops option
457
+ // parsing; the user body itself is not an argv element (#879 / ARG_MAX).
458
+ args.push("--", "-");
462
459
  return args;
463
460
  }
464
461
 
@@ -256,6 +256,8 @@ function spawnHeadlessTurn(options: {
256
256
  readonly env: NodeJS.ProcessEnv;
257
257
  readonly signal?: AbortSignal;
258
258
  readonly timeoutMs?: number;
259
+ /** User dialogue body; omitted from argv so execve cannot E2BIG (#879). */
260
+ readonly stdin?: string;
259
261
  /** Called for each complete stdout line as it arrives (live stream-json). */
260
262
  readonly onStdoutLine?: (line: string) => void;
261
263
  }): Promise<{ code: number | null; stdout: string; stderr: string; timedOut: boolean }> {
@@ -267,8 +269,20 @@ function spawnHeadlessTurn(options: {
267
269
  const child = spawn(options.binary, [...options.args], {
268
270
  cwd: options.cwd,
269
271
  env: options.env,
270
- stdio: ["ignore", "pipe", "pipe"],
272
+ stdio: ["pipe", "pipe", "pipe"],
271
273
  });
274
+ if (child.stdin === null) {
275
+ reject(new Error("headless child stdin pipe was not created"));
276
+ return;
277
+ }
278
+ let stdinDeliveryError: Error | undefined;
279
+ child.stdin.on("error", (error) => {
280
+ stdinDeliveryError ??= error;
281
+ });
282
+ if (options.stdin !== undefined) {
283
+ child.stdin.write(options.stdin);
284
+ }
285
+ child.stdin.end();
272
286
  // Rolling retention only: last result-candidate line for final parse.
273
287
  // Live events go to sitian via onStdoutLine — never accumulate the full stream.
274
288
  let resultStdout = "";
@@ -318,6 +332,10 @@ function spawnHeadlessTurn(options: {
318
332
  settled = true;
319
333
  if (timer !== undefined) clearTimeout(timer);
320
334
  options.signal?.removeEventListener("abort", onAbort);
335
+ if (stdinDeliveryError !== undefined) {
336
+ reject(stdinDeliveryError);
337
+ return;
338
+ }
321
339
  resolve({ code, stdout: resultStdout, stderr, timedOut });
322
340
  };
323
341
  const onAbort = (): void => {
@@ -381,7 +399,6 @@ function terminalFromSpawned(
381
399
 
382
400
  function buildTurnArgs(options: {
383
401
  readonly description: HeadlessHostDescription;
384
- readonly prompt: string;
385
402
  readonly systemPromptPath: string;
386
403
  readonly jsonSchema: Readonly<Record<string, unknown>>;
387
404
  readonly mcpServers: readonly Readonly<Record<string, unknown>>[];
@@ -400,7 +417,6 @@ function buildTurnArgs(options: {
400
417
  }
401
418
  if (options.outputSchemaPath === undefined) throw new Error("codex requires an output schema path");
402
419
  return codexTurnArgs({
403
- prompt: options.prompt,
404
420
  systemPromptPath: options.systemPromptPath,
405
421
  outputSchemaPath: options.outputSchemaPath,
406
422
  mcpServers: options.mcpServers,
@@ -419,7 +435,6 @@ function buildTurnArgs(options: {
419
435
  if (options.mcpConfigPath === undefined) throw new Error("claude print-mode requires an MCP config path");
420
436
  return headlessTurnArgs({
421
437
  description: options.description,
422
- prompt: options.prompt,
423
438
  systemPromptPath: options.systemPromptPath,
424
439
  jsonSchema: options.jsonSchema,
425
440
  mcpConfigPath: options.mcpConfigPath,
@@ -479,7 +494,6 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
479
494
  const gitCommonDir = codex ? resolveGitCommonDir(request.cwd) : undefined;
480
495
  args = buildTurnArgs({
481
496
  description: config.description,
482
- prompt,
483
497
  systemPromptPath,
484
498
  jsonSchema: prepared.jsonSchema,
485
499
  mcpServers: prepared.mcpServers,
@@ -508,6 +522,7 @@ export function createHeadlessRoleTurnHost(config: HeadlessRoleTurnHostConfig):
508
522
  args,
509
523
  cwd: request.cwd,
510
524
  env,
525
+ stdin: prompt,
511
526
  ...(abortSignal === undefined ? {} : { signal: abortSignal }),
512
527
  ...(request.timeoutMs === undefined ? {} : { timeoutMs: request.timeoutMs }),
513
528
  onStdoutLine(line) {
@@ -15,18 +15,22 @@ export type HostToolResult<T = unknown> = {
15
15
  /** Opaque host-owned identity persisted with a Role run. */
16
16
  export type DurablePrincipal = object & { readonly __durablePrincipal?: never };
17
17
 
18
- /** Controlled post-admission failure classes (ADR 0052 / #107). Owner = host contract. */
18
+ /**
19
+ * Controlled post-admission failure classes (ADR 0052 / #107). Owner = host contract.
20
+ * Closed set of typed facts only — never a fabricated "could not classify" label (#881).
21
+ * When no typed confirmation exists, omit cause and keep the original diagnostic / error pointer.
22
+ */
19
23
  export type ControlledFailureCause =
20
24
  | "activation"
21
25
  | "provider"
22
26
  | "session"
23
27
  | "output"
24
- | "timeout"
25
- | "unrecognized";
28
+ | "timeout";
26
29
 
27
30
  /** Production-owned typed failure carried on a resolved turn result. */
28
31
  export type RoleTurnKnownFailure = {
29
- readonly cause: ControlledFailureCause;
32
+ /** Present only when a typed fact confirms the class; omitted when unknown (#881). */
33
+ readonly cause?: ControlledFailureCause;
30
34
  readonly identity?: {
31
35
  readonly name?: string;
32
36
  readonly code?: string | number;
@@ -126,7 +130,7 @@ export type RoleTurnActivation =
126
130
  /** Required comparison-base revision for the unanchored merge-candidate diff. */
127
131
  readonly baseRevision: string;
128
132
  }
129
- | { readonly role: "inspector" }
133
+ | { readonly role: "inspector"; readonly sourceRun?: string }
130
134
  | { readonly role: "gatekeeper" }
131
135
  | { readonly role: "navigator" }
132
136
  | { readonly role: "auditor" }
@@ -156,6 +160,11 @@ export type RoleTurnHostTransition = {
156
160
  readonly priorNativePaths: readonly string[];
157
161
  };
158
162
 
163
+ /** Gate review officers (察院 / 符宝郎 / 审刑院). Single authority for seat checks. */
164
+ export function isOfficerReviewSeat(role: string): boolean {
165
+ return role === "notary" || role === "inspector" || role === "auditor";
166
+ }
167
+
159
168
  /** One main-session turn request over the host-neutral execution seam. */
160
169
  export type RoleTurnRequest = {
161
170
  readonly principal: DurablePrincipal;
@@ -237,7 +246,21 @@ export interface DurablePrincipalAuthority {
237
246
  type HostSessionManager = { getLeafEntry(): HostSessionEntry | undefined; getLeafId(): string | null | undefined; getEntries(): Iterable<HostSessionEntry>; getSessionDir(): string; getSessionFile(): string | undefined; getHeader?(): { readonly type: string; readonly id?: string } | null; setSessionFile?(path: string): void; appendCustomEntry?(customType: string, data?: unknown): unknown; };
238
247
 
239
248
  /** Context supplied by a host for one activation and its interceptable events. */
240
- export type HostContext = { cwd: string; mode: string; model: { readonly provider: string } | undefined; sessionManager: HostSessionManager; signal?: AbortSignal | undefined; ui?: { notify?(message: string, type?: "info" | "warning" | "error"): void }; transcript?(): string; abort(): void; };
249
+ export type HostContext = { cwd: string; mode: string; model: { readonly provider: string } | undefined; sessionManager: HostSessionManager; /** Per-turn admitted run directory (#879); never process-global env. */ runDirectory?: string; /** Per-turn court attempt (#879); never process-global env. */ courtAttemptId?: string; signal?: AbortSignal | undefined; ui?: { notify?(message: string, type?: "info" | "warning" | "error"): void }; transcript?(): string; abort(): void; };
250
+
251
+ /** Per-turn run directory; adapters must project any child-process identity. */
252
+ export function runDirectoryFromHostContext(context: HostContext): string | undefined {
253
+ return typeof context.runDirectory === "string" && context.runDirectory.trim() !== ""
254
+ ? context.runDirectory
255
+ : undefined;
256
+ }
257
+
258
+ /** Per-turn court attempt; absence never inherits ambient process identity. */
259
+ export function courtAttemptIdFromHostContext(context: HostContext): string | undefined {
260
+ return typeof context.courtAttemptId === "string" && context.courtAttemptId.trim() !== ""
261
+ ? context.courtAttemptId
262
+ : undefined;
263
+ }
241
264
 
242
265
  export type HostToolDefinition<S extends TSchema = TSchema, D = unknown, C = HostContext> = { name: string; label: string; description: string; promptSnippet?: string; parameters: S; execute( toolCallId: string, params: Static<S>, signal: AbortSignal | undefined, update: ((result: HostToolResult<D>) => void) | undefined, context: C, ): Promise<HostToolResult<D>>; /**
243
266
  * #641 chain② opt-in: when the output params carry the shared infrastructure
@@ -352,7 +375,7 @@ export interface RoleHost {
352
375
  getAllTools(): Array<{ name: string; sourceInfo?: { path?: string } }>;
353
376
  setActiveTools(names: string[]): void;
354
377
  getActiveTools(): string[];
355
- requireGatekeeperPass?(options: { context: HostContext; subject: HostGatekeeperSubject; signal?: AbortSignal; hostActions: HostGatekeeperActions; toolCallId: string }): Promise<void>;
378
+ requireGatekeeperPass?(options: { context: HostContext; subject: HostGatekeeperSubject; signal?: AbortSignal; hostActions: HostGatekeeperActions; toolCallId: string; submission?: unknown }): Promise<void>;
356
379
  on(event: "before_agent_start", handler: HostEventHandler<"before_agent_start">): void;
357
380
  on(event: "input", handler: HostEventHandler<"input">): void;
358
381
  on(event: "tool_call", handler: HostEventHandler<"tool_call">): void;
@@ -6,6 +6,13 @@
6
6
 
7
7
  export const INSPECTOR_OUTPUT_TOOL_NAME = "ak_inspector_output" as const;
8
8
  export const INSPECTOR_ACCEPTED_TEXT = "察院回执已接受";
9
+ export const INSPECTOR_SOURCE_RUN_FLAG = {
10
+ name: "ak-inspector-source-run",
11
+ definition: {
12
+ description: "察院初铸绑定的父 run 绝对路径",
13
+ type: "string" as const,
14
+ },
15
+ } as const;
9
16
 
10
17
  export type InspectorOutput =
11
18
  | { readonly status: "pass"; readonly findings?: unknown }
package/src/judge-role.ts CHANGED
@@ -122,6 +122,8 @@ export function createJudgeRoleRuntime(
122
122
  ...(signal === undefined ? {} : { signal }),
123
123
  hostActions,
124
124
  toolCallId,
125
+ // #879: this-turn typed payload — identity-bound at submit site.
126
+ submission: parameters,
125
127
  });
126
128
  // #756: 审刑院合规路径 — same review-queue law as 符宝郎/察院.
127
129
  // pass → accept; bounce|escalate → raw auditor receipt back to judge;
@@ -132,6 +134,8 @@ export function createJudgeRoleRuntime(
132
134
  ...(signal === undefined ? {} : { signal }),
133
135
  hostActions,
134
136
  toolCallId,
137
+ // #879: same parent payload for 审刑院; not recovered from session latest.
138
+ submission: parameters,
135
139
  });
136
140
  return {
137
141
  content: [{ type: "text" as const, text: JUDGE_ACCEPTED_TEXT }],
@@ -25,6 +25,7 @@ import {
25
25
  navigatorProviderFailure,
26
26
  navigatorProviderFailureFromDiagnostics,
27
27
  navigatorProviderFailureFromError,
28
+ navigatorProviderFailureFromPublicTerminal,
28
29
  navigatorProviderFailureFromStatus,
29
30
  navigatorUnavailableError,
30
31
  parseNavigatorModelSetting,
@@ -46,6 +47,7 @@ export {
46
47
  navigatorProviderFailure,
47
48
  navigatorProviderFailureFromDiagnostics,
48
49
  navigatorProviderFailureFromError,
50
+ navigatorProviderFailureFromPublicTerminal,
49
51
  navigatorProviderFailureFromStatus,
50
52
  navigatorUnavailableError,
51
53
  parseNavigatorModelSetting,
@@ -8,9 +8,8 @@
8
8
  import { sitianReport } from "./sitian-facade.ts";
9
9
  import {
10
10
  NavigatorUnavailableError,
11
- navigatorProviderFailureFromDiagnostics,
11
+ navigatorProviderFailureFromPublicTerminal,
12
12
  navigatorProviderFailureFromError,
13
- navigatorProviderFailureFromStatus,
14
13
  navigatorUnavailableError,
15
14
  parseNavigatorModelSetting,
16
15
  resolveNavigatorSeatSelection,
@@ -18,43 +17,6 @@ import {
18
17
  type NavigatorSessionFactory,
19
18
  } from "./navigator-session-contracts.ts";
20
19
  import type { NoReceiptLifecycleFacts } from "./receipt-delivery-policy.ts";
21
- import { lastRolePayloadRecord } from "./public-cli/terminal.ts";
22
-
23
- /**
24
- * Classify public-navigator failure terminal from structured decisiveFacts only
25
- * (httpStatus / diagnostics / secondaryEvidence on the terminal) — same fields
26
- * settlement already stamps from typed HTTP observation / provider stop.
27
- */
28
- function providerFailureFromPublicTerminal(outcome: {
29
- readonly cause: string;
30
- readonly diagnostic: string;
31
- readonly decisiveFacts: Readonly<Record<string, unknown>>;
32
- }): NavigatorProviderFailureFact {
33
- const facts = outcome.decisiveFacts;
34
- const secondary =
35
- typeof facts.secondaryEvidence === "object" && facts.secondaryEvidence !== null
36
- ? (facts.secondaryEvidence as Record<string, unknown>)
37
- : undefined;
38
- const httpStatus =
39
- typeof secondary?.httpStatus === "number"
40
- ? secondary.httpStatus
41
- : typeof facts.httpStatus === "number"
42
- ? facts.httpStatus
43
- : typeof facts.errorCode === "number"
44
- ? facts.errorCode
45
- : undefined;
46
- const fromStatus = navigatorProviderFailureFromStatus(httpStatus);
47
- if (fromStatus !== undefined) return fromStatus;
48
- const diagnostics = secondary?.diagnostics ?? facts.diagnostics;
49
- const fromDiagnostics = navigatorProviderFailureFromDiagnostics(diagnostics);
50
- if (fromDiagnostics !== undefined) return fromDiagnostics;
51
- const fromCode = navigatorProviderFailureFromError({
52
- code: secondary?.code ?? facts.errorCode,
53
- });
54
- if (fromCode !== undefined) return fromCode;
55
- if (outcome.cause === "provider") return { source: "transport", cause: "transport" };
56
- return { source: "session", cause: "session" };
57
- }
58
20
 
59
21
  export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
60
22
  return async ({ context, subject, tool }) => {
@@ -122,14 +84,16 @@ export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
122
84
  const outcome = summoned.terminal?.roleOutcome;
123
85
  if (outcome === undefined) {
124
86
  const detail = summoned.stderr?.trim() || `exit ${summoned.exitCode}`;
125
- providerFailure = { source: "transport", cause: "transport" };
87
+ // Known source is the public-summon transport path; unconfirmed cause stays unknown.
88
+ providerFailure = { source: "transport", cause: "unknown" };
126
89
  throw navigatorUnavailableError(
127
- "transport",
90
+ providerFailure.source,
128
91
  new Error(`Navigator public summon produced no terminal (${detail})`),
92
+ providerFailure.cause,
129
93
  );
130
94
  }
131
95
  if (outcome.kind === "failure") {
132
- providerFailure = providerFailureFromPublicTerminal(outcome);
96
+ providerFailure = navigatorProviderFailureFromPublicTerminal(outcome);
133
97
  throw navigatorUnavailableError(
134
98
  providerFailure.source,
135
99
  new Error(outcome.diagnostic),
@@ -147,23 +111,23 @@ export function createNativeNavigatorSessionFactory(): NavigatorSessionFactory {
147
111
  // Not a shape-unusable judgment on the navigator reply (#757).
148
112
  return;
149
113
  }
150
- const candidates = lastRolePayloadRecord(outcome.payloads ?? [])?.candidates;
151
- if (!Array.isArray(candidates)) {
152
- // Accepted reply without candidates array — no advice, no judgment.
153
- return;
114
+ for (const payload of outcome.payloads ?? []) {
115
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) continue;
116
+ const candidates = (payload as { candidates?: unknown }).candidates;
117
+ if (!Array.isArray(candidates)) continue;
118
+ await tool.execute(
119
+ "navigator-public-prepare",
120
+ { candidates },
121
+ undefined,
122
+ undefined,
123
+ context as never,
124
+ );
154
125
  }
155
- // Rejoin attendance prepare tool sink (same candidate shape as public advice).
156
- await tool.execute(
157
- "navigator-public-prepare",
158
- { candidates },
159
- undefined,
160
- undefined,
161
- context as never,
162
- );
163
126
  } catch (error) {
164
127
  if (error instanceof NavigatorUnavailableError) throw error;
165
128
  const fact = navigatorProviderFailureFromError(error);
166
- providerFailure = fact ?? { source: "transport", cause: "transport" };
129
+ // Catch path is the public-summon seam (source transport); untyped cause stays unknown.
130
+ providerFailure = fact ?? { source: "transport", cause: "unknown" };
167
131
  throw navigatorUnavailableError(providerFailure.source, error, providerFailure.cause);
168
132
  }
169
133
  })();
@@ -133,6 +133,45 @@ export function navigatorProviderFailureFromDiagnostics(diagnostics: unknown): N
133
133
  return undefined;
134
134
  }
135
135
 
136
+ /**
137
+ * Classify a public-navigator failure terminal from structured facts only.
138
+ * Untyped cause stays unknown — never relabeled session.
139
+ */
140
+ export function navigatorProviderFailureFromPublicTerminal(outcome: {
141
+ readonly cause?: string;
142
+ readonly diagnostic: string;
143
+ readonly decisiveFacts: Readonly<Record<string, unknown>>;
144
+ }): NavigatorProviderFailureFact {
145
+ const facts = outcome.decisiveFacts;
146
+ const secondary =
147
+ typeof facts.secondaryEvidence === "object" && facts.secondaryEvidence !== null
148
+ ? (facts.secondaryEvidence as Record<string, unknown>)
149
+ : undefined;
150
+ const httpStatus =
151
+ typeof secondary?.httpStatus === "number"
152
+ ? secondary.httpStatus
153
+ : typeof facts.httpStatus === "number"
154
+ ? facts.httpStatus
155
+ : typeof facts.errorCode === "number"
156
+ ? facts.errorCode
157
+ : undefined;
158
+ const fromStatus = navigatorProviderFailureFromStatus(httpStatus);
159
+ if (fromStatus !== undefined) return fromStatus;
160
+ const diagnostics = secondary?.diagnostics ?? facts.diagnostics;
161
+ const fromDiagnostics = navigatorProviderFailureFromDiagnostics(diagnostics);
162
+ if (fromDiagnostics !== undefined) return fromDiagnostics;
163
+ const fromCode = navigatorProviderFailureFromError({
164
+ code: secondary?.code ?? facts.errorCode,
165
+ });
166
+ if (fromCode !== undefined) return fromCode;
167
+ // cause=provider names the known source region only; without status/diagnostics/code
168
+ // the specific cause stays unknown (#881 — never invent transport/session labels).
169
+ if (outcome.cause === "provider") return { source: "transport", cause: "unknown" };
170
+ const typed = navigatorUnavailableKey(outcome.cause);
171
+ if (typed !== undefined) return { source: typed, cause: typed };
172
+ return { source: "unknown", cause: "unknown" };
173
+ }
174
+
136
175
  const navigatorProviderFailureSchema = Type.Object({
137
176
  source: Type.Union([
138
177
  Type.Literal("context"), Type.Literal("session"), Type.Literal("model"), Type.Literal("thinking"),
@@ -6,7 +6,7 @@ import { readFile } from "node:fs/promises";
6
6
  import { resolve } from "node:path";
7
7
 
8
8
  import { loadDoctorCase } from "./doctor-evidence.ts";
9
- import type { HostContext } from "./host-contracts.ts";
9
+ import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.ts";
10
10
  import {
11
11
  navigatorSubjectKey,
12
12
  navigatorSubjectKeyForInput,
@@ -64,10 +64,9 @@ export async function loadNavigatorWorkContext(
64
64
  subjectProvenance = "role_input";
65
65
  }
66
66
  // Public ak-role run: admitted request is the typed Navigator work-context source.
67
- const publicRunDir = process.env.AK_ROLE_RUN_DIR;
67
+ const publicRunDir = runDirectoryFromHostContext(options.context);
68
68
  const currentSessionDir = options.context.sessionManager.getSessionDir();
69
- const isBoundPublicRun = typeof publicRunDir === "string"
70
- && publicRunDir.trim() !== ""
69
+ const isBoundPublicRun = publicRunDir !== undefined
71
70
  && resolve(currentSessionDir) === resolve(publicRunDir, "session");
72
71
  if (
73
72
  options.role === "judge" &&
@@ -179,6 +179,7 @@ export function createNotaryRoleRuntime(
179
179
  });
180
180
  // Evidence assembly only (ADR 0018): soul + bound projection.
181
181
  // Ticket value is envelope-admitted at activate — not re-read from flags here.
182
+ // #879 case dossier freeze→readingMaterial is envelope-owned (shared before_agent_start).
182
183
  pi.on("before_agent_start", (event) => {
183
184
  if (activation === undefined) {
184
185
  throw new Error("符宝郎未激活");
@@ -174,7 +174,7 @@ export const PUBLIC_ROLE_RECORDS = [
174
174
  role: "inspector",
175
175
  phases: [null],
176
176
  outputTool: INSPECTOR_OUTPUT_TOOL_NAME,
177
- inputFlag: undefined,
177
+ inputFlag: "ak-inspector-source-run",
178
178
  phaseFlag: undefined,
179
179
  activationStage: "load-and-install",
180
180
  sessionMaterials: INSPECTOR_SESSION_MATERIALS,
package/src/pi/adapter.ts CHANGED
@@ -21,6 +21,7 @@ import type {
21
21
  import { createOAuthKeepalive, type OAuthKeepaliveOptions } from "../oauth-keepalive.ts";
22
22
  import { createRoleRuntimeExtension, type RoleRuntimeDependencies } from "../role-runtime.ts";
23
23
  import { renderAgentStartMaterials } from "../agent-start-materials.ts";
24
+ import { readUserDialogueStdin } from "../user-dialogue-stdin.ts";
24
25
 
25
26
 
26
27
  export type PiRoleHostAdapter = RoleEnvelopeHost;
@@ -45,6 +46,12 @@ function projectPiContext(context: ExtensionContext, transcriptFromContext?: (co
45
46
  cwd: context.cwd,
46
47
  mode: context.mode,
47
48
  model: context.model === undefined ? undefined : { provider: context.model.provider },
49
+ ...(typeof process.env.AK_ROLE_RUN_DIR === "string" && process.env.AK_ROLE_RUN_DIR.trim() !== ""
50
+ ? { runDirectory: process.env.AK_ROLE_RUN_DIR }
51
+ : {}),
52
+ ...(typeof process.env.AK_ROLE_COURT_ATTEMPT === "string" && process.env.AK_ROLE_COURT_ATTEMPT.trim() !== ""
53
+ ? { courtAttemptId: process.env.AK_ROLE_COURT_ATTEMPT }
54
+ : {}),
48
55
  sessionManager: {
49
56
  getLeafEntry: () => context.sessionManager.getLeafEntry(),
50
57
  getLeafId: () => context.sessionManager.getLeafId(),
@@ -78,6 +85,7 @@ function requirePiGatekeeperPass(options: {
78
85
  signal?: AbortSignal;
79
86
  hostActions: HostGatekeeperActions;
80
87
  toolCallId: string;
88
+ submission?: unknown;
81
89
  }): Promise<void> {
82
90
  return requireGatekeeperPass({
83
91
  context: options.context,
@@ -90,6 +98,7 @@ function requirePiGatekeeperPass(options: {
90
98
  bindSubmissionNonPass: options.hostActions.bindSubmissionNonPass,
91
99
  },
92
100
  toolCallId: options.toolCallId,
101
+ ...(options.submission === undefined ? {} : { submission: options.submission }),
93
102
  });
94
103
  }
95
104
 
@@ -99,13 +108,13 @@ function toPiResult<D>(result: HostToolResult<D>): HostToolResult<D> {
99
108
 
100
109
  /** Fold a host before_agent_start return: push readingMaterial into the provider-visible
101
110
  * systemPrompt via the shared renderer and strip the typed field before Pi sees it. */
102
- function foldBeforeAgentStartReturn(result: unknown): BeforeAgentStartEventResult | void {
111
+ function foldBeforeAgentStartReturn(result: unknown, currentSystemPrompt: string): BeforeAgentStartEventResult | void {
103
112
  if (result === undefined || result === null || typeof result !== "object") return undefined;
104
113
  const record = result as { systemPrompt?: string; readingMaterial?: unknown };
105
114
  const { systemPrompt, readingMaterial } = record;
106
115
  const folded = readingMaterial === undefined
107
116
  ? systemPrompt
108
- : renderAgentStartMaterials(systemPrompt ?? "", [readingMaterial]);
117
+ : renderAgentStartMaterials(systemPrompt ?? currentSystemPrompt, [readingMaterial]);
109
118
  return folded === undefined ? {} : { systemPrompt: folded };
110
119
  }
111
120
 
@@ -136,6 +145,12 @@ export function createPiRoleHostAdapter(
136
145
  options: { transcriptFromContext?: (context: ExtensionContext) => string; oauthKeepalive?: OAuthKeepaliveOptions } = {},
137
146
  ): PiRoleHostAdapter {
138
147
  const keepalive = createOAuthKeepalive(options.oauthKeepalive);
148
+ // Decode transport exactly once before Pi's native handler chain. Pi retains
149
+ // its own transform/image propagation and per-handler error isolation.
150
+ pi.on("input", (value) => {
151
+ const text = readUserDialogueStdin(value.text);
152
+ return text === value.text ? { action: "continue" } : { action: "transform", text };
153
+ });
139
154
  const host: RoleHost = {
140
155
  deliverSubmissionRejection(_rejection) {
141
156
  // #836 删 1/A4.5: no package-authored non-sole resume sentence.
@@ -185,8 +200,8 @@ export function createPiRoleHostAdapter(
185
200
  const [, handler] = registration;
186
201
  pi.on("before_agent_start", (value, ctx) => {
187
202
  const result = handler({ prompt: value.prompt, systemPrompt: value.systemPrompt, systemPromptOptions: value.systemPromptOptions }, context(ctx));
188
- if (result instanceof Promise) return result.then(foldBeforeAgentStartReturn);
189
- return foldBeforeAgentStartReturn(result);
203
+ if (result instanceof Promise) return result.then((settled) => foldBeforeAgentStartReturn(settled, value.systemPrompt));
204
+ return foldBeforeAgentStartReturn(result, value.systemPrompt);
190
205
  });
191
206
  } else if (registration[0] === "input") {
192
207
  const [, handler] = registration;
@@ -49,9 +49,8 @@ function sessionStopDetails(input: {
49
49
  /**
50
50
  * Project a native session assistant stop onto the existing knownFailure chain.
51
51
  * Classification follows two-way testimony: typed HTTP status or SDK structure
52
- * keeps provider; stopReason, configured provider/model, or errorMessage prose
53
- * alone is the existing unrecognized value. Present upstream payload is preserved
54
- * in details without rewriting; missing fields are omitted.
52
+ * keeps provider; stopReason / errorMessage prose alone never invents a class (#881).
53
+ * Present upstream payload is preserved in details without rewriting; missing fields are omitted.
55
54
  */
56
55
  export function knownFailureFromProviderStop(input: {
57
56
  readonly stopReason?: string;
@@ -70,7 +69,7 @@ export function knownFailureFromProviderStop(input: {
70
69
  const diagnostic = nonEmptyString(input.errorMessage);
71
70
  const details = sessionStopDetails(input);
72
71
  return {
73
- cause: hasUpstreamErrorTestimony(input) ? "provider" : "unrecognized",
72
+ ...(hasUpstreamErrorTestimony(input) ? { cause: "provider" as const } : {}),
74
73
  ...(diagnostic === undefined ? {} : { diagnostic }),
75
74
  ...(Object.keys(details).length === 0 ? {} : { details }),
76
75
  };