immune-brain 3.6.6 → 3.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +24 -3
  2. package/README.zh-CN.md +23 -3
  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-work.ts +29 -19
  6. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +932 -0
  7. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +200 -2
  8. package/plugins/immune-brain/dist/claude/mcp-server.mjs +3404 -227
  9. package/plugins/immune-brain/dist/imm-loop.md +22 -0
  10. package/plugins/immune-brain/dist/role-prompts/code-review.md +9 -1
  11. package/plugins/immune-brain/runtime/assurance/coordinator.ts +101 -3
  12. package/plugins/immune-brain/runtime/claude/interaction.ts +16 -3
  13. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +840 -17
  14. package/plugins/immune-brain/runtime/claude/mcp_server.ts +72 -10
  15. package/plugins/immune-brain/runtime/kernel/canary_application.ts +9 -0
  16. package/plugins/immune-brain/runtime/kernel/completion.ts +19 -1
  17. package/plugins/immune-brain/runtime/kernel/reducer.ts +162 -11
  18. package/plugins/immune-brain/runtime/kernel/refutation.ts +82 -0
  19. package/plugins/immune-brain/runtime/kernel/types.ts +34 -1
  20. package/plugins/immune-brain/runtime/kernel/validation.ts +202 -9
  21. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  22. package/plugins/immune-brain/runtime/prompts/code-review.md +9 -1
  23. package/plugins/immune-brain/runtime/unattended/batch_git.ts +775 -0
  24. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +203 -0
  25. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +1224 -0
  26. package/plugins/immune-brain/runtime/unattended/batch_state.ts +360 -0
  27. package/plugins/immune-brain/runtime/unattended/types.ts +60 -0
  28. package/plugins/immune-brain/skills/imm-loop/SKILL.md +1 -0
