@getripple/core 1.0.8 → 1.0.9
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/CHANGELOG.md +27 -0
- package/README.md +38 -28
- package/dist/agent-workflow.d.ts +6 -1
- package/dist/agent-workflow.js +44 -9
- package/dist/agent-workflow.js.map +1 -1
- package/dist/approval.d.ts +1 -1
- package/dist/approval.js +7 -3
- package/dist/approval.js.map +1 -1
- package/dist/audit.d.ts +34 -2
- package/dist/audit.js +152 -3
- package/dist/audit.js.map +1 -1
- package/dist/change-intent.d.ts +85 -0
- package/dist/change-intent.js +557 -15
- package/dist/change-intent.js.map +1 -1
- package/dist/policy.d.ts +9 -0
- package/dist/policy.js +232 -3
- package/dist/policy.js.map +1 -1
- package/dist/readiness.js +21 -12
- package/dist/readiness.js.map +1 -1
- package/dist/staged-check.d.ts +6 -2
- package/dist/staged-check.js +83 -4
- package/dist/staged-check.js.map +1 -1
- package/package.json +3 -3
package/dist/change-intent.js
CHANGED
|
@@ -23,7 +23,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
|
|
|
23
23
|
return result;
|
|
24
24
|
};
|
|
25
25
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
-
exports.defaultChangeIntentPath = exports.loadChangeIntent = exports.saveChangeIntent = exports.buildIntentDriftRepairPlan = exports.validateStagedCheckAgainstIntent = exports.buildAgentHandoffVerdict = exports.buildChangeIntentReadinessSnapshot = exports.buildChangeIntent = void 0;
|
|
26
|
+
exports.appendRippleVerificationEvidence = exports.defaultChangeIntentPath = exports.loadChangeIntent = exports.saveChangeIntent = exports.buildIntentDriftRepairPlan = exports.buildRippleReviewPacket = exports.validateStagedCheckAgainstIntent = exports.buildVerificationCommandSuggestions = exports.buildAgentHandoffVerdict = exports.buildChangeIntentReadinessSnapshot = exports.buildChangeIntent = void 0;
|
|
27
27
|
const crypto = __importStar(require("crypto"));
|
|
28
28
|
const fs = __importStar(require("fs"));
|
|
29
29
|
const path = __importStar(require("path"));
|
|
@@ -90,6 +90,7 @@ function buildChangeIntent(plan, options = {}) {
|
|
|
90
90
|
expectedSymbols,
|
|
91
91
|
protectedContracts,
|
|
92
92
|
verificationTargets: plan.verificationTargets,
|
|
93
|
+
verificationEvidence: [],
|
|
93
94
|
readinessSnapshot: options.readinessSnapshot ?? fallbackReadinessSnapshot(),
|
|
94
95
|
why: plan.why,
|
|
95
96
|
};
|
|
@@ -141,17 +142,122 @@ function buildAgentHandoffVerdict(input) {
|
|
|
141
142
|
};
|
|
142
143
|
}
|
|
143
144
|
exports.buildAgentHandoffVerdict = buildAgentHandoffVerdict;
|
|
145
|
+
function buildVerificationCommandSuggestions(verificationVerdict) {
|
|
146
|
+
if (verificationVerdict.status !== "failed" &&
|
|
147
|
+
verificationVerdict.status !== "review") {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
const latestEvidence = latestVerificationEvidenceByCommand(verificationVerdict.evidence);
|
|
151
|
+
const blockingEvidence = latestEvidence.filter((evidence) => evidence.status !== "passed");
|
|
152
|
+
const evidenceToRerun = blockingEvidence.length > 0 ? blockingEvidence : latestEvidence;
|
|
153
|
+
return evidenceToRerun
|
|
154
|
+
.map((evidence) => `ripple verify --run ${quoteCliArgument(evidence.command)} --intent latest`);
|
|
155
|
+
}
|
|
156
|
+
exports.buildVerificationCommandSuggestions = buildVerificationCommandSuggestions;
|
|
157
|
+
function quoteCliArgument(value) {
|
|
158
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
159
|
+
}
|
|
144
160
|
function validateStagedCheckAgainstIntent(staged, intent, options = {}) {
|
|
145
161
|
const validation = buildIntentValidation(staged, intent, options);
|
|
146
162
|
return {
|
|
147
163
|
...staged,
|
|
148
164
|
requiresAttention: staged.requiresAttention || validation.requiresAttention,
|
|
149
165
|
intentValidation: validation,
|
|
166
|
+
reviewPacket: buildRippleReviewPacket(staged, intent, validation),
|
|
150
167
|
nextRequiredPhase: validation.nextRequiredPhase,
|
|
151
168
|
nextRequiredAction: validation.nextRequiredAction,
|
|
152
169
|
};
|
|
153
170
|
}
|
|
154
171
|
exports.validateStagedCheckAgainstIntent = validateStagedCheckAgainstIntent;
|
|
172
|
+
function buildRippleReviewPacket(staged, intent, validation) {
|
|
173
|
+
const changedFiles = uniqueItems([
|
|
174
|
+
...staged.files.map((file) => file.file),
|
|
175
|
+
...staged.skippedFiles,
|
|
176
|
+
...staged.missingFiles,
|
|
177
|
+
]);
|
|
178
|
+
const changedSymbols = uniqueItems(staged.changedSymbols.map((symbol) => symbol.symbol));
|
|
179
|
+
const contractRiskSymbols = uniqueItems(staged.contractRisks.map((risk) => risk.symbol));
|
|
180
|
+
const outsideBoundaryFiles = uniqueItems([
|
|
181
|
+
...validation.unplannedFiles,
|
|
182
|
+
...validation.boundaryVerdict.changedOutsideBoundaryFiles,
|
|
183
|
+
]);
|
|
184
|
+
const outsideBoundarySymbols = uniqueItems([
|
|
185
|
+
...validation.unplannedSymbols,
|
|
186
|
+
...validation.boundaryVerdict.changedOutsideBoundarySymbols,
|
|
187
|
+
]);
|
|
188
|
+
const verificationTargets = uniqueItems([
|
|
189
|
+
...intent.verificationTargets,
|
|
190
|
+
...staged.files.flatMap((file) => file.verificationTargets),
|
|
191
|
+
]);
|
|
192
|
+
const mustStop = validation.handoff.mustStop;
|
|
193
|
+
const verificationEvidence = normalizeVerificationEvidence(intent.verificationEvidence);
|
|
194
|
+
const reportedVerificationCommands = verificationEvidence
|
|
195
|
+
.filter((evidence) => evidence.source === "reported")
|
|
196
|
+
.map((evidence) => evidence.command);
|
|
197
|
+
const executedVerificationCommands = verificationEvidence
|
|
198
|
+
.filter((evidence) => evidence.source === "executed")
|
|
199
|
+
.map((evidence) => evidence.command);
|
|
200
|
+
return {
|
|
201
|
+
protocol: "ripple-review-packet",
|
|
202
|
+
version: 1,
|
|
203
|
+
intentId: intent.id,
|
|
204
|
+
originalTask: intent.task,
|
|
205
|
+
mode: staged.mode,
|
|
206
|
+
declaredScope: {
|
|
207
|
+
controlMode: intent.controlMode,
|
|
208
|
+
targetFile: intent.targetFile,
|
|
209
|
+
allowedFiles: validation.editableFiles,
|
|
210
|
+
allowedSymbols: validation.allowedSymbols,
|
|
211
|
+
humanGate: validation.humanGate,
|
|
212
|
+
boundaryRisk: validation.boundaryRisk,
|
|
213
|
+
},
|
|
214
|
+
actualChanges: {
|
|
215
|
+
changedFiles,
|
|
216
|
+
changedSymbols,
|
|
217
|
+
contractRiskSymbols,
|
|
218
|
+
skippedFiles: staged.skippedFiles,
|
|
219
|
+
missingFiles: staged.missingFiles,
|
|
220
|
+
},
|
|
221
|
+
scopeFindings: {
|
|
222
|
+
plannedFilesChanged: validation.plannedFilesChanged,
|
|
223
|
+
contextFilesChanged: validation.contextFilesChanged,
|
|
224
|
+
outsideBoundaryFiles,
|
|
225
|
+
outsideBoundarySymbols,
|
|
226
|
+
protectedContractChanges: validation.protectedContractChanges,
|
|
227
|
+
unplannedContractChanges: validation.unplannedContractChanges,
|
|
228
|
+
},
|
|
229
|
+
verification: {
|
|
230
|
+
expectedCommands: verificationTargets,
|
|
231
|
+
testsRun: verificationEvidence.length > 0
|
|
232
|
+
? executedVerificationCommands.length > 0
|
|
233
|
+
? "executed"
|
|
234
|
+
: "reported"
|
|
235
|
+
: "unknown",
|
|
236
|
+
status: validation.verificationVerdict.status,
|
|
237
|
+
decision: validation.verificationVerdict.decision,
|
|
238
|
+
reportedCommands: reportedVerificationCommands,
|
|
239
|
+
executedCommands: executedVerificationCommands,
|
|
240
|
+
evidence: verificationEvidence,
|
|
241
|
+
note: verificationEvidence.length > 0
|
|
242
|
+
? executedVerificationCommands.length > 0
|
|
243
|
+
? "Ripple executed at least one verification command and recorded its exit code."
|
|
244
|
+
: "Ripple recorded reported verification evidence; it did not independently execute these commands."
|
|
245
|
+
: verificationTargets.length > 0
|
|
246
|
+
? "Ripple found verification targets, but it cannot prove they were run from this packet alone."
|
|
247
|
+
: "Ripple found no verification target; use the narrowest manual check before handoff.",
|
|
248
|
+
},
|
|
249
|
+
decision: {
|
|
250
|
+
canContinue: validation.handoff.canContinue,
|
|
251
|
+
mustStop,
|
|
252
|
+
needsHuman: validation.handoff.needsHuman,
|
|
253
|
+
verdict: validation.verdict,
|
|
254
|
+
nextRequiredAction: validation.nextRequiredAction,
|
|
255
|
+
recommendedAction: validation.recommendedAction,
|
|
256
|
+
},
|
|
257
|
+
reviewerNotes: buildReviewPacketNotes(validation, verificationTargets),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
exports.buildRippleReviewPacket = buildRippleReviewPacket;
|
|
155
261
|
function buildIntentDriftRepairPlan(staged) {
|
|
156
262
|
const validation = staged.intentValidation;
|
|
157
263
|
const verificationTargets = uniqueItems(staged.files.flatMap((file) => file.verificationTargets));
|
|
@@ -212,6 +318,7 @@ function buildIntentDriftRepairPlan(staged) {
|
|
|
212
318
|
policyExplanation: validation.policyExplanation,
|
|
213
319
|
policyDrift: validation.policyDrift,
|
|
214
320
|
readinessDrift: validation.readinessDrift,
|
|
321
|
+
verificationVerdict: validation.verificationVerdict,
|
|
215
322
|
status: repairStatus(validation),
|
|
216
323
|
summary: repairSummary(validation),
|
|
217
324
|
recommendedAction: validation.recommendedAction,
|
|
@@ -291,7 +398,7 @@ function buildRepairHandoff(plan) {
|
|
|
291
398
|
approve: plan.boundaryVerdict?.humanRequired
|
|
292
399
|
? [
|
|
293
400
|
"ripple approval --intent latest --agent",
|
|
294
|
-
`ripple approve --intent latest --gate ${approvalGateForHumanGate(plan.boundaryVerdict.humanGate)}`,
|
|
401
|
+
`ripple approve --intent latest --gate ${approvalGateForHumanGate(plan.boundaryVerdict.humanGate)} --reason "<why this boundary is safe>"`,
|
|
295
402
|
]
|
|
296
403
|
: [],
|
|
297
404
|
unstage: plan.commands.unstage,
|
|
@@ -306,6 +413,12 @@ function repairHandoffDecision(plan, canContinue, needsHuman) {
|
|
|
306
413
|
if (plan.readinessDrift?.status === "weakened") {
|
|
307
414
|
return "restore-readiness";
|
|
308
415
|
}
|
|
416
|
+
if (plan.verificationVerdict?.status === "review") {
|
|
417
|
+
return "human-review";
|
|
418
|
+
}
|
|
419
|
+
if (plan.verificationVerdict?.status === "failed") {
|
|
420
|
+
return "repair";
|
|
421
|
+
}
|
|
309
422
|
if (needsHuman) {
|
|
310
423
|
return "human-review";
|
|
311
424
|
}
|
|
@@ -333,6 +446,12 @@ function repairHandoffNextRequiredAction(plan, phase) {
|
|
|
333
446
|
if (plan.readinessDrift?.status === "weakened") {
|
|
334
447
|
return "Restore Ripple readiness with the listed commands or ask the human before continuing.";
|
|
335
448
|
}
|
|
449
|
+
if (plan.verificationVerdict?.status === "failed") {
|
|
450
|
+
return "Repair failed verification evidence, record a passing rerun, then run Ripple again.";
|
|
451
|
+
}
|
|
452
|
+
if (plan.verificationVerdict?.status === "review") {
|
|
453
|
+
return "Ask the human to review incomplete verification evidence before continuing.";
|
|
454
|
+
}
|
|
336
455
|
if (plan.status === "human-review-required" || plan.status === "contract-review-required") {
|
|
337
456
|
return "Ask the human to review the blockers before keeping this change.";
|
|
338
457
|
}
|
|
@@ -362,6 +481,9 @@ function repairHandoffAskHuman(plan) {
|
|
|
362
481
|
if (plan.readinessDrift?.status === "weakened") {
|
|
363
482
|
askHuman.push("Approve continuing only if weaker Ripple readiness is intentional.");
|
|
364
483
|
}
|
|
484
|
+
if (plan.verificationVerdict?.status === "review") {
|
|
485
|
+
askHuman.push(plan.verificationVerdict.summary);
|
|
486
|
+
}
|
|
365
487
|
if (plan.boundaryVerdict?.humanRequired) {
|
|
366
488
|
askHuman.push(`Human gate '${plan.boundaryVerdict.humanGate}' applies to this saved intent.`);
|
|
367
489
|
}
|
|
@@ -369,6 +491,32 @@ function repairHandoffAskHuman(plan) {
|
|
|
369
491
|
}
|
|
370
492
|
function buildRepairActions(input) {
|
|
371
493
|
const actions = [];
|
|
494
|
+
if (input.validation.verificationVerdict.status === "failed" ||
|
|
495
|
+
input.validation.verificationVerdict.status === "review") {
|
|
496
|
+
input.validation.verificationVerdict.evidence
|
|
497
|
+
.filter((evidence) => evidence.status !== "passed")
|
|
498
|
+
.forEach((evidence) => {
|
|
499
|
+
actions.push({
|
|
500
|
+
type: "verify",
|
|
501
|
+
priority: "blocker",
|
|
502
|
+
target: evidence.command,
|
|
503
|
+
command: evidence.command,
|
|
504
|
+
reason: `Reported verification status was ${evidence.status}.`,
|
|
505
|
+
instruction: verificationEvidenceFix(evidence),
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
if (actions.length === 0) {
|
|
509
|
+
input.validation.verificationVerdict.fix.forEach((instruction) => {
|
|
510
|
+
actions.push({
|
|
511
|
+
type: "verify",
|
|
512
|
+
priority: "blocker",
|
|
513
|
+
reason: input.validation.verificationVerdict.summary,
|
|
514
|
+
instruction,
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
return uniqueRepairActions(actions);
|
|
519
|
+
}
|
|
372
520
|
if (input.validation.driftVerdict.status === "pass") {
|
|
373
521
|
input.verificationTargets.slice(0, 8).forEach((target) => {
|
|
374
522
|
actions.push({
|
|
@@ -497,7 +645,23 @@ function saveChangeIntent(workspaceRoot, intent, intentPath) {
|
|
|
497
645
|
exports.saveChangeIntent = saveChangeIntent;
|
|
498
646
|
function loadChangeIntent(workspaceRoot, intentPath = "latest") {
|
|
499
647
|
const targetPath = resolveIntentPath(workspaceRoot, intentPath);
|
|
500
|
-
|
|
648
|
+
let raw;
|
|
649
|
+
try {
|
|
650
|
+
raw = fs.readFileSync(targetPath, "utf8");
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
if (isNodeError(err) && err.code === "ENOENT") {
|
|
654
|
+
throw new Error(`No active Ripple change intent found at ${targetPath}. Run ripple plan --file <file> --task "<task>" --agent --save before an agent edits.`);
|
|
655
|
+
}
|
|
656
|
+
throw err;
|
|
657
|
+
}
|
|
658
|
+
let parsed;
|
|
659
|
+
try {
|
|
660
|
+
parsed = JSON.parse(raw);
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
throw new Error(`Invalid Ripple change intent JSON at ${targetPath}. Ask the human to inspect or recreate the saved boundary before continuing. ${errorMessage(err)}`);
|
|
664
|
+
}
|
|
501
665
|
return assertChangeIntent(parsed, targetPath);
|
|
502
666
|
}
|
|
503
667
|
exports.loadChangeIntent = loadChangeIntent;
|
|
@@ -557,9 +721,11 @@ function buildIntentValidation(staged, intent, options) {
|
|
|
557
721
|
});
|
|
558
722
|
const policyDrift = buildPolicyDriftSummary(intent.policyExplanation, options.currentPolicyExplanation);
|
|
559
723
|
const readinessDrift = buildReadinessDriftSummary(intent.readinessSnapshot, options.currentReadinessSnapshot);
|
|
724
|
+
const verificationVerdict = buildVerificationVerdict(intent.verificationEvidence, changedFiles, staged.changeFingerprint, staged.mode);
|
|
560
725
|
const boundaryGuidance = mergeBoundaryGuidance(guidance, boundaryVerdict);
|
|
561
726
|
const policyGuidance = mergePolicyDriftGuidance(boundaryGuidance, policyDrift);
|
|
562
|
-
const
|
|
727
|
+
const readinessGuidance = mergeReadinessDriftGuidance(policyGuidance, readinessDrift);
|
|
728
|
+
const effectiveGuidance = mergeVerificationGuidance(readinessGuidance, verificationVerdict);
|
|
563
729
|
const driftVerdict = buildDriftVerdict({
|
|
564
730
|
verdict,
|
|
565
731
|
boundaryVerdict,
|
|
@@ -580,6 +746,7 @@ function buildIntentValidation(staged, intent, options) {
|
|
|
580
746
|
boundaryVerdict,
|
|
581
747
|
policyDrift,
|
|
582
748
|
readinessDrift,
|
|
749
|
+
verificationVerdict,
|
|
583
750
|
});
|
|
584
751
|
const nextRequiredAction = validationNextRequiredAction(nextRequiredPhase);
|
|
585
752
|
const validation = {
|
|
@@ -598,6 +765,7 @@ function buildIntentValidation(staged, intent, options) {
|
|
|
598
765
|
policyExplanation: intent.policyExplanation,
|
|
599
766
|
policyDrift,
|
|
600
767
|
readinessDrift,
|
|
768
|
+
verificationVerdict,
|
|
601
769
|
plannedScope: unplannedFiles.length === 0 ? "matched" : "violated",
|
|
602
770
|
editableFiles,
|
|
603
771
|
contextFiles,
|
|
@@ -617,16 +785,55 @@ function buildIntentValidation(staged, intent, options) {
|
|
|
617
785
|
requiresAttention: driftVerdict.status !== "pass" ||
|
|
618
786
|
boundaryVerdict.humanRequired ||
|
|
619
787
|
policyDrift.status === "changed" ||
|
|
620
|
-
readinessDrift.status === "weakened"
|
|
788
|
+
readinessDrift.status === "weakened" ||
|
|
789
|
+
verificationVerdict.status === "failed" ||
|
|
790
|
+
verificationVerdict.status === "review",
|
|
621
791
|
};
|
|
622
792
|
return {
|
|
623
793
|
...validation,
|
|
624
794
|
handoff: buildValidationHandoff(validation),
|
|
625
795
|
};
|
|
626
796
|
}
|
|
797
|
+
function buildReviewPacketNotes(validation, verificationTargets) {
|
|
798
|
+
const notes = [];
|
|
799
|
+
if (validation.boundaryVerdict.changedOutsideBoundaryFiles.length > 0) {
|
|
800
|
+
notes.push("Patch scope crossed: review files outside the declared boundary before keeping this change.");
|
|
801
|
+
}
|
|
802
|
+
if (validation.boundaryVerdict.changedOutsideBoundarySymbols.length > 0) {
|
|
803
|
+
notes.push("Function scope crossed: review changed symbols outside the declared function boundary.");
|
|
804
|
+
}
|
|
805
|
+
if (validation.contextFilesChanged.length > 0) {
|
|
806
|
+
notes.push("Context-only files changed: approve a wider intent if those edits are valid.");
|
|
807
|
+
}
|
|
808
|
+
if (validation.protectedContractChanges.length > 0 ||
|
|
809
|
+
validation.unplannedContractChanges.length > 0) {
|
|
810
|
+
notes.push("Behavior scope requires review: protected or unplanned contracts changed.");
|
|
811
|
+
}
|
|
812
|
+
if (validation.verificationVerdict.status === "failed") {
|
|
813
|
+
notes.push("Verification failed: repair the failed reported check before handoff.");
|
|
814
|
+
}
|
|
815
|
+
else if (validation.verificationVerdict.status === "review") {
|
|
816
|
+
notes.push("Verification incomplete: ask the human to review skipped or unknown reported evidence.");
|
|
817
|
+
}
|
|
818
|
+
else if (validation.verificationVerdict.status === "pass") {
|
|
819
|
+
notes.push("Verification reported: all recorded verification evidence was reported as passed.");
|
|
820
|
+
}
|
|
821
|
+
else if (verificationTargets.length > 0) {
|
|
822
|
+
notes.push("Verification evidence is required before handoff; this packet records expected checks, not proof that they ran.");
|
|
823
|
+
}
|
|
824
|
+
else {
|
|
825
|
+
notes.push("No automated verification target was found; require a focused manual reviewer pass.");
|
|
826
|
+
}
|
|
827
|
+
if (validation.humanGate !== "none") {
|
|
828
|
+
notes.push(`Human gate applies: ${validation.humanGate}.`);
|
|
829
|
+
}
|
|
830
|
+
return uniqueItems(notes);
|
|
831
|
+
}
|
|
627
832
|
function validationNextRequiredPhase(input) {
|
|
628
833
|
if (input.policyDrift.status === "changed" ||
|
|
629
834
|
input.readinessDrift?.status === "weakened" ||
|
|
835
|
+
input.verificationVerdict?.status === "failed" ||
|
|
836
|
+
input.verificationVerdict?.status === "review" ||
|
|
630
837
|
input.boundaryVerdict.status !== "pass" ||
|
|
631
838
|
input.driftVerdict.status !== "pass") {
|
|
632
839
|
return "repair_or_handoff";
|
|
@@ -647,6 +854,7 @@ function buildValidationHandoff(validation) {
|
|
|
647
854
|
validation.driftVerdict.decision === "stop-and-ask-human" ||
|
|
648
855
|
validation.policyDrift.status === "changed" ||
|
|
649
856
|
validation.readinessDrift.status === "weakened" ||
|
|
857
|
+
validation.verificationVerdict.status === "review" ||
|
|
650
858
|
validation.verdict === "dangerous";
|
|
651
859
|
const canContinue = validation.driftVerdict.status === "pass" &&
|
|
652
860
|
!validation.requiresAttention;
|
|
@@ -671,6 +879,12 @@ function validationHandoffDecision(validation, canContinue, needsHuman) {
|
|
|
671
879
|
if (validation.readinessDrift.status === "weakened") {
|
|
672
880
|
return "restore-readiness";
|
|
673
881
|
}
|
|
882
|
+
if (validation.verificationVerdict.status === "review") {
|
|
883
|
+
return "human-review";
|
|
884
|
+
}
|
|
885
|
+
if (validation.verificationVerdict.status === "failed") {
|
|
886
|
+
return "repair";
|
|
887
|
+
}
|
|
674
888
|
if (needsHuman) {
|
|
675
889
|
return "human-review";
|
|
676
890
|
}
|
|
@@ -691,6 +905,10 @@ function validationHandoffFixNow(validation) {
|
|
|
691
905
|
...validation.boundaryVerdict.fix,
|
|
692
906
|
...(validation.policyDrift.status === "changed" ? validation.policyDrift.fix : []),
|
|
693
907
|
...(validation.readinessDrift.status === "weakened" ? validation.readinessDrift.fix : []),
|
|
908
|
+
...(validation.verificationVerdict.status === "failed" ||
|
|
909
|
+
validation.verificationVerdict.status === "review"
|
|
910
|
+
? validation.verificationVerdict.fix
|
|
911
|
+
: []),
|
|
694
912
|
]);
|
|
695
913
|
}
|
|
696
914
|
function validationHandoffAskHuman(validation) {
|
|
@@ -704,6 +922,9 @@ function validationHandoffAskHuman(validation) {
|
|
|
704
922
|
if (validation.readinessDrift.status === "weakened") {
|
|
705
923
|
askHuman.push("Approve continuing only if the weaker Ripple readiness is intentional.");
|
|
706
924
|
}
|
|
925
|
+
if (validation.verificationVerdict.status === "review") {
|
|
926
|
+
askHuman.push(validation.verificationVerdict.summary);
|
|
927
|
+
}
|
|
707
928
|
if (validation.verdict === "dangerous") {
|
|
708
929
|
askHuman.push("Review contract drift before keeping the staged change.");
|
|
709
930
|
}
|
|
@@ -729,7 +950,7 @@ function validationHandoffCommands(validation) {
|
|
|
729
950
|
approve: validation.boundaryVerdict.humanRequired
|
|
730
951
|
? [
|
|
731
952
|
"ripple approval --intent latest --agent",
|
|
732
|
-
`ripple approve --intent latest --gate ${approvalGateForHumanGate(validation.humanGate)}`,
|
|
953
|
+
`ripple approve --intent latest --gate ${approvalGateForHumanGate(validation.humanGate)} --reason "<why this boundary is safe>"`,
|
|
733
954
|
]
|
|
734
955
|
: [],
|
|
735
956
|
unstage: uniqueItems([
|
|
@@ -737,9 +958,12 @@ function validationHandoffCommands(validation) {
|
|
|
737
958
|
...validation.contextFilesChanged,
|
|
738
959
|
...validation.boundaryVerdict.changedOutsideBoundaryFiles,
|
|
739
960
|
]).map((file) => `git restore --staged -- ${file}`),
|
|
740
|
-
verify: validation.
|
|
741
|
-
|
|
742
|
-
|
|
961
|
+
verify: validation.verificationVerdict.status === "failed" ||
|
|
962
|
+
validation.verificationVerdict.status === "review"
|
|
963
|
+
? buildVerificationCommandSuggestions(validation.verificationVerdict)
|
|
964
|
+
: validation.driftVerdict.status === "pass"
|
|
965
|
+
? validation.driftVerdict.fix
|
|
966
|
+
: validation.nextSteps,
|
|
743
967
|
};
|
|
744
968
|
}
|
|
745
969
|
function approvalGateForHumanGate(humanGate) {
|
|
@@ -805,6 +1029,152 @@ function mergeReadinessDriftGuidance(guidance, readinessDrift) {
|
|
|
805
1029
|
]),
|
|
806
1030
|
};
|
|
807
1031
|
}
|
|
1032
|
+
function mergeVerificationGuidance(guidance, verificationVerdict) {
|
|
1033
|
+
if (verificationVerdict.status !== "failed" &&
|
|
1034
|
+
verificationVerdict.status !== "review") {
|
|
1035
|
+
return guidance;
|
|
1036
|
+
}
|
|
1037
|
+
return {
|
|
1038
|
+
recommendedAction: guidance.blockingReasons.length > 0
|
|
1039
|
+
? guidance.recommendedAction
|
|
1040
|
+
: verificationVerdict.status === "failed"
|
|
1041
|
+
? "Repair failed verification before continuing."
|
|
1042
|
+
: "Ask the human to review incomplete verification evidence before continuing.",
|
|
1043
|
+
blockingReasons: uniqueItems([
|
|
1044
|
+
...guidance.blockingReasons,
|
|
1045
|
+
...verificationVerdict.why,
|
|
1046
|
+
]),
|
|
1047
|
+
nextSteps: uniqueItems([
|
|
1048
|
+
...verificationVerdict.fix,
|
|
1049
|
+
...guidance.nextSteps,
|
|
1050
|
+
]),
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function buildVerificationVerdict(value, changedFiles = [], changeFingerprint, changeMode = "staged") {
|
|
1054
|
+
const evidence = normalizeVerificationEvidence(value);
|
|
1055
|
+
const latestEvidence = latestVerificationEvidenceByCommand(evidence);
|
|
1056
|
+
if (evidence.length === 0) {
|
|
1057
|
+
return {
|
|
1058
|
+
status: "not-reported",
|
|
1059
|
+
decision: "continue",
|
|
1060
|
+
label: "UNKNOWN",
|
|
1061
|
+
summary: "UNKNOWN: no verification evidence has been reported.",
|
|
1062
|
+
why: [],
|
|
1063
|
+
fix: [],
|
|
1064
|
+
evidence,
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
const failed = latestEvidence.filter((item) => item.status === "failed");
|
|
1068
|
+
if (failed.length > 0) {
|
|
1069
|
+
return {
|
|
1070
|
+
status: "failed",
|
|
1071
|
+
decision: "repair",
|
|
1072
|
+
label: "FAILED",
|
|
1073
|
+
summary: "FAILED: latest verification evidence includes failing checks.",
|
|
1074
|
+
why: failed.map((item) => verificationEvidenceWhy(item)),
|
|
1075
|
+
fix: failed.map((item) => verificationEvidenceFix(item)),
|
|
1076
|
+
evidence,
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
const incomplete = latestEvidence.filter((item) => item.status === "skipped" || item.status === "unknown");
|
|
1080
|
+
if (incomplete.length > 0) {
|
|
1081
|
+
return {
|
|
1082
|
+
status: "review",
|
|
1083
|
+
decision: "human-review",
|
|
1084
|
+
label: "REVIEW",
|
|
1085
|
+
summary: "REVIEW: latest verification evidence is skipped or unknown, so a human must review before handoff.",
|
|
1086
|
+
why: incomplete.map((item) => verificationEvidenceWhy(item)),
|
|
1087
|
+
fix: incomplete.map((item) => verificationEvidenceFix(item)),
|
|
1088
|
+
evidence,
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
const staleEvidence = staleVerificationEvidence(latestEvidence, {
|
|
1092
|
+
changedFiles,
|
|
1093
|
+
changeFingerprint,
|
|
1094
|
+
changeMode,
|
|
1095
|
+
});
|
|
1096
|
+
if (staleEvidence.length > 0) {
|
|
1097
|
+
return {
|
|
1098
|
+
status: "review",
|
|
1099
|
+
decision: "human-review",
|
|
1100
|
+
label: "REVIEW",
|
|
1101
|
+
summary: "REVIEW: latest verification evidence was recorded for a different change snapshot, so the agent must rerun verification before handoff.",
|
|
1102
|
+
why: staleEvidence.map((item) => staleVerificationWhy(item, changedFiles, changeFingerprint)),
|
|
1103
|
+
fix: staleEvidence.map((item) => `Rerun verification against the current changed files: ${item.command}`),
|
|
1104
|
+
evidence,
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
return {
|
|
1108
|
+
status: "pass",
|
|
1109
|
+
decision: "continue",
|
|
1110
|
+
label: "PASS",
|
|
1111
|
+
summary: "PASS: latest verification evidence for every command was marked passed.",
|
|
1112
|
+
why: latestEvidence.map((item) => verificationEvidenceWhy(item)),
|
|
1113
|
+
fix: ["Keep the passing verification evidence with the final handoff."],
|
|
1114
|
+
evidence,
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
function latestVerificationEvidenceByCommand(evidence) {
|
|
1118
|
+
const latest = new Map();
|
|
1119
|
+
evidence.forEach((item) => {
|
|
1120
|
+
latest.set(item.command, item);
|
|
1121
|
+
});
|
|
1122
|
+
return Array.from(latest.values());
|
|
1123
|
+
}
|
|
1124
|
+
function staleVerificationEvidence(evidence, current) {
|
|
1125
|
+
const currentChangedFiles = normalizeVerificationChangedFiles(current.changedFiles);
|
|
1126
|
+
if (currentChangedFiles.length === 0) {
|
|
1127
|
+
return [];
|
|
1128
|
+
}
|
|
1129
|
+
return evidence.filter((item) => {
|
|
1130
|
+
if (item.changeFingerprint &&
|
|
1131
|
+
current.changeFingerprint) {
|
|
1132
|
+
return item.changeFingerprint !== current.changeFingerprint;
|
|
1133
|
+
}
|
|
1134
|
+
if (!item.changedFiles) {
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
return !sameStringSet(item.changedFiles, currentChangedFiles);
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
function staleVerificationWhy(evidence, changedFiles, changeFingerprint) {
|
|
1141
|
+
if (evidence.changeFingerprint && changeFingerprint) {
|
|
1142
|
+
return `Stale verification proof: ${evidence.command} covered change fingerprint ${evidence.changeFingerprint.slice(0, 12)}, current fingerprint is ${changeFingerprint.slice(0, 12)}.`;
|
|
1143
|
+
}
|
|
1144
|
+
return `Stale verification proof: ${evidence.command} covered ${formatVerificationFileSet(evidence.changedFiles ?? [])}, current changed files are ${formatVerificationFileSet(changedFiles)}.`;
|
|
1145
|
+
}
|
|
1146
|
+
function normalizeVerificationChangedFiles(files) {
|
|
1147
|
+
return uniqueItems(files.map((file) => normalizeVerificationFilePath(file)));
|
|
1148
|
+
}
|
|
1149
|
+
function sameStringSet(left, right) {
|
|
1150
|
+
if (left.length !== right.length) {
|
|
1151
|
+
return false;
|
|
1152
|
+
}
|
|
1153
|
+
const rightSet = new Set(right);
|
|
1154
|
+
return left.every((item) => rightSet.has(item));
|
|
1155
|
+
}
|
|
1156
|
+
function formatVerificationFileSet(files) {
|
|
1157
|
+
return files.length > 0 ? files.join(", ") : "no changed files";
|
|
1158
|
+
}
|
|
1159
|
+
function verificationEvidenceWhy(evidence) {
|
|
1160
|
+
const note = evidence.note ? ` Note: ${evidence.note}` : "";
|
|
1161
|
+
const sourceLabel = evidence.source === "executed" ? "Executed" : "Reported";
|
|
1162
|
+
const exitCode = typeof evidence.exitCode === "number" ? ` exitCode=${evidence.exitCode}.` : "";
|
|
1163
|
+
const duration = typeof evidence.durationMs === "number" ? ` durationMs=${evidence.durationMs}.` : "";
|
|
1164
|
+
return `${sourceLabel} verification ${evidence.status}: ${evidence.command}.${exitCode}${duration}${note}`;
|
|
1165
|
+
}
|
|
1166
|
+
function verificationEvidenceFix(evidence) {
|
|
1167
|
+
if (evidence.status === "failed") {
|
|
1168
|
+
return `Fix the failing verification, rerun it, then record a passing result: ${evidence.command}`;
|
|
1169
|
+
}
|
|
1170
|
+
if (evidence.status === "skipped") {
|
|
1171
|
+
return `Run the skipped verification or ask the human to approve the skip: ${evidence.command}`;
|
|
1172
|
+
}
|
|
1173
|
+
if (evidence.status === "unknown") {
|
|
1174
|
+
return `Resolve the unknown verification result or ask the human to review it: ${evidence.command}`;
|
|
1175
|
+
}
|
|
1176
|
+
return `Keep passing verification evidence in the handoff: ${evidence.command}`;
|
|
1177
|
+
}
|
|
808
1178
|
function buildPolicyDriftSummary(saved, current) {
|
|
809
1179
|
if (!current) {
|
|
810
1180
|
return {
|
|
@@ -1239,6 +1609,12 @@ function validationVerdict(input) {
|
|
|
1239
1609
|
return "matched";
|
|
1240
1610
|
}
|
|
1241
1611
|
function repairStatus(validation) {
|
|
1612
|
+
if (validation.verificationVerdict.status === "failed") {
|
|
1613
|
+
return "repair-required";
|
|
1614
|
+
}
|
|
1615
|
+
if (validation.verificationVerdict.status === "review") {
|
|
1616
|
+
return "human-review-required";
|
|
1617
|
+
}
|
|
1242
1618
|
if (validation.driftVerdict.status === "pass") {
|
|
1243
1619
|
return "no-repair-needed";
|
|
1244
1620
|
}
|
|
@@ -1258,6 +1634,12 @@ function repairStatus(validation) {
|
|
|
1258
1634
|
return "repair-required";
|
|
1259
1635
|
}
|
|
1260
1636
|
function repairSummary(validation) {
|
|
1637
|
+
if (validation.verificationVerdict.status === "failed") {
|
|
1638
|
+
return "Reported verification failed; repair the failing check and record a passing rerun before continuing.";
|
|
1639
|
+
}
|
|
1640
|
+
if (validation.verificationVerdict.status === "review") {
|
|
1641
|
+
return "Reported verification is skipped or unknown; ask the human to review before continuing.";
|
|
1642
|
+
}
|
|
1261
1643
|
if (validation.driftVerdict.status === "pass") {
|
|
1262
1644
|
return "Staged changes match the saved intent; no drift repair is needed.";
|
|
1263
1645
|
}
|
|
@@ -1392,10 +1774,13 @@ function resolveIntentPath(workspaceRoot, intentPath) {
|
|
|
1392
1774
|
}
|
|
1393
1775
|
function assertChangeIntent(value, sourcePath) {
|
|
1394
1776
|
if (!isRecord(value)) {
|
|
1395
|
-
throw new Error(`Invalid Ripple change intent
|
|
1777
|
+
throw new Error(`Invalid Ripple change intent at ${sourcePath}. Ask the human to inspect or recreate the saved boundary before continuing.`);
|
|
1778
|
+
}
|
|
1779
|
+
if (value.protocol === "ripple-closed-intent") {
|
|
1780
|
+
throw new Error(closedIntentErrorMessage(value, sourcePath));
|
|
1396
1781
|
}
|
|
1397
1782
|
if (value.protocol !== INTENT_PROTOCOL || value.version !== INTENT_VERSION) {
|
|
1398
|
-
throw new Error(`
|
|
1783
|
+
throw new Error(`No active Ripple change intent found at ${sourcePath}. Found protocol ${String(value.protocol)} instead of ${INTENT_PROTOCOL}. Run ripple intent status, then create a new saved plan before the agent continues.`);
|
|
1399
1784
|
}
|
|
1400
1785
|
if (typeof value.id !== "string" ||
|
|
1401
1786
|
typeof value.createdAt !== "string" ||
|
|
@@ -1410,10 +1795,30 @@ function assertChangeIntent(value, sourcePath) {
|
|
|
1410
1795
|
!Array.isArray(value.protectedContracts) ||
|
|
1411
1796
|
!Array.isArray(value.verificationTargets) ||
|
|
1412
1797
|
typeof value.why !== "string") {
|
|
1413
|
-
throw new Error(`Malformed Ripple change intent
|
|
1798
|
+
throw new Error(`Malformed Ripple change intent at ${sourcePath}. Ask the human to inspect or recreate the saved boundary before continuing.`);
|
|
1414
1799
|
}
|
|
1415
1800
|
return normalizeChangeIntent(value);
|
|
1416
1801
|
}
|
|
1802
|
+
function closedIntentErrorMessage(value, sourcePath) {
|
|
1803
|
+
const reason = typeof value.reason === "string" && value.reason.trim().length > 0
|
|
1804
|
+
? ` Reason: ${value.reason.trim()}`
|
|
1805
|
+
: "";
|
|
1806
|
+
const closedBy = typeof value.closedBy === "string" && value.closedBy.trim().length > 0
|
|
1807
|
+
? ` Closed by: ${value.closedBy.trim()}.`
|
|
1808
|
+
: "";
|
|
1809
|
+
return [
|
|
1810
|
+
`No active Ripple change intent found at ${sourcePath}; the saved boundary is closed.`,
|
|
1811
|
+
`${closedBy}${reason}`,
|
|
1812
|
+
"Agents must not continue from a closed boundary.",
|
|
1813
|
+
"Run ripple intent status, then create a new saved plan with ripple plan --file <file> --task \"<task>\" --agent --save before editing.",
|
|
1814
|
+
].filter(Boolean).join(" ");
|
|
1815
|
+
}
|
|
1816
|
+
function isNodeError(err) {
|
|
1817
|
+
return err instanceof Error && "code" in err;
|
|
1818
|
+
}
|
|
1819
|
+
function errorMessage(err) {
|
|
1820
|
+
return err instanceof Error ? err.message : String(err);
|
|
1821
|
+
}
|
|
1417
1822
|
function normalizeChangeIntent(intent) {
|
|
1418
1823
|
const controlMode = isControlMode(intent.controlMode) ? intent.controlMode : "file";
|
|
1419
1824
|
const editableFiles = uniqueItems(controlMode === "brainstorm"
|
|
@@ -1463,9 +1868,143 @@ function normalizeChangeIntent(intent) {
|
|
|
1463
1868
|
contextFiles,
|
|
1464
1869
|
allowedFiles: uniqueItems([...editableFiles, ...contextFiles]),
|
|
1465
1870
|
expectedFiles: uniqueItems(intent.expectedFiles.length > 0 ? intent.expectedFiles : editableFiles),
|
|
1871
|
+
verificationEvidence: normalizeVerificationEvidence(intent.verificationEvidence),
|
|
1466
1872
|
readinessSnapshot,
|
|
1467
1873
|
};
|
|
1468
1874
|
}
|
|
1875
|
+
function appendRippleVerificationEvidence(intent, evidence) {
|
|
1876
|
+
const normalizedCommand = evidence.command.trim();
|
|
1877
|
+
if (normalizedCommand.length === 0) {
|
|
1878
|
+
throw new Error("Verification command cannot be empty.");
|
|
1879
|
+
}
|
|
1880
|
+
const normalizedEvidence = {
|
|
1881
|
+
command: normalizedCommand,
|
|
1882
|
+
status: isVerificationStatus(evidence.status) ? evidence.status : "unknown",
|
|
1883
|
+
recordedAt: evidence.recordedAt ?? new Date().toISOString(),
|
|
1884
|
+
source: evidence.source ?? "reported",
|
|
1885
|
+
changedFiles: Array.isArray(evidence.changedFiles)
|
|
1886
|
+
? uniqueItems(evidence.changedFiles.filter((file) => typeof file === "string" && file.trim().length > 0).map((file) => normalizeVerificationFilePath(file.trim())))
|
|
1887
|
+
: undefined,
|
|
1888
|
+
changeMode: isVerificationChangeMode(evidence.changeMode)
|
|
1889
|
+
? evidence.changeMode
|
|
1890
|
+
: undefined,
|
|
1891
|
+
changeFingerprint: typeof evidence.changeFingerprint === "string" &&
|
|
1892
|
+
evidence.changeFingerprint.trim().length > 0
|
|
1893
|
+
? evidence.changeFingerprint.trim()
|
|
1894
|
+
: undefined,
|
|
1895
|
+
exitCode: typeof evidence.exitCode === "number" && Number.isFinite(evidence.exitCode)
|
|
1896
|
+
? Math.trunc(evidence.exitCode)
|
|
1897
|
+
: undefined,
|
|
1898
|
+
durationMs: typeof evidence.durationMs === "number" && Number.isFinite(evidence.durationMs)
|
|
1899
|
+
? Math.max(0, Math.trunc(evidence.durationMs))
|
|
1900
|
+
: undefined,
|
|
1901
|
+
stdoutTail: typeof evidence.stdoutTail === "string" && evidence.stdoutTail.trim().length > 0
|
|
1902
|
+
? evidence.stdoutTail
|
|
1903
|
+
: undefined,
|
|
1904
|
+
stderrTail: typeof evidence.stderrTail === "string" && evidence.stderrTail.trim().length > 0
|
|
1905
|
+
? evidence.stderrTail
|
|
1906
|
+
: undefined,
|
|
1907
|
+
note: typeof evidence.note === "string" && evidence.note.trim().length > 0
|
|
1908
|
+
? evidence.note.trim()
|
|
1909
|
+
: undefined,
|
|
1910
|
+
};
|
|
1911
|
+
return {
|
|
1912
|
+
...intent,
|
|
1913
|
+
verificationEvidence: uniqueVerificationEvidence([
|
|
1914
|
+
...normalizeVerificationEvidence(intent.verificationEvidence),
|
|
1915
|
+
normalizedEvidence,
|
|
1916
|
+
]),
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
exports.appendRippleVerificationEvidence = appendRippleVerificationEvidence;
|
|
1920
|
+
function normalizeVerificationEvidence(value) {
|
|
1921
|
+
if (!Array.isArray(value)) {
|
|
1922
|
+
return [];
|
|
1923
|
+
}
|
|
1924
|
+
return uniqueVerificationEvidence(value.flatMap((item) => {
|
|
1925
|
+
if (!isRecord(item) || typeof item.command !== "string") {
|
|
1926
|
+
return [];
|
|
1927
|
+
}
|
|
1928
|
+
const command = item.command.trim();
|
|
1929
|
+
if (command.length === 0) {
|
|
1930
|
+
return [];
|
|
1931
|
+
}
|
|
1932
|
+
const status = isVerificationStatus(item.status) ? item.status : "unknown";
|
|
1933
|
+
const recordedAt = typeof item.recordedAt === "string" && item.recordedAt.trim().length > 0
|
|
1934
|
+
? item.recordedAt
|
|
1935
|
+
: new Date(0).toISOString();
|
|
1936
|
+
const source = item.source === "executed" ? "executed" : "reported";
|
|
1937
|
+
const changedFiles = Array.isArray(item.changedFiles)
|
|
1938
|
+
? uniqueItems(item.changedFiles.filter((file) => typeof file === "string" && file.trim().length > 0).map((file) => normalizeVerificationFilePath(file.trim())))
|
|
1939
|
+
: undefined;
|
|
1940
|
+
const changeMode = isVerificationChangeMode(item.changeMode)
|
|
1941
|
+
? item.changeMode
|
|
1942
|
+
: undefined;
|
|
1943
|
+
const changeFingerprint = typeof item.changeFingerprint === "string" &&
|
|
1944
|
+
item.changeFingerprint.trim().length > 0
|
|
1945
|
+
? item.changeFingerprint.trim()
|
|
1946
|
+
: undefined;
|
|
1947
|
+
const exitCode = typeof item.exitCode === "number" && Number.isFinite(item.exitCode)
|
|
1948
|
+
? Math.trunc(item.exitCode)
|
|
1949
|
+
: undefined;
|
|
1950
|
+
const durationMs = typeof item.durationMs === "number" && Number.isFinite(item.durationMs)
|
|
1951
|
+
? Math.max(0, Math.trunc(item.durationMs))
|
|
1952
|
+
: undefined;
|
|
1953
|
+
const stdoutTail = typeof item.stdoutTail === "string" && item.stdoutTail.trim().length > 0
|
|
1954
|
+
? item.stdoutTail
|
|
1955
|
+
: undefined;
|
|
1956
|
+
const stderrTail = typeof item.stderrTail === "string" && item.stderrTail.trim().length > 0
|
|
1957
|
+
? item.stderrTail
|
|
1958
|
+
: undefined;
|
|
1959
|
+
const note = typeof item.note === "string" && item.note.trim().length > 0
|
|
1960
|
+
? item.note.trim()
|
|
1961
|
+
: undefined;
|
|
1962
|
+
return [{
|
|
1963
|
+
command,
|
|
1964
|
+
status,
|
|
1965
|
+
recordedAt,
|
|
1966
|
+
source,
|
|
1967
|
+
changedFiles,
|
|
1968
|
+
changeMode,
|
|
1969
|
+
changeFingerprint,
|
|
1970
|
+
exitCode,
|
|
1971
|
+
durationMs,
|
|
1972
|
+
stdoutTail,
|
|
1973
|
+
stderrTail,
|
|
1974
|
+
note,
|
|
1975
|
+
}];
|
|
1976
|
+
}));
|
|
1977
|
+
}
|
|
1978
|
+
function uniqueVerificationEvidence(evidence) {
|
|
1979
|
+
const seen = new Set();
|
|
1980
|
+
return evidence.filter((item) => {
|
|
1981
|
+
const key = [
|
|
1982
|
+
item.command,
|
|
1983
|
+
item.status,
|
|
1984
|
+
item.source,
|
|
1985
|
+
String(item.exitCode ?? ""),
|
|
1986
|
+
(item.changedFiles ?? []).join(","),
|
|
1987
|
+
item.changeMode ?? "",
|
|
1988
|
+
item.changeFingerprint ?? "",
|
|
1989
|
+
item.recordedAt,
|
|
1990
|
+
item.note ?? "",
|
|
1991
|
+
].join("\0");
|
|
1992
|
+
if (seen.has(key)) {
|
|
1993
|
+
return false;
|
|
1994
|
+
}
|
|
1995
|
+
seen.add(key);
|
|
1996
|
+
return true;
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1999
|
+
function normalizeVerificationFilePath(filePath) {
|
|
2000
|
+
return filePath.replace(/\\/g, "/");
|
|
2001
|
+
}
|
|
2002
|
+
function isVerificationStatus(value) {
|
|
2003
|
+
return value === "passed" || value === "failed" || value === "skipped" || value === "unknown";
|
|
2004
|
+
}
|
|
2005
|
+
function isVerificationChangeMode(value) {
|
|
2006
|
+
return value === "staged" || value === "changed" || value === "worktree";
|
|
2007
|
+
}
|
|
1469
2008
|
function normalizeReadinessSnapshot(value) {
|
|
1470
2009
|
if (!isRecord(value)) {
|
|
1471
2010
|
return fallbackReadinessSnapshot();
|
|
@@ -1527,6 +2066,9 @@ function normalizePolicyExplanationSnapshot(value, defaults) {
|
|
|
1527
2066
|
? rawPolicySource
|
|
1528
2067
|
: defaults.policySource;
|
|
1529
2068
|
const rawPolicyRisk = raw?.policyRisk;
|
|
2069
|
+
const requiredGateNextSteps = defaults.humanGate !== "none"
|
|
2070
|
+
? fallbackPolicyExplanationNextSteps(defaults.humanGate)
|
|
2071
|
+
: [];
|
|
1530
2072
|
return {
|
|
1531
2073
|
protocol: "ripple-policy-explanation",
|
|
1532
2074
|
version: 1,
|
|
@@ -1545,7 +2087,7 @@ function normalizePolicyExplanationSnapshot(value, defaults) {
|
|
|
1545
2087
|
matchedRules,
|
|
1546
2088
|
why: rawWhy.length > 0 ? rawWhy : fallbackPolicyExplanationWhy(defaults),
|
|
1547
2089
|
nextSteps: rawNextSteps.length > 0
|
|
1548
|
-
? rawNextSteps
|
|
2090
|
+
? uniqueItems([...requiredGateNextSteps, ...rawNextSteps])
|
|
1549
2091
|
: fallbackPolicyExplanationNextSteps(defaults.humanGate),
|
|
1550
2092
|
};
|
|
1551
2093
|
}
|
|
@@ -1564,10 +2106,10 @@ function fallbackPolicyExplanationWhy(defaults) {
|
|
|
1564
2106
|
}
|
|
1565
2107
|
function fallbackPolicyExplanationNextSteps(humanGate) {
|
|
1566
2108
|
if (humanGate === "required-before-edit") {
|
|
1567
|
-
return ["
|
|
2109
|
+
return ["Agent must get human approval before editing this file."];
|
|
1568
2110
|
}
|
|
1569
2111
|
if (humanGate === "required-before-merge") {
|
|
1570
|
-
return ["
|
|
2112
|
+
return ["Agent must get human approval before merging this change."];
|
|
1571
2113
|
}
|
|
1572
2114
|
return ["Check staged changes against this saved intent before handoff."];
|
|
1573
2115
|
}
|