immune-brain 3.6.9 → 4.0.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.
Files changed (76) hide show
  1. package/package.json +3 -2
  2. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  3. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +18 -2
  4. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +76 -121
  5. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +106 -600
  6. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +1 -0
  7. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +3 -3
  8. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +17 -43
  9. package/plugins/immune-brain/dist/claude/mcp-server.mjs +7587 -5049
  10. package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +11 -12
  11. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
  12. package/plugins/immune-brain/dist/imm-loop.md +27 -25
  13. package/plugins/immune-brain/dist/imm-planner.md +34 -27
  14. package/plugins/immune-brain/dist/imm-review-retro.md +2 -2
  15. package/plugins/immune-brain/dist/role-prompts/code-review.md +3 -1
  16. package/plugins/immune-brain/dist/role-prompts/executor.md +4 -4
  17. package/plugins/immune-brain/runtime/assurance/coordinator.ts +183 -40
  18. package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +240 -0
  19. package/plugins/immune-brain/runtime/assurance/qa.ts +132 -58
  20. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +15 -7
  21. package/plugins/immune-brain/runtime/assurance/verification.ts +246 -206
  22. package/plugins/immune-brain/runtime/authorization_operation.ts +20 -0
  23. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +288 -721
  24. package/plugins/immune-brain/runtime/commands/kernel.ts +158 -67
  25. package/plugins/immune-brain/runtime/github_issue_tracker.ts +1 -1
  26. package/plugins/immune-brain/runtime/kernel/actor_identity.ts +33 -0
  27. package/plugins/immune-brain/runtime/kernel/application.ts +22 -6
  28. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +94 -5
  29. package/plugins/immune-brain/runtime/kernel/authority_port.ts +27 -6
  30. package/plugins/immune-brain/runtime/kernel/backend_claim.ts +43 -16
  31. package/plugins/immune-brain/runtime/kernel/batch_authority.ts +10 -6
  32. package/plugins/immune-brain/runtime/kernel/canary_application.ts +50 -63
  33. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +13 -4
  34. package/plugins/immune-brain/runtime/kernel/completion.ts +5 -14
  35. package/plugins/immune-brain/runtime/kernel/enrollment.ts +124 -34
  36. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +13 -5
  37. package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
  38. package/plugins/immune-brain/runtime/kernel/intent.ts +7 -11
  39. package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +4 -1
  40. package/plugins/immune-brain/runtime/kernel/legacy_task_record.ts +323 -0
  41. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +10 -1
  42. package/plugins/immune-brain/runtime/kernel/reducer.ts +32 -31
  43. package/plugins/immune-brain/runtime/kernel/run_identity.ts +121 -0
  44. package/plugins/immune-brain/runtime/kernel/spec_binding.ts +100 -0
  45. package/plugins/immune-brain/runtime/kernel/sqlite_migration.ts +950 -0
  46. package/plugins/immune-brain/runtime/kernel/sqlite_store.ts +1193 -0
  47. package/plugins/immune-brain/runtime/kernel/storage.ts +1254 -1206
  48. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +129 -755
  49. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +419 -46
  50. package/plugins/immune-brain/runtime/kernel/types.ts +12 -43
  51. package/plugins/immune-brain/runtime/kernel/validation.ts +60 -274
  52. package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +0 -1
  53. package/plugins/immune-brain/runtime/plan_core.ts +27 -65
  54. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  55. package/plugins/immune-brain/runtime/prompts/code-review.md +3 -1
  56. package/plugins/immune-brain/runtime/prompts/executor.md +4 -4
  57. package/plugins/immune-brain/runtime/staged_intent.ts +58 -0
  58. package/plugins/immune-brain/runtime/unattended/batch_git.ts +37 -7
  59. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +42 -2
  60. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +771 -0
  61. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +189 -0
  62. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +35 -0
  63. package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +33 -0
  64. package/plugins/immune-brain/runtime/unattended/types.ts +14 -1
  65. package/plugins/immune-brain/runtime/v4_runtime.ts +19 -23
  66. package/plugins/immune-brain/runtime/verification_descriptor.ts +92 -136
  67. package/plugins/immune-brain/runtime/workspace_scope.ts +98 -13
  68. package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -3
  69. package/plugins/immune-brain/bin/imm-retire-stale-wrapper +0 -4
  70. package/plugins/immune-brain/bin/imm-retired +0 -4
  71. package/plugins/immune-brain/runtime/authority_commit_receipts.ts +0 -716
  72. package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +0 -451
  73. package/plugins/immune-brain/runtime/kernel/legacy.ts +0 -299
  74. package/plugins/immune-brain/runtime/kernel/observation.ts +0 -397
  75. package/plugins/immune-brain/runtime/kernel/readiness.ts +0 -282
  76. package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +0 -132
