@akagilnc/pi-workflow-roles 0.1.3706 → 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,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,19 +1,14 @@
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
  export const DIARIST_OUTPUT_TOOL_NAME = "ak_diarist_output";
8
11
  export const DIARIST_ACCEPTED_TEXT = "起居郎回执已接受";
9
- /** Internal transport: frozen candidate-catalog path for this turn. */
10
- export const DIARIST_SOURCES_FLAG = {
11
- name: "ak-diarist-sources",
12
- definition: {
13
- description: "Frozen source catalog the diarist selects from (may be unbound until LLM asserts ticketNumber)",
14
- type: "string",
15
- },
16
- };
17
12
  export function validateRecordedDiaristOutput(value) {
18
13
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
19
14
  throw new Error("Diarist output has no execution discriminator");
@@ -31,12 +26,13 @@ export function validateRecordedDiaristOutput(value) {
31
26
  throw new Error("Diarist output has no execution discriminator");
32
27
  }
33
28
  /**
34
- * Lenient projection of submitted selections (第 0 条: shape is not an admission
35
- * gate). Rows that carry no usable candidateIndex are dropped from the commit,
36
- * never bounced back at the role.
29
+ * Lenient projection of submitted entries (第 0 条: shape is not an admission
30
+ * gate). Rows that cannot form a lawful volume entry are dropped from the
31
+ * commit, never bounced back at the role. This is write-seam self-check only —
32
+ * not quote/ticket/relevance judgment of LLM content (#779).
37
33
  */
38
- export function projectDiaristSelections(value) {
39
- const rows = value?.selections;
34
+ export function projectDiaristEntries(value) {
35
+ const rows = value?.entries;
40
36
  if (!Array.isArray(rows))
41
37
  return [];
42
38
  const out = [];
@@ -44,15 +40,29 @@ export function projectDiaristSelections(value) {
44
40
  if (row === null || typeof row !== "object" || Array.isArray(row))
45
41
  continue;
46
42
  const record = row;
47
- const index = record.candidateIndex;
48
- if (typeof index !== "number" || !Number.isInteger(index) || index < 0)
43
+ if (typeof record.sourceKind !== "string")
49
44
  continue;
50
- const quotes = Array.isArray(record.quotes)
51
- ? record.quotes.filter((q) => typeof q === "string" && q.length > 0)
52
- : [];
45
+ if (typeof record.transcript !== "string" || record.transcript.length === 0)
46
+ continue;
47
+ if (typeof record.timestamp !== "string" || record.timestamp.length === 0)
48
+ continue;
49
+ if (record.sourceRef === null || typeof record.sourceRef !== "object" || Array.isArray(record.sourceRef)) {
50
+ continue;
51
+ }
52
+ const ref = record.sourceRef;
53
+ const sourceRef = {
54
+ ...(typeof ref.sessionFile === "string" ? { sessionFile: ref.sessionFile } : {}),
55
+ ...(typeof ref.entryId === "string" || typeof ref.entryId === "number"
56
+ ? { entryId: ref.entryId }
57
+ : {}),
58
+ ...(typeof ref.path === "string" ? { path: ref.path } : {}),
59
+ ...(typeof ref.url === "string" ? { url: ref.url } : {}),
60
+ };
53
61
  out.push({
54
- candidateIndex: index,
55
- quotes,
62
+ sourceKind: record.sourceKind,
63
+ sourceRef,
64
+ transcript: record.transcript,
65
+ timestamp: record.timestamp,
56
66
  ...(typeof record.note === "string" ? { note: record.note } : {}),
57
67
  });
58
68
  }
@@ -10,18 +10,27 @@ export const diaristOutputSchema = withInfrastructureFailureDeclaration(openTool
10
10
  description: "completed | escalate — 形状指引,非 schema 闸",
11
11
  }),
12
12
  ticketNumber: Type.Optional(Type.Unknown({
13
- description: "本庭对象票号(正整数)或 null/省略=真无票;机械验真后下游走 typed 键;认不出用 status=escalate,不洗成无录",
13
+ description: "本庭对象票号(正整数)或 null/省略=真无票;下游走 typed 键;认不出用 status=escalate,不洗成无录。机械层不重判。",
14
14
  })),
15
15
  reason: Type.Optional(Type.String({
16
16
  description: "status 为 escalate 时:认不出本庭对象的原因",
17
17
  })),
18
- selections: Type.Array(Type.Object({
19
- candidateIndex: Type.Number({ description: "候选目录中的整块序号" }),
20
- quotes: Type.Array(Type.String(), {
21
- description: "该块 transcript 内的连续原文;机械反验",
18
+ entries: Type.Optional(Type.Array(Type.Object({
19
+ sourceKind: Type.String({
20
+ description: "来源族:cc-session | issue-body-comment | adr-decision-key | ticket-decree-block",
22
21
  }),
22
+ sourceRef: Type.Object({
23
+ sessionFile: Type.Optional(Type.String()),
24
+ entryId: Type.Optional(Type.Unknown()),
25
+ path: Type.Optional(Type.String()),
26
+ url: Type.Optional(Type.String()),
27
+ }, { additionalProperties: true, description: "不可变源指针" }),
28
+ transcript: Type.String({
29
+ description: "整块原文(誊录整块,不指针化)",
30
+ }),
31
+ timestamp: Type.String({ description: "源时间戳 ISO" }),
23
32
  note: Type.Optional(Type.String({ description: "该材料与本案的关系(人读)" })),
24
- }, { additionalProperties: true, description: "一条入录选择" }), { description: "入录选择;空列表合法完局;escalate 时可不交" }),
33
+ }, { additionalProperties: true, description: "一条入录整块" }), { description: "入录整块;空列表合法完局;escalate 时可不交" })),
25
34
  })));
