@akagilnc/pi-workflow-roles 0.1.3565 → 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 (142) hide show
  1. package/README.md +4 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/production-host.js +18477 -8099
  4. package/dist/analyst-gate-cycles-read.js +387 -0
  5. package/dist/archivist-record-entry.js +22 -1
  6. package/dist/audit-escalation.js +6 -0
  7. package/dist/auditor-soul.js +74 -0
  8. package/dist/collector-config.js +86 -0
  9. package/dist/collector-evidence.js +316 -0
  10. package/dist/collector-github.js +527 -0
  11. package/dist/collector-ledger.js +853 -0
  12. package/dist/collector-tool-schemas.js +33 -0
  13. package/dist/compliance-transport.js +104 -52
  14. package/dist/diarist-contracts.js +72 -0
  15. package/dist/diarist-mechanical.js +411 -0
  16. package/dist/diarist-ticket-resolution.js +177 -0
  17. package/dist/diarist.js +311 -0
  18. package/dist/doctor-auditor.js +25 -0
  19. package/dist/doctor-contracts.js +2 -0
  20. package/dist/doctor-evidence.js +142 -0
  21. package/dist/gatekeeper-role.js +104 -81
  22. package/dist/host-transition-prior-native.js +78 -0
  23. package/dist/institutional-resolution.js +1 -95
  24. package/dist/judge-auditor.js +26 -0
  25. package/dist/ledger-session-read.js +227 -0
  26. package/dist/merger-git-state.js +78 -0
  27. package/dist/navigator-attendance.js +15 -7
  28. package/dist/navigator-public-session.js +170 -0
  29. package/dist/navigator-session-contracts.js +36 -40
  30. package/dist/notary-source-run.js +122 -0
  31. package/dist/package-contracts/auditor-output.js +66 -0
  32. package/dist/package-contracts/evidence-child-output.js +34 -0
  33. package/dist/package-contracts/judge-output.js +2 -0
  34. package/dist/package-contracts/terminating-tools.js +44 -3
  35. package/dist/package-resources/method-skill.js +254 -0
  36. package/dist/packaged-role-registry.js +56 -0
  37. package/dist/pi/durable-principal.js +61 -0
  38. package/dist/pi/in-process-session.js +25 -6
  39. package/dist/pi/known-failure.js +52 -0
  40. package/dist/pi/role-turn-host.js +430 -0
  41. package/dist/public-cli/auto-resume.js +414 -0
  42. package/dist/public-cli/cli-errors.js +8 -0
  43. package/dist/public-cli/cli-io.js +1 -0
  44. package/dist/public-cli/command-renderer.js +5 -0
  45. package/dist/public-cli/doctor-run.js +84 -0
  46. package/dist/public-cli/inspector-run.js +136 -0
  47. package/dist/public-cli/instruction-seat-run.js +256 -0
  48. package/dist/public-cli/invocation.js +2225 -0
  49. package/dist/public-cli/judge-run.js +118 -0
  50. package/dist/public-cli/load-production-acp-host.js +40 -0
  51. package/dist/public-cli/main.js +1511 -1124
  52. package/dist/public-cli/notary-run.js +161 -0
  53. package/dist/public-cli/option-definitions.js +1192 -0
  54. package/dist/public-cli/post-admission.js +667 -0
  55. package/dist/public-cli/public-run-credentials.js +49 -0
  56. package/dist/public-cli/registry.js +8 -2
  57. package/dist/public-cli/reviewer-dispatch-rejection.js +77 -0
  58. package/dist/public-cli/run-lifecycle.js +1344 -0
  59. package/dist/public-cli/seat-ticket-binding.js +113 -0
  60. package/dist/public-cli/settlement.js +3441 -0
  61. package/dist/public-cli/terminal.js +135 -0
  62. package/dist/public-cli/turn-request.js +25 -0
  63. package/dist/public-role-summons.js +301 -0
  64. package/dist/receipt-delivery-policy.js +10 -0
  65. package/dist/reviewer-child-executor.js +80 -8
  66. package/dist/reviewer-execution-ledger.js +2 -1
  67. package/dist/run-terminal-artifacts.js +195 -0
  68. package/dist/run-ticket-number.js +40 -0
  69. package/dist/session-assistant-usage.js +107 -0
  70. package/dist/shape-unreadable-failure.js +34 -0
  71. package/dist/submission-errors.js +3 -0
  72. package/dist/submission-ledger.js +388 -0
  73. package/dist/ticket-provenance-contracts.js +115 -0
  74. package/dist/ticket-provenance.js +340 -0
  75. package/extensions/role-runtime.ts +8 -0
  76. package/package.json +1 -1
  77. package/resources/diarist-collect.md +6 -10
  78. package/scripts/build-package.mjs +2 -1
  79. package/souls/diarist.md +9 -0
  80. package/souls/doctor-auditor.md +1 -0
  81. package/souls/evidence-child.md +1 -0
  82. package/src/acp-host/production-host.ts +4 -0
  83. package/src/acp-host/role-envelope.ts +4 -3
  84. package/src/acp-host/role-turn-host.ts +9 -1
  85. package/src/analyst-gate-cycles-read.ts +37 -2
  86. package/src/archivist-record-entry.ts +45 -1
  87. package/src/audit-escalation.ts +17 -0
  88. package/src/auditor-role.ts +22 -0
  89. package/src/auditor-soul.ts +42 -0
  90. package/src/compliance-transport.ts +177 -66
  91. package/src/diarist-contracts.ts +88 -0
  92. package/src/diarist-role.ts +60 -0
  93. package/src/diarist.ts +253 -179
  94. package/src/doctor-auditor.ts +10 -14
  95. package/src/doctor-contracts.ts +1 -0
  96. package/src/doctor-role.ts +2 -2
  97. package/src/evidence-child-role.ts +22 -0
  98. package/src/gatekeeper-pass-envelope.ts +109 -0
  99. package/src/gatekeeper-role.ts +145 -104
  100. package/src/host-contracts.ts +15 -2
  101. package/src/institutional-resolution.ts +8 -163
  102. package/src/judge-auditor.ts +10 -15
  103. package/src/judge-role.ts +8 -0
  104. package/src/navigator-attendance.ts +25 -9
  105. package/src/navigator-public-session.ts +217 -0
  106. package/src/navigator-session-contracts.ts +61 -54
  107. package/src/notary-source-run.ts +3 -1
  108. package/src/package-contracts/auditor-output.ts +82 -0
  109. package/src/package-contracts/evidence-child-output.ts +51 -0
  110. package/src/package-contracts/judge-output.ts +1 -0
  111. package/src/package-contracts/reviewer-output.ts +2 -2
  112. package/src/package-contracts/terminating-tools.ts +53 -3
  113. package/src/packaged-role-registry.ts +63 -0
  114. package/src/pi/adapter.ts +2 -1
  115. package/src/pi/in-process-session.ts +38 -12
  116. package/src/pi/role-turn-host.ts +42 -0
  117. package/src/public-cli/cli.ts +43 -6
  118. package/src/public-cli/countersign-run.ts +9 -129
  119. package/src/public-cli/diarist-run.ts +312 -0
  120. package/src/public-cli/instruction-seat-run.ts +289 -50
  121. package/src/public-cli/invocation.ts +87 -25
  122. package/src/public-cli/option-definitions.ts +79 -0
  123. package/src/public-cli/post-admission.ts +9 -3
  124. package/src/public-cli/registry.ts +7 -1
  125. package/src/public-cli/run-lifecycle.ts +53 -6
  126. package/src/public-cli/settlement.ts +170 -7
  127. package/src/public-cli/terminal.ts +4 -1
  128. package/src/public-role-summons.ts +431 -0
  129. package/src/receipt-delivery-policy.ts +10 -0
  130. package/src/reviewer-agent.ts +1 -1
  131. package/src/reviewer-child-executor.ts +114 -12
  132. package/src/reviewer-execution-ledger.ts +3 -2
  133. package/src/role-runtime.ts +275 -23
  134. package/src/session-assistant-usage.ts +119 -0
  135. package/src/session-opening-materials.ts +1 -1
  136. package/src/shape-unreadable-failure.ts +49 -0
  137. package/src/submission-errors.ts +3 -0
  138. package/src/ticket-provenance-contracts.ts +1 -0
  139. package/src/ticket-provenance.ts +0 -23
  140. package/dist/evidence-child-executor.js +0 -829
  141. package/src/diarist-llm-collector.ts +0 -324
  142. package/src/evidence-child-executor.ts +0 -1140
