immune-brain 2.8.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/README.md +239 -0
- package/README.zh-CN.md +239 -0
- package/package.json +84 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +666 -0
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +1622 -0
- package/plugins/immune-brain/.pi-extension/package.json +11 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +711 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance.ts +154 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +349 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +90 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +214 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +50 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +262 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +28 -0
- package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +237 -0
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +414 -0
- package/plugins/immune-brain/.pi-extension/tsconfig.json +20 -0
- package/plugins/immune-brain/bin/imm-kernel +4 -0
- package/plugins/immune-brain/bin/imm-plan +4 -0
- package/plugins/immune-brain/bin/imm-pr-diag +230 -0
- package/plugins/immune-brain/bin/imm-retire-stale-wrapper +4 -0
- package/plugins/immune-brain/bin/imm-retired +4 -0
- package/plugins/immune-brain/bin/imm-tracker +4 -0
- package/plugins/immune-brain/dist/BASELINE.md +138 -0
- package/plugins/immune-brain/dist/docs/reference/HANDOFF-template.md +122 -0
- package/plugins/immune-brain/dist/docs/reference/design-contract-audit-rubric.md +149 -0
- package/plugins/immune-brain/dist/docs/reference/design-contract-review-checklist.md +55 -0
- package/plugins/immune-brain/dist/docs/reference/i18n-review-checklist.md +110 -0
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +52 -0
- package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +94 -0
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +44 -0
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +105 -0
- package/plugins/immune-brain/dist/docs/reference/ux-heuristic-checklist.md +131 -0
- package/plugins/immune-brain/dist/imm-brainstorm.md +140 -0
- package/plugins/immune-brain/dist/imm-doc-prune.md +137 -0
- package/plugins/immune-brain/dist/imm-loop.md +158 -0
- package/plugins/immune-brain/dist/imm-planner.md +387 -0
- package/plugins/immune-brain/dist/imm-pr-fix.md +71 -0
- package/plugins/immune-brain/dist/registry.yaml +49 -0
- package/plugins/immune-brain/dist/role-prompts/advisory-reviewer.md +16 -0
- package/plugins/immune-brain/dist/role-prompts/arch-explorer.md +14 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +15 -0
- package/plugins/immune-brain/dist/role-prompts/compounder.md +20 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +13 -0
- package/plugins/immune-brain/dist/role-prompts/pr-fix.md +81 -0
- package/plugins/immune-brain/dist/role-prompts/qa.md +23 -0
- package/plugins/immune-brain/dist/role-prompts/test-fixer.md +3 -0
- package/plugins/immune-brain/dist/role-prompts/ui-review.md +14 -0
- package/plugins/immune-brain/runtime/authority_commit_receipts.ts +716 -0
- package/plugins/immune-brain/runtime/canonical_json.ts +19 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +1160 -0
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +1009 -0
- package/plugins/immune-brain/runtime/kernel/application.ts +300 -0
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +284 -0
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +208 -0
- package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +451 -0
- package/plugins/immune-brain/runtime/kernel/backend_claim.ts +197 -0
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +507 -0
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +73 -0
- package/plugins/immune-brain/runtime/kernel/completion.ts +160 -0
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +194 -0
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +123 -0
- package/plugins/immune-brain/runtime/kernel/index.ts +29 -0
- package/plugins/immune-brain/runtime/kernel/intent.ts +563 -0
- package/plugins/immune-brain/runtime/kernel/intent_token_registry.ts +80 -0
- package/plugins/immune-brain/runtime/kernel/legacy.ts +299 -0
- package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +153 -0
- package/plugins/immune-brain/runtime/kernel/observation.ts +395 -0
- package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +169 -0
- package/plugins/immune-brain/runtime/kernel/readiness.ts +282 -0
- package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +132 -0
- package/plugins/immune-brain/runtime/kernel/reducer.ts +624 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +1780 -0
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +791 -0
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +492 -0
- package/plugins/immune-brain/runtime/kernel/types.ts +295 -0
- package/plugins/immune-brain/runtime/kernel/validation.ts +963 -0
- package/plugins/immune-brain/runtime/loop_contract.ts +362 -0
- package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +462 -0
- package/plugins/immune-brain/runtime/plan_core.ts +1053 -0
- package/plugins/immune-brain/runtime/prompts/advisory-reviewer.md +16 -0
- package/plugins/immune-brain/runtime/prompts/arch-explorer.md +14 -0
- package/plugins/immune-brain/runtime/prompts/code-review.md +15 -0
- package/plugins/immune-brain/runtime/prompts/compounder.md +20 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +13 -0
- package/plugins/immune-brain/runtime/prompts/pr-fix.md +81 -0
- package/plugins/immune-brain/runtime/prompts/qa.md +23 -0
- package/plugins/immune-brain/runtime/prompts/test-fixer.md +3 -0
- package/plugins/immune-brain/runtime/prompts/ui-review.md +14 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +160 -0
- package/plugins/immune-brain/runtime/v4_runtime.ts +295 -0
- package/plugins/immune-brain/runtime/verification_descriptor.ts +162 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +623 -0
- package/plugins/immune-brain/skills/.ignore +1 -0
- package/plugins/immune-brain/skills/BASELINE.md +138 -0
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +66 -0
- package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +11 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +52 -0
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +221 -0
- package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +10 -0
- package/plugins/immune-brain/skills/registry.yaml +49 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
// Pure TaskRecord v3 reducer with a closed factual action vocabulary.
|
|
2
|
+
// Never reads files, Git, workspace, or host context. Returns a branded
|
|
3
|
+
// ReducedTaskMutation; the caller cannot construct or serialize it.
|
|
4
|
+
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { completionDecision } from "./completion";
|
|
7
|
+
import {
|
|
8
|
+
REDUCED_MUTATION_BRAND,
|
|
9
|
+
type AuthorityAuditDescriptor,
|
|
10
|
+
type ReducedTaskMutation,
|
|
11
|
+
type TaskAction,
|
|
12
|
+
type TaskFinding,
|
|
13
|
+
type TaskIntentRefV3,
|
|
14
|
+
type TaskIntentV1,
|
|
15
|
+
type TaskRecordV3,
|
|
16
|
+
} from "./types";
|
|
17
|
+
import {
|
|
18
|
+
KernelInvariantError,
|
|
19
|
+
assertKernelInvariantsV3,
|
|
20
|
+
assertTaskRecordUpdateV3,
|
|
21
|
+
parseTaskAction,
|
|
22
|
+
parseTaskRecordV3,
|
|
23
|
+
} from "./validation";
|
|
24
|
+
import { classifyIntentRevision, canonicalIntentHash } from "./intent";
|
|
25
|
+
|
|
26
|
+
const RISK_RANK: Record<TaskIntentV1["risk"], number> = {
|
|
27
|
+
routine: 0,
|
|
28
|
+
material: 1,
|
|
29
|
+
critical: 2,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function transitionLifecycle(record: TaskRecordV3, to: "done" | "stopped"): void {
|
|
33
|
+
if (record.lifecycle !== "active")
|
|
34
|
+
throw new KernelInvariantError([
|
|
35
|
+
`illegal lifecycle transition: ${record.lifecycle} -> ${to}`,
|
|
36
|
+
]);
|
|
37
|
+
record.lifecycle = to;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function stateOf(record: TaskRecordV3): string {
|
|
41
|
+
return `${record.lifecycle}:${record.artifact_state}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stableJson(value: unknown): string {
|
|
45
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
46
|
+
if (value && typeof value === "object") {
|
|
47
|
+
const record = value as Record<string, unknown>;
|
|
48
|
+
return `{${Object.keys(record)
|
|
49
|
+
.sort()
|
|
50
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
|
|
51
|
+
.join(",")}}`;
|
|
52
|
+
}
|
|
53
|
+
const primitive = JSON.stringify(value);
|
|
54
|
+
return primitive === undefined ? "null" : primitive;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function canonicalRecordHash(record: TaskRecordV3): string {
|
|
58
|
+
// Must equal the storage CAS revision: the exact serialized file bytes.
|
|
59
|
+
return `sha256:${createHash("sha256")
|
|
60
|
+
.update(`${JSON.stringify(record, null, 2)}\n`)
|
|
61
|
+
.digest("hex")}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function actionFingerprint(
|
|
65
|
+
action: TaskAction,
|
|
66
|
+
intentRevision: number,
|
|
67
|
+
intentContentHash: string,
|
|
68
|
+
audit: AuthorityAuditDescriptor | null,
|
|
69
|
+
): string {
|
|
70
|
+
// The fingerprint binds the semantic payload, not the caller-supplied CAS
|
|
71
|
+
// expectations, so an exact replay retried against an advanced record
|
|
72
|
+
// still matches its committed history entry while any payload change
|
|
73
|
+
// (including the diff identity) is detected as a conflict.
|
|
74
|
+
const { expected_record_hash: _r, expected_workspace_hash: _w, diff_hash: _d, ...payload } = action;
|
|
75
|
+
const base = audit
|
|
76
|
+
? { action: payload, intentRevision, intentContentHash, audit }
|
|
77
|
+
: { action: payload, intentRevision, intentContentHash };
|
|
78
|
+
return createHash("sha256").update(stableJson(base)).digest("hex");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function historyReason(
|
|
82
|
+
action: TaskAction,
|
|
83
|
+
intentRevision: number,
|
|
84
|
+
intentContentHash: string,
|
|
85
|
+
audit: AuthorityAuditDescriptor | null,
|
|
86
|
+
detail: string | null,
|
|
87
|
+
): string {
|
|
88
|
+
const fingerprint = `action_v2_sha256:${actionFingerprint(
|
|
89
|
+
action,
|
|
90
|
+
intentRevision,
|
|
91
|
+
intentContentHash,
|
|
92
|
+
audit,
|
|
93
|
+
)}`;
|
|
94
|
+
return detail ? `${detail}\n${fingerprint}` : fingerprint;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function recordedActionFingerprint(reason: string | null): string | null {
|
|
98
|
+
const matched = reason?.match(/(?:^|\n)action_v2_sha256:([a-f0-9]{64})$/);
|
|
99
|
+
return matched?.[1] ?? null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function copyRecord(record: TaskRecordV3): TaskRecordV3 {
|
|
103
|
+
return {
|
|
104
|
+
...record,
|
|
105
|
+
intent_snapshot: { ...record.intent_snapshot },
|
|
106
|
+
intent_ref: { ...record.intent_ref },
|
|
107
|
+
attestations: record.attestations.map((item) => ({
|
|
108
|
+
...item,
|
|
109
|
+
acceptance_results: item.acceptance_results.map((result) => ({ ...result })),
|
|
110
|
+
})),
|
|
111
|
+
findings: record.findings.map((item) => ({ ...item })),
|
|
112
|
+
history: record.history.map((item) => ({ ...item })),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function reviewRound(record: TaskRecordV3): number {
|
|
117
|
+
return (
|
|
118
|
+
Math.max(
|
|
119
|
+
0,
|
|
120
|
+
...record.findings
|
|
121
|
+
.filter((item) => item.source === "review")
|
|
122
|
+
.map((item) => item.review_round ?? 0),
|
|
123
|
+
) + 1
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function appendHistory(
|
|
128
|
+
record: TaskRecordV3,
|
|
129
|
+
action: TaskAction,
|
|
130
|
+
from: string,
|
|
131
|
+
detail: string | null,
|
|
132
|
+
audit: AuthorityAuditDescriptor | null,
|
|
133
|
+
): void {
|
|
134
|
+
if (record.history.some((entry) => entry.id === action.event_id))
|
|
135
|
+
throw new KernelInvariantError([
|
|
136
|
+
`history contains duplicate id ${action.event_id}`,
|
|
137
|
+
]);
|
|
138
|
+
const entry: {
|
|
139
|
+
id: string;
|
|
140
|
+
at: string;
|
|
141
|
+
type: string;
|
|
142
|
+
from_state: string;
|
|
143
|
+
to_state: string;
|
|
144
|
+
reason: string;
|
|
145
|
+
authority?: AuthorityAuditDescriptor;
|
|
146
|
+
} = {
|
|
147
|
+
id: action.event_id,
|
|
148
|
+
at: action.at,
|
|
149
|
+
type: action.type,
|
|
150
|
+
from_state: from,
|
|
151
|
+
to_state: stateOf(record),
|
|
152
|
+
reason: historyReason(
|
|
153
|
+
action,
|
|
154
|
+
record.intent_snapshot.revision,
|
|
155
|
+
record.intent_ref.content_hash,
|
|
156
|
+
audit,
|
|
157
|
+
detail,
|
|
158
|
+
),
|
|
159
|
+
};
|
|
160
|
+
if (audit) entry.authority = { ...audit };
|
|
161
|
+
record.history.push(entry as (typeof record.history)[number]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sha256Hex(value: string): string {
|
|
165
|
+
return createHash("sha256").update(value).digest("hex");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function intentRefMatches(intent: TaskIntentV1, ref: TaskIntentRefV3): boolean {
|
|
169
|
+
return (
|
|
170
|
+
ref.path === `docs/plans/${intent.task_id}.intent.json` &&
|
|
171
|
+
ref.content_hash === canonicalIntentHash(intent)
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function hasPrivilegedKind(action: TaskAction): boolean {
|
|
176
|
+
return (
|
|
177
|
+
action.type === "record_approval" ||
|
|
178
|
+
action.type === "record_user_approval" ||
|
|
179
|
+
action.type === "approve_breaking_intent_revision" ||
|
|
180
|
+
action.type === "request_rework" ||
|
|
181
|
+
action.type === "stop" ||
|
|
182
|
+
action.type === "resolve_user_decision"
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Normalized findings digest for a request_rework capability binding. Derived
|
|
188
|
+
* from the same projection the capability issuer must bind (id, kind,
|
|
189
|
+
* acceptance_id, summary), so a capability cannot be bound to one findings
|
|
190
|
+
* set and applied to another.
|
|
191
|
+
*/
|
|
192
|
+
export function findingsDigestV2(findings: TaskFinding[]): string {
|
|
193
|
+
const normalized = findings.map((finding) => ({
|
|
194
|
+
id: finding.id,
|
|
195
|
+
kind: finding.kind,
|
|
196
|
+
acceptance_id: finding.acceptance_id,
|
|
197
|
+
summary: finding.summary,
|
|
198
|
+
}));
|
|
199
|
+
return `sha256:${createHash("sha256").update(stableJson(normalized)).digest("hex")}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function reduceTask(
|
|
203
|
+
recordRaw: TaskRecordV3,
|
|
204
|
+
actionRaw: TaskAction,
|
|
205
|
+
authorityAudit: AuthorityAuditDescriptor | null = null,
|
|
206
|
+
): ReducedTaskMutation {
|
|
207
|
+
const previous = parseTaskRecordV3(recordRaw);
|
|
208
|
+
assertKernelInvariantsV3(previous.intent_snapshot, previous);
|
|
209
|
+
const action = parseTaskAction(actionRaw);
|
|
210
|
+
const record = copyRecord(previous);
|
|
211
|
+
const from = stateOf(record);
|
|
212
|
+
|
|
213
|
+
if (!action.event_id.trim())
|
|
214
|
+
throw new KernelInvariantError(["event_id must be a non-empty string"]);
|
|
215
|
+
if (!action.at.trim())
|
|
216
|
+
throw new KernelInvariantError(["event timestamp must be a non-empty string"]);
|
|
217
|
+
|
|
218
|
+
const privileged = hasPrivilegedKind(action);
|
|
219
|
+
if (privileged && !authorityAudit)
|
|
220
|
+
throw new KernelInvariantError([
|
|
221
|
+
`${action.type} requires an authority audit descriptor`,
|
|
222
|
+
]);
|
|
223
|
+
if (!privileged && authorityAudit)
|
|
224
|
+
throw new KernelInvariantError([
|
|
225
|
+
`${action.type} does not accept an authority audit descriptor`,
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
const recordHash = canonicalRecordHash(previous);
|
|
229
|
+
const intentRevision = previous.intent_snapshot.revision;
|
|
230
|
+
const intentContentHash = previous.intent_ref.content_hash;
|
|
231
|
+
|
|
232
|
+
// Exact committed replay or conflicting event reuse. Replay is detected
|
|
233
|
+
// against the action-expected identity so an identical retry returns the
|
|
234
|
+
// committed snapshot even after the record has advanced.
|
|
235
|
+
const existingEvent = previous.history.find((entry) => entry.id === action.event_id);
|
|
236
|
+
if (existingEvent) {
|
|
237
|
+
if (
|
|
238
|
+
existingEvent.type === action.type &&
|
|
239
|
+
existingEvent.at === action.at &&
|
|
240
|
+
recordedActionFingerprint(existingEvent.reason) ===
|
|
241
|
+
actionFingerprint(action, intentRevision, intentContentHash, authorityAudit)
|
|
242
|
+
)
|
|
243
|
+
return brandResult(previous, null);
|
|
244
|
+
throw new KernelInvariantError([
|
|
245
|
+
`event_id ${action.event_id} conflicts with a recorded action`,
|
|
246
|
+
]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// New events must carry the exact current record identity.
|
|
250
|
+
if (action.expected_record_hash !== recordHash)
|
|
251
|
+
throw new KernelInvariantError([
|
|
252
|
+
`expected record hash mismatch: ${action.expected_record_hash} != ${recordHash}`,
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
const diffHash = action.diff_hash;
|
|
256
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(diffHash))
|
|
257
|
+
throw new KernelInvariantError(["diff_hash must be a canonical sha256 hash"]);
|
|
258
|
+
|
|
259
|
+
switch (action.type) {
|
|
260
|
+
case "record_finding": {
|
|
261
|
+
if (record.lifecycle !== "active")
|
|
262
|
+
throw new KernelInvariantError([
|
|
263
|
+
`cannot record findings while lifecycle is ${record.lifecycle}`,
|
|
264
|
+
]);
|
|
265
|
+
const finding = action.finding;
|
|
266
|
+
if (record.findings.some((item) => item.id === finding.id))
|
|
267
|
+
throw new KernelInvariantError([
|
|
268
|
+
`findings contains duplicate id ${finding.id}`,
|
|
269
|
+
]);
|
|
270
|
+
if (finding.kind === "replan_required")
|
|
271
|
+
throw new KernelInvariantError([
|
|
272
|
+
"record_finding cannot create a replan boundary; use request_rework",
|
|
273
|
+
]);
|
|
274
|
+
if (
|
|
275
|
+
finding.kind === "unresolved_user_decision" &&
|
|
276
|
+
!/^user-decision-[A-Za-z0-9._-]+$/.test(finding.id)
|
|
277
|
+
)
|
|
278
|
+
throw new KernelInvariantError([
|
|
279
|
+
"unresolved_user_decision findings require a canonical user-decision- id",
|
|
280
|
+
]);
|
|
281
|
+
record.findings.push({
|
|
282
|
+
...finding,
|
|
283
|
+
status: "open",
|
|
284
|
+
source: finding.source === "execution" || finding.source === "review" || finding.source === "kernel" ? finding.source : "execution",
|
|
285
|
+
review_round: null,
|
|
286
|
+
});
|
|
287
|
+
appendHistory(record, action, from, finding.id, authorityAudit);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
case "resolve_finding": {
|
|
291
|
+
if (record.lifecycle !== "active")
|
|
292
|
+
throw new KernelInvariantError([
|
|
293
|
+
`cannot resolve findings while lifecycle is ${record.lifecycle}`,
|
|
294
|
+
]);
|
|
295
|
+
const finding = record.findings.find(
|
|
296
|
+
(item) => item.id === action.finding_id,
|
|
297
|
+
);
|
|
298
|
+
if (!finding)
|
|
299
|
+
throw new KernelInvariantError([
|
|
300
|
+
`finding ${action.finding_id} does not exist`,
|
|
301
|
+
]);
|
|
302
|
+
if (finding.kind === "unresolved_user_decision" || finding.kind === "replan_required")
|
|
303
|
+
throw new KernelInvariantError([
|
|
304
|
+
"generic resolve_finding cannot resolve a user decision or replan boundary",
|
|
305
|
+
]);
|
|
306
|
+
if (finding.status === "resolved")
|
|
307
|
+
throw new KernelInvariantError([
|
|
308
|
+
`finding ${action.finding_id} is already resolved`,
|
|
309
|
+
]);
|
|
310
|
+
finding.status = "resolved";
|
|
311
|
+
appendHistory(record, action, from, action.finding_id, authorityAudit);
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case "record_approval": {
|
|
315
|
+
if (record.lifecycle !== "active" || record.artifact_state !== "frozen")
|
|
316
|
+
throw new KernelInvariantError([
|
|
317
|
+
`cannot record approval while state is ${stateOf(record)}`,
|
|
318
|
+
]);
|
|
319
|
+
const approval = action.approval;
|
|
320
|
+
if (approval.kind !== "review" && approval.kind !== "qa")
|
|
321
|
+
throw new KernelInvariantError([
|
|
322
|
+
"record_approval accepts only review or qa approvals",
|
|
323
|
+
]);
|
|
324
|
+
if (!authorityAudit)
|
|
325
|
+
throw new KernelInvariantError([
|
|
326
|
+
"record_approval requires a consumed authority capability",
|
|
327
|
+
]);
|
|
328
|
+
const expectedRole =
|
|
329
|
+
authorityAudit.authority_kind === "review"
|
|
330
|
+
? "reviewer"
|
|
331
|
+
: authorityAudit.authority_kind;
|
|
332
|
+
if (approval.authority_role !== expectedRole)
|
|
333
|
+
throw new KernelInvariantError([
|
|
334
|
+
"approval authority_role must match the consumed capability kind",
|
|
335
|
+
]);
|
|
336
|
+
if (approval.task_revision !== record.intent_snapshot.revision)
|
|
337
|
+
throw new KernelInvariantError(["approval task_revision must equal the current intent revision"]);
|
|
338
|
+
if (approval.intent_content_hash !== record.intent_ref.content_hash)
|
|
339
|
+
throw new KernelInvariantError(["approval intent_content_hash must equal the current intent hash"]);
|
|
340
|
+
if (approval.diff_hash !== diffHash)
|
|
341
|
+
throw new KernelInvariantError(["approval diff_hash must equal the action diff hash"]);
|
|
342
|
+
if (record.attestations.some((item) => item.id === approval.id))
|
|
343
|
+
throw new KernelInvariantError([
|
|
344
|
+
`attestations contains duplicate id ${approval.id}`,
|
|
345
|
+
]);
|
|
346
|
+
record.attestations.push({
|
|
347
|
+
...approval,
|
|
348
|
+
acceptance_results: approval.kind === "qa"
|
|
349
|
+
? record.intent_snapshot.acceptance.map((item) => ({
|
|
350
|
+
acceptance_id: item.id,
|
|
351
|
+
status: "passed" as const,
|
|
352
|
+
summary: `host-attested QA: ${approval.summary}`,
|
|
353
|
+
}))
|
|
354
|
+
: [],
|
|
355
|
+
});
|
|
356
|
+
appendHistory(record, action, from, approval.id, authorityAudit);
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case "record_user_approval": {
|
|
360
|
+
if (record.lifecycle !== "active" || record.artifact_state !== "frozen")
|
|
361
|
+
throw new KernelInvariantError([
|
|
362
|
+
`cannot record user approval while state is ${stateOf(record)}`,
|
|
363
|
+
]);
|
|
364
|
+
const approval = action.approval;
|
|
365
|
+
if (approval.kind !== "user")
|
|
366
|
+
throw new KernelInvariantError([
|
|
367
|
+
"record_user_approval requires kind user",
|
|
368
|
+
]);
|
|
369
|
+
if (!authorityAudit || authorityAudit.authority_kind !== "user")
|
|
370
|
+
throw new KernelInvariantError([
|
|
371
|
+
"record_user_approval requires user authority",
|
|
372
|
+
]);
|
|
373
|
+
if (approval.task_revision !== record.intent_snapshot.revision)
|
|
374
|
+
throw new KernelInvariantError(["approval task_revision must equal the current intent revision"]);
|
|
375
|
+
if (approval.intent_content_hash !== record.intent_ref.content_hash)
|
|
376
|
+
throw new KernelInvariantError(["approval intent_content_hash must equal the current intent hash"]);
|
|
377
|
+
if (approval.diff_hash !== diffHash)
|
|
378
|
+
throw new KernelInvariantError(["approval diff_hash must equal the action diff hash"]);
|
|
379
|
+
if (record.attestations.some((item) => item.id === approval.id))
|
|
380
|
+
throw new KernelInvariantError([
|
|
381
|
+
`attestations contains duplicate id ${approval.id}`,
|
|
382
|
+
]);
|
|
383
|
+
record.attestations.push({ ...approval, acceptance_results: [] });
|
|
384
|
+
appendHistory(record, action, from, approval.id, authorityAudit);
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case "revise_intent":
|
|
388
|
+
case "approve_breaking_intent_revision": {
|
|
389
|
+
if (record.lifecycle !== "active")
|
|
390
|
+
throw new KernelInvariantError([
|
|
391
|
+
`cannot revise intent while lifecycle is ${record.lifecycle}`,
|
|
392
|
+
]);
|
|
393
|
+
const revisionClass = classifyIntentRevision(
|
|
394
|
+
record.intent_snapshot,
|
|
395
|
+
action.next_intent,
|
|
396
|
+
);
|
|
397
|
+
if (action.type === "revise_intent" && revisionClass !== "compatible")
|
|
398
|
+
throw new KernelInvariantError([
|
|
399
|
+
"revise_intent requires a compatible revision",
|
|
400
|
+
]);
|
|
401
|
+
if (action.type === "approve_breaking_intent_revision" && revisionClass !== "breaking")
|
|
402
|
+
throw new KernelInvariantError([
|
|
403
|
+
"approve_breaking_intent_revision requires a breaking revision",
|
|
404
|
+
]);
|
|
405
|
+
if (!authorityAudit && action.type === "approve_breaking_intent_revision")
|
|
406
|
+
throw new KernelInvariantError([
|
|
407
|
+
"breaking intent revision requires user authority",
|
|
408
|
+
]);
|
|
409
|
+
if (action.next_intent.task_id !== record.task_id ||
|
|
410
|
+
action.next_intent.task_id !== record.intent_snapshot.task_id)
|
|
411
|
+
throw new KernelInvariantError([
|
|
412
|
+
"intent revision cannot change task identity",
|
|
413
|
+
]);
|
|
414
|
+
if (
|
|
415
|
+
action.next_intent.goal !== record.intent_snapshot.goal ||
|
|
416
|
+
action.next_intent.owner !== record.intent_snapshot.owner
|
|
417
|
+
)
|
|
418
|
+
throw new KernelInvariantError([
|
|
419
|
+
"intent revision cannot change goal or owner",
|
|
420
|
+
]);
|
|
421
|
+
if (RISK_RANK[action.next_intent.risk] < RISK_RANK[record.intent_snapshot.risk])
|
|
422
|
+
throw new KernelInvariantError([
|
|
423
|
+
"intent revision cannot reduce risk",
|
|
424
|
+
]);
|
|
425
|
+
if (!intentRefMatches(action.next_intent, action.next_intent_ref))
|
|
426
|
+
throw new KernelInvariantError([
|
|
427
|
+
"next intent ref must match the next intent",
|
|
428
|
+
]);
|
|
429
|
+
record.intent_snapshot = { ...action.next_intent };
|
|
430
|
+
record.intent_ref = { ...action.next_intent_ref };
|
|
431
|
+
if (action.type === "approve_breaking_intent_revision") {
|
|
432
|
+
for (const finding of record.findings) {
|
|
433
|
+
if (finding.kind === "replan_required" && finding.status === "open")
|
|
434
|
+
finding.status = "resolved";
|
|
435
|
+
}
|
|
436
|
+
if (record.artifact_state === "frozen") record.artifact_state = "active";
|
|
437
|
+
}
|
|
438
|
+
appendHistory(record, action, from, `intent_revision_${record.intent_snapshot.revision}`, authorityAudit);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
case "request_rework": {
|
|
442
|
+
if (record.lifecycle !== "active" || record.artifact_state !== "frozen")
|
|
443
|
+
throw new KernelInvariantError([
|
|
444
|
+
`cannot request rework while state is ${stateOf(record)}`,
|
|
445
|
+
]);
|
|
446
|
+
if (action.findings.length === 0)
|
|
447
|
+
throw new KernelInvariantError([
|
|
448
|
+
"request_rework requires at least one finding",
|
|
449
|
+
]);
|
|
450
|
+
if (!authorityAudit)
|
|
451
|
+
throw new KernelInvariantError([
|
|
452
|
+
"request_rework requires an authority audit descriptor",
|
|
453
|
+
]);
|
|
454
|
+
if (
|
|
455
|
+
authorityAudit.authority_kind !== "review" &&
|
|
456
|
+
authorityAudit.authority_kind !== "qa" &&
|
|
457
|
+
authorityAudit.authority_kind !== "user"
|
|
458
|
+
)
|
|
459
|
+
throw new KernelInvariantError([
|
|
460
|
+
"request_rework requires review, qa, or user authority",
|
|
461
|
+
]);
|
|
462
|
+
const round = reviewRound(record);
|
|
463
|
+
const reviewAuthorityReworks = record.history.filter(
|
|
464
|
+
(entry) =>
|
|
465
|
+
entry.type === "request_rework" &&
|
|
466
|
+
entry.authority?.authority_kind === "review",
|
|
467
|
+
).length;
|
|
468
|
+
const parkForReplan =
|
|
469
|
+
authorityAudit.authority_kind === "review" && reviewAuthorityReworks >= 1;
|
|
470
|
+
if (!parkForReplan) {
|
|
471
|
+
record.artifact_state = "active";
|
|
472
|
+
record.intent_ref.path = `docs/plans/${record.task_id}.intent.json`;
|
|
473
|
+
}
|
|
474
|
+
const findingIds = new Set(record.findings.map((item) => item.id));
|
|
475
|
+
for (const finding of action.findings) {
|
|
476
|
+
if (findingIds.has(finding.id))
|
|
477
|
+
throw new KernelInvariantError([
|
|
478
|
+
`findings contains duplicate id ${finding.id}`,
|
|
479
|
+
]);
|
|
480
|
+
findingIds.add(finding.id);
|
|
481
|
+
record.findings.push({
|
|
482
|
+
...finding,
|
|
483
|
+
status: "open",
|
|
484
|
+
source: "review",
|
|
485
|
+
review_round: round,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (
|
|
489
|
+
parkForReplan &&
|
|
490
|
+
!record.findings.some(
|
|
491
|
+
(item) => item.status === "open" && item.kind === "replan_required",
|
|
492
|
+
)
|
|
493
|
+
) {
|
|
494
|
+
const disputed =
|
|
495
|
+
action.findings.find((item: TaskFinding) => item.kind === "blocking") ??
|
|
496
|
+
action.findings[0];
|
|
497
|
+
const boundary = {
|
|
498
|
+
id: `${action.event_id}:replan-required`,
|
|
499
|
+
kind: "replan_required" as const,
|
|
500
|
+
status: "open" as const,
|
|
501
|
+
acceptance_id: disputed.acceptance_id,
|
|
502
|
+
source: "kernel" as const,
|
|
503
|
+
review_round: round,
|
|
504
|
+
summary:
|
|
505
|
+
"Review returned this acceptance boundary twice; a durable replan is required.",
|
|
506
|
+
};
|
|
507
|
+
if (findingIds.has(boundary.id))
|
|
508
|
+
throw new KernelInvariantError([
|
|
509
|
+
`findings contains duplicate id ${boundary.id}`,
|
|
510
|
+
]);
|
|
511
|
+
record.findings.push(boundary);
|
|
512
|
+
}
|
|
513
|
+
appendHistory(record, action, from, `review_round_${round}`, authorityAudit);
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
case "complete": {
|
|
517
|
+
if (record.lifecycle !== "active" || record.artifact_state !== "frozen")
|
|
518
|
+
throw new KernelInvariantError([
|
|
519
|
+
`cannot complete while state is ${stateOf(record)}`,
|
|
520
|
+
]);
|
|
521
|
+
const decision = completionDecision(
|
|
522
|
+
record.intent_snapshot,
|
|
523
|
+
record,
|
|
524
|
+
diffHash,
|
|
525
|
+
record.intent_ref.content_hash,
|
|
526
|
+
);
|
|
527
|
+
if (!decision.complete)
|
|
528
|
+
throw new KernelInvariantError([
|
|
529
|
+
"task is not eligible for completion",
|
|
530
|
+
]);
|
|
531
|
+
transitionLifecycle(record, "done");
|
|
532
|
+
appendHistory(record, action, from, null, authorityAudit);
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
case "stop": {
|
|
536
|
+
if (!action.reason.trim())
|
|
537
|
+
throw new KernelInvariantError(["stop requires a reason"]);
|
|
538
|
+
transitionLifecycle(record, "stopped");
|
|
539
|
+
record.artifact_state = "frozen";
|
|
540
|
+
record.intent_ref.path = `docs/plans/archive/${record.task_id}.intent.json`;
|
|
541
|
+
appendHistory(record, action, from, action.reason, authorityAudit);
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
case "resolve_user_decision": {
|
|
545
|
+
if (record.lifecycle !== "active")
|
|
546
|
+
throw new KernelInvariantError([
|
|
547
|
+
`cannot resolve user decisions while lifecycle is ${record.lifecycle}`,
|
|
548
|
+
]);
|
|
549
|
+
if (!action.resolution.trim())
|
|
550
|
+
throw new KernelInvariantError([
|
|
551
|
+
"resolve_user_decision requires a resolution",
|
|
552
|
+
]);
|
|
553
|
+
const finding = record.findings.find(
|
|
554
|
+
(item) => item.id === action.finding_id,
|
|
555
|
+
);
|
|
556
|
+
if (!finding)
|
|
557
|
+
throw new KernelInvariantError([
|
|
558
|
+
`finding ${action.finding_id} does not exist`,
|
|
559
|
+
]);
|
|
560
|
+
if (finding.kind !== "unresolved_user_decision")
|
|
561
|
+
throw new KernelInvariantError([
|
|
562
|
+
`finding ${action.finding_id} is not a user decision`,
|
|
563
|
+
]);
|
|
564
|
+
if (finding.status === "resolved")
|
|
565
|
+
throw new KernelInvariantError([
|
|
566
|
+
`finding ${action.finding_id} is already resolved`,
|
|
567
|
+
]);
|
|
568
|
+
finding.status = "resolved";
|
|
569
|
+
appendHistory(
|
|
570
|
+
record,
|
|
571
|
+
action,
|
|
572
|
+
from,
|
|
573
|
+
`${action.finding_id}: ${action.resolution}`,
|
|
574
|
+
authorityAudit,
|
|
575
|
+
);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
default: {
|
|
579
|
+
const unreachable: never = action as never;
|
|
580
|
+
throw new KernelInvariantError([
|
|
581
|
+
`unsupported task action: ${String((unreachable as TaskAction).type)}`,
|
|
582
|
+
]);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
assertTaskRecordUpdateV3(previous, record, action);
|
|
587
|
+
return brandResult(record, nextWorkingFor(record));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function nextWorkingFor(record: TaskRecordV3): string | null {
|
|
591
|
+
return record.lifecycle === "active" ? record.task_id : null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function brandResult(
|
|
595
|
+
record: TaskRecordV3,
|
|
596
|
+
nextWorking: string | null,
|
|
597
|
+
): ReducedTaskMutation {
|
|
598
|
+
const target = {} as ReducedTaskMutation;
|
|
599
|
+
Object.defineProperty(target, REDUCED_MUTATION_BRAND, {
|
|
600
|
+
value: true,
|
|
601
|
+
enumerable: false,
|
|
602
|
+
writable: false,
|
|
603
|
+
configurable: false,
|
|
604
|
+
});
|
|
605
|
+
Object.defineProperty(target, "record", {
|
|
606
|
+
value: record,
|
|
607
|
+
enumerable: true,
|
|
608
|
+
writable: false,
|
|
609
|
+
});
|
|
610
|
+
Object.defineProperty(target, "next_workspace_working", {
|
|
611
|
+
value: nextWorking,
|
|
612
|
+
enumerable: true,
|
|
613
|
+
writable: false,
|
|
614
|
+
});
|
|
615
|
+
return Object.freeze(target);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
export function isReducedMutation(value: unknown): value is ReducedTaskMutation {
|
|
619
|
+
return (
|
|
620
|
+
!!value &&
|
|
621
|
+
typeof value === "object" &&
|
|
622
|
+
(value as ReducedTaskMutation)[REDUCED_MUTATION_BRAND] === true
|
|
623
|
+
);
|
|
624
|
+
}
|