immune-brain 3.6.3 → 3.6.5
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 +117 -47
- 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 +213 -81
- package/plugins/immune-brain/dist/imm-loop.md +8 -2
- package/plugins/immune-brain/dist/imm-planner.md +9 -4
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +35 -2
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +23 -13
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +133 -45
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +14 -1
- package/plugins/immune-brain/runtime/claude/review_host.ts +0 -10
- package/plugins/immune-brain/runtime/commands/kernel.ts +15 -13
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +107 -11
- package/plugins/immune-brain/runtime/kernel/application.ts +6 -0
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +407 -0
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +17 -8
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +76 -13
- package/plugins/immune-brain/runtime/kernel/index.ts +2 -0
- package/plugins/immune-brain/runtime/kernel/intent.ts +67 -23
- package/plugins/immune-brain/runtime/kernel/observation.ts +2 -0
- package/plugins/immune-brain/runtime/kernel/reducer.ts +34 -8
- 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 +10 -0
- package/plugins/immune-brain/runtime/kernel/validation.ts +10 -6
- package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +2 -2
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
|
@@ -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 {
|
|
@@ -431,6 +436,7 @@ export class AssuranceCoordinator {
|
|
|
431
436
|
sessionGenerationValue(): number { return this.sessionGeneration; }
|
|
432
437
|
|
|
433
438
|
async advance(taskId: string, ctx: HostContext, signal?: AbortSignal, onUpdate?: (update: ForegroundToolUpdate) => void): Promise<AssuranceAdvanceResult> {
|
|
439
|
+
if (this.isInvocationOpen(taskId)) return { state: "blocked", reason: "an authority invocation is already open" };
|
|
434
440
|
const active = this.active(taskId);
|
|
435
441
|
if (active?.state === "review_ready") {
|
|
436
442
|
const reservation = this.reviewReservations.get(taskId);
|
|
@@ -814,6 +820,17 @@ export class AssuranceCoordinator {
|
|
|
814
820
|
return { state: "blocked", reason: `Kernel requires ${settled.projection.next_obligation} after Review` };
|
|
815
821
|
}
|
|
816
822
|
|
|
823
|
+
isReviewVerdictValid(taskId: string, verdictInput: unknown): boolean {
|
|
824
|
+
const reservation = this.reviewReservations.get(taskId);
|
|
825
|
+
if (!reservation) return false;
|
|
826
|
+
try {
|
|
827
|
+
parseAssuranceVerdict(verdictInput, reservation.snapshot);
|
|
828
|
+
return true;
|
|
829
|
+
} catch {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
817
834
|
abandonReview(taskId: string, reason: string): AssuranceSubmitReviewResult {
|
|
818
835
|
const reservation = this.reviewReservations.get(taskId);
|
|
819
836
|
if (!reservation) return { state: "blocked", reason };
|
|
@@ -828,6 +845,12 @@ export class AssuranceCoordinator {
|
|
|
828
845
|
return { state: "review_ready", operation: "review", operation_id: reservation.operationId, snapshot_digest: snapshotDigest(reservation.snapshot), review_bundle_digest: reservation.snapshot.review_bundle_digest ?? "", agent_params: reservation.hostReservation.dispatch };
|
|
829
846
|
}
|
|
830
847
|
|
|
848
|
+
releaseStoppedReview(taskId: string): void {
|
|
849
|
+
const reservation = this.reviewReservations.get(taskId);
|
|
850
|
+
if (reservation) this.releaseReviewReservation(taskId, reservation);
|
|
851
|
+
this.rejectedReviewOperations.delete(taskId);
|
|
852
|
+
}
|
|
853
|
+
|
|
831
854
|
private releaseReviewReservation(taskId: string, reservation: ReviewReservation, rejectionReason?: string): void {
|
|
832
855
|
if (this.reviewReservations.get(taskId) !== reservation) return;
|
|
833
856
|
this.reviewReservations.delete(taskId);
|
|
@@ -873,7 +896,17 @@ export class AssuranceCoordinator {
|
|
|
873
896
|
}
|
|
874
897
|
}
|
|
875
898
|
|
|
876
|
-
|
|
899
|
+
/**
|
|
900
|
+
* Declared as the wider advance result, which made it unusable from
|
|
901
|
+
* `submitReview` without an implicit widen. The value has always been the
|
|
902
|
+
* settlement_unknown member both result unions share.
|
|
903
|
+
*/
|
|
904
|
+
private unknownAfterCommit(
|
|
905
|
+
taskId: string,
|
|
906
|
+
operation: "qa" | "review",
|
|
907
|
+
operationId: string,
|
|
908
|
+
reason: string,
|
|
909
|
+
): { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string } {
|
|
877
910
|
this.unknownOperations.set(taskId, { operation, operationId, reason });
|
|
878
911
|
return { state: "settlement_unknown", operation, operation_id: operationId, reason };
|
|
879
912
|
}
|
|
@@ -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 (
|
|
@@ -20,12 +20,12 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
captureReviewBundle,
|
|
22
22
|
captureReviewManifest,
|
|
23
|
-
ensureReviewRevision,
|
|
24
23
|
writeNativeReviewEvidence,
|
|
24
|
+
type ReviewRevision,
|
|
25
25
|
} from "../assurance/review_evidence";
|
|
26
26
|
import { parseVerificationDescriptor } from "../verification_descriptor";
|
|
27
|
-
import { projectAssurance, type AssuranceProjectionResult } from "../kernel/assurance_projection";
|
|
28
|
-
import type
|
|
27
|
+
import { projectAssurance, type AssuranceProjection, type AssuranceProjectionResult } from "../kernel/assurance_projection";
|
|
28
|
+
import { isTaskRecordV4, type TaskApprovalV2, type TaskRecord } from "../kernel/types";
|
|
29
29
|
import { readTaskRecord, readTaskRecordRaw } from "../kernel/storage";
|
|
30
30
|
import { canonicalIntentHash, parseTaskIntentV1, readTaskIntent } from "../kernel/intent";
|
|
31
31
|
import { capabilityActionFor, createCanaryApplication } from "../kernel/canary_application";
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
isPrivilegedOperation,
|
|
52
52
|
NativeAuthorityError,
|
|
53
53
|
type NativeConfirmationPort,
|
|
54
|
+
type PrivilegedOperation,
|
|
54
55
|
} from "./interaction";
|
|
55
56
|
import { ClaudeReviewHost, FileHookEventLog, type ClaudeHookEvent } from "./review_host";
|
|
56
57
|
import { probeHost, type PermissionMode } from "./capability";
|
|
@@ -118,21 +119,29 @@ export async function submitClaudeReview(
|
|
|
118
119
|
if (observed.release) return coordinator.abandonReview(taskId, observed.reason);
|
|
119
120
|
return { state: "blocked", reason: observed.reason };
|
|
120
121
|
}
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
|
|
123
|
+
if (!parentValid) return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
124
|
+
const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
|
|
125
|
+
if (!receiptValid) {
|
|
126
|
+
return coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict");
|
|
125
127
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
+
const parentJson = extractVerdictJson(verdictInput)!;
|
|
129
|
+
const receiptJson = extractVerdictJson(observed.receipt.result)!;
|
|
130
|
+
if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
|
|
131
|
+
return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
|
|
128
132
|
}
|
|
129
133
|
return coordinator.submitReview(taskId, ctx, verdictInput);
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
/** `extra` arrives as `Record<string, unknown>`; only a real string is a reason. */
|
|
137
|
+
function stopReason(value: unknown): string {
|
|
138
|
+
return typeof value === "string" && value.length > 0 ? value : "user stop";
|
|
139
|
+
}
|
|
140
|
+
|
|
132
141
|
function assertProjectionBinding(before: AssuranceProjectionResult, after: AssuranceProjectionResult, allowDiffChange = false): void {
|
|
133
|
-
const fields =
|
|
142
|
+
const fields: ReadonlyArray<keyof AssuranceProjection> = allowDiffChange
|
|
134
143
|
? ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash"]
|
|
135
|
-
: ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash", "diff_hash"]
|
|
144
|
+
: ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash", "diff_hash"];
|
|
136
145
|
if (before.error || !before.claim || after.error || !after.claim || before.claim.task_id !== after.claim.task_id
|
|
137
146
|
|| fields.some((field) => before.projection[field] !== after.projection[field])) {
|
|
138
147
|
throw new Error("Task changed after native confirmation; authority aborted before capability issuance");
|
|
@@ -146,6 +155,58 @@ function qaOutcomes(record: { attestations: Array<{ kind: string; acceptance_res
|
|
|
146
155
|
);
|
|
147
156
|
}
|
|
148
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Publish the task-scoped synthetic revision for a v4 record and return the
|
|
160
|
+
* exact identity the Review snapshot binds.
|
|
161
|
+
*
|
|
162
|
+
* `submitReview` re-derives this identity and compares all four fields —
|
|
163
|
+
* `manifest_digest` included — against the reservation. Returning the bare
|
|
164
|
+
* commit identity therefore compared a real digest against `undefined` and
|
|
165
|
+
* failed every v4 submission with "Review revision changed before submission",
|
|
166
|
+
* so the manifest is recomputed here rather than only the commit. The outcomes
|
|
167
|
+
* come from the same `qaOutcomes` the Review snapshot is built from, which
|
|
168
|
+
* makes the two digests equal by construction instead of by coincidence.
|
|
169
|
+
*
|
|
170
|
+
* v3 records keep the legacy full-source bundle and return null.
|
|
171
|
+
*/
|
|
172
|
+
export async function ensureClaudeReviewRevision(
|
|
173
|
+
root: string,
|
|
174
|
+
taskId: string,
|
|
175
|
+
projection: AssuranceProjectionResult,
|
|
176
|
+
): Promise<ReviewRevision | null> {
|
|
177
|
+
const current = await readTaskRecord(root, taskId);
|
|
178
|
+
const record = current.record;
|
|
179
|
+
if (!record) throw new Error(`task ${taskId} has no TaskRecord`);
|
|
180
|
+
if (current.revision !== projection.projection.record_revision)
|
|
181
|
+
throw new Error("TaskRecord changed before Review revision preparation");
|
|
182
|
+
if (record.contract !== "assurance_kernel/task_record/v4") return null;
|
|
183
|
+
if (!record.git_base_head)
|
|
184
|
+
throw new Error("Review revision requires a TaskRecord v4 git_base_head");
|
|
185
|
+
const manifest = captureReviewManifest(root, {
|
|
186
|
+
taskId,
|
|
187
|
+
baseHead: record.git_base_head,
|
|
188
|
+
scopeHint: record.intent_snapshot.scope_hint,
|
|
189
|
+
expectedDiffHash: projection.projection.diff_hash,
|
|
190
|
+
intentRevision: projection.projection.intent_revision,
|
|
191
|
+
intentContentHash: projection.projection.intent_content_hash,
|
|
192
|
+
recordRevision: projection.projection.record_revision,
|
|
193
|
+
workspaceRevision: projection.projection.workspace_revision,
|
|
194
|
+
lifecycle: projection.projection.lifecycle,
|
|
195
|
+
artifactState: projection.projection.artifact_state,
|
|
196
|
+
risk: record.intent_snapshot.risk,
|
|
197
|
+
outcomes: qaOutcomes(record),
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
contract: "assurance_kernel/review_revision/v1",
|
|
201
|
+
base_head: manifest.base_head,
|
|
202
|
+
review_tree: manifest.review_tree,
|
|
203
|
+
review_commit: manifest.review_commit,
|
|
204
|
+
review_ref: manifest.review_ref,
|
|
205
|
+
diff_hash: manifest.diff_hash,
|
|
206
|
+
manifest_digest: manifest.manifest_digest,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
149
210
|
async function buildAssuranceSnapshot(
|
|
150
211
|
root: string,
|
|
151
212
|
taskId: string,
|
|
@@ -153,23 +214,25 @@ async function buildAssuranceSnapshot(
|
|
|
153
214
|
projection: AssuranceProjectionResult,
|
|
154
215
|
runner: FrozenRunner,
|
|
155
216
|
) {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
217
|
+
const read = await readTaskRecord(root, taskId);
|
|
218
|
+
const record = read.record;
|
|
219
|
+
if (!record || read.revision !== projection.projection.record_revision) throw new Error("TaskRecord changed before assurance snapshot capture");
|
|
220
|
+
const intent = record.intent_snapshot;
|
|
159
221
|
const descriptors = new Map<string, VerificationDescriptor>();
|
|
160
222
|
for (const item of intent.acceptance) {
|
|
161
223
|
const descriptor = parseVerificationDescriptor(item.verification);
|
|
162
224
|
assertRunnerCompatible(descriptor, runner);
|
|
163
225
|
descriptors.set(item.id, descriptor);
|
|
164
226
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
227
|
+
// `git_base_head` exists only on TaskRecord v4, so this must narrow the union
|
|
228
|
+
// rather than test the contract string into a plain boolean.
|
|
229
|
+
const reviewBundle = role === "review" && !isTaskRecordV4(record)
|
|
230
|
+
? captureReviewBundle(root, intent.scope_hint, projection.projection.diff_hash, qaOutcomes(record))
|
|
168
231
|
: null;
|
|
169
|
-
const reviewManifest = role === "review" &&
|
|
232
|
+
const reviewManifest = role === "review" && isTaskRecordV4(record)
|
|
170
233
|
? captureReviewManifest(root, {
|
|
171
234
|
taskId,
|
|
172
|
-
baseHead: record.
|
|
235
|
+
baseHead: record.git_base_head,
|
|
173
236
|
scopeHint: intent.scope_hint,
|
|
174
237
|
expectedDiffHash: projection.projection.diff_hash,
|
|
175
238
|
intentRevision: projection.projection.intent_revision,
|
|
@@ -179,7 +242,7 @@ async function buildAssuranceSnapshot(
|
|
|
179
242
|
lifecycle: projection.projection.lifecycle,
|
|
180
243
|
artifactState: projection.projection.artifact_state,
|
|
181
244
|
risk: intent.risk,
|
|
182
|
-
outcomes: qaOutcomes(record
|
|
245
|
+
outcomes: qaOutcomes(record),
|
|
183
246
|
})
|
|
184
247
|
: null;
|
|
185
248
|
const dirtyFiles = reviewManifest ? Object.keys(reviewManifest.changed_paths) : reviewBundle ? Object.keys(reviewBundle.dirty_files) : [];
|
|
@@ -295,7 +358,13 @@ export interface ClaudeRuntimeOptions {
|
|
|
295
358
|
cwd: string;
|
|
296
359
|
env?: Record<string, string | undefined>;
|
|
297
360
|
host?: ClaudeReviewHost;
|
|
298
|
-
|
|
361
|
+
/**
|
|
362
|
+
* Overrides layered on top of the real production ports, never a
|
|
363
|
+
* replacement for them. A whole synthetic ports object could previously be
|
|
364
|
+
* substituted here, so a suite could pass while the object production
|
|
365
|
+
* actually wires was never constructed once.
|
|
366
|
+
*/
|
|
367
|
+
ports?: Partial<AssuranceCoordinatorPorts>;
|
|
299
368
|
interactive?: boolean;
|
|
300
369
|
permissionMode?: PermissionMode;
|
|
301
370
|
requestConfirmation?: NativeConfirmationPort;
|
|
@@ -319,11 +388,11 @@ export class ClaudeRuntime {
|
|
|
319
388
|
this.interactive = options.interactive ?? true;
|
|
320
389
|
this.requestConfirmation = options.requestConfirmation;
|
|
321
390
|
this.host = options.host ?? new ClaudeReviewHost(new FileHookEventLog());
|
|
322
|
-
|
|
323
|
-
this.
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
391
|
+
this.coordinator = new AssuranceCoordinator({
|
|
392
|
+
...this.createKernelPorts(),
|
|
393
|
+
...options.ports,
|
|
394
|
+
host: this.host,
|
|
395
|
+
});
|
|
327
396
|
}
|
|
328
397
|
|
|
329
398
|
observe(event: ClaudeHookEvent): void {
|
|
@@ -343,25 +412,25 @@ export class ClaudeRuntime {
|
|
|
343
412
|
await this.coordinator.onSessionShutdown();
|
|
344
413
|
}
|
|
345
414
|
|
|
415
|
+
/**
|
|
416
|
+
* The exact ports object the coordinator runs on. Public so a conformance
|
|
417
|
+
* suite can drive what production wires instead of a hand-built double: the
|
|
418
|
+
* host adapter defects that reached published plugins all lived in this
|
|
419
|
+
* object and none of them were reachable from a test while it was private.
|
|
420
|
+
*/
|
|
421
|
+
kernelPorts(): AssuranceCoordinatorPorts {
|
|
422
|
+
return this.createKernelPorts();
|
|
423
|
+
}
|
|
424
|
+
|
|
346
425
|
private createKernelPorts(): AssuranceCoordinatorPorts {
|
|
347
426
|
return {
|
|
348
427
|
host: this.host,
|
|
349
428
|
projectTask: (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
|
|
350
|
-
readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
|
|
351
|
-
readTaskIntent: (root, taskId) => readTaskIntentForRecord(root, taskId),
|
|
429
|
+
readTaskRecord: async (root, taskId) => readTaskRecord(root, taskId),
|
|
430
|
+
readTaskIntent: async (root, taskId) => readTaskIntentForRecord(root, taskId),
|
|
352
431
|
frozenRunner: async () => resolveBunRunner(),
|
|
353
432
|
buildAssurance: (root, taskId, role, projection, runner) => buildAssuranceSnapshot(root, taskId, role, projection, runner),
|
|
354
|
-
ensureReviewRevision:
|
|
355
|
-
const current = await readTaskRecord(root, taskId);
|
|
356
|
-
if (!current.record) throw new Error(`task ${taskId} has no TaskRecord`);
|
|
357
|
-
if (current.record.contract !== "assurance_kernel/task_record/v4") return null;
|
|
358
|
-
return ensureReviewRevision(root, {
|
|
359
|
-
taskId,
|
|
360
|
-
baseHead: current.record.git_base_head,
|
|
361
|
-
scopeHint: current.record.intent_snapshot.scope_hint,
|
|
362
|
-
expectedDiffHash: projection.projection.diff_hash,
|
|
363
|
-
});
|
|
364
|
-
},
|
|
433
|
+
ensureReviewRevision: (root, taskId, projection) => ensureClaudeReviewRevision(root, taskId, projection),
|
|
365
434
|
runQa: (snapshot, descriptors, runner, options) => runDeterministicQa(snapshot, descriptors, runner, options),
|
|
366
435
|
writeReviewEvidence: (input) => writeNativeReviewEvidence(input.evidence),
|
|
367
436
|
applyVerdict: (ctx, input) => this.applyVerdict(ctx, input),
|
|
@@ -459,6 +528,19 @@ export class ClaudeRuntime {
|
|
|
459
528
|
return submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput);
|
|
460
529
|
}
|
|
461
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Ordinary Kernel operation, not a privileged one: canary_application builds
|
|
533
|
+
* the action without a capability and the Pi Host lists resolve_finding in
|
|
534
|
+
* its ordinary KERNEL_OPERATIONS. The reducer owns every precondition, so
|
|
535
|
+
* this port reads no findings and tests no kind.
|
|
536
|
+
*/
|
|
537
|
+
async resolveFinding(taskId: string, findingId: string) {
|
|
538
|
+
return this.executeOrdinary({ cwd: this.cwd }, {
|
|
539
|
+
taskId,
|
|
540
|
+
operation: { op: "resolve_finding", finding_id: findingId, actor_id: "executor" },
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
462
544
|
async authorize(taskId: string, operation: string, meta: ToolMeta, extra: Record<string, unknown> = {}) {
|
|
463
545
|
if (operation === "repair_authority_state") {
|
|
464
546
|
const authority = reconcileKernelAuthority(this.cwd, taskId);
|
|
@@ -468,7 +550,7 @@ export class ClaudeRuntime {
|
|
|
468
550
|
return repairKernelAuthority(this.cwd, taskId, authority.revision);
|
|
469
551
|
}
|
|
470
552
|
if (!isPrivilegedOperation(operation) && operation !== "request_authorization") throw new Error(`unsupported privileged operation ${operation}`);
|
|
471
|
-
let op = operation;
|
|
553
|
+
let op: PrivilegedOperation | "request_authorization" | "resolve_user_decision" | "authorize_rework" = operation;
|
|
472
554
|
let decisionOp: { finding_id: string; resolution: string } | undefined;
|
|
473
555
|
const projection = await this.status(taskId);
|
|
474
556
|
if (projection.error || !projection.claim) throw new Error(projection.error ?? "no active backend claim");
|
|
@@ -485,6 +567,8 @@ export class ClaudeRuntime {
|
|
|
485
567
|
if (open.length !== 1) throw new Error(`resolve-user-decision requires exactly one open user decision; found ${open.length}`);
|
|
486
568
|
op = "resolve_user_decision";
|
|
487
569
|
decisionOp = { finding_id: open[0].id, resolution: `resume after literal-user decision: ${open[0].summary}` };
|
|
570
|
+
} else if (readiness.state === "authorize_rework") {
|
|
571
|
+
op = "authorize_rework";
|
|
488
572
|
} else {
|
|
489
573
|
throw new Error(readiness.blocked ?? "no unique host-derived authorization operation");
|
|
490
574
|
}
|
|
@@ -584,7 +668,7 @@ export class ClaudeRuntime {
|
|
|
584
668
|
confirmation_ref: confirmation,
|
|
585
669
|
...(op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {}),
|
|
586
670
|
...(op === "resolve_user_decision" && decisionOp ? decisionOp : {}),
|
|
587
|
-
...(op === "stop" ? { reason: extra.reason
|
|
671
|
+
...(op === "stop" ? { reason: stopReason(extra.reason) } : {}),
|
|
588
672
|
});
|
|
589
673
|
throwIfCancelled(meta.signal);
|
|
590
674
|
const result = app.execute({
|
|
@@ -596,13 +680,17 @@ export class ClaudeRuntime {
|
|
|
596
680
|
actor_id: actorId,
|
|
597
681
|
...(op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {}),
|
|
598
682
|
...(op === "resolve_user_decision" && decisionOp ? decisionOp : {}),
|
|
599
|
-
...(op === "stop" ? { reason: extra.reason
|
|
683
|
+
...(op === "stop" ? { reason: stopReason(extra.reason) } : {}),
|
|
600
684
|
} as never,
|
|
601
685
|
prior_intent_token: priorIntent.token,
|
|
602
686
|
diffProvider: diffSnapshotOf,
|
|
603
687
|
now,
|
|
604
688
|
});
|
|
605
|
-
if (
|
|
689
|
+
if (
|
|
690
|
+
op === "stop" ||
|
|
691
|
+
op === "authorize_rework" ||
|
|
692
|
+
op === "approve_breaking_intent_revision"
|
|
693
|
+
) stagePlanningArtifactTransition(this.cwd, result.record);
|
|
606
694
|
return result;
|
|
607
695
|
} catch (error) {
|
|
608
696
|
if (sidecar && priorBytes && priorIndexState) {
|
|
@@ -672,7 +760,7 @@ export class ClaudeRuntime {
|
|
|
672
760
|
stagePlanningArtifactTransition(ctx.cwd, result.record);
|
|
673
761
|
return;
|
|
674
762
|
}
|
|
675
|
-
const approval = {
|
|
763
|
+
const approval: TaskApprovalV2 = {
|
|
676
764
|
id: `approval-${input.snapshot.role}-${randomUUID().slice(0, 8)}`,
|
|
677
765
|
kind: input.snapshot.role === "qa" ? "qa" : "review",
|
|
678
766
|
authority_role: input.snapshot.role === "qa" ? "qa" : "reviewer",
|
|
@@ -707,7 +795,7 @@ export class ClaudeRuntime {
|
|
|
707
795
|
}));
|
|
708
796
|
}
|
|
709
797
|
|
|
710
|
-
private async executeOrdinary(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown } }) {
|
|
798
|
+
private async executeOrdinary(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown; finding_id?: string } }) {
|
|
711
799
|
const { app } = await this.authority();
|
|
712
800
|
const operation = input.operation.op === "revise_intent"
|
|
713
801
|
? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) }
|
|
@@ -26,6 +26,7 @@ export const TOOLS = [
|
|
|
26
26
|
{ name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
|
|
27
27
|
{ name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
|
|
28
28
|
{ name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false },
|
|
29
|
+
{ name: "resolve_finding", description: "Resolve one open blocking or advisory finding whose cause is fixed and verified.", privileged: false },
|
|
29
30
|
] as const;
|
|
30
31
|
|
|
31
32
|
export function listMcpTools() {
|
|
@@ -39,8 +40,13 @@ export function listMcpTools() {
|
|
|
39
40
|
...(tool.name === "approve_breaking_intent_revision" ? { next_intent: { type: "object" } } : {}),
|
|
40
41
|
...(tool.name === "stop" ? { reason: { type: "string" } } : {}),
|
|
41
42
|
...(tool.name === "submit_review" ? { verdict: { type: "object" } } : {}),
|
|
43
|
+
...(tool.name === "resolve_finding" ? { finding_id: { type: "string" } } : {}),
|
|
42
44
|
},
|
|
43
|
-
required: tool.name === "submit_review"
|
|
45
|
+
required: tool.name === "submit_review"
|
|
46
|
+
? ["task_id", "verdict"]
|
|
47
|
+
: tool.name === "resolve_finding"
|
|
48
|
+
? ["task_id", "finding_id"]
|
|
49
|
+
: ["task_id"],
|
|
44
50
|
},
|
|
45
51
|
annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" },
|
|
46
52
|
}));
|
|
@@ -119,6 +125,13 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
|
|
|
119
125
|
if (!Object.hasOwn(args, "verdict")) throw new Error("verdict is required");
|
|
120
126
|
return runtime.submitReview(taskId, args.verdict);
|
|
121
127
|
}
|
|
128
|
+
if (name === "resolve_finding") {
|
|
129
|
+
// Structural only. Which findings may be resolved, and when, stays
|
|
130
|
+
// the reducer's decision; duplicating it here would create a second
|
|
131
|
+
// authority that could drift from the Kernel.
|
|
132
|
+
if (typeof args.finding_id !== "string" || !args.finding_id) throw new Error("finding_id is required");
|
|
133
|
+
return runtime.resolveFinding(taskId, args.finding_id);
|
|
134
|
+
}
|
|
122
135
|
if (name === "request_authorization" || name === "approve_breaking_intent_revision" || name === "stop" || name === "repair_authority_state") {
|
|
123
136
|
return runtime.authorize(taskId, name, toolMeta, args);
|
|
124
137
|
}
|
|
@@ -271,16 +271,6 @@ interface PendingReview {
|
|
|
271
271
|
error?: string;
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
function matchesReservation(event: ClaudeHookEvent, pending: PendingReview): boolean {
|
|
275
|
-
if ("operationId" in event && event.operationId && event.operationId !== pending.request.operationId) return false;
|
|
276
|
-
if ("taskId" in event && event.taskId && event.taskId !== pending.request.taskId) return false;
|
|
277
|
-
if (pending.sessionId && event.sessionId !== pending.sessionId) return false;
|
|
278
|
-
const eventAgentId = "agentId" in event ? event.agentId : "";
|
|
279
|
-
if (event.type === "SubagentStop" && (!eventAgentId || !pending.agentId || eventAgentId !== pending.agentId)) return false;
|
|
280
|
-
if (pending.agentId && eventAgentId && eventAgentId !== pending.agentId) return false;
|
|
281
|
-
return true;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
274
|
function bindsStart(event: Extract<ClaudeHookEvent, { type: "SubagentStart" }>, pending: PendingReview): boolean {
|
|
285
275
|
if (event.taskId && event.taskId !== pending.request.taskId) return false;
|
|
286
276
|
if (event.prompt !== undefined) {
|
|
@@ -248,19 +248,6 @@ function runInspect(root: string): KernelExecution {
|
|
|
248
248
|
),
|
|
249
249
|
};
|
|
250
250
|
}
|
|
251
|
-
let declared: TaskRisk;
|
|
252
|
-
let intent: TaskIntentV1;
|
|
253
|
-
try {
|
|
254
|
-
const raw = JSON.parse(
|
|
255
|
-
readSecureProjectFile(root, `docs/plans/${claim.task_id}.intent.json`),
|
|
256
|
-
) as { risk?: unknown };
|
|
257
|
-
if (raw.risk !== "routine" && raw.risk !== "material" && raw.risk !== "critical")
|
|
258
|
-
throw new Error(`intent.risk is unreadable for ${claim.task_id}`);
|
|
259
|
-
declared = raw.risk;
|
|
260
|
-
intent = parseTaskIntentV1(raw);
|
|
261
|
-
} catch (error) {
|
|
262
|
-
return sourceFailure("inspect", error);
|
|
263
|
-
}
|
|
264
251
|
let recordRead: ReturnType<typeof readTaskRecordRaw>;
|
|
265
252
|
try {
|
|
266
253
|
recordRead = readTaskRecordRaw(root, claim.task_id);
|
|
@@ -285,6 +272,21 @@ function runInspect(root: string): KernelExecution {
|
|
|
285
272
|
};
|
|
286
273
|
}
|
|
287
274
|
const record = recordRead.record;
|
|
275
|
+
let declared: TaskRisk;
|
|
276
|
+
let intent: TaskIntentV1;
|
|
277
|
+
try {
|
|
278
|
+
const raw = JSON.parse(
|
|
279
|
+
readSecureProjectFile(root, record.intent_ref.path),
|
|
280
|
+
) as { risk?: unknown };
|
|
281
|
+
if (raw.risk !== "routine" && raw.risk !== "material" && raw.risk !== "critical")
|
|
282
|
+
throw new Error(`intent.risk is unreadable for ${claim.task_id}`);
|
|
283
|
+
declared = raw.risk;
|
|
284
|
+
intent = parseTaskIntentV1(raw);
|
|
285
|
+
if (canonicalIntentHash(intent) !== record.intent_ref.content_hash)
|
|
286
|
+
throw new Error("TaskIntent sidecar does not match TaskRecord content hash");
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return sourceFailure("inspect", error);
|
|
289
|
+
}
|
|
288
290
|
const workspaceState = readWorkspaceStateRaw(root);
|
|
289
291
|
let identity;
|
|
290
292
|
try {
|