26
35
  /**
27
36
  * 决定工具规格。生命周期装配归注册信封 owner——src/role-runtime.ts(ADR 0018)。
@@ -29,7 +38,7 @@ export const diaristOutputSchema = withInfrastructureFailureDeclaration(openTool
29
38
  export const DIARIST_TOOL_SPEC = {
30
39
  name: DIARIST_OUTPUT_TOOL_NAME,
31
40
  label: "起居郎输出",
32
- description: "起居郎入录选择与本票身份断言;认不出本庭对象则 escalate。",
33
- promptSnippet: "起居郎入录选择与本票身份",
41
+ description: "起居郎入录整块与本票身份断言;认不出本庭对象则 escalate。",
42
+ promptSnippet: "起居郎入录整块与本票身份",
34
43
  parameters: diaristOutputSchema,
35
44
  };
package/dist/diarist.js CHANGED
@@ -1,415 +1,66 @@
1
1
  /**
2
- * 起居郎 mechanical halves — ADR 0075 `diarist-is-role` / `diarist-collector-is-own-turn`.
3
- * Semantic collection is the diarist role's own LLM turn; this module keeps the
4
- * mechanical safeguard band only: source enumeration into a frozen catalog, and
5
- * the verbatim reverse-verify → idempotent sitian append → watermark commit.
2
+ * 起居郎 volume commit — ADR 0075 `diarist-is-role` / #779.
3
+ * Semantic collection is the diarist role's own LLM turn (finds materials itself).
4
+ * This module only appends submitted whole blocks idempotently and refreshes the
5
+ * human view. No frozen catalog, no quote reverse-verify, no ticket-number
6
+ * re-judgment of LLM output (#779 owner: 机械层凭什么能判断llm的输出).
6
7
  * No lifecycle here (ADR 0018): the seat prepares, the role envelope commits.
7
8
  */
