immune-brain 2.8.3 → 3.0.2
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/.claude-plugin/marketplace.json +16 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +9 -2
- package/plugins/immune-brain/.claude-plugin/plugin.json +8 -0
- package/plugins/immune-brain/.mcp.json +8 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +354 -64
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +74 -706
- package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +1 -90
- package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +13 -160
- package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +1 -50
- package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +1 -262
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +3 -2
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +8 -229
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +23 -5
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -0
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +7514 -0
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +58 -0
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +19 -13
- package/plugins/immune-brain/dist/imm-loop.md +6 -7
- package/plugins/immune-brain/dist/imm-planner.md +1 -1
- package/plugins/immune-brain/dist/imm-pr-fix.md +9 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +33 -13
- package/plugins/immune-brain/dist/role-prompts/executor.md +14 -0
- package/plugins/immune-brain/dist/role-prompts/pr-fix.md +9 -0
- package/plugins/immune-brain/dist/role-prompts/test-fixer.md +7 -0
- package/plugins/immune-brain/hooks/hooks.json +55 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +836 -0
- package/plugins/immune-brain/runtime/assurance/enrollment.ts +6 -0
- package/plugins/immune-brain/runtime/assurance/host_port.ts +18 -0
- package/plugins/immune-brain/runtime/assurance/invocations.ts +90 -0
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +50 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +596 -0
- package/plugins/immune-brain/runtime/assurance/verification.ts +233 -0
- package/plugins/immune-brain/runtime/claude/capability.ts +67 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +70 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +789 -0
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +363 -0
- package/plugins/immune-brain/runtime/claude/review_host.ts +645 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +221 -3
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +2 -2
- package/plugins/immune-brain/runtime/kernel/application.ts +8 -5
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +24 -13
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +78 -115
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -4
- package/plugins/immune-brain/runtime/kernel/capability_registry.ts +89 -0
- package/plugins/immune-brain/runtime/kernel/completion.ts +64 -6
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +189 -100
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +37 -80
- package/plugins/immune-brain/runtime/kernel/intent.ts +24 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +31 -0
- package/plugins/immune-brain/runtime/kernel/reducer.ts +36 -13
- package/plugins/immune-brain/runtime/kernel/storage.ts +16 -16
- package/plugins/immune-brain/runtime/kernel/types.ts +32 -2
- package/plugins/immune-brain/runtime/kernel/validation.ts +107 -16
- package/plugins/immune-brain/runtime/loop_contract.ts +17 -2
- package/plugins/immune-brain/runtime/prompts/code-review.md +33 -13
- package/plugins/immune-brain/runtime/prompts/executor.md +14 -0
- package/plugins/immune-brain/runtime/prompts/pr-fix.md +9 -0
- package/plugins/immune-brain/runtime/prompts/test-fixer.md +7 -0
- package/plugins/immune-brain/runtime/v4_runtime.ts +5 -2
- package/plugins/immune-brain/runtime/workspace_scope.ts +191 -5
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +3 -4
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +2 -0
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
// Host-neutral foreground Assurance coordination.
|
|
2
|
+
//
|
|
3
|
+
// Deterministic QA runs to completion inside the caller's Tool execution. A
|
|
4
|
+
// successful QA pass creates one short-lived Host Review reservation; the
|
|
5
|
+
// Parent invokes a foreground reviewer and submit_review consumes its
|
|
6
|
+
// structured verdict. No lifecycle work survives a Tool call except the
|
|
7
|
+
// evidence reservation.
|
|
8
|
+
|
|
9
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
+
import {
|
|
11
|
+
findingsDigest,
|
|
12
|
+
type FrozenRunner,
|
|
13
|
+
type VerificationDescriptor,
|
|
14
|
+
VerificationAbortedError,
|
|
15
|
+
} from "./verification";
|
|
16
|
+
import { createInvocationRegistry, type InvocationState, type InvocationToken } from "./invocations";
|
|
17
|
+
import type { ReviewBundle, ReviewManifestV5, ReviewRevision } from "./review_evidence";
|
|
18
|
+
import { buildRoleDelegationPacket } from "../role_prompt_bridge";
|
|
19
|
+
import type { AssuranceProjectionResult } from "../kernel/assurance_projection";
|
|
20
|
+
import type { AssuranceHostPort, HostReviewReservation } from "./host_port";
|
|
21
|
+
|
|
22
|
+
export type AssuranceRole = "qa" | "review";
|
|
23
|
+
export interface AssuranceCorrelation {
|
|
24
|
+
record_revision: string;
|
|
25
|
+
intent_content_hash: string;
|
|
26
|
+
diff_hash: string;
|
|
27
|
+
}
|
|
28
|
+
export interface HostContext {
|
|
29
|
+
cwd: string;
|
|
30
|
+
}
|
|
31
|
+
export interface TaskTombstone {
|
|
32
|
+
task_id: string;
|
|
33
|
+
lifecycle_status: string;
|
|
34
|
+
terminal_lifecycle: string;
|
|
35
|
+
terminal_event_id: string;
|
|
36
|
+
}
|
|
37
|
+
export interface TaskRecordRead {
|
|
38
|
+
record?: { contract?: string; findings: Array<{ kind: string; status: string }> } | null;
|
|
39
|
+
}
|
|
40
|
+
export interface TaskIntentRead {
|
|
41
|
+
token?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface GithubTerminalProjectionInput {
|
|
45
|
+
task_id: string;
|
|
46
|
+
phase: "done" | "stopped";
|
|
47
|
+
terminal_event_id: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function deriveGithubTerminalProjectionInput(
|
|
51
|
+
taskId: string,
|
|
52
|
+
projection: AssuranceProjectionResult,
|
|
53
|
+
tombstone: TaskTombstone | null,
|
|
54
|
+
): GithubTerminalProjectionInput | null {
|
|
55
|
+
if (
|
|
56
|
+
projection.error
|
|
57
|
+
|| projection.claim !== null
|
|
58
|
+
|| (projection.projection.lifecycle !== "done" && projection.projection.lifecycle !== "stopped")
|
|
59
|
+
|| tombstone?.task_id !== taskId
|
|
60
|
+
|| tombstone.lifecycle_status !== "terminal"
|
|
61
|
+
|| tombstone.terminal_lifecycle !== projection.projection.lifecycle
|
|
62
|
+
) return null;
|
|
63
|
+
return {
|
|
64
|
+
task_id: taskId,
|
|
65
|
+
phase: projection.projection.lifecycle,
|
|
66
|
+
terminal_event_id: tombstone.terminal_event_id,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface AssuranceVerdict {
|
|
71
|
+
contract: "assurance_kernel/assurance_verdict/v2";
|
|
72
|
+
role: AssuranceRole;
|
|
73
|
+
task_id: string;
|
|
74
|
+
snapshot_digest: string;
|
|
75
|
+
decision: "pass" | "rework";
|
|
76
|
+
approval?: {
|
|
77
|
+
kind: "qa" | "review";
|
|
78
|
+
authority_role: "qa" | "reviewer";
|
|
79
|
+
summary: string;
|
|
80
|
+
};
|
|
81
|
+
findings?: Array<{
|
|
82
|
+
id: string;
|
|
83
|
+
kind: "blocking" | "advisory";
|
|
84
|
+
acceptance_id: string | null;
|
|
85
|
+
summary: string;
|
|
86
|
+
findings_digest: string;
|
|
87
|
+
}>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ReviewRevisionIdentity {
|
|
91
|
+
contract: "assurance_kernel/review_revision_identity/v1";
|
|
92
|
+
base_head: string;
|
|
93
|
+
review_commit: string;
|
|
94
|
+
review_tree: string;
|
|
95
|
+
manifest_digest: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface SnapshotDescriptor {
|
|
99
|
+
contract: "assurance_kernel/assurance_snapshot/v2";
|
|
100
|
+
task_id: string;
|
|
101
|
+
role: AssuranceRole;
|
|
102
|
+
record_revision: string;
|
|
103
|
+
workspace_revision: string;
|
|
104
|
+
intent_revision: number;
|
|
105
|
+
intent_content_hash: string;
|
|
106
|
+
diff_hash: string;
|
|
107
|
+
lifecycle: string;
|
|
108
|
+
artifact_state: string;
|
|
109
|
+
risk: "routine" | "material" | "critical";
|
|
110
|
+
fresh_acceptance_ids: string[];
|
|
111
|
+
missing_acceptance_ids: string[];
|
|
112
|
+
stale_attestation_ids: string[];
|
|
113
|
+
acceptance: Array<{ id: string; assertion: string; verification: string }>;
|
|
114
|
+
dirty_files: string[];
|
|
115
|
+
review_bundle_digest: string | null;
|
|
116
|
+
/** Present only when Review authority binds an immutable Git revision (v4). */
|
|
117
|
+
review_revision?: ReviewRevisionIdentity;
|
|
118
|
+
root: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface QaVerificationProgress {
|
|
122
|
+
index: number;
|
|
123
|
+
total: number;
|
|
124
|
+
acceptance_id: string;
|
|
125
|
+
phase: "running" | "passed" | "failed";
|
|
126
|
+
elapsed_ms: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface ForegroundToolUpdate {
|
|
130
|
+
content: Array<{ type: "text"; text: string }>;
|
|
131
|
+
details: Record<string, unknown>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type AssuranceAdvanceResult =
|
|
135
|
+
| { state: "review_ready"; operation: "review"; operation_id: string; snapshot_digest: string; review_bundle_digest: string; agent_params: unknown }
|
|
136
|
+
| { state: "awaiting_user"; operation: "record-user-approval"; operation_id: string }
|
|
137
|
+
| { state: "rework"; operation: "qa"; operation_id: string; summary: string }
|
|
138
|
+
| { state: "cancelled"; operation: "qa" | "review"; operation_id: string; reason: string }
|
|
139
|
+
| { state: "failed"; operation: "qa" | "review"; operation_id: string; reason: string }
|
|
140
|
+
/**
|
|
141
|
+
* Read-only Review evidence preparation failed after QA authority settled.
|
|
142
|
+
* The Kernel obligation stays `run_review`, nothing was lost, and the same
|
|
143
|
+
* deterministic revision can be rebuilt on the next advance.
|
|
144
|
+
*/
|
|
145
|
+
| { state: "review_preparation_failed"; operation: "review"; operation_id: string; reason: string }
|
|
146
|
+
| { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string }
|
|
147
|
+
| { state: "blocked"; reason: string; code?: "verdict_invalid" }
|
|
148
|
+
| { state: "completed" }
|
|
149
|
+
| { state: "stopped" };
|
|
150
|
+
|
|
151
|
+
export type AssuranceSubmitReviewResult =
|
|
152
|
+
| { state: "rework"; operation: "review"; operation_id: string; summary: string }
|
|
153
|
+
| { state: "awaiting_user"; operation: "record-user-approval"; operation_id: string }
|
|
154
|
+
| { state: "review_preparation_failed"; operation: "review"; operation_id: string; reason: string }
|
|
155
|
+
| { state: "completed" }
|
|
156
|
+
| { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string }
|
|
157
|
+
| { state: "blocked"; reason: string; code?: "verdict_invalid" };
|
|
158
|
+
|
|
159
|
+
export type ActiveAssuranceState =
|
|
160
|
+
| { state: "running"; operation: "qa"; operation_id: string; deadline_seconds: number }
|
|
161
|
+
| { state: "review_ready"; operation: "review"; operation_id: string }
|
|
162
|
+
| { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string };
|
|
163
|
+
|
|
164
|
+
export interface AssuranceCoordinatorPorts {
|
|
165
|
+
host: AssuranceHostPort;
|
|
166
|
+
projectTask(root: string, taskId: string): Promise<AssuranceProjectionResult>;
|
|
167
|
+
readTaskRecord(root: string, taskId: string): Promise<TaskRecordRead>;
|
|
168
|
+
readTaskIntent(root: string, taskId: string): Promise<TaskIntentRead>;
|
|
169
|
+
frozenRunner(): Promise<FrozenRunner>;
|
|
170
|
+
buildAssurance(
|
|
171
|
+
root: string,
|
|
172
|
+
taskId: string,
|
|
173
|
+
role: AssuranceRole,
|
|
174
|
+
projection: AssuranceProjectionResult,
|
|
175
|
+
runner: FrozenRunner,
|
|
176
|
+
): Promise<{
|
|
177
|
+
snapshot: SnapshotDescriptor;
|
|
178
|
+
descriptors: Map<string, VerificationDescriptor>;
|
|
179
|
+
reviewBundle: ReviewBundle | null;
|
|
180
|
+
reviewManifest?: ReviewManifestV5 | null;
|
|
181
|
+
}>;
|
|
182
|
+
/**
|
|
183
|
+
* Publish and prove the deterministic task-scoped revision before QA runs, so
|
|
184
|
+
* a preparation failure can never consume or fake a settled QA attestation.
|
|
185
|
+
* Returns null for records still on the legacy v3 bundle path.
|
|
186
|
+
*/
|
|
187
|
+
ensureReviewRevision?(
|
|
188
|
+
root: string,
|
|
189
|
+
taskId: string,
|
|
190
|
+
projection: AssuranceProjectionResult,
|
|
191
|
+
): Promise<ReviewRevision | null>;
|
|
192
|
+
runQa(
|
|
193
|
+
snapshot: SnapshotDescriptor,
|
|
194
|
+
descriptors: Map<string, VerificationDescriptor>,
|
|
195
|
+
runner: FrozenRunner,
|
|
196
|
+
options: { signal?: AbortSignal; onProgress?: (progress: QaVerificationProgress) => void },
|
|
197
|
+
): Promise<AssuranceVerdict>;
|
|
198
|
+
writeReviewEvidence(input: { snapshot: SnapshotDescriptor; evidence: ReviewBundle | ReviewManifestV5 }): { path: string; remove(): void };
|
|
199
|
+
applyVerdict(
|
|
200
|
+
ctx: HostContext,
|
|
201
|
+
input: {
|
|
202
|
+
taskId: string;
|
|
203
|
+
snapshot: SnapshotDescriptor;
|
|
204
|
+
verdict: AssuranceVerdict;
|
|
205
|
+
invocation: InvocationToken;
|
|
206
|
+
actorId: string;
|
|
207
|
+
hooks?: {
|
|
208
|
+
beforeCommit?: () => Promise<void>;
|
|
209
|
+
onCommit?: () => void;
|
|
210
|
+
afterCommit?: () => Promise<void>;
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
): Promise<void>;
|
|
214
|
+
applyOrdinaryOperation(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string } }): Promise<unknown>;
|
|
215
|
+
advanceBeforeProjection?: () => Promise<void>;
|
|
216
|
+
qaBeforeProjection?: () => Promise<void>;
|
|
217
|
+
qaBeforeAuthorityCommit?: () => Promise<void>;
|
|
218
|
+
qaOnAuthorityCommit?: () => void;
|
|
219
|
+
qaAfterAuthorityCommit?: () => Promise<void>;
|
|
220
|
+
qaJobTimeoutMs?: number;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export const QA_MIN_JOB_TIMEOUT_SECONDS = 15 * 60;
|
|
224
|
+
export const QA_MAX_JOB_TIMEOUT_SECONDS = 60 * 60;
|
|
225
|
+
export const QA_JOB_OVERHEAD_SECONDS = 2 * 60;
|
|
226
|
+
export const QA_JOB_TIMEOUT_SECONDS = QA_MIN_JOB_TIMEOUT_SECONDS;
|
|
227
|
+
export const REVIEW_PREPARATION_TIMEOUT_MS = 30_000;
|
|
228
|
+
export const REVIEW_DISPATCH_TIMEOUT_MS = 120_000;
|
|
229
|
+
export const REVIEW_VERDICT_VALIDATION_TIMEOUT_MS = 30_000;
|
|
230
|
+
export const ASSURANCE_STALL_MS = 30_000;
|
|
231
|
+
|
|
232
|
+
type ReviewWorkload = "quick" | "standard" | "heavy";
|
|
233
|
+
export interface ReviewTimingProfile { softDeadlineSeconds: number; stopThresholdSeconds: number }
|
|
234
|
+
export const REVIEW_TIMING_PROFILES: Readonly<Record<ReviewWorkload, ReviewTimingProfile>> = {
|
|
235
|
+
quick: { softDeadlineSeconds: 5 * 60, stopThresholdSeconds: 15 * 60 },
|
|
236
|
+
standard: { softDeadlineSeconds: 10 * 60, stopThresholdSeconds: 30 * 60 },
|
|
237
|
+
heavy: { softDeadlineSeconds: 20 * 60, stopThresholdSeconds: 60 * 60 },
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
function declaredQaJobTimeoutMs(descriptors: Iterable<Pick<VerificationDescriptor, "timeout_ms">>): number {
|
|
241
|
+
let declaredMs = 0;
|
|
242
|
+
for (const descriptor of descriptors) declaredMs += descriptor.timeout_ms;
|
|
243
|
+
return Math.max(QA_MIN_JOB_TIMEOUT_SECONDS * 1000, declaredMs + QA_JOB_OVERHEAD_SECONDS * 1000);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function deriveQaJobTimeoutMs(descriptors: Iterable<Pick<VerificationDescriptor, "timeout_ms">>): number {
|
|
247
|
+
const derivedMs = declaredQaJobTimeoutMs(descriptors);
|
|
248
|
+
if (derivedMs > QA_MAX_JOB_TIMEOUT_SECONDS * 1000)
|
|
249
|
+
throw new Error(`declared QA budget ${Math.ceil(derivedMs / 60_000)} minutes exceeds the maximum of 60 minutes`);
|
|
250
|
+
return derivedMs;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const QUICK_REVIEW_MAX_ACCEPTANCE = 3;
|
|
254
|
+
const QUICK_REVIEW_MAX_FILES = 5;
|
|
255
|
+
const QUICK_REVIEW_MAX_BYTES = 64 * 1024;
|
|
256
|
+
const HEAVY_REVIEW_MIN_ACCEPTANCE = 9;
|
|
257
|
+
const HEAVY_REVIEW_MIN_BYTES = 512 * 1024 + 1;
|
|
258
|
+
|
|
259
|
+
export function classifyReviewWorkload(snapshot: Pick<SnapshotDescriptor, "risk" | "acceptance">, evidence: ReviewBundle | ReviewManifestV5): ReviewWorkload {
|
|
260
|
+
const bytes = Buffer.byteLength(JSON.stringify(evidence));
|
|
261
|
+
const paths = "changed_paths" in evidence ? Object.keys(evidence.changed_paths) : Object.keys(evidence.dirty_files);
|
|
262
|
+
if (snapshot.risk === "critical" || snapshot.acceptance.length >= HEAVY_REVIEW_MIN_ACCEPTANCE || bytes >= HEAVY_REVIEW_MIN_BYTES) return "heavy";
|
|
263
|
+
if (snapshot.risk === "routine" && snapshot.acceptance.length <= QUICK_REVIEW_MAX_ACCEPTANCE && paths.length <= QUICK_REVIEW_MAX_FILES && bytes <= QUICK_REVIEW_MAX_BYTES) return "quick";
|
|
264
|
+
return "standard";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function reviewTurnBudget(workload: ReviewWorkload): number {
|
|
268
|
+
return workload === "quick" ? 12 : workload === "standard" ? 16 : 24;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function snapshotDigest(snapshot: SnapshotDescriptor): string {
|
|
272
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function buildReviewPrompt(snapshot: SnapshotDescriptor, evidencePath?: string): string {
|
|
276
|
+
if (snapshot.role !== "review") throw new Error("native review prompt requires review role");
|
|
277
|
+
const acceptance = snapshot.acceptance.map((item) => `- ${item.id}: ${item.assertion}`).join("\n");
|
|
278
|
+
const digest = snapshotDigest(snapshot);
|
|
279
|
+
const rolePacket = buildRoleDelegationPacket({
|
|
280
|
+
role: "code-review",
|
|
281
|
+
context: {
|
|
282
|
+
task_id: snapshot.task_id,
|
|
283
|
+
review_gate: "imm-code-review",
|
|
284
|
+
changed_files_signature: snapshot.diff_hash,
|
|
285
|
+
snapshot_digest: digest,
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
const revision = snapshot.review_revision;
|
|
289
|
+
const evidenceContract = revision
|
|
290
|
+
? [
|
|
291
|
+
`Review evidence contract: assurance_kernel/review_manifest/v5. The manifest is metadata only; source is read from immutable Git objects.`,
|
|
292
|
+
`Review one immutable Git revision, not live workspace bytes. Read the metadata manifest at ${evidencePath ?? "<evidence-path>"} first; it carries no source. Verify that git rev-parse ${revision.base_head} and git rev-parse ${revision.review_commit} both resolve, that ${revision.review_commit}^{} is a commit whose only parent is ${revision.base_head}, and that its tree is ${revision.review_tree}.`,
|
|
293
|
+
`Analyze the change with git diff ${revision.base_head} ${revision.review_commit} (and git show ${revision.review_commit}:<path> for full files). Every path in changed_paths is the task's work: added, modified, or deleted since Enrollment. Deleted paths have a null oid. Never treat a path that is absent from ${revision.base_head} as pre-existing, and never review files outside the revision.`,
|
|
294
|
+
`Unchanged files are not part of the mutation authority. Read one only when it is directly required by an acceptance assertion, a changed caller, or the same state machine, and cite the path plus the reason in your finding. Repository-wide exploration is out of scope.`,
|
|
295
|
+
`The user-selected worktree may contain staged or committed work that is not in this revision, and revision objects may not be checked out anywhere. Do not read working-tree files as evidence; git object reads against the shared object database are the only source of truth.`,
|
|
296
|
+
]
|
|
297
|
+
: [
|
|
298
|
+
`Verify immutable bundle provenance before analyzing findings. Review the immutable evidence JSON at ${evidencePath ?? "<evidence-path>"}. Read that file first; verify that git rev-parse HEAD in the isolated reviewer worktree equals bundle.head. For every tracked dirty_files entry, verify git rev-parse HEAD:<path> equals base_oid, then compare that immutable HEAD blob with current_content. A null base_oid denotes an untracked current file; a null current_content denotes a deletion. Do not inspect or depend on live task bytes outside the immutable bundle.`,
|
|
299
|
+
`Limit repository inspection to the acceptance assertions and dirty_files contents in the immutable bundle. Do not explore unrelated repository paths.`,
|
|
300
|
+
`The user-selected worktree may contain staged task changes that are absent from the isolated reviewer worktree. Review authority is bound only to the bundle dirty_files current_content bytes and committed HEAD provenance. Analyze code exclusively from those bundle bytes; repository file reads are permitted only for the provenance git commands above. A symbol present in current_content but absent from HEAD is the task change, not an absence.`,
|
|
301
|
+
];
|
|
302
|
+
return [
|
|
303
|
+
rolePacket.prompt,
|
|
304
|
+
...evidenceContract,
|
|
305
|
+
`Do not edit files, create files, run mutating commands, or change Git state. Focus on correctness, regressions, security, and missing tests.`,
|
|
306
|
+
`Execution outcomes for every acceptance were verified deterministically by the Kernel QA layer before this review and are embedded in this bundle under outcomes (the immutable evidence file, acceptance_id -> {status, summary}); do not re-execute descriptors and do not treat the absence of local test runs as a finding. Your review covers evidence provenance, code correctness, regressions, security, and missing tests against the embedded assertions and code.`,
|
|
307
|
+
`Snapshot digest: ${digest}`,
|
|
308
|
+
`TaskRecord revision: ${snapshot.record_revision}`,
|
|
309
|
+
revision
|
|
310
|
+
? `Intent revision ${snapshot.intent_revision} (hash ${snapshot.intent_content_hash}), diff ${snapshot.diff_hash}, review revision ${revision.review_commit} (base ${revision.base_head}, tree ${revision.review_tree}, manifest ${revision.manifest_digest}), state ${snapshot.lifecycle}:${snapshot.artifact_state}.`
|
|
311
|
+
: `Intent revision ${snapshot.intent_revision} (hash ${snapshot.intent_content_hash}), diff ${snapshot.diff_hash}, review bundle ${snapshot.review_bundle_digest}, state ${snapshot.lifecycle}:${snapshot.artifact_state}.`,
|
|
312
|
+
"Acceptance assertions:", acceptance,
|
|
313
|
+
"Reserve the final turn for exactly one strict JSON verdict. Reply with ONLY that object, without markdown fences or commentary.",
|
|
314
|
+
`PASS shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"pass","approval":{"kind":"review","authority_role":"reviewer","summary":"<one line>"}}`,
|
|
315
|
+
`REWORK shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"rework","findings":[{"id":"review-1","kind":"blocking|advisory","acceptance_id":"<id|null>","summary":"<one line>"}]}`,
|
|
316
|
+
`REWORK verdicts must omit the approval field entirely; do not emit "approval": null.`,
|
|
317
|
+
].join("\n");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function parseAssuranceVerdict(input: unknown, snapshot: SnapshotDescriptor): AssuranceVerdict {
|
|
321
|
+
let raw: Record<string, unknown>;
|
|
322
|
+
if (typeof input === "string") {
|
|
323
|
+
const cleaned = input.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("{") && line.endsWith("}")).join("");
|
|
324
|
+
if (!cleaned) throw new Error("reviewer returned no strict JSON verdict");
|
|
325
|
+
try { raw = JSON.parse(cleaned) as Record<string, unknown>; } catch { throw new Error("reviewer verdict is not valid JSON"); }
|
|
326
|
+
} else if (typeof input === "object" && input !== null && !Array.isArray(input)) {
|
|
327
|
+
raw = input as Record<string, unknown>;
|
|
328
|
+
} else {
|
|
329
|
+
throw new Error("reviewer verdict must be a JSON object");
|
|
330
|
+
}
|
|
331
|
+
const allowed = ["contract", "role", "task_id", "snapshot_digest", "decision", "approval", "findings"];
|
|
332
|
+
const unknown = Object.keys(raw).find((key) => !allowed.includes(key));
|
|
333
|
+
if (unknown) throw new Error(`child verdict has unknown field: ${unknown}`);
|
|
334
|
+
if (raw.contract !== "assurance_kernel/assurance_verdict/v2") throw new Error("assurance verdict contract is invalid");
|
|
335
|
+
if (raw.role !== snapshot.role) throw new Error("child verdict role mismatch");
|
|
336
|
+
if (raw.task_id !== snapshot.task_id) throw new Error("child verdict task mismatch");
|
|
337
|
+
if (raw.snapshot_digest !== snapshotDigest(snapshot)) throw new Error("child verdict snapshot digest mismatch");
|
|
338
|
+
if (raw.decision !== "pass" && raw.decision !== "rework") throw new Error("child verdict decision must be pass or rework");
|
|
339
|
+
if (raw.decision === "pass") {
|
|
340
|
+
const approval = raw.approval as Record<string, unknown> | undefined;
|
|
341
|
+
const expectedKind = snapshot.role === "qa" ? "qa" : "review";
|
|
342
|
+
const expectedRole = snapshot.role === "qa" ? "qa" : "reviewer";
|
|
343
|
+
if (!approval || approval.kind !== expectedKind || approval.authority_role !== expectedRole || typeof approval.summary !== "string" || !approval.summary.trim()) throw new Error("pass verdict approval is invalid");
|
|
344
|
+
const unknownApproval = Object.keys(approval).find((key) => !["kind", "authority_role", "summary"].includes(key));
|
|
345
|
+
if (unknownApproval) throw new Error(`pass verdict approval has unknown field: ${unknownApproval}`);
|
|
346
|
+
if (raw.findings !== undefined) throw new Error("pass verdict must omit findings");
|
|
347
|
+
return { contract: "assurance_kernel/assurance_verdict/v2", role: snapshot.role, task_id: snapshot.task_id, snapshot_digest: snapshotDigest(snapshot), decision: "pass", approval: { kind: expectedKind, authority_role: expectedRole, summary: approval.summary } };
|
|
348
|
+
}
|
|
349
|
+
if (!Array.isArray(raw.findings) || raw.findings.length === 0) throw new Error("rework verdict findings are invalid");
|
|
350
|
+
if (raw.approval !== undefined && raw.approval !== null) throw new Error("rework verdict must omit approval");
|
|
351
|
+
const findings = raw.findings.map((item, index) => {
|
|
352
|
+
const finding = item as Record<string, unknown>;
|
|
353
|
+
const unknownFinding = Object.keys(finding).find((key) => !["id", "kind", "acceptance_id", "summary"].includes(key));
|
|
354
|
+
if (unknownFinding) throw new Error(`finding ${index} has unknown field: ${unknownFinding}`);
|
|
355
|
+
if (typeof finding.id !== "string" || !finding.id.trim() || (finding.kind !== "blocking" && finding.kind !== "advisory") || (finding.acceptance_id !== null && typeof finding.acceptance_id !== "string") || typeof finding.summary !== "string" || !finding.summary.trim()) throw new Error(`finding ${index} is invalid`);
|
|
356
|
+
const id = `review-${snapshotDigest(snapshot).slice(7, 19)}-${index + 1}-${finding.id.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 48)}`;
|
|
357
|
+
const normalized = { id, kind: finding.kind as "blocking" | "advisory", acceptance_id: finding.acceptance_id as string | null, summary: finding.summary as string };
|
|
358
|
+
return { ...normalized, findings_digest: findingsDigest([normalized]) };
|
|
359
|
+
});
|
|
360
|
+
return { contract: "assurance_kernel/assurance_verdict/v2", role: snapshot.role, task_id: snapshot.task_id, snapshot_digest: snapshotDigest(snapshot), decision: "rework", findings };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
interface ReviewReservation {
|
|
364
|
+
taskId: string;
|
|
365
|
+
operationId: string;
|
|
366
|
+
correlation: AssuranceCorrelation;
|
|
367
|
+
snapshot: SnapshotDescriptor;
|
|
368
|
+
hostReservation: HostReviewReservation;
|
|
369
|
+
verdictCorrectionRequired: boolean;
|
|
370
|
+
evidence: { path: string; remove(): void };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export const invocationRegistry = createInvocationRegistry();
|
|
374
|
+
|
|
375
|
+
export class AssuranceCoordinator {
|
|
376
|
+
private readonly activeOperations = new Map<string, string>();
|
|
377
|
+
private readonly operationControllers = new Map<string, { operationId: string; controller: AbortController }>();
|
|
378
|
+
private readonly reviewReservations = new Map<string, ReviewReservation>();
|
|
379
|
+
private readonly rejectedReviewOperations = new Map<string, { operationId: string; reason: string }>();
|
|
380
|
+
private readonly unknownOperations = new Map<string, { operation: "qa" | "review"; operationId: string; reason: string }>();
|
|
381
|
+
private readonly sessionInvocations = new Set<InvocationToken>();
|
|
382
|
+
private sessionActive = true;
|
|
383
|
+
private sessionGeneration = 0;
|
|
384
|
+
|
|
385
|
+
constructor(private readonly ports: AssuranceCoordinatorPorts) {}
|
|
386
|
+
|
|
387
|
+
onSessionStart(): void {
|
|
388
|
+
this.sessionActive = true;
|
|
389
|
+
this.sessionGeneration += 1;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async onSessionShutdown(): Promise<void> {
|
|
393
|
+
this.sessionActive = false;
|
|
394
|
+
this.sessionGeneration += 1;
|
|
395
|
+
for (const { controller } of this.operationControllers.values()) {
|
|
396
|
+
if (!controller.signal.aborted) controller.abort(new Error("session shutdown"));
|
|
397
|
+
}
|
|
398
|
+
this.operationControllers.clear();
|
|
399
|
+
this.activeOperations.clear();
|
|
400
|
+
this.rejectedReviewOperations.clear();
|
|
401
|
+
for (const reservation of this.reviewReservations.values()) {
|
|
402
|
+
try { this.ports.host.releaseReview(reservation.hostReservation); } catch { /* cleanup cannot create authority */ }
|
|
403
|
+
this.removeEvidence(reservation);
|
|
404
|
+
}
|
|
405
|
+
this.reviewReservations.clear();
|
|
406
|
+
for (const invocation of [...this.sessionInvocations]) this.closeSessionInvocation(invocation);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
active(taskId: string): ActiveAssuranceState | null {
|
|
410
|
+
const operationId = this.activeOperations.get(taskId);
|
|
411
|
+
if (operationId) return { state: "running", operation: "qa", operation_id: operationId, deadline_seconds: QA_JOB_TIMEOUT_SECONDS };
|
|
412
|
+
const reservation = this.reviewReservations.get(taskId);
|
|
413
|
+
if (reservation) return { state: "review_ready", operation: "review", operation_id: reservation.operationId };
|
|
414
|
+
const unknown = this.unknownOperations.get(taskId);
|
|
415
|
+
if (unknown) return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason };
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
openInvocation(taskId: string): InvocationToken {
|
|
420
|
+
const invocation = invocationRegistry.open(taskId);
|
|
421
|
+
this.sessionInvocations.add(invocation);
|
|
422
|
+
return invocation;
|
|
423
|
+
}
|
|
424
|
+
closeInvocation(invocation: InvocationToken): void { this.closeSessionInvocation(invocation); }
|
|
425
|
+
private closeSessionInvocation(invocation: InvocationToken): void {
|
|
426
|
+
try { invocationRegistry.cancel(invocation); } catch { /* already terminal */ }
|
|
427
|
+
this.sessionInvocations.delete(invocation);
|
|
428
|
+
}
|
|
429
|
+
commitInvocation(invocation: InvocationToken): void { invocationRegistry.commit(invocation); }
|
|
430
|
+
invocationState(invocation: InvocationToken): InvocationState { return invocationRegistry.stateOf(invocation); }
|
|
431
|
+
isInvocationOpen(taskId: string): boolean { return invocationRegistry.isOpen(taskId); }
|
|
432
|
+
sessionActiveValue(): boolean { return this.sessionActive; }
|
|
433
|
+
sessionGenerationValue(): number { return this.sessionGeneration; }
|
|
434
|
+
|
|
435
|
+
async advance(taskId: string, ctx: HostContext, signal?: AbortSignal, onUpdate?: (update: ForegroundToolUpdate) => void): Promise<AssuranceAdvanceResult> {
|
|
436
|
+
const active = this.active(taskId);
|
|
437
|
+
if (active?.state === "review_ready") {
|
|
438
|
+
const reservation = this.reviewReservations.get(taskId);
|
|
439
|
+
let projection: AssuranceProjectionResult;
|
|
440
|
+
try {
|
|
441
|
+
projection = await this.ports.projectTask(ctx.cwd, taskId);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
return { state: "blocked", reason: `cannot validate Review reservation: ${boundedAssuranceError(error)}` };
|
|
444
|
+
}
|
|
445
|
+
const current = projection.projection;
|
|
446
|
+
const matches = !projection.error
|
|
447
|
+
&& projection.claim?.task_id === taskId
|
|
448
|
+
&& current.lifecycle === "active"
|
|
449
|
+
&& current.next_obligation === "run_review"
|
|
450
|
+
&& reservation !== undefined
|
|
451
|
+
&& reservation.snapshot.record_revision === current.record_revision
|
|
452
|
+
&& reservation.snapshot.workspace_revision === current.workspace_revision
|
|
453
|
+
&& reservation.snapshot.intent_revision === current.intent_revision
|
|
454
|
+
&& reservation.snapshot.intent_content_hash === current.intent_content_hash
|
|
455
|
+
&& reservation.snapshot.diff_hash === current.diff_hash;
|
|
456
|
+
if (matches) return this.reviewReadyResult(taskId);
|
|
457
|
+
if (reservation) this.releaseReviewReservation(taskId, reservation);
|
|
458
|
+
this.rejectedReviewOperations.delete(taskId);
|
|
459
|
+
}
|
|
460
|
+
const refreshed = this.active(taskId);
|
|
461
|
+
if (refreshed?.state === "settlement_unknown") return refreshed;
|
|
462
|
+
if (refreshed?.state === "running") return { state: "blocked", reason: `assurance operation ${refreshed.operation_id} is already running` };
|
|
463
|
+
const operationId = randomUUID();
|
|
464
|
+
const operationGeneration = this.sessionGeneration;
|
|
465
|
+
const operationController = new AbortController();
|
|
466
|
+
const relayExternalAbort = () => operationController.abort(
|
|
467
|
+
signal?.reason instanceof Error ? signal.reason : new Error("assurance operation cancelled"),
|
|
468
|
+
);
|
|
469
|
+
signal?.addEventListener("abort", relayExternalAbort, { once: true });
|
|
470
|
+
if (signal?.aborted) relayExternalAbort();
|
|
471
|
+
this.activeOperations.set(taskId, operationId);
|
|
472
|
+
this.operationControllers.set(taskId, { operationId, controller: operationController });
|
|
473
|
+
this.rejectedReviewOperations.delete(taskId);
|
|
474
|
+
let authorityCommitted = false;
|
|
475
|
+
let authorityBoundaryStarted = false;
|
|
476
|
+
let reviewPreparationStarted = false;
|
|
477
|
+
let phase = "preparing";
|
|
478
|
+
const operationLive = () => this.sessionActive
|
|
479
|
+
&& this.sessionGeneration === operationGeneration
|
|
480
|
+
&& this.activeOperations.get(taskId) === operationId
|
|
481
|
+
&& !operationController.signal.aborted;
|
|
482
|
+
const ensureOperationLive = () => {
|
|
483
|
+
if (!operationLive()) throw new VerificationAbortedError();
|
|
484
|
+
};
|
|
485
|
+
const progress = (stage: string, summary: string, details: Record<string, unknown> = {}) => {
|
|
486
|
+
phase = stage;
|
|
487
|
+
onUpdate?.({ content: [{ type: "text", text: summary }], details: { state: "running", operation: "qa", operation_id: operationId, stage, ...details } });
|
|
488
|
+
};
|
|
489
|
+
const aborted = () => operationController.signal.aborted;
|
|
490
|
+
try {
|
|
491
|
+
ensureOperationLive();
|
|
492
|
+
progress(phase, `Preparing deterministic QA for ${taskId}`);
|
|
493
|
+
await this.ports.advanceBeforeProjection?.();
|
|
494
|
+
ensureOperationLive();
|
|
495
|
+
let projection = await this.ports.projectTask(ctx.cwd, taskId);
|
|
496
|
+
ensureOperationLive();
|
|
497
|
+
if (projection.error) return { state: "blocked", reason: projection.error };
|
|
498
|
+
if (projection.projection.lifecycle === "done") return { state: "completed" };
|
|
499
|
+
if (projection.projection.lifecycle === "stopped") return { state: "stopped" };
|
|
500
|
+
if (!projection.claim) return { state: "blocked", reason: "no active backend claim" };
|
|
501
|
+
if (projection.claim.task_id !== taskId) return { state: "blocked", reason: `backend claim belongs to ${projection.claim.task_id}, not ${taskId}` };
|
|
502
|
+
const parked = await this.ports.readTaskRecord(ctx.cwd, taskId);
|
|
503
|
+
ensureOperationLive();
|
|
504
|
+
if (parked.record?.findings.some((finding) => finding.kind === "replan_required" && finding.status === "open")) return { state: "blocked", reason: "review rework limit reached; a durable replan is required" };
|
|
505
|
+
if (projection.projection.artifact_state === "active") {
|
|
506
|
+
if (projection.projection.next_obligation !== "submit_assurance")
|
|
507
|
+
return { state: "blocked", reason: `Kernel requires ${projection.projection.next_obligation}` };
|
|
508
|
+
if (aborted()) return this.cancelled("qa", operationId, "host cancellation before artifact freeze");
|
|
509
|
+
progress("freezing_artifacts", "Freezing planning artifacts for deterministic assurance");
|
|
510
|
+
const freeze = this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "freeze_artifacts", actor_id: "executor" } });
|
|
511
|
+
authorityBoundaryStarted = true;
|
|
512
|
+
await freeze;
|
|
513
|
+
ensureOperationLive();
|
|
514
|
+
projection = await this.ports.projectTask(ctx.cwd, taskId);
|
|
515
|
+
ensureOperationLive();
|
|
516
|
+
if (projection.error || projection.projection.lifecycle !== "active" || projection.projection.artifact_state !== "frozen")
|
|
517
|
+
return this.unknownAfterCommit(taskId, "qa", operationId, projection.error ?? "artifact freeze did not settle");
|
|
518
|
+
// The freeze outcome is proven by the re-projection above, so later
|
|
519
|
+
// read-only preparation failures are never a committed-authority mystery.
|
|
520
|
+
authorityBoundaryStarted = false;
|
|
521
|
+
}
|
|
522
|
+
if (projection.projection.next_obligation === "complete") {
|
|
523
|
+
progress("completing", "Completing the routine task after deterministic QA");
|
|
524
|
+
try {
|
|
525
|
+
await this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "complete", actor_id: "kernel-assurance" } });
|
|
526
|
+
return { state: "completed" };
|
|
527
|
+
} catch (error) {
|
|
528
|
+
return this.unknownAfterCommit(taskId, "qa", operationId, boundedAssuranceError(error));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (projection.projection.next_obligation === "authorize_user")
|
|
532
|
+
return { state: "awaiting_user", operation: "record-user-approval", operation_id: operationId };
|
|
533
|
+
if (projection.projection.next_obligation !== "run_qa" && projection.projection.next_obligation !== "run_review")
|
|
534
|
+
return { state: "blocked", reason: `Kernel requires ${projection.projection.next_obligation}` };
|
|
535
|
+
const qaAlreadySettled = projection.projection.next_obligation === "run_review";
|
|
536
|
+
if (qaAlreadySettled) reviewPreparationStarted = true;
|
|
537
|
+
const runner = await this.ports.frozenRunner();
|
|
538
|
+
ensureOperationLive();
|
|
539
|
+
// Prove the immutable Review revision before any descriptor runs. A task
|
|
540
|
+
// that cannot publish its revision must stop with zero QA attestation
|
|
541
|
+
// rather than approving bytes nobody can review.
|
|
542
|
+
if (!qaAlreadySettled && projection.projection.risk !== "routine") {
|
|
543
|
+
progress("preparing_review_revision", "Proving the immutable Review revision before QA");
|
|
544
|
+
try {
|
|
545
|
+
if (parked.record?.contract === "assurance_kernel/task_record/v4") {
|
|
546
|
+
if (!this.ports.ensureReviewRevision)
|
|
547
|
+
throw new Error("v4 Review revision preparation is unavailable");
|
|
548
|
+
await this.ports.ensureReviewRevision(ctx.cwd, taskId, projection);
|
|
549
|
+
}
|
|
550
|
+
} catch (error) {
|
|
551
|
+
return { state: "blocked", reason: `review revision preparation failed: ${boundedAssuranceError(error)}` };
|
|
552
|
+
}
|
|
553
|
+
ensureOperationLive();
|
|
554
|
+
}
|
|
555
|
+
let qaVerdict: AssuranceVerdict | undefined;
|
|
556
|
+
if (qaAlreadySettled) {
|
|
557
|
+
progress("resuming_review", "Resuming Review preparation from the Kernel assurance projection");
|
|
558
|
+
} else {
|
|
559
|
+
progress("capturing_snapshot", "Capturing the immutable QA snapshot");
|
|
560
|
+
await this.ports.qaBeforeProjection?.();
|
|
561
|
+
ensureOperationLive();
|
|
562
|
+
const assurance = await this.ports.buildAssurance(ctx.cwd, taskId, "qa", projection, runner);
|
|
563
|
+
ensureOperationLive();
|
|
564
|
+
qaVerdict = await this.ports.runQa(assurance.snapshot, assurance.descriptors, runner, {
|
|
565
|
+
signal: operationController.signal,
|
|
566
|
+
onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, { current: item.index, total: item.total, acceptance_id: item.acceptance_id }),
|
|
567
|
+
});
|
|
568
|
+
ensureOperationLive();
|
|
569
|
+
const invocation = this.openInvocation(taskId);
|
|
570
|
+
try {
|
|
571
|
+
progress("settling_qa", "Settling deterministic QA through the Kernel revision boundary");
|
|
572
|
+
authorityBoundaryStarted = true;
|
|
573
|
+
await this.ports.applyVerdict(ctx, {
|
|
574
|
+
taskId,
|
|
575
|
+
snapshot: assurance.snapshot,
|
|
576
|
+
verdict: qaVerdict,
|
|
577
|
+
invocation,
|
|
578
|
+
actorId: "deterministic-qa",
|
|
579
|
+
hooks: {
|
|
580
|
+
beforeCommit: async () => {
|
|
581
|
+
ensureOperationLive();
|
|
582
|
+
await this.ports.qaBeforeAuthorityCommit?.();
|
|
583
|
+
ensureOperationLive();
|
|
584
|
+
},
|
|
585
|
+
onCommit: () => { authorityCommitted = true; this.ports.qaOnAuthorityCommit?.(); },
|
|
586
|
+
afterCommit: async () => {
|
|
587
|
+
await this.ports.qaAfterAuthorityCommit?.();
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
if (!(authorityCommitted && aborted())) {
|
|
592
|
+
ensureOperationLive();
|
|
593
|
+
authorityBoundaryStarted = false;
|
|
594
|
+
}
|
|
595
|
+
} catch (error) {
|
|
596
|
+
if (!authorityCommitted && (aborted() || error instanceof VerificationAbortedError)) return this.cancelled("qa", operationId, "host cancellation before QA authority commit");
|
|
597
|
+
return authorityCommitted
|
|
598
|
+
? this.unknownAfterCommit(taskId, "qa", operationId, boundedAssuranceError(error))
|
|
599
|
+
: { state: "failed", operation: "qa", operation_id: operationId, reason: boundedAssuranceError(error) };
|
|
600
|
+
} finally {
|
|
601
|
+
if (this.invocationState(invocation) === "open") this.closeInvocation(invocation);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (qaVerdict?.decision === "rework") return { state: "rework", operation: "qa", operation_id: operationId, summary: qaVerdict.findings?.map((finding) => finding.summary).join("; ") ?? "deterministic QA requested rework" };
|
|
605
|
+
if (!(authorityCommitted && aborted())) ensureOperationLive();
|
|
606
|
+
progress("preparing_review", "Preparing the reserved foreground Review bundle");
|
|
607
|
+
let fresh: AssuranceProjectionResult;
|
|
608
|
+
try {
|
|
609
|
+
fresh = await this.ports.projectTask(ctx.cwd, taskId);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
return this.reviewPreparationFailed(taskId, operationId, boundedAssuranceError(error));
|
|
612
|
+
}
|
|
613
|
+
if (fresh.error || !fresh.claim) {
|
|
614
|
+
if (qaAlreadySettled) return this.reviewPreparationFailed(taskId, operationId, fresh.error ?? "claim disappeared after QA settlement");
|
|
615
|
+
return this.unknownAfterCommit(taskId, "qa", operationId, fresh.error ?? "claim disappeared after QA settlement");
|
|
616
|
+
}
|
|
617
|
+
if (fresh.projection.next_obligation === "complete") {
|
|
618
|
+
progress("completing", "Deterministic QA passed; completing the routine task");
|
|
619
|
+
try {
|
|
620
|
+
await this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "complete", actor_id: "kernel-assurance" } });
|
|
621
|
+
return { state: "completed" };
|
|
622
|
+
} catch (error) {
|
|
623
|
+
return this.unknownAfterCommit(taskId, "qa", operationId, boundedAssuranceError(error));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (fresh.projection.next_obligation === "authorize_user")
|
|
627
|
+
return { state: "awaiting_user", operation: "record-user-approval", operation_id: operationId };
|
|
628
|
+
if (fresh.projection.next_obligation !== "run_review") {
|
|
629
|
+
if (authorityCommitted && aborted()) return this.unknownAfterCommit(taskId, "qa", operationId, "QA settlement projection did not require Review after cancellation");
|
|
630
|
+
return { state: "blocked", reason: `Kernel requires ${fresh.projection.next_obligation} after QA` };
|
|
631
|
+
}
|
|
632
|
+
reviewPreparationStarted = true;
|
|
633
|
+
authorityCommitted = false;
|
|
634
|
+
authorityBoundaryStarted = false;
|
|
635
|
+
if (aborted()) return this.reviewPreparationFailed(taskId, operationId, "host cancellation after QA authority settlement");
|
|
636
|
+
progress("preparing_review", "Preparing the reserved foreground Review evidence");
|
|
637
|
+
let review: Awaited<ReturnType<AssuranceCoordinatorPorts["buildAssurance"]>>;
|
|
638
|
+
let evidence: { path: string; remove(): void };
|
|
639
|
+
try {
|
|
640
|
+
review = await this.ports.buildAssurance(ctx.cwd, taskId, "review", fresh, runner);
|
|
641
|
+
const payload = review.reviewManifest ?? review.reviewBundle;
|
|
642
|
+
if (!payload) throw new Error("review evidence is missing after QA settlement");
|
|
643
|
+
evidence = this.ports.writeReviewEvidence({ snapshot: review.snapshot, evidence: payload });
|
|
644
|
+
} catch (error) {
|
|
645
|
+
// QA authority is already settled and durable; only the read-only
|
|
646
|
+
// evidence transport failed. Keep run_review and allow a retry.
|
|
647
|
+
return this.reviewPreparationFailed(taskId, operationId, boundedAssuranceError(error));
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
ensureOperationLive();
|
|
651
|
+
} catch (error) {
|
|
652
|
+
try { evidence.remove(); } catch { /* cleanup cannot create authority */ }
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
let hostReservation: HostReviewReservation;
|
|
656
|
+
try {
|
|
657
|
+
ensureOperationLive();
|
|
658
|
+
const workload = classifyReviewWorkload(review.snapshot, review.reviewManifest ?? review.reviewBundle!);
|
|
659
|
+
hostReservation = this.ports.host.prepareReview({
|
|
660
|
+
taskId,
|
|
661
|
+
operationId,
|
|
662
|
+
prompt: buildReviewPrompt(review.snapshot, evidence.path),
|
|
663
|
+
evidencePath: evidence.path,
|
|
664
|
+
maxTurns: reviewTurnBudget(workload),
|
|
665
|
+
});
|
|
666
|
+
ensureOperationLive();
|
|
667
|
+
} catch (error) {
|
|
668
|
+
try { evidence.remove(); } catch { /* cleanup cannot create authority */ }
|
|
669
|
+
return this.reviewPreparationFailed(taskId, operationId, boundedAssuranceError(error));
|
|
670
|
+
}
|
|
671
|
+
const reservation: ReviewReservation = {
|
|
672
|
+
taskId,
|
|
673
|
+
operationId,
|
|
674
|
+
correlation: { record_revision: review.snapshot.record_revision, intent_content_hash: review.snapshot.intent_content_hash, diff_hash: review.snapshot.diff_hash },
|
|
675
|
+
snapshot: review.snapshot,
|
|
676
|
+
hostReservation,
|
|
677
|
+
verdictCorrectionRequired: false,
|
|
678
|
+
evidence,
|
|
679
|
+
};
|
|
680
|
+
this.reviewReservations.set(taskId, reservation);
|
|
681
|
+
progress("review_ready", "QA passed; invoke the reserved foreground Agent, then call submit_review", { snapshot_digest: snapshotDigest(review.snapshot), review_bundle_digest: review.snapshot.review_bundle_digest ?? "", agent_params: hostReservation.dispatch });
|
|
682
|
+
return { state: "review_ready", operation: "review", operation_id: operationId, snapshot_digest: snapshotDigest(review.snapshot), review_bundle_digest: review.snapshot.review_bundle_digest ?? "", agent_params: hostReservation.dispatch };
|
|
683
|
+
} catch (error) {
|
|
684
|
+
if (reviewPreparationStarted) {
|
|
685
|
+
const reason = aborted() || error instanceof VerificationAbortedError
|
|
686
|
+
? `${phase}: host cancellation`
|
|
687
|
+
: `${phase}: ${boundedAssuranceError(error)}`;
|
|
688
|
+
return this.reviewPreparationFailed(taskId, operationId, reason);
|
|
689
|
+
}
|
|
690
|
+
if (authorityCommitted || authorityBoundaryStarted) return this.unknownAfterCommit(taskId, "qa", operationId, `${phase}: ${boundedAssuranceError(error)}`);
|
|
691
|
+
if (aborted() || error instanceof VerificationAbortedError) return this.cancelled("qa", operationId, `${phase}: host cancellation`);
|
|
692
|
+
return { state: "failed", operation: "qa", operation_id: operationId, reason: `${phase}: ${boundedAssuranceError(error)}` };
|
|
693
|
+
} finally {
|
|
694
|
+
if (this.activeOperations.get(taskId) === operationId) this.activeOperations.delete(taskId);
|
|
695
|
+
const controller = this.operationControllers.get(taskId);
|
|
696
|
+
if (controller?.operationId === operationId) this.operationControllers.delete(taskId);
|
|
697
|
+
signal?.removeEventListener("abort", relayExternalAbort);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async submitReview(taskId: string, ctx: HostContext, verdictInput: unknown): Promise<AssuranceSubmitReviewResult> {
|
|
702
|
+
const unknown = this.unknownOperations.get(taskId);
|
|
703
|
+
if (unknown) { this.unknownOperations.delete(taskId); return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason }; }
|
|
704
|
+
const rejected = this.rejectedReviewOperations.get(taskId);
|
|
705
|
+
if (rejected) return { state: "blocked", reason: rejected.reason };
|
|
706
|
+
const reservation = this.reviewReservations.get(taskId);
|
|
707
|
+
if (!reservation) return { state: "blocked", reason: "no active Review operation" };
|
|
708
|
+
let fresh: AssuranceProjectionResult;
|
|
709
|
+
try {
|
|
710
|
+
fresh = await this.ports.projectTask(ctx.cwd, taskId);
|
|
711
|
+
} catch (error) {
|
|
712
|
+
const reason = boundedAssuranceError(error);
|
|
713
|
+
this.releaseReviewReservation(taskId, reservation, reason);
|
|
714
|
+
return { state: "blocked", reason };
|
|
715
|
+
}
|
|
716
|
+
if (fresh.error || !fresh.claim || fresh.claim.task_id !== taskId || fresh.projection.record_revision !== reservation.snapshot.record_revision || fresh.projection.workspace_revision !== reservation.snapshot.workspace_revision || fresh.projection.intent_revision !== reservation.snapshot.intent_revision || fresh.projection.intent_content_hash !== reservation.snapshot.intent_content_hash || fresh.projection.diff_hash !== reservation.snapshot.diff_hash || fresh.projection.lifecycle !== reservation.snapshot.lifecycle || fresh.projection.artifact_state !== reservation.snapshot.artifact_state) {
|
|
717
|
+
const reason = fresh.error ?? "assurance snapshot changed before Review submission";
|
|
718
|
+
this.releaseReviewReservation(taskId, reservation, reason);
|
|
719
|
+
return { state: "blocked", reason };
|
|
720
|
+
}
|
|
721
|
+
if (reservation.snapshot.review_revision) {
|
|
722
|
+
try {
|
|
723
|
+
if (!this.ports.ensureReviewRevision)
|
|
724
|
+
throw new Error("v4 Review revision verification is unavailable");
|
|
725
|
+
const revision = await this.ports.ensureReviewRevision(ctx.cwd, taskId, fresh);
|
|
726
|
+
if (!revision || revision.base_head !== reservation.snapshot.review_revision.base_head || revision.review_commit !== reservation.snapshot.review_revision.review_commit || revision.review_tree !== reservation.snapshot.review_revision.review_tree || revision.manifest_digest !== reservation.snapshot.review_revision.manifest_digest)
|
|
727
|
+
throw new Error("Review revision changed before submission");
|
|
728
|
+
} catch (error) {
|
|
729
|
+
return this.reviewPreparationFailed(taskId, reservation.operationId, boundedAssuranceError(error));
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
let verdict: AssuranceVerdict;
|
|
733
|
+
try { verdict = parseAssuranceVerdict(verdictInput, reservation.snapshot); }
|
|
734
|
+
catch (error) {
|
|
735
|
+
reservation.verdictCorrectionRequired = true;
|
|
736
|
+
return { state: "blocked", code: "verdict_invalid", reason: boundedAssuranceError(error) };
|
|
737
|
+
}
|
|
738
|
+
const invocation = this.openInvocation(taskId);
|
|
739
|
+
this.releaseReviewReservation(taskId, reservation);
|
|
740
|
+
try {
|
|
741
|
+
await this.ports.applyVerdict(ctx, {
|
|
742
|
+
taskId,
|
|
743
|
+
snapshot: reservation.snapshot,
|
|
744
|
+
verdict,
|
|
745
|
+
invocation,
|
|
746
|
+
actorId: "parent-mediated-review",
|
|
747
|
+
});
|
|
748
|
+
} catch (error) {
|
|
749
|
+
const reason = boundedAssuranceError(error);
|
|
750
|
+
return this.invocationState(invocation) === "committed"
|
|
751
|
+
? this.unknownAfterCommit(taskId, "review", reservation.operationId, reason)
|
|
752
|
+
: { state: "blocked", reason };
|
|
753
|
+
} finally {
|
|
754
|
+
this.closeInvocation(invocation);
|
|
755
|
+
}
|
|
756
|
+
if (verdict.decision === "rework") {
|
|
757
|
+
return {
|
|
758
|
+
state: "rework",
|
|
759
|
+
operation: "review",
|
|
760
|
+
operation_id: reservation.operationId,
|
|
761
|
+
summary: verdict.findings?.map((finding) => finding.summary).join("; ") ?? "independent Review requested rework",
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
let settled: AssuranceProjectionResult;
|
|
765
|
+
try {
|
|
766
|
+
settled = await this.ports.projectTask(ctx.cwd, taskId);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
return this.unknownAfterCommit(taskId, "review", reservation.operationId, boundedAssuranceError(error));
|
|
769
|
+
}
|
|
770
|
+
if (settled.error || !settled.claim)
|
|
771
|
+
return this.unknownAfterCommit(taskId, "review", reservation.operationId, settled.error ?? "claim disappeared after Review settlement");
|
|
772
|
+
if (settled.projection.next_obligation === "complete") {
|
|
773
|
+
try {
|
|
774
|
+
await this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "complete", actor_id: "kernel-assurance" } });
|
|
775
|
+
return { state: "completed" };
|
|
776
|
+
} catch (error) {
|
|
777
|
+
return this.unknownAfterCommit(taskId, "review", reservation.operationId, boundedAssuranceError(error));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
if (settled.projection.next_obligation === "authorize_user")
|
|
781
|
+
return { state: "awaiting_user", operation: "record-user-approval", operation_id: reservation.operationId };
|
|
782
|
+
return { state: "blocked", reason: `Kernel requires ${settled.projection.next_obligation} after Review` };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
abandonReview(taskId: string, reason: string): AssuranceSubmitReviewResult {
|
|
786
|
+
const reservation = this.reviewReservations.get(taskId);
|
|
787
|
+
if (!reservation) return { state: "blocked", reason };
|
|
788
|
+
this.releaseReviewReservation(taskId, reservation, reason);
|
|
789
|
+
return { state: "blocked", reason };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
private reviewReadyResult(taskId: string): AssuranceAdvanceResult {
|
|
793
|
+
const reservation = this.reviewReservations.get(taskId);
|
|
794
|
+
if (!reservation) return { state: "blocked", reason: "Review reservation disappeared" };
|
|
795
|
+
if (reservation.verdictCorrectionRequired) return { state: "blocked", code: "verdict_invalid", reason: "Review verdict correction is required before advancing" };
|
|
796
|
+
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 };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private releaseReviewReservation(taskId: string, reservation: ReviewReservation, rejectionReason?: string): void {
|
|
800
|
+
if (this.reviewReservations.get(taskId) !== reservation) return;
|
|
801
|
+
this.reviewReservations.delete(taskId);
|
|
802
|
+
try { this.ports.host.releaseReview(reservation.hostReservation); } catch { /* cleanup cannot create authority */ }
|
|
803
|
+
if (rejectionReason) this.rejectedReviewOperations.set(taskId, { operationId: reservation.operationId, reason: rejectionReason });
|
|
804
|
+
this.removeEvidence(reservation);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
private removeEvidence(reservation: ReviewReservation): void {
|
|
808
|
+
try { reservation.evidence.remove(); } catch { /* cleanup cannot create authority */ }
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private cancelled(operation: "qa" | "review", operationId: string, reason: string): AssuranceAdvanceResult {
|
|
812
|
+
return { state: "cancelled", operation, operation_id: operationId, reason };
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
private reviewPreparationFailed(
|
|
816
|
+
taskId: string,
|
|
817
|
+
operationId: string,
|
|
818
|
+
reason: string,
|
|
819
|
+
): Extract<AssuranceAdvanceResult, { state: "review_preparation_failed" }> {
|
|
820
|
+
const reservation = this.reviewReservations.get(taskId);
|
|
821
|
+
if (reservation) this.releaseReviewReservation(taskId, reservation);
|
|
822
|
+
this.rejectedReviewOperations.delete(taskId);
|
|
823
|
+
return { state: "review_preparation_failed", operation: "review", operation_id: operationId, reason };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
private unknownAfterCommit(taskId: string, operation: "qa" | "review", operationId: string, reason: string): AssuranceAdvanceResult {
|
|
827
|
+
this.unknownOperations.set(taskId, { operation, operationId, reason });
|
|
828
|
+
return { state: "settlement_unknown", operation, operation_id: operationId, reason };
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function boundedAssuranceError(error: unknown): string {
|
|
833
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
834
|
+
const normalized = raw.replace(/\s+/g, " ").trim() || "unknown error";
|
|
835
|
+
return normalized.length <= 300 ? normalized : `${normalized.slice(0, 299)}...`;
|
|
836
|
+
}
|