package/src/diarist.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  /**
2
- * 起居郎 pipeline step — ADR 0075 / #582.
3
- * Not a public seat: no soul, no locator, no gate attendance.
4
- * Runs as the court-pipeline station before countersign turn.
2
+ * 起居郎 mechanical halves — ADR 0075 `diarist-is-role` / `diarist-collector-is-own-turn`.
3
+ * Semantic collection is the diarist role's own LLM turn; this module keeps the
4
+ * mechanical safeguard band only: source enumeration into a frozen catalog, and
5
+ * the verbatim reverse-verify → idempotent sitian append → watermark commit.
6
+ * No lifecycle here (ADR 0018): the seat prepares, the role envelope commits.
5
7
  */
8
+ import { readFileSync } from "node:fs";
9
+
6
10
  import { extractReferencedAdrPaths } from "./adr-path-refs.ts";
7
11
  import {
8
12
  createGhApiRunner,
@@ -22,13 +26,9 @@ import {
22
26
  type DiaristIssueFace,
23
27
  type DiaristSourceBlock,
24
28
  } from "./diarist-mechanical.ts";
25
- import {
26
- createHermesDiaristCollector,
27
- type DiaristLlmCollectResult,
28
- } from "./diarist-llm-collector.ts";
29
+ import type { DiaristSelection } from "./diarist-contracts.ts";
29
30
  import { parseGitHubOriginRemote } from "./reviewer-pinned-git.ts";
30
31
  import {
31
- appendCollectorFailureDiagnostic,
32
32
  appendIssueSourceFailureDiagnostic,
33
33
  appendQuoteVerifyFailureDiagnostic,
34
34
  appendTicketProvenanceEntry,
@@ -40,8 +40,7 @@ import {
40
40
  ticketProvenanceEntryIdentity,
41
41
  writeTicketProvenanceHumanView,
42
42
  } from "./ticket-provenance.ts";
43
- import type { TicketProvenanceEntry } from "./ticket-provenance-contracts.ts";
44
- import { readSitianRecords, type RecordPointer } from "./sitian-facade.ts";
43
+ import { readSitianRecords } from "./sitian-facade.ts";
45
44
  import { execFileSync } from "node:child_process";
46
45
 
47
46
  export type { DiaristIssueFace } from "./diarist-mechanical.ts";
@@ -72,55 +71,15 @@ export class DiaristIssueSourceError extends Error {
72
71
  }
73
72
 
74
73
  /**
75
- * Issue-face fetch for the diarist station.
76
- * Production never soft-returns undefined (Reviewer Spec soft-fetch is not reused).
77
- * Test injectors may return undefined to simulate unavailability — station converts
78
- * that into a typed DiaristIssueSourceError + durable diagnostic.
74
+ * Issue-face fetch for the diarist seat. Unavailability is a typed
75
+ * DiaristIssueSourceError — there is no soft-undefined face.
79
76
  */
80
77
  export type DiaristIssueFaceFetcher = (input: {
81
78
  readonly owner: string;
82
79
  readonly repo: string;
83
80
  readonly ticketNumber: number;
84
81
  readonly signal?: AbortSignal;
85
- }) => Promise<DiaristIssueFace | undefined>;
86
-
87
- export type DiaristRunInput = {
88
- readonly ticketNumber: number;
89
- readonly cwd: string;
90
- /** Explicit package home (admitted run / tests); never process.env.HOME (#604). */
91
- readonly home?: string;
92
- /**
93
- * Frozen GitHub issue face (body + comments). Production loads via shared gh seam.
94
- * Soft-unavailable → omit (no fake face from attachments).
95
- */
96
- readonly issueFace?: DiaristIssueFace;
97
- /** Extra cwd roots whose cc project folders are scanned. */
98
- readonly sessionCwds?: readonly string[];
99
- readonly signal?: AbortSignal;
100
- /** Package root for hermes collector method material resolution. */
101
- readonly packageRoot?: string;
102
- };
103
-
104
- export type DiaristRunResult = {
105
- readonly ticketNumber: number;
106
- /** Safeguard-cleaned source count before incremental filter. */
107
- readonly candidateCount: number;
108
- /** Blocks not yet on the volume — sole set sent to the collector this court. */
109
- readonly freshCount: number;
110
- readonly appended: number;
111
- readonly rejectedQuotes: number;
112
- readonly pointers: readonly RecordPointer[];
113
- readonly entries: readonly TicketProvenanceEntry[];
114
- readonly humanViewFile: string;
115
- readonly volumeRecordFile: string;
116
- readonly collectorStatus:
117
- | "ok"
118
- | "skipped-no-fresh"
119
- | "failed"
120
- | "empty-selection";
121
- readonly collectorError?: string;
122
- readonly llmRawStdout?: string;
123
- };
82
+ }) => Promise<DiaristIssueFace>;
124
83
 
