immune-brain 4.5.0 → 5.0.0
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 +157 -39
- package/README.zh-CN.md +160 -40
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +42 -45
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +189 -476
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +77 -102
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +7 -2
- package/plugins/immune-brain/.pi-extension/tsconfig.json +2 -2
- package/plugins/immune-brain/agents/immune-brain-advisory-reviewer.md +23 -0
- package/plugins/immune-brain/agents/immune-brain-qa.md +42 -0
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +90 -1
- package/plugins/immune-brain/agents/immune-brain-ui-review.md +21 -0
- package/plugins/immune-brain/dist/BASELINE.md +4 -4
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +9924 -9654
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +3 -3
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -1
- package/plugins/immune-brain/dist/imm-brainstorm.md +1 -1
- package/plugins/immune-brain/dist/imm-doc-prune.md +2 -2
- package/plugins/immune-brain/dist/{imm-agent-doc-maintain.md → imm-doc-slim.md} +9 -9
- package/plugins/immune-brain/dist/imm-planner.md +60 -20
- package/plugins/immune-brain/dist/imm-pr-fix.md +1 -1
- package/plugins/immune-brain/dist/{imm-review-retro.md → imm-retro.md} +26 -13
- package/plugins/immune-brain/dist/{imm-loop.md → imm-run.md} +44 -11
- package/plugins/immune-brain/dist/registry.yaml +9 -9
- package/plugins/immune-brain/dist/role-prompts/code-review.md +7 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +35 -2
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +138 -69
- package/plugins/immune-brain/runtime/assurance/host_port.ts +20 -0
- package/plugins/immune-brain/runtime/assurance/qa.ts +2 -2
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +4 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +347 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +1 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +135 -396
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +5 -4
- package/plugins/immune-brain/runtime/claude/review_host.ts +63 -16
- package/plugins/immune-brain/runtime/claude/role_agents.ts +83 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +3 -6
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +738 -677
- package/plugins/immune-brain/runtime/kernel/application.ts +7 -7
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +15 -8
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +5 -14
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +30 -49
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -5
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +0 -1
- package/plugins/immune-brain/runtime/kernel/capability_registry.ts +7 -9
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +170 -17
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +5 -11
- package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
- package/plugins/immune-brain/runtime/kernel/storage.ts +170 -11
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +13 -0
- package/plugins/immune-brain/runtime/kernel/types.ts +2 -1
- package/plugins/immune-brain/runtime/kernel/validation.ts +3 -1
- package/plugins/immune-brain/runtime/loop_contract.ts +22 -22
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +7 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +35 -2
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +27 -0
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +66 -28
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +106 -0
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +3 -9
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +132 -45
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +40 -36
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +98 -209
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +36 -14
- package/plugins/immune-brain/runtime/unattended/types.ts +0 -2
- package/plugins/immune-brain/runtime/workspace_scope.ts +78 -8
- package/plugins/immune-brain/skills/BASELINE.md +4 -4
- package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +1 -1
- package/plugins/immune-brain/skills/{imm-agent-doc-maintain → imm-doc-slim}/SKILL.md +9 -9
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +4 -2
- package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +1 -1
- package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/SKILL.md +7 -7
- package/plugins/immune-brain/skills/{imm-review-retro → imm-retro}/scripts/review_retro.ts +46 -21
- package/plugins/immune-brain/skills/{imm-loop → imm-run}/SKILL.md +7 -7
- package/plugins/immune-brain/skills/registry.yaml +9 -9
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -584
- package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +0 -33
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// Snapshot and verdict authority shared by all Assurance hosts.
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { readTaskRecordRaw, recoverKernelStoreFollowUps } from "../kernel/storage";
|
|
7
|
+
import { readTaskIntent } from "../kernel/intent";
|
|
8
|
+
import { projectAssurance, type AssuranceProjectionResult } from "../kernel/assurance_projection";
|
|
9
|
+
import { capabilityActionFor, createCanaryApplication } from "../kernel/canary_application";
|
|
10
|
+
import { createMutationAuthorityRegistry, digestOfAction } from "../kernel/authority_port";
|
|
11
|
+
import { findingsDigestV2 } from "../kernel/reducer";
|
|
12
|
+
import type { TaskRecord, TaskApprovalV2 } from "../kernel/types";
|
|
13
|
+
import { captureGitTaskSnapshot } from "../workspace_scope";
|
|
14
|
+
import { parseVerificationDescriptor } from "../verification_descriptor";
|
|
15
|
+
import type { VerificationDescriptor } from "./verification";
|
|
16
|
+
import { captureReviewBundle, captureReviewManifest, type ReviewBundle, type ReviewManifestV5, type ReviewRevision } from "./review_evidence";
|
|
17
|
+
import { reviewReworkFindings, reviewAdvisoryRecords, type SnapshotDescriptor, type AssuranceRole, type AssuranceVerdict, type HostContext } from "./coordinator";
|
|
18
|
+
import type { InvocationToken } from "./invocations";
|
|
19
|
+
import type { ConfirmationReferenceSource } from "./host_port";
|
|
20
|
+
|
|
21
|
+
export interface SnapshotDescriptorInput {
|
|
22
|
+
root: string;
|
|
23
|
+
task_id: string;
|
|
24
|
+
role: AssuranceRole;
|
|
25
|
+
run_id?: string | null;
|
|
26
|
+
record_revision: string;
|
|
27
|
+
workspace_revision: string;
|
|
28
|
+
intent_revision: number;
|
|
29
|
+
intent_content_hash: string;
|
|
30
|
+
diff_hash: string;
|
|
31
|
+
lifecycle: string;
|
|
32
|
+
artifact_state: string;
|
|
33
|
+
risk?: "routine" | "material" | "critical";
|
|
34
|
+
fresh_acceptance_ids: string[];
|
|
35
|
+
missing_acceptance_ids: string[];
|
|
36
|
+
stale_attestation_ids: string[];
|
|
37
|
+
acceptance: Array<{ id: string; assertion: string; verification: string }>;
|
|
38
|
+
dirty_files?: string[];
|
|
39
|
+
review_bundle_digest?: string | null;
|
|
40
|
+
/** Present only when Review authority binds an immutable Git revision (v4). */
|
|
41
|
+
review_revision?: {
|
|
42
|
+
contract: "assurance_kernel/review_revision_identity/v1";
|
|
43
|
+
base_head: string;
|
|
44
|
+
review_commit: string;
|
|
45
|
+
review_tree: string;
|
|
46
|
+
manifest_digest: string;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildSnapshot(input: SnapshotDescriptorInput): SnapshotDescriptor {
|
|
51
|
+
return {
|
|
52
|
+
contract: "assurance_kernel/assurance_snapshot/v2",
|
|
53
|
+
task_id: input.task_id,
|
|
54
|
+
run_id: input.run_id ?? null,
|
|
55
|
+
role: input.role,
|
|
56
|
+
record_revision: input.record_revision,
|
|
57
|
+
workspace_revision: input.workspace_revision,
|
|
58
|
+
intent_revision: input.intent_revision,
|
|
59
|
+
intent_content_hash: input.intent_content_hash,
|
|
60
|
+
diff_hash: input.diff_hash,
|
|
61
|
+
lifecycle: input.lifecycle,
|
|
62
|
+
artifact_state: input.artifact_state,
|
|
63
|
+
risk: input.risk ?? "material",
|
|
64
|
+
fresh_acceptance_ids: input.fresh_acceptance_ids,
|
|
65
|
+
missing_acceptance_ids: input.missing_acceptance_ids,
|
|
66
|
+
stale_attestation_ids: input.stale_attestation_ids,
|
|
67
|
+
acceptance: input.acceptance,
|
|
68
|
+
dirty_files: [...(input.dirty_files ?? [])].sort(),
|
|
69
|
+
review_bundle_digest: input.review_bundle_digest ?? null,
|
|
70
|
+
...(input.review_revision ? { review_revision: input.review_revision } : {}),
|
|
71
|
+
root: resolve(input.root),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
export async function ensureReviewRevision(
|
|
77
|
+
root: string,
|
|
78
|
+
taskId: string,
|
|
79
|
+
projection: AssuranceProjectionResult,
|
|
80
|
+
): Promise<ReviewRevision | null> {
|
|
81
|
+
const current = await readTaskRecordRaw(root, taskId);
|
|
82
|
+
const record = current.record;
|
|
83
|
+
if (!record) throw new Error(`task ${taskId} has no TaskRecord before Review preparation`);
|
|
84
|
+
if (current.revision !== projection.projection.record_revision)
|
|
85
|
+
throw new Error("TaskRecord changed before Review preparation");
|
|
86
|
+
if (record.contract !== "assurance_kernel/task_record/v4") return null;
|
|
87
|
+
if (!record.git_base_head)
|
|
88
|
+
throw new Error("Review revision requires a TaskRecord v4 git_base_head");
|
|
89
|
+
const manifest = captureReviewManifest(root, {
|
|
90
|
+
taskId,
|
|
91
|
+
baseHead: record.git_base_head,
|
|
92
|
+
scopeHint: record.intent_snapshot.scope_hint,
|
|
93
|
+
expectedDiffHash: projection.projection.diff_hash,
|
|
94
|
+
intentRevision: projection.projection.intent_revision,
|
|
95
|
+
intentContentHash: projection.projection.intent_content_hash,
|
|
96
|
+
recordRevision: projection.projection.record_revision,
|
|
97
|
+
workspaceRevision: projection.projection.workspace_revision,
|
|
98
|
+
lifecycle: projection.projection.lifecycle,
|
|
99
|
+
artifactState: projection.projection.artifact_state,
|
|
100
|
+
risk: record.intent_snapshot.risk,
|
|
101
|
+
// The same outcomes the Review snapshot is built from. A preflight stand-in
|
|
102
|
+
// only matched the settled QA attestation because deterministic QA happens to
|
|
103
|
+
// write that exact summary, so the submit-time digest comparison held by
|
|
104
|
+
// coincidence rather than by construction.
|
|
105
|
+
outcomes: qaOutcomes(record),
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
contract: "assurance_kernel/review_revision/v1",
|
|
109
|
+
base_head: manifest.base_head,
|
|
110
|
+
review_tree: manifest.review_tree,
|
|
111
|
+
review_commit: manifest.review_commit,
|
|
112
|
+
review_ref: manifest.review_ref,
|
|
113
|
+
diff_hash: manifest.diff_hash,
|
|
114
|
+
manifest_digest: manifest.manifest_digest,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function buildAssuranceSnapshot(
|
|
119
|
+
root: string,
|
|
120
|
+
taskId: string,
|
|
121
|
+
role: AssuranceRole,
|
|
122
|
+
projection: AssuranceProjectionResult,
|
|
123
|
+
readRecord: typeof readTaskRecordRaw = readTaskRecordRaw,
|
|
124
|
+
): Promise<{
|
|
125
|
+
snapshot: SnapshotDescriptor;
|
|
126
|
+
descriptors: Map<string, VerificationDescriptor>;
|
|
127
|
+
reviewBundle: ReviewBundle | null;
|
|
128
|
+
reviewManifest: ReviewManifestV5 | null;
|
|
129
|
+
}> {
|
|
130
|
+
const record = await readRecord(root, taskId);
|
|
131
|
+
if (
|
|
132
|
+
!record.record ||
|
|
133
|
+
record.revision !== projection.projection.record_revision ||
|
|
134
|
+
record.record.intent_snapshot.revision !== projection.projection.intent_revision ||
|
|
135
|
+
record.record.intent_ref.content_hash !== projection.projection.intent_content_hash
|
|
136
|
+
) {
|
|
137
|
+
throw new Error("TaskRecord changed before assurance snapshot capture");
|
|
138
|
+
}
|
|
139
|
+
const intent = record.record.intent_snapshot;
|
|
140
|
+
const acceptance = intent.acceptance;
|
|
141
|
+
const descriptors = new Map<string, VerificationDescriptor>();
|
|
142
|
+
// Review resumes from settled outcomes without resolving verification tools.
|
|
143
|
+
for (const item of role === "qa" ? acceptance : []) {
|
|
144
|
+
const descriptor = parseVerificationDescriptor(item.verification);
|
|
145
|
+
descriptors.set(item.id, descriptor);
|
|
146
|
+
}
|
|
147
|
+
// `git_base_head` is optional on the read shape because v3 records carry
|
|
148
|
+
// none, so the contract test alone does not prove it is present.
|
|
149
|
+
const baseHead = record.record.contract === "assurance_kernel/task_record/v4"
|
|
150
|
+
? record.record.git_base_head
|
|
151
|
+
: undefined;
|
|
152
|
+
if (record.record.contract === "assurance_kernel/task_record/v4" && !baseHead)
|
|
153
|
+
throw new Error("TaskRecord v4 is missing its Enrollment git_base_head");
|
|
154
|
+
const reviewRevision = baseHead
|
|
155
|
+
? {
|
|
156
|
+
contract: "assurance_kernel/review_revision_identity/v1" as const,
|
|
157
|
+
base_head: baseHead,
|
|
158
|
+
review_commit: "",
|
|
159
|
+
review_tree: "",
|
|
160
|
+
manifest_digest: "",
|
|
161
|
+
}
|
|
162
|
+
: null;
|
|
163
|
+
const reviewBundle = role === "review" && !reviewRevision
|
|
164
|
+
? captureReviewBundle(
|
|
165
|
+
root,
|
|
166
|
+
intent.scope_hint,
|
|
167
|
+
projection.projection.diff_hash,
|
|
168
|
+
qaOutcomes(record.record),
|
|
169
|
+
)
|
|
170
|
+
: null;
|
|
171
|
+
const reviewManifest = role === "review" && reviewRevision
|
|
172
|
+
? captureReviewManifest(root, {
|
|
173
|
+
taskId,
|
|
174
|
+
baseHead: reviewRevision.base_head,
|
|
175
|
+
scopeHint: intent.scope_hint,
|
|
176
|
+
expectedDiffHash: projection.projection.diff_hash,
|
|
177
|
+
intentRevision: projection.projection.intent_revision,
|
|
178
|
+
intentContentHash: projection.projection.intent_content_hash,
|
|
179
|
+
recordRevision: projection.projection.record_revision,
|
|
180
|
+
workspaceRevision: projection.projection.workspace_revision,
|
|
181
|
+
lifecycle: projection.projection.lifecycle,
|
|
182
|
+
artifactState: projection.projection.artifact_state,
|
|
183
|
+
risk: intent.risk,
|
|
184
|
+
outcomes: qaOutcomes(record.record),
|
|
185
|
+
})
|
|
186
|
+
: null;
|
|
187
|
+
const taskSnapshot = !reviewBundle && !reviewManifest
|
|
188
|
+
? captureGitTaskSnapshot(root, intent.scope_hint, taskId)
|
|
189
|
+
: null;
|
|
190
|
+
const dirtyFiles = reviewManifest
|
|
191
|
+
? Object.keys(reviewManifest.changed_paths)
|
|
192
|
+
: reviewBundle
|
|
193
|
+
? Object.keys(reviewBundle.dirty_files)
|
|
194
|
+
: Object.keys(taskSnapshot!.staged_files);
|
|
195
|
+
return {
|
|
196
|
+
snapshot: buildSnapshot({
|
|
197
|
+
root,
|
|
198
|
+
task_id: taskId,
|
|
199
|
+
role,
|
|
200
|
+
run_id: projection.projection.run_id,
|
|
201
|
+
record_revision: projection.projection.record_revision,
|
|
202
|
+
workspace_revision: projection.projection.workspace_revision,
|
|
203
|
+
intent_revision: projection.projection.intent_revision,
|
|
204
|
+
intent_content_hash: projection.projection.intent_content_hash,
|
|
205
|
+
diff_hash: projection.projection.diff_hash,
|
|
206
|
+
lifecycle: projection.projection.lifecycle,
|
|
207
|
+
artifact_state: projection.projection.artifact_state,
|
|
208
|
+
risk: intent.risk,
|
|
209
|
+
fresh_acceptance_ids: projection.projection.fresh_acceptance_ids,
|
|
210
|
+
missing_acceptance_ids: projection.projection.missing_acceptance_ids,
|
|
211
|
+
stale_attestation_ids: projection.projection.stale_attestation_ids,
|
|
212
|
+
acceptance,
|
|
213
|
+
dirty_files: dirtyFiles,
|
|
214
|
+
review_bundle_digest: reviewManifest?.manifest_digest ?? reviewBundle?.bundle_digest ?? null,
|
|
215
|
+
review_revision: reviewManifest
|
|
216
|
+
? {
|
|
217
|
+
contract: "assurance_kernel/review_revision_identity/v1",
|
|
218
|
+
base_head: reviewManifest.base_head,
|
|
219
|
+
review_commit: reviewManifest.review_commit,
|
|
220
|
+
review_tree: reviewManifest.review_tree,
|
|
221
|
+
manifest_digest: reviewManifest.manifest_digest,
|
|
222
|
+
}
|
|
223
|
+
: undefined,
|
|
224
|
+
}),
|
|
225
|
+
descriptors,
|
|
226
|
+
reviewBundle,
|
|
227
|
+
reviewManifest,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function qaOutcomes(
|
|
232
|
+
record: TaskRecord,
|
|
233
|
+
): Record<string, { status: "passed" | "failed" | "blocked"; summary: string }> {
|
|
234
|
+
return Object.fromEntries(
|
|
235
|
+
record.attestations
|
|
236
|
+
.filter((item) => item.kind === "qa")
|
|
237
|
+
.flatMap((item) => item.acceptance_results)
|
|
238
|
+
.map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function stagePlanningArtifactTransition(root: string, record: {
|
|
243
|
+
intent_ref: { path: string };
|
|
244
|
+
intent_snapshot: { scope_hint: string[] };
|
|
245
|
+
}): void {
|
|
246
|
+
const intentActive = record.intent_ref.path.replace("docs/plans/archive/", "docs/plans/");
|
|
247
|
+
const intentArchive = intentActive.replace("docs/plans/", "docs/plans/archive/");
|
|
248
|
+
const specActive = record.intent_snapshot.scope_hint.find((path) =>
|
|
249
|
+
/^docs\/specs\/(?!archive\/)[^/]+\.spec\.md$/.test(path)
|
|
250
|
+
&& record.intent_snapshot.scope_hint.includes(path.replace("docs/specs/", "docs/specs/archive/")),
|
|
251
|
+
);
|
|
252
|
+
const candidates = [
|
|
253
|
+
intentActive,
|
|
254
|
+
intentArchive,
|
|
255
|
+
...(specActive ? [specActive, specActive.replace("docs/specs/", "docs/specs/archive/")] : []),
|
|
256
|
+
];
|
|
257
|
+
const paths = candidates.filter((path) => existsSync(join(root, path)) || execFileSync(
|
|
258
|
+
"git",
|
|
259
|
+
["ls-files", "--cached", "--", path],
|
|
260
|
+
{ cwd: root, encoding: "utf8" },
|
|
261
|
+
).trim().length > 0);
|
|
262
|
+
if (paths.length === 0) return;
|
|
263
|
+
execFileSync("git", ["add", "--", ...paths], {
|
|
264
|
+
cwd: root,
|
|
265
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface VerdictApplicationInput {
|
|
270
|
+
taskId: string;
|
|
271
|
+
snapshot: SnapshotDescriptor;
|
|
272
|
+
verdict: AssuranceVerdict;
|
|
273
|
+
invocation: InvocationToken;
|
|
274
|
+
actorId: string;
|
|
275
|
+
hooks?: { beforeCommit?: () => Promise<void>; onCommit?: () => void; afterCommit?: () => Promise<void> };
|
|
276
|
+
}
|
|
277
|
+
export interface VerdictAuthority {
|
|
278
|
+
buildAssurance: typeof buildAssuranceSnapshot;
|
|
279
|
+
ensureReviewRevision: typeof ensureReviewRevision;
|
|
280
|
+
applyVerdict(ctx: HostContext, input: VerdictApplicationInput): Promise<void>;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function createVerdictAuthority(options: {
|
|
284
|
+
confirmationReference: ConfirmationReferenceSource;
|
|
285
|
+
commitInvocation: (token: InvocationToken) => void;
|
|
286
|
+
onReworkApplied?: (ctx: HostContext, taskId: string, findingsCount: number) => void;
|
|
287
|
+
}, registry = createMutationAuthorityRegistry()): VerdictAuthority {
|
|
288
|
+
const app = createCanaryApplication(registry);
|
|
289
|
+
return {
|
|
290
|
+
buildAssurance: buildAssuranceSnapshot,
|
|
291
|
+
ensureReviewRevision,
|
|
292
|
+
async applyVerdict(ctx, input) {
|
|
293
|
+
const { snapshot, verdict, actorId, hooks = {} } = input;
|
|
294
|
+
const fresh = await projectAssurance(ctx.cwd, snapshot.task_id);
|
|
295
|
+
const fields = ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash", "diff_hash", "lifecycle", "artifact_state"] as const;
|
|
296
|
+
const mismatches = fields.filter(field => fresh.projection[field] !== snapshot[field]);
|
|
297
|
+
if (fresh.error || fresh.claim?.task_id !== snapshot.task_id || mismatches.length)
|
|
298
|
+
throw new Error(`assurance snapshot changed before authority application: ${[fresh.error, fresh.claim?.task_id !== snapshot.task_id ? "claim" : null, ...mismatches].filter(Boolean).join(", ")}`);
|
|
299
|
+
recoverKernelStoreFollowUps(ctx.cwd, snapshot.task_id);
|
|
300
|
+
const record = readTaskRecordRaw(ctx.cwd, snapshot.task_id).record;
|
|
301
|
+
if (!record) throw new Error("TaskRecord disappeared before authority application");
|
|
302
|
+
const priorIntentToken = readTaskIntent(ctx.cwd, snapshot.task_id, record.intent_ref.path).token;
|
|
303
|
+
const now = new Date().toISOString();
|
|
304
|
+
const findings = verdict.decision === "rework" ? reviewReworkFindings(verdict) : [];
|
|
305
|
+
const advisories = verdict.decision === "pass" ? reviewAdvisoryRecords(verdict) : [];
|
|
306
|
+
const approval: TaskApprovalV2 | undefined = verdict.decision === "pass" ? {
|
|
307
|
+
id: `approval-${snapshot.role}-${randomUUID().slice(0, 8)}`,
|
|
308
|
+
kind: snapshot.role, authority_role: snapshot.role === "qa" ? "qa" : "reviewer",
|
|
309
|
+
task_revision: snapshot.intent_revision, intent_content_hash: snapshot.intent_content_hash,
|
|
310
|
+
diff_hash: snapshot.diff_hash, actor_id: actorId, summary: verdict.approval!.summary,
|
|
311
|
+
...(snapshot.role === "review" && snapshot.review_revision ? { review_revision: snapshot.review_revision } : {}),
|
|
312
|
+
...(snapshot.role === "review" && advisories.length ? { advisory_findings: advisories } : {}),
|
|
313
|
+
} : undefined;
|
|
314
|
+
const op = verdict.decision === "rework" ? "request_rework" : "record_approval";
|
|
315
|
+
const payload = approval ? { approval } : { findings };
|
|
316
|
+
const action = capabilityActionFor({ op, task_id: snapshot.task_id, at: now, actor_id: actorId, ...payload });
|
|
317
|
+
const capability = registry.issue({
|
|
318
|
+
authority_kind: snapshot.role, task_id: snapshot.task_id,
|
|
319
|
+
...(snapshot.run_id ? { run_id: snapshot.run_id } : {}),
|
|
320
|
+
action_digest: digestOfAction(action), expected_record_hash: snapshot.record_revision,
|
|
321
|
+
intent_revision: snapshot.intent_revision, intent_content_hash: snapshot.intent_content_hash,
|
|
322
|
+
diff_hash: snapshot.diff_hash, actor_id: actorId,
|
|
323
|
+
confirmation_ref: options.confirmationReference({ snapshot, actorId, now }),
|
|
324
|
+
findings_digest: verdict.decision === "rework" ? findingsDigestV2(findings) : null,
|
|
325
|
+
});
|
|
326
|
+
await hooks.beforeCommit?.();
|
|
327
|
+
options.commitInvocation(input.invocation);
|
|
328
|
+
// Preserve the Pi ordering: start settlement, run onCommit, await settlement,
|
|
329
|
+
// then afterCommit. A thrown hook never undoes the committed transition.
|
|
330
|
+
const settlement = (async () => app.execute({ root: ctx.cwd, task_id: snapshot.task_id,
|
|
331
|
+
operation: approval ? { op: "record_approval", capability, approval, actor_id: actorId }
|
|
332
|
+
: { op: "request_rework", capability, findings, actor_id: actorId }, prior_intent_token: priorIntentToken, now }))();
|
|
333
|
+
let hookFailed = false;
|
|
334
|
+
let hookError: unknown;
|
|
335
|
+
try { hooks.onCommit?.(); } catch (error) { hookFailed = true; hookError = error; }
|
|
336
|
+
const result = await settlement;
|
|
337
|
+
try { await hooks.afterCommit?.(); } catch (error) { if (!hookFailed) { hookFailed = true; hookError = error; } }
|
|
338
|
+
if (hookFailed) throw hookError;
|
|
339
|
+
// Staging the planning-artifact transition is a post-commit side effect, so it
|
|
340
|
+
// runs after the hook lifecycle: a hook error is reported untouched, and a
|
|
341
|
+
// staging failure can neither skip afterCommit nor mask the first error.
|
|
342
|
+
if (verdict.decision === "rework") stagePlanningArtifactTransition(ctx.cwd, result.record);
|
|
343
|
+
if (verdict.decision === "rework" && result.record.findings.some(f => f.kind === "replan_required" && f.status === "open"))
|
|
344
|
+
options.onReworkApplied?.(ctx, snapshot.task_id, findings.length);
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
}
|
|
@@ -81,8 +81,7 @@ export interface NativeConfirmationInput {
|
|
|
81
81
|
batch_branch: string;
|
|
82
82
|
children: Array<{ task_id: string; slice_id: string; risk?: string }>;
|
|
83
83
|
excluded: Array<{ task_id: string; slice_id: string; reason: string }>;
|
|
84
|
-
budget: { max_children: number;
|
|
85
|
-
expires_at: string;
|
|
84
|
+
budget: { max_children: number; qa_failure_limit: number };
|
|
86
85
|
};
|
|
87
86
|
signal?: AbortSignal;
|
|
88
87
|
}
|