@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
@@ -159,6 +159,8 @@ async function summonPublicRole(options) {
159
159
  ...options.signal === void 0 ? {} : { signal: options.signal },
160
160
  // #753: reask rides the existing notary same-ticket resume summons.instruction.
161
161
  ...options.reviewReask === void 0 ? {} : { reviewReask: options.reviewReask },
162
+ // #879: verbatim submission body on officer dialogue content channel.
163
+ ...options.gateReviewInstruction === void 0 ? {} : { gateReviewInstruction: options.gateReviewInstruction },
162
164
  ...options.boundTicketNumber === void 0 ? {} : { boundTicketNumber: options.boundTicketNumber },
163
165
  // createRunId is the only remaining env overlay — host is seat-selected above.
164
166
  ...options.createRunId === void 0 ? {} : { createRunId: options.createRunId }
@@ -266,6 +268,11 @@ async function summonGateOfficer(options) {
266
268
  const { homeFromRunDirectory } = await import("./activation-ledger-topology.js");
267
269
  home = homeFromRunDirectory(options.sourceRunDirectory);
268
270
  }
271
+ let gateReviewInstruction;
272
+ if (options.reask === void 0 && options.submission !== void 0) {
273
+ const { readableGateItem } = await import("./readable-gate-item.js");
274
+ gateReviewInstruction = readableGateItem(options.submission);
275
+ }
269
276
  const common = {
270
277
  cwd: options.cwd,
271
278
  ...home === void 0 ? {} : { home },
@@ -273,6 +280,7 @@ async function summonGateOfficer(options) {
273
280
  ...options.io === void 0 ? {} : { io: options.io },
274
281
  ...options.signal === void 0 ? {} : { signal: options.signal },
275
282
  ...options.reask === void 0 ? {} : { reviewReask: options.reask },
283
+ ...gateReviewInstruction === void 0 ? {} : { gateReviewInstruction },
276
284
  ...options.roleTurnHost === void 0 ? {} : { roleTurnHost: options.roleTurnHost },
277
285
  ...options.hostAdapters === void 0 ? {} : { hostAdapters: options.hostAdapters },
278
286
  ...options.createRunId === void 0 ? {} : { createRunId: options.createRunId }
@@ -1,4 +1,5 @@
1
1
  import { resolveActivationLedgerHome, tryHomeFromAkRolesPath, } from "./activation-ledger-topology.js";
2
+ import { courtAttemptIdFromHostContext, runDirectoryFromHostContext, } from "./host-contracts.js";
2
3
  import { isAuditEscalationProjection } from "./audit-escalation.js";
3
4
  import { runIdFromRunDirectory } from "./run-terminal-artifacts.js";
4
5
  import { readSitianRecords, resolveSitianRecordPathInLedger, sitianReport } from "./sitian-facade.js";
@@ -10,8 +11,8 @@ import { failOnInfrastructureFailureDeclaration } from "./package-contracts/term
10
11
  * otherwise session header id. Never a shared "unbound" bucket.
11
12
  */
12
13
  function runIdentity(context) {
13
- const directory = process.env.AK_ROLE_RUN_DIR;
14
- if (typeof directory === "string" && directory.length > 0) {
14
+ const directory = runDirectoryFromHostContext(context);
15
+ if (directory !== undefined) {
15
16
  const fromDir = runIdFromRunDirectory(directory);
16
17
  if (fromDir !== undefined)
17
18
  return fromDir;
@@ -27,8 +28,8 @@ function runIdentity(context) {
27
28
  */
28
29
  export const COURT_ATTEMPT_ENV = "AK_ROLE_COURT_ATTEMPT";
29
30
  function attemptIdentity(context, runId) {
30
- const courtAttempt = process.env[COURT_ATTEMPT_ENV];
31
- if (typeof courtAttempt === "string" && courtAttempt.length > 0)
31
+ const courtAttempt = courtAttemptIdFromHostContext(context);
32
+ if (courtAttempt !== undefined)
32
33
  return courtAttempt;
33
34
  return context.sessionManager.getHeader?.()?.id ?? context.sessionManager.getLeafId?.() ?? `${runId}:initial`;
34
35
  }
@@ -110,39 +111,155 @@ function recordedRole(payload) {
110
111
  return payload.projection.role;
111
112
  return undefined;
112
113
  }
114
+ /** Prefer a more complete row for the same tool call without inventing a sole winner across calls. */
115
+ function rowRank(kind) {
116
+ switch (kind) {
117
+ case "accepted":
118
+ return 4;
119
+ case "audit-escalation":
120
+ return 3;
121
+ case "correctable-rejection":
122
+ case "infrastructure":
123
+ return 2;
124
+ case "candidate":
125
+ return 1;
126
+ }
127
+ }
113
128
  /**
114
- * All recorded role submissions in ledger order (#836 multi-submit).
129
+ * Same-call identity for reader pairing only (#881 / #836).
130
+ * attemptId is already on the record (subject/payload); bare toolCallId alone
131
+ * collapses distinct court attempts that reused a host call id.
132
+ */
133
+ function submissionCallKey(attemptId, toolCallId) {
134
+ return `${attemptId ?? ""}\0${toolCallId}`;
135
+ }
136
+ function rowFromPayload(kind, payload, accepted, roleFallback) {
137
+ const role = recordedRole(payload) ?? roleFallback;
138
+ return {
139
+ ...(role === undefined ? {} : { role }),
140
+ kind,
141
+ accepted,
142
+ ...(typeof payload.toolCallId === "string" && payload.toolCallId.length > 0
143
+ ? { toolCallId: payload.toolCallId }
144
+ : {}),
145
+ };
146
+ }
147
+ /**
148
+ * All recorded role submissions in ledger order (#836 multi-submit / #881).
149
+ * Projects every original payload — sealed, audit-escalation, correctable-rejection,
150
+ * infrastructure, and bare candidate — without outcome-class filtering.
151
+ * Same call (candidate + outcome both carrying params) appears once — keyed by
152
+ * recorded attemptId + toolCallId so distinct court attempts stay distinct (#881).
115
153
  * `accepted` is the original payload; never rebuilt from a status/facts envelope.
116
154
  */
117
- export async function readRecordedSubmissionRows(cwd, runId, homeOrScope) {
118
- const scope = resolveReadScope(homeOrScope);
119
- const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope.home);
120
- const scoped = recordsForAttempt(owned, scope.attemptId);
155
+ function mapOwnedToSubmissionRows(owned) {
156
+ const scoped = owned;
121
157
  const out = [];
158
+ const indexByCall = new Map();
159
+ // Recover seat identity for historical non-sealed rows that omitted role (#881).
160
+ const roleByCall = new Map();
161
+ for (const record of scoped) {
162
+ const payload = record.payload;
163
+ if (typeof payload?.toolCallId !== "string" || payload.toolCallId.length === 0)
164
+ continue;
165
+ const role = recordedRole(payload);
166
+ if (role !== undefined) {
167
+ roleByCall.set(submissionCallKey(recordAttemptId(record), payload.toolCallId), role);
168
+ }
169
+ }
170
+ const take = (row, callKey) => {
171
+ const toolCallId = row.toolCallId;
172
+ if (toolCallId !== undefined && callKey !== undefined) {
173
+ const existingIndex = indexByCall.get(callKey);
174
+ if (existingIndex !== undefined) {
175
+ const existing = out[existingIndex];
176
+ if (rowRank(row.kind) >= rowRank(existing.kind)) {
177
+ out[existingIndex] = {
178
+ ...row,
179
+ toolCallId,
180
+ // Keep a previously recovered role when the upgraded row still omits it.
181
+ ...(row.role === undefined && existing.role !== undefined ? { role: existing.role } : {}),
182
+ };
183
+ }
184
+ else if (existing.role === undefined && row.role !== undefined) {
185
+ out[existingIndex] = { ...existing, role: row.role };
186
+ }
187
+ return;
188
+ }
189
+ indexByCall.set(callKey, out.length);
190
+ }
191
+ out.push(row);
192
+ };
122
193
  for (const record of scoped) {
194
+ const attemptId = recordAttemptId(record);
195
+ if (record.kind === "candidate") {
196
+ const payload = record.payload;
197
+ if (payload?.type !== "candidate" || payload.params === undefined)
198
+ continue;
199
+ const callKey = typeof payload.toolCallId === "string"
200
+ ? submissionCallKey(attemptId, payload.toolCallId)
201
+ : undefined;
202
+ const fallback = callKey !== undefined ? roleByCall.get(callKey) : undefined;
203
+ take(rowFromPayload("candidate", payload, payload.params, fallback), callKey);
204
+ continue;
205
+ }
123
206
  if (record.kind === "sealed") {
124
207
  const payload = record.payload;
125
208
  if (payload?.type !== "sealed" || payload.accepted === undefined)
126
209
  continue;
127
- const role = recordedRole(payload);
128
- if (role === undefined)
129
- continue;
130
- out.push({ role, kind: "accepted", accepted: payload.accepted });
210
+ const callKey = typeof payload.toolCallId === "string"
211
+ ? submissionCallKey(attemptId, payload.toolCallId)
212
+ : undefined;
213
+ const fallback = callKey !== undefined ? roleByCall.get(callKey) : undefined;
214
+ take(rowFromPayload("accepted", payload, payload.accepted, fallback), callKey);
131
215
  continue;
132
216
  }
133
217
  if (record.kind !== "outcome")
134
218
  continue;
135
219
  const payload = record.payload;
136
- if (payload?.type !== "outcome" || payload.outcome !== "audit-escalation" || payload.accepted === undefined) {
220
+ if (payload?.type !== "outcome" || payload.accepted === undefined)
137
221
  continue;
138
- }
139
- const role = recordedRole(payload);
140
- if (role === undefined)
222
+ const outcome = payload.outcome;
223
+ const kind = outcome === "audit-escalation"
224
+ ? "audit-escalation"
225
+ : outcome === "correctable-rejection"
226
+ ? "correctable-rejection"
227
+ : outcome === "infrastructure"
228
+ ? "infrastructure"
229
+ : undefined;
230
+ if (kind === undefined)
141
231
  continue;
142
- out.push({ role, kind: "audit-escalation", accepted: payload.accepted });
232
+ const callKey = typeof payload.toolCallId === "string"
233
+ ? submissionCallKey(attemptId, payload.toolCallId)
234
+ : undefined;
235
+ const fallback = callKey !== undefined ? roleByCall.get(callKey) : undefined;
236
+ take(rowFromPayload(kind, payload, payload.accepted, fallback), callKey);
143
237
  }
144
238
  return out;
145
239
  }
240
+ /**
241
+ * All recorded role submissions in ledger order (#836 multi-submit).
242
+ * `accepted` is the original payload; never rebuilt from a status/facts envelope.
243
+ * Presentation stays run-scoped: attemptId on the read scope is a recording tag,
244
+ * not a visibility gate (#836).
245
+ */
246
+ export async function readRecordedSubmissionRows(cwd, runId, homeOrScope) {
247
+ const scope = resolveReadScope(homeOrScope);
248
+ const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope.home);
249
+ const scoped = recordsForAttempt(owned, scope.attemptId);
250
+ return mapOwnedToSubmissionRows(scoped);
251
+ }
252
+ /**
253
+ * Settlement-only this-court rows (#879 return path).
254
+ * Filters by attemptId for court-scoped roleOutcome; does not replace the
255
+ * run-scoped presentation API above (#836 visibility gate stays pass-through).
256
+ */
257
+ export async function readAttemptScopedSubmissionRows(cwd, runId, attemptId, home) {
258
+ if (attemptId.length === 0)
259
+ return [];
260
+ const { owned } = await readOwnedSubmissionRecords(cwd, runId, home);
261
+ return mapOwnedToSubmissionRows(owned.filter((record) => recordAttemptId(record) === attemptId));
262
+ }
146
263
  /**
147
264
  * All raw role payloads in ledger order (#836 multi-submit).
148
265
  * Returns `accepted` bytes as stored — unknown, never re-wrapped.
@@ -150,7 +267,7 @@ export async function readRecordedSubmissionRows(cwd, runId, homeOrScope) {
150
267
  export async function readRecordedSubmissions(cwd, runId, homeOrScope) {
151
268
  return (await readRecordedSubmissionRows(cwd, runId, homeOrScope)).map((row) => row.accepted);
152
269
  }
153
- /** True when the run has at least one recorded accepted or audit-escalation payload. */
270
+ /** True when the run has at least one recorded original payload (any outcome class). */
154
271
  export async function hasRecordedSubmission(cwd, runId, homeOrScope) {
155
272
  return (await readRecordedSubmissionRows(cwd, runId, homeOrScope)).length > 0;
156
273
  }
@@ -254,6 +371,7 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
254
371
  toolCallId,
255
372
  toolName: tool.name,
256
373
  sequence: ++state.sequence,
374
+ role,
257
375
  params,
258
376
  });
259
377
  let result;
@@ -274,6 +392,7 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
274
392
  outcome: "correctable-rejection",
275
393
  code: "typed-bounce",
276
394
  diagnostic: error instanceof Error ? error.message : String(error),
395
+ role,
277
396
  accepted: params,
278
397
  });
279
398
  throw error;
@@ -285,6 +404,7 @@ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure
285
404
  toolCallId,
286
405
  outcome: "infrastructure",
287
406
  diagnostic: error instanceof Error ? error.message : String(error),
407
+ role,
288
408
  accepted: params,
289
409
  });
