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
|
@@ -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,
|
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
# Internal role: code-review
|
|
2
2
|
|
|
3
|
-
You are the Immune-Brain read-only code review role inside Loop. Review
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
3
|
+
You are the Immune-Brain read-only code review role inside Loop. Review the
|
|
4
|
+
immutable Git revision and bounded evidence supplied by the Parent. For
|
|
5
|
+
`assurance_kernel/review_manifest/v5`, read the metadata manifest first, verify
|
|
6
|
+
`base_head`, `review_commit`, its single parent, `review_tree`, and
|
|
7
|
+
`manifest_digest`, then inspect source only with read-only Git commands such as
|
|
8
|
+
`git diff <base_head> <review_commit>` and `git show <review_commit>:<path>`.
|
|
9
|
+
Never read live worktree bytes as evidence, enumerate neighborhood files, or
|
|
10
|
+
infer task ownership from unchanged paths. Read an unchanged path only when an
|
|
11
|
+
acceptance assertion, changed caller, or same state machine directly requires
|
|
12
|
+
it, and cite the path and reason in the finding. The manifest is metadata only;
|
|
13
|
+
source content must not be copied into the review envelope.
|
|
14
|
+
|
|
15
|
+
Do not edit files, mutate workflow state, approve a successor, or invoke
|
|
16
|
+
another role. The stable Review Gate is `imm-code-review`.
|
|
17
|
+
|
|
18
|
+
## Code Quality Guard
|
|
19
|
+
|
|
20
|
+
Apply the Code Quality Guard reference to the immutable revision: reject
|
|
21
|
+
fabricated success, unknown-error suppression, missing external-boundary
|
|
22
|
+
validation, invented imports/APIs, weakened tests, unauthorized behavior
|
|
23
|
+
changes, and speculative production paths when the diff creates a concrete
|
|
24
|
+
risk. Report only evidence-based correctness, security, regression, or
|
|
25
|
+
material task-local maintenance risks. Pure naming, length, complexity
|
|
26
|
+
thresholds, formatting, and design preference are not findings and must not
|
|
27
|
+
cause style-only rework.
|
|
28
|
+
|
|
29
|
+
Return exactly one JSON object with the fields required by the Loop review
|
|
30
|
+
contract: `contract`, `role`, `task_id`, `snapshot_digest`, `decision` (`pass`
|
|
31
|
+
or `rework`), and for `pass` include `approval` (`kind`, `authority_role`,
|
|
32
|
+
`summary`), for `rework` include `findings` (`id`, `kind`, `acceptance_id`,
|
|
33
|
+
`summary`). Do not invent fields. A passing review has no findings. If the
|
|
34
|
+
checkpoint is `awaiting_user_successor_decision`, stop without dispatch; only
|
|
35
|
+
a literal user may invoke `--approve-successor`.
|
|
@@ -11,3 +11,17 @@ action. Preserve failed and blocked attempts. Do not perform QA,
|
|
|
11
11
|
review, plan mutation, successor approval, Compounder work, or authority
|
|
12
12
|
writes. If the requested change needs scope expansion, stop and return an
|
|
13
13
|
`imm-planner` route with the concrete missing scope and verification reason.
|
|
14
|
+
|
|
15
|
+
## Code Quality Guard
|
|
16
|
+
|
|
17
|
+
Before handoff, check the implementation for real implementation rather than
|
|
18
|
+
mock or hard-coded success, swallowed unexpected errors, missing validation at
|
|
19
|
+
external trust boundaries, invented dependencies or APIs, unauthorized
|
|
20
|
+
observable behavior changes, and production paths without a current caller.
|
|
21
|
+
Do not weaken tests or hide an incomplete result to make Verification pass.
|
|
22
|
+
Treat naming, function length, parameter count, nesting, and abstraction taste
|
|
23
|
+
as contextual signals, never as automatic failure thresholds.
|
|
24
|
+
|
|
25
|
+
Fix in-scope integrity defects before Verification. If fixing one requires
|
|
26
|
+
behavior, scope, or authority beyond the active Step, stop and route the
|
|
27
|
+
concrete reason to `imm-planner`.
|
|
@@ -60,6 +60,15 @@ shard on failure; on second failure, fall back to solo repair.
|
|
|
60
60
|
Re-run project checks and PR-related conflict checks. Compare local HEAD
|
|
61
61
|
against PR head expectation before push.
|
|
62
62
|
|
|
63
|
+
## Code Quality Guard
|
|
64
|
+
|
|
65
|
+
Apply the same integrity boundary while repairing a blocker. Do not clear CI or
|
|
66
|
+
review feedback by swallowing unexpected errors, fabricating success, or using
|
|
67
|
+
a repair that would weaken tests, invent an unavailable API or dependency,
|
|
68
|
+
change unrelated behavior, or widen the PR beyond the named blocker. Preserve
|
|
69
|
+
the PR's observable intent. If the correct repair needs new scope or a product
|
|
70
|
+
decision, stop and report it to the Parent.
|
|
71
|
+
|
|
63
72
|
## Boundary
|
|
64
73
|
|
|
65
74
|
Work only inside the supplied Plan, `plan_id`, changed-file boundary, review
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
# Internal role: test-fixer
|
|
2
2
|
|
|
3
3
|
You are the Immune-Brain bounded test-repair role inside Loop. Edit only the delegated test files listed in `focus_delta.specific_changes` for the active target. Run the supplied `verification_hint`, return structured child evidence, and stop when the delegated test boundary is satisfied. Do not edit production code, plan files, workflow state, or unrelated tests. Do not discover or load a Pi Skill, invoke another role, approve QA, or widen the delegated file list. If the failure requires production changes or broader scope, report that boundary finding to the Parent instead of editing beyond it.
|
|
4
|
+
|
|
5
|
+
## Code Quality Guard
|
|
6
|
+
|
|
7
|
+
Preserve test intent while repairing tests. Do not delete or loosen assertions,
|
|
8
|
+
reduce coverage, replace target behavior with a mock, or change expected
|
|
9
|
+
behavior solely to make the test pass. A production defect is a boundary
|
|
10
|
+
finding for the Parent, not permission to edit production code.
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* retirement. It exposes:
|
|
6
6
|
* - `imm-kernel` intent author/validate (host-neutral TaskIntent drafts)
|
|
7
7
|
* - `imm-kernel status --json` (read-only v3 legacy shadow status)
|
|
8
|
+
* - `imm-kernel inspect --json` (read-only Inspect Projection)
|
|
8
9
|
* - `imm-kernel audit --legacy` (explicit read-only legacy audit)
|
|
9
10
|
* - `imm-plan --routing-status --json` (strict Git-owned route projection)
|
|
10
11
|
* - `imm-plan <plan-path> [--json]` (read-only Plan validation)
|
|
@@ -183,6 +184,7 @@ async function runKernelCli(args: string[], root: string): Promise<{
|
|
|
183
184
|
const sub = args[0] ?? "";
|
|
184
185
|
if (sub === "intent") return runKernelCommand(args, root);
|
|
185
186
|
if (sub === "status" && args.includes("--json")) return runKernelCommand(args, root);
|
|
187
|
+
if (sub === "inspect" && args.includes("--json")) return runKernelCommand(args, root);
|
|
186
188
|
if (sub === "audit") {
|
|
187
189
|
// Explicit read-only legacy audit: bounded, no symlink, deterministic
|
|
188
190
|
// redacted projection. Never writes journal or workflow state.
|
|
@@ -204,7 +206,7 @@ async function runKernelCli(args: string[], root: string): Promise<{
|
|
|
204
206
|
}
|
|
205
207
|
return {
|
|
206
208
|
stdout: "",
|
|
207
|
-
stderr: "invalid_kernel_command: imm-kernel supports intent author|validate, status --json, and audit --legacy only\n",
|
|
209
|
+
stderr: "invalid_kernel_command: imm-kernel supports intent author|validate, status --json, inspect --json, and audit --legacy only\n",
|
|
208
210
|
returncode: 2,
|
|
209
211
|
};
|
|
210
212
|
}
|
|
@@ -236,12 +238,13 @@ async function main(argv: string[]): Promise<number> {
|
|
|
236
238
|
{
|
|
237
239
|
name: "imm-kernel",
|
|
238
240
|
description:
|
|
239
|
-
"v4-only Kernel surface: intent author/validate, status, and explicit legacy audit.",
|
|
241
|
+
"v4-only Kernel surface: intent author/validate, status, inspect, and explicit legacy audit.",
|
|
240
242
|
json_output: true,
|
|
241
243
|
examples: [
|
|
242
244
|
"imm-kernel intent author docs/plans/<task-id>.intent.json --stdin --json",
|
|
243
245
|
"imm-kernel intent validate docs/plans/<task-id>.intent.json --json",
|
|
244
246
|
"imm-kernel status --json",
|
|
247
|
+
"imm-kernel inspect --json",
|
|
245
248
|
"imm-kernel audit --legacy",
|
|
246
249
|
],
|
|
247
250
|
},
|
|
@@ -126,7 +126,7 @@ export interface GitTaskSnapshot {
|
|
|
126
126
|
staged_files: Record<string, GitTaskIndexEntry>;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
const GIT_OBJECT_ID = /^[a-f0-9]{40
|
|
129
|
+
const GIT_OBJECT_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
130
130
|
const TASK_GIT_MODES = new Set(["100644", "100755", "120000"] as const);
|
|
131
131
|
const fatalUtf8 = new TextDecoder("utf-8", { fatal: true });
|
|
132
132
|
const portablePathCollator = new Intl.Collator("und", {
|
|
@@ -195,6 +195,34 @@ function decodeNullPaths(bytes: Buffer, label: string): string[] {
|
|
|
195
195
|
return paths;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
function decodeIndexFlaggedPaths(bytes: Buffer, label: string): Array<{ path: string; flag: "assume-unchanged" | "skip-worktree" }> {
|
|
199
|
+
if (bytes.length === 0) return [];
|
|
200
|
+
if (bytes[bytes.length - 1] !== 0) throw new Error(`${label} is not NUL-terminated`);
|
|
201
|
+
const flagged: Array<{ path: string; flag: "assume-unchanged" | "skip-worktree" }> = [];
|
|
202
|
+
let start = 0;
|
|
203
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
204
|
+
if (bytes[index] !== 0) continue;
|
|
205
|
+
const record = bytes.subarray(start, index);
|
|
206
|
+
if (record.length < 3 || record[1] !== 0x20) throw new Error(`${label} contains a malformed entry`);
|
|
207
|
+
const tag = String.fromCharCode(record[0]);
|
|
208
|
+
if (tag === "h" || tag === "S") {
|
|
209
|
+
flagged.push({
|
|
210
|
+
path: decodeCanonicalGitPath(record.subarray(2), label),
|
|
211
|
+
flag: tag === "h" ? "assume-unchanged" : "skip-worktree",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
start = index + 1;
|
|
215
|
+
}
|
|
216
|
+
return flagged;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function assertNoScopedIndexFlags(root: string, scope: string[], label: string): void {
|
|
220
|
+
const flagged = decodeIndexFlaggedPaths(gitBytes(root, ["ls-files", "-v", "-z", "--"]), "Git index flags")
|
|
221
|
+
.filter(({ path }) => taskPathMatchesScope(path, scope));
|
|
222
|
+
if (flagged.length > 0)
|
|
223
|
+
throw new Error(`${label} contains unsupported index flags: ${flagged.map(({ path, flag }) => `${path} (${flag})`).join(", ")}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
198
226
|
function assertNoCaseFoldCollisions(paths: string[], label: string): void {
|
|
199
227
|
const prefixes: string[] = [];
|
|
200
228
|
for (const path of paths) {
|
|
@@ -300,6 +328,7 @@ function taskSnapshotOnce(root: string, scope: string[]): GitTaskSnapshot {
|
|
|
300
328
|
throw new Error("task snapshot does not support sparse checkout or sparse index");
|
|
301
329
|
if (gitBytes(root, ["ls-files", "--unmerged", "-z"]).length > 0)
|
|
302
330
|
throw new Error("task snapshot does not support unmerged index entries");
|
|
331
|
+
assertNoScopedIndexFlags(root, scope, "task snapshot");
|
|
303
332
|
|
|
304
333
|
const stagedPaths = decodeNullPaths(
|
|
305
334
|
gitBytes(root, ["diff", "--cached", "--no-renames", "--name-only", "-z", head, "--"]),
|
|
@@ -363,11 +392,168 @@ export function captureGitTaskSnapshot(
|
|
|
363
392
|
return before;
|
|
364
393
|
}
|
|
365
394
|
|
|
366
|
-
export
|
|
395
|
+
export interface GitTaskDiffIdentity {
|
|
396
|
+
diff_hash: string;
|
|
397
|
+
changed_paths: string[];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function hashTaskSnapshot(snapshot: object): string {
|
|
401
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function taskDiffIdentity(
|
|
405
|
+
projectRoot: string,
|
|
406
|
+
scopeHint: unknown,
|
|
407
|
+
): GitTaskDiffIdentity {
|
|
367
408
|
const snapshot = captureGitTaskSnapshot(projectRoot, scopeHint);
|
|
368
|
-
return
|
|
369
|
-
|
|
370
|
-
.
|
|
409
|
+
return {
|
|
410
|
+
diff_hash: hashTaskSnapshot(snapshot),
|
|
411
|
+
changed_paths: Object.keys(snapshot.staged_files).sort(comparePaths),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function taskDiffHash(projectRoot: string, scopeHint: unknown): string {
|
|
416
|
+
return taskDiffIdentity(projectRoot, scopeHint).diff_hash;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function gitRequired(root: string, args: string[], failure: string): string {
|
|
420
|
+
const output = git(root, args);
|
|
421
|
+
if (output === null) throw new Error(failure);
|
|
422
|
+
return output.trim();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The v4 scoped revision snapshot: the exact delta between the Enrollment base
|
|
427
|
+
* commit and the current index, restricted to the TaskIntent mutation envelope.
|
|
428
|
+
* It deliberately omits the current HEAD so an out-of-scope commit never
|
|
429
|
+
* invalidates task identity, and it never reads unchanged scope matches.
|
|
430
|
+
*/
|
|
431
|
+
export interface GitTaskRevisionSnapshot {
|
|
432
|
+
kind: "git-task-revision-v1";
|
|
433
|
+
repository_root: string;
|
|
434
|
+
base_head: string;
|
|
435
|
+
base_tree: string;
|
|
436
|
+
scope: string[];
|
|
437
|
+
changed_paths: Record<string, GitTaskIndexEntry>;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function taskRevisionSnapshotOnce(
|
|
441
|
+
root: string,
|
|
442
|
+
scope: string[],
|
|
443
|
+
baseHead: string,
|
|
444
|
+
): GitTaskRevisionSnapshot {
|
|
445
|
+
const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
|
|
446
|
+
const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
|
|
447
|
+
if (!repositoryRoot || !head || !GIT_OBJECT_ID.test(head))
|
|
448
|
+
throw new Error("cannot derive a task revision outside a committed Git workspace");
|
|
449
|
+
if (realpathSync(resolve(repositoryRoot)) !== root)
|
|
450
|
+
throw new Error("task revision repository root does not match the project root");
|
|
451
|
+
if (gitRequired(root, ["cat-file", "-t", baseHead], `task revision base is unreadable: ${baseHead}`) !== "commit")
|
|
452
|
+
throw new Error(`task revision base is not a commit: ${baseHead}`);
|
|
453
|
+
if (git(root, ["merge-base", "--is-ancestor", baseHead, head]) === null)
|
|
454
|
+
throw new Error(
|
|
455
|
+
`task revision base ${baseHead} is no longer an ancestor of HEAD; rewrite the task history or re-enroll`,
|
|
456
|
+
);
|
|
457
|
+
const baseTree = gitRequired(root, ["rev-parse", `${baseHead}^{tree}`], "task revision base tree is unreadable");
|
|
458
|
+
if (!GIT_OBJECT_ID.test(baseTree)) throw new Error("task revision base tree has invalid identity");
|
|
459
|
+
const sparseCheckout = git(root, ["config", "--bool", "core.sparseCheckout"])?.trim();
|
|
460
|
+
const sparseIndex = git(root, ["config", "--bool", "index.sparse"])?.trim();
|
|
461
|
+
if (sparseCheckout === "true" || sparseIndex === "true")
|
|
462
|
+
throw new Error("task revision does not support sparse checkout or sparse index");
|
|
463
|
+
if (gitBytes(root, ["ls-files", "--unmerged", "-z"]).length > 0)
|
|
464
|
+
throw new Error("task revision does not support unmerged index entries");
|
|
465
|
+
assertNoScopedIndexFlags(root, scope, "task revision");
|
|
466
|
+
|
|
467
|
+
const stagedPaths = decodeNullPaths(
|
|
468
|
+
gitBytes(root, ["diff", "--cached", "--no-renames", "--name-only", "-z", baseHead, "--"]),
|
|
469
|
+
"task revision paths",
|
|
470
|
+
);
|
|
471
|
+
const unstagedPaths = decodeNullPaths(
|
|
472
|
+
gitBytes(root, ["diff", "--no-renames", "--name-only", "-z", "--"]),
|
|
473
|
+
"unstaged task revision paths",
|
|
474
|
+
);
|
|
475
|
+
const untrackedPaths = decodeNullPaths(
|
|
476
|
+
gitBytes(root, ["ls-files", "--others", "--exclude-standard", "-z", "--"]),
|
|
477
|
+
"untracked task revision paths",
|
|
478
|
+
);
|
|
479
|
+
const scopedStagedPaths = stagedPaths.filter((path) => taskPathMatchesScope(path, scope));
|
|
480
|
+
const scopedUnstagedPaths = unstagedPaths.filter((path) => taskPathMatchesScope(path, scope));
|
|
481
|
+
const scopedUntrackedPaths = untrackedPaths.filter((path) => taskPathMatchesScope(path, scope));
|
|
482
|
+
assertNoCaseFoldCollisions(
|
|
483
|
+
[...scopedStagedPaths, ...scopedUnstagedPaths, ...scopedUntrackedPaths],
|
|
484
|
+
"Git task revision paths",
|
|
485
|
+
);
|
|
486
|
+
const drift = [...new Set([...scopedUnstagedPaths, ...scopedUntrackedPaths])].sort(comparePaths);
|
|
487
|
+
if (drift.length > 0)
|
|
488
|
+
throw new Error(`task scope contains unstaged or untracked changes: ${drift.join(", ")}`);
|
|
489
|
+
|
|
490
|
+
const changed = [...new Set(scopedStagedPaths)].sort(comparePaths);
|
|
491
|
+
const changedPaths: Record<string, GitTaskIndexEntry> = {};
|
|
492
|
+
for (const path of changed) {
|
|
493
|
+
const current = indexEntry(root, path);
|
|
494
|
+
const base = headEntry(root, baseHead, path);
|
|
495
|
+
if (!current && !base) throw new Error(`task revision path has no index or base identity: ${path}`);
|
|
496
|
+
if (current && base && current.oid === base.oid && current.mode === base.mode)
|
|
497
|
+
throw new Error(`task revision path is not actually changed: ${path}`);
|
|
498
|
+
changedPaths[path] = {
|
|
499
|
+
status: !base ? "added" : !current ? "deleted" : "modified",
|
|
500
|
+
mode: current?.mode ?? null,
|
|
501
|
+
oid: current?.oid ?? null,
|
|
502
|
+
base_mode: base?.mode ?? null,
|
|
503
|
+
base_oid: base?.oid ?? null,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
kind: "git-task-revision-v1",
|
|
508
|
+
repository_root: root,
|
|
509
|
+
base_head: baseHead,
|
|
510
|
+
base_tree: baseTree,
|
|
511
|
+
scope,
|
|
512
|
+
changed_paths: changedPaths,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function captureGitTaskRevisionSnapshot(
|
|
517
|
+
projectRoot: string,
|
|
518
|
+
scopeHint: unknown,
|
|
519
|
+
baseHead: unknown,
|
|
520
|
+
): GitTaskRevisionSnapshot {
|
|
521
|
+
const requestedRoot = resolve(projectRoot);
|
|
522
|
+
const requestedStat = lstatSync(requestedRoot);
|
|
523
|
+
if (requestedStat.isSymbolicLink() || !requestedStat.isDirectory())
|
|
524
|
+
throw new Error("task revision root must be a real directory");
|
|
525
|
+
const root = realpathSync(requestedRoot);
|
|
526
|
+
if (typeof baseHead !== "string" || !GIT_OBJECT_ID.test(baseHead.toLowerCase()))
|
|
527
|
+
throw new Error("task revision base must be a Git commit id");
|
|
528
|
+
const scope = assertCanonicalTaskScope(scopeHint);
|
|
529
|
+
const normalizedBase = baseHead.toLowerCase();
|
|
530
|
+
const before = taskRevisionSnapshotOnce(root, scope, normalizedBase);
|
|
531
|
+
gitTaskSnapshotTestHook?.();
|
|
532
|
+
const after = taskRevisionSnapshotOnce(root, scope, normalizedBase);
|
|
533
|
+
if (JSON.stringify(after) !== JSON.stringify(before))
|
|
534
|
+
throw new Error("Git task revision changed while being captured");
|
|
535
|
+
return before;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function taskRevisionIdentity(
|
|
539
|
+
projectRoot: string,
|
|
540
|
+
scopeHint: unknown,
|
|
541
|
+
baseHead: string,
|
|
542
|
+
): GitTaskDiffIdentity {
|
|
543
|
+
const snapshot = captureGitTaskRevisionSnapshot(projectRoot, scopeHint, baseHead);
|
|
544
|
+
return {
|
|
545
|
+
diff_hash: hashTaskSnapshot(snapshot),
|
|
546
|
+
changed_paths: Object.keys(snapshot.changed_paths).sort(comparePaths),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** The single v4 freshness identity shared by QA, Review, authorization, and completion. */
|
|
551
|
+
export function taskRevisionDiffHash(
|
|
552
|
+
projectRoot: string,
|
|
553
|
+
scopeHint: unknown,
|
|
554
|
+
baseHead: string,
|
|
555
|
+
): string {
|
|
556
|
+
return taskRevisionIdentity(projectRoot, scopeHint, baseHead).diff_hash;
|
|
371
557
|
}
|
|
372
558
|
|
|
373
559
|
function isGitWorkspaceSnapshot(value: unknown): value is GitWorkspaceSnapshot {
|