8
- import { readFileSync } from "node:fs";
9
- import { extractReferencedAdrPaths } from "./adr-path-refs.js";
10
- import { createGhApiRunner, createGhCollectorGitHubTransport, projectGhIssueBody, } from "./collector-github.js";
11
- import { blockToLlmEntry, buildDiaristAnchors, mechanicalSafeguardPipeline, readAdrDecisionKeyBlocks, readCcSessionBlocks, readIssueFaceBlocks, } from "./diarist-mechanical.js";
12
- import { parseGitHubOriginRemote } from "./reviewer-pinned-git.js";
13
- import { appendIssueSourceFailureDiagnostic, appendQuoteVerifyFailureDiagnostic, appendTicketProvenanceEntry, ensureTicketProvenanceVolume, readOfferedIdentities, readTicketProvenance, recordOfferedIdentities, resolveTicketProvenanceVolume, ticketProvenanceEntryIdentity, writeTicketProvenanceHumanView, } from "./ticket-provenance.js";
14
- import { readSitianRecords } from "./sitian-facade.js";
15
- import { execFileSync } from "node:child_process";
9
+ import { appendTicketProvenanceEntry, ensureTicketProvenanceVolume, readTicketProvenance, writeTicketProvenanceHumanView, } from "./ticket-provenance.js";
10
+ import { projectTicketProvenanceEntry } from "./ticket-provenance-contracts.js";
16
11
  /**
17
- * Honest failure of mechanical ticket verification (ADR 0075).
18
- * Must settle as failure — never wash into true-unbound / 无录.
12
+ * Append LLM-submitted whole blocks under a bound ticket.
13
+ * Entry identity keeps the volume idempotent across re-summons.
14
+ * Shape projection is write-seam self-check only — not content judgment (#779).
19
15
  */
