immune-brain 3.6.4 → 3.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.
@@ -436,6 +436,7 @@ export class AssuranceCoordinator {
436
436
  sessionGenerationValue(): number { return this.sessionGeneration; }
437
437
 
438
438
  async advance(taskId: string, ctx: HostContext, signal?: AbortSignal, onUpdate?: (update: ForegroundToolUpdate) => void): Promise<AssuranceAdvanceResult> {
439
+ if (this.isInvocationOpen(taskId)) return { state: "blocked", reason: "an authority invocation is already open" };
439
440
  const active = this.active(taskId);
440
441
  if (active?.state === "review_ready") {
441
442
  const reservation = this.reviewReservations.get(taskId);
@@ -819,6 +820,17 @@ export class AssuranceCoordinator {
819
820
  return { state: "blocked", reason: `Kernel requires ${settled.projection.next_obligation} after Review` };
820
821
  }
821
822
 
823
+ isReviewVerdictValid(taskId: string, verdictInput: unknown): boolean {
824
+ const reservation = this.reviewReservations.get(taskId);
825
+ if (!reservation) return false;
826
+ try {
827
+ parseAssuranceVerdict(verdictInput, reservation.snapshot);
828
+ return true;
829
+ } catch {
830
+ return false;
831
+ }
832
+ }
833
+
822
834
  abandonReview(taskId: string, reason: string): AssuranceSubmitReviewResult {
823
835
  const reservation = this.reviewReservations.get(taskId);
824
836
  if (!reservation) return { state: "blocked", reason };
@@ -833,6 +845,12 @@ export class AssuranceCoordinator {
833
845
  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 };
834
846
  }
835
847
 
848
+ releaseStoppedReview(taskId: string): void {
849
+ const reservation = this.reviewReservations.get(taskId);
850
+ if (reservation) this.releaseReviewReservation(taskId, reservation);
851
+ this.rejectedReviewOperations.delete(taskId);
852
+ }
853
+
836
854
  private releaseReviewReservation(taskId: string, reservation: ReviewReservation, rejectionReason?: string): void {
837
855
  if (this.reviewReservations.get(taskId) !== reservation) return;
838
856
  this.reviewReservations.delete(taskId);
@@ -119,13 +119,16 @@ export async function submitClaudeReview(
119
119
  if (observed.release) return coordinator.abandonReview(taskId, observed.reason);
120
120
  return { state: "blocked", reason: observed.reason };
121
121
  }
122
- const parentJson = extractVerdictJson(verdictInput);
123
- const receiptJson = extractVerdictJson(observed.receipt.result);
124
- if (parentJson && receiptJson && verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
125
- return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
122
+ const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
123
+ if (!parentValid) return coordinator.submitReview(taskId, ctx, verdictInput);
124
+ const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
125
+ if (!receiptValid) {
126
+ return coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict");
126
127
  }
127
- if (parentJson && !receiptJson) {
128
- return { state: "blocked", reason: "reviewer receipt is not a valid verdict" };
128
+ const parentJson = extractVerdictJson(verdictInput)!;
129
+ const receiptJson = extractVerdictJson(observed.receipt.result)!;
130
+ if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
131
+ return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
129
132
  }
130
133
  return coordinator.submitReview(taskId, ctx, verdictInput);
131
134
  }
@@ -525,6 +528,19 @@ export class ClaudeRuntime {
525
528
  return submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput);
526
529
  }
527
530
 
531
+ /**
532
+ * Ordinary Kernel operation, not a privileged one: canary_application builds
533
+ * the action without a capability and the Pi Host lists resolve_finding in
534
+ * its ordinary KERNEL_OPERATIONS. The reducer owns every precondition, so
535
+ * this port reads no findings and tests no kind.
536
+ */
537
+ async resolveFinding(taskId: string, findingId: string) {
538
+ return this.executeOrdinary({ cwd: this.cwd }, {
539
+ taskId,
540
+ operation: { op: "resolve_finding", finding_id: findingId, actor_id: "executor" },
541
+ });
542
+ }
543
+
528
544
  async authorize(taskId: string, operation: string, meta: ToolMeta, extra: Record<string, unknown> = {}) {
529
545
  if (operation === "repair_authority_state") {
530
546
  const authority = reconcileKernelAuthority(this.cwd, taskId);
@@ -534,7 +550,7 @@ export class ClaudeRuntime {
534
550
  return repairKernelAuthority(this.cwd, taskId, authority.revision);
535
551
  }
536
552
  if (!isPrivilegedOperation(operation) && operation !== "request_authorization") throw new Error(`unsupported privileged operation ${operation}`);
537
- let op: PrivilegedOperation | "request_authorization" | "resolve_user_decision" = operation;
553
+ let op: PrivilegedOperation | "request_authorization" | "resolve_user_decision" | "authorize_rework" = operation;
538
554
  let decisionOp: { finding_id: string; resolution: string } | undefined;
539
555
  const projection = await this.status(taskId);
540
556
  if (projection.error || !projection.claim) throw new Error(projection.error ?? "no active backend claim");
@@ -551,6 +567,8 @@ export class ClaudeRuntime {
551
567
  if (open.length !== 1) throw new Error(`resolve-user-decision requires exactly one open user decision; found ${open.length}`);
552
568
  op = "resolve_user_decision";
553
569
  decisionOp = { finding_id: open[0].id, resolution: `resume after literal-user decision: ${open[0].summary}` };
570
+ } else if (readiness.state === "authorize_rework") {
571
+ op = "authorize_rework";
554
572
  } else {
555
573
  throw new Error(readiness.blocked ?? "no unique host-derived authorization operation");
556
574
  }
@@ -668,7 +686,11 @@ export class ClaudeRuntime {
668
686
  diffProvider: diffSnapshotOf,
669
687
  now,
670
688
  });
671
- if (op === "stop" || op === "approve_breaking_intent_revision") stagePlanningArtifactTransition(this.cwd, result.record);
689
+ if (
690
+ op === "stop" ||
691
+ op === "authorize_rework" ||
692
+ op === "approve_breaking_intent_revision"
693
+ ) stagePlanningArtifactTransition(this.cwd, result.record);
672
694
  return result;
673
695
  } catch (error) {
674
696
  if (sidecar && priorBytes && priorIndexState) {
@@ -773,7 +795,7 @@ export class ClaudeRuntime {
773
795
  }));
774
796
  }
775
797
 
776
- private async executeOrdinary(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown } }) {
798
+ private async executeOrdinary(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown; finding_id?: string } }) {
777
799
  const { app } = await this.authority();
778
800
  const operation = input.operation.op === "revise_intent"
779
801
  ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) }
