@akagilnc/pi-workflow-roles 0.1.3708 → 0.1.3712

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.
@@ -1,4 +1,5 @@
1
- import { writeSync } from "node:fs";
1
+ import { readFileSync, writeSync } from "node:fs";
2
+ import { join } from "node:path";
2
3
  import { ExplicitInternalActivationError, } from "./host-contracts.js";
3
4
  import { recordReviewerDispatchRejectionSync } from "./public-cli/reviewer-dispatch-rejection.js";
4
5
  import { Value } from "typebox/value";
@@ -6,7 +7,7 @@ import { sitianReport } from "./sitian-facade.js";
6
7
  import { createSubmissionLedgerHost } from "./submission-ledger.js";
7
8
  import { createCollectorLedger } from "./collector-ledger.js";
8
9
  import { activationTraceRecordSchema, namedActivationCause } from "./activation-trace.js";
9
- import { resolveActivationLedgerHomeForPath } from "./activation-ledger-topology.js";
10
+ import { homeFromRunDirectory, resolveActivationLedgerHomeForPath, } from "./activation-ledger-topology.js";
10
11
  import { appendAcceptedActivationToBook, buildAcceptedActivationFact, correlationIdentityFromEnv, durableSessionPointer, resolveActivationLedgerHome, resolveBookKeyFromGit, } from "./activation-ledger.js";
11
12
  import { writeStderrJsonlRecord } from "./stderr-jsonl.js";
12
13
  import { createToolExecutionObservationFace, systemToolExecutionObservationMonoNow, writeToolExecutionObservationRecord, } from "./tool-execution-observation.js";
@@ -24,8 +25,8 @@ import { GLEANER_LEFT_ACCEPTED_TEXT, GLEANER_LEFT_BASE_FLAG } from "./gleaner-le
24
25
  import { INSPECTOR_TOOL_SPEC, } from "./inspector-role.js";
25
26
  import { INSPECTOR_ACCEPTED_TEXT } from "./inspector-contracts.js";
26
27
  import { DIARIST_TOOL_SPEC, } from "./diarist-role.js";
27
- import { DIARIST_ACCEPTED_TEXT, DIARIST_SOURCES_FLAG, projectDiaristSelections, } from "./diarist-contracts.js";
28
- import { commitDiaristSelections, DiaristTicketVerificationError, loadDiaristSourceCatalog, verifyAssertedTicketNumber, } from "./diarist.js";
28
+ import { DIARIST_ACCEPTED_TEXT, projectDiaristEntries, } from "./diarist-contracts.js";
29
+ import { commitDiaristEntries } from "./diarist.js";
29
30
  import { bindTicketNumberOnRunDirectory } from "./public-cli/invocation.js";
30
31
  import { GATEKEEPER_TOOL_SPEC, } from "./gatekeeper-role.js";
31
32
  import { NAVIGATOR_TOOL_SPEC, } from "./navigator-role.js";
@@ -90,13 +91,6 @@ const GLEANER_LEFT_TRANSPORT_FLAGS = Object.freeze([
90
91
  definition: GLEANER_LEFT_BASE_FLAG.definition,
91
92
  }),
92
93
  ]);
93
- /** Diarist private transport: frozen source catalog path (ADR 0018 / #708). */
94
- const DIARIST_TRANSPORT_FLAGS = Object.freeze([
95
- Object.freeze({
96
- name: DIARIST_SOURCES_FLAG.name,
97
- definition: DIARIST_SOURCES_FLAG.definition,
98
- }),
99
- ]);
100
94
  /**
101
95
  * Decode private transport flags into frozen admitted inputs.
102
96
  * Envelope-owned; necessary JSON decode only (public --authority-ref owns grammar).
@@ -499,8 +493,9 @@ export function createAuditorRoleRuntime(roleHost, dependencies) {
499
493
  };
500
494
  }
501
495
  /**
502
- * LLM typed court-target assertion from diarist output (ADR 0075).
496
+ * LLM typed court-target assertion from diarist output (ADR 0075 / #779).
503
497
  * null/absent = true-unbound; positive safe integer = ticket N.
498
+ * Shape-only read of the typed field — not content judgment of free text.
504
499
  * Any other shape fails honestly — never washes into unbound.
505
500
  */
506
501
  function readDiaristTicketAssertion(submitted) {
@@ -519,21 +514,40 @@ function readDiaristTicketAssertion(submitted) {
519
514
  return { kind: "ticket", ticketNumber: n };
520
515
  }
521
516
  }
