immune-brain 3.6.3 → 3.6.5

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 (31) hide show
  1. package/package.json +6 -3
  2. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  3. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +117 -47
  4. package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +6 -4
  5. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +17 -44
  6. package/plugins/immune-brain/dist/claude/mcp-server.mjs +213 -81
  7. package/plugins/immune-brain/dist/imm-loop.md +8 -2
  8. package/plugins/immune-brain/dist/imm-planner.md +9 -4
  9. package/plugins/immune-brain/runtime/assurance/coordinator.ts +35 -2
  10. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +23 -13
  11. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +133 -45
  12. package/plugins/immune-brain/runtime/claude/mcp_server.ts +14 -1
  13. package/plugins/immune-brain/runtime/claude/review_host.ts +0 -10
  14. package/plugins/immune-brain/runtime/commands/kernel.ts +15 -13
  15. package/plugins/immune-brain/runtime/github_issue_tracker.ts +107 -11
  16. package/plugins/immune-brain/runtime/kernel/application.ts +6 -0
  17. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
  18. package/plugins/immune-brain/runtime/kernel/batch_authority.ts +407 -0
  19. package/plugins/immune-brain/runtime/kernel/canary_application.ts +17 -8
  20. package/plugins/immune-brain/runtime/kernel/enrollment.ts +76 -13
  21. package/plugins/immune-brain/runtime/kernel/index.ts +2 -0
  22. package/plugins/immune-brain/runtime/kernel/intent.ts +67 -23
  23. package/plugins/immune-brain/runtime/kernel/observation.ts +2 -0
  24. package/plugins/immune-brain/runtime/kernel/reducer.ts +34 -8
  25. package/plugins/immune-brain/runtime/kernel/storage.ts +17 -2
  26. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +1 -1
  27. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +0 -4
  28. package/plugins/immune-brain/runtime/kernel/types.ts +10 -0
  29. package/plugins/immune-brain/runtime/kernel/validation.ts +10 -6
  30. package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +2 -2
  31. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "3.6.3",
3
+ "version": "3.6.5",
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.3",
3
+ "version": "3.6.5",
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";
@@ -175,6 +177,7 @@ export type { AssuranceRole } from "./pi-canary-assurance";
175
177
  export type AuthorizeOperation =
176
178
  | "approve-breaking-intent-revision"
177
179
  | "resolve-user-decision"
180
+ | "authorize-rework"
178
181
  | "stop";
179
182
 
180
183
  export interface SnapshotDescriptorInput {
@@ -262,11 +265,18 @@ type LoopToolAction =
262
265
  context: Record<string, unknown>;
263
266
  };
264
267
 
