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
@@ -9,6 +9,7 @@ import {
9
9
  type MutationAuthorityRegistry,
10
10
  } from "./authority_port";
11
11
  import { asTaskDiffSnapshot } from "./completion";
12
+ import { taskDeliveryIdentity } from "../workspace_scope";
12
13
  import { canonicalIntentHash, readTaskIntent } from "./intent";
13
14
  import {
14
15
  inspectIntentTokenPair,
@@ -51,9 +52,9 @@ export interface ApplyTaskActionInput {
51
52
  /** Paired mutation authority registry that must recognize every supplied capability. */
52
53
  registry: MutationAuthorityRegistry;
53
54
  capability?: MutationAuthorityCapabilityV2;
54
- /** Trusted injected diff provider; the action's diff_hash is only an expectation. */
55
- diffProvider: (root: string, record: TaskRecord) => string | { diff_hash: string; changed_paths?: readonly string[] };
56
- now?: number;
55
+ /** Trusted injected diff provider; the action's diff_hash is only an expectation.
56
+ * Defaults to the shared delivery identity selector when omitted. */
57
+ diffProvider?: (root: string, record: TaskRecord) => string | { diff_hash: string; changed_paths?: readonly string[] };
57
58
  /**
58
59
  * Terminal commit mode: record + workspace + active-claim removal + task
59
60
  * tombstone converge through one recoverable terminal transaction marker
@@ -72,8 +73,8 @@ export interface ApplyTaskActionInput {
72
73
  export function applyTaskAction(
73
74
  input: ApplyTaskActionInput,
74
75
  ): StoredTaskMutationV3 {
75
- const { root, task_id, prior_intent_token, registry, capability, diffProvider, now } =
76
- input;
76
+ const { root, task_id, prior_intent_token, registry, capability } = input;
77
+ const diffProvider = input.diffProvider ?? taskDeliveryIdentity;
77
78
  return withKernelStoreLock(root, () => {
78
79
  const current = readTaskRecordRaw(root, task_id);
79
80
  if (!current.record)
@@ -192,7 +193,7 @@ export function applyTaskAction(
192
193
  // replay identity, and conflicting reuse, so any throw is a zero-write
193
194
  // failure (including stale record/workspace CAS).
194
195
  const inspectedAudit = expectedAuthority
195
- ? registry.inspect(capability, expectedAuthority, now)
196
+ ? registry.inspect(capability, expectedAuthority)
196
197
  : null;
197
198
  const authorityRunId = inspectedAudit?.run_id;
198
199
  const mutation = reduceTask(
@@ -237,7 +238,6 @@ export function applyTaskAction(
237
238
  registry.consume(
238
239
  capability as MutationAuthorityCapabilityV2,
239
240
  expectedAuthority,
240
- now,
241
241
  );
242
242
  }
243
243
 
@@ -2,9 +2,9 @@
2
2
  // projection that binds a task's backend claim, TaskRecord v3, workspace,
3
3
  // attestations, findings, and completion facts into one closed result.
4
4
  //
5
- // This module is deliberately INTERNAL to the Kernel runtime: it is imported
6
- // by the Pi extension only through the structural runtime-stub adapter and is
7
- // never re-exported from ./index.ts. Hosts consume the projection without
5
+ // This module is deliberately INTERNAL to the Kernel runtime: both host
6
+ // adapters import it statically and it is never re-exported from ./index.ts.
7
+ // Hosts consume the projection without
8
8
  // reconstructing evidence/approval freshness or authorization readiness.
9
9
  //
10
10
  // It does not know about Pi session state, pending native Review verdicts,
@@ -13,13 +13,15 @@
13
13
 
14
14
  import { readBackendClaim, readTaskTombstone } from "./backend_claim";
15
15
  import {
16
- readTaskRecord,
16
+ readTaskRecordRaw,
17
17
  readAuditTaskPair,
18
+ localRunId,
18
19
  readCommittedRecord,
19
20
  readWorkspaceStateRaw,
20
21
  reconcileKernelAuthority,
21
22
  } from "./storage";
22
23
  import { projectTask, resolveProjectedRisk } from "./completion";
24
+ import { taskDeliveryIdentity } from "../workspace_scope";
23
25
  import type { AssuranceObligation, TaskIntentV1, TaskRecord, TaskRecordV3 } from "./types";
24
26
  import type { TaskRecordV2 } from "./legacy_task_record";
25
27
 
@@ -244,7 +246,9 @@ function projectFromRecord(
244
246
  /**
245
247
  * One closed read-only projection for a task. The injected diffProvider keeps
246
248
  * this module free of git/worktree concerns; hosts supply it from their own
247
- * task-scope diff implementation.
249
+ * task-scope diff implementation. When no provider is supplied the shared
250
+ * delivery identity selector (workspace_scope.taskDeliveryIdentity) computes
251
+ * it; a supplied provider still overrides it (I5).
248
252
  *
249
253
  * Error semantics:
250
254
  * - no authority facts -> error null, claim null, empty projection
@@ -257,7 +261,7 @@ function projectFromRecord(
257
261
  export async function projectAssurance(
258
262
  root: string,
259
263
  taskId: string,
260
- diffProvider: (root: string, record: TaskRecord) => TaskDiffSnapshot,
264
+ diffProvider: (root: string, record: TaskRecord) => TaskDiffSnapshot = taskDeliveryIdentity,
261
265
  ): Promise<AssuranceProjectionResult> {
262
266
  const fail = (
263
267
  error: string,
@@ -303,7 +307,7 @@ export async function projectAssurance(
303
307
  }
304
308
  let read;
305
309
  try {
306
- read = await readTaskRecord(root, taskId);
310
+ read = await readTaskRecordRaw(root, taskId);
307
311
  } catch (error) {
308
312
  if (!terminalOwner || !(error instanceof Error) || !error.message.startsWith("TaskRecord v2")) throw error;
309
313
  return fail(error instanceof Error ? error.message : String(error));
@@ -332,7 +336,10 @@ export async function projectAssurance(
332
336
  },
333
337
  };
334
338
  }
335
- const auditPair = await readAuditTaskPair(root, taskId);
339
+ // Run-aware: with a local run row the store already answered above;
340
+ // without one (a fresh clone or pre-run-layout evidence) only the
341
+ // resolved pair may serve, never another run's directory by drift.
342
+ const auditPair = await readAuditTaskPair(root, taskId, localRunId(root, taskId) ?? undefined);
336
343
  if (!auditPair) return fail(`task ${taskId} has no terminal audit pair`, claim);
337
344
  const workspace = await readWorkspaceStateRaw(root);
338
345
  if (auditPair.record.contract === "assurance_kernel/task_record/v2")
@@ -62,17 +62,15 @@ export interface MutationAuthorityRegistry {
62
62
  readonly brand: symbol;
63
63
  /** Issue one capability bound to an exact binding. Library primitive; no production caller in P2B2. */
64
64
  issue(binding: CapabilityBindingV2, issuedAt?: string): MutationAuthorityCapabilityV2;
65
- /** Inspect without consuming. Fails on missing, expired, mismatched, or reused capability. */
65
+ /** Inspect without consuming. Fails on missing, mismatched, or reused capability. */
66
66
  inspect(
67
67
  capability: MutationAuthorityCapabilityV2 | undefined,
68
68
  expected: MutationAuthorityInspection,
69
- now?: number,
70
69
  ): ValidatedAuthorityV2;
71
70
  /** Consume irreversibly. Returns the validated authority. */
72
71
  consume(
73
72
  capability: MutationAuthorityCapabilityV2,
74
73
  expected: MutationAuthorityInspection,
75
- now?: number,
76
74
  ): ValidatedAuthorityV2;
77
75
  isConsumed(capability: MutationAuthorityCapabilityV2): boolean;
78
76
  }
@@ -88,7 +86,7 @@ export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
88
86
  const inner = createCapabilityRegistry<CapabilityBindingV2, MutationAuthorityInspection, ValidatedAuthorityV2, MutationAuthorityCapabilityV2>(
89
87
  MUTATION_AUTHORITY_CAPABILITY_BRAND,
90
88
  {
91
- validateBinding(binding, issuedAt) {
89
+ validateBinding(binding) {
92
90
  const missing: string[] = [];
93
91
  if (binding.run_id !== undefined && binding.run_id.length === 0)
94
92
  throw new Error("authority capability run_id must not be empty");
@@ -100,12 +98,8 @@ export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
100
98
  throw new Error(`authority capability binding is incomplete: ${missing.join(", ")}`);
101
99
  if (binding.findings_digest !== null && !/^sha256:[a-f0-9]{64}$/.test(binding.findings_digest))
102
100
  throw new Error("authority capability findings_digest must be a canonical sha256 hash");
103
- if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
104
- throw new Error("authority capability must have a future expiry");
105
101
  },
106
- validateAndProject(state, expected, now) {
107
- if (Date.parse(state.expires_at) <= now)
108
- throw new Error("authority capability has expired");
102
+ validateAndProject(state, expected) {
109
103
  const actionDigest = digestOfAction(expected.action);
110
104
  if (state.action_digest !== actionDigest)
111
105
  throw new Error("authority capability action digest mismatch");
@@ -141,7 +135,6 @@ export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
141
135
  actor_id: canonicalActorId(state.actor_id),
142
136
  confirmation_ref: state.confirmation_ref,
143
137
  issued_at: state.issued_at,
144
- expires_at: state.expires_at,
145
138
  },
146
139
  action_digest: actionDigest,
147
140
  ...(state.run_id !== undefined ? { run_id: state.run_id } : {}),
@@ -162,12 +155,11 @@ export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
162
155
  inspect(
163
156
  capability: MutationAuthorityCapabilityV2 | undefined,
164
157
  expected: MutationAuthorityInspection,
165
- now = Date.now(),
166
158
  ): ValidatedAuthorityV2 {
167
159
  if (!capability)
168
160
  throw new Error("privileged action requires an opaque authority capability");
169
161
  try {
170
- return inner.inspect(capability, expected, now);
162
+ return inner.inspect(capability, expected);
171
163
  } catch (err) {
172
164
  if (err instanceof Error && err.message.includes("not recognized by this registry"))
173
165
  throw new Error("privileged action requires an opaque authority capability");
@@ -177,10 +169,9 @@ export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
177
169
  consume(
178
170
  capability: MutationAuthorityCapabilityV2,
179
171
  expected: MutationAuthorityInspection,
180
- now = Date.now(),
181
172
  ): ValidatedAuthorityV2 {
182
173
  try {
183
- return inner.consume(capability, expected, now);
174
+ return inner.consume(capability, expected);
184
175
  } catch (err) {
185
176
  if (err instanceof Error && err.message.includes("not recognized by this registry"))
186
177
  throw new Error("privileged action requires an opaque authority capability");
@@ -8,6 +8,7 @@
8
8
  // plus an advancing HEAD lineage instead.
9
9
 
10
10
  import { createHash } from "node:crypto";
11
+ import { spawnSync } from "node:child_process";
11
12
  import { createCapabilityRegistry } from "./capability_registry";
12
13
  import { isLiteralUserActor } from "./actor_identity";
13
14
  import type { BaseCapabilityBinding, EnrollmentCapabilityBinding } from "./enrollment_authority";
@@ -19,15 +20,6 @@ export const BATCH_AUTHORITY_CAPABILITY_BRAND = Symbol.for(
19
20
 
20
21
  const GIT_COMMIT_ID = /^[a-f0-9]{40}$/;
21
22
 
22
- /** review-7(3rd rework): typed expiry marker thrown at the Kernel enrollment
23
- * boundary so the driver can classify a real enrollment-time expiry as an
24
- * intentional budget stop without free-form message matching. */
25
- export class BatchAuthorizationExpiryError extends Error {
26
- constructor(message: string) {
27
- super(message);
28
- this.name = "BatchAuthorizationExpiryError";
29
- }
30
- }
31
23
 
32
24
  export interface BatchPlanChild {
33
25
  task_id: string;
@@ -39,7 +31,6 @@ export interface BatchPlanChild {
39
31
 
40
32
  export interface BatchBudget {
41
33
  max_children: number;
42
- deadline_at: string;
43
34
  qa_failure_limit: number;
44
35
  }
45
36
 
@@ -63,7 +54,6 @@ export interface ValidatedBatchAuthorization {
63
54
  actor_id: string;
64
55
  confirmation_ref: string;
65
56
  issued_at: string;
66
- expires_at: string;
67
57
  nonce: string;
68
58
  }
69
59
 
@@ -81,7 +71,6 @@ export interface BatchAuthorityRegistry {
81
71
  inspect(
82
72
  capability: object,
83
73
  expected: BatchAuthorizationBinding,
84
- now?: number,
85
74
  ): ValidatedBatchAuthorization;
86
75
  children(capability: object): BatchPlanChild[];
87
76
  consumedChildren(capability: object): string[];
@@ -91,7 +80,6 @@ export interface BatchAuthorityRegistry {
91
80
  capability: object,
92
81
  expected: BatchAuthorizationBinding,
93
82
  taskId: string,
94
- now?: number,
95
83
  ): ValidatedBatchAuthorization;
96
84
  /** Undo one slot consumption when the bound write did not commit. */
97
85
  releaseChild(capability: object, taskId: string): void;
@@ -138,7 +126,6 @@ function requireNonEmpty(binding: BatchAuthorizationBinding): void {
138
126
  "base_head",
139
127
  "actor_id",
140
128
  "confirmation_ref",
141
- "expires_at",
142
129
  "nonce",
143
130
  ] as const) {
144
131
  const value = binding[key];
@@ -149,14 +136,11 @@ function requireNonEmpty(binding: BatchAuthorizationBinding): void {
149
136
  throw new Error(`batch authorization binding is incomplete: ${missing.join(", ")}`);
150
137
  }
151
138
 
152
- function validateBudget(budget: BatchBudget, issuedAt: string): void {
139
+ function validateBudget(budget: BatchBudget): void {
153
140
  if (!Number.isInteger(budget.max_children) || budget.max_children <= 0)
154
141
  throw new Error("batch budget max_children must be a positive integer");
155
142
  if (!Number.isInteger(budget.qa_failure_limit) || budget.qa_failure_limit <= 0)
156
143
  throw new Error("batch budget qa_failure_limit must be a positive integer");
157
- const deadline = Date.parse(budget.deadline_at);
158
- if (Number.isNaN(deadline) || deadline <= Date.parse(issuedAt))
159
- throw new Error("batch budget must have a future deadline_at");
160
144
  }
161
145
 
162
146
  function validateChildren(children: BatchPlanChild[], planDigest: string): void {
@@ -210,7 +194,7 @@ export function createBatchAuthorityRegistry(): BatchAuthorityRegistry {
210
194
  >(
211
195
  BATCH_AUTHORITY_CAPABILITY_BRAND,
212
196
  {
213
- validateBinding(binding, issuedAt) {
197
+ validateBinding(binding) {
214
198
  requireNonEmpty(binding);
215
199
  // Both the canonical literal-user spelling and the historical `user`
216
200
  // spelling are read, so a batch authorized before the convergence keeps
@@ -219,29 +203,15 @@ export function createBatchAuthorityRegistry(): BatchAuthorityRegistry {
219
203
  throw new Error("batch authorization requires a literal-user actor_id");
220
204
  if (!GIT_COMMIT_ID.test(binding.base_head))
221
205
  throw new Error("batch authorization base_head must be a committed 40-hex commit id");
222
- const expires = Date.parse(binding.expires_at);
223
- if (Number.isNaN(expires) || expires <= Date.parse(issuedAt))
224
- throw new Error("batch authorization must have a future expiry");
225
- validateBudget(binding.budget, issuedAt);
206
+ validateBudget(binding.budget);
226
207
  },
227
- validateAndProject(state, expected, now) {
228
- // Fail closed on an unusable clock. `now` reaches here as
229
- // Date.parse(...) from callers, and NaN makes every `<=` compare
230
- // false, which would silently accept an expired authorization.
231
- if (!Number.isFinite(now))
232
- throw new Error("batch authorization requires a valid clock");
233
- const expires = Date.parse(state.expires_at);
234
- if (Number.isNaN(expires) || expires <= now)
235
- throw new BatchAuthorizationExpiryError("batch authorization has expired");
236
- if (Date.parse(state.budget.deadline_at) <= now)
237
- throw new BatchAuthorizationExpiryError("batch authorization deadline has expired");
208
+ validateAndProject(state, expected) {
238
209
  for (const key of Object.keys(expected) as Array<keyof BatchAuthorizationBinding>) {
239
210
  if (key === "budget") {
240
211
  const a = state.budget ?? ({} as BatchBudget);
241
212
  const b = expected.budget ?? ({} as BatchBudget);
242
213
  if (
243
214
  a.max_children !== b.max_children ||
244
- a.deadline_at !== b.deadline_at ||
245
215
  a.qa_failure_limit !== b.qa_failure_limit
246
216
  )
247
217
  throw new Error("batch authorization budget mismatch");
@@ -263,7 +233,6 @@ export function createBatchAuthorityRegistry(): BatchAuthorityRegistry {
263
233
  actor_id: state.actor_id,
264
234
  confirmation_ref: state.confirmation_ref,
265
235
  issued_at: state.issued_at,
266
- expires_at: state.expires_at,
267
236
  nonce: state.nonce,
268
237
  };
269
238
  },
@@ -299,9 +268,9 @@ export function createBatchAuthorityRegistry(): BatchAuthorityRegistry {
299
268
  consumed.set(capability, new Set<string>());
300
269
  return capability;
301
270
  },
302
- inspect(capability, expected, now = Date.now()) {
271
+ inspect(capability, expected) {
303
272
  planOf(capability);
304
- return inner.inspect(capability, expected, now);
273
+ return inner.inspect(capability, expected);
305
274
  },
306
275
  children(capability) {
307
276
  return planOf(capability).map((child) => ({ ...child, blocked_by: [...child.blocked_by] }));
@@ -312,8 +281,8 @@ export function createBatchAuthorityRegistry(): BatchAuthorityRegistry {
312
281
  isChildConsumed(capability, taskId) {
313
282
  return slotsOf(capability).has(taskId);
314
283
  },
315
- consumeChild(capability, expected, taskId, now = Date.now()) {
316
- const validated = this.inspect(capability, expected, now);
284
+ consumeChild(capability, expected, taskId) {
285
+ const validated = this.inspect(capability, expected);
317
286
  const plan = planOf(capability);
318
287
  if (!plan.some((child) => child.task_id === taskId))
319
288
  throw new Error(`batch_child_not_in_plan: ${taskId}`);
@@ -331,6 +300,24 @@ export function createBatchAuthorityRegistry(): BatchAuthorityRegistry {
331
300
  };
332
301
  }
333
302
 
303
+ /**
304
+ * Before a batch has consumed a slot it has created no commit, so its lineage
305
+ * can only stand on the confirmed base_head or on a fast-forward of it: the
306
+ * user's own commit on the batch branch, which the runner adopts. A head that
307
+ * does not descend from base_head is not a lineage.
308
+ */
309
+ export function assertBatchLineageOrigin(root: string, baseHead: string, expectedHead: string): void {
310
+ if (expectedHead === baseHead) return;
311
+ const descends =
312
+ spawnSync("git", ["-C", root, "merge-base", "--is-ancestor", baseHead, expectedHead], {
313
+ stdio: ["ignore", "ignore", "ignore"],
314
+ }).status === 0;
315
+ if (!descends)
316
+ throw new Error(
317
+ `batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${baseHead} or a fast-forward of it, not ${expectedHead}`,
318
+ );
319
+ }
320
+
334
321
  export interface DeriveChildEnrollmentInput {
335
322
  capability: object;
336
323
  binding: BatchAuthorizationBinding;
@@ -355,7 +342,7 @@ export function deriveChildEnrollment(
355
342
  registry: BatchAuthorityRegistry,
356
343
  input: DeriveChildEnrollmentInput,
357
344
  ): DerivedChildEnrollment {
358
- const validated = registry.inspect(input.capability, input.binding, Date.parse(input.now));
345
+ const validated = registry.inspect(input.capability, input.binding);
359
346
  const child = registry
360
347
  .children(input.capability)
361
348
  .find((entry) => entry.task_id === input.task_id);
@@ -385,13 +372,8 @@ export function deriveChildEnrollment(
385
372
  // settled a child there is no commit it could have created, so a
386
373
  // caller-supplied expected_head other than base_head is not a lineage.
387
374
  // Checked last so the intent-divergence reason keeps its precedence.
388
- if (
389
- registry.consumedChildren(input.capability).length === 0 &&
390
- input.expected_head !== validated.base_head
391
- )
392
- throw new Error(
393
- `batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${validated.base_head}, not ${input.expected_head}`,
394
- );
375
+ if (registry.consumedChildren(input.capability).length === 0)
376
+ assertBatchLineageOrigin(root, validated.base_head, input.expected_head);
395
377
 
396
378
  return {
397
379
  child,
@@ -404,7 +386,6 @@ export function deriveChildEnrollment(
404
386
  preparation_digest: preparation.digest,
405
387
  actor_id: validated.actor_id,
406
388
  confirmation_ref: validated.confirmation_ref,
407
- expires_at: validated.expires_at,
408
389
  nonce: `${validated.nonce}:${child.task_id}`,
409
390
  },
410
391
  };
@@ -14,6 +14,7 @@ import {
14
14
  } from "./authority_port";
15
15
  import { applyTaskAction } from "./application";
16
16
  import { asTaskDiffSnapshot } from "./completion";
17
+ import { taskDeliveryIdentity } from "../workspace_scope";
17
18
  import { readTaskIntent } from "./intent";
18
19
  import {
19
20
  inspectIntentTokenPair,
@@ -67,8 +68,9 @@ export interface CanaryExecuteInput {
67
68
  task_id: string;
68
69
  operation: CanaryOperation;
69
70
  prior_intent_token: TaskIntentIdentityToken;
70
- /** Trusted injected diff provider; the application derives the diff identity. */
71
- diffProvider: (root: string, record: TaskRecord) => string | { diff_hash: string; changed_paths?: readonly string[] };
71
+ /** Trusted injected diff provider; the application derives the diff identity.
72
+ * Defaults to the shared delivery identity selector when omitted. */
73
+ diffProvider?: (root: string, record: TaskRecord) => string | { diff_hash: string; changed_paths?: readonly string[] };
72
74
  now?: string;
73
75
  }
74
76
 
@@ -313,7 +315,7 @@ export function createCanaryApplication(
313
315
  record: current.record,
314
316
  };
315
317
  });
316
- const diffHash = asTaskDiffSnapshot(input.diffProvider(input.root, snapshot.record)).diff_hash;
318
+ const diffHash = asTaskDiffSnapshot((input.diffProvider ?? taskDeliveryIdentity)(input.root, snapshot.record)).diff_hash;
317
319
  if (operation.op === "stop" && !("capability" in operation))
318
320
  throw new KernelInvariantError(["stop requires user authority capability"]);
319
321
  const hasBoundSpec = boundSpecPath(snapshot.intent_snapshot) !== undefined;
@@ -431,7 +433,6 @@ export function createCanaryApplication(
431
433
  registry,
432
434
  capability: capability as never,
433
435
  diffProvider: input.diffProvider,
434
- now: Date.parse(now),
435
436
  ...(operation.op === "complete" || operation.op === "stop"
436
437
  ? { terminal: { terminalized_at: now } }
437
438
  : {}),
@@ -492,7 +493,7 @@ export function createCanaryApplication(
492
493
  intent_revision: current.record.intent_snapshot.revision,
493
494
  intent_content_hash: current.record.intent_ref.content_hash,
494
495
  diff_hash: "sha256:" + "0".repeat(64),
495
- }, Date.parse(now));
496
+ });
496
497
  const nextClaim: BackendClaim = {
497
498
  ...claim,
498
499
  lifecycle_status: "draining",
@@ -23,7 +23,6 @@ export interface CanaryWaiver {
23
23
  reason: string;
24
24
  actor: string;
25
25
  confirmation_ref: string;
26
- expires_at: string;
27
26
  nonce: string;
28
27
  }
29
28
 
@@ -4,20 +4,19 @@
4
4
 
5
5
  export interface CapabilityRegistryHooks<TBinding, TExpected, TValidated> {
6
6
  /** Validate the binding at issue time. Throw on invalid input. */
7
- validateBinding(binding: TBinding, issuedAt: string): void;
7
+ validateBinding(binding: TBinding): void;
8
8
  /** Validate stored state against expected input at inspect time. Return the domain-specific validated result. */
9
9
  validateAndProject(
10
10
  state: TBinding & { issued_at: string },
11
11
  expected: TExpected,
12
- now: number,
13
12
  ): TValidated;
14
13
  }
15
14
 
16
15
  export interface CapabilityRegistry<TBinding, TExpected, TValidated, TCapability> {
17
16
  readonly brand: symbol;
18
17
  issue(binding: TBinding, issuedAt?: string): TCapability;
19
- inspect(capability: TCapability, expected: TExpected, now?: number): TValidated;
20
- consume(capability: TCapability, expected: TExpected, now?: number): TValidated;
18
+ inspect(capability: TCapability, expected: TExpected): TValidated;
19
+ consume(capability: TCapability, expected: TExpected): TValidated;
21
20
  isConsumed(capability: TCapability): boolean;
22
21
  }
23
22
 
@@ -53,7 +52,7 @@ export function createCapabilityRegistry<TBinding, TExpected, TValidated, TCapab
53
52
  return {
54
53
  brand,
55
54
  issue(binding: TBinding, issuedAt = new Date().toISOString()): TCapability {
56
- hooks.validateBinding(binding, issuedAt);
55
+ hooks.validateBinding(binding);
57
56
  const capability = Object.freeze(
58
57
  Object.defineProperties(
59
58
  {},
@@ -66,7 +65,7 @@ export function createCapabilityRegistry<TBinding, TExpected, TValidated, TCapab
66
65
  states.set(capability as object, { binding: { ...binding }, issued_at: issuedAt, consumed: false });
67
66
  return capability;
68
67
  },
69
- inspect(capability: TCapability, expected: TExpected, now = Date.now()): TValidated {
68
+ inspect(capability: TCapability, expected: TExpected): TValidated {
70
69
  if (!isCapability(capability))
71
70
  throw new Error(`${domainLabel} capability is not recognized by this registry`);
72
71
  const state = stateOf(capability);
@@ -74,11 +73,10 @@ export function createCapabilityRegistry<TBinding, TExpected, TValidated, TCapab
74
73
  return hooks.validateAndProject(
75
74
  { ...state.binding, issued_at: state.issued_at },
76
75
  expected,
77
- now,
78
76
  );
79
77
  },
80
- consume(capability: TCapability, expected: TExpected, now = Date.now()): TValidated {
81
- const validated = this.inspect(capability, expected, now);
78
+ consume(capability: TCapability, expected: TExpected): TValidated {
79
+ const validated = this.inspect(capability, expected);
82
80
  stateOf(capability).consumed = true;
83
81
  return validated;
84
82
  },