immune-brain 4.6.0 → 5.1.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 +374 -46
- package/README.zh-CN.md +384 -47
- 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 +42 -44
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +143 -474
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +78 -87
- 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 +8601 -6975
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +27 -0
- 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} +231 -10
- 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/dist/role-prompts/lane-steward.md +44 -0
- 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/interaction.ts +6 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +78 -373
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +26 -2
- package/plugins/immune-brain/runtime/claude/review_host.ts +48 -16
- package/plugins/immune-brain/runtime/claude/role_agents.ts +84 -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/local_initiative.ts +94 -0
- package/plugins/immune-brain/runtime/loop_contract.ts +77 -24
- 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/prompts/lane-steward.md +44 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +42 -2
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +57 -27
- package/plugins/immune-brain/runtime/unattended/batch_integration.ts +387 -0
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +106 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +909 -0
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +36 -11
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +53 -36
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +34 -29
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +46 -91
- package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +162 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +356 -8
- package/plugins/immune-brain/runtime/unattended/types.ts +21 -2
- 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 +11 -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)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Reads the Local Initiative carrier, `docs/initiatives/<slug>.md`, into the
|
|
2
|
+
// same Task list the GitHub carrier yields, so an unattended batch can be
|
|
3
|
+
// projected without any GitHub operation. Read-only: this parses a human-owned
|
|
4
|
+
// file and grants, records, and repairs nothing.
|
|
5
|
+
|
|
6
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
|
|
9
|
+
export interface LocalInitiativeObservation {
|
|
10
|
+
contract: "immune_brain/local_initiative_observation/v1";
|
|
11
|
+
initiative_id: string;
|
|
12
|
+
path: string;
|
|
13
|
+
tasks: Array<{
|
|
14
|
+
task_id: string;
|
|
15
|
+
slice_id: string;
|
|
16
|
+
blocked_by: string[];
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
21
|
+
const SLICE_HEADING = /^##\s+([^\s:]+)\s*:/;
|
|
22
|
+
const BLOCKED_BY = /^blocked by\s*:(.*)$/i;
|
|
23
|
+
const TASKS = /^tasks\s*:\s*$/i;
|
|
24
|
+
const BULLET = /^[-*]\s+(.*)$/;
|
|
25
|
+
|
|
26
|
+
export function localInitiativePath(initiativeSlug: string): string {
|
|
27
|
+
return `docs/initiatives/${initiativeSlug}.md`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** True when the Local carrier owns this slug: a regular file, never a symlink. */
|
|
31
|
+
export function hasLocalInitiative(root: string, initiativeSlug: string): boolean {
|
|
32
|
+
const stat = lstatSync(resolve(root, localInitiativePath(initiativeSlug)), { throwIfNoEntry: false });
|
|
33
|
+
if (!stat) return false;
|
|
34
|
+
if (!stat.isFile()) throw new Error(`${localInitiativePath(initiativeSlug)} must be a regular file`);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function unquote(value: string): string {
|
|
39
|
+
return value.trim().replace(/^`(.*)`$/, "$1");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Each `## <slice-id>: <result>` section names its Task under `Tasks:` and may
|
|
44
|
+
* add one `Blocked by: <task-id>, <task-id>` line. A batch child is one Slice,
|
|
45
|
+
* so a section with no Task or several is refused rather than guessed at.
|
|
46
|
+
*/
|
|
47
|
+
export function observeLocalInitiative(root: string, initiativeSlug: string): LocalInitiativeObservation {
|
|
48
|
+
if (!ID_PATTERN.test(initiativeSlug)) throw new Error("initiative_slug is invalid");
|
|
49
|
+
const path = localInitiativePath(initiativeSlug);
|
|
50
|
+
if (!hasLocalInitiative(root, initiativeSlug)) throw new Error(`Local Initiative ${path} does not exist`);
|
|
51
|
+
const slices: Array<{ slice_id: string; tasks: string[]; blocked_by: string[] }> = [];
|
|
52
|
+
let inTasks = false;
|
|
53
|
+
let fence = false;
|
|
54
|
+
for (const raw of readFileSync(resolve(root, path), "utf8").split(/\r?\n/)) {
|
|
55
|
+
const line = raw.trim();
|
|
56
|
+
if (line.startsWith("```")) {
|
|
57
|
+
fence = !fence;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (fence) continue;
|
|
61
|
+
const heading = SLICE_HEADING.exec(line);
|
|
62
|
+
if (heading) {
|
|
63
|
+
slices.push({ slice_id: heading[1]!, tasks: [], blocked_by: [] });
|
|
64
|
+
inTasks = false;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const slice = slices.at(-1);
|
|
68
|
+
if (!slice) continue;
|
|
69
|
+
if (TASKS.test(line)) {
|
|
70
|
+
inTasks = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const blocked = BLOCKED_BY.exec(line);
|
|
74
|
+
if (blocked) {
|
|
75
|
+
slice.blocked_by.push(...blocked[1]!.split(",").map(unquote).filter(Boolean));
|
|
76
|
+
inTasks = false;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const bullet = BULLET.exec(line);
|
|
80
|
+
if (inTasks && bullet) slice.tasks.push(unquote(bullet[1]!));
|
|
81
|
+
else if (line) inTasks = false;
|
|
82
|
+
}
|
|
83
|
+
if (!slices.length) throw new Error(`Local Initiative ${path} declares no Slice`);
|
|
84
|
+
return {
|
|
85
|
+
contract: "immune_brain/local_initiative_observation/v1",
|
|
86
|
+
initiative_id: initiativeSlug,
|
|
87
|
+
path,
|
|
88
|
+
tasks: slices.map((slice) => {
|
|
89
|
+
if (slice.tasks.length !== 1)
|
|
90
|
+
throw new Error(`Local Initiative Slice ${slice.slice_id} must name exactly one Task to run as a batch child`);
|
|
91
|
+
return { task_id: slice.tasks[0]!, slice_id: slice.slice_id, blocked_by: slice.blocked_by };
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -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
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import {
|
|
11
11
|
buildRoleDelegationPacket,
|
|
12
|
+
LANE_EXECUTOR_HOSTS,
|
|
12
13
|
type RoleDelegationContext,
|
|
13
14
|
type RoleDelegationPacket,
|
|
14
15
|
type InternalRole,
|
|
@@ -30,7 +31,8 @@ export type LoopRouteTarget =
|
|
|
30
31
|
| "pr-repair"
|
|
31
32
|
| "architecture-exploration"
|
|
32
33
|
| "advisory-review"
|
|
33
|
-
| "compounder"
|
|
34
|
+
| "compounder"
|
|
35
|
+
| "lane-supply";
|
|
34
36
|
export type LoopRouteNext =
|
|
35
37
|
| "executor"
|
|
36
38
|
| "test-fixer"
|
|
@@ -38,11 +40,12 @@ export type LoopRouteNext =
|
|
|
38
40
|
| "arch-explorer"
|
|
39
41
|
| "advisory-reviewer"
|
|
40
42
|
| "compounder"
|
|
43
|
+
| "lane-steward"
|
|
41
44
|
| "imm_kernel_canary"
|
|
42
45
|
| "imm-planner";
|
|
43
46
|
|
|
44
47
|
export interface LoopRoute {
|
|
45
|
-
entry: "imm-
|
|
48
|
+
entry: "imm-run";
|
|
46
49
|
next: LoopRouteNext;
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -55,27 +58,33 @@ export function resolveLoopRoute(input: {
|
|
|
55
58
|
target: LoopRouteTarget;
|
|
56
59
|
scope_expansion?: boolean;
|
|
57
60
|
}): LoopRoute {
|
|
58
|
-
if (input.scope_expansion) return { entry: "imm-
|
|
61
|
+
if (input.scope_expansion) return { entry: "imm-run", next: "imm-planner" };
|
|
59
62
|
if (
|
|
60
63
|
input.target === "architecture-exploration" &&
|
|
61
64
|
(input.ownership === "brainstorm" || input.ownership === "planner")
|
|
62
65
|
) {
|
|
63
|
-
return { entry: "imm-
|
|
66
|
+
return { entry: "imm-run", next: "arch-explorer" };
|
|
64
67
|
}
|
|
65
68
|
if (
|
|
66
69
|
input.target === "advisory-review" &&
|
|
67
70
|
(input.ownership === "brainstorm" || input.ownership === "planner")
|
|
68
71
|
) {
|
|
69
|
-
return { entry: "imm-
|
|
72
|
+
return { entry: "imm-run", next: "advisory-reviewer" };
|
|
70
73
|
}
|
|
71
74
|
if (input.target === "compounder") {
|
|
72
75
|
if (input.ownership !== "loop") {
|
|
73
76
|
throw new Error("Compounder routing requires Loop ownership");
|
|
74
77
|
}
|
|
75
|
-
return { entry: "imm-
|
|
78
|
+
return { entry: "imm-run", next: "compounder" };
|
|
79
|
+
}
|
|
80
|
+
if (input.target === "lane-supply") {
|
|
81
|
+
if (input.ownership !== "loop") {
|
|
82
|
+
throw new Error("Lane supply routing requires Loop ownership");
|
|
83
|
+
}
|
|
84
|
+
return { entry: "imm-run", next: "lane-steward" };
|
|
76
85
|
}
|
|
77
86
|
if (input.ownership === "kernel") {
|
|
78
|
-
return { entry: "imm-
|
|
87
|
+
return { entry: "imm-run", next: "imm_kernel_canary" };
|
|
79
88
|
}
|
|
80
89
|
const nextByTarget: Record<"step" | "test-repair" | "pr-repair", LoopRouteNext> = {
|
|
81
90
|
step: "executor",
|
|
@@ -88,7 +97,7 @@ export function resolveLoopRoute(input: {
|
|
|
88
97
|
);
|
|
89
98
|
}
|
|
90
99
|
return {
|
|
91
|
-
entry: "imm-
|
|
100
|
+
entry: "imm-run",
|
|
92
101
|
next: nextByTarget[
|
|
93
102
|
input.target as "step" | "test-repair" | "pr-repair"
|
|
94
103
|
],
|
|
@@ -96,14 +105,20 @@ export function resolveLoopRoute(input: {
|
|
|
96
105
|
}
|
|
97
106
|
|
|
98
107
|
export type LoopAction =
|
|
99
|
-
| { entry: "imm-
|
|
108
|
+
| { entry: "imm-run"; next: "executor"; context: LoopRoleContext }
|
|
100
109
|
| {
|
|
101
|
-
entry: "imm-
|
|
102
|
-
next:
|
|
110
|
+
entry: "imm-run";
|
|
111
|
+
next:
|
|
112
|
+
| "test-fixer"
|
|
113
|
+
| "pr-fix"
|
|
114
|
+
| "arch-explorer"
|
|
115
|
+
| "advisory-reviewer"
|
|
116
|
+
| "compounder"
|
|
117
|
+
| "lane-steward";
|
|
103
118
|
dispatch: LoopRoleDispatch;
|
|
104
119
|
}
|
|
105
120
|
| {
|
|
106
|
-
entry: "imm-
|
|
121
|
+
entry: "imm-run";
|
|
107
122
|
next: "imm_kernel_canary";
|
|
108
123
|
tool: {
|
|
109
124
|
name: "imm_kernel_canary";
|
|
@@ -116,8 +131,8 @@ export type LoopAction =
|
|
|
116
131
|
| "complete";
|
|
117
132
|
};
|
|
118
133
|
}
|
|
119
|
-
| { entry: "imm-
|
|
120
|
-
| { entry: "imm-
|
|
134
|
+
| { entry: "imm-run"; next: "imm-planner"; reason: "scope_expansion" }
|
|
135
|
+
| { entry: "imm-run"; next: "none"; reason: "no_reusable_learning" };
|
|
121
136
|
|
|
122
137
|
function hasReusableLearningEvidence(context: RoleDelegationContext): boolean {
|
|
123
138
|
if (
|
|
@@ -140,6 +155,36 @@ function hasReusableLearningEvidence(context: RoleDelegationContext): boolean {
|
|
|
140
155
|
});
|
|
141
156
|
}
|
|
142
157
|
|
|
158
|
+
/**
|
|
159
|
+
* A lane-steward dispatch carries exactly one batch handoff. A provision names
|
|
160
|
+
* the branch, the base and the Executor Hosts to choose from, limited to the
|
|
161
|
+
* allowlist, so a handoff offering any other Host is refused before a role
|
|
162
|
+
* prompt is built. A release names the Lane branch.
|
|
163
|
+
*/
|
|
164
|
+
function assertLaneStewardHandoff(context: RoleDelegationContext): void {
|
|
165
|
+
if (!nonEmptyString(context.lane_branch)) {
|
|
166
|
+
throw new Error("lane-steward dispatch requires a lane_branch");
|
|
167
|
+
}
|
|
168
|
+
if (context.action === "release") return;
|
|
169
|
+
if (context.action !== "provision") {
|
|
170
|
+
throw new Error("lane-steward dispatch requires action provision or release");
|
|
171
|
+
}
|
|
172
|
+
if (!nonEmptyString(context.base_head)) {
|
|
173
|
+
throw new Error("lane-steward provision requires a base_head");
|
|
174
|
+
}
|
|
175
|
+
const hosts = context.executor_hosts;
|
|
176
|
+
const allowed: readonly string[] = LANE_EXECUTOR_HOSTS;
|
|
177
|
+
if (
|
|
178
|
+
!Array.isArray(hosts) ||
|
|
179
|
+
hosts.length === 0 ||
|
|
180
|
+
!hosts.every((host) => typeof host === "string" && allowed.includes(host))
|
|
181
|
+
) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`lane-steward provision offers only ${LANE_EXECUTOR_HOSTS.join(", ")}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
143
188
|
export function buildLoopAction(input: {
|
|
144
189
|
ownership: LoopRouteOwnership;
|
|
145
190
|
target: LoopRouteTarget;
|
|
@@ -149,18 +194,18 @@ export function buildLoopAction(input: {
|
|
|
149
194
|
}): LoopAction {
|
|
150
195
|
const route = resolveLoopRoute(input);
|
|
151
196
|
if (route.next === "imm-planner") {
|
|
152
|
-
return { entry: "imm-
|
|
197
|
+
return { entry: "imm-run", next: "imm-planner", reason: "scope_expansion" };
|
|
153
198
|
}
|
|
154
199
|
if (route.next === "compounder") {
|
|
155
200
|
if (!input.context || !hasReusableLearningEvidence(input.context)) {
|
|
156
201
|
return {
|
|
157
|
-
entry: "imm-
|
|
202
|
+
entry: "imm-run",
|
|
158
203
|
next: "none",
|
|
159
204
|
reason: "no_reusable_learning",
|
|
160
205
|
};
|
|
161
206
|
}
|
|
162
207
|
return {
|
|
163
|
-
entry: "imm-
|
|
208
|
+
entry: "imm-run",
|
|
164
209
|
next: "compounder",
|
|
165
210
|
dispatch: buildLoopRoleDispatch({ role: "compounder", context: input.context }),
|
|
166
211
|
};
|
|
@@ -168,44 +213,52 @@ export function buildLoopAction(input: {
|
|
|
168
213
|
if (!input.context) throw new Error(`Loop ${route.next} action requires context`);
|
|
169
214
|
if (route.next === "executor") {
|
|
170
215
|
return {
|
|
171
|
-
entry: "imm-
|
|
216
|
+
entry: "imm-run",
|
|
172
217
|
next: "executor",
|
|
173
218
|
context: buildRoleDelegationPacket({ role: "executor", context: input.context }),
|
|
174
219
|
};
|
|
175
220
|
}
|
|
176
221
|
if (route.next === "test-fixer") {
|
|
177
222
|
return {
|
|
178
|
-
entry: "imm-
|
|
223
|
+
entry: "imm-run",
|
|
179
224
|
next: "test-fixer",
|
|
180
225
|
dispatch: buildLoopRoleDispatch({ role: "test-fixer", context: input.context }),
|
|
181
226
|
};
|
|
182
227
|
}
|
|
183
228
|
if (route.next === "pr-fix") {
|
|
184
229
|
return {
|
|
185
|
-
entry: "imm-
|
|
230
|
+
entry: "imm-run",
|
|
186
231
|
next: "pr-fix",
|
|
187
232
|
dispatch: buildLoopRoleDispatch({ role: "pr-fix", context: input.context }),
|
|
188
233
|
};
|
|
189
234
|
}
|
|
190
235
|
if (route.next === "arch-explorer") {
|
|
191
236
|
return {
|
|
192
|
-
entry: "imm-
|
|
237
|
+
entry: "imm-run",
|
|
193
238
|
next: "arch-explorer",
|
|
194
239
|
dispatch: buildLoopRoleDispatch({ role: "arch-explorer", context: input.context }),
|
|
195
240
|
};
|
|
196
241
|
}
|
|
242
|
+
if (route.next === "lane-steward") {
|
|
243
|
+
assertLaneStewardHandoff(input.context);
|
|
244
|
+
return {
|
|
245
|
+
entry: "imm-run",
|
|
246
|
+
next: "lane-steward",
|
|
247
|
+
dispatch: buildLoopRoleDispatch({ role: "lane-steward", context: input.context }),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
197
250
|
if (route.next === "advisory-reviewer") {
|
|
198
251
|
if (!nonEmptyString(input.context.lens)) {
|
|
199
252
|
throw new Error("advisory review requires an explicit lens");
|
|
200
253
|
}
|
|
201
254
|
return {
|
|
202
|
-
entry: "imm-
|
|
255
|
+
entry: "imm-run",
|
|
203
256
|
next: "advisory-reviewer",
|
|
204
257
|
dispatch: buildLoopRoleDispatch({ role: "advisory-reviewer", context: input.context }),
|
|
205
258
|
};
|
|
206
259
|
}
|
|
207
260
|
return {
|
|
208
|
-
entry: "imm-
|
|
261
|
+
entry: "imm-run",
|
|
209
262
|
next: "imm_kernel_canary",
|
|
210
263
|
tool: {
|
|
211
264
|
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.1.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
|