@akagilnc/pi-workflow-roles 0.1.3673 → 0.1.3685

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 (42) hide show
  1. package/dist/acp-host/production-host.js +297 -166
  2. package/dist/audit-escalation.js +4 -3
  3. package/dist/diarist-contracts.js +4 -4
  4. package/dist/diarist-role.js +10 -4
  5. package/dist/diarist.js +121 -17
  6. package/dist/doctor-role.js +2 -1
  7. package/dist/gatekeeper-role.js +3 -2
  8. package/dist/judge-role.js +2 -1
  9. package/dist/package-contracts/terminating-tools.js +1 -1
  10. package/dist/public-cli/inspector-run.js +3 -13
  11. package/dist/public-cli/instruction-seat-run.js +6 -31
  12. package/dist/public-cli/invocation.js +32 -0
  13. package/dist/public-cli/judge-run.js +1 -7
  14. package/dist/public-cli/main.js +714 -728
  15. package/dist/public-cli/run-lifecycle.js +0 -28
  16. package/dist/public-cli/seat-ticket-binding.js +15 -104
  17. package/dist/readable-gate-item.js +16 -0
  18. package/dist/role-runtime.js +89 -10
  19. package/dist/submission-errors.js +4 -1
  20. package/package.json +1 -1
  21. package/resources/diarist-collect.md +10 -0
  22. package/src/audit-escalation.ts +4 -3
  23. package/src/diarist-contracts.ts +21 -9
  24. package/src/diarist-role.ts +15 -4
  25. package/src/diarist.ts +192 -22
  26. package/src/doctor-role.ts +2 -1
  27. package/src/gatekeeper-role.ts +3 -2
  28. package/src/judge-role.ts +2 -1
  29. package/src/package-contracts/terminating-tools.ts +1 -1
  30. package/src/public-cli/coder-run.ts +1 -7
  31. package/src/public-cli/countersign-run.ts +238 -86
  32. package/src/public-cli/diarist-run.ts +83 -28
  33. package/src/public-cli/fixer-run.ts +1 -7
  34. package/src/public-cli/inspector-run.ts +3 -17
  35. package/src/public-cli/instruction-seat-run.ts +6 -36
  36. package/src/public-cli/invocation.ts +52 -0
  37. package/src/public-cli/judge-run.ts +1 -7
  38. package/src/public-cli/run-lifecycle.ts +0 -31
  39. package/src/public-cli/seat-ticket-binding.ts +14 -138
  40. package/src/readable-gate-item.ts +20 -0
  41. package/src/role-runtime.ts +107 -9
  42. package/src/submission-errors.ts +4 -1
@@ -827,34 +827,6 @@ export async function findLatestRunIdForSeatTicket(input) {
827
827
  }
828
828
  return best;
829
829
  }
830
- /**
831
- * Ticket identities this book's retained runs already record (#709).
832
- * Read-only reuse surface for seat ticket binding — no ticket number is minted here.
833
- */
834
- export async function collectBookRunTicketNumbers(input) {
835
- const ledgerHome = resolveActivationLedgerHome(input.home);
836
- const runsDir = join(activationBookDirectory(ledgerHome, input.bookKey), "runs");
837
- let entries;
838
- try {
839
- entries = await readdir(runsDir);
840
- }
841
- catch (error) {
842
- if (errorCodeOf(error) === "ENOENT")
843
- return new Set();
844
- throw error;
845
- }
846
- const known = new Set();
847
- for (const entry of entries) {
848
- // runs/ also holds non-run files (institutional-resolution.json, Finder
849
- // .DS_Store); only `<runId>@<role>` directories are runs (#769).
850
- if (!entry.includes("@"))
851
- continue;
852
- const ticketNumber = await readRunTicketNumber(join(runsDir, entry));
853
- if (ticketNumber !== undefined)
854
- known.add(ticketNumber);
855
- }
856
- return known;
857
- }
858
830
  /** Restore optional correlation + typed ticket identity from a durable admitted page. */
