immune-brain 5.2.0 → 5.5.1

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 (77) hide show
  1. package/README.md +4 -4
  2. package/README.zh-CN.md +4 -4
  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 +54 -70
  6. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +163 -28
  7. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +71 -10
  8. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +1 -1
  9. package/plugins/immune-brain/.pi-extension/pi-run-continuation.ts +161 -0
  10. package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -2
  11. package/plugins/immune-brain/dist/BASELINE.md +17 -1
  12. package/plugins/immune-brain/dist/claude/mcp-server.mjs +16145 -12745
  13. package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +3 -1
  14. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +41 -1
  15. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  16. package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
  17. package/plugins/immune-brain/dist/imm-planner.md +63 -19
  18. package/plugins/immune-brain/dist/imm-run-batch.md +398 -0
  19. package/plugins/immune-brain/dist/imm-run.md +89 -344
  20. package/plugins/immune-brain/dist/role-prompts/code-review.md +26 -2
  21. package/plugins/immune-brain/dist/role-prompts/executor.md +35 -10
  22. package/plugins/immune-brain/hooks/hooks.json +22 -2
  23. package/plugins/immune-brain/runtime/assurance/coordinator.ts +203 -13
  24. package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +2 -1
  25. package/plugins/immune-brain/runtime/assurance/enrollment_git_base.ts +99 -14
  26. package/plugins/immune-brain/runtime/assurance/phase_entry.ts +10 -26
  27. package/plugins/immune-brain/runtime/assurance/qa.ts +126 -43
  28. package/plugins/immune-brain/runtime/assurance/qa_findings.ts +8 -0
  29. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +48 -0
  30. package/plugins/immune-brain/runtime/assurance/run_continuation.ts +284 -0
  31. package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +46 -4
  32. package/plugins/immune-brain/runtime/assurance/verification.ts +83 -35
  33. package/plugins/immune-brain/runtime/assurance/workflow_autocommit.ts +222 -0
  34. package/plugins/immune-brain/runtime/claude/interaction.ts +11 -2
  35. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +199 -48
  36. package/plugins/immune-brain/runtime/claude/lane_guard.ts +30 -20
  37. package/plugins/immune-brain/runtime/claude/mcp_server.ts +105 -9
  38. package/plugins/immune-brain/runtime/claude/run_continuation.ts +274 -0
  39. package/plugins/immune-brain/runtime/commands/kernel.ts +41 -103
  40. package/plugins/immune-brain/runtime/github_issue_tracker.ts +60 -2
  41. package/plugins/immune-brain/runtime/kernel/application.ts +4 -1
  42. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +103 -20
  43. package/plugins/immune-brain/runtime/kernel/canary_application.ts +57 -7
  44. package/plugins/immune-brain/runtime/kernel/completion.ts +28 -4
  45. package/plugins/immune-brain/runtime/kernel/enrollment.ts +48 -8
  46. package/plugins/immune-brain/runtime/kernel/intent.ts +169 -104
  47. package/plugins/immune-brain/runtime/kernel/reducer.ts +136 -6
  48. package/plugins/immune-brain/runtime/kernel/risk_floor.ts +286 -0
  49. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +83 -1
  50. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +1 -1
  51. package/plugins/immune-brain/runtime/kernel/types.ts +49 -3
  52. package/plugins/immune-brain/runtime/kernel/validation.ts +147 -7
  53. package/plugins/immune-brain/runtime/loop_contract.ts +5 -0
  54. package/plugins/immune-brain/runtime/plan_core.ts +15 -8
  55. package/plugins/immune-brain/runtime/plugin_source_skew.ts +58 -0
  56. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  57. package/plugins/immune-brain/runtime/prompts/code-review.md +26 -2
  58. package/plugins/immune-brain/runtime/prompts/executor.md +35 -10
  59. package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +5 -3
  60. package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +40 -10
  61. package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +57 -3
  62. package/plugins/immune-brain/runtime/unattended/batch_git.ts +424 -66
  63. package/plugins/immune-brain/runtime/unattended/batch_integration.ts +2 -1
  64. package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +13 -0
  65. package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +43 -24
  66. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +6 -2
  67. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +102 -41
  68. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +9 -3
  69. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +9 -9
  70. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +90 -32
  71. package/plugins/immune-brain/runtime/unattended/batch_state.ts +102 -4
  72. package/plugins/immune-brain/runtime/unattended/parent_delivery_consolidation.ts +248 -0
  73. package/plugins/immune-brain/runtime/workspace_scope.ts +126 -18
  74. package/plugins/immune-brain/skills/BASELINE.md +17 -1
  75. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -3
  76. package/plugins/immune-brain/skills/imm-planner/SKILL.md +8 -2
  77. package/plugins/immune-brain/skills/imm-run/SKILL.md +12 -8
