@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
@@ -52,10 +52,10 @@ export async function runPublicNotary(argv, env, io, parseNotaryArgv) {
52
52
  throw error;
53
53
  }
54
54
  // #747: officer resume key is this parent source-run path (not ticket number).
55
- // #753/#786: gate re-ask and verbatim submission body share summons.instruction
56
- // (reask wins when both present; no parallel stack).
55
+ // #753/#879: gate re-ask and verbatim submission body share summons.instruction
56
+ // (reask wins when both present; no parallel stack). Binding stays sourceRunPath.
57
57
  {
58
- const resumeInstruction = env.reviewReask;
58
+ const resumeInstruction = env.reviewReask ?? env.gateReviewInstruction;
59
59
  const summons = {
60
60
  sourceRunPath: source.runDirectory,
61
61
  sourceRun: source,
@@ -112,7 +112,10 @@ export async function runPublicNotary(argv, env, io, parseNotaryArgv) {
112
112
  : { correlationId: env.correlationId }),
113
113
  continuation: {
114
114
  kind: "initial",
115
- prompt: buildNotaryTransportPrompt(admitted, engineMaterial),
115
+ // #879: gate path first mint carries parent payload as dialogue content;
116
+ // external zero-prompt kickoff unchanged when no body/reask.
117
+ prompt: (env.reviewReask ?? env.gateReviewInstruction)
118
+ ?? buildNotaryTransportPrompt(admitted, engineMaterial),
116
119
  },
117
120
  });