859
831
  function parsePersistedTicketIdentity(record) {
860
832
  const correlationId = typeof record.correlationId === "string" && record.correlationId.trim() !== ""
@@ -1,110 +1,21 @@
1
1
  /**
2
2
  * Shared ticket identity seam for public court seats (#635 / #637 / #709 / #747 / #771).
3
- * Court target = first `#N` in the summons instruction. Other seats reuse that
4
- * number only when this book already records it (retained run pages + 起居录
5
- * volumes); 起居郎 takes it as the caller-supplied identity on a first summons
6
- * (ADR 0081 `initial-court-ticket-supplied`). No seat-side model call, no second
7
- * ticket-number source of truth, no minting from human titles. No `#N` leaves
8
- * the run unbound (真无票), which is lawful. No CLI --ticket and no attachment
9
- * frontmatter binding. #747: officer same-parent resume also lives here.
3
+ *
4
+ * ADR 0075 `diarist-resolves-ticket-llm-layer`: the seat's LLM names the court
5
+ * target as a typed assertion; the mechanical layer only verifies that assertion
6
+ * (complete decimal of N appears in the summons; ticket exists). Code never
7
+ * judges which ticket the summons is about — no prose harvest, no first-#N
8
+ * position pick, no matching book-known numbers against instruction text
9
+ * (锚定宪法; owner 2026-09-08: 代码不准做判断). Other seats reuse a typed
10
+ * identity already handed over (起居郎 assertion / source-run / already-bound
11
+ * resume) — they do not re-recognize from instruction. No CLI --ticket, no
12
+ * attachment frontmatter. #747: officer same-parent resume also lives here.
13
+ *
14
+ * Mechanical verify helpers live on diarist.ts / invocation.ts (sole owners).
15
+ * This module owns same-ticket / same-parent resume only.
10
16
  */
11
- import { existsSync } from "node:fs";
12
- import { ActivationGitRepositoryRequiredError, resolveBookKeyFromGit, } from "../activation-ledger-git.js";
13
- import { resolveTicketProvenanceVolume } from "../ticket-provenance.js";
14
- import { bindAdmittedTicketNumber, } from "./invocation.js";
15
- import { collectBookRunTicketNumbers, findLatestRunIdForSeatTicket, } from "./run-lifecycle.js";
16
- /**
17
- * Hash-marked ticket references in an instruction, in order of first appearance.
18
- * `#582` is one token; a leading-zero run is not a ticket literal; bare digit
19
- * runs (`r1`, `D2`, sha islands, counts) are not ticket references. Callers name
20
- * the court target as the first `#N` (ADR 0081 `initial-court-ticket-supplied`).
21
- */
22
- export function instructionTicketTokens(instruction) {
23
- const tokens = [];
24
- const seen = new Set();
25
- for (const match of instruction.matchAll(/#([1-9]\d*)/g)) {
26
- const parsed = Number(match[1]);
27
- if (!Number.isSafeInteger(parsed))
28
- continue;
29
- if (seen.has(parsed))
30
- continue;
31
- seen.add(parsed);
32
- tokens.push(parsed);
33
- }
34
- return tokens;
35
- }
36
- /**
37
- * Ticket identity this summons reuses, or undefined when the court target is not
38
- * already on this book. The first `#N` is the court target; later `#N` are
39
- * neighbors/PRs and never steal the bind. Known identities come only from
40
- * records this book already holds: 起居录 volume partitions and ticket numbers
41
- * on retained run pages. Nothing matched leaves the run unbound — never guess.
42
- */
43
- export async function resolveKnownTicketNumber(input) {
44
- const tokens = instructionTicketTokens(input.instruction);
45
- if (tokens.length === 0)
46
- return undefined;
47
- const target = tokens[0];
48
- const volume = resolveTicketProvenanceVolume(target, input.projectRoot, input.home);
49
- if (existsSync(volume.volumeDir))
50
- return target;
51
- const runTickets = await readBookRunTickets(input);
52
- return runTickets.has(target) ? target : undefined;
53
- }
54
- /**
55
- * Diarist summons identity (ADR 0081 `initial-court-ticket-supplied`).
56
- * The first `#N` in the caller's dispatch is the supplied court target — the
57
- * working 起居郎 round establishes the volume on a first summons; later `#N`
58
- * (neighbors, PR numbers) do not unbind it. No `#N` stays unbound (真无票).
59
- * Not a second ticket-number source of truth and not an extra recognizer call.
60
- */
61
- export async function resolveDiaristSummonsTicketNumber(input) {
62
- const tokens = instructionTicketTokens(input.instruction);
63
- return tokens[0];
64
- }
65
- /**
66
- * Ticket numbers on this book's retained runs.
67
- * A directory with no book (not a git repository) simply holds no run history —
68
- * admission owns that rejection face, this lookup does not pre-empt it.
69
- */
70
- async function readBookRunTickets(input) {
71
- let bookKey;
72
- try {
73
- bookKey = input.bookKey ?? resolveBookKeyFromGit(input.projectRoot);
74
- }
75
- catch (error) {
76
- if (error instanceof ActivationGitRepositoryRequiredError)
77
- return new Set();
78
- throw error;
79
- }
80
- return await collectBookRunTicketNumbers({ home: input.home, bookKey });
81
- }
82
- /** Bind a reused ticket number onto an admission that is still unbound. */
83
- export async function bindReusedTicketNumber(admitted, ticketNumber) {
84
- if (ticketNumber === undefined)
85
- return;
86
- if (admitted.ticketNumber !== undefined)
87
- return;
88
- await bindAdmittedTicketNumber(admitted, ticketNumber);
89
- }
90
- /**
91
- * Sole seat disposition for an unbound admission (#635 / #709).
92
- * Already-bound admissions short-circuit — resume keeps the identity it has.
93
- * Used by seats that do not pre-resolve for same-ticket resume; the seats that
94
- * do (countersign / inspector / diarist) reuse the number they already resolved.
95
- */
96
- export async function resolveSeatTicketBinding(admitted, env) {
97
- if (admitted.ticketNumber !== undefined)
98
- return admitted.ticketNumber;
99
- const ticketNumber = await resolveKnownTicketNumber({
100
- instruction: admitted.instruction,
101
- projectRoot: admitted.projectRoot,
102
- home: env.home,
103
- bookKey: admitted.bookKey,
104
- });
105
- await bindReusedTicketNumber(admitted, ticketNumber);
106
- return ticketNumber;
107
- }
17
+ import { resolveBookKeyFromGit } from "../activation-ledger-git.js";
18
+ import { findLatestRunIdForSeatTicket, } from "./run-lifecycle.js";
108
19
  /**
109
20
  * Sole same-seat → resume decision (#637 / #724 / #747).
110
21
  * Officer seats (notary/inspector/auditor) look up by parent run path; countersign
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Parent-visible relay of one gate bounce/finding item (#750 / #775).
3
+ *
4
+ * Strings stay as-is. Any other value is JSON-serialized so structured content
5
+ * (article/reason/evidence, category/law/evidence, …) is not lost as
6
+ * `[object Object]` when joined into parent-seat text.
7
+ *
8
+ * Presentation only: does not legislate carrier format for LLM-to-LLM traffic.
9
+ */
10
+ export function readableGateItem(value) {
11
+ return typeof value === "string" ? value : JSON.stringify(value);
12
+ }
13
+ /** Join gate items into one parent-visible line. */
14
+ export function joinReadableGateItems(items, separator = "; ") {
15
+ return items.map(readableGateItem).join(separator);
16
+ }
@@ -25,7 +25,8 @@ import { INSPECTOR_TOOL_SPEC, } from "./inspector-role.js";
25
25
  import { INSPECTOR_ACCEPTED_TEXT } from "./inspector-contracts.js";
26
26
  import { DIARIST_TOOL_SPEC, } from "./diarist-role.js";
27
27
  import { DIARIST_ACCEPTED_TEXT, DIARIST_SOURCES_FLAG, projectDiaristSelections, } from "./diarist-contracts.js";
28
- import { commitDiaristSelections, loadDiaristSourceCatalog, } from "./diarist.js";
28
+ import { commitDiaristSelections, DiaristTicketVerificationError, loadDiaristSourceCatalog, verifyAssertedTicketNumber, } from "./diarist.js";
29
+ import { bindTicketNumberOnRunDirectory } from "./public-cli/invocation.js";
29
30
  import { GATEKEEPER_TOOL_SPEC, } from "./gatekeeper-role.js";
30
31
  import { NAVIGATOR_TOOL_SPEC, } from "./navigator-role.js";
31
32
  import { AUDITOR_TOOL_SPEC, } from "./auditor-role.js";
@@ -495,12 +496,35 @@ export function createAuditorRoleRuntime(roleHost, dependencies) {
495
496
  },
496
497
  };
497
498
  }