125
84
  /**
126
85
  * Production issue-face capability over shared gh execution seams.
@@ -216,10 +175,78 @@ export function resolveDiaristGithubOrigin(
216
175
  return parseGitHubOriginRemote(remoteUrl);
217
176
  }
218
177
 
178
+ /**
179
+ * Acquire the issue face for a bound ticket. Failures are typed and durable on
180
+ * the ticket-provenance volume, then propagated — never washed into empty face.
181
+ */
182
+ export async function loadDiaristIssueFace(input: {
183
+ readonly ticketNumber: number;
184
+ readonly projectRoot: string;
185
+ readonly home?: string;
186
+ readonly fetcher?: DiaristIssueFaceFetcher;
187
+ }): Promise<DiaristIssueFace> {
188
+ const persistAndThrow = (error: DiaristIssueSourceError): DiaristIssueSourceError => {
189
+ appendIssueSourceFailureDiagnostic({
190
+ ticketNumber: input.ticketNumber,
191
+ cwd: input.projectRoot,
192
+ ...(input.home === undefined ? {} : { home: input.home }),
193
+ cause: error.message,
194
+ reason: error.reason,
195
+ });
196
+ return error;
197
+ };
198
+
199
+ const origin = resolveDiaristGithubOrigin(input.projectRoot);
200
+ if (origin === undefined) {
201
+ throw persistAndThrow(
202
+ new DiaristIssueSourceError(
203
+ "origin-unresolved",
204
+ `bound ticket #${input.ticketNumber} issue face requires a resolvable github.com origin remote`,
205
+ ),
206
+ );
207
+ }
208
+
209
+ const fetcher = input.fetcher ?? createDiaristIssueFaceFetcher();
210
+ try {
211
+ return await fetcher({
212
+ owner: origin.owner,
213
+ repo: origin.repo,
214
+ ticketNumber: input.ticketNumber,
215
+ });
216
+ } catch (error) {
217
+ throw persistAndThrow(
218
+ error instanceof DiaristIssueSourceError
219
+ ? error
220
+ : new DiaristIssueSourceError(
221
+ "issue-unavailable",
222
+ `issue face fetch failed for ${origin.owner}/${origin.repo}#${input.ticketNumber}`,
223
+ { cause: error },
224
+ ),
225
+ );
226
+ }
227
+ }
228
+
229
+ /** One frozen candidate the diarist turn may select by index. */
230
+ export type DiaristSourceCandidate = DiaristSourceBlock & {
231
+ readonly candidateIndex: number;
232
+ };
233
+
234
+ /**
235
+ * Frozen per-ticket catalog handed to the diarist turn and re-read by the
236
+ * envelope at accept time. Carries its own volume coordinates so neither side
237
+ * re-derives them from ambient state.
238
+ */
239
+ export type DiaristSourceCatalog = {
240
+ readonly ticketNumber: number;
241
+ readonly cwd: string;
242
+ readonly home?: string;
243
+ readonly candidates: readonly DiaristSourceCandidate[];
244
+ };
245
+
219
246
  /**
220
247
  * Identities already processed for this ticket:
221
248
  * - volume record identities (selected / verify-fail residue)
222
- * - offered watermark (blocks shown to collector, selected or not)
249
+ * - offered watermark (blocks shown to the diarist, selected or not)
223
250
  */
