@getripple/cli 1.0.9 → 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 CHANGED
@@ -544,6 +544,19 @@ function parseCliArgs(argv) {
544
544
  options.base = token.slice("--base=".length);
545
545
  continue;
546
546
  }
547
+ if (token === "--sha") {
548
+ const value = argv[i + 1];
549
+ if (!value || value.startsWith("-")) {
550
+ throw new Error("Missing value for --sha");
551
+ }
552
+ options.sha = value;
553
+ i++;
554
+ continue;
555
+ }
556
+ if (token.startsWith("--sha=")) {
557
+ options.sha = token.slice("--sha=".length);
558
+ continue;
559
+ }
547
560
  if (token === "--budget") {
548
561
  const value = argv[i + 1];
549
562
  if (!value || value.startsWith("-")) {
@@ -627,18 +640,21 @@ function rippleDirectRunnerHookLines() {
627
640
  const GITHUB_ACTIONS_WORKFLOW_PATH = core_1.RIPPLE_CI_WORKFLOW_PATH;
628
641
  function githubActionsWorkflow() {
629
642
  return [
630
- "name: Ripple",
643
+ "name: Ripple Enterprise Gate",
631
644
  "",
632
645
  "on:",
633
646
  " pull_request:",
647
+ " push:",
648
+ " branches: [main, master]",
634
649
  "",
635
650
  "permissions:",
636
651
  " contents: read",
637
652
  " pull-requests: read",
653
+ " checks: write",
638
654
  "",
639
655
  "jobs:",
640
656
  " ripple:",
641
- " name: Ripple architecture gate",
657
+ " name: Ripple authorization gate",
642
658
  " runs-on: ubuntu-latest",
643
659
  " steps:",
644
660
  " - name: Checkout",
@@ -649,8 +665,11 @@ function githubActionsWorkflow() {
649
665
  " uses: actions/setup-node@v4",
650
666
  " with:",
651
667
  " node-version: 20",
652
- " - name: Ripple CI",
653
- ` run: npx -y ${rippleCliPackageSpec()} ci --base origin/\${{ github.base_ref }} --github-annotations`,
668
+ " - name: Ripple CI gate",
669
+ ` run: npx -y ${rippleCliPackageSpec()} ci --base origin/\${{ github.base_ref }} --github-annotations --sha \${{ github.sha }}`,
670
+ " env:",
671
+ " RIPPLE_API_KEY: ${{ secrets.RIPPLE_API_KEY }}",
672
+ " RIPPLE_CLOUD_URL: ${{ secrets.RIPPLE_CLOUD_URL }}",
654
673
  "",
655
674
  ].join("\n");
656
675
  }
@@ -3040,12 +3059,30 @@ function intentCommand(action, options) {
3040
3059
  intentStatusCommand(options);
3041
3060
  return;
3042
3061
  }
3062
+ if (action === "push") {
3063
+ void intentPushCommand(options);
3064
+ return;
3065
+ }
3043
3066
  if (action === "close") {
3044
3067
  closeIntentCommand(options);
3045
3068
  return;
3046
3069
  }
3047
3070
  throw new Error("Usage: ripple intent status [--intent latest|path] or ripple intent close --reason <text> [--intent latest|path]");
3048
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
+ }
3049
3086
  function intentStatusCommand(options) {
3050
3087
  const workspaceRoot = resolveWorkspaceRoot(".");
3051
3088
  const intentRef = options.intent ?? "latest";
@@ -3374,6 +3411,8 @@ async function auditCommand(options) {
3374
3411
  }
3375
3412
  applyStrictExit(options.strict && strictAuditShouldFail(audit));
3376
3413
  }
3414
+ // in: ripple-main/packages/cli/src/index.ts
3415
+ // REPLACE THE ENTIRE OLD `gateCommand` FUNCTION WITH THIS:
3377
3416
  async function gateCommand(options) {
3378
3417
  if (selectedChangeModeCount(options) > 1) {
3379
3418
  throw new Error("Choose one gate/audit mode: --staged, --worktree, or --changed --base <ref>");
@@ -3382,8 +3421,9 @@ async function gateCommand(options) {
3382
3421
  const intentRef = options.intent ?? "latest";
3383
3422
  const mode = selectedChangeMode(options);
3384
3423
  const baseRef = options.base ?? "HEAD";
3424
+ const intentPath = resolveCliIntentPath(workspaceRoot, intentRef);
3385
3425
  try {
3386
- (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
3426
+ (0, core_1.loadChangeIntent)(workspaceRoot, intentPath);
3387
3427
  }
3388
3428
  catch (err) {
3389
3429
  const block = (0, core_1.buildRippleGateIntentBlockSummary)({
@@ -3405,7 +3445,7 @@ async function gateCommand(options) {
3405
3445
  applyStrictExit(true);
3406
3446
  return;
3407
3447
  }
3408
- const audit = await buildAuditFromCliOptions(options);
3448
+ const audit = await buildAuditFromCliOptions({ ...options, intent: intentPath });
3409
3449
  const gate = (0, core_1.buildRippleGateSummary)(audit);
3410
3450
  if (options.json) {
3411
3451
  printJson(gate);
@@ -3416,6 +3456,60 @@ async function gateCommand(options) {
3416
3456
  else {
3417
3457
  printGateSummary(gate);
3418
3458
  }
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.
3475
+ if (process.env.RIPPLE_API_KEY) {
3476
+ const cloudDecision = gate.canContinue ? "continue" :
3477
+ gate.needsHuman ? "human-review" : "blocked";
3478
+ const commitSha = options.sha ?? (0, core_1.getCurrentCommitSha)(child_process_1.execSync);
3479
+ const branch = (0, core_1.getCurrentBranch)(child_process_1.execSync);
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
+ };
3496
+ const cloudResult = await (0, core_1.syncAuditToCloud)({
3497
+ intentId: audit.intent.id,
3498
+ decision: cloudDecision,
3499
+ commitSha,
3500
+ branch,
3501
+ actor,
3502
+ source: "cli",
3503
+ payload: redactedPayload,
3504
+ });
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}`);
3511
+ }
3512
+ }
3419
3513
  applyStrictExit(options.strict && !gate.canContinue);
3420
3514
  }
3421
3515
  function approveCommand(options) {
@@ -3620,14 +3714,21 @@ function approvalCommand(options) {
3620
3714
  }
3621
3715
  printApprovalStatus(status);
3622
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:
3623
3720
  async function ciCommand(options) {
3624
3721
  const workspaceRoot = resolveWorkspaceRoot(".");
3625
3722
  const baseRef = options.base ?? defaultCiBaseRef();
3626
- const hasExplicitIntent = Boolean(options.intent);
3627
- const intentRef = options.intent ?? "latest";
3628
3723
  const files = (0, core_1.listGitChangedFiles)(workspaceRoot, baseRef);
3629
3724
  const emitGithubAnnotations = shouldEmitGithubAnnotations(options);
3630
- if (!hasExplicitIntent) {
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.");
3631
3732
  const summary = await buildCheckSummaryForFiles({
3632
3733
  workspaceRoot,
3633
3734
  files,
@@ -3636,105 +3737,20 @@ async function ciCommand(options) {
3636
3737
  tokenBudget: options.budget,
3637
3738
  });
3638
3739
  const policySync = buildPolicySyncSummary(workspaceRoot);
3639
- if (options.json) {
3640
- printJson({
3641
- ...summary,
3642
- protocol: "ripple-ci-policy-audit",
3643
- version: 1,
3644
- auditMode: true,
3645
- blocking: false,
3646
- intentRequired: false,
3647
- policySync,
3648
- });
3649
- }
3650
- else if (options.agent) {
3651
- printAgentStagedCheckSummary(summary);
3652
- console.log("");
3653
- console.log("ci_mode: policy-audit");
3654
- console.log("blocking: false");
3655
- console.log("intent_required: false");
3656
- console.log(`policy_sync: ${policySync.status}`);
3657
- if (policySync.missingRules.length > 0) {
3658
- console.log("policy_sync_missing_rules:");
3659
- policySync.missingRules.slice(0, 12).forEach((rule) => {
3660
- console.log(`- ${rule.paths.join(", ")} risk=${rule.risk ?? "medium"}`);
3661
- });
3662
- }
3663
- 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.");
3664
- }
3665
- else {
3666
- console.log("Ripple CI policy audit");
3667
- console.log("Status: audit");
3668
- console.log("Blocking: false");
3669
- console.log("Intent: none (local intents are not required in CI audit mode)");
3670
- console.log(`Policy sync: ${policySync.status}`);
3671
- if (policySync.missingRules.length > 0) {
3672
- console.log("");
3673
- console.log("Policy may be missing risky repo surfaces:");
3674
- policySync.missingRules.slice(0, 12).forEach((rule) => {
3675
- console.log(`- ${rule.paths.join(", ")} risk=${rule.risk ?? "medium"}`);
3676
- });
3677
- }
3678
- console.log("");
3679
- printStagedCheckSummary(summary);
3680
- console.log("");
3681
- 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.");
3682
- }
3683
- if (emitGithubAnnotations && !options.json) {
3740
+ if (emitGithubAnnotations) {
3684
3741
  printGithubPolicyAuditAnnotations(summary, policySync);
3685
- }
3686
- writeGithubPolicyAuditStepSummary(summary, policySync);
3687
- return;
3688
- }
3689
- let intent;
3690
- try {
3691
- intent = (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
3692
- }
3693
- catch (err) {
3694
- const summary = await buildCheckSummaryForFiles({
3695
- workspaceRoot,
3696
- files,
3697
- mode: "changed",
3698
- baseRef,
3699
- tokenBudget: options.budget,
3700
- });
3701
- const message = intentLoadFailureMessage(intentRef, err);
3702
- if (options.json) {
3703
- printJson({
3704
- ...summary,
3705
- nextRequiredPhase: MISSING_INTENT_NEXT_REQUIRED_PHASE,
3706
- nextRequiredAction: MISSING_INTENT_NEXT_REQUIRED_ACTION,
3707
- intentLoadError: {
3708
- intent: intentRef,
3709
- message,
3710
- nextRequiredPhase: MISSING_INTENT_NEXT_REQUIRED_PHASE,
3711
- nextRequiredAction: MISSING_INTENT_NEXT_REQUIRED_ACTION,
3712
- },
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.",
3713
3745
  });
3714
3746
  }
3715
- else if (options.agent) {
3716
- printAgentStagedCheckSummary(summary);
3717
- console.log("");
3718
- console.log(`next_required_phase: ${MISSING_INTENT_NEXT_REQUIRED_PHASE}`);
3719
- console.log(`next_required_action: ${MISSING_INTENT_NEXT_REQUIRED_ACTION}`);
3720
- console.log("");
3721
- console.log("intent_error:");
3722
- console.log(`- ${message}`);
3723
- }
3724
- else {
3725
- printStagedCheckSummary(summary);
3726
- console.log("");
3727
- console.log(`Next required phase: ${MISSING_INTENT_NEXT_REQUIRED_PHASE}`);
3728
- console.log(`Next required action: ${MISSING_INTENT_NEXT_REQUIRED_ACTION}`);
3729
- console.log(`Intent error: ${message}`);
3730
- }
3731
- if (emitGithubAnnotations && !options.json) {
3732
- printGithubIntentLoadError(message);
3733
- }
3734
- writeGithubStepSummary({ summary, intentLoadError: message });
3735
- 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);
3736
3750
  return;
3737
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.`);
3738
3754
  const audit = await buildAuditForFiles({
3739
3755
  workspaceRoot,
3740
3756
  files,
@@ -3742,13 +3758,10 @@ async function ciCommand(options) {
3742
3758
  baseRef,
3743
3759
  tokenBudget: options.budget,
3744
3760
  intent,
3745
- currentPolicyExplanation: currentPolicyExplanationForIntent(workspaceRoot, intent),
3761
+ currentPolicyExplanation: (0, core_1.explainRipplePolicyForIntent)((0, core_1.loadRipplePolicy)(workspaceRoot), intent),
3746
3762
  });
3747
3763
  if (options.json) {
3748
- printJson({
3749
- ...audit,
3750
- gate: (0, core_1.buildRippleGateSummary)(audit),
3751
- });
3764
+ printJson({ ...audit, gate: (0, core_1.buildRippleGateSummary)(audit) });
3752
3765
  }
3753
3766
  else if (options.agent) {
3754
3767
  printAgentAuditSummary(audit);
@@ -3760,7 +3773,46 @@ async function ciCommand(options) {
3760
3773
  printGithubAuditAnnotations(audit);
3761
3774
  }
3762
3775
  writeGithubAuditStepSummary(audit);
3763
- applyStrictExit(options.strict && strictAuditShouldFail(audit));
3776
+ // Sync the final audit result back to the cloud for the webhook to read.
3777
+ if (process.env.RIPPLE_API_KEY) {
3778
+ const gate = (0, core_1.buildRippleGateSummary)(audit);
3779
+ const cloudDecision = gate.canContinue ? "continue" :
3780
+ gate.needsHuman ? "human-review" : "blocked";
3781
+ const branch = (0, core_1.getCurrentBranch)(child_process_1.execSync);
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
+ };
3798
+ const cloudResult = await (0, core_1.syncAuditToCloud)({
3799
+ intentId: intent.id,
3800
+ decision: cloudDecision,
3801
+ commitSha: commitSha,
3802
+ branch: branch,
3803
+ actor: actor,
3804
+ source: 'ci',
3805
+ payload: redactedPayload,
3806
+ });
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}`);
3812
+ }
3813
+ }
3814
+ // Exit with a non-zero code if the audit did not pass, failing the CI job.
3815
+ applyStrictExit(!audit.canProceed);
3764
3816
  }
3765
3817
  async function doctorCommand(options) {
3766
3818
  const workspaceRoot = resolveWorkspaceRoot(".");
@@ -4030,11 +4082,11 @@ fi
4030
4082
 
4031
4083
  set +e
4032
4084
 
4033
- ripple_run() {
4034
- ${rippleDirectRunnerHookLines().join("\n")}
4035
- }
4036
-
4037
- if [ -f ".ripple/intents/latest.json" ]; then
4085
+ ripple_run() {
4086
+ ${rippleDirectRunnerHookLines().join("\n")}
4087
+ }
4088
+
4089
+ if [ -f ".ripple/intents/latest.json" ]; then
4038
4090
  echo "[Ripple] Active local intent found. Checking staged changes against approved boundary..."
4039
4091
  ripple_run gate --staged --intent latest --agent --strict
4040
4092
  status=$?
@@ -4083,22 +4135,20 @@ function ripplePreCommitHookScript() {
4083
4135
  "",
4084
4136
  ].join("\n");
4085
4137
  }
4138
+ // ripple-main/packages/cli/src/index.ts
4139
+ // ... (keep all existing functions)
4086
4140
  function ripplePostCommitHookBlock() {
4087
4141
  return [
4088
4142
  RIPPLE_POST_COMMIT_HOOK_START,
4089
- `# Local intents are consumed after a successful commit to avoid ghost-intent blocks.
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.
4090
4146
  set +e
4091
4147
 
4092
- cleared=0
4093
- for intent_file in .ripple/.cache/latest-intent.json .ripple/intents/latest.json; do
4094
- if [ -f "$intent_file" ]; then
4095
- rm "$intent_file"
4096
- cleared=1
4097
- fi
4098
- done
4099
-
4100
- if [ "$cleared" -eq 1 ]; then
4101
- 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."
4102
4152
  fi`,
4103
4153
  RIPPLE_POST_COMMIT_HOOK_END,
4104
4154
  "",