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.
Files changed (66) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/package.json +9 -2
  5. package/plugins/immune-brain/.claude-plugin/plugin.json +8 -0
  6. package/plugins/immune-brain/.mcp.json +8 -0
  7. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +354 -64
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +74 -706
  9. package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +1 -90
  10. package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +13 -160
  11. package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +1 -50
  12. package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +1 -262
  13. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +3 -2
  14. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +8 -229
  15. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +23 -5
  16. package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -0
  17. package/plugins/immune-brain/dist/claude/mcp-server.mjs +7514 -0
  18. package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +58 -0
  19. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +1 -1
  20. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  21. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +19 -13
  22. package/plugins/immune-brain/dist/imm-loop.md +6 -7
  23. package/plugins/immune-brain/dist/imm-planner.md +1 -1
  24. package/plugins/immune-brain/dist/imm-pr-fix.md +9 -0
  25. package/plugins/immune-brain/dist/role-prompts/code-review.md +33 -13
  26. package/plugins/immune-brain/dist/role-prompts/executor.md +14 -0
  27. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +9 -0
  28. package/plugins/immune-brain/dist/role-prompts/test-fixer.md +7 -0
  29. package/plugins/immune-brain/hooks/hooks.json +55 -0
  30. package/plugins/immune-brain/runtime/assurance/coordinator.ts +836 -0
  31. package/plugins/immune-brain/runtime/assurance/enrollment.ts +6 -0
  32. package/plugins/immune-brain/runtime/assurance/host_port.ts +18 -0
  33. package/plugins/immune-brain/runtime/assurance/invocations.ts +90 -0
  34. package/plugins/immune-brain/runtime/assurance/qa_findings.ts +50 -0
  35. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +596 -0
  36. package/plugins/immune-brain/runtime/assurance/verification.ts +233 -0
  37. package/plugins/immune-brain/runtime/claude/capability.ts +67 -0
  38. package/plugins/immune-brain/runtime/claude/interaction.ts +70 -0
  39. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +789 -0
  40. package/plugins/immune-brain/runtime/claude/mcp_server.ts +363 -0
  41. package/plugins/immune-brain/runtime/claude/review_host.ts +645 -0
  42. package/plugins/immune-brain/runtime/commands/kernel.ts +221 -3
  43. package/plugins/immune-brain/runtime/github_issue_tracker.ts +2 -2
  44. package/plugins/immune-brain/runtime/kernel/application.ts +8 -5
  45. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +24 -13
  46. package/plugins/immune-brain/runtime/kernel/authority_port.ts +78 -115
  47. package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -4
  48. package/plugins/immune-brain/runtime/kernel/capability_registry.ts +89 -0
  49. package/plugins/immune-brain/runtime/kernel/completion.ts +64 -6
  50. package/plugins/immune-brain/runtime/kernel/enrollment.ts +189 -100
  51. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +37 -80
  52. package/plugins/immune-brain/runtime/kernel/intent.ts +24 -0
  53. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +31 -0
  54. package/plugins/immune-brain/runtime/kernel/reducer.ts +36 -13
  55. package/plugins/immune-brain/runtime/kernel/storage.ts +16 -16
  56. package/plugins/immune-brain/runtime/kernel/types.ts +32 -2
  57. package/plugins/immune-brain/runtime/kernel/validation.ts +107 -16
  58. package/plugins/immune-brain/runtime/loop_contract.ts +17 -2
  59. package/plugins/immune-brain/runtime/prompts/code-review.md +33 -13
  60. package/plugins/immune-brain/runtime/prompts/executor.md +14 -0
  61. package/plugins/immune-brain/runtime/prompts/pr-fix.md +9 -0
  62. package/plugins/immune-brain/runtime/prompts/test-fixer.md +7 -0
  63. package/plugins/immune-brain/runtime/v4_runtime.ts +5 -2
  64. package/plugins/immune-brain/runtime/workspace_scope.ts +191 -5
  65. package/plugins/immune-brain/skills/imm-loop/SKILL.md +3 -4
  66. 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(item, ["id", "kind", "authority_role", "task_revision", "intent_content_hash", "diff_hash", "actor_id", "summary", "acceptance_results"], path, violations);
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
- export function parseTaskRecordV3(raw: unknown): TaskRecordV3 {
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
- if (value.contract !== TASK_RECORD_CONTRACT_V3)
541
- violations.push(`contract must equal ${TASK_RECORD_CONTRACT_V3}`);
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
- return {
582
- contract: TASK_RECORD_CONTRACT_V3,
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: TaskRecordV3): void {
682
+ export function assertKernelInvariantsV3(intentRaw: TaskIntentV1, recordRaw: TaskRecord): void {
596
683
  const intent = parseTaskIntentV1(intentRaw);
597
- const record = parseTaskRecordV3(recordRaw);
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: TaskRecordV3,
875
- nextRaw: TaskRecordV3,
963
+ previousRaw: TaskRecord,
964
+ nextRaw: TaskRecord,
876
965
  action: TaskAction,
877
966
  ): void {
878
- const previous = parseTaskRecordV3(previousRaw);
879
- const next = parseTaskRecordV3(nextRaw);
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: "general-purpose";
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: "general-purpose",
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 only
4
- the bounded change surface and the evidence supplied by the Parent. Classify
5
- findings, state observable verification criteria, and preserve the current
6
- review gate identity. Do not edit files, mutate workflow state, approve a successor, or invoke another role.
7
-
8
- The stable Review Gate is `imm-code-review`. Return exactly one JSON object
9
- with the fields required by the Loop review contract: `contract`, `role`,
10
- `task_id`, `snapshot_digest`, `decision` (`pass` or `rework`), and for
11
- `pass` include `approval` (`kind`, `authority_role`, `summary`), for `rework`
12
- include `findings` (`id`, `kind`, `acceptance_id`, `summary`). Do not invent
13
- fields. A passing review has no findings. If the checkpoint is
14
- `awaiting_user_successor_decision`, stop without dispatch; only a literal user
15
- may invoke `--approve-successor`.
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,64}$/;
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 function taskDiffHash(projectRoot: string, scopeHint: unknown): string {
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 `sha256:${createHash("sha256")
369
- .update(JSON.stringify(snapshot))
370
- .digest("hex")}`;
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 {