@@ -26,6 +26,7 @@ export const TOOLS = [
26
26
  { name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
27
27
  { name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
28
28
  { name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false },
29
+ { name: "resolve_finding", description: "Resolve one open blocking or advisory finding whose cause is fixed and verified.", privileged: false },
29
30
  ] as const;
30
31
 
31
32
  export function listMcpTools() {
@@ -39,8 +40,13 @@ export function listMcpTools() {
39
40
  ...(tool.name === "approve_breaking_intent_revision" ? { next_intent: { type: "object" } } : {}),
40
41
  ...(tool.name === "stop" ? { reason: { type: "string" } } : {}),
41
42
  ...(tool.name === "submit_review" ? { verdict: { type: "object" } } : {}),
43
+ ...(tool.name === "resolve_finding" ? { finding_id: { type: "string" } } : {}),
42
44
  },
43
- required: tool.name === "submit_review" ? ["task_id", "verdict"] : ["task_id"],
45
+ required: tool.name === "submit_review"
46
+ ? ["task_id", "verdict"]
47
+ : tool.name === "resolve_finding"
48
+ ? ["task_id", "finding_id"]
49
+ : ["task_id"],
44
50
  },
45
51
  annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" },
46
52
  }));
@@ -119,6 +125,13 @@ export function createMcpRuntime(options: McpRuntimeOptions = {}) {
119
125
  if (!Object.hasOwn(args, "verdict")) throw new Error("verdict is required");
120
126
  return runtime.submitReview(taskId, args.verdict);
121
127
  }
128
+ if (name === "resolve_finding") {
129
+ // Structural only. Which findings may be resolved, and when, stays
130
+ // the reducer's decision; duplicating it here would create a second
131
+ // authority that could drift from the Kernel.
132
+ if (typeof args.finding_id !== "string" || !args.finding_id) throw new Error("finding_id is required");
133
+ return runtime.resolveFinding(taskId, args.finding_id);
134
+ }
122
135
  if (name === "request_authorization" || name === "approve_breaking_intent_revision" || name === "stop" || name === "repair_authority_state") {
123
136
  return runtime.authorize(taskId, name, toolMeta, args);
124
137
  }
