@akagilnc/pi-workflow-roles 0.1.3572 → 0.1.3621

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 (129) hide show
  1. package/dist/acp-host/production-host.js +20239 -10556
  2. package/dist/analyst-gate-cycles-read.js +387 -0
  3. package/dist/archivist-record-entry.js +22 -1
  4. package/dist/audit-escalation.js +6 -0
  5. package/dist/auditor-soul.js +74 -0
  6. package/dist/collector-config.js +86 -0
  7. package/dist/collector-evidence.js +316 -0
  8. package/dist/collector-github.js +527 -0
  9. package/dist/collector-ledger.js +853 -0
  10. package/dist/collector-tool-schemas.js +33 -0
  11. package/dist/compliance-transport.js +104 -52
  12. package/dist/diarist-mechanical.js +411 -0
  13. package/dist/diarist-ticket-resolution.js +177 -0
  14. package/dist/diarist.js +311 -0
  15. package/dist/doctor-auditor.js +25 -0
  16. package/dist/doctor-contracts.js +2 -0
  17. package/dist/doctor-evidence.js +142 -0
  18. package/dist/gatekeeper-role.js +104 -81
  19. package/dist/host-transition-prior-native.js +78 -0
  20. package/dist/institutional-resolution.js +1 -95
  21. package/dist/judge-auditor.js +26 -0
  22. package/dist/ledger-session-read.js +227 -0
  23. package/dist/merger-git-state.js +78 -0
  24. package/dist/navigator-attendance.js +15 -7
  25. package/dist/navigator-public-session.js +170 -0
  26. package/dist/navigator-session-contracts.js +36 -40
  27. package/dist/notary-source-run.js +122 -0
  28. package/dist/package-contracts/auditor-output.js +66 -0
  29. package/dist/package-contracts/evidence-child-output.js +34 -0
  30. package/dist/package-contracts/judge-output.js +2 -0
  31. package/dist/package-contracts/terminating-tools.js +25 -2
  32. package/dist/package-resources/method-skill.js +254 -0
  33. package/dist/packaged-role-registry.js +36 -0
  34. package/dist/pi/durable-principal.js +61 -0
  35. package/dist/pi/in-process-session.js +25 -6
  36. package/dist/pi/known-failure.js +52 -0
  37. package/dist/pi/role-turn-host.js +430 -0
  38. package/dist/public-cli/auto-resume.js +414 -0
  39. package/dist/public-cli/cli-errors.js +8 -0
  40. package/dist/public-cli/cli-io.js +1 -0
  41. package/dist/public-cli/command-renderer.js +5 -0
  42. package/dist/public-cli/doctor-run.js +84 -0
  43. package/dist/public-cli/inspector-run.js +136 -0
  44. package/dist/public-cli/instruction-seat-run.js +256 -0
  45. package/dist/public-cli/invocation.js +2225 -0
  46. package/dist/public-cli/judge-run.js +118 -0
  47. package/dist/public-cli/load-production-acp-host.js +40 -0
  48. package/dist/public-cli/main.js +1140 -652
  49. package/dist/public-cli/notary-run.js +161 -0
  50. package/dist/public-cli/option-definitions.js +1192 -0
  51. package/dist/public-cli/post-admission.js +667 -0
  52. package/dist/public-cli/public-run-credentials.js +49 -0
  53. package/dist/public-cli/registry.js +4 -1
  54. package/dist/public-cli/reviewer-dispatch-rejection.js +77 -0
  55. package/dist/public-cli/run-lifecycle.js +1344 -0
  56. package/dist/public-cli/seat-ticket-binding.js +113 -0
  57. package/dist/public-cli/settlement.js +3441 -0
  58. package/dist/public-cli/terminal.js +135 -0
  59. package/dist/public-cli/turn-request.js +25 -0
  60. package/dist/public-role-summons.js +301 -0
  61. package/dist/receipt-delivery-policy.js +10 -0
  62. package/dist/reviewer-child-executor.js +80 -8
  63. package/dist/reviewer-execution-ledger.js +2 -1
  64. package/dist/run-terminal-artifacts.js +195 -0
  65. package/dist/run-ticket-number.js +40 -0
  66. package/dist/session-assistant-usage.js +107 -0
  67. package/dist/shape-unreadable-failure.js +34 -0
  68. package/dist/submission-errors.js +3 -0
  69. package/dist/submission-ledger.js +388 -0
  70. package/dist/ticket-provenance-contracts.js +115 -0
  71. package/dist/ticket-provenance.js +340 -0
  72. package/extensions/role-runtime.ts +7 -0
  73. package/package.json +1 -1
  74. package/scripts/build-package.mjs +2 -1
  75. package/souls/doctor-auditor.md +1 -0
  76. package/souls/evidence-child.md +1 -0
  77. package/src/acp-host/production-host.ts +3 -0
  78. package/src/acp-host/role-envelope.ts +2 -2
  79. package/src/acp-host/role-turn-host.ts +9 -1
  80. package/src/analyst-gate-cycles-read.ts +37 -2
  81. package/src/archivist-record-entry.ts +45 -1
  82. package/src/audit-escalation.ts +17 -0
  83. package/src/auditor-role.ts +22 -0
  84. package/src/auditor-soul.ts +42 -0
  85. package/src/compliance-transport.ts +177 -66
  86. package/src/doctor-auditor.ts +10 -14
  87. package/src/doctor-contracts.ts +1 -0
  88. package/src/doctor-role.ts +2 -2
  89. package/src/evidence-child-role.ts +22 -0
  90. package/src/gatekeeper-pass-envelope.ts +109 -0
  91. package/src/gatekeeper-role.ts +145 -104
  92. package/src/host-contracts.ts +9 -1
  93. package/src/institutional-resolution.ts +8 -163
  94. package/src/judge-auditor.ts +10 -15
  95. package/src/judge-role.ts +8 -0
  96. package/src/navigator-attendance.ts +25 -9
  97. package/src/navigator-public-session.ts +217 -0
  98. package/src/navigator-session-contracts.ts +61 -54
  99. package/src/notary-source-run.ts +3 -1
  100. package/src/package-contracts/auditor-output.ts +82 -0
  101. package/src/package-contracts/evidence-child-output.ts +51 -0
  102. package/src/package-contracts/judge-output.ts +1 -0
  103. package/src/package-contracts/reviewer-output.ts +2 -2
  104. package/src/package-contracts/terminating-tools.ts +31 -1
  105. package/src/packaged-role-registry.ts +43 -0
  106. package/src/pi/adapter.ts +2 -1
  107. package/src/pi/in-process-session.ts +38 -12
  108. package/src/pi/role-turn-host.ts +34 -0
  109. package/src/public-cli/cli.ts +14 -6
  110. package/src/public-cli/instruction-seat-run.ts +289 -50
  111. package/src/public-cli/invocation.ts +66 -25
  112. package/src/public-cli/option-definitions.ts +61 -0
  113. package/src/public-cli/post-admission.ts +7 -1
  114. package/src/public-cli/registry.ts +4 -1
  115. package/src/public-cli/run-lifecycle.ts +22 -3
  116. package/src/public-cli/settlement.ts +120 -5
  117. package/src/public-cli/terminal.ts +2 -0
  118. package/src/public-role-summons.ts +431 -0
  119. package/src/receipt-delivery-policy.ts +10 -0
  120. package/src/reviewer-agent.ts +1 -1
  121. package/src/reviewer-child-executor.ts +114 -12
  122. package/src/reviewer-execution-ledger.ts +3 -2
  123. package/src/role-runtime.ts +158 -17
  124. package/src/session-assistant-usage.ts +119 -0
  125. package/src/session-opening-materials.ts +1 -1
  126. package/src/shape-unreadable-failure.ts +49 -0
  127. package/src/submission-errors.ts +3 -0
  128. package/dist/evidence-child-executor.js +0 -829
  129. package/src/evidence-child-executor.ts +0 -1140