265
- export default function (
266
- pi: ExtensionAPI,
268
+ /**
269
+ * The exact ports object the Pi Assurance progression runs on.
270
+ *
271
+ * This lived as an inline literal inside the anonymous default export, so no
272
+ * test could ever obtain what production wires; every host adapter defect that
273
+ * reached a published plugin lived in this object. Exporting the factory lets
274
+ * the dual-host conformance suite drive the real thing.
275
+ */
276
+ export function createPiAssuranceProgressionPorts(
267
277
  dependencies: CanaryWorkExtensionDependencies = {},
268
- ) {
269
- const progression = new AssuranceProgression({
278
+ ): AssuranceProgressionPorts {
279
+ return {
270
280
  projectTask: (root, taskId) => projectAssuranceState(root, taskId),
271
281
  readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
272
282
  readTaskIntent: (root, taskId) => readTaskIntent(root, taskId),
@@ -294,7 +304,14 @@ export default function (
294
304
  qaOnAuthorityCommit: dependencies.qaOnAuthorityCommit,
295
305
  qaAfterAuthorityCommit: dependencies.qaAfterAuthorityCommit,
296
306
  qaJobTimeoutMs: dependencies.qaJobTimeoutMs,
297
- } satisfies AssuranceProgressionPorts);
307
+ } satisfies AssuranceProgressionPorts;
308
+ }
309
+
310
+ export default function (
311
+ pi: ExtensionAPI,
312
+ dependencies: CanaryWorkExtensionDependencies = {},
313
+ ) {
314
+ const progression = new AssuranceProgression(createPiAssuranceProgressionPorts(dependencies));
298
315
 
299
316
  let railContext: ExtensionContext | undefined;
300
317
  const refreshTaskRail = async (ctx: ExtensionContext) => {
@@ -419,6 +436,7 @@ export default function (
419
436
  Type.Object({ op: Type.Literal("advance_assurance") }),
420
437
  Type.Object({ op: Type.Literal("submit_review"), verdict: Type.Unknown() }),
421
438
  Type.Object({ op: Type.Literal("request_authorization") }),
439
+ Type.Object({ op: Type.Literal("request_stop") }),
422
440
  Type.Object({ op: Type.Literal("repair_authority_state") }),
423
441
  Type.Object({ op: Type.Literal("freeze_artifacts") }),
424
442
  Type.Object({
@@ -514,8 +532,8 @@ export default function (
514
532
  return failCanaryTool(taskId, action.op, "authority_conflict", "authority_repair_failed", message, blocked.next_action);
515
533
  }
516
534
  }
517
- if (action.op === "advance_assurance" || action.op === "request_authorization" || action.op === "submit_review" || action.op === "approve_breaking_intent_revision") {
518
- if ((action.op === "request_authorization" || action.op === "approve_breaking_intent_revision") && ctx.mode !== "tui")
535
+ if (action.op === "advance_assurance" || action.op === "request_stop" || action.op === "request_authorization" || action.op === "submit_review" || action.op === "approve_breaking_intent_revision") {
536
+ if ((action.op === "request_stop" || action.op === "request_authorization" || action.op === "approve_breaking_intent_revision") && ctx.mode !== "tui")
519
537
  return failCanaryTool(taskId, action.op, "blocked", "tui_required", "literal-user authorization is TUI-only", "invoke the TUI Tool");
520
538
  const result = action.op === "advance_assurance"
521
539
  ? await progression.advance(taskId, ctx, signal, (update) => {
@@ -531,7 +549,9 @@ export default function (
531
549
  ctx,
532
550
  (action as { next_intent?: unknown }).next_intent,
533
551
  )
534
- : await requestAuthorization(taskId, ctx);
552
+ : action.op === "request_stop"
553
+ ? await authorizeExactOperation(taskId, "stop", { ...ctx, signal: signal && ctx.signal ? AbortSignal.any([signal, ctx.signal]) : signal ?? ctx.signal })
554
+ : await requestAuthorization(taskId, ctx);
535
555
  const enriched = await enrichAssuranceResult(ctx, taskId, result as unknown as Record<string, unknown>);
536
556
  presentTaskRailResult(ctx, taskId, enriched);
537
557
  throwIfCanaryToolFailure(taskId, action.op, enriched);
@@ -680,7 +700,7 @@ export default function (
680
700
  });
681
701
 
682
702
  type AuthorizationOutcome =
683
- | { state: "applied"; operation: AuthorizeOperation; lifecycle?: string }
703
+ | { state: "applied"; operation: AuthorizeOperation; lifecycle?: string; delivery_error?: string }
684
704
  | { state: "cancelled"; operation: AuthorizeOperation; reason: string }
685
705
  | { state: "blocked"; reason: string };
686
706
 
@@ -710,12 +730,15 @@ export default function (
710
730
  let invocation: InvocationToken;
711
731
  const authorizationGeneration = progression.sessionGenerationValue();
712
732
  try {
733
+ if (operation === "stop" && progression.active(taskId)?.state === "running")
734
+ throw new Error("assurance operation is already running");
713
735
  invocation = progression.openInvocation(taskId);
714
736
  } catch (error) {
715
737
  const reason = error instanceof Error ? error.message : String(error);
716
738
  notifyOnce(ctx, `authorization-open:${taskId}:${reason}`, `cannot authorize ${taskId}: ${reason}`, "error");
717
739
  return { state: "blocked", reason };
718
740
  }
741
+ try {
719
742
  const projection = await projectAssuranceState(ctx.cwd, taskId);
720
743
  if (projection.error || !projection.claim) {
721
744
  const reason = projection.error ?? "no active backend claim";
@@ -825,6 +848,9 @@ export default function (
825
848
  `Resolution: ${userDecisionOperation.resolution}`,
826
849
  ]
827
850
  : []),
851
+ ...(operation === "authorize-rework"
852
+ ? [`Replan boundaries: ${projection.projection.replan_required_ids.join(", ")}`]
853
+ : []),
828
854
  ...(nextIntent
829
855
  ? [
830
856
  `Next Intent: rev ${nextIntent.revision} (${nextIntentHash})`,
@@ -872,16 +898,27 @@ export default function (
872
898
  if (nextIntent) {
873
899
  restoreStagedIntent();
874
900
  }
875
- if (operation !== "stop" && operation !== "approve-breaking-intent-revision")
901
+ if (
902
+ operation !== "stop" &&
903
+ operation !== "approve-breaking-intent-revision" &&
904
+ operation !== "authorize-rework"
905
+ )
876
906
  await recordCancelledUserDecision(ctx, taskId, operation, snapshotDigestRef).catch(() => undefined);
877
907
  progression.closeInvocation(invocation);
908
+ if (operation === "stop" && !ctx.signal?.aborted && !(error instanceof Error && error.name === "AbortError")) {
909
+ return { state: "blocked", reason: "native stop confirmation failed; retry request_stop in this Host" };
910
+ }
878
911
  return { state: "cancelled", operation, reason: "confirmation aborted" };
879
912
  }
880
- if (!confirmed) {
913
+ if (!confirmed || ctx.signal?.aborted) {
881
914
  if (nextIntent) {
882
915
  restoreStagedIntent();
883
916
  }
884
- if (operation !== "stop" && operation !== "approve-breaking-intent-revision")
917
+ if (
918
+ operation !== "stop" &&
919
+ operation !== "approve-breaking-intent-revision" &&
920
+ operation !== "authorize-rework"
921
+ )
885
922
  await recordCancelledUserDecision(ctx, taskId, operation, snapshotDigestRef).catch(() => undefined);
886
923
  progression.closeInvocation(invocation);
887
924
  return { state: "cancelled", operation, reason: "cancelled" };
@@ -894,7 +931,6 @@ export default function (
894
931
  progression.closeInvocation(invocation);
895
932
  return { state: "blocked", reason: "session changed; confirmation discarded" };
896
933
  }
897
- try {
898
934
  // Linearization point: only this fresh affirmative continuation
899
935
  // may mint/apply; timeout/cancel already won open -> cancelled.
900
936
  try {
@@ -916,14 +952,16 @@ export default function (
916
952
  };
917
953
  }
918
954
  const exactOperation = operation === "stop"
919
- ? { op: "stop" as const, reason: "literal user stopped task parked for replan" }
955
+ ? { op: "stop" as const, reason: "literal user requested task stop" }
920
956
  : operation === "approve-breaking-intent-revision"
921
957
  ? {
922
958
  op: "approve_breaking_intent_revision" as const,
923
959
  next_intent: nextIntent!,
924
960
  next_intent_ref: nextIntentRef!,
925
961
  }
926
- : userDecisionOperation!;
962
+ : operation === "authorize-rework"
963
+ ? { op: "authorize_rework" as const }
964
+ : userDecisionOperation!;
927
965
  // The exact host-built operation is shared by capability digest and
928
966
  // application payload; command arguments cannot inject authority fields.
929
967
  try {
@@ -964,9 +1002,11 @@ export default function (
964
1002
  diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
965
1003
  now,
966
1004
  })) as unknown as { record: { lifecycle: string; artifact_state: string; intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } } };
1005
+ if (exactOperation.op === "stop") progression.releaseStoppedReview(taskId);
967
1006
  if (
968
- exactOperation.op === "stop"
969
- || exactOperation.op === "approve_breaking_intent_revision"
1007
+ exactOperation.op === "stop" ||
1008
+ exactOperation.op === "authorize_rework" ||
1009
+ exactOperation.op === "approve_breaking_intent_revision"
970
1010
  ) stagePlanningArtifactTransition(ctx.cwd, result.record);
971
1011
  return { state: "applied", operation, lifecycle: result.record.lifecycle };
972
1012
  } catch (error) {
@@ -978,6 +1018,13 @@ export default function (
978
1018
  }
979
1019
  } catch (error) {
980
1020
  const reason = error instanceof Error ? error.message : String(error);
1021
+ if (operation === "stop") {
1022
+ const terminal = await projectAssuranceState(ctx.cwd, taskId).catch(() => null);
1023
+ if (terminal && !terminal.error && terminal.projection.lifecycle === "stopped") {
1024
+ progression.releaseStoppedReview(taskId);
1025
+ return { state: "applied", operation, lifecycle: "stopped", delivery_error: reason };
1026
+ }
1027
+ }
981
1028
  notifyOnce(ctx, `authorization-apply:${taskId}:${operation}:${reason}`, `authorize failed: ${reason}`, "error");
982
1029
  return { state: "blocked", reason };
983
1030
  } finally {
@@ -999,9 +1046,6 @@ export default function (
999
1046
  return { state: "blocked", reason: "TaskRecord changed while deriving authorization operation" };
1000
1047
  const derived = deriveAuthorizationOperation({
1001
1048
  readiness: projection.projection.authorization,
1002
- hasOpenReplanRequired: read.record.findings.some(
1003
- (finding) => finding.kind === "replan_required" && finding.status === "open",
1004
- ),
1005
1049
  });
1006
1050
  if ("blocked" in derived) return { state: "blocked", reason: derived.blocked };
1007
1051
  return authorizeExactOperation(taskId, derived.operation, ctx);
@@ -1014,15 +1058,14 @@ export default function (
1014
1058
 
1015
1059
  export type DerivedAuthorizationOperation =
1016
1060
  | "resolve-user-decision"
1017
- | "stop";
1061
+ | "authorize-rework";
1018
1062
 
1019
1063
  // Kernel projection is the sole source of authorization readiness.
1020
1064
  export function deriveAuthorizationOperation(input: {
1021
1065
  readiness: AssuranceAuthorizationReadiness;
1022
- hasOpenReplanRequired?: boolean;
1023
1066
  }): { operation: DerivedAuthorizationOperation } | { blocked: string } {
1024
- if (input.hasOpenReplanRequired) return { operation: "stop" };
1025
1067
  if (input.readiness.state === "resolve_user_decision") return { operation: "resolve-user-decision" };
1068
+ if (input.readiness.state === "authorize_rework") return { operation: "authorize-rework" };
1026
1069
  if (input.readiness.blocked) return { blocked: input.readiness.blocked };
1027
1070
  return { blocked: "no unique host-derived authorization operation" };
1028
1071
  }
@@ -1196,7 +1239,7 @@ async function projectAssuranceState(root: string, taskId: string): Promise<Assu
1196
1239
  * Publish and prove the task-scoped synthetic revision for a v4 record. v3
1197
1240
  * records keep the legacy full-source bundle and return null here.
1198
1241
  */
1199
- async function ensureTaskReviewRevision(
1242
+ export async function ensureTaskReviewRevision(
1200
1243
  root: string,
1201
1244
  taskId: string,
1202
1245
  projection: AssuranceProjectionResult,
@@ -1221,7 +1264,11 @@ async function ensureTaskReviewRevision(
1221
1264
  lifecycle: projection.projection.lifecycle,
1222
1265
  artifactState: projection.projection.artifact_state,
1223
1266
  risk: record.intent_snapshot.risk,
1224
- outcomes: reviewPreflightOutcomes(record.intent_snapshot.acceptance),
1267
+ // The same outcomes the Review snapshot is built from. A preflight stand-in
1268
+ // only matched the settled QA attestation because deterministic QA happens to
1269
+ // write that exact summary, so the submit-time digest comparison held by
1270
+ // coincidence rather than by construction.
1271
+ outcomes: qaOutcomes(record),
1225
1272
  });
1226
1273
  return {
1227
1274
  contract: "assurance_kernel/review_revision/v1",
@@ -1266,8 +1313,22 @@ async function reconcileReviewRevisionRefs(root: string): Promise<{ removed: str
1266
1313
  return reconcileReviewRefs(root, live);
1267
1314
  }
1268
1315
 
1316
+ /**
1317
+ * The coordinator port supplies a `HostContext`, which carries no UI. Pi hands
1318
+ * its full `ExtensionContext` through at runtime, so the notice still reaches
1319
+ * the user; a host that does not is left un-notified rather than throwing from
1320
+ * inside an authority commit, where a notification has no authority anyway.
1321
+ */
1322
+ function notifyHost(ctx: HostContext, key: string, message: string, level: "warning" | "error"): void {
1323
+ const ui = (ctx as Partial<UiContext>).ui;
1324
+ if (ui) notifyOnce({ ui }, key, message, level);
1325
+ }
1326
+
1269
1327
  async function applyAssuranceVerdict(
1270
- ctx: ExtensionContext,
1328
+ // The coordinator port hands these a `HostContext`, not the Pi
1329
+ // `ExtensionContext`. Both functions only ever read `cwd`; declaring the
1330
+ // wider host type made the port assignment unsound.
1331
+ ctx: HostContext,
1271
1332
  snapshot: SnapshotDescriptor,
1272
1333
  verdict: AssuranceVerdict,
1273
1334
  invocation: InvocationToken,
@@ -1352,7 +1413,7 @@ async function applyAssuranceVerdict(
1352
1413
  const parked = (result.record as { findings?: Array<{ kind: string; status: string }> }).findings?.some(
1353
1414
  (finding) => finding.kind === "replan_required" && finding.status === "open",
1354
1415
  );
1355
- if (parked) notifyOnce(
1416
+ if (parked) notifyHost(
1356
1417
  ctx,
1357
1418
  `rework-parked:${snapshot.task_id}`,
1358
1419
  `rework applied: review parked for replan with ${findings.length} finding(s)`,
@@ -1431,10 +1492,17 @@ async function buildAssuranceSnapshot(
1431
1492
  assertRunnerCompatible(descriptor, runner);
1432
1493
  descriptors.set(item.id, descriptor);
1433
1494
  }
1434
- const reviewRevision = record.record.contract === "assurance_kernel/task_record/v4"
1495
+ // `git_base_head` is optional on the read shape because v3 records carry
1496
+ // none, so the contract test alone does not prove it is present.
1497
+ const baseHead = record.record.contract === "assurance_kernel/task_record/v4"
1498
+ ? record.record.git_base_head
1499
+ : undefined;
1500
+ if (record.record.contract === "assurance_kernel/task_record/v4" && !baseHead)
1501
+ throw new Error("TaskRecord v4 is missing its Enrollment git_base_head");
1502
+ const reviewRevision = baseHead
1435
1503
  ? {
1436
1504
  contract: "assurance_kernel/review_revision_identity/v1" as const,
1437
- base_head: record.record.git_base_head,
1505
+ base_head: baseHead,
1438
1506
  review_commit: "",
1439
1507
  review_tree: "",
1440
1508
  manifest_digest: "",
@@ -1507,15 +1575,6 @@ async function buildAssuranceSnapshot(
1507
1575
  };
1508
1576
  }
1509
1577
 
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
1578
  function qaOutcomes(
1520
1579
  record: NonNullable<TaskRecordRead["record"]>,
1521
1580
  ): Record<string, { status: "passed" | "failed" | "blocked"; summary: string }> {
@@ -1617,7 +1676,7 @@ function authorityPair(): Promise<{ registry: MutationAuthorityRegistry; app: Ca
1617
1676
  }
1618
1677
 
1619
1678
  async function executeOrdinaryOperation(
1620
- ctx: ExtensionContext,
1679
+ ctx: HostContext,
1621
1680
  input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown } },
1622
1681
  ): Promise<unknown> {
1623
1682
  const { app } = await authorityPair();
@@ -1638,7 +1697,10 @@ async function executeOrdinaryOperation(
1638
1697
  now: new Date().toISOString(),
1639
1698
  });
1640
1699
  if (operation.op === "freeze_artifacts" || operation.op === "stop")
1641
- stagePlanningArtifactTransition(ctx.cwd, result.record);
1700
+ stagePlanningArtifactTransition(
1701
+ ctx.cwd,
1702
+ (result as { record: Parameters<typeof stagePlanningArtifactTransition>[1] }).record,
1703
+ );
1642
1704
  return result;
1643
1705
  } catch (error) {
1644
1706
  if (priorBytes) {
@@ -1741,18 +1803,24 @@ function toolResult(text: string, details?: Record<string, unknown>) {
1741
1803
  * Pi or Hyper adapter upgrade cycles pass a live nested-object Tool-call
1742
1804
  * probe at least 30 days apart.
1743
1805
  */
1744
- function prepareActionArgs(args: unknown): unknown {
1745
- if (args === null || typeof args !== "object" || Array.isArray(args)) return args;
1806
+ /**
1807
+ * Pre-schema normalizer: some hosts deliver `action` as a JSON string. The Pi
1808
+ * runtime validates the returned value against the Tool schema immediately
1809
+ * after this shim, so the parameter type is the schema's, not a claim this
1810
+ * function makes about unvalidated input.
1811
+ */
1812
+ function prepareActionArgs<Params>(args: unknown): Params {
1813
+ if (args === null || typeof args !== "object" || Array.isArray(args)) return args as Params;
1746
1814
  const input = args as Record<string, unknown>;
1747
- if (typeof input.action !== "string") return input;
1815
+ if (typeof input.action !== "string") return input as Params;
1748
1816
  try {
1749
1817
  const parsed: unknown = JSON.parse(input.action);
1750
1818
  if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
1751
- return { ...input, action: parsed };
1819
+ return { ...input, action: parsed } as Params;
1752
1820
  } catch {
1753
1821
  // Unchanged input keeps the normal host schema error authoritative.
1754
1822
  }
1755
- return input;
1823
+ return input as Params;
1756
1824
  }
1757
1825
 
1758
1826
  function stagePlanningArtifactTransition(root: string, record: {
@@ -1785,7 +1853,9 @@ function stagePlanningArtifactTransition(root: string, record: {
1785
1853
  function failCanaryTool(
1786
1854
  taskId: string,
1787
1855
  operation: string,
1788
- state: "blocked" | "failed" | "authority_conflict" | "settlement_unknown",
1856
+ // `review_preparation_failed` is a declared ToolFailureV1 state and a
1857
+ // documented Loop recovery path; omitting it here made it unreportable.
1858
+ state: ToolFailureV1["state"],
1789
1859
  code: string,
1790
1860
  message: string,
1791
1861
  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
  }