@@ -248,19 +248,6 @@ function runInspect(root: string): KernelExecution {
248
248
  ),
249
249
  };
250
250
  }
251
- let declared: TaskRisk;
252
- let intent: TaskIntentV1;
253
- try {
254
- const raw = JSON.parse(
255
- readSecureProjectFile(root, `docs/plans/${claim.task_id}.intent.json`),
256
- ) as { risk?: unknown };
257
- if (raw.risk !== "routine" && raw.risk !== "material" && raw.risk !== "critical")
258
- throw new Error(`intent.risk is unreadable for ${claim.task_id}`);
259
- declared = raw.risk;
260
- intent = parseTaskIntentV1(raw);
261
- } catch (error) {
262
- return sourceFailure("inspect", error);
263
- }
264
251
  let recordRead: ReturnType<typeof readTaskRecordRaw>;
265
252
  try {
266
253
  recordRead = readTaskRecordRaw(root, claim.task_id);
@@ -285,6 +272,21 @@ function runInspect(root: string): KernelExecution {
285
272
  };
286
273
  }
287
274
  const record = recordRead.record;
275
+ let declared: TaskRisk;
276
+ let intent: TaskIntentV1;
277
+ try {
278
+ const raw = JSON.parse(
279
+ readSecureProjectFile(root, record.intent_ref.path),
280
+ ) as { risk?: unknown };
281
+ if (raw.risk !== "routine" && raw.risk !== "material" && raw.risk !== "critical")
282
+ throw new Error(`intent.risk is unreadable for ${claim.task_id}`);
283
+ declared = raw.risk;
284
+ intent = parseTaskIntentV1(raw);
285
+ if (canonicalIntentHash(intent) !== record.intent_ref.content_hash)
286
+ throw new Error("TaskIntent sidecar does not match TaskRecord content hash");
287
+ } catch (error) {
288
+ return sourceFailure("inspect", error);
289
+ }
288
290
  const workspaceState = readWorkspaceStateRaw(root);
289
291
  let identity;
