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.
Files changed (75) hide show
  1. package/README.md +374 -46
  2. package/README.zh-CN.md +384 -47
  3. package/package.json +2 -1
  4. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +42 -44
  6. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +143 -474
  7. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +78 -87
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +7 -2
  9. package/plugins/immune-brain/.pi-extension/tsconfig.json +2 -2
  10. package/plugins/immune-brain/agents/immune-brain-advisory-reviewer.md +23 -0
  11. package/plugins/immune-brain/agents/immune-brain-qa.md +42 -0
  12. package/plugins/immune-brain/agents/immune-brain-reviewer.md +90 -1
  13. package/plugins/immune-brain/agents/immune-brain-ui-review.md +21 -0
  14. package/plugins/immune-brain/dist/BASELINE.md +4 -4
  15. package/plugins/immune-brain/dist/claude/mcp-server.mjs +8601 -6975
  16. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +27 -0
  17. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +3 -3
  18. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -1
  19. package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
  20. package/plugins/immune-brain/dist/imm-doc-prune.md +2 -2
  21. package/plugins/immune-brain/dist/{imm-agent-doc-maintain.md → imm-doc-slim.md} +9 -9
  22. package/plugins/immune-brain/dist/imm-planner.md +23 -8
  23. package/plugins/immune-brain/dist/imm-pr-fix.md +1 -1
  24. package/plugins/immune-brain/dist/{imm-review-retro.md → imm-retro.md} +26 -13
  25. package/plugins/immune-brain/dist/{imm-loop.md → imm-run.md} +231 -10
  26. package/plugins/immune-brain/dist/registry.yaml +9 -9
  27. package/plugins/immune-brain/dist/role-prompts/code-review.md +7 -0
  28. package/plugins/immune-brain/dist/role-prompts/executor.md +35 -2
  29. package/plugins/immune-brain/dist/role-prompts/lane-steward.md +44 -0
  30. package/plugins/immune-brain/runtime/assurance/coordinator.ts +83 -67
  31. package/plugins/immune-brain/runtime/assurance/host_port.ts +20 -0
  32. package/plugins/immune-brain/runtime/assurance/qa.ts +2 -2
  33. package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +347 -0
  34. package/plugins/immune-brain/runtime/claude/interaction.ts +6 -0
  35. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +78 -373
  36. package/plugins/immune-brain/runtime/claude/mcp_server.ts +26 -2
  37. package/plugins/immune-brain/runtime/claude/review_host.ts +48 -16
  38. package/plugins/immune-brain/runtime/claude/role_agents.ts +84 -0
  39. package/plugins/immune-brain/runtime/commands/kernel.ts +3 -6
  40. package/plugins/immune-brain/runtime/kernel/application.ts +6 -3
  41. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +15 -8
  42. package/plugins/immune-brain/runtime/kernel/canary_application.ts +5 -3
  43. package/plugins/immune-brain/runtime/kernel/enrollment.ts +149 -5
  44. package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
  45. package/plugins/immune-brain/runtime/kernel/storage.ts +170 -11
  46. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +13 -0
  47. package/plugins/immune-brain/runtime/local_initiative.ts +94 -0
  48. package/plugins/immune-brain/runtime/loop_contract.ts +77 -24
  49. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  50. package/plugins/immune-brain/runtime/prompts/code-review.md +7 -0
  51. package/plugins/immune-brain/runtime/prompts/executor.md +35 -2
  52. package/plugins/immune-brain/runtime/prompts/lane-steward.md +44 -0
  53. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +42 -2
  54. package/plugins/immune-brain/runtime/unattended/batch_git.ts +57 -27
  55. package/plugins/immune-brain/runtime/unattended/batch_integration.ts +387 -0
  56. package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +106 -0
  57. package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +909 -0
  58. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +36 -11
  59. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +53 -36
  60. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +34 -29
  61. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +46 -91
  62. package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +162 -0
  63. package/plugins/immune-brain/runtime/unattended/batch_state.ts +356 -8
  64. package/plugins/immune-brain/runtime/unattended/types.ts +21 -2
  65. package/plugins/immune-brain/runtime/workspace_scope.ts +34 -0
  66. package/plugins/immune-brain/skills/BASELINE.md +4 -4
  67. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +1 -1
  68. package/plugins/immune-brain/skills/{imm-agent-doc-maintain → imm-doc-slim}/SKILL.md +9 -9
  69. package/plugins/immune-brain/skills/imm-planner/SKILL.md +4 -2
  70. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +1 -1
  71. package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/SKILL.md +7 -7
  72. package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/scripts/review_retro.ts +46 -21
  73. package/plugins/immune-brain/skills/{imm-loop → imm-run}/SKILL.md +11 -7
  74. package/plugins/immune-brain/skills/registry.yaml +9 -9
  75. 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
- function recordFromRun(run: KernelRunRow): TaskRecord {
902
- const record = parseTaskRecord(JSON.parse(run.record_json) as Record<string, unknown>);
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
- const audit = readAuditTaskPair(root, taskId);
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-loop` subagent output.
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-loop";
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-loop", next: "imm-planner" };
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-loop", next: "arch-explorer" };
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-loop", next: "advisory-reviewer" };
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-loop", next: "compounder" };
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-loop", next: "imm_kernel_canary" };
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-loop",
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-loop"; next: "executor"; context: LoopRoleContext }
108
+ | { entry: "imm-run"; next: "executor"; context: LoopRoleContext }
100
109
  | {
101
- entry: "imm-loop";
102
- next: "test-fixer" | "pr-fix" | "arch-explorer" | "advisory-reviewer" | "compounder";
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-loop";
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-loop"; next: "imm-planner"; reason: "scope_expansion" }
120
- | { entry: "imm-loop"; next: "none"; reason: "no_reusable_learning" };
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-loop", next: "imm-planner", reason: "scope_expansion" };
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-loop",
202
+ entry: "imm-run",
158
203
  next: "none",
159
204
  reason: "no_reusable_learning",
160
205
  };
161
206
  }
162
207
  return {
163
- entry: "imm-loop",
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-loop",
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-loop",
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-loop",
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-loop",
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-loop",
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-loop",
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 = "4.6.0" as const;
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 and return commands and
16
- outcomes to the Parent as structured diagnostic evidence. The read-only Loop
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