immune-brain 4.6.0 → 5.1.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 (75) hide show
  1. package/README.md +374 -46
  2. package/README.zh-CN.md +384 -47
  3. package/package.json +2 -1
  4. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +42 -44
  6. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +143 -474
  7. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +78 -87
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +7 -2
  9. package/plugins/immune-brain/.pi-extension/tsconfig.json +2 -2
  10. package/plugins/immune-brain/agents/immune-brain-advisory-reviewer.md +23 -0
  11. package/plugins/immune-brain/agents/immune-brain-qa.md +42 -0
  12. package/plugins/immune-brain/agents/immune-brain-reviewer.md +90 -1
  13. package/plugins/immune-brain/agents/immune-brain-ui-review.md +21 -0
  14. package/plugins/immune-brain/dist/BASELINE.md +4 -4
  15. package/plugins/immune-brain/dist/claude/mcp-server.mjs +8601 -6975
  16. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +27 -0
  17. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +3 -3
  18. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -1
  19. package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
  20. package/plugins/immune-brain/dist/imm-doc-prune.md +2 -2
  21. package/plugins/immune-brain/dist/{imm-agent-doc-maintain.md → imm-doc-slim.md} +9 -9
  22. package/plugins/immune-brain/dist/imm-planner.md +23 -8
  23. package/plugins/immune-brain/dist/imm-pr-fix.md +1 -1
  24. package/plugins/immune-brain/dist/{imm-review-retro.md → imm-retro.md} +26 -13
  25. package/plugins/immune-brain/dist/{imm-loop.md → imm-run.md} +231 -10
  26. package/plugins/immune-brain/dist/registry.yaml +9 -9
  27. package/plugins/immune-brain/dist/role-prompts/code-review.md +7 -0
  28. package/plugins/immune-brain/dist/role-prompts/executor.md +35 -2
  29. package/plugins/immune-brain/dist/role-prompts/lane-steward.md +44 -0
  30. package/plugins/immune-brain/runtime/assurance/coordinator.ts +83 -67
  31. package/plugins/immune-brain/runtime/assurance/host_port.ts +20 -0
  32. package/plugins/immune-brain/runtime/assurance/qa.ts +2 -2
  33. package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +347 -0
  34. package/plugins/immune-brain/runtime/claude/interaction.ts +6 -0
  35. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +78 -373
  36. package/plugins/immune-brain/runtime/claude/mcp_server.ts +26 -2
  37. package/plugins/immune-brain/runtime/claude/review_host.ts +48 -16
  38. package/plugins/immune-brain/runtime/claude/role_agents.ts +84 -0
  39. package/plugins/immune-brain/runtime/commands/kernel.ts +3 -6
  40. package/plugins/immune-brain/runtime/kernel/application.ts +6 -3
  41. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +15 -8
  42. package/plugins/immune-brain/runtime/kernel/canary_application.ts +5 -3
  43. package/plugins/immune-brain/runtime/kernel/enrollment.ts +149 -5
  44. package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
  45. package/plugins/immune-brain/runtime/kernel/storage.ts +170 -11
  46. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +13 -0
  47. package/plugins/immune-brain/runtime/local_initiative.ts +94 -0
  48. package/plugins/immune-brain/runtime/loop_contract.ts +77 -24
  49. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  50. package/plugins/immune-brain/runtime/prompts/code-review.md +7 -0
  51. package/plugins/immune-brain/runtime/prompts/executor.md +35 -2
  52. package/plugins/immune-brain/runtime/prompts/lane-steward.md +44 -0
  53. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +42 -2
  54. package/plugins/immune-brain/runtime/unattended/batch_git.ts +57 -27
  55. package/plugins/immune-brain/runtime/unattended/batch_integration.ts +387 -0
  56. package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +106 -0
  57. package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +909 -0
  58. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +36 -11
  59. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +53 -36
  60. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +34 -29
  61. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +46 -91
  62. package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +162 -0
  63. package/plugins/immune-brain/runtime/unattended/batch_state.ts +356 -8
  64. package/plugins/immune-brain/runtime/unattended/types.ts +21 -2
  65. package/plugins/immune-brain/runtime/workspace_scope.ts +34 -0
  66. package/plugins/immune-brain/skills/BASELINE.md +4 -4
  67. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +1 -1
  68. package/plugins/immune-brain/skills/{imm-agent-doc-maintain → imm-doc-slim}/SKILL.md +9 -9
  69. package/plugins/immune-brain/skills/imm-planner/SKILL.md +4 -2
  70. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +1 -1
  71. package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/SKILL.md +7 -7
  72. package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/scripts/review_retro.ts +46 -21
  73. package/plugins/immune-brain/skills/{imm-loop → imm-run}/SKILL.md +11 -7
  74. package/plugins/immune-brain/skills/registry.yaml +9 -9
  75. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -581
@@ -1,30 +1,23 @@
1
- import { randomUUID } from "node:crypto";
1
+ import { stagePlanningArtifactTransition, type VerdictAuthority } from "../assurance/verdict_authority";
2
+ export { ensureReviewRevision as ensureClaudeReviewRevision } from "../assurance/verdict_authority";
2
3
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
4
  import { execFileSync } from "node:child_process";
