immune-brain 5.3.0 → 5.5.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/README.md +4 -4
- package/README.zh-CN.md +4 -4
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +54 -70
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +163 -28
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +71 -10
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +1 -1
- package/plugins/immune-brain/.pi-extension/pi-run-continuation.ts +161 -0
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -2
- package/plugins/immune-brain/dist/BASELINE.md +13 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +16130 -12734
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +3 -1
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +41 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/imm-planner.md +57 -16
- package/plugins/immune-brain/dist/imm-run-batch.md +398 -0
- package/plugins/immune-brain/dist/imm-run.md +89 -344
- package/plugins/immune-brain/dist/role-prompts/code-review.md +26 -2
- package/plugins/immune-brain/dist/role-prompts/executor.md +35 -10
- package/plugins/immune-brain/hooks/hooks.json +21 -1
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +203 -13
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +2 -1
- package/plugins/immune-brain/runtime/assurance/enrollment_git_base.ts +99 -14
- package/plugins/immune-brain/runtime/assurance/phase_entry.ts +1 -7
- package/plugins/immune-brain/runtime/assurance/qa.ts +126 -43
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +8 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +48 -0
- package/plugins/immune-brain/runtime/assurance/run_continuation.ts +284 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +46 -4
- package/plugins/immune-brain/runtime/assurance/verification.ts +83 -35
- package/plugins/immune-brain/runtime/assurance/workflow_autocommit.ts +222 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +11 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +199 -48
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +8 -15
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +105 -9
- package/plugins/immune-brain/runtime/claude/run_continuation.ts +274 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +41 -103
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +60 -2
- package/plugins/immune-brain/runtime/kernel/application.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +103 -20
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +57 -7
- package/plugins/immune-brain/runtime/kernel/completion.ts +28 -4
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +48 -8
- package/plugins/immune-brain/runtime/kernel/intent.ts +169 -104
- package/plugins/immune-brain/runtime/kernel/reducer.ts +136 -6
- package/plugins/immune-brain/runtime/kernel/risk_floor.ts +286 -0
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +83 -1
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +1 -1
- package/plugins/immune-brain/runtime/kernel/types.ts +49 -3
- package/plugins/immune-brain/runtime/kernel/validation.ts +147 -7
- package/plugins/immune-brain/runtime/loop_contract.ts +5 -0
- package/plugins/immune-brain/runtime/plan_core.ts +15 -8
- package/plugins/immune-brain/runtime/plugin_source_skew.ts +58 -0
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +26 -2
- package/plugins/immune-brain/runtime/prompts/executor.md +35 -10
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +5 -3
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +40 -10
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +57 -3
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +424 -66
- package/plugins/immune-brain/runtime/unattended/batch_integration.ts +2 -1
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +13 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +43 -24
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +6 -2
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +102 -41
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +9 -3
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +9 -9
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +90 -32
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +102 -4
- package/plugins/immune-brain/runtime/unattended/parent_delivery_consolidation.ts +248 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +126 -18
- package/plugins/immune-brain/skills/BASELINE.md +13 -1
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +5 -0
- package/plugins/immune-brain/skills/imm-run/SKILL.md +12 -8
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { snapshotDigest, QaPreparationError, type AssuranceVerdict, type SnapshotDescriptor, type QaCheckDiagnostic, type QaPreparationReason, type QaVerificationProgressInput } from "./coordinator";
|
|
2
|
+
import { snapshotDigest, QaPreparationError, qaPrepareObservation, type AssuranceVerdict, type SnapshotDescriptor, type QaCheckDiagnostic, type QaPreparationReason, type QaVerificationProgressInput } from "./coordinator";
|
|
3
3
|
export { QaPreparationError, type QaCheckDiagnostic, type QaPreparationReason, type QaVerificationProgressInput } from "./coordinator";
|
|
4
|
-
import { assertDeliveryClean, materializeDeliveryWorkspace,
|
|
5
|
-
import { runFixedVerification, resolveVerificationCommand, assertCommandIdentity, verificationPath, VerificationAbortedError, VerificationCleanupError, VerificationLaunchError, type FrozenCommand, type VerificationDescriptor, type VerificationCommand } from "./verification";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { join, sep } from "node:path";
|
|
9
|
-
import { qaFindingId } from "./qa_findings";
|
|
10
|
-
import { captureGitTaskRevisionSnapshot } from "../workspace_scope";
|
|
4
|
+
import { assertDeliveryClean, materializeDeliveryWorkspace, writeDeliveryTree } from "./delivery_workspace";
|
|
5
|
+
import { runFixedVerification, resolveVerificationCommand, assertCommandIdentity, verificationPath, readQaBaseline, VerificationAbortedError, VerificationCleanupError, VerificationLaunchError, type FrozenCommand, type VerificationDescriptor, type VerificationCommand, type QaBaseline } from "./verification";
|
|
6
|
+
import { qaFindingId, qaBaselineFindingId, PRE_EXISTING_BASELINE_PREFIX } from "./qa_findings";
|
|
7
|
+
import { captureGitTaskRevisionSnapshot, inScopeTrackedDrift } from "../workspace_scope";
|
|
11
8
|
import { readTaskRecordRaw } from "../kernel/storage";
|
|
12
9
|
|
|
13
|
-
function
|
|
14
|
-
snapshot: SnapshotDescriptor,
|
|
15
|
-
writeTree: typeof writeDeliveryTree,
|
|
16
|
-
): string {
|
|
17
|
-
if (snapshot.review_revision?.review_tree) return snapshot.review_revision.review_tree;
|
|
10
|
+
function recordForSnapshot(snapshot: SnapshotDescriptor) {
|
|
18
11
|
const record = readTaskRecordRaw(snapshot.root, snapshot.task_id).record;
|
|
19
12
|
if (!record || record.contract !== "assurance_kernel/task_record/v4" || !record.git_base_head)
|
|
20
13
|
throw new Error("QA delivery requires a TaskRecord v4 git_base_head");
|
|
14
|
+
return record;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function deliveryTreeForSnapshot(snapshot: SnapshotDescriptor): string {
|
|
18
|
+
if (snapshot.review_revision?.review_tree) return snapshot.review_revision.review_tree;
|
|
19
|
+
const record = recordForSnapshot(snapshot);
|
|
21
20
|
const captured = captureGitTaskRevisionSnapshot(
|
|
22
21
|
snapshot.root,
|
|
23
22
|
record.intent_snapshot.scope_hint,
|
|
@@ -36,17 +35,27 @@ export async function runDeterministicQa(
|
|
|
36
35
|
options: {
|
|
37
36
|
signal?: AbortSignal;
|
|
38
37
|
onProgress?: (progress: QaVerificationProgressInput) => void;
|
|
39
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Task QA runs in the task's working directory. Only the Lane batch
|
|
40
|
+
* integration recheck asks for a copy: its merge candidate is never
|
|
41
|
+
* checked out, so there is no working directory to run in.
|
|
42
|
+
*/
|
|
43
|
+
delivery?: "materialized";
|
|
44
|
+
// Coordination tests inject a fake executor without a real repository.
|
|
40
45
|
_runFixedVerification?: typeof runFixedVerification;
|
|
41
46
|
_resolveVerificationCommand?: typeof resolveVerificationCommand;
|
|
42
47
|
_materializeDeliveryWorkspace?: typeof materializeDeliveryWorkspace;
|
|
43
|
-
|
|
48
|
+
/** Replaces the frozen tree and, in place, the comparison with it. */
|
|
49
|
+
_frozenTree?: { tree: string; drift?: () => string[] };
|
|
50
|
+
/** Replaces the Enrollment `base_head` the project baseline is read from. */
|
|
51
|
+
_baseHead?: string;
|
|
44
52
|
} = {},
|
|
45
53
|
): Promise<AssuranceVerdict> {
|
|
46
54
|
if (snapshot.role !== "qa") throw new Error("deterministic QA requires qa role");
|
|
47
55
|
const live = () => { if (options.signal?.aborted) throw new VerificationAbortedError(); };
|
|
48
56
|
live();
|
|
49
|
-
|
|
57
|
+
type QaItem = { id: string; index: number; descriptor: VerificationDescriptor; baseline?: string };
|
|
58
|
+
const groups = new Map<string, QaItem[]>();
|
|
50
59
|
for (const [index, item] of snapshot.acceptance.entries()) {
|
|
51
60
|
const descriptor = descriptors.get(item.id);
|
|
52
61
|
if (!descriptor) throw new Error(`verification descriptor missing for ${item.id}`);
|
|
@@ -55,12 +64,66 @@ export async function runDeterministicQa(
|
|
|
55
64
|
group.push({ id: item.id, index, descriptor });
|
|
56
65
|
groups.set(key, group);
|
|
57
66
|
}
|
|
58
|
-
const
|
|
59
|
-
|
|
67
|
+
const materialized = options.delivery === "materialized";
|
|
68
|
+
// The project baseline comes from the Enrollment base_head tree, never the
|
|
69
|
+
// delivery. Only task QA in the working directory carries it: the Lane batch
|
|
70
|
+
// integration recheck reruns acceptance descriptors on a merge candidate.
|
|
71
|
+
const baselineHead = materialized ? undefined : options._baseHead
|
|
72
|
+
?? (options._frozenTree ? undefined : recordForSnapshot(snapshot).git_base_head);
|
|
73
|
+
const baseline: QaBaseline | null = baselineHead ? readQaBaseline(snapshot.root, baselineHead) : null;
|
|
74
|
+
const totalChecks = snapshot.acceptance.length + (baseline?.checks.length ?? 0);
|
|
75
|
+
for (const [offset, check] of (baseline?.checks ?? []).entries()) {
|
|
76
|
+
// Baseline checks run after every acceptance descriptor, so they never share
|
|
77
|
+
// a group with one. Diagnostics reuse the acceptance index space past the
|
|
78
|
+
// acceptance count and name the check as `baseline:<id>`.
|
|
79
|
+
const key = `baseline:${JSON.stringify(check.descriptor.environment)}`;
|
|
80
|
+
const group = groups.get(key) ?? [];
|
|
81
|
+
group.push({ id: `baseline:${check.id}`, index: snapshot.acceptance.length + offset, descriptor: check.descriptor, baseline: check.id });
|
|
82
|
+
groups.set(key, group);
|
|
83
|
+
}
|
|
84
|
+
const tree = options._frozenTree?.tree ?? deliveryTreeForSnapshot(snapshot);
|
|
85
|
+
const drift = materialized ? null : options._frozenTree
|
|
86
|
+
? options._frozenTree.drift ?? (() => [])
|
|
87
|
+
: ((scope: unknown) => () => inScopeTrackedDrift(snapshot.root, tree, scope))(recordForSnapshot(snapshot).intent_snapshot.scope_hint);
|
|
60
88
|
const path = verificationPath();
|
|
89
|
+
// Rerun one failed baseline check against the materialized base_head tree,
|
|
90
|
+
// under the same descriptor. Anything short of the identical failure, a
|
|
91
|
+
// materialization or prepare problem included, leaves the finding blocking.
|
|
92
|
+
const failsIdenticallyAtBase = async (item: QaItem, observed: { exit_code: number; timed_out: boolean; output_limited: boolean }, baseTree: string): Promise<boolean> => {
|
|
93
|
+
live();
|
|
94
|
+
let copy: ReturnType<typeof materializeDeliveryWorkspace>;
|
|
95
|
+
try { copy = (options._materializeDeliveryWorkspace ?? materializeDeliveryWorkspace)(snapshot.root, baseTree); }
|
|
96
|
+
catch { return false; }
|
|
97
|
+
const diagnostic = (reason: QaPreparationReason, stage: QaCheckDiagnostic["stage"]): QaCheckDiagnostic[] => [{ acceptance_id: item.id,
|
|
98
|
+
descriptor_ref: `acceptance/${item.index}/verification/command`,
|
|
99
|
+
descriptor_digest: `sha256:${createHash("sha256").update(JSON.stringify(item.descriptor)).digest("hex")}`,
|
|
100
|
+
stage, outcome: reason, elapsed_ms: 0, exit_code: null, stdout_bytes: null, stderr_bytes: null }];
|
|
101
|
+
const run = async (command: VerificationCommand) => {
|
|
102
|
+
const frozen = (options._resolveVerificationCommand ?? resolveVerificationCommand)(copy.root, command, path);
|
|
103
|
+
return (options._runFixedVerification ?? runFixedVerification)(copy.root, command, frozen, { signal: options.signal, path });
|
|
104
|
+
};
|
|
105
|
+
try {
|
|
106
|
+
const prepare = item.descriptor.environment.prepare;
|
|
107
|
+
if (prepare) {
|
|
108
|
+
const prepared = await run(prepare);
|
|
109
|
+
if (prepared.exit_code !== 0 || prepared.timed_out || prepared.output_limited) return false;
|
|
110
|
+
}
|
|
111
|
+
const result = await run(item.descriptor.command);
|
|
112
|
+
live();
|
|
113
|
+
return result.exit_code === observed.exit_code && result.timed_out === observed.timed_out && result.output_limited === observed.output_limited;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error instanceof VerificationAbortedError) throw error;
|
|
116
|
+
if (error instanceof VerificationCleanupError) throw new QaPreparationError("resolution", [item.id], "process_cleanup_failed", diagnostic("process_cleanup_failed", "resolution"));
|
|
117
|
+
return false;
|
|
118
|
+
} finally {
|
|
119
|
+
try { copy.cleanup(); }
|
|
120
|
+
catch { throw new QaPreparationError("integrity", [item.id], "delivery_cleanup_failed", diagnostic("delivery_cleanup_failed", "integrity")); }
|
|
121
|
+
}
|
|
122
|
+
};
|
|
61
123
|
const findings: NonNullable<AssuranceVerdict["findings"]> = [];
|
|
62
124
|
const evidence: unknown[] = [];
|
|
63
125
|
const allCommands: Array<{ frozen: FrozenCommand; diagnostics: QaCheckDiagnostic[] }> = [];
|
|
126
|
+
const baselineFailures: Array<{ item: QaItem; finding: (typeof findings)[number]; observed: { exit_code: number; timed_out: boolean; output_limited: boolean } }> = [];
|
|
64
127
|
for (const [environmentKey, group] of groups) {
|
|
65
128
|
live();
|
|
66
129
|
const ids = group.map(item => item.id);
|
|
@@ -72,23 +135,30 @@ export async function runDeterministicQa(
|
|
|
72
135
|
descriptor_digest: `sha256:${createHash("sha256").update(JSON.stringify(item.descriptor)).digest("hex")}`,
|
|
73
136
|
stage, outcome, elapsed_ms: elapsed, exit_code: result?.exit_code ?? null,
|
|
74
137
|
stdout_bytes: result ? Buffer.byteLength(result.stdout) : null, stderr_bytes: result ? Buffer.byteLength(result.stderr) : null }));
|
|
75
|
-
const
|
|
76
|
-
|
|
138
|
+
const delivery = (() => {
|
|
139
|
+
if (!materialized) return { root: snapshot.root, seal: undefined, cleanup: () => {} };
|
|
140
|
+
let copy: ReturnType<typeof materializeDeliveryWorkspace> | undefined;
|
|
77
141
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return { delivery, home, deliveryPrefix: `${realpathSync(delivery.root)}${sep}` };
|
|
142
|
+
copy = (options._materializeDeliveryWorkspace ?? materializeDeliveryWorkspace)(snapshot.root, tree);
|
|
143
|
+
return { root: copy.root, seal: copy.seal as string | undefined, cleanup: copy.cleanup };
|
|
81
144
|
} catch {
|
|
82
145
|
let reason: QaPreparationReason = "delivery_unavailable";
|
|
83
|
-
try {
|
|
146
|
+
try { copy?.cleanup(); }
|
|
84
147
|
catch { reason = "delivery_cleanup_failed"; }
|
|
85
148
|
throw new QaPreparationError("prepare", ids, reason, diagnostics("prepare", reason));
|
|
86
149
|
}
|
|
87
150
|
})();
|
|
88
151
|
const clean = () => {
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
|
|
152
|
+
let paths: string[] | undefined;
|
|
153
|
+
try {
|
|
154
|
+
if (drift) { paths = drift(); if (!paths.length) return; }
|
|
155
|
+
else {
|
|
156
|
+
if (!options._materializeDeliveryWorkspace) assertDeliveryClean(delivery.root, tree, delivery.seal, environment.writable_paths);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
} catch { paths = undefined; }
|
|
160
|
+
throw new QaPreparationError("integrity", ids, "protected_input_or_output_drift", diagnostics("integrity", "protected_input_or_output_drift"),
|
|
161
|
+
paths ? qaPrepareObservation(0, "", "working_directory", paths) : undefined);
|
|
92
162
|
};
|
|
93
163
|
const groupCommands: FrozenCommand[] = [];
|
|
94
164
|
const execute = async (command: VerificationCommand, stage: "prepare" | "check", affected = group) => {
|
|
@@ -100,7 +170,7 @@ export async function runDeterministicQa(
|
|
|
100
170
|
try { frozen = (options._resolveVerificationCommand ?? resolveVerificationCommand)(delivery.root, command, path); }
|
|
101
171
|
catch { throw fail("resolution", "executable_or_cwd_unavailable"); }
|
|
102
172
|
let result;
|
|
103
|
-
try { result = await (options._runFixedVerification ?? runFixedVerification)(delivery.root, command, frozen, { signal: options.signal,
|
|
173
|
+
try { result = await (options._runFixedVerification ?? runFixedVerification)(delivery.root, command, frozen, { signal: options.signal, path }); }
|
|
104
174
|
catch (error) {
|
|
105
175
|
if (error instanceof VerificationAbortedError) throw error;
|
|
106
176
|
if (error instanceof VerificationLaunchError) throw fail("resolution", "process_launch_failed");
|
|
@@ -109,17 +179,13 @@ export async function runDeterministicQa(
|
|
|
109
179
|
}
|
|
110
180
|
live(); clean();
|
|
111
181
|
groupCommands.push(frozen);
|
|
112
|
-
if (frozen?.entry)
|
|
113
|
-
|
|
114
|
-
if (!command.executable.startsWith("./")) allCommands.push({ frozen, diagnostics: identityDiagnostics });
|
|
115
|
-
else if (frozen.interpreter && !frozen.interpreter.path.startsWith(deliveryPrefix))
|
|
116
|
-
allCommands.push({ frozen: { entry: frozen.interpreter, interpreter: null, interpreter_args: [] }, diagnostics: identityDiagnostics });
|
|
117
|
-
}
|
|
182
|
+
if (frozen?.entry && !command.executable.startsWith("./"))
|
|
183
|
+
allCommands.push({ frozen, diagnostics: diagnostics("integrity", "command_identity_changed", affected, stage, Math.round(performance.now() - started), result) });
|
|
118
184
|
// Paths and process output are not persisted. Identity hashes describe
|
|
119
185
|
// observed entries, not a claim about every transitive dependency.
|
|
120
186
|
evidence.push({ stage, environment: createHash("sha256").update(environmentKey).digest("hex"),
|
|
121
187
|
command: createHash("sha256").update(JSON.stringify(command)).digest("hex"),
|
|
122
|
-
entry: frozen?.entry?.content_hash ?? null,
|
|
188
|
+
entry: frozen?.entry?.content_hash ?? null,
|
|
123
189
|
exit_code: result.exit_code, timed_out: result.timed_out, output_limited: result.output_limited });
|
|
124
190
|
if (Buffer.byteLength(JSON.stringify(evidence)) > 16_384)
|
|
125
191
|
throw fail("prepare", "execution_metadata_limit_exceeded");
|
|
@@ -132,32 +198,46 @@ export async function runDeterministicQa(
|
|
|
132
198
|
const result = await execute(environment.prepare, "prepare");
|
|
133
199
|
if (result.exit_code !== 0 || result.timed_out || result.output_limited)
|
|
134
200
|
throw new QaPreparationError("prepare", ids, result.timed_out ? "timeout" : result.output_limited ? "output_limit" : "nonzero_exit",
|
|
135
|
-
diagnostics("prepare", result.timed_out ? "timeout" : result.output_limited ? "output_limit" : "nonzero_exit", group, "prepare", Math.round(performance.now() - prepareStarted), result)
|
|
201
|
+
diagnostics("prepare", result.timed_out ? "timeout" : result.output_limited ? "output_limit" : "nonzero_exit", group, "prepare", Math.round(performance.now() - prepareStarted), result),
|
|
202
|
+
qaPrepareObservation(environment.prepare.timeout_ms, result.stderr, materialized ? "isolated_materialization" : "working_directory"));
|
|
136
203
|
}
|
|
137
204
|
for (const item of group) {
|
|
138
205
|
const started = performance.now();
|
|
139
206
|
const progress = (phase: QaVerificationProgressInput["phase"], outcome: QaCheckDiagnostic["outcome"], result?: { exit_code: number; stdout: string; stderr: string }) => options.onProgress?.({
|
|
140
|
-
index: item.index + 1, total:
|
|
207
|
+
index: item.index + 1, total: totalChecks, acceptance_id: item.id, phase, elapsed_ms: Math.round(performance.now() - started),
|
|
141
208
|
diagnostic: diagnostics("check", outcome, [item], "check", Math.round(performance.now() - started), result)[0],
|
|
142
209
|
});
|
|
143
210
|
progress("running", "running");
|
|
144
211
|
const result = await execute(item.descriptor.command, "check", [item]);
|
|
145
212
|
const failed = result.exit_code !== 0 || result.timed_out || result.output_limited;
|
|
146
213
|
progress(failed ? "failed" : "passed", result.timed_out ? "timeout" : result.output_limited ? "output_limit" : failed ? "nonzero_exit" : "passed", result);
|
|
147
|
-
if (failed)
|
|
148
|
-
|
|
214
|
+
if (failed) {
|
|
215
|
+
const finding = { id: item.baseline ? qaBaselineFindingId(item.baseline, snapshotDigest(snapshot)) : qaFindingId(item.id, snapshotDigest(snapshot)),
|
|
216
|
+
kind: "blocking" as const, acceptance_id: item.baseline ? null : item.id,
|
|
217
|
+
summary: `${item.baseline ? `baseline check ${item.baseline} ` : ""}verification failed (exit ${result.exit_code}${result.timed_out ? ", timed out" : ""}${result.output_limited ? ", output limit" : ""}) stdout=${Buffer.byteLength(result.stdout)}B stderr=${Buffer.byteLength(result.stderr)}B`, findings_digest: "" };
|
|
218
|
+
findings.push(finding);
|
|
219
|
+
if (item.baseline) baselineFailures.push({ item, finding, observed: { exit_code: result.exit_code, timed_out: result.timed_out, output_limited: result.output_limited } });
|
|
220
|
+
}
|
|
149
221
|
}
|
|
150
|
-
// Recheck while
|
|
222
|
+
// Recheck while a materialized copy still exists; its cleanup removes project entries.
|
|
151
223
|
try {
|
|
152
224
|
for (const frozen of groupCommands) if (frozen?.entry) assertCommandIdentity(frozen);
|
|
153
225
|
} catch { throw new QaPreparationError("integrity", ids, "command_identity_changed", diagnostics("integrity", "command_identity_changed")); }
|
|
154
226
|
} finally {
|
|
155
|
-
try {
|
|
227
|
+
try { delivery.cleanup(); }
|
|
156
228
|
catch { throw new QaPreparationError("integrity", ids, "delivery_cleanup_failed", diagnostics("integrity", "delivery_cleanup_failed")); }
|
|
157
229
|
}
|
|
158
230
|
}
|
|
159
231
|
live();
|
|
160
|
-
//
|
|
232
|
+
// A baseline failure also present at base_head is disclosed, not blocking. Only
|
|
233
|
+
// baseline checks get this tolerance; an acceptance descriptor never does.
|
|
234
|
+
const disclosed: NonNullable<AssuranceVerdict["findings"]> = [];
|
|
235
|
+
for (const failure of baselineFailures) {
|
|
236
|
+
if (!baseline || !(await failsIdenticallyAtBase(failure.item, failure.observed, baseline.tree))) continue;
|
|
237
|
+
findings.splice(findings.indexOf(failure.finding), 1);
|
|
238
|
+
disclosed.push({ ...failure.finding, kind: "advisory", summary: `${PRE_EXISTING_BASELINE_PREFIX} ${failure.finding.summary}` });
|
|
239
|
+
}
|
|
240
|
+
// Host entries must still match before approval.
|
|
161
241
|
for (const entry of allCommands) {
|
|
162
242
|
try { if (entry.frozen?.entry) assertCommandIdentity(entry.frozen); }
|
|
163
243
|
catch { throw new QaPreparationError("integrity", entry.diagnostics.map(item => item.acceptance_id), "command_identity_changed", entry.diagnostics); }
|
|
@@ -165,10 +245,13 @@ export async function runDeterministicQa(
|
|
|
165
245
|
const base = { contract: "assurance_kernel/assurance_verdict/v2" as const, role: "qa" as const,
|
|
166
246
|
task_id: snapshot.task_id, snapshot_digest: snapshotDigest(snapshot) };
|
|
167
247
|
if (findings.length) return { ...base, decision: "rework", findings };
|
|
248
|
+
const disclosure = disclosed.length
|
|
249
|
+
? `; ${disclosed.map(item => item.summary).join("; ")}` : "";
|
|
168
250
|
const executionDigest = createHash("sha256").update(JSON.stringify({
|
|
169
251
|
tree, platform: process.platform, arch: process.arch, observations: evidence,
|
|
170
252
|
})).digest("hex");
|
|
171
253
|
return { ...base, decision: "pass", approval: { kind: "qa", authority_role: "qa",
|
|
172
|
-
summary: `all ${
|
|
254
|
+
summary: `all ${totalChecks} project verification check(s) passed${disclosure}; execution=sha256:${executionDigest}` },
|
|
255
|
+
...(disclosed.length ? { findings: disclosed } : {}) };
|
|
173
256
|
}
|
|
174
257
|
|
|
@@ -19,6 +19,14 @@ export function qaEvidenceFreshnessId(snapshotDigest: string): string {
|
|
|
19
19
|
return `qa-evidence-freshness-${digest16}-${attemptRef(snapshotDigest)}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/** Summary prefix that marks a baseline failure also present at `base_head`. */
|
|
23
|
+
export const PRE_EXISTING_BASELINE_PREFIX = "pre-existing baseline failure:";
|
|
24
|
+
|
|
25
|
+
/** Digest-scoped id for a project baseline check finding; `acceptance_id` stays null. */
|
|
26
|
+
export function qaBaselineFindingId(checkId: string, snapshotDigest: string): string {
|
|
27
|
+
return `qa-baseline-${checkId}-${attemptRef(snapshotDigest)}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
22
30
|
function attemptRef(snapshotDigest: string): string {
|
|
23
31
|
const digest8 = snapshotDigest.slice("sha256:".length, "sha256:".length + 8);
|
|
24
32
|
return `${digest8}-${randomUUID().slice(0, 6)}`;
|
|
@@ -287,9 +287,54 @@ export interface ReviewManifestV5 {
|
|
|
287
287
|
lifecycle: string;
|
|
288
288
|
artifact_state: string;
|
|
289
289
|
risk: string;
|
|
290
|
+
/** Round 3 and later only: the prior round's reviewed commit. */
|
|
291
|
+
prior_review_commit?: string;
|
|
292
|
+
/** Paths whose tree entries differ between the prior and current review trees. */
|
|
293
|
+
delta_paths?: string[];
|
|
294
|
+
/** Round 3 and later only: why no delta is available, so the rule is skipped. */
|
|
295
|
+
delta_skipped?: "prior_review_revision_absent" | "prior_review_revision_unresolvable";
|
|
290
296
|
manifest_digest: string;
|
|
291
297
|
}
|
|
292
298
|
|
|
299
|
+
/** The first Review round that judges only the rework delta. */
|
|
300
|
+
export const REVIEW_DELTA_FROM_ROUND = 3;
|
|
301
|
+
|
|
302
|
+
export interface ReviewDeltaInput {
|
|
303
|
+
/** The round this Review opens, derived from the Kernel's review findings. */
|
|
304
|
+
round: number;
|
|
305
|
+
/** The revision the previous round reviewed; null for a legacy record. */
|
|
306
|
+
prior: { review_commit: string; review_tree: string } | null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function reviewDeltaFields(
|
|
310
|
+
root: string,
|
|
311
|
+
delta: ReviewDeltaInput | undefined,
|
|
312
|
+
currentTree: string,
|
|
313
|
+
): Pick<ReviewManifestV5, "prior_review_commit" | "delta_paths" | "delta_skipped"> {
|
|
314
|
+
if (!delta || delta.round < REVIEW_DELTA_FROM_ROUND) return {};
|
|
315
|
+
if (!delta.prior) return { delta_skipped: "prior_review_revision_absent" };
|
|
316
|
+
try {
|
|
317
|
+
gitEvidence(root, ["cat-file", "-e", `${delta.prior.review_commit}^{commit}`]);
|
|
318
|
+
gitEvidence(root, ["cat-file", "-e", `${delta.prior.review_tree}^{tree}`]);
|
|
319
|
+
const names = gitEvidenceBytes(root, [
|
|
320
|
+
"diff-tree",
|
|
321
|
+
"-r",
|
|
322
|
+
"--name-only",
|
|
323
|
+
"--no-renames",
|
|
324
|
+
"-z",
|
|
325
|
+
delta.prior.review_tree,
|
|
326
|
+
currentTree,
|
|
327
|
+
]).toString("utf8");
|
|
328
|
+
return {
|
|
329
|
+
prior_review_commit: delta.prior.review_commit,
|
|
330
|
+
delta_paths: names.split("\0").filter(Boolean).sort(),
|
|
331
|
+
};
|
|
332
|
+
} catch {
|
|
333
|
+
// A pruned or unreachable prior revision must never block Review.
|
|
334
|
+
return { delta_skipped: "prior_review_revision_unresolvable" };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
293
338
|
function manifestDigest(manifest: Omit<ReviewManifestV5, "manifest_digest">): string {
|
|
294
339
|
return `sha256:${createHash("sha256").update(JSON.stringify(manifest)).digest("hex")}`;
|
|
295
340
|
}
|
|
@@ -485,6 +530,8 @@ export function captureReviewManifest(
|
|
|
485
530
|
artifactState: string;
|
|
486
531
|
risk: string;
|
|
487
532
|
outcomes: Record<string, ReviewOutcome>;
|
|
533
|
+
/** Present once the task has a Review history; absent keeps rounds 1-2 byte-identical. */
|
|
534
|
+
delta?: ReviewDeltaInput;
|
|
488
535
|
},
|
|
489
536
|
): ReviewManifestV5 {
|
|
490
537
|
const { snapshot, revision } = publishInput(root, input);
|
|
@@ -508,6 +555,7 @@ export function captureReviewManifest(
|
|
|
508
555
|
lifecycle: input.lifecycle,
|
|
509
556
|
artifact_state: input.artifactState,
|
|
510
557
|
risk: input.risk,
|
|
558
|
+
...reviewDeltaFields(root, input.delta, revision.review_tree),
|
|
511
559
|
};
|
|
512
560
|
const manifest = { ...unsigned, manifest_digest: manifestDigest(unsigned) };
|
|
513
561
|
if (Buffer.byteLength(JSON.stringify(manifest)) > MAX_REVIEW_BUNDLE_BYTES)
|