@getripple/cli 1.0.10 → 1.0.11
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/dist/index.js +120 -260
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3059,12 +3059,30 @@ function intentCommand(action, options) {
|
|
|
3059
3059
|
intentStatusCommand(options);
|
|
3060
3060
|
return;
|
|
3061
3061
|
}
|
|
3062
|
+
if (action === "push") {
|
|
3063
|
+
void intentPushCommand(options);
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3062
3066
|
if (action === "close") {
|
|
3063
3067
|
closeIntentCommand(options);
|
|
3064
3068
|
return;
|
|
3065
3069
|
}
|
|
3066
3070
|
throw new Error("Usage: ripple intent status [--intent latest|path] or ripple intent close --reason <text> [--intent latest|path]");
|
|
3067
3071
|
}
|
|
3072
|
+
async function intentPushCommand(options) {
|
|
3073
|
+
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
3074
|
+
const intentRef = options.intent ?? "latest";
|
|
3075
|
+
const intent = (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
|
|
3076
|
+
console.log(`[Ripple] Pushing active intent '${intent.id}' to Ripple Cloud...`);
|
|
3077
|
+
const result = await (0, core_1.pushIntentToCloud)(intent);
|
|
3078
|
+
if (result.sent) {
|
|
3079
|
+
console.log("✅ Intent pushed successfully. This is now the active boundary for CI checks.");
|
|
3080
|
+
}
|
|
3081
|
+
else {
|
|
3082
|
+
console.error(`❌ Failed to push intent: ${result.error}`);
|
|
3083
|
+
process.exitCode = 1;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3068
3086
|
function intentStatusCommand(options) {
|
|
3069
3087
|
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
3070
3088
|
const intentRef = options.intent ?? "latest";
|
|
@@ -3393,6 +3411,8 @@ async function auditCommand(options) {
|
|
|
3393
3411
|
}
|
|
3394
3412
|
applyStrictExit(options.strict && strictAuditShouldFail(audit));
|
|
3395
3413
|
}
|
|
3414
|
+
// in: ripple-main/packages/cli/src/index.ts
|
|
3415
|
+
// REPLACE THE ENTIRE OLD `gateCommand` FUNCTION WITH THIS:
|
|
3396
3416
|
async function gateCommand(options) {
|
|
3397
3417
|
if (selectedChangeModeCount(options) > 1) {
|
|
3398
3418
|
throw new Error("Choose one gate/audit mode: --staged, --worktree, or --changed --base <ref>");
|
|
@@ -3401,8 +3421,9 @@ async function gateCommand(options) {
|
|
|
3401
3421
|
const intentRef = options.intent ?? "latest";
|
|
3402
3422
|
const mode = selectedChangeMode(options);
|
|
3403
3423
|
const baseRef = options.base ?? "HEAD";
|
|
3424
|
+
const intentPath = resolveCliIntentPath(workspaceRoot, intentRef);
|
|
3404
3425
|
try {
|
|
3405
|
-
(0, core_1.loadChangeIntent)(workspaceRoot,
|
|
3426
|
+
(0, core_1.loadChangeIntent)(workspaceRoot, intentPath);
|
|
3406
3427
|
}
|
|
3407
3428
|
catch (err) {
|
|
3408
3429
|
const block = (0, core_1.buildRippleGateIntentBlockSummary)({
|
|
@@ -3424,7 +3445,7 @@ async function gateCommand(options) {
|
|
|
3424
3445
|
applyStrictExit(true);
|
|
3425
3446
|
return;
|
|
3426
3447
|
}
|
|
3427
|
-
const audit = await buildAuditFromCliOptions(options);
|
|
3448
|
+
const audit = await buildAuditFromCliOptions({ ...options, intent: intentPath });
|
|
3428
3449
|
const gate = (0, core_1.buildRippleGateSummary)(audit);
|
|
3429
3450
|
if (options.json) {
|
|
3430
3451
|
printJson(gate);
|
|
@@ -3435,16 +3456,43 @@ async function gateCommand(options) {
|
|
|
3435
3456
|
else {
|
|
3436
3457
|
printGateSummary(gate);
|
|
3437
3458
|
}
|
|
3438
|
-
//
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3459
|
+
// Consume intent on pass to prevent "ghost intents" if commit is aborted.
|
|
3460
|
+
if (gate.canContinue && intentRef === "latest" && mode === "staged") {
|
|
3461
|
+
try {
|
|
3462
|
+
const consumedIntentCachePath = path.join(workspaceRoot, ".ripple", ".cache", "consumed-intent.json");
|
|
3463
|
+
if (fs.existsSync(consumedIntentCachePath)) {
|
|
3464
|
+
fs.unlinkSync(consumedIntentCachePath);
|
|
3465
|
+
}
|
|
3466
|
+
fs.renameSync(intentPath, consumedIntentCachePath);
|
|
3467
|
+
console.log("\n[Ripple] Intent validated and consumed by gate. Ready for commit.");
|
|
3468
|
+
}
|
|
3469
|
+
catch (err) {
|
|
3470
|
+
const errorDetail = err instanceof Error ? err.message : String(err);
|
|
3471
|
+
console.warn(`[Ripple] CRITICAL WARNING: Could not consume intent file. To prevent a future 'ghost intent' block, manually delete '${intentPath}' after your commit. Error: ${errorDetail}`);
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
// Sync a redacted, privacy-safe audit receipt to the cloud.
|
|
3442
3475
|
if (process.env.RIPPLE_API_KEY) {
|
|
3443
3476
|
const cloudDecision = gate.canContinue ? "continue" :
|
|
3444
3477
|
gate.needsHuman ? "human-review" : "blocked";
|
|
3445
|
-
const commitSha = (0, core_1.getCurrentCommitSha)(child_process_1.execSync);
|
|
3478
|
+
const commitSha = options.sha ?? (0, core_1.getCurrentCommitSha)(child_process_1.execSync);
|
|
3446
3479
|
const branch = (0, core_1.getCurrentBranch)(child_process_1.execSync);
|
|
3447
3480
|
const actor = (0, core_1.getCurrentActor)(child_process_1.execSync);
|
|
3481
|
+
const redactedPayload = {
|
|
3482
|
+
decision: gate.decision,
|
|
3483
|
+
status: gate.status,
|
|
3484
|
+
risk: {
|
|
3485
|
+
level: gate.risk.level,
|
|
3486
|
+
score: gate.risk.score,
|
|
3487
|
+
summary: gate.risk.summary,
|
|
3488
|
+
},
|
|
3489
|
+
auditStatus: audit.status,
|
|
3490
|
+
approvalStatus: audit.approvalStatus.status,
|
|
3491
|
+
intent: audit.intent,
|
|
3492
|
+
mode: audit.mode,
|
|
3493
|
+
baseRef: audit.baseRef,
|
|
3494
|
+
blockingReasons: audit.blockingReasons,
|
|
3495
|
+
};
|
|
3448
3496
|
const cloudResult = await (0, core_1.syncAuditToCloud)({
|
|
3449
3497
|
intentId: audit.intent.id,
|
|
3450
3498
|
decision: cloudDecision,
|
|
@@ -3452,42 +3500,16 @@ async function gateCommand(options) {
|
|
|
3452
3500
|
branch,
|
|
3453
3501
|
actor,
|
|
3454
3502
|
source: "cli",
|
|
3455
|
-
payload:
|
|
3456
|
-
// Identity
|
|
3457
|
-
intentId: audit.intent.id,
|
|
3458
|
-
commitSha,
|
|
3459
|
-
branch,
|
|
3460
|
-
actor,
|
|
3461
|
-
source: "pre-commit-gate",
|
|
3462
|
-
// Intent declared scope — what the developer said they would change
|
|
3463
|
-
task: audit.intent.task,
|
|
3464
|
-
targetFile: audit.intent.targetFile,
|
|
3465
|
-
controlMode: audit.intent.controlMode,
|
|
3466
|
-
boundaryRisk: audit.intent.boundaryRisk,
|
|
3467
|
-
humanGate: audit.intent.humanGate,
|
|
3468
|
-
// Gate verdict — what Ripple actually decided
|
|
3469
|
-
decision: cloudDecision,
|
|
3470
|
-
status: gate.status,
|
|
3471
|
-
canContinue: gate.canContinue,
|
|
3472
|
-
mustStop: gate.mustStop,
|
|
3473
|
-
needsHuman: gate.needsHuman,
|
|
3474
|
-
// Evidence — what actually changed
|
|
3475
|
-
blockingReasons: audit.blockingReasons ?? [],
|
|
3476
|
-
changedFiles: audit.changedFiles ?? [],
|
|
3477
|
-
changedOutsideBoundaryFiles: audit.stagedCheck?.intentValidation?.boundaryVerdict?.changedOutsideBoundaryFiles ?? [],
|
|
3478
|
-
},
|
|
3503
|
+
payload: redactedPayload,
|
|
3479
3504
|
});
|
|
3480
|
-
if (
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
console.warn(`\n[Ripple Cloud] ⚠ Sync failed (non-blocking): ${cloudResult.error}`);
|
|
3487
|
-
}
|
|
3505
|
+
if (cloudResult.sent) {
|
|
3506
|
+
const icon = cloudDecision === "continue" ? "✓" : "⛔";
|
|
3507
|
+
console.log(`\n[Ripple Cloud] ${icon} Gate decision recorded: ${cloudDecision} (${commitSha.slice(0, 7)})`);
|
|
3508
|
+
}
|
|
3509
|
+
else if (cloudResult.error) {
|
|
3510
|
+
console.warn(`\n[Ripple Cloud] ⚠ Sync failed (non-blocking): ${cloudResult.error}`);
|
|
3488
3511
|
}
|
|
3489
3512
|
}
|
|
3490
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
3491
3513
|
applyStrictExit(options.strict && !gate.canContinue);
|
|
3492
3514
|
}
|
|
3493
3515
|
function approveCommand(options) {
|
|
@@ -3692,14 +3714,21 @@ function approvalCommand(options) {
|
|
|
3692
3714
|
}
|
|
3693
3715
|
printApprovalStatus(status);
|
|
3694
3716
|
}
|
|
3717
|
+
// REPLACE WITH THIS NEW FUNCTION
|
|
3718
|
+
// in: ripple-main/packages/cli/src/index.ts
|
|
3719
|
+
// REPLACE THE ENTIRE OLD `ciCommand` FUNCTION WITH THIS:
|
|
3695
3720
|
async function ciCommand(options) {
|
|
3696
3721
|
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
3697
3722
|
const baseRef = options.base ?? defaultCiBaseRef();
|
|
3698
|
-
const hasExplicitIntent = Boolean(options.intent);
|
|
3699
|
-
const intentRef = options.intent ?? "latest";
|
|
3700
3723
|
const files = (0, core_1.listGitChangedFiles)(workspaceRoot, baseRef);
|
|
3701
3724
|
const emitGithubAnnotations = shouldEmitGithubAnnotations(options);
|
|
3702
|
-
|
|
3725
|
+
const commitSha = options.sha ?? (0, core_1.getCurrentCommitSha)(child_process_1.execSync);
|
|
3726
|
+
// FETCH THE AUTHORITATIVE INTENT FROM THE CLOUD.
|
|
3727
|
+
const intent = await (0, core_1.fetchActiveIntentForCommit)(commitSha);
|
|
3728
|
+
if (!intent) {
|
|
3729
|
+
// If no active intent is found in the cloud, run a policy-only audit.
|
|
3730
|
+
// This provides a baseline check without enforcing a specific boundary.
|
|
3731
|
+
console.log("[Ripple CI] No active cloud intent found. Running in policy-only audit mode.");
|
|
3703
3732
|
const summary = await buildCheckSummaryForFiles({
|
|
3704
3733
|
workspaceRoot,
|
|
3705
3734
|
files,
|
|
@@ -3708,130 +3737,20 @@ async function ciCommand(options) {
|
|
|
3708
3737
|
tokenBudget: options.budget,
|
|
3709
3738
|
});
|
|
3710
3739
|
const policySync = buildPolicySyncSummary(workspaceRoot);
|
|
3711
|
-
if (
|
|
3712
|
-
printJson({
|
|
3713
|
-
...summary,
|
|
3714
|
-
protocol: "ripple-ci-policy-audit",
|
|
3715
|
-
version: 1,
|
|
3716
|
-
auditMode: true,
|
|
3717
|
-
blocking: false,
|
|
3718
|
-
intentRequired: false,
|
|
3719
|
-
policySync,
|
|
3720
|
-
});
|
|
3721
|
-
}
|
|
3722
|
-
else if (options.agent) {
|
|
3723
|
-
printAgentStagedCheckSummary(summary);
|
|
3724
|
-
console.log("");
|
|
3725
|
-
console.log("ci_mode: policy-audit");
|
|
3726
|
-
console.log("blocking: false");
|
|
3727
|
-
console.log("intent_required: false");
|
|
3728
|
-
console.log(`policy_sync: ${policySync.status}`);
|
|
3729
|
-
if (policySync.missingRules.length > 0) {
|
|
3730
|
-
console.log("policy_sync_missing_rules:");
|
|
3731
|
-
policySync.missingRules.slice(0, 12).forEach((rule) => {
|
|
3732
|
-
console.log(`- ${rule.paths.join(", ")} risk=${rule.risk ?? "medium"}`);
|
|
3733
|
-
});
|
|
3734
|
-
}
|
|
3735
|
-
console.log("next_required_action: Review policy-risk findings and policy-sync warnings before merge. Use --intent latest --strict only when you want an intent-bound hard gate.");
|
|
3736
|
-
}
|
|
3737
|
-
else {
|
|
3738
|
-
console.log("Ripple CI policy audit");
|
|
3739
|
-
console.log("Status: audit");
|
|
3740
|
-
console.log("Blocking: false");
|
|
3741
|
-
console.log("Intent: none (local intents are not required in CI audit mode)");
|
|
3742
|
-
console.log(`Policy sync: ${policySync.status}`);
|
|
3743
|
-
if (policySync.missingRules.length > 0) {
|
|
3744
|
-
console.log("");
|
|
3745
|
-
console.log("Policy may be missing risky repo surfaces:");
|
|
3746
|
-
policySync.missingRules.slice(0, 12).forEach((rule) => {
|
|
3747
|
-
console.log(`- ${rule.paths.join(", ")} risk=${rule.risk ?? "medium"}`);
|
|
3748
|
-
});
|
|
3749
|
-
}
|
|
3750
|
-
console.log("");
|
|
3751
|
-
printStagedCheckSummary(summary);
|
|
3752
|
-
console.log("");
|
|
3753
|
-
console.log("Next action: Review policy-risk findings and policy-sync warnings before merge. Use --intent latest --strict only when you want an intent-bound hard gate.");
|
|
3754
|
-
}
|
|
3755
|
-
if (emitGithubAnnotations && !options.json) {
|
|
3740
|
+
if (emitGithubAnnotations) {
|
|
3756
3741
|
printGithubPolicyAuditAnnotations(summary, policySync);
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
if (process.env.RIPPLE_API_KEY) {
|
|
3761
|
-
const commitSha = options.sha ?? (0, core_1.getCurrentCommitSha)(child_process_1.execSync);
|
|
3762
|
-
const branch = (0, core_1.getCurrentBranch)(child_process_1.execSync);
|
|
3763
|
-
const actor = (0, core_1.getCurrentActor)(child_process_1.execSync);
|
|
3764
|
-
const syntheticIntentId = `ci-policy-${commitSha.slice(0, 16)}`;
|
|
3765
|
-
const cloudResult = await (0, core_1.syncAuditToCloud)({
|
|
3766
|
-
protocol: 'ripple-audit', // FOUNDER FIX: Satisfy the strict Zod Cloud Schema
|
|
3767
|
-
intentId: syntheticIntentId,
|
|
3768
|
-
decision: 'continue',
|
|
3769
|
-
commitSha,
|
|
3770
|
-
branch,
|
|
3771
|
-
actor,
|
|
3772
|
-
source: 'ci',
|
|
3773
|
-
payload: { mode: 'policy-audit', commitSha, branch, actor },
|
|
3774
|
-
});
|
|
3775
|
-
if (!options.json && !options.agent) {
|
|
3776
|
-
if (cloudResult.sent) {
|
|
3777
|
-
console.log(`\n[Ripple Cloud] ✓ Policy audit synced (${commitSha.slice(0, 7)})`);
|
|
3778
|
-
}
|
|
3779
|
-
else if (cloudResult.error) {
|
|
3780
|
-
console.warn(`\n[Ripple Cloud] ⚠ Sync failed: ${cloudResult.error}`);
|
|
3781
|
-
}
|
|
3782
|
-
}
|
|
3783
|
-
}
|
|
3784
|
-
return;
|
|
3785
|
-
}
|
|
3786
|
-
let intent;
|
|
3787
|
-
try {
|
|
3788
|
-
intent = (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
|
|
3789
|
-
}
|
|
3790
|
-
catch (err) {
|
|
3791
|
-
const summary = await buildCheckSummaryForFiles({
|
|
3792
|
-
workspaceRoot,
|
|
3793
|
-
files,
|
|
3794
|
-
mode: "changed",
|
|
3795
|
-
baseRef,
|
|
3796
|
-
tokenBudget: options.budget,
|
|
3797
|
-
});
|
|
3798
|
-
const message = intentLoadFailureMessage(intentRef, err);
|
|
3799
|
-
if (options.json) {
|
|
3800
|
-
printJson({
|
|
3801
|
-
...summary,
|
|
3802
|
-
nextRequiredPhase: MISSING_INTENT_NEXT_REQUIRED_PHASE,
|
|
3803
|
-
nextRequiredAction: MISSING_INTENT_NEXT_REQUIRED_ACTION,
|
|
3804
|
-
intentLoadError: {
|
|
3805
|
-
intent: intentRef,
|
|
3806
|
-
message,
|
|
3807
|
-
nextRequiredPhase: MISSING_INTENT_NEXT_REQUIRED_PHASE,
|
|
3808
|
-
nextRequiredAction: MISSING_INTENT_NEXT_REQUIRED_ACTION,
|
|
3809
|
-
},
|
|
3742
|
+
printGithubNoticeAnnotation({
|
|
3743
|
+
title: "Ripple: Policy Audit Mode",
|
|
3744
|
+
message: "No active intent was found in Ripple Cloud for this commit. To enable strict boundary checks, run `ripple intent push` locally after creating a plan.",
|
|
3810
3745
|
});
|
|
3811
3746
|
}
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
console.log(`next_required_phase: ${MISSING_INTENT_NEXT_REQUIRED_PHASE}`);
|
|
3816
|
-
console.log(`next_required_action: ${MISSING_INTENT_NEXT_REQUIRED_ACTION}`);
|
|
3817
|
-
console.log("");
|
|
3818
|
-
console.log("intent_error:");
|
|
3819
|
-
console.log(`- ${message}`);
|
|
3820
|
-
}
|
|
3821
|
-
else {
|
|
3822
|
-
printStagedCheckSummary(summary);
|
|
3823
|
-
console.log("");
|
|
3824
|
-
console.log(`Next required phase: ${MISSING_INTENT_NEXT_REQUIRED_PHASE}`);
|
|
3825
|
-
console.log(`Next required action: ${MISSING_INTENT_NEXT_REQUIRED_ACTION}`);
|
|
3826
|
-
console.log(`Intent error: ${message}`);
|
|
3827
|
-
}
|
|
3828
|
-
if (emitGithubAnnotations && !options.json) {
|
|
3829
|
-
printGithubIntentLoadError(message);
|
|
3830
|
-
}
|
|
3831
|
-
writeGithubStepSummary({ summary, intentLoadError: message });
|
|
3832
|
-
applyStrictExit(options.strict);
|
|
3747
|
+
writeGithubPolicyAuditStepSummary(summary, policySync);
|
|
3748
|
+
// Policy-only audits do not block the build unless strict mode is on.
|
|
3749
|
+
applyStrictExit(options.strict && summary.requiresAttention);
|
|
3833
3750
|
return;
|
|
3834
3751
|
}
|
|
3752
|
+
// An active intent was found. Run the full, strict audit against the cloud boundary.
|
|
3753
|
+
console.log(`[Ripple CI] Active intent '${intent.id}' found in cloud. Running full boundary check.`);
|
|
3835
3754
|
const audit = await buildAuditForFiles({
|
|
3836
3755
|
workspaceRoot,
|
|
3837
3756
|
files,
|
|
@@ -3839,13 +3758,10 @@ async function ciCommand(options) {
|
|
|
3839
3758
|
baseRef,
|
|
3840
3759
|
tokenBudget: options.budget,
|
|
3841
3760
|
intent,
|
|
3842
|
-
currentPolicyExplanation:
|
|
3761
|
+
currentPolicyExplanation: (0, core_1.explainRipplePolicyForIntent)((0, core_1.loadRipplePolicy)(workspaceRoot), intent),
|
|
3843
3762
|
});
|
|
3844
3763
|
if (options.json) {
|
|
3845
|
-
printJson({
|
|
3846
|
-
...audit,
|
|
3847
|
-
gate: (0, core_1.buildRippleGateSummary)(audit),
|
|
3848
|
-
});
|
|
3764
|
+
printJson({ ...audit, gate: (0, core_1.buildRippleGateSummary)(audit) });
|
|
3849
3765
|
}
|
|
3850
3766
|
else if (options.agent) {
|
|
3851
3767
|
printAgentAuditSummary(audit);
|
|
@@ -3857,45 +3773,46 @@ async function ciCommand(options) {
|
|
|
3857
3773
|
printGithubAuditAnnotations(audit);
|
|
3858
3774
|
}
|
|
3859
3775
|
writeGithubAuditStepSummary(audit);
|
|
3860
|
-
//
|
|
3861
|
-
// Sends the audit result to ripple-cloud so the GitHub App can verify it.
|
|
3862
|
-
// Only runs when RIPPLE_API_KEY is set. Never blocks or throws.
|
|
3776
|
+
// Sync the final audit result back to the cloud for the webhook to read.
|
|
3863
3777
|
if (process.env.RIPPLE_API_KEY) {
|
|
3864
3778
|
const gate = (0, core_1.buildRippleGateSummary)(audit);
|
|
3865
3779
|
const cloudDecision = gate.canContinue ? "continue" :
|
|
3866
3780
|
gate.needsHuman ? "human-review" : "blocked";
|
|
3867
|
-
const commitSha = options.sha ?? (0, core_1.getCurrentCommitSha)(child_process_1.execSync);
|
|
3868
3781
|
const branch = (0, core_1.getCurrentBranch)(child_process_1.execSync);
|
|
3869
3782
|
const actor = (0, core_1.getCurrentActor)(child_process_1.execSync);
|
|
3783
|
+
const redactedPayload = {
|
|
3784
|
+
decision: gate.decision,
|
|
3785
|
+
status: gate.status,
|
|
3786
|
+
risk: {
|
|
3787
|
+
level: gate.risk.level,
|
|
3788
|
+
score: gate.risk.score,
|
|
3789
|
+
summary: gate.risk.summary,
|
|
3790
|
+
},
|
|
3791
|
+
auditStatus: audit.status,
|
|
3792
|
+
approvalStatus: audit.approvalStatus.status,
|
|
3793
|
+
intent: audit.intent,
|
|
3794
|
+
mode: audit.mode,
|
|
3795
|
+
baseRef: audit.baseRef,
|
|
3796
|
+
blockingReasons: audit.blockingReasons,
|
|
3797
|
+
};
|
|
3870
3798
|
const cloudResult = await (0, core_1.syncAuditToCloud)({
|
|
3871
3799
|
intentId: intent.id,
|
|
3872
3800
|
decision: cloudDecision,
|
|
3873
|
-
commitSha,
|
|
3874
|
-
branch,
|
|
3875
|
-
actor,
|
|
3876
|
-
source:
|
|
3877
|
-
payload:
|
|
3878
|
-
intentId: intent.id,
|
|
3879
|
-
commitSha,
|
|
3880
|
-
branch,
|
|
3881
|
-
actor,
|
|
3882
|
-
decision: cloudDecision,
|
|
3883
|
-
gate,
|
|
3884
|
-
violations: audit.violations?.length ?? 0,
|
|
3885
|
-
riskLevel: audit.riskLevel ?? "unknown",
|
|
3886
|
-
},
|
|
3801
|
+
commitSha: commitSha,
|
|
3802
|
+
branch: branch,
|
|
3803
|
+
actor: actor,
|
|
3804
|
+
source: 'ci',
|
|
3805
|
+
payload: redactedPayload,
|
|
3887
3806
|
});
|
|
3888
|
-
if (
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
console.warn(`[Ripple Cloud] ⚠ Sync failed (non-blocking): ${cloudResult.error}`);
|
|
3894
|
-
}
|
|
3807
|
+
if (cloudResult.sent) {
|
|
3808
|
+
console.log(`[Ripple Cloud] ✓ Audit synced (${commitSha.slice(0, 7)})`);
|
|
3809
|
+
}
|
|
3810
|
+
else if (cloudResult.error) {
|
|
3811
|
+
console.warn(`[Ripple Cloud] ⚠ Sync failed (non-blocking): ${cloudResult.error}`);
|
|
3895
3812
|
}
|
|
3896
3813
|
}
|
|
3897
|
-
//
|
|
3898
|
-
applyStrictExit(
|
|
3814
|
+
// Exit with a non-zero code if the audit did not pass, failing the CI job.
|
|
3815
|
+
applyStrictExit(!audit.canProceed);
|
|
3899
3816
|
}
|
|
3900
3817
|
async function doctorCommand(options) {
|
|
3901
3818
|
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
@@ -4218,77 +4135,20 @@ function ripplePreCommitHookScript() {
|
|
|
4218
4135
|
"",
|
|
4219
4136
|
].join("\n");
|
|
4220
4137
|
}
|
|
4138
|
+
// ripple-main/packages/cli/src/index.ts
|
|
4139
|
+
// ... (keep all existing functions)
|
|
4221
4140
|
function ripplePostCommitHookBlock() {
|
|
4222
4141
|
return [
|
|
4223
4142
|
RIPPLE_POST_COMMIT_HOOK_START,
|
|
4224
|
-
`#
|
|
4225
|
-
#
|
|
4143
|
+
`# After a successful commit, this hook cleans up the consumed intent marker
|
|
4144
|
+
# left by the 'ripple gate' command. This prevents old intents from being
|
|
4145
|
+
# reused accidentally if a commit is amended or rebased.
|
|
4226
4146
|
set +e
|
|
4227
4147
|
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
INTENT_FILE=""
|
|
4234
|
-
for intent_file in .ripple/.cache/latest-intent.json .ripple/intents/latest.json; do
|
|
4235
|
-
if [ -f "$intent_file" ]; then
|
|
4236
|
-
INTENT_FILE="$intent_file"
|
|
4237
|
-
break
|
|
4238
|
-
fi
|
|
4239
|
-
done
|
|
4240
|
-
|
|
4241
|
-
# Cloud sync — only if API key is set and we have an intent to sync
|
|
4242
|
-
if [ -n "$RIPPLE_API_KEY" ] && [ -n "$INTENT_FILE" ]; then
|
|
4243
|
-
INTENT_ID=$(node -e "try{const d=require('fs').readFileSync(process.argv[1],'utf8');console.log(JSON.parse(d).id||'')}catch(e){console.log('')}" "$INTENT_FILE" 2>/dev/null || echo "")
|
|
4244
|
-
if [ -n "$INTENT_ID" ]; then
|
|
4245
|
-
node -e "
|
|
4246
|
-
const https = require('https');
|
|
4247
|
-
const http = require('http');
|
|
4248
|
-
const url = new URL(process.env.CLOUD_URL + '/api/audit');
|
|
4249
|
-
const isHttps = url.protocol === 'https:';
|
|
4250
|
-
const body = JSON.stringify({
|
|
4251
|
-
intentId: process.env.INTENT_ID,
|
|
4252
|
-
decision: 'continue',
|
|
4253
|
-
commitSha: process.env.COMMIT_SHA,
|
|
4254
|
-
branch: process.env.BRANCH,
|
|
4255
|
-
actor: process.env.ACTOR,
|
|
4256
|
-
source: 'cli',
|
|
4257
|
-
payload: { commitSha: process.env.COMMIT_SHA, source: 'post-commit-hook', branch: process.env.BRANCH }
|
|
4258
|
-
});
|
|
4259
|
-
const lib = isHttps ? https : http;
|
|
4260
|
-
const req = lib.request({
|
|
4261
|
-
hostname: url.hostname,
|
|
4262
|
-
port: url.port || (isHttps ? 443 : 80),
|
|
4263
|
-
path: url.pathname,
|
|
4264
|
-
method: 'POST',
|
|
4265
|
-
headers: {
|
|
4266
|
-
'Content-Type': 'application/json',
|
|
4267
|
-
'Authorization': 'Bearer ' + process.env.RIPPLE_API_KEY,
|
|
4268
|
-
'Content-Length': Buffer.byteLength(body)
|
|
4269
|
-
}
|
|
4270
|
-
}, res => {
|
|
4271
|
-
if (res.statusCode < 300) process.stdout.write('[Ripple Cloud] Commit synced (' + process.env.COMMIT_SHA.slice(0,7) + ')\\n');
|
|
4272
|
-
});
|
|
4273
|
-
req.setTimeout(3000, () => req.destroy());
|
|
4274
|
-
req.on('error', () => {});
|
|
4275
|
-
req.write(body);
|
|
4276
|
-
req.end();
|
|
4277
|
-
" 2>/dev/null || true
|
|
4278
|
-
fi
|
|
4279
|
-
fi
|
|
4280
|
-
|
|
4281
|
-
# Clear the local intent after sync
|
|
4282
|
-
cleared=0
|
|
4283
|
-
for intent_file in .ripple/.cache/latest-intent.json .ripple/intents/latest.json; do
|
|
4284
|
-
if [ -f "$intent_file" ]; then
|
|
4285
|
-
rm "$intent_file"
|
|
4286
|
-
cleared=1
|
|
4287
|
-
fi
|
|
4288
|
-
done
|
|
4289
|
-
|
|
4290
|
-
if [ "$cleared" -eq 1 ]; then
|
|
4291
|
-
echo "[Ripple] Consumed and cleared local intent."
|
|
4148
|
+
CONSUMED_INTENT_FILE=".ripple/.cache/consumed-intent.json"
|
|
4149
|
+
if [ -f "$CONSUMED_INTENT_FILE" ]; then
|
|
4150
|
+
rm "$CONSUMED_INTENT_FILE"
|
|
4151
|
+
echo "[Ripple] Cleaned up consumed intent marker."
|
|
4292
4152
|
fi`,
|
|
4293
4153
|
RIPPLE_POST_COMMIT_HOOK_END,
|
|
4294
4154
|
"",
|