@akagilnc/pi-workflow-roles 0.1.3565 → 0.1.3621
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/README.md +4 -1
- package/README.zh-CN.md +1 -1
- package/dist/acp-host/production-host.js +18477 -8099
- package/dist/analyst-gate-cycles-read.js +387 -0
- package/dist/archivist-record-entry.js +22 -1
- package/dist/audit-escalation.js +6 -0
- package/dist/auditor-soul.js +74 -0
- package/dist/collector-config.js +86 -0
- package/dist/collector-evidence.js +316 -0
- package/dist/collector-github.js +527 -0
- package/dist/collector-ledger.js +853 -0
- package/dist/collector-tool-schemas.js +33 -0
- package/dist/compliance-transport.js +104 -52
- package/dist/diarist-contracts.js +72 -0
- package/dist/diarist-mechanical.js +411 -0
- package/dist/diarist-ticket-resolution.js +177 -0
- package/dist/diarist.js +311 -0
- package/dist/doctor-auditor.js +25 -0
- package/dist/doctor-contracts.js +2 -0
- package/dist/doctor-evidence.js +142 -0
- package/dist/gatekeeper-role.js +104 -81
- package/dist/host-transition-prior-native.js +78 -0
- package/dist/institutional-resolution.js +1 -95
- package/dist/judge-auditor.js +26 -0
- package/dist/ledger-session-read.js +227 -0
- package/dist/merger-git-state.js +78 -0
- package/dist/navigator-attendance.js +15 -7
- package/dist/navigator-public-session.js +170 -0
- package/dist/navigator-session-contracts.js +36 -40
- package/dist/notary-source-run.js +122 -0
- package/dist/package-contracts/auditor-output.js +66 -0
- package/dist/package-contracts/evidence-child-output.js +34 -0
- package/dist/package-contracts/judge-output.js +2 -0
- package/dist/package-contracts/terminating-tools.js +44 -3
- package/dist/package-resources/method-skill.js +254 -0
- package/dist/packaged-role-registry.js +56 -0
- package/dist/pi/durable-principal.js +61 -0
- package/dist/pi/in-process-session.js +25 -6
- package/dist/pi/known-failure.js +52 -0
- package/dist/pi/role-turn-host.js +430 -0
- package/dist/public-cli/auto-resume.js +414 -0
- package/dist/public-cli/cli-errors.js +8 -0
- package/dist/public-cli/cli-io.js +1 -0
- package/dist/public-cli/command-renderer.js +5 -0
- package/dist/public-cli/doctor-run.js +84 -0
- package/dist/public-cli/inspector-run.js +136 -0
- package/dist/public-cli/instruction-seat-run.js +256 -0
- package/dist/public-cli/invocation.js +2225 -0
- package/dist/public-cli/judge-run.js +118 -0
- package/dist/public-cli/load-production-acp-host.js +40 -0
- package/dist/public-cli/main.js +1511 -1124
- package/dist/public-cli/notary-run.js +161 -0
- package/dist/public-cli/option-definitions.js +1192 -0
- package/dist/public-cli/post-admission.js +667 -0
- package/dist/public-cli/public-run-credentials.js +49 -0
- package/dist/public-cli/registry.js +8 -2
- package/dist/public-cli/reviewer-dispatch-rejection.js +77 -0
- package/dist/public-cli/run-lifecycle.js +1344 -0
- package/dist/public-cli/seat-ticket-binding.js +113 -0
- package/dist/public-cli/settlement.js +3441 -0
- package/dist/public-cli/terminal.js +135 -0
- package/dist/public-cli/turn-request.js +25 -0
- package/dist/public-role-summons.js +301 -0
- package/dist/receipt-delivery-policy.js +10 -0
- package/dist/reviewer-child-executor.js +80 -8
- package/dist/reviewer-execution-ledger.js +2 -1
- package/dist/run-terminal-artifacts.js +195 -0
- package/dist/run-ticket-number.js +40 -0
- package/dist/session-assistant-usage.js +107 -0
- package/dist/shape-unreadable-failure.js +34 -0
- package/dist/submission-errors.js +3 -0
- package/dist/submission-ledger.js +388 -0
- package/dist/ticket-provenance-contracts.js +115 -0
- package/dist/ticket-provenance.js +340 -0
- package/extensions/role-runtime.ts +8 -0
- package/package.json +1 -1
- package/resources/diarist-collect.md +6 -10
- package/scripts/build-package.mjs +2 -1
- package/souls/diarist.md +9 -0
- package/souls/doctor-auditor.md +1 -0
- package/souls/evidence-child.md +1 -0
- package/src/acp-host/production-host.ts +4 -0
- package/src/acp-host/role-envelope.ts +4 -3
- package/src/acp-host/role-turn-host.ts +9 -1
- package/src/analyst-gate-cycles-read.ts +37 -2
- package/src/archivist-record-entry.ts +45 -1
- package/src/audit-escalation.ts +17 -0
- package/src/auditor-role.ts +22 -0
- package/src/auditor-soul.ts +42 -0
- package/src/compliance-transport.ts +177 -66
- package/src/diarist-contracts.ts +88 -0
- package/src/diarist-role.ts +60 -0
- package/src/diarist.ts +253 -179
- package/src/doctor-auditor.ts +10 -14
- package/src/doctor-contracts.ts +1 -0
- package/src/doctor-role.ts +2 -2
- package/src/evidence-child-role.ts +22 -0
- package/src/gatekeeper-pass-envelope.ts +109 -0
- package/src/gatekeeper-role.ts +145 -104
- package/src/host-contracts.ts +15 -2
- package/src/institutional-resolution.ts +8 -163
- package/src/judge-auditor.ts +10 -15
- package/src/judge-role.ts +8 -0
- package/src/navigator-attendance.ts +25 -9
- package/src/navigator-public-session.ts +217 -0
- package/src/navigator-session-contracts.ts +61 -54
- package/src/notary-source-run.ts +3 -1
- package/src/package-contracts/auditor-output.ts +82 -0
- package/src/package-contracts/evidence-child-output.ts +51 -0
- package/src/package-contracts/judge-output.ts +1 -0
- package/src/package-contracts/reviewer-output.ts +2 -2
- package/src/package-contracts/terminating-tools.ts +53 -3
- package/src/packaged-role-registry.ts +63 -0
- package/src/pi/adapter.ts +2 -1
- package/src/pi/in-process-session.ts +38 -12
- package/src/pi/role-turn-host.ts +42 -0
- package/src/public-cli/cli.ts +43 -6
- package/src/public-cli/countersign-run.ts +9 -129
- package/src/public-cli/diarist-run.ts +312 -0
- package/src/public-cli/instruction-seat-run.ts +289 -50
- package/src/public-cli/invocation.ts +87 -25
- package/src/public-cli/option-definitions.ts +79 -0
- package/src/public-cli/post-admission.ts +9 -3
- package/src/public-cli/registry.ts +7 -1
- package/src/public-cli/run-lifecycle.ts +53 -6
- package/src/public-cli/settlement.ts +170 -7
- package/src/public-cli/terminal.ts +4 -1
- package/src/public-role-summons.ts +431 -0
- package/src/receipt-delivery-policy.ts +10 -0
- package/src/reviewer-agent.ts +1 -1
- package/src/reviewer-child-executor.ts +114 -12
- package/src/reviewer-execution-ledger.ts +3 -2
- package/src/role-runtime.ts +275 -23
- package/src/session-assistant-usage.ts +119 -0
- package/src/session-opening-materials.ts +1 -1
- package/src/shape-unreadable-failure.ts +49 -0
- package/src/submission-errors.ts +3 -0
- package/src/ticket-provenance-contracts.ts +1 -0
- package/src/ticket-provenance.ts +0 -23
- package/dist/evidence-child-executor.js +0 -829
- package/src/diarist-llm-collector.ts +0 -324
- package/src/evidence-child-executor.ts +0 -1140
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 起居郎 mechanical safeguard band — ADR 0075 / #582.
|
|
3
|
+
* Deterministic: source enum helpers, candidate anchors, dedup, notification
|
|
4
|
+
* filter, and verbatim quote reverse-verify. Never a production relevance gate.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
8
|
+
import { basename, join, resolve } from "node:path";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { pathContainedIn, physicallyContainedIn, } from "./activation-ledger-topology.js";
|
|
11
|
+
export const DEFAULT_QUOTE_MIN = 6;
|
|
12
|
+
export class DiaristSourceReadError extends Error {
|
|
13
|
+
code = "diarist-source-read";
|
|
14
|
+
reason;
|
|
15
|
+
sourcePath;
|
|
16
|
+
constructor(reason, sourcePath, message, options) {
|
|
17
|
+
super(message, options);
|
|
18
|
+
this.name = "DiaristSourceReadError";
|
|
19
|
+
this.reason = reason;
|
|
20
|
+
this.sourcePath = sourcePath;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Extract 「」 quotes from ticket face text for mechanical anchors.
|
|
25
|
+
* Only quotes meeting min length are kept.
|
|
26
|
+
*/
|
|
27
|
+
export function extractCornerQuotes(text, minLength = DEFAULT_QUOTE_MIN) {
|
|
28
|
+
const out = [];
|
|
29
|
+
const re = /「([^」]+)」/g;
|
|
30
|
+
let match;
|
|
31
|
+
while ((match = re.exec(text)) !== null) {
|
|
32
|
+
const q = match[1].trim();
|
|
33
|
+
if (q.length >= minLength)
|
|
34
|
+
out.push(q);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/** Build the mechanical anchor set from ticket number + face body. */
|
|
39
|
+
export function buildDiaristAnchors(input) {
|
|
40
|
+
const fromBody = input.ticketBody === undefined ? [] : extractCornerQuotes(input.ticketBody);
|
|
41
|
+
const quotes = [...fromBody, ...(input.extraQuotes ?? [])];
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
const uniqueQuotes = [];
|
|
44
|
+
for (const q of quotes) {
|
|
45
|
+
if (seen.has(q))
|
|
46
|
+
continue;
|
|
47
|
+
seen.add(q);
|
|
48
|
+
uniqueQuotes.push(q);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
ticketNumber: input.ticketNumber,
|
|
52
|
+
quotes: uniqueQuotes,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Drop notification/system noise blocks (typed flags only — no prose match). */
|
|
56
|
+
export function filterNotifications(blocks) {
|
|
57
|
+
return blocks.filter((b) => !b.isNotification);
|
|
58
|
+
}
|
|
59
|
+
function blockKey(block) {
|
|
60
|
+
const r = block.sourceRef;
|
|
61
|
+
return [
|
|
62
|
+
block.sourceKind,
|
|
63
|
+
r.sessionFile ?? "",
|
|
64
|
+
r.entryId === undefined ? "" : String(r.entryId),
|
|
65
|
+
r.path ?? "",
|
|
66
|
+
r.url ?? "",
|
|
67
|
+
createHash("sha256").update(block.transcript, "utf8").digest("hex").slice(0, 16),
|
|
68
|
+
].join("|");
|
|
69
|
+
}
|
|
70
|
+
/** Dedup by source pointer + transcript digest (compression replay ×N). */
|
|
71
|
+
export function dedupeSourceBlocks(blocks) {
|
|
72
|
+
const seen = new Set();
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const block of blocks) {
|
|
75
|
+
const key = blockKey(block);
|
|
76
|
+
if (seen.has(key))
|
|
77
|
+
continue;
|
|
78
|
+
seen.add(key);
|
|
79
|
+
out.push(block);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
export function verifyQuotesVerbatim(sourceText, quotes) {
|
|
84
|
+
const failed = [];
|
|
85
|
+
for (const q of quotes) {
|
|
86
|
+
if (q.length === 0)
|
|
87
|
+
continue;
|
|
88
|
+
if (!sourceText.includes(q))
|
|
89
|
+
failed.push(q);
|
|
90
|
+
}
|
|
91
|
+
if (failed.length === 0)
|
|
92
|
+
return { ok: true };
|
|
93
|
+
return { ok: false, failedQuotes: failed };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Mechanical safeguard only — never a relevance gate (ADR 0075 / 锚定宪法).
|
|
97
|
+
* filter notifications (typed) → dedupe. Semantic selection is LLM-only.
|
|
98
|
+
*/
|
|
99
|
+
export function mechanicalSafeguardPipeline(blocks) {
|
|
100
|
+
return dedupeSourceBlocks(filterNotifications(blocks));
|
|
101
|
+
}
|
|
102
|
+
/** Claude Code project-dir encoding: abs path with / → -. */
|
|
103
|
+
export function encodeClaudeProjectDir(cwd) {
|
|
104
|
+
const abs = cwd.startsWith("/") ? cwd : `/${cwd}`;
|
|
105
|
+
return abs.replace(/\//g, "-");
|
|
106
|
+
}
|
|
107
|
+
function isRecord(value) {
|
|
108
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
109
|
+
}
|
|
110
|
+
function messageText(message) {
|
|
111
|
+
if (typeof message === "string")
|
|
112
|
+
return message;
|
|
113
|
+
if (!isRecord(message))
|
|
114
|
+
return "";
|
|
115
|
+
const content = message.content;
|
|
116
|
+
if (typeof content === "string")
|
|
117
|
+
return content;
|
|
118
|
+
if (!Array.isArray(content))
|
|
119
|
+
return "";
|
|
120
|
+
const parts = [];
|
|
121
|
+
for (const part of content) {
|
|
122
|
+
if (typeof part === "string") {
|
|
123
|
+
parts.push(part);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!isRecord(part))
|
|
127
|
+
continue;
|
|
128
|
+
if (typeof part.text === "string")
|
|
129
|
+
parts.push(part.text);
|
|
130
|
+
else if (typeof part.content === "string")
|
|
131
|
+
parts.push(part.content);
|
|
132
|
+
}
|
|
133
|
+
return parts.join("\n");
|
|
134
|
+
}
|
|
135
|
+
function isNotificationRow(type, text) {
|
|
136
|
+
if (type === "queue-operation" || type === "system" || type === "progress") {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (type === "user") {
|
|
140
|
+
// Local command / meta noise common in cc transcripts.
|
|
141
|
+
if (text.startsWith("<command-name>") || text.startsWith("<local-command-")) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
if (text.startsWith("<command-message>") || text.startsWith("<command-args>")) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Enumerate cc session user/assistant turns and human queued-command attachments under Claude projects roots.
|
|
152
|
+
* cc-sessions-first source family (ADR 0075).
|
|
153
|
+
* Directory/file/non-empty JSONL line failures throw DiaristSourceReadError
|
|
154
|
+
* (失败诚实:不得把未读懂洗成没有来源块).
|
|
155
|
+
*/
|
|
156
|
+
export function readCcSessionBlocks(options) {
|
|
157
|
+
const root = options.projectsRoot ?? join(homedir(), ".claude", "projects");
|
|
158
|
+
if (!existsSync(root))
|
|
159
|
+
return [];
|
|
160
|
+
const blocks = [];
|
|
161
|
+
const dirs = new Set();
|
|
162
|
+
for (const cwd of options.cwds) {
|
|
163
|
+
dirs.add(join(root, encodeClaudeProjectDir(cwd)));
|
|
164
|
+
}
|
|
165
|
+
// Also accept already-encoded folder names passed as cwd (test convenience).
|
|
166
|
+
for (const cwd of options.cwds) {
|
|
167
|
+
if (cwd.includes("projects"))
|
|
168
|
+
continue;
|
|
169
|
+
const direct = join(root, cwd);
|
|
170
|
+
if (existsSync(direct))
|
|
171
|
+
dirs.add(direct);
|
|
172
|
+
}
|
|
173
|
+
for (const dir of dirs) {
|
|
174
|
+
if (!existsSync(dir))
|
|
175
|
+
continue;
|
|
176
|
+
let dirStat;
|
|
177
|
+
try {
|
|
178
|
+
dirStat = statSync(dir);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
throw new DiaristSourceReadError("readdir-failed", dir, `diarist cc session dir unstatable (${dir})`, { cause: error });
|
|
182
|
+
}
|
|
183
|
+
if (!dirStat.isDirectory())
|
|
184
|
+
continue;
|
|
185
|
+
let files;
|
|
186
|
+
try {
|
|
187
|
+
files = readdirSync(dir).filter((name) => name.endsWith(".jsonl"));
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
throw new DiaristSourceReadError("readdir-failed", dir, `diarist cc session dir unreadable (${dir})`, { cause: error });
|
|
191
|
+
}
|
|
192
|
+
for (const name of files) {
|
|
193
|
+
const sessionFile = join(dir, name);
|
|
194
|
+
let text;
|
|
195
|
+
try {
|
|
196
|
+
text = readFileSync(sessionFile, "utf8");
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
throw new DiaristSourceReadError("file-unreadable", sessionFile, `diarist cc session file unreadable (${sessionFile})`, { cause: error });
|
|
200
|
+
}
|
|
201
|
+
const lines = text.split("\n");
|
|
202
|
+
for (let lineNo = 0; lineNo < lines.length; lineNo += 1) {
|
|
203
|
+
const line = lines[lineNo];
|
|
204
|
+
if (!line.trim())
|
|
205
|
+
continue;
|
|
206
|
+
let parsed;
|
|
207
|
+
try {
|
|
208
|
+
parsed = JSON.parse(line);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
throw new DiaristSourceReadError("jsonl-line-unparseable", sessionFile, `diarist cc session JSONL line ${lineNo + 1} unparseable (${sessionFile})`, { cause: error });
|
|
212
|
+
}
|
|
213
|
+
// Unknown row shape (not user/assistant or a human queued-command attachment) is ignored — not a read failure.
|
|
214
|
+
if (!isRecord(parsed))
|
|
215
|
+
continue;
|
|
216
|
+
const type = typeof parsed.type === "string" ? parsed.type : "";
|
|
217
|
+
const queuedCommand = type === "attachment" &&
|
|
218
|
+
isRecord(parsed.attachment) &&
|
|
219
|
+
parsed.attachment.type === "queued_command" &&
|
|
220
|
+
isRecord(parsed.attachment.origin) &&
|
|
221
|
+
parsed.attachment.origin.kind === "human" &&
|
|
222
|
+
typeof parsed.attachment.prompt === "string"
|
|
223
|
+
? parsed.attachment.prompt
|
|
224
|
+
: undefined;
|
|
225
|
+
if (type !== "user" && type !== "assistant" && queuedCommand === undefined) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const transcript = queuedCommand ?? messageText(parsed.message ?? parsed);
|
|
229
|
+
if (transcript.trim() === "")
|
|
230
|
+
continue;
|
|
231
|
+
const entryId = typeof parsed.uuid === "string"
|
|
232
|
+
? parsed.uuid
|
|
233
|
+
: typeof parsed.id === "string"
|
|
234
|
+
? parsed.id
|
|
235
|
+
: `${basename(sessionFile)}:${lineNo + 1}`;
|
|
236
|
+
const timestamp = typeof parsed.timestamp === "string"
|
|
237
|
+
? parsed.timestamp
|
|
238
|
+
: new Date(0).toISOString();
|
|
239
|
+
const notification = isNotificationRow(type, transcript);
|
|
240
|
+
blocks.push({
|
|
241
|
+
sourceKind: "cc-session",
|
|
242
|
+
sourceRef: { sessionFile, entryId },
|
|
243
|
+
transcript,
|
|
244
|
+
timestamp,
|
|
245
|
+
isUserTurn: type === "user" || queuedCommand !== undefined,
|
|
246
|
+
isNotification: notification,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return blocks;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Issue body + comments + 「」 decree spans as typed source blocks.
|
|
255
|
+
* Each block keeps its own sourceRef (url / comment id) — never merge attachments here.
|
|
256
|
+
*/
|
|
257
|
+
export function readIssueFaceBlocks(input) {
|
|
258
|
+
const fallbackTs = input.timestamp ?? new Date(0).toISOString();
|
|
259
|
+
const blocks = [];
|
|
260
|
+
const body = input.face.body;
|
|
261
|
+
if (body.trim() !== "") {
|
|
262
|
+
blocks.push({
|
|
263
|
+
sourceKind: "issue-body-comment",
|
|
264
|
+
sourceRef: { url: input.face.bodyUrl, entryId: "body" },
|
|
265
|
+
transcript: body,
|
|
266
|
+
timestamp: fallbackTs,
|
|
267
|
+
isUserTurn: true,
|
|
268
|
+
isNotification: false,
|
|
269
|
+
});
|
|
270
|
+
const quotes = extractCornerQuotes(body);
|
|
271
|
+
for (let i = 0; i < quotes.length; i += 1) {
|
|
272
|
+
const q = quotes[i];
|
|
273
|
+
blocks.push({
|
|
274
|
+
sourceKind: "ticket-decree-block",
|
|
275
|
+
sourceRef: { url: input.face.bodyUrl, entryId: `decree-${i + 1}` },
|
|
276
|
+
transcript: q,
|
|
277
|
+
timestamp: fallbackTs,
|
|
278
|
+
isUserTurn: true,
|
|
279
|
+
isNotification: false,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
for (const comment of input.face.comments) {
|
|
284
|
+
if (comment.body.trim() === "")
|
|
285
|
+
continue;
|
|
286
|
+
blocks.push({
|
|
287
|
+
sourceKind: "issue-body-comment",
|
|
288
|
+
sourceRef: {
|
|
289
|
+
url: comment.htmlUrl,
|
|
290
|
+
entryId: comment.id,
|
|
291
|
+
},
|
|
292
|
+
transcript: comment.body,
|
|
293
|
+
timestamp: comment.createdAt || fallbackTs,
|
|
294
|
+
isUserTurn: true,
|
|
295
|
+
isNotification: false,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return blocks;
|
|
299
|
+
}
|
|
300
|
+
/** Relative root every ADR decision-key read must stay under (ADR 0038). */
|
|
301
|
+
export const DIARIST_ADR_ROOT_REL = "docs/adr";
|
|
302
|
+
/**
|
|
303
|
+
* Read applicable ADR files as decision-key source blocks.
|
|
304
|
+
* Real IO seam owns lexical + physical containment under cwd/docs/adr (ADR 0038).
|
|
305
|
+
* Missing / unreadable / escape → typed DiaristSourceReadError (失败诚实).
|
|
306
|
+
*/
|
|
307
|
+
export function readAdrDecisionKeyBlocks(input) {
|
|
308
|
+
const timestamp = input.timestamp ?? new Date(0).toISOString();
|
|
309
|
+
const blocks = [];
|
|
310
|
+
const cwdAbs = resolve(input.cwd);
|
|
311
|
+
const adrRootAbs = resolve(cwdAbs, DIARIST_ADR_ROOT_REL);
|
|
312
|
+
let adrRootReal;
|
|
313
|
+
try {
|
|
314
|
+
// Prefer the real ADR root when it exists; otherwise fall back to cwd real
|
|
315
|
+
// so physical checks still have a stable anchor before the first ADR file.
|
|
316
|
+
adrRootReal = existsSync(adrRootAbs)
|
|
317
|
+
? realpathSync(adrRootAbs)
|
|
318
|
+
: resolve(realpathSync(cwdAbs), DIARIST_ADR_ROOT_REL);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
throw new DiaristSourceReadError("adr-unreadable", cwdAbs, `diarist ADR root is not resolvable (${adrRootAbs})`, { cause: error });
|
|
322
|
+
}
|
|
323
|
+
for (const rel of input.adrPaths) {
|
|
324
|
+
const segments = rel.split(/[/\\]/);
|
|
325
|
+
// Claimed path must sit under docs/adr/ with no traversal segments.
|
|
326
|
+
if (segments[0] !== "docs" ||
|
|
327
|
+
segments[1] !== "adr" ||
|
|
328
|
+
segments.length < 3 ||
|
|
329
|
+
segments.some((part) => part === "" || part === "." || part === "..")) {
|
|
330
|
+
throw new DiaristSourceReadError("adr-escape", resolve(cwdAbs, rel), `diarist referenced ADR escapes docs/adr (${rel})`);
|
|
331
|
+
}
|
|
332
|
+
const abs = resolve(cwdAbs, rel);
|
|
333
|
+
// Lexical containment under docs/adr before any read (ADR 0038).
|
|
334
|
+
if (abs !== adrRootAbs && !pathContainedIn(adrRootAbs, abs)) {
|
|
335
|
+
throw new DiaristSourceReadError("adr-escape", abs, `diarist referenced ADR escapes docs/adr (${rel})`);
|
|
336
|
+
}
|
|
337
|
+
if (!existsSync(abs)) {
|
|
338
|
+
throw new DiaristSourceReadError("adr-missing", abs, `diarist referenced ADR missing (${rel})`);
|
|
339
|
+
}
|
|
340
|
+
let realAbs;
|
|
341
|
+
try {
|
|
342
|
+
realAbs = realpathSync(abs);
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
throw new DiaristSourceReadError("adr-unreadable", abs, `diarist ADR path is not resolvable (${abs})`, { cause: error });
|
|
346
|
+
}
|
|
347
|
+
if (realAbs !== adrRootReal && !physicallyContainedIn(adrRootReal, realAbs)) {
|
|
348
|
+
throw new DiaristSourceReadError("adr-escape", abs, `diarist referenced ADR escapes docs/adr physically (${rel})`);
|
|
349
|
+
}
|
|
350
|
+
let text;
|
|
351
|
+
try {
|
|
352
|
+
text = readFileSync(realAbs, "utf8");
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
throw new DiaristSourceReadError("adr-unreadable", realAbs, `diarist ADR file unreadable (${realAbs})`, { cause: error });
|
|
356
|
+
}
|
|
357
|
+
if (text.trim() === "")
|
|
358
|
+
continue;
|
|
359
|
+
blocks.push({
|
|
360
|
+
sourceKind: "adr-decision-key",
|
|
361
|
+
sourceRef: { path: rel },
|
|
362
|
+
transcript: text,
|
|
363
|
+
timestamp,
|
|
364
|
+
isUserTurn: true,
|
|
365
|
+
isNotification: false,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return blocks;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Build an llm-semantic entry after reverse-verify succeeds.
|
|
372
|
+
* `quotes` are the LLM-claimed quotes that must be verbatim in transcript.
|
|
373
|
+
* basis.anchors record ticket # + mechanical/claimed quotes for audit only.
|
|
374
|
+
* Failure returns failedQuotes only — caller records a single diagnostic (not a
|
|
375
|
+
* disguised diary entry / dead basis.method).
|
|
376
|
+
*/
|
|
377
|
+
export function blockToLlmEntry(block, input) {
|
|
378
|
+
const verify = verifyQuotesVerbatim(block.transcript, input.quotes);
|
|
379
|
+
if (!verify.ok) {
|
|
380
|
+
return {
|
|
381
|
+
ok: false,
|
|
382
|
+
failedQuotes: verify.failedQuotes,
|
|
383
|
+
cause: `verbatim verify failed: ${verify.failedQuotes.join(" | ")}`,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
// Ticket + mechanical face quotes + claimed quotes — reference/反验 only.
|
|
387
|
+
const anchorNotes = [`#${input.anchors.ticketNumber}`];
|
|
388
|
+
const seen = new Set(anchorNotes);
|
|
389
|
+
for (const q of [...input.anchors.quotes, ...input.quotes]) {
|
|
390
|
+
if (seen.has(q))
|
|
391
|
+
continue;
|
|
392
|
+
seen.add(q);
|
|
393
|
+
anchorNotes.push(q);
|
|
394
|
+
if (anchorNotes.length >= 12)
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
ok: true,
|
|
399
|
+
entry: {
|
|
400
|
+
basis: {
|
|
401
|
+
method: "llm-semantic",
|
|
402
|
+
anchors: anchorNotes,
|
|
403
|
+
...(input.note === undefined ? {} : { note: input.note }),
|
|
404
|
+
},
|
|
405
|
+
sourceKind: block.sourceKind,
|
|
406
|
+
sourceRef: block.sourceRef,
|
|
407
|
+
transcript: block.transcript,
|
|
408
|
+
timestamp: block.timestamp,
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 起居郎 pre-court ticket resolution (#582 / diarist-resolves-ticket-llm-layer).
|
|
3
|
+
* Unbound countersign: LLM typed assertion over accepted instruction, then two
|
|
4
|
+
* mechanical checks on N (complete decimal number in instruction; live GitHub issue).
|
|
5
|
+
* Verification failure is controlled failure — never washed to true-unbound.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { createGhApiRunner, projectGhIssueBody, } from "./collector-github.js";
|
|
11
|
+
import { ENGINE_DETOUR_STAGED_PROMPT_TOKEN, runEngineDetourOnce, } from "./engine-detour.js";
|
|
12
|
+
export const DIARIST_RESOLVE_TICKET_METHOD_RELATIVE = "resources/diarist-resolve-ticket.md";
|
|
13
|
+
export class DiaristTicketResolutionError extends Error {
|
|
14
|
+
code = "diarist-ticket-resolution";
|
|
15
|
+
reason;
|
|
16
|
+
constructor(reason, message, options) {
|
|
17
|
+
super(message, options);
|
|
18
|
+
this.name = "DiaristTicketResolutionError";
|
|
19
|
+
this.reason = reason;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function resolveDiaristResolveTicketMethodPath(packageRoot = fileURLToPath(new URL("..", import.meta.url))) {
|
|
23
|
+
return join(packageRoot, DIARIST_RESOLVE_TICKET_METHOD_RELATIVE);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Decision-key exact identity: complete decimal number N appears in instruction.
|
|
27
|
+
* A longer number's digit substring (e.g. 82 inside #582) is not N.
|
|
28
|
+
*/
|
|
29
|
+
export function instructionContainsTicketNumber(instruction, ticketNumber) {
|
|
30
|
+
if (!Number.isSafeInteger(ticketNumber) || ticketNumber < 1)
|
|
31
|
+
return false;
|
|
32
|
+
// Complete number token: not preceded or followed by another digit.
|
|
33
|
+
return new RegExp(`(?<!\\d)${String(ticketNumber)}(?!\\d)`).test(instruction);
|
|
34
|
+
}
|
|
35
|
+
/** Sole stdout shapes: {assertion:"ticket",ticketNumber:N} | {assertion:"true-unbound"}. */
|
|
36
|
+
export function parseDiaristTicketResolverStdout(stdout) {
|
|
37
|
+
const trimmed = stdout.trim();
|
|
38
|
+
if (trimmed.length === 0) {
|
|
39
|
+
throw new DiaristTicketResolutionError("empty-stdout", "diarist ticket resolver stdout is empty");
|
|
40
|
+
}
|
|
41
|
+
let parsed;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(trimmed);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new DiaristTicketResolutionError("unparseable-json", "diarist ticket resolver stdout is not JSON", { cause: error });
|
|
47
|
+
}
|
|
48
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
49
|
+
throw new DiaristTicketResolutionError("not-object", "diarist ticket resolver stdout must be one JSON object");
|
|
50
|
+
}
|
|
51
|
+
const record = parsed;
|
|
52
|
+
if (!Object.prototype.hasOwnProperty.call(record, "assertion")) {
|
|
53
|
+
throw new DiaristTicketResolutionError("assertion-missing", "diarist ticket resolver stdout missing assertion");
|
|
54
|
+
}
|
|
55
|
+
if (record.assertion === "true-unbound")
|
|
56
|
+
return { kind: "true-unbound" };
|
|
57
|
+
const n = record.ticketNumber;
|
|
58
|
+
if (record.assertion === "ticket" &&
|
|
59
|
+
typeof n === "number" &&
|
|
60
|
+
Number.isSafeInteger(n) &&
|
|
61
|
+
n >= 1) {
|
|
62
|
+
return { kind: "ticket", ticketNumber: n };
|
|
63
|
+
}
|
|
64
|
+
throw new DiaristTicketResolutionError("assertion-uninterpretable", "diarist ticket resolver assertion is not ticket|true-unbound with safe ticketNumber");
|
|
65
|
+
}
|
|
66
|
+
const HERMES_RESOLVER_TOOLSET = "context_engine";
|
|
67
|
+
/** Hermes resolver: method bytes + instruction only; empty toolset; staged prompt. */
|
|
68
|
+
export function createHermesDiaristTicketResolver(options = {}) {
|
|
69
|
+
const methodPath = resolveDiaristResolveTicketMethodPath(options.packageRoot);
|
|
70
|
+
if (!existsSync(methodPath)) {
|
|
71
|
+
throw new Error(`diarist resolve-ticket method material missing (${methodPath})`);
|
|
72
|
+
}
|
|
73
|
+
const method = readFileSync(methodPath, "utf8");
|
|
74
|
+
if (method.trim().length === 0) {
|
|
75
|
+
throw new Error(`diarist resolve-ticket method material is empty (${methodPath})`);
|
|
76
|
+
}
|
|
77
|
+
const executable = options.executable ?? "hermes";
|
|
78
|
+
return async (input) => {
|
|
79
|
+
const argv = [
|
|
80
|
+
executable,
|
|
81
|
+
"chat",
|
|
82
|
+
"--query-file",
|
|
83
|
+
ENGINE_DETOUR_STAGED_PROMPT_TOKEN,
|
|
84
|
+
"-Q",
|
|
85
|
+
"--no-restore-cwd",
|
|
86
|
+
"--ignore-rules",
|
|
87
|
+
"-t",
|
|
88
|
+
HERMES_RESOLVER_TOOLSET,
|
|
89
|
+
];
|
|
90
|
+
let result;
|
|
91
|
+
try {
|
|
92
|
+
result = await runEngineDetourOnce({
|
|
93
|
+
argv,
|
|
94
|
+
stagedPrompt: JSON.stringify({ method, instruction: input.instruction }),
|
|
95
|
+
cwd: options.cwd ?? process.cwd(),
|
|
96
|
+
...(options.env === undefined ? {} : { env: options.env }),
|
|
97
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw new DiaristTicketResolutionError("engine-failed", "diarist ticket resolver engine failed to launch", { cause: error });
|
|
102
|
+
}
|
|
103
|
+
if (result.code !== 0) {
|
|
104
|
+
throw new DiaristTicketResolutionError("engine-failed", `diarist ticket resolver engine exited ${result.code}: ${result.stderr.slice(0, 500)}`);
|
|
105
|
+
}
|
|
106
|
+
return parseDiaristTicketResolverStdout(result.stdout);
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** Live ticket check over shared gh issue-body projection. */
|
|
110
|
+
export function createGhTicketExistenceChecker(options) {
|
|
111
|
+
const runner = options?.runner ?? createGhApiRunner();
|
|
112
|
+
return async (input) => {
|
|
113
|
+
const projected = await projectGhIssueBody(runner, {
|
|
114
|
+
owner: input.owner,
|
|
115
|
+
repo: input.repo,
|
|
116
|
+
ticketNumber: input.ticketNumber,
|
|
117
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
118
|
+
});
|
|
119
|
+
if (projected.status === "available")
|
|
120
|
+
return true;
|
|
121
|
+
if (projected.status === "unavailable"
|
|
122
|
+
&& projected.reason === "http-non-2xx"
|
|
123
|
+
&& projected.httpStatus === 404) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (projected.status === "unavailable" && projected.reason === "pull-request") {
|
|
127
|
+
throw new DiaristTicketResolutionError("ticket-is-pull-request", `ticket #${input.ticketNumber} resolves to a pull request, not an issue`, { cause: projected });
|
|
128
|
+
}
|
|
129
|
+
if (projected.status === "unavailable") {
|
|
130
|
+
throw new DiaristTicketResolutionError("ticket-unavailable", `ticket verification unavailable for ${input.owner}/${input.repo}#${input.ticketNumber}`, { cause: projected });
|
|
131
|
+
}
|
|
132
|
+
throw new DiaristTicketResolutionError("ticket-invalid", `ticket verification returned an invalid issue payload for ${input.owner}/${input.repo}#${input.ticketNumber}`, { cause: projected });
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** Mechanical verify after LLM assertion. true-unbound passes; ticket needs both checks. */
|
|
136
|
+
export async function verifyDiaristTicketAssertion(input) {
|
|
137
|
+
if (input.assertion.kind === "true-unbound")
|
|
138
|
+
return input.assertion;
|
|
139
|
+
const n = input.assertion.ticketNumber;
|
|
140
|
+
if (!instructionContainsTicketNumber(input.instruction, n)) {
|
|
141
|
+
throw new DiaristTicketResolutionError("number-not-in-instruction", `diarist ticket assertion #${n} complete decimal number does not appear in accepted instruction`);
|
|
142
|
+
}
|
|
143
|
+
if (input.origin === undefined) {
|
|
144
|
+
throw new DiaristTicketResolutionError("origin-unresolved", `diarist ticket assertion #${n} requires a resolvable github.com origin remote`);
|
|
145
|
+
}
|
|
146
|
+
let exists;
|
|
147
|
+
try {
|
|
148
|
+
exists = await input.checkExistence({
|
|
149
|
+
owner: input.origin.owner,
|
|
150
|
+
repo: input.origin.repo,
|
|
151
|
+
ticketNumber: n,
|
|
152
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
if (error instanceof DiaristTicketResolutionError)
|
|
157
|
+
throw error;
|
|
158
|
+
throw new DiaristTicketResolutionError("ticket-unavailable", `diarist ticket assertion #${n} live verification failed`, { cause: error });
|
|
159
|
+
}
|
|
160
|
+
if (!exists) {
|
|
161
|
+
throw new DiaristTicketResolutionError("ticket-missing", `diarist ticket assertion #${n} does not exist as a live issue on ${input.origin.owner}/${input.origin.repo}`);
|
|
162
|
+
}
|
|
163
|
+
return input.assertion;
|
|
164
|
+
}
|
|
165
|
+
export async function resolveDiaristTicketFromInstruction(input) {
|
|
166
|
+
const assertion = await input.resolver({
|
|
167
|
+
instruction: input.instruction,
|
|
168
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
169
|
+
});
|
|
170
|
+
return verifyDiaristTicketAssertion({
|
|
171
|
+
assertion,
|
|
172
|
+
instruction: input.instruction,
|
|
173
|
+
origin: input.origin,
|
|
174
|
+
checkExistence: input.checkExistence,
|
|
175
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
176
|
+
});
|
|
177
|
+
}
|