4
5
  import { join } from "node:path";
5
6
  import {
6
7
  AssuranceCoordinator,
7
- reviewAdvisoryRecords,
8
- reviewReworkFindings,
9
8
  type AssuranceCoordinatorPorts,
10
9
  type AssuranceSubmitReviewResult,
11
- type AssuranceVerdict,
12
10
  type HostContext,
13
- type SnapshotDescriptor,
14
11
  } from "../assurance/coordinator";
15
12
  import {
16
- type VerificationDescriptor,
17
13
  } from "../assurance/verification";
18
14
  import {
19
- captureReviewManifest,
20
15
  writeNativeReviewEvidence,
21
- type ReviewRevision,
22
16
  } from "../assurance/review_evidence";
23
- import { parseVerificationDescriptor } from "../verification_descriptor";
24
17
  import { projectAssurance, type AssuranceProjection, type AssuranceProjectionResult } from "../kernel/assurance_projection";
25
- import { type TaskApprovalV2, type TaskFinding, type TaskRecord } from "../kernel/types";
18
+ import { type TaskFinding, type TaskRecord } from "../kernel/types";
26
19
  import { findingsDigestV2 } from "../kernel/reducer";
27
- import { readTaskRecord, readTaskRecordRaw, recoverKernelStoreFollowUps } from "../kernel/storage";
20
+ import { readTaskRecordRaw, recoverKernelStoreFollowUps } from "../kernel/storage";
28
21
  import { canonicalIntentHash, parseTaskIntentV1, readTaskIntent } from "../kernel/intent";
29
22
  import { capabilityActionFor, createCanaryApplication } from "../kernel/canary_application";