224
251
  async function loadSeenEntryIdentities(
225
252
  ticketNumber: number,
@@ -257,7 +284,11 @@ function blockEntryIdentity(
257
284
  * Mechanical layer does not prose-filter for relevance (锚定宪法).
258
285
  * Attachments are never merged in as fake issue-body-comment.
259
286
  */
260
- async function loadSourceBlocks(input: DiaristRunInput): Promise<DiaristSourceBlock[]> {
287
+ function loadSourceBlocks(input: {
288
+ readonly cwd: string;
289
+ readonly issueFace?: DiaristIssueFace;
290
+ readonly sessionCwds?: readonly string[];
291
+ }): DiaristSourceBlock[] {
261
292
  const cwds = input.sessionCwds ?? [input.cwd];
262
293
  const blocks: DiaristSourceBlock[] = [...readCcSessionBlocks({ cwds })];
263
294
  if (input.issueFace !== undefined) {
@@ -279,162 +310,205 @@ async function loadSourceBlocks(input: DiaristRunInput): Promise<DiaristSourceBl
279
310
  return blocks;
280
311
  }
281
312
 
282
- function faceTextForAnchors(face: DiaristIssueFace | undefined): string | undefined {
283
- if (face === undefined) return undefined;
284
- const parts = [face.body, ...face.comments.map((c) => c.body)].filter(
285
- (t) => t.trim() !== "",
286
- );
287
- if (parts.length === 0) return undefined;
288
- return parts.join("\n");
289
- }
313
+ export type PrepareDiaristSourceCatalogInput = {
314
+ readonly ticketNumber: number;
315
+ readonly cwd: string;
316
+ /** Explicit package home (admitted run / tests); never process.env.HOME (#604). */
317
+ readonly home?: string;
318
+ /** Frozen GitHub issue face (body + comments) from the shared gh seam. */
319
+ readonly issueFace?: DiaristIssueFace;
320
+ /** Extra cwd roots whose cc project folders are scanned. */
321
+ readonly sessionCwds?: readonly string[];
322
+ };
290
323
 
291
324
  /**
292
- * Run one diarist pass for a ticket: mechanical candidates → LLM collect →
293
- * reverse-verify → idempotent sitian append → human view refresh.
294
- * Always establishes the per-ticket volume + md.
325
+ * Mechanical half A — source enumeration into a frozen catalog.
326
+ * Establishes the per-ticket volume + human view for every bound run (ADR 0075
327
+ * `ticket-provenance-file` 每票一份起居录), then offers only blocks whose entry
328
+ * identity is not already on the volume or the offered watermark (增量幂等).
295
329
  */
296
- export async function runDiarist(input: DiaristRunInput): Promise<DiaristRunResult> {
297
- const homeOpt = input.home === undefined ? {} : { home: input.home };
298
- // Per-ticket volume exists for every bound court, including empty/fail paths.
299
- const volumePaths = ensureTicketProvenanceVolume(input.ticketNumber, input.cwd, input.home);
300
-
301
- const anchorText = faceTextForAnchors(input.issueFace);
302
- const anchors: DiaristAnchorSet = buildDiaristAnchors({
330
+ export async function prepareDiaristSourceCatalog(
331
+ input: PrepareDiaristSourceCatalogInput,
332
+ ): Promise<DiaristSourceCatalog> {
333
+ ensureTicketProvenanceVolume(input.ticketNumber, input.cwd, input.home);
334
+ const volume = await readTicketProvenance(input.ticketNumber, input.cwd, input.home);
335
+ writeTicketProvenanceHumanView({
303
336
  ticketNumber: input.ticketNumber,
304
- ...(anchorText === undefined ? {} : { ticketBody: anchorText }),
337
+ cwd: input.cwd,
338
+ ...(input.home === undefined ? {} : { home: input.home }),
339
+ entries: volume.entries,
305
340
  });
306
- const rawBlocks = await loadSourceBlocks(input);
341
+
342
+ const rawBlocks = loadSourceBlocks(input);
307
343
  // Safeguard only (notify filter + dedupe) — never prose-based exclusion.
308
344
  const safeguarded = mechanicalSafeguardPipeline(rawBlocks);
309
- // Incremental: only blocks whose entry identity is not yet on the volume
310
- // are offered to the collector (ADR 0075 refresh-every-court = 增量幂等).
311
345
  const seen = await loadSeenEntryIdentities(input.ticketNumber, input.cwd, input.home);
312
346
  const fresh = safeguarded.filter(
313
347
  (block) => !seen.has(blockEntryIdentity(input.ticketNumber, block)),
314
348
  );
315
349
 
316
- let collectorStatus: DiaristRunResult["collectorStatus"];
317
- let collectorError: string | undefined;
318
- let llmRawStdout: string | undefined;
319
- let collect: DiaristLlmCollectResult | undefined;
320
-
321
- // Production composition always runs the hermes collector (ADR 0075).
322
- // No injectable skip / alternate collector on this seam.
323
- const collector = createHermesDiaristCollector({
350
+ return {
351
+ ticketNumber: input.ticketNumber,
324
352
  cwd: input.cwd,
325
- ...(input.packageRoot === undefined ? {} : { packageRoot: input.packageRoot }),
326
- });
353
+ ...(input.home === undefined ? {} : { home: input.home }),
354
+ candidates: fresh.map((block, candidateIndex) => ({ ...block, candidateIndex })),
355
+ };
356
+ }
327
357
 
328
- if (fresh.length === 0) {
329
- collectorStatus = "skipped-no-fresh";
330
- } else {
331
- try {
332
- collect = await collector({
333
- ticketNumber: input.ticketNumber,
334
- candidates: fresh,
335
- ...(input.signal === undefined ? {} : { signal: input.signal }),
336
- });
337
- llmRawStdout = collect.rawStdout;
338
- collectorStatus =
339
- collect.selections.length === 0 ? "empty-selection" : "ok";
340
- // Watermark advances only after durable volume writes below — never
341
- // before selected entries / quote diagnostics are committed.
342
- } catch (error) {
343
- collectorStatus = "failed";
344
- collectorError =
345
- error instanceof Error ? error.message : String(error);
346
- // Durable true-cause on the ticket volume (append-only history).
347
- appendCollectorFailureDiagnostic({
348
- ticketNumber: input.ticketNumber,
349
- cwd: input.cwd,
350
- ...homeOpt,
351
- collectorError,
352
- });
353
- }
358
+ export function serializeDiaristSourceCatalog(catalog: DiaristSourceCatalog): string {
359
+ return JSON.stringify(catalog);
360
+ }
361
+
362
+ /** Read a frozen catalog written by the seat. Unreadable/malformed fails loudly. */
363
+ export function loadDiaristSourceCatalog(path: string): DiaristSourceCatalog {
364
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
365
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
366
+ throw new Error(`diarist source catalog is not an object (${path})`);
367
+ }
368
+ const record = parsed as Record<string, unknown>;
369
+ if (typeof record.ticketNumber !== "number" || typeof record.cwd !== "string") {
370
+ throw new Error(`diarist source catalog is missing ticket coordinates (${path})`);
354
371
  }
372
+ if (!Array.isArray(record.candidates)) {
373
+ throw new Error(`diarist source catalog is missing candidates (${path})`);
374
+ }
375
+ return parsed as DiaristSourceCatalog;
376
+ }
377
+
378
+ /**
379
+ * 失败真因 — typed causes for submitted rows that never reached the volume.
380
+ * Names the cause only; the per-row detail stays on the volume diagnostic.
381
+ */
382
+ export type DiaristFailureCause =
383
+ /** Row pointed at no candidate in this turn's frozen catalog. */
384
+ | "unknown-candidate"
385
+ /** Row's quotes failed verbatim reverse-verify. */
386
+ | "quote-verify-rejected";
387
+
388
+ /** Honest machine facts about what this turn actually committed to the volume. */
389
+ export type DiaristCommitFacts = {
390
+ readonly ticketNumber: number;
391
+ /** Candidates offered to the diarist this turn. */
392
+ readonly offered: number;
393
+ /** New entries this turn actually put on the volume (entry-count delta). */
394
+ readonly appended: number;
395
+ /** Selections rejected by verbatim reverse-verify. */
396
+ readonly rejectedQuotes: number;
397
+ /** Offered identities newly written to the watermark this turn. */
398
+ readonly watermarked: number;
399
+ readonly volumeRecordFile: string;
400
+ readonly humanViewFile: string;
401
+ /**
402
+ * What this turn's collection amounted to. A turn whose every row was
403
+ * dropped is `nothing-appended`, never `empty-selection` — the diarist
404
+ * selecting nothing and the safeguard band rejecting everything are
405
+ * different events. Why rows were dropped is `failureCauses`.
406
+ */
407
+ readonly collectorStatus:
408
+ | "ok"
409
+ | "empty-selection"
410
+ | "nothing-appended"
411
+ | "skipped-no-fresh";
412
+ /**
413
+ * 失败真因: empty when every submitted row landed. Populated whenever rows
414
+ * were dropped — including a partial turn whose collectorStatus is `ok`.
415
+ */
416
+ readonly failureCauses: readonly DiaristFailureCause[];
417
+ };
418
+
419
+ /**
420
+ * Mechanical half B — commit the diarist turn's selections.
421
+ * Verbatim reverse-verify → idempotent sitian append → watermark → human view.
422
+ * Verify failure records a single typed diagnostic and drops that selection; it
423
+ * never bounces the receipt (第 0 条) and never enters the volume as an entry.
424
+ */
425
+ export async function commitDiaristSelections(input: {
426
+ readonly catalog: DiaristSourceCatalog;
427
+ readonly selections: readonly DiaristSelection[];
428
+ }): Promise<DiaristCommitFacts> {
429
+ const { ticketNumber, cwd } = input.catalog;
430
+ const homeOpt = input.catalog.home === undefined ? {} : { home: input.catalog.home };
431
+ const volumePaths = ensureTicketProvenanceVolume(ticketNumber, cwd, input.catalog.home);
355
432
 
356
- const pointers: RecordPointer[] = [];
357
- const accepted: TicketProvenanceEntry[] = [];
433
+ const anchors: DiaristAnchorSet = buildDiaristAnchors({ ticketNumber });
434
+ // Entry-count baseline: sitian entry identity already makes a repeat of an
435
+ // already-recorded block a no-op append, so the honest `appended` is what the
436
+ // volume gained — not how many rows survived verify.
437
+ const before = await readTicketProvenance(ticketNumber, cwd, input.catalog.home);
438
+ let acceptedRows = 0;
358
439
  let rejectedQuotes = 0;
440
+ let unknownCandidate = false;
359
441
 
360
- // Only a successful collect (ok / empty-selection already branched) with
361
- // selections present can enter the volume. empty-selection has collect with [].
362
- if (collect !== undefined && collectorStatus === "ok") {
363
- for (const selection of collect.selections) {
364
- // triage is human-face only — never a machine gate (collector contract).
365
- // Inclusion is solely: selection present + quote reverse-verify pass.
366
- const block = fresh[selection.candidateIndex];
367
- if (block === undefined) continue;
368
- const projected = blockToLlmEntry(block, {
369
- anchors,
370
- quotes: selection.quotes,
371
- ...(selection.note === undefined ? {} : { note: selection.note }),
372
- });
373
- if (!projected.ok) {
374
- rejectedQuotes += 1;
375
- // Single diagnostic expression — never a disguised diary entry.
376
- const ptr = appendQuoteVerifyFailureDiagnostic({
377
- ticketNumber: input.ticketNumber,
378
- cwd: input.cwd,
379
- ...homeOpt,
380
- cause: projected.cause,
381
- });
382
- pointers.push(ptr);
383
- continue;
384
- }
385
- const ptr = appendTicketProvenanceEntry({
386
- ticketNumber: input.ticketNumber,
387
- cwd: input.cwd,
442
+ for (const selection of input.selections) {
443
+ const block = input.catalog.candidates[selection.candidateIndex];
444
+ if (block === undefined) {
445
+ unknownCandidate = true;
446
+ continue;
447
+ }
448
+ const projected = blockToLlmEntry(block, {
449
+ anchors,
450
+ quotes: selection.quotes,
451
+ ...(selection.note === undefined ? {} : { note: selection.note }),
452
+ });
453
+ if (!projected.ok) {
454
+ rejectedQuotes += 1;
455
+ // Single diagnostic expression — never a disguised diary entry.
456
+ appendQuoteVerifyFailureDiagnostic({
457
+ ticketNumber,
458
+ cwd,
388
459
  ...homeOpt,
389
- entry: projected.entry,
390
- source: "diarist",
460
+ cause: projected.cause,
391
461
  });
392
- pointers.push(ptr);
393
- accepted.push(projected.entry);
462
+ continue;
394
463
  }
395
- }
396
-
397
- // Successful collector pass (incl. empty selection): mark all offered
398
- // identities only after the volume writes above, so a crash mid-commit
399
- // still retries the batch next court. Entry identity and quote-verify
400
- // diagnostic identity are stable — retry does not duplicate either.
401
- // Failure does not advance the watermark (retry honestly).
402
- if (
403
- collect !== undefined &&
404
- (collectorStatus === "ok" || collectorStatus === "empty-selection")
405
- ) {
406
- recordOfferedIdentities({
407
- ticketNumber: input.ticketNumber,
408
- cwd: input.cwd,
464
+ appendTicketProvenanceEntry({
465
+ ticketNumber,
466
+ cwd,
409
467
  ...homeOpt,
410
- identities: fresh.map((block) =>
411
- blockEntryIdentity(input.ticketNumber, block),
412
- ),
468
+ entry: projected.entry,
469
+ source: "diarist",
413
470
  });
471
+ acceptedRows += 1;
414
472
  }
415
473
 
416
- // Refresh human view from the full volume (includes prior court runs).
417
- // Always write — empty courts still get the md face next to the JSONL.
418
- const volume = await readTicketProvenance(input.ticketNumber, input.cwd, input.home);
474
+ // Watermark advances only after the durable volume writes above, so a crash
475
+ // mid-commit retries the batch on the next summons. Entry identity and
476
+ // quote-verify diagnostic identity are stable — retry duplicates neither.
477
+ const identities = input.catalog.candidates.map((block) =>
478
+ blockEntryIdentity(ticketNumber, block),
479
+ );
480
+ const alreadyWatermarked = readOfferedIdentities(ticketNumber, cwd, input.catalog.home);
481
+ const watermarked = identities.filter((identity) => !alreadyWatermarked.has(identity)).length;
482
+ recordOfferedIdentities({ ticketNumber, cwd, ...homeOpt, identities });
483
+
484
+ // Refresh the human view from the full volume (includes prior summons).
485
+ const volume = await readTicketProvenance(ticketNumber, cwd, input.catalog.home);
419
486
  const humanViewFile = writeTicketProvenanceHumanView({
420
- ticketNumber: input.ticketNumber,
421
- cwd: input.cwd,
487
+ ticketNumber,
488
+ cwd,
422
489
  ...homeOpt,
423
490
  entries: volume.entries,
424
491
  });
425
492
 
426
493
  return {
427
- ticketNumber: input.ticketNumber,
428
- candidateCount: safeguarded.length,
429
- freshCount: fresh.length,
430
- appended: accepted.length,
494
+ ticketNumber,
495
+ offered: input.catalog.candidates.length,
496
+ appended: volume.entries.length - before.entries.length,
431
497
  rejectedQuotes,
432
- pointers,
433
- entries: volume.entries,
434
- humanViewFile,
498
+ watermarked,
435
499
  volumeRecordFile: volumePaths.recordFile,
436
- collectorStatus,
437
- ...(collectorError === undefined ? {} : { collectorError }),
438
- ...(llmRawStdout === undefined ? {} : { llmRawStdout }),
500
+ humanViewFile,
501
+ collectorStatus:
502
+ input.catalog.candidates.length === 0
503
+ ? "skipped-no-fresh"
504
+ : acceptedRows > 0
505
+ ? "ok"
506
+ : input.selections.length === 0
507
+ ? "empty-selection"
508
+ : "nothing-appended",
509
+ failureCauses: [
510
+ ...(unknownCandidate ? (["unknown-candidate"] as const) : []),
511
+ ...(rejectedQuotes > 0 ? (["quote-verify-rejected"] as const) : []),
512
+ ],
439
513
  };
440
514
  }
@@ -1,8 +1,7 @@
1
1
  import { auditorRunDirectory } from "./auditor-dossier-tool.ts";
2
- import { loadAuditorSoul } from "./auditor-soul.ts";
3
2
  import {
4
- createComplianceDecisionTool,
5
3
  runComplianceAudit,
4
+ type AuditorSummon,
6
5
  type ComplianceDecision,
7
6
  } from "./compliance-transport.ts";
8
7
  import {
@@ -17,16 +16,13 @@ export const DOCTOR_AUDIT_TOOL_NAME = "ak_doctor_audit_decision";
17
16
  export type DoctorAuditOptions = {
18
17
  context: HostContext;
19
18
  signal?: AbortSignal;
19
+ /** Same seam as runComplianceAudit options — offline tracers only. */
20
+ summonAuditor?: AuditorSummon;
20
21
  };
21
22
 
22
- const tool = createComplianceDecisionTool(
23
- DOCTOR_AUDIT_TOOL_NAME,
24
- "提交 typed pass/revise/escalate 决议(太医署审刑)。",
25
- );
26
-
27
23
  /**
28
- * Doctor auditor: zero hand-delivered materials.
29
- * Candidate testimony must already be on the parent-session books.
24
+ * Doctor compliance via public auditor activation (#675 / ADR 0062 / owner r11).
25
+ * 审刑院 is an independent role; subject=doctor selects souls/doctor-auditor.md.
30
26
  */
31
27
  export function createPiDoctorAuditor(): (options: DoctorAuditOptions) => Promise<ComplianceDecision> {
32
28
  return async (options) => {
@@ -36,13 +32,13 @@ export function createPiDoctorAuditor(): (options: DoctorAuditOptions) => Promis
36
32
  requireAuditMaterials(subjects);
37
33
 
38
34
  return runComplianceAudit({
39
- tool,
40
- systemPrompt: await loadAuditorSoul("doctor"),
41
- roleLabel: "Doctor Soul compliance audit",
42
- invalidDecisionLabel: "invalid Doctor audit decision",
35
+ subject: "doctor",
43
36
  context: options.context,
44
- ...(auditorRunDirectory(options.context) === undefined ? {} : { runDirectory: auditorRunDirectory(options.context) }),
37
+ ...(auditorRunDirectory(options.context) === undefined
38
+ ? {}
39
+ : { runDirectory: auditorRunDirectory(options.context) }),
45
40
  ...(options.signal === undefined ? {} : { signal: options.signal }),
41
+ ...(options.summonAuditor === undefined ? {} : { summonAuditor: options.summonAuditor }),
46
42
  });
47
43
  };
48
44
  }
@@ -7,6 +7,7 @@ export const DOCTOR_EVIDENCE_TOOL_NAME = "ak_doctor_evidence";
7
7
  export const DOCTOR_OUTPUT_TOOL_NAME = "ak_doctor_output";
8
8
  export const DOCTOR_ACCEPTED_TEXT = "太医署回执已接受";
9
9
  export const DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT = "太医署回执已接受;审计无回执";
10
+ export const DOCTOR_ACCEPTED_AUDIT_UNREADABLE_TEXT = "太医署回执已接受;审计形状不可读";
10
11
  export const DOCTOR_OUTPUT_TOOL_DESCRIPTION = "提交唯一终局单案证词;completed 允许空 findings;runtime 补记派生成本入回执。";
11
12
  export const DOCTOR_TARGET_KINDS = ["law", "gate", "template", "station", "seat"] as const;
12
13
  export type DoctorTargetKind = typeof DOCTOR_TARGET_KINDS[number];
@@ -2,7 +2,7 @@ import type { RoleHost, HostContext, HostToolResult } from "./host-contracts.ts"
2
2
  import { disposeComplianceDecision } from "./audit-escalation.ts";
3
3
  import { ComplianceResponseRetentionError, type ComplianceDecision } from "./compliance-transport.ts";
4
4
  import { DOCTOR_CANDIDATE_ENTRY_TYPE } from "./dossier-resolution.ts";
5
- import { DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT, DOCTOR_ACCEPTED_TEXT, DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
5
+ import { DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT, DOCTOR_ACCEPTED_AUDIT_UNREADABLE_TEXT, DOCTOR_ACCEPTED_TEXT, DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_DESCRIPTION, DOCTOR_OUTPUT_TOOL_NAME, DoctorEvidenceStore, doctorEvidenceReadSchema, doctorSubmissionSchema, validateDoctorOutput, type DoctorCase } from "./doctor-contracts.ts";
6
6
 
7
7
  import { sitianReport } from "./sitian-facade.ts";
8
8
 
@@ -35,7 +35,7 @@ export function createDoctorRoleRuntime(pi: RoleHost, dependencies: DoctorRoleDe
35
35
  let activation: { soul: string; patient: DoctorCase; store: DoctorEvidenceStore } | undefined; let registered = false; pi.registerFlag(DOCTOR_CASE_FLAG.name, DOCTOR_CASE_FLAG.definition);
36
36
  return { async activate() { const path = pi.getFlag(DOCTOR_CASE_FLAG.name); if (typeof path !== "string" || !path.trim()) throw new Error("Doctor requires --ak-doctor-case"); const soul = (await dependencies.loadSoul()).trim(); if (!soul) throw new Error("Doctor soul is empty"); const patient = await dependencies.loadCase(path); activation = { soul, patient, store: new DoctorEvidenceStore(patient) };
37
37
  if (!registered) { registered = true; pi.registerTool({ name: DOCTOR_EVIDENCE_TOOL_NAME, label: "太医署证据", description: "分页读取留存的 Pi session 字节。", parameters: doctorEvidenceReadSchema, async execute(_id: string, params: { evidenceId: string; offset?: number; limit?: number }) { if (!activation) throw new Error("太医署未激活"); const details = activation.store.read(params.evidenceId, params.offset, params.limit); return { content: [{ type: "text" as const, text: JSON.stringify(details) }], details }; } });
38
- pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "太医署输出", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id: string, params: unknown, signal: AbortSignal | undefined, _update: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> { if (!activation) throw new Error("太医署未激活"); const testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendCandidate(ctx, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx } : { context: ctx, signal }); } catch (error) { host.failInfrastructure(error, ctx, id); } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; const acceptedDetails = details; return disposeComplianceDecision<HostToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true as const, ...usageProjection }), revise: (violations) => { throw new Error(`太医署回执违 soul:${violations.join("; ")}`); }, escalate: (result) => result }, acceptedDetails); } });
38
+ pi.registerTool({ name: DOCTOR_OUTPUT_TOOL_NAME, label: "太医署输出", description: DOCTOR_OUTPUT_TOOL_DESCRIPTION, parameters: doctorSubmissionSchema, async execute(id: string, params: unknown, signal: AbortSignal | undefined, _update: unknown, ctx: HostContext): Promise<HostToolResult<unknown>> { if (!activation) throw new Error("太医署未激活"); const testimony = validateDoctorOutput(params, activation.patient, activation.store); try { appendCandidate(ctx, { version: 1, testimony, readRecord: activation.store.readRecord(), patientIdentity: activation.patient.identity }); } catch (error) { host.failInfrastructure(error, ctx, id); } let audit: ComplianceDecision; try { audit = await dependencies.auditCompliance(signal === undefined ? { context: ctx } : { context: ctx, signal }); } catch (error) { host.failInfrastructure(error, ctx, id); } const details = testimony.status === "completed" ? { ...testimony, cost: activation.patient.cost } : testimony; const acceptedDetails = details; return disposeComplianceDecision<HostToolResult<unknown>>(audit, { pass: (usage) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_TEXT }], details: acceptedDetails, terminate: true as const, ...(usage === undefined ? {} : { usage }) }), noReceipt: (auditNoReceipt, usageProjection) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_AUDIT_NO_RECEIPT_TEXT }], details: { ...acceptedDetails, auditNoReceipt }, terminate: true as const, ...usageProjection }), unreadable: (auditUnreadable, usageProjection) => ({ content: [{ type: "text" as const, text: DOCTOR_ACCEPTED_AUDIT_UNREADABLE_TEXT }], details: { ...acceptedDetails, auditUnreadable }, terminate: true as const, ...usageProjection }), revise: (violations) => { throw new Error(`太医署回执违 soul:${violations.join("; ")}`); }, escalate: (result) => result }, acceptedDetails); } });
39
39
  pi.on("before_agent_start", (event) => { if (!activation) throw new Error("太医署未激活"); const catalog = { version: activation.patient.version, identity: activation.patient.identity, admittedMetrics: { provenance: "由留存 session 字节推导,封入受理回执。", cost: activation.patient.cost }, lawfulTargetKeys: ["case", ...activation.patient.cost.invocations.sources], evidence: activation.patient.evidence.map(({ id, kind, sha256, byteLength, contentLength }) => ({ id, kind, sha256, byteLength, contentLength })) }; return { systemPrompt: `${event.systemPrompt}\n\n<doctor_soul>\n${activation.soul}\n</doctor_soul>\n\n<doctor_case>\n${JSON.stringify(catalog)}\n</doctor_case>` }; }); }
40
40
  const required = [DOCTOR_EVIDENCE_TOOL_NAME, DOCTOR_OUTPUT_TOOL_NAME]; const names = pi.getAllTools().map((tool) => tool.name); for (const name of required) if (names.filter((item) => item === name).length !== 1) throw new Error(`Doctor required tool collision or missing: ${name}`); pi.setActiveTools(required); const active = pi.getActiveTools?.() ?? required; if (active.length !== 2 || !required.every((name) => active.includes(name))) throw new Error("Doctor active tool narrowing failed"); } };
41
41
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Public Evidence-Child role — filed-officer envelope (#675).
3
+ */
4
+ import {
5
+ EVIDENCE_CHILD_ACCEPTED_TEXT,
6
+ EVIDENCE_CHILD_OUTPUT_TOOL_NAME,
7
+ evidenceChildOutputSchema,
8
+ } from "./package-contracts/evidence-child-output.ts";
9
+
10
+ export { EVIDENCE_CHILD_ACCEPTED_TEXT, EVIDENCE_CHILD_OUTPUT_TOOL_NAME };
11
+
12
+ export type EvidenceChildRuntimeDependencies = {
13
+ loadSoul(): Promise<string>;
14
+ };
15
+
16
+ export const EVIDENCE_CHILD_TOOL_SPEC = {
17
+ name: EVIDENCE_CHILD_OUTPUT_TOOL_NAME,
18
+ label: "取证输出",
19
+ description: "提交取证报告。",
20
+ promptSnippet: "提交取证报告",
21
+ parameters: evidenceChildOutputSchema,
22
+ } as const;