immune-brain 2.8.2 → 3.0.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 (66) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/package.json +9 -2
  5. package/plugins/immune-brain/.claude-plugin/plugin.json +8 -0
  6. package/plugins/immune-brain/.mcp.json +8 -0
  7. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +354 -64
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +74 -706
  9. package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +1 -90
  10. package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +13 -160
  11. package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +1 -50
  12. package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +1 -262
  13. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +3 -2
  14. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +8 -229
  15. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +23 -5
  16. package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -0
  17. package/plugins/immune-brain/dist/claude/mcp-server.mjs +7514 -0
  18. package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +58 -0
  19. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +1 -1
  20. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  21. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +19 -13
  22. package/plugins/immune-brain/dist/imm-loop.md +6 -7
  23. package/plugins/immune-brain/dist/imm-planner.md +1 -1
  24. package/plugins/immune-brain/dist/imm-pr-fix.md +9 -0
  25. package/plugins/immune-brain/dist/role-prompts/code-review.md +33 -13
  26. package/plugins/immune-brain/dist/role-prompts/executor.md +14 -0
  27. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +9 -0
  28. package/plugins/immune-brain/dist/role-prompts/test-fixer.md +7 -0
  29. package/plugins/immune-brain/hooks/hooks.json +55 -0
  30. package/plugins/immune-brain/runtime/assurance/coordinator.ts +836 -0
  31. package/plugins/immune-brain/runtime/assurance/enrollment.ts +6 -0
  32. package/plugins/immune-brain/runtime/assurance/host_port.ts +18 -0
  33. package/plugins/immune-brain/runtime/assurance/invocations.ts +90 -0
  34. package/plugins/immune-brain/runtime/assurance/qa_findings.ts +50 -0
  35. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +596 -0
  36. package/plugins/immune-brain/runtime/assurance/verification.ts +233 -0
  37. package/plugins/immune-brain/runtime/claude/capability.ts +67 -0
  38. package/plugins/immune-brain/runtime/claude/interaction.ts +70 -0
  39. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +789 -0
  40. package/plugins/immune-brain/runtime/claude/mcp_server.ts +363 -0
  41. package/plugins/immune-brain/runtime/claude/review_host.ts +645 -0
  42. package/plugins/immune-brain/runtime/commands/kernel.ts +221 -3
  43. package/plugins/immune-brain/runtime/github_issue_tracker.ts +28 -13
  44. package/plugins/immune-brain/runtime/kernel/application.ts +8 -5
  45. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +24 -13
  46. package/plugins/immune-brain/runtime/kernel/authority_port.ts +78 -115
  47. package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -4
  48. package/plugins/immune-brain/runtime/kernel/capability_registry.ts +89 -0
  49. package/plugins/immune-brain/runtime/kernel/completion.ts +64 -6
  50. package/plugins/immune-brain/runtime/kernel/enrollment.ts +189 -100
  51. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +37 -80
  52. package/plugins/immune-brain/runtime/kernel/intent.ts +24 -0
  53. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +31 -0
  54. package/plugins/immune-brain/runtime/kernel/reducer.ts +36 -13
  55. package/plugins/immune-brain/runtime/kernel/storage.ts +16 -16
  56. package/plugins/immune-brain/runtime/kernel/types.ts +32 -2
  57. package/plugins/immune-brain/runtime/kernel/validation.ts +107 -16
  58. package/plugins/immune-brain/runtime/loop_contract.ts +17 -2
  59. package/plugins/immune-brain/runtime/prompts/code-review.md +33 -13
  60. package/plugins/immune-brain/runtime/prompts/executor.md +14 -0
  61. package/plugins/immune-brain/runtime/prompts/pr-fix.md +9 -0
  62. package/plugins/immune-brain/runtime/prompts/test-fixer.md +7 -0
  63. package/plugins/immune-brain/runtime/v4_runtime.ts +5 -2
  64. package/plugins/immune-brain/runtime/workspace_scope.ts +191 -5
  65. package/plugins/immune-brain/skills/imm-loop/SKILL.md +3 -4
  66. package/plugins/immune-brain/skills/imm-planner/SKILL.md +2 -0
@@ -1,10 +1,7 @@
1
1
  // P2B2 mutation authority consumption port. NOT exported from kernel/index.ts.
