immune-brain 4.2.0 → 4.4.0

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.
@@ -506,7 +506,7 @@ a State Ledger. Keep historical Plan validation strictly read-only.
506
506
 
507
507
  ## Verification
508
508
 
509
- - Validate every candidate through `imm-kernel intent validate <path> --json` after authoring and staging. Require `valid: true` and `enrollment_ready: true` before Enrollment.
509
+ - Validate every candidate through `imm-kernel intent validate <path> --json` after authoring and staging. Require `valid: true` and `enrollment_ready: true` before Enrollment. When `git_base_initialization_required: true`, report that the existing native Enrollment confirmation includes an empty root commit; leave initialization to the Host. An invalid `git_base` blocks Enrollment and requires Git repair.
510
510
  - For complex work, verify Spec design metadata, document language, reference closure, concrete descriptor paths, and complete Brainstorm traceability before handoff. Simple TaskIntent-only work verifies those properties on the Intent.
511
511
  - Enrollment validates descriptor structure only. Deterministic QA owns descriptor execution after implementation; planning does not run the acceptance suite.
512
512
  - Managed execution handoff is Git-tracked TaskIntent author/validate plus current-Host native Enrollment. Do not sync a v3 State Ledger or invoke a missing dispatcher.
@@ -0,0 +1,83 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { preparePiCanary, readGitHead, revalidatePiCanary, type PiCanaryPreparation, type PiCanaryPrepareInput } from "../kernel/pi_canary_prepare";
3
+ import { withKernelStoreLock } from "../kernel/storage";
4
+
5
+ export type EnrollmentGitBase = { state: "committed"; head: string } | { state: "unborn"; branch: string };
6
+
7
+ function git(root: string, args: string[], input?: string, env = process.env): string {
8
+ return execFileSync("git", args, { cwd: root, encoding: "utf8", input, env, stdio: ["pipe", "pipe", "pipe"] }).trim();
9
+ }
10
+
11
+ /** Read-only. A missing branch is bootstrap-able; a broken existing ref is not. */
12
+ export function inspectEnrollmentGitBase(root: string): EnrollmentGitBase {
13
+ if (git(root, ["rev-parse", "--is-inside-work-tree"]) !== "true")
14
+ throw new Error("Enrollment requires a Git working tree");
15
+ try { return { state: "committed", head: readGitHead(root) }; } catch { /* Inspect the unborn case below. */ }
16
+ const branch = git(root, ["symbolic-ref", "--no-recurse", "HEAD"]);
17
+ if (!branch.startsWith("refs/heads/")) throw new Error("Enrollment HEAD must name a local branch");
18
+ const ref = spawnSync("git", ["show-ref", "--verify", "--quiet", branch], { cwd: root });
19
+ const symbolic = spawnSync("git", ["symbolic-ref", "-q", branch], { cwd: root });
20
+ if (ref.status !== 1 || symbolic.status !== 1)
21
+ throw new Error("Enrollment Git HEAD is invalid; only an absent branch can be initialized");
22
+ return { state: "unborn", branch };
23
+ }
24
+
25
+ export function enrollmentGitBaseNotice(base: EnrollmentGitBase): string | undefined {
26
+ return base.state === "unborn"
27
+ ? `Git: create an empty initial commit on ${base.branch} before Enrollment; staged files and working tree remain unchanged. This commit remains if later Enrollment fails or is cancelled.`
28
+ : undefined;
29
+ }
30
+
31
+ function identityEnv(root: string): NodeJS.ProcessEnv {
32
+ const configured = (key: string): string => {
33
+ const result = spawnSync("git", ["config", "--get", key], { cwd: root, encoding: "utf8" });
34
+ if (result.status !== 0 && result.status !== 1) throw new Error(`Cannot read Git identity: ${key}`);
35
+ return result.stdout?.trim() ?? "";
36
+ };
37
+ const name = configured("user.name") || "Immune-Brain Enrollment";
38
+ const email = configured("user.email") || "enrollment@immune-brain.local";
39
+ return { ...process.env,
40
+ GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || name, GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL || email,
41
+ GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || name, GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || email,
42
+ };
43
+ }
44
+
45
+ /** Called only after native confirmation. Never stages files or writes Git config. */
46
+ export function initializeEnrollmentGitBase(
47
+ root: string, input: PiCanaryPrepareInput, previous: PiCanaryPreparation,
48
+ base: EnrollmentGitBase, signal?: AbortSignal,
49
+ ): PiCanaryPreparation {
50
+ const assertUnchanged = () => {
51
+ signal?.throwIfAborted();
52
+ if (!revalidatePiCanary(root, input, previous).unchanged
53
+ || JSON.stringify(inspectEnrollmentGitBase(root)) !== JSON.stringify(base))
54
+ throw new Error("Workspace or Git branch changed after confirmation; retry Enrollment");
55
+ };
56
+ assertUnchanged();
57
+ if (base.state === "committed") return previous;
58
+ return withKernelStoreLock(root, () => {
59
+ // CAS the captured branch itself, never HEAD: a concurrent symbolic HEAD
60
+ // switch must not redirect the write to another branch. Recheck HEAD after
61
+ // the write and report any created base if the workspace changed.
62
+ let created: string | undefined;
63
+ try {
64
+ assertUnchanged();
65
+ if (!previous.intent || previous.backend_claim.present || previous.task_record_v3?.present || previous.workspace.current_working !== null)
66
+ throw new Error("Empty Git base initialization requires an unowned workspace and a tracked TaskIntent");
67
+ const tree = git(root, ["hash-object", "-t", "tree", "-w", "--stdin"], "");
68
+ const commit = git(root, ["commit-tree", tree, "-m", "chore: initialize empty Enrollment base"], undefined, identityEnv(root));
69
+ assertUnchanged();
70
+ git(root, ["update-ref", "--no-deref", base.branch, commit, "0".repeat(commit.length)]);
71
+ created = commit;
72
+ const current = preparePiCanary(root, input);
73
+ const withoutGit = ({ digest, git_base_head, git_error, ...owners }: PiCanaryPreparation) => owners;
74
+ if (git(root, ["symbolic-ref", "--no-recurse", "HEAD"]) !== base.branch || current.git_base_head !== commit
75
+ || JSON.stringify(withoutGit(current)) !== JSON.stringify(withoutGit(previous)))
76
+ throw new Error("Workspace changed during empty Git base initialization; retry Enrollment");
77
+ return current;
78
+ } catch (error) {
79
+ if (created) throw new Error(`${error instanceof Error ? error.message : String(error)}; empty initial commit ${created} remains`);
80
+ throw error;
81
+ }
82
+ });
83
+ }
@@ -74,6 +74,7 @@ export interface NativeConfirmationInput {
74
74
  intentRevision?: number;
75
75
  intentContentHash?: string;
76
76
  bindingDigest?: string;
77
+ gitBaseNotice?: string;
77
78
  planDigest?: string;
78
79
  batchDetails?: {
79
80
  initiative_slug: string;
@@ -39,6 +39,7 @@ import {
39
39
  } from "../kernel/enrollment_authority";
40
40
  import { enrollCanaryTask, runEnrollmentRehearsal } from "../kernel/enrollment";
41
41
  import { reconcileKernelAuthority, repairKernelAuthority } from "../kernel/storage";
42
+ import { inspectEnrollmentGitBase, enrollmentGitBaseNotice, initializeEnrollmentGitBase } from "../assurance/enrollment_git_base";
42
43
  import { preparePiCanary, revalidatePiCanary } from "../kernel/pi_canary_prepare";
43
44
  import { runDeterministicQa } from "../assurance/qa";
44
45
  import { taskDiffIdentity, taskRevisionIdentity } from "../workspace_scope";
@@ -524,6 +525,7 @@ export class ClaudeRuntime {
524
525
  intentRevision?: number;
525
526
  intentContentHash?: string;
526
527
  bindingDigest?: string;
528
+ gitBaseNotice?: string;
527
529
  } = {}): Promise<{ confirmation_ref: string }> {
528
530
  throwIfCancelled(meta.signal);
529
531
  const probe = probeHost(this.env, process.platform, this.hostVersion);
@@ -557,45 +559,55 @@ export class ClaudeRuntime {
557
559
 
558
560
  async enroll(taskId: string, meta: ToolMeta) {
559
561
  const now = new Date().toISOString();
560
- const preparation = await preparePiCanary(this.cwd, { task_id: taskId, now });
562
+ let preparation = await preparePiCanary(this.cwd, { task_id: taskId, now });
561
563
  const intent = await readTaskIntentForRecord(this.cwd, taskId);
564
+ const gitBase = inspectEnrollmentGitBase(this.cwd);
562
565
  const gate = await this.gate("enroll", { ...meta, taskId }, {
563
566
  risk: intent.intent.risk,
564
567
  intentRevision: preparation.intent?.revision,
565
568
  intentContentHash: preparation.intent?.content_hash,
566
569
  bindingDigest: preparation.digest,
570
+ gitBaseNotice: enrollmentGitBaseNotice(gitBase),
567
571
  });
568
572
  const { unchanged } = await revalidatePiCanary(this.cwd, { task_id: taskId, now }, preparation);
569
573
  if (!unchanged) throw new NativeAuthorityError("workspace_changed", "workspace changed after native confirmation");
570
574
  if (!preparation.intent) throw new Error("enrollment requires a readable TaskIntent");
571
575
  throwIfCancelled(meta.signal);
572
- const nonce = enrollmentNonce();
573
- const binding: EnrollmentCapabilityBinding = {
574
- task_id: taskId,
575
- intent_path: preparation.intent.path,
576
- intent_revision: preparation.intent.revision,
577
- intent_content_hash: preparation.intent.content_hash,
578
- preparation_digest: preparation.digest,
579
- actor_id: LITERAL_USER_ACTOR_ID,
580
- confirmation_ref: gate.confirmation_ref,
581
- expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
582
- nonce,
583
- };
584
- const capability = this.enrollmentRegistry.issue(binding);
585
- const input = {
586
- task_id: taskId,
587
- intent_path: binding.intent_path,
588
- intent_revision: binding.intent_revision,
589
- preparation_digest: binding.preparation_digest,
590
- capability,
591
- capability_binding: binding,
592
- now,
593
- };
594
- const rehearsal = runEnrollmentRehearsal(this.cwd, input, capability, this.enrollmentRegistry);
595
- if (!rehearsal.rehearsed || rehearsal.evidence.outcome !== "ready") {
596
- throw new Error(`Kernel enrollment rehearsal failed: ${rehearsal.evidence.blockers.join("; ")}`);
576
+ preparation = initializeEnrollmentGitBase(this.cwd, { task_id: taskId, now }, preparation, gitBase, meta.signal);
577
+ const gitBaseNote = gitBase.state === "unborn" ? `; empty initial commit ${preparation.git_base_head} remains` : "";
578
+ try {
579
+ if (!preparation.intent) throw new Error("Enrollment requires a readable TaskIntent");
580
+ const nonce = enrollmentNonce();
581
+ const binding: EnrollmentCapabilityBinding = {
582
+ task_id: taskId,
583
+ intent_path: preparation.intent.path,
584
+ intent_revision: preparation.intent.revision,
585
+ intent_content_hash: preparation.intent.content_hash,
586
+ preparation_digest: preparation.digest,
587
+ actor_id: LITERAL_USER_ACTOR_ID,
588
+ confirmation_ref: gate.confirmation_ref,
589
+ expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
590
+ nonce,
591
+ };
592
+ const capability = this.enrollmentRegistry.issue(binding);
593
+ const input = {
594
+ task_id: taskId,
595
+ intent_path: binding.intent_path,
596
+ intent_revision: binding.intent_revision,
597
+ preparation_digest: binding.preparation_digest,
598
+ capability,
599
+ capability_binding: binding,
600
+ now,
601
+ };
602
+ const rehearsal = runEnrollmentRehearsal(this.cwd, input, capability, this.enrollmentRegistry);
603
+ if (!rehearsal.rehearsed || rehearsal.evidence.outcome !== "ready") {
604
+ throw new Error(`Kernel enrollment rehearsal failed: ${rehearsal.evidence.blockers.join("; ")}`);
605
+ }
606
+ return enrollCanaryTask(this.cwd, input, this.enrollmentRegistry);
607
+ } catch (error) {
608
+ if (gitBaseNote) throw new Error(`${error instanceof Error ? error.message : String(error)}${gitBaseNote}`);
609
+ throw error;
597
610
  }
598
- return enrollCanaryTask(this.cwd, input, this.enrollmentRegistry);
599
611
  }
600
612
 
601
613
  async advance(taskId: string, signal?: AbortSignal) {
@@ -646,6 +658,13 @@ export class ClaudeRuntime {
646
658
  }
647
659
  }
648
660
 
661
+ async reviseIntent(taskId: string, nextIntent: unknown) {
662
+ return this.executeOrdinary({ cwd: this.cwd }, {
663
+ taskId,
664
+ operation: { op: "revise_intent", next_intent: nextIntent, actor_id: "executor" },
665
+ });
666
+ }
667
+
649
668
  /**
650
669
  * Ordinary Kernel operation, not a privileged one: canary_application builds
651
670
  * the action without a capability and the Pi Host lists resolve_finding in
@@ -23,6 +23,7 @@ export const TOOLS = [
23
23
  { name: "advance_assurance", description: "Advance frozen Assurance through deterministic QA and Review reservation.", privileged: false },
24
24
  { name: "submit_review", description: "Submit the Parent-mediated Review verdict bound to the correlated receipt.", privileged: false },
25
25
  { name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
26
+ { name: "revise_intent", description: "Apply a compatible TaskIntent revision.", privileged: false },
26
27
  { name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
27
28
  { name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
28
29
  { name: "start_unattended_batch", description: "Start an unattended serial batch run for an Initiative after native confirmation.", privileged: true },
@@ -42,7 +43,7 @@ export function listMcpTools() {
42
43
  ? { initiative_slug: { type: "string" } }
43
44
  : {
44
45
  task_id: { type: "string" },
45
- ...(tool.name === "approve_breaking_intent_revision" ? { next_intent: { type: "object" } } : {}),
46
+ ...(tool.name === "approve_breaking_intent_revision" || tool.name === "revise_intent" ? { next_intent: { type: "object" } } : {}),
46
47
  ...(tool.name === "stop" ? { reason: { type: "string" } } : {}),
47
48
  ...(tool.name === "submit_review" ? { verdict: { type: "object" } } : {}),
48
49
  ...(tool.name === "resolve_finding" ? { finding_id: { type: "string" } } : {}),
@@ -57,7 +58,9 @@ export function listMcpTools() {
57
58
  ? ["task_id", "finding_id"]
58
59
  : tool.name === "refute_finding"
59
60
  ? ["task_id", "finding_id", "attestation_id"]
60
- : ["task_id"],
61
+ : tool.name === "revise_intent"
62
+ ? ["task_id", "next_intent"]
63
+ : ["task_id"],
61
64
  },
62
65
  annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" },
63
66
  }));
@@ -162,6 +165,10 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
162
165
  if (!Object.hasOwn(args, "verdict")) throw new Error("verdict is required");
163
166
  return runtime.submitReview(taskId, args.verdict);
164
167
  }
168
+ if (name === "revise_intent") {
169
+ if (!Object.hasOwn(args, "next_intent")) throw new Error("next_intent is required");
170
+ return runtime.reviseIntent(taskId, args.next_intent);
171
+ }
165
172
  if (name === "resolve_finding") {
166
173
  // Structural only. Which findings may be resolved, and when, stays
167
174
  // the reducer's decision; duplicating it here would create a second
@@ -378,6 +385,7 @@ export function elicitationParams(input: NativeConfirmationInput) {
378
385
  input.intentRevision !== undefined ? `Intent revision: ${input.intentRevision}` : null,
379
386
  input.intentContentHash ? `Intent hash: ${input.intentContentHash}` : null,
380
387
  input.bindingDigest ? `Binding digest: ${input.bindingDigest}` : null,
388
+ input.gitBaseNotice,
381
389
  ].filter(Boolean);
382
390
  return {
383
391
  mode: "form",
@@ -40,6 +40,7 @@ import { projectLegacyAudit } from "../kernel/legacy_audit";
40
40
  import { inspectStorageLayout } from "../kernel/storage_paths";
41
41
  import { migrateLegacyLayout } from "../kernel/storage_layout_migration";
42
42
  import { readBackendClaim } from "../kernel/backend_claim";
43
+ import { inspectEnrollmentGitBase, type EnrollmentGitBase } from "../assurance/enrollment_git_base";
43
44
  import { probeKernelStore } from "../kernel";
44
45
 
45
46
  export interface KernelCommandResult {
@@ -1134,8 +1135,11 @@ function runIntentValidate(args: string[], root: string): KernelExecution {
1134
1135
  gitTracked = false;
1135
1136
  }
1136
1137
 
1138
+ let gitBase: EnrollmentGitBase | { state: "invalid"; reason: string };
1139
+ try { gitBase = inspectEnrollmentGitBase(canonicalRoot); }
1140
+ catch (error) { gitBase = { state: "invalid", reason: error instanceof Error ? error.message : String(error) }; }
1137
1141
  const contentHash = canonicalIntentHash(intent);
1138
- const enrollmentReady = gitTracked && allEligible;
1142
+ const enrollmentReady = gitTracked && allEligible && gitBase.state !== "invalid";
1139
1143
  return {
1140
1144
  result: jsonResult(
1141
1145
  {
@@ -1148,6 +1152,8 @@ function runIntentValidate(args: string[], root: string): KernelExecution {
1148
1152
  acceptance_ids: intent.acceptance.map((item) => item.id),
1149
1153
  verification: verification,
1150
1154
  git_ownership: gitTracked ? "tracked" : "untracked",
1155
+ git_base: gitBase,
1156
+ git_base_initialization_required: gitBase.state === "unborn",
1151
1157
  enrollment_ready: enrollmentReady,
1152
1158
  },
1153
1159
  0,
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/plugin_versioning.ts from the root package.json.
2
- export const PLUGIN_VERSION = "4.2.0" as const;
2
+ export const PLUGIN_VERSION = "4.4.0" as const;
@@ -93,6 +93,8 @@ Privileged effects include:
93
93
 
94
94
  Routine Managed enrollment uses one current-Host native confirmation bound to the TaskIntent content hash after Planner validation. Explicit Plan-only requests stop with candidate artifacts and do not invoke Enrollment; execution-bearing requests open the native gate directly without chat pre-confirmation. Enrollment validates intent, Git ownership, scope, workspace claim, and final authority preconditions without executing acceptance descriptors; deterministic QA executes them after implementation. The routine task proceeds from that single confirmation through enrollment, execution, and QA without a second human stop. Do not request confirmation for local in-scope edits, local verification, ordinary Direct rework, scoped diff review, or completion reporting. Managed evidence, QA, Review, and completion authority remain governed by their Managed contracts; R2 does not weaken them. Managed native-authority failures fail closed with one stable reason and exactly one same-Host recovery action; never offer a Pi, Direct Path, cross-Host/worktree, unmanaged, or automatic-retry fallback.
95
95
 
96
+ For an unborn symbolic Git HEAD, the same Enrollment confirmation discloses an empty root commit. After approval, the Host creates that commit without staging project content or changing the index/worktree, then revalidates ownership and rehearses Enrollment against the new base. Git identity uses existing environment/config values with an explicit automation fallback, never config writes. Decline or pre-initialization cancellation leaves HEAD unborn; if later Enrollment fails or is cancelled, report the remaining commit. Invalid Git refs are errors, not initialization candidates.
97
+
96
98
  ## Parallel Read-Only Dispatch
97
99
 
98
100
  State mutations, step activations, QA decisions, and plan switches remain