immune-brain 4.5.0 → 5.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 (79) hide show
  1. package/README.md +157 -39
  2. package/README.zh-CN.md +160 -40
  3. package/package.json +1 -1
  4. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +42 -45
  6. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +189 -476
  7. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +77 -102
  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 +9924 -9654
  16. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +3 -3
  17. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -1
  18. package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
  19. package/plugins/immune-brain/dist/imm-doc-prune.md +2 -2
  20. package/plugins/immune-brain/dist/{imm-agent-doc-maintain.md → imm-doc-slim.md} +9 -9
  21. package/plugins/immune-brain/dist/imm-planner.md +60 -20
  22. package/plugins/immune-brain/dist/imm-pr-fix.md +1 -1
  23. package/plugins/immune-brain/dist/{imm-review-retro.md → imm-retro.md} +26 -13
  24. package/plugins/immune-brain/dist/{imm-loop.md → imm-run.md} +44 -11
  25. package/plugins/immune-brain/dist/registry.yaml +9 -9
  26. package/plugins/immune-brain/dist/role-prompts/code-review.md +7 -0
  27. package/plugins/immune-brain/dist/role-prompts/executor.md +35 -2
  28. package/plugins/immune-brain/runtime/assurance/coordinator.ts +138 -69
  29. package/plugins/immune-brain/runtime/assurance/host_port.ts +20 -0
  30. package/plugins/immune-brain/runtime/assurance/qa.ts +2 -2
  31. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +4 -0
  32. package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +347 -0
  33. package/plugins/immune-brain/runtime/claude/interaction.ts +1 -2
  34. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +135 -396
  35. package/plugins/immune-brain/runtime/claude/mcp_server.ts +5 -4
  36. package/plugins/immune-brain/runtime/claude/review_host.ts +63 -16
  37. package/plugins/immune-brain/runtime/claude/role_agents.ts +83 -0
  38. package/plugins/immune-brain/runtime/commands/kernel.ts +3 -6
  39. package/plugins/immune-brain/runtime/github_issue_tracker.ts +738 -677
  40. package/plugins/immune-brain/runtime/kernel/application.ts +7 -7
  41. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +15 -8
  42. package/plugins/immune-brain/runtime/kernel/authority_port.ts +5 -14
  43. package/plugins/immune-brain/runtime/kernel/batch_authority.ts +30 -49
  44. package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -5
  45. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +0 -1
  46. package/plugins/immune-brain/runtime/kernel/capability_registry.ts +7 -9
  47. package/plugins/immune-brain/runtime/kernel/enrollment.ts +170 -17
  48. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +5 -11
  49. package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
  50. package/plugins/immune-brain/runtime/kernel/storage.ts +170 -11
  51. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +13 -0
  52. package/plugins/immune-brain/runtime/kernel/types.ts +2 -1
  53. package/plugins/immune-brain/runtime/kernel/validation.ts +3 -1
  54. package/plugins/immune-brain/runtime/loop_contract.ts +22 -22
  55. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  56. package/plugins/immune-brain/runtime/prompts/code-review.md +7 -0
  57. package/plugins/immune-brain/runtime/prompts/executor.md +35 -2
  58. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +27 -0
  59. package/plugins/immune-brain/runtime/unattended/batch_git.ts +66 -28
  60. package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +106 -0
  61. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +3 -9
  62. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +132 -45
  63. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
  64. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +40 -36
  65. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +98 -209
  66. package/plugins/immune-brain/runtime/unattended/batch_state.ts +36 -14
  67. package/plugins/immune-brain/runtime/unattended/types.ts +0 -2
  68. package/plugins/immune-brain/runtime/workspace_scope.ts +78 -8
  69. package/plugins/immune-brain/skills/BASELINE.md +4 -4
  70. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +1 -1
  71. package/plugins/immune-brain/skills/{imm-agent-doc-maintain → imm-doc-slim}/SKILL.md +9 -9
  72. package/plugins/immune-brain/skills/imm-planner/SKILL.md +4 -2
  73. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +1 -1
  74. package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/SKILL.md +7 -7
  75. package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/scripts/review_retro.ts +46 -21
  76. package/plugins/immune-brain/skills/{imm-loop → imm-run}/SKILL.md +7 -7
  77. package/plugins/immune-brain/skills/registry.yaml +9 -9
  78. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -584
  79. package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +0 -33
@@ -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,14 +30,13 @@ 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
- import { startConfirmationDeadline } from "../unattended/confirmation_deadline";
48
40
  import { deriveAuthorizationOperation } from "../authorization_operation";
