immune-brain 2.8.2 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +28 -13
- 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
|
@@ -6,20 +6,21 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
import { completionDecision } from "./completion";
|
|
7
7
|
import {
|
|
8
8
|
REDUCED_MUTATION_BRAND,
|
|
9
|
+
TASK_RECORD_CONTRACT_V4,
|
|
9
10
|
type AuthorityAuditDescriptor,
|
|
10
11
|
type ReducedTaskMutation,
|
|
11
12
|
type TaskAction,
|
|
12
13
|
type TaskFinding,
|
|
13
14
|
type TaskIntentRefV3,
|
|
14
15
|
type TaskIntentV1,
|
|
15
|
-
type
|
|
16
|
+
type TaskRecord,
|
|
16
17
|
} from "./types";
|
|
17
18
|
import {
|
|
18
19
|
KernelInvariantError,
|
|
19
20
|
assertKernelInvariantsV3,
|
|
20
21
|
assertTaskRecordUpdateV3,
|
|
21
22
|
parseTaskAction,
|
|
22
|
-
|
|
23
|
+
parseTaskRecord,
|
|
23
24
|
} from "./validation";
|
|
24
25
|
import { classifyIntentRevision, canonicalIntentHash } from "./intent";
|
|
25
26
|
|
|
@@ -29,7 +30,7 @@ const RISK_RANK: Record<TaskIntentV1["risk"], number> = {
|
|
|
29
30
|
critical: 2,
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
function transitionLifecycle(record:
|
|
33
|
+
function transitionLifecycle(record: TaskRecord, to: "done" | "stopped"): void {
|
|
33
34
|
if (record.lifecycle !== "active")
|
|
34
35
|
throw new KernelInvariantError([
|
|
35
36
|
`illegal lifecycle transition: ${record.lifecycle} -> ${to}`,
|
|
@@ -37,7 +38,7 @@ function transitionLifecycle(record: TaskRecordV3, to: "done" | "stopped"): void
|
|
|
37
38
|
record.lifecycle = to;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function stateOf(record:
|
|
41
|
+
function stateOf(record: TaskRecord): string {
|
|
41
42
|
return `${record.lifecycle}:${record.artifact_state}`;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -54,7 +55,7 @@ function stableJson(value: unknown): string {
|
|
|
54
55
|
return primitive === undefined ? "null" : primitive;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
export function canonicalRecordHash(record:
|
|
58
|
+
export function canonicalRecordHash(record: TaskRecord): string {
|
|
58
59
|
// Must equal the storage CAS revision: the exact serialized file bytes.
|
|
59
60
|
return `sha256:${createHash("sha256")
|
|
60
61
|
.update(`${JSON.stringify(record, null, 2)}\n`)
|
|
@@ -99,7 +100,7 @@ export function recordedActionFingerprint(reason: string | null): string | null
|
|
|
99
100
|
return matched?.[1] ?? null;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
function copyRecord(record:
|
|
103
|
+
function copyRecord(record: TaskRecord): TaskRecord {
|
|
103
104
|
return {
|
|
104
105
|
...record,
|
|
105
106
|
intent_snapshot: { ...record.intent_snapshot },
|
|
@@ -113,7 +114,7 @@ function copyRecord(record: TaskRecordV3): TaskRecordV3 {
|
|
|
113
114
|
};
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
function reviewRound(record:
|
|
117
|
+
function reviewRound(record: TaskRecord): number {
|
|
117
118
|
return (
|
|
118
119
|
Math.max(
|
|
119
120
|
0,
|
|
@@ -125,7 +126,7 @@ function reviewRound(record: TaskRecordV3): number {
|
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
function appendHistory(
|
|
128
|
-
record:
|
|
129
|
+
record: TaskRecord,
|
|
129
130
|
action: TaskAction,
|
|
130
131
|
from: string,
|
|
131
132
|
detail: string | null,
|
|
@@ -200,11 +201,12 @@ export function findingsDigestV2(findings: TaskFinding[]): string {
|
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
export function reduceTask(
|
|
203
|
-
recordRaw:
|
|
204
|
+
recordRaw: TaskRecord,
|
|
204
205
|
actionRaw: TaskAction,
|
|
205
206
|
authorityAudit: AuthorityAuditDescriptor | null = null,
|
|
207
|
+
changedPaths?: readonly string[],
|
|
206
208
|
): ReducedTaskMutation {
|
|
207
|
-
const previous =
|
|
209
|
+
const previous = parseTaskRecord(recordRaw);
|
|
208
210
|
assertKernelInvariantsV3(previous.intent_snapshot, previous);
|
|
209
211
|
const action = parseTaskAction(actionRaw);
|
|
210
212
|
const record = copyRecord(previous);
|
|
@@ -343,8 +345,23 @@ export function reduceTask(
|
|
|
343
345
|
throw new KernelInvariantError([
|
|
344
346
|
`attestations contains duplicate id ${approval.id}`,
|
|
345
347
|
]);
|
|
348
|
+
// A v4 review attestation must pin the exact immutable revision the
|
|
349
|
+
// reviewer analyzed; nothing else may carry that identity.
|
|
350
|
+
const reviewRevision = approval.review_revision;
|
|
351
|
+
if (reviewRevision && approval.kind !== "review")
|
|
352
|
+
throw new KernelInvariantError(["review_revision is only valid on review approvals"]);
|
|
353
|
+
if (record.contract !== TASK_RECORD_CONTRACT_V4) {
|
|
354
|
+
if (reviewRevision)
|
|
355
|
+
throw new KernelInvariantError(["review_revision requires a TaskRecord v4"]);
|
|
356
|
+
} else if (approval.kind === "review") {
|
|
357
|
+
if (!reviewRevision)
|
|
358
|
+
throw new KernelInvariantError(["v4 review approval requires review_revision"]);
|
|
359
|
+
if (reviewRevision.base_head !== record.git_base_head)
|
|
360
|
+
throw new KernelInvariantError(["review_revision.base_head must equal the Enrollment git_base_head"]);
|
|
361
|
+
}
|
|
362
|
+
const { review_revision: storedReviewRevision, ...approvalWithoutRevision } = approval;
|
|
346
363
|
record.attestations.push({
|
|
347
|
-
...
|
|
364
|
+
...approvalWithoutRevision,
|
|
348
365
|
acceptance_results: approval.kind === "qa"
|
|
349
366
|
? record.intent_snapshot.acceptance.map((item) => ({
|
|
350
367
|
acceptance_id: item.id,
|
|
@@ -352,6 +369,7 @@ export function reduceTask(
|
|
|
352
369
|
summary: `host-attested QA: ${approval.summary}`,
|
|
353
370
|
}))
|
|
354
371
|
: [],
|
|
372
|
+
...(storedReviewRevision ? { review_revision: storedReviewRevision } : {}),
|
|
355
373
|
});
|
|
356
374
|
appendHistory(record, action, from, approval.id, authorityAudit);
|
|
357
375
|
break;
|
|
@@ -380,6 +398,8 @@ export function reduceTask(
|
|
|
380
398
|
throw new KernelInvariantError([
|
|
381
399
|
`attestations contains duplicate id ${approval.id}`,
|
|
382
400
|
]);
|
|
401
|
+
if (approval.review_revision)
|
|
402
|
+
throw new KernelInvariantError(["review_revision is only valid on review approvals"]);
|
|
383
403
|
record.attestations.push({ ...approval, acceptance_results: [] });
|
|
384
404
|
appendHistory(record, action, from, approval.id, authorityAudit);
|
|
385
405
|
break;
|
|
@@ -518,11 +538,14 @@ export function reduceTask(
|
|
|
518
538
|
throw new KernelInvariantError([
|
|
519
539
|
`cannot complete while state is ${stateOf(record)}`,
|
|
520
540
|
]);
|
|
541
|
+
if (!Array.isArray(changedPaths))
|
|
542
|
+
throw new KernelInvariantError(["complete requires trusted changed paths"]);
|
|
521
543
|
const decision = completionDecision(
|
|
522
544
|
record.intent_snapshot,
|
|
523
545
|
record,
|
|
524
546
|
diffHash,
|
|
525
547
|
record.intent_ref.content_hash,
|
|
548
|
+
changedPaths,
|
|
526
549
|
);
|
|
527
550
|
if (!decision.complete)
|
|
528
551
|
throw new KernelInvariantError([
|
|
@@ -587,12 +610,12 @@ export function reduceTask(
|
|
|
587
610
|
return brandResult(record, nextWorkingFor(record));
|
|
588
611
|
}
|
|
589
612
|
|
|
590
|
-
function nextWorkingFor(record:
|
|
613
|
+
function nextWorkingFor(record: TaskRecord): string | null {
|
|
591
614
|
return record.lifecycle === "active" ? record.task_id : null;
|
|
592
615
|
}
|
|
593
616
|
|
|
594
617
|
function brandResult(
|
|
595
|
-
record:
|
|
618
|
+
record: TaskRecord,
|
|
596
619
|
nextWorking: string | null,
|
|
597
620
|
): ReducedTaskMutation {
|
|
598
621
|
const target = {} as ReducedTaskMutation;
|
|
@@ -33,12 +33,12 @@ import {
|
|
|
33
33
|
stateTransactionPath,
|
|
34
34
|
stateWorkspacePath,
|
|
35
35
|
} from "./storage_paths";
|
|
36
|
-
import {
|
|
36
|
+
import { parseTaskRecord, parseTaskRecordV2 } from "./validation";
|
|
37
37
|
import type {
|
|
38
38
|
TaskLifecycle,
|
|
39
39
|
TaskPhase,
|
|
40
40
|
TaskRecordV2,
|
|
41
|
-
|
|
41
|
+
TaskRecord,
|
|
42
42
|
StoredTaskMutationV3,
|
|
43
43
|
V3AuthorityObservation,
|
|
44
44
|
} from "./types";
|
|
@@ -649,7 +649,7 @@ function parseWorkspaceTransactionV2(raw: Record<string, unknown>): WorkspaceTra
|
|
|
649
649
|
...(artifactRelocations.length > 0 ? { artifact_relocations: artifactRelocations } : {}),
|
|
650
650
|
};
|
|
651
651
|
validateTaskId(transaction.task_id);
|
|
652
|
-
const record =
|
|
652
|
+
const record = parseTaskRecord(
|
|
653
653
|
JSON.parse(transaction.next_record_content),
|
|
654
654
|
);
|
|
655
655
|
if (record.task_id !== transaction.task_id)
|
|
@@ -736,7 +736,7 @@ function completeTransactionV2Locked(
|
|
|
736
736
|
transaction.expected_workspace_hash,
|
|
737
737
|
transaction.next_workspace_content,
|
|
738
738
|
);
|
|
739
|
-
const record =
|
|
739
|
+
const record = parseTaskRecord(
|
|
740
740
|
JSON.parse(transaction.next_record_content),
|
|
741
741
|
);
|
|
742
742
|
const workspace = parseWorkspaceContent(transaction.next_workspace_content);
|
|
@@ -791,7 +791,7 @@ function recoverAnyPendingTransactionLocked(root: string): void {
|
|
|
791
791
|
export function readTaskRecordRaw(
|
|
792
792
|
root: string,
|
|
793
793
|
taskId: string,
|
|
794
|
-
): { revision: string; record:
|
|
794
|
+
): { revision: string; record: TaskRecord | null } {
|
|
795
795
|
validateTaskId(taskId);
|
|
796
796
|
const relativePath = stateTaskRecordPath(taskId);
|
|
797
797
|
if (currentRevision(root, relativePath) === MISSING_REVISION)
|
|
@@ -802,7 +802,7 @@ export function readTaskRecordRaw(
|
|
|
802
802
|
throw new KernelStoreSecurityError(
|
|
803
803
|
"TaskRecord v2 is not supported in the state layout; v2 records belong to the historical audit layout",
|
|
804
804
|
);
|
|
805
|
-
const record =
|
|
805
|
+
const record = parseTaskRecord(raw);
|
|
806
806
|
if (record.task_id !== taskId)
|
|
807
807
|
throw new KernelStoreSecurityError("task record v3 identity is inconsistent");
|
|
808
808
|
return { revision: revisionFor(content), record };
|
|
@@ -821,7 +821,7 @@ export function readAuditTaskPair(
|
|
|
821
821
|
taskId: string,
|
|
822
822
|
): {
|
|
823
823
|
recordRevision: string;
|
|
824
|
-
record:
|
|
824
|
+
record: TaskRecord | TaskRecordV2;
|
|
825
825
|
proof: TaskTombstone;
|
|
826
826
|
} | null {
|
|
827
827
|
validateTaskId(taskId);
|
|
@@ -846,7 +846,7 @@ export function readAuditTaskPair(
|
|
|
846
846
|
"terminal audit proof does not match its task record",
|
|
847
847
|
);
|
|
848
848
|
const raw = JSON.parse(recordContent) as { contract?: unknown };
|
|
849
|
-
let record:
|
|
849
|
+
let record: TaskRecord | TaskRecordV2;
|
|
850
850
|
if (raw.contract === "assurance_kernel/task_record/v2") {
|
|
851
851
|
const legacy = parseTaskRecordV2(raw);
|
|
852
852
|
if (
|
|
@@ -858,7 +858,7 @@ export function readAuditTaskPair(
|
|
|
858
858
|
);
|
|
859
859
|
record = legacy;
|
|
860
860
|
} else {
|
|
861
|
-
const current =
|
|
861
|
+
const current = parseTaskRecord(raw);
|
|
862
862
|
if (
|
|
863
863
|
current.task_id !== taskId ||
|
|
864
864
|
(current.lifecycle !== "done" && current.lifecycle !== "stopped")
|
|
@@ -874,7 +874,7 @@ export function readAuditTaskPair(
|
|
|
874
874
|
export function readTaskRecord(
|
|
875
875
|
root: string,
|
|
876
876
|
taskId: string,
|
|
877
|
-
): { revision: string; record:
|
|
877
|
+
): { revision: string; record: TaskRecord | null } {
|
|
878
878
|
return withKernelStoreLock(root, () => readTaskRecordRaw(root, taskId));
|
|
879
879
|
}
|
|
880
880
|
|
|
@@ -883,7 +883,7 @@ export function commitTaskRecordLocked(
|
|
|
883
883
|
root: string,
|
|
884
884
|
taskId: string,
|
|
885
885
|
expectedRecordHash: string,
|
|
886
|
-
nextRecord:
|
|
886
|
+
nextRecord: TaskRecord,
|
|
887
887
|
expectedWorkspaceHash: string,
|
|
888
888
|
nextWorkspace: WorkspaceState,
|
|
889
889
|
artifactRelocations: ArtifactRelocationV1[] = [],
|
|
@@ -1358,7 +1358,7 @@ export function commitEnrollmentLocked(
|
|
|
1358
1358
|
);
|
|
1359
1359
|
}
|
|
1360
1360
|
return {
|
|
1361
|
-
record:
|
|
1361
|
+
record: parseTaskRecord(JSON.parse(transaction.next_record_content)),
|
|
1362
1362
|
workspace: parseWorkspaceContent(transaction.next_workspace_content),
|
|
1363
1363
|
};
|
|
1364
1364
|
}
|
|
@@ -1572,7 +1572,7 @@ function parseTerminalMarker(raw: Record<string, unknown>): TerminalMarker {
|
|
|
1572
1572
|
expected_claim_sha256: expectedClaim,
|
|
1573
1573
|
at: raw.at as string,
|
|
1574
1574
|
};
|
|
1575
|
-
const record =
|
|
1575
|
+
const record = parseTaskRecord(JSON.parse(marker.audit_record_content));
|
|
1576
1576
|
if (record.task_id !== marker.task_id)
|
|
1577
1577
|
throw new KernelStoreSecurityError("terminal marker task identity is inconsistent");
|
|
1578
1578
|
if (record.lifecycle !== "done" && record.lifecycle !== "stopped")
|
|
@@ -1709,7 +1709,7 @@ export function commitTerminalLocked(
|
|
|
1709
1709
|
taskId: string,
|
|
1710
1710
|
transaction: WorkspaceTransactionV2,
|
|
1711
1711
|
tombstone: TaskTombstone,
|
|
1712
|
-
): { record:
|
|
1712
|
+
): { record: TaskRecord; workspace: WorkspaceState } {
|
|
1713
1713
|
validateTaskId(taskId);
|
|
1714
1714
|
if (transaction.task_id !== taskId)
|
|
1715
1715
|
throw new KernelStoreSecurityError(
|
|
@@ -1728,7 +1728,7 @@ export function commitTerminalLocked(
|
|
|
1728
1728
|
throw new KernelStoreSecurityError(
|
|
1729
1729
|
"terminal proof must match the terminal record bytes",
|
|
1730
1730
|
);
|
|
1731
|
-
const terminalRecord =
|
|
1731
|
+
const terminalRecord = parseTaskRecord(
|
|
1732
1732
|
JSON.parse(transaction.next_record_content) as Record<string, unknown>,
|
|
1733
1733
|
);
|
|
1734
1734
|
if (tombstone.terminal_lifecycle !== terminalRecord.lifecycle)
|
|
@@ -1774,7 +1774,7 @@ export function commitTerminalLocked(
|
|
|
1774
1774
|
);
|
|
1775
1775
|
}
|
|
1776
1776
|
return {
|
|
1777
|
-
record:
|
|
1777
|
+
record: parseTaskRecord(JSON.parse(transaction.next_record_content)),
|
|
1778
1778
|
workspace: parseWorkspaceContent(transaction.next_workspace_content),
|
|
1779
1779
|
};
|
|
1780
1780
|
}
|
|
@@ -99,6 +99,9 @@ export interface V3AuthorityObservation {
|
|
|
99
99
|
export const TASK_INTENT_CONTRACT_V1 = "assurance_kernel/task_intent/v1" as const;
|
|
100
100
|
export const TASK_RECORD_CONTRACT_V2 = "assurance_kernel/task_record/v2" as const;
|
|
101
101
|
export const TASK_RECORD_CONTRACT_V3 = "assurance_kernel/task_record/v3" as const;
|
|
102
|
+
export const TASK_RECORD_CONTRACT_V4 = "assurance_kernel/task_record/v4" as const;
|
|
103
|
+
export const REVIEW_REVISION_IDENTITY_CONTRACT =
|
|
104
|
+
"assurance_kernel/review_revision_identity/v1" as const;
|
|
102
105
|
|
|
103
106
|
export interface TaskIntentAcceptanceItemV1 {
|
|
104
107
|
id: string;
|
|
@@ -148,6 +151,20 @@ export interface TaskApprovalV2 {
|
|
|
148
151
|
diff_hash: string;
|
|
149
152
|
actor_id: string;
|
|
150
153
|
summary: string;
|
|
154
|
+
/**
|
|
155
|
+
* Immutable Git revision identity the reviewer actually analyzed. Legal only
|
|
156
|
+
* on a review attestation of a TaskRecord v4; the strict parser rejects it
|
|
157
|
+
* anywhere else, so QA and user attestations never carry revision bytes.
|
|
158
|
+
*/
|
|
159
|
+
review_revision?: ReviewRevisionIdentityV1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface ReviewRevisionIdentityV1 {
|
|
163
|
+
contract: typeof REVIEW_REVISION_IDENTITY_CONTRACT;
|
|
164
|
+
base_head: string;
|
|
165
|
+
review_commit: string;
|
|
166
|
+
review_tree: string;
|
|
167
|
+
manifest_digest: string;
|
|
151
168
|
}
|
|
152
169
|
|
|
153
170
|
export interface TaskHistoryEntryV2 {
|
|
@@ -206,6 +223,19 @@ export interface TaskRecordV3 {
|
|
|
206
223
|
history: TaskHistoryEntryV3[];
|
|
207
224
|
}
|
|
208
225
|
|
|
226
|
+
/**
|
|
227
|
+
* TaskRecord v4 binds the Enrollment commit so Review, QA, authorization, and
|
|
228
|
+
* completion can all derive one scoped revision from base to final index tree.
|
|
229
|
+
* `git_base_head` is immutable from Enrollment through terminal settlement.
|
|
230
|
+
*/
|
|
231
|
+
export interface TaskRecordV4 extends Omit<TaskRecordV3, "contract"> {
|
|
232
|
+
contract: typeof TASK_RECORD_CONTRACT_V4;
|
|
233
|
+
git_base_head: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The record shape every Kernel owner passes around during the v3 drain window. */
|
|
237
|
+
export type TaskRecord = TaskRecordV3 | TaskRecordV4;
|
|
238
|
+
|
|
209
239
|
export interface TaskProjectionV3 extends CompletionDecision {
|
|
210
240
|
contract: "assurance_kernel/projection/v3";
|
|
211
241
|
task_id: string;
|
|
@@ -268,13 +298,13 @@ export const REDUCED_MUTATION_BRAND = Symbol("assurance-kernel-reduced-mutation-
|
|
|
268
298
|
|
|
269
299
|
export interface ReducedTaskMutation {
|
|
270
300
|
readonly [REDUCED_MUTATION_BRAND]: true;
|
|
271
|
-
readonly record:
|
|
301
|
+
readonly record: TaskRecord;
|
|
272
302
|
readonly next_workspace_working: string | null;
|
|
273
303
|
}
|
|
274
304
|
|
|
275
305
|
export interface StoredTaskMutationV3 {
|
|
276
306
|
revision: string;
|
|
277
|
-
record:
|
|
307
|
+
record: TaskRecord;
|
|
278
308
|
workspace: {
|
|
279
309
|
revision: string;
|
|
280
310
|
state: WorkspaceStateLike;
|
|
@@ -4,6 +4,8 @@ import {
|
|
|
4
4
|
TASK_PHASES,
|
|
5
5
|
TASK_RECORD_CONTRACT_V2,
|
|
6
6
|
TASK_RECORD_CONTRACT_V3,
|
|
7
|
+
TASK_RECORD_CONTRACT_V4,
|
|
8
|
+
REVIEW_REVISION_IDENTITY_CONTRACT,
|
|
7
9
|
type ApprovalAuthorityRole,
|
|
8
10
|
type ApprovalKind,
|
|
9
11
|
type EvidenceStatus,
|
|
@@ -19,11 +21,16 @@ import {
|
|
|
19
21
|
type TaskHistoryEntryV2,
|
|
20
22
|
type TaskHistoryEntryV3,
|
|
21
23
|
type TaskIntentV1,
|
|
24
|
+
type ReviewRevisionIdentityV1,
|
|
25
|
+
type TaskRecord,
|
|
22
26
|
type TaskRecordV2,
|
|
23
27
|
type TaskRecordV3,
|
|
28
|
+
type TaskRecordV4,
|
|
24
29
|
} from "./types";
|
|
25
30
|
import { canonicalIntentHash, parseTaskIntentV1 } from "./intent";
|
|
26
31
|
|
|
32
|
+
const GIT_OBJECT_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
33
|
+
|
|
27
34
|
export class KernelValidationError extends Error {
|
|
28
35
|
readonly code = "kernel_schema_invalid";
|
|
29
36
|
|
|
@@ -320,11 +327,13 @@ function parseApprovalV2(
|
|
|
320
327
|
value: unknown,
|
|
321
328
|
index: number,
|
|
322
329
|
violations: string[],
|
|
330
|
+
allowReviewRevision = false,
|
|
323
331
|
): TaskApprovalV2 {
|
|
324
332
|
const item = objectAt(value, `record.approvals[${index}]`, violations);
|
|
325
333
|
rejectUnknown(
|
|
326
334
|
item,
|
|
327
|
-
["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary"
|
|
335
|
+
["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary",
|
|
336
|
+
...(allowReviewRevision ? ["review_revision"] : [])],
|
|
328
337
|
`record.approvals[${index}]`,
|
|
329
338
|
violations,
|
|
330
339
|
);
|
|
@@ -356,18 +365,52 @@ function parseApprovalV2(
|
|
|
356
365
|
diff_hash: diffHash,
|
|
357
366
|
actor_id: stringAt(item.actor_id, `record.approvals[${index}].actor_id`, violations),
|
|
358
367
|
summary: stringAt(item.summary, `record.approvals[${index}].summary`, violations),
|
|
368
|
+
...(allowReviewRevision && item.review_revision !== undefined
|
|
369
|
+
? { review_revision: parseReviewRevisionIdentity(item.review_revision, `record.approvals[${index}].review_revision`, violations) }
|
|
370
|
+
: {}),
|
|
359
371
|
};
|
|
360
372
|
}
|
|
361
373
|
|
|
374
|
+
function parseReviewRevisionIdentity(
|
|
375
|
+
value: unknown,
|
|
376
|
+
path: string,
|
|
377
|
+
violations: string[],
|
|
378
|
+
): ReviewRevisionIdentityV1 {
|
|
379
|
+
const item = objectAt(value, path, violations);
|
|
380
|
+
rejectUnknown(item, ["contract", "base_head", "review_commit", "review_tree", "manifest_digest"], path, violations);
|
|
381
|
+
if (item.contract !== REVIEW_REVISION_IDENTITY_CONTRACT)
|
|
382
|
+
violations.push(`${path}.contract must equal ${REVIEW_REVISION_IDENTITY_CONTRACT}`);
|
|
383
|
+
const identity = {
|
|
384
|
+
contract: REVIEW_REVISION_IDENTITY_CONTRACT,
|
|
385
|
+
base_head: stringAt(item.base_head, `${path}.base_head`, violations),
|
|
386
|
+
review_commit: stringAt(item.review_commit, `${path}.review_commit`, violations),
|
|
387
|
+
review_tree: stringAt(item.review_tree, `${path}.review_tree`, violations),
|
|
388
|
+
manifest_digest: stringAt(item.manifest_digest, `${path}.manifest_digest`, violations),
|
|
389
|
+
};
|
|
390
|
+
for (const field of ["base_head", "review_commit", "review_tree"] as const)
|
|
391
|
+
if (!GIT_OBJECT_ID.test(identity[field]))
|
|
392
|
+
violations.push(`${path}.${field} must be a lowercase Git object id`);
|
|
393
|
+
if (!SHA256_HEX.test(identity.manifest_digest))
|
|
394
|
+
violations.push(`${path}.manifest_digest must be sha256:<64 hex>`);
|
|
395
|
+
return identity;
|
|
396
|
+
}
|
|
397
|
+
|
|
362
398
|
function parseAttestationV3(
|
|
363
399
|
value: unknown,
|
|
364
400
|
index: number,
|
|
365
401
|
acceptanceIds: Set<string>,
|
|
366
402
|
violations: string[],
|
|
403
|
+
allowReviewRevision = false,
|
|
367
404
|
): TaskAttestationV3 {
|
|
368
405
|
const path = `record.attestations[${index}]`;
|
|
369
406
|
const item = objectAt(value, path, violations);
|
|
370
|
-
rejectUnknown(
|
|
407
|
+
rejectUnknown(
|
|
408
|
+
item,
|
|
409
|
+
["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary", "acceptance_results",
|
|
410
|
+
...(allowReviewRevision ? ["review_revision"] : [])],
|
|
411
|
+
path,
|
|
412
|
+
violations,
|
|
413
|
+
);
|
|
371
414
|
const kind = enumAt(item.kind, APPROVAL_KINDS, `${path}.kind`, violations);
|
|
372
415
|
const intentContentHash = stringAt(item.intent_content_hash, `${path}.intent_content_hash`, violations);
|
|
373
416
|
const diffHash = stringAt(item.diff_hash, `${path}.diff_hash`, violations);
|
|
@@ -392,6 +435,13 @@ function parseAttestationV3(
|
|
|
392
435
|
if (resultIds.length !== acceptanceIds.size || new Set(resultIds).size !== acceptanceIds.size)
|
|
393
436
|
violations.push(`${path}.acceptance_results must cover every acceptance exactly once`);
|
|
394
437
|
}
|
|
438
|
+
const reviewRevision = item.review_revision === undefined
|
|
439
|
+
? undefined
|
|
440
|
+
: parseReviewRevisionIdentity(item.review_revision, `${path}.review_revision`, violations);
|
|
441
|
+
if (reviewRevision && kind !== "review")
|
|
442
|
+
violations.push(`${path}.review_revision is only valid on review attestations`);
|
|
443
|
+
if (allowReviewRevision && kind === "review" && !reviewRevision)
|
|
444
|
+
violations.push(`${path}.review_revision is required for v4 review attestations`);
|
|
395
445
|
return {
|
|
396
446
|
id: stringAt(item.id, `${path}.id`, violations),
|
|
397
447
|
kind,
|
|
@@ -402,6 +452,7 @@ function parseAttestationV3(
|
|
|
402
452
|
actor_id: stringAt(item.actor_id, `${path}.actor_id`, violations),
|
|
403
453
|
summary: stringAt(item.summary, `${path}.summary`, violations),
|
|
404
454
|
acceptance_results: acceptanceResults,
|
|
455
|
+
...(reviewRevision ? { review_revision: reviewRevision } : {}),
|
|
405
456
|
};
|
|
406
457
|
}
|
|
407
458
|
|
|
@@ -528,17 +579,19 @@ export function parseTaskRecordV2(raw: unknown): TaskRecordV2 {
|
|
|
528
579
|
};
|
|
529
580
|
}
|
|
530
581
|
|
|
531
|
-
|
|
582
|
+
function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskRecordV3 | TaskRecordV4 {
|
|
532
583
|
const violations: string[] = [];
|
|
533
584
|
const value = objectAt(raw, "record", violations);
|
|
534
585
|
rejectUnknown(
|
|
535
586
|
value,
|
|
536
|
-
["contract", "task_id", "intent_snapshot", "intent_ref", "lifecycle", "artifact_state", "baseline", "attestations", "findings", "history"
|
|
587
|
+
["contract", "task_id", "intent_snapshot", "intent_ref", "lifecycle", "artifact_state", "baseline", "attestations", "findings", "history",
|
|
588
|
+
...(version === 4 ? ["git_base_head"] : [])],
|
|
537
589
|
"record",
|
|
538
590
|
violations,
|
|
539
591
|
);
|
|
540
|
-
|
|
541
|
-
|
|
592
|
+
const expectedContract = version === 4 ? TASK_RECORD_CONTRACT_V4 : TASK_RECORD_CONTRACT_V3;
|
|
593
|
+
if (value.contract !== expectedContract)
|
|
594
|
+
violations.push(`contract must equal ${expectedContract}`);
|
|
542
595
|
|
|
543
596
|
let snapshot: TaskIntentV1 | null = null;
|
|
544
597
|
try {
|
|
@@ -570,31 +623,65 @@ export function parseTaskRecordV3(raw: unknown): TaskRecordV3 {
|
|
|
570
623
|
|
|
571
624
|
const baseline = stringAt(value.baseline, "record.baseline", violations);
|
|
572
625
|
if (!SHA256_HEX.test(baseline)) violations.push("record.baseline must be sha256:<64 hex>");
|
|
626
|
+
let gitBaseHead = "";
|
|
627
|
+
if (version === 4) {
|
|
628
|
+
const rawGitBaseHead = stringAt(value.git_base_head, "record.git_base_head", violations);
|
|
629
|
+
if (rawGitBaseHead !== rawGitBaseHead.toLowerCase())
|
|
630
|
+
violations.push("record.git_base_head must be lowercase");
|
|
631
|
+
gitBaseHead = rawGitBaseHead;
|
|
632
|
+
if (!GIT_OBJECT_ID.test(gitBaseHead))
|
|
633
|
+
violations.push("record.git_base_head must be a lowercase Git commit id");
|
|
634
|
+
}
|
|
573
635
|
const acceptanceIds = new Set(snapshot ? snapshot.acceptance.map((item) => item.id) : []);
|
|
574
|
-
const attestations = arrayAt(value.attestations, "record.attestations", violations).map((item, index) => parseAttestationV3(item, index, acceptanceIds, violations));
|
|
636
|
+
const attestations = arrayAt(value.attestations, "record.attestations", violations).map((item, index) => parseAttestationV3(item, index, acceptanceIds, violations, version === 4));
|
|
637
|
+
if (version === 4) {
|
|
638
|
+
for (const [index, attestation] of attestations.entries()) {
|
|
639
|
+
if (attestation.kind === "review" && attestation.review_revision && attestation.review_revision.base_head !== gitBaseHead)
|
|
640
|
+
violations.push(`record.attestations[${index}].review_revision.base_head must equal record.git_base_head`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
575
643
|
const findings = arrayAt(value.findings, "record.findings", violations).map((item, index) => parseFinding(item, index, violations));
|
|
576
644
|
const history = arrayAt(value.history, "record.history", violations).map((item, index) => parseHistoryV3(item, index, violations));
|
|
577
645
|
uniqueIds(attestations, "record.attestations", violations);
|
|
578
646
|
uniqueIds(findings, "record.findings", violations);
|
|
579
647
|
uniqueIds(history, "record.history", violations);
|
|
580
648
|
if (violations.length > 0) throw new KernelValidationError(violations);
|
|
581
|
-
|
|
582
|
-
contract:
|
|
649
|
+
const record = {
|
|
650
|
+
contract: expectedContract,
|
|
583
651
|
task_id: taskId,
|
|
584
652
|
intent_snapshot: snapshot as TaskIntentV1,
|
|
585
653
|
intent_ref: { path: refPath, content_hash: refContentHash },
|
|
586
654
|
lifecycle: lifecycle as TaskRecordV3["lifecycle"],
|
|
587
655
|
artifact_state: artifactState as TaskRecordV3["artifact_state"],
|
|
588
656
|
baseline,
|
|
657
|
+
...(version === 4 ? { git_base_head: gitBaseHead } : {}),
|
|
589
658
|
attestations,
|
|
590
659
|
findings,
|
|
591
660
|
history,
|
|
592
661
|
};
|
|
662
|
+
return record as TaskRecordV3 | TaskRecordV4;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/** Strict v3 drain parser: unknown fields and revision identity stay illegal. */
|
|
666
|
+
export function parseTaskRecordV3(raw: unknown): TaskRecordV3 {
|
|
667
|
+
return parseTaskRecordAtVersion(raw, 3) as TaskRecordV3;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Strict v4 parser: the Enrollment base commit is mandatory. */
|
|
671
|
+
export function parseTaskRecordV4(raw: unknown): TaskRecordV4 {
|
|
672
|
+
return parseTaskRecordAtVersion(raw, 4) as TaskRecordV4;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** Version-dispatched parser used by every durable Kernel owner. */
|
|
676
|
+
export function parseTaskRecord(raw: unknown): TaskRecord {
|
|
677
|
+
const contract = (raw as { contract?: unknown } | null)?.contract;
|
|
678
|
+
if (contract === TASK_RECORD_CONTRACT_V4) return parseTaskRecordV4(raw);
|
|
679
|
+
return parseTaskRecordV3(raw);
|
|
593
680
|
}
|
|
594
681
|
|
|
595
|
-
export function assertKernelInvariantsV3(intentRaw: TaskIntentV1, recordRaw:
|
|
682
|
+
export function assertKernelInvariantsV3(intentRaw: TaskIntentV1, recordRaw: TaskRecord): void {
|
|
596
683
|
const intent = parseTaskIntentV1(intentRaw);
|
|
597
|
-
const record =
|
|
684
|
+
const record = parseTaskRecord(recordRaw);
|
|
598
685
|
const violations: string[] = [];
|
|
599
686
|
if (intent.task_id !== record.task_id) violations.push("intent and record task_id must match");
|
|
600
687
|
if (intent.revision !== record.intent_snapshot.revision) violations.push("intent revision must match record snapshot");
|
|
@@ -752,7 +839,9 @@ export function parseTaskAction(raw: unknown): TaskAction {
|
|
|
752
839
|
"action",
|
|
753
840
|
violations,
|
|
754
841
|
);
|
|
755
|
-
const approval = parseApprovalV2(value.approval, 0, violations);
|
|
842
|
+
const approval = parseApprovalV2(value.approval, 0, violations, true);
|
|
843
|
+
if (approval.review_revision && approval.kind !== "review")
|
|
844
|
+
violations.push("action.approval.review_revision is only valid on review approvals");
|
|
756
845
|
action = {
|
|
757
846
|
...base,
|
|
758
847
|
type: base.type,
|
|
@@ -871,12 +960,12 @@ export function parseTaskAction(raw: unknown): TaskAction {
|
|
|
871
960
|
}
|
|
872
961
|
|
|
873
962
|
export function assertTaskRecordUpdateV3(
|
|
874
|
-
previousRaw:
|
|
875
|
-
nextRaw:
|
|
963
|
+
previousRaw: TaskRecord,
|
|
964
|
+
nextRaw: TaskRecord,
|
|
876
965
|
action: TaskAction,
|
|
877
966
|
): void {
|
|
878
|
-
const previous =
|
|
879
|
-
const next =
|
|
967
|
+
const previous = parseTaskRecord(previousRaw);
|
|
968
|
+
const next = parseTaskRecord(nextRaw);
|
|
880
969
|
const violations: string[] = [];
|
|
881
970
|
|
|
882
971
|
if (next.contract !== previous.contract)
|
|
@@ -885,6 +974,8 @@ export function assertTaskRecordUpdateV3(
|
|
|
885
974
|
violations.push("record task_id must remain immutable");
|
|
886
975
|
if (next.baseline !== previous.baseline)
|
|
887
976
|
violations.push("record baseline must remain immutable");
|
|
977
|
+
if (previous.contract === TASK_RECORD_CONTRACT_V4 && next.contract === TASK_RECORD_CONTRACT_V4 && next.git_base_head !== previous.git_base_head)
|
|
978
|
+
violations.push("record git_base_head must remain immutable");
|
|
888
979
|
|
|
889
980
|
const isIntentAction =
|
|
890
981
|
action.type === "revise_intent" ||
|
|
@@ -217,7 +217,7 @@ export function buildLoopAction(input: {
|
|
|
217
217
|
export interface LoopRoleDispatch {
|
|
218
218
|
packet: RoleDelegationPacket;
|
|
219
219
|
call: {
|
|
220
|
-
subagent_type:
|
|
220
|
+
subagent_type: LoopRoleSubagent;
|
|
221
221
|
description: string;
|
|
222
222
|
prompt: string;
|
|
223
223
|
inherit_context: false;
|
|
@@ -226,6 +226,21 @@ export interface LoopRoleDispatch {
|
|
|
226
226
|
};
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Authoritative code review runs on the host's configured `Review` agent so the
|
|
231
|
+
* user's own model/provider selection applies. Architecture exploration uses
|
|
232
|
+
* the host `Explore` agent. Every other internal role, and especially the
|
|
233
|
+
* tool-less planning `advisory-reviewer`, stays on the generic agent and never
|
|
234
|
+
* inherits review authority.
|
|
235
|
+
*/
|
|
236
|
+
export type LoopRoleSubagent = "Review" | "Explore" | "general-purpose";
|
|
237
|
+
|
|
238
|
+
export function loopRoleSubagentFor(role: LoopRole): LoopRoleSubagent {
|
|
239
|
+
if (role === "code-review") return "Review";
|
|
240
|
+
if (role === "arch-explorer") return "Explore";
|
|
241
|
+
return "general-purpose";
|
|
242
|
+
}
|
|
243
|
+
|
|
229
244
|
export function buildLoopRoleDispatch(input: {
|
|
230
245
|
role: LoopRole;
|
|
231
246
|
context: RoleDelegationContext;
|
|
@@ -235,7 +250,7 @@ export function buildLoopRoleDispatch(input: {
|
|
|
235
250
|
return {
|
|
236
251
|
packet,
|
|
237
252
|
call: {
|
|
238
|
-
subagent_type:
|
|
253
|
+
subagent_type: loopRoleSubagentFor(input.role),
|
|
239
254
|
description: input.description ?? `${input.role} internal role`,
|
|
240
255
|
prompt: packet.prompt,
|
|
241
256
|
inherit_context: false,
|