immune-brain 4.6.0 → 5.0.0
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 +154 -37
- package/README.zh-CN.md +160 -40
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +42 -44
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +142 -474
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +54 -84
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +7 -2
- package/plugins/immune-brain/.pi-extension/tsconfig.json +2 -2
- package/plugins/immune-brain/agents/immune-brain-advisory-reviewer.md +23 -0
- package/plugins/immune-brain/agents/immune-brain-qa.md +42 -0
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +90 -1
- package/plugins/immune-brain/agents/immune-brain-ui-review.md +21 -0
- package/plugins/immune-brain/dist/BASELINE.md +4 -4
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +7369 -7016
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +3 -3
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -1
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-doc-prune.md +2 -2
- package/plugins/immune-brain/dist/{imm-agent-doc-maintain.md → imm-doc-slim.md} +9 -9
- package/plugins/immune-brain/dist/imm-planner.md +23 -8
- package/plugins/immune-brain/dist/imm-pr-fix.md +1 -1
- package/plugins/immune-brain/dist/{imm-review-retro.md → imm-retro.md} +26 -13
- package/plugins/immune-brain/dist/{imm-loop.md → imm-run.md} +33 -7
- package/plugins/immune-brain/dist/registry.yaml +9 -9
- package/plugins/immune-brain/dist/role-prompts/code-review.md +7 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +35 -2
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +83 -67
- package/plugins/immune-brain/runtime/assurance/host_port.ts +20 -0
- package/plugins/immune-brain/runtime/assurance/qa.ts +2 -2
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +347 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +62 -369
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +2 -0
- package/plugins/immune-brain/runtime/claude/review_host.ts +48 -16
- package/plugins/immune-brain/runtime/claude/role_agents.ts +83 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +3 -6
- package/plugins/immune-brain/runtime/kernel/application.ts +6 -3
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +15 -8
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +5 -3
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +149 -5
- package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
- package/plugins/immune-brain/runtime/kernel/storage.ts +170 -11
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +13 -0
- package/plugins/immune-brain/runtime/loop_contract.ts +22 -22
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +7 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +35 -2
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +27 -0
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +57 -27
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +106 -0
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +12 -14
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +34 -29
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +26 -90
- package/plugins/immune-brain/runtime/workspace_scope.ts +34 -0
- package/plugins/immune-brain/skills/BASELINE.md +4 -4
- package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +1 -1
- package/plugins/immune-brain/skills/{imm-agent-doc-maintain → imm-doc-slim}/SKILL.md +9 -9
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +4 -2
- package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +1 -1
- package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/SKILL.md +7 -7
- package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/scripts/review_retro.ts +46 -21
- package/plugins/immune-brain/skills/{imm-loop → imm-run}/SKILL.md +7 -7
- package/plugins/immune-brain/skills/registry.yaml +9 -9
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -581
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
stateDatabasePath,
|
|
57
57
|
} from "./storage_paths";
|
|
58
58
|
import { canonicalRecordHash } from "./reducer";
|
|
59
|
-
import { parseTaskRecord } from "./validation";
|
|
59
|
+
import { KernelInvariantError, parseTaskRecord } from "./validation";
|
|
60
60
|
import { parseTaskRecordV2, parseTaskRecordV3, type TaskRecordV2 } from "./legacy_task_record";
|
|
61
61
|
import {
|
|
62
62
|
assertRunBinding,
|
|
@@ -101,6 +101,7 @@ import type {
|
|
|
101
101
|
StoredTaskMutationV3,
|
|
102
102
|
V3AuthorityObservation,
|
|
103
103
|
} from "./types";
|
|
104
|
+
import { TASK_RECORD_CONTRACT_V4 } from "./types";
|
|
104
105
|
|
|
105
106
|
export {
|
|
106
107
|
KernelSchemaError,
|
|
@@ -898,13 +899,33 @@ export function readWorkspaceStateRaw(root: string): {
|
|
|
898
899
|
};
|
|
899
900
|
}
|
|
900
901
|
|
|
901
|
-
|
|
902
|
-
|
|
902
|
+
/**
|
|
903
|
+
* The record a settled run carries, for a run of any historical contract.
|
|
904
|
+
*
|
|
905
|
+
* A migrated store preserves the terminal record bytes it was imported with:
|
|
906
|
+
* a genuine legacy v3 record is settled evidence the same way a live v4 one
|
|
907
|
+
* is, so the settled read parses both and validates nothing less. A record
|
|
908
|
+
* satisfying neither parser is unreadable and fails closed.
|
|
909
|
+
*/
|
|
910
|
+
function settledRecordFromRun(run: KernelRunRow): TaskRecord | TaskRecordV3 {
|
|
911
|
+
let record: TaskRecord | TaskRecordV3;
|
|
912
|
+
try {
|
|
913
|
+
record = parseTaskRecord(JSON.parse(run.record_json) as Record<string, unknown>);
|
|
914
|
+
} catch {
|
|
915
|
+
record = parseTaskRecordV3(JSON.parse(run.record_json) as Record<string, unknown>);
|
|
916
|
+
}
|
|
903
917
|
if (record.task_id !== run.task_id)
|
|
904
918
|
throw new KernelStoreSecurityError("task record identity is inconsistent with its run");
|
|
905
919
|
return record;
|
|
906
920
|
}
|
|
907
921
|
|
|
922
|
+
function recordFromRun(run: KernelRunRow): TaskRecord {
|
|
923
|
+
const record = settledRecordFromRun(run);
|
|
924
|
+
if (record.contract !== TASK_RECORD_CONTRACT_V4)
|
|
925
|
+
throw new KernelInvariantError([`a live run must hold a v4 record, found ${record.contract}`]);
|
|
926
|
+
return record;
|
|
927
|
+
}
|
|
928
|
+
|
|
908
929
|
export function readTaskRecordRaw(
|
|
909
930
|
root: string,
|
|
910
931
|
taskId: string,
|
|
@@ -948,6 +969,93 @@ export function readCommittedRecord(
|
|
|
948
969
|
return read ?? null;
|
|
949
970
|
}
|
|
950
971
|
|
|
972
|
+
/** The one definition of the enrollment Run-id convention: `enroll-<task>-<created_at>`. */
|
|
973
|
+
export function enrollmentEventIdFor(taskId: string, createdAt: string): string {
|
|
974
|
+
validateTaskId(taskId);
|
|
975
|
+
return `enroll-${taskId}-${createdAt}`;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* The Authority Store's answer to "the settled Run of this Task": this
|
|
980
|
+
* worktree's run row, its identity-validated record, its proof, whether its
|
|
981
|
+
* audit pair is exported, and the raw bytes for byte-exact comparison. Run
|
|
982
|
+
* resolution and proof matching happen behind this read, so a Task with
|
|
983
|
+
* several Runs never yields another Run's evidence, and an active Task or a
|
|
984
|
+
* Task with no Run reads as null.
|
|
985
|
+
*/
|
|
986
|
+
export interface SettledRunEvidence {
|
|
987
|
+
contract: "assurance_kernel/settled_run_evidence/v1";
|
|
988
|
+
task_id: string;
|
|
989
|
+
run_id: string;
|
|
990
|
+
created_at: string;
|
|
991
|
+
enrollment_event_id: string;
|
|
992
|
+
lifecycle: "done" | "stopped";
|
|
993
|
+
/** The settled record, which may be a migrated historical v3. */
|
|
994
|
+
record: TaskRecord | TaskRecordV3;
|
|
995
|
+
/** canonicalRecordHash(record): the record's canonical CAS identity. */
|
|
996
|
+
record_revision: string;
|
|
997
|
+
/** revisionForContent(record_json): the identity the proof binds. */
|
|
998
|
+
record_revision_raw: string;
|
|
999
|
+
/** The store's raw record bytes; equal to the exported audit record. */
|
|
1000
|
+
record_json: string;
|
|
1001
|
+
proof: TaskTombstone;
|
|
1002
|
+
/** The store's raw proof bytes; equal to the exported audit proof. */
|
|
1003
|
+
proof_json: string;
|
|
1004
|
+
audit_exported: boolean;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
export function readSettledRunEvidence(root: string, taskId: string): SettledRunEvidence | null {
|
|
1008
|
+
validateTaskId(taskId);
|
|
1009
|
+
const read = withKernelRead(root, (db) => {
|
|
1010
|
+
const run = readRunRowByTask(db, taskId);
|
|
1011
|
+
if (!run || run.state === "active") return null;
|
|
1012
|
+
// A settled run always carries the proof its settlement committed; a
|
|
1013
|
+
// store missing it is corrupt and must never project as a settled task.
|
|
1014
|
+
if (run.terminal_proof_json === null)
|
|
1015
|
+
throw new KernelStoreSecurityError(
|
|
1016
|
+
`task ${taskId} is ${run.state} without a committed terminal proof`,
|
|
1017
|
+
);
|
|
1018
|
+
const record = settledRecordFromRun(run);
|
|
1019
|
+
const record_revision = canonicalRecordHash(record as TaskRecord);
|
|
1020
|
+
// The proof binds the raw committed bytes, not the canonical
|
|
1021
|
+
// re-serialization: a store migrated from the prior runtime keeps
|
|
1022
|
+
// historical record bytes whose serialization need not be canonical.
|
|
1023
|
+
const record_revision_raw = revisionForContent(run.record_json);
|
|
1024
|
+
const proof = parseTaskTombstone(
|
|
1025
|
+
JSON.parse(run.terminal_proof_json) as Record<string, unknown>,
|
|
1026
|
+
);
|
|
1027
|
+
if (proof.task_id !== taskId)
|
|
1028
|
+
throw new KernelStoreSecurityError("settled run proof identity is inconsistent");
|
|
1029
|
+
if (proof.final_record_hash !== record_revision_raw)
|
|
1030
|
+
throw new KernelStoreSecurityError("settled run proof does not match its task record");
|
|
1031
|
+
// Three independent facts of one settlement must agree before the
|
|
1032
|
+
// read answers: the run row's state, the record's own lifecycle, and
|
|
1033
|
+
// the proof's terminal lifecycle. A store that disagrees with itself
|
|
1034
|
+
// is corrupt evidence and must fail closed rather than project a
|
|
1035
|
+
// settled task whose record is still live.
|
|
1036
|
+
if (record.lifecycle !== run.state || proof.terminal_lifecycle !== run.state)
|
|
1037
|
+
throw new KernelStoreSecurityError(
|
|
1038
|
+
`settled run facts disagree: run state ${run.state}, record lifecycle ${record.lifecycle}, proof terminal ${proof.terminal_lifecycle}`,
|
|
1039
|
+
);
|
|
1040
|
+
return {
|
|
1041
|
+
contract: "assurance_kernel/settled_run_evidence/v1" as const,
|
|
1042
|
+
task_id: taskId,
|
|
1043
|
+
run_id: run.run_id,
|
|
1044
|
+
created_at: run.created_at,
|
|
1045
|
+
enrollment_event_id: run.enrollment_event_id,
|
|
1046
|
+
lifecycle: run.state as "done" | "stopped",
|
|
1047
|
+
record,
|
|
1048
|
+
record_revision,
|
|
1049
|
+
record_revision_raw,
|
|
1050
|
+
record_json: run.record_json,
|
|
1051
|
+
proof,
|
|
1052
|
+
proof_json: run.terminal_proof_json,
|
|
1053
|
+
audit_exported: run.audit_exported_at !== null,
|
|
1054
|
+
};
|
|
1055
|
+
});
|
|
1056
|
+
return read ?? null;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
951
1059
|
/**
|
|
952
1060
|
* Read the immutable terminal audit pair for one task:
|
|
953
1061
|
* `.imm/audit/<task-id>/task-record.json` plus `terminal-proof.json`.
|
|
@@ -1023,13 +1131,6 @@ export function readAuditTaskPair(
|
|
|
1023
1131
|
return { recordRevision, record, proof };
|
|
1024
1132
|
}
|
|
1025
1133
|
|
|
1026
|
-
export function readTaskRecord(
|
|
1027
|
-
root: string,
|
|
1028
|
-
taskId: string,
|
|
1029
|
-
): { revision: string; record: TaskRecord | null } {
|
|
1030
|
-
return readTaskRecordRaw(root, taskId);
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
1134
|
// ---------------------------------------------------------------------------
|
|
1034
1135
|
// Audit export: deterministic, idempotent, retryable, never authority.
|
|
1035
1136
|
// ---------------------------------------------------------------------------
|
|
@@ -1073,6 +1174,60 @@ function convergePendingRelocations(root: string, db: DatabaseSync): void {
|
|
|
1073
1174
|
}
|
|
1074
1175
|
}
|
|
1075
1176
|
|
|
1177
|
+
/** Settled task evidence: the terminal record and proof of a task's own run. */
|
|
1178
|
+
export interface SettledTaskEvidence {
|
|
1179
|
+
/** The run that owns this evidence; `null` for the legacy flat layout. */
|
|
1180
|
+
runId: string | null;
|
|
1181
|
+
record: TaskRecordV2 | TaskRecordV3 | TaskRecord;
|
|
1182
|
+
recordRevision: string;
|
|
1183
|
+
proof: TaskTombstone;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* The settled evidence of a task, read run-aware.
|
|
1188
|
+
*
|
|
1189
|
+
* One read decides which of three worlds this worktree is in, because a
|
|
1190
|
+
* decision made from two reads can be stale between them:
|
|
1191
|
+
*
|
|
1192
|
+
* - **absent** (no Kernel store, or no row for the task): a pre-store terminal
|
|
1193
|
+
* or a repository fixture, whose single flat audit pair is this worktree's
|
|
1194
|
+
* evidence. This is the only world where the flat layout is read at all.
|
|
1195
|
+
* - **active**: the task's run is in flight, so it has no settled evidence; an
|
|
1196
|
+
* exported or fetched pair of the same task is another run's and never
|
|
1197
|
+
* answers while this worktree's own run is live.
|
|
1198
|
+
* - **terminal**: the task's own terminal run, read run-scoped; until its
|
|
1199
|
+
* export exists the evidence is unreadable (fail closed), never another run's.
|
|
1200
|
+
*
|
|
1201
|
+
* A settlement committed between two reads cannot turn the terminal world into
|
|
1202
|
+
* the absent one, because the world is decided before any pair is resolved.
|
|
1203
|
+
*/
|
|
1204
|
+
export function readSettledTaskEvidence(root: string, taskId: string): SettledTaskEvidence | null {
|
|
1205
|
+
validateTaskId(taskId);
|
|
1206
|
+
const localRun = withKernelRead(root, (db) => readRunRowByTask(db, taskId));
|
|
1207
|
+
if (!localRun) {
|
|
1208
|
+
const legacy = readAuditTaskPair(root, taskId);
|
|
1209
|
+
if (!legacy) return null;
|
|
1210
|
+
return {
|
|
1211
|
+
runId: null,
|
|
1212
|
+
record: legacy.record as SettledTaskEvidence["record"],
|
|
1213
|
+
recordRevision: legacy.recordRevision,
|
|
1214
|
+
proof: legacy.proof,
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
if (localRun.state === "active") return null;
|
|
1218
|
+
const settled = readSettledRunEvidence(root, taskId);
|
|
1219
|
+
if (!settled) return null;
|
|
1220
|
+
return {
|
|
1221
|
+
runId: settled.run_id,
|
|
1222
|
+
record: settled.record,
|
|
1223
|
+
// The pair identity is the raw committed bytes the proof binds; the
|
|
1224
|
+
// canonical revision stays available on the run evidence for callers
|
|
1225
|
+
// that need the TaskRecord's canonical CAS identity.
|
|
1226
|
+
recordRevision: settled.record_revision_raw,
|
|
1227
|
+
proof: settled.proof,
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1076
1231
|
// ---------------------------------------------------------------------------
|
|
1077
1232
|
// Journal.
|
|
1078
1233
|
// ---------------------------------------------------------------------------
|
|
@@ -1755,7 +1910,11 @@ export function reconcileKernelAuthority(
|
|
|
1755
1910
|
const legacy = projected ? null : retiredFileStoreDiagnostic(root, null, taskId);
|
|
1756
1911
|
if (legacy) return conflictProjection(taskId, legacy);
|
|
1757
1912
|
try {
|
|
1758
|
-
|
|
1913
|
+
// Run-aware: when this worktree's store carries the task's run, only that
|
|
1914
|
+
// run's own evidence may classify the task; the resolved/flat fallback
|
|
1915
|
+
// below serves only pre-run-layout evidence of a task with no local run.
|
|
1916
|
+
const localRun = localRunId(root, taskId);
|
|
1917
|
+
const audit = readAuditTaskPair(root, taskId, localRun ?? undefined);
|
|
1759
1918
|
if (audit)
|
|
1760
1919
|
return {
|
|
1761
1920
|
contract: "assurance_kernel/authority_projection/v1",
|
|
@@ -212,6 +212,19 @@ export function auditTerminalProofPath(taskId: string): string {
|
|
|
212
212
|
return `${auditTaskDirPath(taskId)}/terminal-proof.json`;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/** Unattended batch run state: `.imm/state/batches/<batch-id>.json`. */
|
|
216
|
+
export function batchStatePath(batchId: string): string {
|
|
217
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(batchId))
|
|
218
|
+
throw new Error(`invalid batch identity: ${batchId}`);
|
|
219
|
+
return `${BATCH_STATE_RELATIVE}/${batchId}.json`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Durable batch child commit evidence: `.imm/state/batches/commits/<batch-id>-<task-id>.json`. */
|
|
223
|
+
export function batchCommitEvidencePath(batchId: string, taskId: string): string {
|
|
224
|
+
validateTaskId(taskId);
|
|
225
|
+
return `${BATCH_STATE_RELATIVE}/commits/${batchId}-${taskId}.json`;
|
|
226
|
+
}
|
|
227
|
+
|
|
215
228
|
/** Historical v3 machine evidence root under the tracked audit store. */
|
|
216
229
|
export function legacyV3Path(...segments: string[]): string {
|
|
217
230
|
for (const segment of segments)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime validation for `imm-
|
|
2
|
+
* Runtime validation for `imm-run` subagent output.
|
|
3
3
|
*
|
|
4
4
|
* The loop parent collects QA and reviewer decisions from isolated children.
|
|
5
5
|
* Those decisions are untrusted input: they arrive as free-form JSON produced
|
|
@@ -42,7 +42,7 @@ export type LoopRouteNext =
|
|
|
42
42
|
| "imm-planner";
|
|
43
43
|
|
|
44
44
|
export interface LoopRoute {
|
|
45
|
-
entry: "imm-
|
|
45
|
+
entry: "imm-run";
|
|
46
46
|
next: LoopRouteNext;
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -55,27 +55,27 @@ export function resolveLoopRoute(input: {
|
|
|
55
55
|
target: LoopRouteTarget;
|
|
56
56
|
scope_expansion?: boolean;
|
|
57
57
|
}): LoopRoute {
|
|
58
|
-
if (input.scope_expansion) return { entry: "imm-
|
|
58
|
+
if (input.scope_expansion) return { entry: "imm-run", next: "imm-planner" };
|
|
59
59
|
if (
|
|
60
60
|
input.target === "architecture-exploration" &&
|
|
61
61
|
(input.ownership === "brainstorm" || input.ownership === "planner")
|
|
62
62
|
) {
|
|
63
|
-
return { entry: "imm-
|
|
63
|
+
return { entry: "imm-run", next: "arch-explorer" };
|
|
64
64
|
}
|
|
65
65
|
if (
|
|
66
66
|
input.target === "advisory-review" &&
|
|
67
67
|
(input.ownership === "brainstorm" || input.ownership === "planner")
|
|
68
68
|
) {
|
|
69
|
-
return { entry: "imm-
|
|
69
|
+
return { entry: "imm-run", next: "advisory-reviewer" };
|
|
70
70
|
}
|
|
71
71
|
if (input.target === "compounder") {
|
|
72
72
|
if (input.ownership !== "loop") {
|
|
73
73
|
throw new Error("Compounder routing requires Loop ownership");
|
|
74
74
|
}
|
|
75
|
-
return { entry: "imm-
|
|
75
|
+
return { entry: "imm-run", next: "compounder" };
|
|
76
76
|
}
|
|
77
77
|
if (input.ownership === "kernel") {
|
|
78
|
-
return { entry: "imm-
|
|
78
|
+
return { entry: "imm-run", next: "imm_kernel_canary" };
|
|
79
79
|
}
|
|
80
80
|
const nextByTarget: Record<"step" | "test-repair" | "pr-repair", LoopRouteNext> = {
|
|
81
81
|
step: "executor",
|
|
@@ -88,7 +88,7 @@ export function resolveLoopRoute(input: {
|
|
|
88
88
|
);
|
|
89
89
|
}
|
|
90
90
|
return {
|
|
91
|
-
entry: "imm-
|
|
91
|
+
entry: "imm-run",
|
|
92
92
|
next: nextByTarget[
|
|
93
93
|
input.target as "step" | "test-repair" | "pr-repair"
|
|
94
94
|
],
|
|
@@ -96,14 +96,14 @@ export function resolveLoopRoute(input: {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
export type LoopAction =
|
|
99
|
-
| { entry: "imm-
|
|
99
|
+
| { entry: "imm-run"; next: "executor"; context: LoopRoleContext }
|
|
100
100
|
| {
|
|
101
|
-
entry: "imm-
|
|
101
|
+
entry: "imm-run";
|
|
102
102
|
next: "test-fixer" | "pr-fix" | "arch-explorer" | "advisory-reviewer" | "compounder";
|
|
103
103
|
dispatch: LoopRoleDispatch;
|
|
104
104
|
}
|
|
105
105
|
| {
|
|
106
|
-
entry: "imm-
|
|
106
|
+
entry: "imm-run";
|
|
107
107
|
next: "imm_kernel_canary";
|
|
108
108
|
tool: {
|
|
109
109
|
name: "imm_kernel_canary";
|
|
@@ -116,8 +116,8 @@ export type LoopAction =
|
|
|
116
116
|
| "complete";
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
-
| { entry: "imm-
|
|
120
|
-
| { entry: "imm-
|
|
119
|
+
| { entry: "imm-run"; next: "imm-planner"; reason: "scope_expansion" }
|
|
120
|
+
| { entry: "imm-run"; next: "none"; reason: "no_reusable_learning" };
|
|
121
121
|
|
|
122
122
|
function hasReusableLearningEvidence(context: RoleDelegationContext): boolean {
|
|
123
123
|
if (
|
|
@@ -149,18 +149,18 @@ export function buildLoopAction(input: {
|
|
|
149
149
|
}): LoopAction {
|
|
150
150
|
const route = resolveLoopRoute(input);
|
|
151
151
|
if (route.next === "imm-planner") {
|
|
152
|
-
return { entry: "imm-
|
|
152
|
+
return { entry: "imm-run", next: "imm-planner", reason: "scope_expansion" };
|
|
153
153
|
}
|
|
154
154
|
if (route.next === "compounder") {
|
|
155
155
|
if (!input.context || !hasReusableLearningEvidence(input.context)) {
|
|
156
156
|
return {
|
|
157
|
-
entry: "imm-
|
|
157
|
+
entry: "imm-run",
|
|
158
158
|
next: "none",
|
|
159
159
|
reason: "no_reusable_learning",
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
return {
|
|
163
|
-
entry: "imm-
|
|
163
|
+
entry: "imm-run",
|
|
164
164
|
next: "compounder",
|
|
165
165
|
dispatch: buildLoopRoleDispatch({ role: "compounder", context: input.context }),
|
|
166
166
|
};
|
|
@@ -168,28 +168,28 @@ export function buildLoopAction(input: {
|
|
|
168
168
|
if (!input.context) throw new Error(`Loop ${route.next} action requires context`);
|
|
169
169
|
if (route.next === "executor") {
|
|
170
170
|
return {
|
|
171
|
-
entry: "imm-
|
|
171
|
+
entry: "imm-run",
|
|
172
172
|
next: "executor",
|
|
173
173
|
context: buildRoleDelegationPacket({ role: "executor", context: input.context }),
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
176
|
if (route.next === "test-fixer") {
|
|
177
177
|
return {
|
|
178
|
-
entry: "imm-
|
|
178
|
+
entry: "imm-run",
|
|
179
179
|
next: "test-fixer",
|
|
180
180
|
dispatch: buildLoopRoleDispatch({ role: "test-fixer", context: input.context }),
|
|
181
181
|
};
|
|
182
182
|
}
|
|
183
183
|
if (route.next === "pr-fix") {
|
|
184
184
|
return {
|
|
185
|
-
entry: "imm-
|
|
185
|
+
entry: "imm-run",
|
|
186
186
|
next: "pr-fix",
|
|
187
187
|
dispatch: buildLoopRoleDispatch({ role: "pr-fix", context: input.context }),
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
190
|
if (route.next === "arch-explorer") {
|
|
191
191
|
return {
|
|
192
|
-
entry: "imm-
|
|
192
|
+
entry: "imm-run",
|
|
193
193
|
next: "arch-explorer",
|
|
194
194
|
dispatch: buildLoopRoleDispatch({ role: "arch-explorer", context: input.context }),
|
|
195
195
|
};
|
|
@@ -199,13 +199,13 @@ export function buildLoopAction(input: {
|
|
|
199
199
|
throw new Error("advisory review requires an explicit lens");
|
|
200
200
|
}
|
|
201
201
|
return {
|
|
202
|
-
entry: "imm-
|
|
202
|
+
entry: "imm-run",
|
|
203
203
|
next: "advisory-reviewer",
|
|
204
204
|
dispatch: buildLoopRoleDispatch({ role: "advisory-reviewer", context: input.context }),
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
return {
|
|
208
|
-
entry: "imm-
|
|
208
|
+
entry: "imm-run",
|
|
209
209
|
next: "imm_kernel_canary",
|
|
210
210
|
tool: {
|
|
211
211
|
name: "imm_kernel_canary",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/plugin_versioning.ts from the root package.json.
|
|
2
|
-
export const PLUGIN_VERSION = "
|
|
2
|
+
export const PLUGIN_VERSION = "5.0.0" as const;
|
|
@@ -46,6 +46,13 @@ covers the whole lifecycle including cleanup, ranking tolerates missing
|
|
|
46
46
|
embeddings, the budget covers the complete serialized request, and each success,
|
|
47
47
|
failure, and timeout outcome carries its own duration evidence.
|
|
48
48
|
|
|
49
|
+
When an acceptance assertion, or the Spec it references, names an agreed seam,
|
|
50
|
+
check that the delivered tests exercise that acceptance at that seam. A missing
|
|
51
|
+
seam, or a seam silently replaced by a different one, is a finding carrying
|
|
52
|
+
`violated.kind` of `acceptance` with `ref` the acceptance id. When no seam is
|
|
53
|
+
named, its absence is not a finding: candidates written before this rule carries
|
|
54
|
+
no defect for lacking one.
|
|
55
|
+
|
|
49
56
|
A required check that is skipped, that matches zero tests, or whose prerequisite
|
|
50
57
|
is absent does not prove the acceptance; do not accept it as evidence. Full
|
|
51
58
|
suite coverage reported at a coarse pass count is not per-invariant evidence.
|
|
@@ -12,8 +12,9 @@ scoped implementation, staging and focused diagnostics. Enrollment itself does
|
|
|
12
12
|
not start QA; the Parent owns the subsequent Kernel Assurance and foreground
|
|
13
13
|
Review handoff under the same still-valid batch authorization.
|
|
14
14
|
|
|
15
|
-
Before handoff, run the permitted diagnostic checks
|
|
16
|
-
|
|
15
|
+
Before handoff, run the permitted diagnostic checks, complete the Delivery
|
|
16
|
+
Evidence check below and return its table to the Parent as structured diagnostic
|
|
17
|
+
evidence. The read-only Loop
|
|
17
18
|
runtime action only constructs the dispatch envelope; it does not store
|
|
18
19
|
evidence or change task state. Preserve failed and blocked attempts. Do not
|
|
19
20
|
perform QA,
|
|
@@ -21,6 +22,38 @@ review, plan mutation, successor approval, Compounder work, or authority
|
|
|
21
22
|
writes. If the requested change needs scope expansion, stop and return an
|
|
22
23
|
`imm-planner` route with the concrete missing scope and verification reason.
|
|
23
24
|
|
|
25
|
+
## Delivery Evidence
|
|
26
|
+
|
|
27
|
+
Reuse the TaskIntent and any bound Spec's agreed seams and controls. Before the
|
|
28
|
+
first Assurance attempt, return one compact row per acceptance; update affected
|
|
29
|
+
rows after repair. This is handoff evidence, not a new artifact or approval gate.
|
|
30
|
+
|
|
31
|
+
| Acceptance / invariant | Entry and consumers | Positive / negative / boundary controls | Command, outcome and input | Finding coverage |
|
|
32
|
+
|---|---|---|---|---|
|
|
33
|
+
|
|
34
|
+
For changed shared behavior, trace the real entry through its state owner,
|
|
35
|
+
write, serialization, reload and downstream consumer as applicable. Include
|
|
36
|
+
both affected hosts and recovery/replay branches. An async, return-type or
|
|
37
|
+
exception-contract change requires checking every affected caller's await,
|
|
38
|
+
cancellation and error propagation.
|
|
39
|
+
|
|
40
|
+
Use the real producer for the success control where available. Derive rejection
|
|
41
|
+
controls from a successful state, change only the target condition, and assert
|
|
42
|
+
the intended rejection reason and required side effects. A control must fail
|
|
43
|
+
when its target protection is removed. For byte-preservation claims compare the
|
|
44
|
+
relevant raw files, index and refs; for observed behavior link each required
|
|
45
|
+
criterion to its concrete observation.
|
|
46
|
+
|
|
47
|
+
Name whether each check consumed worktree or delivery bytes. Before Assurance,
|
|
48
|
+
verify task-owned changes are staged within scope, generated mirrors match,
|
|
49
|
+
and applicable type/build checks pass. On QA failure, reproduce the reported
|
|
50
|
+
failure under its delivery conditions before another attempt. For rework, name
|
|
51
|
+
the violated invariant, all known trigger classes and their controls in Finding
|
|
52
|
+
coverage; account for affected callers sharing that invariant within scope.
|
|
53
|
+
Record missing evidence as blocked and repair it before handoff. The Loop owns
|
|
54
|
+
verdict submission, finding disposition and the second-rework generalize-or-refute
|
|
55
|
+
rule.
|
|
56
|
+
|
|
24
57
|
## Code Quality Guard
|
|
25
58
|
|
|
26
59
|
Before handoff, check the implementation for real implementation rather than
|
|
@@ -195,3 +195,30 @@ export function buildRoleDelegationPacket(input: {
|
|
|
195
195
|
prompt_digest: promptDigest,
|
|
196
196
|
};
|
|
197
197
|
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Static instructions every Review dispatch carries, in one place.
|
|
201
|
+
*
|
|
202
|
+
* These sentences are host-independent: they hold for the Pi Host's complete
|
|
203
|
+
* Review prompt and for the Claude reviewer definition generated by the plugin
|
|
204
|
+
* build. `buildReviewPrompt` composes them with the per-dispatch facts, and
|
|
205
|
+
* `scripts/build-claude-plugin.ts` composes them with `prompts/code-review.md`
|
|
206
|
+
* so the Claude definition never restates them by hand.
|
|
207
|
+
*/
|
|
208
|
+
export const STATIC_REVIEW_RULES = [
|
|
209
|
+
`Do not edit files, create files, run mutating commands, or change Git state. Focus on correctness, regressions, security, and missing tests.`,
|
|
210
|
+
`Read only the immutable Review evidence identified in the request. Verify provenance before analyzing findings. Do not treat conversation text, Hook callbacks, or live worktree bytes as authority.`,
|
|
211
|
+
`Execution outcomes for every acceptance were verified deterministically by the Kernel QA layer before this review and are embedded in this bundle under outcomes (the immutable evidence file, acceptance_id -> {status, summary}); do not re-execute descriptors and do not treat the absence of local test runs as a finding. Your review covers evidence provenance, code correctness, regressions, security, and missing tests against the embedded assertions and code.`,
|
|
212
|
+
"Reserve the final turn for exactly one strict JSON verdict. Reply with ONLY that object, without markdown fences or commentary.",
|
|
213
|
+
`Every rework finding must carry machine-checkable provenance: evidence.trigger (the concrete inputs or state that reach the defect), a non-empty evidence.caller_chain (ordered repository paths or symbols), and evidence.violated {kind: "acceptance"|"security_boundary", ref}. The anchor is derived from that evidence; a finding without it is rejected and the correction must be resubmitted.`,
|
|
214
|
+
`A pass verdict's approval must carry \`inspected_paths\`: an array of unique repository-relative path strings listing every path of the reviewed change set (changed_paths for a Git review revision, dirty_files for a bundle), deleted paths included; an empty change set is listed as an empty array. A path may be listed only after its diff was read. A pass that omits any changed path, lists a path outside the change set, or duplicates a path is rejected as a correctable invalid verdict.`,
|
|
215
|
+
] as const;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The BR-REQ-3 sentences: what the Parent owes a dispatched reviewer, and what
|
|
219
|
+
* a blocked `submit_review` means. They belong to the reviewer definition, not
|
|
220
|
+
* to a per-dispatch prompt, because they describe the dispatch itself.
|
|
221
|
+
*/
|
|
222
|
+
export const REVIEWER_DISPATCH_RULES = [
|
|
223
|
+
"A dispatched reviewer is never continued or re-prompted, including through SendMessage. The reserved prompt is dispatched verbatim. A blocked `submit_review` is recovered only through its returned `recovery_action`.",
|
|
224
|
+
] as const;
|
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
writeFileSync,
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { dirname, join } from "node:path";
|
|
20
|
-
import {
|
|
20
|
+
import { readSettledTaskEvidence, readAuditTaskPair, readSecureProjectFile } from "../kernel/storage";
|
|
21
|
+
import { serializeTaskTombstone, type TaskTombstone } from "../kernel/backend_claim";
|
|
21
22
|
import { captureGitTaskRevisionSnapshot, pathMatchesScope } from "../workspace_scope";
|
|
22
23
|
import { expectedBatchHead, findExistingActiveBatch, findSettledBatchRecord } from "./batch_preflight";
|
|
23
24
|
export { classifyBatchLineage, type BatchLineage } from "./batch_preflight";
|
|
@@ -444,6 +445,22 @@ function readBatchCommitEvidence(
|
|
|
444
445
|
* Preserves files on failure without destructive rollback (Finding 1).
|
|
445
446
|
* Records durable commit evidence for tamper-proof crash recovery (Finding 1).
|
|
446
447
|
*/
|
|
448
|
+
/** Structural proof equality: every field, in every order. */
|
|
449
|
+
function proofEquals(a: TaskTombstone, b: TaskTombstone): boolean {
|
|
450
|
+
return serializeTaskTombstone(a) === serializeTaskTombstone(b);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function requireMatchingSettledAudit(
|
|
454
|
+
auditPair: NonNullable<ReturnType<typeof readAuditTaskPair>>,
|
|
455
|
+
settled: NonNullable<ReturnType<typeof readSettledTaskEvidence>>,
|
|
456
|
+
): void {
|
|
457
|
+
// Legacy evidence has no local Run; its audit pair is its authority.
|
|
458
|
+
if (settled.runId === null) return;
|
|
459
|
+
if (auditPair.recordRevision !== settled.recordRevision || !proofEquals(auditPair.proof, settled.proof)) {
|
|
460
|
+
throw new Error("batch commit exported audit pair does not match the settled run");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
447
464
|
export async function commitBatchChild(input: {
|
|
448
465
|
root: string;
|
|
449
466
|
taskId: string;
|
|
@@ -454,28 +471,38 @@ export async function commitBatchChild(input: {
|
|
|
454
471
|
}): Promise<{ commit: string }> {
|
|
455
472
|
const { root, taskId, batchId, expectedHead, branch: expectedBranch } = input;
|
|
456
473
|
|
|
457
|
-
// 1. Verify
|
|
458
|
-
// The committed local run owns this task's evidence:
|
|
459
|
-
// directory for the same task is not this batch's
|
|
460
|
-
// clears the active run, so the
|
|
461
|
-
|
|
462
|
-
const
|
|
474
|
+
// 1. Verify the Authority Store reports that child done through its settled
|
|
475
|
+
// Run read (Finding 2). The committed local run owns this task's evidence:
|
|
476
|
+
// another worktree's run directory for the same task is not this batch's
|
|
477
|
+
// audit pair. Settlement clears the active run, so the read includes
|
|
478
|
+
// terminal runs, and the exported pair is what the commit stages.
|
|
479
|
+
const settled = readSettledTaskEvidence(root, taskId);
|
|
480
|
+
if (!settled) {
|
|
481
|
+
throw new Error(`cannot commit child ${taskId}: task is not settled done (no settled run)`);
|
|
482
|
+
}
|
|
483
|
+
// The export is what the commit stages: a terminal run must have reached it.
|
|
484
|
+
const auditPair = readAuditTaskPair(root, taskId, settled.runId ?? undefined);
|
|
463
485
|
if (!auditPair) {
|
|
464
486
|
throw new Error(`cannot commit child ${taskId}: task is not settled done (audit pair missing)`);
|
|
465
487
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
488
|
+
requireMatchingSettledAudit(auditPair, settled);
|
|
489
|
+
const settledLifecycle =
|
|
490
|
+
"lifecycle" in settled.record ? settled.record.lifecycle : (settled.record as { phase: string }).phase;
|
|
491
|
+
if (settledLifecycle !== "done") {
|
|
492
|
+
throw new Error(`cannot commit child ${taskId}: task is not settled done (lifecycle is ${settledLifecycle})`);
|
|
469
493
|
}
|
|
470
|
-
if (
|
|
494
|
+
if (settled.proof.terminal_lifecycle !== "done") {
|
|
471
495
|
throw new Error(`cannot commit child ${taskId}: terminal proof lifecycle is not done`);
|
|
472
496
|
}
|
|
473
|
-
if (
|
|
497
|
+
if (settled.record.task_id !== taskId || settled.proof.task_id !== taskId) {
|
|
474
498
|
throw new Error(`cannot commit child ${taskId}: audit task id mismatch`);
|
|
475
499
|
}
|
|
476
500
|
|
|
477
501
|
// 2. Read scope_hint and goal from the verified terminal TaskRecord.intent_snapshot (Finding 1)
|
|
478
|
-
|
|
502
|
+
// The store-validated record decides scope and attestation once the run is
|
|
503
|
+
// known; the export decides them only in the pre-store legacy layout.
|
|
504
|
+
const authoritative = settled.runId === null ? auditPair.record : settled.record;
|
|
505
|
+
const intentSnapshot = authoritative.intent_snapshot;
|
|
479
506
|
const goal = typeof intentSnapshot.goal === "string" ? intentSnapshot.goal : "";
|
|
480
507
|
const scopeHint = Array.isArray(intentSnapshot.scope_hint)
|
|
481
508
|
? intentSnapshot.scope_hint.filter((s): s is string => typeof s === "string")
|
|
@@ -549,7 +576,7 @@ export async function commitBatchChild(input: {
|
|
|
549
576
|
spawnSync("git", ["-C", root, "reset", "--quiet"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
550
577
|
throw new Error("dirty_outside_scope");
|
|
551
578
|
}
|
|
552
|
-
const record =
|
|
579
|
+
const record = authoritative as {
|
|
553
580
|
contract?: string;
|
|
554
581
|
git_base_head?: string;
|
|
555
582
|
attestations?: Array<{ kind: string; diff_hash: string }>;
|
|
@@ -738,6 +765,20 @@ export async function lookupBatchCommit(input: {
|
|
|
738
765
|
}
|
|
739
766
|
}
|
|
740
767
|
|
|
768
|
+
// Evidence identity is required even when the caller omits lineage checks.
|
|
769
|
+
const adopted = readSettledTaskEvidence(root, taskId);
|
|
770
|
+
const auditPair = adopted ? readAuditTaskPair(root, taskId, adopted.runId ?? undefined) : null;
|
|
771
|
+
if (!adopted || !auditPair) {
|
|
772
|
+
throw new Error(`batch_head_lineage_broken: adopted commit lacks terminal audit pair for ${taskId}`);
|
|
773
|
+
}
|
|
774
|
+
requireMatchingSettledAudit(auditPair, adopted);
|
|
775
|
+
const authoritative = adopted.runId !== null ? adopted.record : auditPair.record;
|
|
776
|
+
const lifecycle =
|
|
777
|
+
"lifecycle" in authoritative ? authoritative.lifecycle : (authoritative as { phase: string }).phase;
|
|
778
|
+
if (lifecycle !== "done") {
|
|
779
|
+
throw new Error(`batch_head_lineage_broken: adopted commit task lifecycle is not done: ${lifecycle}`);
|
|
780
|
+
}
|
|
781
|
+
|
|
741
782
|
// Finding 3: Verify terminal audit, expectedHead, single parent, and commit tree delta within authorized scope
|
|
742
783
|
if (expectedHead !== undefined) {
|
|
743
784
|
const currentHeadResult = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
@@ -766,20 +807,9 @@ export async function lookupBatchCommit(input: {
|
|
|
766
807
|
);
|
|
767
808
|
}
|
|
768
809
|
|
|
769
|
-
// Verify terminal audit exists and is settled done
|
|
770
|
-
const adoptedRun = localRunId(root, taskId);
|
|
771
|
-
const auditPair = readAuditTaskPair(root, taskId, adoptedRun ?? undefined);
|
|
772
|
-
if (!auditPair) {
|
|
773
|
-
throw new Error(`batch_head_lineage_broken: adopted commit lacks terminal audit pair for ${taskId}`);
|
|
774
|
-
}
|
|
775
|
-
const lifecycle = "lifecycle" in auditPair.record ? auditPair.record.lifecycle : auditPair.record.phase;
|
|
776
|
-
if (lifecycle !== "done") {
|
|
777
|
-
throw new Error(`batch_head_lineage_broken: adopted commit task lifecycle is not done: ${lifecycle}`);
|
|
778
|
-
}
|
|
779
|
-
|
|
780
810
|
// Verify commit tree delta is strictly within authorized scope_hint
|
|
781
|
-
const scopeHint = Array.isArray(
|
|
782
|
-
?
|
|
811
|
+
const scopeHint = Array.isArray(authoritative.intent_snapshot.scope_hint)
|
|
812
|
+
? authoritative.intent_snapshot.scope_hint.filter((s): s is string => typeof s === "string")
|
|
783
813
|
: [];
|
|
784
814
|
const deltaResult = spawnSync(
|
|
785
815
|
"git",
|