@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,388 @@
1
+ import { resolveActivationLedgerHome, tryHomeFromAkRolesPath, } from "./activation-ledger-topology.js";
2
+ import { isAuditEscalationProjection, projectAuditEscalation } from "./audit-escalation.js";
3
+ import { GatekeeperEscalationError } from "./gatekeeper-role.js";
4
+ import { acceptedFacts, isTerminatingToolName, } from "./package-contracts/terminating-tools.js";
5
+ import { runIdFromRunDirectory } from "./run-terminal-artifacts.js";
6
+ import { readSitianRecords, resolveSitianRecordPathInLedger, sitianReport } from "./sitian-facade.js";
7
+ import { isCorrectableExecuteError } from "./submission-correctable-error.js";
8
+ import { failOnInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.js";
9
+ /**
10
+ * Admitted run identity for the ledger subject.
11
+ * Prefer AK_ROLE_RUN_DIR via sole runDirectory→runId parser (public-CLI correlation);
12
+ * otherwise session header id. Never a shared "unbound" bucket.
13
+ * Session-path layout fallback omitted: bare Pi and public CLI both expose header id
14
+ * when a session exists; missing both is infrastructure, not a third guess.
15
+ */
16
+ function runIdentity(context) {
17
+ const directory = process.env.AK_ROLE_RUN_DIR;
18
+ if (typeof directory === "string" && directory.length > 0) {
19
+ const fromDir = runIdFromRunDirectory(directory);
20
+ if (fromDir !== undefined)
21
+ return fromDir;
22
+ }
23
+ const headerId = context.sessionManager.getHeader?.()?.id;
24
+ if (typeof headerId === "string" && headerId.length > 0)
25
+ return headerId;
26
+ throw new Error("提交账需要已受理的 run 身份");
27
+ }
28
+ /**
29
+ * Court-turn attempt identity (#637 same-ticket re-summons).
30
+ * AK_ROLE_COURT_ATTEMPT is injected only for a new court turn on an already-retained
31
+ * run (forceContinuation / summons). Manual resume and first mint omit it so the
32
+ * session-stable id keeps sole-final + sealed-idempotent semantics.
33
+ */
34
+ export const COURT_ATTEMPT_ENV = "AK_ROLE_COURT_ATTEMPT";
35
+ function attemptIdentity(context, runId) {
36
+ const courtAttempt = process.env[COURT_ATTEMPT_ENV];
37
+ if (typeof courtAttempt === "string" && courtAttempt.length > 0)
38
+ return courtAttempt;
39
+ return context.sessionManager.getHeader?.()?.id ?? context.sessionManager.getLeafId?.() ?? `${runId}:initial`;
40
+ }
41
+ function isAcceptedProjection(value) {
42
+ if (typeof value !== "object" || value === null)
43
+ return false;
44
+ const candidate = value;
45
+ return candidate.kind === "accepted" && typeof candidate.role === "string" && typeof candidate.status === "string" && typeof candidate.decisiveFacts === "object" && candidate.decisiveFacts !== null;
46
+ }
47
+ function isAuditEscalationTerminalProjection(value) {
48
+ if (typeof value !== "object" || value === null)
49
+ return false;
50
+ const candidate = value;
51
+ return (candidate.kind === "audit_escalation" &&
52
+ typeof candidate.role === "string" &&
53
+ candidate.status === "audit_escalation" &&
54
+ typeof candidate.decisiveFacts === "object" &&
55
+ candidate.decisiveFacts !== null);
56
+ }
57
+ function submissionRecordFile(cwd, runId, home) {
58
+ const ledgerHome = resolveActivationLedgerHome(home);
59
+ return resolveSitianRecordPathInLedger({
60
+ level: "event",
61
+ kind: "candidate",
62
+ subject: { runId },
63
+ cwd,
64
+ }, ledgerHome).recordFile;
65
+ }
66
+ async function readOwnedSubmissionRecords(cwd, runId, home) {
67
+ const file = submissionRecordFile(cwd, runId, home);
68
+ const { records } = await readSitianRecords(file);
69
+ return {
70
+ file,
71
+ owned: records.filter((record) => typeof record.subject === "object" && record.subject !== null && record.subject.runId === runId),
72
+ };
73
+ }
74
+ function resolveReadScope(homeOrScope) {
75
+ if (homeOrScope === undefined)
76
+ return {};
77
+ if (typeof homeOrScope === "string")
78
+ return { home: homeOrScope };
79
+ return homeOrScope;
80
+ }
81
+ function recordAttemptId(record) {
82
+ if (typeof record.subject === "object" && record.subject !== null) {
83
+ const fromSubject = record.subject.attemptId;
84
+ if (typeof fromSubject === "string" && fromSubject.length > 0)
85
+ return fromSubject;
86
+ }
87
+ if (typeof record.payload === "object" && record.payload !== null) {
88
+ const fromPayload = record.payload.attemptId;
89
+ if (typeof fromPayload === "string" && fromPayload.length > 0)
90
+ return fromPayload;
91
+ }
92
+ return undefined;
93
+ }
94
+ function recordsForAttempt(owned, attemptId) {
95
+ if (attemptId === undefined)
96
+ return owned;
97
+ return owned.filter((record) => recordAttemptId(record) === attemptId);
98
+ }
99
+ /** Settlement read seam: typed sealed projection only, never host session JSONL.
100
+ * Omit attemptId for run-scoped latest sealed (manual resume idempotent).
101
+ * Pass attemptId for the current court turn so a prior seal cannot wash this turn. */
102
+ export async function readSealedSubmission(cwd, runId, homeOrScope) {
103
+ const scope = resolveReadScope(homeOrScope);
104
+ const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope.home);
105
+ const scoped = recordsForAttempt(owned, scope.attemptId);
106
+ for (let index = scoped.length - 1; index >= 0; index -= 1) {
107
+ const record = scoped[index];
108
+ if (record?.kind === "post-seal-anomaly")
109
+ return undefined;
110
+ if (record?.kind !== "sealed")
111
+ continue;
112
+ const payload = record.payload;
113
+ if (payload?.type === "sealed" && isAcceptedProjection(payload.projection))
114
+ return payload.projection;
115
+ }
116
+ return undefined;
117
+ }
118
+ /** Non-final audit-escalation projection written by the submission ledger. */
119
+ export async function readAuditEscalationSubmission(cwd, runId, homeOrScope) {
120
+ const scope = resolveReadScope(homeOrScope);
121
+ const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope.home);
122
+ const scoped = recordsForAttempt(owned, scope.attemptId);
123
+ for (let index = scoped.length - 1; index >= 0; index -= 1) {
124
+ const record = scoped[index];
125
+ if (record?.kind !== "outcome")
126
+ continue;
127
+ const payload = record.payload;
128
+ if (payload?.type !== "outcome" || payload.outcome !== "audit-escalation")
129
+ continue;
130
+ if (isAuditEscalationTerminalProjection(payload.projection))
131
+ return payload.projection;
132
+ }
133
+ return undefined;
134
+ }
135
+ /** Latest non-final outcome on the run ledger (settlement residual precedence). */
136
+ export async function readLatestSubmissionOutcome(cwd, runId, homeOrScope) {
137
+ const scope = resolveReadScope(homeOrScope);
138
+ const { owned } = await readOwnedSubmissionRecords(cwd, runId, scope.home);
139
+ const scoped = recordsForAttempt(owned, scope.attemptId);
140
+ for (let index = scoped.length - 1; index >= 0; index -= 1) {
141
+ const record = scoped[index];
142
+ if (record?.kind !== "outcome")
143
+ continue;
144
+ const payload = record.payload;
145
+ if (payload?.type === "outcome" && typeof payload.outcome === "string") {
146
+ return payload;
147
+ }
148
+ }
149
+ return undefined;
150
+ }
151
+ function sealedAttemptIdFromRecord(record) {
152
+ if (record.kind !== "sealed")
153
+ return undefined;
154
+ const payload = record.payload;
155
+ return payload?.type === "sealed" && typeof payload.attemptId === "string" && payload.attemptId.length > 0
156
+ ? payload.attemptId
157
+ : undefined;
158
+ }
159
+ async function restoreState(cwd, runId, home) {
160
+ const { file, owned } = await readOwnedSubmissionRecords(cwd, runId, home);
161
+ const last = owned.at(-1);
162
+ const sealedAttemptIds = new Set();
163
+ for (const record of owned) {
164
+ const attemptId = sealedAttemptIdFromRecord(record);
165
+ if (attemptId !== undefined)
166
+ sealedAttemptIds.add(attemptId);
167
+ }
168
+ return {
169
+ ...(last === undefined ? {} : { prior: { identity: last.identity, recordFile: file, kind: last.kind, level: last.level } }),
170
+ sealedAttemptIds,
171
+ sequence: owned.reduce((maximum, record) => {
172
+ const payload = record.payload;
173
+ return payload?.type === "candidate" && typeof payload.sequence === "number" ? Math.max(maximum, payload.sequence) : maximum;
174
+ }, 0),
175
+ };
176
+ }
177
+ /** Pipeline-owned sole-final state and durable projection. */
178
+ export function createSubmissionLedgerHost(host, outputTools, failInfrastructure = (error) => { throw error; }, projectClosure = () => undefined, options) {
179
+ const states = new Map();
180
+ const rounds = new Map();
181
+ const resolveHomeFromContext = (context) => {
182
+ if (options?.home !== undefined)
183
+ return options.home;
184
+ const sessionFile = context.sessionManager.getSessionFile?.() || context.sessionManager.getSessionDir?.();
185
+ return typeof sessionFile === "string" && sessionFile.length > 0
186
+ ? tryHomeFromAkRolesPath(sessionFile)
187
+ : undefined;
188
+ };
189
+ const stateFor = (context, runId) => states.get(runId) ?? (() => {
190
+ const home = resolveHomeFromContext(context);
191
+ const pending = restoreState(context.cwd, runId, home);
192
+ states.set(runId, pending);
193
+ return pending;
194
+ })();
195
+ const appendFor = (state, context, runId, attemptId, event) => {
196
+ // Home only — must match submissionRecordFile topology (cwd + runId hash under
197
+ // ledger home). Do not pass sessionParent: that nests under the session dir and
198
+ // breaks restoreState reads which resolve the hash path without sessionParent.
199
+ const home = resolveHomeFromContext(context);
200
+ const pointer = sitianReport({
201
+ level: "event",
202
+ kind: event.type,
203
+ subject: { runId, attemptId },
204
+ ...(state.prior === undefined ? {} : { priorEventId: state.prior.identity }),
205
+ payload: event,
206
+ source: "role-runtime",
207
+ cwd: context.cwd,
208
+ ...(home !== undefined ? { home } : {}),
209
+ });
210
+ state.prior = pointer;
211
+ return pointer;
212
+ };
213
+ host.on("tool_execution_start", async ({ toolCallId, toolName }, context) => {
214
+ try {
215
+ const runId = runIdentity(context);
216
+ const attemptId = attemptIdentity(context, runId);
217
+ const state = await stateFor(context, runId);
218
+ if (state.sealedAttemptIds.has(attemptId)) {
219
+ appendFor(state, context, runId, attemptId, { type: "post-seal-anomaly", attemptId, toolCallId, toolName });
220
+ }
221
+ }
222
+ catch (error) {
223
+ failInfrastructure(error, context);
224
+ }
225
+ });
226
+ host.on("turn_end", async (event, context) => {
227
+ try {
228
+ const runId = runIdentity(context);
229
+ const attemptId = attemptIdentity(context, runId);
230
+ const candidates = rounds.get(attemptId);
231
+ if (candidates === undefined)
232
+ return;
233
+ rounds.delete(attemptId);
234
+ const state = await stateFor(context, runId);
235
+ const calls = event.calls.map(({ toolCallId: id, toolName: name }) => ({ id, name }));
236
+ appendFor(state, context, runId, attemptId, { type: "roundContext", attemptId, calls });
237
+ const sole = calls.length === 1 && candidates.length === 1 && calls[0]?.id === candidates[0]?.toolCallId;
238
+ if (!sole) {
239
+ for (const candidate of candidates)
240
+ appendFor(state, context, runId, attemptId, { type: "outcome", attemptId, toolCallId: candidate.toolCallId, outcome: "correctable-rejection", code: "non-sole-round" });
241
+ if (host.deliverSubmissionRejection === undefined) {
242
+ throw new Error("宿主未提供模型可见的交卷封驳接缝");
243
+ }
244
+ context.abort();
245
+ await host.deliverSubmissionRejection({
246
+ kind: "correctable-rejection",
247
+ code: "non-sole-round",
248
+ toolCallIds: candidates.map(({ toolCallId }) => toolCallId),
249
+ });
250
+ return;
251
+ }
252
+ const candidate = candidates[0];
253
+ if (candidate.auditProjection !== undefined) {
254
+ appendFor(state, context, runId, attemptId, { type: "outcome", attemptId, toolCallId: candidate.toolCallId, outcome: "audit-escalation", projection: candidate.auditProjection });
255
+ await projectClosure(candidate.auditProjection, context);
256
+ context.abort();
257
+ return;
258
+ }
259
+ const details = typeof candidate.result.details === "object" && candidate.result.details !== null ? candidate.result.details : {};
260
+ const status = acceptedFacts(candidate.toolName, details).status;
261
+ if (typeof status !== "string" || status.length === 0)
262
+ throw new Error("提交账封账缺少 acceptedFacts.status");
263
+ const projection = { kind: "accepted", role: candidate.role, status, decisiveFacts: details };
264
+ appendFor(state, candidate.context, runId, attemptId, { type: "sealed", attemptId, toolCallId: candidate.toolCallId, accepted: candidate.result.details, projection });
265
+ state.sealedAttemptIds.add(attemptId);
266
+ await projectClosure(projection, context);
267
+ context.abort();
268
+ }
269
+ catch (error) {
270
+ failInfrastructure(error, context);
271
+ }
272
+ });
273
+ return {
274
+ ...host,
275
+ registerTool(tool) {
276
+ const role = outputTools.get(tool.name);
277
+ if (role === undefined)
278
+ return host.registerTool(tool);
279
+ host.registerTool({
280
+ ...tool,
281
+ async execute(toolCallId, params, signal, update, context) {
282
+ const runId = runIdentity(context);
283
+ const attemptId = attemptIdentity(context, runId);
284
+ const state = await stateFor(context, runId);
285
+ const append = (event) => appendFor(state, context, runId, attemptId, event);
286
+ if (state.sealedAttemptIds.has(attemptId))
287
+ throw new Error("提交账已封账");
288
+ // #541 / #575: shared infra-declaration fail lives on the ledger seam,
289
+ // before any role execute or sole-final work (one owner for every seat).
290
+ // #641 chain②: seats may opt into bouncing a machine-verified normal
291
+ // completion misdeclared as infrastructure failure — the bounce throws a
292
+ // correctable error from the declaration call, so it lands in the same
293
+ // correctable-rejection path as execute-thrown rejections below. The
294
+ // candidate is booked first so the bounced attempt stays observable.
295
+ append({ type: "candidate", attemptId, toolCallId, toolName: tool.name, sequence: ++state.sequence });
296
+ const projectOfficerEscalation = (officer, original, deliveredOutput = original) => projectAuditEscalation({
297
+ ...original,
298
+ status: "escalate",
299
+ officer,
300
+ reason: original.reason === undefined
301
+ ? `门下省${officer}上呈(原卷未附 reason)`
302
+ : original.reason,
303
+ }, deliveredOutput);
304
+ let result;
305
+ try {
306
+ failOnInfrastructureFailureDeclaration(params, {
307
+ failInfrastructure(error, ctx) {
308
+ failInfrastructure(error, ctx);
309
+ },
310
+ }, context, toolCallId, tool.bounceInfrastructureDeclaration);
311
+ result = await tool.execute(toolCallId, params, signal, update, context);
312
+ }
313
+ catch (error) {
314
+ if (error instanceof GatekeeperEscalationError) {
315
+ const decision = error.gatekeeper;
316
+ result = projectOfficerEscalation(error.gatekeeper.officer, decision, params);
317
+ }
318
+ else if (isCorrectableExecuteError(error)) {
319
+ append({
320
+ type: "outcome",
321
+ attemptId,
322
+ toolCallId,
323
+ outcome: "correctable-rejection",
324
+ code: "typed-bounce",
325
+ diagnostic: error instanceof Error ? error.message : String(error),
326
+ });
327
+ throw error;
328
+ }
329
+ else {
330
+ append({
331
+ type: "outcome",
332
+ attemptId,
333
+ toolCallId,
334
+ outcome: "infrastructure",
335
+ diagnostic: error instanceof Error ? error.message : String(error),
336
+ });
337
+ throw error;
338
+ }
339
+ }
340
+ if ((role === "inspector" || role === "notary") &&
341
+ typeof result.details === "object" &&
342
+ result.details !== null &&
343
+ !Array.isArray(result.details) &&
344
+ result.details.status === "escalate") {
345
+ const details = result.details;
346
+ result = projectOfficerEscalation(role, details);
347
+ }
348
+ if (isAuditEscalationProjection(result.details)) {
349
+ const candidates = rounds.get(attemptId) ?? [];
350
+ candidates.push({
351
+ toolCallId,
352
+ toolName: tool.name,
353
+ role,
354
+ result,
355
+ context,
356
+ auditProjection: {
357
+ kind: "audit_escalation",
358
+ role,
359
+ status: "audit_escalation",
360
+ decisiveFacts: result.details,
361
+ },
362
+ });
363
+ rounds.set(attemptId, candidates);
364
+ return {
365
+ content: [],
366
+ details: { submissionDisposition: "pending-round-closure" },
367
+ };
368
+ }
369
+ if (result.terminate !== true) {
370
+ append({ type: "outcome", attemptId, toolCallId, outcome: "correctable-rejection", code: "non-terminate" });
371
+ return result;
372
+ }
373
+ if (!isTerminatingToolName(tool.name)) {
374
+ append({ type: "outcome", attemptId, toolCallId, outcome: "infrastructure", diagnostic: `non-terminating tool ${tool.name}` });
375
+ throw new Error("提交账只受理终止工具");
376
+ }
377
+ const candidates = rounds.get(attemptId) ?? [];
378
+ candidates.push({ toolCallId, toolName: tool.name, role, result, context });
379
+ rounds.set(attemptId, candidates);
380
+ return {
381
+ content: [],
382
+ details: { submissionDisposition: "pending-round-closure" },
383
+ };
384
+ },
385
+ });
386
+ },
387
+ };
388
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * 起居录(ticket-provenance)typed contracts — ADR 0075 / #582.
3
+ * JSONL is the sole authority; md is a derived human face.
4
+ */
5
+ /** Sitian kind for per-ticket court diary volumes. */
6
+ export const TICKET_PROVENANCE_KIND = "ticket-provenance";
7
+ /** Human-read view filename co-located with the JSONL volume. */
8
+ export const TICKET_PROVENANCE_HUMAN_VIEW = "起居录.md";
9
+ /**
10
+ * Incremental watermark: identities already offered to the collector this ticket
11
+ * (selected or not). Process state next to the volume — not a diary dual-source.
12
+ */
13
+ export const TICKET_PROVENANCE_OFFERED_WATERMARK = "offered-identities.jsonl";
14
+ /** Payload discriminator: diagnostic residue (not a diary body entry). */
15
+ export const TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC = "diagnostic";
16
+ function isRecord(value) {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+ const SOURCE_KINDS = new Set([
20
+ "cc-session",
21
+ "issue-body-comment",
22
+ "adr-decision-key",
23
+ "ticket-decree-block",
24
+ ]);
25
+ const BASIS_METHODS = new Set(["llm-semantic"]);
26
+ const DIAGNOSTIC_KINDS = new Set([
27
+ "collector-failed",
28
+ "issue-source-failed",
29
+ "quote-verify-failed",
30
+ ]);
31
+ /**
32
+ * Project a typed diagnostic payload (recordClass discriminator only).
33
+ * Disguised diary-entry shapes are not diagnostics — no branch-intermediate compat.
34
+ */
35
+ export function projectTicketProvenanceDiagnostic(value) {
36
+ if (!isRecord(value))
37
+ return undefined;
38
+ if (value.recordClass !== TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC) {
39
+ return undefined;
40
+ }
41
+ if (typeof value.diagnosticKind !== "string" ||
42
+ !DIAGNOSTIC_KINDS.has(value.diagnosticKind)) {
43
+ return undefined;
44
+ }
45
+ if (typeof value.cause !== "string" || value.cause.length === 0) {
46
+ return undefined;
47
+ }
48
+ if (typeof value.recordedAt !== "string" || value.recordedAt.length === 0) {
49
+ return undefined;
50
+ }
51
+ return {
52
+ recordClass: TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC,
53
+ diagnosticKind: value.diagnosticKind,
54
+ cause: value.cause,
55
+ recordedAt: value.recordedAt,
56
+ ...(typeof value.reason === "string" ? { reason: value.reason } : {}),
57
+ };
58
+ }
59
+ /**
60
+ * Project a lawful diary entry from unknown payload bytes.
61
+ * Shape is not an admission gate for role output; this is the diarist/write seam
62
+ * self-check so garbage does not enter the volume.
63
+ * Diagnostics (recordClass discriminator) are not entries.
64
+ */
65
+ export function projectTicketProvenanceEntry(value) {
66
+ if (!isRecord(value))
67
+ return undefined;
68
+ // Diagnostics are a separate projection — never body entries.
69
+ if (value.recordClass === TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC) {
70
+ return undefined;
71
+ }
72
+ if (!isRecord(value.basis))
73
+ return undefined;
74
+ if (typeof value.basis.method !== "string" || !BASIS_METHODS.has(value.basis.method)) {
75
+ return undefined;
76
+ }
77
+ if (typeof value.sourceKind !== "string" || !SOURCE_KINDS.has(value.sourceKind)) {
78
+ return undefined;
79
+ }
80
+ if (!isRecord(value.sourceRef))
81
+ return undefined;
82
+ if (typeof value.transcript !== "string" || value.transcript.length === 0) {
83
+ return undefined;
84
+ }
85
+ if (typeof value.timestamp !== "string" || value.timestamp.length === 0) {
86
+ return undefined;
87
+ }
88
+ const basis = {
89
+ method: value.basis.method,
90
+ ...(Array.isArray(value.basis.anchors)
91
+ ? {
92
+ anchors: value.basis.anchors.filter((item) => typeof item === "string"),
93
+ }
94
+ : {}),
95
+ ...(typeof value.basis.note === "string" ? { note: value.basis.note } : {}),
96
+ };
97
+ const sourceRef = {
98
+ ...(typeof value.sourceRef.sessionFile === "string"
99
+ ? { sessionFile: value.sourceRef.sessionFile }
100
+ : {}),
101
+ ...(typeof value.sourceRef.entryId === "string" ||
102
+ typeof value.sourceRef.entryId === "number"
103
+ ? { entryId: value.sourceRef.entryId }
104
+ : {}),
105
+ ...(typeof value.sourceRef.path === "string" ? { path: value.sourceRef.path } : {}),
106
+ ...(typeof value.sourceRef.url === "string" ? { url: value.sourceRef.url } : {}),
107
+ };
108
+ return {
109
+ basis,
110
+ sourceKind: value.sourceKind,
111
+ sourceRef,
112
+ transcript: value.transcript,
113
+ timestamp: value.timestamp,
114
+ };
115
+ }