290
410
  throw error;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Typed user-dialogue stdin transport (#879 / #471).
3
+ * Pi's piped-stdin reader trims the wrapper; the body field keeps original bytes.
4
+ * No size cap and no path/pointer substitute — the whole dialogue rides the envelope.
5
+ */
6
+ export const USER_DIALOGUE_STDIN_KIND = "ak-user-dialogue";
7
+ export function encodeUserDialogueStdin(body) {
8
+ return JSON.stringify({ kind: USER_DIALOGUE_STDIN_KIND, body });
9
+ }
10
+ function isUserDialogueEnvelope(value) {
11
+ if (value === null || typeof value !== "object" || Array.isArray(value))
12
+ return false;
13
+ const record = value;
14
+ return record.kind === USER_DIALOGUE_STDIN_KIND && typeof record.body === "string";
15
+ }
16
+ /**
17
+ * Host read: recover original dialogue after a trim-capable pipe.
18
+ * Non-envelope text (interactive Pi, officer JSON, headless raw body) is unchanged.
19
+ */
20
+ export function readUserDialogueStdin(raw) {
21
+ const trimmed = raw.trim();
22
+ if (!trimmed.startsWith("{"))
23
+ return raw;
24
+ try {
25
+ const parsed = JSON.parse(trimmed);
26
+ if (isUserDialogueEnvelope(parsed))
27
+ return parsed.body;
28
+ }
29
+ catch {
30
+ // Not our envelope — leave caller bytes intact.
31
+ }
32
+ return raw;
33
+ }
@@ -111,6 +111,7 @@ export default function roleRuntime(pi: ExtensionAPI): void {
111
111
  registerNavigatorModelCommand(pi);
112
112
  const navigatorSessionFactory = createNativeNavigatorSessionFactory();
113
113
  createPiRoleRuntimeExtension({
114
+ packageRoot,
114
115
  loadJudgeSoul: () => loadMainRoleSessionMaterials("judge"),
115
116
  loadFixerSoul: () => loadMainRoleSessionMaterials("fixer"),
116
117
  loadFixPacket: (path) => readFile(path, "utf8"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.4021",
3
+ "version": "0.1.4062",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,6 +1,6 @@
1
1
  # #836 删 5:从代码迁出的机器指令句清单
2
2
 
3
- 供陛下过目。不改 soul。确需对角色说的话归入本包资源(ADR 0073)。指针句(附件路径、卷宗指针、「请重读」)保留为中立机器文本,不在本表。
3
+ 供陛下过目。不改 soul。确需对角色说的话归入本包资源(ADR 0073)。指针句(附件路径、卷宗指针)保留为中立机器文本/绑定材料,不在本表。「请重读」开场已由 #879 废止,不再保留。
4
4
 
5
5
  | 原位点 | 原句(代码自写) | 处置 |
6
6
  |---|---|---|
@@ -26,7 +26,7 @@ export type DoctorAuditOptions = {
26
26
  */
27
27
  export function createPiDoctorAuditor(): (options: DoctorAuditOptions) => Promise<ComplianceDecision> {
28
28
  return async (options) => {
29
- const dossier = resolveAuditDossier();
29
+ const dossier = resolveAuditDossier(options.context);
30
30
  requireAuditMaterials(dossier);
31
31
  const subjects = readDoctorAuditSubjects(options.context);
32
32
  requireAuditMaterials(subjects);
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Unique dossier-resolution seam for 审刑院 (#233).
3
- * Machine pointers only: cwd + AK_ROLE_RUN_DIR. No latest-run / mtime / global scan.
3
+ * Machine pointers only: cwd + per-turn HostContext (Pi child env fallback).
4
+ * No latest-run / mtime / global scan.
4
5
  */
5
6
  import { existsSync, statSync } from "node:fs";
6
7
  import { resolve } from "node:path";
7
8
 
9
+ import { runDirectoryFromHostContext, type HostContext } from "./host-contracts.ts";
10
+
8
11
  export const AUDIT_RUN_DIR_ENV = "AK_ROLE_RUN_DIR" as const;
9
12
  export const DOCTOR_CANDIDATE_ENTRY_TYPE = "ak_doctor_audit_candidate" as const;
10
13
 
@@ -33,18 +36,33 @@ export type SubjectIncomplete = {
33
36
  };
34
37
  export type SubjectResolution = SubjectOk | SubjectIncomplete;
35
38
 
39
+ function isHostContext(value: object): value is HostContext {
40
+ return "sessionManager" in value;
41
+ }
42
+
43
+ function dossierPointerFrom(source?: HostContext | NodeJS.ProcessEnv): string | undefined {
44
+ if (source !== undefined && isHostContext(source)) {
45
+ return runDirectoryFromHostContext(source);
46
+ }
47
+ const env = source ?? process.env;
48
+ const raw = env[AUDIT_RUN_DIR_ENV];
49
+ return typeof raw === "string" && raw.trim() !== "" ? raw : undefined;
50
+ }
51
+
36
52
  /**
37
- * Resolve the per-run dossier pointer injected by the public CLI.
53
+ * Resolve the per-run dossier pointer.
38
54
  *
55
+ * ACP/headless: typed HostContext.runDirectory.
56
+ * Pi child: HostContext projected from env, or env fallback when no context.
39
57
  * Absent pointer = bare Pi internal seam (ADR 0052): audit proceeds; the model
40
- * self-locates the dossier from its own fall-volume position per soul. Public CLI
41
- * always injects the pointer — only then does the machine validate the path.
42
- * Concurrent runs stay isolated because a present pointer is per-process.
58
+ * self-locates the dossier from its own fall-volume position per soul.
43
59
  */
44
- export function resolveAuditDossier(env: NodeJS.ProcessEnv = process.env): DossierResolution {
45
- const raw = env[AUDIT_RUN_DIR_ENV];
60
+ export function resolveAuditDossier(
61
+ source?: HostContext | NodeJS.ProcessEnv,
62
+ ): DossierResolution {
63
+ const raw = dossierPointerFrom(source);
46
64
  // Bare Pi activation seam: no machine gate when the pointer was never injected.
47
- if (typeof raw !== "string" || raw.trim() === "") {
65
+ if (raw === undefined) {
48
66
  return { status: "ok" };
49
67
  }
50
68
  const runDirectory = resolve(raw);
@@ -9,6 +9,7 @@ import type {
9
9
  RoleTurnRequest,
10
10
  RoleTurnResult,
11
11
  } from "./host-contracts.ts";
12
+ import { isOfficerReviewSeat } from "./host-contracts.ts";
12
13
 
13
14
  export const EXTERNAL_ROLE_TURN_ROUND_LIMIT = 8 as const;
14
15
 
@@ -54,8 +55,16 @@ export function mergeRoleTurnAbortSignals(
54
55
  return AbortSignal.any([prepared, request]);
55
56
  }
56
57
 
58
+ /**
59
+ * Host-transition prior-native paths are machine handoff material (#617 DK-4).
60
+ * #879: station-child officer dialogue must not receive those paths spliced into
61
+ * the review prompt — officers already hold --source-run / binding pointers.
62
+ */
57
63
  export function promptWithPriorNativePaths(basePrompt: string, request: RoleTurnRequest): string {
58
64
  if (request.continuation.kind !== "resume") return basePrompt;
65
+ if (request.stationChild === true && isOfficerReviewSeat(request.activation.role)) {
66
+ return basePrompt;
67
+ }
59
68
  const paths = request.hostTransition?.priorNativePaths;
60
69
  if (paths === undefined || paths.length === 0) return basePrompt;
61
70
  return `${basePrompt}\n${paths.join("\n")}`;
@@ -74,6 +74,11 @@ export async function requireGatekeeperPass(options: {
74
74
  readonly signal?: AbortSignal;
75
75
  readonly hostActions: GatekeeperPassHostActions;
76
76
  readonly toolCallId: string;
77
+ /**
78
+ * In-flight parent typed payload for this gate turn (#879). Relayed verbatim
79
+ * as officer dialogue content; binding pointer stays the parent run directory.
80
+ */
81
+ readonly submission?: unknown;
77
82
  /** Lowest seam: same as runGatekeeper options.summonOfficer — offline tracers only. */
78
83
  readonly summonOfficer?: GateOfficerSummon;
79
84
  }): Promise<void> {
@@ -85,6 +90,7 @@ export async function requireGatekeeperPass(options: {
85
90
  subject: options.subject,
86
91
  ...(options.signal === undefined ? {} : { signal: options.signal }),
87
92
  ...(options.summonOfficer === undefined ? {} : { summonOfficer: options.summonOfficer }),
93
+ ...(options.submission === undefined ? {} : { submission: options.submission }),
88
94
  ...(reask === undefined ? {} : { reask }),
89
95
  });
90
96
  const gatekeeper = projected.result;
@@ -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 {