immune-brain 3.6.2 → 3.6.4
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/package.json +6 -3
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +72 -29
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +6 -4
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +17 -44
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +182 -62
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +17 -2
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +23 -13
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +103 -37
- package/plugins/immune-brain/runtime/claude/review_host.ts +131 -25
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +13 -8
- package/plugins/immune-brain/runtime/kernel/application.ts +5 -0
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +4 -0
- package/plugins/immune-brain/runtime/kernel/index.ts +2 -0
- package/plugins/immune-brain/runtime/kernel/observation.ts +2 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +17 -2
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +1 -1
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +0 -4
- package/plugins/immune-brain/runtime/kernel/types.ts +9 -0
- package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +2 -2
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
|
@@ -42,7 +42,7 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
// plugins/immune-brain/runtime/plugin_version.ts
|
|
45
|
-
var PLUGIN_VERSION = "3.6.
|
|
45
|
+
var PLUGIN_VERSION = "3.6.4";
|
|
46
46
|
|
|
47
47
|
// plugins/immune-brain/runtime/claude/interaction.ts
|
|
48
48
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -99,7 +99,7 @@ function enrollmentNonce() {
|
|
|
99
99
|
|
|
100
100
|
// plugins/immune-brain/runtime/claude/review_host.ts
|
|
101
101
|
import { createHash as createHash2 } from "node:crypto";
|
|
102
|
-
import { constants, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, rmSync, writeSync, closeSync } from "node:fs";
|
|
102
|
+
import { constants, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, rmSync, writeSync, closeSync } from "node:fs";
|
|
103
103
|
import { dirname, join } from "node:path";
|
|
104
104
|
import { tmpdir } from "node:os";
|
|
105
105
|
var REVIEWER_AGENT = "immune-brain-reviewer";
|
|
@@ -183,7 +183,7 @@ function appendPrivate(path, dir, line) {
|
|
|
183
183
|
closeSync(fd);
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
-
function readPrivate(path) {
|
|
186
|
+
function readPrivate(path, maxBytes = Number.POSITIVE_INFINITY) {
|
|
187
187
|
let fd;
|
|
188
188
|
try {
|
|
189
189
|
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
@@ -194,6 +194,8 @@ function readPrivate(path) {
|
|
|
194
194
|
const stat = fstatSync(fd);
|
|
195
195
|
if (!stat.isFile() || !ownedByUs(stat) || (stat.mode & 511) !== 384)
|
|
196
196
|
return;
|
|
197
|
+
if (stat.size > maxBytes)
|
|
198
|
+
return;
|
|
197
199
|
const buf = Buffer.alloc(stat.size);
|
|
198
200
|
readSync(fd, buf, 0, stat.size, 0);
|
|
199
201
|
return buf.toString("utf8");
|
|
@@ -201,6 +203,67 @@ function readPrivate(path) {
|
|
|
201
203
|
closeSync(fd);
|
|
202
204
|
}
|
|
203
205
|
}
|
|
206
|
+
var MAX_TRANSCRIPT_BYTES = 32 * 1024 * 1024;
|
|
207
|
+
function parseAsyncAgentLaunch(result) {
|
|
208
|
+
let payload;
|
|
209
|
+
try {
|
|
210
|
+
payload = JSON.parse(result);
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
if (!payload || typeof payload !== "object")
|
|
215
|
+
return null;
|
|
216
|
+
const obj = payload;
|
|
217
|
+
if (obj.isAsync !== true && obj.status !== "async_launched")
|
|
218
|
+
return null;
|
|
219
|
+
const agentId = typeof obj.agentId === "string" ? obj.agentId : "";
|
|
220
|
+
const outputFile = typeof obj.outputFile === "string" ? obj.outputFile : "";
|
|
221
|
+
if (!agentId || !outputFile)
|
|
222
|
+
return null;
|
|
223
|
+
return { agentId, outputFile };
|
|
224
|
+
}
|
|
225
|
+
function readAgentTranscriptResult(transcript, agentId) {
|
|
226
|
+
let last = null;
|
|
227
|
+
for (const line of transcript.split(`
|
|
228
|
+
`)) {
|
|
229
|
+
if (!line.trim())
|
|
230
|
+
continue;
|
|
231
|
+
let row;
|
|
232
|
+
try {
|
|
233
|
+
row = JSON.parse(line);
|
|
234
|
+
} catch {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (row.type !== "assistant" || row.agentId !== agentId)
|
|
238
|
+
continue;
|
|
239
|
+
const message = row.message;
|
|
240
|
+
if (!message || typeof message !== "object")
|
|
241
|
+
continue;
|
|
242
|
+
const content = message.content;
|
|
243
|
+
if (!Array.isArray(content))
|
|
244
|
+
continue;
|
|
245
|
+
let text = "";
|
|
246
|
+
for (const block of content) {
|
|
247
|
+
if (!block || typeof block !== "object")
|
|
248
|
+
continue;
|
|
249
|
+
const part = block;
|
|
250
|
+
if (part.type === "text" && typeof part.text === "string")
|
|
251
|
+
text += part.text;
|
|
252
|
+
}
|
|
253
|
+
if (text.trim())
|
|
254
|
+
last = text;
|
|
255
|
+
}
|
|
256
|
+
return last;
|
|
257
|
+
}
|
|
258
|
+
function readAgentTranscript(path) {
|
|
259
|
+
let resolved;
|
|
260
|
+
try {
|
|
261
|
+
resolved = realpathSync(path);
|
|
262
|
+
} catch {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
return readPrivate(resolved, MAX_TRANSCRIPT_BYTES);
|
|
266
|
+
}
|
|
204
267
|
function hookEventPath(sessionId, root = tmpdir()) {
|
|
205
268
|
return join(cacheDir(root), `${sessionHash(sessionId)}.jsonl`);
|
|
206
269
|
}
|
|
@@ -288,6 +351,7 @@ class ClaudeReviewHost {
|
|
|
288
351
|
this.log = log;
|
|
289
352
|
}
|
|
290
353
|
prepareReview(request) {
|
|
354
|
+
this.drain();
|
|
291
355
|
const initialCursors = new Map;
|
|
292
356
|
const sessionCursors = new Map;
|
|
293
357
|
for (const sessionId of this.log.sessions()) {
|
|
@@ -325,15 +389,23 @@ ${request.prompt}`,
|
|
|
325
389
|
for (let i = start;i < events.length; i++) {
|
|
326
390
|
const event = events[i];
|
|
327
391
|
if (event.type === "SessionEnd") {
|
|
328
|
-
this.log.clear(event.sessionId);
|
|
329
|
-
ended = true;
|
|
330
|
-
this.appliedBySession.delete(event.sessionId);
|
|
331
392
|
for (const [id, state] of this.pending) {
|
|
332
393
|
if (state.startEvent?.sessionId === event.sessionId || state.postEvent?.sessionId === event.sessionId || state.stopEvent?.sessionId === event.sessionId) {
|
|
333
394
|
this.pending.delete(id);
|
|
334
395
|
}
|
|
335
396
|
}
|
|
336
|
-
|
|
397
|
+
for (const state of this.pending.values()) {
|
|
398
|
+
const current = state.initialCursors.get(sessionId) ?? 0;
|
|
399
|
+
if (i + 1 > current)
|
|
400
|
+
state.initialCursors.set(sessionId, i + 1);
|
|
401
|
+
}
|
|
402
|
+
if (i === events.length - 1) {
|
|
403
|
+
this.log.clear(event.sessionId);
|
|
404
|
+
ended = true;
|
|
405
|
+
this.appliedBySession.delete(event.sessionId);
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
337
409
|
}
|
|
338
410
|
for (const state of this.pending.values()) {
|
|
339
411
|
if (state.consumed || state.error || i < (state.initialCursors.get(sessionId) ?? 0))
|
|
@@ -460,13 +532,23 @@ ${request.prompt}`,
|
|
|
460
532
|
if (state.startEvent.sessionId !== state.postEvent.sessionId || state.startEvent.sessionId !== state.stopEvent.sessionId || state.startEvent.agentId !== state.postEvent.agentId || state.startEvent.agentId !== state.stopEvent.agentId) {
|
|
461
533
|
return { ok: false, reason: "foreground Agent terminal event correlation mismatch", release: true };
|
|
462
534
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
535
|
+
const actorId = `claude:${state.startEvent.agentId ?? reservation.id}`;
|
|
536
|
+
const launch = parseAsyncAgentLaunch(state.postEvent.result);
|
|
537
|
+
if (!launch) {
|
|
538
|
+
return { ok: true, receipt: { actorId, result: state.postEvent.result } };
|
|
539
|
+
}
|
|
540
|
+
if (launch.agentId !== state.postEvent.agentId) {
|
|
541
|
+
return { ok: false, reason: "async Agent launch envelope names a different agent", release: true };
|
|
542
|
+
}
|
|
543
|
+
const transcript = readAgentTranscript(launch.outputFile);
|
|
544
|
+
if (transcript === undefined) {
|
|
545
|
+
return { ok: false, reason: "async Agent transcript is not readable", release: false };
|
|
546
|
+
}
|
|
547
|
+
const verdict = readAgentTranscriptResult(transcript, launch.agentId);
|
|
548
|
+
if (!verdict?.trim()) {
|
|
549
|
+
return { ok: false, reason: "async Agent transcript carries no reviewer result", release: false };
|
|
550
|
+
}
|
|
551
|
+
return { ok: true, receipt: { actorId, result: verdict } };
|
|
470
552
|
}
|
|
471
553
|
consumeReview(reservation) {
|
|
472
554
|
const result = this.inspectReview(reservation);
|
|
@@ -514,6 +596,11 @@ function parseHookStdin(raw) {
|
|
|
514
596
|
if (typeof toolInputObj.prompt === "string")
|
|
515
597
|
prompt = toolInputObj.prompt;
|
|
516
598
|
}
|
|
599
|
+
if (!prompt && typeof payload.tool_response === "object" && payload.tool_response !== null) {
|
|
600
|
+
const toolResponseObj = payload.tool_response;
|
|
601
|
+
if (typeof toolResponseObj.prompt === "string")
|
|
602
|
+
prompt = toolResponseObj.prompt;
|
|
603
|
+
}
|
|
517
604
|
let extractedOpId = operationId;
|
|
518
605
|
let extractedTaskId = taskId;
|
|
519
606
|
for (const candidate of [payload.input, payload.tool_input, payload.tool_response, payload.toolResponse, payload.response, payload.result]) {
|
|
@@ -602,7 +689,7 @@ import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypt
|
|
|
602
689
|
// plugins/immune-brain/runtime/assurance/verification.ts
|
|
603
690
|
import { createHash as createHash3 } from "node:crypto";
|
|
604
691
|
import { execFileSync, spawn } from "node:child_process";
|
|
605
|
-
import { realpathSync, statSync } from "node:fs";
|
|
692
|
+
import { realpathSync as realpathSync2, statSync } from "node:fs";
|
|
606
693
|
import { isAbsolute as isAbsolute2, resolve, sep as sep2, relative } from "node:path";
|
|
607
694
|
|
|
608
695
|
// plugins/immune-brain/runtime/verification_descriptor.ts
|
|
@@ -705,7 +792,7 @@ function resolveBunRunner() {
|
|
|
705
792
|
}
|
|
706
793
|
let real;
|
|
707
794
|
try {
|
|
708
|
-
real =
|
|
795
|
+
real = realpathSync2(executable);
|
|
709
796
|
} catch {
|
|
710
797
|
throw new VerificationDescriptorError("bun runner realpath is unresolvable");
|
|
711
798
|
}
|
|
@@ -1710,7 +1797,7 @@ import {
|
|
|
1710
1797
|
writeFileSync,
|
|
1711
1798
|
statSync as statSync2,
|
|
1712
1799
|
readFileSync as readFileSync3,
|
|
1713
|
-
realpathSync as
|
|
1800
|
+
realpathSync as realpathSync4,
|
|
1714
1801
|
chmodSync
|
|
1715
1802
|
} from "node:fs";
|
|
1716
1803
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
@@ -1724,7 +1811,7 @@ import {
|
|
|
1724
1811
|
lstatSync as lstatSync2,
|
|
1725
1812
|
readFileSync as readFileSync2,
|
|
1726
1813
|
readlinkSync,
|
|
1727
|
-
realpathSync as
|
|
1814
|
+
realpathSync as realpathSync3
|
|
1728
1815
|
} from "node:fs";
|
|
1729
1816
|
import { resolve as resolve2 } from "node:path";
|
|
1730
1817
|
function git(root, args) {
|
|
@@ -1891,7 +1978,7 @@ function taskSnapshotOnce(root, scope) {
|
|
|
1891
1978
|
const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
|
|
1892
1979
|
if (!repositoryRoot || !head || !GIT_OBJECT_ID.test(head))
|
|
1893
1980
|
throw new Error("cannot derive task snapshot outside a committed Git workspace");
|
|
1894
|
-
if (
|
|
1981
|
+
if (realpathSync3(resolve2(repositoryRoot)) !== root)
|
|
1895
1982
|
throw new Error("task snapshot repository root does not match the project root");
|
|
1896
1983
|
const sparseCheckout = git(root, ["config", "--bool", "core.sparseCheckout"])?.trim();
|
|
1897
1984
|
const sparseIndex = git(root, ["config", "--bool", "index.sparse"])?.trim();
|
|
@@ -1935,7 +2022,7 @@ function captureGitTaskSnapshot(projectRoot, scopeHint) {
|
|
|
1935
2022
|
const requestedStat = lstatSync2(requestedRoot);
|
|
1936
2023
|
if (requestedStat.isSymbolicLink() || !requestedStat.isDirectory())
|
|
1937
2024
|
throw new Error("task snapshot root must be a real directory");
|
|
1938
|
-
const root =
|
|
2025
|
+
const root = realpathSync3(requestedRoot);
|
|
1939
2026
|
const scope = assertCanonicalTaskScope(scopeHint);
|
|
1940
2027
|
const before = taskSnapshotOnce(root, scope);
|
|
1941
2028
|
gitTaskSnapshotTestHook?.();
|
|
@@ -1968,7 +2055,7 @@ function taskRevisionSnapshotOnce(root, scope, baseHead) {
|
|
|
1968
2055
|
const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
|
|
1969
2056
|
if (!repositoryRoot || !head || !GIT_OBJECT_ID.test(head))
|
|
1970
2057
|
throw new Error("cannot derive a task revision outside a committed Git workspace");
|
|
1971
|
-
if (
|
|
2058
|
+
if (realpathSync3(resolve2(repositoryRoot)) !== root)
|
|
1972
2059
|
throw new Error("task revision repository root does not match the project root");
|
|
1973
2060
|
if (gitRequired(root, ["cat-file", "-t", baseHead], `task revision base is unreadable: ${baseHead}`) !== "commit")
|
|
1974
2061
|
throw new Error(`task revision base is not a commit: ${baseHead}`);
|
|
@@ -2025,7 +2112,7 @@ function captureGitTaskRevisionSnapshot(projectRoot, scopeHint, baseHead) {
|
|
|
2025
2112
|
const requestedStat = lstatSync2(requestedRoot);
|
|
2026
2113
|
if (requestedStat.isSymbolicLink() || !requestedStat.isDirectory())
|
|
2027
2114
|
throw new Error("task revision root must be a real directory");
|
|
2028
|
-
const root =
|
|
2115
|
+
const root = realpathSync3(requestedRoot);
|
|
2029
2116
|
if (typeof baseHead !== "string" || !GIT_OBJECT_ID.test(baseHead.toLowerCase()))
|
|
2030
2117
|
throw new Error("task revision base must be a Git commit id");
|
|
2031
2118
|
const scope = assertCanonicalTaskScope(scopeHint);
|
|
@@ -2397,13 +2484,10 @@ function captureReviewManifest(root, input) {
|
|
|
2397
2484
|
throw new Error("immutable review manifest metadata exceeds bounded output limit");
|
|
2398
2485
|
return manifest;
|
|
2399
2486
|
}
|
|
2400
|
-
function ensureReviewRevision(root, input) {
|
|
2401
|
-
return publishInput(root, input).revision;
|
|
2402
|
-
}
|
|
2403
2487
|
function writeNativeReviewEvidence(payload) {
|
|
2404
2488
|
const rawDirectory = mkdtempSync(join4(tmpdir2(), "imm-canary-native-review-"));
|
|
2405
2489
|
try {
|
|
2406
|
-
const directory =
|
|
2490
|
+
const directory = realpathSync4(rawDirectory);
|
|
2407
2491
|
chmodSync(directory, 493);
|
|
2408
2492
|
const path = join4(directory, "evidence.json");
|
|
2409
2493
|
writeFileSync(path, JSON.stringify(payload), { encoding: "utf8", mode: 420, flag: "wx" });
|
|
@@ -2418,7 +2502,7 @@ function writeNativeReviewEvidence(payload) {
|
|
|
2418
2502
|
}
|
|
2419
2503
|
}
|
|
2420
2504
|
function assertReviewArtifact(path) {
|
|
2421
|
-
const targetPath =
|
|
2505
|
+
const targetPath = realpathSync4(path);
|
|
2422
2506
|
let stat;
|
|
2423
2507
|
try {
|
|
2424
2508
|
stat = statSync2(targetPath);
|
|
@@ -2612,7 +2696,7 @@ import {
|
|
|
2612
2696
|
mkdirSync as mkdirSync2,
|
|
2613
2697
|
openSync as openSync3,
|
|
2614
2698
|
readFileSync as readFileSync6,
|
|
2615
|
-
realpathSync as
|
|
2699
|
+
realpathSync as realpathSync6,
|
|
2616
2700
|
renameSync,
|
|
2617
2701
|
rmSync as rmSync3,
|
|
2618
2702
|
writeFileSync as writeFileSync2
|
|
@@ -2629,6 +2713,9 @@ var TASK_RECORD_CONTRACT_V2 = "assurance_kernel/task_record/v2";
|
|
|
2629
2713
|
var TASK_RECORD_CONTRACT_V3 = "assurance_kernel/task_record/v3";
|
|
2630
2714
|
var TASK_RECORD_CONTRACT_V4 = "assurance_kernel/task_record/v4";
|
|
2631
2715
|
var REVIEW_REVISION_IDENTITY_CONTRACT = "assurance_kernel/review_revision_identity/v1";
|
|
2716
|
+
function isTaskRecordV4(record) {
|
|
2717
|
+
return record.contract === TASK_RECORD_CONTRACT_V4;
|
|
2718
|
+
}
|
|
2632
2719
|
var REDUCED_MUTATION_BRAND = Symbol("assurance-kernel-reduced-mutation-v2");
|
|
2633
2720
|
var MUTATION_AUTHORITY_CAPABILITY_BRAND = Symbol("assurance-kernel-mutation-authority-capability");
|
|
2634
2721
|
|
|
@@ -2641,7 +2728,7 @@ import {
|
|
|
2641
2728
|
lstatSync as lstatSync4,
|
|
2642
2729
|
openSync as openSync2,
|
|
2643
2730
|
readFileSync as readFileSync5,
|
|
2644
|
-
realpathSync as
|
|
2731
|
+
realpathSync as realpathSync5
|
|
2645
2732
|
} from "node:fs";
|
|
2646
2733
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
2647
2734
|
import { join as join6, resolve as resolve4, sep as sep3 } from "node:path";
|
|
@@ -2939,7 +3026,7 @@ function resolveCanonicalRoot(root) {
|
|
|
2939
3026
|
const rootStat = lstatSync4(resolved);
|
|
2940
3027
|
if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
|
|
2941
3028
|
throw new Error("project root must be a real directory, not a symlink");
|
|
2942
|
-
return
|
|
3029
|
+
return realpathSync5(resolved);
|
|
2943
3030
|
}
|
|
2944
3031
|
function resolveSidecarPath(canonicalRoot, activePath, archivedPath) {
|
|
2945
3032
|
if (sidecarPresent(canonicalRoot, activePath))
|
|
@@ -3016,7 +3103,7 @@ function readTaskIntent(root, taskId, requestedPath) {
|
|
|
3016
3103
|
const after = lstatSync4(target);
|
|
3017
3104
|
assertSameIdentity(statIdentity(before), after, "intent sidecar");
|
|
3018
3105
|
assertIdentitiesUnchanged(pathIdentities, canonicalRoot, sidecarPath);
|
|
3019
|
-
const canonicalAgain =
|
|
3106
|
+
const canonicalAgain = realpathSync5(root);
|
|
3020
3107
|
if (canonicalAgain !== canonicalRoot)
|
|
3021
3108
|
throw new Error("canonical project root drifted while being read");
|
|
3022
3109
|
if (lstatSync4(canonicalAgain).isSymbolicLink())
|
|
@@ -3759,7 +3846,7 @@ function revisionFor(content) {
|
|
|
3759
3846
|
}
|
|
3760
3847
|
function canonicalRoot(root) {
|
|
3761
3848
|
try {
|
|
3762
|
-
return
|
|
3849
|
+
return realpathSync6(root);
|
|
3763
3850
|
} catch {
|
|
3764
3851
|
throw new KernelStoreSecurityError("project root is unavailable");
|
|
3765
3852
|
}
|
|
@@ -5650,6 +5737,8 @@ function applyTaskAction(input) {
|
|
|
5650
5737
|
};
|
|
5651
5738
|
}
|
|
5652
5739
|
if (input.terminal) {
|
|
5740
|
+
if (nextRecord.lifecycle === "active")
|
|
5741
|
+
throw new Error("terminal settlement requires a done or stopped TaskRecord lifecycle");
|
|
5653
5742
|
const tombstone = {
|
|
5654
5743
|
contract: TASK_TOMBSTONE_CONTRACT,
|
|
5655
5744
|
task_id,
|
|
@@ -6389,6 +6478,8 @@ function enrollCanaryTask(root, input, registry) {
|
|
|
6389
6478
|
if (checks.gitBaseHead !== gitBaseHead)
|
|
6390
6479
|
throw new Error("Git HEAD moved after the enrollment confirmation");
|
|
6391
6480
|
registry.consume(input.capability, input.capability_binding);
|
|
6481
|
+
if (!gitBaseHead)
|
|
6482
|
+
throw new Error("enrollment requires a committed Git HEAD");
|
|
6392
6483
|
const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
|
|
6393
6484
|
const nextWorkspace = {
|
|
6394
6485
|
...checks.workspace.state,
|
|
@@ -6564,6 +6655,9 @@ async function submitClaudeReview(host, coordinator, ctx, taskId, verdictInput)
|
|
|
6564
6655
|
}
|
|
6565
6656
|
return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
6566
6657
|
}
|
|
6658
|
+
function stopReason(value) {
|
|
6659
|
+
return typeof value === "string" && value.length > 0 ? value : "user stop";
|
|
6660
|
+
}
|
|
6567
6661
|
function assertProjectionBinding(before, after, allowDiffChange = false) {
|
|
6568
6662
|
const fields = allowDiffChange ? ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash"] : ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash", "diff_hash"];
|
|
6569
6663
|
if (before.error || !before.claim || after.error || !after.claim || before.claim.task_id !== after.claim.task_id || fields.some((field) => before.projection[field] !== after.projection[field])) {
|
|
@@ -6573,22 +6667,57 @@ function assertProjectionBinding(before, after, allowDiffChange = false) {
|
|
|
6573
6667
|
function qaOutcomes(record) {
|
|
6574
6668
|
return Object.fromEntries(record.attestations.filter((item) => item.kind === "qa").flatMap((item) => item.acceptance_results).map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]));
|
|
6575
6669
|
}
|
|
6670
|
+
async function ensureClaudeReviewRevision(root, taskId, projection) {
|
|
6671
|
+
const current = await readTaskRecord(root, taskId);
|
|
6672
|
+
const record = current.record;
|
|
6673
|
+
if (!record)
|
|
6674
|
+
throw new Error(`task ${taskId} has no TaskRecord`);
|
|
6675
|
+
if (current.revision !== projection.projection.record_revision)
|
|
6676
|
+
throw new Error("TaskRecord changed before Review revision preparation");
|
|
6677
|
+
if (record.contract !== "assurance_kernel/task_record/v4")
|
|
6678
|
+
return null;
|
|
6679
|
+
if (!record.git_base_head)
|
|
6680
|
+
throw new Error("Review revision requires a TaskRecord v4 git_base_head");
|
|
6681
|
+
const manifest = captureReviewManifest(root, {
|
|
6682
|
+
taskId,
|
|
6683
|
+
baseHead: record.git_base_head,
|
|
6684
|
+
scopeHint: record.intent_snapshot.scope_hint,
|
|
6685
|
+
expectedDiffHash: projection.projection.diff_hash,
|
|
6686
|
+
intentRevision: projection.projection.intent_revision,
|
|
6687
|
+
intentContentHash: projection.projection.intent_content_hash,
|
|
6688
|
+
recordRevision: projection.projection.record_revision,
|
|
6689
|
+
workspaceRevision: projection.projection.workspace_revision,
|
|
6690
|
+
lifecycle: projection.projection.lifecycle,
|
|
6691
|
+
artifactState: projection.projection.artifact_state,
|
|
6692
|
+
risk: record.intent_snapshot.risk,
|
|
6693
|
+
outcomes: qaOutcomes(record)
|
|
6694
|
+
});
|
|
6695
|
+
return {
|
|
6696
|
+
contract: "assurance_kernel/review_revision/v1",
|
|
6697
|
+
base_head: manifest.base_head,
|
|
6698
|
+
review_tree: manifest.review_tree,
|
|
6699
|
+
review_commit: manifest.review_commit,
|
|
6700
|
+
review_ref: manifest.review_ref,
|
|
6701
|
+
diff_hash: manifest.diff_hash,
|
|
6702
|
+
manifest_digest: manifest.manifest_digest
|
|
6703
|
+
};
|
|
6704
|
+
}
|
|
6576
6705
|
async function buildAssuranceSnapshot(root, taskId, role, projection, runner) {
|
|
6577
|
-
const
|
|
6578
|
-
|
|
6706
|
+
const read = await readTaskRecord(root, taskId);
|
|
6707
|
+
const record = read.record;
|
|
6708
|
+
if (!record || read.revision !== projection.projection.record_revision)
|
|
6579
6709
|
throw new Error("TaskRecord changed before assurance snapshot capture");
|
|
6580
|
-
const intent = record.
|
|
6710
|
+
const intent = record.intent_snapshot;
|
|
6581
6711
|
const descriptors = new Map;
|
|
6582
6712
|
for (const item of intent.acceptance) {
|
|
6583
6713
|
const descriptor = parseVerificationDescriptor(item.verification);
|
|
6584
6714
|
assertRunnerCompatible(descriptor, runner);
|
|
6585
6715
|
descriptors.set(item.id, descriptor);
|
|
6586
6716
|
}
|
|
6587
|
-
const
|
|
6588
|
-
const
|
|
6589
|
-
const reviewManifest = role === "review" && v4 ? captureReviewManifest(root, {
|
|
6717
|
+
const reviewBundle = role === "review" && !isTaskRecordV4(record) ? captureReviewBundle(root, intent.scope_hint, projection.projection.diff_hash, qaOutcomes(record)) : null;
|
|
6718
|
+
const reviewManifest = role === "review" && isTaskRecordV4(record) ? captureReviewManifest(root, {
|
|
6590
6719
|
taskId,
|
|
6591
|
-
baseHead: record.
|
|
6720
|
+
baseHead: record.git_base_head,
|
|
6592
6721
|
scopeHint: intent.scope_hint,
|
|
6593
6722
|
expectedDiffHash: projection.projection.diff_hash,
|
|
6594
6723
|
intentRevision: projection.projection.intent_revision,
|
|
@@ -6598,7 +6727,7 @@ async function buildAssuranceSnapshot(root, taskId, role, projection, runner) {
|
|
|
6598
6727
|
lifecycle: projection.projection.lifecycle,
|
|
6599
6728
|
artifactState: projection.projection.artifact_state,
|
|
6600
6729
|
risk: intent.risk,
|
|
6601
|
-
outcomes: qaOutcomes(record
|
|
6730
|
+
outcomes: qaOutcomes(record)
|
|
6602
6731
|
}) : null;
|
|
6603
6732
|
const dirtyFiles = reviewManifest ? Object.keys(reviewManifest.changed_paths) : reviewBundle ? Object.keys(reviewBundle.dirty_files) : [];
|
|
6604
6733
|
const snapshot = {
|
|
@@ -6697,11 +6826,11 @@ class ClaudeRuntime {
|
|
|
6697
6826
|
this.interactive = options.interactive ?? true;
|
|
6698
6827
|
this.requestConfirmation = options.requestConfirmation;
|
|
6699
6828
|
this.host = options.host ?? new ClaudeReviewHost(new FileHookEventLog);
|
|
6700
|
-
|
|
6701
|
-
this.
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6829
|
+
this.coordinator = new AssuranceCoordinator({
|
|
6830
|
+
...this.createKernelPorts(),
|
|
6831
|
+
...options.ports,
|
|
6832
|
+
host: this.host
|
|
6833
|
+
});
|
|
6705
6834
|
}
|
|
6706
6835
|
observe(event) {
|
|
6707
6836
|
this.host.observe(event);
|
|
@@ -6715,27 +6844,18 @@ class ClaudeRuntime {
|
|
|
6715
6844
|
async shutdown() {
|
|
6716
6845
|
await this.coordinator.onSessionShutdown();
|
|
6717
6846
|
}
|
|
6847
|
+
kernelPorts() {
|
|
6848
|
+
return this.createKernelPorts();
|
|
6849
|
+
}
|
|
6718
6850
|
createKernelPorts() {
|
|
6719
6851
|
return {
|
|
6720
6852
|
host: this.host,
|
|
6721
6853
|
projectTask: (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
|
|
6722
|
-
readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
|
|
6723
|
-
readTaskIntent: (root, taskId) => readTaskIntentForRecord(root, taskId),
|
|
6854
|
+
readTaskRecord: async (root, taskId) => readTaskRecord(root, taskId),
|
|
6855
|
+
readTaskIntent: async (root, taskId) => readTaskIntentForRecord(root, taskId),
|
|
6724
6856
|
frozenRunner: async () => resolveBunRunner(),
|
|
6725
6857
|
buildAssurance: (root, taskId, role, projection, runner) => buildAssuranceSnapshot(root, taskId, role, projection, runner),
|
|
6726
|
-
ensureReviewRevision:
|
|
6727
|
-
const current = await readTaskRecord(root, taskId);
|
|
6728
|
-
if (!current.record)
|
|
6729
|
-
throw new Error(`task ${taskId} has no TaskRecord`);
|
|
6730
|
-
if (current.record.contract !== "assurance_kernel/task_record/v4")
|
|
6731
|
-
return null;
|
|
6732
|
-
return ensureReviewRevision(root, {
|
|
6733
|
-
taskId,
|
|
6734
|
-
baseHead: current.record.git_base_head,
|
|
6735
|
-
scopeHint: current.record.intent_snapshot.scope_hint,
|
|
6736
|
-
expectedDiffHash: projection.projection.diff_hash
|
|
6737
|
-
});
|
|
6738
|
-
},
|
|
6858
|
+
ensureReviewRevision: (root, taskId, projection) => ensureClaudeReviewRevision(root, taskId, projection),
|
|
6739
6859
|
runQa: (snapshot, descriptors, runner, options) => runDeterministicQa(snapshot, descriptors, runner, options),
|
|
6740
6860
|
writeReviewEvidence: (input) => writeNativeReviewEvidence(input.evidence),
|
|
6741
6861
|
applyVerdict: (ctx, input) => this.applyVerdict(ctx, input),
|
|
@@ -6950,7 +7070,7 @@ class ClaudeRuntime {
|
|
|
6950
7070
|
confirmation_ref: confirmation,
|
|
6951
7071
|
...op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {},
|
|
6952
7072
|
...op === "resolve_user_decision" && decisionOp ? decisionOp : {},
|
|
6953
|
-
...op === "stop" ? { reason: extra.reason
|
|
7073
|
+
...op === "stop" ? { reason: stopReason(extra.reason) } : {}
|
|
6954
7074
|
});
|
|
6955
7075
|
throwIfCancelled(meta.signal);
|
|
6956
7076
|
const result = app.execute({
|
|
@@ -6962,7 +7082,7 @@ class ClaudeRuntime {
|
|
|
6962
7082
|
actor_id: actorId,
|
|
6963
7083
|
...op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {},
|
|
6964
7084
|
...op === "resolve_user_decision" && decisionOp ? decisionOp : {},
|
|
6965
|
-
...op === "stop" ? { reason: extra.reason
|
|
7085
|
+
...op === "stop" ? { reason: stopReason(extra.reason) } : {}
|
|
6966
7086
|
},
|
|
6967
7087
|
prior_intent_token: priorIntent.token,
|
|
6968
7088
|
diffProvider: diffSnapshotOf,
|
|
@@ -17,6 +17,7 @@ import { createInvocationRegistry, type InvocationState, type InvocationToken }
|
|
|
17
17
|
import type { ReviewBundle, ReviewManifestV5, ReviewRevision } from "./review_evidence";
|
|
18
18
|
import { buildRoleDelegationPacket } from "../role_prompt_bridge";
|
|
19
19
|
import type { AssuranceProjectionResult } from "../kernel/assurance_projection";
|
|
20
|
+
import type { TaskIntentIdentityToken } from "../kernel/intent_token_registry";
|
|
20
21
|
import type { AssuranceHostPort, HostReviewReservation } from "./host_port";
|
|
21
22
|
|
|
22
23
|
export type AssuranceRole = "qa" | "review";
|
|
@@ -38,7 +39,11 @@ export interface TaskRecordRead {
|
|
|
38
39
|
record?: { contract?: string; findings: Array<{ kind: string; status: string }> } | null;
|
|
39
40
|
}
|
|
40
41
|
export interface TaskIntentRead {
|
|
41
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The kernel's own intent identity token. Declared as `string` before this
|
|
44
|
+
* port was ever type checked, which is a shape no host reader has produced.
|
|
45
|
+
*/
|
|
46
|
+
token?: TaskIntentIdentityToken;
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
export interface GithubTerminalProjectionInput {
|
|
@@ -873,7 +878,17 @@ export class AssuranceCoordinator {
|
|
|
873
878
|
}
|
|
874
879
|
}
|
|
875
880
|
|
|
876
|
-
|
|
881
|
+
/**
|
|
882
|
+
* Declared as the wider advance result, which made it unusable from
|
|
883
|
+
* `submitReview` without an implicit widen. The value has always been the
|
|
884
|
+
* settlement_unknown member both result unions share.
|
|
885
|
+
*/
|
|
886
|
+
private unknownAfterCommit(
|
|
887
|
+
taskId: string,
|
|
888
|
+
operation: "qa" | "review",
|
|
889
|
+
operationId: string,
|
|
890
|
+
reason: string,
|
|
891
|
+
): { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string } {
|
|
877
892
|
this.unknownOperations.set(taskId, { operation, operationId, reason });
|
|
878
893
|
return { state: "settlement_unknown", operation, operation_id: operationId, reason };
|
|
879
894
|
}
|
|
@@ -240,14 +240,32 @@ const SNAPSHOT_IDENTITY = {
|
|
|
240
240
|
date: "1970-01-01T00:00:00 +0000",
|
|
241
241
|
};
|
|
242
242
|
|
|
243
|
-
|
|
243
|
+
/**
|
|
244
|
+
* The bare synthetic-commit identity. `publishReviewRevision` can prove these
|
|
245
|
+
* five fields from Git alone; it has no manifest inputs and therefore cannot
|
|
246
|
+
* produce a digest.
|
|
247
|
+
*/
|
|
248
|
+
export interface ReviewRevisionCommit {
|
|
244
249
|
contract: "assurance_kernel/review_revision/v1";
|
|
245
250
|
base_head: string;
|
|
246
251
|
review_tree: string;
|
|
247
252
|
review_commit: string;
|
|
248
253
|
review_ref: string;
|
|
249
254
|
diff_hash: string;
|
|
250
|
-
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The full Review revision identity a host port must return.
|
|
259
|
+
*
|
|
260
|
+
* `manifest_digest` was optional here, which is the type-level hole that let a
|
|
261
|
+
* host return the bare commit identity and still satisfy
|
|
262
|
+
* `AssuranceCoordinatorPorts.ensureReviewRevision`. `submitReview` compares all
|
|
263
|
+
* four identity fields against the reservation, so an absent digest compared
|
|
264
|
+
* against a real one and failed every v4 submission at runtime instead of at
|
|
265
|
+
* build time. Requiring it makes that omission a compile error.
|
|
266
|
+
*/
|
|
267
|
+
export interface ReviewRevision extends ReviewRevisionCommit {
|
|
268
|
+
manifest_digest: string;
|
|
251
269
|
}
|
|
252
270
|
|
|
253
271
|
export interface ReviewManifestV5 {
|
|
@@ -354,7 +372,7 @@ export function publishReviewRevision(
|
|
|
354
372
|
snapshot: GitTaskRevisionSnapshot,
|
|
355
373
|
diffHash: string,
|
|
356
374
|
taskId: string,
|
|
357
|
-
):
|
|
375
|
+
): ReviewRevisionCommit {
|
|
358
376
|
if (snapshot.base_head !== snapshot.base_head.toLowerCase() || !GIT_COMMIT_ID.test(snapshot.base_head))
|
|
359
377
|
throw new Error("review revision base has invalid identity");
|
|
360
378
|
if (!REVISION_DIFF_HASH.test(diffHash)) throw new Error("review revision diff hash has invalid identity");
|
|
@@ -429,7 +447,7 @@ function publishInput(
|
|
|
429
447
|
scopeHint: unknown;
|
|
430
448
|
expectedDiffHash: string;
|
|
431
449
|
},
|
|
432
|
-
): { snapshot: GitTaskRevisionSnapshot; revision:
|
|
450
|
+
): { snapshot: GitTaskRevisionSnapshot; revision: ReviewRevisionCommit } {
|
|
433
451
|
if (typeof input.baseHead !== "string" || !GIT_COMMIT_ID.test(input.baseHead))
|
|
434
452
|
throw new Error("review requires a TaskRecord v4 git_base_head");
|
|
435
453
|
if (!REVISION_DIFF_HASH.test(input.expectedDiffHash))
|
|
@@ -486,14 +504,6 @@ export function captureReviewManifest(
|
|
|
486
504
|
return manifest;
|
|
487
505
|
}
|
|
488
506
|
|
|
489
|
-
/** Publish and return the exact revision a v4 task must use. */
|
|
490
|
-
export function ensureReviewRevision(
|
|
491
|
-
root: string,
|
|
492
|
-
input: { taskId: string; baseHead: string; scopeHint: unknown; expectedDiffHash: string },
|
|
493
|
-
): ReviewRevision {
|
|
494
|
-
return publishInput(root, input).revision;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
507
|
export function listReviewRefs(root: string): Array<{ ref: string; commit: string; taskId: string }> {
|
|
498
508
|
const output = gitEvidence(root, ["for-each-ref", "--format=%(refname) %(objectname)", `${REVIEW_REF_NAMESPACE}/`]);
|
|
499
509
|
const refs: Array<{ ref: string; commit: string; taskId: string }> = [];
|
|
@@ -534,7 +544,7 @@ export function reconcileReviewRefs(
|
|
|
534
544
|
return { removed, failed };
|
|
535
545
|
}
|
|
536
546
|
|
|
537
|
-
export function deleteReviewRef(root: string, revision:
|
|
547
|
+
export function deleteReviewRef(root: string, revision: ReviewRevisionCommit): void {
|
|
538
548
|
const parts = revision.review_ref.split("/");
|
|
539
549
|
let validRef = false;
|
|
540
550
|
if (
|