@@ -3,11 +3,13 @@ import { createHash } from "node:crypto";
3
3
  import {
4
4
  existsSync,
5
5
  lstatSync,
6
+ mkdirSync,
6
7
  readFileSync,
7
8
  readlinkSync,
8
9
  realpathSync,
10
+ writeFileSync,
9
11
  } from "node:fs";
10
- import { resolve } from "node:path";
12
+ import { dirname, join, resolve } from "node:path";
11
13
 
12
14
  export interface GitWorkspaceSnapshot {
13
15
  kind: "git-workspace-v1";
@@ -34,6 +36,75 @@ function comparePaths(left: string, right: string): number {
34
36
  return 0;
35
37
  }
36
38
 
39
+ function isDeliveryAttachment(path: string): boolean {
40
+ return path === ".imm/audit" || path.startsWith(".imm/audit/");
41
+ }
42
+
43
+ function isNonDeliveryPath(path: string): boolean {
44
+ return isDeliveryAttachment(path) || isRuntimeAuthorityPath(path);
45
+ }
46
+
47
+ function isOwnPlanningSidecar(path: string, taskId?: string): boolean {
48
+ return Boolean(taskId) && (path === `docs/plans/${taskId}.intent.json` || path === `docs/plans/archive/${taskId}.intent.json`);
49
+ }
50
+
51
+ function isOtherTaskSidecar(path: string, taskId?: string): boolean {
52
+ return /^docs\/plans\/(?:archive\/)?[^/]+\.intent\.json$/.test(path) && !isOwnPlanningSidecar(path, taskId);
53
+ }
54
+
55
+ function isPlanningNoise(path: string, taskId?: string): boolean {
56
+ return path.startsWith("docs/plans/") && !isOtherTaskSidecar(path, taskId) && !isOwnPlanningSidecar(path, taskId);
57
+ }
58
+
59
+ function enrollmentBaselinePath(root: string): string {
60
+ return join(root, ".imm/state/enrollment-baseline.json");
61
+ }
62
+
63
+ export function writeEnrollmentBaseline(root: string): void {
64
+ const snapshot = captureGitWorkspaceSnapshot(root);
65
+ if (!snapshot) return;
66
+ const path = enrollmentBaselinePath(root);
67
+ mkdirSync(dirname(path), { recursive: true });
68
+ writeFileSync(path, `${JSON.stringify(snapshot)}\n`);
69
+ }
70
+
71
+ function readEnrollmentBaseline(root: string): GitWorkspaceSnapshot | null {
72
+ const baselinePath = enrollmentBaselinePath(root);
73
+ if (!existsSync(baselinePath)) return null;
74
+ try {
75
+ const baseline = JSON.parse(readFileSync(baselinePath, "utf8")) as GitWorkspaceSnapshot;
76
+ return isGitWorkspaceSnapshot(baseline) ? baseline : null;
77
+ } catch {
78
+ throw new Error("enrollment baseline is unreadable");
79
+ }
80
+ }
81
+
82
+ function assertNoEnvelopeEscape(
83
+ root: string,
84
+ stagedPaths: readonly string[],
85
+ scope: string[],
86
+ taskId?: string,
87
+ ): void {
88
+ const baseline = readEnrollmentBaseline(root);
89
+ const current = baseline ? captureGitWorkspaceSnapshot(root) : null;
90
+ if (baseline && !current) throw new Error("enrollment baseline cannot be compared because Git is unavailable");
91
+ const escaped = [...new Set(stagedPaths)]
92
+ .filter((path) => !isNonDeliveryPath(path) && !isOwnPlanningSidecar(path, taskId) && !isPlanningNoise(path, taskId) && !taskPathMatchesScope(path, scope))
93
+ .filter((path) => !baseline || baseline.dirty_files[path] !== current?.dirty_files[path])
94
+ .sort(comparePaths);
95
+ if (escaped.length > 0)
96
+ throw new Error(`task delivery contains paths outside the authorization envelope: ${escaped.join(", ")}`);
97
+ if (!baseline || !current) return;
98
+ const staged = new Set(stagedPaths);
99
+ const mixed = [...new Set([...Object.keys(baseline.dirty_files), ...Object.keys(current.dirty_files)])]
100
+ .filter((path) => !isNonDeliveryPath(path) && !isOwnPlanningSidecar(path, taskId) && !isPlanningNoise(path, taskId) && !taskPathMatchesScope(path, scope))
101
+ .filter((path) => !isOtherTaskSidecar(path, taskId) || staged.has(path))
102
+ .filter((path) => baseline.dirty_files[path] !== current.dirty_files[path])
103
+ .sort(comparePaths);
104
+ if (mixed.length > 0)
105
+ throw new Error(`task delivery contains paths outside the authorization envelope: ${mixed.join(", ")}`);
106
+ }
107
+
37
108
  function isRuntimeAuthorityPath(path: string): boolean {
38
109
  return (
39
110
  // Kernel v2 task state, workspace coordination, and authority journals are
@@ -315,7 +386,7 @@ function taskPathMatchesScope(path: string, scope: string[]): boolean {
315
386
  return scope.some((scopePath) => pathMatchesScope(path, scopePath));
316
387
  }
317
388
 
318
- function taskSnapshotOnce(root: string, scope: string[]): GitTaskSnapshot {
389
+ function taskSnapshotOnce(root: string, scope: string[], taskId?: string): GitTaskSnapshot {
319
390
  const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
320
391
  const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
321
392
  if (!repositoryRoot || !head || !GIT_OBJECT_ID.test(head))
@@ -343,14 +414,18 @@ function taskSnapshotOnce(root: string, scope: string[]): GitTaskSnapshot {
343
414
  "untracked task paths",
344
415
  );
345
416
  assertNoCaseFoldCollisions([...stagedPaths, ...unstagedPaths, ...untrackedPaths], "Git task paths");
417
+ assertNoEnvelopeEscape(root, stagedPaths, scope, taskId);
346
418
  const uncommittedInScope = [...new Set([...unstagedPaths, ...untrackedPaths])]
347
419
  .filter((path) => taskPathMatchesScope(path, scope))
348
420
  .sort(comparePaths);
349
421
  if (uncommittedInScope.length > 0)
350
422
  throw new Error(`task scope contains unstaged or untracked changes: ${uncommittedInScope.join(", ")}`);
351
423
 
424
+ const baseline = readEnrollmentBaseline(root);
425
+ const current = baseline ? captureGitWorkspaceSnapshot(root) : null;
352
426
  const taskPaths = [...new Set(stagedPaths)]
353
- .filter((path) => taskPathMatchesScope(path, scope))
427
+ .filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope))
428
+ .filter((path) => !(baseline && current && baseline.dirty_files[path] === current.dirty_files[path] && baseline.dirty_files[path] !== undefined))
354
429
  .sort(comparePaths);
355
430
  const stagedFiles: Record<string, GitTaskIndexEntry> = {};
356
431
  for (const path of taskPaths) {
@@ -377,6 +452,7 @@ function taskSnapshotOnce(root: string, scope: string[]): GitTaskSnapshot {
377
452
  export function captureGitTaskSnapshot(
378
453
  projectRoot: string,
379
454
  scopeHint: unknown,
455
+ taskId?: string,
380
456
  ): GitTaskSnapshot {
381
457
  const requestedRoot = resolve(projectRoot);
382
458
  const requestedStat = lstatSync(requestedRoot);
@@ -384,9 +460,9 @@ export function captureGitTaskSnapshot(
384
460
  throw new Error("task snapshot root must be a real directory");
385
461
  const root = realpathSync(requestedRoot);
386
462
  const scope = assertCanonicalTaskScope(scopeHint);
387
- const before = taskSnapshotOnce(root, scope);
463
+ const before = taskSnapshotOnce(root, scope, taskId);
388
464
  gitTaskSnapshotTestHook?.();
389
- const after = taskSnapshotOnce(root, scope);
465
+ const after = taskSnapshotOnce(root, scope, taskId);
390
466
  if (JSON.stringify(after) !== JSON.stringify(before))
391
467
  throw new Error("Git task snapshot changed while being captured");
392
468
  return before;
@@ -404,16 +480,17 @@ function hashTaskSnapshot(snapshot: object): string {
404
480
  export function taskDiffIdentity(
405
481
  projectRoot: string,
406
482
  scopeHint: unknown,
483
+ taskId?: string,
407
484
  ): GitTaskDiffIdentity {
408
- const snapshot = captureGitTaskSnapshot(projectRoot, scopeHint);
485
+ const snapshot = captureGitTaskSnapshot(projectRoot, scopeHint, taskId);
409
486
  return {
410
487
  diff_hash: hashTaskSnapshot(snapshot),
411
488
  changed_paths: Object.keys(snapshot.staged_files).sort(comparePaths),
412
489
  };
413
490
  }
414
491
 
415
- export function taskDiffHash(projectRoot: string, scopeHint: unknown): string {
416
- return taskDiffIdentity(projectRoot, scopeHint).diff_hash;
492
+ export function taskDiffHash(projectRoot: string, scopeHint: unknown, taskId?: string): string {
493
+ return taskDiffIdentity(projectRoot, scopeHint, taskId).diff_hash;
417
494
  }
418
495
 
419
496
  function gitRequired(root: string, args: string[], failure: string): string {
@@ -441,6 +518,7 @@ function taskRevisionSnapshotOnce(
441
518
  root: string,
442
519
  scope: string[],
443
520
  baseHead: string,
521
+ taskId?: string,
444
522
  ): GitTaskRevisionSnapshot {
445
523
  const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
446
524
  const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
@@ -476,7 +554,11 @@ function taskRevisionSnapshotOnce(
476
554
  gitBytes(root, ["ls-files", "--others", "--exclude-standard", "-z", "--"]),
477
555
  "untracked task revision paths",
478
556
  );
479
- const scopedStagedPaths = stagedPaths.filter((path) => taskPathMatchesScope(path, scope));
557
+ assertNoEnvelopeEscape(root, stagedPaths, scope, taskId);
558
+ const baseline = readEnrollmentBaseline(root);
559
+ const current = baseline ? captureGitWorkspaceSnapshot(root) : null;
560
+ const scopedStagedPaths = stagedPaths.filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope))
561
+ .filter((path) => !(baseline && current && baseline.dirty_files[path] === current.dirty_files[path] && baseline.dirty_files[path] !== undefined));
480
562
  const scopedUnstagedPaths = unstagedPaths.filter((path) => taskPathMatchesScope(path, scope));
481
563
  const scopedUntrackedPaths = untrackedPaths.filter((path) => taskPathMatchesScope(path, scope));
482
564
  assertNoCaseFoldCollisions(
@@ -517,6 +599,7 @@ export function captureGitTaskRevisionSnapshot(
517
599
  projectRoot: string,
518
600
  scopeHint: unknown,
519
601
  baseHead: unknown,
602
+ taskId?: string,
520
603
  ): GitTaskRevisionSnapshot {
521
604
  const requestedRoot = resolve(projectRoot);
522
605
  const requestedStat = lstatSync(requestedRoot);
@@ -527,9 +610,9 @@ export function captureGitTaskRevisionSnapshot(
527
610
  throw new Error("task revision base must be a Git commit id");
528
611
  const scope = assertCanonicalTaskScope(scopeHint);
529
612
  const normalizedBase = baseHead.toLowerCase();
530
- const before = taskRevisionSnapshotOnce(root, scope, normalizedBase);
613
+ const before = taskRevisionSnapshotOnce(root, scope, normalizedBase, taskId);
531
614
  gitTaskSnapshotTestHook?.();
532
- const after = taskRevisionSnapshotOnce(root, scope, normalizedBase);
615
+ const after = taskRevisionSnapshotOnce(root, scope, normalizedBase, taskId);
533
616
  if (JSON.stringify(after) !== JSON.stringify(before))
534
617
  throw new Error("Git task revision changed while being captured");
535
618
  return before;
@@ -539,8 +622,9 @@ export function taskRevisionIdentity(
539
622
  projectRoot: string,
540
623
  scopeHint: unknown,
541
624
  baseHead: string,
625
+ taskId?: string,
542
626
  ): GitTaskDiffIdentity {
543
- const snapshot = captureGitTaskRevisionSnapshot(projectRoot, scopeHint, baseHead);
627
+ const snapshot = captureGitTaskRevisionSnapshot(projectRoot, scopeHint, baseHead, taskId);
544
628
  return {
545
629
  diff_hash: hashTaskSnapshot(snapshot),
546
630
  changed_paths: Object.keys(snapshot.changed_paths).sort(comparePaths),
@@ -552,8 +636,9 @@ export function taskRevisionDiffHash(
552
636
  projectRoot: string,
553
637
  scopeHint: unknown,
554
638
  baseHead: string,
639
+ taskId?: string,
555
640
  ): string {
556
- return taskRevisionIdentity(projectRoot, scopeHint, baseHead).diff_hash;
641
+ return taskRevisionIdentity(projectRoot, scopeHint, baseHead, taskId).diff_hash;
557
642
  }
558
643
 
559
644
  function isGitWorkspaceSnapshot(value: unknown): value is GitWorkspaceSnapshot {
@@ -9,8 +9,8 @@ Use [`../../dist/imm-planner.md`](../../dist/imm-planner.md) as the canonical co
9
9
  index, not a whole-document read. Explicit entry only: ordinary host requests stay
10
10
  host-native.
11
11
 
12
- Mandatory constraints before any action: Planner writes candidate Specs and
13
- TaskIntents only; it never implements, overwrites an enrolled TaskIntent, or
12
+ Mandatory constraints before any action: Planner writes a TaskIntent, and a Spec
13
+ only for complex work; it never implements, overwrites an enrolled TaskIntent, or
14
14
  grants execution authority — only the native Enrollment gate can.
15
15
 
16
16
  Section routes - load a section's instructions only when its branch applies.
@@ -35,7 +35,7 @@ own routes instead of these stages.
35
35
  - retirement design: [Retirement Completion Contract](../../dist/imm-planner.md#retirement-completion-contract)
36
36
  - optional research dispatch: [Research Dispatch](../../dist/imm-planner.md#research-dispatch)
37
37
 
38
- Plan-only requests stop after candidate Spec/TaskIntent validation. Requests
38
+ Plan-only requests stop after candidate TaskIntent validation, plus Spec validation when the work is complex. Requests
39
39
  that include execution invoke the current Host's native Enrollment gate
40
40
  directly, without chat pre-confirmation. Native-gate failure stays fail-closed
41
41
  in that Host: report its reason and one retry action only; never suggest
@@ -1,4 +0,0 @@
1
- #!/bin/sh
2
- set -eu
3
- PLUGIN_ROOT=$(cd "$(dirname "$0")/.." && pwd -P)
4
- exec bun "$PLUGIN_ROOT/runtime/v4_runtime.ts" cli imm-retire-stale-wrapper "$@"
@@ -1,4 +0,0 @@
1
- #!/bin/sh
2
- set -eu
3
- PLUGIN_ROOT=$(cd "$(dirname "$0")/.." && pwd -P)
4
- exec bun "$PLUGIN_ROOT/runtime/v4_runtime.ts" cli imm-work "$@"