290
292
  try {
@@ -57,6 +57,7 @@ export interface InitiativePublicationInput {
57
57
  tasks: Array<{
58
58
  slice_id: string;
59
59
  intent: string;
60
+ acceptance: Array<{ id: string; summary: string }>;
60
61
  projection?: TaskProjection;
61
62
  }>;
62
63
  }
@@ -85,6 +86,18 @@ export interface GithubInitiativePublicationResult {
85
86
  message: string;
86
87
  }
87
88
 
89
+ export interface GithubInitiativeObservation {
90
+ contract: "immune_brain/github_initiative_observation/v1";
91
+ initiative_id: string;
92
+ issue_number: number;
93
+ tasks: Array<{
94
+ task_id: string;
95
+ slice_id: string;
96
+ issue_number: number;
97
+ blocked_by: string[];
98
+ }>;
99
+ }
100
+
88
101
  export interface TaskProjection {
89
102
  result?: string;
90
103
  current_behavior?: string;
@@ -552,6 +565,63 @@ async function readBlockedByIds(
552
565
  }
553
566
  }
554
567
 
568
+ export async function observeGithubInitiative(
569
+ root: string,
570
+ initiativeId: string,
571
+ gh: GhTransport = createGhTransport(),
572
+ ): Promise<GithubInitiativeObservation> {
573
+ const id = identifier(initiativeId, "initiative_id");
574
+ const source = await snapshot(resolve(root), gh, "create-initiative");
575
+ if ("contract" in source) throw new Error(source.message);
576
+ const parent = initiativeLookup(source.issues, source.repository.id, id);
577
+ if (parent.kind === "missing") throw new Error(`Initiative ${id} is not published`);
578
+ if (parent.kind === "ambiguous") throw new Error(parent.message);
579
+ const subIssueNumbers = await readSubIssueNumbers(root, gh, "create-initiative", source.repository, parent.issue.number);
580
+ if (!Array.isArray(subIssueNumbers)) throw new Error(subIssueNumbers.message);
581
+ if (new Set(subIssueNumbers).size !== subIssueNumbers.length)
582
+ throw new Error(`Initiative ${id} has duplicate native Sub-issue relations`);
583
+ const tasks = subIssueNumbers.map((issueNumber) => {
584
+ const matches = source.issues.filter((issue) => issue.number === issueNumber);
585
+ if (matches.length !== 1) throw new Error(`Initiative ${id} references an unreadable Sub-issue #${issueNumber}`);
586
+ const issue = matches[0];
587
+ const taskId = ownershipMarkerValue(issue.body, "task-id");
588
+ const sliceId = ownershipMarkerValue(issue.body, "slice-id");
589
+ if (!taskId || !sliceId || ownershipMarkerValue(issue.body, "initiative-id") !== id)
590
+ throw new Error(`Sub-issue #${issueNumber} has invalid Initiative ownership markers`);
591
+ const owned = ownedTaskLookup(source.issues, source.repository.id, taskId, id, sliceId);
592
+ if (owned.kind !== "found" || owned.issue.number !== issueNumber)
593
+ throw new Error(owned.kind === "ambiguous" ? owned.message : `Sub-issue #${issueNumber} has invalid Task ownership`);
594
+ return { task_id: taskId, slice_id: sliceId, issue_number: issueNumber, issue_id: issue.id };
595
+ });
596
+ if (new Set(tasks.map((task) => task.task_id)).size !== tasks.length)
597
+ throw new Error(`Initiative ${id} has duplicate Task identities`);
598
+ if (new Set(tasks.map((task) => task.slice_id)).size !== tasks.length)
599
+ throw new Error(`Initiative ${id} has duplicate Slice identities`);
600
+ const taskByIssueId = new Map(tasks.map((task) => [task.issue_id, task.task_id]));
601
+ const observed: GithubInitiativeObservation["tasks"] = [];
602
+ for (const task of tasks.sort((left, right) => left.task_id < right.task_id ? -1 : left.task_id > right.task_id ? 1 : 0)) {
603
+ const blockerIds = await readBlockedByIds(root, gh, "create-initiative", source.repository, task.issue_number);
604
+ if (!Array.isArray(blockerIds)) throw new Error(blockerIds.message);
605
+ const blockedBy = blockerIds.map((blockerId) => {
606
+ const blocker = taskByIssueId.get(blockerId);
607
+ if (!blocker) throw new Error(`Task ${task.task_id} depends on an Issue outside Initiative ${id}`);
608
+ return blocker;
609
+ }).sort();
610
+ observed.push({
611
+ task_id: task.task_id,
612
+ slice_id: task.slice_id,
613
+ issue_number: task.issue_number,
614
+ blocked_by: blockedBy,
615
+ });
616
+ }
617
+ return {
618
+ contract: "immune_brain/github_initiative_observation/v1",
619
+ initiative_id: id,
620
+ issue_number: parent.issue.number,
621
+ tasks: observed,
622
+ };
623
+ }
624
+
555
625
  async function confirmBlockedBy(
556
626
  root: string,
557
627
  gh: GhTransport,
@@ -1070,7 +1140,7 @@ function preflightPublication(root: string, input: InitiativePublicationInput):
1070
1140
  const publications = input.tasks.map((task, index) => {
1071
1141
  if (!task || typeof task !== "object" || Array.isArray(task)) throw new Error(`tasks[${index}] must be an object`);
1072
1142
  if (typeof task.intent !== "string") throw new Error(`tasks[${index}].intent must be a string`);
1073
- return taskPublication(root, input.initiative_id, task.slice_id, task.intent, task.projection);
1143
+ return taskPublication(root, input.initiative_id, task.slice_id, task.intent, task.acceptance, task.projection);
1074
1144
  });
1075
1145
  const operations = publications.map((publication) => publication.operation);
1076
1146
  const taskIds = new Set<string>();
@@ -1264,7 +1334,14 @@ function isSuccessfulTrackerStatus(status: TrackerStatus): boolean {
1264
1334
  return status === "created" || status === "updated" || status === "already_current";
1265
1335
  }
1266
1336
 
1267
- function taskPublication(root: string, initiativeId: string, sliceId: string, intentPath: string, projection?: TaskProjection): PreparedPublicationTask {
1337
+ function taskPublication(
1338
+ root: string,
1339
+ initiativeId: string,
1340
+ sliceId: string,
1341
+ intentPath: string,
1342
+ acceptance: unknown,
1343
+ projection?: TaskProjection,
1344
+ ): PreparedPublicationTask {
1268
1345
  const absoluteRoot = resolve(root);
1269
1346
  const absolutePath = resolve(absoluteRoot, intentPath);
1270
1347
  const rel = relative(absoluteRoot, absolutePath);
@@ -1275,6 +1352,20 @@ function taskPublication(root: string, initiativeId: string, sliceId: string, in
1275
1352
  const read = readTaskIntent(absoluteRoot, taskId);
1276
1353
  if (read.intent_ref.path !== rel) throw new Error("TaskIntent path must match its canonical sidecar path");
1277
1354
  const intent = read.intent;
1355
+ if (!Array.isArray(acceptance)) throw new Error(`Task ${taskId} requires public acceptance summaries`);
1356
+ const expectedIds = new Set(intent.acceptance.map((item) => item.id));
1357
+ const publicById = new Map<string, { id: string; summary: string }>();
1358
+ acceptance.forEach((item, index) => {
1359
+ if (!item || typeof item !== "object" || Array.isArray(item))
1360
+ throw new Error(`Task ${taskId} acceptance[${index}] must be an object`);
1361
+ const raw = item as Record<string, unknown>;
1362
+ const id = identifier(raw.id, `Task ${taskId} acceptance[${index}].id`);
1363
+ if (!expectedIds.has(id)) throw new Error(`Task ${taskId} has unknown public acceptance id: ${id}`);
1364
+ if (publicById.has(id)) throw new Error(`Task ${taskId} has duplicate public acceptance id: ${id}`);
1365
+ publicById.set(id, { id, summary: projectionText(raw.summary, `Task ${taskId} acceptance[${index}].summary`, 500) });
1366
+ });
1367
+ const missingIds = [...expectedIds].filter((id) => !publicById.has(id));
1368
+ if (missingIds.length) throw new Error(`Task ${taskId} is missing public acceptance ids: ${missingIds.join(", ")}`);
1278
1369
  return {
1279
1370
  operation: validateOperation({
1280
1371
  op: "upsert-task",
@@ -1283,7 +1374,7 @@ function taskPublication(root: string, initiativeId: string, sliceId: string, in
1283
1374
  slice_id: sliceId,
1284
1375
  goal: intent.goal,
1285
1376
  risk: intent.risk,
1286
- acceptance: intent.acceptance.map((item) => ({ id: item.id, summary: item.assertion })),
1377
+ acceptance: intent.acceptance.map((item) => publicById.get(item.id)!),
1287
1378
  projection,
1288
1379
  }) as Extract<TrackerOperation, { op: "upsert-task" }>,
1289
1380
  intent_path: read.intent_ref.path,
@@ -160,6 +160,7 @@ export function applyTaskAction(
160
160
  action.type === "record_approval" ||
161
161
  action.type === "approve_breaking_intent_revision" ||
162
162
  action.type === "request_rework" ||
163
+ action.type === "authorize_rework" ||
163
164
  action.type === "stop" ||
164
165
  action.type === "resolve_user_decision";
165
166
 
@@ -25,9 +25,10 @@ export interface AssuranceAuthorizationReadiness {
25
25
  /**
26
26
  * Kernel-decidable authorization readiness:
27
27
  * - "resolve_user_decision": exactly one open unresolved-user-decision finding;
28
+ * - "authorize_rework": an open replan boundary can be overridden by the user;
28
29
  * - "none": nothing uniquely decidable from Kernel facts.
29
30
  */
30
- state: "resolve_user_decision" | "none";
31
+ state: "resolve_user_decision" | "authorize_rework" | "none";
31
32
  /** Non-null only when Kernel facts prove the authorization is blocked. */
32
33
  blocked: string | null;
33
34
  }
@@ -75,6 +76,8 @@ export function deriveAssuranceAuthorization(input: {
75
76
  state: "none",
76
77
  blocked: `resolve-user-decision requires exactly one open user decision; found ${input.open_user_decision_count}`,
77
78
  };
79
+ if (input.next_obligation === "revise_intent")
80
+ return { state: "authorize_rework", blocked: null };
78
81
  return { state: "none", blocked: null };
79
82
  }
80
83