immune-brain 5.3.0 → 5.5.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/README.md +4 -4
- package/README.zh-CN.md +4 -4
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +54 -70
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +163 -28
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +71 -10
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +1 -1
- package/plugins/immune-brain/.pi-extension/pi-run-continuation.ts +161 -0
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -2
- package/plugins/immune-brain/dist/BASELINE.md +13 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +16130 -12734
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +3 -1
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +41 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/imm-planner.md +57 -16
- package/plugins/immune-brain/dist/imm-run-batch.md +398 -0
- package/plugins/immune-brain/dist/imm-run.md +89 -344
- package/plugins/immune-brain/dist/role-prompts/code-review.md +26 -2
- package/plugins/immune-brain/dist/role-prompts/executor.md +35 -10
- package/plugins/immune-brain/hooks/hooks.json +21 -1
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +203 -13
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +2 -1
- package/plugins/immune-brain/runtime/assurance/enrollment_git_base.ts +99 -14
- package/plugins/immune-brain/runtime/assurance/phase_entry.ts +1 -7
- package/plugins/immune-brain/runtime/assurance/qa.ts +126 -43
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +8 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +48 -0
- package/plugins/immune-brain/runtime/assurance/run_continuation.ts +284 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +46 -4
- package/plugins/immune-brain/runtime/assurance/verification.ts +83 -35
- package/plugins/immune-brain/runtime/assurance/workflow_autocommit.ts +222 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +11 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +199 -48
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +8 -15
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +105 -9
- package/plugins/immune-brain/runtime/claude/run_continuation.ts +274 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +41 -103
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +60 -2
- package/plugins/immune-brain/runtime/kernel/application.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +103 -20
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +57 -7
- package/plugins/immune-brain/runtime/kernel/completion.ts +28 -4
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +48 -8
- package/plugins/immune-brain/runtime/kernel/intent.ts +169 -104
- package/plugins/immune-brain/runtime/kernel/reducer.ts +136 -6
- package/plugins/immune-brain/runtime/kernel/risk_floor.ts +286 -0
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +83 -1
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +1 -1
- package/plugins/immune-brain/runtime/kernel/types.ts +49 -3
- package/plugins/immune-brain/runtime/kernel/validation.ts +147 -7
- package/plugins/immune-brain/runtime/loop_contract.ts +5 -0
- package/plugins/immune-brain/runtime/plan_core.ts +15 -8
- package/plugins/immune-brain/runtime/plugin_source_skew.ts +58 -0
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +26 -2
- package/plugins/immune-brain/runtime/prompts/executor.md +35 -10
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +5 -3
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +40 -10
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +57 -3
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +424 -66
- package/plugins/immune-brain/runtime/unattended/batch_integration.ts +2 -1
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +13 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +43 -24
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +6 -2
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +102 -41
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +9 -3
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +9 -9
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +90 -32
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +102 -4
- package/plugins/immune-brain/runtime/unattended/parent_delivery_consolidation.ts +248 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +126 -18
- package/plugins/immune-brain/skills/BASELINE.md +13 -1
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +5 -0
- package/plugins/immune-brain/skills/imm-run/SKILL.md +12 -8
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
type FindingKind,
|
|
13
13
|
type FindingSource,
|
|
14
14
|
type FindingStatus,
|
|
15
|
+
type ReplanCause,
|
|
16
|
+
type ResidualRiskAcceptanceV1,
|
|
15
17
|
type TaskAction,
|
|
16
18
|
type ReviewAdvisoryFindingV1,
|
|
17
19
|
type TaskApprovalV2,
|
|
@@ -156,7 +158,10 @@ const FINDING_KINDS: FindingKind[] = [
|
|
|
156
158
|
"unresolved_user_decision",
|
|
157
159
|
"replan_required",
|
|
158
160
|
];
|
|
159
|
-
const FINDING_STATUSES: FindingStatus[] = ["open", "resolved", "refuted"];
|
|
161
|
+
const FINDING_STATUSES: FindingStatus[] = ["open", "resolved", "refuted", "accepted_risk"];
|
|
162
|
+
const REPLAN_CAUSES: ReplanCause[] = ["budget_exhausted", "repeated_security_boundary"];
|
|
163
|
+
/** Upper bound of one `authorize_rework` grant. */
|
|
164
|
+
export const MAX_REWORK_GRANT_ROUNDS = 5;
|
|
160
165
|
const FINDING_SOURCES: FindingSource[] = [
|
|
161
166
|
"execution",
|
|
162
167
|
"review",
|
|
@@ -240,6 +245,7 @@ export function parseFinding(
|
|
|
240
245
|
"anchor",
|
|
241
246
|
"evidence",
|
|
242
247
|
"counterevidence",
|
|
248
|
+
"replan_cause",
|
|
243
249
|
],
|
|
244
250
|
path,
|
|
245
251
|
violations,
|
|
@@ -267,6 +273,9 @@ export function parseFinding(
|
|
|
267
273
|
),
|
|
268
274
|
}
|
|
269
275
|
: {}),
|
|
276
|
+
...(item.replan_cause !== undefined
|
|
277
|
+
? { replan_cause: enumAt(item.replan_cause, REPLAN_CAUSES, `${path}.replan_cause`, violations) }
|
|
278
|
+
: {}),
|
|
270
279
|
};
|
|
271
280
|
}
|
|
272
281
|
|
|
@@ -277,7 +286,7 @@ function parseHistoryV3(
|
|
|
277
286
|
): TaskHistoryEntryV3 {
|
|
278
287
|
const path = `record.history[${index}]`;
|
|
279
288
|
const item = objectAt(value, path, violations);
|
|
280
|
-
rejectUnknown(item, ["id", "at", "type", "from_state", "to_state", "reason", "authority"], path, violations);
|
|
289
|
+
rejectUnknown(item, ["id", "at", "type", "from_state", "to_state", "reason", "authority", "review_revision"], path, violations);
|
|
281
290
|
let authority: AuthorityAuditDescriptor | undefined;
|
|
282
291
|
if (item.authority !== undefined) {
|
|
283
292
|
const auth = objectAt(item.authority, `${path}.authority`, violations);
|
|
@@ -292,6 +301,11 @@ function parseHistoryV3(
|
|
|
292
301
|
: {}),
|
|
293
302
|
};
|
|
294
303
|
}
|
|
304
|
+
const reviewRevision = item.review_revision === undefined
|
|
305
|
+
? undefined
|
|
306
|
+
: parseReviewRevisionIdentity(item.review_revision, `${path}.review_revision`, violations);
|
|
307
|
+
if (reviewRevision && authority?.authority_kind !== "review")
|
|
308
|
+
violations.push(`${path}.review_revision is only valid on review-authored history`);
|
|
295
309
|
return {
|
|
296
310
|
id: stringAt(item.id, `${path}.id`, violations),
|
|
297
311
|
at: stringAt(item.at, `${path}.at`, violations),
|
|
@@ -300,6 +314,7 @@ function parseHistoryV3(
|
|
|
300
314
|
to_state: stringAt(item.to_state, `${path}.to_state`, violations),
|
|
301
315
|
reason: stringAt(item.reason, `${path}.reason`, violations),
|
|
302
316
|
...(authority ? { authority } : {}),
|
|
317
|
+
...(reviewRevision ? { review_revision: reviewRevision } : {}),
|
|
303
318
|
};
|
|
304
319
|
}
|
|
305
320
|
|
|
@@ -488,13 +503,46 @@ function parseAttestationV3(
|
|
|
488
503
|
};
|
|
489
504
|
}
|
|
490
505
|
|
|
506
|
+
function parseResidualRiskAcceptance(
|
|
507
|
+
value: unknown,
|
|
508
|
+
path: string,
|
|
509
|
+
violations: string[],
|
|
510
|
+
): ResidualRiskAcceptanceV1 {
|
|
511
|
+
const item = objectAt(value, path, violations);
|
|
512
|
+
rejectUnknown(
|
|
513
|
+
item,
|
|
514
|
+
["intent_revision", "intent_content_hash", "diff_hash", "finding_ids", "reason", "actor_id", "accepted_at"],
|
|
515
|
+
path,
|
|
516
|
+
violations,
|
|
517
|
+
);
|
|
518
|
+
const intentContentHash = stringAt(item.intent_content_hash, `${path}.intent_content_hash`, violations);
|
|
519
|
+
if (!SHA256_HEX.test(intentContentHash)) violations.push(`${path}.intent_content_hash must be sha256:<64 hex>`);
|
|
520
|
+
const diffHash = stringAt(item.diff_hash, `${path}.diff_hash`, violations);
|
|
521
|
+
if (!SHA256_HEX.test(diffHash)) violations.push(`${path}.diff_hash must be sha256:<64 hex>`);
|
|
522
|
+
const reason = stringAt(item.reason, `${path}.reason`, violations);
|
|
523
|
+
if (reason.trim() === "") violations.push(`${path}.reason must be non-empty`);
|
|
524
|
+
const findingIds = arrayAt(item.finding_ids, `${path}.finding_ids`, violations).map((entry, index) =>
|
|
525
|
+
stringAt(entry, `${path}.finding_ids[${index}]`, violations),
|
|
526
|
+
);
|
|
527
|
+
if (findingIds.length === 0) violations.push(`${path}.finding_ids must name at least one finding`);
|
|
528
|
+
return {
|
|
529
|
+
intent_revision: positiveInteger(item.intent_revision, `${path}.intent_revision`, violations),
|
|
530
|
+
intent_content_hash: intentContentHash,
|
|
531
|
+
diff_hash: diffHash,
|
|
532
|
+
finding_ids: findingIds,
|
|
533
|
+
reason,
|
|
534
|
+
actor_id: stringAt(item.actor_id, `${path}.actor_id`, violations),
|
|
535
|
+
accepted_at: stringAt(item.accepted_at, `${path}.accepted_at`, violations),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
491
539
|
export function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskRecordV3 | TaskRecordV4 {
|
|
492
540
|
const violations: string[] = [];
|
|
493
541
|
const value = objectAt(raw, "record", violations);
|
|
494
542
|
rejectUnknown(
|
|
495
543
|
value,
|
|
496
544
|
["contract", "task_id", "intent_snapshot", "intent_ref", "lifecycle", "artifact_state", "baseline", "attestations", "findings", "history",
|
|
497
|
-
...(version === 4 ? ["git_base_head"] : [])],
|
|
545
|
+
...(version === 4 ? ["git_base_head", "review_round_budget_extension", "residual_risk_acceptance"] : [])],
|
|
498
546
|
"record",
|
|
499
547
|
violations,
|
|
500
548
|
);
|
|
@@ -551,6 +599,28 @@ export function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskReco
|
|
|
551
599
|
}
|
|
552
600
|
const findings = arrayAt(value.findings, "record.findings", violations).map((item, index) => parseFinding(item, index, violations));
|
|
553
601
|
const history = arrayAt(value.history, "record.history", violations).map((item, index) => parseHistoryV3(item, index, violations));
|
|
602
|
+
for (const [index, finding] of findings.entries()) {
|
|
603
|
+
const path = `record.findings[${index}]`;
|
|
604
|
+
if (finding.replan_cause !== undefined && finding.kind !== "replan_required")
|
|
605
|
+
violations.push(`${path}.replan_cause is only valid on a replan_required finding`);
|
|
606
|
+
if (finding.status === "accepted_risk" && (finding.kind !== "blocking" || finding.source !== "review"))
|
|
607
|
+
violations.push(`${path} accepted_risk requires a blocking Review finding`);
|
|
608
|
+
}
|
|
609
|
+
let budgetExtension: number | undefined;
|
|
610
|
+
if (value.review_round_budget_extension !== undefined) {
|
|
611
|
+
budgetExtension = positiveInteger(value.review_round_budget_extension, "record.review_round_budget_extension", violations);
|
|
612
|
+
}
|
|
613
|
+
const residualRiskAcceptance = value.residual_risk_acceptance === undefined
|
|
614
|
+
? undefined
|
|
615
|
+
: parseResidualRiskAcceptance(value.residual_risk_acceptance, "record.residual_risk_acceptance", violations);
|
|
616
|
+
if (findings.some((finding) => finding.status === "accepted_risk") && residualRiskAcceptance === undefined)
|
|
617
|
+
violations.push("record.residual_risk_acceptance is required while a finding is accepted_risk");
|
|
618
|
+
if (residualRiskAcceptance) {
|
|
619
|
+
for (const id of residualRiskAcceptance.finding_ids) {
|
|
620
|
+
if (!findings.some((finding) => finding.id === id && finding.status === "accepted_risk"))
|
|
621
|
+
violations.push(`record.residual_risk_acceptance.finding_ids names ${id}, which is not accepted_risk`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
554
624
|
uniqueIds(attestations, "record.attestations", violations);
|
|
555
625
|
uniqueIds(findings, "record.findings", violations);
|
|
556
626
|
uniqueIds(history, "record.history", violations);
|
|
@@ -613,6 +683,8 @@ export function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskReco
|
|
|
613
683
|
artifact_state: artifactState as TaskRecordV3["artifact_state"],
|
|
614
684
|
baseline,
|
|
615
685
|
...(version === 4 ? { git_base_head: gitBaseHead } : {}),
|
|
686
|
+
...(version === 4 && budgetExtension !== undefined ? { review_round_budget_extension: budgetExtension } : {}),
|
|
687
|
+
...(version === 4 && residualRiskAcceptance !== undefined ? { residual_risk_acceptance: residualRiskAcceptance } : {}),
|
|
616
688
|
attestations,
|
|
617
689
|
findings,
|
|
618
690
|
history,
|
|
@@ -664,6 +736,7 @@ const ACTION_V2_TYPES = [
|
|
|
664
736
|
"complete",
|
|
665
737
|
"stop",
|
|
666
738
|
"authorize_rework",
|
|
739
|
+
"accept_residual_risk",
|
|
667
740
|
"resolve_user_decision",
|
|
668
741
|
] as const;
|
|
669
742
|
|
|
@@ -849,16 +922,33 @@ export function parseTaskAction(raw: unknown): TaskAction {
|
|
|
849
922
|
};
|
|
850
923
|
break;
|
|
851
924
|
}
|
|
852
|
-
case "complete":
|
|
853
|
-
case "authorize_rework": {
|
|
925
|
+
case "complete": {
|
|
854
926
|
rejectUnknown(value, [...ACTION_BASE_FIELDS], "action", violations);
|
|
855
927
|
action = { ...base, type: base.type };
|
|
856
928
|
break;
|
|
857
929
|
}
|
|
930
|
+
case "authorize_rework": {
|
|
931
|
+
rejectUnknown(value, [...ACTION_BASE_FIELDS, "rounds"], "action", violations);
|
|
932
|
+
let rounds: number | undefined;
|
|
933
|
+
if (value.rounds !== undefined) {
|
|
934
|
+
rounds = positiveInteger(value.rounds, "action.rounds", violations);
|
|
935
|
+
if (rounds > MAX_REWORK_GRANT_ROUNDS)
|
|
936
|
+
violations.push(`action.rounds must be an integer from 1 to ${MAX_REWORK_GRANT_ROUNDS}`);
|
|
937
|
+
}
|
|
938
|
+
action = { ...base, type: "authorize_rework", ...(rounds !== undefined ? { rounds } : {}) };
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
case "accept_residual_risk": {
|
|
942
|
+
rejectUnknown(value, [...ACTION_BASE_FIELDS, "reason"], "action", violations);
|
|
943
|
+
const reason = stringAt(value.reason, "action.reason", violations);
|
|
944
|
+
if (reason.trim() === "") violations.push("accept_residual_risk requires a non-empty reason");
|
|
945
|
+
action = { ...base, type: "accept_residual_risk", reason };
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
858
948
|
case "request_rework": {
|
|
859
949
|
rejectUnknown(
|
|
860
950
|
value,
|
|
861
|
-
[...ACTION_BASE_FIELDS, "findings"],
|
|
951
|
+
[...ACTION_BASE_FIELDS, "findings", "review_revision"],
|
|
862
952
|
"action",
|
|
863
953
|
violations,
|
|
864
954
|
);
|
|
@@ -869,7 +959,15 @@ export function parseTaskAction(raw: unknown): TaskAction {
|
|
|
869
959
|
).map((item, index) => parseFinding(item, index, violations));
|
|
870
960
|
if (findings.length === 0)
|
|
871
961
|
violations.push("action.findings must contain at least one finding");
|
|
872
|
-
|
|
962
|
+
const reviewRevision = value.review_revision === undefined
|
|
963
|
+
? undefined
|
|
964
|
+
: parseReviewRevisionIdentity(value.review_revision, "action.review_revision", violations);
|
|
965
|
+
action = {
|
|
966
|
+
...base,
|
|
967
|
+
type: "request_rework",
|
|
968
|
+
findings,
|
|
969
|
+
...(reviewRevision ? { review_revision: reviewRevision } : {}),
|
|
970
|
+
};
|
|
873
971
|
break;
|
|
874
972
|
}
|
|
875
973
|
case "stop": {
|
|
@@ -961,6 +1059,37 @@ export function assertTaskRecordUpdateV3(
|
|
|
961
1059
|
violations.push("only artifact transitions may change intent_ref path");
|
|
962
1060
|
}
|
|
963
1061
|
|
|
1062
|
+
// The budget extension grows only by an authorize_rework grant, and the
|
|
1063
|
+
// residual-risk acceptance changes only by accept_residual_risk.
|
|
1064
|
+
const previousExtension = previous.review_round_budget_extension ?? 0;
|
|
1065
|
+
const expectedExtension = previousExtension +
|
|
1066
|
+
(action.type === "authorize_rework" ? (action.rounds ?? 1) : 0);
|
|
1067
|
+
if ((next.review_round_budget_extension ?? 0) !== expectedExtension)
|
|
1068
|
+
violations.push("review_round_budget_extension changes only by an authorize_rework grant");
|
|
1069
|
+
if (action.type === "accept_residual_risk") {
|
|
1070
|
+
const acceptance = next.residual_risk_acceptance;
|
|
1071
|
+
const newlyAccepted = next.findings
|
|
1072
|
+
.filter((item) => item.status === "accepted_risk" &&
|
|
1073
|
+
previous.findings.find((prior) => prior.id === item.id)?.status !== "accepted_risk")
|
|
1074
|
+
.map((item) => item.id)
|
|
1075
|
+
.sort();
|
|
1076
|
+
if (!acceptance)
|
|
1077
|
+
violations.push("accept_residual_risk must record residual_risk_acceptance");
|
|
1078
|
+
else if (
|
|
1079
|
+
acceptance.diff_hash !== action.diff_hash ||
|
|
1080
|
+
acceptance.intent_revision !== next.intent_snapshot.revision ||
|
|
1081
|
+
acceptance.intent_content_hash !== next.intent_ref.content_hash ||
|
|
1082
|
+
acceptance.reason !== action.reason ||
|
|
1083
|
+
acceptance.actor_id !== action.actor_id ||
|
|
1084
|
+
JSON.stringify([...acceptance.finding_ids].sort()) !== JSON.stringify(newlyAccepted)
|
|
1085
|
+
)
|
|
1086
|
+
violations.push("residual_risk_acceptance must bind the accepting action and the findings it accepted");
|
|
1087
|
+
} else if (
|
|
1088
|
+
JSON.stringify(next.residual_risk_acceptance ?? null) !==
|
|
1089
|
+
JSON.stringify(previous.residual_risk_acceptance ?? null)
|
|
1090
|
+
)
|
|
1091
|
+
violations.push("residual_risk_acceptance changes only by accept_residual_risk");
|
|
1092
|
+
|
|
964
1093
|
// Attestations and history are append-only.
|
|
965
1094
|
if (next.attestations.length < previous.attestations.length)
|
|
966
1095
|
violations.push("attestations are append-only");
|
|
@@ -978,6 +1107,7 @@ export function assertTaskRecordUpdateV3(
|
|
|
978
1107
|
: action.type === "resolve_user_decision"
|
|
979
1108
|
? [action.finding_id]
|
|
980
1109
|
: action.type === "authorize_rework" ||
|
|
1110
|
+
action.type === "accept_residual_risk" ||
|
|
981
1111
|
action.type === "approve_breaking_intent_revision"
|
|
982
1112
|
? previous.findings
|
|
983
1113
|
.filter((item) => item.kind === "replan_required" && item.status === "open")
|
|
@@ -1013,6 +1143,16 @@ export function assertTaskRecordUpdateV3(
|
|
|
1013
1143
|
)) &&
|
|
1014
1144
|
withoutFields(prior, ["status"]) === withoutFields(current, ["status"])
|
|
1015
1145
|
) &&
|
|
1146
|
+
!(
|
|
1147
|
+
// accept_residual_risk moves an open blocking Review finding to
|
|
1148
|
+
// accepted_risk and changes nothing else about it.
|
|
1149
|
+
action.type === "accept_residual_risk" &&
|
|
1150
|
+
prior.kind === "blocking" &&
|
|
1151
|
+
prior.source === "review" &&
|
|
1152
|
+
prior.status === "open" &&
|
|
1153
|
+
current.status === "accepted_risk" &&
|
|
1154
|
+
withoutFields(prior, ["status"]) === withoutFields(current, ["status"])
|
|
1155
|
+
) &&
|
|
1016
1156
|
!(
|
|
1017
1157
|
action.type === "refute_finding" &&
|
|
1018
1158
|
prior.id === action.finding_id &&
|
|
@@ -56,6 +56,10 @@ export interface LoopRoute {
|
|
|
56
56
|
export function resolveLoopRoute(input: {
|
|
57
57
|
ownership: LoopRouteOwnership;
|
|
58
58
|
target: LoopRouteTarget;
|
|
59
|
+
/**
|
|
60
|
+
* A scope change that is not a same-directory file addition. The owner adds a
|
|
61
|
+
* sibling file itself with `revise_intent` (ADR 0021) and stays in the Loop.
|
|
62
|
+
*/
|
|
59
63
|
scope_expansion?: boolean;
|
|
60
64
|
}): LoopRoute {
|
|
61
65
|
if (input.scope_expansion) return { entry: "imm-run", next: "imm-planner" };
|
|
@@ -189,6 +193,7 @@ export function buildLoopAction(input: {
|
|
|
189
193
|
ownership: LoopRouteOwnership;
|
|
190
194
|
target: LoopRouteTarget;
|
|
191
195
|
context?: RoleDelegationContext;
|
|
196
|
+
/** See `resolveLoopRoute`: a same-directory file addition is not one. */
|
|
192
197
|
scope_expansion?: boolean;
|
|
193
198
|
kernel_operation?: Extract<LoopAction, { next: "imm_kernel_canary" }>["tool"]["operation"];
|
|
194
199
|
}): LoopAction {
|
|
@@ -665,7 +665,7 @@ function validateSpecDesign(spec: ParsedSpecDesign | null): {
|
|
|
665
665
|
warnings: [
|
|
666
666
|
{
|
|
667
667
|
code: "spec_design_metadata_missing",
|
|
668
|
-
message: `${spec.path} uses the legacy design contract; add Design risk
|
|
668
|
+
message: `${spec.path} uses the legacy design contract; add Design risk (and Diagram decision with Diagram reason only when a diagram is required) when revising it.`,
|
|
669
669
|
field: "design_metadata",
|
|
670
670
|
},
|
|
671
671
|
],
|
|
@@ -679,13 +679,20 @@ function validateSpecDesign(spec: ParsedSpecDesign | null): {
|
|
|
679
679
|
if (!risk || !["low", "medium", "high"].includes(risk)) {
|
|
680
680
|
errors.push("Referenced Spec Design risk must be Low, Medium, or High.");
|
|
681
681
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (
|
|
688
|
-
|
|
682
|
+
// A Spec that needs no diagram records neither diagram field; once either
|
|
683
|
+
// field is present the pair must be complete.
|
|
684
|
+
const diagramFieldsRecorded = Boolean(
|
|
685
|
+
spec.diagram_decision || spec.diagram_reason,
|
|
686
|
+
);
|
|
687
|
+
if (diagramFieldsRecorded) {
|
|
688
|
+
if (!decision || !["required", "not_required"].includes(decision)) {
|
|
689
|
+
errors.push(
|
|
690
|
+
"Referenced Spec Diagram decision must be required or not_required.",
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
if (!spec.diagram_reason) {
|
|
694
|
+
errors.push("Referenced Spec is missing Diagram reason.");
|
|
695
|
+
}
|
|
689
696
|
}
|
|
690
697
|
if (["medium", "high"].includes(risk || "") && !spec.has_technical_design) {
|
|
691
698
|
errors.push(
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Read-only observation for working on Immune-Brain with Immune-Brain: when
|
|
2
|
+
// the workspace is this plugin's own source repository but the Host loaded the
|
|
3
|
+
// plugin from somewhere else (an installed package or cache), tool behavior
|
|
4
|
+
// follows the loaded build, not the workspace changes. The plugin version does
|
|
5
|
+
// not move during development, so the generated Claude bundle stands in for
|
|
6
|
+
// the build: every runtime change regenerates it under the build parity check.
|
|
7
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { runningRuntimeSource } from "./kernel/runtime_contracts";
|
|
10
|
+
|
|
11
|
+
const WORKSPACE_PLUGIN_DIR = join("plugins", "immune-brain");
|
|
12
|
+
const PLUGIN_MANIFEST = join(".claude-plugin", "plugin.json");
|
|
13
|
+
const BUILD_PROXY = join("dist", "claude", "mcp-server.mjs");
|
|
14
|
+
const MAX_LEVELS = 8;
|
|
15
|
+
|
|
16
|
+
export interface PluginSourceSkewWarning {
|
|
17
|
+
code: "loaded_plugin_differs_from_workspace";
|
|
18
|
+
paths: string[];
|
|
19
|
+
message: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function pluginRootAbove(dir: string): string | null {
|
|
23
|
+
let current = dir;
|
|
24
|
+
for (let level = 0; level < MAX_LEVELS; level++) {
|
|
25
|
+
if (existsSync(join(current, PLUGIN_MANIFEST))) return current;
|
|
26
|
+
const parent = dirname(current);
|
|
27
|
+
if (parent === current) return null;
|
|
28
|
+
current = parent;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sameBuild(a: string, b: string): boolean {
|
|
34
|
+
const left = join(a, BUILD_PROXY), right = join(b, BUILD_PROXY);
|
|
35
|
+
if (!existsSync(left) || !existsSync(right)) return false;
|
|
36
|
+
if (statSync(left).size !== statSync(right).size) return false;
|
|
37
|
+
return readFileSync(left).equals(readFileSync(right));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Null unless `root` is the plugin source repository and the loaded plugin is a different, differing build. */
|
|
41
|
+
export function pluginSourceSkewWarning(root: string, loadedFrom: string = runningRuntimeSource()): PluginSourceSkewWarning | null {
|
|
42
|
+
try {
|
|
43
|
+
const workspace = join(root, WORKSPACE_PLUGIN_DIR);
|
|
44
|
+
if (!existsSync(join(workspace, PLUGIN_MANIFEST))) return null;
|
|
45
|
+
const loaded = pluginRootAbove(loadedFrom);
|
|
46
|
+
if (!loaded) return null;
|
|
47
|
+
const workspaceReal = realpathSync(workspace), loadedReal = realpathSync(loaded);
|
|
48
|
+
if (workspaceReal === loadedReal || sameBuild(loadedReal, workspaceReal)) return null;
|
|
49
|
+
return {
|
|
50
|
+
code: "loaded_plugin_differs_from_workspace",
|
|
51
|
+
paths: [loadedReal, workspaceReal],
|
|
52
|
+
message: `this Host runs Immune-Brain from ${loadedReal}, whose build differs from the workspace plugin ${workspaceReal}; tools follow the loaded build, so reload the Host with the workspace plugin before relying on workspace runtime changes`,
|
|
53
|
+
};
|
|
54
|
+
} catch {
|
|
55
|
+
// An observation never blocks status.
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/plugin_versioning.ts from the root package.json.
|
|
2
|
-
export const PLUGIN_VERSION = "5.
|
|
2
|
+
export const PLUGIN_VERSION = "5.5.1" as const;
|
|
@@ -46,6 +46,27 @@ cite the acceptance whose principle it violates as `acceptance_id` and
|
|
|
46
46
|
to be written into the acceptance text; a missing literal clause is not itself a
|
|
47
47
|
finding and does not call for an Intent revision.
|
|
48
48
|
|
|
49
|
+
## Trigger and Threat Model
|
|
50
|
+
|
|
51
|
+
`evidence.trigger` must read "concrete input or state → observable wrong
|
|
52
|
+
result". A finding that cannot name the wrong result is not blocking. Judge
|
|
53
|
+
against the threat model and supported environment declared in the Spec or
|
|
54
|
+
TaskIntent goal: a defect that needs an attacker, input source, repository
|
|
55
|
+
format, encoding, or platform outside that declaration is advisory. When
|
|
56
|
+
nothing is declared, assume inputs come from the repository's own tooling and
|
|
57
|
+
the user's trusted workspace. `security_boundary` means a security, data-loss,
|
|
58
|
+
or authority boundary the change actually crosses.
|
|
59
|
+
|
|
60
|
+
From Review round 3 the manifest carries `prior_review_commit` and
|
|
61
|
+
`delta_paths`: the paths the latest rework changed. Review that delta and its
|
|
62
|
+
direct interactions. A blocking finding is admitted only when at least one
|
|
63
|
+
`caller_chain` entry names a path in `delta_paths`, or `violated.kind` is
|
|
64
|
+
`security_boundary`; an edge case that already existed in round 1 is advisory
|
|
65
|
+
unless it crosses a security, data-loss, or authority boundary. A verdict that
|
|
66
|
+
blocks outside the delta is rejected for resubmission. When the manifest carries
|
|
67
|
+
`delta_skipped` instead, no delta exists for the round and this rule does not
|
|
68
|
+
apply.
|
|
69
|
+
|
|
49
70
|
## Invariant and Evidence Coverage
|
|
50
71
|
|
|
51
72
|
Judge whether the change actually closes the invariant its acceptance names,
|
|
@@ -95,7 +116,7 @@ contract: `contract`, `role`, `task_id`, `snapshot_digest`, `decision` (`pass`
|
|
|
95
116
|
or `rework`), and for `pass` include `approval` (`kind`, `authority_role`,
|
|
96
117
|
`summary`), for `rework` include `findings` (`id`, `kind`, `acceptance_id`,
|
|
97
118
|
`summary`, `evidence`). Every rework finding's `evidence` must carry
|
|
98
|
-
`trigger` (the concrete
|
|
119
|
+
`trigger` (the concrete input or state that reaches the defect and the observable wrong result it produces), a non-empty
|
|
99
120
|
`caller_chain` (ordered repository paths or symbols), and `violated`
|
|
100
121
|
(`kind`: `acceptance` or `security_boundary`, `ref`: the acceptance id or
|
|
101
122
|
boundary name). Do not invent fields, and never send an anchor yourself: the
|
|
@@ -104,6 +125,9 @@ violated.ref, caller_chain}`, so an identical claim keeps one stable identity
|
|
|
104
125
|
across review rounds while a different call chain is a different claim. A
|
|
105
126
|
passing review carries no blocking findings; non-blocking notes may ride along as
|
|
106
127
|
`kind: "advisory"` findings, and every finding, advisory included, needs the
|
|
107
|
-
same machine-checkable evidence.
|
|
128
|
+
same machine-checkable evidence. Advisory notes are archived on a pass and never
|
|
129
|
+
become Executor work. A rework carries only `kind: "blocking"` findings, at
|
|
130
|
+
least one: the coordinator rejects a rework with an advisory finding or without
|
|
131
|
+
a blocking one, so submit `pass` with `inspected_paths` when nothing blocks. If the
|
|
108
132
|
checkpoint is `awaiting_user_successor_decision`, stop without dispatch; only
|
|
109
133
|
a literal user may invoke `--approve-successor`.
|
|
@@ -10,23 +10,41 @@ On a batch `handoff.role=executor`, implement only its named enrolled child;
|
|
|
10
10
|
the batch stays `running` with that child `enrolled`. Return to the Parent after
|
|
11
11
|
scoped implementation, staging and focused diagnostics. Enrollment itself does
|
|
12
12
|
not start QA; the Parent owns the subsequent Kernel Assurance and foreground
|
|
13
|
-
Review handoff under the same still-valid batch authorization.
|
|
13
|
+
Review handoff under the same still-valid batch authorization. A child settlement
|
|
14
|
+
returns execution to the bound Parent; continue dependency-ready independent
|
|
15
|
+
work until whole-plan integration and final verification pass. Critical and new
|
|
16
|
+
repair tasks use their own native Enrollment; stopped or excluded work never
|
|
17
|
+
counts as Parent success.
|
|
14
18
|
|
|
15
|
-
Before handoff, run the permitted diagnostic checks
|
|
16
|
-
|
|
17
|
-
evidence. The read-only Loop
|
|
19
|
+
Before handoff, run the permitted diagnostic checks and stage the task-owned
|
|
20
|
+
changes. On rework, also complete the Delivery Evidence check below and return
|
|
21
|
+
its table to the Parent as structured diagnostic evidence. The read-only Loop
|
|
18
22
|
runtime action only constructs the dispatch envelope; it does not store
|
|
19
23
|
evidence or change task state. Preserve failed and blocked attempts. Do not
|
|
20
24
|
perform QA,
|
|
21
25
|
review, plan mutation, successor approval, Compounder work, or authority
|
|
22
|
-
writes.
|
|
23
|
-
|
|
26
|
+
writes. A sibling file in a directory the scope already names a file in is a
|
|
27
|
+
same-directory addition: submit `revise_intent` with the complete next intent
|
|
28
|
+
and continue, without a Planner route or a native gate. If the requested change
|
|
29
|
+
needs scope expansion beyond that (a glob, a new directory, a removed entry, a
|
|
30
|
+
path the project's risk floor names in `.imm/risk-floor.json`), stop and return an `imm-planner` route
|
|
31
|
+
with the concrete missing scope and verification reason.
|
|
24
32
|
|
|
25
33
|
## Delivery Evidence
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
Prefer direct checks. For truncated display, use an explicit shell with
|
|
36
|
+
`pipefail`, preserve the check's real exit status and a task-owned full log;
|
|
37
|
+
display success is not check success. Keep global shell settings unchanged.
|
|
38
|
+
When the Spec or user requests test-first, observe a business-assertion red
|
|
39
|
+
before implementation, then the corresponding green at the same seam; missing
|
|
40
|
+
modules, syntax errors or harness failures are not that red. A known omitted
|
|
41
|
+
generated output blocks handoff: fix it within scope or use the existing Intent
|
|
42
|
+
revision path before delivery.
|
|
43
|
+
|
|
44
|
+
Reuse the TaskIntent and any bound Spec's agreed seams and controls. Return one
|
|
45
|
+
compact row per violated acceptance or invariant only when handling rework; a
|
|
46
|
+
first delivery needs no table. This is handoff evidence, not a new artifact or
|
|
47
|
+
approval gate.
|
|
30
48
|
|
|
31
49
|
| Acceptance / invariant | Entry and consumers | Positive / negative / boundary controls | Command, outcome and input | Finding coverage |
|
|
32
50
|
|---|---|---|---|---|
|
|
@@ -37,6 +55,13 @@ both affected hosts and recovery/replay branches. An async, return-type or
|
|
|
37
55
|
exception-contract change requires checking every affected caller's await,
|
|
38
56
|
cancellation and error propagation.
|
|
39
57
|
|
|
58
|
+
Before Assurance, run the existing tests that call a changed entry or assert
|
|
59
|
+
the changed contract, found by searching the tests for its exported names,
|
|
60
|
+
including tests outside `scope_hint`. Focused acceptance descriptors do not run
|
|
61
|
+
them, so a stale caller test otherwise surfaces only at final verification. A
|
|
62
|
+
failing caller test outside the scope is a scope gap: add it through the Intent
|
|
63
|
+
revision path before delivery, never leave it for final verification.
|
|
64
|
+
|
|
40
65
|
Use the real producer for the success control where available. Derive rejection
|
|
41
66
|
controls from a successful state, change only the target condition, and assert
|
|
42
67
|
the intended rejection reason and required side effects. A control must fail
|
|
@@ -75,7 +100,7 @@ internal return values that a public assertion already covers. Before adding a
|
|
|
75
100
|
test, search existing tests for the same behavior and extend or parameterize
|
|
76
101
|
them instead of duplicating setup. When the change retires or replaces a
|
|
77
102
|
behavior, update or delete the tests that protected it within scope and name
|
|
78
|
-
the retired behavior or continuing owner in
|
|
103
|
+
the retired behavior or continuing owner in the handoff.
|
|
79
104
|
Treat naming, function length, parameter count, nesting, and abstraction taste
|
|
80
105
|
as contextual signals, never as automatic failure thresholds.
|
|
81
106
|
|
|
@@ -13,6 +13,7 @@ import { projectAssurance } from "../kernel/assurance_projection";
|
|
|
13
13
|
import { createMutationAuthorityRegistry, digestOfAction, type CapabilityBindingV2 } from "../kernel/authority_port";
|
|
14
14
|
import { capabilityActionFor, createCanaryApplication } from "../kernel/canary_application";
|
|
15
15
|
import { canonicalIntentHash, classifyIntentRevision, parseTaskIntentV1, readTaskIntent } from "../kernel/intent";
|
|
16
|
+
import type { RiskFloor } from "../kernel/risk_floor";
|
|
16
17
|
import { boundSpecPath } from "../kernel/spec_binding";
|
|
17
18
|
import { readTaskRecordRaw, recoverKernelStoreFollowUps } from "../kernel/storage";
|
|
18
19
|
import type { TaskIntentV1 } from "../kernel/types";
|
|
@@ -69,8 +70,8 @@ export function parseLaneRevision(value: unknown): LaneRevisionRequest | undefin
|
|
|
69
70
|
* TaskIntent as the batch authorized it; `current` is the Lane's enrolled one.
|
|
70
71
|
* Null means covered.
|
|
71
72
|
*/
|
|
72
|
-
export function delegatedRevisionRefusal(authorized: TaskIntentV1, current: TaskIntentV1, next: TaskIntentV1): string | null {
|
|
73
|
-
if (classifyIntentRevision(current, next) !== "breaking") return "only a breaking revision needs this approval; apply a compatible one with revise_intent";
|
|
73
|
+
export function delegatedRevisionRefusal(authorized: TaskIntentV1, current: TaskIntentV1, next: TaskIntentV1, floor: RiskFloor): string | null {
|
|
74
|
+
if (classifyIntentRevision(current, next, floor) !== "breaking") return "only a breaking revision needs this approval; apply a compatible one with revise_intent";
|
|
74
75
|
if (next.revision <= current.revision) return "the revision number must increase";
|
|
75
76
|
if (next.goal !== authorized.goal || next.owner !== authorized.owner || next.task_id !== authorized.task_id)
|
|
76
77
|
return "goal, owner and task identity are outside the delegation";
|
|
@@ -99,7 +100,8 @@ export function applyDelegatedIntentRevision(input: {
|
|
|
99
100
|
return (async () => {
|
|
100
101
|
const { lane_root: root, task_id: taskId, next_intent: nextIntent, now } = input;
|
|
101
102
|
recoverKernelStoreFollowUps(root, taskId);
|
|
102
|
-
const
|
|
103
|
+
const record = readTaskRecordRaw(root, taskId).record;
|
|
104
|
+
const prior = readTaskIntent(root, taskId, record?.intent_ref.path, record?.intent_snapshot);
|
|
103
105
|
const before = await projectAssurance(root, taskId, taskDeliveryIdentity);
|
|
104
106
|
if (before.error || before.claim?.task_id !== taskId || before.projection.lifecycle !== "active")
|
|
105
107
|
throw new Error(`the Lane does not hold an active run of ${taskId}`);
|