@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.
- package/dist/acp-host/production-host.js +151 -514
- package/dist/adr-path-refs.js +1 -1
- package/dist/diarist-contracts.js +31 -21
- package/dist/diarist-role.js +17 -8
- package/dist/diarist.js +39 -388
- package/dist/package-contracts/terminating-tools.js +1 -1
- package/dist/packaged-role-registry.js +4 -8
- package/dist/pi/role-turn-host.js +1 -7
- package/dist/public-cli/main.js +93 -1470
- package/dist/role-runtime.js +60 -88
- package/dist/ticket-provenance-contracts.js +0 -5
- package/dist/ticket-provenance.js +2 -149
- package/package.json +1 -1
- package/resources/diarist-collect.md +21 -11
- package/src/acp-host/role-envelope.ts +1 -1
- package/src/adr-path-refs.ts +1 -1
- package/src/diarist-contracts.ts +48 -32
- package/src/diarist-role.ts +31 -16
- package/src/diarist.ts +54 -635
- package/src/host-contracts.ts +1 -5
- package/src/package-contracts/terminating-tools.ts +1 -1
- package/src/packaged-role-registry.ts +4 -8
- package/src/pi/role-turn-host.ts +1 -7
- package/src/public-cli/countersign-run.ts +2 -3
- package/src/public-cli/diarist-run.ts +15 -117
- package/src/role-runtime.ts +73 -105
- package/src/ticket-provenance-contracts.ts +6 -12
- package/src/ticket-provenance.ts +1 -207
- package/dist/diarist-mechanical.js +0 -411
- package/src/diarist-mechanical.ts +0 -571
package/dist/adr-path-refs.js
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
|
35
|
-
* gate). Rows that
|
|
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
|
|
39
|
-
const rows = value?.
|
|
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
|
-
|
|
48
|
-
if (typeof index !== "number" || !Number.isInteger(index) || index < 0)
|
|
43
|
+
if (typeof record.sourceKind !== "string")
|
|
49
44
|
continue;
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
}
|
package/dist/diarist-role.js
CHANGED
|
@@ -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
|
|
13
|
+
description: "本庭对象票号(正整数)或 null/省略=真无票;下游走 typed 键;认不出用 status=escalate,不洗成无录。机械层不重判。",
|
|
14
14
|
})),
|
|
15
15
|
reason: Type.Optional(Type.String({
|
|
16
16
|
description: "status 为 escalate 时:认不出本庭对象的原因",
|
|
17
17
|
})),
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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: "
|
|
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: "
|
|
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
|
-
* 起居郎
|
|
3
|
-
* Semantic collection is the diarist role's own LLM turn
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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 {
|
|
9
|
-
import {
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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 (
|
|
361
|
-
|
|
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
|
|
42
|
+
entry,
|
|
376
43
|
source: "diarist",
|
|
377
44
|
});
|
|
378
|
-
acceptedRows += 1;
|
|
379
45
|
}
|
|
380
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
rejectedQuotes,
|
|
400
|
-
watermarked,
|
|
56
|
+
appended,
|
|
57
|
+
dropped,
|
|
401
58
|
volumeRecordFile: volumePaths.recordFile,
|
|
402
59
|
humanViewFile,
|
|
403
|
-
collectorStatus:
|
|
404
|
-
? "
|
|
405
|
-
:
|
|
406
|
-
? "
|
|
407
|
-
:
|
|
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
|
|
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
|
|
202
|
-
//
|
|
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:
|
|
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;
|