@akagilnc/pi-workflow-roles 0.1.3558 → 0.1.3572

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 (49) hide show
  1. package/README.md +4 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/acp-host/description.js +21 -0
  4. package/dist/{grok → acp-host}/mcp-relay.mjs +5 -5
  5. package/dist/{grok → acp-host}/production-host.js +860 -162
  6. package/dist/diarist-contracts.js +72 -0
  7. package/dist/host-descriptions.js +34 -0
  8. package/dist/package-contracts/terminating-tools.js +20 -2
  9. package/dist/packaged-role-registry.js +20 -0
  10. package/dist/public-cli/config.js +23 -10
  11. package/dist/public-cli/main.js +622 -671
  12. package/dist/public-cli/registry.js +4 -1
  13. package/extensions/role-runtime.ts +1 -0
  14. package/package.json +1 -1
  15. package/resources/diarist-collect.md +6 -10
  16. package/scripts/build-package.mjs +8 -8
  17. package/souls/diarist.md +9 -0
  18. package/src/acp-host/description.ts +43 -0
  19. package/src/{grok → acp-host}/mcp-relay.mjs +5 -5
  20. package/src/{grok → acp-host}/production-host.ts +31 -37
  21. package/src/{grok → acp-host}/role-envelope.ts +25 -24
  22. package/src/{grok → acp-host}/role-turn-host.ts +46 -81
  23. package/src/{grok → acp-host}/session-identity.ts +7 -7
  24. package/src/diarist-contracts.ts +88 -0
  25. package/src/diarist-role.ts +60 -0
  26. package/src/diarist.ts +253 -179
  27. package/src/host-contracts.ts +11 -4
  28. package/src/host-descriptions.ts +51 -0
  29. package/src/host-transition-prior-native.ts +12 -24
  30. package/src/package-contracts/terminating-tools.ts +22 -2
  31. package/src/packaged-role-registry.ts +20 -0
  32. package/src/pi/role-turn-host.ts +10 -1
  33. package/src/public-cli/cli.ts +39 -9
  34. package/src/public-cli/config.ts +24 -11
  35. package/src/public-cli/countersign-run.ts +9 -129
  36. package/src/public-cli/diarist-run.ts +312 -0
  37. package/src/public-cli/invocation.ts +23 -2
  38. package/src/public-cli/load-production-acp-host.ts +52 -0
  39. package/src/public-cli/option-definitions.ts +18 -0
  40. package/src/public-cli/post-admission.ts +3 -3
  41. package/src/public-cli/registry.ts +3 -0
  42. package/src/public-cli/run-lifecycle.ts +31 -3
  43. package/src/public-cli/settlement.ts +50 -2
  44. package/src/public-cli/terminal.ts +2 -1
  45. package/src/role-runtime.ts +117 -6
  46. package/src/ticket-provenance-contracts.ts +1 -0
  47. package/src/ticket-provenance.ts +0 -23
  48. package/src/diarist-llm-collector.ts +0 -324
  49. package/src/public-cli/load-production-grok-host.ts +0 -35
@@ -3,9 +3,10 @@ import { createInterface } from "node:readline";
3
3
 
4
4
  import type { RoleTurnHost, RoleTurnKnownFailure, RoleTurnRequest, RoleTurnResult } from "../host-contracts.ts";
5
5
  import { renderAgentStartMaterials } from "../agent-start-materials.ts";
6
+ import type { AcpHostDescription } from "./description.ts";
6
7
 
