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
|
@@ -12,8 +12,9 @@ scoped implementation, staging and focused diagnostics. Enrollment itself does
|
|
|
12
12
|
not start QA; the Parent owns the subsequent Kernel Assurance and foreground
|
|
13
13
|
Review handoff under the same still-valid batch authorization.
|
|
14
14
|
|
|
15
|
-
Before handoff, run the permitted diagnostic checks
|
|
16
|
-
|
|
15
|
+
Before handoff, run the permitted diagnostic checks, complete the Delivery
|
|
16
|
+
Evidence check below and return its table to the Parent as structured diagnostic
|
|
17
|
+
evidence. The read-only Loop
|
|
17
18
|
runtime action only constructs the dispatch envelope; it does not store
|
|
18
19
|
evidence or change task state. Preserve failed and blocked attempts. Do not
|
|
19
20
|
perform QA,
|
|
@@ -21,6 +22,38 @@ review, plan mutation, successor approval, Compounder work, or authority
|
|
|
21
22
|
writes. If the requested change needs scope expansion, stop and return an
|
|
22
23
|
`imm-planner` route with the concrete missing scope and verification reason.
|
|
23
24
|
|
|
25
|
+
## Delivery Evidence
|
|
26
|
+
|
|
27
|
+
Reuse the TaskIntent and any bound Spec's agreed seams and controls. Before the
|
|
28
|
+
first Assurance attempt, return one compact row per acceptance; update affected
|
|
29
|
+
rows after repair. This is handoff evidence, not a new artifact or approval gate.
|
|
30
|
+
|
|
31
|
+
| Acceptance / invariant | Entry and consumers | Positive / negative / boundary controls | Command, outcome and input | Finding coverage |
|
|
32
|
+
|---|---|---|---|---|
|
|
33
|
+
|
|
34
|
+
For changed shared behavior, trace the real entry through its state owner,
|
|
35
|
+
write, serialization, reload and downstream consumer as applicable. Include
|
|
36
|
+
both affected hosts and recovery/replay branches. An async, return-type or
|
|
37
|
+
exception-contract change requires checking every affected caller's await,
|
|
38
|
+
cancellation and error propagation.
|
|
39
|
+
|
|
40
|
+
Use the real producer for the success control where available. Derive rejection
|
|
41
|
+
controls from a successful state, change only the target condition, and assert
|
|
42
|
+
the intended rejection reason and required side effects. A control must fail
|
|
43
|
+
when its target protection is removed. For byte-preservation claims compare the
|
|
44
|
+
relevant raw files, index and refs; for observed behavior link each required
|
|
45
|
+
criterion to its concrete observation.
|
|
46
|
+
|
|
47
|
+
Name whether each check consumed worktree or delivery bytes. Before Assurance,
|
|
48
|
+
verify task-owned changes are staged within scope, generated mirrors match,
|
|
49
|
+
and applicable type/build checks pass. On QA failure, reproduce the reported
|
|
50
|
+
failure under its delivery conditions before another attempt. For rework, name
|
|
51
|
+
the violated invariant, all known trigger classes and their controls in Finding
|
|
52
|
+
coverage; account for affected callers sharing that invariant within scope.
|
|
53
|
+
Record missing evidence as blocked and repair it before handoff. The Loop owns
|
|
54
|
+
verdict submission, finding disposition and the second-rework generalize-or-refute
|
|
55
|
+
rule.
|
|
56
|
+
|
|
24
57
|
## Code Quality Guard
|
|
25
58
|
|
|
26
59
|
Before handoff, check the implementation for real implementation rather than
|
|
@@ -15,8 +15,10 @@ import {
|
|
|
15
15
|
VerificationAbortedError,
|
|
16
16
|
} from "./verification";
|
|
17
17
|
import { createInvocationRegistry, type InvocationState, type InvocationToken } from "./invocations";
|
|
18
|
-
import type { ReviewBundle, ReviewManifestV5
|
|
19
|
-
import {
|
|
18
|
+
import type { ReviewBundle, ReviewManifestV5 } from "./review_evidence";
|
|
19
|
+
import { createVerdictAuthority, type VerdictAuthority } from "./verdict_authority";
|
|
20
|
+
import type { ConfirmationReferenceSource } from "./host_port";
|
|
21
|
+
import { buildRoleDelegationPacket, STATIC_REVIEW_RULES } from "../role_prompt_bridge";
|
|
20
22
|
import type { AssuranceProjectionResult } from "../kernel/assurance_projection";
|
|
21
23
|
import type { TaskIntentIdentityToken } from "../kernel/intent_token_registry";
|
|
22
24
|
import type { AssuranceHostPort, HostReviewReservation } from "./host_port";
|
|
@@ -316,7 +318,10 @@ export interface ForegroundToolUpdate {
|
|
|
316
318
|
details: Record<string, unknown>;
|
|
317
319
|
}
|
|
318
320
|
|
|
321
|
+
/** Wall-clock milliseconds spent per advance stage; the stage names are the progress stages. */
|
|
322
|
+
export interface AssuranceTimings { total_ms: number; stage_ms: Record<string, number> }
|
|
319
323
|
type AssuranceRecoveryFields = {
|
|
324
|
+
timings?: AssuranceTimings;
|
|
320
325
|
diagnostics?: QaCheckDiagnostic[];
|
|
321
326
|
environment_failure?: boolean;
|
|
322
327
|
recovery?: AssuranceRecovery;
|
|
@@ -343,7 +348,7 @@ export type AssuranceSubmitReviewResult = AssuranceRecoveryFields & (
|
|
|
343
348
|
| { state: "review_preparation_failed"; operation: "review"; operation_id: string; reason: string }
|
|
344
349
|
| { state: "completed" }
|
|
345
350
|
| { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string }
|
|
346
|
-
| { state: "blocked"; reason: string; code?: "verdict_invalid" });
|
|
351
|
+
| { state: "blocked"; reason: string; code?: "verdict_invalid"; recovery_action?: string });
|
|
347
352
|
|
|
348
353
|
export type ActiveAssuranceState =
|
|
349
354
|
| { state: "running"; operation: "qa"; operation_id: string; deadline_seconds: number }
|
|
@@ -352,51 +357,17 @@ export type ActiveAssuranceState =
|
|
|
352
357
|
|
|
353
358
|
export interface AssuranceCoordinatorPorts {
|
|
354
359
|
host: AssuranceHostPort;
|
|
360
|
+
confirmationReference: ConfirmationReferenceSource;
|
|
361
|
+
onReworkApplied?: (ctx: HostContext, taskId: string, findingsCount: number) => void;
|
|
355
362
|
projectTask(root: string, taskId: string): Promise<AssuranceProjectionResult>;
|
|
356
363
|
readTaskRecord(root: string, taskId: string): Promise<TaskRecordRead>;
|
|
357
364
|
readTaskIntent(root: string, taskId: string): Promise<TaskIntentRead>;
|
|
358
|
-
buildAssurance(
|
|
359
|
-
root: string,
|
|
360
|
-
taskId: string,
|
|
361
|
-
role: AssuranceRole,
|
|
362
|
-
projection: AssuranceProjectionResult,
|
|
363
|
-
): Promise<{
|
|
364
|
-
snapshot: SnapshotDescriptor;
|
|
365
|
-
descriptors: Map<string, VerificationDescriptor>;
|
|
366
|
-
reviewBundle: ReviewBundle | null;
|
|
367
|
-
reviewManifest?: ReviewManifestV5 | null;
|
|
368
|
-
}>;
|
|
369
|
-
/**
|
|
370
|
-
* Publish and prove the deterministic task-scoped revision before QA runs, so
|
|
371
|
-
* a preparation failure can never consume or fake a settled QA attestation.
|
|
372
|
-
* Returns null for records still on the legacy v3 bundle path.
|
|
373
|
-
*/
|
|
374
|
-
ensureReviewRevision?(
|
|
375
|
-
root: string,
|
|
376
|
-
taskId: string,
|
|
377
|
-
projection: AssuranceProjectionResult,
|
|
378
|
-
): Promise<ReviewRevision | null>;
|
|
379
365
|
runQa(
|
|
380
366
|
snapshot: SnapshotDescriptor,
|
|
381
367
|
descriptors: Map<string, VerificationDescriptor>,
|
|
382
368
|
options: { signal?: AbortSignal; onProgress?: (progress: QaVerificationProgress) => void },
|
|
383
369
|
): Promise<AssuranceVerdict>;
|
|
384
370
|
writeReviewEvidence(input: { snapshot: SnapshotDescriptor; evidence: ReviewBundle | ReviewManifestV5 }): { path: string; remove(): void };
|
|
385
|
-
applyVerdict(
|
|
386
|
-
ctx: HostContext,
|
|
387
|
-
input: {
|
|
388
|
-
taskId: string;
|
|
389
|
-
snapshot: SnapshotDescriptor;
|
|
390
|
-
verdict: AssuranceVerdict;
|
|
391
|
-
invocation: InvocationToken;
|
|
392
|
-
actorId: string;
|
|
393
|
-
hooks?: {
|
|
394
|
-
beforeCommit?: () => Promise<void>;
|
|
395
|
-
onCommit?: () => void;
|
|
396
|
-
afterCommit?: () => Promise<void>;
|
|
397
|
-
};
|
|
398
|
-
},
|
|
399
|
-
): Promise<void>;
|
|
400
371
|
applyOrdinaryOperation(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string } }): Promise<unknown>;
|
|
401
372
|
advanceBeforeProjection?: () => Promise<void>;
|
|
402
373
|
qaBeforeProjection?: () => Promise<void>;
|
|
@@ -404,12 +375,23 @@ export interface AssuranceCoordinatorPorts {
|
|
|
404
375
|
qaOnAuthorityCommit?: () => void;
|
|
405
376
|
qaAfterAuthorityCommit?: () => Promise<void>;
|
|
406
377
|
qaJobTimeoutMs?: number;
|
|
378
|
+
/** Attach per-stage `timings` to advance results. Defaults to IMM_ASSURANCE_TIMINGS=1. */
|
|
379
|
+
reportTimings?: boolean;
|
|
380
|
+
/** Overrides QA_MAX_IDENTICAL_FAILURES; tests only. */
|
|
381
|
+
qaMaxIdenticalFailures?: number;
|
|
407
382
|
}
|
|
408
383
|
|
|
409
384
|
export const QA_MIN_JOB_TIMEOUT_SECONDS = 15 * 60;
|
|
410
385
|
export const QA_MAX_JOB_TIMEOUT_SECONDS = 60 * 60;
|
|
411
386
|
export const QA_JOB_OVERHEAD_SECONDS = 2 * 60;
|
|
412
387
|
export const QA_JOB_TIMEOUT_SECONDS = QA_MIN_JOB_TIMEOUT_SECONDS;
|
|
388
|
+
/**
|
|
389
|
+
* A QA run that fails before attestation writes no authority, so the Kernel
|
|
390
|
+
* would happily rerun the same descriptors forever. After this many identical
|
|
391
|
+
* failures on the same snapshot (record revision, intent hash, diff hash) the
|
|
392
|
+
* coordinator refuses to rerun until the snapshot changes.
|
|
393
|
+
*/
|
|
394
|
+
export const QA_MAX_IDENTICAL_FAILURES = 2;
|
|
413
395
|
export const REVIEW_PREPARATION_TIMEOUT_MS = 30_000;
|
|
414
396
|
export const REVIEW_DISPATCH_TIMEOUT_MS = 120_000;
|
|
415
397
|
export const REVIEW_VERDICT_VALIDATION_TIMEOUT_MS = 30_000;
|
|
@@ -464,19 +446,19 @@ export function snapshotDigest(snapshot: SnapshotDescriptor): string {
|
|
|
464
446
|
return `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
|
|
465
447
|
}
|
|
466
448
|
|
|
467
|
-
|
|
449
|
+
/**
|
|
450
|
+
* The per-dispatch Review facts: which evidence to read, which revision it pins,
|
|
451
|
+
* what the acceptances assert, and the exact verdict shapes.
|
|
452
|
+
*
|
|
453
|
+
* This is the whole prompt for a Host whose reviewer definition already carries
|
|
454
|
+
* the role contract and `STATIC_REVIEW_RULES` — the Claude Host. A Host whose
|
|
455
|
+
* dispatched agent carries no instructions (Pi) gets `buildReviewPrompt`, which
|
|
456
|
+
* prepends both.
|
|
457
|
+
*/
|
|
458
|
+
export function buildReviewSnapshotPrompt(snapshot: SnapshotDescriptor, evidencePath?: string): string {
|
|
468
459
|
if (snapshot.role !== "review") throw new Error("native review prompt requires review role");
|
|
469
460
|
const acceptance = snapshot.acceptance.map((item) => `- ${item.id}: ${item.assertion}`).join("\n");
|
|
470
461
|
const digest = snapshotDigest(snapshot);
|
|
471
|
-
const rolePacket = buildRoleDelegationPacket({
|
|
472
|
-
role: "code-review",
|
|
473
|
-
context: {
|
|
474
|
-
task_id: snapshot.task_id,
|
|
475
|
-
review_gate: "imm-code-review",
|
|
476
|
-
changed_files_signature: snapshot.diff_hash,
|
|
477
|
-
snapshot_digest: digest,
|
|
478
|
-
},
|
|
479
|
-
});
|
|
480
462
|
const revision = snapshot.review_revision;
|
|
481
463
|
const evidenceContract = revision
|
|
482
464
|
? [
|
|
@@ -492,25 +474,44 @@ export function buildReviewPrompt(snapshot: SnapshotDescriptor, evidencePath?: s
|
|
|
492
474
|
`The user-selected worktree may contain staged task changes that are absent from the isolated reviewer worktree. Review authority is bound only to the bundle dirty_files current_content bytes and committed HEAD provenance. Analyze code exclusively from those bundle bytes; repository file reads are permitted only for the provenance git commands above. A symbol present in current_content but absent from HEAD is the task change, not an absence.`,
|
|
493
475
|
];
|
|
494
476
|
return [
|
|
495
|
-
rolePacket.prompt,
|
|
496
477
|
...evidenceContract,
|
|
497
|
-
`Do not edit files, create files, run mutating commands, or change Git state. Focus on correctness, regressions, security, and missing tests.`,
|
|
498
|
-
`Execution outcomes for every acceptance were verified deterministically by the Kernel QA layer before this review and are embedded in this bundle under outcomes (the immutable evidence file, acceptance_id -> {status, summary}); do not re-execute descriptors and do not treat the absence of local test runs as a finding. Your review covers evidence provenance, code correctness, regressions, security, and missing tests against the embedded assertions and code.`,
|
|
499
478
|
`Snapshot digest: ${digest}`,
|
|
500
479
|
`TaskRecord revision: ${snapshot.record_revision}`,
|
|
501
480
|
revision
|
|
502
481
|
? `Intent revision ${snapshot.intent_revision} (hash ${snapshot.intent_content_hash}), diff ${snapshot.diff_hash}, review revision ${revision.review_commit} (base ${revision.base_head}, tree ${revision.review_tree}, manifest ${revision.manifest_digest}), state ${snapshot.lifecycle}:${snapshot.artifact_state}.`
|
|
503
482
|
: `Intent revision ${snapshot.intent_revision} (hash ${snapshot.intent_content_hash}), diff ${snapshot.diff_hash}, review bundle ${snapshot.review_bundle_digest}, state ${snapshot.lifecycle}:${snapshot.artifact_state}.`,
|
|
504
483
|
"Acceptance assertions:", acceptance,
|
|
505
|
-
|
|
506
|
-
`
|
|
507
|
-
`PASS shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"pass","approval":{"kind":"review","authority_role":"reviewer","summary":"<one line>"}}`,
|
|
508
|
-
`A pass verdict may carry non-blocking notes as findings, but every one of them must set kind "advisory"; a blocking finding is a rework verdict and must omit approval: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"pass","approval":{"kind":"review","authority_role":"reviewer","summary":"<one line>"},"findings":[{"id":"review-1","kind":"advisory","acceptance_id":"<id|null>","summary":"<one line>","evidence":{"trigger":"<concrete inputs or state>","caller_chain":["<path-or-symbol>"],"violated":{"kind":"acceptance|security_boundary","ref":"<acceptance id or boundary>"}}}]}`,
|
|
484
|
+
`PASS shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"pass","approval":{"kind":"review","authority_role":"reviewer","summary":"<one line>","inspected_paths":["<every changed path of the reviewed change set>"]}}`,
|
|
485
|
+
`A pass verdict may carry non-blocking notes as findings, but every one of them must set kind "advisory"; a blocking finding is a rework verdict and must omit approval: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"pass","approval":{"kind":"review","authority_role":"reviewer","summary":"<one line>","inspected_paths":["<every changed path of the reviewed change set>"]},"findings":[{"id":"review-1","kind":"advisory","acceptance_id":"<id|null>","summary":"<one line>","evidence":{"trigger":"<concrete inputs or state>","caller_chain":["<path-or-symbol>"],"violated":{"kind":"acceptance|security_boundary","ref":"<acceptance id or boundary>"}}}]}`,
|
|
509
486
|
`REWORK shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"rework","findings":[{"id":"review-1","kind":"blocking|advisory","acceptance_id":"<id|null>","summary":"<one line>","evidence":{"trigger":"<concrete inputs or state>","caller_chain":["<path-or-symbol>"],"violated":{"kind":"acceptance|security_boundary","ref":"<acceptance id or boundary>"}}}]}`,
|
|
510
487
|
`REWORK verdicts must omit the approval field entirely; do not emit "approval": null.`,
|
|
511
488
|
].join("\n");
|
|
512
489
|
}
|
|
513
490
|
|
|
491
|
+
/**
|
|
492
|
+
* The complete Review prompt: the internal role contract, the static review
|
|
493
|
+
* rules, and the per-dispatch facts. Dispatched by a Host whose agent carries no
|
|
494
|
+
* instructions of its own.
|
|
495
|
+
*/
|
|
496
|
+
export function buildReviewPrompt(snapshot: SnapshotDescriptor, evidencePath?: string): string {
|
|
497
|
+
if (snapshot.role !== "review") throw new Error("native review prompt requires review role");
|
|
498
|
+
const digest = snapshotDigest(snapshot);
|
|
499
|
+
const rolePacket = buildRoleDelegationPacket({
|
|
500
|
+
role: "code-review",
|
|
501
|
+
context: {
|
|
502
|
+
task_id: snapshot.task_id,
|
|
503
|
+
review_gate: "imm-code-review",
|
|
504
|
+
changed_files_signature: snapshot.diff_hash,
|
|
505
|
+
snapshot_digest: digest,
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
return [
|
|
509
|
+
rolePacket.prompt,
|
|
510
|
+
...STATIC_REVIEW_RULES,
|
|
511
|
+
buildReviewSnapshotPrompt(snapshot, evidencePath),
|
|
512
|
+
].join("\n");
|
|
513
|
+
}
|
|
514
|
+
|
|
514
515
|
function parseVerdictEvidence(value: unknown, index: number): FindingEvidence {
|
|
515
516
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
516
517
|
throw new Error(`finding ${index} evidence is required`);
|
|
@@ -579,7 +580,28 @@ export function parseAssuranceVerdict(input: unknown, snapshot: SnapshotDescript
|
|
|
579
580
|
const expectedKind = snapshot.role === "qa" ? "qa" : "review";
|
|
580
581
|
const expectedRole = snapshot.role === "qa" ? "qa" : "reviewer";
|
|
581
582
|
if (!approval || approval.kind !== expectedKind || approval.authority_role !== expectedRole || typeof approval.summary !== "string" || !approval.summary.trim()) throw new Error("pass verdict approval is invalid");
|
|
582
|
-
|
|
583
|
+
// A Review pass must claim the paths it inspected: BR-DEC-3 converts silent
|
|
584
|
+
// partial coverage into an explicit false statement. The list is checked
|
|
585
|
+
// here against the reviewed change set and removed before settlement, so
|
|
586
|
+
// the Kernel approval and TaskRecord never see it. QA verdicts treat the
|
|
587
|
+
// field as unknown, exactly as before.
|
|
588
|
+
if (snapshot.role === "review") {
|
|
589
|
+
const required = snapshot.dirty_files;
|
|
590
|
+
const listed = approval.inspected_paths;
|
|
591
|
+
if (listed === undefined) throw new Error(`review pass verdict approval.inspected_paths is required; it must list every reviewed changed path${required.length ? `: ${required.join(", ")}` : " (none for this change set)"}`);
|
|
592
|
+
if (!Array.isArray(listed) || listed.some((entry) => typeof entry !== "string" || !entry.trim())) throw new Error("review pass verdict approval.inspected_paths must be an array of repository-relative path strings");
|
|
593
|
+
const seen = new Set<string>();
|
|
594
|
+
for (const entry of listed as string[]) {
|
|
595
|
+
if (seen.has(entry)) throw new Error(`review pass verdict approval.inspected_paths lists a duplicate path: ${entry}`);
|
|
596
|
+
seen.add(entry);
|
|
597
|
+
}
|
|
598
|
+
const foreign = (listed as string[]).filter((entry) => !required.includes(entry));
|
|
599
|
+
if (foreign.length) throw new Error(`review pass verdict approval.inspected_paths lists a path outside the reviewed change set: ${foreign.join(", ")}`);
|
|
600
|
+
const missing = required.filter((entry) => !seen.has(entry));
|
|
601
|
+
if (missing.length) throw new Error(`review pass verdict approval.inspected_paths omits reviewed changed paths: ${missing.join(", ")}`);
|
|
602
|
+
}
|
|
603
|
+
const allowedApproval = snapshot.role === "review" ? ["kind", "authority_role", "summary", "inspected_paths"] : ["kind", "authority_role", "summary"];
|
|
604
|
+
const unknownApproval = Object.keys(approval).find((key) => !allowedApproval.includes(key));
|
|
583
605
|
if (unknownApproval) throw new Error(`pass verdict approval has unknown field: ${unknownApproval}`);
|
|
584
606
|
const passFindings = raw.findings === undefined ? [] : parseVerdictFindings(raw.findings as unknown[], snapshot);
|
|
585
607
|
if (passFindings.some((finding) => finding.kind !== "advisory")) throw new Error("pass verdict findings must all be advisory");
|
|
@@ -705,12 +727,21 @@ export class AssuranceCoordinator {
|
|
|
705
727
|
private readonly operationControllers = new Map<string, { operationId: string; controller: AbortController }>();
|
|
706
728
|
private readonly reviewReservations = new Map<string, ReviewReservation>();
|
|
707
729
|
private readonly rejectedReviewOperations = new Map<string, { operationId: string; reason: string }>();
|
|
730
|
+
private readonly qaFailureStreaks = new Map<string, { key: string; count: number }>();
|
|
731
|
+
private readonly qaAttemptKeys = new Map<string, string>();
|
|
708
732
|
private readonly unknownOperations = new Map<string, { operation: "qa" | "review"; operationId: string; reason: string }>();
|
|
709
733
|
private readonly sessionInvocations = new Set<InvocationToken>();
|
|
710
734
|
private sessionActive = true;
|
|
711
735
|
private sessionGeneration = 0;
|
|
712
736
|
|
|
713
|
-
|
|
737
|
+
readonly authority: VerdictAuthority;
|
|
738
|
+
constructor(private readonly ports: AssuranceCoordinatorPorts, authorityOverrides: Partial<VerdictAuthority> = {}) {
|
|
739
|
+
this.authority = { ...createVerdictAuthority({
|
|
740
|
+
confirmationReference: ports.confirmationReference,
|
|
741
|
+
commitInvocation: token => this.commitInvocation(token),
|
|
742
|
+
onReworkApplied: ports.onReworkApplied,
|
|
743
|
+
}), ...authorityOverrides };
|
|
744
|
+
}
|
|
714
745
|
|
|
715
746
|
onSessionStart(): void {
|
|
716
747
|
this.sessionActive = true;
|
|
@@ -731,6 +762,8 @@ export class AssuranceCoordinator {
|
|
|
731
762
|
this.removeEvidence(reservation);
|
|
732
763
|
}
|
|
733
764
|
this.reviewReservations.clear();
|
|
765
|
+
this.qaFailureStreaks.clear();
|
|
766
|
+
this.qaAttemptKeys.clear();
|
|
734
767
|
for (const invocation of [...this.sessionInvocations]) this.closeSessionInvocation(invocation);
|
|
735
768
|
}
|
|
736
769
|
|
|
@@ -762,7 +795,19 @@ export class AssuranceCoordinator {
|
|
|
762
795
|
|
|
763
796
|
async advance(taskId: string, ctx: HostContext, signal?: AbortSignal, onUpdate?: (update: ForegroundToolUpdate) => void): Promise<AssuranceAdvanceResult> {
|
|
764
797
|
const checks = new Map<string, QaCheckDiagnostic>();
|
|
798
|
+
const started = performance.now();
|
|
799
|
+
const stageMs: Record<string, number> = {};
|
|
800
|
+
let stage = "preparing";
|
|
801
|
+
let stageStarted = started;
|
|
802
|
+
const closeStage = (now: number) => { stageMs[stage] = (stageMs[stage] ?? 0) + Math.round(now - stageStarted); };
|
|
765
803
|
const result = await this.advanceOnce(taskId, ctx, signal, update => {
|
|
804
|
+
const next = update.details.stage;
|
|
805
|
+
if (typeof next === "string" && next !== stage) {
|
|
806
|
+
const now = performance.now();
|
|
807
|
+
closeStage(now);
|
|
808
|
+
stage = next;
|
|
809
|
+
stageStarted = now;
|
|
810
|
+
}
|
|
766
811
|
const diagnostic = update.details.diagnostic as QaCheckDiagnostic | undefined;
|
|
767
812
|
if (diagnostic) {
|
|
768
813
|
checks.set(diagnostic.descriptor_ref, diagnostic);
|
|
@@ -772,8 +817,25 @@ export class AssuranceCoordinator {
|
|
|
772
817
|
}
|
|
773
818
|
onUpdate?.(update);
|
|
774
819
|
});
|
|
820
|
+
this.recordQaAttempt(taskId, result);
|
|
775
821
|
const diagnostics = result.diagnostics ?? [...checks.values()];
|
|
776
|
-
|
|
822
|
+
const enriched = diagnostics.length ? { ...result, diagnostics } : result;
|
|
823
|
+
if (!(this.ports.reportTimings ?? process.env.IMM_ASSURANCE_TIMINGS === "1")) return this.withRecovery(taskId, ctx, enriched);
|
|
824
|
+
const finished = performance.now();
|
|
825
|
+
closeStage(finished);
|
|
826
|
+
return this.withRecovery(taskId, ctx, { ...enriched, timings: { total_ms: Math.round(finished - started), stage_ms: stageMs } });
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
private recordQaAttempt(taskId: string, result: AssuranceAdvanceResult): void {
|
|
830
|
+
const key = this.qaAttemptKeys.get(taskId);
|
|
831
|
+
this.qaAttemptKeys.delete(taskId);
|
|
832
|
+
if (!key || result.state === "cancelled") return;
|
|
833
|
+
if (result.state !== "failed" || result.operation !== "qa") {
|
|
834
|
+
this.qaFailureStreaks.delete(taskId);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const streak = this.qaFailureStreaks.get(taskId);
|
|
838
|
+
this.qaFailureStreaks.set(taskId, { key, count: streak?.key === key ? streak.count + 1 : 1 });
|
|
777
839
|
}
|
|
778
840
|
|
|
779
841
|
private async withRecovery<T extends AssuranceAdvanceResult | AssuranceSubmitReviewResult>(taskId: string, ctx: HostContext, enriched: T): Promise<T> {
|
|
@@ -926,9 +988,9 @@ export class AssuranceCoordinator {
|
|
|
926
988
|
progress("preparing_review_revision", "Proving the immutable Review revision before QA");
|
|
927
989
|
try {
|
|
928
990
|
if (parked.record?.contract === "assurance_kernel/task_record/v4") {
|
|
929
|
-
if (!this.
|
|
991
|
+
if (!this.authority.ensureReviewRevision)
|
|
930
992
|
throw new Error("v4 Review revision preparation is unavailable");
|
|
931
|
-
await this.
|
|
993
|
+
await this.authority.ensureReviewRevision(ctx.cwd, taskId, projection);
|
|
932
994
|
}
|
|
933
995
|
} catch (error) {
|
|
934
996
|
return { state: "blocked", reason: `review revision preparation failed: ${boundedAssuranceError(error)}` };
|
|
@@ -942,12 +1004,18 @@ export class AssuranceCoordinator {
|
|
|
942
1004
|
progress("capturing_snapshot", "Capturing the immutable QA snapshot");
|
|
943
1005
|
await this.ports.qaBeforeProjection?.();
|
|
944
1006
|
ensureOperationLive();
|
|
945
|
-
const assurance = await this.
|
|
1007
|
+
const assurance = await this.authority.buildAssurance(ctx.cwd, taskId, "qa", projection);
|
|
946
1008
|
ensureOperationLive();
|
|
947
1009
|
// Preparation counts against the same ceiling as the checks, so the
|
|
948
1010
|
// aggregate clock starts before the first prepared group runs. A host
|
|
949
1011
|
// may only tighten the derived budget, never raise it past the ceiling.
|
|
950
|
-
const
|
|
1012
|
+
const qaKey = `${assurance.snapshot.record_revision}|${assurance.snapshot.intent_content_hash}|${assurance.snapshot.diff_hash}`;
|
|
1013
|
+
const streak = this.qaFailureStreaks.get(taskId);
|
|
1014
|
+
const maxFailures = this.ports.qaMaxIdenticalFailures ?? QA_MAX_IDENTICAL_FAILURES;
|
|
1015
|
+
if (streak?.key === qaKey && streak.count >= maxFailures)
|
|
1016
|
+
return { state: "blocked", reason: `deterministic QA already failed ${streak.count} times on this exact snapshot; change the workspace diff or intent, or restart the session after fixing the environment, before retrying` };
|
|
1017
|
+
this.qaAttemptKeys.set(taskId, qaKey);
|
|
1018
|
+
const declaredQaJobMs = deriveQaJobTimeoutMs(assurance.descriptors.values());
|
|
951
1019
|
const qaJobTimeoutMs = Math.min(declaredQaJobMs, this.ports.qaJobTimeoutMs ?? declaredQaJobMs);
|
|
952
1020
|
const qaJobDeadline = setTimeout(() => {
|
|
953
1021
|
qaJobBudgetExceeded = true;
|
|
@@ -974,7 +1042,7 @@ export class AssuranceCoordinator {
|
|
|
974
1042
|
try {
|
|
975
1043
|
progress("settling_qa", "Settling deterministic QA through the Kernel revision boundary");
|
|
976
1044
|
authorityBoundaryStarted = true;
|
|
977
|
-
await this.
|
|
1045
|
+
await this.authority.applyVerdict(ctx, {
|
|
978
1046
|
taskId,
|
|
979
1047
|
snapshot: assurance.snapshot,
|
|
980
1048
|
verdict: qaVerdict,
|
|
@@ -1037,10 +1105,10 @@ export class AssuranceCoordinator {
|
|
|
1037
1105
|
authorityBoundaryStarted = false;
|
|
1038
1106
|
if (aborted()) return this.reviewPreparationFailed(taskId, operationId, "host cancellation after QA authority settlement");
|
|
1039
1107
|
progress("preparing_review", "Preparing the reserved foreground Review evidence");
|
|
1040
|
-
let review: Awaited<ReturnType<
|
|
1108
|
+
let review: Awaited<ReturnType<VerdictAuthority["buildAssurance"]>>;
|
|
1041
1109
|
let evidence: { path: string; remove(): void };
|
|
1042
1110
|
try {
|
|
1043
|
-
review = await this.
|
|
1111
|
+
review = await this.authority.buildAssurance(ctx.cwd, taskId, "review", fresh);
|
|
1044
1112
|
const payload = review.reviewManifest ?? review.reviewBundle;
|
|
1045
1113
|
if (!payload) throw new Error("review evidence is missing after QA settlement");
|
|
1046
1114
|
evidence = this.ports.writeReviewEvidence({ snapshot: review.snapshot, evidence: payload });
|
|
@@ -1063,6 +1131,7 @@ export class AssuranceCoordinator {
|
|
|
1063
1131
|
taskId,
|
|
1064
1132
|
operationId,
|
|
1065
1133
|
prompt: buildReviewPrompt(review.snapshot, evidence.path),
|
|
1134
|
+
snapshotPrompt: buildReviewSnapshotPrompt(review.snapshot, evidence.path),
|
|
1066
1135
|
evidencePath: evidence.path,
|
|
1067
1136
|
maxTurns: reviewTurnBudget(workload),
|
|
1068
1137
|
});
|
|
@@ -1149,9 +1218,9 @@ export class AssuranceCoordinator {
|
|
|
1149
1218
|
}
|
|
1150
1219
|
if (reservation.snapshot.review_revision) {
|
|
1151
1220
|
try {
|
|
1152
|
-
if (!this.
|
|
1221
|
+
if (!this.authority.ensureReviewRevision)
|
|
1153
1222
|
throw new Error("v4 Review revision verification is unavailable");
|
|
1154
|
-
const revision = await this.
|
|
1223
|
+
const revision = await this.authority.ensureReviewRevision(ctx.cwd, taskId, fresh);
|
|
1155
1224
|
if (!revision || revision.base_head !== reservation.snapshot.review_revision.base_head || revision.review_commit !== reservation.snapshot.review_revision.review_commit || revision.review_tree !== reservation.snapshot.review_revision.review_tree || revision.manifest_digest !== reservation.snapshot.review_revision.manifest_digest)
|
|
1156
1225
|
throw new Error("Review revision changed before submission");
|
|
1157
1226
|
} catch (error) {
|
|
@@ -1167,7 +1236,7 @@ export class AssuranceCoordinator {
|
|
|
1167
1236
|
const invocation = this.openInvocation(taskId);
|
|
1168
1237
|
this.releaseReviewReservation(taskId, reservation);
|
|
1169
1238
|
try {
|
|
1170
|
-
await this.
|
|
1239
|
+
await this.authority.applyVerdict(ctx, {
|
|
1171
1240
|
taskId,
|
|
1172
1241
|
snapshot: reservation.snapshot,
|
|
1173
1242
|
verdict,
|
|
@@ -1,7 +1,27 @@
|
|
|
1
|
+
import type { SnapshotDescriptor } from "./coordinator";
|
|
2
|
+
|
|
3
|
+
export type ConfirmationReferenceSource = (input: {
|
|
4
|
+
snapshot: SnapshotDescriptor;
|
|
5
|
+
actorId: string;
|
|
6
|
+
now: string;
|
|
7
|
+
}) => string;
|
|
8
|
+
|
|
1
9
|
export interface ReviewRequest {
|
|
2
10
|
taskId: string;
|
|
3
11
|
operationId: string;
|
|
12
|
+
/**
|
|
13
|
+
* The complete Review prompt: internal role contract, static review rules and
|
|
14
|
+
* the per-dispatch facts. Dispatched by a Host whose reviewer agent carries no
|
|
15
|
+
* instructions of its own.
|
|
16
|
+
*/
|
|
4
17
|
prompt: string;
|
|
18
|
+
/**
|
|
19
|
+
* The per-dispatch facts alone, a strict suffix of `prompt`. A Host whose
|
|
20
|
+
* reviewer definition already carries the role contract and the static rules
|
|
21
|
+
* dispatches only this, so the static instructions stay generated from one
|
|
22
|
+
* source instead of being sent twice.
|
|
23
|
+
*/
|
|
24
|
+
snapshotPrompt: string;
|
|
5
25
|
evidencePath: string;
|
|
6
26
|
maxTurns: number;
|
|
7
27
|
}
|
|
@@ -8,14 +8,14 @@ import { tmpdir } from "node:os";
|
|
|
8
8
|
import { join, sep } from "node:path";
|
|
9
9
|
import { qaFindingId } from "./qa_findings";
|
|
10
10
|
import { captureGitTaskRevisionSnapshot } from "../workspace_scope";
|
|
11
|
-
import {
|
|
11
|
+
import { readTaskRecordRaw } from "../kernel/storage";
|
|
12
12
|
|
|
13
13
|
function deliveryTreeForSnapshot(
|
|
14
14
|
snapshot: SnapshotDescriptor,
|
|
15
15
|
writeTree: typeof writeDeliveryTree,
|
|
16
16
|
): string {
|
|
17
17
|
if (snapshot.review_revision?.review_tree) return snapshot.review_revision.review_tree;
|
|
18
|
-
const record =
|
|
18
|
+
const record = readTaskRecordRaw(snapshot.root, snapshot.task_id).record;
|
|
19
19
|
if (!record || record.contract !== "assurance_kernel/task_record/v4" || !record.git_base_head)
|
|
20
20
|
throw new Error("QA delivery requires a TaskRecord v4 git_base_head");
|
|
21
21
|
const captured = captureGitTaskRevisionSnapshot(
|
|
@@ -463,6 +463,10 @@ function publishInput(
|
|
|
463
463
|
const recomputed = `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
|
|
464
464
|
if (recomputed !== input.expectedDiffHash)
|
|
465
465
|
throw new Error("review task revision does not match assurance snapshot");
|
|
466
|
+
// A revision whose tree equals the base carries nothing to review; reserving
|
|
467
|
+
// Review for it would settle QA outcomes against bytes it does not contain.
|
|
468
|
+
if (Object.keys(snapshot.changed_paths).length === 0)
|
|
469
|
+
throw new Error("review revision carries no task change; stage the task's in-scope work before Review");
|
|
466
470
|
return { snapshot, revision: publishReviewRevision(snapshot.repository_root, snapshot, recomputed, input.taskId) };
|
|
467
471
|
}
|
|
468
472
|
|