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.
- package/.claude-plugin/marketplace.json +16 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/package.json +9 -2
- package/plugins/immune-brain/.claude-plugin/plugin.json +8 -0
- package/plugins/immune-brain/.mcp.json +8 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +354 -64
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +74 -706
- package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +1 -90
- package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +13 -160
- package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +1 -50
- package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +1 -262
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +3 -2
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +8 -229
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +23 -5
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -0
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +7514 -0
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +58 -0
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +19 -13
- package/plugins/immune-brain/dist/imm-loop.md +6 -7
- package/plugins/immune-brain/dist/imm-planner.md +1 -1
- package/plugins/immune-brain/dist/imm-pr-fix.md +9 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +33 -13
- package/plugins/immune-brain/dist/role-prompts/executor.md +14 -0
- package/plugins/immune-brain/dist/role-prompts/pr-fix.md +9 -0
- package/plugins/immune-brain/dist/role-prompts/test-fixer.md +7 -0
- package/plugins/immune-brain/hooks/hooks.json +55 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +836 -0
- package/plugins/immune-brain/runtime/assurance/enrollment.ts +6 -0
- package/plugins/immune-brain/runtime/assurance/host_port.ts +18 -0
- package/plugins/immune-brain/runtime/assurance/invocations.ts +90 -0
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +50 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +596 -0
- package/plugins/immune-brain/runtime/assurance/verification.ts +233 -0
- package/plugins/immune-brain/runtime/claude/capability.ts +67 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +70 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +789 -0
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +363 -0
- package/plugins/immune-brain/runtime/claude/review_host.ts +645 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +221 -3
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +28 -13
- package/plugins/immune-brain/runtime/kernel/application.ts +8 -5
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +24 -13
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +78 -115
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -4
- package/plugins/immune-brain/runtime/kernel/capability_registry.ts +89 -0
- package/plugins/immune-brain/runtime/kernel/completion.ts +64 -6
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +189 -100
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +37 -80
- package/plugins/immune-brain/runtime/kernel/intent.ts +24 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +31 -0
- package/plugins/immune-brain/runtime/kernel/reducer.ts +36 -13
- package/plugins/immune-brain/runtime/kernel/storage.ts +16 -16
- package/plugins/immune-brain/runtime/kernel/types.ts +32 -2
- package/plugins/immune-brain/runtime/kernel/validation.ts +107 -16
- package/plugins/immune-brain/runtime/loop_contract.ts +17 -2
- package/plugins/immune-brain/runtime/prompts/code-review.md +33 -13
- package/plugins/immune-brain/runtime/prompts/executor.md +14 -0
- package/plugins/immune-brain/runtime/prompts/pr-fix.md +9 -0
- package/plugins/immune-brain/runtime/prompts/test-fixer.md +7 -0
- package/plugins/immune-brain/runtime/v4_runtime.ts +5 -2
- package/plugins/immune-brain/runtime/workspace_scope.ts +191 -5
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +3 -4
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +2 -0
|
@@ -9,14 +9,14 @@ import {
|
|
|
9
9
|
type EnrollmentCapabilityBinding,
|
|
10
10
|
} from "./enrollment_authority";
|
|
11
11
|
import { readTaskTombstone, type BackendClaim } from "./backend_claim";
|
|
12
|
-
import { preparePiCanary } from "./pi_canary_prepare";
|
|
12
|
+
import { preparePiCanary, readGitHead } from "./pi_canary_prepare";
|
|
13
13
|
import {
|
|
14
14
|
commitEnrollmentLocked,
|
|
15
15
|
readTaskRecordRaw,
|
|
16
16
|
readWorkspaceStateRaw,
|
|
17
17
|
withKernelStoreLock,
|
|
18
18
|
} from "./storage";
|
|
19
|
-
import type {
|
|
19
|
+
import type { TaskRecord, TaskRecordV4, WorkspaceStateLike } from "./types";
|
|
20
20
|
|
|
21
21
|
export interface EnrollCanaryInput {
|
|
22
22
|
task_id: string;
|
|
@@ -29,17 +29,121 @@ export interface EnrollCanaryInput {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface EnrollCanaryResult {
|
|
32
|
-
record:
|
|
32
|
+
record: TaskRecord;
|
|
33
33
|
backend_claim: BackendClaim;
|
|
34
34
|
workspace: { revision: string; state: WorkspaceStateLike };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
type EnrollmentPreconditionState = {
|
|
38
|
+
validated: ReturnType<EnrollmentAuthorityRegistry["inspect"]> | null;
|
|
39
|
+
current: ReturnType<typeof readTaskRecordRaw> | null;
|
|
40
|
+
workspace: ReturnType<typeof readWorkspaceStateRaw> | null;
|
|
41
|
+
intent: Awaited<ReturnType<typeof readTaskIntent>> | null;
|
|
42
|
+
gitBaseHead: string | null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type EnrollmentPreconditionResult = EnrollmentPreconditionState & {
|
|
46
|
+
blockers: string[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function runEnrollmentPreconditionChecks<T>(
|
|
50
|
+
root: string,
|
|
51
|
+
input: EnrollCanaryInput,
|
|
52
|
+
capability: object,
|
|
53
|
+
registry: EnrollmentAuthorityRegistry,
|
|
54
|
+
mode: "report" | "fail_fast",
|
|
55
|
+
beforeLock: (validated: EnrollmentPreconditionState["validated"]) => void,
|
|
56
|
+
onReady: (state: EnrollmentPreconditionState) => T,
|
|
57
|
+
): T;
|
|
58
|
+
function runEnrollmentPreconditionChecks(
|
|
59
|
+
root: string,
|
|
60
|
+
input: EnrollCanaryInput,
|
|
61
|
+
capability: object,
|
|
62
|
+
registry: EnrollmentAuthorityRegistry,
|
|
63
|
+
mode: "report" | "fail_fast",
|
|
64
|
+
beforeLock: (validated: EnrollmentPreconditionState["validated"]) => void,
|
|
65
|
+
): EnrollmentPreconditionResult;
|
|
66
|
+
function runEnrollmentPreconditionChecks<T>(
|
|
67
|
+
root: string,
|
|
68
|
+
input: EnrollCanaryInput,
|
|
69
|
+
capability: object,
|
|
70
|
+
registry: EnrollmentAuthorityRegistry,
|
|
71
|
+
mode: "report" | "fail_fast",
|
|
72
|
+
beforeLock: (validated: EnrollmentPreconditionState["validated"]) => void,
|
|
73
|
+
onReady?: (state: EnrollmentPreconditionState) => T,
|
|
74
|
+
): T | EnrollmentPreconditionResult {
|
|
75
|
+
const blockers: string[] = [];
|
|
76
|
+
let validated: EnrollmentPreconditionState["validated"] = null;
|
|
77
|
+
try {
|
|
78
|
+
validated = registry.inspect(capability, input.capability_binding);
|
|
79
|
+
if (mode === "fail_fast" && validated.task_id !== input.task_id)
|
|
80
|
+
throw new Error("enrollment capability task mismatch");
|
|
81
|
+
} catch (error) {
|
|
82
|
+
if (mode === "fail_fast") throw error;
|
|
83
|
+
blockers.push(`capability: ${error instanceof Error ? error.message : String(error)}`);
|
|
84
|
+
}
|
|
85
|
+
beforeLock(validated);
|
|
86
|
+
|
|
87
|
+
return withKernelStoreLock(root, () => {
|
|
88
|
+
let current: EnrollmentPreconditionState["current"] = null;
|
|
89
|
+
let workspace: EnrollmentPreconditionState["workspace"] = null;
|
|
90
|
+
let intent: EnrollmentPreconditionState["intent"] = null;
|
|
91
|
+
let gitBaseHead: string | null = null;
|
|
92
|
+
const fail = (report: string, error: unknown) => {
|
|
93
|
+
if (mode === "fail_fast")
|
|
94
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
95
|
+
blockers.push(report);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const tombstone = readTaskTombstone(root, input.task_id);
|
|
99
|
+
if (tombstone) {
|
|
100
|
+
fail(
|
|
101
|
+
"task tombstone exists; same-task re-enrollment is forbidden",
|
|
102
|
+
new Error(`task ${input.task_id} is terminal; same-task re-enrollment is forbidden`),
|
|
103
|
+
);
|
|
104
|
+
} else {
|
|
105
|
+
current = readTaskRecordRaw(root, input.task_id);
|
|
106
|
+
if (current.record)
|
|
107
|
+
fail(
|
|
108
|
+
"task record already exists",
|
|
109
|
+
new Error(`task ${input.task_id} already has a TaskRecord`),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
workspace = readWorkspaceStateRaw(root);
|
|
113
|
+
if (workspace.state.current_working !== null)
|
|
114
|
+
fail(
|
|
115
|
+
`workspace already owned by ${workspace.state.current_working}`,
|
|
116
|
+
new Error(`workspace is already owned by ${workspace.state.current_working}`),
|
|
117
|
+
);
|
|
118
|
+
try {
|
|
119
|
+
intent = readTaskIntent(root, input.task_id);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
fail(`intent: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
gitBaseHead = readGitHead(root);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
fail(`git base: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const state: EnrollmentPreconditionState = {
|
|
130
|
+
validated,
|
|
131
|
+
current,
|
|
132
|
+
workspace,
|
|
133
|
+
intent,
|
|
134
|
+
gitBaseHead,
|
|
135
|
+
};
|
|
136
|
+
return onReady ? onReady(state) : { ...state, blockers };
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildTaskRecordV4(
|
|
38
141
|
input: EnrollCanaryInput,
|
|
39
142
|
intent: Awaited<ReturnType<typeof readTaskIntent>>,
|
|
40
|
-
|
|
143
|
+
gitBaseHead: string,
|
|
144
|
+
): TaskRecordV4 {
|
|
41
145
|
return {
|
|
42
|
-
contract: "assurance_kernel/task_record/
|
|
146
|
+
contract: "assurance_kernel/task_record/v4",
|
|
43
147
|
task_id: input.task_id,
|
|
44
148
|
intent_snapshot: intent.intent,
|
|
45
149
|
intent_ref: {
|
|
@@ -49,6 +153,7 @@ function buildTaskRecordV3(
|
|
|
49
153
|
lifecycle: "active",
|
|
50
154
|
artifact_state: "active",
|
|
51
155
|
baseline: intent.content_hash,
|
|
156
|
+
git_base_head: gitBaseHead,
|
|
52
157
|
attestations: [],
|
|
53
158
|
findings: [],
|
|
54
159
|
history: [],
|
|
@@ -75,43 +180,25 @@ export function runEnrollmentRehearsal(
|
|
|
75
180
|
generated_at: string;
|
|
76
181
|
};
|
|
77
182
|
} {
|
|
78
|
-
const
|
|
79
|
-
|
|
183
|
+
const preconditions = runEnrollmentPreconditionChecks(
|
|
184
|
+
root,
|
|
185
|
+
input,
|
|
186
|
+
capability,
|
|
187
|
+
registry,
|
|
188
|
+
"report",
|
|
189
|
+
() => undefined,
|
|
190
|
+
);
|
|
191
|
+
return {
|
|
80
192
|
rehearsed: true,
|
|
81
193
|
writes_performed: false,
|
|
82
194
|
evidence: {
|
|
83
|
-
contract: "assurance_kernel/enrollment_rehearsal/v1"
|
|
195
|
+
contract: "assurance_kernel/enrollment_rehearsal/v1",
|
|
84
196
|
task_id: input.task_id,
|
|
85
|
-
outcome:
|
|
86
|
-
blockers,
|
|
197
|
+
outcome: preconditions.blockers.length === 0 ? "ready" : "not_ready",
|
|
198
|
+
blockers: preconditions.blockers,
|
|
87
199
|
generated_at: input.now,
|
|
88
200
|
},
|
|
89
201
|
};
|
|
90
|
-
try {
|
|
91
|
-
// capability inspect (does not consume)
|
|
92
|
-
registry.inspect(capability, input.capability_binding);
|
|
93
|
-
} catch (error) {
|
|
94
|
-
blockers.push(`capability: ${error instanceof Error ? error.message : String(error)}`);
|
|
95
|
-
}
|
|
96
|
-
withKernelStoreLock(root, () => {
|
|
97
|
-
const tombstone = readTaskTombstone(root, input.task_id);
|
|
98
|
-
if (tombstone) {
|
|
99
|
-
blockers.push("task tombstone exists; same-task re-enrollment is forbidden");
|
|
100
|
-
} else {
|
|
101
|
-
const current = readTaskRecordRaw(root, input.task_id);
|
|
102
|
-
if (current.record) blockers.push("task record already exists");
|
|
103
|
-
}
|
|
104
|
-
const workspace = readWorkspaceStateRaw(root);
|
|
105
|
-
if (workspace.state.current_working !== null)
|
|
106
|
-
blockers.push(`workspace already owned by ${workspace.state.current_working}`);
|
|
107
|
-
});
|
|
108
|
-
try {
|
|
109
|
-
readTaskIntent(root, input.task_id);
|
|
110
|
-
} catch (error) {
|
|
111
|
-
blockers.push(`intent: ${error instanceof Error ? error.message : String(error)}`);
|
|
112
|
-
}
|
|
113
|
-
result.evidence.outcome = blockers.length === 0 ? "ready" : "not_ready";
|
|
114
|
-
return result;
|
|
115
202
|
}
|
|
116
203
|
|
|
117
204
|
/**
|
|
@@ -124,71 +211,73 @@ export function enrollCanaryTask(
|
|
|
124
211
|
input: EnrollCanaryInput,
|
|
125
212
|
registry: EnrollmentAuthorityRegistry,
|
|
126
213
|
): EnrollCanaryResult {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
214
|
+
let gitBaseHead: string | null = null;
|
|
215
|
+
return runEnrollmentPreconditionChecks(
|
|
216
|
+
root,
|
|
217
|
+
input,
|
|
218
|
+
input.capability,
|
|
219
|
+
registry,
|
|
220
|
+
"fail_fast",
|
|
221
|
+
() => {
|
|
222
|
+
// v4 storage retirement: the capability is bound to the preparation
|
|
223
|
+
// digest computed from Kernel owners only. Recompute it before the
|
|
224
|
+
// locked owner reads and reject if the owner set changed.
|
|
225
|
+
const recomputed = preparePiCanary(root, { task_id: input.task_id, now: input.now });
|
|
226
|
+
if (recomputed.digest !== input.preparation_digest)
|
|
227
|
+
throw new Error("enrollment preparation digest mismatch");
|
|
228
|
+
gitBaseHead = recomputed.git_base_head;
|
|
229
|
+
if (!gitBaseHead)
|
|
230
|
+
throw new Error(recomputed.git_error ?? "enrollment requires a committed Git HEAD");
|
|
231
|
+
},
|
|
232
|
+
(checks) => {
|
|
233
|
+
if (!checks.validated || !checks.intent || !checks.workspace || !checks.current)
|
|
234
|
+
throw new Error("enrollment precondition state incomplete");
|
|
235
|
+
if (checks.intent.intent.revision !== input.intent_revision)
|
|
236
|
+
throw new Error("intent revision mismatch");
|
|
237
|
+
if (checks.intent.content_hash !== checks.validated.intent_content_hash)
|
|
238
|
+
throw new Error("intent content hash mismatch");
|
|
239
|
+
// The confirmation is bound to this exact commit; a moved HEAD means the
|
|
240
|
+
// operator approved a different base than the one being recorded.
|
|
241
|
+
if (checks.gitBaseHead !== gitBaseHead)
|
|
242
|
+
throw new Error("Git HEAD moved after the enrollment confirmation");
|
|
155
243
|
|
|
156
|
-
|
|
157
|
-
|
|
244
|
+
// consume immediately before the marker write
|
|
245
|
+
registry.consume(input.capability, input.capability_binding);
|
|
158
246
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
task_id: input.task_id,
|
|
168
|
-
intent_revision: input.intent_revision,
|
|
169
|
-
intent_content_hash: intent.content_hash,
|
|
170
|
-
enrollment_event_id: `enroll-${input.task_id}-${input.now}`,
|
|
171
|
-
lifecycle_status: "active",
|
|
172
|
-
created_at: input.now,
|
|
173
|
-
updated_at: input.now,
|
|
174
|
-
};
|
|
175
|
-
const mutation = commitEnrollmentLocked(
|
|
176
|
-
root,
|
|
177
|
-
input.task_id,
|
|
178
|
-
{
|
|
179
|
-
contract: "assurance_kernel/workspace_transaction/v2",
|
|
247
|
+
const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
|
|
248
|
+
const nextWorkspace: WorkspaceStateLike = {
|
|
249
|
+
...checks.workspace.state,
|
|
250
|
+
current_working: input.task_id,
|
|
251
|
+
};
|
|
252
|
+
const claim: BackendClaim = {
|
|
253
|
+
contract: "assurance_kernel/backend_claim/v2",
|
|
254
|
+
backend: "kernel",
|
|
180
255
|
task_id: input.task_id,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
256
|
+
intent_revision: input.intent_revision,
|
|
257
|
+
intent_content_hash: checks.intent.content_hash,
|
|
258
|
+
enrollment_event_id: `enroll-${input.task_id}-${input.now}`,
|
|
259
|
+
lifecycle_status: "active",
|
|
260
|
+
created_at: input.now,
|
|
261
|
+
updated_at: input.now,
|
|
262
|
+
};
|
|
263
|
+
const mutation = commitEnrollmentLocked(
|
|
264
|
+
root,
|
|
265
|
+
input.task_id,
|
|
266
|
+
{
|
|
267
|
+
contract: "assurance_kernel/workspace_transaction/v2",
|
|
268
|
+
task_id: input.task_id,
|
|
269
|
+
expected_record_hash: checks.current.revision,
|
|
270
|
+
next_record_content: `${JSON.stringify(record, null, 2)}\n`,
|
|
271
|
+
expected_workspace_hash: checks.workspace.revision,
|
|
272
|
+
next_workspace_content: `${JSON.stringify(nextWorkspace, null, 2)}\n`,
|
|
273
|
+
},
|
|
274
|
+
claim as unknown as Record<string, unknown>,
|
|
275
|
+
);
|
|
276
|
+
return {
|
|
277
|
+
record: mutation.record,
|
|
278
|
+
backend_claim: claim,
|
|
279
|
+
workspace: { revision: "", state: mutation.workspace },
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
);
|
|
194
283
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// P2B1 enrollment authority. NOT exported from kernel/index.ts.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// The production registry is created only inside the Pi extension activation
|
|
5
|
-
// closure; tests create isolated registries via the fixture seam.
|
|
2
|
+
// Uses the shared capability registry factory; enrollment-specific binding
|
|
3
|
+
// validation and projection remain here.
|
|
6
4
|
|
|
5
|
+
import { createCapabilityRegistry } from "./capability_registry";
|
|
7
6
|
import { MUTATION_AUTHORITY_CAPABILITY_BRAND } from "./types";
|
|
8
7
|
|
|
9
8
|
export const ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollment-capability-brand");
|
|
@@ -20,11 +19,6 @@ export interface EnrollmentCapabilityBinding {
|
|
|
20
19
|
nonce: string;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
|
-
interface EnrollmentCapabilityState extends EnrollmentCapabilityBinding {
|
|
24
|
-
issued_at: string;
|
|
25
|
-
consumed: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
22
|
export interface ValidatedEnrollment {
|
|
29
23
|
task_id: string;
|
|
30
24
|
intent_path: string;
|
|
@@ -48,76 +42,39 @@ export interface EnrollmentAuthorityRegistry {
|
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
export function createEnrollmentAuthorityRegistry(): EnrollmentAuthorityRegistry {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
{
|
|
84
|
-
[ENROLLMENT_CAPABILITY_BRAND]: { value: true, enumerable: false, writable: false, configurable: false },
|
|
85
|
-
[brand]: { value: true, enumerable: false, writable: false, configurable: false },
|
|
86
|
-
},
|
|
87
|
-
),
|
|
88
|
-
);
|
|
89
|
-
states.set(capability, { ...binding, issued_at: issuedAt, consumed: false });
|
|
90
|
-
return capability;
|
|
91
|
-
},
|
|
92
|
-
inspect(capability: object, expected: EnrollmentCapabilityBinding, now = Date.now()): ValidatedEnrollment {
|
|
93
|
-
if (!isEnrollmentCapability(capability)) throw new Error("enrollment capability required");
|
|
94
|
-
const state = stateOf(capability);
|
|
95
|
-
if (state.consumed) throw new Error("enrollment capability already consumed");
|
|
96
|
-
if (Date.parse(state.expires_at) <= now) throw new Error("enrollment capability has expired");
|
|
97
|
-
for (const key of Object.keys(expected) as Array<keyof EnrollmentCapabilityBinding>) {
|
|
98
|
-
if (state[key] !== expected[key])
|
|
99
|
-
throw new Error(`enrollment capability ${key} mismatch`);
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
task_id: state.task_id,
|
|
103
|
-
intent_path: state.intent_path,
|
|
104
|
-
intent_revision: state.intent_revision,
|
|
105
|
-
intent_content_hash: state.intent_content_hash,
|
|
106
|
-
preparation_digest: state.preparation_digest,
|
|
107
|
-
actor_id: state.actor_id,
|
|
108
|
-
confirmation_ref: state.confirmation_ref,
|
|
109
|
-
issued_at: state.issued_at,
|
|
110
|
-
expires_at: state.expires_at,
|
|
111
|
-
nonce: state.nonce,
|
|
112
|
-
};
|
|
113
|
-
},
|
|
114
|
-
consume(capability: object, expected: EnrollmentCapabilityBinding, now = Date.now()): ValidatedEnrollment {
|
|
115
|
-
const validated = this.inspect(capability, expected, now);
|
|
116
|
-
stateOf(capability).consumed = true;
|
|
117
|
-
return validated;
|
|
118
|
-
},
|
|
119
|
-
isConsumed(capability: object): boolean {
|
|
120
|
-
return stateOf(capability).consumed;
|
|
45
|
+
return createCapabilityRegistry<EnrollmentCapabilityBinding, EnrollmentCapabilityBinding, ValidatedEnrollment>(
|
|
46
|
+
ENROLLMENT_CAPABILITY_BRAND,
|
|
47
|
+
{
|
|
48
|
+
validateBinding(binding, issuedAt) {
|
|
49
|
+
const missing: string[] = [];
|
|
50
|
+
for (const [key, value] of Object.entries(binding)) {
|
|
51
|
+
if (value === undefined || value === null || value === "") missing.push(key);
|
|
52
|
+
}
|
|
53
|
+
if (missing.length > 0)
|
|
54
|
+
throw new Error(`enrollment capability binding is incomplete: ${missing.join(", ")}`);
|
|
55
|
+
if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
|
|
56
|
+
throw new Error("enrollment capability must have a future expiry");
|
|
57
|
+
},
|
|
58
|
+
validateAndProject(state, expected, now) {
|
|
59
|
+
if (Date.parse(state.expires_at) <= now) throw new Error("enrollment capability has expired");
|
|
60
|
+
for (const key of Object.keys(expected) as Array<keyof EnrollmentCapabilityBinding>) {
|
|
61
|
+
if (state[key] !== expected[key])
|
|
62
|
+
throw new Error(`enrollment capability ${key} mismatch`);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
task_id: state.task_id,
|
|
66
|
+
intent_path: state.intent_path,
|
|
67
|
+
intent_revision: state.intent_revision,
|
|
68
|
+
intent_content_hash: state.intent_content_hash,
|
|
69
|
+
preparation_digest: state.preparation_digest,
|
|
70
|
+
actor_id: state.actor_id,
|
|
71
|
+
confirmation_ref: state.confirmation_ref,
|
|
72
|
+
issued_at: state.issued_at,
|
|
73
|
+
expires_at: state.expires_at,
|
|
74
|
+
nonce: state.nonce,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
121
77
|
},
|
|
122
|
-
|
|
78
|
+
"enrollment",
|
|
79
|
+
);
|
|
123
80
|
}
|
|
@@ -46,6 +46,16 @@ export const RISK_FLOOR_SCOPE_PREFIXES = [
|
|
|
46
46
|
"plugins/immune-brain/.pi-extension",
|
|
47
47
|
] as const;
|
|
48
48
|
|
|
49
|
+
// Concrete changed paths use a narrower, segment-safe prefix policy. Keep this
|
|
50
|
+
// separate from the glob-aware scope_hint policy above: the two inputs have
|
|
51
|
+
// different contracts and docs-only scope remains routine today.
|
|
52
|
+
export const CHANGED_PATH_RISK_FLOOR_PREFIXES = [
|
|
53
|
+
"plugins/immune-brain/runtime/kernel",
|
|
54
|
+
"plugins/immune-brain/.pi-extension",
|
|
55
|
+
"docs/specs",
|
|
56
|
+
"docs/plans",
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
49
59
|
function segmentMatches(e: string, s: string): boolean {
|
|
50
60
|
if (e === "*") return true;
|
|
51
61
|
if (!e.includes("*") && !e.includes("?")) return e === s;
|
|
@@ -330,6 +340,20 @@ export function canonicalIntentHash(intent: unknown): string {
|
|
|
330
340
|
|
|
331
341
|
const RISK_RANK: Record<TaskRisk, number> = { routine: 0, material: 1, critical: 2 };
|
|
332
342
|
|
|
343
|
+
export function classifyTaskRisk(
|
|
344
|
+
changedPaths: readonly string[],
|
|
345
|
+
declaredRisk: TaskRisk,
|
|
346
|
+
): TaskRisk {
|
|
347
|
+
const pathFloor = changedPaths.some((path) =>
|
|
348
|
+
CHANGED_PATH_RISK_FLOOR_PREFIXES.some(
|
|
349
|
+
(prefix) => path === prefix || path.startsWith(`${prefix}/`),
|
|
350
|
+
),
|
|
351
|
+
)
|
|
352
|
+
? "material"
|
|
353
|
+
: "routine";
|
|
354
|
+
return RISK_RANK[declaredRisk] >= RISK_RANK[pathFloor] ? declaredRisk : pathFloor;
|
|
355
|
+
}
|
|
356
|
+
|
|
333
357
|
function contentHashWithoutRevision(intent: TaskIntentV1): string {
|
|
334
358
|
const { revision: _revision, ...rest } = intent;
|
|
335
359
|
return canonicalIntentHash(rest);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// enrollment call, or writer. Direct import cannot authorize or mutate.
|
|
5
5
|
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
7
8
|
import { resolve } from "node:path";
|
|
8
9
|
import { readBackendClaim, readTaskTombstone } from "./backend_claim";
|
|
9
10
|
import { readTaskIntent } from "./intent";
|
|
@@ -13,6 +14,21 @@ import {
|
|
|
13
14
|
} from "./storage";
|
|
14
15
|
|
|
15
16
|
const SOURCE_PATH = ".imm/state/workspace.json";
|
|
17
|
+
const GIT_OBJECT_ID = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The Enrollment base commit. v4 TaskRecords bind this exact commit so QA,
|
|
21
|
+
* Review, authorization, and completion can all derive one scoped revision.
|
|
22
|
+
*/
|
|
23
|
+
export function readGitHead(root: string): string {
|
|
24
|
+
const result = spawnSync("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
});
|
|
27
|
+
const head = typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
28
|
+
if (result.status !== 0 || !GIT_OBJECT_ID.test(head))
|
|
29
|
+
throw new Error("enrollment requires a committed Git HEAD in the project root");
|
|
30
|
+
return head.toLowerCase();
|
|
31
|
+
}
|
|
16
32
|
|
|
17
33
|
export interface PiCanaryPrepareInput {
|
|
18
34
|
task_id: string;
|
|
@@ -43,6 +59,9 @@ export interface PiCanaryPreparation {
|
|
|
43
59
|
lifecycle: string | null;
|
|
44
60
|
artifact_state: string | null;
|
|
45
61
|
} | null;
|
|
62
|
+
/** Commit the confirmation is bound to; becomes TaskRecord v4 `git_base_head`. */
|
|
63
|
+
git_base_head: string | null;
|
|
64
|
+
git_error: string | null;
|
|
46
65
|
workspace: {
|
|
47
66
|
current_working: string | null;
|
|
48
67
|
};
|
|
@@ -139,6 +158,16 @@ export function preparePiCanary(root: string, input: PiCanaryPrepareInput): PiCa
|
|
|
139
158
|
`workspace owner ${state.state.current_working} contradicts backend claim task ${claim.task_id}`,
|
|
140
159
|
);
|
|
141
160
|
|
|
161
|
+
// A missing HEAD is a readiness blocker, not a crash: the rehearsal and the
|
|
162
|
+
// native gate must be able to report it while still binding the digest.
|
|
163
|
+
let gitBaseHead: string | null = null;
|
|
164
|
+
let gitError: string | null = null;
|
|
165
|
+
try {
|
|
166
|
+
gitBaseHead = readGitHead(canonicalRoot);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
gitError = error instanceof Error ? error.message : String(error);
|
|
169
|
+
}
|
|
170
|
+
|
|
142
171
|
const preparation: PiCanaryPreparation = {
|
|
143
172
|
contract: "assurance_kernel/pi_canary_preparation/v1",
|
|
144
173
|
task_id: input.task_id,
|
|
@@ -148,6 +177,8 @@ export function preparePiCanary(root: string, input: PiCanaryPrepareInput): PiCa
|
|
|
148
177
|
backend_claim: backend,
|
|
149
178
|
task_tombstone: taskTombstone,
|
|
150
179
|
task_record_v3: record,
|
|
180
|
+
git_base_head: gitBaseHead,
|
|
181
|
+
git_error: gitError,
|
|
151
182
|
workspace,
|
|
152
183
|
digest: "",
|
|
153
184
|
};
|