immune-brain 3.6.2 → 3.6.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "3.6.2",
3
+ "version": "3.6.4",
4
4
  "description": "Immune-Brain agent skill system",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -30,13 +30,16 @@
30
30
  "scripts": {
31
31
  "changeset": "changeset",
32
32
  "changeset:version": "changeset version && bun scripts/build-claude-plugin.ts && bun scripts/plugin_versioning.ts validate",
33
- "verify:release": "bun scripts/plugin_versioning.ts validate && bun scripts/build-claude-plugin.ts --check && bun scripts/sync-dist-docs.ts --check && bun test && npm pack --dry-run --ignore-scripts",
33
+ "verify:release": "bun scripts/plugin_versioning.ts validate && bun scripts/build-claude-plugin.ts --check && bun scripts/sync-dist-docs.ts --check && bun run typecheck && bun test && npm pack --dry-run --ignore-scripts",
34
34
  "changeset:publish": "bun run verify:release && changeset publish",
35
- "release": "bun run changeset:publish"
35
+ "release": "bun run changeset:publish",
36
+ "typecheck": "tsc -p tsconfig.json"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@changesets/changelog-github": "^0.5.2",
39
40
  "@changesets/cli": "^2.29.8",
41
+ "@types/bun": "^1.4.1",
42
+ "typescript": "5.7",
40
43
  "zod": "^4.1.8"
41
44
  },
42
45
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "3.6.2",
3
+ "version": "3.6.4",
4
4
  "description": "Immune-Brain Claude Code Host: native Enrollment, QA, Review, and Kernel settlement.",
