@getripple/cli 1.0.8 → 1.0.10

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
@@ -26,6 +26,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
27
  const fs = __importStar(require("fs"));
28
28
  const path = __importStar(require("path"));
29
+ const child_process_1 = require("child_process");
29
30
  const core_1 = require("@getripple/core");
30
31
  const CONTROL_MODES = ["brainstorm", "function", "file", "task", "pr"];
31
32
  function usage() {
@@ -45,18 +46,25 @@ function usage() {
45
46
  " ripple callers <file>::<symbol>",
46
47
  " ripple history [--last N]",
47
48
  " ripple plan --file <file> --task <task> [--mode file|function|brainstorm|task|pr] [--symbol name] [--budget N] [--save]",
49
+ " ripple intent status [--intent latest|path]",
50
+ " ripple intent close --reason text [--intent latest|path]",
48
51
  " ripple check --staged [--intent latest|path] [--strict]",
52
+ " ripple check --worktree [--intent latest|path] [--strict]",
49
53
  " ripple check --changed --base <ref> [--intent latest|path] [--strict]",
50
- " ripple audit [--intent latest|path] [--changed --base <ref>] [--strict]",
51
- " ripple gate [--intent latest|path] [--changed --base <ref>] [--strict]",
54
+ " ripple audit [--intent latest|path] [--worktree|--changed --base <ref>] [--strict]",
55
+ " ripple gate [--intent latest|path] [--worktree|--changed --base <ref>] [--strict]",
52
56
  " ripple approval [--intent latest|path] [--gate before-risky-edit|before-merge]",
53
- " ripple approve [--intent latest|path] [--gate before-risky-edit|before-merge] [--reason text]",
57
+ " ripple approve [--intent latest|path] [--gate before-risky-edit|before-merge] --reason text",
58
+ " ripple verify --run <test command> [--intent latest|path] [--note text]",
59
+ " ripple verify --command <test command> --status passed|failed|skipped|unknown [--intent latest|path] [--note text]",
54
60
  " ripple repair [--intent latest|path] [--strict]",
55
61
  " ripple ci [--base <ref>] [--intent latest|path] [--github-annotations]",
56
62
  " ripple init-ci [--print] [--force]",
57
63
  " ripple policy init [--print] [--force]",
58
64
  " ripple policy explain --file <file>",
59
65
  " ripple agent",
66
+ " ripple agent setup [--print] [--force]",
67
+ " ripple hook install [--print] [--force]",
60
68
  "",
61
69
  "Options:",
62
70
  " --json, -j Print machine-readable JSON",
@@ -67,14 +75,15 @@ function usage() {
67
75
  " --mode MODE Agent control boundary for saved plans (default: file)",
68
76
  " --symbol NAME Allowed symbol for --mode function",
69
77
  " --gate GATE Human approval gate for approve (before-risky-edit or before-merge)",
70
- " --reason TEXT Human approval reason",
78
+ " --reason TEXT Required human approval reason",
71
79
  " --approved-by NAME Human approver name for approval records",
72
80
  " --budget N Token budget for plan (default: 4000)",
73
81
  " --staged Check currently staged JS/TS files",
74
82
  " --changed Check JS/TS files changed against --base",
83
+ " --worktree Check unstaged working-tree JS/TS changes",
75
84
  " --base REF Base git ref for --changed checks (default: HEAD)",
76
85
  " --save Save a change intent from ripple plan",
77
- " --intent REF Validate changes against saved intent (latest, id, or path; ci default: latest)",
86
+ " --intent REF Validate changes against saved intent (latest, id, or path; local checks only)",
78
87
  " --strict Exit non-zero when check/repair detects missing intent, drift, or contract danger",
79
88
  " --github-annotations Emit GitHub Actions annotations for CI findings",
80
89
  " --print Print generated setup content instead of writing files",
@@ -86,10 +95,14 @@ function usage() {
86
95
  " ripple workflow",
87
96
  " ripple doctor --agent",
88
97
  " ripple agent",
98
+ " ripple agent setup",
89
99
  " ripple agent --json",
90
100
  " ripple plan --file src/auth.ts --task \"change token refresh behavior\" --mode file --agent --save",
91
101
  " ripple plan --file src/auth.ts --symbol refreshToken --task \"fix retry behavior\" --mode function --agent --save",
102
+ " ripple intent status",
103
+ " ripple intent close --reason \"task finished\"",
92
104
  " ripple check --staged --agent --intent latest",
105
+ " ripple check --worktree --agent --intent latest",
93
106
  " ripple audit --agent --intent latest",
94
107
  " ripple gate --agent --intent latest",
95
108
  " ripple approval --intent latest --agent",
@@ -97,7 +110,7 @@ function usage() {
97
110
  " ripple repair --agent --intent latest",
98
111
  " ripple check --staged --intent latest --strict",
99
112
  " ripple check --changed --base origin/main --strict",
100
- " ripple ci --base origin/main --intent latest --github-annotations",
113
+ " ripple ci --base origin/main --github-annotations",
101
114
  " ripple init",
102
115
  " ripple init-ci",
103
116
  " ripple policy init",
@@ -139,6 +152,9 @@ function agentWorkflowGuide() {
139
152
  ` ${workflow.commands.auditCurrentChange}`,
140
153
  ` ${workflow.commands.gateCurrentChange}`,
141
154
  "",
155
+ "Record verification evidence:",
156
+ ` ${workflow.commands.recordVerification}`,
157
+ "",
142
158
  "If staged changes drift:",
143
159
  ` ${workflow.commands.repairIntentDrift}`,
144
160
  "",
@@ -158,6 +174,135 @@ function agentWorkflowGuide() {
158
174
  ...workflow.example.map((command) => ` ${command}`),
159
175
  ].join("\n");
160
176
  }
177
+ function mcpServerConfig(workspaceRoot) {
178
+ return {
179
+ mcpServers: {
180
+ ripple: {
181
+ command: "npx",
182
+ args: ["-y", "@getripple/mcp", "--workspace", workspaceRoot],
183
+ },
184
+ },
185
+ };
186
+ }
187
+ function mcpServerConfigJson(workspaceRoot) {
188
+ return JSON.stringify(mcpServerConfig(workspaceRoot), null, 2);
189
+ }
190
+ const RIPPLE_AGENT_SETUP_FILE_NAMES = ["AGENTS.md", "CLAUDE.md", ".cursorrules"];
191
+ const RIPPLE_DEFAULT_AGENT_SETUP_FILE = ".cursorrules";
192
+ function agentInstructionMarkdown(_workspaceRoot, _fileName) {
193
+ const workflow = (0, core_1.getAgentWorkflowSummary)();
194
+ return [
195
+ "# RIPPLE AGENT PROTOCOL",
196
+ "You are connected to Ripple MCP for this repo.",
197
+ `1. BEFORE editing: MUST call \`${workflow.mcpTools.planBeforeEditing}\` with saveIntent=true.`,
198
+ `2. AFTER editing: MUST call \`${workflow.mcpTools.gateCurrentChange}\` or \`${workflow.mcpTools.checkChangedAgainstBase}\`.`,
199
+ "3. If mustStop=true or needsHuman=true: STOP and ask the human.",
200
+ "4. DO NOT edit `.ripple/` policy/cache/intent files unless explicitly requested.",
201
+ "5. DO NOT claim Ripple passed unless you called a Ripple MCP tool.",
202
+ ].join("\n");
203
+ }
204
+ const RIPPLE_AGENT_SECTION_START = "<!-- RIPPLE:START -->";
205
+ const RIPPLE_AGENT_SECTION_END = "<!-- RIPPLE:END -->";
206
+ function rippleAgentManagedSection(content) {
207
+ return [
208
+ RIPPLE_AGENT_SECTION_START,
209
+ content.trimEnd(),
210
+ RIPPLE_AGENT_SECTION_END,
211
+ "",
212
+ ].join("\n");
213
+ }
214
+ function resolveAgentSetupFileNames(workspaceRoot) {
215
+ const existing = RIPPLE_AGENT_SETUP_FILE_NAMES.filter((fileName) => fs.existsSync(path.join(workspaceRoot, fileName)));
216
+ return existing.length > 0 ? existing : [RIPPLE_DEFAULT_AGENT_SETUP_FILE];
217
+ }
218
+ function agentSetupFiles(workspaceRoot) {
219
+ return resolveAgentSetupFileNames(workspaceRoot).map((fileName) => ({
220
+ path: fileName,
221
+ absolutePath: path.join(workspaceRoot, fileName),
222
+ content: rippleAgentManagedSection(agentInstructionMarkdown(workspaceRoot, fileName)),
223
+ }));
224
+ }
225
+ function buildAgentSetupSummary(workspaceRoot, files) {
226
+ const mcpArgs = ["-y", "@getripple/mcp", "--workspace", workspaceRoot];
227
+ return {
228
+ protocol: "ripple-agent-setup",
229
+ version: 1,
230
+ workspace: workspaceRoot,
231
+ files,
232
+ mcp: {
233
+ serverName: "ripple",
234
+ command: "npx",
235
+ args: mcpArgs,
236
+ workspace: workspaceRoot,
237
+ config: mcpServerConfig(workspaceRoot),
238
+ },
239
+ setupRequired: [
240
+ "Open your agent or IDE MCP settings.",
241
+ "Add a new MCP server named ripple.",
242
+ `Use command: npx ${mcpArgs.join(" ")}`,
243
+ "Restart or reload the agent so Ripple MCP tools become available.",
244
+ ],
245
+ nextSteps: [
246
+ "Ask the agent to call ripple_get_agent_workflow to confirm MCP connectivity.",
247
+ "Before edits, the agent should call ripple_plan_context with saveIntent enabled.",
248
+ "After edits, the agent should call ripple_gate or ripple_check_changed before handoff.",
249
+ ],
250
+ };
251
+ }
252
+ function printAgentSetupSummary(summary) {
253
+ console.log("Ripple agent setup");
254
+ console.log(`Workspace: ${summary.workspace}`);
255
+ console.log("");
256
+ console.log("Generated files:");
257
+ summary.files.forEach((file) => {
258
+ console.log(` - ${file.path}: ${file.status}`);
259
+ });
260
+ console.log("");
261
+ console.log("ACTION REQUIRED: connect Ripple MCP to your agent/IDE.");
262
+ console.log("");
263
+ console.log("MCP server:");
264
+ console.log(" name: ripple");
265
+ console.log(" command: npx");
266
+ console.log(` args: ${summary.mcp.args.join(" ")}`);
267
+ console.log("");
268
+ console.log("Paste this MCP config if your client accepts JSON:");
269
+ console.log(mcpServerConfigJson(summary.workspace));
270
+ console.log("");
271
+ console.log("Cursor / Claude / agent steps:");
272
+ summary.setupRequired.forEach((step, index) => console.log(` ${index + 1}. ${step}`));
273
+ console.log("");
274
+ console.log("Next:");
275
+ summary.nextSteps.forEach((step) => console.log(` - ${step}`));
276
+ }
277
+ function agentSetupCommand(options) {
278
+ const workspaceRoot = resolveWorkspaceRoot(".");
279
+ const files = agentSetupFiles(workspaceRoot);
280
+ if (options.print) {
281
+ const summary = buildAgentSetupSummary(workspaceRoot, files.map((file) => ({
282
+ path: file.path,
283
+ status: "printed",
284
+ written: false,
285
+ overwritten: false,
286
+ content: file.content,
287
+ })));
288
+ if (options.json) {
289
+ printJson(summary);
290
+ return;
291
+ }
292
+ process.stdout.write(files
293
+ .flatMap((file) => [`# ${file.path}`, file.content.trimEnd(), ""])
294
+ .join("\n"));
295
+ return;
296
+ }
297
+ const writtenFiles = files.map((file) => writeAgentSetupFile(file, options.force));
298
+ const summary = buildAgentSetupSummary(workspaceRoot, writtenFiles);
299
+ if (options.json) {
300
+ printJson(summary);
301
+ }
302
+ else {
303
+ printAgentSetupSummary(summary);
304
+ }
305
+ }
161
306
  function parseCliArgs(argv) {
162
307
  let command;
163
308
  const args = [];
@@ -168,6 +313,7 @@ function parseCliArgs(argv) {
168
313
  budget: 4000,
169
314
  staged: false,
170
315
  changed: false,
316
+ worktree: false,
171
317
  save: false,
172
318
  strict: false,
173
319
  githubAnnotations: false,
@@ -192,6 +338,10 @@ function parseCliArgs(argv) {
192
338
  options.changed = true;
193
339
  continue;
194
340
  }
341
+ if (token === "--worktree") {
342
+ options.worktree = true;
343
+ continue;
344
+ }
195
345
  if (token === "--save") {
196
346
  options.save = true;
197
347
  continue;
@@ -290,6 +440,58 @@ function parseCliArgs(argv) {
290
440
  options.gate = parseApprovalGate(token.slice("--gate=".length));
291
441
  continue;
292
442
  }
443
+ if (token === "--run") {
444
+ const value = argv[i + 1];
445
+ if (!value || value.startsWith("-")) {
446
+ throw new Error("Missing value for --run");
447
+ }
448
+ options.verificationRunCommand = value;
449
+ i++;
450
+ continue;
451
+ }
452
+ if (token.startsWith("--run=")) {
453
+ options.verificationRunCommand = token.slice("--run=".length);
454
+ continue;
455
+ }
456
+ if (token === "--command") {
457
+ const value = argv[i + 1];
458
+ if (!value || value.startsWith("-")) {
459
+ throw new Error("Missing value for --command");
460
+ }
461
+ options.verificationCommand = value;
462
+ i++;
463
+ continue;
464
+ }
465
+ if (token.startsWith("--command=")) {
466
+ options.verificationCommand = token.slice("--command=".length);
467
+ continue;
468
+ }
469
+ if (token === "--status") {
470
+ const value = argv[i + 1];
471
+ if (!value || value.startsWith("-")) {
472
+ throw new Error("Missing value for --status");
473
+ }
474
+ options.verificationStatus = parseVerificationStatus(value);
475
+ i++;
476
+ continue;
477
+ }
478
+ if (token.startsWith("--status=")) {
479
+ options.verificationStatus = parseVerificationStatus(token.slice("--status=".length));
480
+ continue;
481
+ }
482
+ if (token === "--note") {
483
+ const value = argv[i + 1];
484
+ if (!value || value.startsWith("-")) {
485
+ throw new Error("Missing value for --note");
486
+ }
487
+ options.note = value;
488
+ i++;
489
+ continue;
490
+ }
491
+ if (token.startsWith("--note=")) {
492
+ options.note = token.slice("--note=".length);
493
+ continue;
494
+ }
293
495
  if (token === "--reason") {
294
496
  const value = argv[i + 1];
295
497
  if (!value || value.startsWith("-")) {
@@ -342,6 +544,19 @@ function parseCliArgs(argv) {
342
544
  options.base = token.slice("--base=".length);
343
545
  continue;
344
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
+ }
345
560
  if (token === "--budget") {
346
561
  const value = argv[i + 1];
347
562
  if (!value || value.startsWith("-")) {
@@ -376,6 +591,12 @@ function parseControlMode(value) {
376
591
  }
377
592
  throw new Error(`--mode must be one of: ${CONTROL_MODES.join(", ")}`);
378
593
  }
594
+ function parseVerificationStatus(value) {
595
+ if (value === "passed" || value === "failed" || value === "skipped" || value === "unknown") {
596
+ return value;
597
+ }
598
+ throw new Error("--status must be one of: passed, failed, skipped, unknown");
599
+ }
379
600
  function parseApprovalGate(value) {
380
601
  if (value === "before-risky-edit" || value === "before-merge") {
381
602
  return value;
@@ -392,21 +613,48 @@ function version() {
392
613
  return "0.0.0";
393
614
  }
394
615
  }
616
+ function rippleCliPackageSpec() {
617
+ const currentVersion = version();
618
+ return currentVersion === "0.0.0"
619
+ ? "@getripple/cli"
620
+ : `@getripple/cli@${currentVersion}`;
621
+ }
622
+ function shellSingleQuote(value) {
623
+ return `'${value.replace(/'/g, `'\\''`).replace(/\\/g, "/")}'`;
624
+ }
625
+ function rippleDirectRunnerHookLines() {
626
+ return [
627
+ ` ripple_direct_node=${shellSingleQuote(process.execPath)}`,
628
+ ` ripple_direct_cli=${shellSingleQuote(__filename)}`,
629
+ ` if [ -x "./node_modules/.bin/ripple" ]; then`,
630
+ ` "./node_modules/.bin/ripple" "$@"`,
631
+ ` elif [ -f "$ripple_direct_cli" ] && [ -f "$ripple_direct_node" ]; then`,
632
+ ` "$ripple_direct_node" "$ripple_direct_cli" "$@"`,
633
+ ` elif command -v ripple >/dev/null 2>&1; then`,
634
+ ` ripple "$@"`,
635
+ ` else`,
636
+ ` npx -y ${rippleCliPackageSpec()} "$@"`,
637
+ ` fi`,
638
+ ];
639
+ }
395
640
  const GITHUB_ACTIONS_WORKFLOW_PATH = core_1.RIPPLE_CI_WORKFLOW_PATH;
396
641
  function githubActionsWorkflow() {
397
642
  return [
398
- "name: Ripple",
643
+ "name: Ripple Enterprise Gate",
399
644
  "",
400
645
  "on:",
401
646
  " pull_request:",
647
+ " push:",
648
+ " branches: [main, master]",
402
649
  "",
403
650
  "permissions:",
404
651
  " contents: read",
405
652
  " pull-requests: read",
653
+ " checks: write",
406
654
  "",
407
655
  "jobs:",
408
656
  " ripple:",
409
- " name: Ripple architecture gate",
657
+ " name: Ripple authorization gate",
410
658
  " runs-on: ubuntu-latest",
411
659
  " steps:",
412
660
  " - name: Checkout",
@@ -417,8 +665,11 @@ function githubActionsWorkflow() {
417
665
  " uses: actions/setup-node@v4",
418
666
  " with:",
419
667
  " node-version: 20",
420
- " - name: Ripple CI",
421
- " run: npx -y @getripple/cli@latest ci --base origin/${{ github.base_ref }} --intent latest --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 }}",
422
673
  "",
423
674
  ].join("\n");
424
675
  }
@@ -434,7 +685,7 @@ function defaultInitNextSteps(readiness) {
434
685
  ...(readiness?.nextSteps ?? []),
435
686
  "Run ripple plan --file <file> --task \"<task>\" --mode file --agent --save.",
436
687
  "Run ripple doctor --agent --strict after saving the first intent.",
437
- "Commit .ripple/policy.json, .ripple/intents/latest.json, approvals when needed, and .github/workflows/ripple.yml.",
688
+ "Commit .ripple/policy.json, approvals when needed, and .github/workflows/ripple.yml. Keep local intents out of PRs.",
438
689
  ]);
439
690
  }
440
691
  function uniqueLines(lines) {
@@ -488,7 +739,7 @@ function intentLoadFailureMessage(intentRef, error) {
488
739
  const detail = error instanceof Error ? error.message : String(error);
489
740
  return [
490
741
  `Could not load Ripple change intent '${intentRef}'.`,
491
- "Run ripple plan --save and include .ripple/intents/latest.json in the PR, or pass a valid --intent path.",
742
+ "Run ripple plan --save for local intent-based checks, or pass a valid --intent path. Do not commit local latest intents into PRs.",
492
743
  detail,
493
744
  ].join(" ");
494
745
  }
@@ -534,6 +785,149 @@ function writeGithubAuditStepSummary(audit) {
534
785
  console.error(`Ripple CLI warning: Could not write GitHub step summary: ${message}`);
535
786
  }
536
787
  }
788
+ function writeGithubPolicyAuditStepSummary(summary, policySync) {
789
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY?.trim();
790
+ if (!summaryPath) {
791
+ return;
792
+ }
793
+ try {
794
+ fs.appendFileSync(summaryPath, buildGithubPolicyAuditStepSummary(summary, policySync), "utf8");
795
+ }
796
+ catch (err) {
797
+ const message = err instanceof Error ? err.message : String(err);
798
+ console.error(`Ripple CLI warning: Could not write GitHub step summary: ${message}`);
799
+ }
800
+ }
801
+ function pushMarkdownList(lines, title, items, limit) {
802
+ lines.push(`#### ${title}`);
803
+ if (items.length === 0) {
804
+ lines.push("- none");
805
+ return;
806
+ }
807
+ items.slice(0, limit).forEach((item) => lines.push(`- ${item}`));
808
+ if (items.length > limit) {
809
+ lines.push(`- ...and ${items.length - limit} more`);
810
+ }
811
+ }
812
+ function appendGithubReviewPacket(lines, packet) {
813
+ if (!packet) {
814
+ return;
815
+ }
816
+ lines.push("### Review packet", "");
817
+ lines.push(`- Protocol: ${packet.protocol} v${packet.version}`);
818
+ lines.push(`- Task: ${packet.originalTask}`);
819
+ lines.push(`- Mode: ${packet.mode}`);
820
+ lines.push(`- Declared scope: ${packet.declaredScope.controlMode} ${packet.declaredScope.targetFile}`);
821
+ lines.push(`- Human gate: ${packet.declaredScope.humanGate}`);
822
+ lines.push(`- Boundary risk: ${packet.declaredScope.boundaryRisk}`);
823
+ lines.push(`- Tests run: ${packet.verification.testsRun}`);
824
+ if (packet.verification.evidence.length > 0) {
825
+ lines.push(`- Verification status: ${verificationEvidenceStatusLabel(packet.verification.evidence)}`);
826
+ }
827
+ lines.push(`- Decision: ${packet.decision.verdict}`);
828
+ lines.push(`- Can continue: ${packet.decision.canContinue}`);
829
+ lines.push(`- Must stop: ${packet.decision.mustStop}`);
830
+ lines.push(`- Needs human: ${packet.decision.needsHuman}`);
831
+ lines.push(`- Next required action: ${packet.decision.nextRequiredAction}`);
832
+ lines.push("");
833
+ pushMarkdownList(lines, "Allowed files", packet.declaredScope.allowedFiles, 12);
834
+ lines.push("");
835
+ pushMarkdownList(lines, "Allowed symbols", packet.declaredScope.allowedSymbols, 12);
836
+ lines.push("");
837
+ pushMarkdownList(lines, "Actual changed files", packet.actualChanges.changedFiles, 20);
838
+ lines.push("");
839
+ pushMarkdownList(lines, "Changed symbols", packet.actualChanges.changedSymbols, 16);
840
+ lines.push("");
841
+ pushMarkdownList(lines, "Outside boundary files", packet.scopeFindings.outsideBoundaryFiles, 20);
842
+ lines.push("");
843
+ pushMarkdownList(lines, "Outside boundary symbols", packet.scopeFindings.outsideBoundarySymbols, 16);
844
+ lines.push("");
845
+ pushMarkdownList(lines, "Contract changes to review", uniqueItems([
846
+ ...packet.scopeFindings.protectedContractChanges,
847
+ ...packet.scopeFindings.unplannedContractChanges,
848
+ ]), 16);
849
+ lines.push("");
850
+ pushMarkdownList(lines, "Verification expected", packet.verification.expectedCommands, 20);
851
+ lines.push("");
852
+ pushMarkdownList(lines, "Verification evidence", packet.verification.evidence.map(formatVerificationEvidence), 20);
853
+ lines.push("");
854
+ lines.push("#### Verification note");
855
+ lines.push(`- ${packet.verification.note}`);
856
+ lines.push("");
857
+ pushMarkdownList(lines, "Reviewer notes", packet.reviewerNotes, 12);
858
+ lines.push("");
859
+ }
860
+ function buildGithubPolicyAuditStepSummary(summary, policySync) {
861
+ const pushList = (lines, title, items, limit) => {
862
+ lines.push(`#### ${title}`);
863
+ if (items.length === 0) {
864
+ lines.push("- none");
865
+ return;
866
+ }
867
+ items.slice(0, limit).forEach((item) => lines.push(`- ${item}`));
868
+ if (items.length > limit) {
869
+ lines.push(`- ...and ${items.length - limit} more`);
870
+ }
871
+ };
872
+ const lines = [
873
+ "## Ripple architecture gate",
874
+ "",
875
+ "Status: audit",
876
+ "Mode: policy-only",
877
+ "Blocking: false",
878
+ "Intent: none (local intents are not required in CI audit mode)",
879
+ `Checked files: ${summary.checkedFiles}`,
880
+ `Highest risk: ${summary.highestRisk}`,
881
+ `Requires attention: ${summary.requiresAttention}`,
882
+ "",
883
+ ];
884
+ if (summary.baseRef) {
885
+ lines.splice(5, 0, `Base ref: ${summary.baseRef}`);
886
+ }
887
+ if (summary.files.length > 0) {
888
+ lines.push("### Changed files", "");
889
+ summary.files.slice(0, 20).forEach((file) => {
890
+ lines.push(`- ${file.file} (${file.modificationRisk}, importers: ${file.importerCount})`);
891
+ });
892
+ if (summary.files.length > 20) {
893
+ lines.push(`- ...and ${summary.files.length - 20} more`);
894
+ }
895
+ lines.push("");
896
+ }
897
+ if (policySync) {
898
+ lines.push("### Policy sync", "");
899
+ lines.push(`Status: ${policySync.status}`);
900
+ if (policySync.missingRules.length > 0) {
901
+ policySync.missingRules.slice(0, 12).forEach((rule) => {
902
+ lines.push(`- ${rule.paths.join(", ")} (risk: ${rule.risk ?? "medium"})`);
903
+ });
904
+ if (policySync.missingRules.length > 12) {
905
+ lines.push(`- ...and ${policySync.missingRules.length - 12} more`);
906
+ }
907
+ }
908
+ else {
909
+ lines.push("- up to date");
910
+ }
911
+ lines.push("");
912
+ }
913
+ lines.push("### Agent actions", "");
914
+ pushList(lines, "Trusted findings", summary.agentActions.trustedFindings, 12);
915
+ lines.push("");
916
+ pushList(lines, "Verify before merge", summary.agentActions.verifyBeforeCommit, 12);
917
+ lines.push("");
918
+ pushList(lines, "Manual review recommended", summary.agentActions.manualReviewRequired, 12);
919
+ lines.push("");
920
+ const verificationTargets = uniqueItems(summary.files.flatMap((file) => file.verificationTargets));
921
+ if (verificationTargets.length > 0) {
922
+ lines.push("### Verify", "");
923
+ verificationTargets.slice(0, 20).forEach((target) => lines.push(`- ${target}`));
924
+ if (verificationTargets.length > 20) {
925
+ lines.push(`- ...and ${verificationTargets.length - 20} more`);
926
+ }
927
+ lines.push("");
928
+ }
929
+ return `${lines.join("\n")}\n`;
930
+ }
537
931
  function buildGithubStepSummary(input) {
538
932
  const { summary, intentLoadError } = input;
539
933
  const validation = summary.intentValidation;
@@ -588,6 +982,7 @@ function buildGithubStepSummary(input) {
588
982
  else {
589
983
  lines.push("### Intent", "", "- No saved change intent was provided.", `- Next required phase: ${nextRequiredPhase}`, `- Next required action: ${nextRequiredAction}`, "");
590
984
  }
985
+ appendGithubReviewPacket(lines, summary.reviewPacket);
591
986
  const blockingReasons = validation?.blockingReasons ?? [];
592
987
  if (blockingReasons.length > 0) {
593
988
  lines.push("### Blocking reasons", "");
@@ -681,6 +1076,7 @@ function buildGithubAuditStepSummary(audit) {
681
1076
  }
682
1077
  pushList(lines, "Approval why", audit.approvalStatus.why, 8);
683
1078
  lines.push("");
1079
+ appendGithubReviewPacket(lines, audit.reviewPacket);
684
1080
  if (audit.blockingReasons.length > 0) {
685
1081
  lines.push("### Blocking reasons", "");
686
1082
  audit.blockingReasons.forEach((reason) => lines.push(`- ${reason}`));
@@ -763,13 +1159,13 @@ function printGithubCheckAnnotations(summary) {
763
1159
  return;
764
1160
  }
765
1161
  if (validation.policyDrift.status === "changed") {
766
- printGithubErrorAnnotation({
1162
+ printGithubWarningAnnotation({
767
1163
  file: validation.targetFile,
768
1164
  title: "Ripple policy drift",
769
1165
  message: validation.policyDrift.summary,
770
1166
  });
771
1167
  validation.policyDrift.changedFields.slice(0, 8).forEach((field) => {
772
- printGithubErrorAnnotation({
1168
+ printGithubWarningAnnotation({
773
1169
  file: validation.targetFile,
774
1170
  title: "Ripple policy drift",
775
1171
  message: field,
@@ -777,13 +1173,13 @@ function printGithubCheckAnnotations(summary) {
777
1173
  });
778
1174
  }
779
1175
  if (validation.readinessDrift.status === "weakened") {
780
- printGithubErrorAnnotation({
1176
+ printGithubWarningAnnotation({
781
1177
  file: validation.targetFile,
782
1178
  title: "Ripple readiness drift",
783
1179
  message: validation.readinessDrift.summary,
784
1180
  });
785
1181
  validation.readinessDrift.weakenedFields.slice(0, 8).forEach((field) => {
786
- printGithubErrorAnnotation({
1182
+ printGithubWarningAnnotation({
787
1183
  file: validation.targetFile,
788
1184
  title: "Ripple readiness drift",
789
1185
  message: `Weakened readiness field: ${field}`,
@@ -791,7 +1187,7 @@ function printGithubCheckAnnotations(summary) {
791
1187
  });
792
1188
  }
793
1189
  validation.unplannedFiles.forEach((file) => {
794
- printGithubErrorAnnotation({
1190
+ printGithubWarningAnnotation({
795
1191
  file,
796
1192
  title: "Ripple intent drift",
797
1193
  message: `Unplanned file changed: ${file}`,
@@ -799,7 +1195,7 @@ function printGithubCheckAnnotations(summary) {
799
1195
  });
800
1196
  validation.unplannedSymbols.forEach((symbol) => {
801
1197
  const file = symbolFile(symbol);
802
- printGithubErrorAnnotation({
1198
+ printGithubWarningAnnotation({
803
1199
  file,
804
1200
  title: "Ripple symbol drift",
805
1201
  message: `Unplanned symbol changed: ${symbol}`,
@@ -853,24 +1249,58 @@ function printGithubCheckAnnotations(summary) {
853
1249
  });
854
1250
  });
855
1251
  summary.agentActions.manualReviewRequired.slice(0, 12).forEach((action) => {
856
- printGithubErrorAnnotation({
1252
+ printGithubWarningAnnotation({
857
1253
  file: actionFile(action),
858
1254
  title: "Ripple manual review required",
859
1255
  message: action,
860
1256
  });
861
1257
  });
862
1258
  }
1259
+ function printGithubPolicyAuditAnnotations(summary, policySync) {
1260
+ if (summary.requiresAttention) {
1261
+ printGithubWarningAnnotation({
1262
+ title: "Ripple policy audit",
1263
+ message: `Policy audit detected ${summary.highestRisk} risk changes. Ripple is in audit mode, so this does not block merge. Ensure human review before merging.`,
1264
+ });
1265
+ }
1266
+ else {
1267
+ printGithubNoticeAnnotation({
1268
+ title: "Ripple policy audit",
1269
+ message: "Policy audit completed without high-risk findings.",
1270
+ });
1271
+ }
1272
+ if (policySync && policySync.missingRules.length > 0) {
1273
+ printGithubWarningAnnotation({
1274
+ title: "Ripple policy rot",
1275
+ message: `Policy may be missing ${policySync.missingRules.length} risky repo surface(s). Run ripple policy sync and review .ripple/policy.json.`,
1276
+ });
1277
+ }
1278
+ summary.agentActions.verifyBeforeCommit.slice(0, 12).forEach((action) => {
1279
+ printGithubWarningAnnotation({
1280
+ file: actionFile(action),
1281
+ title: "Ripple verify before merge",
1282
+ message: action,
1283
+ });
1284
+ });
1285
+ summary.agentActions.manualReviewRequired.slice(0, 12).forEach((action) => {
1286
+ printGithubWarningAnnotation({
1287
+ file: actionFile(action),
1288
+ title: "Ripple manual review recommended",
1289
+ message: action,
1290
+ });
1291
+ });
1292
+ }
863
1293
  function printGithubAuditAnnotations(audit) {
864
1294
  const gate = (0, core_1.buildRippleGateSummary)(audit);
865
1295
  if (audit.status !== "pass") {
866
- printGithubErrorAnnotation({
1296
+ printGithubWarningAnnotation({
867
1297
  file: audit.intent.targetFile,
868
1298
  title: "Ripple gate closed",
869
1299
  message: `${gate.status}/${gate.decision}: next=${gate.nextRequiredPhase}. ${gate.nextRequiredAction}`,
870
1300
  });
871
1301
  }
872
1302
  if (audit.approvalStatus.required && !audit.approvalStatus.approved) {
873
- printGithubErrorAnnotation({
1303
+ printGithubWarningAnnotation({
874
1304
  file: audit.intent.targetFile,
875
1305
  title: "Ripple approval required",
876
1306
  message: audit.approvalStatus.summary,
@@ -930,6 +1360,28 @@ function relativeToWorkspace(workspaceRoot, filePath) {
930
1360
  function normalizeProjectPath(filePath) {
931
1361
  return filePath.replace(/\\/g, "/").replace(/^\.\/+/, "");
932
1362
  }
1363
+ function resolveCliIntentPath(workspaceRoot, intentRef) {
1364
+ const normalized = intentRef.trim();
1365
+ if (normalized.length === 0 || normalized === "latest") {
1366
+ return (0, core_1.defaultChangeIntentPath)(workspaceRoot);
1367
+ }
1368
+ if (path.isAbsolute(normalized)) {
1369
+ return normalized;
1370
+ }
1371
+ if (normalized.endsWith(".json") || normalized.includes("/") || normalized.includes("\\")) {
1372
+ return path.resolve(workspaceRoot, normalized);
1373
+ }
1374
+ return path.join(workspaceRoot, ".ripple", "intents", `${normalized}.json`);
1375
+ }
1376
+ function isActiveChangeIntentFile(filePath) {
1377
+ try {
1378
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
1379
+ return parsed.protocol === "ripple-change-intent";
1380
+ }
1381
+ catch {
1382
+ return false;
1383
+ }
1384
+ }
933
1385
  function formatEventLine(event) {
934
1386
  const target = event.target ? ` -> ${event.target}` : "";
935
1387
  const details = [
@@ -1021,6 +1473,15 @@ function printDoctorSummary(summary) {
1021
1473
  summary.enforcement.gaps.forEach((gap) => console.log(` - ${gap}`));
1022
1474
  }
1023
1475
  console.log("");
1476
+ console.log("Policy sync:");
1477
+ console.log(` status: ${summary.policySync.status}`);
1478
+ if (summary.policySync.missingRules.length > 0) {
1479
+ console.log(" missing coverage:");
1480
+ summary.policySync.missingRules.slice(0, 12).forEach((rule) => {
1481
+ console.log(` - ${rule.paths.join(", ")} risk=${rule.risk ?? "medium"}`);
1482
+ });
1483
+ }
1484
+ console.log("");
1024
1485
  console.log("Next steps:");
1025
1486
  summary.nextSteps.forEach((step) => console.log(` - ${step}`));
1026
1487
  }
@@ -1032,6 +1493,21 @@ function printInitSummary(summary) {
1032
1493
  summary.files.forEach((file) => {
1033
1494
  console.log(` - ${file.path}: ${file.status}`);
1034
1495
  });
1496
+ if (summary.agentSetup) {
1497
+ console.log("");
1498
+ console.log("Agent setup files:");
1499
+ summary.agentSetup.files.forEach((file) => {
1500
+ console.log(` - ${file.path}: ${file.status}`);
1501
+ });
1502
+ }
1503
+ if (summary.hooks) {
1504
+ console.log("");
1505
+ console.log("Git hooks:");
1506
+ console.log(` - ${summary.hooks.path}: ${summary.hooks.preCommitAction ?? summary.hooks.status}`);
1507
+ if (summary.hooks.postCommitPath) {
1508
+ console.log(` - ${summary.hooks.postCommitPath}: ${summary.hooks.postCommitAction ?? summary.hooks.status}`);
1509
+ }
1510
+ }
1035
1511
  if (summary.readiness) {
1036
1512
  console.log("");
1037
1513
  console.log("Readiness after init:");
@@ -1067,6 +1543,13 @@ function printAgentDoctorSummary(summary) {
1067
1543
  console.log(`git_ignore: ${summary.checks.gitIgnore.ok ? "ok" : "missing"} - ${summary.checks.gitIgnore.detail}`);
1068
1544
  console.log(`ci_workflow: ${summary.checks.ciWorkflow.ok ? "ok" : "missing"} - ${summary.checks.ciWorkflow.detail}`);
1069
1545
  console.log(`latest_intent: ${summary.checks.latestIntent.ok ? "ok" : "missing"} - ${summary.checks.latestIntent.detail}`);
1546
+ console.log(`policy_sync: ${summary.policySync.status}`);
1547
+ if (summary.policySync.missingRules.length > 0) {
1548
+ console.log("policy_sync_missing_rules:");
1549
+ summary.policySync.missingRules.slice(0, 12).forEach((rule) => {
1550
+ console.log(`- ${rule.paths.join(", ")} risk=${rule.risk ?? "medium"}`);
1551
+ });
1552
+ }
1070
1553
  console.log("");
1071
1554
  printAgentList("why", summary.why);
1072
1555
  console.log("");
@@ -1492,6 +1975,10 @@ function printAgentStagedCheckSummary(summary) {
1492
1975
  console.log(`checked_js_ts_files: ${summary.stagedFiles}`);
1493
1976
  console.log(`checked_files: ${summary.checkedFiles}`);
1494
1977
  printAgentIntentValidation(summary.intentValidation);
1978
+ if (summary.reviewPacket) {
1979
+ console.log("");
1980
+ printAgentReviewPacket(summary.reviewPacket);
1981
+ }
1495
1982
  console.log("");
1496
1983
  printAgentList("trusted_findings", summary.agentActions.trustedFindings);
1497
1984
  console.log("");
@@ -1760,6 +2247,8 @@ function printAgentAuditSummary(summary) {
1760
2247
  console.log(`repair_status: ${summary.repairPlan.status}`);
1761
2248
  console.log(`recommended_action: ${summary.recommendedAction}`);
1762
2249
  console.log("");
2250
+ printAgentReviewPacket(summary.reviewPacket);
2251
+ console.log("");
1763
2252
  printAgentHandoffBlock("handoff", summary.handoff);
1764
2253
  if (summary.approvalStatus.approval) {
1765
2254
  console.log(`approved_by: ${summary.approvalStatus.approval.approvedBy}`);
@@ -1817,6 +2306,8 @@ function printAgentGateSummary(summary) {
1817
2306
  console.log(`risk_score: ${summary.risk.score}`);
1818
2307
  console.log(`risk_summary: ${summary.risk.summary}`);
1819
2308
  console.log("");
2309
+ printAgentReviewPacket(summary.reviewPacket);
2310
+ console.log("");
1820
2311
  printAgentList("risk_reasons", compactGateRiskReasons(summary));
1821
2312
  console.log("");
1822
2313
  printAgentList("risk_evidence", compactGateRiskEvidence(summary));
@@ -1845,15 +2336,43 @@ function printAgentGateSummary(summary) {
1845
2336
  console.log("");
1846
2337
  printAgentList("commands_verify", summary.commands.verify);
1847
2338
  }
1848
- function printGateSummary(summary) {
1849
- const statusLabel = summary.canContinue ? "CONTINUE" : "STOP";
1850
- console.log(`Ripple gate: ${statusLabel}`);
1851
- console.log(gateHeadline(summary));
1852
- console.log("");
1853
- console.log(`Decision: ${summary.decision}`);
1854
- console.log(`Can continue: ${formatYesNo(summary.canContinue)}`);
1855
- console.log(`Must stop: ${formatYesNo(summary.mustStop)}`);
1856
- printGateRiskSummary(summary);
2339
+ function printAgentGateIntentBlock(summary) {
2340
+ console.log("RIPPLE_GATE_INTENT_BLOCK");
2341
+ console.log(`protocol: ${summary.protocol}`);
2342
+ console.log(`status: ${summary.status}`);
2343
+ console.log(`decision: ${summary.decision}`);
2344
+ console.log(`can_continue: ${summary.canContinue}`);
2345
+ console.log(`must_stop: ${summary.mustStop}`);
2346
+ console.log(`needs_human: ${summary.needsHuman}`);
2347
+ console.log(`next_required_phase: ${summary.nextRequiredPhase}`);
2348
+ console.log(`next_required_action: ${summary.nextRequiredAction}`);
2349
+ console.log(`summary: ${summary.summary}`);
2350
+ console.log(`mode: ${summary.mode}`);
2351
+ if (summary.baseRef) {
2352
+ console.log(`base_ref: ${summary.baseRef}`);
2353
+ }
2354
+ console.log(`intent_ref: ${summary.intentRef}`);
2355
+ console.log(`intent_state: ${summary.intentState}`);
2356
+ console.log(`intent_error: ${summary.intentLoadError}`);
2357
+ console.log("");
2358
+ printAgentList("why", summary.why);
2359
+ console.log("");
2360
+ printAgentList("fix_now", summary.fixNow);
2361
+ console.log("");
2362
+ printAgentList("ask_human", summary.askHuman);
2363
+ console.log("");
2364
+ printAgentList("commands_plan", summary.commands.plan);
2365
+ }
2366
+ function printGateSummary(summary) {
2367
+ const statusLabel = summary.canContinue ? "CONTINUE" : "STOP";
2368
+ console.log(`Ripple gate: ${statusLabel}`);
2369
+ console.log(gateHeadline(summary));
2370
+ console.log("");
2371
+ console.log(`Decision: ${summary.decision}`);
2372
+ console.log(`Can continue: ${formatYesNo(summary.canContinue)}`);
2373
+ console.log(`Must stop: ${formatYesNo(summary.mustStop)}`);
2374
+ console.log("");
2375
+ printReviewPacketSummary(summary.reviewPacket);
1857
2376
  console.log("");
1858
2377
  console.log("Intent:");
1859
2378
  console.log(` Task: ${summary.intent.task}`);
@@ -1865,6 +2384,7 @@ function printGateSummary(summary) {
1865
2384
  printHumanList("Allowed:", gateAllowedItems(summary));
1866
2385
  const outsideBoundary = gateChangedOutsideItems(summary);
1867
2386
  printHumanList(outsideBoundary.length > 0 ? "Changed outside boundary:" : "Changed files:", outsideBoundary.length > 0 ? outsideBoundary : summary.changedFiles);
2387
+ console.log("");
1868
2388
  printHumanList("Why:", compactGateReasons(summary));
1869
2389
  printHumanList("Fix now:", compactGateFixes(summary));
1870
2390
  if (summary.canContinue) {
@@ -1873,6 +2393,28 @@ function printGateSummary(summary) {
1873
2393
  else {
1874
2394
  printHumanList("Commands:", compactGateCommands(summary));
1875
2395
  }
2396
+ printGateRiskSummary(summary);
2397
+ }
2398
+ function printGateIntentBlock(summary) {
2399
+ console.log("Ripple gate: STOP");
2400
+ console.log("Agent must stop before continuing.");
2401
+ console.log("");
2402
+ console.log(`Decision: ${summary.decision}`);
2403
+ console.log(`Can continue: ${formatYesNo(summary.canContinue)}`);
2404
+ console.log(`Must stop: ${formatYesNo(summary.mustStop)}`);
2405
+ console.log(`Needs human: ${formatYesNo(summary.needsHuman)}`);
2406
+ console.log(`Next required phase: ${summary.nextRequiredPhase}`);
2407
+ console.log(`Next required action: ${summary.nextRequiredAction}`);
2408
+ console.log("");
2409
+ console.log("Intent:");
2410
+ console.log(` Ref: ${summary.intentRef}`);
2411
+ console.log(` State: ${summary.intentState}`);
2412
+ console.log(` Error: ${summary.intentLoadError}`);
2413
+ console.log("");
2414
+ printHumanList("Why:", summary.why);
2415
+ printHumanList("Fix now:", summary.fixNow);
2416
+ printHumanList("Ask human:", summary.askHuman);
2417
+ printHumanList("Commands:", summary.commands.plan);
1876
2418
  }
1877
2419
  function printGateRiskSummary(summary) {
1878
2420
  console.log("");
@@ -1967,6 +2509,8 @@ function printAuditSummary(summary) {
1967
2509
  console.log(` next required action: ${gate.nextRequiredAction}`);
1968
2510
  console.log(` summary: ${gate.summary}`);
1969
2511
  console.log("");
2512
+ printReviewPacketSummary(summary.reviewPacket);
2513
+ console.log("");
1970
2514
  console.log("Approval:");
1971
2515
  console.log(` status: ${summary.approvalStatus.status}`);
1972
2516
  console.log(` decision: ${summary.approvalStatus.decision}`);
@@ -2125,9 +2669,83 @@ function printHumanList(title, items) {
2125
2669
  }
2126
2670
  items.forEach((item) => console.log(` - ${item}`));
2127
2671
  }
2672
+ function formatVerificationEvidence(evidence) {
2673
+ const note = evidence.note ? ` note=${evidence.note}` : "";
2674
+ const exitCode = typeof evidence.exitCode === "number" ? ` exitCode=${evidence.exitCode}` : "";
2675
+ const duration = typeof evidence.durationMs === "number" ? ` durationMs=${evidence.durationMs}` : "";
2676
+ const files = evidence.changedFiles
2677
+ ? ` files=${evidence.changedFiles.length > 0 ? evidence.changedFiles.join(",") : "none"}`
2678
+ : "";
2679
+ const mode = evidence.changeMode ? ` mode=${evidence.changeMode}` : "";
2680
+ const fingerprint = evidence.changeFingerprint
2681
+ ? ` fingerprint=${evidence.changeFingerprint.slice(0, 12)}`
2682
+ : "";
2683
+ return `${evidence.status}: ${evidence.command} (${evidence.source}${exitCode}${duration}${files}${mode}${fingerprint} ${evidence.recordedAt})${note}`;
2684
+ }
2685
+ function verificationEvidenceStatusLabel(evidence) {
2686
+ if (evidence.length === 0) {
2687
+ return "none";
2688
+ }
2689
+ if (evidence.some((item) => item.status === "failed")) {
2690
+ return "failed";
2691
+ }
2692
+ if (evidence.some((item) => item.status === "unknown")) {
2693
+ return "unknown";
2694
+ }
2695
+ if (evidence.some((item) => item.status === "skipped")) {
2696
+ return "skipped";
2697
+ }
2698
+ return "passed";
2699
+ }
2700
+ function printReviewPacketSummary(packet) {
2701
+ console.log("Review packet:");
2702
+ console.log(` protocol: ${packet.protocol}`);
2703
+ console.log(` task: ${packet.originalTask}`);
2704
+ console.log(` declared scope: ${packet.declaredScope.controlMode} ${packet.declaredScope.targetFile}`);
2705
+ console.log(` human gate: ${packet.declaredScope.humanGate}`);
2706
+ console.log(` boundary risk: ${packet.declaredScope.boundaryRisk}`);
2707
+ printHumanList(" changed files", packet.actualChanges.changedFiles);
2708
+ printHumanList(" outside boundary files", packet.scopeFindings.outsideBoundaryFiles);
2709
+ printHumanList(" outside boundary symbols", packet.scopeFindings.outsideBoundarySymbols);
2710
+ printHumanList(" verification expected", packet.verification.expectedCommands);
2711
+ console.log(` tests run: ${packet.verification.testsRun}`);
2712
+ if (packet.verification.evidence.length > 0) {
2713
+ console.log(` verification status: ${verificationEvidenceStatusLabel(packet.verification.evidence)}`);
2714
+ }
2715
+ printHumanList(" verification evidence", packet.verification.evidence.map(formatVerificationEvidence));
2716
+ console.log(` can continue: ${formatYesNo(packet.decision.canContinue)}`);
2717
+ console.log(` must stop: ${formatYesNo(packet.decision.mustStop)}`);
2718
+ console.log(` needs human: ${formatYesNo(packet.decision.needsHuman)}`);
2719
+ printHumanList(" reviewer notes", packet.reviewerNotes);
2720
+ }
2721
+ function printAgentReviewPacket(packet) {
2722
+ console.log(`review_packet_protocol: ${packet.protocol}`);
2723
+ console.log(`review_packet_version: ${packet.version}`);
2724
+ console.log(`review_packet_task: ${packet.originalTask}`);
2725
+ console.log(`review_packet_scope: ${packet.declaredScope.controlMode} ${packet.declaredScope.targetFile}`);
2726
+ console.log(`review_packet_human_gate: ${packet.declaredScope.humanGate}`);
2727
+ console.log(`review_packet_boundary_risk: ${packet.declaredScope.boundaryRisk}`);
2728
+ console.log(`review_packet_tests_run: ${packet.verification.testsRun}`);
2729
+ console.log(`review_packet_verification_status: ${verificationEvidenceStatusLabel(packet.verification.evidence)}`);
2730
+ console.log(`review_packet_can_continue: ${packet.decision.canContinue}`);
2731
+ console.log(`review_packet_must_stop: ${packet.decision.mustStop}`);
2732
+ console.log(`review_packet_needs_human: ${packet.decision.needsHuman}`);
2733
+ console.log("");
2734
+ printAgentList("review_packet_changed_files", packet.actualChanges.changedFiles);
2735
+ console.log("");
2736
+ printAgentList("review_packet_outside_boundary_files", packet.scopeFindings.outsideBoundaryFiles);
2737
+ console.log("");
2738
+ printAgentList("review_packet_outside_boundary_symbols", packet.scopeFindings.outsideBoundarySymbols);
2739
+ console.log("");
2740
+ printAgentList("review_packet_verification_expected", packet.verification.expectedCommands);
2741
+ console.log("");
2742
+ printAgentList("review_packet_verification_reported", packet.verification.evidence.map(formatVerificationEvidence));
2743
+ console.log("");
2744
+ printAgentList("review_packet_reviewer_notes", packet.reviewerNotes);
2745
+ }
2128
2746
  function printStagedCheckSummary(summary) {
2129
2747
  const adapter = summary.adapterSupport.primaryAdapter;
2130
- console.log(summary.mode === "changed" ? "Ripple changed-files check" : "Ripple staged check");
2748
+ console.log(summary.mode === "changed" ? "Ripple changed-files check" : summary.mode === "worktree" ? "Ripple worktree check" : "Ripple staged check");
2131
2749
  console.log(`Workspace: ${summary.workspace}`);
2132
2750
  console.log(`Mode: ${summary.mode}`);
2133
2751
  if (summary.baseRef) {
@@ -2151,6 +2769,10 @@ function printStagedCheckSummary(summary) {
2151
2769
  printReadinessDriftSummary("Readiness drift:", summary.intentValidation.readinessDrift);
2152
2770
  console.log(`Planned scope: ${summary.intentValidation.plannedScope}`);
2153
2771
  }
2772
+ if (summary.reviewPacket) {
2773
+ console.log("");
2774
+ printReviewPacketSummary(summary.reviewPacket);
2775
+ }
2154
2776
  if (summary.skippedFiles.length > 0) {
2155
2777
  console.log(`Skipped non-source files: ${summary.skippedFiles.length}`);
2156
2778
  }
@@ -2163,7 +2785,9 @@ function printStagedCheckSummary(summary) {
2163
2785
  console.log("");
2164
2786
  console.log(summary.mode === "changed"
2165
2787
  ? "No changed JS/TS files found."
2166
- : "No staged JS/TS files found.");
2788
+ : summary.mode === "worktree"
2789
+ ? "No worktree JS/TS changes found."
2790
+ : "No staged JS/TS files found.");
2167
2791
  return;
2168
2792
  }
2169
2793
  console.log("");
@@ -2374,7 +2998,7 @@ async function planCommand(options) {
2374
2998
  const output = savedIntent
2375
2999
  ? {
2376
3000
  ...summary,
2377
- policyExplanation,
3001
+ policyExplanation: savedIntent.intent.policyExplanation,
2378
3002
  changeIntent: savedIntent.intent,
2379
3003
  changeIntentPath: savedIntent.path,
2380
3004
  }
@@ -2419,6 +3043,140 @@ function currentPolicyExplanationForIntent(workspaceRoot, intent) {
2419
3043
  function currentReadinessSnapshotForEngine(workspaceRoot, engine) {
2420
3044
  return (0, core_1.buildChangeIntentReadinessSnapshot)((0, core_1.buildRippleReadinessSummary)(workspaceRoot, engine));
2421
3045
  }
3046
+ function intentSnapshot(intent) {
3047
+ return {
3048
+ id: intent.id,
3049
+ createdAt: intent.createdAt,
3050
+ task: intent.task,
3051
+ targetFile: intent.targetFile,
3052
+ controlMode: intent.controlMode,
3053
+ humanGate: intent.humanGate,
3054
+ boundaryRisk: intent.boundaryRisk,
3055
+ };
3056
+ }
3057
+ function intentCommand(action, options) {
3058
+ if (!action || action === "status") {
3059
+ intentStatusCommand(options);
3060
+ return;
3061
+ }
3062
+ if (action === "close") {
3063
+ closeIntentCommand(options);
3064
+ return;
3065
+ }
3066
+ throw new Error("Usage: ripple intent status [--intent latest|path] or ripple intent close --reason <text> [--intent latest|path]");
3067
+ }
3068
+ function intentStatusCommand(options) {
3069
+ const workspaceRoot = resolveWorkspaceRoot(".");
3070
+ const intentRef = options.intent ?? "latest";
3071
+ const intentPath = resolveCliIntentPath(workspaceRoot, intentRef);
3072
+ const exists = fs.existsSync(intentPath);
3073
+ const active = exists && isActiveChangeIntentFile(intentPath);
3074
+ const output = {
3075
+ protocol: "ripple-intent-status",
3076
+ version: 1,
3077
+ workspace: workspaceRoot,
3078
+ intentRef,
3079
+ intentPath: relativeToWorkspace(workspaceRoot, intentPath),
3080
+ exists,
3081
+ active,
3082
+ nextSteps: active
3083
+ ? [
3084
+ "Run ripple gate --intent latest before continuing.",
3085
+ "Run ripple intent close --reason \"<why this boundary is done>\" when the task boundary is complete or intentionally replaced.",
3086
+ ]
3087
+ : [
3088
+ "Run ripple plan --file <file> --task \"<task>\" --agent --save before an agent edits.",
3089
+ ],
3090
+ };
3091
+ if (active) {
3092
+ output.intent = intentSnapshot((0, core_1.loadChangeIntent)(workspaceRoot, intentRef));
3093
+ }
3094
+ if (options.json) {
3095
+ printJson(output);
3096
+ return;
3097
+ }
3098
+ printIntentStatus(output);
3099
+ }
3100
+ function closeIntentCommand(options) {
3101
+ const workspaceRoot = resolveWorkspaceRoot(".");
3102
+ const intentRef = options.intent ?? "latest";
3103
+ const intentPath = resolveCliIntentPath(workspaceRoot, intentRef);
3104
+ if (!fs.existsSync(intentPath) || !isActiveChangeIntentFile(intentPath)) {
3105
+ throw new Error(`No active Ripple intent exists at ${relativeToWorkspace(workspaceRoot, intentPath)}.`);
3106
+ }
3107
+ const reason = options.reason?.trim();
3108
+ if (!reason) {
3109
+ throw new Error("Closing an active Ripple intent requires --reason.");
3110
+ }
3111
+ const intent = (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
3112
+ const closedAt = new Date().toISOString();
3113
+ const closedBy = options.approvedBy?.trim() || "human";
3114
+ const archivePath = archivedIntentPath(workspaceRoot, intent, closedAt);
3115
+ const archive = {
3116
+ protocol: "ripple-closed-intent",
3117
+ version: 1,
3118
+ closedAt,
3119
+ closedBy,
3120
+ reason,
3121
+ originalIntentPath: relativeToWorkspace(workspaceRoot, intentPath),
3122
+ intent,
3123
+ };
3124
+ fs.mkdirSync(path.dirname(archivePath), { recursive: true });
3125
+ fs.writeFileSync(archivePath, `${JSON.stringify(archive, null, 2)}\n`, "utf8");
3126
+ fs.writeFileSync(intentPath, `${JSON.stringify(archive, null, 2)}\n`, "utf8");
3127
+ const output = {
3128
+ protocol: "ripple-intent-close",
3129
+ version: 1,
3130
+ workspace: workspaceRoot,
3131
+ intentRef,
3132
+ intentPath: relativeToWorkspace(workspaceRoot, intentPath),
3133
+ archivePath: relativeToWorkspace(workspaceRoot, archivePath),
3134
+ closedAt,
3135
+ closedBy,
3136
+ reason,
3137
+ intent: intentSnapshot(intent),
3138
+ nextSteps: [
3139
+ "Run ripple intent status to confirm no active saved boundary remains.",
3140
+ "Run ripple plan --file <file> --task \"<task>\" --agent --save to start the next agent boundary.",
3141
+ ],
3142
+ };
3143
+ if (options.json) {
3144
+ printJson(output);
3145
+ return;
3146
+ }
3147
+ printIntentClose(output);
3148
+ }
3149
+ function archivedIntentPath(workspaceRoot, intent, closedAt) {
3150
+ const timestamp = closedAt.replace(/[:.]/g, "-");
3151
+ const safeId = intent.id.replace(/[^a-zA-Z0-9._-]/g, "-");
3152
+ return path.join(workspaceRoot, ".ripple", "intents", "archive", `${timestamp}-${safeId}.json`);
3153
+ }
3154
+ function printIntentStatus(summary) {
3155
+ console.log("Ripple intent status");
3156
+ console.log(`Intent: ${summary.intentRef}`);
3157
+ console.log(`Path: ${summary.intentPath}`);
3158
+ console.log(`Active: ${formatYesNo(summary.active)}`);
3159
+ if (summary.intent) {
3160
+ console.log(`Id: ${summary.intent.id}`);
3161
+ console.log(`Task: ${summary.intent.task}`);
3162
+ console.log(`Target: ${summary.intent.targetFile}`);
3163
+ console.log(`Control mode: ${summary.intent.controlMode}`);
3164
+ console.log(`Human gate: ${summary.intent.humanGate}`);
3165
+ console.log(`Boundary risk: ${summary.intent.boundaryRisk}`);
3166
+ }
3167
+ printHumanList("Next:", summary.nextSteps);
3168
+ }
3169
+ function printIntentClose(summary) {
3170
+ console.log("Ripple intent closed");
3171
+ console.log(`Intent: ${summary.intent.id}`);
3172
+ console.log(`Task: ${summary.intent.task}`);
3173
+ console.log(`Target: ${summary.intent.targetFile}`);
3174
+ console.log(`Closed by: ${summary.closedBy}`);
3175
+ console.log(`Reason: ${summary.reason}`);
3176
+ console.log(`Archived: ${summary.archivePath}`);
3177
+ console.log(`Closed marker: ${summary.intentPath}`);
3178
+ printHumanList("Next:", summary.nextSteps);
3179
+ }
2422
3180
  function approvalStatusOutput(intent, status) {
2423
3181
  return {
2424
3182
  ...status,
@@ -2478,32 +3236,60 @@ async function buildCheckSummaryForFiles(input) {
2478
3236
  engine.dispose();
2479
3237
  }
2480
3238
  }
2481
- async function checkCommand(options) {
2482
- if (!options.staged && !options.changed) {
2483
- throw new Error("Missing check mode. Usage: ripple check --staged or ripple check --changed --base <ref>");
3239
+ function selectedChangeMode(options) {
3240
+ if (options.changed) {
3241
+ return "changed";
3242
+ }
3243
+ if (options.worktree) {
3244
+ return "worktree";
3245
+ }
3246
+ return "staged";
3247
+ }
3248
+ function selectedChangeModeCount(options) {
3249
+ return [options.staged, options.changed, options.worktree].filter(Boolean).length;
3250
+ }
3251
+ function listFilesForChangeMode(workspaceRoot, mode, baseRef) {
3252
+ if (mode === "changed") {
3253
+ return (0, core_1.listGitChangedFiles)(workspaceRoot, baseRef);
2484
3254
  }
2485
- if (options.staged && options.changed) {
2486
- throw new Error("Choose one check mode: --staged or --changed");
3255
+ if (mode === "worktree") {
3256
+ return (0, core_1.listGitWorktreeFiles)(workspaceRoot);
3257
+ }
3258
+ return (0, core_1.listGitStagedFiles)(workspaceRoot);
3259
+ }
3260
+ async function checkCommand(options) {
3261
+ if (selectedChangeModeCount(options) !== 1) {
3262
+ throw new Error("Choose one check mode: --staged, --worktree, or --changed --base <ref>");
2487
3263
  }
2488
3264
  const workspaceRoot = resolveWorkspaceRoot(".");
2489
3265
  const baseRef = options.base ?? "HEAD";
2490
- const checkFiles = options.changed
2491
- ? (0, core_1.listGitChangedFiles)(workspaceRoot, baseRef)
2492
- : (0, core_1.listGitStagedFiles)(workspaceRoot);
3266
+ const mode = selectedChangeMode(options);
3267
+ let loadedIntent;
3268
+ if (options.intent) {
3269
+ try {
3270
+ loadedIntent = (0, core_1.loadChangeIntent)(workspaceRoot, options.intent);
3271
+ }
3272
+ catch (err) {
3273
+ if (!options.strict && !options.githubAnnotations) {
3274
+ throw err;
3275
+ }
3276
+ }
3277
+ }
3278
+ const checkFiles = listFilesForChangeMode(workspaceRoot, mode, baseRef);
2493
3279
  const engine = createFastCheckEngine(workspaceRoot);
2494
3280
  try {
2495
3281
  await runWithQuietEngine(() => engine.fastCheckScan(fastCheckCandidateFiles(checkFiles)));
2496
3282
  const stagedSummary = (0, core_1.buildStagedCheckSummary)(engine, {
2497
3283
  workspaceRoot,
2498
3284
  stagedFiles: checkFiles,
2499
- mode: options.changed ? "changed" : "staged",
2500
- baseRef: options.changed ? baseRef : undefined,
3285
+ mode,
3286
+ baseRef: mode === "changed" ? baseRef : undefined,
2501
3287
  tokenBudget: options.budget,
2502
3288
  });
2503
3289
  let summary = stagedSummary;
2504
3290
  if (options.intent) {
2505
3291
  try {
2506
- const intent = (0, core_1.loadChangeIntent)(workspaceRoot, options.intent);
3292
+ const intent = loadedIntent ?? (0, core_1.loadChangeIntent)(workspaceRoot, options.intent);
2507
3293
  summary = (0, core_1.validateStagedCheckAgainstIntent)(stagedSummary, intent, {
2508
3294
  currentPolicyExplanation: currentPolicyExplanationForIntent(workspaceRoot, intent),
2509
3295
  currentReadinessSnapshot: currentReadinessSnapshotForEngine(workspaceRoot, engine),
@@ -2571,18 +3357,16 @@ async function checkCommand(options) {
2571
3357
  }
2572
3358
  }
2573
3359
  async function buildAuditFromCliOptions(options) {
2574
- if (options.staged && options.changed) {
2575
- throw new Error("Choose one gate/audit mode: --staged or --changed");
3360
+ if (selectedChangeModeCount(options) > 1) {
3361
+ throw new Error("Choose one gate/audit mode: --staged, --worktree, or --changed --base <ref>");
2576
3362
  }
2577
3363
  const workspaceRoot = resolveWorkspaceRoot(".");
2578
3364
  const intentRef = options.intent ?? "latest";
2579
- const mode = options.changed ? "changed" : "staged";
3365
+ const mode = selectedChangeMode(options);
2580
3366
  const baseRef = options.base ?? "HEAD";
2581
- const files = mode === "changed"
2582
- ? (0, core_1.listGitChangedFiles)(workspaceRoot, baseRef)
2583
- : (0, core_1.listGitStagedFiles)(workspaceRoot);
2584
3367
  const intent = (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
2585
3368
  const currentPolicyExplanation = currentPolicyExplanationForIntent(workspaceRoot, intent);
3369
+ const files = listFilesForChangeMode(workspaceRoot, mode, baseRef);
2586
3370
  return buildAuditForFiles({
2587
3371
  workspaceRoot,
2588
3372
  files,
@@ -2610,6 +3394,36 @@ async function auditCommand(options) {
2610
3394
  applyStrictExit(options.strict && strictAuditShouldFail(audit));
2611
3395
  }
2612
3396
  async function gateCommand(options) {
3397
+ if (selectedChangeModeCount(options) > 1) {
3398
+ throw new Error("Choose one gate/audit mode: --staged, --worktree, or --changed --base <ref>");
3399
+ }
3400
+ const workspaceRoot = resolveWorkspaceRoot(".");
3401
+ const intentRef = options.intent ?? "latest";
3402
+ const mode = selectedChangeMode(options);
3403
+ const baseRef = options.base ?? "HEAD";
3404
+ try {
3405
+ (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
3406
+ }
3407
+ catch (err) {
3408
+ const block = (0, core_1.buildRippleGateIntentBlockSummary)({
3409
+ workspaceRoot,
3410
+ mode,
3411
+ baseRef: mode === "changed" ? baseRef : undefined,
3412
+ intentRef,
3413
+ error: err,
3414
+ });
3415
+ if (options.json) {
3416
+ printJson(block);
3417
+ }
3418
+ else if (options.agent) {
3419
+ printAgentGateIntentBlock(block);
3420
+ }
3421
+ else {
3422
+ printGateIntentBlock(block);
3423
+ }
3424
+ applyStrictExit(true);
3425
+ return;
3426
+ }
2613
3427
  const audit = await buildAuditFromCliOptions(options);
2614
3428
  const gate = (0, core_1.buildRippleGateSummary)(audit);
2615
3429
  if (options.json) {
@@ -2621,11 +3435,67 @@ async function gateCommand(options) {
2621
3435
  else {
2622
3436
  printGateSummary(gate);
2623
3437
  }
3438
+ // ─── Ripple Cloud Sync ───────────────────────────────────────────────────
3439
+ // Records the gate decision to ripple-cloud for the compliance audit trail.
3440
+ // Runs on EVERY gate call — including the pre-commit hook blocking scenario.
3441
+ // Never blocks. Never throws. The commit result is unaffected by cloud sync.
3442
+ if (process.env.RIPPLE_API_KEY) {
3443
+ const cloudDecision = gate.canContinue ? "continue" :
3444
+ gate.needsHuman ? "human-review" : "blocked";
3445
+ const commitSha = (0, core_1.getCurrentCommitSha)(child_process_1.execSync);
3446
+ const branch = (0, core_1.getCurrentBranch)(child_process_1.execSync);
3447
+ const actor = (0, core_1.getCurrentActor)(child_process_1.execSync);
3448
+ const cloudResult = await (0, core_1.syncAuditToCloud)({
3449
+ intentId: audit.intent.id,
3450
+ decision: cloudDecision,
3451
+ commitSha,
3452
+ branch,
3453
+ actor,
3454
+ 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
+ },
3479
+ });
3480
+ if (!options.json && !options.agent) {
3481
+ if (cloudResult.sent) {
3482
+ const icon = cloudDecision === "continue" ? "✓" : "⛔";
3483
+ console.log(`\n[Ripple Cloud] ${icon} Gate decision recorded: ${cloudDecision} (${commitSha.slice(0, 7)})`);
3484
+ }
3485
+ else if (cloudResult.error) {
3486
+ console.warn(`\n[Ripple Cloud] ⚠ Sync failed (non-blocking): ${cloudResult.error}`);
3487
+ }
3488
+ }
3489
+ }
3490
+ // ────────────────────────────────────────────────────────────────────────
2624
3491
  applyStrictExit(options.strict && !gate.canContinue);
2625
3492
  }
2626
3493
  function approveCommand(options) {
2627
3494
  const workspaceRoot = resolveWorkspaceRoot(".");
2628
3495
  const intentRef = options.intent ?? "latest";
3496
+ if (!options.reason || options.reason.trim().length === 0) {
3497
+ throw new Error("Approval requires --reason explaining why this boundary is approved.");
3498
+ }
2629
3499
  const intent = (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
2630
3500
  const approval = (0, core_1.recordRippleApproval)(workspaceRoot, intent, {
2631
3501
  gate: options.gate,
@@ -2642,6 +3512,171 @@ function approveCommand(options) {
2642
3512
  }
2643
3513
  printApprovalRecord(approval);
2644
3514
  }
3515
+ function executeVerificationCommand(workspaceRoot, command, note) {
3516
+ const startedAt = Date.now();
3517
+ const result = (0, child_process_1.spawnSync)(command, {
3518
+ cwd: workspaceRoot,
3519
+ shell: true,
3520
+ encoding: "utf8",
3521
+ maxBuffer: 1024 * 1024,
3522
+ windowsHide: true,
3523
+ });
3524
+ const durationMs = Math.max(0, Date.now() - startedAt);
3525
+ const exitCode = typeof result.status === "number" ? result.status : 1;
3526
+ const stderr = [
3527
+ typeof result.stderr === "string" ? result.stderr : "",
3528
+ result.error ? result.error.message : "",
3529
+ ].filter(Boolean).join("\n");
3530
+ return {
3531
+ command,
3532
+ status: exitCode === 0 ? "passed" : "failed",
3533
+ exitCode,
3534
+ durationMs,
3535
+ stdoutTail: outputTail(typeof result.stdout === "string" ? result.stdout : ""),
3536
+ stderrTail: outputTail(stderr),
3537
+ note,
3538
+ };
3539
+ }
3540
+ function outputTail(value, maxLength = 4000) {
3541
+ const normalized = value.replace(/\r\n/g, "\n").trim();
3542
+ if (normalized.length === 0) {
3543
+ return undefined;
3544
+ }
3545
+ return normalized.length > maxLength ? normalized.slice(-maxLength) : normalized;
3546
+ }
3547
+ function currentChangeSnapshotForVerification(workspaceRoot, intent) {
3548
+ const scope = verificationSnapshotScope(intent);
3549
+ const snapshot = (changedFiles, diff, changeMode) => ({
3550
+ changedFiles: changedFiles
3551
+ .map(normalizeProjectPath)
3552
+ .filter(core_1.isRippleSourceFile)
3553
+ .filter((file) => scope.size === 0 || scope.has(file)),
3554
+ changeMode,
3555
+ changeFingerprint: (0, core_1.fingerprintRippleChangeDiff)(diff),
3556
+ });
3557
+ try {
3558
+ const diff = (0, core_1.listGitChangedDiff)(workspaceRoot, "HEAD");
3559
+ const changedFiles = (0, core_1.listGitChangedFiles)(workspaceRoot, "HEAD");
3560
+ const changedSnapshot = snapshot(changedFiles, diff, "changed");
3561
+ if (changedSnapshot.changedFiles.length > 0) {
3562
+ return changedSnapshot;
3563
+ }
3564
+ }
3565
+ catch {
3566
+ // Repositories without a HEAD commit fall back to staged/worktree snapshots.
3567
+ }
3568
+ try {
3569
+ const diff = (0, core_1.listGitStagedDiff)(workspaceRoot);
3570
+ const stagedSnapshot = snapshot((0, core_1.listGitStagedFiles)(workspaceRoot), diff, "staged");
3571
+ if (stagedSnapshot.changedFiles.length > 0) {
3572
+ return stagedSnapshot;
3573
+ }
3574
+ }
3575
+ catch {
3576
+ // Fall through to worktree or empty coverage.
3577
+ }
3578
+ try {
3579
+ return snapshot((0, core_1.listGitWorktreeFiles)(workspaceRoot), (0, core_1.listGitWorktreeDiff)(workspaceRoot), "worktree");
3580
+ }
3581
+ catch {
3582
+ return { changedFiles: [], changeMode: "staged" };
3583
+ }
3584
+ }
3585
+ function verificationSnapshotScope(intent) {
3586
+ return new Set([
3587
+ ...intent.editableFiles,
3588
+ ...intent.expectedFiles,
3589
+ intent.targetFile,
3590
+ ].map(normalizeProjectPath));
3591
+ }
3592
+ function verifyCommand(options) {
3593
+ const workspaceRoot = process.cwd();
3594
+ const intentRef = options.intent ?? "latest";
3595
+ const reportedCommand = options.verificationCommand?.trim();
3596
+ const runCommand = options.verificationRunCommand?.trim();
3597
+ if (reportedCommand && runCommand) {
3598
+ throw new Error("Use either --run for Ripple-executed evidence or --command/--status for reported evidence, not both.");
3599
+ }
3600
+ if (!reportedCommand && !runCommand) {
3601
+ throw new Error("Usage: ripple verify --run <test command> [--intent latest|path] or ripple verify --command <test command> --status passed|failed|skipped|unknown [--intent latest|path]");
3602
+ }
3603
+ if (runCommand && options.verificationStatus) {
3604
+ throw new Error("--status is only valid with --command. Use --run to let Ripple compute passed/failed from the exit code.");
3605
+ }
3606
+ const intent = (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
3607
+ const executed = runCommand
3608
+ ? executeVerificationCommand(workspaceRoot, runCommand, options.note)
3609
+ : undefined;
3610
+ const changeSnapshot = currentChangeSnapshotForVerification(workspaceRoot, intent);
3611
+ const updatedIntent = (0, core_1.appendRippleVerificationEvidence)(intent, {
3612
+ command: executed?.command ?? reportedCommand ?? "",
3613
+ status: executed?.status ?? options.verificationStatus ?? "unknown",
3614
+ source: executed ? "executed" : "reported",
3615
+ changedFiles: changeSnapshot.changedFiles,
3616
+ changeMode: changeSnapshot.changeMode,
3617
+ changeFingerprint: changeSnapshot.changeFingerprint,
3618
+ exitCode: executed?.exitCode,
3619
+ durationMs: executed?.durationMs,
3620
+ stdoutTail: executed?.stdoutTail,
3621
+ stderrTail: executed?.stderrTail,
3622
+ note: executed?.note ?? options.note,
3623
+ });
3624
+ const intentPath = (0, core_1.saveChangeIntent)(workspaceRoot, updatedIntent, intentRef);
3625
+ const evidence = updatedIntent.verificationEvidence[updatedIntent.verificationEvidence.length - 1];
3626
+ const evidenceSourceLabel = evidence.source === "executed"
3627
+ ? "Ripple executed this command and recorded its exit code."
3628
+ : "Ripple recorded reported evidence only; it did not independently run this command.";
3629
+ const output = {
3630
+ protocol: "ripple-verification-evidence",
3631
+ version: 1,
3632
+ workspace: workspaceRoot,
3633
+ intentPath,
3634
+ intentId: updatedIntent.id,
3635
+ evidence,
3636
+ totalEvidence: updatedIntent.verificationEvidence.length,
3637
+ nextSteps: [
3638
+ "Run ripple gate --intent latest --json to include this evidence in the review packet.",
3639
+ evidence.status === "failed"
3640
+ ? "Fix the failing verification, rerun ripple verify --run, then run ripple gate again."
3641
+ : "Run ripple gate again before handoff so the continue/stop decision includes this evidence.",
3642
+ evidenceSourceLabel,
3643
+ ],
3644
+ };
3645
+ if (options.json) {
3646
+ printJson(output);
3647
+ return;
3648
+ }
3649
+ console.log("Ripple verification evidence");
3650
+ console.log(`Intent: ${output.intentId}`);
3651
+ console.log(`Intent path: ${path.relative(workspaceRoot, output.intentPath) || output.intentPath}`);
3652
+ console.log(`Status: ${output.evidence.status}`);
3653
+ console.log(`Command: ${output.evidence.command}`);
3654
+ console.log(`Source: ${output.evidence.source}`);
3655
+ if (typeof output.evidence.exitCode === "number") {
3656
+ console.log(`Exit code: ${output.evidence.exitCode}`);
3657
+ }
3658
+ if (typeof output.evidence.durationMs === "number") {
3659
+ console.log(`Duration ms: ${output.evidence.durationMs}`);
3660
+ }
3661
+ if (output.evidence.changedFiles) {
3662
+ printHumanList("Changed files covered:", output.evidence.changedFiles);
3663
+ }
3664
+ if (output.evidence.changeMode) {
3665
+ console.log(`Change mode: ${output.evidence.changeMode}`);
3666
+ }
3667
+ if (output.evidence.changeFingerprint) {
3668
+ console.log(`Change fingerprint: ${output.evidence.changeFingerprint.slice(0, 12)}`);
3669
+ }
3670
+ console.log(`Recorded at: ${output.evidence.recordedAt}`);
3671
+ if (output.evidence.note) {
3672
+ console.log(`Note: ${output.evidence.note}`);
3673
+ }
3674
+ if (output.evidence.stderrTail) {
3675
+ console.log("Stderr tail:");
3676
+ console.log(output.evidence.stderrTail);
3677
+ }
3678
+ console.log(`Note: ${evidenceSourceLabel}`);
3679
+ }
2645
3680
  function approvalCommand(options) {
2646
3681
  const workspaceRoot = resolveWorkspaceRoot(".");
2647
3682
  const intentRef = options.intent ?? "latest";
@@ -2660,9 +3695,94 @@ function approvalCommand(options) {
2660
3695
  async function ciCommand(options) {
2661
3696
  const workspaceRoot = resolveWorkspaceRoot(".");
2662
3697
  const baseRef = options.base ?? defaultCiBaseRef();
3698
+ const hasExplicitIntent = Boolean(options.intent);
2663
3699
  const intentRef = options.intent ?? "latest";
2664
3700
  const files = (0, core_1.listGitChangedFiles)(workspaceRoot, baseRef);
2665
3701
  const emitGithubAnnotations = shouldEmitGithubAnnotations(options);
3702
+ if (!hasExplicitIntent) {
3703
+ const summary = await buildCheckSummaryForFiles({
3704
+ workspaceRoot,
3705
+ files,
3706
+ mode: "changed",
3707
+ baseRef,
3708
+ tokenBudget: options.budget,
3709
+ });
3710
+ const policySync = buildPolicySyncSummary(workspaceRoot);
3711
+ if (options.json) {
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) {
3756
+ printGithubPolicyAuditAnnotations(summary, policySync);
3757
+ }
3758
+ writeGithubPolicyAuditStepSummary(summary, policySync);
3759
+ // FOUNDER FIX: Add Cloud Sync to the Policy Audit Path
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
+ }
2666
3786
  let intent;
2667
3787
  try {
2668
3788
  intent = (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
@@ -2709,7 +3829,7 @@ async function ciCommand(options) {
2709
3829
  printGithubIntentLoadError(message);
2710
3830
  }
2711
3831
  writeGithubStepSummary({ summary, intentLoadError: message });
2712
- process.exitCode = 1;
3832
+ applyStrictExit(options.strict);
2713
3833
  return;
2714
3834
  }
2715
3835
  const audit = await buildAuditForFiles({
@@ -2737,7 +3857,45 @@ async function ciCommand(options) {
2737
3857
  printGithubAuditAnnotations(audit);
2738
3858
  }
2739
3859
  writeGithubAuditStepSummary(audit);
2740
- applyStrictExit(strictAuditShouldFail(audit));
3860
+ // ─── Ripple Cloud Sync ──────────────────────────────────────────────────
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.
3863
+ if (process.env.RIPPLE_API_KEY) {
3864
+ const gate = (0, core_1.buildRippleGateSummary)(audit);
3865
+ const cloudDecision = gate.canContinue ? "continue" :
3866
+ gate.needsHuman ? "human-review" : "blocked";
3867
+ const commitSha = options.sha ?? (0, core_1.getCurrentCommitSha)(child_process_1.execSync);
3868
+ const branch = (0, core_1.getCurrentBranch)(child_process_1.execSync);
3869
+ const actor = (0, core_1.getCurrentActor)(child_process_1.execSync);
3870
+ const cloudResult = await (0, core_1.syncAuditToCloud)({
3871
+ intentId: intent.id,
3872
+ decision: cloudDecision,
3873
+ commitSha,
3874
+ branch,
3875
+ actor,
3876
+ source: "ci",
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
+ },
3887
+ });
3888
+ if (!options.json && !options.agent) {
3889
+ if (cloudResult.sent) {
3890
+ console.log(`[Ripple Cloud] ✓ Audit synced (${commitSha.slice(0, 7)})`);
3891
+ }
3892
+ else if (cloudResult.error) {
3893
+ console.warn(`[Ripple Cloud] ⚠ Sync failed (non-blocking): ${cloudResult.error}`);
3894
+ }
3895
+ }
3896
+ }
3897
+ // ────────────────────────────────────────────────────────────────────────
3898
+ applyStrictExit(options.strict && strictAuditShouldFail(audit));
2741
3899
  }
2742
3900
  async function doctorCommand(options) {
2743
3901
  const workspaceRoot = resolveWorkspaceRoot(".");
@@ -2745,14 +3903,19 @@ async function doctorCommand(options) {
2745
3903
  try {
2746
3904
  await runWithQuietEngine(() => engine.initialScan());
2747
3905
  const summary = (0, core_1.buildRippleReadinessSummary)(workspaceRoot, engine);
3906
+ const policySync = buildPolicySyncSummary(workspaceRoot);
3907
+ const output = {
3908
+ ...summary,
3909
+ policySync,
3910
+ };
2748
3911
  if (options.json) {
2749
- printJson(summary);
3912
+ printJson(output);
2750
3913
  }
2751
3914
  else if (options.agent) {
2752
- printAgentDoctorSummary(summary);
3915
+ printAgentDoctorSummary(output);
2753
3916
  }
2754
3917
  else {
2755
- printDoctorSummary(summary);
3918
+ printDoctorSummary(output);
2756
3919
  }
2757
3920
  applyStrictExit(options.strict && summary.status !== "ready");
2758
3921
  }
@@ -2762,7 +3925,7 @@ async function doctorCommand(options) {
2762
3925
  }
2763
3926
  async function initCommand(options) {
2764
3927
  const workspaceRoot = resolveWorkspaceRoot(".");
2765
- const policy = (0, core_1.defaultRipplePolicy)();
3928
+ const { policy } = (0, core_1.buildSmartRipplePolicy)(workspaceRoot);
2766
3929
  const policyContents = (0, core_1.formatRipplePolicy)(policy);
2767
3930
  const workflow = githubActionsWorkflow();
2768
3931
  const gitignoreBlock = rippleGitIgnoreBlock();
@@ -2785,6 +3948,32 @@ async function initCommand(options) {
2785
3948
  },
2786
3949
  ];
2787
3950
  if (options.print) {
3951
+ const agentFiles = agentSetupFiles(workspaceRoot);
3952
+ const agentSetup = buildAgentSetupSummary(workspaceRoot, agentFiles.map((file) => ({
3953
+ path: file.path,
3954
+ status: "printed",
3955
+ written: false,
3956
+ overwritten: false,
3957
+ content: file.content,
3958
+ })));
3959
+ const preCommitContent = ripplePreCommitHookScript();
3960
+ const postCommitContent = ripplePostCommitHookScript();
3961
+ const hookPath = preferredHookPath(workspaceRoot, "pre-commit");
3962
+ const postCommitHookPath = preferredHookPath(workspaceRoot, "post-commit");
3963
+ const hooks = {
3964
+ protocol: "ripple-hook-install",
3965
+ version: 1,
3966
+ workspace: workspaceRoot,
3967
+ path: normalizeHookPathForOutput(workspaceRoot, hookPath),
3968
+ postCommitPath: normalizeHookPathForOutput(workspaceRoot, postCommitHookPath),
3969
+ status: "printed",
3970
+ written: false,
3971
+ overwritten: false,
3972
+ content: [preCommitContent, postCommitContent].join("\n--- ripple-post-commit ---\n"),
3973
+ preCommitContent,
3974
+ postCommitContent,
3975
+ nextSteps: ["Review the hook scripts, then run ripple init to write the full local setup."],
3976
+ };
2788
3977
  const summary = {
2789
3978
  protocol: "ripple-init",
2790
3979
  version: 1,
@@ -2796,20 +3985,30 @@ async function initCommand(options) {
2796
3985
  overwritten: false,
2797
3986
  content: file.content,
2798
3987
  })),
3988
+ agentSetup,
3989
+ hooks,
2799
3990
  nextSteps: defaultInitNextSteps(),
2800
3991
  };
2801
3992
  if (options.json) {
2802
3993
  printJson(summary);
2803
3994
  return;
2804
3995
  }
2805
- process.stdout.write(files
2806
- .flatMap((file) => [`# ${file.path}`, file.content.trimEnd(), ""])
2807
- .join("\n"));
3996
+ process.stdout.write([
3997
+ ...files.flatMap((file) => [`# ${file.path}`, file.content.trimEnd(), ""]),
3998
+ ...agentFiles.flatMap((file) => [`# ${file.path}`, file.content.trimEnd(), ""]),
3999
+ "# .git hooks",
4000
+ preCommitContent.trimEnd(),
4001
+ "--- ripple-post-commit ---",
4002
+ postCommitContent.trimEnd(),
4003
+ "",
4004
+ ].join("\n"));
2808
4005
  return;
2809
4006
  }
2810
4007
  const writtenFiles = files.map((file) => file.merge
2811
4008
  ? writeRippleGitIgnoreFile(file.absolutePath)
2812
4009
  : writeInitFile(file, options.force));
4010
+ const agentSetup = buildAgentSetupSummary(workspaceRoot, agentSetupFiles(workspaceRoot).map((file) => writeAgentSetupFile(file, options.force)));
4011
+ const hooks = installRippleHooks(workspaceRoot);
2813
4012
  const engine = createCliEngine(workspaceRoot);
2814
4013
  try {
2815
4014
  await runWithQuietEngine(() => engine.initialScan());
@@ -2819,6 +4018,8 @@ async function initCommand(options) {
2819
4018
  version: 1,
2820
4019
  workspace: workspaceRoot,
2821
4020
  files: writtenFiles,
4021
+ agentSetup,
4022
+ hooks,
2822
4023
  readiness,
2823
4024
  nextSteps: defaultInitNextSteps(readiness),
2824
4025
  };
@@ -2833,6 +4034,65 @@ async function initCommand(options) {
2833
4034
  engine.dispose();
2834
4035
  }
2835
4036
  }
4037
+ function writeAgentSetupFile(file, force) {
4038
+ const existed = fs.existsSync(file.absolutePath);
4039
+ const nextSection = normalizeLf(file.content);
4040
+ if (!existed || force) {
4041
+ fs.mkdirSync(path.dirname(file.absolutePath), { recursive: true });
4042
+ fs.writeFileSync(file.absolutePath, ensureTrailingLf(nextSection), "utf8");
4043
+ return {
4044
+ path: file.path,
4045
+ status: existed ? "overwritten" : "written",
4046
+ written: true,
4047
+ overwritten: existed,
4048
+ };
4049
+ }
4050
+ const existing = fs.readFileSync(file.absolutePath, "utf8");
4051
+ const updated = mergeRippleManagedSection(existing, nextSection);
4052
+ if (updated.content === existing) {
4053
+ return {
4054
+ path: file.path,
4055
+ status: "exists",
4056
+ written: false,
4057
+ overwritten: false,
4058
+ };
4059
+ }
4060
+ fs.writeFileSync(file.absolutePath, updated.content, "utf8");
4061
+ return {
4062
+ path: file.path,
4063
+ status: updated.action,
4064
+ written: true,
4065
+ overwritten: false,
4066
+ };
4067
+ }
4068
+ function mergeRippleManagedSection(existing, nextSection) {
4069
+ const normalizedExisting = normalizeLf(existing);
4070
+ const start = normalizedExisting.indexOf(RIPPLE_AGENT_SECTION_START);
4071
+ const end = normalizedExisting.indexOf(RIPPLE_AGENT_SECTION_END);
4072
+ if (start !== -1 && end !== -1 && end > start) {
4073
+ const afterEnd = end + RIPPLE_AGENT_SECTION_END.length;
4074
+ const withoutOldSection = `${normalizedExisting.slice(0, start)}${normalizedExisting.slice(afterEnd)}`;
4075
+ return {
4076
+ content: appendRippleSectionAtBottom(withoutOldSection, nextSection),
4077
+ action: "updated",
4078
+ };
4079
+ }
4080
+ return {
4081
+ content: appendRippleSectionAtBottom(normalizedExisting, nextSection),
4082
+ action: "appended",
4083
+ };
4084
+ }
4085
+ function appendRippleSectionAtBottom(existing, nextSection) {
4086
+ const base = normalizeLf(existing).replace(/\n*$/, "");
4087
+ const separator = base.length === 0 ? "" : "\n\n";
4088
+ return `${base}${separator}${ensureTrailingLf(normalizeLf(nextSection))}`;
4089
+ }
4090
+ function normalizeLf(value) {
4091
+ return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
4092
+ }
4093
+ function ensureTrailingLf(value) {
4094
+ return value.endsWith("\n") ? value : `${value}\n`;
4095
+ }
2836
4096
  function writeInitFile(file, force) {
2837
4097
  const existed = fs.existsSync(file.absolutePath);
2838
4098
  if (existed && !force) {
@@ -2890,6 +4150,315 @@ function gitIgnoreContainsRippleCache(contents) {
2890
4150
  line === ".ripple/.cache" ||
2891
4151
  line === ".ripple/.cache/**");
2892
4152
  }
4153
+ const RIPPLE_PRE_COMMIT_HOOK_START = "# >>> ripple pre-commit hook";
4154
+ const RIPPLE_PRE_COMMIT_HOOK_END = "# <<< ripple pre-commit hook";
4155
+ const RIPPLE_POST_COMMIT_HOOK_START = "# >>> ripple post-commit hook";
4156
+ const RIPPLE_POST_COMMIT_HOOK_END = "# <<< ripple post-commit hook";
4157
+ function ripplePreCommitHookBlock() {
4158
+ return [
4159
+ RIPPLE_PRE_COMMIT_HOOK_START,
4160
+ `# Policy is permanent. Intent is local. Git staged diff is truth.
4161
+ ripple_previous_status=$?
4162
+ if [ "$ripple_previous_status" -ne 0 ]; then
4163
+ exit "$ripple_previous_status"
4164
+ fi
4165
+
4166
+ set +e
4167
+
4168
+ ripple_run() {
4169
+ ${rippleDirectRunnerHookLines().join("\n")}
4170
+ }
4171
+
4172
+ if [ -f ".ripple/intents/latest.json" ]; then
4173
+ echo "[Ripple] Active local intent found. Checking staged changes against approved boundary..."
4174
+ ripple_run gate --staged --intent latest --agent --strict
4175
+ status=$?
4176
+ if [ "$status" -ne 0 ]; then
4177
+ cat <<'EOF'
4178
+ [RIPPLE STOP] Commit blocked by Ripple active-intent boundary.
4179
+
4180
+ If you are an AI agent:
4181
+ - DO NOT retry the commit.
4182
+ - Repair the unauthorized change or ask the human to approve a wider scope.
4183
+
4184
+ If you are a human developer:
4185
+ - Review the Ripple output above.
4186
+ - To bypass this local hook intentionally, run: git commit --no-verify
4187
+ EOF
4188
+ exit $status
4189
+ fi
4190
+ else
4191
+ echo "[Ripple] No active local intent found. Running staged policy/contract awareness check..."
4192
+ ripple_run check --staged --agent
4193
+ status=$?
4194
+ if [ "$status" -ne 0 ]; then
4195
+ cat <<'EOF'
4196
+ [RIPPLE WARNING] Ripple could not complete the no-intent staged check.
4197
+
4198
+ If you are an AI agent:
4199
+ - Stop and ask the human before continuing.
4200
+
4201
+ If you are a human developer:
4202
+ - Review the error above.
4203
+ - To bypass this local hook intentionally, run: git commit --no-verify
4204
+ EOF
4205
+ exit $status
4206
+ fi
4207
+ fi`,
4208
+ RIPPLE_PRE_COMMIT_HOOK_END,
4209
+ "",
4210
+ ].join("\n");
4211
+ }
4212
+ function ripplePreCommitHookScript() {
4213
+ return [
4214
+ "#!/bin/sh",
4215
+ "# Ripple pre-commit hook - generated by `ripple hook install`.",
4216
+ ripplePreCommitHookBlock(),
4217
+ "exit 0",
4218
+ "",
4219
+ ].join("\n");
4220
+ }
4221
+ function ripplePostCommitHookBlock() {
4222
+ return [
4223
+ RIPPLE_POST_COMMIT_HOOK_START,
4224
+ `# Local intents are consumed after a successful commit.
4225
+ # If RIPPLE_API_KEY is set, the commit is synced to ripple-cloud for GitHub App verification.
4226
+ set +e
4227
+
4228
+ COMMIT_SHA=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
4229
+ BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
4230
+ ACTOR=$(git config user.name 2>/dev/null || git config user.email 2>/dev/null || echo "unknown")
4231
+ CLOUD_URL=\${RIPPLE_CLOUD_URL:-https://your-ripple-cloud.vercel.app}
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."
4292
+ fi`,
4293
+ RIPPLE_POST_COMMIT_HOOK_END,
4294
+ "",
4295
+ ].join("\n");
4296
+ }
4297
+ function ripplePostCommitHookScript() {
4298
+ return [
4299
+ "#!/bin/sh",
4300
+ "# Ripple post-commit hook - generated by `ripple hook install`.",
4301
+ ripplePostCommitHookBlock(),
4302
+ "exit 0",
4303
+ "",
4304
+ ].join("\n");
4305
+ }
4306
+ function normalizeHookPathForOutput(workspaceRoot, hookPath) {
4307
+ return path.relative(workspaceRoot, hookPath).split(path.sep).join("/");
4308
+ }
4309
+ function preferredHookPath(workspaceRoot, hookName) {
4310
+ const huskyDir = path.join(workspaceRoot, ".husky");
4311
+ if (fs.existsSync(huskyDir) && fs.statSync(huskyDir).isDirectory()) {
4312
+ return path.join(huskyDir, hookName);
4313
+ }
4314
+ return path.join(workspaceRoot, ".git", "hooks", hookName);
4315
+ }
4316
+ function installRippleHookBlock(input) {
4317
+ const { hookPath, fullScript, block, marker } = input;
4318
+ if (!fs.existsSync(hookPath)) {
4319
+ fs.mkdirSync(path.dirname(hookPath), { recursive: true });
4320
+ fs.writeFileSync(hookPath, fullScript, { encoding: "utf8", mode: 0o755 });
4321
+ try {
4322
+ fs.chmodSync(hookPath, 0o755);
4323
+ }
4324
+ catch {
4325
+ // chmod is best-effort on Windows.
4326
+ }
4327
+ return "created";
4328
+ }
4329
+ const existing = fs.readFileSync(hookPath, "utf8");
4330
+ if (existing.includes(marker)) {
4331
+ return "already-present";
4332
+ }
4333
+ const separator = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
4334
+ fs.writeFileSync(hookPath, `${existing}${separator}\n${block}\n`, "utf8");
4335
+ try {
4336
+ fs.chmodSync(hookPath, 0o755);
4337
+ }
4338
+ catch {
4339
+ // chmod is best-effort on Windows.
4340
+ }
4341
+ return "appended";
4342
+ }
4343
+ function installRippleHooks(workspaceRoot) {
4344
+ if (!fs.existsSync(path.join(workspaceRoot, ".git"))) {
4345
+ throw new Error("Cannot install Ripple hook because .git was not found. Run this inside a Git worktree.");
4346
+ }
4347
+ const hookPath = preferredHookPath(workspaceRoot, "pre-commit");
4348
+ const postCommitHookPath = preferredHookPath(workspaceRoot, "post-commit");
4349
+ const content = ripplePreCommitHookScript();
4350
+ const postCommitContent = ripplePostCommitHookScript();
4351
+ const preCommitAction = installRippleHookBlock({
4352
+ hookPath,
4353
+ fullScript: content,
4354
+ block: ripplePreCommitHookBlock(),
4355
+ marker: RIPPLE_PRE_COMMIT_HOOK_START,
4356
+ });
4357
+ const postCommitAction = installRippleHookBlock({
4358
+ hookPath: postCommitHookPath,
4359
+ fullScript: postCommitContent,
4360
+ block: ripplePostCommitHookBlock(),
4361
+ marker: RIPPLE_POST_COMMIT_HOOK_START,
4362
+ });
4363
+ const wroteSomething = preCommitAction !== "already-present" || postCommitAction !== "already-present";
4364
+ return {
4365
+ protocol: "ripple-hook-install",
4366
+ version: 1,
4367
+ workspace: workspaceRoot,
4368
+ path: normalizeHookPathForOutput(workspaceRoot, hookPath),
4369
+ postCommitPath: normalizeHookPathForOutput(workspaceRoot, postCommitHookPath),
4370
+ status: wroteSomething ? "written" : "exists",
4371
+ written: wroteSomething,
4372
+ overwritten: false,
4373
+ preCommitAction,
4374
+ postCommitAction,
4375
+ nextSteps: [
4376
+ "Run ripple plan --file <file> --task \"<task>\" --mode file --agent --save before AI edits.",
4377
+ "Commit normally; Ripple will block active-intent drift and clear consumed local intents after successful commits.",
4378
+ ],
4379
+ };
4380
+ }
4381
+ function hookInstallCommand(subcommand, options) {
4382
+ if (subcommand !== "install") {
4383
+ throw new Error("Usage: ripple hook install [--print] [--force]");
4384
+ }
4385
+ const workspaceRoot = resolveWorkspaceRoot(".");
4386
+ const hookPath = preferredHookPath(workspaceRoot, "pre-commit");
4387
+ const postCommitHookPath = preferredHookPath(workspaceRoot, "post-commit");
4388
+ const relativeHookPath = normalizeHookPathForOutput(workspaceRoot, hookPath);
4389
+ const relativePostCommitHookPath = normalizeHookPathForOutput(workspaceRoot, postCommitHookPath);
4390
+ const content = ripplePreCommitHookScript();
4391
+ const postCommitContent = ripplePostCommitHookScript();
4392
+ if (options.print) {
4393
+ const summary = {
4394
+ protocol: "ripple-hook-install",
4395
+ version: 1,
4396
+ workspace: workspaceRoot,
4397
+ path: relativeHookPath,
4398
+ postCommitPath: relativePostCommitHookPath,
4399
+ status: "printed",
4400
+ written: false,
4401
+ overwritten: false,
4402
+ content: [content, postCommitContent].join("\n--- ripple-post-commit ---\n"),
4403
+ preCommitContent: content,
4404
+ postCommitContent,
4405
+ nextSteps: ["Review the hook scripts, then run ripple hook install to write them."],
4406
+ };
4407
+ if (options.json) {
4408
+ printJson(summary);
4409
+ }
4410
+ else {
4411
+ process.stdout.write(content);
4412
+ process.stdout.write("\n--- ripple-post-commit ---\n");
4413
+ process.stdout.write(postCommitContent);
4414
+ }
4415
+ return;
4416
+ }
4417
+ if (!fs.existsSync(path.join(workspaceRoot, ".git"))) {
4418
+ throw new Error("Cannot install Ripple hook because .git was not found. Run this inside a Git worktree.");
4419
+ }
4420
+ const preCommitAction = installRippleHookBlock({
4421
+ hookPath,
4422
+ fullScript: content,
4423
+ block: ripplePreCommitHookBlock(),
4424
+ marker: RIPPLE_PRE_COMMIT_HOOK_START,
4425
+ });
4426
+ const postCommitAction = installRippleHookBlock({
4427
+ hookPath: postCommitHookPath,
4428
+ fullScript: postCommitContent,
4429
+ block: ripplePostCommitHookBlock(),
4430
+ marker: RIPPLE_POST_COMMIT_HOOK_START,
4431
+ });
4432
+ const wroteSomething = preCommitAction !== "already-present" || postCommitAction !== "already-present";
4433
+ const summary = {
4434
+ protocol: "ripple-hook-install",
4435
+ version: 1,
4436
+ workspace: workspaceRoot,
4437
+ path: relativeHookPath,
4438
+ postCommitPath: relativePostCommitHookPath,
4439
+ status: wroteSomething ? "written" : "exists",
4440
+ written: wroteSomething,
4441
+ overwritten: false,
4442
+ preCommitAction,
4443
+ postCommitAction,
4444
+ nextSteps: [
4445
+ "Commit normally. Ripple will check staged changes before each commit.",
4446
+ "After a successful commit, Ripple clears consumed local intents to prevent ghost-intent blocks.",
4447
+ "Humans can intentionally bypass local hooks with git commit --no-verify.",
4448
+ ],
4449
+ };
4450
+ if (options.json) {
4451
+ printJson(summary);
4452
+ }
4453
+ else {
4454
+ console.log(wroteSomething ? "Ripple Git hooks integrated" : "Ripple Git hooks already integrated");
4455
+ console.log(`Pre-commit: ${relativeHookPath} (${preCommitAction})`);
4456
+ console.log(`Post-commit: ${relativePostCommitHookPath} (${postCommitAction})`);
4457
+ console.log("Active intent: blocks staged drift against latest local plan.");
4458
+ console.log("No intent: warns with staged policy/contract awareness and lets humans stay in control.");
4459
+ console.log("Post-commit: clears consumed local intents to prevent ghost-intent blocks.");
4460
+ }
4461
+ }
2893
4462
  function initCiCommand(options) {
2894
4463
  const workflow = githubActionsWorkflow();
2895
4464
  const workspaceRoot = resolveWorkspaceRoot(".");
@@ -2924,7 +4493,7 @@ function initCiCommand(options) {
2924
4493
  }
2925
4494
  console.log(existed ? "Ripple CI workflow overwritten" : "Ripple CI workflow written");
2926
4495
  console.log(`Path: ${relativeTargetPath}`);
2927
- console.log("Command: npx -y @getripple/cli@latest ci --base origin/${{ github.base_ref }} --intent latest --github-annotations");
4496
+ console.log(`Command: npx -y ${rippleCliPackageSpec()} ci --base origin/\${{ github.base_ref }} --github-annotations`);
2928
4497
  }
2929
4498
  function policyCommand(args, options) {
2930
4499
  const subcommand = args[0];
@@ -2932,15 +4501,19 @@ function policyCommand(args, options) {
2932
4501
  policyInitCommand(options);
2933
4502
  return;
2934
4503
  }
4504
+ if (subcommand === "sync") {
4505
+ policySyncCommand(options);
4506
+ return;
4507
+ }
2935
4508
  if (subcommand === "explain") {
2936
4509
  policyExplainCommand(options);
2937
4510
  return;
2938
4511
  }
2939
- throw new Error("Usage: ripple policy init [--print] [--force] or ripple policy explain --file <file>");
4512
+ throw new Error("Usage: ripple policy init [--print] [--force], ripple policy sync [--json], or ripple policy explain --file <file>");
2940
4513
  }
2941
4514
  function policyInitCommand(options) {
2942
- const policy = (0, core_1.defaultRipplePolicy)();
2943
4515
  const workspaceRoot = resolveWorkspaceRoot(".");
4516
+ const { policy, detections } = (0, core_1.buildSmartRipplePolicy)(workspaceRoot);
2944
4517
  const targetPath = (0, core_1.ripplePolicyPath)(workspaceRoot);
2945
4518
  const relativeTargetPath = core_1.RIPPLE_POLICY_PATH.split(path.sep).join("/");
2946
4519
  const contents = (0, core_1.formatRipplePolicy)(policy);
@@ -2949,6 +4522,7 @@ function policyInitCommand(options) {
2949
4522
  printJson({
2950
4523
  path: relativeTargetPath,
2951
4524
  policy,
4525
+ detections,
2952
4526
  written: false,
2953
4527
  });
2954
4528
  }
@@ -2969,6 +4543,7 @@ function policyInitCommand(options) {
2969
4543
  written: true,
2970
4544
  overwritten: existed,
2971
4545
  policy,
4546
+ detections,
2972
4547
  });
2973
4548
  return;
2974
4549
  }
@@ -2976,6 +4551,189 @@ function policyInitCommand(options) {
2976
4551
  console.log(`Path: ${relativeTargetPath}`);
2977
4552
  console.log(`Default mode: ${policy.defaultMode ?? "file"}`);
2978
4553
  console.log(`Risk rules: ${policy.riskRules?.length ?? 0}`);
4554
+ if (detections.length > 0) {
4555
+ console.log("Smart detections:");
4556
+ detections.forEach((detection) => {
4557
+ console.log(`- ${detection.kind}: ${detection.evidence.join(", ")}`);
4558
+ });
4559
+ }
4560
+ }
4561
+ function policySyncCommand(options) {
4562
+ const workspaceRoot = resolveWorkspaceRoot(".");
4563
+ const summary = buildPolicySyncSummary(workspaceRoot);
4564
+ if (options.json) {
4565
+ printJson(summary);
4566
+ return;
4567
+ }
4568
+ printPolicySyncSummary(summary);
4569
+ }
4570
+ function buildPolicySyncSummary(workspaceRoot) {
4571
+ const loadedPolicy = (0, core_1.loadRipplePolicy)(workspaceRoot);
4572
+ const { policy: smartPolicy, detections } = (0, core_1.buildSmartRipplePolicy)(workspaceRoot);
4573
+ const existingRules = loadedPolicy.policy.riskRules ?? [];
4574
+ const missingRules = [];
4575
+ const missingRuleKeys = new Set();
4576
+ const detectionSummaries = detections.map((detection) => {
4577
+ let missingForDetection = 0;
4578
+ detection.rules.forEach((rule) => {
4579
+ if (policyRuleIsCovered(rule, existingRules)) {
4580
+ return;
4581
+ }
4582
+ const key = policyRuleKey(rule);
4583
+ if (missingRuleKeys.has(key)) {
4584
+ return;
4585
+ }
4586
+ missingRuleKeys.add(key);
4587
+ missingForDetection += 1;
4588
+ missingRules.push({
4589
+ ...clonePolicyRule(rule),
4590
+ reason: `${detection.kind}: ${detection.evidence.join(", ")}`,
4591
+ });
4592
+ });
4593
+ return {
4594
+ kind: detection.kind,
4595
+ evidence: detection.evidence,
4596
+ missingRules: missingForDetection,
4597
+ };
4598
+ });
4599
+ if (!loadedPolicy.exists) {
4600
+ (smartPolicy.riskRules ?? []).forEach((rule) => {
4601
+ const key = policyRuleKey(rule);
4602
+ if (missingRuleKeys.has(key)) {
4603
+ return;
4604
+ }
4605
+ missingRuleKeys.add(key);
4606
+ missingRules.push({
4607
+ ...clonePolicyRule(rule),
4608
+ reason: "policy file missing",
4609
+ });
4610
+ });
4611
+ }
4612
+ const status = missingRules.length > 0 ? "update-available" : "up-to-date";
4613
+ return {
4614
+ protocol: "ripple-policy-sync",
4615
+ version: 1,
4616
+ workspace: workspaceRoot,
4617
+ policyPath: core_1.RIPPLE_POLICY_PATH.split(path.sep).join("/"),
4618
+ policyExists: loadedPolicy.exists,
4619
+ status,
4620
+ missingRules,
4621
+ detections: detectionSummaries,
4622
+ nextSteps: policySyncNextSteps(status, loadedPolicy.exists),
4623
+ };
4624
+ }
4625
+ function clonePolicyRule(rule) {
4626
+ return {
4627
+ paths: [...rule.paths],
4628
+ ...(rule.risk ? { risk: rule.risk } : {}),
4629
+ ...(rule.requireHumanBeforeEdit === true ? { requireHumanBeforeEdit: true } : {}),
4630
+ ...(rule.requireHumanBeforeMerge === true ? { requireHumanBeforeMerge: true } : {}),
4631
+ ...(rule.allowPrMode === true ? { allowPrMode: true } : {}),
4632
+ };
4633
+ }
4634
+ function policyRuleIsCovered(suggested, existingRules) {
4635
+ return existingRules.some((existing) => {
4636
+ if (!suggested.paths.every((suggestedPath) => existing.paths.some((existingPath) => policyPathPatternCovers(existingPath, suggestedPath)))) {
4637
+ return false;
4638
+ }
4639
+ if (suggested.risk && comparePolicyRisk(existing.risk, suggested.risk) < 0) {
4640
+ return false;
4641
+ }
4642
+ if (suggested.requireHumanBeforeEdit === true && existing.requireHumanBeforeEdit !== true) {
4643
+ return false;
4644
+ }
4645
+ if (suggested.requireHumanBeforeMerge === true && existing.requireHumanBeforeMerge !== true) {
4646
+ return false;
4647
+ }
4648
+ return true;
4649
+ });
4650
+ }
4651
+ function policyPathPatternCovers(existingPattern, suggestedPattern) {
4652
+ const existing = normalizePolicyPattern(existingPattern);
4653
+ const suggested = normalizePolicyPattern(suggestedPattern);
4654
+ if (existing === suggested) {
4655
+ return true;
4656
+ }
4657
+ if (existing === "**" || existing === "**/*") {
4658
+ return true;
4659
+ }
4660
+ if (existing.endsWith("/**")) {
4661
+ const base = existing.slice(0, -3);
4662
+ return suggested === base || suggested.startsWith(`${base}/`);
4663
+ }
4664
+ if (!suggested.includes("*") && policyGlobToRegExp(existing).test(suggested)) {
4665
+ return true;
4666
+ }
4667
+ return false;
4668
+ }
4669
+ function normalizePolicyPattern(pattern) {
4670
+ return pattern.replace(/\\\\/g, "/").replace(/^\.\//, "");
4671
+ }
4672
+ function policyGlobToRegExp(pattern) {
4673
+ const normalized = normalizePolicyPattern(pattern);
4674
+ let source = "";
4675
+ for (let index = 0; index < normalized.length; index += 1) {
4676
+ const char = normalized[index];
4677
+ const next = normalized[index + 1];
4678
+ if (char === "*" && next === "*") {
4679
+ source += ".*";
4680
+ index += 1;
4681
+ continue;
4682
+ }
4683
+ if (char === "*") {
4684
+ source += "[^/]*";
4685
+ continue;
4686
+ }
4687
+ source += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
4688
+ }
4689
+ return new RegExp(`^${source}$`);
4690
+ }
4691
+ function comparePolicyRisk(existing, suggested) {
4692
+ const levels = ["low", "medium", "high", "critical"];
4693
+ return levels.indexOf(existing ?? "low") - levels.indexOf(suggested ?? "low");
4694
+ }
4695
+ function policyRuleKey(rule) {
4696
+ return JSON.stringify({
4697
+ paths: rule.paths.map(normalizePolicyPattern).sort(),
4698
+ risk: rule.risk ?? "",
4699
+ requireHumanBeforeEdit: rule.requireHumanBeforeEdit === true,
4700
+ requireHumanBeforeMerge: rule.requireHumanBeforeMerge === true,
4701
+ allowPrMode: rule.allowPrMode === true,
4702
+ });
4703
+ }
4704
+ function policySyncNextSteps(status, policyExists) {
4705
+ if (!policyExists) {
4706
+ return [
4707
+ "Run ripple policy init to create .ripple/policy.json from the current repository shape.",
4708
+ "Review the suggested risk rules before committing the policy.",
4709
+ ];
4710
+ }
4711
+ if (status === "up-to-date") {
4712
+ return ["No policy update is required right now."];
4713
+ }
4714
+ return [
4715
+ "Review the suggested missing rules with a human maintainer.",
4716
+ "Update .ripple/policy.json only after approving the new trust boundaries.",
4717
+ ];
4718
+ }
4719
+ function printPolicySyncSummary(summary) {
4720
+ console.log("Ripple policy sync");
4721
+ console.log(`Policy: ${summary.policyPath}${summary.policyExists ? "" : " (missing)"}`);
4722
+ console.log(`Status: ${summary.status}`);
4723
+ if (summary.missingRules.length > 0) {
4724
+ console.log("");
4725
+ console.log("Detected risky paths not covered by policy:");
4726
+ summary.missingRules.forEach((rule) => {
4727
+ console.log(`- ${rule.paths.join(", ")} risk=${rule.risk ?? "medium"}${rule.requireHumanBeforeEdit ? " human-before-edit" : ""}${rule.requireHumanBeforeMerge ? " human-before-merge" : ""}`);
4728
+ console.log(` reason: ${rule.reason}`);
4729
+ });
4730
+ }
4731
+ else {
4732
+ console.log("Policy is up to date with current smart detections.");
4733
+ }
4734
+ console.log("");
4735
+ console.log("Next:");
4736
+ summary.nextSteps.forEach((step) => console.log(`- ${step}`));
2979
4737
  }
2980
4738
  function policyExplainCommand(options) {
2981
4739
  if (!options.file) {
@@ -3035,8 +4793,8 @@ function printAgentPolicyExplanation(explanation) {
3035
4793
  }
3036
4794
  async function repairCommand(options) {
3037
4795
  const workspaceRoot = resolveWorkspaceRoot(".");
3038
- const stagedFiles = (0, core_1.listGitStagedFiles)(workspaceRoot);
3039
4796
  const intent = (0, core_1.loadChangeIntent)(workspaceRoot, options.intent ?? "latest");
4797
+ const stagedFiles = (0, core_1.listGitStagedFiles)(workspaceRoot);
3040
4798
  const engine = createFastCheckEngine(workspaceRoot);
3041
4799
  try {
3042
4800
  await runWithQuietEngine(() => engine.fastCheckScan(fastCheckCandidateFiles(stagedFiles, intent)));
@@ -3299,6 +5057,13 @@ async function main() {
3299
5057
  return;
3300
5058
  }
3301
5059
  if (command === "agent") {
5060
+ if (arg === "setup") {
5061
+ agentSetupCommand(options);
5062
+ return;
5063
+ }
5064
+ if (arg && arg !== "setup") {
5065
+ throw new Error("Usage: ripple agent or ripple agent setup [--print] [--force]");
5066
+ }
3302
5067
  if (options.json) {
3303
5068
  printJson((0, core_1.getAgentWorkflowSummary)());
3304
5069
  }
@@ -3307,6 +5072,10 @@ async function main() {
3307
5072
  }
3308
5073
  return;
3309
5074
  }
5075
+ if (command === "hook") {
5076
+ hookInstallCommand(arg, options);
5077
+ return;
5078
+ }
3310
5079
  if (command === "init") {
3311
5080
  await initCommand(options);
3312
5081
  return;
@@ -3355,6 +5124,10 @@ async function main() {
3355
5124
  await planCommand(options);
3356
5125
  return;
3357
5126
  }
5127
+ if (command === "intent") {
5128
+ intentCommand(arg, options);
5129
+ return;
5130
+ }
3358
5131
  if (command === "check") {
3359
5132
  await checkCommand(options);
3360
5133
  return;
@@ -3367,6 +5140,10 @@ async function main() {
3367
5140
  await gateCommand(options);
3368
5141
  return;
3369
5142
  }
5143
+ if (command === "verify") {
5144
+ verifyCommand(options);
5145
+ return;
5146
+ }
3370
5147
  if (command === "approval") {
3371
5148
  approvalCommand(options);
3372
5149
  return;