@akagilnc/pi-workflow-roles 0.1.4035 → 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 (54) hide show
  1. package/dist/acp-host/production-host.js +476 -250
  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 +503 -264
  7. package/dist/host-contracts.js +16 -0
  8. package/dist/inspector-contracts.js +8 -0
  9. package/dist/packaged-role-registry.js +1 -1
  10. package/dist/pi/role-turn-host.js +43 -13
  11. package/dist/public-cli/case-dossier-delivery.js +60 -1
  12. package/dist/public-cli/inspector-run.js +17 -3
  13. package/dist/public-cli/instruction-seat-run.js +9 -6
  14. package/dist/public-cli/main.js +239 -112
  15. package/dist/public-cli/notary-run.js +7 -4
  16. package/dist/public-cli/post-admission.js +66 -59
  17. package/dist/public-cli/run-lifecycle.js +3 -0
  18. package/dist/public-cli/settlement.js +24 -7
  19. package/dist/public-role-summons.js +8 -0
  20. package/dist/submission-ledger.js +30 -8
  21. package/dist/user-dialogue-stdin.js +33 -0
  22. package/extensions/role-runtime.ts +1 -0
  23. package/package.json +1 -1
  24. package/resources/836-deleted-machine-instruction-inventory.md +1 -1
  25. package/src/doctor-auditor.ts +1 -1
  26. package/src/dossier-resolution.ts +26 -8
  27. package/src/external-host-turn-loop.ts +9 -0
  28. package/src/gatekeeper-pass-envelope.ts +6 -0
  29. package/src/gatekeeper-role.ts +47 -28
  30. package/src/headless-host/description.ts +8 -11
  31. package/src/headless-host/role-turn-host.ts +20 -5
  32. package/src/host-contracts.ts +22 -3
  33. package/src/inspector-contracts.ts +7 -0
  34. package/src/judge-role.ts +4 -0
  35. package/src/navigator-work-context.ts +3 -4
  36. package/src/notary-role.ts +1 -0
  37. package/src/packaged-role-registry.ts +1 -1
  38. package/src/pi/adapter.ts +19 -4
  39. package/src/pi/role-turn-host.ts +48 -14
  40. package/src/public-cli/case-dossier-delivery.ts +86 -1
  41. package/src/public-cli/inspector-run.ts +26 -2
  42. package/src/public-cli/instruction-seat-run.ts +17 -9
  43. package/src/public-cli/invocation.ts +2 -0
  44. package/src/public-cli/notary-run.ts +13 -4
  45. package/src/public-cli/post-admission.ts +74 -65
  46. package/src/public-cli/run-lifecycle.ts +3 -0
  47. package/src/public-cli/settlement.ts +32 -6
  48. package/src/public-role-summons.ts +31 -7
  49. package/src/role-envelope.ts +9 -30
  50. package/src/role-runtime-dependencies.ts +1 -0
  51. package/src/role-runtime.ts +95 -19
  52. package/src/submission-ledger.ts +48 -13
  53. package/src/user-dialogue-stdin.ts +37 -0
  54. package/src/worker-role.ts +4 -0