522
- throw new DiaristTicketVerificationError("assertion-uninterpretable", "diarist ticketNumber must be a safe integer >= 1, null, or absent");
517
+ throw new Error("diarist ticketNumber must be a safe integer >= 1, null, or absent");
518
+ }
519
+ /** Run coordinates + optional pre-bound ticket from durable pages (#779). */
520
+ function readDiaristRunCoordinates() {
521
+ const runDirectory = process.env.AK_ROLE_RUN_DIR;
522
+ if (typeof runDirectory !== "string" || runDirectory.trim() === "") {
523
+ throw new Error("diarist accept requires AK_ROLE_RUN_DIR");
524
+ }
525
+ const admittedPath = join(runDirectory, "admitted-request.json");
526
+ const admitted = JSON.parse(readFileSync(admittedPath, "utf8"));
527
+ if (typeof admitted.projectRoot !== "string" || admitted.projectRoot.trim() === "") {
528
+ throw new Error(`diarist admitted-request missing projectRoot (${admittedPath})`);
529
+ }
530
+ const bound = typeof admitted.ticketNumber === "number" &&
531
+ Number.isSafeInteger(admitted.ticketNumber) &&
532
+ admitted.ticketNumber >= 1
533
+ ? admitted.ticketNumber
534
+ : undefined;
535
+ return {
536
+ runDirectory,
537
+ projectRoot: admitted.projectRoot,
538
+ home: homeFromRunDirectory(runDirectory),
539
+ ...(bound === undefined ? {} : { boundTicketNumber: bound }),
540
+ };
523
541
  }
524
542
  /**
525
- * #708: 起居郎 public seat on the shared filed-officer envelope.
526
- * Semantic collection happened in this role's own turn; the accept hook runs the
527
- * mechanical band (ticket verify when first-summons + verbatim reverse-verify →
528
- * idempotent sitian append → watermark) and records the resulting sitian facts
529
- * next to the receipt. Machine facts never come from model self-report (锚定宪法).
543
+ * #708 / #779: 起居郎 public seat on the shared filed-officer envelope.
544
+ * Semantic collection happened in this role's own turn (LLM finds materials).
545
+ * Accept hook: bind typed ticket assertion → idempotent sitian append of whole
546
+ * blocks the LLM submitted. No frozen catalog, no quote/ticket reverse-verify
547
+ * of LLM output. Machine facts never come from model self-report (锚定宪法).
530
548
  */
