immune-brain 3.6.4 → 3.6.6

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 (42) hide show
  1. package/README.md +45 -0
  2. package/package.json +1 -1
  3. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  4. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +76 -20
  5. package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +38 -10
  6. package/plugins/immune-brain/dist/BASELINE.md +48 -15
  7. package/plugins/immune-brain/dist/claude/mcp-server.mjs +166 -57
  8. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  9. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
  10. package/plugins/immune-brain/dist/imm-agent-doc-maintain.md +9 -1
  11. package/plugins/immune-brain/dist/imm-brainstorm.md +49 -35
  12. package/plugins/immune-brain/dist/imm-doc-prune.md +7 -1
  13. package/plugins/immune-brain/dist/imm-loop.md +31 -13
  14. package/plugins/immune-brain/dist/imm-planner.md +74 -32
  15. package/plugins/immune-brain/dist/imm-pr-fix.md +6 -2
  16. package/plugins/immune-brain/dist/role-prompts/executor.md +18 -10
  17. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +5 -2
  18. package/plugins/immune-brain/runtime/assurance/coordinator.ts +18 -0
  19. package/plugins/immune-brain/runtime/assurance/verification.ts +13 -2
  20. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +31 -9
  21. package/plugins/immune-brain/runtime/claude/mcp_server.ts +14 -1
  22. package/plugins/immune-brain/runtime/commands/kernel.ts +15 -13
  23. package/plugins/immune-brain/runtime/github_issue_tracker.ts +1112 -20
  24. package/plugins/immune-brain/runtime/kernel/application.ts +1 -0
  25. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
  26. package/plugins/immune-brain/runtime/kernel/batch_authority.ts +407 -0
  27. package/plugins/immune-brain/runtime/kernel/canary_application.ts +24 -9
  28. package/plugins/immune-brain/runtime/kernel/enrollment.ts +72 -13
  29. package/plugins/immune-brain/runtime/kernel/intent.ts +67 -23
  30. package/plugins/immune-brain/runtime/kernel/reducer.ts +37 -9
  31. package/plugins/immune-brain/runtime/kernel/types.ts +1 -0
  32. package/plugins/immune-brain/runtime/kernel/validation.ts +10 -6
  33. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  34. package/plugins/immune-brain/runtime/prompts/executor.md +18 -10
  35. package/plugins/immune-brain/runtime/prompts/pr-fix.md +5 -2
  36. package/plugins/immune-brain/skills/BASELINE.md +48 -15
  37. package/plugins/immune-brain/skills/imm-agent-doc-maintain/SKILL.md +20 -4
  38. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +24 -64
  39. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +18 -3
  40. package/plugins/immune-brain/skills/imm-loop/SKILL.md +20 -6
  41. package/plugins/immune-brain/skills/imm-planner/SKILL.md +35 -8
  42. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +17 -3
@@ -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.6";
46
46
 
47
47
  // plugins/immune-brain/runtime/claude/interaction.ts
48
48
  import { createHash, randomUUID } from "node:crypto";
@@ -688,7 +688,7 @@ import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypt
688
688
 
689
689
  // plugins/immune-brain/runtime/assurance/verification.ts
690
690
  import { createHash as createHash3 } from "node:crypto";
691
- import { execFileSync, spawn } from "node:child_process";
691
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
692
692
  import { realpathSync as realpathSync2, statSync } from "node:fs";
693
693
  import { isAbsolute as isAbsolute2, resolve, sep as sep2, relative } from "node:path";
694
694
 
@@ -792,7 +792,15 @@ function resolveBunRunner() {
792
792
  }
793
793
  let real;
794
794
  try {
795
- real = realpathSync2(executable);
795
+ const execPath = spawnSync(executable, ["-e", "console.log(process.execPath)"], {
796
+ encoding: "utf8",
797
+ stdio: ["ignore", "pipe", "ignore"]
798
+ });
799
+ if (execPath.status === 0 && execPath.stdout.trim().length > 0) {
800
+ real = realpathSync2(execPath.stdout.trim());
801
+ } else {
802
+ real = realpathSync2(executable);
803
+ }
796
804
  } catch {
797
805
  throw new VerificationDescriptorError("bun runner realpath is unresolvable");
798
806
  }
@@ -1351,6 +1359,8 @@ class AssuranceCoordinator {
1351
1359
  return this.sessionGeneration;
1352
1360
  }
