@unson/brainbase-mcp 0.3.1 → 0.4.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,325 @@
1
+ import { JudgmentDAGValidationError, validateJudgmentDAG } from './judgment-dag-core.js';
2
+ export class JudgmentDAGExecutionError extends Error {
3
+ code;
4
+ node_id;
5
+ node_type;
6
+ runner_type;
7
+ failure_kind;
8
+ cause;
9
+ constructor(code, message, details = {}) {
10
+ super(message);
11
+ this.name = 'JudgmentDAGExecutionError';
12
+ this.code = code;
13
+ this.node_id = details.node_id;
14
+ this.node_type = details.node_type;
15
+ this.runner_type = details.runner_type;
16
+ this.failure_kind = details.failure_kind;
17
+ this.cause = details.cause;
18
+ }
19
+ }
20
+ function isRecord(value) {
21
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
22
+ }
23
+ function isPlainRecord(value) {
24
+ if (!isRecord(value)) {
25
+ return false;
26
+ }
27
+ const prototype = Object.getPrototypeOf(value);
28
+ return prototype === Object.prototype || prototype === null;
29
+ }
30
+ function invalidJSON(path, reason) {
31
+ throw new JudgmentDAGExecutionError('invalid_json', `${path} must be a JSON value: ${reason}`);
32
+ }
33
+ function snapshotJSON(value, path, active = new Set()) {
34
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') {
35
+ return value;
36
+ }
37
+ if (typeof value === 'number') {
38
+ if (Number.isFinite(value)) {
39
+ return value;
40
+ }
41
+ invalidJSON(path, 'numbers must be finite');
42
+ }
43
+ if (typeof value !== 'object') {
44
+ invalidJSON(path, 'undefined, functions, symbols, and bigint are not supported');
45
+ }
46
+ const objectValue = value;
47
+ if (active.has(objectValue)) {
48
+ invalidJSON(path, 'cyclic values are not supported');
49
+ }
50
+ active.add(objectValue);
51
+ let snapshot;
52
+ if (Array.isArray(value)) {
53
+ const result = [];
54
+ for (let index = 0; index < value.length; index += 1) {
55
+ if (!(index in value)) {
56
+ invalidJSON(`${path}[${index}]`, 'array holes are not supported');
57
+ }
58
+ result.push(snapshotJSON(value[index], `${path}[${index}]`, active));
59
+ }
60
+ snapshot = result;
61
+ }
62
+ else {
63
+ if (!isPlainRecord(value)) {
64
+ invalidJSON(path, 'only plain objects are supported');
65
+ }
66
+ const result = {};
67
+ for (const key of Object.keys(value)) {
68
+ Object.defineProperty(result, key, {
69
+ configurable: true,
70
+ enumerable: true,
71
+ value: snapshotJSON(value[key], `${path}.${key}`, active),
72
+ writable: true
73
+ });
74
+ }
75
+ snapshot = result;
76
+ }
77
+ active.delete(objectValue);
78
+ return snapshot;
79
+ }
80
+ function deepFreeze(value) {
81
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
82
+ Object.freeze(value);
83
+ for (const child of Object.values(value)) {
84
+ deepFreeze(child);
85
+ }
86
+ }
87
+ return value;
88
+ }
89
+ function invalidRequest(message) {
90
+ return new JudgmentDAGExecutionError('invalid_request', message);
91
+ }
92
+ function snapshotRequest(value) {
93
+ let requestRecord;
94
+ try {
95
+ if (!isPlainRecord(value)) {
96
+ throw invalidRequest('request must be a plain object');
97
+ }
98
+ requestRecord = value;
99
+ }
100
+ catch (error) {
101
+ if (error instanceof JudgmentDAGExecutionError) {
102
+ throw error;
103
+ }
104
+ throw invalidRequest('request must be a readable plain object');
105
+ }
106
+ let runId;
107
+ let dag;
108
+ let input;
109
+ let runners;
110
+ try {
111
+ // Read each public request field exactly once. The local values below are
112
+ // the only values used for validation, execution, and recording.
113
+ runId = requestRecord.run_id;
114
+ dag = requestRecord.dag;
115
+ input = requestRecord.input;
116
+ runners = requestRecord.runners;
117
+ }
118
+ catch {
119
+ throw invalidRequest('request fields must be readable');
120
+ }
121
+ if (typeof runId !== 'string' || runId.trim().length === 0) {
122
+ throw invalidRequest('request.run_id must be a non-empty string');
123
+ }
124
+ try {
125
+ if (!isPlainRecord(runners)) {
126
+ throw invalidRequest('request.runners must be a plain object');
127
+ }
128
+ }
129
+ catch (error) {
130
+ if (error instanceof JudgmentDAGExecutionError) {
131
+ throw error;
132
+ }
133
+ throw invalidRequest('request.runners must be a readable plain object');
134
+ }
135
+ return { run_id: runId, dag, input, runners };
136
+ }
137
+ function snapshotRequestJSON(value, path) {
138
+ try {
139
+ return snapshotJSON(value, path);
140
+ }
141
+ catch (error) {
142
+ if (error instanceof JudgmentDAGExecutionError) {
143
+ throw error;
144
+ }
145
+ throw invalidRequest(`${path} must be readable`);
146
+ }
147
+ }
148
+ function snapshotRunnerOutput(value, node) {
149
+ try {
150
+ return snapshotJSON(value, `node ${node.id} output`);
151
+ }
152
+ catch {
153
+ // Runner output is an untrusted boundary. Do not expose an arbitrary
154
+ // getter/proxy exception or preserve it as cause; retain only stable node
155
+ // context in the public machine-readable error.
156
+ throw new JudgmentDAGExecutionError('invalid_json', `node ${node.id} output must be a JSON value`, {
157
+ node_id: node.id,
158
+ node_type: node.node_type,
159
+ runner_type: node.runner_type
160
+ });
161
+ }
162
+ }
163
+ function validateRequestDAG(value) {
164
+ try {
165
+ return validateJudgmentDAG(value);
166
+ }
167
+ catch (error) {
168
+ if (error instanceof JudgmentDAGValidationError) {
169
+ throw error;
170
+ }
171
+ throw invalidRequest('request.dag must be readable');
172
+ }
173
+ }
174
+ function requiredRunnerEntries(dag, executionOrder, runners) {
175
+ const nodeById = new Map(dag.nodes.map((node) => [node.id, node]));
176
+ const required = new Map();
177
+ for (const nodeId of executionOrder) {
178
+ const node = nodeById.get(nodeId);
179
+ if (node === undefined) {
180
+ throw invalidRequest(`validated execution order references unknown node ${nodeId}`);
181
+ }
182
+ const runnerType = node.runner_type;
183
+ if (required.has(runnerType)) {
184
+ continue;
185
+ }
186
+ let hasRegistration;
187
+ let registration;
188
+ try {
189
+ hasRegistration = Object.prototype.hasOwnProperty.call(runners, runnerType);
190
+ registration = hasRegistration ? runners[runnerType] : undefined;
191
+ }
192
+ catch {
193
+ throw new JudgmentDAGExecutionError('invalid_runner', `Runner registration for ${runnerType} is invalid`, { node_id: node.id, runner_type: runnerType });
194
+ }
195
+ if (!hasRegistration || registration === undefined) {
196
+ throw new JudgmentDAGExecutionError('missing_runner', `DAG node ${node.id} requires a ${runnerType} runner registration`, { node_id: node.id, runner_type: runnerType });
197
+ }
198
+ let registrationRecord;
199
+ let version = undefined;
200
+ let run = undefined;
201
+ try {
202
+ registrationRecord = isPlainRecord(registration) ? registration : undefined;
203
+ if (registrationRecord !== undefined) {
204
+ version = registrationRecord.version;
205
+ run = registrationRecord.run;
206
+ }
207
+ }
208
+ catch {
209
+ throw new JudgmentDAGExecutionError('invalid_runner', `Runner registration for ${runnerType} is invalid`, { node_id: node.id, runner_type: runnerType });
210
+ }
211
+ if (registrationRecord === undefined ||
212
+ typeof version !== 'string' ||
213
+ version.trim().length === 0 ||
214
+ typeof run !== 'function') {
215
+ throw new JudgmentDAGExecutionError('invalid_runner', `Runner registration for ${runnerType} is invalid`, { node_id: node.id, runner_type: runnerType });
216
+ }
217
+ // Capture the registration's immutable contract before the first runner
218
+ // invocation. A runner is allowed to close over the caller's registration
219
+ // object, so retaining that object here would let it rewrite the version
220
+ // recorded for nodes that execute later in the same run.
221
+ required.set(runnerType, {
222
+ version,
223
+ run: run
224
+ });
225
+ }
226
+ return required;
227
+ }
228
+ /**
229
+ * Execute a validated local DAG sequentially using only explicit runner
230
+ * registrations. No clock, randomness, filesystem, network, or persistence
231
+ * is consulted by this function.
232
+ */
233
+ export async function executeJudgmentDAG(request) {
234
+ const requestSnapshot = snapshotRequest(request);
235
+ const runIdSnapshot = requestSnapshot.run_id;
236
+ // Keep the validator's machine-readable error and details intact. Validate
237
+ // the captured source before cloning, then validate the exact JSON snapshot
238
+ // that will be executed and recorded so no later mutation can change the
239
+ // execution order or node lookup basis.
240
+ validateRequestDAG(requestSnapshot.dag);
241
+ const dagSnapshot = snapshotRequestJSON(requestSnapshot.dag, 'request.dag');
242
+ const validation = validateRequestDAG(dagSnapshot);
243
+ const inputSnapshot = snapshotRequestJSON(requestSnapshot.input, 'request.input');
244
+ const runners = requiredRunnerEntries(dagSnapshot, validation.execution_order, requestSnapshot.runners);
245
+ const nodeById = new Map(dagSnapshot.nodes.map((node) => [node.id, node]));
246
+ const outputByNode = new Map();
247
+ const nodeRecords = [];
248
+ for (const nodeId of validation.execution_order) {
249
+ const node = nodeById.get(nodeId);
250
+ if (node === undefined) {
251
+ throw invalidRequest(`validated execution order references unknown node ${nodeId}`);
252
+ }
253
+ const registration = runners.get(node.runner_type);
254
+ if (registration === undefined) {
255
+ throw new JudgmentDAGExecutionError('missing_runner', `DAG node ${node.id} requires a ${node.runner_type} runner registration`, { node_id: node.id, runner_type: node.runner_type });
256
+ }
257
+ const dependencyOutputs = node.depends_on
258
+ .slice()
259
+ .sort()
260
+ .map((dependencyId) => {
261
+ const output = outputByNode.get(dependencyId);
262
+ if (output === undefined) {
263
+ throw invalidRequest(`dependency ${dependencyId} has not executed before ${node.id}`);
264
+ }
265
+ return {
266
+ node_id: dependencyId,
267
+ output
268
+ };
269
+ });
270
+ const runnerInput = deepFreeze({
271
+ run_id: runIdSnapshot,
272
+ dag: dagSnapshot,
273
+ node,
274
+ input: inputSnapshot,
275
+ dependency_outputs: dependencyOutputs
276
+ });
277
+ let rawOutput;
278
+ let runnerResult;
279
+ try {
280
+ runnerResult = registration.run(runnerInput);
281
+ }
282
+ catch (error) {
283
+ throw new JudgmentDAGExecutionError('runner_failed', `Runner ${node.runner_type} failed for node ${node.id}`, {
284
+ node_id: node.id,
285
+ runner_type: node.runner_type,
286
+ failure_kind: 'sync_throw',
287
+ cause: error
288
+ });
289
+ }
290
+ try {
291
+ rawOutput = await runnerResult;
292
+ }
293
+ catch (error) {
294
+ throw new JudgmentDAGExecutionError('runner_failed', `Runner ${node.runner_type} failed for node ${node.id}`, {
295
+ node_id: node.id,
296
+ runner_type: node.runner_type,
297
+ failure_kind: 'async_reject',
298
+ cause: error
299
+ });
300
+ }
301
+ const output = snapshotRunnerOutput(rawOutput, node);
302
+ outputByNode.set(node.id, output);
303
+ nodeRecords.push({
304
+ node_id: node.id,
305
+ runner_type: node.runner_type,
306
+ runner_version: registration.version,
307
+ input_contract: node.input_contract,
308
+ output_contract: node.output_contract,
309
+ input: inputSnapshot,
310
+ dependency_outputs: dependencyOutputs,
311
+ output
312
+ });
313
+ }
314
+ const runnerVersions = [...runners.entries()]
315
+ .sort(([left], [right]) => left.localeCompare(right))
316
+ .map(([runner_type, registration]) => ({ runner_type, version: registration.version }));
317
+ return deepFreeze({
318
+ run_id: runIdSnapshot,
319
+ dag: dagSnapshot,
320
+ input: inputSnapshot,
321
+ execution_order: validation.execution_order.slice(),
322
+ runner_versions: runnerVersions,
323
+ nodes: nodeRecords
324
+ });
325
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Side-effect-free public entrypoint for the Judgment DAG core contract.
3
+ *
4
+ * The package root remains the MCP server entrypoint. Consumers that only
5
+ * need to validate a DAG can import this subpath without starting MCP.
6
+ */
7
+ export { JUDGMENT_DAG_EDGE_RELATIONS, JUDGMENT_DAG_ALLOWED_KEYS, JUDGMENT_DAG_LAYERS, JUDGMENT_DAG_NODE_TYPE_TO_LAYER, JUDGMENT_DAG_NODE_TYPES, JUDGMENT_DAG_RUNNER_TYPES, JUDGMENT_DAG_SCOPE_TYPES, JudgmentDAGValidationError, assertValidJudgmentDAG, validateJudgmentDAG } from './judgment-dag-core.js';
8
+ export { executeJudgmentDAG, JudgmentDAGExecutionError } from './judgment-dag-runner.js';
9
+ export type { JudgmentDAG, JudgmentDAGEdge, JudgmentDAGEdgeRelation, JudgmentDAGLayer, JudgmentDAGMetadata, JudgmentDAGNode, JudgmentDAGNodeType, JudgmentDAGRunnerType, JudgmentDAGScope, JudgmentDAGScopeType, JudgmentDAGValidationCode, JudgmentDAGValidationDetails, JudgmentDAGValidationResult } from './judgment-dag-core.js';
10
+ export type { JudgmentDAGDependencyOutput, JudgmentDAGExecutionCode, JudgmentDAGExecutionErrorDetails, JudgmentDAGJSONValue, JudgmentDAGNodeRunRecord, JudgmentDAGRunRecord, JudgmentDAGRunRequest, JudgmentDAGRunnerFailureKind, JudgmentDAGRunnerInput, JudgmentDAGRunnerRegistration, JudgmentDAGRunnerVersion } from './judgment-dag-runner.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Side-effect-free public entrypoint for the Judgment DAG core contract.
3
+ *
4
+ * The package root remains the MCP server entrypoint. Consumers that only
5
+ * need to validate a DAG can import this subpath without starting MCP.
6
+ */
7
+ export { JUDGMENT_DAG_EDGE_RELATIONS, JUDGMENT_DAG_ALLOWED_KEYS, JUDGMENT_DAG_LAYERS, JUDGMENT_DAG_NODE_TYPE_TO_LAYER, JUDGMENT_DAG_NODE_TYPES, JUDGMENT_DAG_RUNNER_TYPES, JUDGMENT_DAG_SCOPE_TYPES, JudgmentDAGValidationError, assertValidJudgmentDAG, validateJudgmentDAG } from './judgment-dag-core.js';
8
+ export { executeJudgmentDAG, JudgmentDAGExecutionError } from './judgment-dag-runner.js';
@@ -1,3 +1,4 @@
1
+ import { type JudgmentAutonomyEvaluation, type JudgmentAutonomyResolver } from './judgment-autonomy.js';
1
2
  type Environment = Record<string, string | undefined>;
2
3
  export interface ConversationMessage {
3
4
  sequence?: number;
@@ -104,8 +105,12 @@ export interface JudgmentHookOutput {
104
105
  export interface JudgmentHostOptions {
105
106
  env?: Environment;
106
107
  resolver?: (request: JudgmentRequest) => Promise<JudgmentReceipt>;
108
+ autonomyResolver?: JudgmentAutonomyResolver;
109
+ autonomyMode?: JudgmentAutonomyMode;
110
+ autonomyCanaryProjects?: readonly string[];
107
111
  trustedConversationMessages?: ConversationMessage[];
108
112
  }
113
+ export type JudgmentAutonomyMode = 'off' | 'canary' | 'on';
109
114
  interface LocalResolverOptions {
110
115
  now?: () => Date;
111
116
  id?: () => string;
@@ -179,8 +184,24 @@ export interface JudgmentFinalReceipt {
179
184
  owner_audit_line_count?: number;
180
185
  final_digest: string;
181
186
  }
187
+ export interface JudgmentAutonomyReceipt {
188
+ schema_version: 'brainbase-judgment-autonomy-receipt-v1';
189
+ session_ref: string;
190
+ turn_id: string;
191
+ recorded_at: string;
192
+ mode: Exclude<JudgmentAutonomyMode, 'off'>;
193
+ project_code: string | null;
194
+ case_id: string;
195
+ verdict: JudgmentAutonomyEvaluation['verdict'];
196
+ reason_code: JudgmentAutonomyEvaluation['reason_code'];
197
+ source: JudgmentAutonomyEvaluation['source'];
198
+ question_digest: string | null;
199
+ basis_entity_ids: string[];
200
+ autonomy_digest: string;
201
+ }
182
202
  export interface JudgmentStopBlock {
183
203
  decision: 'block';
204
+ block_kind?: 'autonomy_continue' | 'audit_repair';
184
205
  reason: string;
185
206
  }
186
207
  export declare function canonicalJson(value: unknown): string;
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { closeSync, constants as fsConstants, fsyncSync, linkSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
5
+ import { evaluateJudgmentAutonomy, renderJudgmentAutonomyContinuation } from './judgment-autonomy.js';
5
6
  const RUNTIME_VERSION = 'portable-judgment-runtime-1.0.0';
6
7
  const ZERO_CALL_DISPLAY_LINE = '📚 Brainbase未参照: 必須参照なし・実呼び出し0回 ✓';
7
8
  const PORTABLE_KNOWLEDGE_TOOLS = {
@@ -306,6 +307,7 @@ function journalPaths(sessionRef, turnId, env) {
306
307
  events: join(directory, `${turnRef}.events`),
307
308
  final: join(directory, `${turnRef}.final.json`),
308
309
  continuation: join(directory, `${turnRef}.continuation.json`),
310
+ autonomy: join(directory, `${turnRef}.autonomy`),
309
311
  lock: join(directory, `${turnRef}.lock`)
310
312
  };
311
313
  }
@@ -1334,6 +1336,180 @@ async function recordToolUse(payload, env) {
1334
1336
  return event;
1335
1337
  });
1336
1338
  }
1339
+ function resolveAutonomyMode(options, env) {
1340
+ const rawMode = options.autonomyMode ?? env.BRAINBASE_JUDGMENT_AUTONOMY_MODE ?? 'off';
1341
+ if (!['off', 'canary', 'on'].includes(rawMode)) {
1342
+ throw new Error('judgment_autonomy_mode_invalid');
1343
+ }
1344
+ const configuredProjects = options.autonomyCanaryProjects
1345
+ ?? (env.BRAINBASE_JUDGMENT_AUTONOMY_CANARY_PROJECTS ?? '').split(',');
1346
+ const canaryProjects = [...new Set(configuredProjects.map((value) => value.trim()).filter(Boolean))];
1347
+ if (rawMode === 'canary' && canaryProjects.length === 0) {
1348
+ throw new Error('judgment_autonomy_canary_project_missing');
1349
+ }
1350
+ return { mode: rawMode, canaryProjects };
1351
+ }
1352
+ function readAutonomyReceipt(path) {
1353
+ const value = readJsonArtifact(path);
1354
+ if (value.schema_version !== 'brainbase-judgment-autonomy-receipt-v1'
1355
+ || typeof value.session_ref !== 'string'
1356
+ || typeof value.turn_id !== 'string'
1357
+ || typeof value.recorded_at !== 'string'
1358
+ || !['canary', 'on'].includes(String(value.mode))
1359
+ || (value.project_code !== null && typeof value.project_code !== 'string')
1360
+ || typeof value.case_id !== 'string'
1361
+ || !['continue', 'human_required', 'not_applicable'].includes(String(value.verdict))
1362
+ || typeof value.reason_code !== 'string'
1363
+ || !['deterministic', 'same_codex', 'independent_resolver'].includes(String(value.source))
1364
+ || (value.question_digest !== null && typeof value.question_digest !== 'string')
1365
+ || !Array.isArray(value.basis_entity_ids)
1366
+ || !value.basis_entity_ids.every((entry) => typeof entry === 'string')
1367
+ || typeof value.autonomy_digest !== 'string') {
1368
+ throw new Error(`judgment_journal_invalid:${basename(path)}`);
1369
+ }
1370
+ if (value.autonomy_digest !== artifactDigest(value, 'autonomy_digest')) {
1371
+ throw new Error(`judgment_autonomy_receipt_digest_mismatch:${basename(path)}`);
1372
+ }
1373
+ return value;
1374
+ }
1375
+ function autonomyReceiptProjection(receipt) {
1376
+ const { recorded_at: _recordedAt, autonomy_digest: _digest, ...projection } = receipt;
1377
+ return projection;
1378
+ }
1379
+ function persistAutonomyReceipt(paths, sessionRef, snapshot, evaluation, mode) {
1380
+ const path = join(paths.autonomy, `${evaluation.case_id}.json`);
1381
+ const basisEntityIds = evaluation.resolver_decision?.basis.map((entry) => entry.entity_id) ?? [];
1382
+ const receiptWithoutDigest = {
1383
+ schema_version: 'brainbase-judgment-autonomy-receipt-v1',
1384
+ session_ref: sessionRef,
1385
+ turn_id: snapshot.context.turn_id,
1386
+ recorded_at: new Date().toISOString(),
1387
+ mode,
1388
+ project_code: snapshot.context.project_code ?? null,
1389
+ case_id: evaluation.case_id,
1390
+ verdict: evaluation.verdict,
1391
+ reason_code: evaluation.reason_code,
1392
+ source: evaluation.source,
1393
+ question_digest: evaluation.question ? sha256(evaluation.question) : null,
1394
+ basis_entity_ids: basisEntityIds
1395
+ };
1396
+ const receipt = {
1397
+ ...receiptWithoutDigest,
1398
+ autonomy_digest: artifactDigest(receiptWithoutDigest, 'autonomy_digest')
1399
+ };
1400
+ try {
1401
+ const existing = readAutonomyReceipt(path);
1402
+ if (canonicalJson(autonomyReceiptProjection(existing)) !== canonicalJson(autonomyReceiptProjection(receipt))) {
1403
+ throw new Error('judgment_autonomy_receipt_conflict');
1404
+ }
1405
+ return existing;
1406
+ }
1407
+ catch (error) {
1408
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
1409
+ throw error;
1410
+ }
1411
+ writeImmutableJson(path, receipt);
1412
+ return receipt;
1413
+ }
1414
+ async function autonomyStopSnapshot(payload, env) {
1415
+ const answer = typeof payload.last_assistant_message === 'string' ? payload.last_assistant_message : '';
1416
+ if (!answer.trim())
1417
+ return null;
1418
+ const { sessionRef, turnId } = hookIdentity(payload);
1419
+ const paths = journalPaths(sessionRef, turnId, env);
1420
+ return withTurnLock(paths.lock, async () => {
1421
+ let episode;
1422
+ try {
1423
+ episode = readEpisode(paths.episode);
1424
+ verifyEpisodeAdoption(episode, paths.target);
1425
+ }
1426
+ catch (error) {
1427
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
1428
+ throw new Error('judgment_orphan_stop');
1429
+ }
1430
+ throw error;
1431
+ }
1432
+ if (episode.session_ref !== sessionRef || episode.turn_id !== turnId) {
1433
+ throw new Error('judgment_episode_binding_mismatch');
1434
+ }
1435
+ try {
1436
+ readFinal(paths.final);
1437
+ return null;
1438
+ }
1439
+ catch (error) {
1440
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
1441
+ throw error;
1442
+ }
1443
+ const events = readEvents(paths.events);
1444
+ return {
1445
+ episode_digest: episode.episode_digest,
1446
+ event_set_digest: sha256(canonicalJson(events)),
1447
+ context: {
1448
+ turn_id: turnId,
1449
+ request: episode.adoption.request.request,
1450
+ final_answer: answer,
1451
+ ...(episode.adoption.receipt.project_code
1452
+ ? { project_code: episode.adoption.receipt.project_code }
1453
+ : {}),
1454
+ selected_dag_ids: [...episode.adoption.receipt.selected_dag_ids]
1455
+ }
1456
+ };
1457
+ });
1458
+ }
1459
+ async function evaluateAutonomyStop(payload, env, options) {
1460
+ const config = resolveAutonomyMode(options, env);
1461
+ if (config.mode === 'off')
1462
+ return null;
1463
+ const snapshot = await autonomyStopSnapshot(payload, env);
1464
+ if (!snapshot)
1465
+ return null;
1466
+ if (config.mode === 'canary'
1467
+ && (!snapshot.context.project_code || !config.canaryProjects.includes(snapshot.context.project_code))) {
1468
+ return null;
1469
+ }
1470
+ // A resumed Stop must not invoke an independent LLM twice. Reclassify with
1471
+ // the pure fallback only; repeated routine/semantic escalation fails loud.
1472
+ const evaluation = await evaluateJudgmentAutonomy(snapshot.context, payload.stop_hook_active === true ? undefined : options.autonomyResolver);
1473
+ const { sessionRef, turnId } = hookIdentity(payload);
1474
+ const paths = journalPaths(sessionRef, turnId, env);
1475
+ const stillActive = await withTurnLock(paths.lock, async () => {
1476
+ const episode = readEpisode(paths.episode);
1477
+ verifyEpisodeAdoption(episode, paths.target);
1478
+ if (episode.episode_digest !== snapshot.episode_digest) {
1479
+ throw new Error('judgment_autonomy_episode_changed');
1480
+ }
1481
+ const events = readEvents(paths.events);
1482
+ if (sha256(canonicalJson(events)) !== snapshot.event_set_digest) {
1483
+ throw new Error('judgment_autonomy_event_set_changed');
1484
+ }
1485
+ try {
1486
+ readFinal(paths.final);
1487
+ return false;
1488
+ }
1489
+ catch (error) {
1490
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
1491
+ return true;
1492
+ throw error;
1493
+ }
1494
+ });
1495
+ if (!stillActive)
1496
+ return null;
1497
+ if (evaluation.verdict === 'continue' && payload.stop_hook_active === true) {
1498
+ throw new Error('judgment_autonomy_continuation_exhausted');
1499
+ }
1500
+ if (evaluation.verdict !== 'not_applicable') {
1501
+ await withTurnLock(paths.lock, async () => {
1502
+ persistAutonomyReceipt(paths, sessionRef, snapshot, evaluation, config.mode);
1503
+ });
1504
+ }
1505
+ if (evaluation.verdict !== 'continue')
1506
+ return null;
1507
+ return {
1508
+ decision: 'block',
1509
+ block_kind: 'autonomy_continue',
1510
+ reason: renderJudgmentAutonomyContinuation(evaluation)
1511
+ };
1512
+ }
1337
1513
  async function finalizeEpisode(payload, env) {
1338
1514
  const { sessionRef, turnId } = hookIdentity(payload);
1339
1515
  const paths = journalPaths(sessionRef, turnId, env);
@@ -1423,6 +1599,7 @@ async function finalizeEpisode(payload, env) {
1423
1599
  ];
1424
1600
  return {
1425
1601
  decision: 'block',
1602
+ block_kind: 'audit_repair',
1426
1603
  reason: `Brainbase judgment episodeを完了する前に${reasons.join('。その後')}。監査行の後に、元の回答本文をそのまま続けてください。`
1427
1604
  };
1428
1605
  }
@@ -1457,6 +1634,9 @@ export async function processJudgmentHook(payload, options = {}) {
1457
1634
  return event ? { systemMessage: event.display_line } : {};
1458
1635
  }
1459
1636
  if (eventName === 'Stop') {
1637
+ const autonomyBlock = await evaluateAutonomyStop(payload, env, options);
1638
+ if (autonomyBlock)
1639
+ return autonomyBlock;
1460
1640
  const output = await finalizeEpisode(payload, env);
1461
1641
  if ('decision' in output && output.decision === 'block' && payload.stop_hook_active === true) {
1462
1642
  throw new Error('judgment_stop_repair_exhausted');