30
23
  import {
@@ -37,12 +30,12 @@ import {
37
30
  createEnrollmentAuthorityRegistry,
38
31
  type EnrollmentCapabilityBinding,
39
32
  } from "../kernel/enrollment_authority";
40
- import { enrollCanaryTask, runEnrollmentRehearsal } from "../kernel/enrollment";
33
+ import { enrollTask } from "../kernel/enrollment";
41
34
  import { reconcileKernelAuthority, repairKernelAuthority } from "../kernel/storage";
42
35
  import { inspectEnrollmentGitBase, enrollmentGitBaseNotice, initializeEnrollmentGitBase } from "../assurance/enrollment_git_base";
43
36
  import { preparePiCanary, revalidatePiCanary } from "../kernel/pi_canary_prepare";
44
37
  import { runDeterministicQa } from "../assurance/qa";
45
- import { taskDiffIdentity, taskRevisionIdentity } from "../workspace_scope";
38
+ import { taskDeliveryIdentity } from "../workspace_scope";
46
39
  import { batchReason } from "../unattended/batch_reasons";
47
40
  import { deriveAuthorizationOperation } from "../authorization_operation";
48
41
  import { LITERAL_USER_ACTOR_ID, canonicalActorId } from "../kernel/actor_identity";
@@ -56,21 +49,17 @@ import { readTaskTombstone } from "../kernel/backend_claim";
56
49
  import {
57
50
  authorizeBatch,
58
51
  projectBatchPreflight,
59
- readActiveClaimTaskId,
60
- isOwnBatchClaim,
61
52
  findResumableBatchSlugForTask,
62
53
  } from "../unattended/batch_preflight";
63
54
  import {
64
55
  startBatch,
65
- batchQaFailureFacts,
66
56
  type BatchRunnerKernelPort,
67
57
  type BatchRunReport,
68
58
  } from "../unattended/batch_runner";
59
+ import { createBatchKernelPort } from "../unattended/batch_kernel_port";
60
+ import type { LaneOffer } from "../unattended/batch_lanes";
69
61
  import {
70
62
  createBatchAuthorityRegistry,
71
- deriveChildEnrollment,
72
- type BatchAuthorityRegistry,
73
- type BatchAuthorizationBinding,
74
63
  } from "../kernel/batch_authority";
75
64
  import {
76
65
  type BatchRunnerGitPort,
@@ -78,7 +67,7 @@ import {
78
67
  import type {
79
68
  InitiativeObservationReader,
80
69
  } from "../unattended/types";
81
- import { observeGithubInitiative } from "../github_issue_tracker";
70
+ import { observeInitiative } from "../unattended/batch_plan";
82
71
  import {
83
72
  confirmationRef,
84
73
  enrollmentNonce,
@@ -96,11 +85,7 @@ export function diffSnapshotOf(root: string, record: TaskRecord): {
96
85
  diff_hash: string;
97
86
  changed_paths: readonly string[];
98
87
  } {
99
- if (record.contract === "assurance_kernel/task_record/v4") {
100
- if (!record.git_base_head) throw new Error("TaskRecord v4 is missing git_base_head");
101
- return taskRevisionIdentity(root, record.intent_snapshot.scope_hint, record.git_base_head, record.task_id);
102
- }
103
- return taskDiffIdentity(root, record.intent_snapshot.scope_hint, record.task_id);
88
+ return taskDeliveryIdentity(root, record);
104
89
  }
105
90
 
106
91
  export function diffHashOf(root: string, record: TaskRecord): string {
@@ -144,6 +129,21 @@ function verdictFingerprint(raw: Record<string, unknown>): string {
144
129
  });
145
130
  }
146
131
 
132
+ const RELEASED_REVIEW_RECOVERY =
133
+ "Call advance_assurance to obtain a new Review reservation, then dispatch one fresh reviewer with the returned envelope unchanged";
134
+ const RETAINED_REVIEW_RECOVERY =
135
+ "Wait for the dispatched reviewer to finish, then call submit_review again with its verdict; do not dispatch or continue another reviewer";
136
+ const MISMATCH_REVIEW_RECOVERY =
137
+ "Resubmit the reviewer's verdict exactly as the reviewer returned it";
138
+
139
+ function withReviewRecovery(
140
+ result: AssuranceSubmitReviewResult,
141
+ recovery_action: string,
142
+ ): AssuranceSubmitReviewResult {
143
+ if (result.state !== "blocked" || result.code === "verdict_invalid") return result;
144
+ return { ...result, recovery_action };
145
+ }
146
+
147
147
  export async function submitClaudeReview(
148
148
  host: ClaudeReviewHost,
149
149
  coordinator: AssuranceCoordinator,
@@ -154,19 +154,19 @@ export async function submitClaudeReview(
154
154
  if (verdictInput === undefined) throw new Error("verdict is required");
155
155
  const observed = host.inspectReviewForTask(taskId);
156
156
  if (!observed.ok) {
157
- if (observed.release) return coordinator.abandonReview(taskId, observed.reason);
158
- return { state: "blocked", reason: observed.reason };
157
+ if (observed.release) return withReviewRecovery(coordinator.abandonReview(taskId, observed.reason), RELEASED_REVIEW_RECOVERY);
158
+ return { state: "blocked", reason: observed.reason, recovery_action: RETAINED_REVIEW_RECOVERY };
159
159
  }
160
160
  const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
161
161
  if (!parentValid) return coordinator.submitReview(taskId, ctx, verdictInput);
162
162
  const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
163
163
  if (!receiptValid) {
164
- return coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict");
164
+ return withReviewRecovery(coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict"), RELEASED_REVIEW_RECOVERY);
165
165
  }
166
166
  const parentJson = extractVerdictJson(verdictInput)!;
167
167
  const receiptJson = extractVerdictJson(observed.receipt.result)!;
168
168
  if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
169
- return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
169
+ return { state: "blocked", reason: "parent verdict does not match reviewer receipt", recovery_action: MISMATCH_REVIEW_RECOVERY };
170
170
  }
171
171
  return coordinator.submitReview(taskId, ctx, verdictInput);
172
172
  }
@@ -200,150 +200,6 @@ function assertProjectionBinding(before: AssuranceProjectionResult, after: Assur
200
200
  }
201
201
  }
202
202
 
203
- function qaOutcomes(record: { attestations: Array<{ kind: string; acceptance_results: Array<{ acceptance_id: string; status: "passed" | "failed" | "blocked"; summary: string }> }> }) {
204
- return Object.fromEntries(
205
- record.attestations.filter((item) => item.kind === "qa").flatMap((item) => item.acceptance_results)
206
- .map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]),
207
- );
208
- }
209
-
210
- /**
211
- * Publish the task-scoped synthetic revision for a v4 record and return the
212
- * exact identity the Review snapshot binds.
213
- *
214
- * `submitReview` re-derives this identity and compares all four fields —
215
- * `manifest_digest` included — against the reservation. Returning the bare
216
- * commit identity therefore compared a real digest against `undefined` and
217
- * failed every v4 submission with "Review revision changed before submission",
218
- * so the manifest is recomputed here rather than only the commit. The outcomes
219
- * come from the same `qaOutcomes` the Review snapshot is built from, which
220
- * makes the two digests equal by construction instead of by coincidence.
221
- *
222
- * v3 records keep the legacy full-source bundle and return null.
223
- */
224
- export async function ensureClaudeReviewRevision(
225
- root: string,
226
- taskId: string,
227
- projection: AssuranceProjectionResult,
228
- ): Promise<ReviewRevision | null> {
229
- const current = await readTaskRecord(root, taskId);
230
- const record = current.record;
231
- if (!record) throw new Error(`task ${taskId} has no TaskRecord`);
232
- if (current.revision !== projection.projection.record_revision)
233
- throw new Error("TaskRecord changed before Review revision preparation");
234
- if (record.contract !== "assurance_kernel/task_record/v4") return null;
235
- if (!record.git_base_head)
236
- throw new Error("Review revision requires a TaskRecord v4 git_base_head");
237
- const manifest = captureReviewManifest(root, {
238
- taskId,
239
- baseHead: record.git_base_head,
240
- scopeHint: record.intent_snapshot.scope_hint,
241
- expectedDiffHash: projection.projection.diff_hash,
242
- intentRevision: projection.projection.intent_revision,
243
- intentContentHash: projection.projection.intent_content_hash,
244
- recordRevision: projection.projection.record_revision,
245
- workspaceRevision: projection.projection.workspace_revision,
246
- lifecycle: projection.projection.lifecycle,
247
- artifactState: projection.projection.artifact_state,
248
- risk: record.intent_snapshot.risk,
249
- outcomes: qaOutcomes(record),
250
- });
251
- return {
252
- contract: "assurance_kernel/review_revision/v1",
253
- base_head: manifest.base_head,
254
- review_tree: manifest.review_tree,
255
- review_commit: manifest.review_commit,
256
- review_ref: manifest.review_ref,
257
- diff_hash: manifest.diff_hash,
258
- manifest_digest: manifest.manifest_digest,
259
- };
260
- }
261
-
262
- async function buildAssuranceSnapshot(
263
- root: string,
264
- taskId: string,
265
- role: "qa" | "review",
266
- projection: AssuranceProjectionResult,
267
- ) {
268
- const read = await readTaskRecord(root, taskId);
269
- const record = read.record;
270
- if (!record || read.revision !== projection.projection.record_revision) throw new Error("TaskRecord changed before assurance snapshot capture");
271
- const intent = record.intent_snapshot;
272
- const descriptors = new Map<string, VerificationDescriptor>();
273
- for (const item of role === "qa" ? intent.acceptance : []) {
274
- const descriptor = parseVerificationDescriptor(item.verification);
275
- descriptors.set(item.id, descriptor);
276
- }
277
- const reviewBundle = null;
278
- const reviewManifest = role === "review"
279
- ? captureReviewManifest(root, {
280
- taskId,
281
- baseHead: record.git_base_head,
282
- scopeHint: intent.scope_hint,
283
- expectedDiffHash: projection.projection.diff_hash,
284
- intentRevision: projection.projection.intent_revision,
285
- intentContentHash: projection.projection.intent_content_hash,
286
- recordRevision: projection.projection.record_revision,
287
- workspaceRevision: projection.projection.workspace_revision,
288
- lifecycle: projection.projection.lifecycle,
289
- artifactState: projection.projection.artifact_state,
290
- risk: intent.risk,
291
- outcomes: qaOutcomes(record),
292
- })
293
- : null;
294
- const dirtyFiles = reviewManifest ? Object.keys(reviewManifest.changed_paths) : [];
295
- const snapshot: SnapshotDescriptor = {
296
- contract: "assurance_kernel/assurance_snapshot/v2",
297
- task_id: taskId,
298
- run_id: projection.projection.run_id,
299
- role,
300
- record_revision: projection.projection.record_revision,
301
- workspace_revision: projection.projection.workspace_revision,
302
- intent_revision: projection.projection.intent_revision,
303
- intent_content_hash: projection.projection.intent_content_hash,
304
- diff_hash: projection.projection.diff_hash,
305
- lifecycle: projection.projection.lifecycle,
306
- artifact_state: projection.projection.artifact_state,
307
- risk: intent.risk,
308
- fresh_acceptance_ids: projection.projection.fresh_acceptance_ids,
309
- missing_acceptance_ids: projection.projection.missing_acceptance_ids,
310
- stale_attestation_ids: projection.projection.stale_attestation_ids,
311
- acceptance: intent.acceptance,
312
- dirty_files: dirtyFiles,
313
- review_bundle_digest: reviewManifest?.manifest_digest ?? null,
314
- root,
315
- ...(reviewManifest
316
- ? {
317
- review_revision: {
318
- contract: "assurance_kernel/review_revision_identity/v1",
319
- base_head: reviewManifest.base_head,
320
- review_commit: reviewManifest.review_commit,
321
- review_tree: reviewManifest.review_tree,
322
- manifest_digest: reviewManifest.manifest_digest,
323
- },
324
- }
325
- : {}),
326
- };
327
- return { snapshot, descriptors, reviewBundle, reviewManifest };
328
- }
329
-
330
- function stagePlanningArtifactTransition(root: string, record: { intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } }): void {
331
- const intentActive = record.intent_ref.path.replace("docs/plans/archive/", "docs/plans/");
332
- const intentArchive = intentActive.replace("docs/plans/", "docs/plans/archive/");
333
- const specActive = record.intent_snapshot.scope_hint.find((path) =>
334
- /^docs\/specs\/(?!archive\/)[^/]+\.spec\.md$/.test(path)
335
- && record.intent_snapshot.scope_hint.includes(path.replace("docs/specs/", "docs/specs/archive/")),
336
- );
337
- const candidates = [
338
- intentActive,
339
- intentArchive,
340
- ...(specActive ? [specActive, specActive.replace("docs/specs/", "docs/specs/archive/")] : []),
341
- ];
342
- const paths = candidates.filter((path) => existsSync(join(root, path)) || execFileSync("git", ["ls-files", "--cached", "--", path], { cwd: root, encoding: "utf8" }).trim().length > 0);
343
- if (paths.length === 0) return;
344
- execFileSync("git", ["add", "--", ...paths], { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
345
- }
346
-
347
203
  async function mintCapability(
348
204
  registry: MutationAuthorityRegistry,
349
205
  input: {
@@ -413,6 +269,8 @@ export interface ClaudeRuntimeOptions {
413
269
  * actually wires was never constructed once.
414
270
  */
415
271
  ports?: Partial<AssuranceCoordinatorPorts>;
272
+ /** Optional authority test seam, separate from host ports. */
273
+ authorityOverrides?: Partial<VerdictAuthority>;
416
274
  interactive?: boolean;
417
275
  permissionMode?: PermissionMode;
418
276
  requestConfirmation?: NativeConfirmationPort;
@@ -469,7 +327,8 @@ export class ClaudeRuntime {
469
327
  ...this.createKernelPorts(),
470
328
  ...options.ports,
471
329
  host: this.host,
472
- });
330
+ confirmationReference: ({ actorId }) => `claude:${actorId}`,
331
+ }, options.authorityOverrides);
473
332
  }
474
333
 
475
334
  observe(event: ClaudeHookEvent): void {
@@ -502,14 +361,12 @@ export class ClaudeRuntime {
502
361
  private createKernelPorts(): AssuranceCoordinatorPorts {
503
362
  return {
504
363
  host: this.host,
364
+ confirmationReference: ({ actorId }) => `claude:${actorId}`,
505
365
  projectTask: (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
506
- readTaskRecord: async (root, taskId) => readTaskRecord(root, taskId),
366
+ readTaskRecord: async (root, taskId) => readTaskRecordRaw(root, taskId),
507
367
  readTaskIntent: async (root, taskId) => readTaskIntentForRecord(root, taskId),
508
- buildAssurance: (root, taskId, role, projection) => buildAssuranceSnapshot(root, taskId, role, projection),
509
- ensureReviewRevision: (root, taskId, projection) => ensureClaudeReviewRevision(root, taskId, projection),
510
368
  runQa: (snapshot, descriptors, options) => runDeterministicQa(snapshot, descriptors, options),
511
369
  writeReviewEvidence: (input) => writeNativeReviewEvidence(input.evidence),
512
- applyVerdict: (ctx, input) => this.applyVerdict(ctx, input),
513
370
  applyOrdinaryOperation: (ctx, input) => this.executeOrdinary(ctx, input),
514
371
  };
515
372
  }
@@ -588,21 +445,7 @@ export class ClaudeRuntime {
588
445
  confirmation_ref: gate.confirmation_ref,
589
446
  nonce,
590
447
  };
591
- const capability = this.enrollmentRegistry.issue(binding);
592
- const input = {
593
- task_id: taskId,
594
- intent_path: binding.intent_path,
595
- intent_revision: binding.intent_revision,
596
- preparation_digest: binding.preparation_digest,
597
- capability,
598
- capability_binding: binding,
599
- now,
600
- };
601
- const rehearsal = runEnrollmentRehearsal(this.cwd, input, capability, this.enrollmentRegistry);
602
- if (!rehearsal.rehearsed || rehearsal.evidence.outcome !== "ready") {
603
- throw new Error(`Kernel enrollment rehearsal failed: ${rehearsal.evidence.blockers.join("; ")}`);
604
- }
605
- return enrollCanaryTask(this.cwd, input, this.enrollmentRegistry);
448
+ return await enrollTask(this.cwd, this.enrollmentRegistry, { binding, now });
606
449
  } catch (error) {
607
450
  if (gitBaseNote) throw new Error(`${error instanceof Error ? error.message : String(error)}${gitBaseNote}`);
608
451
  throw error;
@@ -766,7 +609,7 @@ export class ClaudeRuntime {
766
609
  const derived = deriveAuthorizationOperation({ readiness: projection.projection.authorization });
767
610
  if ("blocked" in derived) throw new Error(derived.blocked);
768
611
  if (derived.operation === "resolve-user-decision") {
769
- const record = await readTaskRecord(this.cwd, taskId);
612
+ const record = await readTaskRecordRaw(this.cwd, taskId);
770
613
  const open = (record.record?.findings ?? []).filter(
771
614
  (finding) => finding.kind === "unresolved_user_decision" && finding.status === "open",
772
615
  );
@@ -800,7 +643,7 @@ export class ClaudeRuntime {
800
643
  if (sidecar && nextIntent) {
801
644
  writeFileSync(sidecar, `${JSON.stringify(nextIntent, null, 2)}\n`);
802
645
  execFileSync("git", ["add", "--", priorIntent.intent_ref.path], { cwd: this.cwd, stdio: ["ignore", "pipe", "pipe"] });
803
- const preparedRecord = await readTaskRecord(this.cwd, taskId);
646
+ const preparedRecord = await readTaskRecordRaw(this.cwd, taskId);
804
647
  if (!preparedRecord.record) {
805
648
  throw new NativeAuthorityError("workspace_changed", "TaskRecord changed before the breaking revision digest");
806
649
  }
@@ -823,7 +666,7 @@ export class ClaudeRuntime {
823
666
  });
824
667
  } catch (error) {
825
668
  if (stagedSnapshot) {
826
- const current = await readTaskRecord(this.cwd, taskId);
669
+ const current = await readTaskRecordRaw(this.cwd, taskId);
827
670
  if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) {
828
671
  restoreStagedIntent();
829
672
  }
@@ -885,7 +728,7 @@ export class ClaudeRuntime {
885
728
  ) stagePlanningArtifactTransition(this.cwd, result.record);
886
729
  } catch (error) {
887
730
  if (stagedSnapshot) {
888
- const current = await readTaskRecord(this.cwd, taskId);
731
+ const current = await readTaskRecordRaw(this.cwd, taskId);
889
732
  if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) {
890
733
  restoreStagedIntent();
891
734
  }
@@ -899,94 +742,6 @@ export class ClaudeRuntime {
899
742
  return this.withTerminalTracker(taskId, committed);
900
743
  }
901
744
 
902
- private async applyVerdict(
903
- ctx: HostContext,
904
- input: {
905
- taskId: string;
906
- snapshot: SnapshotDescriptor;
907
- verdict: AssuranceVerdict;
908
- invocation: { /* token */ };
909
- actorId: string;
910
- hooks?: { beforeCommit?: () => Promise<void>; onCommit?: () => void; afterCommit?: () => Promise<void> };
911
- },
912
- ): Promise<void> {
913
- const { registry, app } = await this.authority();
914
- const priorIntentToken = (await readTaskIntentForRecord(ctx.cwd, input.taskId)).token;
915
- const now = new Date().toISOString();
916
- const commitAndApply = async <T>(apply: () => Promise<T>): Promise<T> => {
917
- this.coordinator.commitInvocation(input.invocation as never);
918
- const settlement = apply();
919
- input.hooks?.onCommit?.();
920
- const result = await settlement;
921
- await input.hooks?.afterCommit?.();
922
- return result;
923
- };
924
- if (input.verdict.decision === "rework") {
925
- const findings = reviewReworkFindings(input.verdict);
926
- const capability = await mintCapability(registry, {
927
- authority_kind: input.snapshot.role,
928
- task_id: input.taskId,
929
- run_id: input.snapshot.run_id,
930
- action_kind: "request_rework",
931
- expected_record_hash: input.snapshot.record_revision,
932
- intent_revision: input.snapshot.intent_revision,
933
- intent_content_hash: input.snapshot.intent_content_hash,
934
- diff_hash: input.snapshot.diff_hash,
935
- actor_id: input.actorId,
936
- findings,
937
- now,
938
- confirmation_ref: `claude:${input.actorId}`,
939
- });
940
- await input.hooks?.beforeCommit?.();
941
- const result = await commitAndApply(async () => app.execute({
942
- root: ctx.cwd,
943
- task_id: input.taskId,
944
- operation: { op: "request_rework", capability, findings: findings as never[], actor_id: input.actorId },
945
- prior_intent_token: priorIntentToken,
946
- diffProvider: diffSnapshotOf,
947
- now,
948
- }));
949
- stagePlanningArtifactTransition(ctx.cwd, result.record);
950
- return;
951
- }
952
- const advisories = input.verdict.decision === "pass" ? reviewAdvisoryRecords(input.verdict) : [];
953
- const approval: TaskApprovalV2 = {
954
- id: `approval-${input.snapshot.role}-${randomUUID().slice(0, 8)}`,
955
- kind: input.snapshot.role === "qa" ? "qa" : "review",
956
- authority_role: input.snapshot.role === "qa" ? "qa" : "reviewer",
957
- task_revision: input.snapshot.intent_revision,
958
- intent_content_hash: input.snapshot.intent_content_hash,
959
- diff_hash: input.snapshot.diff_hash,
960
- actor_id: input.actorId,
961
- summary: input.verdict.approval!.summary,
962
- ...(input.snapshot.role === "review" && input.snapshot.review_revision ? { review_revision: input.snapshot.review_revision } : {}),
963
- ...(input.snapshot.role === "review" && advisories.length > 0 ? { advisory_findings: advisories } : {}),
964
- };
965
- const capability = await mintCapability(registry, {
966
- authority_kind: input.snapshot.role,
967
- task_id: input.taskId,
968
- run_id: input.snapshot.run_id,
969
- action_kind: "record_approval",
970
- expected_record_hash: input.snapshot.record_revision,
971
- intent_revision: input.snapshot.intent_revision,
972
- intent_content_hash: input.snapshot.intent_content_hash,
973
- diff_hash: input.snapshot.diff_hash,
974
- actor_id: input.actorId,
975
- approval,
976
- now,
977
- confirmation_ref: `claude:${input.actorId}`,
978
- });
979
- await input.hooks?.beforeCommit?.();
980
- await commitAndApply(async () => app.execute({
981
- root: ctx.cwd,
982
- task_id: input.taskId,
983
- operation: { op: "record_approval", capability, approval, actor_id: input.actorId },
984
- prior_intent_token: priorIntentToken,
985
- diffProvider: diffSnapshotOf,
986
- now,
987
- }));
988
- }
989
-
990
745
  private async executeOrdinary(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown; finding_id?: string; attestation_id?: string } }) {
991
746
  const { app } = await this.authority();
992
747
  const operation = input.operation.op === "revise_intent"
@@ -1019,7 +774,7 @@ export class ClaudeRuntime {
1019
774
  return result;
1020
775
  } catch (error) {
1021
776
  if (priorStaged) {
1022
- const current = await readTaskRecord(ctx.cwd, input.taskId);
777
+ const current = await readTaskRecordRaw(ctx.cwd, input.taskId);
1023
778
  if (current.record?.intent_snapshot.revision === priorIntent.intent.revision)
1024
779
  restoreStagedIntentShared(ctx.cwd, priorStaged);
1025
780
  }
@@ -1030,7 +785,7 @@ export class ClaudeRuntime {
1030
785
  async startUnattendedBatch(
1031
786
  initiativeSlug: string,
1032
787
  meta: ToolMeta,
1033
- options: { reuseOnly?: boolean } = {},
788
+ options: { reuseOnly?: boolean; max_parallel?: number; lane_offers?: LaneOffer[] } = {},
1034
789
  ): Promise<ClaudeBatchStartResult> {
1035
790
  throwIfCancelled(meta.signal);
1036
791
  const reuseOnly = options.reuseOnly === true;
@@ -1053,7 +808,7 @@ export class ClaudeRuntime {
1053
808
  root: this.cwd,
1054
809
  initiative_slug: initiativeSlug,
1055
810
  now,
1056
- readInitiative: this.readInitiative ?? observeGithubInitiative,
811
+ readInitiative: this.readInitiative ?? observeInitiative,
1057
812
  });
1058
813
  if (!preflight.ok) {
1059
814
  return {
@@ -1076,7 +831,7 @@ export class ClaudeRuntime {
1076
831
  initiative_slug: initiativeSlug,
1077
832
  now,
1078
833
  projection: preflight.projection,
1079
- readInitiative: this.readInitiative ?? observeGithubInitiative,
834
+ readInitiative: this.readInitiative ?? observeInitiative,
1080
835
  nonce: enrollmentNonce(),
1081
836
  gate: async (facts) => {
1082
837
  // A continuation after a foreground child never opens a gate: when the
@@ -1113,6 +868,15 @@ export class ClaudeRuntime {
1113
868
  reason: child.reason,
1114
869
  })),
1115
870
  budget: facts.budget,
871
+ ...(options.max_parallel !== undefined
872
+ ? {
873
+ lane_mode: {
874
+ max_parallel: options.max_parallel,
875
+ parallel_groups: facts.parallel_groups,
876
+ serialized: facts.scope_conflicts,
877
+ },
878
+ }
879
+ : {}),
1116
880
  ...(facts.reuse_blockers.length > 0
1117
881
  ? {
1118
882
  re_confirmation_required: facts.reuse_blockers,
@@ -1179,17 +943,30 @@ export class ClaudeRuntime {
1179
943
  now = new Date().toISOString();
1180
944
  const capability = this.batchRegistry.issue(binding, recoveryChildren as any, now);
1181
945
 
1182
- const basePort = this.createBatchKernelPort(this.batchRegistry, capability, binding);
1183
- const kernelPort: BatchRunnerKernelPort = {
1184
- ...basePort,
1185
- ownsTaskClaim: (taskId) => {
1186
- if (isResuming && taskId === readActiveClaimTaskId(this.cwd)) {
1187
- // Re-verify the CURRENT claim identity synchronously.
1188
- return isOwnBatchClaim(this.cwd, existingBatch!, taskId, batchBranch);
1189
- }
1190
- return basePort.ownsTaskClaim(taskId);
1191
- },
946
+ // review-batch-partial-port-fabricates-enrollment: the production child
947
+ // Kernel port is the shared runtime/unattended one; this Host supplies
948
+ // only its own advanceTask progression seam (D5).
949
+ const advanceTask: BatchRunnerKernelPort["advanceTask"] = async (root, taskId) => {
950
+ const result = await this.coordinator.advance(taskId, { cwd: root });
951
+ const facts = { diagnostics: result.diagnostics, environment_failure: result.environment_failure, recovery: result.recovery };
952
+ if (result.state === "completed") return { state: "completed", ...facts };
953
+ if (result.state === "stopped") return { state: "stopped", ...facts };
954
+ if (result.state === "rework") return { state: "rework", operation: result.operation, summary: result.summary, ...facts };
955
+ if (result.state === "review_ready") return { state: "review_ready", operation_id: result.operation_id, ...facts };
956
+ if (result.state === "review_preparation_failed") return { state: result.state, operation: result.operation, operation_id: result.operation_id, reason: result.reason, ...facts };
957
+ if (result.state === "blocked") return { state: "blocked", reason: result.reason, ...facts };
958
+ return { state: "failed", reason: (result as { reason?: string }).reason ?? "advance failed", ...facts };
1192
959
  };
960
+ const kernelPort = createBatchKernelPort({
961
+ root: this.cwd,
962
+ enrollmentRegistry: this.enrollmentRegistry,
963
+ registry: this.batchRegistry,
964
+ capability,
965
+ binding,
966
+ advanceTask,
967
+ resume: { isResuming, existingBatch, batchBranch },
968
+ overrides: this.batchKernel,
969
+ });
1193
970
  const report = await startBatch({
1194
971
  root: this.cwd,
1195
972
  batch_id: batchId,
@@ -1204,6 +981,8 @@ export class ClaudeRuntime {
1204
981
  now,
1205
982
  kernel: kernelPort,
1206
983
  git: this.batchGit,
984
+ ...(options.max_parallel !== undefined ? { max_parallel: options.max_parallel } : {}),
985
+ ...(options.lane_offers !== undefined ? { lane_offers: options.lane_offers } : {}),
1207
986
  });
1208
987
 
1209
988
  // review-3 & review-batch-preflight-recovery-is-diagnostic: map rejected batch state to rejected result with same-Host recovery action
@@ -1215,80 +994,6 @@ export class ClaudeRuntime {
1215
994
  report,
1216
995
  };
1217
996
  }
1218
-
1219
- private createBatchKernelPort(
1220
- registry: BatchAuthorityRegistry,
1221
- capability: object,
1222
- binding: BatchAuthorizationBinding,
1223
- ): BatchRunnerKernelPort {
1224
- // review-batch-partial-port-fabricates-enrollment: construct real production port first, never fabricate stubs
1225
- const realPort: BatchRunnerKernelPort = {
1226
- enrollTask: async ({ root, task_id, batch }) => {
1227
- const now = new Date().toISOString();
1228
- const derived = deriveChildEnrollment(root, batch.registry, {
1229
- capability: batch.capability,
1230
- binding,
1231
- task_id,
1232
- expected_head: batch.binding.expected_head,
1233
- now,
1234
- });
1235
- const enrollmentCapability = this.enrollmentRegistry.issue(derived.binding);
1236
- const input = {
1237
- task_id,
1238
- intent_path: derived.binding.intent_path,
1239
- intent_revision: derived.binding.intent_revision,
1240
- preparation_digest: derived.binding.preparation_digest,
1241
- capability: enrollmentCapability,
1242
- capability_binding: derived.binding,
1243
- batch: {
1244
- registry: batch.registry,
1245
- capability: batch.capability,
1246
- binding,
1247
- expected_head: batch.binding.expected_head,
1248
- },
1249
- now,
1250
- };
1251
- const rehearsal = runEnrollmentRehearsal(root, input, enrollmentCapability, this.enrollmentRegistry);
1252
- if (!rehearsal.rehearsed || rehearsal.evidence.outcome !== "ready") {
1253
- throw new Error(`Kernel enrollment rehearsal failed: ${rehearsal.evidence.blockers.join("; ")}`);
1254
- }
1255
- // The enrollment itself is the effect; its record is re-read below.
1256
- enrollCanaryTask(root, input, this.enrollmentRegistry);
1257
- const recordRaw = readTaskRecordRaw(root, task_id);
1258
- return { record_revision: recordRaw.revision };
1259
- },
1260
- advanceTask: async (root, taskId) => {
1261
- const result = await this.coordinator.advance(taskId, { cwd: root });
1262
- const facts = { diagnostics: result.diagnostics, environment_failure: result.environment_failure, recovery: result.recovery };
1263
- if (result.state === "completed") return { state: "completed", ...facts };
1264
- if (result.state === "stopped") return { state: "stopped", ...facts };
1265
- if (result.state === "rework") return { state: "rework", operation: result.operation, summary: result.summary, ...facts };
1266
- if (result.state === "review_ready") return { state: "review_ready", operation_id: result.operation_id, ...facts };
1267
- if (result.state === "review_preparation_failed") return { state: result.state, operation: result.operation, operation_id: result.operation_id, reason: result.reason, ...facts };
1268
- if (result.state === "blocked") return { state: "blocked", reason: result.reason, ...facts };
1269
- return { state: "failed", reason: (result as { reason?: string }).reason ?? "advance failed", ...facts };
1270
- },
1271
- projectTask: async (root, taskId) => {
1272
- const fresh = await projectAssurance(root, taskId, diffSnapshotOf);
1273
- if (!fresh.claim || fresh.projection.lifecycle !== "active" || fresh.error !== null) return fresh;
1274
- const { record, revision } = readTaskRecordRaw(root, taskId);
1275
- if (!record || revision !== fresh.projection.record_revision) throw new Error(`Kernel recovery projection changed for ${taskId}`);
1276
- return { ...fresh, ...batchQaFailureFacts(record) };
1277
- },
1278
- ownsTaskClaim: (taskId) => {
1279
- return registry.isChildConsumed(capability, taskId);
1280
- },
1281
- validateBatchAuthorization: (input) => {
1282
- return input.registry.inspect(input.capability, input.binding as never);
1283
- },
1284
- };
1285
-
1286
- if (!this.batchKernel) return realPort;
1287
- return {
1288
- ...realPort,
1289
- ...this.batchKernel,
1290
- };
1291
- }
1292
997
  }
1293
998
 
1294
999
  export interface ToolMeta {