499
+ /**
500
+ * LLM typed court-target assertion from diarist output (ADR 0075).
501
+ * null/absent = true-unbound; positive safe integer = ticket N.
502
+ * Any other shape fails honestly — never washes into unbound.
503
+ */
504
+ function readDiaristTicketAssertion(submitted) {
505
+ if (submitted === undefined || !("ticketNumber" in submitted)) {
506
+ return { kind: "true-unbound" };
507
+ }
508
+ const raw = submitted.ticketNumber;
509
+ if (raw === null)
510
+ return { kind: "true-unbound" };
511
+ if (typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 1) {
512
+ return { kind: "ticket", ticketNumber: raw };
513
+ }
514
+ if (typeof raw === "string" && /^[1-9]\d*$/.test(raw)) {
515
+ const n = Number(raw);
516
+ if (Number.isSafeInteger(n) && n >= 1) {
517
+ return { kind: "ticket", ticketNumber: n };
518
+ }
519
+ }
520
+ throw new DiaristTicketVerificationError("assertion-uninterpretable", "diarist ticketNumber must be a safe integer >= 1, null, or absent");
521
+ }
498
522
  /**
499
523
  * #708: 起居郎 public seat on the shared filed-officer envelope.
500
524
  * Semantic collection happened in this role's own turn; the accept hook runs the
501
- * mechanical band (verbatim reverse-verify → idempotent sitian append →
502
- * watermark) and records the resulting sitian facts next to the receipt.
503
- * Machine facts never come from model self-report (锚定宪法).
525
+ * mechanical band (ticket verify when first-summons + verbatim reverse-verify →
526
+ * idempotent sitian append → watermark) and records the resulting sitian facts
527
+ * next to the receipt. Machine facts never come from model self-report (锚定宪法).
504
528
  */