1353
1361
  async advance(taskId, ctx, signal, onUpdate) {
1362
+ if (this.isInvocationOpen(taskId))
1363
+ return { state: "blocked", reason: "an authority invocation is already open" };
1354
1364
  const active = this.active(taskId);
1355
1365
  if (active?.state === "review_ready") {
1356
1366
  const reservation = this.reviewReservations.get(taskId);
@@ -1728,6 +1738,17 @@ class AssuranceCoordinator {
1728
1738
  }
1729
1739
  return { state: "blocked", reason: `Kernel requires ${settled.projection.next_obligation} after Review` };
1730
1740
  }
1741
+ isReviewVerdictValid(taskId, verdictInput) {
1742
+ const reservation = this.reviewReservations.get(taskId);
1743
+ if (!reservation)
1744
+ return false;
1745
+ try {
1746
+ parseAssuranceVerdict(verdictInput, reservation.snapshot);
1747
+ return true;
1748
+ } catch {
1749
+ return false;
1750
+ }
1751
+ }
1731
1752
  abandonReview(taskId, reason) {
1732
1753
  const reservation = this.reviewReservations.get(taskId);
1733
1754
  if (!reservation)
@@ -1743,6 +1764,12 @@ class AssuranceCoordinator {
1743
1764
  return { state: "blocked", code: "verdict_invalid", reason: "Review verdict correction is required before advancing" };
1744
1765
  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
1766
  }
1767
+ releaseStoppedReview(taskId) {
1768
+ const reservation = this.reviewReservations.get(taskId);
1769
+ if (reservation)
1770
+ this.releaseReviewReservation(taskId, reservation);
1771
+ this.rejectedReviewOperations.delete(taskId);
1772
+ }
1746
1773
  releaseReviewReservation(taskId, reservation, rejectionReason) {
1747
1774
  if (this.reviewReservations.get(taskId) !== reservation)
1748
1775
  return;
@@ -1804,7 +1831,7 @@ import { tmpdir as tmpdir2 } from "node:os";
1804
1831
  import { join as join4 } from "node:path";
1805
1832
 
1806
1833
  // plugins/immune-brain/runtime/workspace_scope.ts
1807
- import { spawnSync } from "node:child_process";
1834
+ import { spawnSync as spawnSync2 } from "node:child_process";
1808
1835
  import { createHash as createHash6 } from "node:crypto";
1809
1836
  import {
1810
1837
  existsSync as existsSync2,
@@ -1815,7 +1842,7 @@ import {
1815
1842
  } from "node:fs";
1816
1843
  import { resolve as resolve2 } from "node:path";
1817
1844
  function git(root, args) {
1818
- const result = spawnSync("git", ["-C", root, ...args], {
1845
+ const result = spawnSync2("git", ["-C", root, ...args], {
1819
1846
  encoding: "utf8",
1820
1847
  stdio: ["ignore", "pipe", "pipe"]
1821
1848
  });
@@ -1839,7 +1866,7 @@ var portablePathCollator = new Intl.Collator("und", {
1839
1866
  });
1840
1867
  var gitTaskSnapshotTestHook;
1841
1868
  function gitBytes(root, args) {
1842
- const result = spawnSync("git", ["-C", root, ...args], {
1869
+ const result = spawnSync2("git", ["-C", root, ...args], {
1843
1870
  encoding: null,
1844
1871
  stdio: ["ignore", "pipe", "pipe"],
1845
1872
  maxBuffer: 8 * 1024 * 1024
@@ -3001,6 +3028,15 @@ function classifyIntentRevision(previous, next) {
3001
3028
  return "breaking";
3002
3029
  return next.revision > previous.revision ? "compatible" : "breaking";
3003
3030
  }
3031
+
3032
+ class TaskIntentObservationError extends Error {
3033
+ code;
3034
+ constructor(code, message) {
3035
+ super(message);
3036
+ this.name = "TaskIntentObservationError";
3037
+ this.code = code;
3038
+ }
3039
+ }
3004
3040
  var intentReaderTestHook = null;
3005
3041
  function validateTaskId3(taskId) {
3006
3042
  if (!TASK_ID_PATTERN.test(taskId))
@@ -3065,7 +3101,7 @@ function assertIdentitiesUnchanged(expected, canonicalRoot, relativePath) {
3065
3101
  throw new Error(`path component changed while being read: ${parts.slice(0, index + 1).join("/")}`);
3066
3102
  }
3067
3103
  }
3068
- function readTaskIntent(root, taskId, requestedPath) {
3104
+ function readTaskIntentSource(root, taskId, requestedPath) {
3069
3105
  validateTaskId3(taskId);
3070
3106
  const canonicalRoot = resolveCanonicalRoot(root);
3071
3107
  const activePath = `${INTENT_SIDECAR_RELATIVE_PREFIX}${taskId}.intent.json`;
@@ -3077,17 +3113,19 @@ function readTaskIntent(root, taskId, requestedPath) {
3077
3113
  if (!target.startsWith(canonicalRoot + sep3))
3078
3114
  throw new Error("intent sidecar escapes project root");
3079
3115
  if (!sidecarPresent(canonicalRoot, sidecarPath))
3080
- throw new Error(`TaskIntent sidecar is missing at ${sidecarPath}`);
3116
+ throw new TaskIntentObservationError("missing", `TaskIntent sidecar is missing at ${sidecarPath}`);
3081
3117
  const pathIdentities = collectPathIdentities(canonicalRoot, sidecarPath);
3082
3118
  const fileIdentity = pathIdentities[pathIdentities.length - 1];
3083
3119
  try {
3084
3120
  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");
3121
+ } catch (error) {
3122
+ if (typeof error === "object" && error !== null && "status" in error && error.status === 1)
3123
+ throw new TaskIntentObservationError("invalid", "TaskIntent sidecar is not Git-tracked");
3124
+ throw error;
3087
3125
  }
3088
3126
  const before = lstatSync4(target);
3089
3127
  if (!before.isFile() || before.size > INTENT_MAX_BYTES)
3090
- throw new Error("TaskIntent sidecar must be a regular file no larger than 64 KiB");
3128
+ throw new TaskIntentObservationError("invalid", "TaskIntent sidecar must be a regular file no larger than 64 KiB");
3091
3129
  const fd = openSync2(target, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
3092
3130
  let bytes;
3093
3131
  try {
@@ -3099,7 +3137,7 @@ function readTaskIntent(root, taskId, requestedPath) {
3099
3137
  closeSync2(fd);
3100
3138
  }
3101
3139
  if (bytes.byteLength > INTENT_MAX_BYTES)
3102
- throw new Error("TaskIntent sidecar exceeds 64 KiB");
3140
+ throw new TaskIntentObservationError("invalid", "TaskIntent sidecar exceeds 64 KiB");
3103
3141
  const after = lstatSync4(target);
3104
3142
  assertSameIdentity(statIdentity(before), after, "intent sidecar");
3105
3143
  assertIdentitiesUnchanged(pathIdentities, canonicalRoot, sidecarPath);
@@ -3113,23 +3151,11 @@ function readTaskIntent(root, taskId, requestedPath) {
3113
3151
  try {
3114
3152
  intent = parseTaskIntentV1(JSON.parse(bytes.toString("utf8")));
3115
3153
  } catch (error) {
3116
- throw new Error(`TaskIntent sidecar is invalid: ${error instanceof Error ? error.message : String(error)}`);
3154
+ throw new TaskIntentObservationError("invalid", `TaskIntent sidecar is invalid: ${error instanceof Error ? error.message : String(error)}`);
3117
3155
  }
3118
3156
  if (intent.task_id !== taskId)
3119
- throw new Error("intent.task_id does not match the sidecar filename task id");
3157
+ throw new TaskIntentObservationError("invalid", "intent.task_id does not match the sidecar filename task id");
3120
3158
  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
3159
  return {
3134
3160
  intent,
3135
3161
  content_hash: contentHash,
@@ -3138,9 +3164,24 @@ function readTaskIntent(root, taskId, requestedPath) {
3138
3164
  revision: intent.revision,
3139
3165
  content_hash: contentHash
3140
3166
  },
3141
- token
3167
+ identity: {
3168
+ canonical_root: canonicalRoot,
3169
+ sidecar_path: sidecarPath,
3170
+ path_dev: fileIdentity.dev,
3171
+ path_ino: fileIdentity.ino,
3172
+ fd_dev: before.dev,
3173
+ fd_ino: before.ino,
3174
+ fd_size: before.size,
3175
+ fd_mtime_ms: before.mtimeMs,
3176
+ source_bytes_sha256: sourceBytesSha256,
3177
+ intent_content_hash: contentHash
3178
+ }
3142
3179
  };
3143
3180
  }
3181
+ function readTaskIntent(root, taskId, requestedPath) {
3182
+ const { identity, ...observed } = readTaskIntentSource(root, taskId, requestedPath);
3183
+ return { ...observed, token: mintToken(identity) };
3184
+ }
3144
3185
 
3145
3186
  // plugins/immune-brain/runtime/kernel/validation.ts
3146
3187
  var GIT_OBJECT_ID3 = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
@@ -3645,6 +3686,7 @@ var ACTION_V2_TYPES = [
3645
3686
  "request_rework",
3646
3687
  "complete",
3647
3688
  "stop",
3689
+ "authorize_rework",
3648
3690
  "resolve_user_decision"
3649
3691
  ];
3650
3692
  var ACTION_BASE_FIELDS = [
@@ -3740,7 +3782,8 @@ function parseTaskAction(raw) {
3740
3782
  };
3741
3783
  break;
3742
3784
  }
3743
- case "complete": {
3785
+ case "complete":
3786
+ case "authorize_rework": {
3744
3787
  rejectUnknown2(value, [...ACTION_BASE_FIELDS], "action", violations);
3745
3788
  action = { ...base, type: base.type };
3746
3789
  break;
@@ -3798,7 +3841,7 @@ function assertTaskRecordUpdateV3(previousRaw, nextRaw, action) {
3798
3841
  violations.push("non-intent action cannot change the intent snapshot");
3799
3842
  if (next.intent_ref.content_hash !== previous.intent_ref.content_hash)
3800
3843
  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")
3844
+ if (next.intent_ref.path !== previous.intent_ref.path && action.type !== "request_rework" && action.type !== "authorize_rework" && action.type !== "stop")
3802
3845
  violations.push("only artifact transitions may change intent_ref path");
3803
3846
  }
3804
3847
  if (next.attestations.length < previous.attestations.length)
@@ -3810,7 +3853,7 @@ function assertTaskRecordUpdateV3(previousRaw, nextRaw, action) {
3810
3853
  if (!current || JSON.stringify(current) !== JSON.stringify(prior))
3811
3854
  violations.push(`attestation ${prior.id} was rewritten`);
3812
3855
  }
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) : [];
3856
+ 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
3857
  const reworkFindingIds = action.type === "request_rework" ? new Set(action.findings.map((item) => item.id)) : new Set;
3815
3858
  for (const prior of previous.findings) {
3816
3859
  const current = next.findings.find((item) => item.id === prior.id);
@@ -5039,6 +5082,8 @@ function deriveAssuranceAuthorization(input) {
5039
5082
  state: "none",
5040
5083
  blocked: `resolve-user-decision requires exactly one open user decision; found ${input.open_user_decision_count}`
5041
5084
  };
5085
+ if (input.next_obligation === "revise_intent")
5086
+ return { state: "authorize_rework", blocked: null };
5042
5087
  return { state: "none", blocked: null };
5043
5088
  }
5044
5089
  function emptyProjection() {
@@ -5286,10 +5331,12 @@ function appendHistory(record, action, from, detail, audit) {
5286
5331
  record.history.push(entry);
5287
5332
  }
5288
5333
  function intentRefMatches(intent, ref) {
5289
- return ref.path === `docs/plans/${intent.task_id}.intent.json` && ref.content_hash === canonicalIntentHash(intent);
5334
+ const activePath = `docs/plans/${intent.task_id}.intent.json`;
5335
+ const archivedPath = `docs/plans/archive/${intent.task_id}.intent.json`;
5336
+ return (ref.path === activePath || ref.path === archivedPath) && ref.content_hash === canonicalIntentHash(intent);
5290
5337
  }
5291
5338
  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";
5339
+ 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
5340
  }
5294
5341
  function findingsDigestV2(findings) {
5295
5342
  const normalized = findings.map((finding) => ({
@@ -5507,8 +5554,8 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
5507
5554
  "request_rework requires review, qa, or user authority"
5508
5555
  ]);
5509
5556
  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;
5557
+ const hasPriorBlockingReviewRework = record.findings.some((finding) => finding.source === "review" && finding.kind === "blocking" && finding.review_round !== null);
5558
+ const parkForReplan = authorityAudit.authority_kind === "review" && hasPriorBlockingReviewRework && action.findings.some((finding) => finding.kind === "blocking");
5512
5559
  if (!parkForReplan) {
5513
5560
  record.artifact_state = "active";
5514
5561
  record.intent_ref.path = `docs/plans/${record.task_id}.intent.json`;
@@ -5523,8 +5570,8 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
5523
5570
  record.findings.push({
5524
5571
  ...finding,
5525
5572
  status: "open",
5526
- source: "review",
5527
- review_round: round
5573
+ source: authorityAudit.authority_kind === "review" ? "review" : "execution",
5574
+ review_round: authorityAudit.authority_kind === "review" ? round : null
5528
5575
  });
5529
5576
  }
5530
5577
  if (parkForReplan && !record.findings.some((item) => item.status === "open" && item.kind === "replan_required")) {
@@ -5547,6 +5594,27 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
5547
5594
  appendHistory(record, action, from, `review_round_${round}`, authorityAudit);
5548
5595
  break;
5549
5596
  }
5597
+ case "authorize_rework": {
5598
+ if (record.lifecycle !== "active")
5599
+ throw new KernelInvariantError([
5600
+ `cannot authorize rework while lifecycle is ${record.lifecycle}`
5601
+ ]);
5602
+ if (authorityAudit?.authority_kind !== "user")
5603
+ throw new KernelInvariantError([
5604
+ "authorize_rework requires literal-user authority"
5605
+ ]);
5606
+ const open = record.findings.filter((finding) => finding.kind === "replan_required" && finding.status === "open");
5607
+ if (open.length === 0)
5608
+ throw new KernelInvariantError([
5609
+ "authorize_rework requires an open replan boundary"
5610
+ ]);
5611
+ for (const finding of open)
5612
+ finding.status = "resolved";
5613
+ record.artifact_state = "active";
5614
+ record.intent_ref.path = `docs/plans/${record.task_id}.intent.json`;
5615
+ appendHistory(record, action, from, open.map((finding) => finding.id).join(","), authorityAudit);
5616
+ break;
5617
+ }
5550
5618
  case "complete": {
5551
5619
  if (record.lifecycle !== "active" || record.artifact_state !== "frozen")
5552
5620
  throw new KernelInvariantError([
@@ -5677,7 +5745,7 @@ function applyTaskAction(input) {
5677
5745
  "intent token does not match the committed record intent"
5678
5746
  ]);
5679
5747
  }
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";
5748
+ 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
5749
  const expectedAuthority = privileged ? {
5682
5750
  task_id,
5683
5751
  action,
@@ -5803,6 +5871,8 @@ function capabilityActionFor(input) {
5803
5871
  return { ...base, approval: input.approval };
5804
5872
  case "request_rework":
5805
5873
  return { ...base, findings: input.findings };
5874
+ case "authorize_rework":
5875
+ return { ...base, type: "authorize_rework" };
5806
5876
  case "stop":
5807
5877
  return { ...base, reason: input.reason };
5808
5878
  case "approve_breaking_intent_revision":
@@ -5949,7 +6019,7 @@ function createCanaryApplication(registry) {
5949
6019
  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
6020
  if (operation.op === "complete" && hasBoundSpec && snapshot.record.artifact_state !== "frozen")
5951
6021
  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;
6022
+ 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
6023
  const event_id = `${operation.op}:${input.task_id}:${at}`;
5954
6024
  const base = {
5955
6025
  event_id,
@@ -5991,7 +6061,7 @@ function createCanaryApplication(registry) {
5991
6061
  type: "revise_intent",
5992
6062
  next_intent: operation.next_intent,
5993
6063
  next_intent_ref: {
5994
- path: `docs/plans/${operation.next_intent.task_id}.intent.json`,
6064
+ path: snapshot.record.artifact_state === "frozen" ? `docs/plans/archive/${operation.next_intent.task_id}.intent.json` : `docs/plans/${operation.next_intent.task_id}.intent.json`,
5995
6065
  content_hash: canonicalIntentHash(operation.next_intent)
5996
6066
  }
5997
6067
  };
@@ -6015,6 +6085,10 @@ function createCanaryApplication(registry) {
6015
6085
  capability = operation.capability;
6016
6086
  action = { ...base, type: "stop", reason: operation.reason };
6017
6087
  break;
6088
+ case "authorize_rework":
6089
+ capability = operation.capability;
6090
+ action = { ...base, type: "authorize_rework" };
6091
+ break;
6018
6092
  case "resolve_user_decision":
6019
6093
  capability = operation.capability;
6020
6094
  action = {
@@ -6272,12 +6346,12 @@ function createEnrollmentAuthorityRegistry() {
6272
6346
 
6273
6347
  // plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts
6274
6348
  import { createHash as createHash12 } from "node:crypto";
6275
- import { spawnSync as spawnSync2 } from "node:child_process";
6349
+ import { spawnSync as spawnSync3 } from "node:child_process";
6276
6350
  import { resolve as resolve6 } from "node:path";
6277
6351
  var SOURCE_PATH = ".imm/state/workspace.json";
6278
6352
  var GIT_OBJECT_ID4 = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
6279
6353
  function readGitHead(root) {
6280
- const result = spawnSync2("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
6354
+ const result = spawnSync3("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
6281
6355
  encoding: "utf8"
6282
6356
  });
6283
6357
  const head = typeof result.stdout === "string" ? result.stdout.trim() : "";
@@ -6477,7 +6551,16 @@ function enrollCanaryTask(root, input, registry) {
6477
6551
  throw new Error("intent content hash mismatch");
6478
6552
  if (checks.gitBaseHead !== gitBaseHead)
6479
6553
  throw new Error("Git HEAD moved after the enrollment confirmation");
6554
+ if (input.batch) {
6555
+ const batch = input.batch.registry.inspect(input.batch.capability, input.batch.binding, Date.parse(input.now));
6556
+ if (input.batch.registry.consumedChildren(input.batch.capability).length === 0 && input.batch.expected_head !== batch.base_head)
6557
+ 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}`);
6558
+ if (checks.gitBaseHead !== input.batch.expected_head)
6559
+ throw new Error(`batch_head_lineage_broken: expected ${input.batch.expected_head}, found ${checks.gitBaseHead}`);
6560
+ }
6480
6561
  registry.consume(input.capability, input.capability_binding);
6562
+ if (input.batch)
6563
+ input.batch.registry.consumeChild(input.batch.capability, input.batch.binding, input.task_id, Date.parse(input.now));
6481
6564
  if (!gitBaseHead)
6482
6565
  throw new Error("enrollment requires a committed Git HEAD");
6483
6566
  const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
@@ -6496,16 +6579,23 @@ function enrollCanaryTask(root, input, registry) {
6496
6579
  created_at: input.now,
6497
6580
  updated_at: input.now
6498
6581
  };
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)}
6582
+ let mutation;
6583
+ try {
6584
+ mutation = commitEnrollmentLocked(root, input.task_id, {
6585
+ contract: "assurance_kernel/workspace_transaction/v2",
6586
+ task_id: input.task_id,
6587
+ expected_record_hash: checks.current.revision,
6588
+ next_record_content: `${JSON.stringify(record, null, 2)}
6504
6589
  `,
6505
- expected_workspace_hash: checks.workspace.revision,
6506
- next_workspace_content: `${JSON.stringify(nextWorkspace, null, 2)}
6590
+ expected_workspace_hash: checks.workspace.revision,
6591
+ next_workspace_content: `${JSON.stringify(nextWorkspace, null, 2)}
6507
6592
  `
6508
- }, claim);
6593
+ }, claim);
6594
+ } catch (error) {
6595
+ if (input.batch)
6596
+ input.batch.registry.releaseChild(input.batch.capability, input.task_id);
6597
+ throw error;
6598
+ }
6509
6599
  return {
6510
6600
  record: mutation.record,
6511
6601
  backend_claim: claim,
@@ -6645,14 +6735,18 @@ async function submitClaudeReview(host, coordinator, ctx, taskId, verdictInput)
6645
6735
  return coordinator.abandonReview(taskId, observed.reason);
6646
6736
  return { state: "blocked", reason: observed.reason };
6647
6737
  }
6738
+ const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
6739
+ if (!parentValid)
6740
+ return coordinator.submitReview(taskId, ctx, verdictInput);
6741
+ const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
6742
+ if (!receiptValid) {
6743
+ return coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict");
6744
+ }
6648
6745
  const parentJson = extractVerdictJson(verdictInput);
6649
6746
  const receiptJson = extractVerdictJson(observed.receipt.result);
6650
- if (parentJson && receiptJson && verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
6747
+ if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
6651
6748
  return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
6652
6749
  }
6653
- if (parentJson && !receiptJson) {
6654
- return { state: "blocked", reason: "reviewer receipt is not a valid verdict" };
6655
- }
6656
6750
  return coordinator.submitReview(taskId, ctx, verdictInput);
6657
6751
  }
6658
6752
  function stopReason(value) {
@@ -6948,6 +7042,12 @@ class ClaudeRuntime {
6948
7042
  async submitReview(taskId, verdictInput) {
6949
7043
  return submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput);
6950
7044
  }
7045
+ async resolveFinding(taskId, findingId) {
7046
+ return this.executeOrdinary({ cwd: this.cwd }, {
7047
+ taskId,
7048
+ operation: { op: "resolve_finding", finding_id: findingId, actor_id: "executor" }
7049
+ });
7050
+ }
6951
7051
  async authorize(taskId, operation, meta, extra = {}) {
6952
7052
  if (operation === "repair_authority_state") {
6953
7053
  const authority = reconcileKernelAuthority(this.cwd, taskId);
@@ -6972,6 +7072,8 @@ class ClaudeRuntime {
6972
7072
  throw new Error(`resolve-user-decision requires exactly one open user decision; found ${open.length}`);
6973
7073
  op = "resolve_user_decision";
6974
7074
  decisionOp = { finding_id: open[0].id, resolution: `resume after literal-user decision: ${open[0].summary}` };
7075
+ } else if (readiness.state === "authorize_rework") {
7076
+ op = "authorize_rework";
6975
7077
  } else {
6976
7078
  throw new Error(readiness.blocked ?? "no unique host-derived authorization operation");
6977
7079
  }
@@ -7088,7 +7190,7 @@ class ClaudeRuntime {
7088
7190
  diffProvider: diffSnapshotOf,
7089
7191
  now
7090
7192
  });
7091
- if (op === "stop" || op === "approve_breaking_intent_revision")
7193
+ if (op === "stop" || op === "authorize_rework" || op === "approve_breaking_intent_revision")
7092
7194
  stagePlanningArtifactTransition(this.cwd, result.record);
7093
7195
  return result;
7094
7196
  } catch (error) {
@@ -7224,7 +7326,8 @@ var TOOLS = [
7224
7326
  { name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
7225
7327
  { name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
7226
7328
  { 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 }
7329
+ { name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false },
7330
+ { name: "resolve_finding", description: "Resolve one open blocking or advisory finding whose cause is fixed and verified.", privileged: false }
7228
7331
  ];
7229
7332
  function listMcpTools() {
7230
7333
  return TOOLS.map((tool) => ({
@@ -7236,9 +7339,10 @@ function listMcpTools() {
7236
7339
  task_id: { type: "string" },
7237
7340
  ...tool.name === "approve_breaking_intent_revision" ? { next_intent: { type: "object" } } : {},
7238
7341
  ...tool.name === "stop" ? { reason: { type: "string" } } : {},
7239
- ...tool.name === "submit_review" ? { verdict: { type: "object" } } : {}
7342
+ ...tool.name === "submit_review" ? { verdict: { type: "object" } } : {},
7343
+ ...tool.name === "resolve_finding" ? { finding_id: { type: "string" } } : {}
7240
7344
  },
7241
- required: tool.name === "submit_review" ? ["task_id", "verdict"] : ["task_id"]
7345
+ required: tool.name === "submit_review" ? ["task_id", "verdict"] : tool.name === "resolve_finding" ? ["task_id", "finding_id"] : ["task_id"]
7242
7346
  },
7243
7347
  annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" }
7244
7348
  }));
@@ -7305,6 +7409,11 @@ function createMcpRuntime(options = {}) {
7305
7409
  throw new Error("verdict is required");
7306
7410
  return runtime.submitReview(taskId, args.verdict);
7307
7411
  }
7412
+ if (name === "resolve_finding") {
7413
+ if (typeof args.finding_id !== "string" || !args.finding_id)
7414
+ throw new Error("finding_id is required");
7415
+ return runtime.resolveFinding(taskId, args.finding_id);
7416
+ }
7308
7417
  if (name === "request_authorization" || name === "approve_breaking_intent_revision" || name === "stop" || name === "repair_authority_state") {
7309
7418
  return runtime.authorize(taskId, name, toolMeta, args);
7310
7419
  }
@@ -23,7 +23,7 @@ Apply the gate when the task touches one or more of these surfaces:
23
23
  - **Technical Design baseline**: keep the Spec as the single design authority and make each Plan Step reference the applicable decision or invariant instead of duplicating design prose.
24
24
  - **design-view selection**: for Medium/High risk, select every materially relevant technical-design view from architecture layers, service/component interfaces, data flow, state transitions, and temporal sequence. Record selected views and why omitted views cannot affect the design. Low risk remains concise.
25
25
  - **TaskIntent decomposition**: use Technical Design boundaries as one retain/split criterion with outcome, Verification, dependency, risk, rollback, compatibility, and authority. Split a successor TaskIntent only when a service, state-machine owner, migration, independently promotable layer, or sequence dependency needs independent verification, rollback, authorization, or settlement. A TaskIntent should normally change one primary trust-boundary invariant, but traversing several boundaries or updating both sides of one authority chain does not itself require a split. Split independently verifiable, reversible, authorizable, migratable, or settleable trust invariants. Keep multiple trust-boundary changes together only for one atomic security outcome whose split would create an unsafe or unusable intermediate state, and record that rationale in the Spec. Treat this as Planner judgment rather than a schema field or Enrollment counting rule. Do not split merely because the design names several layers, files, or services, and do not revive prose Plan authority.
26
- - **Mermaid intent**: use Mermaid only when it clarifies structure, sequence, data flow, or state transitions; it is not a universal gate or a second source of truth. Every new or revised Spec records `**Diagram decision**: required|not_required` and a non-empty `**Diagram reason**:`. A `required` decision must include Mermaid; `not_required` explains why prose is sufficient.
26
+ - **Mermaid intent**: use Mermaid only when it clarifies structure, sequence, data flow, or state transitions; it is not a universal gate or a second source of truth. Medium/High risk Specs record `**Diagram decision**: required|not_required` and a non-empty `**Diagram reason**:`. A `required` decision must include Mermaid; `not_required` explains why prose is sufficient. Low-risk Specs omit the empty ceremony and record neither field.
27
27
  - **Design Conformance**: before final closure, require Spec-to-implementation evidence. A local implementation mismatch routes to `rework`; a structural or intended design change routes to `replan` through Planner. QA cannot silently approve a design change.
28
28
  - **Brainstorm traceability**: ensure every `BR-*` item listed in `Brainstorm manifest` is mapped in `Brainstorm Trace`.
29
29
  - **roadmap information preservation**: for large or multi-phase work, distinguish the Roadmap from the current executable slice, preserve deferred phase goals, open questions, promotion criteria, and candidate next Plans.
@@ -104,7 +104,7 @@ Parent workflow role 必须:
104
104
  4. 把 partial/error 标记为 `degraded`。
105
105
  5. 保留自身 baseline review,不把最终判断权交给 child。
106
106
 
107
- 普通 advisory/discovery 的每次启动都消耗一个 candidate budget slot;失败、取消、timeout 或 result_untrusted 均丢弃该输出且不得自动重试。Parent 仅在剩余候选仍独立有用且 evidence budget 仍需要时继续,否则转 solo/fail-closed fallback,并记录 `dispatch_failed` 或 `child_timeout`。该规则不改变 Kernel authority Review 的显式恢复协议。Child 永远不获得实现、Plan write、workflow mutation 或 QA closure authority。
107
+ 普通 advisory/discovery 的每次启动都消耗一个 candidate budget slot;失败、取消、timeout 或 result_untrusted 均丢弃该输出且不得自动重试。Parent 仅在剩余候选仍独立有用且 evidence budget 仍需要时继续,否则转 solo/fail-closed fallback,并记录 `dispatch_failed` 或 `child_timeout`。Read-only eligibility 与 Pi 的 one-foreground-child 调度限制是两回事:只读调查可同时存在多个待派发候选,但实际执行仍逐个 foreground child 串行消费,不得把多个 foreground Agent 假定为并发 batch。该规则不改变 Kernel authority Review 的显式恢复协议。Child 永远不获得实现、Plan write、workflow mutation 或 QA closure authority。
108
108
 
109
109
  If Kernel Review dispatch fails, the Parent does not call `submit_review`; the existing Review reservation and immutable evidence remain available for a later foreground retry. A malformed verdict may be corrected and resubmitted. A stale snapshot, explicit release, successful settlement, or session shutdown removes the reservation and evidence. There is no retry counter, dispatch receipt state machine, or provider-specific recovery path.
110
110
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: imm-agent-doc-maintain
3
- description: Use to minimize tracked AGENTS.md, CLAUDE.md, and GEMINI.md files to necessary non-discoverable context after an explicit, hash-bound, user-approved manifest; never installs or validates project contracts.
3
+ description: Use when the user explicitly requests Immune-Brain minimization of tracked AGENTS.md, CLAUDE.md, or GEMINI.md.
4
4
  ---
5
5
 
6
6
  # Immune-Brain: Agent Doc Maintain
@@ -37,6 +37,8 @@ commit.
37
37
 
38
38
  ## Ordered Maintenance Protocol
39
39
 
40
+ ### Inventory and Manifest
41
+
40
42
  1. **Establish repository safety.** Mutation requires a Git worktree. Only
41
43
  tracked regular files named exactly `AGENTS.md`, `CLAUDE.md`, or `GEMINI.md`,
42
44
  at the repository root or in nested tracked directories, are candidates.
@@ -100,6 +102,8 @@ commit.
100
102
  repository scope or declared precedence are `BLOCKED`. Filename convention,
101
103
  nesting, or guessed host behavior alone may not resolve a conflict.
102
104
 
105
+ ### Manifest Approval and Recovery
106
+
103
107
  7. **Produce one exact manifest.** `audit` mode stops after the manifest.
104
108
  Mutation mode also stops until the literal user approves exact manifest
105
109
  entries (for example, "all recommendations except 4 and 7"). Broad approval
@@ -107,6 +111,8 @@ commit.
107
111
  no manifest is persisted. No fixed line, byte, percentage, or Token target
108
112
  authorizes removal.
109
113
 
114
+ ### Approved Mutation
115
+
110
116
  8. **Revalidate and mutate minimally.** Re-read candidate bytes, Git status,
111
117
  content hash, references, precedence evidence, and active scope immediately
112
118
  before each approved change. Drift blocks that item. Never execute commands
@@ -116,6 +122,8 @@ commit.
116
122
  basics, and explicit user requirements are never simplified away for
117
123
  brevity.
118
124
 
125
+ ### Verify and Report
126
+
119
127
  9. **Verify and report.** Re-scan modified instruction relationships, local
120
128
  pointer targets, duplicate retained meanings, unresolved conflicts,
121
129
  source/package public-surface parity, existing focused documentation