@unson/brainbase-mcp 0.1.0 → 0.2.1

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.
@@ -0,0 +1,1474 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { closeSync, constants as fsConstants, fsyncSync, linkSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
+ const RUNTIME_VERSION = 'portable-judgment-runtime-1.0.0';
6
+ const ZERO_CALL_DISPLAY_LINE = '📚 Brainbase未参照: 必須参照なし・実呼び出し0回 ✓';
7
+ const PORTABLE_KNOWLEDGE_TOOLS = {
8
+ get_context: { auditKind: 'routing', label: 'Brainbase参照先' },
9
+ search: { auditKind: 'search', label: 'Brainbase検索' },
10
+ search_personal_kg: { auditKind: 'retrieval', label: 'Brainbase取得' }
11
+ };
12
+ const NODES = {
13
+ entry: { id: 'entry', kind: 'common', instruction: 'Enter once for this managed turn.' },
14
+ reconcile: { id: 'reconcile', kind: 'common', instruction: 'Classify the current request from canonical conversation context.' },
15
+ goal: { id: 'goal', kind: 'judgment', instruction: 'Fix the actual goal and success condition.' },
16
+ 'direct-answer': { id: 'direct-answer', kind: 'judgment', instruction: 'Answer the bounded question directly.' },
17
+ 'source-intent': { id: 'source-intent', kind: 'judgment', instruction: 'Identify the required knowledge and its trustworthy local source.' },
18
+ 'knowledge-handoff': { id: 'knowledge-handoff', kind: 'judgment', instruction: 'Retrieve only the knowledge needed for this turn.' },
19
+ 'problem-frame': { id: 'problem-frame', kind: 'judgment', instruction: 'Test whether the stated problem is only a symptom.' },
20
+ observe: { id: 'observe', kind: 'judgment', instruction: 'Separate observed evidence from explanations.' },
21
+ hypothesis: { id: 'hypothesis', kind: 'judgment', instruction: 'Form a causal hypothesis that the evidence can test.' },
22
+ verify: { id: 'verify', kind: 'judgment', instruction: 'Try to falsify the hypothesis before changing code.' },
23
+ implement: { id: 'implement', kind: 'judgment', instruction: 'Make the smallest change that satisfies the verified intent.' },
24
+ 'organization-context': { id: 'organization-context', kind: 'judgment', instruction: 'Identify actors, incentives, and decision ownership.' },
25
+ 'current-state': { id: 'current-state', kind: 'judgment', instruction: 'Verify the current operational state.' },
26
+ runbook: { id: 'runbook', kind: 'judgment', instruction: 'Select the current local runbook or explicit recovery procedure.' },
27
+ evidence: { id: 'evidence', kind: 'judgment', instruction: 'Define evidence for success and failure.' },
28
+ 'personal-context': { id: 'personal-context', kind: 'judgment', instruction: 'Use only owner-approved local context and decision principles.' },
29
+ 'authority-check': { id: 'authority-check', kind: 'constraint', instruction: 'Identify actor, scope, and approval authority separately from judgment.' },
30
+ 'enforcement-point': { id: 'enforcement-point', kind: 'constraint', instruction: 'Use the host or workflow permission boundary to enforce action authority.' },
31
+ clarification: { id: 'clarification', kind: 'clarification', instruction: 'Ask only for the missing referent or context, then continue normally.' },
32
+ merge: { id: 'merge', kind: 'common', instruction: 'Join only the selected judgment branches.' },
33
+ receipt: { id: 'receipt', kind: 'common', instruction: 'Use this request-bound receipt for the current turn only.' }
34
+ };
35
+ const DAGS = {
36
+ 'direct.v1': ['goal', 'direct-answer'],
37
+ 'knowledge.v1': ['source-intent', 'knowledge-handoff'],
38
+ 'engineering.v1': ['goal', 'problem-frame', 'observe', 'hypothesis', 'verify', 'implement'],
39
+ 'organization.v1': ['goal', 'organization-context'],
40
+ 'operations.v1': ['current-state', 'runbook', 'evidence'],
41
+ 'personal-judgment.v1': ['goal', 'personal-context'],
42
+ 'authority.v1': ['authority-check', 'enforcement-point'],
43
+ 'clarification.v1': ['clarification']
44
+ };
45
+ const MANIFEST_DIGEST = sha256(canonicalJson({ runtime_version: RUNTIME_VERSION, nodes: NODES, dags: DAGS }));
46
+ const INTENT_TERMS = [
47
+ ['operate', ['deploy', 'publish', 'restart', 'デプロイ', '公開', '再起動', '復旧', '運用']],
48
+ ['implement', ['implement', 'fix', 'change', 'update', 'delete', 'create', '実装', '修正', '変更', '更新', '削除', '作って']],
49
+ ['diagnose', ['why', 'cause', 'diagnose', 'なぜ', '原因', '診断', '何が問題', '欠陥']],
50
+ ['investigate', ['investigate', 'search', 'inspect', '調べ', '検索', '探して', '確認して', 'ログを見']],
51
+ ['design', ['design', 'derive', '設計', '考えて', 'どうすべき', '演繹']],
52
+ ['review', ['review', 'audit', 'レビュー', '評価して', '監査']],
53
+ ['answer', ['explain', 'what is', '説明', '教えて', '答えて', 'とは', '意味']]
54
+ ];
55
+ const DOMAIN_TERMS = [
56
+ ['operations', ['deploy', 'restart', 'runbook', 'incident', 'デプロイ', '再起動', '復旧', '運用', '障害']],
57
+ ['engineering', ['implementation', 'code', 'api', 'bug', 'test', 'architecture', 'refactor', '実装', 'コード', 'api', 'バグ', 'テスト', '設計', 'pr', 'リファクタ', 'resolver', 'host']],
58
+ ['knowledge', ['search', 'knowledge', 'source', '調べ', '検索', '資料', '情報', '知識']],
59
+ ['organization', ['organization', 'hiring', 'authority', '組織', '採用', '人事', '権限構造', 'インセンティブ']],
60
+ ['personal_judgment', ['my judgment', 'decision principle', '俺の判断', '私の判断', '判断基準', '思考アルゴリズム']]
61
+ ];
62
+ const FOLLOW_UP_TERMS = ['that', 'it', 'continue', 'do that', 'それ', 'その形', 'これ', 'こちら', '続けて', 'もう一度', 'では', 'それでいい', 'そうして'];
63
+ const AUTHORITY_INTENTS = new Set(['implement', 'operate']);
64
+ export function canonicalJson(value) {
65
+ if (value === null || typeof value === 'boolean' || typeof value === 'string')
66
+ return JSON.stringify(value);
67
+ if (typeof value === 'number') {
68
+ if (!Number.isFinite(value))
69
+ throw new TypeError('canonical JSON only supports finite numbers');
70
+ return JSON.stringify(value);
71
+ }
72
+ if (Array.isArray(value))
73
+ return `[${value.map((entry) => canonicalJson(entry)).join(',')}]`;
74
+ if (value && typeof value === 'object') {
75
+ return `{${Object.keys(value).sort(compareCodePoints).map((key) => {
76
+ const entry = value[key];
77
+ if (entry === undefined)
78
+ throw new TypeError('canonical JSON does not support undefined');
79
+ return `${JSON.stringify(key)}:${canonicalJson(entry)}`;
80
+ }).join(',')}}`;
81
+ }
82
+ throw new TypeError(`canonical JSON does not support ${typeof value}`);
83
+ }
84
+ function compareCodePoints(left, right) {
85
+ const a = Array.from(left, (value) => value.codePointAt(0));
86
+ const b = Array.from(right, (value) => value.codePointAt(0));
87
+ for (let index = 0; index < Math.min(a.length, b.length); index += 1) {
88
+ if (a[index] !== b[index])
89
+ return a[index] - b[index];
90
+ }
91
+ return a.length - b.length;
92
+ }
93
+ function sha256(value) {
94
+ return createHash('sha256').update(value).digest('hex');
95
+ }
96
+ function record(value) {
97
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
98
+ }
99
+ function contentText(content) {
100
+ if (typeof content === 'string')
101
+ return content;
102
+ if (!Array.isArray(content))
103
+ return '';
104
+ return content.map((item) => {
105
+ if (typeof item === 'string')
106
+ return item;
107
+ const block = record(item);
108
+ if (!block || !['input_text', 'output_text', 'text'].includes(String(block.type)))
109
+ return '';
110
+ return typeof block.text === 'string' ? block.text : '';
111
+ }).filter(Boolean).join('\n');
112
+ }
113
+ function isInjectedHostEnvelope(text) {
114
+ const trimmed = text.trimStart();
115
+ return trimmed.startsWith('<hook_prompt')
116
+ || trimmed.startsWith('<recommended_plugins>')
117
+ || trimmed.startsWith('# AGENTS.md instructions for ')
118
+ || trimmed.startsWith('<environment_context>')
119
+ || trimmed.startsWith('<app-context>');
120
+ }
121
+ function pathInside(path, root) {
122
+ const delta = relative(root, path);
123
+ return delta === '' || (!delta.startsWith(`..${sep}`) && delta !== '..' && !isAbsolute(delta));
124
+ }
125
+ function transcriptRoots(env) {
126
+ const configured = (env.BRAINBASE_JUDGMENT_TRANSCRIPT_ROOTS ?? '')
127
+ .split(delimiter)
128
+ .map((value) => value.trim())
129
+ .filter(Boolean);
130
+ const defaultCodexHome = join(homedir(), '.codex');
131
+ const defaultRoots = [...new Set([
132
+ env.CODEX_HOME ? join(env.CODEX_HOME, 'sessions') : null,
133
+ join(defaultCodexHome, 'sessions')
134
+ ].filter((value) => Boolean(value)))];
135
+ return (configured.length > 0 ? configured : defaultRoots)
136
+ .flatMap((path) => {
137
+ try {
138
+ return [realpathSync(path)];
139
+ }
140
+ catch {
141
+ return [];
142
+ }
143
+ });
144
+ }
145
+ function normalizeMessages(messages) {
146
+ return messages.flatMap((message, sequence) => {
147
+ if (!message || !['user', 'assistant'].includes(message.role) || typeof message.text !== 'string' || !message.text.trim())
148
+ return [];
149
+ if (isInjectedHostEnvelope(message.text))
150
+ return [];
151
+ return [{
152
+ sequence,
153
+ turn_id: typeof message.turn_id === 'string' ? message.turn_id : null,
154
+ role: message.role,
155
+ phase: typeof message.phase === 'string' ? message.phase : null,
156
+ text: message.text
157
+ }];
158
+ });
159
+ }
160
+ function readCanonicalTranscript(payload, env, trustedConversationMessages) {
161
+ if (Array.isArray(trustedConversationMessages)) {
162
+ return { messages: normalizeMessages(trustedConversationMessages), complete: true };
163
+ }
164
+ if (!payload.transcript_path)
165
+ return { messages: [], complete: false };
166
+ let canonicalPath;
167
+ try {
168
+ canonicalPath = realpathSync(payload.transcript_path);
169
+ if (!statSync(canonicalPath).isFile())
170
+ return { messages: [], complete: false };
171
+ }
172
+ catch {
173
+ return { messages: [], complete: false };
174
+ }
175
+ const roots = transcriptRoots(env);
176
+ if (roots.length === 0 || !roots.some((root) => pathInside(canonicalPath, root))) {
177
+ return { messages: [], complete: false };
178
+ }
179
+ const messages = [];
180
+ let sessionMatched = false;
181
+ let truncatedTail = false;
182
+ try {
183
+ const lines = readFileSync(canonicalPath, 'utf8').split('\n');
184
+ let lastNonEmptyIndex = -1;
185
+ for (let index = lines.length - 1; index >= 0; index -= 1) {
186
+ if (lines[index].trim()) {
187
+ lastNonEmptyIndex = index;
188
+ break;
189
+ }
190
+ }
191
+ for (let index = 0; index < lines.length; index += 1) {
192
+ const line = lines[index];
193
+ if (!line.trim())
194
+ continue;
195
+ let parsed;
196
+ try {
197
+ parsed = JSON.parse(line);
198
+ }
199
+ catch (error) {
200
+ if (index === lastNonEmptyIndex) {
201
+ truncatedTail = true;
202
+ break;
203
+ }
204
+ throw new Error(`judgment_transcript_corrupt_line_${index + 1}`, { cause: error });
205
+ }
206
+ const envelope = record(parsed);
207
+ const eventPayload = record(envelope?.payload);
208
+ if (!envelope || !eventPayload)
209
+ continue;
210
+ if (envelope.type === 'session_meta') {
211
+ const ids = [eventPayload.id, eventPayload.session_id].filter((value) => typeof value === 'string');
212
+ sessionMatched = sessionMatched || !payload.session_id || ids.includes(payload.session_id);
213
+ continue;
214
+ }
215
+ if (envelope.type !== 'response_item' || eventPayload.type !== 'message')
216
+ continue;
217
+ if (eventPayload.role !== 'user' && eventPayload.role !== 'assistant')
218
+ continue;
219
+ const text = contentText(eventPayload.content);
220
+ if (!text.trim())
221
+ continue;
222
+ const metadata = record(eventPayload.internal_chat_message_metadata_passthrough) ?? record(eventPayload.metadata);
223
+ messages.push({
224
+ role: eventPayload.role,
225
+ turn_id: typeof metadata?.turn_id === 'string'
226
+ ? metadata.turn_id
227
+ : typeof eventPayload.turn_id === 'string' ? eventPayload.turn_id : null,
228
+ phase: typeof metadata?.phase === 'string'
229
+ ? metadata.phase
230
+ : typeof eventPayload.phase === 'string' ? eventPayload.phase : null,
231
+ text
232
+ });
233
+ }
234
+ }
235
+ catch (error) {
236
+ if (error instanceof Error && error.message.startsWith('judgment_transcript_corrupt_line_'))
237
+ throw error;
238
+ return { messages: [], complete: false };
239
+ }
240
+ return {
241
+ messages: sessionMatched ? normalizeMessages(messages) : [],
242
+ complete: sessionMatched && !truncatedTail
243
+ };
244
+ }
245
+ function findRepoRoot(start) {
246
+ let current = resolve(start);
247
+ for (;;) {
248
+ try {
249
+ statSync(join(current, '.git'));
250
+ return current;
251
+ }
252
+ catch {
253
+ const parent = dirname(current);
254
+ if (parent === current)
255
+ return null;
256
+ current = parent;
257
+ }
258
+ }
259
+ }
260
+ function instructionBindings(cwd) {
261
+ const discoveredRoot = findRepoRoot(cwd);
262
+ if (!discoveredRoot)
263
+ return [];
264
+ const root = realpathSync(discoveredRoot);
265
+ const workingDirectory = realpathSync(resolve(cwd));
266
+ if (!pathInside(workingDirectory, root))
267
+ return [];
268
+ const paths = [join(root, 'AGENTS.md')];
269
+ const segments = relative(root, workingDirectory).split(sep).filter(Boolean);
270
+ let current = root;
271
+ for (const segment of segments) {
272
+ current = join(current, segment);
273
+ paths.push(join(current, 'AGENTS.md'));
274
+ }
275
+ const seen = new Set();
276
+ return paths.flatMap((path, index) => {
277
+ try {
278
+ const canonical = realpathSync(path);
279
+ if (seen.has(canonical) || !statSync(canonical).isFile())
280
+ return [];
281
+ seen.add(canonical);
282
+ return [{
283
+ scope: index === 0 ? 'repository' : 'directory',
284
+ source_ref: relative(root, canonical).replaceAll(sep, '/') || 'AGENTS.md',
285
+ digest: sha256(readFileSync(canonical))
286
+ }];
287
+ }
288
+ catch {
289
+ return [];
290
+ }
291
+ });
292
+ }
293
+ function journalRoot(env) {
294
+ if (env.BRAINBASE_JUDGMENT_JOURNAL_DIR)
295
+ return resolve(env.BRAINBASE_JUDGMENT_JOURNAL_DIR);
296
+ const personalOs = resolve(env.BRAINBASE_PERSONAL_OS_DIR ?? join(homedir(), '.brainbase', 'personal-os'));
297
+ return join(personalOs, 'judgment-journal');
298
+ }
299
+ function journalPaths(sessionRef, turnId, env) {
300
+ const directory = join(journalRoot(env), sessionRef);
301
+ const turnRef = sha256(turnId);
302
+ return {
303
+ directory,
304
+ target: join(directory, `${turnRef}.json`),
305
+ episode: join(directory, `${turnRef}.episode.json`),
306
+ events: join(directory, `${turnRef}.events`),
307
+ final: join(directory, `${turnRef}.final.json`),
308
+ continuation: join(directory, `${turnRef}.continuation.json`),
309
+ lock: join(directory, `${turnRef}.lock`)
310
+ };
311
+ }
312
+ function acceptedProjection(receipt) {
313
+ return {
314
+ turn_id: receipt.turn_id,
315
+ resolution_id: receipt.resolution_id,
316
+ request_digest: receipt.request_digest,
317
+ context_digest: receipt.context_digest,
318
+ plan_digest: receipt.plan_digest,
319
+ status: receipt.status,
320
+ classification: receipt.classification,
321
+ classification_evidence: receipt.classification_evidence,
322
+ selected_dag_ids: receipt.selected_dag_ids
323
+ };
324
+ }
325
+ function readAdoptionEntry(path) {
326
+ let parsed;
327
+ try {
328
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
329
+ }
330
+ catch (error) {
331
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
332
+ throw error;
333
+ throw new Error(`judgment_journal_corrupt:${basename(path)}`, { cause: error });
334
+ }
335
+ const entry = record(parsed);
336
+ const request = record(entry?.request);
337
+ const receipt = record(entry?.receipt);
338
+ const supportedSchema = entry?.schema_version === 'brainbase-judgment-adoption-v1'
339
+ || entry?.schema_version === 'brainbase-judgment-adoption-v2';
340
+ if (!supportedSchema
341
+ || typeof entry.accepted_at !== 'string'
342
+ || typeof entry.request_text_digest !== 'string'
343
+ || !request
344
+ || typeof request.request !== 'string'
345
+ || typeof request.turn_id !== 'string'
346
+ || !record(request.conversation_context)
347
+ || !receipt) {
348
+ throw new Error(`judgment_journal_invalid:${basename(path)}`);
349
+ }
350
+ if (entry.request_text_digest !== sha256(request.request)) {
351
+ throw new Error(`judgment_journal_request_digest_mismatch:${basename(path)}`);
352
+ }
353
+ const verifiedReceipt = verifyReceipt(receipt, request);
354
+ if (entry.schema_version === 'brainbase-judgment-adoption-v2') {
355
+ if (typeof entry.receipt_digest !== 'string'
356
+ || entry.receipt_digest !== sha256(canonicalJson(verifiedReceipt))) {
357
+ throw new Error(`judgment_adoption_receipt_digest_mismatch:${basename(path)}`);
358
+ }
359
+ return {
360
+ schema_version: 'brainbase-judgment-adoption-v2',
361
+ accepted_at: entry.accepted_at,
362
+ request_text_digest: entry.request_text_digest,
363
+ request,
364
+ receipt: verifiedReceipt,
365
+ receipt_digest: entry.receipt_digest,
366
+ owner_audit: verifyOwnerAudit(entry.owner_audit, verifiedReceipt)
367
+ };
368
+ }
369
+ return {
370
+ schema_version: 'brainbase-judgment-adoption-v1',
371
+ accepted_at: entry.accepted_at,
372
+ request_text_digest: entry.request_text_digest,
373
+ request,
374
+ receipt: verifiedReceipt,
375
+ owner_audit: buildOwnerAudit(request, verifiedReceipt, { historicalExact: false })
376
+ };
377
+ }
378
+ function priorReceipts(sessionRef, currentTurnId, env) {
379
+ const { directory, target } = journalPaths(sessionRef, currentTurnId, env);
380
+ let names;
381
+ try {
382
+ names = readdirSync(directory).filter((name) => /^[a-f0-9]{64}\.json$/u.test(name));
383
+ }
384
+ catch (error) {
385
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
386
+ return [];
387
+ throw error;
388
+ }
389
+ return names.filter((name) => name !== basename(target)).map((name) => {
390
+ const entry = readAdoptionEntry(join(directory, name));
391
+ if (entry.request.conversation_context.session_ref !== sessionRef) {
392
+ throw new Error(`judgment_journal_session_mismatch:${name}`);
393
+ }
394
+ return { accepted_at: entry.accepted_at, projection: acceptedProjection(entry.receipt) };
395
+ }).sort((left, right) => left.accepted_at.localeCompare(right.accepted_at))
396
+ .map((entry) => entry.projection);
397
+ }
398
+ export function buildJudgmentRequest(payload, { env = process.env, trustedConversationMessages } = {}) {
399
+ const request = typeof payload.prompt === 'string' && payload.prompt.trim() ? payload.prompt : '';
400
+ const turnId = typeof payload.turn_id === 'string' ? payload.turn_id.trim() : '';
401
+ const sessionId = typeof payload.session_id === 'string' ? payload.session_id : '';
402
+ if (!request || !turnId || !sessionId)
403
+ throw new TypeError('UserPromptSubmit requires prompt, turn_id, and session_id');
404
+ const sessionRef = sha256(sessionId);
405
+ const transcript = readCanonicalTranscript(payload, env, trustedConversationMessages);
406
+ const messages = transcript.messages.filter((message) => !(message.role === 'user' && message.turn_id === turnId && message.text === request));
407
+ const trailingMessage = messages.at(-1);
408
+ if (trailingMessage?.role === 'user' && trailingMessage.turn_id === null && trailingMessage.text === request) {
409
+ trailingMessage.turn_id = turnId;
410
+ }
411
+ else {
412
+ messages.push({ sequence: messages.length, turn_id: turnId, role: 'user', phase: null, text: request });
413
+ }
414
+ const cwd = typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd();
415
+ const repoRoot = findRepoRoot(cwd);
416
+ const projectBinding = env.BRAINBASE_JUDGMENT_PROJECT_CODE
417
+ ?? (repoRoot ? basename(repoRoot).replace(/\.git$/u, '') : null);
418
+ const contextWithoutDigest = {
419
+ schema_version: 'brainbase-conversation-context-v1',
420
+ session_ref: sessionRef,
421
+ messages: messages.map((message, sequence) => ({ ...message, sequence })),
422
+ prior_receipts: priorReceipts(sessionRef, turnId, env),
423
+ runtime: {
424
+ host: 'codex',
425
+ model: typeof payload.model === 'string' && payload.model ? payload.model : null,
426
+ permission_mode: typeof payload.permission_mode === 'string' && payload.permission_mode ? payload.permission_mode : null,
427
+ project_binding: projectBinding
428
+ },
429
+ instruction_bindings: instructionBindings(cwd),
430
+ completeness: transcript.complete ? 'complete' : 'partial'
431
+ };
432
+ return {
433
+ request,
434
+ turn_id: turnId,
435
+ ...(projectBinding ? { project_code: projectBinding } : {}),
436
+ conversation_context: {
437
+ ...contextWithoutDigest,
438
+ source_digest: sha256(canonicalJson(contextWithoutDigest))
439
+ }
440
+ };
441
+ }
442
+ function includesTerm(text, terms) {
443
+ const normalized = text.toLocaleLowerCase();
444
+ return terms.some((term) => {
445
+ const normalizedTerm = term.toLocaleLowerCase();
446
+ if (!/^[a-z0-9_-]+$/u.test(normalizedTerm))
447
+ return normalized.includes(normalizedTerm);
448
+ const escaped = normalizedTerm.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
449
+ return new RegExp(`(^|[^a-z0-9_])${escaped}([^a-z0-9_]|$)`, 'u').test(normalized);
450
+ });
451
+ }
452
+ function matchIntent(text) {
453
+ return INTENT_TERMS.find(([, terms]) => includesTerm(text, terms))?.[0] ?? null;
454
+ }
455
+ function matchDomains(text) {
456
+ return DOMAIN_TERMS.filter(([, terms]) => includesTerm(text, terms)).map(([domain]) => domain);
457
+ }
458
+ function priorClassification(request) {
459
+ const receipts = request.conversation_context.prior_receipts;
460
+ for (let index = receipts.length - 1; index >= 0; index -= 1) {
461
+ const projection = record(receipts[index]);
462
+ const classification = record(projection?.classification);
463
+ if (classification && Array.isArray(classification.domains) && typeof classification.intent === 'string') {
464
+ return {
465
+ classification,
466
+ source: 'prior_receipt',
467
+ turnIds: typeof projection?.turn_id === 'string' ? [projection.turn_id] : []
468
+ };
469
+ }
470
+ }
471
+ const priorUsers = request.conversation_context.messages
472
+ .filter((message) => message.role === 'user' && message.turn_id !== request.turn_id)
473
+ .reverse();
474
+ for (const message of priorUsers) {
475
+ const intent = matchIntent(message.text);
476
+ const domains = matchDomains(message.text);
477
+ if (intent || domains.length > 0) {
478
+ return {
479
+ classification: {
480
+ intent: intent ?? 'answer',
481
+ domains: domains.length > 0 ? domains : ['general'],
482
+ confidence: 'inferred'
483
+ },
484
+ source: 'prior_message',
485
+ turnIds: message.turn_id ? [message.turn_id] : []
486
+ };
487
+ }
488
+ }
489
+ return null;
490
+ }
491
+ function dagForDomain(domain) {
492
+ return {
493
+ general: 'direct.v1',
494
+ knowledge: 'knowledge.v1',
495
+ engineering: 'engineering.v1',
496
+ organization: 'organization.v1',
497
+ operations: 'operations.v1',
498
+ personal_judgment: 'personal-judgment.v1'
499
+ }[domain];
500
+ }
501
+ function buildActiveGraph(dagIds) {
502
+ const activeNodes = ['entry', 'reconcile'];
503
+ const edges = [['entry', 'reconcile']];
504
+ for (const dagId of dagIds) {
505
+ const path = DAGS[dagId];
506
+ if (!path)
507
+ throw new Error(`unknown judgment DAG: ${dagId}`);
508
+ if (path[0])
509
+ edges.push(['reconcile', path[0]]);
510
+ for (let index = 0; index < path.length; index += 1) {
511
+ const node = path[index];
512
+ if (!activeNodes.includes(node))
513
+ activeNodes.push(node);
514
+ const next = path[index + 1];
515
+ if (next)
516
+ edges.push([node, next]);
517
+ }
518
+ if (path.at(-1))
519
+ edges.push([path.at(-1), 'merge']);
520
+ }
521
+ activeNodes.push('merge', 'receipt');
522
+ edges.push(['merge', 'receipt']);
523
+ return {
524
+ active_nodes: activeNodes,
525
+ active_edges: edges,
526
+ active_node_definitions: activeNodes.map((id) => NODES[id])
527
+ };
528
+ }
529
+ export async function resolveLocalJudgment(request, options = {}) {
530
+ const now = options.now ?? (() => new Date());
531
+ const id = options.id ?? randomUUID;
532
+ const followsPrior = includesTerm(request.request, FOLLOW_UP_TERMS);
533
+ const prior = followsPrior ? priorClassification(request) : null;
534
+ const detectedIntent = matchIntent(request.request);
535
+ const detectedDomains = matchDomains(request.request);
536
+ if (followsPrior && !prior && detectedDomains.length === 0) {
537
+ return makeReceipt(request, {
538
+ id: id(),
539
+ now: now(),
540
+ status: 'needs_clarification',
541
+ classification: null,
542
+ evidence: { source: 'resolver', source_turn_ids: [], matcher_ids: [] },
543
+ reasons: ['conversation_referent_missing'],
544
+ dagIds: ['clarification.v1'],
545
+ unresolved: ['conversation_referent'],
546
+ rationale: ['The Host kept the turn managed and selected a clarification instead of refusing the response.']
547
+ });
548
+ }
549
+ const inheritedDomains = detectedDomains.length === 0 && prior ? prior.classification.domains : [];
550
+ const domains = detectedDomains.length > 0
551
+ ? detectedDomains
552
+ : inheritedDomains.length > 0 ? inheritedDomains : ['general'];
553
+ const intent = detectedIntent ?? prior?.classification.intent ?? 'answer';
554
+ const inherited = detectedDomains.length === 0 && Boolean(prior);
555
+ const classification = {
556
+ intent,
557
+ domains,
558
+ confidence: inherited ? 'inferred' : 'confirmed'
559
+ };
560
+ const dagIds = [...new Set(domains.map(dagForDomain))];
561
+ if (AUTHORITY_INTENTS.has(intent))
562
+ dagIds.push('authority.v1');
563
+ const matcherIds = [
564
+ ...(detectedIntent ? [`intent:${detectedIntent}`] : []),
565
+ ...detectedDomains.map((domain) => `domain:${domain}`)
566
+ ];
567
+ return makeReceipt(request, {
568
+ id: id(),
569
+ now: now(),
570
+ status: 'resolved',
571
+ classification,
572
+ evidence: {
573
+ source: inherited && prior ? prior.source : 'current_request',
574
+ source_turn_ids: inherited && prior ? prior.turnIds : [request.turn_id],
575
+ matcher_ids: matcherIds
576
+ },
577
+ reasons: inherited ? ['classification_inherited_from_prior_turn'] : [],
578
+ dagIds,
579
+ unresolved: [],
580
+ rationale: ['The local Resolver selected only the judgment branches required by this turn.']
581
+ });
582
+ }
583
+ function makeReceipt(request, input) {
584
+ const graph = buildActiveGraph(input.dagIds);
585
+ const receiptWithoutPlan = {
586
+ resolution_id: input.id,
587
+ resolved_at: input.now.toISOString(),
588
+ turn_id: request.turn_id,
589
+ request_digest: sha256(canonicalJson(request)),
590
+ context_digest: sha256(canonicalJson(request.conversation_context)),
591
+ status: input.status,
592
+ runtime_version: RUNTIME_VERSION,
593
+ manifest_digest: MANIFEST_DIGEST,
594
+ host_binding: {
595
+ adapter_id: 'brainbase-local',
596
+ adapter_version: '1',
597
+ status: 'managed',
598
+ enforcement_level: 'host_contract'
599
+ },
600
+ ...(request.project_code ? { project_code: request.project_code } : {}),
601
+ classification: input.classification,
602
+ classification_evidence: input.evidence,
603
+ reconciliation_reasons: input.reasons,
604
+ selected_dag_ids: input.dagIds,
605
+ applicable_policies: [{
606
+ id: 'portable.action-authorization-separate.v1',
607
+ instruction: 'A judgment receipt never authorizes a write or external action.'
608
+ }],
609
+ ...graph,
610
+ unresolved: input.unresolved,
611
+ rationale: input.rationale
612
+ };
613
+ return {
614
+ ...receiptWithoutPlan,
615
+ plan_digest: receiptPlanDigest(receiptWithoutPlan)
616
+ };
617
+ }
618
+ function receiptPlanDigest(receipt) {
619
+ const planValue = { ...receipt };
620
+ delete planValue.resolution_id;
621
+ delete planValue.resolved_at;
622
+ delete planValue.request_digest;
623
+ delete planValue.plan_digest;
624
+ return sha256(canonicalJson(planValue));
625
+ }
626
+ function verifyReceipt(receipt, request) {
627
+ if (!record(receipt) || receipt.turn_id !== request.turn_id)
628
+ throw new Error('judgment_receipt_turn_mismatch');
629
+ if (receipt.request_digest !== sha256(canonicalJson(request)))
630
+ throw new Error('judgment_receipt_request_mismatch');
631
+ if (receipt.context_digest !== sha256(canonicalJson(request.conversation_context)))
632
+ throw new Error('judgment_receipt_context_mismatch');
633
+ if (receipt.runtime_version !== RUNTIME_VERSION || receipt.manifest_digest !== MANIFEST_DIGEST) {
634
+ throw new Error('judgment_receipt_runtime_mismatch');
635
+ }
636
+ if (receipt.host_binding?.adapter_id !== 'brainbase-local'
637
+ || receipt.host_binding.adapter_version !== '1'
638
+ || receipt.host_binding.status !== 'managed'
639
+ || receipt.host_binding.enforcement_level !== 'host_contract') {
640
+ throw new Error('judgment_receipt_binding_unmanaged');
641
+ }
642
+ if (!Array.isArray(receipt.active_node_definitions) || receipt.active_node_definitions.length === 0) {
643
+ throw new Error('judgment_receipt_active_nodes_missing');
644
+ }
645
+ if (receipt.plan_digest !== receiptPlanDigest(receipt)) {
646
+ throw new Error('judgment_receipt_plan_mismatch');
647
+ }
648
+ return receipt;
649
+ }
650
+ function existingAdoption(request, env) {
651
+ const { target } = journalPaths(request.conversation_context.session_ref, request.turn_id, env);
652
+ try {
653
+ const entry = readAdoptionEntry(target);
654
+ if (entry.request_text_digest !== sha256(request.request)
655
+ || entry.request.turn_id !== request.turn_id
656
+ || entry.request.conversation_context.session_ref !== request.conversation_context.session_ref) {
657
+ throw new Error('judgment_turn_receipt_conflict');
658
+ }
659
+ return entry;
660
+ }
661
+ catch (error) {
662
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
663
+ return null;
664
+ throw error;
665
+ }
666
+ }
667
+ function existingAdoptionForPayload(payload, env) {
668
+ const request = typeof payload.prompt === 'string' && payload.prompt.trim() ? payload.prompt : '';
669
+ const turnId = typeof payload.turn_id === 'string' ? payload.turn_id.trim() : '';
670
+ const sessionId = typeof payload.session_id === 'string' ? payload.session_id : '';
671
+ if (!request || !turnId || !sessionId)
672
+ return null;
673
+ const sessionRef = sha256(sessionId);
674
+ const { target } = journalPaths(sessionRef, turnId, env);
675
+ try {
676
+ const entry = readAdoptionEntry(target);
677
+ if (entry.request_text_digest !== sha256(request)
678
+ || entry.request.turn_id !== turnId
679
+ || entry.request.conversation_context.session_ref !== sessionRef) {
680
+ throw new Error('judgment_turn_receipt_conflict');
681
+ }
682
+ return entry;
683
+ }
684
+ catch (error) {
685
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
686
+ return null;
687
+ throw error;
688
+ }
689
+ }
690
+ function adoptReceipt(request, receipt, env) {
691
+ const { directory, target } = journalPaths(request.conversation_context.session_ref, request.turn_id, env);
692
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
693
+ const ownerAudit = buildOwnerAudit(request, receipt);
694
+ const entry = {
695
+ schema_version: 'brainbase-judgment-adoption-v2',
696
+ accepted_at: new Date().toISOString(),
697
+ request_text_digest: sha256(request.request),
698
+ request,
699
+ receipt,
700
+ receipt_digest: sha256(canonicalJson(receipt)),
701
+ owner_audit: ownerAudit
702
+ };
703
+ const temp = join(directory, `.${basename(target)}.${process.pid}.${randomUUID()}.tmp`);
704
+ let descriptor = null;
705
+ try {
706
+ descriptor = openSync(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
707
+ writeFileSync(descriptor, `${JSON.stringify(entry)}\n`);
708
+ fsyncSync(descriptor);
709
+ closeSync(descriptor);
710
+ descriptor = null;
711
+ try {
712
+ linkSync(temp, target);
713
+ return entry;
714
+ }
715
+ catch (error) {
716
+ if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST'))
717
+ throw error;
718
+ return existingAdoption(request, env);
719
+ }
720
+ }
721
+ finally {
722
+ if (descriptor !== null) {
723
+ try {
724
+ closeSync(descriptor);
725
+ }
726
+ catch {
727
+ // Preserve the primary write error.
728
+ }
729
+ }
730
+ try {
731
+ unlinkSync(temp);
732
+ }
733
+ catch {
734
+ // The target link is authoritative; temporary cleanup is best effort.
735
+ }
736
+ }
737
+ }
738
+ function artifactDigest(value, digestKey) {
739
+ const projection = { ...value };
740
+ delete projection[digestKey];
741
+ return sha256(canonicalJson(projection));
742
+ }
743
+ function readJsonArtifact(path) {
744
+ try {
745
+ const value = JSON.parse(readFileSync(path, 'utf8'));
746
+ if (!record(value))
747
+ throw new Error('not an object');
748
+ return value;
749
+ }
750
+ catch (error) {
751
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
752
+ throw error;
753
+ throw new Error(`judgment_journal_corrupt:${basename(path)}`, { cause: error });
754
+ }
755
+ }
756
+ function writeImmutableJson(path, value) {
757
+ const directory = dirname(path);
758
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
759
+ const temp = join(directory, `.${basename(path)}.${process.pid}.${randomUUID()}.tmp`);
760
+ let descriptor = null;
761
+ try {
762
+ descriptor = openSync(temp, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
763
+ writeFileSync(descriptor, `${JSON.stringify(value)}\n`);
764
+ fsyncSync(descriptor);
765
+ closeSync(descriptor);
766
+ descriptor = null;
767
+ linkSync(temp, path);
768
+ }
769
+ finally {
770
+ if (descriptor !== null) {
771
+ try {
772
+ closeSync(descriptor);
773
+ }
774
+ catch { /* Preserve the primary error. */ }
775
+ }
776
+ try {
777
+ unlinkSync(temp);
778
+ }
779
+ catch { /* The immutable target is authoritative. */ }
780
+ }
781
+ }
782
+ async function withTurnLock(lockPath, operation) {
783
+ mkdirSync(dirname(lockPath), { recursive: true, mode: 0o700 });
784
+ let descriptor = null;
785
+ for (let attempt = 0; attempt < 100; attempt += 1) {
786
+ try {
787
+ descriptor = openSync(lockPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
788
+ writeFileSync(descriptor, `${process.pid}\n`);
789
+ break;
790
+ }
791
+ catch (error) {
792
+ if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST'))
793
+ throw error;
794
+ await new Promise((resolveWait) => setTimeout(resolveWait, 10));
795
+ }
796
+ }
797
+ if (descriptor === null)
798
+ throw new Error('judgment_episode_lock_timeout');
799
+ let operationError;
800
+ try {
801
+ return await operation();
802
+ }
803
+ catch (error) {
804
+ operationError = error;
805
+ throw error;
806
+ }
807
+ finally {
808
+ let releaseError;
809
+ try {
810
+ closeSync(descriptor);
811
+ }
812
+ catch (error) {
813
+ releaseError = error;
814
+ }
815
+ try {
816
+ unlinkSync(lockPath);
817
+ }
818
+ catch (error) {
819
+ releaseError ??= error;
820
+ }
821
+ if (releaseError && operationError === undefined) {
822
+ throw new Error('judgment_episode_lock_release_failed', { cause: releaseError });
823
+ }
824
+ }
825
+ }
826
+ function readEpisode(path) {
827
+ const value = readJsonArtifact(path);
828
+ if (value.schema_version !== 'brainbase-judgment-episode-v1'
829
+ || typeof value.session_ref !== 'string'
830
+ || typeof value.turn_id !== 'string'
831
+ || typeof value.started_at !== 'string'
832
+ || typeof value.request_text_digest !== 'string'
833
+ || typeof value.initial_receipt_digest !== 'string'
834
+ || !record(value.adoption)
835
+ || !record(value.audit_contract)
836
+ || typeof value.episode_digest !== 'string') {
837
+ throw new Error(`judgment_journal_invalid:${basename(path)}`);
838
+ }
839
+ if (value.episode_digest !== artifactDigest(value, 'episode_digest')) {
840
+ throw new Error(`judgment_episode_digest_mismatch:${basename(path)}`);
841
+ }
842
+ const adoption = value.adoption;
843
+ const auditContract = value.audit_contract;
844
+ if (auditContract.schema_version !== 'brainbase-owner-audit-contract-v1'
845
+ || auditContract.repair_body_policy !== 'preserve'
846
+ || (auditContract.zero_call_display_line !== null && typeof auditContract.zero_call_display_line !== 'string')
847
+ || (auditContract.zero_call_display_line_digest !== null && typeof auditContract.zero_call_display_line_digest !== 'string')
848
+ || (typeof auditContract.zero_call_display_line === 'string'
849
+ && auditContract.zero_call_display_line_digest !== sha256(auditContract.zero_call_display_line))
850
+ || (auditContract.zero_call_display_line === null && auditContract.zero_call_display_line_digest !== null)) {
851
+ throw new Error(`judgment_owner_audit_contract_invalid:${basename(path)}`);
852
+ }
853
+ if (value.initial_receipt_digest !== sha256(canonicalJson(adoption.receipt))) {
854
+ throw new Error(`judgment_episode_receipt_digest_mismatch:${basename(path)}`);
855
+ }
856
+ return value;
857
+ }
858
+ function verifyEpisodeAdoption(episode, adoptionPath) {
859
+ const adoption = readAdoptionEntry(adoptionPath);
860
+ if (sha256(canonicalJson(adoption.receipt)) !== episode.initial_receipt_digest
861
+ || adoption.request.turn_id !== episode.turn_id
862
+ || adoption.request.conversation_context.session_ref !== episode.session_ref) {
863
+ throw new Error('judgment_episode_adoption_mismatch');
864
+ }
865
+ }
866
+ function readToolEvent(path) {
867
+ const value = readJsonArtifact(path);
868
+ if (value.schema_version !== 'brainbase-judgment-tool-event-v1'
869
+ || typeof value.session_ref !== 'string'
870
+ || typeof value.turn_id !== 'string'
871
+ || typeof value.sequence !== 'number'
872
+ || typeof value.recorded_at !== 'string'
873
+ || typeof value.tool_use_id !== 'string'
874
+ || typeof value.tool_name !== 'string'
875
+ || !['routing', 'search', 'retrieval'].includes(String(value.audit_kind))
876
+ || typeof value.success !== 'boolean'
877
+ || !Array.isArray(value.satisfies)
878
+ || !value.satisfies.every((capability) => typeof capability === 'string')
879
+ || typeof value.display_line !== 'string'
880
+ || typeof value.event_fingerprint !== 'string'
881
+ || typeof value.event_digest !== 'string') {
882
+ throw new Error(`judgment_journal_invalid:${basename(path)}`);
883
+ }
884
+ if (value.event_digest !== artifactDigest(value, 'event_digest')) {
885
+ throw new Error(`judgment_tool_event_digest_mismatch:${basename(path)}`);
886
+ }
887
+ return value;
888
+ }
889
+ function readEvents(directory) {
890
+ let names;
891
+ try {
892
+ names = readdirSync(directory).filter((name) => name.endsWith('.json')).sort();
893
+ }
894
+ catch (error) {
895
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
896
+ return [];
897
+ throw error;
898
+ }
899
+ const events = names.map((name) => readToolEvent(join(directory, name)));
900
+ events.forEach((event, index) => {
901
+ if (event.sequence !== index + 1)
902
+ throw new Error('judgment_tool_event_sequence_invalid');
903
+ });
904
+ return events;
905
+ }
906
+ function readFinal(path) {
907
+ const value = readJsonArtifact(path);
908
+ if (value.schema_version !== 'brainbase-judgment-final-v1'
909
+ || typeof value.session_ref !== 'string'
910
+ || typeof value.turn_id !== 'string'
911
+ || typeof value.finalized_at !== 'string'
912
+ || typeof value.initial_receipt_digest !== 'string'
913
+ || typeof value.event_count !== 'number'
914
+ || typeof value.event_set_digest !== 'string'
915
+ || typeof value.answer_digest !== 'string'
916
+ || typeof value.final_digest !== 'string') {
917
+ throw new Error(`judgment_journal_invalid:${basename(path)}`);
918
+ }
919
+ if (value.final_digest !== artifactDigest(value, 'final_digest')) {
920
+ throw new Error(`judgment_final_digest_mismatch:${basename(path)}`);
921
+ }
922
+ return value;
923
+ }
924
+ function readContinuation(path) {
925
+ const value = readJsonArtifact(path);
926
+ if (value.schema_version !== 'brainbase-judgment-continuation-v1'
927
+ || typeof value.requested_at !== 'string'
928
+ || !Array.isArray(value.missing_capabilities)
929
+ || typeof value.expected_audit_lines_digest !== 'string'
930
+ || typeof value.continuation_digest !== 'string') {
931
+ throw new Error(`judgment_journal_invalid:${basename(path)}`);
932
+ }
933
+ if (value.continuation_digest !== artifactDigest(value, 'continuation_digest')) {
934
+ throw new Error(`judgment_continuation_digest_mismatch:${basename(path)}`);
935
+ }
936
+ if (value.answer_body_binding !== undefined) {
937
+ const binding = record(value.answer_body_binding);
938
+ if (!binding || binding.schema_version !== 'brainbase-answer-body-binding-v1'
939
+ || typeof binding.body_digest !== 'string'
940
+ || typeof binding.character_count !== 'number') {
941
+ throw new Error(`judgment_answer_body_binding_invalid:${basename(path)}`);
942
+ }
943
+ }
944
+ return value;
945
+ }
946
+ function requiredAuditLines(episode, events) {
947
+ return [
948
+ episode.adoption.owner_audit.display_line,
949
+ ...(events.length === 0 && episode.audit_contract.zero_call_display_line
950
+ ? [episode.audit_contract.zero_call_display_line]
951
+ : []),
952
+ ...events.map((event) => event.display_line)
953
+ ];
954
+ }
955
+ function answerContainsExactAuditPrefix(answer, expectedLines) {
956
+ const lines = answer.replaceAll('\r\n', '\n').split('\n').map((line) => line.replace(/[ \t]+$/u, ''));
957
+ const expected = expectedLines.map((line) => line.replace(/[ \t]+$/u, ''));
958
+ if (!expected.every((line, index) => lines[index] === line))
959
+ return false;
960
+ const counts = new Map(expected.map((line) => [line, expected.filter((candidate) => candidate === line).length]));
961
+ return [...counts].every(([line, count]) => lines.filter((candidate) => candidate === line).length === count);
962
+ }
963
+ function normalizedAnswerBody(answer, expectedLines) {
964
+ const auditLines = new Set(expectedLines.map((line) => line.replace(/[ \t]+$/u, '')));
965
+ const bodyLines = answer.replaceAll('\r\n', '\n').split('\n')
966
+ .map((line) => line.replace(/[ \t]+$/u, ''))
967
+ .filter((line) => !auditLines.has(line));
968
+ while (bodyLines.length > 0 && (bodyLines[0] === '' || /^(?:🧠 |📚 Brainbase|⚠️ (?:Brainbase|判断))/u.test(bodyLines[0])))
969
+ bodyLines.shift();
970
+ while (bodyLines.at(-1) === '')
971
+ bodyLines.pop();
972
+ return bodyLines.join('\n');
973
+ }
974
+ const OWNER_EXCERPT_LIMIT = 26;
975
+ const PRIOR_EVIDENCE_SOURCES = new Set([
976
+ 'prior_receipt',
977
+ 'prior_message'
978
+ ]);
979
+ function sanitizeOwnerExcerpt(value) {
980
+ const redacted = String(value ?? '')
981
+ .replace(/\b(token|api[_-]?key|secret|password)\s*=\s*[^\s]+/giu, '$1=[秘密情報]')
982
+ .replace(/\b(?:sk-[a-z0-9_-]{8,}|ghp_[a-z0-9_]{8,}|github_pat_[a-z0-9_]{8,}|xox[a-z]-[a-z0-9-]{8,}|AIza[a-z0-9_-]{8,})\b/giu, '[秘密情報]')
983
+ .replace(/[\u0000-\u001f\u007f]+/gu, ' ')
984
+ .replace(/[「」]/gu, ' ')
985
+ .replace(/\s+/gu, ' ')
986
+ .trim();
987
+ const points = Array.from(redacted);
988
+ return points.length > OWNER_EXCERPT_LIMIT
989
+ ? `${points.slice(0, OWNER_EXCERPT_LIMIT).join('')}…`
990
+ : redacted;
991
+ }
992
+ function ownerEvidenceSource(request, receipt) {
993
+ const sourceTurnIds = Array.isArray(receipt.classification_evidence?.source_turn_ids)
994
+ ? receipt.classification_evidence.source_turn_ids.filter((turnId) => typeof turnId === 'string')
995
+ : [];
996
+ const inherited = receipt.reconciliation_reasons?.includes('classification_inherited_from_prior_turn');
997
+ const prior = PRIOR_EVIDENCE_SOURCES.has(receipt.classification_evidence?.source) || inherited;
998
+ if (!prior) {
999
+ return { sourceKind: 'current_request', text: request.request, sourceTurnIds };
1000
+ }
1001
+ const messages = Array.isArray(request.conversation_context?.messages)
1002
+ ? request.conversation_context.messages
1003
+ : [];
1004
+ const exact = [...messages].reverse().find((message) => (message.role === 'user'
1005
+ && message.turn_id !== request.turn_id
1006
+ && typeof message.turn_id === 'string'
1007
+ && sourceTurnIds.includes(message.turn_id)));
1008
+ const fallback = sourceTurnIds.length === 0
1009
+ ? [...messages].reverse().find((message) => message.role === 'user' && message.turn_id !== request.turn_id)
1010
+ : undefined;
1011
+ return {
1012
+ sourceKind: exact ? 'prior_turn' : fallback ? 'prior_turn_fallback' : 'prior_turn_unavailable',
1013
+ text: exact?.text ?? fallback?.text ?? '',
1014
+ sourceTurnIds
1015
+ };
1016
+ }
1017
+ function ownerDecision(receipt) {
1018
+ if (receipt.selected_dag_ids?.includes('knowledge.v1'))
1019
+ return 'Brainbase内検索が必要と判断';
1020
+ const byIntent = {
1021
+ implement: '実装依頼として継続',
1022
+ investigate: '調査として確認',
1023
+ diagnose: '原因診断として確認',
1024
+ review: 'レビューとして確認',
1025
+ design: '設計として検討',
1026
+ answer: '質問として回答',
1027
+ operate: '運用依頼として対応'
1028
+ };
1029
+ return receipt.classification?.intent
1030
+ ? byIntent[receipt.classification.intent] ?? '回答方針を確認'
1031
+ : '回答方針を確認';
1032
+ }
1033
+ function verifyOwnerAudit(ownerAuditValue, receipt) {
1034
+ const ownerAudit = record(ownerAuditValue);
1035
+ if (!ownerAudit || ownerAudit.schema_version !== 'brainbase-owner-audit-v1') {
1036
+ throw new Error('judgment_owner_audit_missing');
1037
+ }
1038
+ if (ownerAudit.source_receipt_digest !== sha256(canonicalJson(receipt))) {
1039
+ throw new Error('judgment_owner_audit_receipt_mismatch');
1040
+ }
1041
+ if (typeof ownerAudit.display_line !== 'string'
1042
+ || ownerAudit.text_digest !== sha256(ownerAudit.display_line)) {
1043
+ throw new Error('judgment_owner_audit_digest_mismatch');
1044
+ }
1045
+ return ownerAudit;
1046
+ }
1047
+ export function buildOwnerAudit(request, receipt, { historicalExact = true } = {}) {
1048
+ const evidence = ownerEvidenceSource(request, receipt);
1049
+ const excerpt = sanitizeOwnerExcerpt(evidence.text);
1050
+ let decision = ownerDecision(receipt);
1051
+ let displayLine;
1052
+ if (receipt.status === 'needs_clarification' || receipt.selected_dag_ids.includes('clarification.v1')) {
1053
+ decision = '確認質問';
1054
+ displayLine = `⚠️ 判断参照: 「${excerpt || '現在の依頼'}」の対象を特定できず → ${decision}`;
1055
+ }
1056
+ else if (evidence.sourceKind === 'prior_turn_unavailable') {
1057
+ decision = '判断証跡を要確認';
1058
+ displayLine = `⚠️ 判断参照: 参照元の会話を確認できず → ${decision}`;
1059
+ }
1060
+ else {
1061
+ const prefix = evidence.sourceKind.startsWith('prior_turn') ? '直前の' : '';
1062
+ displayLine = `🧠 判断参照: ${prefix}「${excerpt || '現在の依頼'}」を参照 → ${decision} ✓`;
1063
+ }
1064
+ return {
1065
+ schema_version: 'brainbase-owner-audit-v1',
1066
+ renderer_version: '1',
1067
+ locale: 'ja-JP',
1068
+ historical_exact: historicalExact,
1069
+ source_receipt_digest: sha256(canonicalJson(receipt)),
1070
+ source_kind: evidence.sourceKind,
1071
+ source_turn_ids: evidence.sourceTurnIds,
1072
+ source_excerpt: excerpt,
1073
+ decision,
1074
+ display_line: displayLine,
1075
+ text_digest: sha256(displayLine)
1076
+ };
1077
+ }
1078
+ export function buildOwnerReferenceLine(request, receipt) {
1079
+ return buildOwnerAudit(request, receipt).display_line;
1080
+ }
1081
+ function successOutput(request, receipt, ownerAudit = buildOwnerAudit(request, receipt)) {
1082
+ const ownerReferenceLine = ownerAudit.display_line;
1083
+ const executionPlan = {
1084
+ status: receipt.status,
1085
+ active_edges: receipt.active_edges,
1086
+ active_node_definitions: receipt.active_node_definitions,
1087
+ unresolved: receipt.unresolved
1088
+ };
1089
+ const auditContract = {
1090
+ schema_version: 'brainbase-audit-contract-v1',
1091
+ owner_line: ownerReferenceLine,
1092
+ zero_call_line: receipt.selected_dag_ids.includes('knowledge.v1') ? null : ZERO_CALL_DISPLAY_LINE,
1093
+ portable_knowledge_tools: Object.keys(PORTABLE_KNOWLEDGE_TOOLS),
1094
+ required_order: ['judgment', 'knowledge', 'warning'],
1095
+ exact_once: true
1096
+ };
1097
+ const context = [
1098
+ 'Brainbase Judgment Resolver Host contract completed before model generation.',
1099
+ 'This is the only accepted receipt for the current turn. Do not call a resolver again or reclassify the turn.',
1100
+ 'Use active_node_definitions in active_edges order. A clarification receipt means ask only the clarification selected by the receipt and continue the response.',
1101
+ 'A judgment receipt is not action authorization. Normal host permissions and approval boundaries remain in force.',
1102
+ `The first line of every user-facing response must be exactly this Host-generated line:\n${ownerReferenceLine}`,
1103
+ 'Do not alter, translate, summarize, omit, or repeat that owner-visible line. It reports judgment evidence, not action authorization or completed knowledge retrieval.',
1104
+ 'After a portable Brainbase MCP call, preserve each Host-generated PostToolUse systemMessage after the judgment line in journal order. A success line proves only that call; a warning does not satisfy required knowledge retrieval.',
1105
+ receipt.selected_dag_ids.includes('knowledge.v1')
1106
+ ? 'This route requires a successful portable knowledge call; do not claim a zero-call completion.'
1107
+ : `If no portable Brainbase MCP call occurs, put this exact line immediately after the judgment line:\n${ZERO_CALL_DISPLAY_LINE}`,
1108
+ `Execution plan: ${JSON.stringify(executionPlan)}`,
1109
+ `audit_contract: ${JSON.stringify(auditContract)}`
1110
+ ].join('\n');
1111
+ return {
1112
+ continue: true,
1113
+ suppressOutput: true,
1114
+ receipt,
1115
+ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context }
1116
+ };
1117
+ }
1118
+ export async function runJudgmentHost(payload, options = {}) {
1119
+ const env = options.env ?? process.env;
1120
+ const sessionId = typeof payload.session_id === 'string' ? payload.session_id : '';
1121
+ const turnId = typeof payload.turn_id === 'string' ? payload.turn_id.trim() : '';
1122
+ if (!sessionId || !turnId)
1123
+ throw new TypeError('UserPromptSubmit requires prompt, turn_id, and session_id');
1124
+ const paths = journalPaths(sha256(sessionId), turnId, env);
1125
+ return withTurnLock(paths.lock, async () => {
1126
+ try {
1127
+ const episode = readEpisode(paths.episode);
1128
+ verifyEpisodeAdoption(episode, paths.target);
1129
+ const requestText = typeof payload.prompt === 'string' ? payload.prompt.trim() : '';
1130
+ if (!requestText || episode.request_text_digest !== sha256(requestText)) {
1131
+ throw new Error('judgment_episode_binding_mismatch');
1132
+ }
1133
+ return successOutput(episode.adoption.request, episode.adoption.receipt, episode.adoption.owner_audit);
1134
+ }
1135
+ catch (error) {
1136
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
1137
+ throw error;
1138
+ }
1139
+ const adopted = existingAdoptionForPayload(payload, env);
1140
+ const request = adopted?.request ?? buildJudgmentRequest(payload, {
1141
+ env,
1142
+ trustedConversationMessages: options.trustedConversationMessages
1143
+ });
1144
+ const existing = adopted ?? existingAdoption(request, env);
1145
+ const adoption = existing ?? adoptReceipt(request, verifyReceipt(await (options.resolver ?? resolveLocalJudgment)(request), request), env);
1146
+ const episodeWithoutDigest = {
1147
+ schema_version: 'brainbase-judgment-episode-v1',
1148
+ session_ref: request.conversation_context.session_ref,
1149
+ turn_id: request.turn_id,
1150
+ started_at: new Date().toISOString(),
1151
+ request_text_digest: sha256(request.request),
1152
+ initial_receipt_digest: sha256(canonicalJson(adoption.receipt)),
1153
+ adoption,
1154
+ audit_contract: {
1155
+ schema_version: 'brainbase-owner-audit-contract-v1',
1156
+ repair_body_policy: 'preserve',
1157
+ zero_call_display_line: adoption.receipt.selected_dag_ids.includes('knowledge.v1')
1158
+ ? null
1159
+ : ZERO_CALL_DISPLAY_LINE,
1160
+ zero_call_display_line_digest: adoption.receipt.selected_dag_ids.includes('knowledge.v1')
1161
+ ? null
1162
+ : sha256(ZERO_CALL_DISPLAY_LINE)
1163
+ }
1164
+ };
1165
+ const episode = {
1166
+ ...episodeWithoutDigest,
1167
+ episode_digest: sha256(canonicalJson(episodeWithoutDigest))
1168
+ };
1169
+ writeImmutableJson(paths.episode, episode);
1170
+ return successOutput(adoption.request, adoption.receipt, adoption.owner_audit);
1171
+ });
1172
+ }
1173
+ function hookIdentity(payload) {
1174
+ const sessionId = typeof payload.session_id === 'string' ? payload.session_id : '';
1175
+ const turnId = typeof payload.turn_id === 'string' ? payload.turn_id.trim() : '';
1176
+ if (!sessionId || !turnId)
1177
+ throw new TypeError('Judgment lifecycle hook requires turn_id and session_id');
1178
+ return { sessionRef: sha256(sessionId), turnId };
1179
+ }
1180
+ function portableKnowledgeTool(toolName) {
1181
+ const normalized = toolName.split(/__|[/.]/u).filter(Boolean).at(-1) ?? '';
1182
+ return normalized in PORTABLE_KNOWLEDGE_TOOLS ? normalized : null;
1183
+ }
1184
+ function validContentBlock(value) {
1185
+ const block = record(value);
1186
+ if (!block || typeof block.type !== 'string')
1187
+ return false;
1188
+ if (block.type === 'text')
1189
+ return typeof block.text === 'string';
1190
+ if (block.type === 'image' || block.type === 'audio') {
1191
+ return typeof block.data === 'string' && typeof block.mimeType === 'string';
1192
+ }
1193
+ if (block.type === 'resource')
1194
+ return record(block.resource) !== null;
1195
+ if (block.type === 'resource_link')
1196
+ return typeof block.uri === 'string';
1197
+ return false;
1198
+ }
1199
+ function callResultStatus(response) {
1200
+ const result = record(response);
1201
+ if (!result
1202
+ || !Array.isArray(result.content)
1203
+ || !result.content.every(validContentBlock)
1204
+ || (result.isError !== undefined && typeof result.isError !== 'boolean')) {
1205
+ return 'malformed';
1206
+ }
1207
+ if (result.isError === true)
1208
+ return 'failure';
1209
+ if (result.content.length === 0)
1210
+ return 'empty';
1211
+ const hasMeaningfulContent = result.content.some((value) => {
1212
+ const block = record(value);
1213
+ return block && (block.type !== 'text' || (typeof block.text === 'string' && block.text.trim().length > 0));
1214
+ });
1215
+ return hasMeaningfulContent ? 'success' : 'empty';
1216
+ }
1217
+ function sourceSelection(response) {
1218
+ const result = record(response);
1219
+ if (!result || !Array.isArray(result.content))
1220
+ return null;
1221
+ for (const value of result.content) {
1222
+ const block = record(value);
1223
+ if (!block || block.type !== 'text' || typeof block.text !== 'string')
1224
+ continue;
1225
+ try {
1226
+ const parsed = JSON.parse(block.text);
1227
+ const parsedRecord = record(parsed);
1228
+ const selection = record(parsedRecord?.source_selection);
1229
+ if (!selection)
1230
+ continue;
1231
+ const selected = Array.isArray(selection.selected)
1232
+ ? selection.selected.filter((item) => typeof item === 'string')
1233
+ : [];
1234
+ const excluded = Array.isArray(selection.excluded)
1235
+ ? selection.excluded.flatMap((item) => {
1236
+ const excludedItem = record(item);
1237
+ if (!excludedItem || typeof excludedItem.source !== 'string' || typeof excludedItem.reason !== 'string')
1238
+ return [];
1239
+ return [{ source: excludedItem.source, reason: excludedItem.reason }];
1240
+ })
1241
+ : [];
1242
+ if (selected.length > 0 || excluded.length > 0)
1243
+ return { selected, excluded };
1244
+ }
1245
+ catch {
1246
+ // Text content is not required to be JSON.
1247
+ }
1248
+ }
1249
+ return null;
1250
+ }
1251
+ function toolDisplayLine(tool, input, response, status) {
1252
+ const contract = PORTABLE_KNOWLEDGE_TOOLS[tool];
1253
+ const target = contract.auditKind === 'routing'
1254
+ ? tool
1255
+ : sanitizeOwnerExcerpt(record(input)?.query ?? tool);
1256
+ if (status !== 'success') {
1257
+ const result = status === 'failure' ? '失敗応答' : status === 'malformed' ? '不正な応答形式' : '空応答';
1258
+ return `⚠️ ${contract.label}: 「${target}」→ ${result}を記録`;
1259
+ }
1260
+ const selection = sourceSelection(response);
1261
+ const selectionParts = selection ? [
1262
+ ...(selection.selected.length > 0 ? [`採用: ${selection.selected.map(sanitizeOwnerExcerpt).join('・')}`] : []),
1263
+ ...(selection.excluded.length > 0 ? [`除外: ${selection.excluded.map((item) => `${sanitizeOwnerExcerpt(item.source)}(${sanitizeOwnerExcerpt(item.reason)})`).join('・')}`] : [])
1264
+ ] : [];
1265
+ const result = selectionParts.length > 0 ? selectionParts.join('/') : '正常応答を確認';
1266
+ return `📚 ${contract.label}: 「${target}」→ ${result} ✓`;
1267
+ }
1268
+ async function recordToolUse(payload, env) {
1269
+ const { sessionRef, turnId } = hookIdentity(payload);
1270
+ const paths = journalPaths(sessionRef, turnId, env);
1271
+ const toolUseId = typeof payload.tool_use_id === 'string' ? payload.tool_use_id.trim() : '';
1272
+ const toolName = typeof payload.tool_name === 'string' ? payload.tool_name.trim() : '';
1273
+ if (!toolUseId || !toolName)
1274
+ throw new TypeError('PostToolUse requires tool_use_id and tool_name');
1275
+ const portableTool = portableKnowledgeTool(toolName);
1276
+ if (!portableTool)
1277
+ return null;
1278
+ return withTurnLock(paths.lock, async () => {
1279
+ let episode;
1280
+ try {
1281
+ episode = readEpisode(paths.episode);
1282
+ verifyEpisodeAdoption(episode, paths.target);
1283
+ }
1284
+ catch (error) {
1285
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
1286
+ throw new Error('judgment_orphan_post_tool_use');
1287
+ }
1288
+ throw error;
1289
+ }
1290
+ if (episode.session_ref !== sessionRef || episode.turn_id !== turnId) {
1291
+ throw new Error('judgment_episode_binding_mismatch');
1292
+ }
1293
+ try {
1294
+ readFinal(paths.final);
1295
+ throw new Error('judgment_episode_already_finalized');
1296
+ }
1297
+ catch (error) {
1298
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
1299
+ throw error;
1300
+ }
1301
+ const toolInput = payload.tool_input ?? null;
1302
+ const toolResponse = payload.tool_response ?? null;
1303
+ const responseStatus = callResultStatus(toolResponse);
1304
+ const eventFingerprint = sha256(canonicalJson({ tool_use_id: toolUseId, tool_name: toolName, tool_input: toolInput, tool_response: toolResponse }));
1305
+ const events = readEvents(paths.events);
1306
+ const replay = events.find((event) => event.tool_use_id === toolUseId);
1307
+ if (replay) {
1308
+ if (replay.event_fingerprint !== eventFingerprint)
1309
+ throw new Error('judgment_tool_event_conflict');
1310
+ return replay;
1311
+ }
1312
+ const eventWithoutDigest = {
1313
+ schema_version: 'brainbase-judgment-tool-event-v1',
1314
+ session_ref: sessionRef,
1315
+ turn_id: turnId,
1316
+ sequence: events.length + 1,
1317
+ recorded_at: new Date().toISOString(),
1318
+ tool_use_id: toolUseId,
1319
+ tool_name: toolName,
1320
+ tool_input: toolInput,
1321
+ tool_response: toolResponse,
1322
+ audit_kind: PORTABLE_KNOWLEDGE_TOOLS[portableTool].auditKind,
1323
+ success: responseStatus === 'success',
1324
+ satisfies: responseStatus === 'success' ? ['knowledge.resolve'] : [],
1325
+ display_line: toolDisplayLine(portableTool, toolInput, toolResponse, responseStatus),
1326
+ event_fingerprint: eventFingerprint
1327
+ };
1328
+ const event = {
1329
+ ...eventWithoutDigest,
1330
+ event_digest: sha256(canonicalJson(eventWithoutDigest))
1331
+ };
1332
+ const name = `${String(event.sequence).padStart(8, '0')}-${sha256(toolUseId)}.json`;
1333
+ writeImmutableJson(join(paths.events, name), event);
1334
+ return event;
1335
+ });
1336
+ }
1337
+ async function finalizeEpisode(payload, env) {
1338
+ const { sessionRef, turnId } = hookIdentity(payload);
1339
+ const paths = journalPaths(sessionRef, turnId, env);
1340
+ const answerDigest = sha256(typeof payload.last_assistant_message === 'string' ? payload.last_assistant_message : '');
1341
+ return withTurnLock(paths.lock, async () => {
1342
+ let episode;
1343
+ try {
1344
+ episode = readEpisode(paths.episode);
1345
+ verifyEpisodeAdoption(episode, paths.target);
1346
+ }
1347
+ catch (error) {
1348
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
1349
+ throw new Error('judgment_orphan_stop');
1350
+ throw error;
1351
+ }
1352
+ if (episode.session_ref !== sessionRef || episode.turn_id !== turnId) {
1353
+ throw new Error('judgment_episode_binding_mismatch');
1354
+ }
1355
+ const events = readEvents(paths.events);
1356
+ const eventSetDigest = sha256(canonicalJson(events));
1357
+ try {
1358
+ const existing = readFinal(paths.final);
1359
+ if (existing.initial_receipt_digest !== episode.initial_receipt_digest
1360
+ || existing.event_count !== events.length
1361
+ || existing.event_set_digest !== eventSetDigest
1362
+ || existing.answer_digest !== answerDigest) {
1363
+ throw new Error('judgment_final_conflict');
1364
+ }
1365
+ return existing;
1366
+ }
1367
+ catch (error) {
1368
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
1369
+ throw error;
1370
+ }
1371
+ const expectedAuditLines = requiredAuditLines(episode, events);
1372
+ const expectedAuditLinesDigest = sha256(canonicalJson(expectedAuditLines));
1373
+ const answer = typeof payload.last_assistant_message === 'string' ? payload.last_assistant_message : '';
1374
+ const missingKnowledge = episode.adoption.receipt.selected_dag_ids.includes('knowledge.v1')
1375
+ && !events.some((event) => event.success && event.satisfies.includes('knowledge.resolve'));
1376
+ const missingOwnerAudit = !answerContainsExactAuditPrefix(answer, expectedAuditLines);
1377
+ let continuation = null;
1378
+ try {
1379
+ continuation = readContinuation(paths.continuation);
1380
+ if (continuation.expected_audit_lines_digest !== expectedAuditLinesDigest) {
1381
+ throw new Error('judgment_continuation_audit_lines_mismatch');
1382
+ }
1383
+ }
1384
+ catch (error) {
1385
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
1386
+ throw error;
1387
+ }
1388
+ const body = normalizedAnswerBody(answer, expectedAuditLines);
1389
+ const binding = continuation?.answer_body_binding;
1390
+ const missingAnswerBody = Boolean(binding) && (binding?.character_count !== body.length || binding.body_digest !== sha256(body));
1391
+ if (missingKnowledge || missingOwnerAudit || missingAnswerBody) {
1392
+ const missingCapabilities = [
1393
+ ...(missingKnowledge ? ['knowledge.resolve'] : []),
1394
+ ...(missingOwnerAudit ? ['owner.audit.display'] : []),
1395
+ ...(missingAnswerBody ? ['answer.body.preservation'] : [])
1396
+ ];
1397
+ if (!continuation) {
1398
+ const continuationWithoutDigest = {
1399
+ schema_version: 'brainbase-judgment-continuation-v1',
1400
+ requested_at: new Date().toISOString(),
1401
+ missing_capabilities: missingCapabilities,
1402
+ expected_audit_lines_digest: expectedAuditLinesDigest,
1403
+ ...(!missingKnowledge && missingOwnerAudit ? {
1404
+ answer_body_binding: {
1405
+ schema_version: 'brainbase-answer-body-binding-v1',
1406
+ body_digest: sha256(body),
1407
+ character_count: body.length
1408
+ }
1409
+ } : {})
1410
+ };
1411
+ continuation = {
1412
+ ...continuationWithoutDigest,
1413
+ continuation_digest: sha256(canonicalJson(continuationWithoutDigest))
1414
+ };
1415
+ writeImmutableJson(paths.continuation, continuation);
1416
+ }
1417
+ const reasons = [
1418
+ ...(missingKnowledge ? ['required capability knowledge.resolveを実行し、成功した結果をepisodeへ記録する'] : []),
1419
+ ...(missingOwnerAudit ? [`最終回答の先頭に次の監査行をそのまま、この順番で各1回だけ表示する:\n${expectedAuditLines.join('\n')}`] : []),
1420
+ ...((missingAnswerBody || continuation.answer_body_binding) ? [
1421
+ '最初に差し戻された回答の監査行以外の本文を、削除・要約・置換せずそのまま残す'
1422
+ ] : [])
1423
+ ];
1424
+ return {
1425
+ decision: 'block',
1426
+ reason: `Brainbase judgment episodeを完了する前に${reasons.join('。その後')}。監査行の後に、元の回答本文をそのまま続けてください。`
1427
+ };
1428
+ }
1429
+ const finalWithoutDigest = {
1430
+ schema_version: 'brainbase-judgment-final-v1',
1431
+ session_ref: sessionRef,
1432
+ turn_id: turnId,
1433
+ finalized_at: new Date().toISOString(),
1434
+ initial_receipt_digest: episode.initial_receipt_digest,
1435
+ event_count: events.length,
1436
+ event_set_digest: eventSetDigest,
1437
+ answer_digest: answerDigest,
1438
+ completion_status: 'complete',
1439
+ owner_audit_complete: true,
1440
+ owner_audit_line_count: expectedAuditLines.length
1441
+ };
1442
+ const final = {
1443
+ ...finalWithoutDigest,
1444
+ final_digest: sha256(canonicalJson(finalWithoutDigest))
1445
+ };
1446
+ writeImmutableJson(paths.final, final);
1447
+ return final;
1448
+ });
1449
+ }
1450
+ export async function processJudgmentHook(payload, options = {}) {
1451
+ const eventName = payload.hook_event_name ?? payload.hookEventName ?? 'UserPromptSubmit';
1452
+ const env = options.env ?? process.env;
1453
+ if (eventName === 'UserPromptSubmit')
1454
+ return runJudgmentHost(payload, options);
1455
+ if (eventName === 'PostToolUse') {
1456
+ const event = await recordToolUse(payload, env);
1457
+ return event ? { systemMessage: event.display_line } : {};
1458
+ }
1459
+ if (eventName === 'Stop') {
1460
+ const output = await finalizeEpisode(payload, env);
1461
+ if ('decision' in output && output.decision === 'block' && payload.stop_hook_active === true) {
1462
+ throw new Error('judgment_stop_repair_exhausted');
1463
+ }
1464
+ return output;
1465
+ }
1466
+ return {};
1467
+ }
1468
+ export function blockedJudgmentOutput(reason) {
1469
+ return {
1470
+ continue: false,
1471
+ suppressOutput: false,
1472
+ stopReason: `Brainbase Judgment Host failed before the turn (${reason}).`
1473
+ };
1474
+ }