@getripple/core 1.0.7 → 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.
@@ -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
- const parsed = JSON.parse(fs.readFileSync(targetPath, "utf8"));
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 effectiveGuidance = mergeReadinessDriftGuidance(policyGuidance, readinessDrift);
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.driftVerdict.status === "pass"
741
- ? validation.driftVerdict.fix
742
- : validation.nextSteps,
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: ${sourcePath}`);
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(`Unsupported Ripple change intent: ${sourcePath}`);
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: ${sourcePath}`);
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 ["Ask the human to approve before the agent edits this file."];
2109
+ return ["Agent must get human approval before editing this file."];
1568
2110
  }
1569
2111
  if (humanGate === "required-before-merge") {
1570
- return ["Require human review before merging this change."];
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
  }