immune-brain 2.8.3 → 3.0.2
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 +2 -2
- 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
|
@@ -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 {
|
|
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,
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
171
|
+
record: TaskRecord,
|
|
121
172
|
currentDiffHash: string,
|
|
122
173
|
currentIntentContentHash: string,
|
|
174
|
+
changedPaths: readonly string[] = [],
|
|
123
175
|
): TaskProjectionV3 {
|
|
124
|
-
const decision = completionDecision(
|
|
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 ||
|
|
@@ -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
|
}
|