49
41
  import { LITERAL_USER_ACTOR_ID, canonicalActorId } from "../kernel/actor_identity";
50
42
  import {
@@ -57,20 +49,16 @@ import { readTaskTombstone } from "../kernel/backend_claim";
57
49
  import {
58
50
  authorizeBatch,
59
51
  projectBatchPreflight,
60
- readActiveClaimTaskId,
61
- isOwnBatchClaim,
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";
69
60
  import {
70
61
  createBatchAuthorityRegistry,
71
- deriveChildEnrollment,
72
- type BatchAuthorityRegistry,
73
- type BatchAuthorizationBinding,
74
62
  } from "../kernel/batch_authority";
75
63
  import {
76
64
  type BatchRunnerGitPort,
@@ -96,11 +84,7 @@ export function diffSnapshotOf(root: string, record: TaskRecord): {
96
84
  diff_hash: string;
97
85
  changed_paths: readonly string[];
98
86
  } {
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);
87
+ return taskDeliveryIdentity(root, record);
104
88
  }
105
89
 
106
90
  export function diffHashOf(root: string, record: TaskRecord): string {
@@ -144,6 +128,21 @@ function verdictFingerprint(raw: Record<string, unknown>): string {
144
128
  });
145
129
  }
146
130
 
131
+ const RELEASED_REVIEW_RECOVERY =
132
+ "Call advance_assurance to obtain a new Review reservation, then dispatch one fresh reviewer with the returned envelope unchanged";
133
+ const RETAINED_REVIEW_RECOVERY =
134
+ "Wait for the dispatched reviewer to finish, then call submit_review again with its verdict; do not dispatch or continue another reviewer";
135
+ const MISMATCH_REVIEW_RECOVERY =
136
+ "Resubmit the reviewer's verdict exactly as the reviewer returned it";
137
+
138
+ function withReviewRecovery(
139
+ result: AssuranceSubmitReviewResult,
140
+ recovery_action: string,
141
+ ): AssuranceSubmitReviewResult {
142
+ if (result.state !== "blocked" || result.code === "verdict_invalid") return result;
143
+ return { ...result, recovery_action };
144
+ }
145
+
147
146
  export async function submitClaudeReview(
148
147
  host: ClaudeReviewHost,
149
148
  coordinator: AssuranceCoordinator,
@@ -154,19 +153,19 @@ export async function submitClaudeReview(
154
153
  if (verdictInput === undefined) throw new Error("verdict is required");
155
154
  const observed = host.inspectReviewForTask(taskId);
156
155
  if (!observed.ok) {
157
- if (observed.release) return coordinator.abandonReview(taskId, observed.reason);
158
- return { state: "blocked", reason: observed.reason };
156
+ if (observed.release) return withReviewRecovery(coordinator.abandonReview(taskId, observed.reason), RELEASED_REVIEW_RECOVERY);
157
+ return { state: "blocked", reason: observed.reason, recovery_action: RETAINED_REVIEW_RECOVERY };
159
158
  }
160
159
  const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
161
160
  if (!parentValid) return coordinator.submitReview(taskId, ctx, verdictInput);
162
161
  const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
163
162
  if (!receiptValid) {
164
- return coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict");
163
+ return withReviewRecovery(coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict"), RELEASED_REVIEW_RECOVERY);
165
164
  }
166
165
  const parentJson = extractVerdictJson(verdictInput)!;
167
166
  const receiptJson = extractVerdictJson(observed.receipt.result)!;
168
167
  if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
169
- return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
168
+ return { state: "blocked", reason: "parent verdict does not match reviewer receipt", recovery_action: MISMATCH_REVIEW_RECOVERY };
170
169
  }
171
170
  return coordinator.submitReview(taskId, ctx, verdictInput);
172
171
  }
@@ -200,150 +199,6 @@ function assertProjectionBinding(before: AssuranceProjectionResult, after: Assur
200
199
  }
201
200
  }
202
201
 
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
202
  async function mintCapability(
348
203
  registry: MutationAuthorityRegistry,
349
204
  input: {
@@ -391,7 +246,6 @@ async function mintCapability(
391
246
  diff_hash: input.diff_hash,
392
247
  actor_id: input.actor_id,
393
248
  confirmation_ref: input.confirmation_ref,
394
- expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
395
249
  findings_digest: input.action_kind === "request_rework"
396
250
  ? findingsDigestV2(input.findings as TaskFinding[])
397
251
  : null,
@@ -414,6 +268,8 @@ export interface ClaudeRuntimeOptions {
414
268
  * actually wires was never constructed once.
415
269
  */
416
270
  ports?: Partial<AssuranceCoordinatorPorts>;
271
+ /** Optional authority test seam, separate from host ports. */
272
+ authorityOverrides?: Partial<VerdictAuthority>;
417
273
  interactive?: boolean;
418
274
  permissionMode?: PermissionMode;
419
275
  requestConfirmation?: NativeConfirmationPort;
@@ -470,7 +326,8 @@ export class ClaudeRuntime {
470
326
  ...this.createKernelPorts(),
471
327
  ...options.ports,
472
328
  host: this.host,
473
- });
329
+ confirmationReference: ({ actorId }) => `claude:${actorId}`,
330
+ }, options.authorityOverrides);
474
331
  }
475
332
 
476
333
  observe(event: ClaudeHookEvent): void {
@@ -503,14 +360,12 @@ export class ClaudeRuntime {
503
360
  private createKernelPorts(): AssuranceCoordinatorPorts {
504
361
  return {
505
362
  host: this.host,
363
+ confirmationReference: ({ actorId }) => `claude:${actorId}`,
506
364
  projectTask: (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
507
- readTaskRecord: async (root, taskId) => readTaskRecord(root, taskId),
365
+ readTaskRecord: async (root, taskId) => readTaskRecordRaw(root, taskId),
508
366
  readTaskIntent: async (root, taskId) => readTaskIntentForRecord(root, taskId),
509
- buildAssurance: (root, taskId, role, projection) => buildAssuranceSnapshot(root, taskId, role, projection),
510
- ensureReviewRevision: (root, taskId, projection) => ensureClaudeReviewRevision(root, taskId, projection),
511
367
  runQa: (snapshot, descriptors, options) => runDeterministicQa(snapshot, descriptors, options),
512
368
  writeReviewEvidence: (input) => writeNativeReviewEvidence(input.evidence),
513
- applyVerdict: (ctx, input) => this.applyVerdict(ctx, input),
514
369
  applyOrdinaryOperation: (ctx, input) => this.executeOrdinary(ctx, input),
515
370
  };
516
371
  }
@@ -587,44 +442,74 @@ export class ClaudeRuntime {
587
442
  preparation_digest: preparation.digest,
588
443
  actor_id: LITERAL_USER_ACTOR_ID,
589
444
  confirmation_ref: gate.confirmation_ref,
590
- expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
591
445
  nonce,
592
446
  };
593
- const capability = this.enrollmentRegistry.issue(binding);
594
- const input = {
595
- task_id: taskId,
596
- intent_path: binding.intent_path,
597
- intent_revision: binding.intent_revision,
598
- preparation_digest: binding.preparation_digest,
599
- capability,
600
- capability_binding: binding,
601
- now,
602
- };
603
- const rehearsal = runEnrollmentRehearsal(this.cwd, input, capability, this.enrollmentRegistry);
604
- if (!rehearsal.rehearsed || rehearsal.evidence.outcome !== "ready") {
605
- throw new Error(`Kernel enrollment rehearsal failed: ${rehearsal.evidence.blockers.join("; ")}`);
606
- }
607
- return enrollCanaryTask(this.cwd, input, this.enrollmentRegistry);
447
+ return await enrollTask(this.cwd, this.enrollmentRegistry, { binding, now });
608
448
  } catch (error) {
609
449
  if (gitBaseNote) throw new Error(`${error instanceof Error ? error.message : String(error)}${gitBaseNote}`);
610
450
  throw error;
611
451
  }
612
452
  }
613
453
 
614
- async advance(taskId: string, signal?: AbortSignal) {
615
- return this.withTerminalTracker(
454
+ async advance(taskId: string, signal?: AbortSignal, meta?: ToolMeta) {
455
+ return this.closeOutBatchChild(
616
456
  taskId,
617
- await this.coordinator.advance(taskId, { cwd: this.cwd }, signal),
457
+ await this.withTerminalTracker(taskId, await this.coordinator.advance(taskId, { cwd: this.cwd }, signal)),
458
+ meta,
618
459
  );
619
460
  }
620
461
 
621
- async submitReview(taskId: string, verdictInput: unknown) {
622
- return this.withTerminalTracker(
462
+ async submitReview(taskId: string, verdictInput: unknown, meta?: ToolMeta) {
463
+ return this.closeOutBatchChild(
623
464
  taskId,
624
- await submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput),
465
+ await this.withTerminalTracker(
466
+ taskId,
467
+ await submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput),
468
+ ),
469
+ meta,
625
470
  );
626
471
  }
627
472
 
473
+ /**
474
+ * Gate-free closeout: a batch child that reached Kernel `done` in the
475
+ * foreground re-enters the batch in the same call, reusing its authorization.
476
+ * Only `done` continues; a stopped child, a parked child and a live Review
477
+ * obligation end where they do today. The continuation is transport: the
478
+ * Kernel result is returned unchanged, and any failure is reported beside it
479
+ * with one retry action, never thrown into a path that could read as "the
480
+ * mutation did not happen".
481
+ */
482
+ private async closeOutBatchChild<T>(taskId: string, result: T, meta?: ToolMeta): Promise<T> {
483
+ if (!meta || result === null || typeof result !== "object") return result;
484
+ const lifecycle = (result as { record?: { lifecycle?: unknown } }).record?.lifecycle;
485
+ if ((result as { state?: unknown }).state !== "completed" && lifecycle !== "done") return result;
486
+ let slug: string | null;
487
+ try {
488
+ slug = findResumableBatchSlugForTask(this.cwd, taskId);
489
+ } catch {
490
+ return result;
491
+ }
492
+ if (!slug) return result;
493
+ try {
494
+ // The batch commit takes exactly the scope envelope plus this task's own
495
+ // audit evidence, and its preflight refuses unstaged changes: stage only
496
+ // that evidence, the step an Executor otherwise does by hand.
497
+ if (existsSync(join(this.cwd, ".imm", "audit", taskId)))
498
+ execFileSync("git", ["-C", this.cwd, "add", "--", `.imm/audit/${taskId}`], { stdio: "ignore" });
499
+ const batch = await this.startUnattendedBatch(slug, meta, { reuseOnly: true });
500
+ return { ...(result as object), batch } as T;
501
+ } catch (error) {
502
+ return {
503
+ ...(result as object),
504
+ batch: {
505
+ state: "rejected",
506
+ reason: `batch continuation failed: ${error instanceof Error ? error.message : String(error)}`,
507
+ recovery_action: "call start_unattended_batch with the same Initiative to continue the batch",
508
+ },
509
+ } as T;
510
+ }
511
+ }
512
+
628
513
  /**
629
514
  * Post-settlement GitHub tracker projection: the same shared step the Pi Host
630
515
  * runs, so an opted-in terminal projection no longer depends on which Host
@@ -723,7 +608,7 @@ export class ClaudeRuntime {
723
608
  const derived = deriveAuthorizationOperation({ readiness: projection.projection.authorization });
724
609
  if ("blocked" in derived) throw new Error(derived.blocked);
725
610
  if (derived.operation === "resolve-user-decision") {
726
- const record = await readTaskRecord(this.cwd, taskId);
611
+ const record = await readTaskRecordRaw(this.cwd, taskId);
727
612
  const open = (record.record?.findings ?? []).filter(
728
613
  (finding) => finding.kind === "unresolved_user_decision" && finding.status === "open",
729
614
  );
@@ -757,7 +642,7 @@ export class ClaudeRuntime {
757
642
  if (sidecar && nextIntent) {
758
643
  writeFileSync(sidecar, `${JSON.stringify(nextIntent, null, 2)}\n`);
759
644
  execFileSync("git", ["add", "--", priorIntent.intent_ref.path], { cwd: this.cwd, stdio: ["ignore", "pipe", "pipe"] });
760
- const preparedRecord = await readTaskRecord(this.cwd, taskId);
645
+ const preparedRecord = await readTaskRecordRaw(this.cwd, taskId);
761
646
  if (!preparedRecord.record) {
762
647
  throw new NativeAuthorityError("workspace_changed", "TaskRecord changed before the breaking revision digest");
763
648
  }
@@ -780,7 +665,7 @@ export class ClaudeRuntime {
780
665
  });
781
666
  } catch (error) {
782
667
  if (stagedSnapshot) {
783
- const current = await readTaskRecord(this.cwd, taskId);
668
+ const current = await readTaskRecordRaw(this.cwd, taskId);
784
669
  if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) {
785
670
  restoreStagedIntent();
786
671
  }
@@ -842,7 +727,7 @@ export class ClaudeRuntime {
842
727
  ) stagePlanningArtifactTransition(this.cwd, result.record);
843
728
  } catch (error) {
844
729
  if (stagedSnapshot) {
845
- const current = await readTaskRecord(this.cwd, taskId);
730
+ const current = await readTaskRecordRaw(this.cwd, taskId);
846
731
  if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) {
847
732
  restoreStagedIntent();
848
733
  }
@@ -856,94 +741,6 @@ export class ClaudeRuntime {
856
741
  return this.withTerminalTracker(taskId, committed);
857
742
  }
858
743
 
859
- private async applyVerdict(
860
- ctx: HostContext,
861
- input: {
862
- taskId: string;
863
- snapshot: SnapshotDescriptor;
864
- verdict: AssuranceVerdict;
865
- invocation: { /* token */ };
866
- actorId: string;
867
- hooks?: { beforeCommit?: () => Promise<void>; onCommit?: () => void; afterCommit?: () => Promise<void> };
868
- },
869
- ): Promise<void> {
870
- const { registry, app } = await this.authority();
871
- const priorIntentToken = (await readTaskIntentForRecord(ctx.cwd, input.taskId)).token;
872
- const now = new Date().toISOString();
873
- const commitAndApply = async <T>(apply: () => Promise<T>): Promise<T> => {
874
- this.coordinator.commitInvocation(input.invocation as never);
875
- const settlement = apply();
876
- input.hooks?.onCommit?.();
877
- const result = await settlement;
878
- await input.hooks?.afterCommit?.();
879
- return result;
880
- };
881
- if (input.verdict.decision === "rework") {
882
- const findings = reviewReworkFindings(input.verdict);
883
- const capability = await mintCapability(registry, {
884
- authority_kind: input.snapshot.role,
885
- task_id: input.taskId,
886
- run_id: input.snapshot.run_id,
887
- action_kind: "request_rework",
888
- expected_record_hash: input.snapshot.record_revision,
889
- intent_revision: input.snapshot.intent_revision,
890
- intent_content_hash: input.snapshot.intent_content_hash,
891
- diff_hash: input.snapshot.diff_hash,
892
- actor_id: input.actorId,
893
- findings,
894
- now,
895
- confirmation_ref: `claude:${input.actorId}`,
896
- });
897
- await input.hooks?.beforeCommit?.();
898
- const result = await commitAndApply(async () => app.execute({
899
- root: ctx.cwd,
900
- task_id: input.taskId,
901
- operation: { op: "request_rework", capability, findings: findings as never[], actor_id: input.actorId },
902
- prior_intent_token: priorIntentToken,
903
- diffProvider: diffSnapshotOf,
904
- now,
905
- }));
906
- stagePlanningArtifactTransition(ctx.cwd, result.record);
907
- return;
908
- }
909
- const advisories = input.verdict.decision === "pass" ? reviewAdvisoryRecords(input.verdict) : [];
910
- const approval: TaskApprovalV2 = {
911
- id: `approval-${input.snapshot.role}-${randomUUID().slice(0, 8)}`,
912
- kind: input.snapshot.role === "qa" ? "qa" : "review",
913
- authority_role: input.snapshot.role === "qa" ? "qa" : "reviewer",
914
- task_revision: input.snapshot.intent_revision,
915
- intent_content_hash: input.snapshot.intent_content_hash,
916
- diff_hash: input.snapshot.diff_hash,
917
- actor_id: input.actorId,
918
- summary: input.verdict.approval!.summary,
919
- ...(input.snapshot.role === "review" && input.snapshot.review_revision ? { review_revision: input.snapshot.review_revision } : {}),
920
- ...(input.snapshot.role === "review" && advisories.length > 0 ? { advisory_findings: advisories } : {}),
921
- };
922
- const capability = await mintCapability(registry, {
923
- authority_kind: input.snapshot.role,
924
- task_id: input.taskId,
925
- run_id: input.snapshot.run_id,
926
- action_kind: "record_approval",
927
- expected_record_hash: input.snapshot.record_revision,
928
- intent_revision: input.snapshot.intent_revision,
929
- intent_content_hash: input.snapshot.intent_content_hash,
930
- diff_hash: input.snapshot.diff_hash,
931
- actor_id: input.actorId,
932
- approval,
933
- now,
934
- confirmation_ref: `claude:${input.actorId}`,
935
- });
936
- await input.hooks?.beforeCommit?.();
937
- await commitAndApply(async () => app.execute({
938
- root: ctx.cwd,
939
- task_id: input.taskId,
940
- operation: { op: "record_approval", capability, approval, actor_id: input.actorId },
941
- prior_intent_token: priorIntentToken,
942
- diffProvider: diffSnapshotOf,
943
- now,
944
- }));
945
- }
946
-
947
744
  private async executeOrdinary(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown; finding_id?: string; attestation_id?: string } }) {
948
745
  const { app } = await this.authority();
949
746
  const operation = input.operation.op === "revise_intent"
@@ -976,7 +773,7 @@ export class ClaudeRuntime {
976
773
  return result;
977
774
  } catch (error) {
978
775
  if (priorStaged) {
979
- const current = await readTaskRecord(ctx.cwd, input.taskId);
776
+ const current = await readTaskRecordRaw(ctx.cwd, input.taskId);
980
777
  if (current.record?.intent_snapshot.revision === priorIntent.intent.revision)
981
778
  restoreStagedIntentShared(ctx.cwd, priorStaged);
982
779
  }
@@ -987,15 +784,18 @@ export class ClaudeRuntime {
987
784
  async startUnattendedBatch(
988
785
  initiativeSlug: string,
989
786
  meta: ToolMeta,
787
+ options: { reuseOnly?: boolean } = {},
990
788
  ): Promise<ClaudeBatchStartResult> {
991
789
  throwIfCancelled(meta.signal);
790
+ const reuseOnly = options.reuseOnly === true;
992
791
  const probe = probeHost(this.env, process.platform, this.hostVersion);
993
792
  if (!probe.ok) throw new NativeAuthorityError("unsupported_host", probe.reason);
793
+ // A reuse-only continuation never opens a gate, so it needs no confirmation port.
994
794
  const interactive = meta.interactive ?? this.interactive;
995
- if (!interactive) throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
795
+ if (!reuseOnly && !interactive) throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
996
796
  // The text is table-owned; only the form (a thrown native error rather than
997
797
  // a returned envelope) is this Host's.
998
- if (!this.requestConfirmation)
798
+ if (!reuseOnly && !this.requestConfirmation)
999
799
  throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
1000
800
 
1001
801
  // 1. Host-independent batch preflight: claim ownership, branch
@@ -1022,7 +822,7 @@ export class ClaudeRuntime {
1022
822
  const budget = preflight.projection.budget;
1023
823
  const planDigest = preflight.projection.plan_digest;
1024
824
  const recoveryChildren = preflight.projection.recovery_children;
1025
- // 5. Literal-user gate plus the shared reuse/expiry decision, the post-gate
825
+ // 5. Literal-user gate plus the shared reuse decision, the post-gate
1026
826
  // claim/drift cascade, and the Batch Authorization binding. The Host supplies
1027
827
  // only its gate, its confirmation reference, and its binding nonce.
1028
828
  const authorization = await authorizeBatch<ClaudeBatchStartResult>({
@@ -1033,9 +833,19 @@ export class ClaudeRuntime {
1033
833
  readInitiative: this.readInitiative ?? observeGithubInitiative,
1034
834
  nonce: enrollmentNonce(),
1035
835
  gate: async (facts) => {
1036
- // review-4: the bounded elicitation deadline is shared behavior now, so
1037
- // both Hosts bound an unanswered confirmation by the same setting.
1038
- const deadline = startConfirmationDeadline({ env: this.env, signal: meta.signal });
836
+ // A continuation after a foreground child never opens a gate: when the
837
+ // authorization cannot be reused, the user re-enters the batch tool.
838
+ if (reuseOnly)
839
+ return {
840
+ kind: "host_rejection",
841
+ value: {
842
+ state: "blocked",
843
+ reason: `batch authorization cannot be reused (${facts.reuse_blockers.join(", ")})`,
844
+ recovery_action: "call start_unattended_batch with the same Initiative to confirm a fresh authorization",
845
+ },
846
+ };
847
+ // The gate settles only on the literal user's answer or the caller's
848
+ // cancellation signal.
1039
849
  let confirmationResult: { decision: NativeDecision; requestId: string };
1040
850
  try {
1041
851
  confirmationResult = await this.requestConfirmation!({
@@ -1057,7 +867,6 @@ export class ClaudeRuntime {
1057
867
  reason: child.reason,
1058
868
  })),
1059
869
  budget: facts.budget,
1060
- expires_at: facts.expires_at,
1061
870
  ...(facts.reuse_blockers.length > 0
1062
871
  ? {
1063
872
  re_confirmation_required: facts.reuse_blockers,
@@ -1065,10 +874,9 @@ export class ClaudeRuntime {
1065
874
  }
1066
875
  : {}),
1067
876
  },
1068
- signal: deadline.signal,
877
+ signal: meta.signal,
1069
878
  });
1070
879
  } catch (err) {
1071
- if (deadline.timedOut()) return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
1072
880
  if (meta.signal?.aborted)
1073
881
  return { kind: "host_rejection", value: batchReason("cancelled_before_execution") };
1074
882
  if (err instanceof NativeAuthorityError) {
@@ -1088,13 +896,8 @@ export class ClaudeRuntime {
1088
896
  kind: "host_rejection",
1089
897
  value: batchReason("confirmation_failed", err instanceof Error ? err.message : String(err)),
1090
898
  };
1091
- } finally {
1092
- deadline.clear();
1093
899
  }
1094
900
 
1095
- // A transport that answers "cancel" on abort is still a timeout, not a user
1096
- // decision: the Host that owns the transport reports which one it was.
1097
- if (deadline.timedOut()) return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
1098
901
  if (confirmationResult.decision === "cancel" && meta.signal?.aborted)
1099
902
  return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
1100
903
  if (meta.signal?.aborted)
@@ -1122,27 +925,38 @@ export class ClaudeRuntime {
1122
925
  });
1123
926
  if (authorization.outcome === "host_rejection") return authorization.value;
1124
927
  if (authorization.outcome === "rejected") return authorization.rejection;
1125
- const { binding, batch_id: batchId, expires_at: expiresAt } = authorization;
928
+ const { binding, batch_id: batchId } = authorization;
1126
929
 
1127
930
  // review-1: verify cancellation signal right before authority issuance and startBatch
1128
931
  if (meta.signal?.aborted) return batchReason("cancelled_before_execution");
1129
932
 
1130
933
  now = new Date().toISOString();
1131
- if (Date.parse(expiresAt) <= Date.parse(now))
1132
- return batchReason("confirmation_failed", "confirmed batch deadline expired before authorization issuance");
1133
934
  const capability = this.batchRegistry.issue(binding, recoveryChildren as any, now);
1134
935
 
1135
- const basePort = this.createBatchKernelPort(this.batchRegistry, capability, binding);
1136
- const kernelPort: BatchRunnerKernelPort = {
1137
- ...basePort,
1138
- ownsTaskClaim: (taskId) => {
1139
- if (isResuming && taskId === readActiveClaimTaskId(this.cwd)) {
1140
- // Re-verify the CURRENT claim identity synchronously.
1141
- return isOwnBatchClaim(this.cwd, existingBatch!, taskId, batchBranch);
1142
- }
1143
- return basePort.ownsTaskClaim(taskId);
1144
- },
936
+ // review-batch-partial-port-fabricates-enrollment: the production child
937
+ // Kernel port is the shared runtime/unattended one; this Host supplies
938
+ // only its own advanceTask progression seam (D5).
939
+ const advanceTask: BatchRunnerKernelPort["advanceTask"] = async (root, taskId) => {
940
+ const result = await this.coordinator.advance(taskId, { cwd: root });
941
+ const facts = { diagnostics: result.diagnostics, environment_failure: result.environment_failure, recovery: result.recovery };
942
+ if (result.state === "completed") return { state: "completed", ...facts };
943
+ if (result.state === "stopped") return { state: "stopped", ...facts };
944
+ if (result.state === "rework") return { state: "rework", operation: result.operation, summary: result.summary, ...facts };
945
+ if (result.state === "review_ready") return { state: "review_ready", operation_id: result.operation_id, ...facts };
946
+ if (result.state === "review_preparation_failed") return { state: result.state, operation: result.operation, operation_id: result.operation_id, reason: result.reason, ...facts };
947
+ if (result.state === "blocked") return { state: "blocked", reason: result.reason, ...facts };
948
+ return { state: "failed", reason: (result as { reason?: string }).reason ?? "advance failed", ...facts };
1145
949
  };
950
+ const kernelPort = createBatchKernelPort({
951
+ root: this.cwd,
952
+ enrollmentRegistry: this.enrollmentRegistry,
953
+ registry: this.batchRegistry,
954
+ capability,
955
+ binding,
956
+ advanceTask,
957
+ resume: { isResuming, existingBatch, batchBranch },
958
+ overrides: this.batchKernel,
959
+ });
1146
960
  const report = await startBatch({
1147
961
  root: this.cwd,
1148
962
  batch_id: batchId,
@@ -1153,7 +967,6 @@ export class ClaudeRuntime {
1153
967
  plan_digest: planDigest,
1154
968
  base_head: binding.base_head,
1155
969
  confirmation_time: now,
1156
- authorization_expires_at: expiresAt,
1157
970
  budget,
1158
971
  now,
1159
972
  kernel: kernelPort,
@@ -1169,80 +982,6 @@ export class ClaudeRuntime {
1169
982
  report,
1170
983
  };
1171
984
  }
1172
-
1173
- private createBatchKernelPort(
1174
- registry: BatchAuthorityRegistry,
1175
- capability: object,
1176
- binding: BatchAuthorizationBinding,
1177
- ): BatchRunnerKernelPort {
1178
- // review-batch-partial-port-fabricates-enrollment: construct real production port first, never fabricate stubs
1179
- const realPort: BatchRunnerKernelPort = {
1180
- enrollTask: async ({ root, task_id, batch }) => {
1181
- const now = new Date().toISOString();
1182
- const derived = deriveChildEnrollment(root, batch.registry, {
1183
- capability: batch.capability,
1184
- binding,
1185
- task_id,
1186
- expected_head: batch.binding.expected_head,
1187
- now,
1188
- });
1189
- const enrollmentCapability = this.enrollmentRegistry.issue(derived.binding);
1190
- const input = {
1191
- task_id,
1192
- intent_path: derived.binding.intent_path,
1193
- intent_revision: derived.binding.intent_revision,
1194
- preparation_digest: derived.binding.preparation_digest,
1195
- capability: enrollmentCapability,
1196
- capability_binding: derived.binding,
1197
- batch: {
1198
- registry: batch.registry,
1199
- capability: batch.capability,
1200
- binding,
1201
- expected_head: batch.binding.expected_head,
1202
- },
1203
- now,
1204
- };
1205
- const rehearsal = runEnrollmentRehearsal(root, input, enrollmentCapability, this.enrollmentRegistry);
1206
- if (!rehearsal.rehearsed || rehearsal.evidence.outcome !== "ready") {
1207
- throw new Error(`Kernel enrollment rehearsal failed: ${rehearsal.evidence.blockers.join("; ")}`);
1208
- }
1209
- // The enrollment itself is the effect; its record is re-read below.
1210
- enrollCanaryTask(root, input, this.enrollmentRegistry);
1211
- const recordRaw = readTaskRecordRaw(root, task_id);
1212
- return { record_revision: recordRaw.revision };
1213
- },
1214
- advanceTask: async (root, taskId) => {
1215
- const result = await this.coordinator.advance(taskId, { cwd: root });
1216
- const facts = { diagnostics: result.diagnostics, environment_failure: result.environment_failure, recovery: result.recovery };
1217
- if (result.state === "completed") return { state: "completed", ...facts };
1218
- if (result.state === "stopped") return { state: "stopped", ...facts };
1219
- if (result.state === "rework") return { state: "rework", operation: result.operation, summary: result.summary, ...facts };
1220
- if (result.state === "review_ready") return { state: "review_ready", operation_id: result.operation_id, ...facts };
1221
- if (result.state === "review_preparation_failed") return { state: result.state, operation: result.operation, operation_id: result.operation_id, reason: result.reason, ...facts };
1222
- if (result.state === "blocked") return { state: "blocked", reason: result.reason, ...facts };
1223
- return { state: "failed", reason: (result as { reason?: string }).reason ?? "advance failed", ...facts };
1224
- },
1225
- projectTask: async (root, taskId) => {
1226
- const fresh = await projectAssurance(root, taskId, diffSnapshotOf);
1227
- if (!fresh.claim || fresh.projection.lifecycle !== "active" || fresh.error !== null) return fresh;
1228
- const { record, revision } = readTaskRecordRaw(root, taskId);
1229
- if (!record || revision !== fresh.projection.record_revision) throw new Error(`Kernel recovery projection changed for ${taskId}`);
1230
- return { ...fresh, ...batchQaFailureFacts(record) };
1231
- },
1232
- ownsTaskClaim: (taskId) => {
1233
- return registry.isChildConsumed(capability, taskId);
1234
- },
1235
- validateBatchAuthorization: (input) => {
1236
- return input.registry.inspect(input.capability, input.binding as never);
1237
- },
1238
- };
1239
-
1240
- if (!this.batchKernel) return realPort;
1241
- return {
1242
- ...realPort,
1243
- ...this.batchKernel,
1244
- };
1245
- }
1246
985
  }
1247
986
 
1248
987
  export interface ToolMeta {