118
121
  return await runPostAdmissionOneShot({
@@ -8,13 +8,14 @@
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { readFile, writeFile } from "node:fs/promises";
10
10
  import { isAbsolute, join, resolve } from "node:path";
11
- import { buildResumeContinuationPrompt, GATE_DOSSIER_POINTER_PREFIX, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
11
+ import { buildResumeContinuationPrompt, RESUME_TRANSPORT_ENVELOPE, } from "./run-lifecycle.js";
12
12
  import { CliUsageError } from "./cli-errors.js";
13
13
  import { buildInstructionTransportPrompt, freezeAttachmentsIntoRun, } from "./invocation.js";
14
14
  import { pathContainedIn } from "../activation-ledger-topology.js";
15
15
  import { pickEngineAxis } from "../package-resources/engine-material.js";
16
16
  import { resolveHostAwareSessionAvailability } from "../session-identity.js";
17
- import { projectCaseDossierPointerSection } from "./case-dossier-delivery.js";
17
+ import { isOfficerReviewSeat } from "../host-contracts.js";
18
+ import { deliverCaseDossierAsAttachment, projectCaseDossierPointerSection, } from "./case-dossier-delivery.js";
18
19
  /** Original error bytes, never relabeled — a secondary fact riding beside a classified cause. */
19
20
  function describeCaughtError(error) {
20
21
  if (error instanceof Error) {
@@ -30,6 +31,14 @@ function appendContinuationSection(continuation, section) {
30
31
  ? { kind: "initial", prompt }
31
32
  : { kind: "resume", prompt };
32
33
  }
34
+ /**
35
+ * Nested gate summons (station child) on an officer seat: dialogue content is
36
+ * peer words only (#879). ADR 0081 case dossier still hangs via the existing
37
+ * attachments freeze seam — never into the peer body, never RoleTurnRequest.materials.
38
+ */
39
+ function isStationChildOfficerDialogue(role, env) {
40
+ return env.stationChild === true && isOfficerReviewSeat(role);
41
+ }
33
42
  import { projectHostTransitionPriorNative } from "../host-transition-prior-native.js";
34
43
  import { missingCredentialPreDispatchFailure, postRunMissingCredentialFailure, } from "./public-run-credentials.js";
35
44
  import { acquireRunWriterLease, clearCurrentCourt, clearTypedProviderHttpObservation, describeErrorIdentity, markRunRunning, readCurrentCourt, recordCurrentCourt, renderResumeCommand, RunWriterLeaseHeldError, } from "./run-lifecycle.js";
@@ -324,9 +333,9 @@ export async function dispatchPostAdmissionTurn(input) {
324
333
  // Turn request is assembled after beforeDispatch so this turn sees whatever it
325
334
  // settled — the seat's ticket bind re-projection and any court diarist station
326
335
  // writes (#742). Case dossier delivery (ADR 0081 / #709) rides here once for
327
- // every public entry: first call, same-ticket re-summons and manual resume
328
- // alike. System refs append their own neutral section; caller frozen
329
- // attachments and the seat's own prompt bytes are never rewritten.
336
+ // every public entry. #879: station-child officer dialogue keeps peer body
337
+ // intact — 起居录 hangs via existing attachments freeze (not prompt wrap,
338
+ // not RoleTurnRequest.materials). Other entries keep the neutral prompt section.
330
339
  let turnRequest = env.signal === undefined ? request : { ...request, signal: env.signal };
331
340
  if (env.stationChild !== undefined) {
332
341
  turnRequest = { ...turnRequest, stationChild: env.stationChild };
@@ -334,16 +343,31 @@ export async function dispatchPostAdmissionTurn(input) {
334
343
  if (hostTransition !== undefined) {
335
344
  turnRequest = { ...turnRequest, hostTransition };
336
345
  }
337
- const dossierSection = await projectCaseDossierPointerSection({
338
- ticketNumber: admitted.ticketNumber,
339
- projectRoot: admitted.projectRoot,
340
- home: env.home,
341
- });
342
- if (dossierSection !== undefined) {
343
- turnRequest = {
344
- ...turnRequest,
345
- continuation: appendContinuationSection(turnRequest.continuation, dossierSection),
346
- };
346
+ if (isStationChildOfficerDialogue(admitted.role, env)) {
347
+ // 0081 non-body face: freeze pointer section under run/attachments/.
348
+ // Peer dialogue continuation.prompt stays parent payload only — the seat
349
+ // consumes the freeze via loadCaseDossierReadingMaterial → existing
350
+ // agent-start readingMaterial / systemPrompt.materials fold (not prompt splice,
351
+ // not RoleTurnRequest.materials).
352
+ await deliverCaseDossierAsAttachment({
353
+ ticketNumber: admitted.ticketNumber,
354
+ projectRoot: admitted.projectRoot,
355
+ home: env.home,
356
+ runDirectory: admitted.runDirectory,
357
+ });
358
+ }
359
+ else {
360
+ const dossierSection = await projectCaseDossierPointerSection({
361
+ ticketNumber: admitted.ticketNumber,
362
+ projectRoot: admitted.projectRoot,
363
+ home: env.home,
364
+ });
365
+ if (dossierSection !== undefined) {
366
+ turnRequest = {
367
+ ...turnRequest,
368
+ continuation: appendContinuationSection(turnRequest.continuation, dossierSection),
369
+ };
370
+ }
347
371
  }
348
372
  // Authoritative host write happens here, at the real dispatch boundary —
349
373
  // immediately before the turn actually starts, after every retryable
@@ -643,46 +667,40 @@ export async function dispatchPostAdmissionTurn(input) {
643
667
  }
644
668
  }
645
669
  /**
646
- * Shared resume continuation projection (#471 / #600 / #633 / #637 / #755):
670
+ * Shared resume continuation projection (#471 / #600 / #633 / #637 / #755 / #879):
647
671
  * seat-table model/engine/timeout axes, restored correlation, and either
648
672
  * - manual resume (no same-ticket summons): package envelope / optional caller
649
673
  * message, with engine-axis handbook via buildResumeContinuationPrompt, or
650
674
  * - same-ticket summons (审核循环续话): caller/peer words + optional frozen
651
- * attachment paths only — no「重新读」、no engine handbook packaging
652
- * (#750/#755), whether or not attachments are present.
675
+ * attachment paths only — no「请重读」、no code-authored content substitute,
676
+ * no engine handbook packaging (#750/#755/#879).
653
677
  * Caller message wins as prompt base when supplied (bytes unchanged, including
654
- * blank/whitespace); else summons instruction. Attachment projection must not
655
- * re-interpret the caller message as instructionEmpty.
678
+ * blank/whitespace); else summons instruction (parent payload or reask).
679
+ * Binding pointer stays on summons.sourceRunPath / activation — not dialogue content.
656
680
  * Seats add only their activation projection. Call prepareSummonsResumeMaterials
657
681
  * first when request.summons carries instruction or attachment paths.
658
682
  */
659
683
  export function resumeTurnRequestProjectionOptions(admitted, request, env, summonsPrepared) {
660
- const officerSourcePath = request.summons?.sourceRunPath;
661
- const withReread = (body) => {
662
- if (officerSourcePath === undefined
663
- || (admitted.role !== "notary"
664
- && admitted.role !== "inspector"
665
- && admitted.role !== "auditor")) {
666
- return body;
667
- }
668
- if (body.startsWith("请重读"))
669
- return body;
670
- return `请重读\n${body}`;
671
- };
684
+ // #879: officer dialogue content is caller/peer words only — no「请重读」,
685
+ // no code-authored constant substitute, no attachment-list wrap of peer body.
686
+ // Binding pointer stays on summons sourceRunPath / activation / attachments.
687
+ const officerDialogue = isStationChildOfficerDialogue(admitted.role, env);
672
688
  let prompt;
673
689
  if (request.message !== undefined) {
674
690
  if (summonsPrepared !== undefined) {
675
- // #755: same-ticket review / open-court — caller words + optional paths.
676
- // Attachments are not a gate: message-only summons must stay plain too.
677
- prompt = withReread(buildInstructionTransportPrompt({
678
- instruction: request.message,
679
- instructionEmpty: false,
680
- attachments: summonsPrepared.attachments,
681
- }));
691
+ // #755: same-ticket review / open-court — caller words.
692
+ // #879 station-child officer: words only (attachments are independent freeze).
693
+ prompt = officerDialogue
694
+ ? request.message
695
+ : buildInstructionTransportPrompt({
696
+ instruction: request.message,
697
+ instructionEmpty: false,
698
+ attachments: summonsPrepared.attachments,
699
+ });
682
700
  }
683
701
  else if (request.summons !== undefined) {
684
702
  // #755: same-ticket summons without prepared materials — caller words only.
685
- prompt = withReread(request.message);
703
+ prompt = request.message;
686
704
  }
687
705
  else {
688
706
  // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
@@ -694,27 +712,16 @@ export function resumeTurnRequestProjectionOptions(admitted, request, env, summo
694
712
  }
695
713
  }
696
714
  else if (summonsPrepared !== undefined) {
697
- // #755: same-ticket review summons — instruction/attachments only.
698
- prompt = withReread(buildInstructionTransportPrompt(summonsPrepared));
715
+ // #879 station-child officer: instruction bytes === peer body/reask (no wrap).
716
+ // Other seats keep #755 instruction + optional attachment path listing.
717
+ prompt = officerDialogue
718
+ ? (summonsPrepared.instructionEmpty ? "" : summonsPrepared.instruction)
719
+ : buildInstructionTransportPrompt(summonsPrepared);
699
720
  }
700
721
  else if (request.summons !== undefined) {
701
- // #755: same-ticket summons with no instruction/attachments (e.g. notary
702
- // source-run pointer). #836: officer resume opening adds「请重读」+ path pointer.
703
- const path = request.summons.sourceRunPath;
704
- if (path !== undefined &&
705
- (admitted.role === "notary" ||
706
- admitted.role === "inspector" ||
707
- admitted.role === "auditor")) {
708
- prompt = `请重读\n${GATE_DOSSIER_POINTER_PREFIX}${path}`;
709
- }
710
- else if (admitted.role === "notary" ||
711
- admitted.role === "inspector" ||
712
- admitted.role === "auditor") {
713
- prompt = "请重读";
714
- }
715
- else {
716
- prompt = "";
717
- }
722
+ // #879: same-ticket summons with no instruction (e.g. notary source-run binding
723
+ // only). Pointer is activation/sourceRun material — not dialogue content.
724
+ prompt = "";
718
725
  }
719
726
  else {
720
727
  // Bare manual resume — outsourcing engine axis keeps handbook (#600/#736).
@@ -1389,6 +1389,9 @@ export async function loadResumableInspectorRun(home, runId, authority) {
1389
1389
  const admitted = {
1390
1390
  role: "inspector",
1391
1391
  ...resumedBaseAdmitted(loaded),
1392
+ ...(loaded.admittedFields.sourceRunPath === undefined
1393
+ ? {}
1394
+ : { sourceRunPath: loaded.admittedFields.sourceRunPath }),
1392
1395
  };
1393
1396
  return seatLoadedResult(loaded, admitted);
1394
1397
  }
@@ -8,7 +8,7 @@ import { appendFile, lstat, mkdir, readFile, readdir, writeFile } from "node:fs/
8
8
  import { dirname, join } from "node:path";
9
9
  import { readAnalystGateCyclesFromAuditorRoles, } from "../analyst-gate-cycles-read.js";
10
10
  import { readSitianRecords, resolveSitianRecordPath, sitianReport } from "../sitian-facade.js";
11
- import { hasFreshAttemptSubmission, readRecordedSubmissionRows, readRecordedSubmissions, } from "../submission-ledger.js";
11
+ import { hasFreshAttemptSubmission, readAttemptScopedSubmissionRows, readRecordedSubmissionRows, readRecordedSubmissions, } from "../submission-ledger.js";
12
12
  import { isAuditEscalationResult } from "../audit-escalation.js";
13
13
  import { AUDITOR_SOUL_ROLES } from "../auditor-soul.js";
14
14
  import { DOCTOR_AUDIT_TOOL_NAME } from "../doctor-auditor.js";
@@ -67,6 +67,17 @@ function roleOutcomeFromRows(role, rows) {
67
67
  return { kind: "accepted", role, payloads };
68
68
  }
69
69
  async function sealedLedgerOutcome(admitted, role, scope) {
70
+ const home = sealedLedgerHome(admitted);
71
+ // #879: when court scope is present, roleOutcome is this-court original only —
72
+ // never last-wins over an undivided historical array, and never falls back to
73
+ // full-run history when this court sealed zero rows. #836 presentation of full
74
+ // history stays on attachRecordedSubmissions / terminal.submissions (run-scoped).
75
+ if (scope?.courtAttemptId !== undefined && scope.courtAttemptId.length > 0) {
76
+ const thisCourt = await readAttemptScopedSubmissionRows(admitted.projectRoot, admitted.runId, scope.courtAttemptId, home);
77
+ // Scope present + zero this-court rows → no this-court outcome (not run history).
78
+ return roleOutcomeFromRows(role, thisCourt);
79
+ }
80
+ // No court scope: keep run-scoped ledger face (#836).
70
81
  const rows = await readRecordedSubmissionRows(admitted.projectRoot, admitted.runId, ledgerReadScope(admitted, scope));
71
82
  return roleOutcomeFromRows(role, rows);
72
83
  }
@@ -93,16 +104,22 @@ export function withSubmissions(terminal, submissions) {
93
104
  if (submissions.length === 0)
94
105
  return terminal;
95
106
  const roleOutcome = terminal.roleOutcome;
96
- // Full ledger sequence always wins the role-result block (#881): do not keep a
97
- // narrower pre-filtered payloads array when attach brings the complete set.
98
- const withPayloads = roleOutcome.kind === "accepted" || roleOutcome.kind === "audit_escalation" || roleOutcome.kind === "failure"
99
- ? { ...roleOutcome, payloads: submissions }
100
- : roleOutcome;
107
+ // #879 keeps an already scoped this-court result; #881 full run history is
108
+ // presented independently on terminal.submissions. Only fill a missing result payload.
109
+ const withPayloads = roleOutcome.kind === "accepted" || roleOutcome.kind === "audit_escalation"
110
+ ? { ...roleOutcome, payloads: (roleOutcome.payloads ?? []).length > 0 ? roleOutcome.payloads : submissions }
111
+ : roleOutcome.kind === "failure"
112
+ ? { ...roleOutcome, payloads: roleOutcome.payloads ?? submissions }
113
+ : roleOutcome;
101
114
  return { ...terminal, roleOutcome: withPayloads, submissions };
102
115
  }
103
116
  /** Attach full ledger submissions onto any settled terminal (#836). */
104
117
  export async function attachRecordedSubmissions(admitted, terminal, scope) {
105
- return withSubmissions(terminal, await recordedSubmissionPayloads(admitted, scope));
118
+ // #836 / #879: submissions presentation is always run-scoped history. Do not
119
+ // pass courtAttemptId here — roleOutcome already carries this-court payloads
120
+ // from sealedLedgerOutcome when scoped; withSubmissions keeps them.
121
+ void scope;
122
+ return withSubmissions(terminal, await recordedSubmissionPayloads(admitted, undefined));
106
123
  }
107
124
  /**
108
125
  * Host ended with no recorded submission — lawful no_receipt (exit 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
  }
@@ -151,10 +152,8 @@ function rowFromPayload(kind, payload, accepted, roleFallback) {
151
152
  * recorded attemptId + toolCallId so distinct court attempts stay distinct (#881).
152
153
  * `accepted` is the original payload; never rebuilt from a status/facts envelope.
153
154
  */
154
- export async function readRecordedSubmissionRows(cwd, runId, homeOrScope) {
155
- const scope = resolveReadScope(homeOrScope);
156
- const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope.home);
157
- const scoped = recordsForAttempt(owned, scope.attemptId);
155
+ function mapOwnedToSubmissionRows(owned) {
156
+ const scoped = owned;
158
157
  const out = [];
159
158
  const indexByCall = new Map();
160
159
  // Recover seat identity for historical non-sealed rows that omitted role (#881).
@@ -238,6 +237,29 @@ export async function readRecordedSubmissionRows(cwd, runId, homeOrScope) {
238
237
  }
239
238
  return out;
240
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
+ }
241
263
  /**
242
264
  * All raw role payloads in ledger order (#836 multi-submit).
243
265
  * Returns `accepted` bytes as stored — unknown, never re-wrapped.
@@ -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.4035",
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;