505
529
  export function createDiaristRoleRuntime(roleHost, dependencies, getSourcesFlag) {
506
530
  // This turn's catalog, snapshotted at activate: the candidateIndexes the role
@@ -516,22 +540,77 @@ export function createDiaristRoleRuntime(roleHost, dependencies, getSourcesFlag)
516
540
  const submitted = parameters !== null && typeof parameters === "object" && !Array.isArray(parameters)
517
541
  ? parameters
518
542
  : undefined;
519
- // True-unbound summons: no ticket, no catalog, no diary committed — so
520
- // this turn produced no sitian facts. A self-reported `sitian` is not
521
- // one (锚定宪法); drop it rather than let it ride into decisiveFacts.
522
- // Carrying the field is not a reason to bounce the receipt (第 0 条).
543
+ // LLM cannot identify the court target — escalate without machine facts.
544
+ // Strip sitian (锚定宪法) and ticketNumber (unverified — must not leak
545
+ // into caller-visible admitted typed key via decisiveFacts mirror).
546
+ if (submitted?.status === "escalate") {
547
+ if (!("sitian" in submitted) && !("ticketNumber" in submitted)) {
548
+ return submitted;
549
+ }
550
+ const stripped = { ...submitted };
551
+ delete stripped.sitian;
552
+ delete stripped.ticketNumber;
553
+ return stripped;
554
+ }
555
+ const assertion = readDiaristTicketAssertion(submitted);
556
+ // No frozen catalog: only true-unbound is lawful without volume work.
557
+ // A self-reported `sitian` is not machine fact (锚定宪法).
523
558
  if (catalog === undefined) {
559
+ if (assertion.kind === "ticket") {
560
+ throw new DiaristTicketVerificationError("assertion-uninterpretable", `diarist asserted ticket #${assertion.ticketNumber} but no source catalog was frozen for this turn`);
561
+ }
524
562
  if (submitted === undefined || !("sitian" in submitted))
525
563
  return undefined;
526
564
  const stripped = { ...submitted };
527
565
  delete stripped.sitian;
528
566
  return stripped;
529
567
  }
568
+ // Already bound before the turn (typed handoff / resume): skip re-recognition
569
+ // (ADR 0075 已绑定 ticket 优先) and commit under that identity.
570
+ if (catalog.ticketNumber !== undefined) {
571
+ const facts = await commitDiaristSelections({
572
+ catalog,
573
+ selections: projectDiaristSelections(parameters),
574
+ });
575
+ return {
576
+ ...(submitted ?? { receipt: parameters }),
577
+ ticketNumber: catalog.ticketNumber,
578
+ sitian: facts,
579
+ };
580
+ }
581
+ // First summons: LLM typed assertion → mechanical verify → bind → commit.
582
+ // true-unbound → 无录 (no volume). Verification failure throws (失败诚实).
583
+ // Cannot-identify must arrive as status=escalate above — never as silent unbound.
584
+ if (assertion.kind === "true-unbound") {
585
+ if (submitted === undefined || !("sitian" in submitted)) {
586
+ return { ...(submitted ?? { receipt: parameters }), ticketNumber: null };
587
+ }
588
+ const stripped = {
589
+ ...submitted,
590
+ ticketNumber: null,
591
+ };
592
+ delete stripped.sitian;
593
+ return stripped;
594
+ }
595
+ await verifyAssertedTicketNumber({
596
+ ticketNumber: assertion.ticketNumber,
597
+ instruction: catalog.instruction,
598
+ projectRoot: catalog.projectRoot,
599
+ });
600
+ await bindTicketNumberOnRunDirectory(catalog.runDirectory, assertion.ticketNumber);
601
+ const boundCatalog = {
602
+ ...catalog,
603
+ ticketNumber: assertion.ticketNumber,
604
+ };
530
605
  const facts = await commitDiaristSelections({
531
- catalog,
606
+ catalog: boundCatalog,
532
607
  selections: projectDiaristSelections(parameters),
533
608
  });
534
- return { ...(submitted ?? { receipt: parameters }), sitian: facts };
609
+ return {
610
+ ...(submitted ?? { receipt: parameters }),
611
+ ticketNumber: assertion.ticketNumber,
612
+ sitian: facts,
613
+ };
535
614
  },