20
- export class DiaristTicketVerificationError extends Error {
21
- code = "diarist-ticket-verification";
22
- reason;
23
- constructor(reason, message, options) {
24
- super(message, options);
25
- this.name = "DiaristTicketVerificationError";
26
- this.reason = reason;
27
- }
28
- }
29
- /**
30
- * Decision-key exact identity: complete decimal number N appears in instruction.
31
- * A longer number's digit substring (e.g. 82 inside 582) is not N.
32
- * Verifies a typed claim — does not harvest candidates from prose.
33
- */
34
- export function instructionContainsTicketNumber(instruction, ticketNumber) {
35
- if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1)
36
- return false;
37
- // Complete decimal token: not preceded or followed by another digit.
38
- // Plain string (not template) so the digit-class backslash survives emit.
39
- return new RegExp("(?<!\\d)" + String(ticketNumber) + "(?!\\d)").test(instruction);
40
- }
41
- /**
42
- * Production live-ticket check over shared gh issue-body projection.
43
- * Available issue face → exists; unavailable/invalid → missing.
44
- */
45
- export function createGhTicketExistenceChecker(options) {
46
- const runner = options?.runner ?? createGhApiRunner();
47
- return async (input) => {
48
- const projected = await projectGhIssueBody(runner, {
49
- owner: input.owner,
50
- repo: input.repo,
51
- ticketNumber: input.ticketNumber,
52
- ...(input.signal === undefined ? {} : { signal: input.signal }),
53
- });
54
- return projected.status === "available";
55
- };
56
- }
57
- /**
58
- * Mechanical verify of an LLM typed ticket assertion (ADR 0075).
59
- * Complete decimal of N must appear in the summons; ticket must exist live.
60
- * Throws DiaristTicketVerificationError — caller must not wash into 无录.
61
- */
62
- export async function verifyAssertedTicketNumber(input) {
63
- const n = input.ticketNumber;
64
- if (!Number.isSafeInteger(n) || n < 1) {
65
- throw new DiaristTicketVerificationError("assertion-uninterpretable", `diarist ticket assertion must be a safe integer >= 1, got ${String(n)}`);
66
- }
67
- if (!instructionContainsTicketNumber(input.instruction, n)) {
68
- throw new DiaristTicketVerificationError("number-not-in-instruction", `diarist ticket assertion #${n} complete decimal number does not appear in accepted instruction`);
69
- }
70
- const origin = resolveDiaristGithubOrigin(input.projectRoot);
71
- if (origin === undefined) {
72
- throw new DiaristTicketVerificationError("origin-unresolved", `diarist ticket assertion #${n} requires a resolvable github.com origin remote for live verification`);
73
- }
74
- const checkExistence = input.checkExistence ?? createGhTicketExistenceChecker();
75
- let exists;
76
- try {
77
- exists = await checkExistence({
78
- owner: origin.owner,
79
- repo: origin.repo,
80
- ticketNumber: n,
81
- ...(input.signal === undefined ? {} : { signal: input.signal }),
82
- });
83
- }
84
- catch (error) {
85
- throw new DiaristTicketVerificationError("ticket-missing", `diarist ticket assertion #${n} live verification failed`, { cause: error });
86
- }
87
- if (!exists) {
88
- throw new DiaristTicketVerificationError("ticket-missing", `diarist ticket assertion #${n} does not exist as a live issue on ${origin.owner}/${origin.repo}`);
89
- }
90
- }
91
- /** Bound-ticket issue source failure — not a soft degrade to empty face. */
92
- export class DiaristIssueSourceError extends Error {
93
- code = "diarist-issue-source";
94
- reason;
95
- constructor(reason, message, options) {
96
- super(message, options);
97
- this.name = "DiaristIssueSourceError";
98
- this.reason = reason;
99
- }
100
- }
101
- /**
102
- * Production issue-face capability over shared gh execution seams.
103
- * Body fetch/parse is sole-owned by projectGhIssueBody; this maps hard disposition
104
- * (typed DiaristIssueSourceError) instead of Reviewer Spec soft-undefined.
105
- * Comment list failures after body success keep true cause under comments-failed.
106
- */
107
- export function createDiaristIssueFaceFetcher(options) {
108
- const runner = options?.runner ?? createGhApiRunner();
109
- const transport = options?.transport ?? createGhCollectorGitHubTransport();
110
- return async (input) => {
111
- const projected = await projectGhIssueBody(runner, input);
112
- let body;
113
- if (projected.status === "available") {
114
- body = projected.body;
115
- }
116
- else if (projected.status === "unavailable") {
117
- if (projected.reason === "pull-request") {
118
- throw new DiaristIssueSourceError("issue-is-pull-request", `ticket #${input.ticketNumber} resolves to a pull request, not an issue face`, projected.cause === undefined ? undefined : { cause: projected.cause });
119
- }
120
- throw new DiaristIssueSourceError("issue-unavailable", `issue face unavailable for ${input.owner}/${input.repo}#${input.ticketNumber}`, projected.cause === undefined ? undefined : { cause: projected.cause });
121
- }
122
- else if (projected.reason === "not-json") {
123
- throw new DiaristIssueSourceError("issue-not-json", `issue face payload is not JSON for ${input.owner}/${input.repo}#${input.ticketNumber}`, projected.cause === undefined ? undefined : { cause: projected.cause });
124
- }
125
- else if (projected.reason === "not-object") {
126
- throw new DiaristIssueSourceError("issue-not-object", `issue face payload must be a JSON object for ${input.owner}/${input.repo}#${input.ticketNumber}`);
127
- }
128
- else {
129
- throw new DiaristIssueSourceError("issue-body-invalid", `issue face body must be string or null for ${input.owner}/${input.repo}#${input.ticketNumber}`);
130
- }
131
- let listed;
132
- try {
133
- listed = await transport.listIssueComments({
134
- owner: input.owner,
135
- repo: input.repo,
136
- prNumber: input.ticketNumber,
137
- ...(input.signal === undefined ? {} : { signal: input.signal }),
138
- });
139
- }
140
- catch (error) {
141
- throw new DiaristIssueSourceError("comments-failed", `issue comments fetch failed for ${input.owner}/${input.repo}#${input.ticketNumber}`, { cause: error });
142
- }
143
- const bodyUrl = `https://github.com/${input.owner}/${input.repo}/issues/${input.ticketNumber}`;
144
- return {
145
- body,
146
- bodyUrl,
147
- comments: listed.items.map((c) => ({
148
- id: c.id,
149
- body: c.body,
150
- createdAt: c.createdAt,
151
- htmlUrl: c.htmlUrl,
152
- })),
153
- };
154
- };
155
- }
156
- /** github.com owner/repo from project origin remote; undefined when absent/non-github. */
157
- export function resolveDiaristGithubOrigin(projectRoot) {
158
- let remoteUrl;
159
- try {
160
- remoteUrl = execFileSync("git", ["remote", "get-url", "origin"], {
161
- cwd: projectRoot,
162
- encoding: "utf8",
163
- stdio: ["ignore", "pipe", "pipe"],
164
- }).trim();
165
- }
166
- catch {
167
- return undefined;
168
- }
169
- if (remoteUrl.length === 0)
170
- return undefined;
171
- return parseGitHubOriginRemote(remoteUrl);
172
- }
173
- /**
174
- * Acquire the issue face for a bound ticket. Failures are typed and durable on
175
- * the ticket-provenance volume, then propagated — never washed into empty face.
176
- */
177
- export async function loadDiaristIssueFace(input) {
178
- const persistAndThrow = (error) => {
179
- appendIssueSourceFailureDiagnostic({
180
- ticketNumber: input.ticketNumber,
181
- cwd: input.projectRoot,
182
- ...(input.home === undefined ? {} : { home: input.home }),
183
- cause: error.message,
184
- reason: error.reason,
185
- });
186
- return error;
187
- };
188
- const origin = resolveDiaristGithubOrigin(input.projectRoot);
189
- if (origin === undefined) {
190
- throw persistAndThrow(new DiaristIssueSourceError("origin-unresolved", `bound ticket #${input.ticketNumber} issue face requires a resolvable github.com origin remote`));
191
- }
192
- const fetcher = input.fetcher ?? createDiaristIssueFaceFetcher();
193
- try {
194
- return await fetcher({
195
- owner: origin.owner,
196
- repo: origin.repo,
197
- ticketNumber: input.ticketNumber,
198
- });
199
- }
200
- catch (error) {
201
- throw persistAndThrow(error instanceof DiaristIssueSourceError
202
- ? error
203
- : new DiaristIssueSourceError("issue-unavailable", `issue face fetch failed for ${origin.owner}/${origin.repo}#${input.ticketNumber}`, { cause: error }));
204
- }
205
- }
206
- /**
207
- * Identities already processed for this ticket:
208
- * - volume record identities (selected / verify-fail residue)
209
- * - offered watermark (blocks shown to the diarist, selected or not)
210
- */
211
- async function loadSeenEntryIdentities(ticketNumber, cwd, home) {
212
- const { recordFile } = resolveTicketProvenanceVolume(ticketNumber, cwd, home);
213
- const { records } = await readSitianRecords(recordFile);
214
- const seen = new Set();
215
- for (const record of records) {
216
- if (typeof record.identity === "string" && record.identity.length > 0) {
217
- seen.add(record.identity);
218
- }
219
- }
220
- for (const identity of readOfferedIdentities(ticketNumber, cwd, home)) {
221
- seen.add(identity);
222
- }
223
- return seen;
224
- }
225
- function blockEntryIdentity(ticketNumber, block) {
226
- return ticketProvenanceEntryIdentity({
227
- ticketNumber,
228
- sourceKind: block.sourceKind,
229
- sourceRef: block.sourceRef,
230
- transcript: block.transcript,
231
- });
232
- }
233
- /**
234
- * Single typed candidate stream: cc sessions + GitHub issue face/comments/decree + referenced ADRs.
235
- * Mechanical layer does not prose-filter for relevance (锚定宪法).
236
- * Attachments are never merged in as fake issue-body-comment.
237
- */
238
- function loadSourceBlocks(input) {
239
- const cwds = input.sessionCwds ?? [input.cwd];
240
- const blocks = [...readCcSessionBlocks({ cwds })];
241
- if (input.issueFace !== undefined) {
242
- blocks.push(...readIssueFaceBlocks({ face: input.issueFace }));
243
- const faceText = [
244
- input.issueFace.body,
245
- ...input.issueFace.comments.map((c) => c.body),
246
- ].join("\n");
247
- const adrPaths = extractReferencedAdrPaths(faceText);
248
- if (adrPaths.length > 0) {
249
- blocks.push(...readAdrDecisionKeyBlocks({
250
- cwd: input.cwd,
251
- adrPaths,
252
- }));
253
- }
254
- }
255
- return blocks;
256
- }
257
- /**
258
- * Mechanical half A — source enumeration into a frozen catalog.
259
- * When ticketNumber is known: establishes the per-ticket volume + human view
260
- * (ADR 0075 `ticket-provenance-file` 每票一份起居录) and offers only blocks not
261
- * yet on the volume / offered watermark (增量幂等).
262
- * When ticketNumber is absent (first summons): offers session candidates without
263
- * minting a volume — the LLM turn asserts identity; accept verifies and mints.
264
- */
265
- export async function prepareDiaristSourceCatalog(input) {
266
- const known = input.ticketNumber;
267
- if (known !== undefined) {
268
- ensureTicketProvenanceVolume(known, input.cwd, input.home);
269
- const volume = await readTicketProvenance(known, input.cwd, input.home);
270
- writeTicketProvenanceHumanView({
271
- ticketNumber: known,
272
- cwd: input.cwd,
273
- ...(input.home === undefined ? {} : { home: input.home }),
274
- entries: volume.entries,
275
- });
276
- }
277
- const rawBlocks = loadSourceBlocks(input);
278
- // Safeguard only (notify filter + dedupe) — never prose-based exclusion.
279
- const safeguarded = mechanicalSafeguardPipeline(rawBlocks);
280
- const seen = known === undefined
281
- ? new Set()
282
- : await loadSeenEntryIdentities(known, input.cwd, input.home);
283
- const fresh = safeguarded.filter((block) => known === undefined || !seen.has(blockEntryIdentity(known, block)));
284
- return {
285
- ...(known === undefined ? {} : { ticketNumber: known }),
286
- instruction: input.instruction,
287
- runDirectory: input.runDirectory,
288
- projectRoot: input.projectRoot,
289
- cwd: input.cwd,
290
- ...(input.home === undefined ? {} : { home: input.home }),
291
- candidates: fresh.map((block, candidateIndex) => ({ ...block, candidateIndex })),
292
- };
293
- }
294
- export function serializeDiaristSourceCatalog(catalog) {
295
- return JSON.stringify(catalog);
296
- }
297
- /** Read a frozen catalog written by the seat. Unreadable/malformed fails loudly. */
298
- export function loadDiaristSourceCatalog(path) {
299
- const parsed = JSON.parse(readFileSync(path, "utf8"));
300
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
301
- throw new Error(`diarist source catalog is not an object (${path})`);
302
- }
303
- const record = parsed;
304
- if (typeof record.cwd !== "string") {
305
- throw new Error(`diarist source catalog is missing cwd (${path})`);
306
- }
307
- if (typeof record.instruction !== "string") {
308
- throw new Error(`diarist source catalog is missing instruction (${path})`);
309
- }
310
- if (typeof record.runDirectory !== "string") {
311
- throw new Error(`diarist source catalog is missing runDirectory (${path})`);
312
- }
313
- if (typeof record.projectRoot !== "string") {
314
- throw new Error(`diarist source catalog is missing projectRoot (${path})`);
315
- }
316
- if (record.ticketNumber !== undefined &&
317
- (typeof record.ticketNumber !== "number" ||
318
- !Number.isSafeInteger(record.ticketNumber) ||
319
- record.ticketNumber < 1)) {
320
- throw new Error(`diarist source catalog ticketNumber is not a safe ticket (${path})`);
321
- }
322
- if (!Array.isArray(record.candidates)) {
323
- throw new Error(`diarist source catalog is missing candidates (${path})`);
324
- }
325
- return parsed;
326
- }
327
- /**
328
- * Mechanical half B — commit the diarist turn's selections.
329
- * Verbatim reverse-verify → idempotent sitian append → watermark → human view.
330
- * Verify failure records a single typed diagnostic and drops that selection; it
331
- * never bounces the receipt (第 0 条) and never enters the volume as an entry.
332
- */
333
- export async function commitDiaristSelections(input) {
334
- const ticketNumber = input.catalog.ticketNumber;
335
- if (ticketNumber === undefined) {
336
- throw new Error("commitDiaristSelections requires a bound ticketNumber on the catalog");
337
- }
338
- const { cwd } = input.catalog;
339
- const homeOpt = input.catalog.home === undefined ? {} : { home: input.catalog.home };
340
- const volumePaths = ensureTicketProvenanceVolume(ticketNumber, cwd, input.catalog.home);
341
- const anchors = buildDiaristAnchors({ ticketNumber });
342
- // Entry-count baseline: sitian entry identity already makes a repeat of an
343
- // already-recorded block a no-op append, so the honest `appended` is what the
344
- // volume gained — not how many rows survived verify.
345
- const before = await readTicketProvenance(ticketNumber, cwd, input.catalog.home);
346
- let acceptedRows = 0;
347
- let rejectedQuotes = 0;
348
- let unknownCandidate = false;
349
- for (const selection of input.selections) {
350
- const block = input.catalog.candidates[selection.candidateIndex];
351
- if (block === undefined) {
352
- unknownCandidate = true;
353
- continue;
354
- }
355
- const projected = blockToLlmEntry(block, {
356
- anchors,
357
- quotes: selection.quotes,
358
- ...(selection.note === undefined ? {} : { note: selection.note }),
16
+ export async function commitDiaristEntries(input) {
17
+ const { ticketNumber, cwd } = input;
18
+ const homeOpt = input.home === undefined ? {} : { home: input.home };
19
+ const volumePaths = ensureTicketProvenanceVolume(ticketNumber, cwd, input.home);
20
+ const before = await readTicketProvenance(ticketNumber, cwd, input.home);
21
+ let dropped = 0;
22
+ for (const submitted of input.entries) {
23
+ const entry = projectTicketProvenanceEntry({
24
+ basis: {
25
+ method: "llm-semantic",
26
+ anchors: [`#${ticketNumber}`],
27
+ ...(submitted.note === undefined ? {} : { note: submitted.note }),
28
+ },
29
+ sourceKind: submitted.sourceKind,
30
+ sourceRef: submitted.sourceRef,
31
+ transcript: submitted.transcript,
32
+ timestamp: submitted.timestamp,
359
33
  });
360
- if (!projected.ok) {
361
- rejectedQuotes += 1;
362
- // Single diagnostic expression — never a disguised diary entry.
363
- appendQuoteVerifyFailureDiagnostic({
364
- ticketNumber,
365
- cwd,
366
- ...homeOpt,
367
- cause: projected.cause,
368
- });
34
+ if (entry === undefined) {
35
+ dropped += 1;
369
36
  continue;
370
37
  }
371
38
  appendTicketProvenanceEntry({
372
39
  ticketNumber,
373
40
  cwd,
374
41
  ...homeOpt,
375
- entry: projected.entry,
42
+ entry,
376
43
  source: "diarist",
377
44
  });
378
- acceptedRows += 1;
379
45
  }
380
- // Watermark advances only after the durable volume writes above, so a crash
381
- // mid-commit retries the batch on the next summons. Entry identity and
382
- // quote-verify diagnostic identity are stable — retry duplicates neither.
383
- const identities = input.catalog.candidates.map((block) => blockEntryIdentity(ticketNumber, block));
384
- const alreadyWatermarked = readOfferedIdentities(ticketNumber, cwd, input.catalog.home);
385
- const watermarked = identities.filter((identity) => !alreadyWatermarked.has(identity)).length;
386
- recordOfferedIdentities({ ticketNumber, cwd, ...homeOpt, identities });
387
- // Refresh the human view from the full volume (includes prior summons).
388
- const volume = await readTicketProvenance(ticketNumber, cwd, input.catalog.home);
46
+ const volume = await readTicketProvenance(ticketNumber, cwd, input.home);
389
47
  const humanViewFile = writeTicketProvenanceHumanView({
390
48
  ticketNumber,
391
49
  cwd,
392
50
  ...homeOpt,
393
51
  entries: volume.entries,
394
52
  });
53
+ const appended = volume.entries.length - before.entries.length;
395
54
  return {
396
55
  ticketNumber,
397
- offered: input.catalog.candidates.length,
398
- appended: volume.entries.length - before.entries.length,
399
- rejectedQuotes,
400
- watermarked,
56
+ appended,
57
+ dropped,
401
58
  volumeRecordFile: volumePaths.recordFile,
402
59
  humanViewFile,
403
- collectorStatus: input.catalog.candidates.length === 0
404
- ? "skipped-no-fresh"
405
- : acceptedRows > 0
406
- ? "ok"
407
- : input.selections.length === 0
408
- ? "empty-selection"
409
- : "nothing-appended",
410
- failureCauses: [
411
- ...(unknownCandidate ? ["unknown-candidate"] : []),
412
- ...(rejectedQuotes > 0 ? ["quote-verify-rejected"] : []),
413
- ],
60
+ collectorStatus: appended > 0
61
+ ? "ok"
62
+ : input.entries.length === 0
63
+ ? "empty-selection"
64
+ : "nothing-appended",
414
65
  };
415
66
  }
@@ -194,7 +194,7 @@ export function validateAcceptedLifecycle(toolName, argumentsValue, detailsValue
194
194
  if (toolName === DIARIST_OUTPUT_TOOL_NAME) {
195
195
  // Envelope-owned mechanical sitian facts are runtime-bound on details only
196
196
  // (same shape as the Doctor runtime cost); the submitted arguments carry the
197
- // role's own selections. Machine facts never come from model self-report.
197
+ // role's own entries. Machine facts never come from model self-report.
198
198
  const { sitian: _mechanical, ...submitted } = details;
199
199
  const testimony = validateAcceptedDetails(toolName, argumentsValue);
200
200
  if (!deepEqual(testimony, submitted))
@@ -11,10 +11,7 @@ import { COUNTERSIGN_OUTPUT_TOOL_NAME } from "./countersign-contracts.js";
11
11
  import { GLEANER_LEFT_OUTPUT_TOOL_NAME } from "./gleaner-left-contracts.js";
12
12
  import { INSPECTOR_OUTPUT_TOOL_NAME } from "./inspector-contracts.js";
13
13
  import { AUDITOR_OUTPUT_TOOL_NAME } from "./package-contracts/auditor-output.js";
14
- import {
15
- DIARIST_OUTPUT_TOOL_NAME,
16
- DIARIST_SOURCES_FLAG
17
- } from "./diarist-contracts.js";
14
+ import { DIARIST_OUTPUT_TOOL_NAME } from "./diarist-contracts.js";
18
15
  const NOTARY_SESSION_MATERIALS = [
19
16
  "CLAUDE.md",
20
17
  "souls/notary.md",
@@ -198,14 +195,13 @@ const PUBLIC_ROLE_RECORDS = [
198
195
  activationStage: "load-and-install",
199
196
  sessionMaterials: AUDITOR_PUBLIC_SESSION_MATERIALS
200
197
  },
201
- // #708 / ADR 0075 `diarist-is-role`: 起居郎 is a seat like any other. The
202
- // frozen source catalog rides the shared input-flag seam; it is absent for a
203
- // true-unbound summons (no ticket → no diary).
198
+ // #708 / ADR 0075 `diarist-is-role` / #779: 起居郎 is a seat like any other.
199
+ // No input flag — LLM finds materials itself (caller-transparent).
204
200
  {
205
201
  role: "diarist",
206
202
  phases: [null],
207
203
  outputTool: DIARIST_OUTPUT_TOOL_NAME,
208
- inputFlag: DIARIST_SOURCES_FLAG.name,
204
+ inputFlag: void 0,
209
205
  phaseFlag: void 0,
210
206
  activationStage: "load-and-install",
211
207
  sessionMaterials: [
@@ -128,13 +128,7 @@ function buildActivationFlagArgs(activation) {
128
128
  case "auditor":
129
129
  return ["--ak-role", "auditor"];
130
130
  case "diarist":
131
- return [
132
- "--ak-role",
133
- "diarist",
134
- ...(activation.sourcesPath === undefined
135
- ? []
136
- : ["--ak-diarist-sources", activation.sourcesPath]),
137
- ];
131
+ return ["--ak-role", "diarist"];
138
132
  default: {
139
133
  const _exhaustive = activation;
140
134
  return _exhaustive;