5
5
  "author": {
6
6
  "name": "Immune-Brain Team"
@@ -56,6 +56,7 @@ import {
56
56
  clearTerminalTaskRailOnInput,
57
57
  loopResultDetails,
58
58
  notifyOnce,
59
+ type UiContext,
59
60
  presentTaskOverviewOverlay,
60
61
  presentTaskRail,
61
62
  presentTaskRailResult,
@@ -68,7 +69,7 @@ import {
68
69
  type UserAttentionEventV1,
69
70
  type UserAttentionReason,
70
71
  } from "./pi-canary-interaction";
71
- import { isToolFailureState, throwToolFailure } from "./pi-canary-tool-failure";
72
+ import { isToolFailureState, throwToolFailure, type ToolFailureV1 } from "./pi-canary-tool-failure";
72
73
  import { taskDiffIdentity, taskRevisionIdentity, captureGitTaskSnapshot } from "../runtime/workspace_scope";
73
74
  import {
74
75
  AssuranceProgression,
@@ -87,6 +88,7 @@ import {
87
88
  type AssuranceProgressionPorts,
88
89
  type AssuranceSubmitReviewResult,
89
90
  type AssuranceVerdict,
91
+ type HostContext,
90
92
  type QaVerificationProgress,
91
93
  type SnapshotDescriptor,
92
94
  } from "./pi-canary-assurance-progression";
@@ -262,11 +264,18 @@ type LoopToolAction =
262
264
  context: Record<string, unknown>;
263
265
  };
264
266
 
265
- export default function (
266
- pi: ExtensionAPI,
267
+ /**
268
+ * The exact ports object the Pi Assurance progression runs on.
269
+ *
270
+ * This lived as an inline literal inside the anonymous default export, so no
271
+ * test could ever obtain what production wires; every host adapter defect that
272
+ * reached a published plugin lived in this object. Exporting the factory lets
273
+ * the dual-host conformance suite drive the real thing.
274
+ */
275
+ export function createPiAssuranceProgressionPorts(
267
276
  dependencies: CanaryWorkExtensionDependencies = {},
268
- ) {
269
- const progression = new AssuranceProgression({
277
+ ): AssuranceProgressionPorts {
278
+ return {
270
279
  projectTask: (root, taskId) => projectAssuranceState(root, taskId),
271
280
  readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
272
281
  readTaskIntent: (root, taskId) => readTaskIntent(root, taskId),
@@ -294,7 +303,14 @@ export default function (
294
303
  qaOnAuthorityCommit: dependencies.qaOnAuthorityCommit,
295
304
  qaAfterAuthorityCommit: dependencies.qaAfterAuthorityCommit,
296
305
  qaJobTimeoutMs: dependencies.qaJobTimeoutMs,
297
- } satisfies AssuranceProgressionPorts);
306
+ } satisfies AssuranceProgressionPorts;
307
+ }
308
+
309
+ export default function (
310
+ pi: ExtensionAPI,
311
+ dependencies: CanaryWorkExtensionDependencies = {},
312
+ ) {
313
+ const progression = new AssuranceProgression(createPiAssuranceProgressionPorts(dependencies));
298
314
 
299
315
  let railContext: ExtensionContext | undefined;
300
316
  const refreshTaskRail = async (ctx: ExtensionContext) => {
@@ -1196,7 +1212,7 @@ async function projectAssuranceState(root: string, taskId: string): Promise<Assu
1196
1212
  * Publish and prove the task-scoped synthetic revision for a v4 record. v3
1197
1213
  * records keep the legacy full-source bundle and return null here.
1198
1214
  */
1199
- async function ensureTaskReviewRevision(
1215
+ export async function ensureTaskReviewRevision(
1200
1216
  root: string,
1201
1217
  taskId: string,
1202
1218
  projection: AssuranceProjectionResult,
@@ -1221,7 +1237,11 @@ async function ensureTaskReviewRevision(
1221
1237
  lifecycle: projection.projection.lifecycle,
1222
1238
  artifactState: projection.projection.artifact_state,
1223
1239
  risk: record.intent_snapshot.risk,
1224
- outcomes: reviewPreflightOutcomes(record.intent_snapshot.acceptance),
1240
+ // The same outcomes the Review snapshot is built from. A preflight stand-in
1241
+ // only matched the settled QA attestation because deterministic QA happens to
1242
+ // write that exact summary, so the submit-time digest comparison held by
1243
+ // coincidence rather than by construction.
1244
+ outcomes: qaOutcomes(record),
1225
1245
  });
1226
1246
  return {
1227
1247
  contract: "assurance_kernel/review_revision/v1",
@@ -1266,8 +1286,22 @@ async function reconcileReviewRevisionRefs(root: string): Promise<{ removed: str
1266
1286
  return reconcileReviewRefs(root, live);
1267
1287
  }
1268
1288
 
1289
+ /**
1290
+ * The coordinator port supplies a `HostContext`, which carries no UI. Pi hands
1291
+ * its full `ExtensionContext` through at runtime, so the notice still reaches
1292
+ * the user; a host that does not is left un-notified rather than throwing from
1293
+ * inside an authority commit, where a notification has no authority anyway.
1294
+ */
1295
+ function notifyHost(ctx: HostContext, key: string, message: string, level: "warning" | "error"): void {
1296
+ const ui = (ctx as Partial<UiContext>).ui;
1297
+ if (ui) notifyOnce({ ui }, key, message, level);
1298
+ }
1299
+
1269
1300
  async function applyAssuranceVerdict(
1270
- ctx: ExtensionContext,
1301
+ // The coordinator port hands these a `HostContext`, not the Pi
1302
+ // `ExtensionContext`. Both functions only ever read `cwd`; declaring the
1303
+ // wider host type made the port assignment unsound.
1304
+ ctx: HostContext,
1271
1305
  snapshot: SnapshotDescriptor,
1272
1306
  verdict: AssuranceVerdict,
1273
1307
  invocation: InvocationToken,
@@ -1352,7 +1386,7 @@ async function applyAssuranceVerdict(
1352
1386
  const parked = (result.record as { findings?: Array<{ kind: string; status: string }> }).findings?.some(
1353
1387
  (finding) => finding.kind === "replan_required" && finding.status === "open",
1354
1388
  );
1355
- if (parked) notifyOnce(
1389
+ if (parked) notifyHost(
1356
1390
  ctx,
1357
1391
  `rework-parked:${snapshot.task_id}`,
1358
1392
  `rework applied: review parked for replan with ${findings.length} finding(s)`,
@@ -1431,10 +1465,17 @@ async function buildAssuranceSnapshot(
1431
1465
  assertRunnerCompatible(descriptor, runner);
1432
1466
  descriptors.set(item.id, descriptor);
1433
1467
  }
1434
- const reviewRevision = record.record.contract === "assurance_kernel/task_record/v4"
1468
+ // `git_base_head` is optional on the read shape because v3 records carry
1469
+ // none, so the contract test alone does not prove it is present.
1470
+ const baseHead = record.record.contract === "assurance_kernel/task_record/v4"
1471
+ ? record.record.git_base_head
1472
+ : undefined;
1473
+ if (record.record.contract === "assurance_kernel/task_record/v4" && !baseHead)
1474
+ throw new Error("TaskRecord v4 is missing its Enrollment git_base_head");
1475
+ const reviewRevision = baseHead
1435
1476
  ? {
1436
1477
  contract: "assurance_kernel/review_revision_identity/v1" as const,
1437
- base_head: record.record.git_base_head,
1478
+ base_head: baseHead,
1438
1479
  review_commit: "",
1439
1480
  review_tree: "",
1440
1481
  manifest_digest: "",
@@ -1507,15 +1548,6 @@ async function buildAssuranceSnapshot(
1507
1548
  };
1508
1549
  }
1509
1550
 
1510
- function reviewPreflightOutcomes(
1511
- acceptance: Array<{ id: string }>,
1512
- ): Record<string, { status: "passed"; summary: string }> {
1513
- const summary = `host-attested QA: all ${acceptance.length} fixed verification descriptor(s) passed`;
1514
- return Object.fromEntries(
1515
- acceptance.map((item) => [item.id, { status: "passed" as const, summary }]),
1516
- );
1517
- }
1518
-
1519
1551
  function qaOutcomes(
1520
1552
  record: NonNullable<TaskRecordRead["record"]>,
1521
1553
  ): Record<string, { status: "passed" | "failed" | "blocked"; summary: string }> {
@@ -1617,7 +1649,7 @@ function authorityPair(): Promise<{ registry: MutationAuthorityRegistry; app: Ca
1617
1649
  }
1618
1650
 
1619
1651
  async function executeOrdinaryOperation(
1620
- ctx: ExtensionContext,
1652
+ ctx: HostContext,
1621
1653
  input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown } },
1622
1654
  ): Promise<unknown> {
1623
1655
  const { app } = await authorityPair();
@@ -1638,7 +1670,10 @@ async function executeOrdinaryOperation(
1638
1670
  now: new Date().toISOString(),
1639
1671
  });
1640
1672
  if (operation.op === "freeze_artifacts" || operation.op === "stop")
1641
- stagePlanningArtifactTransition(ctx.cwd, result.record);
1673
+ stagePlanningArtifactTransition(
1674
+ ctx.cwd,
1675
+ (result as { record: Parameters<typeof stagePlanningArtifactTransition>[1] }).record,
1676
+ );
1642
1677
  return result;
1643
1678
  } catch (error) {
1644
1679
  if (priorBytes) {
@@ -1741,18 +1776,24 @@ function toolResult(text: string, details?: Record<string, unknown>) {
1741
1776
  * Pi or Hyper adapter upgrade cycles pass a live nested-object Tool-call
1742
1777
  * probe at least 30 days apart.
1743
1778
  */
1744
- function prepareActionArgs(args: unknown): unknown {
1745
- if (args === null || typeof args !== "object" || Array.isArray(args)) return args;
1779
+ /**
1780
+ * Pre-schema normalizer: some hosts deliver `action` as a JSON string. The Pi
1781
+ * runtime validates the returned value against the Tool schema immediately
1782
+ * after this shim, so the parameter type is the schema's, not a claim this
1783
+ * function makes about unvalidated input.
1784
+ */
1785
+ function prepareActionArgs<Params>(args: unknown): Params {
1786
+ if (args === null || typeof args !== "object" || Array.isArray(args)) return args as Params;
1746
1787
  const input = args as Record<string, unknown>;
1747
- if (typeof input.action !== "string") return input;
1788
+ if (typeof input.action !== "string") return input as Params;
1748
1789
  try {
1749
1790
  const parsed: unknown = JSON.parse(input.action);
1750
1791
  if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
1751
- return { ...input, action: parsed };
1792
+ return { ...input, action: parsed } as Params;
1752
1793
  } catch {
1753
1794
  // Unchanged input keeps the normal host schema error authoritative.
1754
1795
  }
1755
- return input;
1796
+ return input as Params;
1756
1797
  }
1757
1798
 
1758
1799
  function stagePlanningArtifactTransition(root: string, record: {
@@ -1785,7 +1826,9 @@ function stagePlanningArtifactTransition(root: string, record: {
1785
1826
  function failCanaryTool(
1786
1827
  taskId: string,
1787
1828
  operation: string,
1788
- state: "blocked" | "failed" | "authority_conflict" | "settlement_unknown",
1829
+ // `review_preparation_failed` is a declared ToolFailureV1 state and a
1830
+ // documented Loop recovery path; omitting it here made it unreportable.
1831
+ state: ToolFailureV1["state"],
1789
1832
  code: string,
1790
1833
  message: string,
1791
1834
  nextAction: string,
@@ -1,4 +1,4 @@
1
- import { DynamicBorder, type ExtensionAPI, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
1
+ import { DynamicBorder, type ExtensionAPI, type ExtensionContext, type Theme, type ThemeColor } from "@earendil-works/pi-coding-agent";
2
2
  import { Container, SelectList, Text, type Component, type SelectItem } from "@earendil-works/pi-tui";
3
3
 
4
4
  export const USER_ATTENTION_EVENT = "immune-brain:user-attention.v1" as const;
@@ -75,7 +75,7 @@ export interface AuthorityDialogOptions<T extends string> {
75
75
  }
76
76
 
77
77
  type EventPublisher = Pick<ExtensionAPI, "events">;
78
- type UiContext = Pick<ExtensionContext, "ui">;
78
+ export type UiContext = Pick<ExtensionContext, "ui">;
79
79
 
80
80
  const terminalRailUis = new WeakSet<object>();
81
81
  const deliveredNotifications = new WeakMap<object, Set<string>>();
@@ -373,7 +373,9 @@ function emitAttention(pi: EventPublisher, event: UserAttentionEventV1): void {
373
373
  }
374
374
 
375
375
  function formatTaskRailState(state: TaskRailState, theme?: Theme): string {
376
- const symbolAndColor: Record<TaskRailState, { symbol: string; color: string }> = {
376
+ // Typed as the theme's own colour union: a plain `string` here silently
377
+ // accepted a name the theme cannot resolve.
378
+ const symbolAndColor: Record<TaskRailState, { symbol: string; color: ThemeColor }> = {
377
379
  Planning: { symbol: "●", color: "muted" },
378
380
  "Approval required": { symbol: "▲", color: "accent" },
379
381
  Working: { symbol: "●", color: "accent" },
@@ -383,7 +385,7 @@ function formatTaskRailState(state: TaskRailState, theme?: Theme): string {
383
385
  Completed: { symbol: "✓", color: "success" },
384
386
  Stopped: { symbol: "■", color: "muted" },
385
387
  };
386
- const cfg = symbolAndColor[state] ?? { symbol: "●", color: "dim" };
388
+ const cfg: { symbol: string; color: ThemeColor } = symbolAndColor[state] ?? { symbol: "●", color: "dim" };
387
389
  if (!theme) return `${cfg.symbol} ${state}`;
388
390
  return `${theme.fg(cfg.color, cfg.symbol)} ${theme.fg(cfg.color, state)}`;
389
391
  }
@@ -1,8 +1,10 @@
1
1
  // Extension-local runtime adapter: type-isolated, executable stub.
2
2
  // Extensions import this file directly (relative path, resolvable by the Pi
3
3
  // extension loader); it forwards to the real Kernel modules via dynamic
4
- // import. The runtime source graph (with its pre-existing type debt) is never
5
- // type-checked from the extension.
4
+ // import. Shared contracts are re-exported as types from the real Kernel
5
+ // modules: `export type` is erased at compile time, so the extension still
6
+ // carries no static runtime import, but a Kernel contract change now breaks
7
+ // the extension's build instead of silently drifting from it.
6
8
 
7
9
  // --- Types (structural contracts, no runtime import) ---
8
10
  export interface EnrollmentCapabilityBinding {
@@ -178,40 +180,15 @@ export interface TaskRecordRead {
178
180
 
179
181
  // --- Assurance projection (host-neutral Kernel facts, not exported from the
180
182
  // public Kernel index) ---
181
- export interface AssuranceAuthorizationReadiness {
182
- state: "resolve_user_decision" | "none";
183
- blocked: string | null;
184
- }
185
- export interface AssuranceProjection {
186
- record_revision: string;
187
- workspace_revision: string;
188
- intent_revision: number;
189
- intent_content_hash: string;
190
- diff_hash: string;
191
- lifecycle: "active" | "done" | "stopped" | "";
192
- artifact_state: "active" | "frozen" | "";
193
- risk: "routine" | "material" | "critical" | "";
194
- next_obligation: "resolve_findings" | "resolve_user_decision" | "revise_intent" | "submit_assurance" | "run_qa" | "run_review" | "complete" | "none";
195
- fresh_acceptance_ids: string[];
196
- missing_acceptance_ids: string[];
197
- stale_attestation_ids: string[];
198
- fresh_approval_kinds: string[];
199
- missing_approval_kinds: string[];
200
- blocking_finding_ids: string[];
201
- unresolved_user_decision_ids: string[];
202
- replan_required_ids: string[];
203
- independence_violations: string[];
204
- open_user_decision_count: number;
205
- completion_ready: boolean;
206
- authorization: AssuranceAuthorizationReadiness;
207
- }
208
- export interface AssuranceProjectionResult {
209
- contract: "assurance_kernel/assurance_projection/v1";
210
- task_id: string;
211
- error: string | null;
212
- claim: { task_id: string; lifecycle_status: string } | null;
213
- projection: AssuranceProjection;
214
- }
183
+ export type {
184
+ AssuranceAuthorizationReadiness,
185
+ AssuranceProjection,
186
+ AssuranceProjectionResult,
187
+ } from "../runtime/kernel/assurance_projection";
188
+ import type {
189
+ AssuranceAuthorizationReadiness,
190
+ AssuranceProjectionResult,
191
+ } from "../runtime/kernel/assurance_projection";
215
192
 
216
193
  // --- Runtime forwarding (dynamic import keeps the graph out of tsc) ---
217
194
  function kernelPath(module: string): string {
@@ -349,12 +326,8 @@ export interface TaskIntentV1 {
349
326
  acceptance: Array<{ id: string; assertion: string; verification: string }>;
350
327
  scope_hint: string[];
351
328
  }
352
- export interface TaskIntentRead {
353
- token: object;
354
- content_hash: string;
355
- intent: TaskIntentV1;
356
- intent_ref: { path: string; revision: number; content_hash: string };
357
- }
329
+ export type { ReadTaskIntentResult as TaskIntentRead } from "../runtime/kernel/intent";
330
+ import type { ReadTaskIntentResult as TaskIntentRead } from "../runtime/kernel/intent";
358
331
  export interface WorkspaceRead {
359
332
  revision: string;
360
333
  state: { contract: string; current_working: string | null };
@@ -420,7 +393,7 @@ export async function projectAssurance(
420
393
  },
421
394
  ): Promise<AssuranceProjectionResult> {
422
395
  const mod = await import(/* @vite-ignore */ kernelPath("assurance_projection"));
423
- return mod.projectAssurance(root, taskId, diffProvider) as unknown as AssuranceProjectionResult;
396
+ return mod.projectAssurance(root, taskId, diffProvider) as AssuranceProjectionResult;
424
397
  }
425
398
 
426
399
  export async function deriveAssuranceAuthorization(input: {
@@ -428,5 +401,5 @@ export async function deriveAssuranceAuthorization(input: {
428
401
  open_user_decision_count: number;
429
402
  }): Promise<AssuranceAuthorizationReadiness> {
430
403
  const mod = await import(/* @vite-ignore */ kernelPath("assurance_projection"));
431
- return mod.deriveAssuranceAuthorization(input) as unknown as AssuranceAuthorizationReadiness;
404
+ return mod.deriveAssuranceAuthorization(input) as AssuranceAuthorizationReadiness;
432
405
  }