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
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
// P2B2 mutation authority consumption port. NOT exported from kernel/index.ts.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
146
|
+
if (!capability)
|
|
155
147
|
throw new Error("privileged action requires an opaque authority capability");
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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 {
|
|
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 ||
|