2
- // Registry/application pairing: each registry owns its capability state in a
3
- // closure-private WeakMap, and capabilities are only recognized by the
4
- // registry that issued them. No module-level singleton and no test issuer
5
- // exist here; the production registry/application pair is created inside the
6
- // Pi lifecycle extension activation closure, and tests issue capabilities
7
- // through tests/fixtures/mutation-authority-test-seam.ts.
2
+ // Uses the shared capability registry factory; mutation-specific binding
3
+ // validation, action digest, findings digest enforcement, and projection
4
+ // remain here.
8
5
 
9
6
  import { createHash } from "node:crypto";
10
7
 
@@ -15,6 +12,7 @@ import {
15
12
  type MutationAuthorityKind,
16
13
  type TaskAction,
17
14
  } from "./types";
15
+ import { createCapabilityRegistry } from "./capability_registry";
18
16
 
19
17
  export interface CapabilityBindingV2 {
20
18
  authority_kind: MutationAuthorityKind;
@@ -31,11 +29,6 @@ export interface CapabilityBindingV2 {
31
29
  findings_digest: string | null;
32
30
  }
33
31
 
34
- interface CapabilityState extends CapabilityBindingV2 {
35
- issued_at: string;
36
- consumed: boolean;
37
- }
38
-
39
32
  export interface ValidatedAuthorityV2 {
40
33
  audit: AuthorityAuditDescriptor;
41
34
  action_digest: string;
@@ -84,125 +77,95 @@ export function digestOfAction(action: TaskAction): string {
84
77
  }
85
78
 
86
79
  export function createMutationAuthorityRegistry(): MutationAuthorityRegistry {
87
- const states = new WeakMap<object, CapabilityState>();
88
- const brand = Symbol("assurance-kernel-mutation-authority-registry");
89
-
90
- function isCapability(value: unknown): value is MutationAuthorityCapabilityV2 {
91
- return (
92
- !!value &&
93
- typeof value === "object" &&
94
- (value as Record<symbol, unknown>)[MUTATION_AUTHORITY_CAPABILITY_BRAND] === true &&
95
- (value as Record<symbol, unknown>)[brand] === true
96
- );
97
- }
98
-
99
- function stateOf(capability: MutationAuthorityCapabilityV2): CapabilityState {
100
- const state = states.get(capability);
101
- if (!state) throw new Error("authority capability is not recognized by this registry");
102
- return state;
103
- }
104
-
105
- function validateBinding(binding: CapabilityBindingV2, issuedAt: string): void {
106
- const missing: string[] = [];
107
- for (const [key, value] of Object.entries(binding)) {
108
- if (key === "findings_digest") continue;
109
- if (value === undefined || value === null || value === "") missing.push(key);
110
- }
111
- if (missing.length > 0)
112
- throw new Error(`authority capability binding is incomplete: ${missing.join(", ")}`);
113
- if (binding.findings_digest !== null && !/^sha256:[a-f0-9]{64}$/.test(binding.findings_digest))
114
- throw new Error("authority capability findings_digest must be a canonical sha256 hash");
115
- if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
116
- throw new Error("authority capability must have a future expiry");
117
- }
118
-
119
- return {
120
- brand,
121
- issue(binding: CapabilityBindingV2, issuedAt = new Date().toISOString()): MutationAuthorityCapabilityV2 {
122
- validateBinding(binding, issuedAt);
123
- const capability = Object.freeze(
124
- Object.defineProperties(
125
- {},
126
- {
127
- [MUTATION_AUTHORITY_CAPABILITY_BRAND]: {
128
- value: true,
129
- enumerable: false,
130
- writable: false,
131
- configurable: false,
132
- },
133
- [brand]: {
134
- value: true,
135
- enumerable: false,
136
- writable: false,
137
- configurable: false,
138
- },
80
+ const inner = createCapabilityRegistry<CapabilityBindingV2, MutationAuthorityInspection, ValidatedAuthorityV2, MutationAuthorityCapabilityV2>(
81
+ MUTATION_AUTHORITY_CAPABILITY_BRAND,
82
+ {
83
+ validateBinding(binding, issuedAt) {
84
+ const missing: string[] = [];
85
+ for (const [key, value] of Object.entries(binding)) {
86
+ if (key === "findings_digest") continue;
87
+ if (value === undefined || value === null || value === "") missing.push(key);
88
+ }
89
+ if (missing.length > 0)
90
+ throw new Error(`authority capability binding is incomplete: ${missing.join(", ")}`);
91
+ if (binding.findings_digest !== null && !/^sha256:[a-f0-9]{64}$/.test(binding.findings_digest))
92
+ throw new Error("authority capability findings_digest must be a canonical sha256 hash");
93
+ if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
94
+ throw new Error("authority capability must have a future expiry");
95
+ },
96
+ validateAndProject(state, expected, now) {
97
+ if (Date.parse(state.expires_at) <= now)
98
+ throw new Error("authority capability has expired");
99
+ const actionDigest = digestOfAction(expected.action);
100
+ if (state.action_digest !== actionDigest)
101
+ throw new Error("authority capability action digest mismatch");
102
+ if (state.task_id !== expected.task_id)
103
+ throw new Error("authority capability task mismatch");
104
+ if (state.expected_record_hash !== expected.expected_record_hash)
105
+ throw new Error("authority capability record hash mismatch");
106
+ if (state.intent_revision !== expected.intent_revision)
107
+ throw new Error("authority capability intent revision mismatch");
108
+ if (state.intent_content_hash !== expected.intent_content_hash)
109
+ throw new Error("authority capability intent hash mismatch");
110
+ if (state.diff_hash !== expected.diff_hash)
111
+ throw new Error("authority capability diff hash mismatch");
112
+ if (expected.findings_digest !== undefined) {
113
+ if (state.findings_digest === null)
114
+ throw new Error("authority capability is not bound to findings");
115
+ if (state.findings_digest !== expected.findings_digest)
116
+ throw new Error("authority capability findings digest mismatch");
117
+ }
118
+ return {
119
+ audit: {
120
+ authority_kind: state.authority_kind,
121
+ actor_id: state.actor_id,
122
+ confirmation_ref: state.confirmation_ref,
123
+ issued_at: state.issued_at,
124
+ expires_at: state.expires_at,
139
125
  },
140
- ),
141
- ) as MutationAuthorityCapabilityV2;
142
- states.set(capability, {
143
- ...binding,
144
- issued_at: issuedAt,
145
- consumed: false,
146
- });
147
- return capability;
126
+ action_digest: actionDigest,
127
+ expected_record_hash: state.expected_record_hash,
128
+ intent_revision: state.intent_revision,
129
+ intent_content_hash: state.intent_content_hash,
130
+ diff_hash: state.diff_hash,
131
+ task_id: state.task_id,
132
+ };
133
+ },
148
134
  },
135
+ "authority",
136
+ );
137
+
138
+ return {
139
+ brand: inner.brand,
140
+ issue: inner.issue.bind(inner),
149
141
  inspect(
150
142
  capability: MutationAuthorityCapabilityV2 | undefined,
151
143
  expected: MutationAuthorityInspection,
152
144
  now = Date.now(),
153
145
  ): ValidatedAuthorityV2 {
154
- if (!capability || !isCapability(capability))
146
+ if (!capability)
155
147
  throw new Error("privileged action requires an opaque authority capability");
156
- const state = stateOf(capability);
157
- if (state.consumed) throw new Error("authority capability is already consumed");
158
- if (Date.parse(state.expires_at) <= now)
159
- throw new Error("authority capability has expired");
160
- const actionDigest = digestOfAction(expected.action);
161
- if (state.action_digest !== actionDigest)
162
- throw new Error("authority capability action digest mismatch");
163
- if (state.task_id !== expected.task_id)
164
- throw new Error("authority capability task mismatch");
165
- if (state.expected_record_hash !== expected.expected_record_hash)
166
- throw new Error("authority capability record hash mismatch");
167
- if (state.intent_revision !== expected.intent_revision)
168
- throw new Error("authority capability intent revision mismatch");
169
- if (state.intent_content_hash !== expected.intent_content_hash)
170
- throw new Error("authority capability intent hash mismatch");
171
- if (state.diff_hash !== expected.diff_hash)
172
- throw new Error("authority capability diff hash mismatch");
173
- if (expected.findings_digest !== undefined) {
174
- if (state.findings_digest === null)
175
- throw new Error("authority capability is not bound to findings");
176
- if (state.findings_digest !== expected.findings_digest)
177
- throw new Error("authority capability findings digest mismatch");
148
+ try {
149
+ return inner.inspect(capability, expected, now);
150
+ } catch (err) {
151
+ if (err instanceof Error && err.message.includes("not recognized by this registry"))
152
+ throw new Error("privileged action requires an opaque authority capability");
153
+ throw err;
178
154
  }
179
- return {
180
- audit: {
181
- authority_kind: state.authority_kind,
182
- actor_id: state.actor_id,
183
- confirmation_ref: state.confirmation_ref,
184
- issued_at: state.issued_at,
185
- expires_at: state.expires_at,
186
- },
187
- action_digest: actionDigest,
188
- expected_record_hash: state.expected_record_hash,
189
- intent_revision: state.intent_revision,
190
- intent_content_hash: state.intent_content_hash,
191
- diff_hash: state.diff_hash,
192
- task_id: state.task_id,
193
- };
194
155
  },
195
156
  consume(
196
157
  capability: MutationAuthorityCapabilityV2,
197
158
  expected: MutationAuthorityInspection,
198
159
  now = Date.now(),
199
160
  ): ValidatedAuthorityV2 {
200
- const validated = this.inspect(capability, expected, now);
201
- stateOf(capability).consumed = true;
202
- return validated;
203
- },
204
- isConsumed(capability: MutationAuthorityCapabilityV2): boolean {
205
- return stateOf(capability).consumed;
161
+ try {
162
+ return inner.consume(capability, expected, now);
163
+ } catch (err) {
164
+ if (err instanceof Error && err.message.includes("not recognized by this registry"))
165
+ throw new Error("privileged action requires an opaque authority capability");
166
+ throw err;
167
+ }
206
168
  },
169
+ isConsumed: inner.isConsumed.bind(inner),
207
170
  };
208
171
  }
@@ -12,6 +12,7 @@ import {
12
12
  type MutationAuthorityRegistry,
13
13
  } from "./authority_port";
14
14
  import { applyTaskAction } from "./application";
15
+ import { asTaskDiffSnapshot } from "./completion";
15
16
  import { readTaskIntent } from "./intent";
16
17
  import {
17
18
  inspectIntentTokenPair,
@@ -23,7 +24,7 @@ import {
23
24
  type BackendClaim,
24
25
  } from "./backend_claim";
25
26
  import { canonicalIntentHash } from "./intent";
26
- import { parseTaskRecordV3 } from "./validation";
27
+ import { parseTaskRecord } from "./validation";
27
28
  import type { TaskIntentIdentityToken } from "./intent_token_registry";
28
29
  import {
29
30
  commitDrainLocked,
@@ -41,6 +42,7 @@ import type {
41
42
  TaskApprovalV2,
42
43
  TaskFinding,
43
44
  TaskIntentV1,
45
+ TaskRecord,
44
46
  } from "./types";
45
47
 
46
48
  export type CanaryOperation =
@@ -62,7 +64,7 @@ export interface CanaryExecuteInput {
62
64
  operation: CanaryOperation;
63
65
  prior_intent_token: TaskIntentIdentityToken;
64
66
  /** Trusted injected diff provider; the application derives the diff identity. */
65
- diffProvider: (root: string, intent: TaskIntentV1) => string;
67
+ diffProvider: (root: string, record: TaskRecord) => string | { diff_hash: string; changed_paths?: readonly string[] };
66
68
  now?: string;
67
69
  }
68
70
 
@@ -264,7 +266,7 @@ export function createCanaryApplication(
264
266
  consumeIntentToken(input.prior_intent_token);
265
267
  consumeIntentToken(fresh.token);
266
268
  const at = input.now ?? new Date().toISOString();
267
- const nextRecord = parseTaskRecordV3({
269
+ const nextRecord = parseTaskRecord({
268
270
  ...current.record,
269
271
  intent_ref: { ...current.record.intent_ref, path: transition.next_intent_path },
270
272
  artifact_state: transition.next_artifact_state,
@@ -318,7 +320,7 @@ export function createCanaryApplication(
318
320
  record: current.record,
319
321
  };
320
322
  });
321
- const diffHash = input.diffProvider(input.root, snapshot.intent_snapshot);
323
+ const diffHash = asTaskDiffSnapshot(input.diffProvider(input.root, snapshot.record)).diff_hash;
322
324
  if (operation.op === "stop" && !("capability" in operation))
323
325
  throw new KernelInvariantError(["stop requires user authority capability"]);
324
326
  const hasBoundSpec = snapshot.intent_snapshot.scope_hint.some(
@@ -0,0 +1,89 @@
1
+ // Shared opaque capability registry primitive. NOT exported from kernel/index.ts.
2
+ // Each registry owns its WeakMap; capabilities are only recognized by the
3
+ // registry that issued them. No module-level singleton.
4
+
5
+ export interface CapabilityRegistryHooks<TBinding, TExpected, TValidated> {
6
+ /** Validate the binding at issue time. Throw on invalid input. */
7
+ validateBinding(binding: TBinding, issuedAt: string): void;
8
+ /** Validate stored state against expected input at inspect time. Return the domain-specific validated result. */
9
+ validateAndProject(
10
+ state: TBinding & { issued_at: string },
11
+ expected: TExpected,
12
+ now: number,
13
+ ): TValidated;
14
+ }
15
+
16
+ export interface CapabilityRegistry<TBinding, TExpected, TValidated, TCapability> {
17
+ readonly brand: symbol;
18
+ issue(binding: TBinding, issuedAt?: string): TCapability;
19
+ inspect(capability: TCapability, expected: TExpected, now?: number): TValidated;
20
+ consume(capability: TCapability, expected: TExpected, now?: number): TValidated;
21
+ isConsumed(capability: TCapability): boolean;
22
+ }
23
+
24
+ interface StoredState<TBinding> {
25
+ binding: TBinding;
26
+ issued_at: string;
27
+ consumed: boolean;
28
+ }
29
+
30
+ export function createCapabilityRegistry<TBinding, TExpected, TValidated, TCapability = object>(
31
+ capabilityBrand: symbol,
32
+ hooks: CapabilityRegistryHooks<TBinding, TExpected, TValidated>,
33
+ domainLabel: string,
34
+ ): CapabilityRegistry<TBinding, TExpected, TValidated, TCapability> {
35
+ const states = new WeakMap<object, StoredState<TBinding>>();
36
+ const brand = Symbol(`${domainLabel}-registry`);
37
+
38
+ function isCapability(value: unknown): value is TCapability {
39
+ return (
40
+ !!value &&
41
+ typeof value === "object" &&
42
+ (value as Record<symbol, unknown>)[capabilityBrand] === true &&
43
+ (value as Record<symbol, unknown>)[brand] === true
44
+ );
45
+ }
46
+
47
+ function stateOf(capability: TCapability): StoredState<TBinding> {
48
+ const state = states.get(capability as object);
49
+ if (!state) throw new Error(`${domainLabel} capability is not recognized by this registry`);
50
+ return state;
51
+ }
52
+
53
+ return {
54
+ brand,
55
+ issue(binding: TBinding, issuedAt = new Date().toISOString()): TCapability {
56
+ hooks.validateBinding(binding, issuedAt);
57
+ const capability = Object.freeze(
58
+ Object.defineProperties(
59
+ {},
60
+ {
61
+ [capabilityBrand]: { value: true, enumerable: false, writable: false, configurable: false },
62
+ [brand]: { value: true, enumerable: false, writable: false, configurable: false },
63
+ },
64
+ ),
65
+ ) as TCapability;
66
+ states.set(capability as object, { binding: { ...binding }, issued_at: issuedAt, consumed: false });
67
+ return capability;
68
+ },
69
+ inspect(capability: TCapability, expected: TExpected, now = Date.now()): TValidated {
70
+ if (!isCapability(capability))
71
+ throw new Error(`${domainLabel} capability is not recognized by this registry`);
72
+ const state = stateOf(capability);
73
+ if (state.consumed) throw new Error(`${domainLabel} capability already consumed`);
74
+ return hooks.validateAndProject(
75
+ { ...state.binding, issued_at: state.issued_at },
76
+ expected,
77
+ now,
78
+ );
79
+ },
80
+ consume(capability: TCapability, expected: TExpected, now = Date.now()): TValidated {
81
+ const validated = this.inspect(capability, expected, now);
82
+ stateOf(capability).consumed = true;
83
+ return validated;
84
+ },
85
+ isConsumed(capability: TCapability): boolean {
86
+ return stateOf(capability).consumed;
87
+ },
88
+ };
89
+ }
@@ -1,12 +1,13 @@
1
+ import { classifyTaskRisk } from "./intent";
1
2
  import type {
2
3
  ApprovalKind,
3
4
  CompletionDecision,
4
5
  TaskAttestationV3,
5
6
  TaskIntentV1,
6
7
  TaskProjectionV3,
7
- TaskRecordV3,
8
+ TaskRecord,
8
9
  } from "./types";
9
- import { assertKernelInvariantsV3 } from "./validation";
10
+ import { assertKernelInvariantsV3, KernelInvariantError } from "./validation";
10
11
 
11
12
  const REQUIRED_ATTESTATIONS: Record<TaskIntentV1["risk"], ApprovalKind[]> = {
12
13
  routine: ["qa"],
@@ -14,6 +15,55 @@ const REQUIRED_ATTESTATIONS: Record<TaskIntentV1["risk"], ApprovalKind[]> = {
14
15
  critical: ["qa", "review", "user"],
15
16
  };
16
17
 
18
+ function archiveActivePlanningPath(path: string): string | null {
19
+ const matched = path.match(/^docs\/(plans|specs)\/([^/]+)$/);
20
+ return matched ? `docs/${matched[1]}/archive/${matched[2]}` : null;
21
+ }
22
+
23
+ function ownSidecarPaths(intent: TaskIntentV1): Set<string> {
24
+ const activeIntent = `docs/plans/${intent.task_id}.intent.json`;
25
+ const archivedIntent = archiveActivePlanningPath(activeIntent);
26
+ const excluded = new Set<string>([activeIntent]);
27
+ if (archivedIntent) excluded.add(archivedIntent);
28
+ const specs = intent.scope_hint.filter((path) => {
29
+ const archived = archiveActivePlanningPath(path);
30
+ return archived !== null && /^docs\/specs\/[^/]+\.spec\.md$/.test(path) && intent.scope_hint.includes(archived);
31
+ });
32
+ if (specs.length > 1) {
33
+ throw new KernelInvariantError([
34
+ `artifact transition requires at most one scope-bound Spec; found ${specs.length}`,
35
+ ]);
36
+ }
37
+ const spec = specs[0];
38
+ if (spec) {
39
+ excluded.add(spec);
40
+ const archived = archiveActivePlanningPath(spec);
41
+ if (archived) excluded.add(archived);
42
+ }
43
+ return excluded;
44
+ }
45
+
46
+ export type TaskDiffInput = string | { diff_hash: string; changed_paths?: readonly string[] };
47
+
48
+ export function asTaskDiffSnapshot(value: TaskDiffInput): {
49
+ diff_hash: string;
50
+ changed_paths?: readonly string[];
51
+ } {
52
+ if (typeof value === "string") return { diff_hash: value };
53
+ return { diff_hash: value.diff_hash, changed_paths: value.changed_paths };
54
+ }
55
+
56
+ export function resolveProjectedRisk(
57
+ intent: TaskIntentV1,
58
+ changedPaths: readonly string[] = [],
59
+ ): TaskIntentV1["risk"] {
60
+ const excluded = ownSidecarPaths(intent);
61
+ return classifyTaskRisk(
62
+ changedPaths.filter((path) => !excluded.has(path)),
63
+ intent.risk,
64
+ );
65
+ }
66
+
17
67
  function hasDistinctAuthorityAssignment(
18
68
  required: ApprovalKind[],
19
69
  candidates: TaskAttestationV3[],
@@ -32,9 +82,10 @@ function hasDistinctAuthorityAssignment(
32
82
 
33
83
  export function completionDecision(
34
84
  intent: TaskIntentV1,
35
- record: TaskRecordV3,
85
+ record: TaskRecord,
36
86
  currentDiffHash: string,
37
87
  currentIntentContentHash: string,
88
+ changedPaths: readonly string[] = [],
38
89
  ): CompletionDecision {
39
90
  assertKernelInvariantsV3(intent, record);
40
91
  const freshAttestations = record.attestations.filter(
@@ -63,7 +114,7 @@ export function completionDecision(
63
114
  )
64
115
  .map((item) => item.id);
65
116
 
66
- const requiredKinds = REQUIRED_ATTESTATIONS[intent.risk];
117
+ const requiredKinds = REQUIRED_ATTESTATIONS[resolveProjectedRisk(intent, changedPaths)];
67
118
  const candidates = freshAttestations.filter((item) => requiredKinds.includes(item.kind));
68
119
  const missingApprovalKinds = requiredKinds.filter(
69
120
  (kind) => !candidates.some((attestation) => attestation.kind === kind),
@@ -117,11 +168,18 @@ export function completionDecision(
117
168
 
118
169
  export function projectTask(
119
170
  intent: TaskIntentV1,
120
- record: TaskRecordV3,
171
+ record: TaskRecord,
121
172
  currentDiffHash: string,
122
173
  currentIntentContentHash: string,
174
+ changedPaths: readonly string[] = [],
123
175
  ): TaskProjectionV3 {
124
- const decision = completionDecision(intent, record, currentDiffHash, currentIntentContentHash);
176
+ const decision = completionDecision(
177
+ intent,
178
+ record,
179
+ currentDiffHash,
180
+ currentIntentContentHash,
181
+ changedPaths,
182
+ );
125
183
  const blocked =
126
184
  decision.blocking_finding_ids.length > 0 ||
127
185
  decision.unresolved_user_decision_ids.length > 0 ||