7
- /** ACP v1 surface used by the Grok adapter. Protocol details stay in this module. */
8
- export interface GrokAcpConnection {
8
+ /** ACP v1 surface used by the generic ACP adapter. Protocol details stay in this module. */
9
+ export interface AcpConnection {
9
10
  request(method: string, params: Readonly<Record<string, unknown>>): Promise<Readonly<Record<string, unknown>>>;
10
11
  notify(method: string, params: Readonly<Record<string, unknown>>): void;
11
12
  /** Subscribe to agent→client notifications (session/update stream, etc.). */
@@ -15,7 +16,7 @@ export interface GrokAcpConnection {
15
16
 
16
17
  /** The shared envelope, prepared before session/new (systemPrompt delivery) and
17
18
  * able to observe the host's real builtin tool surface once it arrives post-session. */
18
- export type GrokPreparedTurn = Readonly<{
19
+ export type AcpPreparedTurn = Readonly<{
19
20
  mcpServers: readonly Readonly<Record<string, unknown>>[];
20
21
  /**
21
22
  * Structured system-prompt authority. `body` is the provider-facing prompt
@@ -45,24 +46,26 @@ export type GrokPreparedTurn = Readonly<{
45
46
  }>;
46
47
 
47
48
  /** Fold structured system-prompt authority into the provider-visible ACP override. */
48
- export function renderGrokSystemPromptOverride(authority: {
49
+ export function renderAcpSystemPromptOverride(authority: {
49
50
  readonly body: string;
50
51
  readonly materials: readonly unknown[];
51
52
  }): string {
52
53
  return renderAgentStartMaterials(authority.body, authority.materials);
53
54
  }
54
55
 
55
- export type GrokSessionIdentityAuthority = Readonly<{
56
+ export type AcpSessionIdentityAuthority = Readonly<{
56
57
  load(principal: RoleTurnRequest["principal"]): Promise<string | undefined>;
57
58
  bind(principal: RoleTurnRequest["principal"], sessionId: string): Promise<void>;
58
59
  /** Durable principal session path for layout ownership / isAvailable — not a rebuild source (#617 DK-4). */
59
60
  resolveSessionFile(principal: RoleTurnRequest["principal"]): string;
60
61
  }>;
61
62
 
62
- export type GrokRoleTurnHostConfig = Readonly<{
63
- sessionIdentity: GrokSessionIdentityAuthority;
64
- connect(request: RoleTurnRequest): Promise<GrokAcpConnection>;
65
- prepare(request: RoleTurnRequest): Promise<GrokPreparedTurn>;
63
+ export type AcpRoleTurnHostConfig = Readonly<{
64
+ sessionIdentity: AcpSessionIdentityAuthority;
65
+ /** Whether a bound resume reuses the native session or mints a fresh one. */
66
+ boundResume: AcpHostDescription["boundResume"];
67
+ connect(request: RoleTurnRequest): Promise<AcpConnection>;
68
+ prepare(request: RoleTurnRequest): Promise<AcpPreparedTurn>;
66
69
  }>;
67
70
 
68
71
  function failure(cause: "activation" | "session" | "output", name: string, code: string, details?: Readonly<Record<string, unknown>>): RoleTurnResult {
@@ -81,7 +84,7 @@ function failure(cause: "activation" | "session" | "output", name: string, code:
81
84
  type RpcReply = { readonly id?: unknown; readonly method?: unknown; readonly params?: unknown; readonly result?: unknown; readonly error?: unknown };
82
85
 
83
86
  function hostAbortedError(): Error & { readonly code: "host-aborted" } {
84
- return Object.assign(new Error("Grok host aborted"), { code: "host-aborted" as const });
87
+ return Object.assign(new Error("ACP host aborted"), { code: "host-aborted" as const });
85
88
  }
86
89
 
87
90
  function acpError(code: string, message: string, cause?: unknown): Error & { readonly code: string } {
@@ -89,23 +92,14 @@ function acpError(code: string, message: string, cause?: unknown): Error & { rea
89
92
  }
90
93
 
91
94
  /** One ACP JSON-RPC stdio process. Natural close/SIGTERM are its only lifecycle exits. */
92
- export function connectGrokAcpStdio(options: {
95
+ export function connectAcpStdio(options: {
93
96
  readonly binary: string;
97
+ readonly args: readonly string[];
94
98
  readonly cwd: string;
95
99
  readonly env: NodeJS.ProcessEnv;
96
- readonly model?: string;
97
- readonly toolset?: string;
98
100
  readonly onNotification?: (method: string, params: Readonly<Record<string, unknown>>) => void;
99
- }): Promise<GrokAcpConnection> {
100
- const args = [
101
- "agent",
102
- ...(options.model === undefined ? [] : ["--model", options.model]),
103
- "stdio",
104
- ];
105
- const env = options.toolset === undefined
106
- ? options.env
107
- : { ...options.env, GROK_CONFIG: JSON.stringify({ toolset: options.toolset }) };
108
- const child = spawn(options.binary, args, { cwd: options.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
101
+ }): Promise<AcpConnection> {
102
+ const child = spawn(options.binary, [...options.args], { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"] });
109
103
  const pending = new Map<number, { resolve(value: Readonly<Record<string, unknown>>): void; reject(error: Error): void }>();
110
104
  const notificationHandlers: Array<(method: string, params: Readonly<Record<string, unknown>>) => void> = [];
111
105
  if (options.onNotification !== undefined) notificationHandlers.push(options.onNotification);
@@ -128,12 +122,12 @@ export function connectGrokAcpStdio(options: {
128
122
  child.kill("SIGTERM");
129
123
  };
130
124
  child.stderr.setEncoding("utf8").on("data", (chunk: string) => { stderr += chunk; });
131
- child.on("error", (error) => settleClosed(acpError("acp-process-error", `Grok ACP process error: ${error.message}`, error)));
125
+ child.on("error", (error) => settleClosed(acpError("acp-process-error", `ACP process error: ${error.message}`, error)));
132
126
  createInterface({ input: child.stdout }).on("line", (line) => {
133
127
  let message: RpcReply;
134
128
  try { message = JSON.parse(line) as RpcReply; }
135
129
  catch (error) {
136
- terminate(acpError("acp-invalid-json", `Invalid Grok ACP JSON: ${String(error)}`, error));
130
+ terminate(acpError("acp-invalid-json", `Invalid ACP JSON: ${String(error)}`, error));
137
131
  return;
138
132
  }
139
133
  if (typeof message.method === "string") {
@@ -142,14 +136,14 @@ export function connectGrokAcpStdio(options: {
142
136
  for (const handler of notificationHandlers) handler(message.method, params);
143
137
  if (typeof message.id === "number") {
144
138
  if (message.method !== "session/request_permission") {
145
- terminate(acpError("acp-unsupported-client-request", `Unsupported Grok ACP client request: ${message.method}`));
139
+ terminate(acpError("acp-unsupported-client-request", `Unsupported ACP client request: ${message.method}`));
146
140
  return;
147
141
  }
148
142
  const choices = Array.isArray(params.options) ? params.options : [];
149
143
  const selected = choices.find((value) =>
150
144
  typeof value === "object" && value !== null && (value as { kind?: unknown }).kind === "allow_once") as { optionId?: unknown } | undefined;
151
145
  if (typeof selected?.optionId !== "string") {
152
- terminate(acpError("acp-permission-missing-allow-once", "Grok ACP permission request omitted allow_once"));
146
+ terminate(acpError("acp-permission-missing-allow-once", "ACP permission request omitted allow_once"));
153
147
  return;
154
148
  }
155
149
  child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: message.id, result: { outcome: { outcome: "selected", optionId: selected.optionId } } })}\n`);
@@ -160,13 +154,13 @@ export function connectGrokAcpStdio(options: {
160
154
  const waiter = pending.get(message.id);
161
155
  if (waiter === undefined) return;
162
156
  pending.delete(message.id);
163
- if (message.error !== undefined) waiter.reject(acpError("acp-upstream-error", `Grok ACP error: ${JSON.stringify(message.error)}`));
157
+ if (message.error !== undefined) waiter.reject(acpError("acp-upstream-error", `ACP error: ${JSON.stringify(message.error)}`));
164
158
  else waiter.resolve((message.result ?? {}) as Readonly<Record<string, unknown>>);
165
159
  });
166
- child.on("close", (code) => settleClosed(acpError("acp-closed", `Grok ACP closed (${String(code)}): ${stderr}`)));
160
+ child.on("close", (code) => settleClosed(acpError("acp-closed", `ACP closed (${String(code)}): ${stderr}`)));
167
161
  return Promise.resolve({
168
162
  request(method, params) {
169
- if (closed) return Promise.reject(terminalError ?? acpError("acp-connection-closed", "Grok ACP connection is closed"));
163
+ if (closed) return Promise.reject(terminalError ?? acpError("acp-connection-closed", "ACP connection is closed"));
170
164
  const id = ++nextId;
171
165
  return new Promise((resolve, reject) => {
172
166
  pending.set(id, { resolve, reject });
@@ -175,12 +169,12 @@ export function connectGrokAcpStdio(options: {
175
169
  const waiter = pending.get(id);
176
170
  if (waiter === undefined) return;
177
171
  pending.delete(id);
178
- waiter.reject(acpError("acp-write-failed", `Grok ACP write failed: ${error.message}`, error));
172
+ waiter.reject(acpError("acp-write-failed", `ACP write failed: ${error.message}`, error));
179
173
  });
180
174
  });
181
175
  },
182
176
  notify(method, params) {
183
- if (closed) throw terminalError ?? acpError("acp-connection-closed", "Grok ACP connection is closed");
177
+ if (closed) throw terminalError ?? acpError("acp-connection-closed", "ACP connection is closed");
184
178
  child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
185
179
  },
186
180
  onNotification(handler) {
@@ -188,7 +182,7 @@ export function connectGrokAcpStdio(options: {
188
182
  },
189
183
  async close() {
190
184
  if (closed) return;
191
- settleClosed(acpError("acp-connection-closed", "Grok ACP connection is closed"));
185
+ settleClosed(acpError("acp-connection-closed", "ACP connection is closed"));
192
186
  child.stdin.end();
193
187
  child.kill("SIGTERM");
194
188
  await new Promise<void>((resolve) => child.once("close", () => resolve()));
@@ -197,23 +191,23 @@ export function connectGrokAcpStdio(options: {
197
191
  }
198
192
 
199
193
  /**
200
- * Main-session Grok adapter. The injected composition callbacks are the shared
194
+ * Main-session ACP adapter. The injected composition callbacks are the shared
201
195
  * envelope boundary: this module owns ACP lifecycle, never role policy.
202
196
  */
203
- export function createGrokRoleTurnHost(config: GrokRoleTurnHostConfig): RoleTurnHost {
197
+ export function createAcpRoleTurnHost(config: AcpRoleTurnHostConfig): RoleTurnHost {
204
198
  let serial = Promise.resolve();
205
199
  return {
206
200
  executeTurn(request) {
207
201
  const execution = serial.then(async (): Promise<RoleTurnResult> => {
208
202
  const continuation = request.continuation;
209
203
  const prepared = await config.prepare(request);
210
- let connection: GrokAcpConnection | undefined;
204
+ let connection: AcpConnection | undefined;
211
205
  let sessionId: string | undefined;
212
206
  let accepted = false;
213
207
  try {
214
208
  // AK injection proof is prepared MCP composition (envelope).
215
209
  if (prepared.mcpServers.length === 0) {
216
- return failure("activation", "UncontrolledGrokSession", "ak-config-missing");
210
+ return failure("activation", "UncontrolledAcpSession", "ak-config-missing");
217
211
  }
218
212
  connection = await config.connect(request);
219
213
  const initialized = await connection.request("initialize", {
@@ -227,60 +221,47 @@ export function createGrokRoleTurnHost(config: GrokRoleTurnHostConfig): RoleTurn
227
221
  const availableModels = Array.isArray(modelState?.availableModels) ? modelState.availableModels : undefined;
228
222
  if (request.model !== undefined && availableModels !== undefined && !availableModels.some((entry) =>
229
223
  typeof entry === "object" && entry !== null && (entry as { modelId?: unknown }).modelId === request.model?.model)) {
230
- return failure("activation", "GrokHostModelMismatch", "host-model-mismatch", {
224
+ return failure("activation", "AcpHostModelMismatch", "host-model-mismatch", {
231
225
  provider: request.model.provider,
232
226
  model: request.model.model,
233
227
  });
234
228
  }
235
229
 
236
- // #617 DK-7: hand Pi session path once; Grok reads the file itself.
230
+ // #617 DK-7 / #732: hand the projected prior-native session paths once,
231
+ // whichever record family they came from; the host reads the files itself.
237
232
  const priorNativePaths =
238
233
  continuation.kind === "resume"
239
- && request.hostTransition?.previousHost === "pi"
240
- ? request.hostTransition.priorNativePaths
234
+ ? request.hostTransition?.priorNativePaths
241
235
  : undefined;
242
- if (continuation.kind === "resume") {
236
+ if (continuation.kind === "resume" && config.boundResume === "session/load") {
237
+ // Same-host resume reuses the native ACP session via session/load.
243
238
  const boundSessionId = await config.sessionIdentity.load(request.principal);
244
239
  if (boundSessionId !== undefined && boundSessionId !== "") {
245
- // Same-host Grok resume reuses native ACP session via session/load.
246
240
  const loaded = await connection.request("session/load", {
247
241
  sessionId: boundSessionId,
248
242
  cwd: request.cwd,
249
243
  mcpServers: prepared.mcpServers,
250
- _meta: { systemPromptOverride: renderGrokSystemPromptOverride(prepared.systemPrompt), yoloMode: false },
244
+ _meta: { systemPromptOverride: renderAcpSystemPromptOverride(prepared.systemPrompt), yoloMode: false },
251
245
  });
252
246
  sessionId = typeof loaded.sessionId === "string" && loaded.sessionId !== ""
253
247
  ? loaded.sessionId
254
248
  : boundSessionId;
255
- } else {
256
- // Unbound resume (cross-host or lost binding): session/new + bind.
257
- const session = await connection.request(
258
- "session/new",
259
- {
260
- cwd: request.cwd,
261
- mcpServers: prepared.mcpServers,
262
- _meta: { systemPromptOverride: renderGrokSystemPromptOverride(prepared.systemPrompt), yoloMode: false },
263
- },
264
- );
265
- sessionId = typeof session.sessionId === "string" ? session.sessionId : undefined;
266
- if (sessionId === undefined || sessionId === "") {
267
- return failure("session", "GrokAcpSessionFailure", "session-id-missing");
268
- }
269
- await config.sessionIdentity.bind(request.principal, sessionId);
270
249
  }
271
- } else {
272
- // Initial run: session/new + bind.
250
+ }
251
+ if (sessionId === undefined) {
252
+ // Initial run, unbound resume (cross-host / lost binding), or a host
253
+ // whose bound resume is session/new: mint the session and bind it.
273
254
  const session = await connection.request(
274
255
  "session/new",
275
256
  {
276
257
  cwd: request.cwd,
277
258
  mcpServers: prepared.mcpServers,
278
- _meta: { systemPromptOverride: renderGrokSystemPromptOverride(prepared.systemPrompt), yoloMode: false },
259
+ _meta: { systemPromptOverride: renderAcpSystemPromptOverride(prepared.systemPrompt), yoloMode: false },
279
260
  },
280
261
  );
281
262
  sessionId = typeof session.sessionId === "string" ? session.sessionId : undefined;
282
263
  if (sessionId === undefined || sessionId === "") {
283
- return failure("session", "GrokAcpSessionFailure", "session-id-missing");
264
+ return failure("session", "AcpSessionFailure", "session-id-missing");
284
265
  }
285
266
  await config.sessionIdentity.bind(request.principal, sessionId);
286
267
  }
@@ -353,7 +334,7 @@ export function createGrokRoleTurnHost(config: GrokRoleTurnHostConfig): RoleTurn
353
334
  throw error;
354
335
  }
355
336
  if (result.stopReason === "refusal") {
356
- return failure("output", "GrokAcpRefusal", "refusal", { sessionId });
337
+ return failure("output", "AcpRefusal", "refusal", { sessionId });
357
338
  }
358
339
  // session/prompt resolution is the sole typed round boundary before seal
359
340
  // when the turn ends without host abort; abort path closes above.
@@ -370,7 +351,7 @@ export function createGrokRoleTurnHost(config: GrokRoleTurnHostConfig): RoleTurn
370
351
  }
371
352
  prompt = `The prior terminal submission was rejected (${closure.retry.code}). Resubmit it as the sole terminal tool call. Rejected call ids: ${closure.retry.toolCallIds.join(", ") || "none"}.`;
372
353
  }
373
- return failure("output", "GrokAcpRoundLimit", "round-retry-limit", { sessionId });
354
+ return failure("output", "AcpRoundLimit", "round-retry-limit", { sessionId });
374
355
  } finally {
375
356
  if (connection !== undefined) {
376
357
  if (sessionId !== undefined && !accepted) {
@@ -389,19 +370,3 @@ export function createGrokRoleTurnHost(config: GrokRoleTurnHostConfig): RoleTurn
389
370
  },
390
371
  };
391
372
  }
392
-
393
- const PRIVATE_COMPAT_ENV = Object.fromEntries(
394
- ["CLAUDE", "CURSOR", "CODEX"].flatMap((vendor) =>
395
- ["SKILLS", "RULES", "AGENTS", "MCPS", "HOOKS", "SESSIONS"].map((kind) =>
396
- [`GROK_${vendor}_${kind}_ENABLED`, "false"] as const)),
397
- );
398
-
399
- /** Child environment shared by ACP agent processes. */
400
- export function controlledGrokChildEnv(base: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
401
- return {
402
- ...base,
403
- ...PRIVATE_COMPAT_ENV,
404
- GROK_MEMORY: "0",
405
- GROK_SUBAGENTS: "0",
406
- };
407
- }
@@ -2,15 +2,15 @@ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
 
4
4
  import type { DurablePrincipal, DurablePrincipalAuthority } from "../host-contracts.ts";
5
- import type { GrokSessionIdentityAuthority } from "./role-turn-host.ts";
6
-
7
- /** ACP binding filename written only by the grok host on initial bind. */
8
- export const GROK_ACP_SESSION_BINDING = "grok-acp-session.json";
5
+ import type { AcpSessionIdentityAuthority } from "./role-turn-host.ts";
9
6
 
10
7
  /** Durable ACP binding stored beside the host-owned session principal. */
11
- export function createGrokSessionIdentityAuthority(authority: DurablePrincipalAuthority): GrokSessionIdentityAuthority {
8
+ export function createAcpSessionIdentityAuthority(
9
+ authority: DurablePrincipalAuthority,
10
+ sessionBindingFile: string,
11
+ ): AcpSessionIdentityAuthority {
12
12
  const bindingPath = (principal: DurablePrincipal): string =>
13
- join(authority.decode(principal).sessionDirectory, GROK_ACP_SESSION_BINDING);
13
+ join(authority.decode(principal).sessionDirectory, sessionBindingFile);
14
14
  return {
15
15
  resolveSessionFile(principal) {
16
16
  return authority.decode(principal).sessionFile;
@@ -19,7 +19,7 @@ export function createGrokSessionIdentityAuthority(authority: DurablePrincipalAu
19
19
  try {
20
20
  const value: unknown = JSON.parse(await readFile(bindingPath(principal), "utf8"));
21
21
  if (typeof value !== "object" || value === null || typeof (value as { sessionId?: unknown }).sessionId !== "string") {
22
- throw new Error("durable Grok ACP session binding is invalid");
22
+ throw new Error("durable ACP session binding is invalid");
23
23
  }
24
24
  return (value as { sessionId: string }).sessionId;
25
25
  } catch (error) {
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Public 起居郎 (diarist) terminating receipt contracts — ADR 0075 `diarist-is-role`.
3
+ * Lawful explicit release: completed, with empty or nonempty selections.
4
+ * Machine facts about the volume come from the mechanical sitian seam, never
5
+ * from model self-report (锚定宪法); this module owns the receipt shape only.
6
+ */
7
+
8
+ export const DIARIST_OUTPUT_TOOL_NAME = "ak_diarist_output";
9
+ export const DIARIST_ACCEPTED_TEXT = "起居郎回执已接受";
10
+
11
+ /** Internal transport: frozen candidate-catalog path for this turn's ticket. */
12
+ export const DIARIST_SOURCES_FLAG = {
13
+ name: "ak-diarist-sources",
14
+ definition: {
15
+ description: "Frozen per-ticket source catalog the diarist selects from",
16
+ type: "string" as const,
17
+ },
18
+ } as const;
19
+
20
+ /** One selected block reference into the frozen candidate catalog. */
21
+ export type DiaristSelection = {
22
+ /** Index into the catalog the diarist received. */
23
+ readonly candidateIndex: number;
24
+ /** Quotes claimed verbatim from that block (reverse-verified mechanically). */
25
+ readonly quotes: readonly string[];
26
+ /** Human-facing note (relation to this case). Not a machine gate. */
27
+ readonly note?: string;
28
+ };
29
+
30
+ export type DiaristOutput = {
31
+ readonly status: "completed";
32
+ readonly selections: readonly DiaristSelection[];
33
+ };
34
+
35
+ export function validateRecordedDiaristOutput(value: unknown): DiaristOutput {
36
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
37
+ throw new Error("Diarist output has no execution discriminator");
38
+ }
39
+ let status: unknown;
40
+ try {
41
+ status = (value as Record<string, unknown>).status;
42
+ } catch {
43
+ throw new Error("Diarist output has no execution discriminator");
44
+ }
45
+ if (status === "completed") {
46
+ return value as DiaristOutput;
47
+ }
48
+ throw new Error("Diarist output has no execution discriminator");
49
+ }
50
+
51
+ /**
52
+ * Lenient projection of submitted selections (第 0 条: shape is not an admission
53
+ * gate). Rows that carry no usable candidateIndex are dropped from the commit,
54
+ * never bounced back at the role.
55
+ */
56
+ export function projectDiaristSelections(value: unknown): DiaristSelection[] {
57
+ const rows = (value as { selections?: unknown } | null)?.selections;
58
+ if (!Array.isArray(rows)) return [];
59
+ const out: DiaristSelection[] = [];
60
+ for (const row of rows) {
61
+ if (row === null || typeof row !== "object" || Array.isArray(row)) continue;
62
+ const record = row as Record<string, unknown>;
63
+ const index = record.candidateIndex;
64
+ if (typeof index !== "number" || !Number.isInteger(index) || index < 0) continue;
65
+ const quotes = Array.isArray(record.quotes)
66
+ ? record.quotes.filter((q): q is string => typeof q === "string" && q.length > 0)
67
+ : [];
68
+ out.push({
69
+ candidateIndex: index,
70
+ quotes,
71
+ ...(typeof record.note === "string" ? { note: record.note } : {}),
72
+ });
73
+ }
74
+ return out;
75
+ }
76
+
77
+ /** Machine-facing facts from an accepted receipt. Submitted rows retained as-is. */
78
+ export function diaristDecisiveFacts(
79
+ output: DiaristOutput,
80
+ ): Record<string, unknown> {
81
+ const facts: Record<string, unknown> = { status: output.status };
82
+ const selections = (output as { selections?: unknown }).selections;
83
+ if (Array.isArray(selections)) facts.selections = selections;
84
+ // Mechanical sitian facts recorded by the envelope at accept time.
85
+ const sitian = (output as { sitian?: unknown }).sitian;
86
+ if (sitian !== undefined) facts.sitian = sitian;
87
+ return facts;
88
+ }
@@ -0,0 +1,60 @@
1
+ import type { Static } from "typebox";
2
+ import { Type } from "typebox";
3
+
4
+ import { openToolObject } from "./open-tool-schema.ts";
5
+ import { withInfrastructureFailureDeclaration } from "./package-contracts/terminating-infrastructure.ts";
6
+ import {
7
+ DIARIST_OUTPUT_TOOL_NAME,
8
+ validateRecordedDiaristOutput,
9
+ type DiaristOutput,
10
+ } from "./diarist-contracts.ts";
11
+
12
+ export {
13
+ DIARIST_ACCEPTED_TEXT,
14
+ DIARIST_OUTPUT_TOOL_NAME,
15
+ } from "./diarist-contracts.ts";
16
+ export type { DiaristOutput };
17
+ export { validateRecordedDiaristOutput };
18
+
19
+ /** 起居郎交卷形状;形状指引,非 schema 闸。 */
20
+ export const diaristOutputSchema = withInfrastructureFailureDeclaration(
21
+ openToolObject(
22
+ Type.Object({
23
+ status: Type.Unknown({
24
+ description: "completed — 形状指引,非 schema 闸",
25
+ }),
26
+ selections: Type.Array(
27
+ Type.Object(
28
+ {
29
+ candidateIndex: Type.Number({ description: "候选目录中的整块序号" }),
30
+ quotes: Type.Array(Type.String(), {
31
+ description: "该块 transcript 内的连续原文;机械反验",
32
+ }),
33
+ note: Type.Optional(
34
+ Type.String({ description: "该材料与本案的关系(人读)" }),
35
+ ),
36
+ },
37
+ { additionalProperties: true, description: "一条入录选择" },
38
+ ),
39
+ { description: "入录选择;空列表合法完局" },
40
+ ),
41
+ }),
42
+ ),
43
+ );
44
+
45
+ export type DiaristOutputParameters = Static<typeof diaristOutputSchema>;
46
+
47
+ export type DiaristRuntimeDependencies = {
48
+ loadSoul(): Promise<string>;
49
+ };
50
+
51
+ /**
52
+ * 决定工具规格。生命周期装配归注册信封 owner——src/role-runtime.ts(ADR 0018)。
53
+ */
54
+ export const DIARIST_TOOL_SPEC = {
55
+ name: DIARIST_OUTPUT_TOOL_NAME,
56
+ label: "起居郎输出",
57
+ description: "起居郎入录选择。",
58
+ promptSnippet: "起居郎入录选择",
59
+ parameters: diaristOutputSchema,
60
+ } as const;