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
@@ -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.3";
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;
@@ -2484,9 +2503,6 @@ function captureReviewManifest(root, input) {
2484
2503
  throw new Error("immutable review manifest metadata exceeds bounded output limit");
2485
2504
  return manifest;
2486
2505
  }
2487
- function ensureReviewRevision(root, input) {
2488
- return publishInput(root, input).revision;
2489
- }
2490
2506
  function writeNativeReviewEvidence(payload) {
2491
2507
  const rawDirectory = mkdtempSync(join4(tmpdir2(), "imm-canary-native-review-"));
2492
2508
  try {
@@ -2716,6 +2732,9 @@ var TASK_RECORD_CONTRACT_V2 = "assurance_kernel/task_record/v2";
2716
2732
  var TASK_RECORD_CONTRACT_V3 = "assurance_kernel/task_record/v3";
2717
2733
  var TASK_RECORD_CONTRACT_V4 = "assurance_kernel/task_record/v4";
2718
2734
  var REVIEW_REVISION_IDENTITY_CONTRACT = "assurance_kernel/review_revision_identity/v1";
2735
+ function isTaskRecordV4(record) {
2736
+ return record.contract === TASK_RECORD_CONTRACT_V4;
2737
+ }
2719
2738
  var REDUCED_MUTATION_BRAND = Symbol("assurance-kernel-reduced-mutation-v2");
2720
2739
  var MUTATION_AUTHORITY_CAPABILITY_BRAND = Symbol("assurance-kernel-mutation-authority-capability");
2721
2740
 
@@ -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,
@@ -5737,6 +5795,8 @@ function applyTaskAction(input) {
5737
5795
  };
5738
5796
  }
5739
5797
  if (input.terminal) {
5798
+ if (nextRecord.lifecycle === "active")
5799
+ throw new Error("terminal settlement requires a done or stopped TaskRecord lifecycle");
5740
5800
  const tombstone = {
5741
5801
  contract: TASK_TOMBSTONE_CONTRACT,
5742
5802
  task_id,
@@ -5801,6 +5861,8 @@ function capabilityActionFor(input) {
5801
5861
  return { ...base, approval: input.approval };
5802
5862
  case "request_rework":
5803
5863
  return { ...base, findings: input.findings };
5864
+ case "authorize_rework":
5865
+ return { ...base, type: "authorize_rework" };
5804
5866
  case "stop":
5805
5867
  return { ...base, reason: input.reason };
5806
5868
  case "approve_breaking_intent_revision":
@@ -5947,7 +6009,7 @@ function createCanaryApplication(registry) {
5947
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)));
5948
6010
  if (operation.op === "complete" && hasBoundSpec && snapshot.record.artifact_state !== "frozen")
5949
6011
  throw new KernelInvariantError(["complete requires frozen planning artifacts"]);
5950
- 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;
5951
6013
  const event_id = `${operation.op}:${input.task_id}:${at}`;
5952
6014
  const base = {
5953
6015
  event_id,
@@ -6013,6 +6075,10 @@ function createCanaryApplication(registry) {
6013
6075
  capability = operation.capability;
6014
6076
  action = { ...base, type: "stop", reason: operation.reason };
6015
6077
  break;
6078
+ case "authorize_rework":
6079
+ capability = operation.capability;
6080
+ action = { ...base, type: "authorize_rework" };
6081
+ break;
6016
6082
  case "resolve_user_decision":
6017
6083
  capability = operation.capability;
6018
6084
  action = {
@@ -6475,7 +6541,18 @@ function enrollCanaryTask(root, input, registry) {
6475
6541
  throw new Error("intent content hash mismatch");
6476
6542
  if (checks.gitBaseHead !== gitBaseHead)
6477
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
+ }
6478
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));
6554
+ if (!gitBaseHead)
6555
+ throw new Error("enrollment requires a committed Git HEAD");
6479
6556
  const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
6480
6557
  const nextWorkspace = {
6481
6558
  ...checks.workspace.state,
@@ -6492,16 +6569,23 @@ function enrollCanaryTask(root, input, registry) {
6492
6569
  created_at: input.now,
6493
6570
  updated_at: input.now
6494
6571
  };
6495
- const mutation = commitEnrollmentLocked(root, input.task_id, {
6496
- contract: "assurance_kernel/workspace_transaction/v2",
6497
- task_id: input.task_id,
6498
- expected_record_hash: checks.current.revision,
6499
- 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)}
6500
6579
  `,
6501
- expected_workspace_hash: checks.workspace.revision,
6502
- next_workspace_content: `${JSON.stringify(nextWorkspace, null, 2)}
6580
+ expected_workspace_hash: checks.workspace.revision,
6581
+ next_workspace_content: `${JSON.stringify(nextWorkspace, null, 2)}
6503
6582
  `
6504
- }, 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
+ }
6505
6589
  return {
6506
6590
  record: mutation.record,
6507
6591
  backend_claim: claim,
@@ -6641,16 +6725,23 @@ async function submitClaudeReview(host, coordinator, ctx, taskId, verdictInput)
6641
6725
  return coordinator.abandonReview(taskId, observed.reason);
6642
6726
  return { state: "blocked", reason: observed.reason };
6643
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
+ }
6644
6735
  const parentJson = extractVerdictJson(verdictInput);
6645
6736
  const receiptJson = extractVerdictJson(observed.receipt.result);
6646
- if (parentJson && receiptJson && verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
6737
+ if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
6647
6738
  return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
6648
6739
  }
6649
- if (parentJson && !receiptJson) {
6650
- return { state: "blocked", reason: "reviewer receipt is not a valid verdict" };
6651
- }
6652
6740
  return coordinator.submitReview(taskId, ctx, verdictInput);
6653
6741
  }
6742
+ function stopReason(value) {
6743
+ return typeof value === "string" && value.length > 0 ? value : "user stop";
6744
+ }
6654
6745
  function assertProjectionBinding(before, after, allowDiffChange = false) {
6655
6746
  const fields = allowDiffChange ? ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash"] : ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash", "diff_hash"];
6656
6747
  if (before.error || !before.claim || after.error || !after.claim || before.claim.task_id !== after.claim.task_id || fields.some((field) => before.projection[field] !== after.projection[field])) {
@@ -6660,22 +6751,57 @@ function assertProjectionBinding(before, after, allowDiffChange = false) {
6660
6751
  function qaOutcomes(record) {
6661
6752
  return Object.fromEntries(record.attestations.filter((item) => item.kind === "qa").flatMap((item) => item.acceptance_results).map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]));
6662
6753
  }
6754
+ async function ensureClaudeReviewRevision(root, taskId, projection) {
6755
+ const current = await readTaskRecord(root, taskId);
6756
+ const record = current.record;
6757
+ if (!record)
6758
+ throw new Error(`task ${taskId} has no TaskRecord`);
6759
+ if (current.revision !== projection.projection.record_revision)
6760
+ throw new Error("TaskRecord changed before Review revision preparation");
6761
+ if (record.contract !== "assurance_kernel/task_record/v4")
6762
+ return null;
6763
+ if (!record.git_base_head)
6764
+ throw new Error("Review revision requires a TaskRecord v4 git_base_head");
6765
+ const manifest = captureReviewManifest(root, {
6766
+ taskId,
6767
+ baseHead: record.git_base_head,
6768
+ scopeHint: record.intent_snapshot.scope_hint,
6769
+ expectedDiffHash: projection.projection.diff_hash,
6770
+ intentRevision: projection.projection.intent_revision,
6771
+ intentContentHash: projection.projection.intent_content_hash,
6772
+ recordRevision: projection.projection.record_revision,
6773
+ workspaceRevision: projection.projection.workspace_revision,
6774
+ lifecycle: projection.projection.lifecycle,
6775
+ artifactState: projection.projection.artifact_state,
6776
+ risk: record.intent_snapshot.risk,
6777
+ outcomes: qaOutcomes(record)
6778
+ });
6779
+ return {
6780
+ contract: "assurance_kernel/review_revision/v1",
6781
+ base_head: manifest.base_head,
6782
+ review_tree: manifest.review_tree,
6783
+ review_commit: manifest.review_commit,
6784
+ review_ref: manifest.review_ref,
6785
+ diff_hash: manifest.diff_hash,
6786
+ manifest_digest: manifest.manifest_digest
6787
+ };
6788
+ }
6663
6789
  async function buildAssuranceSnapshot(root, taskId, role, projection, runner) {
6664
- const record = await readTaskRecord(root, taskId);
6665
- if (!record.record || record.revision !== projection.projection.record_revision)
6790
+ const read = await readTaskRecord(root, taskId);
6791
+ const record = read.record;
6792
+ if (!record || read.revision !== projection.projection.record_revision)
6666
6793
  throw new Error("TaskRecord changed before assurance snapshot capture");
6667
- const intent = record.record.intent_snapshot;
6794
+ const intent = record.intent_snapshot;
6668
6795
  const descriptors = new Map;
6669
6796
  for (const item of intent.acceptance) {
6670
6797
  const descriptor = parseVerificationDescriptor(item.verification);
6671
6798
  assertRunnerCompatible(descriptor, runner);
6672
6799
  descriptors.set(item.id, descriptor);
6673
6800
  }
6674
- const v4 = record.record.contract === "assurance_kernel/task_record/v4";
6675
- const reviewBundle = role === "review" && !v4 ? captureReviewBundle(root, intent.scope_hint, projection.projection.diff_hash, qaOutcomes(record.record)) : null;
6676
- const reviewManifest = role === "review" && v4 ? captureReviewManifest(root, {
6801
+ const reviewBundle = role === "review" && !isTaskRecordV4(record) ? captureReviewBundle(root, intent.scope_hint, projection.projection.diff_hash, qaOutcomes(record)) : null;
6802
+ const reviewManifest = role === "review" && isTaskRecordV4(record) ? captureReviewManifest(root, {
6677
6803
  taskId,
6678
- baseHead: record.record.git_base_head,
6804
+ baseHead: record.git_base_head,
6679
6805
  scopeHint: intent.scope_hint,
6680
6806
  expectedDiffHash: projection.projection.diff_hash,
6681
6807
  intentRevision: projection.projection.intent_revision,
@@ -6685,7 +6811,7 @@ async function buildAssuranceSnapshot(root, taskId, role, projection, runner) {
6685
6811
  lifecycle: projection.projection.lifecycle,
6686
6812
  artifactState: projection.projection.artifact_state,
6687
6813
  risk: intent.risk,
6688
- outcomes: qaOutcomes(record.record)
6814
+ outcomes: qaOutcomes(record)
6689
6815
  }) : null;
6690
6816
  const dirtyFiles = reviewManifest ? Object.keys(reviewManifest.changed_paths) : reviewBundle ? Object.keys(reviewBundle.dirty_files) : [];
6691
6817
  const snapshot = {
@@ -6784,11 +6910,11 @@ class ClaudeRuntime {
6784
6910
  this.interactive = options.interactive ?? true;
6785
6911
  this.requestConfirmation = options.requestConfirmation;
6786
6912
  this.host = options.host ?? new ClaudeReviewHost(new FileHookEventLog);
6787
- if (options.ports) {
6788
- this.coordinator = new AssuranceCoordinator({ ...options.ports, host: this.host });
6789
- return;
6790
- }
6791
- this.coordinator = new AssuranceCoordinator(this.createKernelPorts());
6913
+ this.coordinator = new AssuranceCoordinator({
6914
+ ...this.createKernelPorts(),
6915
+ ...options.ports,
6916
+ host: this.host
6917
+ });
6792
6918
  }
6793
6919
  observe(event) {
6794
6920
  this.host.observe(event);
@@ -6802,27 +6928,18 @@ class ClaudeRuntime {
6802
6928
  async shutdown() {
6803
6929
  await this.coordinator.onSessionShutdown();
6804
6930
  }
6931
+ kernelPorts() {
6932
+ return this.createKernelPorts();
6933
+ }
6805
6934
  createKernelPorts() {
6806
6935
  return {
6807
6936
  host: this.host,
6808
6937
  projectTask: (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
6809
- readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
6810
- readTaskIntent: (root, taskId) => readTaskIntentForRecord(root, taskId),
6938
+ readTaskRecord: async (root, taskId) => readTaskRecord(root, taskId),
6939
+ readTaskIntent: async (root, taskId) => readTaskIntentForRecord(root, taskId),
6811
6940
  frozenRunner: async () => resolveBunRunner(),
6812
6941
  buildAssurance: (root, taskId, role, projection, runner) => buildAssuranceSnapshot(root, taskId, role, projection, runner),
6813
- ensureReviewRevision: async (root, taskId, projection) => {
6814
- const current = await readTaskRecord(root, taskId);
6815
- if (!current.record)
6816
- throw new Error(`task ${taskId} has no TaskRecord`);
6817
- if (current.record.contract !== "assurance_kernel/task_record/v4")
6818
- return null;
6819
- return ensureReviewRevision(root, {
6820
- taskId,
6821
- baseHead: current.record.git_base_head,
6822
- scopeHint: current.record.intent_snapshot.scope_hint,
6823
- expectedDiffHash: projection.projection.diff_hash
6824
- });
6825
- },
6942
+ ensureReviewRevision: (root, taskId, projection) => ensureClaudeReviewRevision(root, taskId, projection),
6826
6943
  runQa: (snapshot, descriptors, runner, options) => runDeterministicQa(snapshot, descriptors, runner, options),
6827
6944
  writeReviewEvidence: (input) => writeNativeReviewEvidence(input.evidence),
6828
6945
  applyVerdict: (ctx, input) => this.applyVerdict(ctx, input),
@@ -6915,6 +7032,12 @@ class ClaudeRuntime {
6915
7032
  async submitReview(taskId, verdictInput) {
6916
7033
  return submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput);
6917
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
+ }
6918
7041
  async authorize(taskId, operation, meta, extra = {}) {
6919
7042
  if (operation === "repair_authority_state") {
6920
7043
  const authority = reconcileKernelAuthority(this.cwd, taskId);
@@ -6939,6 +7062,8 @@ class ClaudeRuntime {
6939
7062
  throw new Error(`resolve-user-decision requires exactly one open user decision; found ${open.length}`);
6940
7063
  op = "resolve_user_decision";
6941
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";
6942
7067
  } else {
6943
7068
  throw new Error(readiness.blocked ?? "no unique host-derived authorization operation");
6944
7069
  }
@@ -7037,7 +7162,7 @@ class ClaudeRuntime {
7037
7162
  confirmation_ref: confirmation,
7038
7163
  ...op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {},
7039
7164
  ...op === "resolve_user_decision" && decisionOp ? decisionOp : {},
7040
- ...op === "stop" ? { reason: extra.reason ?? "user stop" } : {}
7165
+ ...op === "stop" ? { reason: stopReason(extra.reason) } : {}
7041
7166
  });
7042
7167
  throwIfCancelled(meta.signal);
7043
7168
  const result = app.execute({
@@ -7049,13 +7174,13 @@ class ClaudeRuntime {
7049
7174
  actor_id: actorId,
7050
7175
  ...op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {},
7051
7176
  ...op === "resolve_user_decision" && decisionOp ? decisionOp : {},
7052
- ...op === "stop" ? { reason: extra.reason ?? "user stop" } : {}
7177
+ ...op === "stop" ? { reason: stopReason(extra.reason) } : {}
7053
7178
  },
7054
7179
  prior_intent_token: priorIntent.token,
7055
7180
  diffProvider: diffSnapshotOf,
7056
7181
  now
7057
7182
  });
7058
- if (op === "stop" || op === "approve_breaking_intent_revision")
7183
+ if (op === "stop" || op === "authorize_rework" || op === "approve_breaking_intent_revision")
7059
7184
  stagePlanningArtifactTransition(this.cwd, result.record);
7060
7185
  return result;
7061
7186
  } catch (error) {
@@ -7191,7 +7316,8 @@ var TOOLS = [
7191
7316
  { name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
7192
7317
  { name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
7193
7318
  { name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
7194
- { 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 }
7195
7321
  ];
7196
7322
  function listMcpTools() {
7197
7323
  return TOOLS.map((tool) => ({
@@ -7203,9 +7329,10 @@ function listMcpTools() {
7203
7329
  task_id: { type: "string" },
7204
7330
  ...tool.name === "approve_breaking_intent_revision" ? { next_intent: { type: "object" } } : {},
7205
7331
  ...tool.name === "stop" ? { reason: { type: "string" } } : {},
7206
- ...tool.name === "submit_review" ? { verdict: { type: "object" } } : {}
7332
+ ...tool.name === "submit_review" ? { verdict: { type: "object" } } : {},
7333
+ ...tool.name === "resolve_finding" ? { finding_id: { type: "string" } } : {}
7207
7334
  },
7208
- 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"]
7209
7336
  },
7210
7337
  annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" }
7211
7338
  }));
@@ -7272,6 +7399,11 @@ function createMcpRuntime(options = {}) {
7272
7399
  throw new Error("verdict is required");
7273
7400
  return runtime.submitReview(taskId, args.verdict);
7274
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
+ }
7275
7407
  if (name === "request_authorization" || name === "approve_breaking_intent_revision" || name === "stop" || name === "repair_authority_state") {
7276
7408
  return runtime.authorize(taskId, name, toolMeta, args);
7277
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
@@ -163,18 +163,23 @@ After approval, author, stage, and validate every TaskIntent in the decompositio
163
163
  with `valid: true` and `enrollment_ready: true`. Resolve `../bin/imm-tracker` from this packaged contract; do not assume a bare command is on `PATH`. Submit the entire approved set once through
164
164
  `imm-tracker publish-initiative --stdin --json`. Its input contains the confirmed
165
165
  Initiative slug and goal, Parent projection, and every Child's `slice_id`,
166
- canonical TaskIntent path, and public projection. The Parent projection requires
166
+ canonical TaskIntent path, bounded public `acceptance` summaries, and public
167
+ projection. The Parent projection requires
167
168
  `problem`, `result`, and `design`, and may include `decisions`,
168
169
  `testing_strategy`, and `out_of_scope`. `design` records Initiative-level
169
170
  invariants, Slice boundaries and ordering, shared interfaces or state flow, and
170
171
  material compatibility decisions. Every Parent Slice must correspond to one
171
172
  published Child; future checklist-only Slices are not allowed in the batch.
172
173
 
173
- Each Child projection may contain `result`, `current_behavior`,
174
+ Each Child must provide public `acceptance` entries with `id` and a 1-500
175
+ character `summary`. Their IDs must match every canonical TaskIntent acceptance
176
+ ID exactly once. Canonical assertion prose is authority evidence and must never
177
+ be copied into public GitHub projection. Each Child projection may contain
178
+ `result`, `current_behavior`,
174
179
  `desired_behavior`, `key_interfaces`, `verification`, `blocked_by` Task IDs,
175
180
  `out_of_scope`, and `agent_handoff`. The tracker rereads every canonical
176
- TaskIntent for identity, risk, and acceptance; projection fields never widen
177
- TaskIntent scope or authority. It validates the complete dependency graph before
181
+ TaskIntent for identity, risk, and acceptance IDs; projection fields and public
182
+ summaries never widen TaskIntent scope or authority. It validates the complete dependency graph before
178
183
  remote writes, creates the Parent once, creates all Children, attaches every
179
184
  Child as a native Sub-issue, creates native `blocked_by` relations, and rereads
180
185
  the complete topology. The Child Agent Brief includes a direct Parent Issue link.