@@ -0,0 +1,340 @@
1
+ /**
2
+ * 起居录 volume helpers — ADR 0075 / #582.
3
+ * Write/read via sitian facade only; no parallel destination logic.
4
+ */
5
+ import { createHash } from "node:crypto";
6
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { resolveSitianRecordPath, sitianReport, readSitianRecords, } from "./sitian-facade.js";
9
+ import { TICKET_PROVENANCE_HUMAN_VIEW, TICKET_PROVENANCE_KIND, TICKET_PROVENANCE_OFFERED_WATERMARK, TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC, projectTicketProvenanceDiagnostic, projectTicketProvenanceEntry, } from "./ticket-provenance-contracts.js";
10
+ /** Subject string for ticket-keyed volumes — history follows the ticket. */
11
+ export function ticketProvenanceSubject(ticketNumber) {
12
+ if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
13
+ throw new Error(`ticket-provenance subject requires a positive ticket number, got ${String(ticketNumber)}`);
14
+ }
15
+ return String(ticketNumber);
16
+ }
17
+ /**
18
+ * Deterministic entry identity for sitian entry-level idempotency.
19
+ * Same ticket + source pointer + transcript → same identity → no re-append.
20
+ */
21
+ export function ticketProvenanceEntryIdentity(input) {
22
+ const ref = input.sourceRef;
23
+ const refKey = [
24
+ ref.sessionFile ?? "",
25
+ ref.entryId === undefined ? "" : String(ref.entryId),
26
+ ref.path ?? "",
27
+ ref.url ?? "",
28
+ ].join("\u0001");
29
+ const material = [
30
+ String(input.ticketNumber),
31
+ input.sourceKind,
32
+ refKey,
33
+ input.transcript,
34
+ ].join("\u0000");
35
+ return createHash("sha256").update(material, "utf8").digest("hex");
36
+ }
37
+ /** Append one transcribed block; returns existing pointer on identity hit. */
38
+ export function appendTicketProvenanceEntry(input) {
39
+ const subject = ticketProvenanceSubject(input.ticketNumber);
40
+ const identity = ticketProvenanceEntryIdentity({
41
+ ticketNumber: input.ticketNumber,
42
+ sourceKind: input.entry.sourceKind,
43
+ sourceRef: input.entry.sourceRef,
44
+ transcript: input.entry.transcript,
45
+ });
46
+ return sitianReport({
47
+ level: "event",
48
+ kind: TICKET_PROVENANCE_KIND,
49
+ identity,
50
+ subject,
51
+ cwd: input.cwd,
52
+ ...(input.home === undefined ? {} : { home: input.home }),
53
+ host: input.host ?? "diarist",
54
+ source: input.source ?? "diarist",
55
+ payload: input.entry,
56
+ raw: input.entry.sourceRef.sessionFile !== undefined &&
57
+ input.entry.sourceRef.entryId !== undefined
58
+ ? {
59
+ sessionFile: input.entry.sourceRef.sessionFile,
60
+ entryId: input.entry.sourceRef.entryId,
61
+ }
62
+ : undefined,
63
+ });
64
+ }
65
+ /** Resolve volume paths for a ticket without writing. */
66
+ export function resolveTicketProvenanceVolume(ticketNumber, cwd, home) {
67
+ const path = resolveSitianRecordPath({
68
+ level: "event",
69
+ kind: TICKET_PROVENANCE_KIND,
70
+ subject: ticketProvenanceSubject(ticketNumber),
71
+ cwd,
72
+ ...(home === undefined ? {} : { home }),
73
+ });
74
+ return {
75
+ recordFile: path.recordFile,
76
+ volumeDir: path.sessionDir,
77
+ humanViewFile: join(path.sessionDir, TICKET_PROVENANCE_HUMAN_VIEW),
78
+ offeredWatermarkFile: join(path.sessionDir, TICKET_PROVENANCE_OFFERED_WATERMARK),
79
+ };
80
+ }
81
+ /** Honest failure when the offered-identity watermark cannot be read as written. */
82
+ export class TicketProvenanceWatermarkError extends Error {
83
+ code = "ticket-provenance-watermark";
84
+ reason;
85
+ constructor(reason, message, options) {
86
+ super(message, options);
87
+ this.name = "TicketProvenanceWatermarkError";
88
+ this.reason = reason;
89
+ }
90
+ }
91
+ /**
92
+ * Read identities already offered to the collector (append-only watermark).
93
+ * Absent file → empty set. Any non-empty line that is not a JSON object with a
94
+ * non-empty string `identity` throws TicketProvenanceWatermarkError (失败诚实:
95
+ * never interpret corruption as "unseen" and silently re-offer).
96
+ */
97
+ export function readOfferedIdentities(ticketNumber, cwd, home) {
98
+ const { offeredWatermarkFile } = resolveTicketProvenanceVolume(ticketNumber, cwd, home);
99
+ if (!existsSync(offeredWatermarkFile))
100
+ return new Set();
101
+ let text;
102
+ try {
103
+ text = readFileSync(offeredWatermarkFile, "utf8");
104
+ }
105
+ catch (error) {
106
+ throw new TicketProvenanceWatermarkError("unreadable", `ticket-provenance offered watermark unreadable (${offeredWatermarkFile})`, { cause: error });
107
+ }
108
+ const seen = new Set();
109
+ const lines = text.split("\n");
110
+ for (let index = 0; index < lines.length; index += 1) {
111
+ const trimmed = lines[index].trim();
112
+ if (!trimmed)
113
+ continue;
114
+ let parsed;
115
+ try {
116
+ parsed = JSON.parse(trimmed);
117
+ }
118
+ catch (error) {
119
+ throw new TicketProvenanceWatermarkError("malformed-json", `ticket-provenance offered watermark malformed JSON at line ${index + 1} (${offeredWatermarkFile})`, { cause: error });
120
+ }
121
+ if (typeof parsed !== "object" ||
122
+ parsed === null ||
123
+ Array.isArray(parsed) ||
124
+ typeof parsed.identity !== "string" ||
125
+ parsed.identity.length === 0) {
126
+ throw new TicketProvenanceWatermarkError("bad-shape", `ticket-provenance offered watermark bad shape at line ${index + 1} (${offeredWatermarkFile})`);
127
+ }
128
+ seen.add(parsed.identity);
129
+ }
130
+ return seen;
131
+ }
132
+ /**
133
+ * Append offered identities after a successful collector pass (selected or not).
134
+ * Idempotent per identity within the file (skip already-present). Creates volume
135
+ * dir only when writing the first watermark row.
136
+ */
137
+ export function recordOfferedIdentities(input) {
138
+ if (input.identities.length === 0)
139
+ return;
140
+ const { volumeDir, offeredWatermarkFile } = resolveTicketProvenanceVolume(input.ticketNumber, input.cwd, input.home);
141
+ const already = new Set(readOfferedIdentities(input.ticketNumber, input.cwd, input.home));
142
+ const rows = [];
143
+ for (const identity of input.identities) {
144
+ if (identity.length === 0 || already.has(identity))
145
+ continue;
146
+ rows.push(`${JSON.stringify({ identity })}\n`);
147
+ already.add(identity);
148
+ }
149
+ if (rows.length === 0)
150
+ return;
151
+ mkdirSync(volumeDir, { recursive: true });
152
+ appendFileSync(offeredWatermarkFile, rows.join(""), "utf8");
153
+ }
154
+ /** Read projected diary entries + diagnostics for a ticket (empty when volume absent). */
155
+ export async function readTicketProvenance(ticketNumber, cwd, home) {
156
+ const { recordFile } = resolveTicketProvenanceVolume(ticketNumber, cwd, home);
157
+ const { records } = await readSitianRecords(recordFile);
158
+ const entries = [];
159
+ const diagnostics = [];
160
+ let skipped = 0;
161
+ for (const record of records) {
162
+ if (record.kind !== TICKET_PROVENANCE_KIND) {
163
+ skipped += 1;
164
+ continue;
165
+ }
166
+ // Diagnostics first — same partition, separate projection (never body).
167
+ const diagnostic = projectTicketProvenanceDiagnostic(record.payload);
168
+ if (diagnostic !== undefined) {
169
+ diagnostics.push(diagnostic);
170
+ continue;
171
+ }
172
+ const entry = projectTicketProvenanceEntry(record.payload);
173
+ if (entry === undefined) {
174
+ skipped += 1;
175
+ continue;
176
+ }
177
+ entries.push(entry);
178
+ }
179
+ return { entries, diagnostics, records, recordFile, skipped };
180
+ }
181
+ /**
182
+ * Fence long enough that any backtick run inside `text` cannot close the block.
183
+ * Presentation helper only — not a machine contract; not exported.
184
+ */
185
+ function markdownFenceFor(text) {
186
+ let longest = 0;
187
+ const runs = text.match(/`+/g);
188
+ if (runs !== null) {
189
+ for (const run of runs) {
190
+ if (run.length > longest)
191
+ longest = run.length;
192
+ }
193
+ }
194
+ return "`".repeat(Math.max(3, longest + 1));
195
+ }
196
+ /**
197
+ * Render a local human-read markdown view from entries.
198
+ * Presentation only — machines bite JSONL. No wording lock for consumers.
199
+ */
200
+ export function renderTicketProvenanceMarkdown(input) {
201
+ const lines = [
202
+ `# 起居录 · #${input.ticketNumber}`,
203
+ "",
204
+ `条目数:${input.entries.length}`,
205
+ "",
206
+ ];
207
+ let index = 0;
208
+ for (const entry of input.entries) {
209
+ index += 1;
210
+ lines.push(`## ${index}. ${entry.sourceKind} · ${entry.timestamp}`);
211
+ lines.push("");
212
+ lines.push(`- basis.method: \`${entry.basis.method}\``);
213
+ if (entry.basis.anchors !== undefined && entry.basis.anchors.length > 0) {
214
+ lines.push(`- anchors: ${entry.basis.anchors.map((a) => `\`${a}\``).join(", ")}`);
215
+ }
216
+ if (entry.basis.note !== undefined) {
217
+ lines.push(`- note: ${entry.basis.note}`);
218
+ }
219
+ const refParts = [];
220
+ if (entry.sourceRef.sessionFile !== undefined) {
221
+ refParts.push(`sessionFile=${entry.sourceRef.sessionFile}`);
222
+ }
223
+ if (entry.sourceRef.entryId !== undefined) {
224
+ refParts.push(`entryId=${String(entry.sourceRef.entryId)}`);
225
+ }
226
+ if (entry.sourceRef.path !== undefined) {
227
+ refParts.push(`path=${entry.sourceRef.path}`);
228
+ }
229
+ if (entry.sourceRef.url !== undefined) {
230
+ refParts.push(`url=${entry.sourceRef.url}`);
231
+ }
232
+ if (refParts.length > 0) {
233
+ lines.push(`- sourceRef: ${refParts.join(" · ")}`);
234
+ }
235
+ lines.push("");
236
+ const fence = markdownFenceFor(entry.transcript);
237
+ lines.push(fence);
238
+ lines.push(entry.transcript);
239
+ lines.push(fence);
240
+ lines.push("");
241
+ }
242
+ return lines.join("\n");
243
+ }
244
+ /**
245
+ * Ensure the per-ticket JSONL volume partition exists (empty file OK).
246
+ * Every bound-ticket court establishes the volume even with zero diary entries
247
+ * (ADR 0075 ticket-provenance-file — 每票一份起居录). Does not forge entries.
248
+ */
249
+ export function ensureTicketProvenanceVolume(ticketNumber, cwd, home) {
250
+ const volume = resolveTicketProvenanceVolume(ticketNumber, cwd, home);
251
+ mkdirSync(volume.volumeDir, { recursive: true });
252
+ // Append-open creates an absent volume without ever truncating rows committed
253
+ // by a concurrent first writer.
254
+ appendFileSync(volume.recordFile, "", "utf8");
255
+ return volume;
256
+ }
257
+ /**
258
+ * Append a typed diagnostic onto the ticket-provenance volume (append-only).
259
+ * Same sitian kind + subject partition as diary entries; payload uses recordClass
260
+ * discriminator — never forged as a source entry. No sidecar / parallel ledger.
261
+ */
262
+ export function appendTicketProvenanceDiagnostic(input) {
263
+ const subject = ticketProvenanceSubject(input.ticketNumber);
264
+ // quote-verify: content-stable identity so mid-batch retry does not duplicate.
265
+ // collector/issue-source: recordedAt keeps per-court failure history.
266
+ const identityParts = input.diagnostic.diagnosticKind === "quote-verify-failed"
267
+ ? [
268
+ subject,
269
+ input.diagnostic.recordClass,
270
+ input.diagnostic.diagnosticKind,
271
+ input.diagnostic.cause,
272
+ input.diagnostic.reason ?? "",
273
+ ]
274
+ : [
275
+ subject,
276
+ input.diagnostic.recordClass,
277
+ input.diagnostic.diagnosticKind,
278
+ input.diagnostic.recordedAt,
279
+ input.diagnostic.cause,
280
+ input.diagnostic.reason ?? "",
281
+ ];
282
+ const identity = createHash("sha256")
283
+ .update(identityParts.join("\u0000"), "utf8")
284
+ .digest("hex");
285
+ return sitianReport({
286
+ level: "event",
287
+ kind: TICKET_PROVENANCE_KIND,
288
+ identity,
289
+ subject,
290
+ cwd: input.cwd,
291
+ ...(input.home === undefined ? {} : { home: input.home }),
292
+ host: input.host ?? "diarist",
293
+ source: input.source ?? "diarist-diagnostic",
294
+ payload: input.diagnostic,
295
+ });
296
+ }
297
+ /** Issue-face source acquisition failure — typed diagnostic on the ticket volume. */
298
+ export function appendIssueSourceFailureDiagnostic(input) {
299
+ const recordedAt = input.recordedAt ?? new Date().toISOString();
300
+ return appendTicketProvenanceDiagnostic({
301
+ ticketNumber: input.ticketNumber,
302
+ cwd: input.cwd,
303
+ ...(input.home === undefined ? {} : { home: input.home }),
304
+ source: "diarist-issue-source",
305
+ diagnostic: {
306
+ recordClass: TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC,
307
+ diagnosticKind: "issue-source-failed",
308
+ cause: input.cause,
309
+ recordedAt,
310
+ reason: input.reason,
311
+ },
312
+ });
313
+ }
314
+ /** Quote reverse-verify failure — single diagnostic expression (not a diary entry). */
315
+ export function appendQuoteVerifyFailureDiagnostic(input) {
316
+ const recordedAt = input.recordedAt ?? new Date().toISOString();
317
+ return appendTicketProvenanceDiagnostic({
318
+ ticketNumber: input.ticketNumber,
319
+ cwd: input.cwd,
320
+ ...(input.home === undefined ? {} : { home: input.home }),
321
+ source: "diarist-quote-verify",
322
+ diagnostic: {
323
+ recordClass: TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC,
324
+ diagnosticKind: "quote-verify-failed",
325
+ cause: input.cause,
326
+ recordedAt,
327
+ },
328
+ });
329
+ }
330
+ /** Write the co-located human view next to the JSONL volume (derived, not dual-source).
331
+ * Ensures volume dir exists so empty courts still get the md face. */
332
+ export function writeTicketProvenanceHumanView(input) {
333
+ const volume = ensureTicketProvenanceVolume(input.ticketNumber, input.cwd, input.home);
334
+ const md = renderTicketProvenanceMarkdown({
335
+ ticketNumber: input.ticketNumber,
336
+ entries: input.entries,
337
+ });
338
+ writeFileSync(volume.humanViewFile, md, "utf8");
339
+ return volume.humanViewFile;
340
+ }
@@ -36,10 +36,15 @@ import {
36
36
  formatNavigatorRoleHelp,
37
37
  } from "../src/role-runtime.ts";
38
38
  import { createPiJudgeAuditor } from "../src/judge-auditor.ts";
39
+ import { loadAuditorSoulFromSubjectInput } from "../src/auditor-soul.ts";
39
40
  import { loadGatekeeperSessionMaterials, loadMainRoleSessionMaterials } from "../src/session-opening-materials.ts";
40
41
  const extensionPath = fileURLToPath(import.meta.url);
41
42
  const packageRoot = fileURLToPath(new URL("..", import.meta.url));
42
43
  const navigatorRoutePlaybookPath = fileURLToPath(new URL("../resources/navigator-route-playbook.md", import.meta.url));
44
+ // #675: nested public summons (gate/auditor/evidence-child) resolve root via env under jiti.
45
+ if (process.env.AK_ROLE_PACKAGE_ROOT === undefined || process.env.AK_ROLE_PACKAGE_ROOT.trim() === "") {
46
+ process.env.AK_ROLE_PACKAGE_ROOT = packageRoot;
47
+ }
43
48
 
44
49
  // Cold `pi -e <extension> --help` must cover installed-package process startup under CI load.
45
50
  // This bound is process-startup budget only — not settlement-to-visible presentation latency.
@@ -131,6 +136,8 @@ export default function roleRuntime(pi: ExtensionAPI): void {
131
136
  loadInspectorSoul: () => loadMainRoleSessionMaterials("inspector"),
132
137
  loadGatekeeperSoul: () => loadGatekeeperSessionMaterials("gatekeeper"),
133
138
  loadNavigatorSoul: () => loadMainRoleSessionMaterials("navigator"),
139
+ loadAuditorSoul: () => loadAuditorSoulFromSubjectInput(),
140
+ loadEvidenceChildSoul: () => loadMainRoleSessionMaterials("evidence-child"),
134
141
  loadNotarySourceRun: loadNotarySourceRunLocator,
135
142
  loadNavigatorWorkContext: (options) => loadNavigatorWorkContext(pi, options),
136
143
  createNavigatorAttendance: (options) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3572",
3
+ "version": "0.1.3621",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -14,8 +14,9 @@ const entries = [
14
14
  // Navigator package-graph dependencies used by attendance settlement.
15
15
  // navigator-session-contracts is a static import of the published attendance root
16
16
  // (#590: published non-bundle graph must stay closed under its own relative edges).
17
- "evidence-child-executor",
17
+ "navigator-public-session",
18
18
  "navigator-session-contracts",
19
+ "public-role-summons",
19
20
  "pi/in-process-session",
20
21
  "activation-ledger-git",
21
22
  "activation-ledger-topology",
@@ -21,3 +21,4 @@
21
21
  冲突导致无法判断合规时,提交 `escalate` 并写明问题和可选项。
22
22
 
23
23
  输出调用 `ak_doctor_audit_decision`。
24
+
@@ -0,0 +1 @@
1
+ # 取证子
@@ -23,6 +23,7 @@ import { loadPackagedCanonicalSkillBinding } from "../package-resources/method-s
23
23
  import { createPerDispatchReviewerAgent } from "../reviewer-agent.ts";
24
24
  import { formatNavigatorRoleHelp, type RoleRuntimeDependencies } from "../role-runtime.ts";
25
25
  import { createReviewerPinnedGitReader } from "../reviewer-pinned-git.ts";
26
+ import { loadAuditorSoulFromSubjectInput } from "../auditor-soul.ts";
26
27
  import { loadGatekeeperSessionMaterials, loadMainRoleSessionMaterials } from "../session-opening-materials.ts";
27
28
  import { acpStdioArgs, resolveAcpBinary, type AcpHostDescription } from "./description.ts";
28
29
  import { createComposedAcpRoleTurnHost } from "./role-envelope.ts";
@@ -61,6 +62,8 @@ export function createAcpRoleRuntimeDependencies(packageRoot: string): RoleRunti
61
62
  loadInspectorSoul: () => loadMainRoleSessionMaterials("inspector"),
62
63
  loadGatekeeperSoul: () => loadGatekeeperSessionMaterials("gatekeeper"),
63
64
  loadNavigatorSoul: () => loadMainRoleSessionMaterials("navigator"),
65
+ loadAuditorSoul: () => loadAuditorSoulFromSubjectInput(),
66
+ loadEvidenceChildSoul: () => loadMainRoleSessionMaterials("evidence-child"),
64
67
  loadNotarySoul: () => loadMainRoleSessionMaterials("notary"),
65
68
  loadCountersignSoul: () => loadMainRoleSessionMaterials("countersign"),
66
69
  loadGleanerLeftSoul: () => loadMainRoleSessionMaterials("gleaner-left"),
@@ -4,7 +4,7 @@ import { createServer, type Server, type Socket } from "node:net";
4
4
  import { basename, dirname, join } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
- import { requireGatekeeperPass } from "../gatekeeper-role.ts";
7
+ import { requireGatekeeperPass } from "../gatekeeper-pass-envelope.ts";
8
8
  import type {
9
9
  HostContext,
10
10
  HostEventRegistration,
@@ -298,7 +298,7 @@ export async function prepareAcpRoleEnvelope(options: {
298
298
  if (record.kind === "role_infrastructure_failure") return;
299
299
  const code = typeof record.code === "string" && record.code.length > 0
300
300
  ? record.code
301
- : record.status === "bounce" || record.status === "no_receipt"
301
+ : record.status === "bounce" || record.status === "no_receipt" || record.status === "unreadable"
302
302
  ? record.status
303
303
  : undefined;
304
304
  if (code === undefined) return;
@@ -270,7 +270,15 @@ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHo
270
270
  priorNativePaths !== undefined && priorNativePaths.length > 0
271
271
  ? `${prepared.prompt}\n${priorNativePaths.join("\n")}`
272
272
  : prepared.prompt;
273
- const abortSignal = prepared.abortSignal;
273
+ // Envelope infra abort and parent cancellation (#675 nested summons) are
274
+ // the same race face here: either one ends the ACP round and the finally
275
+ // block cancels + closes the child session.
276
+ const abortSignal =
277
+ request.signal === undefined
278
+ ? prepared.abortSignal
279
+ : prepared.abortSignal === undefined
280
+ ? request.signal
281
+ : AbortSignal.any([prepared.abortSignal, request.signal]);
274
282
  const activeConnection = connection;
275
283
 
276
284
  /** Race ACP prompt against envelope abort so infra failInfrastructure cannot hang (#593). */
@@ -434,8 +434,34 @@ function pairGateRounds(
434
434
  .map((round, index) => ({ ...round, roundIndex: index + 1 }));
435
435
  }
436
436
 
437
+ /** Resolve a direct-officer-run-pointer file to the officer session 正本 path. */
438
+ async function resolveOfficerSessionFromPointerFile(
439
+ pointerPath: string,
440
+ ): Promise<string | undefined> {
441
+ const { readFile } = await import("node:fs/promises");
442
+ let raw: unknown;
443
+ try {
444
+ raw = JSON.parse(await readFile(pointerPath, "utf8"));
445
+ } catch (error) {
446
+ throw new Error(
447
+ `direct officer run pointer unreadable in ${pointerPath}: ${error instanceof Error ? error.message : String(error)}`,
448
+ { cause: error },
449
+ );
450
+ }
451
+ if (!isRecord(raw) || raw.kind !== "direct-officer-run-pointer" || raw.version !== 1) {
452
+ throw new Error(`direct officer run pointer has unknown shape in ${pointerPath}`);
453
+ }
454
+ const sessionFile = raw.sessionFile;
455
+ if (typeof sessionFile !== "string" || sessionFile.trim() === "") {
456
+ throw new Error(`direct officer run pointer missing sessionFile in ${pointerPath}`);
457
+ }
458
+ return sessionFile;
459
+ }
460
+
437
461
  /**
438
462
  * Read and pair gate-cycle rounds from one or more auditor-roles directories.
463
+ * Accepts historical nested JSONL volumes and #675 direct-officer-run-pointer
464
+ * files that name the independent officer session 正本.
439
465
  * ENOENT (directory truly absent) → []. ENOTDIR and other errors propagate
440
466
  * (failure honesty — damaged topology must not wash to zero rounds).
441
467
  *
@@ -456,7 +482,11 @@ export async function readAnalystGateCyclesFromAuditorRoles(
456
482
  try {
457
483
  const entries = await readdir(directory, { withFileTypes: true });
458
484
  names = entries
459
- .filter((e) => e.isFile() && e.name.endsWith(".jsonl"))
485
+ .filter(
486
+ (e) =>
487
+ e.isFile()
488
+ && (e.name.endsWith(".jsonl") || e.name.endsWith(".pointer.json")),
489
+ )
460
490
  .map((e) => e.name)
461
491
  .sort();
462
492
  } catch (error) {
@@ -464,7 +494,12 @@ export async function readAnalystGateCyclesFromAuditorRoles(
464
494
  throw error;
465
495
  }
466
496
  for (const name of names) {
467
- const classified = await classifyAuditorVolume(join(directory, name));
497
+ const path = join(directory, name);
498
+ const sessionPath = name.endsWith(".pointer.json")
499
+ ? await resolveOfficerSessionFromPointerFile(path)
500
+ : path;
501
+ if (sessionPath === undefined) continue;
502
+ const classified = await classifyAuditorVolume(sessionPath);
468
503
  for (const volume of classified) {
469
504
  if (
470
505
  options.parentSessionFile !== undefined &&
@@ -3,7 +3,7 @@
3
3
  * 调用方只声明自己是谁的什么;落点由候簿拓扑算出,签名不含任何落点/路径参数。
4
4
  * 「谁调了谁」复用 Pi parentSession + ADR 0047 correlation,不新增 caller 字段。
5
5
  */
6
- import { existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
6
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
7
7
  import { dirname, resolve, join } from "node:path";
8
8
  import { SessionManager } from "@earendil-works/pi-coding-agent";
9
9
 
@@ -224,3 +224,47 @@ export function createRecordSessionOpen(options: CreateRecordSessionOptions): Re
224
224
  export function createRecordSession(options: CreateRecordSessionOptions): SessionManager {
225
225
  return createRecordSessionOpen(options).session;
226
226
  }
227
+
228
+
229
+ /** Typed parent-side pointer to an independent officer run 正本 (ADR 0079 / #675). */
230
+ export const DIRECT_OFFICER_RUN_POINTER_KIND = "direct-officer-run-pointer" as const;
231
+
232
+ export type DirectOfficerRunPointer = {
233
+ readonly version: 1;
234
+ readonly kind: typeof DIRECT_OFFICER_RUN_POINTER_KIND;
235
+ readonly officer: "inspector" | "notary";
236
+ /** Absolute path to the officer session.jsonl 正本. */
237
+ readonly sessionFile: string;
238
+ /** Officer run directory when known. */
239
+ readonly runDirectory?: string;
240
+ };
241
+
242
+ /**
243
+ * Book a typed pointer under parent session/auditor-roles (same nest owner as
244
+ * createRecordSession). Never fabricates user/assistant/toolResult rows (#675).
245
+ * Directory placement stays with the archivist record entry (ADR 0018 / 0065).
246
+ */
247
+ export function bookDirectOfficerRunPointer(options: {
248
+ readonly parentSessionFile: string;
249
+ readonly officer: "inspector" | "notary";
250
+ readonly sessionFile: string;
251
+ readonly runDirectory?: string;
252
+ }): DirectOfficerRunPointer {
253
+ const nest = join(dirname(options.parentSessionFile), "auditor-roles");
254
+ mkdirSync(nest, { recursive: true });
255
+ const pointer: DirectOfficerRunPointer = {
256
+ version: 1,
257
+ kind: DIRECT_OFFICER_RUN_POINTER_KIND,
258
+ officer: options.officer,
259
+ sessionFile: options.sessionFile,
260
+ ...(options.runDirectory !== undefined && options.runDirectory.trim() !== ""
261
+ ? { runDirectory: options.runDirectory }
262
+ : {}),
263
+ };
264
+ writeFileSync(
265
+ join(nest, `${options.officer}-${Date.now().toString(36)}.pointer.json`),
266
+ `${JSON.stringify(pointer)}\n`,
267
+ "utf8",
268
+ );
269
+ return pointer;
270
+ }
@@ -175,6 +175,14 @@ export type ComplianceDecisionHandlers<T> = {
175
175
  facts: Extract<ComplianceDecision, { status: "no-receipt" }>,
176
176
  usageProjection: { usage?: Usage },
177
177
  ) => T | PromiseLike<T>;
178
+ /**
179
+ * Parent work stands with typed unreadable audit fact (ADR 0055 / §0).
180
+ * Must not collapse into ordinary pass (forged role decision).
181
+ */
182
+ unreadable?: (
183
+ facts: Extract<ComplianceDecision, { status: "unreadable" }>,
184
+ usageProjection: { usage?: Usage },
185
+ ) => T | PromiseLike<T>;
178
186
  revise: (violations: readonly unknown[]) => T | PromiseLike<T>;
179
187
  escalate: (result: AuditEscalationToolResult) => T | PromiseLike<T>;
180
188
  };
@@ -201,6 +209,15 @@ export async function disposeComplianceDecision<T>(
201
209
  decision,
202
210
  decision.usage === undefined ? {} : { usage: decision.usage },
203
211
  );
212
+ case "unreadable":
213
+ // Parent candidate stands; audit shape failure is a typed parallel fact — not pass.
214
+ if (handlers.unreadable === undefined) {
215
+ throw new Error("Compliance unreadable projection handler is unavailable");
216
+ }
217
+ return await handlers.unreadable(
218
+ decision,
219
+ decision.usage === undefined ? {} : { usage: decision.usage },
220
+ );
204
221
  case "revise":
205
222
  return await handlers.revise(decision.violations);
206
223
  case "escalate":
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Public Auditor (审刑院) role — filed-officer envelope (#675).
3
+ */
4
+ import {
5
+ AUDITOR_ACCEPTED_TEXT,
6
+ AUDITOR_OUTPUT_TOOL_NAME,
7
+ auditorOutputSchema,
8
+ } from "./package-contracts/auditor-output.ts";
9
+
10
+ export { AUDITOR_ACCEPTED_TEXT, AUDITOR_OUTPUT_TOOL_NAME };
11
+
12
+ export type AuditorRuntimeDependencies = {
13
+ loadSoul(): Promise<string>;
14
+ };
15
+
16
+ export const AUDITOR_TOOL_SPEC = {
17
+ name: AUDITOR_OUTPUT_TOOL_NAME,
18
+ label: "审刑院输出",
19
+ description: "审刑院终局回执,状态为 pass、revise 或 escalate。",
20
+ promptSnippet: "审刑院终局回执",
21
+ parameters: auditorOutputSchema,
22
+ } as const;