@@ -0,0 +1,203 @@
1
+ import { createHash } from "node:crypto";
2
+ import { stableStringify } from "../canonical_json";
3
+ import { observeGithubInitiative, type GithubInitiativeObservation } from "../github_issue_tracker";
4
+ import { readTaskTombstone } from "../kernel/backend_claim";
5
+ import {
6
+ observeTaskIntent,
7
+ TaskIntentObservationError,
8
+ } from "../kernel/intent";
9
+ import { readTaskRecordRaw } from "../kernel/storage";
10
+ import type {
11
+ BatchPlan,
12
+ BatchPlanBudget,
13
+ BatchPlanChild,
14
+ BatchPlanDigestChild,
15
+ InitiativeObservationReader,
16
+ ProjectBatchPlanInput,
17
+ } from "./types";
18
+
19
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
20
+ const DEFAULT_DEADLINE_MS = 8 * 60 * 60 * 1_000;
21
+ const DEFAULT_QA_FAILURE_LIMIT = 2;
22
+
23
+ function compareIds(left: string, right: string): number {
24
+ return left < right ? -1 : left > right ? 1 : 0;
25
+ }
26
+
27
+ function positiveSafeInteger(value: unknown, name: string): number {
28
+ if (!Number.isSafeInteger(value) || (value as number) <= 0)
29
+ throw new Error(`${name} must be a positive safe integer`);
30
+ return value as number;
31
+ }
32
+
33
+ const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
34
+
35
+ function timestamp(value: unknown, name: string): { milliseconds: number; iso: string } {
36
+ if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN.test(value))
37
+ throw new Error(`${name} must be an ISO timestamp`);
38
+ const milliseconds = Date.parse(value);
39
+ if (!Number.isFinite(milliseconds))
40
+ throw new Error(`${name} must be an ISO timestamp`);
41
+ return { milliseconds, iso: new Date(milliseconds).toISOString() };
42
+ }
43
+
44
+ function normalizeObservation(value: GithubInitiativeObservation, initiativeSlug: string): GithubInitiativeObservation {
45
+ if (!value || value.contract !== "immune_brain/github_initiative_observation/v1")
46
+ throw new Error("tracker returned an invalid Initiative observation contract");
47
+ if (value.initiative_id !== initiativeSlug)
48
+ throw new Error("tracker returned an observation for another Initiative");
49
+ if (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0)
50
+ throw new Error("tracker returned an invalid Initiative Issue number");
51
+ if (!Array.isArray(value.tasks)) throw new Error("tracker returned an invalid Initiative Task list");
52
+ const taskIds = new Set<string>();
53
+ const sliceIds = new Set<string>();
54
+ const tasks = value.tasks.map((task, index) => {
55
+ if (!task || typeof task !== "object") throw new Error(`tracker Task ${index} is invalid`);
56
+ if (!ID_PATTERN.test(task.task_id)) throw new Error(`tracker Task ${index} has an invalid task_id`);
57
+ if (!ID_PATTERN.test(task.slice_id)) throw new Error(`tracker Task ${index} has an invalid slice_id`);
58
+ if (!Number.isSafeInteger(task.issue_number) || task.issue_number <= 0)
59
+ throw new Error(`tracker Task ${task.task_id} has an invalid Issue number`);
60
+ if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN.test(id)))
61
+ throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
62
+ if (taskIds.has(task.task_id)) throw new Error(`tracker returned duplicate Task ${task.task_id}`);
63
+ if (sliceIds.has(task.slice_id)) throw new Error(`tracker returned duplicate Slice ${task.slice_id}`);
64
+ taskIds.add(task.task_id);
65
+ sliceIds.add(task.slice_id);
66
+ const blockedBy = [...task.blocked_by].sort();
67
+ if (new Set(blockedBy).size !== blockedBy.length || blockedBy.includes(task.task_id))
68
+ throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
69
+ return { ...task, blocked_by: blockedBy };
70
+ });
71
+ for (const task of tasks) {
72
+ const unknown = task.blocked_by.find((id) => !taskIds.has(id));
73
+ if (unknown) throw new Error(`tracker Task ${task.task_id} depends on unknown Task ${unknown}`);
74
+ }
75
+ return { ...value, tasks: tasks.sort((left, right) => compareIds(left.task_id, right.task_id)) };
76
+ }
77
+
78
+ function dependencyOrder(observation: GithubInitiativeObservation): GithubInitiativeObservation["tasks"] {
79
+ const remaining = new Map(observation.tasks.map((task) => [task.task_id, task]));
80
+ const done = new Set<string>();
81
+ const order: GithubInitiativeObservation["tasks"] = [];
82
+ while (remaining.size) {
83
+ const ready = [...remaining.values()]
84
+ .filter((task) => task.blocked_by.every((id) => done.has(id)))
85
+ .sort((left, right) => compareIds(left.task_id, right.task_id));
86
+ if (!ready.length) throw new Error("Initiative Task dependencies must form an acyclic graph");
87
+ for (const task of ready) {
88
+ remaining.delete(task.task_id);
89
+ done.add(task.task_id);
90
+ order.push(task);
91
+ }
92
+ }
93
+ return order;
94
+ }
95
+
96
+ function dependencyClosures(order: GithubInitiativeObservation["tasks"]): Map<string, string[]> {
97
+ const closures = new Map<string, string[]>();
98
+ for (const task of order) {
99
+ const closure = new Set<string>();
100
+ for (const dependency of task.blocked_by) {
101
+ closure.add(dependency);
102
+ for (const transitive of closures.get(dependency) ?? []) closure.add(transitive);
103
+ }
104
+ closures.set(task.task_id, [...closure].sort());
105
+ }
106
+ return closures;
107
+ }
108
+
109
+ function budget(input: ProjectBatchPlanInput, enrollableCount: number, confirmationTime: { milliseconds: number; iso: string }): BatchPlanBudget {
110
+ const maxChildren = input.budget?.max_children === undefined
111
+ ? positiveSafeInteger(enrollableCount, "budget.max_children")
112
+ : positiveSafeInteger(input.budget.max_children, "budget.max_children");
113
+ if (maxChildren > enrollableCount)
114
+ throw new Error("budget.max_children exceeds the enrollable Child count");
115
+ const qaFailureLimit = input.budget?.qa_failure_limit === undefined
116
+ ? DEFAULT_QA_FAILURE_LIMIT
117
+ : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
118
+ const deadline = input.budget?.deadline_at === undefined
119
+ ? new Date(confirmationTime.milliseconds + DEFAULT_DEADLINE_MS).toISOString()
120
+ : timestamp(input.budget.deadline_at, "budget.deadline_at").iso;
121
+ if (Date.parse(deadline) <= confirmationTime.milliseconds)
122
+ throw new Error("budget.deadline_at must be later than confirmation_time");
123
+ return { max_children: maxChildren, deadline_at: deadline, qa_failure_limit: qaFailureLimit };
124
+ }
125
+
126
+ export async function projectBatchPlan(
127
+ root: string,
128
+ initiativeSlug: string,
129
+ input: ProjectBatchPlanInput,
130
+ readInitiative: InitiativeObservationReader = observeGithubInitiative,
131
+ ): Promise<BatchPlan> {
132
+ if (!ID_PATTERN.test(initiativeSlug)) throw new Error("initiative_slug is invalid");
133
+ if (!input || typeof input !== "object") throw new Error("batch plan input is required");
134
+ const confirmationTime = timestamp(input.confirmation_time, "confirmation_time");
135
+ const trackerObservation = normalizeObservation(await readInitiative(root, initiativeSlug), initiativeSlug);
136
+ const order = dependencyOrder(trackerObservation);
137
+ const closures = dependencyClosures(order);
138
+ const children: BatchPlanChild[] = [];
139
+ for (const task of order) {
140
+ const blockedBy = closures.get(task.task_id)!;
141
+ const child = { task_id: task.task_id, slice_id: task.slice_id, blocked_by: blockedBy };
142
+ const tombstone = readTaskTombstone(root, task.task_id);
143
+ const record = readTaskRecordRaw(root, task.task_id).record;
144
+ if (tombstone && record) throw new Error(`Task ${task.task_id} has conflicting settled and active state`);
145
+ if (tombstone) {
146
+ children.push({ ...child, status: "already_settled", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
147
+ continue;
148
+ }
149
+ if (record) {
150
+ children.push({ ...child, status: "already_owned", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
151
+ continue;
152
+ }
153
+ const intentPath = `docs/plans/${task.task_id}.intent.json`;
154
+ try {
155
+ const read = observeTaskIntent(root, task.task_id, intentPath);
156
+ children.push({
157
+ ...child,
158
+ status: read.intent.risk === "critical" ? "needs_human" : "enrollable",
159
+ reason: read.intent.risk === "critical" ? "critical" : null,
160
+ intent_path: read.intent_ref.path,
161
+ intent_revision: read.intent_ref.revision,
162
+ intent_content_hash: read.content_hash,
163
+ });
164
+ } catch (error) {
165
+ if (!(error instanceof TaskIntentObservationError)) throw error;
166
+ children.push({ ...child, status: "needs_human", reason: "invalid_intent", intent_path: null, intent_revision: null, intent_content_hash: null });
167
+ }
168
+ }
169
+ const directDependencies = new Map(order.map((task) => [task.task_id, task.blocked_by]));
170
+ const childById = new Map(children.map((child) => [child.task_id, child]));
171
+ for (const child of children) {
172
+ if (child.status !== "enrollable") continue;
173
+ if (directDependencies.get(child.task_id)!.some((id) => {
174
+ const status = childById.get(id)!.status;
175
+ return status === "already_owned" || status === "needs_human" || status === "blocked";
176
+ })) {
177
+ child.status = "blocked";
178
+ child.reason = "dependency_unavailable";
179
+ }
180
+ }
181
+ const enrollable: BatchPlanDigestChild[] = children
182
+ .filter((child) => child.status === "enrollable")
183
+ .map((child) => ({
184
+ task_id: child.task_id,
185
+ intent_path: child.intent_path!,
186
+ intent_revision: child.intent_revision!,
187
+ intent_content_hash: child.intent_content_hash!,
188
+ blocked_by: child.blocked_by.filter((id) => childById.get(id)?.status === "enrollable"),
189
+ }));
190
+ if (!enrollable.length)
191
+ throw new Error("batch plan has no enrollable children; nothing to confirm");
192
+ const planDigest = `sha256:${createHash("sha256").update(stableStringify(enrollable)).digest("hex")}`;
193
+ return {
194
+ contract: "assurance_kernel/batch_plan/v1",
195
+ initiative_slug: initiativeSlug,
196
+ confirmation_time: confirmationTime.iso,
197
+ tracker_observation: trackerObservation,
198
+ children,
199
+ enrollable,
200
+ plan_digest: planDigest,
201
+ budget: budget(input, enrollable.length, confirmationTime),
202
+ };
203
+ }