531
- export function createDiaristRoleRuntime(roleHost, dependencies, getSourcesFlag) {
532
- // This turn's catalog, snapshotted at activate: the candidateIndexes the role
533
- // saw and the rows accept commits are the same bytes. Accept never re-reads
534
- // the file, so anything written to it mid-turn cannot become a diary entry.
535
- let catalog;
536
- const runtime = createFiledOfficerRuntime(roleHost, {
549
+ export function createDiaristRoleRuntime(roleHost, dependencies) {
550
+ return createFiledOfficerRuntime(roleHost, {
537
551
  role: "diarist",
538
552
  tool: DIARIST_TOOL_SPEC,
539
553
  acceptedText: DIARIST_ACCEPTED_TEXT,
@@ -543,8 +557,8 @@ export function createDiaristRoleRuntime(roleHost, dependencies, getSourcesFlag)
543
557
  ? parameters
544
558
  : undefined;
545
559
  // LLM cannot identify the court target — escalate without machine facts.
546
- // Strip sitian (锚定宪法) and ticketNumber (unverified — must not leak
547
- // into caller-visible admitted typed key via decisiveFacts mirror).
560
+ // Strip sitian (锚定宪法) and ticketNumber (must not leak into
561
+ // caller-visible admitted typed key via decisiveFacts mirror).
548
562
  if (submitted?.status === "escalate") {
549
563
  if (!("sitian" in submitted) && !("ticketNumber" in submitted)) {
550
564
  return submitted;
@@ -555,35 +569,17 @@ export function createDiaristRoleRuntime(roleHost, dependencies, getSourcesFlag)
555
569
  return stripped;
556
570
  }
557
571
  const assertion = readDiaristTicketAssertion(submitted);
558
- // No frozen catalog: only true-unbound is lawful without volume work.
559
- // A self-reported `sitian` is not machine fact (锚定宪法).
560
- if (catalog === undefined) {
561
- if (assertion.kind === "ticket") {
562
- throw new DiaristTicketVerificationError("assertion-uninterpretable", `diarist asserted ticket #${assertion.ticketNumber} but no source catalog was frozen for this turn`);
563
- }
564
- if (submitted === undefined || !("sitian" in submitted))
565
- return undefined;
566
- const stripped = { ...submitted };
567
- delete stripped.sitian;
568
- return stripped;
569
- }
572
+ const coords = readDiaristRunCoordinates();
570
573
  // Already bound before the turn (typed handoff / resume): skip re-recognition
571
- // (ADR 0075 已绑定 ticket 优先) and commit under that identity.
572
- if (catalog.ticketNumber !== undefined) {
573
- const facts = await commitDiaristSelections({
574
- catalog,
575
- selections: projectDiaristSelections(parameters),
576
- });
577
- return {
578
- ...(submitted ?? { receipt: parameters }),
579
- ticketNumber: catalog.ticketNumber,
580
- sitian: facts,
581
- };
582
- }
583
- // First summons: LLM typed assertion → mechanical verify → bind → commit.
584
- // true-unbound → 无录 (no volume). Verification failure throws (失败诚实).
585
- // Cannot-identify must arrive as status=escalate above — never as silent unbound.
586
- if (assertion.kind === "true-unbound") {
574
+ // and commit under that identity (ADR 0075 已绑定 ticket 优先).
575
+ const ticketNumber = coords.boundTicketNumber !== undefined
576
+ ? coords.boundTicketNumber
577
+ : assertion.kind === "ticket"
578
+ ? assertion.ticketNumber
579
+ : undefined;
580
+ // true-unbound → 无录 (no volume). Cannot-identify must arrive as
581
+ // status=escalate above — never as silent unbound.
582
+ if (ticketNumber === undefined) {
587
583
  if (submitted === undefined || !("sitian" in submitted)) {
588
584
  return { ...(submitted ?? { receipt: parameters }), ticketNumber: null };
589
585
  }
@@ -594,43 +590,22 @@ export function createDiaristRoleRuntime(roleHost, dependencies, getSourcesFlag)
594
590
  delete stripped.sitian;
595
591
  return stripped;
596
592
  }
597
- await verifyAssertedTicketNumber({
598
- ticketNumber: assertion.ticketNumber,
599
- instruction: catalog.instruction,
600
- projectRoot: catalog.projectRoot,
601
- });
602
- await bindTicketNumberOnRunDirectory(catalog.runDirectory, assertion.ticketNumber);
603
- const boundCatalog = {
604
- ...catalog,
605
- ticketNumber: assertion.ticketNumber,
606
- };
607
- const facts = await commitDiaristSelections({
608
- catalog: boundCatalog,
609
- selections: projectDiaristSelections(parameters),
593
+ if (coords.boundTicketNumber === undefined) {
594
+ await bindTicketNumberOnRunDirectory(coords.runDirectory, ticketNumber);
595
+ }
596
+ const facts = await commitDiaristEntries({
597
+ ticketNumber,
598
+ cwd: coords.projectRoot,
599
+ home: coords.home,
600
+ entries: projectDiaristEntries(parameters),
610
601
  });
611
602
  return {
612
603
  ...(submitted ?? { receipt: parameters }),
613
- ticketNumber: assertion.ticketNumber,
604
+ ticketNumber,
614
605
  sitian: facts,
615
606
  };
616
607
  },
617
608
  }, dependencies);
618
- return {
619
- async activate() {
620
- catalog = loadFrozenDiaristCatalog(getSourcesFlag);
621
- return runtime.activate();
622
- },
623
- };
624
- }
625
- /** Envelope-owned decode + one-shot load of the frozen catalog (ADR 0018). */
626
- function loadFrozenDiaristCatalog(getFlag) {
627
- const raw = getFlag();
628
- if (raw === undefined)
629
- return undefined;
630
- if (typeof raw !== "string" || raw.trim() === "") {
631
- throw new Error("Diarist source catalog flag must be a nonempty path");
632
- }
633
- return loadDiaristSourceCatalog(raw);
634
609
  }
635
610
  /** Countersign status words the queue reads (#753). */
636
611
  const COUNTERSIGN_QUEUE_STATUSES = new Set(["converged", "continue", "escalate"]);
@@ -695,9 +670,6 @@ export function createRoleRuntimeExtension(dependencies) {
695
670
  for (const flag of GLEANER_LEFT_TRANSPORT_FLAGS) {
696
671
  roleHost.registerFlag(flag.name, flag.definition);
697
672
  }
698
- for (const flag of DIARIST_TRANSPORT_FLAGS) {
699
- roleHost.registerFlag(flag.name, flag.definition);
700
- }
701
673
  let admitted = false;
702
674
  let selectedRole;
703
675
  /** Live Reviewer parent activation for envelope agent_start prompt assembly. */
@@ -1164,7 +1136,7 @@ export function createRoleRuntimeExtension(dependencies) {
1164
1136
  throw new Error("Diarist runtime dependencies are not configured");
1165
1137
  return dependencies.loadDiaristSoul();
1166
1138
  },
1167
- }, () => envelopeHost.host.getFlag(DIARIST_SOURCES_FLAG.name));
1139
+ });
1168
1140
  let sessionMergerGitState = dependencies.mergerGitState;
1169
1141
  const merger = createMergerRoleRuntime(roleHost, {
1170
1142
  async loadSoul() { if (!dependencies.loadMergerSoul)
@@ -6,11 +6,6 @@
6
6
  export const TICKET_PROVENANCE_KIND = "ticket-provenance";
7
7
  /** Human-read view filename co-located with the JSONL volume. */
8
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
9
  /** Payload discriminator: diagnostic residue (not a diary body entry). */
15
10
  export const TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC = "diagnostic";
16
11
  function isRecord(value) {
@@ -3,10 +3,10 @@
3
3
  * Write/read via sitian facade only; no parallel destination logic.
4
4
  */
5
5
  import { createHash } from "node:crypto";
6
- import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
7
7
  import { join } from "node:path";
8
8
  import { resolveSitianRecordPath, sitianReport, readSitianRecords, } from "./sitian-facade.js";
9
- import { TICKET_PROVENANCE_HUMAN_VIEW, TICKET_PROVENANCE_KIND, TICKET_PROVENANCE_OFFERED_WATERMARK, TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC, projectTicketProvenanceDiagnostic, projectTicketProvenanceEntry, } from "./ticket-provenance-contracts.js";
9
+ import { TICKET_PROVENANCE_HUMAN_VIEW, TICKET_PROVENANCE_KIND, projectTicketProvenanceDiagnostic, projectTicketProvenanceEntry, } from "./ticket-provenance-contracts.js";
10
10
  /** Subject string for ticket-keyed volumes — history follows the ticket. */
11
11
  export function ticketProvenanceSubject(ticketNumber) {
12
12
  if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1) {
@@ -75,82 +75,8 @@ export function resolveTicketProvenanceVolume(ticketNumber, cwd, home) {
75
75
  recordFile: path.recordFile,
76
76
  volumeDir: path.sessionDir,
77
77
  humanViewFile: join(path.sessionDir, TICKET_PROVENANCE_HUMAN_VIEW),
78
- offeredWatermarkFile: join(path.sessionDir, TICKET_PROVENANCE_OFFERED_WATERMARK),
79
78
  };
80
79
  }
81
- /** Honest failure when the offered-identity watermark cannot be read as written. */
82
- export class TicketProvenanceWatermarkError extends Error {
83
- code = "ticket-provenance-watermark";
84
- reason;
85
- constructor(reason, message, options) {
86
- super(message, options);
87
- this.name = "TicketProvenanceWatermarkError";
88
- this.reason = reason;
89
- }
90
- }
91
- /**
92
- * Read identities already offered to the collector (append-only watermark).
93
- * Absent file → empty set. Any non-empty line that is not a JSON object with a
94
- * non-empty string `identity` throws TicketProvenanceWatermarkError (失败诚实:
95
- * never interpret corruption as "unseen" and silently re-offer).
96
- */
97
- export function readOfferedIdentities(ticketNumber, cwd, home) {
98
- const { offeredWatermarkFile } = resolveTicketProvenanceVolume(ticketNumber, cwd, home);
99
- if (!existsSync(offeredWatermarkFile))
100
- return new Set();
101
- let text;
102
- try {
103
- text = readFileSync(offeredWatermarkFile, "utf8");
104
- }
105
- catch (error) {
106
- throw new TicketProvenanceWatermarkError("unreadable", `ticket-provenance offered watermark unreadable (${offeredWatermarkFile})`, { cause: error });
107
- }
108
- const seen = new Set();
109
- const lines = text.split("\n");
110
- for (let index = 0; index < lines.length; index += 1) {
111
- const trimmed = lines[index].trim();
112
- if (!trimmed)
113
- continue;
114
- let parsed;
115
- try {
116
- parsed = JSON.parse(trimmed);
117
- }
118
- catch (error) {
119
- throw new TicketProvenanceWatermarkError("malformed-json", `ticket-provenance offered watermark malformed JSON at line ${index + 1} (${offeredWatermarkFile})`, { cause: error });
120
- }
121
- if (typeof parsed !== "object" ||
122
- parsed === null ||
123
- Array.isArray(parsed) ||
124
- typeof parsed.identity !== "string" ||
125
- parsed.identity.length === 0) {
126
- throw new TicketProvenanceWatermarkError("bad-shape", `ticket-provenance offered watermark bad shape at line ${index + 1} (${offeredWatermarkFile})`);
127
- }
128
- seen.add(parsed.identity);
129
- }
130
- return seen;
131
- }
132
- /**
133
- * Append offered identities after a successful collector pass (selected or not).
134
- * Idempotent per identity within the file (skip already-present). Creates volume
135
- * dir only when writing the first watermark row.
136
- */
137
- export function recordOfferedIdentities(input) {
138
- if (input.identities.length === 0)
139
- return;
140
- const { volumeDir, offeredWatermarkFile } = resolveTicketProvenanceVolume(input.ticketNumber, input.cwd, input.home);
141
- const already = new Set(readOfferedIdentities(input.ticketNumber, input.cwd, input.home));
142
- const rows = [];
143
- for (const identity of input.identities) {
144
- if (identity.length === 0 || already.has(identity))
145
- continue;
146
- rows.push(`${JSON.stringify({ identity })}\n`);
147
- already.add(identity);
148
- }
149
- if (rows.length === 0)
150
- return;
151
- mkdirSync(volumeDir, { recursive: true });
152
- appendFileSync(offeredWatermarkFile, rows.join(""), "utf8");
153
- }
154
80
  /** Read projected diary entries + diagnostics for a ticket (empty when volume absent). */
155
81
  export async function readTicketProvenance(ticketNumber, cwd, home) {
156
82
  const { recordFile } = resolveTicketProvenanceVolume(ticketNumber, cwd, home);
@@ -254,79 +180,6 @@ export function ensureTicketProvenanceVolume(ticketNumber, cwd, home) {
254
180
  appendFileSync(volume.recordFile, "", "utf8");
255
181
  return volume;
256
182
  }
257
- /**
258
- * Append a typed diagnostic onto the ticket-provenance volume (append-only).
259
- * Same sitian kind + subject partition as diary entries; payload uses recordClass
260
- * discriminator — never forged as a source entry. No sidecar / parallel ledger.
261
- */
262
- export function appendTicketProvenanceDiagnostic(input) {
263
- const subject = ticketProvenanceSubject(input.ticketNumber);
264
- // quote-verify: content-stable identity so mid-batch retry does not duplicate.
265
- // collector/issue-source: recordedAt keeps per-court failure history.
266
- const identityParts = input.diagnostic.diagnosticKind === "quote-verify-failed"
267
- ? [
268
- subject,
269
- input.diagnostic.recordClass,
270
- input.diagnostic.diagnosticKind,
271
- input.diagnostic.cause,
272
- input.diagnostic.reason ?? "",
273
- ]
274
- : [
275
- subject,
276
- input.diagnostic.recordClass,
277
- input.diagnostic.diagnosticKind,
278
- input.diagnostic.recordedAt,
279
- input.diagnostic.cause,
280
- input.diagnostic.reason ?? "",
281
- ];
282
- const identity = createHash("sha256")
283
- .update(identityParts.join("\u0000"), "utf8")
284
- .digest("hex");
285
- return sitianReport({
286
- level: "event",
287
- kind: TICKET_PROVENANCE_KIND,
288
- identity,
289
- subject,
290
- cwd: input.cwd,
291
- ...(input.home === undefined ? {} : { home: input.home }),
292
- host: input.host ?? "diarist",
293
- source: input.source ?? "diarist-diagnostic",
294
- payload: input.diagnostic,
295
- });
296
- }
297
- /** Issue-face source acquisition failure — typed diagnostic on the ticket volume. */
298
- export function appendIssueSourceFailureDiagnostic(input) {
299
- const recordedAt = input.recordedAt ?? new Date().toISOString();
300
- return appendTicketProvenanceDiagnostic({
301
- ticketNumber: input.ticketNumber,
302
- cwd: input.cwd,
303
- ...(input.home === undefined ? {} : { home: input.home }),
304
- source: "diarist-issue-source",
305
- diagnostic: {
306
- recordClass: TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC,
307
- diagnosticKind: "issue-source-failed",
308
- cause: input.cause,
309
- recordedAt,
310
- reason: input.reason,
311
- },
312
- });
313
- }
314
- /** Quote reverse-verify failure — single diagnostic expression (not a diary entry). */
315
- export function appendQuoteVerifyFailureDiagnostic(input) {
316
- const recordedAt = input.recordedAt ?? new Date().toISOString();
317
- return appendTicketProvenanceDiagnostic({
318
- ticketNumber: input.ticketNumber,
319
- cwd: input.cwd,
320
- ...(input.home === undefined ? {} : { home: input.home }),
321
- source: "diarist-quote-verify",
322
- diagnostic: {
323
- recordClass: TICKET_PROVENANCE_RECORD_CLASS_DIAGNOSTIC,
324
- diagnosticKind: "quote-verify-failed",
325
- cause: input.cause,
326
- recordedAt,
327
- },
328
- });
329
- }
330
183
  /** Write the co-located human view next to the JSONL volume (derived, not dual-source).
331
184
  * Ensures volume dir exists so empty courts still get the md face. */
332
185
  export function writeTicketProvenanceHumanView(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akagilnc/pi-workflow-roles",
3
- "version": "0.1.3708",
3
+ "version": "0.1.3712",
4
4
  "description": "Soul-bound workflow roles for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,10 +1,20 @@
1
1
  # 起居郎收集方法(owner-domain)
2
2
 
3
- 本文件是起居郎语义收集的方法真源(ADR 0075 `diarist-llm-collector` / `transcribe-whole-blocks`)。
3
+ 本文件是起居郎语义收集的方法真源(ADR 0075 `diarist-llm-collector` / `transcribe-whole-blocks`;#779 调用方无感)。
4
4
 
5
5
  ## 任务
6
6
 
7
- 从本票已冻结的来源块文件中,选出**与本票决策相关**的整块。文件路径见本轮材料中的路径指针,自行读取;不要等别人把正文贴进对话。
7
+ 为本案找出决策相关材料,以**整块**交卷入本票起居录。材料由你自己去找——不要等调用方贴路径、附件或预切清单。
8
+
9
+ ## 如何找材料
10
+
11
+ 你是 LLM,自己读:
12
+
13
+ - 本仓 Claude 会话卷:`~/.claude/projects/` 下与本仓相关的目录(路径里的 `_` 在目录名里常写作 `-`;主 clone 与 worktree 可能共用或分列,自行辨认)
14
+ - 本票票面与评论:用 `gh` 或等价方式读 GitHub issue
15
+ - 票面引用的 ADR 原件:仓内 `docs/adr/`
16
+
17
+ 调用方只给传召文与 `--project`;不附清单、不写候选路径。找不到就如实说明,不编造。
8
18
 
9
19
  ## 何谓决策相关
10
20
 
@@ -16,20 +26,20 @@
16
26
  - 陛下(owner)原话、确认、否决、收口条件
17
27
  - 为上述裁决提供必要上下文的紧邻块
18
28
 
19
- 无关闲聊、纯工具噪声、与本票决策无涉的他票材料不选。
29
+ 无关闲聊、纯工具噪声、与本票决策无涉的他票材料不选。相关性由你裁决。
20
30
 
21
31
  ## 誊录整块
22
32
 
23
- - 入选即整块保留:指向文件中的完整 transcript,不指针化、不摘要替代正文。
24
- - 引语必须是该块 transcript 内的**连续原文子串**。拼接、改写、跨段拼贴会在机械反验中失败并拒入录。
25
- - 宁多勿漏:拿不准是否决策相关时入选,由下游反验与人读面承接。
33
+ - 入选即整块保留:交卷对象里写完整 `transcript`,并带上可回指的 `sourceRef`(会话文件+entry、issue URL、ADR path 等)。
34
+ - 不指针化、不摘要替代正文。
35
+ - 宁多勿漏:拿不准是否决策相关时入选。
26
36
 
27
37
  ## 本票身份
28
38
 
29
- 由你(本席 LLM)判断当前要办的是哪张票,在交卷对象上以 typed 字段交出——代码不做认票裁定(ADR 0075 `diarist-resolves-ticket-llm-layer`;owner:代码不准做判断)。机械层只验你断言的号逐字出现于指令且票真实存在;后续衙门沿用 typed 键,不再另找。
39
+ 由你(本席 LLM)判断当前要办的是哪张票,在交卷对象上以 typed 字段交出——代码不做认票裁定(ADR 0075 `diarist-resolves-ticket-llm-layer`;owner:代码不准做判断/机械层不判断 LLM 输出)。
30
40
 
31
- - 派单已给明确票号(含 issue 链接或等价指称)时:`status: completed` 且 `ticketNumber: N`。
32
- - 派单是真无票对象(方案/派单/处置案等):`status: completed` 且 `ticketNumber: null`(或省略)。真无票是合法结果,不入录。
33
- - 派单看起来有票、但你认不出本庭对象是哪一张:`status: escalate` 并写明 `reason`。识别不了就上抛,不得省略票号假装无录,也不得猜一个号。
34
- - 不得从候选材料、旁及的他票或任何猜测里挑一个号顶替派单未给出的身份。
41
+ - 认得出本庭对象:`status: completed` 且 `ticketNumber: N`,并交 `entries`(可空)。
42
+ - 真无票对象(方案/派单/处置案等):`status: completed` 且 `ticketNumber: null`(或省略)。真无票是合法结果,不入录。
43
+ - 认不出本庭对象是哪一张:`status: escalate` 并写明 `reason`。识别不了就上抛,不得省略票号假装无录,也不得猜一个号。
44
+ - 不得从旁及的他票或任何猜测里挑一个号顶替认不出的身份。
35
45
  - 传召文自然会提轮次、决定编号、commit sha、邻票号;你认的是本庭对象,不是文中出现的每一个数字。
@@ -79,7 +79,7 @@ export function projectAcpActivationFlags(request: RoleTurnRequest): Map<string,
79
79
  : "casePath" in activation ? activation.casePath
80
80
  : "inputPath" in activation ? activation.inputPath
81
81
  : "sourceRun" in activation ? activation.sourceRun
82
- : "sourcesPath" in activation ? activation.sourcesPath : undefined;
82
+ : undefined;
83
83
  if (path !== undefined) flags.set(inputFlag, path);
84
84
  }
85
85
  if (activation.role === "fixer" && activation.prerequisitesPath !== undefined) flags.set("ak-fixer-prerequisites", activation.prerequisitesPath);
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Shared docs/adr path extraction from free text (issue body / ticket face).
3
- * Single authority for reviewer Spec discovery and diarist source enumeration.
3
+ * Single authority for reviewer Spec discovery.
4
4
  *
5
5
  * Shape only: segments under docs/adr/ without `..` or empty parts. Real IO
6
6
  * confinement still lives at the read seam (ADR 0038).
@@ -1,29 +1,31 @@
1
1
  /**
2
2
  * Public 起居郎 (diarist) terminating receipt contracts — ADR 0075 `diarist-is-role`.
3
- * Lawful explicit releases: completed (入录选择) | escalate (认不出本庭对象上抛).
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
+ *
7
+ * #779: no frozen candidate catalog; LLM finds sources and submits whole blocks.
8
+ * Mechanical layer does not judge LLM output (no quote/ticket/relevance verify).
6
9
  */
7
10
 
11
+ import type {
12
+ TicketProvenanceSourceKind,
13
+ TicketProvenanceSourceRef,
14
+ } from "./ticket-provenance-contracts.ts";
15
+
8
16
  export const DIARIST_OUTPUT_TOOL_NAME = "ak_diarist_output";
9
17
  export const DIARIST_ACCEPTED_TEXT = "起居郎回执已接受";
10
18
 
11
- /** Internal transport: frozen candidate-catalog path for this turn. */
12
- export const DIARIST_SOURCES_FLAG = {
13
- name: "ak-diarist-sources",
14
- definition: {
15
- description:
16
- "Frozen source catalog the diarist selects from (may be unbound until LLM asserts ticketNumber)",
17
- type: "string" as const,
18
- },
19
- } as const;
20
-
21
- /** One selected block reference into the frozen candidate catalog. */
22
- export type DiaristSelection = {
23
- /** Index into the catalog the diarist received. */
24
- readonly candidateIndex: number;
25
- /** Quotes claimed verbatim from that block (reverse-verified mechanically). */
26
- readonly quotes: readonly string[];
19
+ /**
20
+ * One whole block the diarist chose to enter (ADR 0075 `transcribe-whole-blocks`).
21
+ * LLM supplies the block bytes + source pointer; mechanical layer appends as-is.
22
+ */
23
+ export type DiaristEntrySubmission = {
24
+ readonly sourceKind: TicketProvenanceSourceKind;
25
+ readonly sourceRef: TicketProvenanceSourceRef;
26
+ /** Whole-block transcript — not a pointer-only stand-in. */
27
+ readonly transcript: string;
28
+ readonly timestamp: string;
27
29
  /** Human-facing note (relation to this case). Not a machine gate. */
28
30
  readonly note?: string;
29
31
  };
@@ -31,13 +33,14 @@ export type DiaristSelection = {
31
33
  export type DiaristOutput =
32
34
  | {
33
35
  readonly status: "completed";
34
- readonly selections: readonly DiaristSelection[];
35
36
  /**
36
37
  * Typed court-target assertion (ADR 0075 `diarist-resolves-ticket-llm-layer`).
37
38
  * Positive integer = 本庭对象=票N; null/absent = true-unbound (真无票→无录).
38
- * Mechanical layer verifies N; verification failure is failure, never 无录.
39
+ * LLM owns recognition; mechanical layer does not re-judge the number.
39
40
  */
40
41
  readonly ticketNumber?: number | null;
42
+ /** Whole blocks to append under the asserted ticket. Empty list is lawful. */
43
+ readonly entries?: readonly DiaristEntrySubmission[];
41
44
  }
42
45
  | {
43
46
  /** LLM cannot tell which ticket this summons is about — escalate, never wash into 无录. */
@@ -62,25 +65,38 @@ export function validateRecordedDiaristOutput(value: unknown): DiaristOutput {
62
65
  }
63
66
 
64
67
  /**
65
- * Lenient projection of submitted selections (第 0 条: shape is not an admission
66
- * gate). Rows that carry no usable candidateIndex are dropped from the commit,
67
- * never bounced back at the role.
68
+ * Lenient projection of submitted entries (第 0 条: shape is not an admission
69
+ * gate). Rows that cannot form a lawful volume entry are dropped from the
70
+ * commit, never bounced back at the role. This is write-seam self-check only —
71
+ * not quote/ticket/relevance judgment of LLM content (#779).
68
72
  */
69
- export function projectDiaristSelections(value: unknown): DiaristSelection[] {
70
- const rows = (value as { selections?: unknown } | null)?.selections;
73
+ export function projectDiaristEntries(value: unknown): DiaristEntrySubmission[] {
74
+ const rows = (value as { entries?: unknown } | null)?.entries;
71
75
  if (!Array.isArray(rows)) return [];
72
- const out: DiaristSelection[] = [];
76
+ const out: DiaristEntrySubmission[] = [];
73
77
  for (const row of rows) {
74
78
  if (row === null || typeof row !== "object" || Array.isArray(row)) continue;
75
79
  const record = row as Record<string, unknown>;
76
- const index = record.candidateIndex;
77
- if (typeof index !== "number" || !Number.isInteger(index) || index < 0) continue;
78
- const quotes = Array.isArray(record.quotes)
79
- ? record.quotes.filter((q): q is string => typeof q === "string" && q.length > 0)
80
- : [];
80
+ if (typeof record.sourceKind !== "string") continue;
81
+ if (typeof record.transcript !== "string" || record.transcript.length === 0) continue;
82
+ if (typeof record.timestamp !== "string" || record.timestamp.length === 0) continue;
83
+ if (record.sourceRef === null || typeof record.sourceRef !== "object" || Array.isArray(record.sourceRef)) {
84
+ continue;
85
+ }
86
+ const ref = record.sourceRef as Record<string, unknown>;
87
+ const sourceRef: TicketProvenanceSourceRef = {
88
+ ...(typeof ref.sessionFile === "string" ? { sessionFile: ref.sessionFile } : {}),
89
+ ...(typeof ref.entryId === "string" || typeof ref.entryId === "number"
90
+ ? { entryId: ref.entryId }
91
+ : {}),
92
+ ...(typeof ref.path === "string" ? { path: ref.path } : {}),
93
+ ...(typeof ref.url === "string" ? { url: ref.url } : {}),
94
+ };
81
95
  out.push({
82
- candidateIndex: index,
83
- quotes,
96
+ sourceKind: record.sourceKind as TicketProvenanceSourceKind,
97
+ sourceRef,
98
+ transcript: record.transcript,
99
+ timestamp: record.timestamp,
84
100
  ...(typeof record.note === "string" ? { note: record.note } : {}),
85
101
  });
86
102
  }