536
615
  }, dependencies);
537
616
  return {
@@ -1,6 +1,9 @@
1
+ import { joinReadableGateItems } from "./readable-gate-item.js";
1
2
  function gatekeeperNonPassMessage(result) {
2
3
  if (result.status === "bounce") {
3
- const findings = result.findings.length === 0 ? "(无 findings)" : result.findings.join("; ");
4
+ const findings = result.findings.length === 0
5
+ ? "(无 findings)"
6
+ : joinReadableGateItems(result.findings);
4
7
  return `门下省打回重写,findings:${findings}`;
5
8
  }
6
9
  if (result.status === "unreadable") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3673",
3
+ "version": "0.1.3685",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -23,3 +23,13 @@
23
23
  - 入选即整块保留:指向文件中的完整 transcript,不指针化、不摘要替代正文。
24
24
  - 引语必须是该块 transcript 内的**连续原文子串**。拼接、改写、跨段拼贴会在机械反验中失败并拒入录。
25
25
  - 宁多勿漏:拿不准是否决策相关时入选,由下游反验与人读面承接。
26
+
27
+ ## 本票身份
28
+
29
+ 由你(本席 LLM)判断当前要办的是哪张票,在交卷对象上以 typed 字段交出——代码不做认票裁定(ADR 0075 `diarist-resolves-ticket-llm-layer`;owner:代码不准做判断)。机械层只验你断言的号逐字出现于指令且票真实存在;后续衙门沿用 typed 键,不再另找。
30
+
31
+ - 派单已给明确票号(含 issue 链接或等价指称)时:`status: completed` 且 `ticketNumber: N`。
32
+ - 派单是真无票对象(方案/派单/处置案等):`status: completed` 且 `ticketNumber: null`(或省略)。真无票是合法结果,不入录。
33
+ - 派单看起来有票、但你认不出本庭对象是哪一张:`status: escalate` 并写明 `reason`。识别不了就上抛,不得省略票号假装无录,也不得猜一个号。
34
+ - 不得从候选材料、旁及的他票或任何猜测里挑一个号顶替派单未给出的身份。
35
+ - 传召文自然会提轮次、决定编号、commit sha、邻票号;你认的是本庭对象,不是文中出现的每一个数字。
@@ -3,6 +3,7 @@ import type { Usage } from "@earendil-works/pi-ai";
3
3
  import type {
4
4
  ComplianceDecision,
5
5
  } from "./compliance-transport.ts";
6
+ import { readableGateItem } from "./readable-gate-item.ts";
6
7
 
7
8
  export const AUDIT_ESCALATION_KIND = "audit_escalation" as const;
8
9
 
@@ -126,17 +127,17 @@ function humanDecisionText(
126
127
  lines.push(`Reason: ${result.reason}`);
127
128
  }
128
129
  if (Array.isArray(result.conflicts)) {
129
- lines.push("Conflicts:", ...result.conflicts.map((conflict) => `- ${conflict}`));
130
+ lines.push("Conflicts:", ...result.conflicts.map((conflict) => `- ${readableGateItem(conflict)}`));
130
131
  }
131
132
  if (officer !== undefined && Array.isArray(result.findings) && result.findings.length > 0) {
132
- lines.push("Findings:", ...result.findings.map((finding) => `- ${finding}`));
133
+ lines.push("Findings:", ...result.findings.map((finding) => `- ${readableGateItem(finding)}`));
133
134
  }
134
135
  const gate = result.auditDecisionGate;
135
136
  if (gate !== null && typeof gate === "object" && !Array.isArray(gate)) {
136
137
  const record = gate as Record<string, unknown>;
137
138
  if (typeof record.question === "string") lines.push(`Question: ${record.question}`);
138
139
  if (Array.isArray(record.options)) {
139
- lines.push("Options:", ...record.options.map((option) => `- ${option}`));
140
+ lines.push("Options:", ...record.options.map((option) => `- ${readableGateItem(option)}`));
140
141
  }
141
142
  }
142
143
  return lines.join("\n");
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Public 起居郎 (diarist) terminating receipt contracts — ADR 0075 `diarist-is-role`.
3
- * Lawful explicit release: completed, with empty or nonempty selections.
3
+ * Lawful explicit releases: completed (入录选择) | escalate (认不出本庭对象上抛).
4
4
  * Machine facts about the volume come from the mechanical sitian seam, never
5
5
  * from model self-report (锚定宪法); this module owns the receipt shape only.
6
6
  */
@@ -8,11 +8,12 @@
8
8
  export const DIARIST_OUTPUT_TOOL_NAME = "ak_diarist_output";
9
9
  export const DIARIST_ACCEPTED_TEXT = "起居郎回执已接受";
10
10
 
11
- /** Internal transport: frozen candidate-catalog path for this turn's ticket. */
11
+ /** Internal transport: frozen candidate-catalog path for this turn. */
12
12
  export const DIARIST_SOURCES_FLAG = {
13
13
  name: "ak-diarist-sources",
14
14
  definition: {
15
- description: "Frozen per-ticket source catalog the diarist selects from",
15
+ description:
16
+ "Frozen source catalog the diarist selects from (may be unbound until LLM asserts ticketNumber)",
16
17
  type: "string" as const,
17
18
  },
18
19
  } as const;
@@ -27,10 +28,22 @@ export type DiaristSelection = {
27
28
  readonly note?: string;
28
29
  };
29
30
 
30
- export type DiaristOutput = {
31
- readonly status: "completed";
32
- readonly selections: readonly DiaristSelection[];
33
- };
31
+ export type DiaristOutput =
32
+ | {
33
+ readonly status: "completed";
34
+ readonly selections: readonly DiaristSelection[];
35
+ /**
36
+ * Typed court-target assertion (ADR 0075 `diarist-resolves-ticket-llm-layer`).
37
+ * Positive integer = 本庭对象=票N; null/absent = true-unbound (真无票→无录).
38
+ * Mechanical layer verifies N; verification failure is failure, never 无录.
39
+ */
40
+ readonly ticketNumber?: number | null;
41
+ }
42
+ | {
43
+ /** LLM cannot tell which ticket this summons is about — escalate, never wash into 无录. */
44
+ readonly status: "escalate";
45
+ readonly reason: string;
46
+ };
34
47
 
35
48
  export function validateRecordedDiaristOutput(value: unknown): DiaristOutput {
36
49
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -42,7 +55,7 @@ export function validateRecordedDiaristOutput(value: unknown): DiaristOutput {
42
55
  } catch {
43
56
  throw new Error("Diarist output has no execution discriminator");
44
57
  }
45
- if (status === "completed") {
58
+ if (status === "completed" || status === "escalate") {
46
59
  return value as DiaristOutput;
47
60
  }
48
61
  throw new Error("Diarist output has no execution discriminator");
@@ -73,4 +86,3 @@ export function projectDiaristSelections(value: unknown): DiaristSelection[] {
73
86
  }
74
87
  return out;
75
88
  }
76
-
@@ -21,8 +21,19 @@ export const diaristOutputSchema = withInfrastructureFailureDeclaration(
21
21
  openToolObject(
22
22
  Type.Object({
23
23
  status: Type.Unknown({
24
- description: "completed — 形状指引,非 schema 闸",
24
+ description: "completed | escalate — 形状指引,非 schema 闸",
25
25
  }),
26
+ ticketNumber: Type.Optional(
27
+ Type.Unknown({
28
+ description:
29
+ "本庭对象票号(正整数)或 null/省略=真无票;机械验真后下游走 typed 键;认不出用 status=escalate,不洗成无录",
30
+ }),
31
+ ),
32
+ reason: Type.Optional(
33
+ Type.String({
34
+ description: "status 为 escalate 时:认不出本庭对象的原因",
35
+ }),
36
+ ),
26
37
  selections: Type.Array(
27
38
  Type.Object(
28
39
  {
@@ -36,7 +47,7 @@ export const diaristOutputSchema = withInfrastructureFailureDeclaration(
36
47
  },
37
48
  { additionalProperties: true, description: "一条入录选择" },
38
49
  ),
39
- { description: "入录选择;空列表合法完局" },
50
+ { description: "入录选择;空列表合法完局;escalate 时可不交" },
40
51
  ),
41
52
  }),
42
53
  ),
@@ -54,7 +65,7 @@ export type DiaristRuntimeDependencies = {
54
65
  export const DIARIST_TOOL_SPEC = {
55
66
  name: DIARIST_OUTPUT_TOOL_NAME,
56
67
  label: "起居郎输出",
57
- description: "起居郎入录选择。",
58
- promptSnippet: "起居郎入录选择",
68
+ description: "起居郎入录选择与本票身份断言;认不出本庭对象则 escalate。",
69
+ promptSnippet: "起居郎入录选择与本票身份",
59
70
  parameters: diaristOutputSchema,
60
71
  } as const;