@@ -86,18 +86,30 @@ export type GateOfficerSummon = (
86
86
  * conclusion (#753 / #756). Hosted as same-ticket resume instruction.
87
87
  */
88
88
  reask?: string,
89
+ /**
90
+ * In-flight parent 交卷 body (tool-call arguments). Production default relays
91
+ * it verbatim on the officer dialogue content channel (#786 / #879).
92
+ * Identity-bound at the submit site — never recovered as latest toolCall.
93
+ */
94
+ submission?: unknown,
89
95
  ) => Promise<PublicSummonResult>;
90
96
 
91
97
  export type RunGatekeeperOptions = {
92
98
  readonly context: ExtensionContext | HostContext;
93
99
  readonly subject: GatekeeperSubject;
94
100
  readonly signal?: AbortSignal;
95
- /** Run directory of the parent role (pointer-only summons, ADR 0079). */
101
+ /** Run directory of the parent role (binding pointer, ADR 0079 / #879). */
96
102
  readonly runDirectory?: string;
97
103
  /**
98
104
  * Plain-language re-ask for this summon (resume speaker after non-three-state).
99
105
  */
100
106
  readonly reask?: string;
107
+ /**
108
+ * In-flight parent typed payload for this gate turn. Relayed verbatim as
109
+ * officer dialogue content (#879). Call site passes the current tool args —
110
+ * code must not scan session latest/mtime to recover it.
111
+ */
112
+ readonly submission?: unknown;
101
113
  /**
102
114
  * Test seam for public-role summons. Production calls the shared public
103
115
  * activation path (#675); inject only in offline tracers.
@@ -198,15 +210,26 @@ function projectOfficerDecision(
198
210
  }
199
211
 
200
212
  /**
201
- * Project a public-role terminal onto the gate queue surface.
202
- * Host failure stays failure; recorded officer payloads ride beside it (#836 A.3).
203
- * Multiple recorded payloads are all kept — code does not pick last-wins.
213
+ * This-court officer payloads for the parent return path (#879).
214
+ * Only settlement-scoped roleOutcome.payloads (courtAttempt seal) carry this-court
215
+ * identity. Never guess from undivided submissions — sole row included — history
216
+ * stays on terminal.submissions (#836 presentation).
204
217
  */
205
- function officerPayloads(terminal: TerminalResult | undefined): readonly unknown[] {
218
+ function thisCourtOfficerPayloads(terminal: TerminalResult | undefined): readonly unknown[] {
206
219
  const outcome = terminal?.roleOutcome;
207
220
  if (outcome !== undefined && (outcome.kind === "accepted" || outcome.kind === "audit_escalation")) {
208
- return outcome.payloads ?? terminal?.submissions ?? [];
221
+ if (outcome.payloads !== undefined && outcome.payloads.length > 0) return outcome.payloads;
222
+ }
223
+ if (outcome?.kind === "failure" && outcome.payloads !== undefined && outcome.payloads.length > 0) {
224
+ return outcome.payloads;
209
225
  }
226
+ // No sole-row identity guess: undivided submissions are not this-court (#879).
227
+ return [];
228
+ }
229
+
230
+ /** Failure/transport channel may still surface every recorded payload beside the failure (#836 A.3). */
231
+ function officerFailurePayloads(terminal: TerminalResult | undefined): readonly unknown[] {
232
+ const outcome = terminal?.roleOutcome;
210
233
  if (outcome?.kind === "failure") return outcome.payloads ?? terminal?.submissions ?? [];
211
234
  return terminal?.submissions ?? [];
212
235
  }
@@ -219,19 +242,9 @@ function projectOfficerPayloads(
219
242
  if (payloads.length === 0) {
220
243
  return projectOfficerDecision(officer, undefined, fallbackStatus);
221
244
  }
222
- // Present every recorded payload; queue only the latest conclusion so a reask
223
- // can converge (#836 呈现 ≠ 排队).
224
- const queued = projectOfficerDecision(officer, payloads[payloads.length - 1], fallbackStatus);
225
- if (payloads.length === 1) return queued;
226
- if (
227
- queued.status === "pass"
228
- || queued.status === "bounce"
229
- || queued.status === "escalate"
230
- || queued.status === "needs_reask"
231
- ) {
232
- return { ...queued, receipt: payloads };
233
- }
234
- return queued;
245
+ // This-court multi-submit: queue the latest seal of THIS court only (#836 呈现≠排队).
246
+ // Single this-court seal is the common path. Never fold history into an array receipt.
247
+ return projectOfficerDecision(officer, payloads[payloads.length - 1], fallbackStatus);
235
248
  }
236
249
 
237
250
  function projectOfficerTerminal(
@@ -240,16 +253,17 @@ function projectOfficerTerminal(
240
253
  ): GatekeeperResult {
241
254
  const terminal: TerminalResult | undefined = summoned.terminal;
242
255
  const outcome = terminal?.roleOutcome;
243
- const recorded = officerPayloads(terminal);
256
+ const thisCourt = thisCourtOfficerPayloads(terminal);
244
257
  if (outcome === undefined) {
245
258
  const detail = summoned.stderr ?? "";
259
+ const failurePayloads = officerFailurePayloads(terminal);
246
260
  return {
247
261
  status: "transport_failure",
248
262
  stage: officer,
249
263
  reason: detail.length > 0
250
264
  ? `${gateSeatLabel(officer)} public summon exit ${summoned.exitCode}: ${detail}`
251
265
  : `${gateSeatLabel(officer)} public summon produced no terminal (exit ${summoned.exitCode})`,
252
- submission: recorded.length > 0 ? recorded : summoned,
266
+ submission: failurePayloads.length > 0 ? failurePayloads : summoned,
253
267
  };
254
268
  }
255
269
  if (outcome.kind === "no_receipt") {
@@ -263,30 +277,33 @@ function projectOfficerTerminal(
263
277
  };
264
278
  }
265
279
  if (outcome.kind === "failure") {
280
+ const failurePayloads = officerFailurePayloads(terminal);
266
281
  return {
267
282
  status: "transport_failure",
268
283
  stage: officer,
269
284
  reason: outcome.diagnostic,
270
- submission: recorded.length > 0 ? recorded : outcome.decisiveFacts,
285
+ submission: failurePayloads.length > 0 ? failurePayloads : outcome.decisiveFacts,
271
286
  };
272
287
  }
273
288
  if (outcome.kind === "audit_escalation") {
274
289
  return {
275
290
  status: "escalate",
276
291
  officer,
277
- receipt: recorded.length === 1 ? recorded[0] : recorded,
292
+ // This-court receipt only (#879) — historical rows remain on terminal.submissions.
293
+ receipt: thisCourt.length > 0 ? thisCourt[thisCourt.length - 1] : retainedReceipt(outcome),
278
294
  };
279
295
  }
280
296
  if (outcome.kind === "accepted") {
281
297
  // outcome.status is the fixture/compat leaf: production settlement leaves
282
298
  // it undefined once payloads are recorded, so this only matters when a
283
299
  // caller still supplies status without any recorded payload (#836 hang).
284
- return projectOfficerPayloads(officer, recorded, outcome.status);
300
+ return projectOfficerPayloads(officer, thisCourt, outcome.status);
285
301
  }
286
302
  return {
287
303
  status: "needs_reask",
288
304
  officer,
289
- receipt: recorded.length > 0 ? recorded : retainedReceipt(outcome),
305
+ // This-court receipt only (#879).
306
+ receipt: thisCourt.length > 0 ? thisCourt[thisCourt.length - 1] : retainedReceipt(outcome),
290
307
  };
291
308
  }
292
309
 
@@ -324,13 +341,13 @@ export async function projectGatekeeperRun(
324
341
  },
325
342
  };
326
343
  }
327
- // #836: officers receive the whole parent run directory pointer and find the
328
- // submission themselves — code no longer picks latest toolCall leaf (A7.1–A7.3).
344
+ // #879: binding pointer = parent run directory; dialogue content = this-turn
345
+ // typed payload passed explicitly (never latest-toolCall recovery, A7.1–A7.3).
329
346
  let summoned: PublicSummonResult;
330
347
  try {
331
348
  const summon =
332
349
  options.summonOfficer
333
- ?? (async (nextOfficer, sourceRunDirectory, officerSignal, reask) => {
350
+ ?? (async (nextOfficer, sourceRunDirectory, officerSignal, reask, nextSubmission) => {
334
351
  const { summonGateOfficer } = await import("./public-role-summons.ts");
335
352
  return summonGateOfficer({
336
353
  officer: nextOfficer,
@@ -338,6 +355,7 @@ export async function projectGatekeeperRun(
338
355
  cwd: options.context.cwd ?? process.cwd(),
339
356
  ...(officerSignal === undefined ? {} : { signal: officerSignal }),
340
357
  ...(reask === undefined ? {} : { reask }),
358
+ ...(nextSubmission === undefined ? {} : { submission: nextSubmission }),
341
359
  ...(options.home === undefined ? {} : { home: options.home }),
342
360
  ...(options.packageRoot === undefined ? {} : { packageRoot: options.packageRoot }),
343
361
  ...(options.roleTurnHost === undefined ? {} : { roleTurnHost: options.roleTurnHost }),
@@ -349,6 +367,7 @@ export async function projectGatekeeperRun(
349
367
  runDirectory,
350
368
  options.signal,
351
369
  options.reask,
370
+ options.submission,
352
371
  );
353
372
  } catch (error) {
354
373
  return {
@@ -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) {
@@ -130,7 +130,7 @@ export type RoleTurnActivation =
130
130
  /** Required comparison-base revision for the unanchored merge-candidate diff. */
131
131
  readonly baseRevision: string;
132
132
  }
133
- | { readonly role: "inspector" }
133
+ | { readonly role: "inspector"; readonly sourceRun?: string }
134
134
  | { readonly role: "gatekeeper" }
135
135
  | { readonly role: "navigator" }
136
136
  | { readonly role: "auditor" }
@@ -160,6 +160,11 @@ export type RoleTurnHostTransition = {
160
160
  readonly priorNativePaths: readonly string[];
161
161
  };
162
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
+
163
168
  /** One main-session turn request over the host-neutral execution seam. */
164
169
  export type RoleTurnRequest = {
165
170
  readonly principal: DurablePrincipal;
@@ -241,7 +246,21 @@ export interface DurablePrincipalAuthority {
241
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; };
242
247
 
243
248
  /** Context supplied by a host for one activation and its interceptable events. */
244
- 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
+ }
245
264
 
246
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>>; /**
247
266
  * #641 chain② opt-in: when the output params carry the shared infrastructure
@@ -356,7 +375,7 @@ export interface RoleHost {
356
375
  getAllTools(): Array<{ name: string; sourceInfo?: { path?: string } }>;
357
376
  setActiveTools(names: string[]): void;
358
377
  getActiveTools(): string[];
359
- 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>;
360
379
  on(event: "before_agent_start", handler: HostEventHandler<"before_agent_start">): void;
361
380
  on(event: "input", handler: HostEventHandler<"input">): void;
362
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 }],
@@ -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;