immune-brain 3.6.4 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "3.6.4",
3
+ "version": "3.6.5",
4
4
  "description": "Immune-Brain agent skill system",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "3.6.4",
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"
@@ -177,6 +177,7 @@ export type { AssuranceRole } from "./pi-canary-assurance";
177
177
  export type AuthorizeOperation =
178
178
  | "approve-breaking-intent-revision"
179
179
  | "resolve-user-decision"
180
+ | "authorize-rework"
180
181
  | "stop";
181
182
 
182
183
  export interface SnapshotDescriptorInput {
@@ -435,6 +436,7 @@ export default function (
435
436
  Type.Object({ op: Type.Literal("advance_assurance") }),
436
437
  Type.Object({ op: Type.Literal("submit_review"), verdict: Type.Unknown() }),
437
438
  Type.Object({ op: Type.Literal("request_authorization") }),
439
+ Type.Object({ op: Type.Literal("request_stop") }),
438
440
  Type.Object({ op: Type.Literal("repair_authority_state") }),
439
441
  Type.Object({ op: Type.Literal("freeze_artifacts") }),
440
442
  Type.Object({
@@ -530,8 +532,8 @@ export default function (
530
532
  return failCanaryTool(taskId, action.op, "authority_conflict", "authority_repair_failed", message, blocked.next_action);
531
533
  }
532
534
  }
533
- if (action.op === "advance_assurance" || action.op === "request_authorization" || action.op === "submit_review" || action.op === "approve_breaking_intent_revision") {
534
- 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")
535
537
  return failCanaryTool(taskId, action.op, "blocked", "tui_required", "literal-user authorization is TUI-only", "invoke the TUI Tool");
536
538
  const result = action.op === "advance_assurance"
537
539
  ? await progression.advance(taskId, ctx, signal, (update) => {
@@ -547,7 +549,9 @@ export default function (
547
549
  ctx,
548
550
  (action as { next_intent?: unknown }).next_intent,
549
551
  )
550
- : 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);
551
555
  const enriched = await enrichAssuranceResult(ctx, taskId, result as unknown as Record<string, unknown>);
552
556
  presentTaskRailResult(ctx, taskId, enriched);
553
557
  throwIfCanaryToolFailure(taskId, action.op, enriched);
@@ -696,7 +700,7 @@ export default function (
696
700
  });
697
701
 
698
702
  type AuthorizationOutcome =
699
- | { state: "applied"; operation: AuthorizeOperation; lifecycle?: string }
703
+ | { state: "applied"; operation: AuthorizeOperation; lifecycle?: string; delivery_error?: string }
700
704
  | { state: "cancelled"; operation: AuthorizeOperation; reason: string }
701
705
  | { state: "blocked"; reason: string };
702
706
 
@@ -726,12 +730,15 @@ export default function (
726
730
  let invocation: InvocationToken;
727
731
  const authorizationGeneration = progression.sessionGenerationValue();
728
732
  try {
733
+ if (operation === "stop" && progression.active(taskId)?.state === "running")
734
+ throw new Error("assurance operation is already running");
729
735
  invocation = progression.openInvocation(taskId);
730
736
  } catch (error) {
731
737
  const reason = error instanceof Error ? error.message : String(error);
732
738
  notifyOnce(ctx, `authorization-open:${taskId}:${reason}`, `cannot authorize ${taskId}: ${reason}`, "error");
733
739
  return { state: "blocked", reason };
734
740
  }
741
+ try {
735
742
  const projection = await projectAssuranceState(ctx.cwd, taskId);
736
743
  if (projection.error || !projection.claim) {
737
744
  const reason = projection.error ?? "no active backend claim";
@@ -841,6 +848,9 @@ export default function (
841
848
  `Resolution: ${userDecisionOperation.resolution}`,
842
849
  ]
843
850
  : []),
851
+ ...(operation === "authorize-rework"
852
+ ? [`Replan boundaries: ${projection.projection.replan_required_ids.join(", ")}`]
853
+ : []),
844
854
  ...(nextIntent
845
855
  ? [
846
856
  `Next Intent: rev ${nextIntent.revision} (${nextIntentHash})`,
@@ -888,16 +898,27 @@ export default function (
888
898
  if (nextIntent) {
889
899
  restoreStagedIntent();
890
900
  }
891
- if (operation !== "stop" && operation !== "approve-breaking-intent-revision")
901
+ if (
902
+ operation !== "stop" &&
903
+ operation !== "approve-breaking-intent-revision" &&
904
+ operation !== "authorize-rework"
905
+ )
892
906
  await recordCancelledUserDecision(ctx, taskId, operation, snapshotDigestRef).catch(() => undefined);
893
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
+ }
894
911
  return { state: "cancelled", operation, reason: "confirmation aborted" };
895
912
  }
896
- if (!confirmed) {
913
+ if (!confirmed || ctx.signal?.aborted) {
897
914
  if (nextIntent) {
898
915
  restoreStagedIntent();
899
916
  }
900
- if (operation !== "stop" && operation !== "approve-breaking-intent-revision")
917
+ if (
918
+ operation !== "stop" &&
919
+ operation !== "approve-breaking-intent-revision" &&
920
+ operation !== "authorize-rework"
921
+ )
901
922
  await recordCancelledUserDecision(ctx, taskId, operation, snapshotDigestRef).catch(() => undefined);
902
923
  progression.closeInvocation(invocation);
903
924
  return { state: "cancelled", operation, reason: "cancelled" };
@@ -910,7 +931,6 @@ export default function (
910
931
  progression.closeInvocation(invocation);
911
932
  return { state: "blocked", reason: "session changed; confirmation discarded" };
912
933
  }
913
- try {
914
934
  // Linearization point: only this fresh affirmative continuation
915
935
  // may mint/apply; timeout/cancel already won open -> cancelled.
916
936
  try {
@@ -932,14 +952,16 @@ export default function (
932
952
  };
933
953
  }
934
954
  const exactOperation = operation === "stop"
935
- ? { op: "stop" as const, reason: "literal user stopped task parked for replan" }
955
+ ? { op: "stop" as const, reason: "literal user requested task stop" }
936
956
  : operation === "approve-breaking-intent-revision"
937
957
  ? {
938
958
  op: "approve_breaking_intent_revision" as const,
939
959
  next_intent: nextIntent!,
940
960
  next_intent_ref: nextIntentRef!,
941
961
  }
942
- : userDecisionOperation!;
962
+ : operation === "authorize-rework"
963
+ ? { op: "authorize_rework" as const }
964
+ : userDecisionOperation!;
943
965
  // The exact host-built operation is shared by capability digest and
944
966
  // application payload; command arguments cannot inject authority fields.
945
967
  try {
@@ -980,9 +1002,11 @@ export default function (
980
1002
  diffProvider: (root: string, record: NonNullable<TaskRecordRead["record"]>) => diffSnapshotOf(root, record),
981
1003
  now,
982
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);
983
1006
  if (
984
- exactOperation.op === "stop"
985
- || exactOperation.op === "approve_breaking_intent_revision"
1007
+ exactOperation.op === "stop" ||
1008
+ exactOperation.op === "authorize_rework" ||
1009
+ exactOperation.op === "approve_breaking_intent_revision"
986
1010
  ) stagePlanningArtifactTransition(ctx.cwd, result.record);
987
1011
  return { state: "applied", operation, lifecycle: result.record.lifecycle };
988
1012
  } catch (error) {
@@ -994,6 +1018,13 @@ export default function (
994
1018
  }
995
1019
  } catch (error) {
996
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
+ }
997
1028
  notifyOnce(ctx, `authorization-apply:${taskId}:${operation}:${reason}`, `authorize failed: ${reason}`, "error");
998
1029
  return { state: "blocked", reason };
999
1030
  } finally {
@@ -1015,9 +1046,6 @@ export default function (
1015
1046
  return { state: "blocked", reason: "TaskRecord changed while deriving authorization operation" };
1016
1047
  const derived = deriveAuthorizationOperation({
1017
1048
  readiness: projection.projection.authorization,
1018
- hasOpenReplanRequired: read.record.findings.some(
1019
- (finding) => finding.kind === "replan_required" && finding.status === "open",
1020
- ),
1021
1049
  });
1022
1050
  if ("blocked" in derived) return { state: "blocked", reason: derived.blocked };
1023
1051
  return authorizeExactOperation(taskId, derived.operation, ctx);
@@ -1030,15 +1058,14 @@ export default function (
1030
1058
 
1031
1059
  export type DerivedAuthorizationOperation =
1032
1060
  | "resolve-user-decision"
1033
- | "stop";
1061
+ | "authorize-rework";
1034
1062
 
1035
1063
  // Kernel projection is the sole source of authorization readiness.
1036
1064
  export function deriveAuthorizationOperation(input: {
1037
1065
  readiness: AssuranceAuthorizationReadiness;
1038
- hasOpenReplanRequired?: boolean;
1039
1066
  }): { operation: DerivedAuthorizationOperation } | { blocked: string } {
1040
- if (input.hasOpenReplanRequired) return { operation: "stop" };
1041
1067
  if (input.readiness.state === "resolve_user_decision") return { operation: "resolve-user-decision" };
1068
+ if (input.readiness.state === "authorize_rework") return { operation: "authorize-rework" };
1042
1069
  if (input.readiness.blocked) return { blocked: input.readiness.blocked };
1043
1070
  return { blocked: "no unique host-derived authorization operation" };
1044
1071
  }
@@ -42,7 +42,7 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
42
42
  }
43
43
 
44
44
  // plugins/immune-brain/runtime/plugin_version.ts
45
- var PLUGIN_VERSION = "3.6.4";
45
+ var PLUGIN_VERSION = "3.6.5";
46
46
 
47
47
  // plugins/immune-brain/runtime/claude/interaction.ts
48
48
  import { createHash, randomUUID } from "node:crypto";
@@ -1351,6 +1351,8 @@ class AssuranceCoordinator {
1351
1351
  return this.sessionGeneration;
1352
1352
  }
1353
1353
  async advance(taskId, ctx, signal, onUpdate) {
1354
+ if (this.isInvocationOpen(taskId))
1355
+ return { state: "blocked", reason: "an authority invocation is already open" };
1354
1356
  const active = this.active(taskId);
1355
1357
  if (active?.state === "review_ready") {
1356
1358
  const reservation = this.reviewReservations.get(taskId);
@@ -1728,6 +1730,17 @@ class AssuranceCoordinator {
1728
1730
  }
1729
1731
  return { state: "blocked", reason: `Kernel requires ${settled.projection.next_obligation} after Review` };
1730
1732
  }
1733
+ isReviewVerdictValid(taskId, verdictInput) {
1734
+ const reservation = this.reviewReservations.get(taskId);
1735
+ if (!reservation)
1736
+ return false;
1737
+ try {
1738
+ parseAssuranceVerdict(verdictInput, reservation.snapshot);
1739
+ return true;
1740
+ } catch {
1741
+ return false;
1742
+ }
1743
+ }
1731
1744
  abandonReview(taskId, reason) {
1732
1745
  const reservation = this.reviewReservations.get(taskId);
1733
1746
  if (!reservation)
@@ -1743,6 +1756,12 @@ class AssuranceCoordinator {
1743
1756
  return { state: "blocked", code: "verdict_invalid", reason: "Review verdict correction is required before advancing" };
1744
1757
  return { state: "review_ready", operation: "review", operation_id: reservation.operationId, snapshot_digest: snapshotDigest(reservation.snapshot), review_bundle_digest: reservation.snapshot.review_bundle_digest ?? "", agent_params: reservation.hostReservation.dispatch };
1745
1758
  }
1759
+ releaseStoppedReview(taskId) {
1760
+ const reservation = this.reviewReservations.get(taskId);
1761
+ if (reservation)
1762
+ this.releaseReviewReservation(taskId, reservation);
1763
+ this.rejectedReviewOperations.delete(taskId);
1764
+ }
1746
1765
  releaseReviewReservation(taskId, reservation, rejectionReason) {
1747
1766
  if (this.reviewReservations.get(taskId) !== reservation)
1748
1767
  return;
@@ -3001,6 +3020,15 @@ function classifyIntentRevision(previous, next) {
3001
3020
  return "breaking";
3002
3021
  return next.revision > previous.revision ? "compatible" : "breaking";
3003
3022
  }
3023
+
3024
+ class TaskIntentObservationError extends Error {
3025
+ code;
3026
+ constructor(code, message) {
3027
+ super(message);
3028
+ this.name = "TaskIntentObservationError";
3029
+ this.code = code;
3030
+ }
3031
+ }
3004
3032
  var intentReaderTestHook = null;
3005
3033
  function validateTaskId3(taskId) {
3006
3034
  if (!TASK_ID_PATTERN.test(taskId))
@@ -3065,7 +3093,7 @@ function assertIdentitiesUnchanged(expected, canonicalRoot, relativePath) {
3065
3093
  throw new Error(`path component changed while being read: ${parts.slice(0, index + 1).join("/")}`);
3066
3094
  }
3067
3095
  }
3068
- function readTaskIntent(root, taskId, requestedPath) {
3096
+ function readTaskIntentSource(root, taskId, requestedPath) {
3069
3097
  validateTaskId3(taskId);
3070
3098
  const canonicalRoot = resolveCanonicalRoot(root);
3071
3099
  const activePath = `${INTENT_SIDECAR_RELATIVE_PREFIX}${taskId}.intent.json`;
@@ -3077,17 +3105,19 @@ function readTaskIntent(root, taskId, requestedPath) {
3077
3105
  if (!target.startsWith(canonicalRoot + sep3))
3078
3106
  throw new Error("intent sidecar escapes project root");
3079
3107
  if (!sidecarPresent(canonicalRoot, sidecarPath))
3080
- throw new Error(`TaskIntent sidecar is missing at ${sidecarPath}`);
3108
+ throw new TaskIntentObservationError("missing", `TaskIntent sidecar is missing at ${sidecarPath}`);
3081
3109
  const pathIdentities = collectPathIdentities(canonicalRoot, sidecarPath);
3082
3110
  const fileIdentity = pathIdentities[pathIdentities.length - 1];
3083
3111
  try {
3084
3112
  execFileSync3("git", ["ls-files", "--error-unmatch", "--", sidecarPath], { cwd: canonicalRoot, stdio: ["ignore", "pipe", "pipe"] });
3085
- } catch {
3086
- throw new Error("TaskIntent sidecar is not Git-tracked");
3113
+ } catch (error) {
3114
+ if (typeof error === "object" && error !== null && "status" in error && error.status === 1)
3115
+ throw new TaskIntentObservationError("invalid", "TaskIntent sidecar is not Git-tracked");
3116
+ throw error;
3087
3117
  }
3088
3118
  const before = lstatSync4(target);
3089
3119
  if (!before.isFile() || before.size > INTENT_MAX_BYTES)
3090
- throw new Error("TaskIntent sidecar must be a regular file no larger than 64 KiB");
3120
+ throw new TaskIntentObservationError("invalid", "TaskIntent sidecar must be a regular file no larger than 64 KiB");
3091
3121
  const fd = openSync2(target, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
3092
3122
  let bytes;
3093
3123
  try {
@@ -3099,7 +3129,7 @@ function readTaskIntent(root, taskId, requestedPath) {
3099
3129
  closeSync2(fd);
3100
3130
  }
3101
3131
  if (bytes.byteLength > INTENT_MAX_BYTES)
3102
- throw new Error("TaskIntent sidecar exceeds 64 KiB");
3132
+ throw new TaskIntentObservationError("invalid", "TaskIntent sidecar exceeds 64 KiB");
3103
3133
  const after = lstatSync4(target);
3104
3134
  assertSameIdentity(statIdentity(before), after, "intent sidecar");
3105
3135
  assertIdentitiesUnchanged(pathIdentities, canonicalRoot, sidecarPath);
@@ -3113,23 +3143,11 @@ function readTaskIntent(root, taskId, requestedPath) {
3113
3143
  try {
3114
3144
  intent = parseTaskIntentV1(JSON.parse(bytes.toString("utf8")));
3115
3145
  } catch (error) {
3116
- throw new Error(`TaskIntent sidecar is invalid: ${error instanceof Error ? error.message : String(error)}`);
3146
+ throw new TaskIntentObservationError("invalid", `TaskIntent sidecar is invalid: ${error instanceof Error ? error.message : String(error)}`);
3117
3147
  }
3118
3148
  if (intent.task_id !== taskId)
3119
- throw new Error("intent.task_id does not match the sidecar filename task id");
3149
+ throw new TaskIntentObservationError("invalid", "intent.task_id does not match the sidecar filename task id");
3120
3150
  const contentHash = canonicalIntentHash(intent);
3121
- const token = mintToken({
3122
- canonical_root: canonicalRoot,
3123
- sidecar_path: sidecarPath,
3124
- path_dev: fileIdentity.dev,
3125
- path_ino: fileIdentity.ino,
3126
- fd_dev: before.dev,
3127
- fd_ino: before.ino,
3128
- fd_size: before.size,
3129
- fd_mtime_ms: before.mtimeMs,
3130
- source_bytes_sha256: sourceBytesSha256,
3131
- intent_content_hash: contentHash
3132
- });
3133
3151
  return {
3134
3152
  intent,
3135
3153
  content_hash: contentHash,
@@ -3138,9 +3156,24 @@ function readTaskIntent(root, taskId, requestedPath) {
3138
3156
  revision: intent.revision,
3139
3157
  content_hash: contentHash
3140
3158
  },
3141
- token
3159
+ identity: {
3160
+ canonical_root: canonicalRoot,
3161
+ sidecar_path: sidecarPath,
3162
+ path_dev: fileIdentity.dev,
3163
+ path_ino: fileIdentity.ino,
3164
+ fd_dev: before.dev,
3165
+ fd_ino: before.ino,
3166
+ fd_size: before.size,
3167
+ fd_mtime_ms: before.mtimeMs,
3168
+ source_bytes_sha256: sourceBytesSha256,
3169
+ intent_content_hash: contentHash
3170
+ }
3142
3171
  };
3143
3172
  }
3173
+ function readTaskIntent(root, taskId, requestedPath) {
3174
+ const { identity, ...observed } = readTaskIntentSource(root, taskId, requestedPath);
3175
+ return { ...observed, token: mintToken(identity) };
3176
+ }
3144
3177
 
3145
3178
  // plugins/immune-brain/runtime/kernel/validation.ts
3146
3179
  var GIT_OBJECT_ID3 = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
@@ -3645,6 +3678,7 @@ var ACTION_V2_TYPES = [
3645
3678
  "request_rework",
3646
3679
  "complete",
3647
3680
  "stop",
3681
+ "authorize_rework",
3648
3682
  "resolve_user_decision"
3649
3683
  ];
3650
3684
  var ACTION_BASE_FIELDS = [
@@ -3740,7 +3774,8 @@ function parseTaskAction(raw) {
3740
3774
  };
3741
3775
  break;
3742
3776
  }
3743
- case "complete": {
3777
+ case "complete":
3778
+ case "authorize_rework": {
3744
3779
  rejectUnknown2(value, [...ACTION_BASE_FIELDS], "action", violations);
3745
3780
  action = { ...base, type: base.type };
3746
3781
  break;
@@ -3798,7 +3833,7 @@ function assertTaskRecordUpdateV3(previousRaw, nextRaw, action) {
3798
3833
  violations.push("non-intent action cannot change the intent snapshot");
3799
3834
  if (next.intent_ref.content_hash !== previous.intent_ref.content_hash)
3800
3835
  violations.push("non-intent action cannot change intent_ref content hash");
3801
- if (next.intent_ref.path !== previous.intent_ref.path && action.type !== "request_rework" && action.type !== "stop")
3836
+ if (next.intent_ref.path !== previous.intent_ref.path && action.type !== "request_rework" && action.type !== "authorize_rework" && action.type !== "stop")
3802
3837
  violations.push("only artifact transitions may change intent_ref path");
3803
3838
  }
3804
3839
  if (next.attestations.length < previous.attestations.length)
@@ -3810,7 +3845,7 @@ function assertTaskRecordUpdateV3(previousRaw, nextRaw, action) {
3810
3845
  if (!current || JSON.stringify(current) !== JSON.stringify(prior))
3811
3846
  violations.push(`attestation ${prior.id} was rewritten`);
3812
3847
  }
3813
- const resolvingFindingIds = action.type === "resolve_finding" ? [action.finding_id] : action.type === "resolve_user_decision" ? [action.finding_id] : action.type === "approve_breaking_intent_revision" ? previous.findings.filter((item) => item.kind === "replan_required" && item.status === "open").map((item) => item.id) : [];
3848
+ const resolvingFindingIds = action.type === "resolve_finding" ? [action.finding_id] : action.type === "resolve_user_decision" ? [action.finding_id] : action.type === "authorize_rework" || action.type === "approve_breaking_intent_revision" ? previous.findings.filter((item) => item.kind === "replan_required" && item.status === "open").map((item) => item.id) : [];
3814
3849
  const reworkFindingIds = action.type === "request_rework" ? new Set(action.findings.map((item) => item.id)) : new Set;
3815
3850
  for (const prior of previous.findings) {
3816
3851
  const current = next.findings.find((item) => item.id === prior.id);
@@ -5039,6 +5074,8 @@ function deriveAssuranceAuthorization(input) {
5039
5074
  state: "none",
5040
5075
  blocked: `resolve-user-decision requires exactly one open user decision; found ${input.open_user_decision_count}`
5041
5076
  };
5077
+ if (input.next_obligation === "revise_intent")
5078
+ return { state: "authorize_rework", blocked: null };
5042
5079
  return { state: "none", blocked: null };
5043
5080
  }
5044
5081
  function emptyProjection() {
@@ -5289,7 +5326,7 @@ function intentRefMatches(intent, ref) {
5289
5326
  return ref.path === `docs/plans/${intent.task_id}.intent.json` && ref.content_hash === canonicalIntentHash(intent);
5290
5327
  }
5291
5328
  function hasPrivilegedKind(action) {
5292
- return action.type === "record_approval" || action.type === "approve_breaking_intent_revision" || action.type === "request_rework" || action.type === "stop" || action.type === "resolve_user_decision";
5329
+ return action.type === "record_approval" || action.type === "approve_breaking_intent_revision" || action.type === "request_rework" || action.type === "authorize_rework" || action.type === "stop" || action.type === "resolve_user_decision";
5293
5330
  }
5294
5331
  function findingsDigestV2(findings) {
5295
5332
  const normalized = findings.map((finding) => ({
@@ -5507,8 +5544,8 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
5507
5544
  "request_rework requires review, qa, or user authority"
5508
5545
  ]);
5509
5546
  const round = reviewRound(record);
5510
- const reviewAuthorityReworks = record.history.filter((entry) => entry.type === "request_rework" && entry.authority?.authority_kind === "review").length;
5511
- const parkForReplan = authorityAudit.authority_kind === "review" && reviewAuthorityReworks >= 1;
5547
+ const hasPriorBlockingReviewRework = record.findings.some((finding) => finding.source === "review" && finding.kind === "blocking" && finding.review_round !== null);
5548
+ const parkForReplan = authorityAudit.authority_kind === "review" && hasPriorBlockingReviewRework && action.findings.some((finding) => finding.kind === "blocking");
5512
5549
  if (!parkForReplan) {
5513
5550
  record.artifact_state = "active";
5514
5551
  record.intent_ref.path = `docs/plans/${record.task_id}.intent.json`;
@@ -5523,8 +5560,8 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
5523
5560
  record.findings.push({
5524
5561
  ...finding,
5525
5562
  status: "open",
5526
- source: "review",
5527
- review_round: round
5563
+ source: authorityAudit.authority_kind === "review" ? "review" : "execution",
5564
+ review_round: authorityAudit.authority_kind === "review" ? round : null
5528
5565
  });
5529
5566
  }
5530
5567
  if (parkForReplan && !record.findings.some((item) => item.status === "open" && item.kind === "replan_required")) {
@@ -5547,6 +5584,27 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
5547
5584
  appendHistory(record, action, from, `review_round_${round}`, authorityAudit);
5548
5585
  break;
5549
5586
  }
5587
+ case "authorize_rework": {
5588
+ if (record.lifecycle !== "active")
5589
+ throw new KernelInvariantError([
5590
+ `cannot authorize rework while lifecycle is ${record.lifecycle}`
5591
+ ]);
5592
+ if (authorityAudit?.authority_kind !== "user")
5593
+ throw new KernelInvariantError([
5594
+ "authorize_rework requires literal-user authority"
5595
+ ]);
5596
+ const open = record.findings.filter((finding) => finding.kind === "replan_required" && finding.status === "open");
5597
+ if (open.length === 0)
5598
+ throw new KernelInvariantError([
5599
+ "authorize_rework requires an open replan boundary"
5600
+ ]);
5601
+ for (const finding of open)
5602
+ finding.status = "resolved";
5603
+ record.artifact_state = "active";
5604
+ record.intent_ref.path = `docs/plans/${record.task_id}.intent.json`;
5605
+ appendHistory(record, action, from, open.map((finding) => finding.id).join(","), authorityAudit);
5606
+ break;
5607
+ }
5550
5608
  case "complete": {
5551
5609
  if (record.lifecycle !== "active" || record.artifact_state !== "frozen")
5552
5610
  throw new KernelInvariantError([
@@ -5677,7 +5735,7 @@ function applyTaskAction(input) {
5677
5735
  "intent token does not match the committed record intent"
5678
5736
  ]);
5679
5737
  }
5680
- const privileged = action.type === "record_approval" || action.type === "approve_breaking_intent_revision" || action.type === "request_rework" || action.type === "stop" || action.type === "resolve_user_decision";
5738
+ const privileged = action.type === "record_approval" || action.type === "approve_breaking_intent_revision" || action.type === "request_rework" || action.type === "authorize_rework" || action.type === "stop" || action.type === "resolve_user_decision";
5681
5739
  const expectedAuthority = privileged ? {
5682
5740
  task_id,
5683
5741
  action,
@@ -5803,6 +5861,8 @@ function capabilityActionFor(input) {
5803
5861
  return { ...base, approval: input.approval };
5804
5862
  case "request_rework":
5805
5863
  return { ...base, findings: input.findings };
5864
+ case "authorize_rework":
5865
+ return { ...base, type: "authorize_rework" };
5806
5866
  case "stop":
5807
5867
  return { ...base, reason: input.reason };
5808
5868
  case "approve_breaking_intent_revision":
@@ -5949,7 +6009,7 @@ function createCanaryApplication(registry) {
5949
6009
  const hasBoundSpec = snapshot.intent_snapshot.scope_hint.some((path) => /^docs\/specs\/(?!archive\/)[^/]+\.spec\.md$/.test(path) && snapshot.intent_snapshot.scope_hint.includes(archivePath(path)));
5950
6010
  if (operation.op === "complete" && hasBoundSpec && snapshot.record.artifact_state !== "frozen")
5951
6011
  throw new KernelInvariantError(["complete requires frozen planning artifacts"]);
5952
- const artifactTransition = snapshot.record.artifact_state === "frozen" && (operation.op === "request_rework" || operation.op === "approve_breaking_intent_revision") ? transitionFor(input.root, snapshot.record, "restore") : operation.op === "stop" && snapshot.record.artifact_state !== "frozen" ? transitionFor(input.root, snapshot.record, "freeze", true) : undefined;
6012
+ const artifactTransition = snapshot.record.artifact_state === "frozen" && (operation.op === "request_rework" || operation.op === "authorize_rework" || operation.op === "approve_breaking_intent_revision") ? transitionFor(input.root, snapshot.record, "restore") : operation.op === "stop" && snapshot.record.artifact_state !== "frozen" ? transitionFor(input.root, snapshot.record, "freeze", true) : undefined;
5953
6013
  const event_id = `${operation.op}:${input.task_id}:${at}`;
5954
6014
  const base = {
5955
6015
  event_id,
@@ -6015,6 +6075,10 @@ function createCanaryApplication(registry) {
6015
6075
  capability = operation.capability;
6016
6076
  action = { ...base, type: "stop", reason: operation.reason };
6017
6077
  break;
6078
+ case "authorize_rework":
6079
+ capability = operation.capability;
6080
+ action = { ...base, type: "authorize_rework" };
6081
+ break;
6018
6082
  case "resolve_user_decision":
6019
6083
  capability = operation.capability;
6020
6084
  action = {
@@ -6477,7 +6541,16 @@ function enrollCanaryTask(root, input, registry) {
6477
6541
  throw new Error("intent content hash mismatch");
6478
6542
  if (checks.gitBaseHead !== gitBaseHead)
6479
6543
  throw new Error("Git HEAD moved after the enrollment confirmation");
6544
+ if (input.batch) {
6545
+ const batch = input.batch.registry.inspect(input.batch.capability, input.batch.binding, Date.parse(input.now));
6546
+ if (input.batch.registry.consumedChildren(input.batch.capability).length === 0 && input.batch.expected_head !== batch.base_head)
6547
+ throw new Error(`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${batch.base_head}, not ${input.batch.expected_head}`);
6548
+ if (checks.gitBaseHead !== input.batch.expected_head)
6549
+ throw new Error(`batch_head_lineage_broken: expected ${input.batch.expected_head}, found ${checks.gitBaseHead}`);
6550
+ }
6480
6551
  registry.consume(input.capability, input.capability_binding);
6552
+ if (input.batch)
6553
+ input.batch.registry.consumeChild(input.batch.capability, input.batch.binding, input.task_id, Date.parse(input.now));
6481
6554
  if (!gitBaseHead)
6482
6555
  throw new Error("enrollment requires a committed Git HEAD");
6483
6556
  const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
@@ -6496,16 +6569,23 @@ function enrollCanaryTask(root, input, registry) {
6496
6569
  created_at: input.now,
6497
6570
  updated_at: input.now
6498
6571
  };
6499
- const mutation = commitEnrollmentLocked(root, input.task_id, {
6500
- contract: "assurance_kernel/workspace_transaction/v2",
6501
- task_id: input.task_id,
6502
- expected_record_hash: checks.current.revision,
6503
- next_record_content: `${JSON.stringify(record, null, 2)}
6572
+ let mutation;
6573
+ try {
6574
+ mutation = commitEnrollmentLocked(root, input.task_id, {
6575
+ contract: "assurance_kernel/workspace_transaction/v2",
6576
+ task_id: input.task_id,
6577
+ expected_record_hash: checks.current.revision,
6578
+ next_record_content: `${JSON.stringify(record, null, 2)}
6504
6579
  `,
6505
- expected_workspace_hash: checks.workspace.revision,
6506
- next_workspace_content: `${JSON.stringify(nextWorkspace, null, 2)}
6580
+ expected_workspace_hash: checks.workspace.revision,
6581
+ next_workspace_content: `${JSON.stringify(nextWorkspace, null, 2)}
6507
6582
  `
6508
- }, claim);
6583
+ }, claim);
6584
+ } catch (error) {
6585
+ if (input.batch)
6586
+ input.batch.registry.releaseChild(input.batch.capability, input.task_id);
6587
+ throw error;
6588
+ }
6509
6589
  return {
6510
6590
  record: mutation.record,
6511
6591
  backend_claim: claim,
@@ -6645,14 +6725,18 @@ async function submitClaudeReview(host, coordinator, ctx, taskId, verdictInput)
6645
6725
  return coordinator.abandonReview(taskId, observed.reason);
6646
6726
  return { state: "blocked", reason: observed.reason };
6647
6727
  }
6728
+ const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
6729
+ if (!parentValid)
6730
+ return coordinator.submitReview(taskId, ctx, verdictInput);
6731
+ const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
6732
+ if (!receiptValid) {
6733
+ return coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict");
6734
+ }
6648
6735
  const parentJson = extractVerdictJson(verdictInput);
6649
6736
  const receiptJson = extractVerdictJson(observed.receipt.result);
6650
- if (parentJson && receiptJson && verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
6737
+ if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
6651
6738
  return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
6652
6739
  }
6653
- if (parentJson && !receiptJson) {
6654
- return { state: "blocked", reason: "reviewer receipt is not a valid verdict" };
6655
- }
6656
6740
  return coordinator.submitReview(taskId, ctx, verdictInput);
6657
6741
  }
6658
6742
  function stopReason(value) {
@@ -6948,6 +7032,12 @@ class ClaudeRuntime {
6948
7032
  async submitReview(taskId, verdictInput) {
6949
7033
  return submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput);
6950
7034
  }
7035
+ async resolveFinding(taskId, findingId) {
7036
+ return this.executeOrdinary({ cwd: this.cwd }, {
7037
+ taskId,
7038
+ operation: { op: "resolve_finding", finding_id: findingId, actor_id: "executor" }
7039
+ });
7040
+ }
6951
7041
  async authorize(taskId, operation, meta, extra = {}) {
6952
7042
  if (operation === "repair_authority_state") {
6953
7043
  const authority = reconcileKernelAuthority(this.cwd, taskId);
@@ -6972,6 +7062,8 @@ class ClaudeRuntime {
6972
7062
  throw new Error(`resolve-user-decision requires exactly one open user decision; found ${open.length}`);
6973
7063
  op = "resolve_user_decision";
6974
7064
  decisionOp = { finding_id: open[0].id, resolution: `resume after literal-user decision: ${open[0].summary}` };
7065
+ } else if (readiness.state === "authorize_rework") {
7066
+ op = "authorize_rework";
6975
7067
  } else {
6976
7068
  throw new Error(readiness.blocked ?? "no unique host-derived authorization operation");
6977
7069
  }
@@ -7088,7 +7180,7 @@ class ClaudeRuntime {
7088
7180
  diffProvider: diffSnapshotOf,
7089
7181
  now
7090
7182
  });
7091
- if (op === "stop" || op === "approve_breaking_intent_revision")
7183
+ if (op === "stop" || op === "authorize_rework" || op === "approve_breaking_intent_revision")
7092
7184
  stagePlanningArtifactTransition(this.cwd, result.record);
7093
7185
  return result;
7094
7186
  } catch (error) {
@@ -7224,7 +7316,8 @@ var TOOLS = [
7224
7316
  { name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
7225
7317
  { name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
7226
7318
  { name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
7227
- { name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false }
7319
+ { name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false },
7320
+ { name: "resolve_finding", description: "Resolve one open blocking or advisory finding whose cause is fixed and verified.", privileged: false }
7228
7321
  ];
7229
7322
  function listMcpTools() {
7230
7323
  return TOOLS.map((tool) => ({
@@ -7236,9 +7329,10 @@ function listMcpTools() {
7236
7329
  task_id: { type: "string" },
7237
7330
  ...tool.name === "approve_breaking_intent_revision" ? { next_intent: { type: "object" } } : {},
7238
7331
  ...tool.name === "stop" ? { reason: { type: "string" } } : {},
7239
- ...tool.name === "submit_review" ? { verdict: { type: "object" } } : {}
7332
+ ...tool.name === "submit_review" ? { verdict: { type: "object" } } : {},
7333
+ ...tool.name === "resolve_finding" ? { finding_id: { type: "string" } } : {}
7240
7334
  },
7241
- required: tool.name === "submit_review" ? ["task_id", "verdict"] : ["task_id"]
7335
+ required: tool.name === "submit_review" ? ["task_id", "verdict"] : tool.name === "resolve_finding" ? ["task_id", "finding_id"] : ["task_id"]
7242
7336
  },
7243
7337
  annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" }
7244
7338
  }));
@@ -7305,6 +7399,11 @@ function createMcpRuntime(options = {}) {
7305
7399
  throw new Error("verdict is required");
7306
7400
  return runtime.submitReview(taskId, args.verdict);
7307
7401
  }
7402
+ if (name === "resolve_finding") {
7403
+ if (typeof args.finding_id !== "string" || !args.finding_id)
7404
+ throw new Error("finding_id is required");
7405
+ return runtime.resolveFinding(taskId, args.finding_id);
7406
+ }
7308
7407
  if (name === "request_authorization" || name === "approve_breaking_intent_revision" || name === "stop" || name === "repair_authority_state") {
7309
7408
  return runtime.authorize(taskId, name, toolMeta, args);
7310
7409
  }
@@ -83,8 +83,14 @@ not bypass them. Do not poll or create detached jobs.
83
83
  - Invoke `approve_breaking_intent_revision` with the complete next intent
84
84
  directly; the native Host gate is the single user decision. Do not overwrite
85
85
  enrolled intent sidecars or ask for chat pre-confirmation.
86
- - On `awaiting_user`, invoke `request_authorization` directly. It is reserved
87
- for a concrete unresolved decision or explicit stop, not risk tier alone.
86
+ - On `awaiting_user`, invoke `request_authorization` directly for a concrete
87
+ unresolved decision or rework authorization, not risk tier alone.
88
+ - When the user explicitly asks to stop a Pi task, invoke
89
+ `imm_kernel_canary({ task_id, action: { op: "request_stop" } })` directly.
90
+ Its single native confirmation authorizes existing Kernel stop settlement.
91
+ Cancellation is not task termination. A busy invocation must finish or be
92
+ cancelled through existing Host controls before requesting stop; never clear
93
+ claims manually or use this operation to force-kill QA.
88
94
  - Invoke `repair_authority_state` directly for a proven stale claim. Kernel
89
95
  revalidation removes only the redundant claim without user interaction.
90
96
  - A Managed native authority failure stays fail-closed. Report its stable reason