@@ -41,6 +41,7 @@ import {
41
41
  } from "./storage";
42
42
  import { KernelInvariantError } from "./validation";
43
43
  import type {
44
+ ReviewRevisionIdentityV1,
44
45
  StoredTaskMutationV3,
45
46
  TaskAction,
46
47
  TaskApprovalV2,
@@ -54,13 +55,21 @@ export type CanaryOperation =
54
55
  | { op: "record_finding"; finding: Omit<TaskFinding, "status" | "source" | "review_round">; actor_id: string }
55
56
  | { op: "resolve_finding"; finding_id: string; actor_id: string }
56
57
  | { op: "refute_finding"; finding_id: string; attestation_id: string; actor_id: string }
57
- | { op: "request_rework"; capability: object; findings: TaskFinding[]; actor_id: string }
58
+ | {
59
+ op: "request_rework";
60
+ capability: object;
61
+ findings: TaskFinding[];
62
+ actor_id: string;
63
+ /** Review-authored rework: the immutable revision Review analyzed. */
64
+ review_revision?: ReviewRevisionIdentityV1;
65
+ }
58
66
  | { op: "record_approval"; capability: object; approval: TaskApprovalV2; actor_id: string }
59
67
  | { op: "revise_intent"; next_intent: TaskIntentV1; actor_id: string }
60
68
  | { op: "approve_breaking_intent_revision"; capability: object; next_intent: TaskIntentV1; actor_id: string }
61
69
  | { op: "complete"; actor_id: string }
62
70
  | { op: "stop"; capability: object; reason: string; actor_id: string }
63
- | { op: "authorize_rework"; capability: object; actor_id: string }
71
+ | { op: "authorize_rework"; capability: object; actor_id: string; rounds?: number }
72
+ | { op: "accept_residual_risk"; capability: object; reason: string; actor_id: string }
64
73
  | { op: "resolve_user_decision"; capability: object; finding_id: string; resolution: string; actor_id: string };
65
74
 
66
75
  export interface CanaryExecuteInput {
@@ -116,6 +125,17 @@ export function beginDrainCapabilityAction(taskId: string, at: string): TaskActi
116
125
  * capability minted against this action's digest always validates.
117
126
  * "begin-drain" delegates to the canonical begin_drain action.
118
127
  */
128
+ /** Fixed key order so the minted digest equals the parsed action's digest. */
129
+ function canonicalReviewRevision(revision: ReviewRevisionIdentityV1): ReviewRevisionIdentityV1 {
130
+ return {
131
+ contract: revision.contract,
132
+ base_head: revision.base_head,
133
+ review_commit: revision.review_commit,
134
+ review_tree: revision.review_tree,
135
+ manifest_digest: revision.manifest_digest,
136
+ };
137
+ }
138
+
119
139
  export function capabilityActionFor(input: {
120
140
  op: string;
121
141
  task_id: string;
@@ -128,6 +148,9 @@ export function capabilityActionFor(input: {
128
148
  next_intent_ref?: unknown;
129
149
  finding_id?: string;
130
150
  resolution?: unknown;
151
+ review_revision?: ReviewRevisionIdentityV1;
152
+ /** authorize_rework only: further Review rounds granted, 1-5. */
153
+ rounds?: number;
131
154
  }): TaskAction {
132
155
  if (input.op === "begin-drain") {
133
156
  return beginDrainCapabilityAction(input.task_id, input.at);
@@ -148,9 +171,21 @@ export function capabilityActionFor(input: {
148
171
  case "record_approval":
149
172
  return { ...base, approval: input.approval } as TaskAction;
150
173
  case "request_rework":
151
- return { ...base, findings: input.findings } as TaskAction;
174
+ return {
175
+ ...base,
176
+ findings: input.findings,
177
+ ...(input.review_revision ? { review_revision: canonicalReviewRevision(input.review_revision) } : {}),
178
+ } as TaskAction;
152
179
  case "authorize_rework":
153
- return { ...base, type: "authorize_rework" } as TaskAction;
180
+ // `rounds` joins the digest only when present, so a legacy caller
181
+ // keeps the exact digest it always minted.
182
+ return {
183
+ ...base,
184
+ type: "authorize_rework",
185
+ ...(input.rounds !== undefined ? { rounds: input.rounds } : {}),
186
+ } as TaskAction;
187
+ case "accept_residual_risk":
188
+ return { ...base, type: "accept_residual_risk", reason: input.reason } as TaskAction;
154
189
  case "stop":
155
190
  return { ...base, reason: input.reason } as TaskAction;
156
191
  case "approve_breaking_intent_revision":
@@ -216,7 +251,7 @@ export function createCanaryApplication(
216
251
  throw new KernelInvariantError([`artifact freeze requires active lifecycle, got ${current.record.lifecycle}`]);
217
252
  if (workspace.state.current_working !== input.task_id)
218
253
  throw new KernelInvariantError([`workspace is not owned by task ${input.task_id}`]);
219
- const fresh = readTaskIntent(input.root, input.task_id, current.record.intent_ref.path);
254
+ const fresh = readTaskIntent(input.root, input.task_id, current.record.intent_ref.path, current.record.intent_snapshot);
220
255
  const pair = inspectIntentTokenPair(input.prior_intent_token, fresh.token);
221
256
  if (
222
257
  pair.prior.intent_content_hash !== current.record.intent_ref.content_hash
@@ -376,7 +411,14 @@ export function createCanaryApplication(
376
411
  break;
377
412
  case "request_rework":
378
413
  capability = operation.capability;
379
- action = { ...base, type: "request_rework", findings: operation.findings };
414
+ action = {
415
+ ...base,
416
+ type: "request_rework",
417
+ findings: operation.findings,
418
+ ...(operation.review_revision
419
+ ? { review_revision: canonicalReviewRevision(operation.review_revision) }
420
+ : {}),
421
+ };
380
422
  break;
381
423
  case "record_approval":
382
424
  capability = operation.capability;
@@ -414,7 +456,15 @@ export function createCanaryApplication(
414
456
  break;
415
457
  case "authorize_rework":
416
458
  capability = operation.capability;
417
- action = { ...base, type: "authorize_rework" };
459
+ action = {
460
+ ...base,
461
+ type: "authorize_rework",
462
+ ...(operation.rounds !== undefined ? { rounds: operation.rounds } : {}),
463
+ };
464
+ break;
465
+ case "accept_residual_risk":
466
+ capability = operation.capability;
467
+ action = { ...base, type: "accept_residual_risk", reason: operation.reason };
418
468
  break;
419
469
  case "resolve_user_decision":
420
470
  capability = operation.capability;
@@ -1,4 +1,5 @@
1
1
  import { classifyTaskRisk } from "./intent";
2
+ import { NO_RISK_FLOOR, raiseRisk, scopeRiskFloor, type RiskFloor } from "./risk_floor";
2
3
  import { archivePath, boundSpecPath, inspectSpecBinding } from "./spec_binding";
3
4
  import type {
4
5
  ApprovalKind,
@@ -48,13 +49,16 @@ export function asTaskDiffSnapshot(value: TaskDiffInput): {
48
49
  export function resolveProjectedRisk(
49
50
  intent: TaskIntentV1,
50
51
  changedPaths: readonly string[] = [],
52
+ floor: RiskFloor = NO_RISK_FLOOR,
51
53
  ): TaskIntentV1["risk"] {
54
+ const risk = raiseRisk(intent.risk, scopeRiskFloor(intent.scope_hint, floor));
52
55
  // Invalid bindings have no trusted planning-path exclusions.
53
- if (!inspectSpecBinding(intent).ok) return classifyTaskRisk(changedPaths, intent.risk);
56
+ if (!inspectSpecBinding(intent).ok) return classifyTaskRisk(changedPaths, risk, floor);
54
57
  const excluded = ownSidecarPaths(intent);
55
58
  return classifyTaskRisk(
56
59
  changedPaths.filter((path) => !excluded.has(path)),
57
- intent.risk,
60
+ risk,
61
+ floor,
58
62
  );
59
63
  }
60
64
 
@@ -80,6 +84,7 @@ export function completionDecision(
80
84
  currentDiffHash: string,
81
85
  currentIntentContentHash: string,
82
86
  changedPaths: readonly string[] = [],
87
+ floor: RiskFloor = NO_RISK_FLOOR,
83
88
  ): CompletionDecision {
84
89
  assertKernelInvariantsV3(intent, record);
85
90
  const freshAttestations = record.attestations.filter(
@@ -108,7 +113,22 @@ export function completionDecision(
108
113
  )
109
114
  .map((item) => item.id);
110
115
 
111
- const requiredKinds = REQUIRED_ATTESTATIONS[resolveProjectedRisk(intent, changedPaths)];
116
+ // A literal-user residual-risk acceptance stands in for the Review
117
+ // approval only while it names this exact intent revision, intent hash and
118
+ // diff, and every finding it accepted is still accepted. QA, acceptance and
119
+ // every other obligation are unaffected, and any later change makes it stale.
120
+ const acceptance = record.residual_risk_acceptance;
121
+ const residualRiskAccepted =
122
+ acceptance !== undefined &&
123
+ acceptance.intent_revision === intent.revision &&
124
+ acceptance.intent_content_hash === currentIntentContentHash &&
125
+ acceptance.diff_hash === currentDiffHash &&
126
+ acceptance.finding_ids.every((id) =>
127
+ record.findings.some((item) => item.id === id && item.status === "accepted_risk"),
128
+ );
129
+ const requiredKinds = REQUIRED_ATTESTATIONS[
130
+ resolveProjectedRisk(intent, changedPaths, floor)
131
+ ].filter((kind) => !(residualRiskAccepted && kind === "review"));
112
132
  const candidates = freshAttestations.filter((item) => requiredKinds.includes(item.kind));
113
133
  const missingApprovalKinds = requiredKinds.filter(
114
134
  (kind) => !candidates.some((attestation) => attestation.kind === kind),
@@ -160,6 +180,8 @@ export function completionDecision(
160
180
 
161
181
  return {
162
182
  complete:
183
+ // An invalid Spec binding is never complete, whatever the project's floor says.
184
+ inspectSpecBinding(intent).ok &&
163
185
  missingAcceptanceIds.length === 0 &&
164
186
  missingApprovalKinds.length === 0 &&
165
187
  blockingFindingIds.length === 0 &&
@@ -183,6 +205,7 @@ export function projectTask(
183
205
  currentDiffHash: string,
184
206
  currentIntentContentHash: string,
185
207
  changedPaths: readonly string[] = [],
208
+ floor: RiskFloor = NO_RISK_FLOOR,
186
209
  ): TaskProjectionV3 {
187
210
  if (record.lifecycle === "active" && !inspectSpecBinding(intent).ok) {
188
211
  assertKernelInvariantsV3(intent, record);
@@ -198,7 +221,7 @@ export function projectTask(
198
221
  fresh_acceptance_ids: [],
199
222
  missing_acceptance_ids: intent.acceptance.map((item) => item.id),
200
223
  stale_attestation_ids: record.attestations.map((item) => item.id),
201
- missing_approval_kinds: REQUIRED_ATTESTATIONS[resolveProjectedRisk(intent, changedPaths)],
224
+ missing_approval_kinds: REQUIRED_ATTESTATIONS[resolveProjectedRisk(intent, changedPaths, floor)],
202
225
  blocking_finding_ids: record.findings.filter((item) => item.kind === "blocking" && item.status !== "resolved").map((item) => item.id),
203
226
  unresolved_user_decision_ids: record.findings.filter((item) => item.kind === "unresolved_user_decision" && item.status === "open").map((item) => item.id),
204
227
  replan_required_ids: record.findings.filter((item) => item.kind === "replan_required" && item.status === "open").map((item) => item.id),
@@ -211,6 +234,7 @@ export function projectTask(
211
234
  currentDiffHash,
212
235
  currentIntentContentHash,
213
236
  changedPaths,
237
+ floor,
214
238
  );
215
239
  const blocked =
216
240
  decision.blocking_finding_ids.length > 0 ||
@@ -54,9 +54,35 @@ export interface EnrollCanaryInput {
54
54
  capability: object;
55
55
  capability_binding: EnrollmentCapabilityBinding;
56
56
  batch?: EnrollBatchContext;
57
+ /**
58
+ * The in-scope paths the confirmed Enrollment gate listed as already staged,
59
+ * modified or untracked. Only these are claimed as task work; absent means
60
+ * the gate listed none, so any in-scope dirt refuses the Enrollment.
61
+ */
62
+ disclosed_scope_paths?: readonly string[];
57
63
  now: string;
58
64
  }
59
65
 
66
+ /**
67
+ * What an Enrollment gate must show about a scope that is already dirty: the
68
+ * paths, and the one line both Hosts print for them. A scope that cannot be
69
+ * inspected discloses nothing, and the Enrollment itself reports why.
70
+ */
71
+ export function enrollmentScopeDisclosure(root: string, taskId: string): { paths: string[]; notice: string | null } {
72
+ let paths: string[] = [];
73
+ try {
74
+ paths = dirtyScopePaths(root, readTaskIntent(root, taskId).intent.scope_hint, taskId);
75
+ } catch {
76
+ return { paths: [], notice: null };
77
+ }
78
+ return {
79
+ paths,
80
+ notice: paths.length === 0
81
+ ? null
82
+ : `In-scope changes that already exist (${paths.length}) and become task work on confirmation: ${paths.join(", ")}`,
83
+ };
84
+ }
85
+
60
86
  /**
61
87
  * A rehearsal that is not ready rejects through this single typed error so the
62
88
  * "Kernel enrollment rehearsal failed" message is produced in exactly one place
@@ -80,6 +106,8 @@ export interface EnrollmentEntryRequest {
80
106
  binding: EnrollmentCapabilityBinding;
81
107
  /** Present only for a batch-derived child enrollment. */
82
108
  batch?: EnrollBatchContext;
109
+ /** The in-scope paths the confirmed gate listed; see `EnrollCanaryInput`. */
110
+ disclosed_scope_paths?: readonly string[];
83
111
  now: string;
84
112
  /**
85
113
  * Optional caller checkpoint run after a ready rehearsal and before the
@@ -111,6 +139,8 @@ type EnrollmentPreconditionState = {
111
139
  workspace: ReturnType<typeof readWorkspaceStateRaw> | null;
112
140
  intent: Awaited<ReturnType<typeof readTaskIntent>> | null;
113
141
  gitBaseHead: string | null;
142
+ /** In-scope dirt the confirmed gate listed; left out of the Enrollment baseline. */
143
+ claimedScopePaths: string[];
114
144
  };
115
145
 
116
146
  type EnrollmentPreconditionResult = EnrollmentPreconditionState & {
@@ -160,6 +190,7 @@ function runEnrollmentPreconditionChecks<T>(
160
190
  let workspace: EnrollmentPreconditionState["workspace"] = null;
161
191
  let intent: EnrollmentPreconditionState["intent"] = null;
162
192
  let gitBaseHead: string | null = null;
193
+ let claimedScopePaths: string[] = [];
163
194
  const fail = (report: string, error: unknown) => {
164
195
  if (mode === "fail_fast")
165
196
  throw error instanceof Error ? error : new Error(String(error));
@@ -210,16 +241,20 @@ function runEnrollmentPreconditionChecks<T>(
210
241
  } catch (error) {
211
242
  fail(`git base: ${error instanceof Error ? error.message : String(error)}`, error);
212
243
  }
213
- // In-scope dirt present now would be recorded in the Enrollment baseline as
214
- // user dirt and withheld from every later task snapshot, so the task could
215
- // never deliver it. Refuse before the capability is consumed.
244
+ // In-scope dirt the confirmed gate listed is claimed: it is left out of the
245
+ // Enrollment baseline and becomes task work. Any other in-scope dirt would
246
+ // be recorded there as user dirt and withheld from every later task
247
+ // snapshot, so the task could never deliver it. Refuse that before the
248
+ // capability is consumed; nothing is claimed that the gate did not show.
216
249
  if (intent && gitBaseHead) {
217
250
  try {
251
+ const disclosed = new Set(input.disclosed_scope_paths ?? []);
218
252
  const dirty = dirtyScopePaths(root, intent.intent.scope_hint, input.task_id);
219
- if (dirty.length > 0) {
220
- const message = `task scope is already dirty before Enrollment: ${dirty.join(", ")}; commit, stash or revert these paths first`;
253
+ const unlisted = dirty.filter((path) => !disclosed.has(path));
254
+ if (unlisted.length > 0) {
255
+ const message = `task scope is already dirty before Enrollment: ${unlisted.join(", ")}; the confirmed Enrollment gate did not list these paths, so enroll again from the Host to review them`;
221
256
  fail(message, new Error(message));
222
- }
257
+ } else claimedScopePaths = dirty;
223
258
  } catch (error) {
224
259
  fail(`scope: ${error instanceof Error ? error.message : String(error)}`, error);
225
260
  }
@@ -231,6 +266,7 @@ function runEnrollmentPreconditionChecks<T>(
231
266
  workspace,
232
267
  intent,
233
268
  gitBaseHead,
269
+ claimedScopePaths,
234
270
  };
235
271
  return onReady ? onReady(state) : { ...state, blockers };
236
272
  });
@@ -276,6 +312,8 @@ export function runEnrollmentRehearsal(
276
312
  task_id: string;
277
313
  outcome: "ready" | "not_ready";
278
314
  blockers: string[];
315
+ /** In-scope dirt the gate listed, which this Enrollment would claim as task work. */
316
+ claimed_scope_paths: string[];
279
317
  generated_at: string;
280
318
  };
281
319
  } {
@@ -295,6 +333,7 @@ export function runEnrollmentRehearsal(
295
333
  task_id: input.task_id,
296
334
  outcome: preconditions.blockers.length === 0 ? "ready" : "not_ready",
297
335
  blockers: preconditions.blockers,
336
+ claimed_scope_paths: preconditions.claimedScopePaths,
298
337
  generated_at: input.now,
299
338
  },
300
339
  };
@@ -357,7 +396,7 @@ export async function enrollTask(
357
396
  registry: EnrollmentAuthorityRegistry,
358
397
  request: EnrollmentEntryRequest,
359
398
  ): Promise<EnrollCanaryResult | EnrollmentEntryOutcome> {
360
- const { binding, batch, now, checkpoint } = request;
399
+ const { binding, batch, now, checkpoint, disclosed_scope_paths } = request;
361
400
  // (1) Issue the capability for the supplied binding.
362
401
  const capability = registry.issue(binding);
363
402
  const input: EnrollCanaryInput = {
@@ -368,6 +407,7 @@ export async function enrollTask(
368
407
  capability,
369
408
  capability_binding: binding,
370
409
  ...(batch ? { batch } : {}),
410
+ ...(disclosed_scope_paths ? { disclosed_scope_paths } : {}),
371
411
  now,
372
412
  };
373
413
  // Replay of a lost Enrollment: the exact request already committed here, so
@@ -554,7 +594,7 @@ export function enrollCanaryTask(
554
594
  claim as unknown as Record<string, unknown>,
555
595
  digest,
556
596
  );
557
- writeEnrollmentBaseline(root);
597
+ writeEnrollmentBaseline(root, checks.claimedScopePaths);
558
598
  return {
559
599
  record: mutation.record,
560
600
  backend_claim: claim,