immune-brain 3.6.6 → 3.6.7
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 +24 -3
- package/README.zh-CN.md +23 -3
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +29 -19
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +932 -0
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +200 -2
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +3404 -227
- package/plugins/immune-brain/dist/imm-loop.md +22 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +9 -1
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +101 -3
- package/plugins/immune-brain/runtime/claude/interaction.ts +16 -3
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +840 -17
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +72 -10
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +9 -0
- package/plugins/immune-brain/runtime/kernel/completion.ts +19 -1
- package/plugins/immune-brain/runtime/kernel/reducer.ts +162 -11
- package/plugins/immune-brain/runtime/kernel/refutation.ts +82 -0
- package/plugins/immune-brain/runtime/kernel/types.ts +34 -1
- package/plugins/immune-brain/runtime/kernel/validation.ts +202 -9
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +9 -1
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +775 -0
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +203 -0
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +1224 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +360 -0
- package/plugins/immune-brain/runtime/unattended/types.ts +60 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +1 -0
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
type ApprovalAuthorityRole,
|
|
10
10
|
type ApprovalKind,
|
|
11
11
|
type EvidenceStatus,
|
|
12
|
+
type FindingCounterevidence,
|
|
13
|
+
type FindingEvidence,
|
|
12
14
|
type FindingKind,
|
|
13
15
|
type FindingSource,
|
|
14
16
|
type FindingStatus,
|
|
@@ -28,6 +30,7 @@ import {
|
|
|
28
30
|
type TaskRecordV4,
|
|
29
31
|
} from "./types";
|
|
30
32
|
import { canonicalIntentHash, parseTaskIntentV1 } from "./intent";
|
|
33
|
+
import { refutationIdentity, refutationIsLive, anchorForEvidence, isFreshPassingQaAttestation } from "./refutation";
|
|
31
34
|
|
|
32
35
|
const GIT_OBJECT_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
33
36
|
|
|
@@ -157,7 +160,7 @@ const FINDING_KINDS: FindingKind[] = [
|
|
|
157
160
|
"unresolved_user_decision",
|
|
158
161
|
"replan_required",
|
|
159
162
|
];
|
|
160
|
-
const FINDING_STATUSES: FindingStatus[] = ["open", "resolved"];
|
|
163
|
+
const FINDING_STATUSES: FindingStatus[] = ["open", "resolved", "refuted"];
|
|
161
164
|
const FINDING_SOURCES: FindingSource[] = [
|
|
162
165
|
"execution",
|
|
163
166
|
"review",
|
|
@@ -171,6 +174,56 @@ const APPROVAL_AUTHORITY_ROLES: ApprovalAuthorityRole[] = [
|
|
|
171
174
|
"user",
|
|
172
175
|
];
|
|
173
176
|
|
|
177
|
+
function nullableAnchor(
|
|
178
|
+
value: unknown,
|
|
179
|
+
path: string,
|
|
180
|
+
violations: string[],
|
|
181
|
+
): string | null {
|
|
182
|
+
if (value === null || value === undefined) return null;
|
|
183
|
+
return stringAt(value, path, violations);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseFindingEvidence(
|
|
187
|
+
value: unknown,
|
|
188
|
+
path: string,
|
|
189
|
+
violations: string[],
|
|
190
|
+
): FindingEvidence | null {
|
|
191
|
+
if (value === null || value === undefined) return null;
|
|
192
|
+
const item = objectAt(value, path, violations);
|
|
193
|
+
rejectUnknown(item, ["trigger", "caller_chain", "violated"], path, violations);
|
|
194
|
+
const trigger = stringAt(item.trigger, `${path}.trigger`, violations);
|
|
195
|
+
const chain = arrayAt(item.caller_chain, `${path}.caller_chain`, violations);
|
|
196
|
+
if (chain.length === 0)
|
|
197
|
+
violations.push(`${path}.caller_chain must contain at least one entry`);
|
|
198
|
+
const caller_chain = chain.map((entry, index) =>
|
|
199
|
+
stringAt(entry, `${path}.caller_chain[${index}]`, violations),
|
|
200
|
+
);
|
|
201
|
+
const violatedRaw = objectAt(item.violated, `${path}.violated`, violations);
|
|
202
|
+
rejectUnknown(violatedRaw, ["kind", "ref"], `${path}.violated`, violations);
|
|
203
|
+
const kind = enumAt(
|
|
204
|
+
violatedRaw.kind,
|
|
205
|
+
["acceptance", "security_boundary"] as const,
|
|
206
|
+
`${path}.violated.kind`,
|
|
207
|
+
violations,
|
|
208
|
+
);
|
|
209
|
+
const ref = stringAt(violatedRaw.ref, `${path}.violated.ref`, violations);
|
|
210
|
+
return { trigger, caller_chain, violated: { kind, ref } };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function parseFindingCounterevidence(
|
|
214
|
+
value: unknown,
|
|
215
|
+
path: string,
|
|
216
|
+
violations: string[],
|
|
217
|
+
): FindingCounterevidence | null {
|
|
218
|
+
if (value === null || value === undefined) return null;
|
|
219
|
+
const item = objectAt(value, path, violations);
|
|
220
|
+
rejectUnknown(item, ["attestation_id", "acceptance_id"], path, violations);
|
|
221
|
+
return {
|
|
222
|
+
attestation_id: stringAt(item.attestation_id, `${path}.attestation_id`, violations),
|
|
223
|
+
acceptance_id: stringAt(item.acceptance_id, `${path}.acceptance_id`, violations),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
174
227
|
function parseFinding(
|
|
175
228
|
value: unknown,
|
|
176
229
|
index: number,
|
|
@@ -180,7 +233,18 @@ function parseFinding(
|
|
|
180
233
|
const item = objectAt(value, path, violations);
|
|
181
234
|
rejectUnknown(
|
|
182
235
|
item,
|
|
183
|
-
[
|
|
236
|
+
[
|
|
237
|
+
"id",
|
|
238
|
+
"kind",
|
|
239
|
+
"status",
|
|
240
|
+
"acceptance_id",
|
|
241
|
+
"source",
|
|
242
|
+
"review_round",
|
|
243
|
+
"summary",
|
|
244
|
+
"anchor",
|
|
245
|
+
"evidence",
|
|
246
|
+
"counterevidence",
|
|
247
|
+
],
|
|
184
248
|
path,
|
|
185
249
|
violations,
|
|
186
250
|
);
|
|
@@ -192,6 +256,21 @@ function parseFinding(
|
|
|
192
256
|
source: enumAt(item.source, FINDING_SOURCES, `${path}.source`, violations),
|
|
193
257
|
review_round: nullablePositiveInteger(item.review_round, `${path}.review_round`, violations),
|
|
194
258
|
summary: stringAt(item.summary, `${path}.summary`, violations),
|
|
259
|
+
...(item.anchor !== undefined
|
|
260
|
+
? { anchor: nullableAnchor(item.anchor, `${path}.anchor`, violations) }
|
|
261
|
+
: {}),
|
|
262
|
+
...(item.evidence !== undefined
|
|
263
|
+
? { evidence: parseFindingEvidence(item.evidence, `${path}.evidence`, violations) }
|
|
264
|
+
: {}),
|
|
265
|
+
...(item.counterevidence !== undefined
|
|
266
|
+
? {
|
|
267
|
+
counterevidence: parseFindingCounterevidence(
|
|
268
|
+
item.counterevidence,
|
|
269
|
+
`${path}.counterevidence`,
|
|
270
|
+
violations,
|
|
271
|
+
),
|
|
272
|
+
}
|
|
273
|
+
: {}),
|
|
195
274
|
};
|
|
196
275
|
}
|
|
197
276
|
|
|
@@ -645,6 +724,55 @@ function parseTaskRecordAtVersion(raw: unknown, version: 3 | 4): TaskRecordV3 |
|
|
|
645
724
|
uniqueIds(attestations, "record.attestations", violations);
|
|
646
725
|
uniqueIds(findings, "record.findings", violations);
|
|
647
726
|
uniqueIds(history, "record.history", violations);
|
|
727
|
+
// A persisted refutation is authority: it suppresses a blocking finding
|
|
728
|
+
// until its bound evidence goes stale. The record must therefore prove the
|
|
729
|
+
// binding is one the Kernel could have written, or a malformed record could
|
|
730
|
+
// suppress a finding with evidence for a different acceptance.
|
|
731
|
+
for (const [index, finding] of findings.entries()) {
|
|
732
|
+
const path = `record.findings[${index}]`;
|
|
733
|
+
if (finding.anchor != null && !SHA256_HEX.test(finding.anchor))
|
|
734
|
+
violations.push(`${path}.anchor must be sha256:<64 hex>`);
|
|
735
|
+
const anchor = finding.anchor ?? null;
|
|
736
|
+
const evidence = finding.evidence ?? null;
|
|
737
|
+
if ((anchor === null) !== (evidence === null))
|
|
738
|
+
violations.push(`${path} must carry anchor and evidence together`);
|
|
739
|
+
if (anchor !== null && evidence !== null && anchor !== anchorForEvidence(evidence))
|
|
740
|
+
violations.push(`${path}.anchor must equal the digest of its evidence`);
|
|
741
|
+
// A refuted user decision or replan boundary would hide the very gate
|
|
742
|
+
// those findings exist to hold; only review claims may be refuted.
|
|
743
|
+
if (
|
|
744
|
+
finding.status === "refuted" &&
|
|
745
|
+
(finding.kind === "unresolved_user_decision" || finding.kind === "replan_required")
|
|
746
|
+
)
|
|
747
|
+
violations.push(`${path} refuted requires a blocking or advisory finding`);
|
|
748
|
+
const counterevidence = finding.counterevidence ?? null;
|
|
749
|
+
if (finding.status === "refuted" && !counterevidence)
|
|
750
|
+
violations.push(`${path} status refuted requires counterevidence`);
|
|
751
|
+
if (!counterevidence) continue;
|
|
752
|
+
if (finding.status === "open")
|
|
753
|
+
violations.push(`${path} open finding cannot carry counterevidence`);
|
|
754
|
+
// The binding need not name the finding's own acceptance: an inherited
|
|
755
|
+
// refutation carries the QA evidence it was originally proved with. It
|
|
756
|
+
// must name an acceptance that evidence actually reports as passed, or
|
|
757
|
+
// the refutation would assert nothing.
|
|
758
|
+
const attestation = attestations.find(
|
|
759
|
+
(item) => item.id === counterevidence.attestation_id,
|
|
760
|
+
);
|
|
761
|
+
if (!attestation || attestation.kind !== "qa")
|
|
762
|
+
violations.push(
|
|
763
|
+
`${path}.counterevidence.attestation_id must reference a qa attestation`,
|
|
764
|
+
);
|
|
765
|
+
else if (
|
|
766
|
+
!attestation.acceptance_results.some(
|
|
767
|
+
(result) =>
|
|
768
|
+
result.acceptance_id === counterevidence.acceptance_id &&
|
|
769
|
+
result.status === "passed",
|
|
770
|
+
)
|
|
771
|
+
)
|
|
772
|
+
violations.push(
|
|
773
|
+
`${path}.counterevidence.acceptance_id must be an acceptance its attestation passed`,
|
|
774
|
+
);
|
|
775
|
+
}
|
|
648
776
|
if (violations.length > 0) throw new KernelValidationError(violations);
|
|
649
777
|
const record = {
|
|
650
778
|
contract: expectedContract,
|
|
@@ -729,6 +857,7 @@ export function assertKernelInvariantsV2(
|
|
|
729
857
|
const ACTION_V2_TYPES = [
|
|
730
858
|
"record_finding",
|
|
731
859
|
"resolve_finding",
|
|
860
|
+
"refute_finding",
|
|
732
861
|
"record_approval",
|
|
733
862
|
"revise_intent",
|
|
734
863
|
"approve_breaking_intent_revision",
|
|
@@ -831,6 +960,29 @@ export function parseTaskAction(raw: unknown): TaskAction {
|
|
|
831
960
|
};
|
|
832
961
|
break;
|
|
833
962
|
}
|
|
963
|
+
case "refute_finding": {
|
|
964
|
+
rejectUnknown(
|
|
965
|
+
value,
|
|
966
|
+
[...ACTION_BASE_FIELDS, "finding_id", "attestation_id"],
|
|
967
|
+
"action",
|
|
968
|
+
violations,
|
|
969
|
+
);
|
|
970
|
+
action = {
|
|
971
|
+
...base,
|
|
972
|
+
type: "refute_finding",
|
|
973
|
+
finding_id: stringAt(
|
|
974
|
+
value.finding_id,
|
|
975
|
+
"action.finding_id",
|
|
976
|
+
violations,
|
|
977
|
+
),
|
|
978
|
+
attestation_id: stringAt(
|
|
979
|
+
value.attestation_id,
|
|
980
|
+
"action.attestation_id",
|
|
981
|
+
violations,
|
|
982
|
+
),
|
|
983
|
+
};
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
834
986
|
case "record_approval": {
|
|
835
987
|
rejectUnknown(
|
|
836
988
|
value,
|
|
@@ -1032,22 +1184,63 @@ export function assertTaskRecordUpdateV3(
|
|
|
1032
1184
|
.filter((item) => item.kind === "replan_required" && item.status === "open")
|
|
1033
1185
|
.map((item) => item.id)
|
|
1034
1186
|
: [];
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1187
|
+
// A transition may change only the fields it writes; a legal-looking
|
|
1188
|
+
// transition must not carry a rewritten claim along with it.
|
|
1189
|
+
const withoutFields = (finding: TaskFinding, fields: readonly string[]): string => {
|
|
1190
|
+
const copy: Record<string, unknown> = { ...finding };
|
|
1191
|
+
for (const field of fields) delete copy[field];
|
|
1192
|
+
return JSON.stringify(copy);
|
|
1193
|
+
};
|
|
1039
1194
|
for (const prior of previous.findings) {
|
|
1040
1195
|
const current = next.findings.find((item) => item.id === prior.id);
|
|
1041
1196
|
if (!current)
|
|
1042
1197
|
violations.push(`finding item ${prior.id} was removed`);
|
|
1043
1198
|
else if (
|
|
1044
1199
|
JSON.stringify(current) !== JSON.stringify(prior) &&
|
|
1200
|
+
// A finding may move to resolved from open, or from a refutation
|
|
1201
|
+
// whose bound evidence went stale (the Spec's stale -> resolved
|
|
1202
|
+
// edge); a refutation that is still live already suppresses the
|
|
1203
|
+
// finding and may not be made permanent. Only refute_finding may set
|
|
1204
|
+
// or renew refuted, and only stale refutations are rebindable.
|
|
1045
1205
|
!(
|
|
1046
1206
|
resolvingFindingIds.includes(prior.id) &&
|
|
1047
|
-
prior.status
|
|
1048
|
-
current.status === "resolved"
|
|
1207
|
+
prior.status !== "resolved" &&
|
|
1208
|
+
current.status === "resolved" &&
|
|
1209
|
+
(prior.status === "open" ||
|
|
1210
|
+
!refutationIsLive(
|
|
1211
|
+
prior,
|
|
1212
|
+
previous.attestations,
|
|
1213
|
+
refutationIdentity(previous, action.diff_hash),
|
|
1214
|
+
)) &&
|
|
1215
|
+
withoutFields(prior, ["status"]) === withoutFields(current, ["status"])
|
|
1049
1216
|
) &&
|
|
1050
|
-
!(
|
|
1217
|
+
!(
|
|
1218
|
+
action.type === "refute_finding" &&
|
|
1219
|
+
prior.id === action.finding_id &&
|
|
1220
|
+
prior.status !== "resolved" &&
|
|
1221
|
+
current.status === "refuted" &&
|
|
1222
|
+
// First refutation, or the Spec's stale -> refuted renewal: a
|
|
1223
|
+
// refutation that is still live may neither be renewed nor
|
|
1224
|
+
// rebound to other evidence.
|
|
1225
|
+
(prior.status === "open" ||
|
|
1226
|
+
!refutationIsLive(
|
|
1227
|
+
prior,
|
|
1228
|
+
previous.attestations,
|
|
1229
|
+
refutationIdentity(previous, action.diff_hash),
|
|
1230
|
+
)) &&
|
|
1231
|
+
current.counterevidence?.attestation_id === action.attestation_id &&
|
|
1232
|
+
// The renewal binds the acceptance the finding names, not just any
|
|
1233
|
+
// acceptance the attestation happens to pass (Spec: the evidence
|
|
1234
|
+
// must cover the finding's own acceptance_id).
|
|
1235
|
+
current.counterevidence?.acceptance_id === prior.acceptance_id &&
|
|
1236
|
+
isFreshPassingQaAttestation(
|
|
1237
|
+
previous.attestations.find((item) => item.id === action.attestation_id),
|
|
1238
|
+
current.counterevidence?.acceptance_id ?? "",
|
|
1239
|
+
refutationIdentity(previous, action.diff_hash),
|
|
1240
|
+
) &&
|
|
1241
|
+
withoutFields(prior, ["status", "counterevidence"]) ===
|
|
1242
|
+
withoutFields(current, ["status", "counterevidence"])
|
|
1243
|
+
)
|
|
1051
1244
|
)
|
|
1052
1245
|
violations.push(`finding item ${prior.id} was rewritten`);
|
|
1053
1246
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/plugin_versioning.ts from the root package.json.
|
|
2
|
-
export const PLUGIN_VERSION = "3.6.
|
|
2
|
+
export const PLUGIN_VERSION = "3.6.7" as const;
|
|
@@ -30,6 +30,14 @@ Return exactly one JSON object with the fields required by the Loop review
|
|
|
30
30
|
contract: `contract`, `role`, `task_id`, `snapshot_digest`, `decision` (`pass`
|
|
31
31
|
or `rework`), and for `pass` include `approval` (`kind`, `authority_role`,
|
|
32
32
|
`summary`), for `rework` include `findings` (`id`, `kind`, `acceptance_id`,
|
|
33
|
-
`summary`).
|
|
33
|
+
`summary`, `evidence`). Every rework finding's `evidence` must carry
|
|
34
|
+
`trigger` (the concrete inputs or state that reach the defect), a non-empty
|
|
35
|
+
`caller_chain` (ordered repository paths or symbols), and `violated`
|
|
36
|
+
(`kind`: `acceptance` or `security_boundary`, `ref`: the acceptance id or
|
|
37
|
+
boundary name). Do not invent fields, and never send an anchor yourself: the
|
|
38
|
+
Kernel derives it as the sha256 of the canonical `{violated.kind,
|
|
39
|
+
violated.ref, caller_chain}`, so an identical claim keeps one stable identity
|
|
40
|
+
across review rounds while a different call chain is a different claim. A
|
|
41
|
+
passing review has no findings. If the
|
|
34
42
|
checkpoint is `awaiting_user_successor_decision`, stop without dispatch; only
|
|
35
43
|
a literal user may invoke `--approve-successor`.
|