Skip to content

Commit 08d1715

Browse files
committed
Add review packet contract for agent changes
1 parent e053a66 commit 08d1715

3 files changed

Lines changed: 270 additions & 0 deletions

File tree

‎packages/cli/src/index.ts‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
RipplePolicyExplanation,
4545
RipplePolicyRiskRule,
4646
RippleReadinessSummary,
47+
RippleReviewPacket,
4748
RIPPLE_CACHE_GITIGNORE_ENTRY,
4849
RIPPLE_CI_WORKFLOW_PATH,
4950
RIPPLE_GITIGNORE_PATH,
@@ -2340,6 +2341,10 @@ function printAgentStagedCheckSummary(summary: StagedCheckWithIntentSummary): vo
23402341
console.log(`checked_js_ts_files: ${summary.stagedFiles}`);
23412342
console.log(`checked_files: ${summary.checkedFiles}`);
23422343
printAgentIntentValidation(summary.intentValidation);
2344+
if (summary.reviewPacket) {
2345+
console.log("");
2346+
printAgentReviewPacket(summary.reviewPacket);
2347+
}
23432348

23442349
console.log("");
23452350
printAgentList("trusted_findings", summary.agentActions.trustedFindings);
@@ -2638,6 +2643,8 @@ function printAgentAuditSummary(summary: RippleAuditSummary): void {
26382643
console.log(`repair_status: ${summary.repairPlan.status}`);
26392644
console.log(`recommended_action: ${summary.recommendedAction}`);
26402645
console.log("");
2646+
printAgentReviewPacket(summary.reviewPacket);
2647+
console.log("");
26412648
printAgentHandoffBlock("handoff", summary.handoff);
26422649
if (summary.approvalStatus.approval) {
26432650
console.log(`approved_by: ${summary.approvalStatus.approval.approvedBy}`);
@@ -2696,6 +2703,8 @@ function printAgentGateSummary(summary: RippleGateSummary): void {
26962703
console.log(`risk_score: ${summary.risk.score}`);
26972704
console.log(`risk_summary: ${summary.risk.summary}`);
26982705
console.log("");
2706+
printAgentReviewPacket(summary.reviewPacket);
2707+
console.log("");
26992708
printAgentList("risk_reasons", compactGateRiskReasons(summary));
27002709
console.log("");
27012710
printAgentList("risk_evidence", compactGateRiskEvidence(summary));
@@ -2748,6 +2757,9 @@ function printGateSummary(summary: RippleGateSummary): void {
27482757
outsideBoundary.length > 0 ? "Changed outside boundary:" : "Changed files:",
27492758
outsideBoundary.length > 0 ? outsideBoundary : summary.changedFiles
27502759
);
2760+
console.log("");
2761+
printReviewPacketSummary(summary.reviewPacket);
2762+
console.log("");
27512763
printHumanList("Why:", compactGateReasons(summary));
27522764
printHumanList("Fix now:", compactGateFixes(summary));
27532765
if (summary.canContinue) {
@@ -2861,6 +2873,8 @@ function printAuditSummary(summary: RippleAuditSummary): void {
28612873
console.log(` next required action: ${gate.nextRequiredAction}`);
28622874
console.log(` summary: ${gate.summary}`);
28632875
console.log("");
2876+
printReviewPacketSummary(summary.reviewPacket);
2877+
console.log("");
28642878
console.log("Approval:");
28652879
console.log(` status: ${summary.approvalStatus.status}`);
28662880
console.log(` decision: ${summary.approvalStatus.decision}`);
@@ -3023,6 +3037,47 @@ function printHumanList(title: string, items: string[]): void {
30233037
items.forEach((item) => console.log(` - ${item}`));
30243038
}
30253039

3040+
function printReviewPacketSummary(packet: RippleReviewPacket): void {
3041+
console.log("Review packet:");
3042+
console.log(` protocol: ${packet.protocol}`);
3043+
console.log(` task: ${packet.originalTask}`);
3044+
console.log(` declared scope: ${packet.declaredScope.controlMode} ${packet.declaredScope.targetFile}`);
3045+
console.log(` human gate: ${packet.declaredScope.humanGate}`);
3046+
console.log(` boundary risk: ${packet.declaredScope.boundaryRisk}`);
3047+
printHumanList(" changed files", packet.actualChanges.changedFiles);
3048+
printHumanList(" outside boundary files", packet.scopeFindings.outsideBoundaryFiles);
3049+
printHumanList(" outside boundary symbols", packet.scopeFindings.outsideBoundarySymbols);
3050+
printHumanList(" verification expected", packet.verification.expectedCommands);
3051+
console.log(` tests run: ${packet.verification.testsRun}`);
3052+
console.log(` can continue: ${formatYesNo(packet.decision.canContinue)}`);
3053+
console.log(` must stop: ${formatYesNo(packet.decision.mustStop)}`);
3054+
console.log(` needs human: ${formatYesNo(packet.decision.needsHuman)}`);
3055+
printHumanList(" reviewer notes", packet.reviewerNotes);
3056+
}
3057+
3058+
function printAgentReviewPacket(packet: RippleReviewPacket): void {
3059+
console.log(`review_packet_protocol: ${packet.protocol}`);
3060+
console.log(`review_packet_version: ${packet.version}`);
3061+
console.log(`review_packet_task: ${packet.originalTask}`);
3062+
console.log(`review_packet_scope: ${packet.declaredScope.controlMode} ${packet.declaredScope.targetFile}`);
3063+
console.log(`review_packet_human_gate: ${packet.declaredScope.humanGate}`);
3064+
console.log(`review_packet_boundary_risk: ${packet.declaredScope.boundaryRisk}`);
3065+
console.log(`review_packet_tests_run: ${packet.verification.testsRun}`);
3066+
console.log(`review_packet_can_continue: ${packet.decision.canContinue}`);
3067+
console.log(`review_packet_must_stop: ${packet.decision.mustStop}`);
3068+
console.log(`review_packet_needs_human: ${packet.decision.needsHuman}`);
3069+
console.log("");
3070+
printAgentList("review_packet_changed_files", packet.actualChanges.changedFiles);
3071+
console.log("");
3072+
printAgentList("review_packet_outside_boundary_files", packet.scopeFindings.outsideBoundaryFiles);
3073+
console.log("");
3074+
printAgentList("review_packet_outside_boundary_symbols", packet.scopeFindings.outsideBoundarySymbols);
3075+
console.log("");
3076+
printAgentList("review_packet_verification_expected", packet.verification.expectedCommands);
3077+
console.log("");
3078+
printAgentList("review_packet_reviewer_notes", packet.reviewerNotes);
3079+
}
3080+
30263081
function printStagedCheckSummary(summary: StagedCheckWithIntentSummary): void {
30273082
const adapter = summary.adapterSupport.primaryAdapter;
30283083
console.log(summary.mode === "changed" ? "Ripple changed-files check" : summary.mode === "worktree" ? "Ripple worktree check" : "Ripple staged check");
@@ -3054,6 +3109,10 @@ function printStagedCheckSummary(summary: StagedCheckWithIntentSummary): void {
30543109
printReadinessDriftSummary("Readiness drift:", summary.intentValidation.readinessDrift);
30553110
console.log(`Planned scope: ${summary.intentValidation.plannedScope}`);
30563111
}
3112+
if (summary.reviewPacket) {
3113+
console.log("");
3114+
printReviewPacketSummary(summary.reviewPacket);
3115+
}
30573116

30583117
if (summary.skippedFiles.length > 0) {
30593118
console.log(`Skipped non-source files: ${summary.skippedFiles.length}`);

‎packages/core/src/audit.ts‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
IntentDriftRepairPlan,
66
RippleAgentHandoffDecision,
77
RippleAgentHandoffVerdict,
8+
RippleReviewPacket,
89
StagedCheckWithIntentSummary,
910
} from "./change-intent";
1011
import { buildAgentHandoffVerdict } from "./change-intent";
@@ -51,6 +52,7 @@ export type RippleAuditSummary = {
5152
changedFiles: string[];
5253
verificationTargets: string[];
5354
risk: RippleRiskSummary;
55+
reviewPacket: RippleReviewPacket;
5456
handoff: RippleAgentHandoffVerdict;
5557
};
5658

@@ -83,6 +85,7 @@ export type RippleGateSummary = {
8385
askHuman: string[];
8486
commands: RippleAgentHandoffVerdict["commands"];
8587
risk: RippleRiskSummary;
88+
reviewPacket: RippleReviewPacket;
8689
};
8790

8891
export function buildRippleAuditSummary(input: {
@@ -154,6 +157,7 @@ export function buildRippleAuditSummary(input: {
154157
nextSteps,
155158
changedFiles: input.stagedCheck.files.map((file) => file.file),
156159
verificationTargets: input.repairPlan.verificationTargets,
160+
reviewPacket: input.stagedCheck.reviewPacket ?? missingReviewPacket(input.stagedCheck, input.intent),
157161
risk: buildRippleRiskSummary({
158162
boundaryRisk: input.intent.boundaryRisk,
159163
allowedFiles: validation.editableFiles,
@@ -208,6 +212,61 @@ export function buildRippleGateSummary(audit: RippleAuditSummary): RippleGateSum
208212
askHuman: handoff.askHuman,
209213
commands: handoff.commands,
210214
risk: audit.risk,
215+
reviewPacket: audit.reviewPacket,
216+
};
217+
}
218+
219+
function missingReviewPacket(
220+
stagedCheck: StagedCheckWithIntentSummary,
221+
intent: ChangeIntent
222+
): RippleReviewPacket {
223+
const validation = stagedCheck.intentValidation;
224+
if (!validation) {
225+
throw new Error("Review packet requires a validated saved intent.");
226+
}
227+
return {
228+
protocol: "ripple-review-packet",
229+
version: 1,
230+
intentId: intent.id,
231+
originalTask: intent.task,
232+
mode: stagedCheck.mode,
233+
declaredScope: {
234+
controlMode: intent.controlMode,
235+
targetFile: intent.targetFile,
236+
allowedFiles: validation.editableFiles,
237+
allowedSymbols: validation.allowedSymbols,
238+
humanGate: validation.humanGate,
239+
boundaryRisk: validation.boundaryRisk,
240+
},
241+
actualChanges: {
242+
changedFiles: stagedCheck.files.map((file) => file.file),
243+
changedSymbols: stagedCheck.changedSymbols.map((symbol) => symbol.symbol),
244+
contractRiskSymbols: stagedCheck.contractRisks.map((risk) => risk.symbol),
245+
skippedFiles: stagedCheck.skippedFiles,
246+
missingFiles: stagedCheck.missingFiles,
247+
},
248+
scopeFindings: {
249+
plannedFilesChanged: validation.plannedFilesChanged,
250+
contextFilesChanged: validation.contextFilesChanged,
251+
outsideBoundaryFiles: validation.boundaryVerdict.changedOutsideBoundaryFiles,
252+
outsideBoundarySymbols: validation.boundaryVerdict.changedOutsideBoundarySymbols,
253+
protectedContractChanges: validation.protectedContractChanges,
254+
unplannedContractChanges: validation.unplannedContractChanges,
255+
},
256+
verification: {
257+
expectedCommands: stagedCheck.files.flatMap((file) => file.verificationTargets),
258+
testsRun: "unknown",
259+
note: "Ripple records expected verification, not proof that tests were run.",
260+
},
261+
decision: {
262+
canContinue: validation.handoff.canContinue,
263+
mustStop: validation.handoff.mustStop,
264+
needsHuman: validation.handoff.needsHuman,
265+
verdict: validation.verdict,
266+
nextRequiredAction: validation.nextRequiredAction,
267+
recommendedAction: validation.recommendedAction,
268+
},
269+
reviewerNotes: validation.nextSteps,
211270
};
212271
}
213272

‎packages/core/src/change-intent.ts‎

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,51 @@ export type RippleAgentHandoffVerdict = {
150150
commands: RippleAgentHandoffCommands;
151151
};
152152

153+
export type RippleReviewPacket = {
154+
protocol: "ripple-review-packet";
155+
version: 1;
156+
intentId: string;
157+
originalTask: string;
158+
mode: StagedCheckSummary["mode"];
159+
declaredScope: {
160+
controlMode: ControlMode;
161+
targetFile: string;
162+
allowedFiles: string[];
163+
allowedSymbols: string[];
164+
humanGate: HumanGate;
165+
boundaryRisk: ControlBoundaryRisk;
166+
};
167+
actualChanges: {
168+
changedFiles: string[];
169+
changedSymbols: string[];
170+
contractRiskSymbols: string[];
171+
skippedFiles: string[];
172+
missingFiles: string[];
173+
};
174+
scopeFindings: {
175+
plannedFilesChanged: string[];
176+
contextFilesChanged: string[];
177+
outsideBoundaryFiles: string[];
178+
outsideBoundarySymbols: string[];
179+
protectedContractChanges: string[];
180+
unplannedContractChanges: string[];
181+
};
182+
verification: {
183+
expectedCommands: string[];
184+
testsRun: "unknown";
185+
note: string;
186+
};
187+
decision: {
188+
canContinue: boolean;
189+
mustStop: boolean;
190+
needsHuman: boolean;
191+
verdict: ChangeIntentVerdict;
192+
nextRequiredAction: string;
193+
recommendedAction: string;
194+
};
195+
reviewerNotes: string[];
196+
};
197+
153198
export type BoundaryVerdictStatus = "pass" | "drift" | "danger";
154199
export type BoundaryDecision =
155200
| "continue"
@@ -208,6 +253,7 @@ export type ChangeIntentValidationSummary = {
208253

209254
export type StagedCheckWithIntentSummary = StagedCheckSummary & {
210255
intentValidation?: ChangeIntentValidationSummary;
256+
reviewPacket?: RippleReviewPacket;
211257
nextRequiredPhase?: AgentRuntimeNextPhaseId;
212258
nextRequiredAction?: string;
213259
};
@@ -457,11 +503,86 @@ export function validateStagedCheckAgainstIntent(
457503
...staged,
458504
requiresAttention: staged.requiresAttention || validation.requiresAttention,
459505
intentValidation: validation,
506+
reviewPacket: buildRippleReviewPacket(staged, intent, validation),
460507
nextRequiredPhase: validation.nextRequiredPhase,
461508
nextRequiredAction: validation.nextRequiredAction,
462509
};
463510
}
464511

512+
export function buildRippleReviewPacket(
513+
staged: StagedCheckSummary,
514+
intent: ChangeIntent,
515+
validation: ChangeIntentValidationSummary
516+
): RippleReviewPacket {
517+
const changedFiles = uniqueItems([
518+
...staged.files.map((file) => file.file),
519+
...staged.skippedFiles,
520+
...staged.missingFiles,
521+
]);
522+
const changedSymbols = uniqueItems(staged.changedSymbols.map((symbol) => symbol.symbol));
523+
const contractRiskSymbols = uniqueItems(staged.contractRisks.map((risk) => risk.symbol));
524+
const outsideBoundaryFiles = uniqueItems([
525+
...validation.unplannedFiles,
526+
...validation.boundaryVerdict.changedOutsideBoundaryFiles,
527+
]);
528+
const outsideBoundarySymbols = uniqueItems([
529+
...validation.unplannedSymbols,
530+
...validation.boundaryVerdict.changedOutsideBoundarySymbols,
531+
]);
532+
const verificationTargets = uniqueItems([
533+
...intent.verificationTargets,
534+
...staged.files.flatMap((file) => file.verificationTargets),
535+
]);
536+
const mustStop = validation.handoff.mustStop;
537+
538+
return {
539+
protocol: "ripple-review-packet",
540+
version: 1,
541+
intentId: intent.id,
542+
originalTask: intent.task,
543+
mode: staged.mode,
544+
declaredScope: {
545+
controlMode: intent.controlMode,
546+
targetFile: intent.targetFile,
547+
allowedFiles: validation.editableFiles,
548+
allowedSymbols: validation.allowedSymbols,
549+
humanGate: validation.humanGate,
550+
boundaryRisk: validation.boundaryRisk,
551+
},
552+
actualChanges: {
553+
changedFiles,
554+
changedSymbols,
555+
contractRiskSymbols,
556+
skippedFiles: staged.skippedFiles,
557+
missingFiles: staged.missingFiles,
558+
},
559+
scopeFindings: {
560+
plannedFilesChanged: validation.plannedFilesChanged,
561+
contextFilesChanged: validation.contextFilesChanged,
562+
outsideBoundaryFiles,
563+
outsideBoundarySymbols,
564+
protectedContractChanges: validation.protectedContractChanges,
565+
unplannedContractChanges: validation.unplannedContractChanges,
566+
},
567+
verification: {
568+
expectedCommands: verificationTargets,
569+
testsRun: "unknown",
570+
note: verificationTargets.length > 0
571+
? "Ripple found verification targets, but it cannot prove they were run from this packet alone."
572+
: "Ripple found no verification target; use the narrowest manual check before handoff.",
573+
},
574+
decision: {
575+
canContinue: validation.handoff.canContinue,
576+
mustStop,
577+
needsHuman: validation.handoff.needsHuman,
578+
verdict: validation.verdict,
579+
nextRequiredAction: validation.nextRequiredAction,
580+
recommendedAction: validation.recommendedAction,
581+
},
582+
reviewerNotes: buildReviewPacketNotes(validation, verificationTargets),
583+
};
584+
}
585+
465586
export function buildIntentDriftRepairPlan(
466587
staged: StagedCheckWithIntentSummary
467588
): IntentDriftRepairPlan {
@@ -1020,6 +1141,37 @@ function buildIntentValidation(
10201141
};
10211142
}
10221143

1144+
function buildReviewPacketNotes(
1145+
validation: ChangeIntentValidationSummary,
1146+
verificationTargets: string[]
1147+
): string[] {
1148+
const notes: string[] = [];
1149+
if (validation.boundaryVerdict.changedOutsideBoundaryFiles.length > 0) {
1150+
notes.push("Patch scope crossed: review files outside the declared boundary before keeping this change.");
1151+
}
1152+
if (validation.boundaryVerdict.changedOutsideBoundarySymbols.length > 0) {
1153+
notes.push("Function scope crossed: review changed symbols outside the declared function boundary.");
1154+
}
1155+
if (validation.contextFilesChanged.length > 0) {
1156+
notes.push("Context-only files changed: approve a wider intent if those edits are valid.");
1157+
}
1158+
if (
1159+
validation.protectedContractChanges.length > 0 ||
1160+
validation.unplannedContractChanges.length > 0
1161+
) {
1162+
notes.push("Behavior scope requires review: protected or unplanned contracts changed.");
1163+
}
1164+
if (verificationTargets.length > 0) {
1165+
notes.push("Verification evidence is required before handoff; this packet records expected checks, not proof that they ran.");
1166+
} else {
1167+
notes.push("No automated verification target was found; require a focused manual reviewer pass.");
1168+
}
1169+
if (validation.humanGate !== "none") {
1170+
notes.push(`Human gate applies: ${validation.humanGate}.`);
1171+
}
1172+
return uniqueItems(notes);
1173+
}
1174+
10231175
function validationNextRequiredPhase(input: {
10241176
driftVerdict: DriftVerdictSummary;
10251177
boundaryVerdict: BoundaryVerdictSummary;

0 commit comments

Comments
 (0)