@getripple/cli 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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("-")) {
@@ -376,6 +578,12 @@ function parseControlMode(value) {
376
578
  }
377
579
  throw new Error(`--mode must be one of: ${CONTROL_MODES.join(", ")}`);
378
580
  }
581
+ function parseVerificationStatus(value) {
582
+ if (value === "passed" || value === "failed" || value === "skipped" || value === "unknown") {
583
+ return value;
584
+ }
585
+ throw new Error("--status must be one of: passed, failed, skipped, unknown");
586
+ }
379
587
  function parseApprovalGate(value) {
380
588
  if (value === "before-risky-edit" || value === "before-merge") {
381
589
  return value;
@@ -392,6 +600,30 @@ function version() {
392
600
  return "0.0.0";
393
601
  }
394
602
  }
603
+ function rippleCliPackageSpec() {
604
+ const currentVersion = version();
605
+ return currentVersion === "0.0.0"
606
+ ? "@getripple/cli"
607
+ : `@getripple/cli@${currentVersion}`;
608
+ }
609
+ function shellSingleQuote(value) {
610
+ return `'${value.replace(/'/g, `'\\''`).replace(/\\/g, "/")}'`;
611
+ }
612
+ function rippleDirectRunnerHookLines() {
613
+ return [
614
+ ` ripple_direct_node=${shellSingleQuote(process.execPath)}`,
615
+ ` ripple_direct_cli=${shellSingleQuote(__filename)}`,
616
+ ` if [ -x "./node_modules/.bin/ripple" ]; then`,
617
+ ` "./node_modules/.bin/ripple" "$@"`,
618
+ ` elif [ -f "$ripple_direct_cli" ] && [ -f "$ripple_direct_node" ]; then`,
619
+ ` "$ripple_direct_node" "$ripple_direct_cli" "$@"`,
620
+ ` elif command -v ripple >/dev/null 2>&1; then`,
621
+ ` ripple "$@"`,
622
+ ` else`,
623
+ ` npx -y ${rippleCliPackageSpec()} "$@"`,
624
+ ` fi`,
625
+ ];
626
+ }
395
627
  const GITHUB_ACTIONS_WORKFLOW_PATH = core_1.RIPPLE_CI_WORKFLOW_PATH;
396
628
  function githubActionsWorkflow() {
397
629
  return [
@@ -418,7 +650,7 @@ function githubActionsWorkflow() {
418
650
  " with:",
419
651
  " node-version: 20",
420
652
  " - name: Ripple CI",
421
- " run: npx -y @getripple/cli@latest ci --base origin/${{ github.base_ref }} --intent latest --github-annotations",
653
+ ` run: npx -y ${rippleCliPackageSpec()} ci --base origin/\${{ github.base_ref }} --github-annotations`,
422
654
  "",
423
655
  ].join("\n");
424
656
  }
@@ -434,7 +666,7 @@ function defaultInitNextSteps(readiness) {
434
666
  ...(readiness?.nextSteps ?? []),
435
667
  "Run ripple plan --file <file> --task \"<task>\" --mode file --agent --save.",
436
668
  "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.",
669
+ "Commit .ripple/policy.json, approvals when needed, and .github/workflows/ripple.yml. Keep local intents out of PRs.",
438
670
  ]);
439
671
  }
440
672
  function uniqueLines(lines) {
@@ -488,7 +720,7 @@ function intentLoadFailureMessage(intentRef, error) {
488
720
  const detail = error instanceof Error ? error.message : String(error);
489
721
  return [
490
722
  `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.",
723
+ "Run ripple plan --save for local intent-based checks, or pass a valid --intent path. Do not commit local latest intents into PRs.",
492
724
  detail,
493
725
  ].join(" ");
494
726
  }
@@ -534,6 +766,149 @@ function writeGithubAuditStepSummary(audit) {
534
766
  console.error(`Ripple CLI warning: Could not write GitHub step summary: ${message}`);
535
767
  }
536
768
  }
769
+ function writeGithubPolicyAuditStepSummary(summary, policySync) {
770
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY?.trim();
771
+ if (!summaryPath) {
772
+ return;
773
+ }
774
+ try {
775
+ fs.appendFileSync(summaryPath, buildGithubPolicyAuditStepSummary(summary, policySync), "utf8");
776
+ }
777
+ catch (err) {
778
+ const message = err instanceof Error ? err.message : String(err);
779
+ console.error(`Ripple CLI warning: Could not write GitHub step summary: ${message}`);
780
+ }
781
+ }
782
+ function pushMarkdownList(lines, title, items, limit) {
783
+ lines.push(`#### ${title}`);
784
+ if (items.length === 0) {
785
+ lines.push("- none");
786
+ return;
787
+ }
788
+ items.slice(0, limit).forEach((item) => lines.push(`- ${item}`));
789
+ if (items.length > limit) {
790
+ lines.push(`- ...and ${items.length - limit} more`);
791
+ }
792
+ }
793
+ function appendGithubReviewPacket(lines, packet) {
794
+ if (!packet) {
795
+ return;
796
+ }
797
+ lines.push("### Review packet", "");
798
+ lines.push(`- Protocol: ${packet.protocol} v${packet.version}`);
799
+ lines.push(`- Task: ${packet.originalTask}`);
800
+ lines.push(`- Mode: ${packet.mode}`);
801
+ lines.push(`- Declared scope: ${packet.declaredScope.controlMode} ${packet.declaredScope.targetFile}`);
802
+ lines.push(`- Human gate: ${packet.declaredScope.humanGate}`);
803
+ lines.push(`- Boundary risk: ${packet.declaredScope.boundaryRisk}`);
804
+ lines.push(`- Tests run: ${packet.verification.testsRun}`);
805
+ if (packet.verification.evidence.length > 0) {
806
+ lines.push(`- Verification status: ${verificationEvidenceStatusLabel(packet.verification.evidence)}`);
807
+ }
808
+ lines.push(`- Decision: ${packet.decision.verdict}`);
809
+ lines.push(`- Can continue: ${packet.decision.canContinue}`);
810
+ lines.push(`- Must stop: ${packet.decision.mustStop}`);
811
+ lines.push(`- Needs human: ${packet.decision.needsHuman}`);
812
+ lines.push(`- Next required action: ${packet.decision.nextRequiredAction}`);
813
+ lines.push("");
814
+ pushMarkdownList(lines, "Allowed files", packet.declaredScope.allowedFiles, 12);
815
+ lines.push("");
816
+ pushMarkdownList(lines, "Allowed symbols", packet.declaredScope.allowedSymbols, 12);
817
+ lines.push("");
818
+ pushMarkdownList(lines, "Actual changed files", packet.actualChanges.changedFiles, 20);
819
+ lines.push("");
820
+ pushMarkdownList(lines, "Changed symbols", packet.actualChanges.changedSymbols, 16);
821
+ lines.push("");
822
+ pushMarkdownList(lines, "Outside boundary files", packet.scopeFindings.outsideBoundaryFiles, 20);
823
+ lines.push("");
824
+ pushMarkdownList(lines, "Outside boundary symbols", packet.scopeFindings.outsideBoundarySymbols, 16);
825
+ lines.push("");
826
+ pushMarkdownList(lines, "Contract changes to review", uniqueItems([
827
+ ...packet.scopeFindings.protectedContractChanges,
828
+ ...packet.scopeFindings.unplannedContractChanges,
829
+ ]), 16);
830
+ lines.push("");
831
+ pushMarkdownList(lines, "Verification expected", packet.verification.expectedCommands, 20);
832
+ lines.push("");
833
+ pushMarkdownList(lines, "Verification evidence", packet.verification.evidence.map(formatVerificationEvidence), 20);
834
+ lines.push("");
835
+ lines.push("#### Verification note");
836
+ lines.push(`- ${packet.verification.note}`);
837
+ lines.push("");
838
+ pushMarkdownList(lines, "Reviewer notes", packet.reviewerNotes, 12);
839
+ lines.push("");
840
+ }
841
+ function buildGithubPolicyAuditStepSummary(summary, policySync) {
842
+ const pushList = (lines, title, items, limit) => {
843
+ lines.push(`#### ${title}`);
844
+ if (items.length === 0) {
845
+ lines.push("- none");
846
+ return;
847
+ }
848
+ items.slice(0, limit).forEach((item) => lines.push(`- ${item}`));
849
+ if (items.length > limit) {
850
+ lines.push(`- ...and ${items.length - limit} more`);
851
+ }
852
+ };
853
+ const lines = [
854
+ "## Ripple architecture gate",
855
+ "",
856
+ "Status: audit",
857
+ "Mode: policy-only",
858
+ "Blocking: false",
859
+ "Intent: none (local intents are not required in CI audit mode)",
860
+ `Checked files: ${summary.checkedFiles}`,
861
+ `Highest risk: ${summary.highestRisk}`,
862
+ `Requires attention: ${summary.requiresAttention}`,
863
+ "",
864
+ ];
865
+ if (summary.baseRef) {
866
+ lines.splice(5, 0, `Base ref: ${summary.baseRef}`);
867
+ }
868
+ if (summary.files.length > 0) {
869
+ lines.push("### Changed files", "");
870
+ summary.files.slice(0, 20).forEach((file) => {
871
+ lines.push(`- ${file.file} (${file.modificationRisk}, importers: ${file.importerCount})`);
872
+ });
873
+ if (summary.files.length > 20) {
874
+ lines.push(`- ...and ${summary.files.length - 20} more`);
875
+ }
876
+ lines.push("");
877
+ }
878
+ if (policySync) {
879
+ lines.push("### Policy sync", "");
880
+ lines.push(`Status: ${policySync.status}`);
881
+ if (policySync.missingRules.length > 0) {
882
+ policySync.missingRules.slice(0, 12).forEach((rule) => {
883
+ lines.push(`- ${rule.paths.join(", ")} (risk: ${rule.risk ?? "medium"})`);
884
+ });
885
+ if (policySync.missingRules.length > 12) {
886
+ lines.push(`- ...and ${policySync.missingRules.length - 12} more`);
887
+ }
888
+ }
889
+ else {
890
+ lines.push("- up to date");
891
+ }
892
+ lines.push("");
893
+ }
894
+ lines.push("### Agent actions", "");
895
+ pushList(lines, "Trusted findings", summary.agentActions.trustedFindings, 12);
896
+ lines.push("");
897
+ pushList(lines, "Verify before merge", summary.agentActions.verifyBeforeCommit, 12);
898
+ lines.push("");
899
+ pushList(lines, "Manual review recommended", summary.agentActions.manualReviewRequired, 12);
900
+ lines.push("");
901
+ const verificationTargets = uniqueItems(summary.files.flatMap((file) => file.verificationTargets));
902
+ if (verificationTargets.length > 0) {
903
+ lines.push("### Verify", "");
904
+ verificationTargets.slice(0, 20).forEach((target) => lines.push(`- ${target}`));
905
+ if (verificationTargets.length > 20) {
906
+ lines.push(`- ...and ${verificationTargets.length - 20} more`);
907
+ }
908
+ lines.push("");
909
+ }
910
+ return `${lines.join("\n")}\n`;
911
+ }
537
912
  function buildGithubStepSummary(input) {
538
913
  const { summary, intentLoadError } = input;
539
914
  const validation = summary.intentValidation;
@@ -588,6 +963,7 @@ function buildGithubStepSummary(input) {
588
963
  else {
589
964
  lines.push("### Intent", "", "- No saved change intent was provided.", `- Next required phase: ${nextRequiredPhase}`, `- Next required action: ${nextRequiredAction}`, "");
590
965
  }
966
+ appendGithubReviewPacket(lines, summary.reviewPacket);
591
967
  const blockingReasons = validation?.blockingReasons ?? [];
592
968
  if (blockingReasons.length > 0) {
593
969
  lines.push("### Blocking reasons", "");
@@ -681,6 +1057,7 @@ function buildGithubAuditStepSummary(audit) {
681
1057
  }
682
1058
  pushList(lines, "Approval why", audit.approvalStatus.why, 8);
683
1059
  lines.push("");
1060
+ appendGithubReviewPacket(lines, audit.reviewPacket);
684
1061
  if (audit.blockingReasons.length > 0) {
685
1062
  lines.push("### Blocking reasons", "");
686
1063
  audit.blockingReasons.forEach((reason) => lines.push(`- ${reason}`));
@@ -763,13 +1140,13 @@ function printGithubCheckAnnotations(summary) {
763
1140
  return;
764
1141
  }
765
1142
  if (validation.policyDrift.status === "changed") {
766
- printGithubErrorAnnotation({
1143
+ printGithubWarningAnnotation({
767
1144
  file: validation.targetFile,
768
1145
  title: "Ripple policy drift",
769
1146
  message: validation.policyDrift.summary,
770
1147
  });
771
1148
  validation.policyDrift.changedFields.slice(0, 8).forEach((field) => {
772
- printGithubErrorAnnotation({
1149
+ printGithubWarningAnnotation({
773
1150
  file: validation.targetFile,
774
1151
  title: "Ripple policy drift",
775
1152
  message: field,
@@ -777,13 +1154,13 @@ function printGithubCheckAnnotations(summary) {
777
1154
  });
778
1155
  }
779
1156
  if (validation.readinessDrift.status === "weakened") {
780
- printGithubErrorAnnotation({
1157
+ printGithubWarningAnnotation({
781
1158
  file: validation.targetFile,
782
1159
  title: "Ripple readiness drift",
783
1160
  message: validation.readinessDrift.summary,
784
1161
  });
785
1162
  validation.readinessDrift.weakenedFields.slice(0, 8).forEach((field) => {
786
- printGithubErrorAnnotation({
1163
+ printGithubWarningAnnotation({
787
1164
  file: validation.targetFile,
788
1165
  title: "Ripple readiness drift",
789
1166
  message: `Weakened readiness field: ${field}`,
@@ -791,7 +1168,7 @@ function printGithubCheckAnnotations(summary) {
791
1168
  });
792
1169
  }
793
1170
  validation.unplannedFiles.forEach((file) => {
794
- printGithubErrorAnnotation({
1171
+ printGithubWarningAnnotation({
795
1172
  file,
796
1173
  title: "Ripple intent drift",
797
1174
  message: `Unplanned file changed: ${file}`,
@@ -799,7 +1176,7 @@ function printGithubCheckAnnotations(summary) {
799
1176
  });
800
1177
  validation.unplannedSymbols.forEach((symbol) => {
801
1178
  const file = symbolFile(symbol);
802
- printGithubErrorAnnotation({
1179
+ printGithubWarningAnnotation({
803
1180
  file,
804
1181
  title: "Ripple symbol drift",
805
1182
  message: `Unplanned symbol changed: ${symbol}`,
@@ -853,24 +1230,58 @@ function printGithubCheckAnnotations(summary) {
853
1230
  });
854
1231
  });
855
1232
  summary.agentActions.manualReviewRequired.slice(0, 12).forEach((action) => {
856
- printGithubErrorAnnotation({
1233
+ printGithubWarningAnnotation({
857
1234
  file: actionFile(action),
858
1235
  title: "Ripple manual review required",
859
1236
  message: action,
860
1237
  });
861
1238
  });
862
1239
  }
1240
+ function printGithubPolicyAuditAnnotations(summary, policySync) {
1241
+ if (summary.requiresAttention) {
1242
+ printGithubWarningAnnotation({
1243
+ title: "Ripple policy audit",
1244
+ message: `Policy audit detected ${summary.highestRisk} risk changes. Ripple is in audit mode, so this does not block merge. Ensure human review before merging.`,
1245
+ });
1246
+ }
1247
+ else {
1248
+ printGithubNoticeAnnotation({
1249
+ title: "Ripple policy audit",
1250
+ message: "Policy audit completed without high-risk findings.",
1251
+ });
1252
+ }
1253
+ if (policySync && policySync.missingRules.length > 0) {
1254
+ printGithubWarningAnnotation({
1255
+ title: "Ripple policy rot",
1256
+ message: `Policy may be missing ${policySync.missingRules.length} risky repo surface(s). Run ripple policy sync and review .ripple/policy.json.`,
1257
+ });
1258
+ }
1259
+ summary.agentActions.verifyBeforeCommit.slice(0, 12).forEach((action) => {
1260
+ printGithubWarningAnnotation({
1261
+ file: actionFile(action),
1262
+ title: "Ripple verify before merge",
1263
+ message: action,
1264
+ });
1265
+ });
1266
+ summary.agentActions.manualReviewRequired.slice(0, 12).forEach((action) => {
1267
+ printGithubWarningAnnotation({
1268
+ file: actionFile(action),
1269
+ title: "Ripple manual review recommended",
1270
+ message: action,
1271
+ });
1272
+ });
1273
+ }
863
1274
  function printGithubAuditAnnotations(audit) {
864
1275
  const gate = (0, core_1.buildRippleGateSummary)(audit);
865
1276
  if (audit.status !== "pass") {
866
- printGithubErrorAnnotation({
1277
+ printGithubWarningAnnotation({
867
1278
  file: audit.intent.targetFile,
868
1279
  title: "Ripple gate closed",
869
1280
  message: `${gate.status}/${gate.decision}: next=${gate.nextRequiredPhase}. ${gate.nextRequiredAction}`,
870
1281
  });
871
1282
  }
872
1283
  if (audit.approvalStatus.required && !audit.approvalStatus.approved) {
873
- printGithubErrorAnnotation({
1284
+ printGithubWarningAnnotation({
874
1285
  file: audit.intent.targetFile,
875
1286
  title: "Ripple approval required",
876
1287
  message: audit.approvalStatus.summary,
@@ -930,6 +1341,28 @@ function relativeToWorkspace(workspaceRoot, filePath) {
930
1341
  function normalizeProjectPath(filePath) {
931
1342
  return filePath.replace(/\\/g, "/").replace(/^\.\/+/, "");
932
1343
  }
1344
+ function resolveCliIntentPath(workspaceRoot, intentRef) {
1345
+ const normalized = intentRef.trim();
1346
+ if (normalized.length === 0 || normalized === "latest") {
1347
+ return (0, core_1.defaultChangeIntentPath)(workspaceRoot);
1348
+ }
1349
+ if (path.isAbsolute(normalized)) {
1350
+ return normalized;
1351
+ }
1352
+ if (normalized.endsWith(".json") || normalized.includes("/") || normalized.includes("\\")) {
1353
+ return path.resolve(workspaceRoot, normalized);
1354
+ }
1355
+ return path.join(workspaceRoot, ".ripple", "intents", `${normalized}.json`);
1356
+ }
1357
+ function isActiveChangeIntentFile(filePath) {
1358
+ try {
1359
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
1360
+ return parsed.protocol === "ripple-change-intent";
1361
+ }
1362
+ catch {
1363
+ return false;
1364
+ }
1365
+ }
933
1366
  function formatEventLine(event) {
934
1367
  const target = event.target ? ` -> ${event.target}` : "";
935
1368
  const details = [
@@ -1021,6 +1454,15 @@ function printDoctorSummary(summary) {
1021
1454
  summary.enforcement.gaps.forEach((gap) => console.log(` - ${gap}`));
1022
1455
  }
1023
1456
  console.log("");
1457
+ console.log("Policy sync:");
1458
+ console.log(` status: ${summary.policySync.status}`);
1459
+ if (summary.policySync.missingRules.length > 0) {
1460
+ console.log(" missing coverage:");
1461
+ summary.policySync.missingRules.slice(0, 12).forEach((rule) => {
1462
+ console.log(` - ${rule.paths.join(", ")} risk=${rule.risk ?? "medium"}`);
1463
+ });
1464
+ }
1465
+ console.log("");
1024
1466
  console.log("Next steps:");
1025
1467
  summary.nextSteps.forEach((step) => console.log(` - ${step}`));
1026
1468
  }
@@ -1032,6 +1474,21 @@ function printInitSummary(summary) {
1032
1474
  summary.files.forEach((file) => {
1033
1475
  console.log(` - ${file.path}: ${file.status}`);
1034
1476
  });
1477
+ if (summary.agentSetup) {
1478
+ console.log("");
1479
+ console.log("Agent setup files:");
1480
+ summary.agentSetup.files.forEach((file) => {
1481
+ console.log(` - ${file.path}: ${file.status}`);
1482
+ });
1483
+ }
1484
+ if (summary.hooks) {
1485
+ console.log("");
1486
+ console.log("Git hooks:");
1487
+ console.log(` - ${summary.hooks.path}: ${summary.hooks.preCommitAction ?? summary.hooks.status}`);
1488
+ if (summary.hooks.postCommitPath) {
1489
+ console.log(` - ${summary.hooks.postCommitPath}: ${summary.hooks.postCommitAction ?? summary.hooks.status}`);
1490
+ }
1491
+ }
1035
1492
  if (summary.readiness) {
1036
1493
  console.log("");
1037
1494
  console.log("Readiness after init:");
@@ -1067,6 +1524,13 @@ function printAgentDoctorSummary(summary) {
1067
1524
  console.log(`git_ignore: ${summary.checks.gitIgnore.ok ? "ok" : "missing"} - ${summary.checks.gitIgnore.detail}`);
1068
1525
  console.log(`ci_workflow: ${summary.checks.ciWorkflow.ok ? "ok" : "missing"} - ${summary.checks.ciWorkflow.detail}`);
1069
1526
  console.log(`latest_intent: ${summary.checks.latestIntent.ok ? "ok" : "missing"} - ${summary.checks.latestIntent.detail}`);
1527
+ console.log(`policy_sync: ${summary.policySync.status}`);
1528
+ if (summary.policySync.missingRules.length > 0) {
1529
+ console.log("policy_sync_missing_rules:");
1530
+ summary.policySync.missingRules.slice(0, 12).forEach((rule) => {
1531
+ console.log(`- ${rule.paths.join(", ")} risk=${rule.risk ?? "medium"}`);
1532
+ });
1533
+ }
1070
1534
  console.log("");
1071
1535
  printAgentList("why", summary.why);
1072
1536
  console.log("");
@@ -1492,6 +1956,10 @@ function printAgentStagedCheckSummary(summary) {
1492
1956
  console.log(`checked_js_ts_files: ${summary.stagedFiles}`);
1493
1957
  console.log(`checked_files: ${summary.checkedFiles}`);
1494
1958
  printAgentIntentValidation(summary.intentValidation);
1959
+ if (summary.reviewPacket) {
1960
+ console.log("");
1961
+ printAgentReviewPacket(summary.reviewPacket);
1962
+ }
1495
1963
  console.log("");
1496
1964
  printAgentList("trusted_findings", summary.agentActions.trustedFindings);
1497
1965
  console.log("");
@@ -1760,6 +2228,8 @@ function printAgentAuditSummary(summary) {
1760
2228
  console.log(`repair_status: ${summary.repairPlan.status}`);
1761
2229
  console.log(`recommended_action: ${summary.recommendedAction}`);
1762
2230
  console.log("");
2231
+ printAgentReviewPacket(summary.reviewPacket);
2232
+ console.log("");
1763
2233
  printAgentHandoffBlock("handoff", summary.handoff);
1764
2234
  if (summary.approvalStatus.approval) {
1765
2235
  console.log(`approved_by: ${summary.approvalStatus.approval.approvedBy}`);
@@ -1817,6 +2287,8 @@ function printAgentGateSummary(summary) {
1817
2287
  console.log(`risk_score: ${summary.risk.score}`);
1818
2288
  console.log(`risk_summary: ${summary.risk.summary}`);
1819
2289
  console.log("");
2290
+ printAgentReviewPacket(summary.reviewPacket);
2291
+ console.log("");
1820
2292
  printAgentList("risk_reasons", compactGateRiskReasons(summary));
1821
2293
  console.log("");
1822
2294
  printAgentList("risk_evidence", compactGateRiskEvidence(summary));
@@ -1845,15 +2317,43 @@ function printAgentGateSummary(summary) {
1845
2317
  console.log("");
1846
2318
  printAgentList("commands_verify", summary.commands.verify);
1847
2319
  }
1848
- function printGateSummary(summary) {
1849
- const statusLabel = summary.canContinue ? "CONTINUE" : "STOP";
1850
- console.log(`Ripple gate: ${statusLabel}`);
1851
- console.log(gateHeadline(summary));
2320
+ function printAgentGateIntentBlock(summary) {
2321
+ console.log("RIPPLE_GATE_INTENT_BLOCK");
2322
+ console.log(`protocol: ${summary.protocol}`);
2323
+ console.log(`status: ${summary.status}`);
2324
+ console.log(`decision: ${summary.decision}`);
2325
+ console.log(`can_continue: ${summary.canContinue}`);
2326
+ console.log(`must_stop: ${summary.mustStop}`);
2327
+ console.log(`needs_human: ${summary.needsHuman}`);
2328
+ console.log(`next_required_phase: ${summary.nextRequiredPhase}`);
2329
+ console.log(`next_required_action: ${summary.nextRequiredAction}`);
2330
+ console.log(`summary: ${summary.summary}`);
2331
+ console.log(`mode: ${summary.mode}`);
2332
+ if (summary.baseRef) {
2333
+ console.log(`base_ref: ${summary.baseRef}`);
2334
+ }
2335
+ console.log(`intent_ref: ${summary.intentRef}`);
2336
+ console.log(`intent_state: ${summary.intentState}`);
2337
+ console.log(`intent_error: ${summary.intentLoadError}`);
2338
+ console.log("");
2339
+ printAgentList("why", summary.why);
2340
+ console.log("");
2341
+ printAgentList("fix_now", summary.fixNow);
2342
+ console.log("");
2343
+ printAgentList("ask_human", summary.askHuman);
2344
+ console.log("");
2345
+ printAgentList("commands_plan", summary.commands.plan);
2346
+ }
2347
+ function printGateSummary(summary) {
2348
+ const statusLabel = summary.canContinue ? "CONTINUE" : "STOP";
2349
+ console.log(`Ripple gate: ${statusLabel}`);
2350
+ console.log(gateHeadline(summary));
1852
2351
  console.log("");
1853
2352
  console.log(`Decision: ${summary.decision}`);
1854
2353
  console.log(`Can continue: ${formatYesNo(summary.canContinue)}`);
1855
2354
  console.log(`Must stop: ${formatYesNo(summary.mustStop)}`);
1856
- printGateRiskSummary(summary);
2355
+ console.log("");
2356
+ printReviewPacketSummary(summary.reviewPacket);
1857
2357
  console.log("");
1858
2358
  console.log("Intent:");
1859
2359
  console.log(` Task: ${summary.intent.task}`);
@@ -1865,6 +2365,7 @@ function printGateSummary(summary) {
1865
2365
  printHumanList("Allowed:", gateAllowedItems(summary));
1866
2366
  const outsideBoundary = gateChangedOutsideItems(summary);
1867
2367
  printHumanList(outsideBoundary.length > 0 ? "Changed outside boundary:" : "Changed files:", outsideBoundary.length > 0 ? outsideBoundary : summary.changedFiles);
2368
+ console.log("");
1868
2369
  printHumanList("Why:", compactGateReasons(summary));
1869
2370
  printHumanList("Fix now:", compactGateFixes(summary));
1870
2371
  if (summary.canContinue) {
@@ -1873,6 +2374,28 @@ function printGateSummary(summary) {
1873
2374
  else {
1874
2375
  printHumanList("Commands:", compactGateCommands(summary));
1875
2376
  }
2377
+ printGateRiskSummary(summary);
2378
+ }
2379
+ function printGateIntentBlock(summary) {
2380
+ console.log("Ripple gate: STOP");
2381
+ console.log("Agent must stop before continuing.");
2382
+ console.log("");
2383
+ console.log(`Decision: ${summary.decision}`);
2384
+ console.log(`Can continue: ${formatYesNo(summary.canContinue)}`);
2385
+ console.log(`Must stop: ${formatYesNo(summary.mustStop)}`);
2386
+ console.log(`Needs human: ${formatYesNo(summary.needsHuman)}`);
2387
+ console.log(`Next required phase: ${summary.nextRequiredPhase}`);
2388
+ console.log(`Next required action: ${summary.nextRequiredAction}`);
2389
+ console.log("");
2390
+ console.log("Intent:");
2391
+ console.log(` Ref: ${summary.intentRef}`);
2392
+ console.log(` State: ${summary.intentState}`);
2393
+ console.log(` Error: ${summary.intentLoadError}`);
2394
+ console.log("");
2395
+ printHumanList("Why:", summary.why);
2396
+ printHumanList("Fix now:", summary.fixNow);
2397
+ printHumanList("Ask human:", summary.askHuman);
2398
+ printHumanList("Commands:", summary.commands.plan);
1876
2399
  }
1877
2400
  function printGateRiskSummary(summary) {
1878
2401
  console.log("");
@@ -1967,6 +2490,8 @@ function printAuditSummary(summary) {
1967
2490
  console.log(` next required action: ${gate.nextRequiredAction}`);
1968
2491
  console.log(` summary: ${gate.summary}`);
1969
2492
  console.log("");
2493
+ printReviewPacketSummary(summary.reviewPacket);
2494
+ console.log("");
1970
2495
  console.log("Approval:");
1971
2496
  console.log(` status: ${summary.approvalStatus.status}`);
1972
2497
  console.log(` decision: ${summary.approvalStatus.decision}`);
@@ -2125,9 +2650,83 @@ function printHumanList(title, items) {
2125
2650
  }
2126
2651
  items.forEach((item) => console.log(` - ${item}`));
2127
2652
  }
2653
+ function formatVerificationEvidence(evidence) {
2654
+ const note = evidence.note ? ` note=${evidence.note}` : "";
2655
+ const exitCode = typeof evidence.exitCode === "number" ? ` exitCode=${evidence.exitCode}` : "";
2656
+ const duration = typeof evidence.durationMs === "number" ? ` durationMs=${evidence.durationMs}` : "";
2657
+ const files = evidence.changedFiles
2658
+ ? ` files=${evidence.changedFiles.length > 0 ? evidence.changedFiles.join(",") : "none"}`
2659
+ : "";
2660
+ const mode = evidence.changeMode ? ` mode=${evidence.changeMode}` : "";
2661
+ const fingerprint = evidence.changeFingerprint
2662
+ ? ` fingerprint=${evidence.changeFingerprint.slice(0, 12)}`
2663
+ : "";
2664
+ return `${evidence.status}: ${evidence.command} (${evidence.source}${exitCode}${duration}${files}${mode}${fingerprint} ${evidence.recordedAt})${note}`;
2665
+ }
2666
+ function verificationEvidenceStatusLabel(evidence) {
2667
+ if (evidence.length === 0) {
2668
+ return "none";
2669
+ }
2670
+ if (evidence.some((item) => item.status === "failed")) {
2671
+ return "failed";
2672
+ }
2673
+ if (evidence.some((item) => item.status === "unknown")) {
2674
+ return "unknown";
2675
+ }
2676
+ if (evidence.some((item) => item.status === "skipped")) {
2677
+ return "skipped";
2678
+ }
2679
+ return "passed";
2680
+ }
2681
+ function printReviewPacketSummary(packet) {
2682
+ console.log("Review packet:");
2683
+ console.log(` protocol: ${packet.protocol}`);
2684
+ console.log(` task: ${packet.originalTask}`);
2685
+ console.log(` declared scope: ${packet.declaredScope.controlMode} ${packet.declaredScope.targetFile}`);
2686
+ console.log(` human gate: ${packet.declaredScope.humanGate}`);
2687
+ console.log(` boundary risk: ${packet.declaredScope.boundaryRisk}`);
2688
+ printHumanList(" changed files", packet.actualChanges.changedFiles);
2689
+ printHumanList(" outside boundary files", packet.scopeFindings.outsideBoundaryFiles);
2690
+ printHumanList(" outside boundary symbols", packet.scopeFindings.outsideBoundarySymbols);
2691
+ printHumanList(" verification expected", packet.verification.expectedCommands);
2692
+ console.log(` tests run: ${packet.verification.testsRun}`);
2693
+ if (packet.verification.evidence.length > 0) {
2694
+ console.log(` verification status: ${verificationEvidenceStatusLabel(packet.verification.evidence)}`);
2695
+ }
2696
+ printHumanList(" verification evidence", packet.verification.evidence.map(formatVerificationEvidence));
2697
+ console.log(` can continue: ${formatYesNo(packet.decision.canContinue)}`);
2698
+ console.log(` must stop: ${formatYesNo(packet.decision.mustStop)}`);
2699
+ console.log(` needs human: ${formatYesNo(packet.decision.needsHuman)}`);
2700
+ printHumanList(" reviewer notes", packet.reviewerNotes);
2701
+ }
2702
+ function printAgentReviewPacket(packet) {
2703
+ console.log(`review_packet_protocol: ${packet.protocol}`);
2704
+ console.log(`review_packet_version: ${packet.version}`);
2705
+ console.log(`review_packet_task: ${packet.originalTask}`);
2706
+ console.log(`review_packet_scope: ${packet.declaredScope.controlMode} ${packet.declaredScope.targetFile}`);
2707
+ console.log(`review_packet_human_gate: ${packet.declaredScope.humanGate}`);
2708
+ console.log(`review_packet_boundary_risk: ${packet.declaredScope.boundaryRisk}`);
2709
+ console.log(`review_packet_tests_run: ${packet.verification.testsRun}`);
2710
+ console.log(`review_packet_verification_status: ${verificationEvidenceStatusLabel(packet.verification.evidence)}`);
2711
+ console.log(`review_packet_can_continue: ${packet.decision.canContinue}`);
2712
+ console.log(`review_packet_must_stop: ${packet.decision.mustStop}`);
2713
+ console.log(`review_packet_needs_human: ${packet.decision.needsHuman}`);
2714
+ console.log("");
2715
+ printAgentList("review_packet_changed_files", packet.actualChanges.changedFiles);
2716
+ console.log("");
2717
+ printAgentList("review_packet_outside_boundary_files", packet.scopeFindings.outsideBoundaryFiles);
2718
+ console.log("");
2719
+ printAgentList("review_packet_outside_boundary_symbols", packet.scopeFindings.outsideBoundarySymbols);
2720
+ console.log("");
2721
+ printAgentList("review_packet_verification_expected", packet.verification.expectedCommands);
2722
+ console.log("");
2723
+ printAgentList("review_packet_verification_reported", packet.verification.evidence.map(formatVerificationEvidence));
2724
+ console.log("");
2725
+ printAgentList("review_packet_reviewer_notes", packet.reviewerNotes);
2726
+ }
2128
2727
  function printStagedCheckSummary(summary) {
2129
2728
  const adapter = summary.adapterSupport.primaryAdapter;
2130
- console.log(summary.mode === "changed" ? "Ripple changed-files check" : "Ripple staged check");
2729
+ console.log(summary.mode === "changed" ? "Ripple changed-files check" : summary.mode === "worktree" ? "Ripple worktree check" : "Ripple staged check");
2131
2730
  console.log(`Workspace: ${summary.workspace}`);
2132
2731
  console.log(`Mode: ${summary.mode}`);
2133
2732
  if (summary.baseRef) {
@@ -2151,6 +2750,10 @@ function printStagedCheckSummary(summary) {
2151
2750
  printReadinessDriftSummary("Readiness drift:", summary.intentValidation.readinessDrift);
2152
2751
  console.log(`Planned scope: ${summary.intentValidation.plannedScope}`);
2153
2752
  }
2753
+ if (summary.reviewPacket) {
2754
+ console.log("");
2755
+ printReviewPacketSummary(summary.reviewPacket);
2756
+ }
2154
2757
  if (summary.skippedFiles.length > 0) {
2155
2758
  console.log(`Skipped non-source files: ${summary.skippedFiles.length}`);
2156
2759
  }
@@ -2163,7 +2766,9 @@ function printStagedCheckSummary(summary) {
2163
2766
  console.log("");
2164
2767
  console.log(summary.mode === "changed"
2165
2768
  ? "No changed JS/TS files found."
2166
- : "No staged JS/TS files found.");
2769
+ : summary.mode === "worktree"
2770
+ ? "No worktree JS/TS changes found."
2771
+ : "No staged JS/TS files found.");
2167
2772
  return;
2168
2773
  }
2169
2774
  console.log("");
@@ -2374,7 +2979,7 @@ async function planCommand(options) {
2374
2979
  const output = savedIntent
2375
2980
  ? {
2376
2981
  ...summary,
2377
- policyExplanation,
2982
+ policyExplanation: savedIntent.intent.policyExplanation,
2378
2983
  changeIntent: savedIntent.intent,
2379
2984
  changeIntentPath: savedIntent.path,
2380
2985
  }
@@ -2419,6 +3024,140 @@ function currentPolicyExplanationForIntent(workspaceRoot, intent) {
2419
3024
  function currentReadinessSnapshotForEngine(workspaceRoot, engine) {
2420
3025
  return (0, core_1.buildChangeIntentReadinessSnapshot)((0, core_1.buildRippleReadinessSummary)(workspaceRoot, engine));
2421
3026
  }
3027
+ function intentSnapshot(intent) {
3028
+ return {
3029
+ id: intent.id,
3030
+ createdAt: intent.createdAt,
3031
+ task: intent.task,
3032
+ targetFile: intent.targetFile,
3033
+ controlMode: intent.controlMode,
3034
+ humanGate: intent.humanGate,
3035
+ boundaryRisk: intent.boundaryRisk,
3036
+ };
3037
+ }
3038
+ function intentCommand(action, options) {
3039
+ if (!action || action === "status") {
3040
+ intentStatusCommand(options);
3041
+ return;
3042
+ }
3043
+ if (action === "close") {
3044
+ closeIntentCommand(options);
3045
+ return;
3046
+ }
3047
+ throw new Error("Usage: ripple intent status [--intent latest|path] or ripple intent close --reason <text> [--intent latest|path]");
3048
+ }
3049
+ function intentStatusCommand(options) {
3050
+ const workspaceRoot = resolveWorkspaceRoot(".");
3051
+ const intentRef = options.intent ?? "latest";
3052
+ const intentPath = resolveCliIntentPath(workspaceRoot, intentRef);
3053
+ const exists = fs.existsSync(intentPath);
3054
+ const active = exists && isActiveChangeIntentFile(intentPath);
3055
+ const output = {
3056
+ protocol: "ripple-intent-status",
3057
+ version: 1,
3058
+ workspace: workspaceRoot,
3059
+ intentRef,
3060
+ intentPath: relativeToWorkspace(workspaceRoot, intentPath),
3061
+ exists,
3062
+ active,
3063
+ nextSteps: active
3064
+ ? [
3065
+ "Run ripple gate --intent latest before continuing.",
3066
+ "Run ripple intent close --reason \"<why this boundary is done>\" when the task boundary is complete or intentionally replaced.",
3067
+ ]
3068
+ : [
3069
+ "Run ripple plan --file <file> --task \"<task>\" --agent --save before an agent edits.",
3070
+ ],
3071
+ };
3072
+ if (active) {
3073
+ output.intent = intentSnapshot((0, core_1.loadChangeIntent)(workspaceRoot, intentRef));
3074
+ }
3075
+ if (options.json) {
3076
+ printJson(output);
3077
+ return;
3078
+ }
3079
+ printIntentStatus(output);
3080
+ }
3081
+ function closeIntentCommand(options) {
3082
+ const workspaceRoot = resolveWorkspaceRoot(".");
3083
+ const intentRef = options.intent ?? "latest";
3084
+ const intentPath = resolveCliIntentPath(workspaceRoot, intentRef);
3085
+ if (!fs.existsSync(intentPath) || !isActiveChangeIntentFile(intentPath)) {
3086
+ throw new Error(`No active Ripple intent exists at ${relativeToWorkspace(workspaceRoot, intentPath)}.`);
3087
+ }
3088
+ const reason = options.reason?.trim();
3089
+ if (!reason) {
3090
+ throw new Error("Closing an active Ripple intent requires --reason.");
3091
+ }
3092
+ const intent = (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
3093
+ const closedAt = new Date().toISOString();
3094
+ const closedBy = options.approvedBy?.trim() || "human";
3095
+ const archivePath = archivedIntentPath(workspaceRoot, intent, closedAt);
3096
+ const archive = {
3097
+ protocol: "ripple-closed-intent",
3098
+ version: 1,
3099
+ closedAt,
3100
+ closedBy,
3101
+ reason,
3102
+ originalIntentPath: relativeToWorkspace(workspaceRoot, intentPath),
3103
+ intent,
3104
+ };
3105
+ fs.mkdirSync(path.dirname(archivePath), { recursive: true });
3106
+ fs.writeFileSync(archivePath, `${JSON.stringify(archive, null, 2)}\n`, "utf8");
3107
+ fs.writeFileSync(intentPath, `${JSON.stringify(archive, null, 2)}\n`, "utf8");
3108
+ const output = {
3109
+ protocol: "ripple-intent-close",
3110
+ version: 1,
3111
+ workspace: workspaceRoot,
3112
+ intentRef,
3113
+ intentPath: relativeToWorkspace(workspaceRoot, intentPath),
3114
+ archivePath: relativeToWorkspace(workspaceRoot, archivePath),
3115
+ closedAt,
3116
+ closedBy,
3117
+ reason,
3118
+ intent: intentSnapshot(intent),
3119
+ nextSteps: [
3120
+ "Run ripple intent status to confirm no active saved boundary remains.",
3121
+ "Run ripple plan --file <file> --task \"<task>\" --agent --save to start the next agent boundary.",
3122
+ ],
3123
+ };
3124
+ if (options.json) {
3125
+ printJson(output);
3126
+ return;
3127
+ }
3128
+ printIntentClose(output);
3129
+ }
3130
+ function archivedIntentPath(workspaceRoot, intent, closedAt) {
3131
+ const timestamp = closedAt.replace(/[:.]/g, "-");
3132
+ const safeId = intent.id.replace(/[^a-zA-Z0-9._-]/g, "-");
3133
+ return path.join(workspaceRoot, ".ripple", "intents", "archive", `${timestamp}-${safeId}.json`);
3134
+ }
3135
+ function printIntentStatus(summary) {
3136
+ console.log("Ripple intent status");
3137
+ console.log(`Intent: ${summary.intentRef}`);
3138
+ console.log(`Path: ${summary.intentPath}`);
3139
+ console.log(`Active: ${formatYesNo(summary.active)}`);
3140
+ if (summary.intent) {
3141
+ console.log(`Id: ${summary.intent.id}`);
3142
+ console.log(`Task: ${summary.intent.task}`);
3143
+ console.log(`Target: ${summary.intent.targetFile}`);
3144
+ console.log(`Control mode: ${summary.intent.controlMode}`);
3145
+ console.log(`Human gate: ${summary.intent.humanGate}`);
3146
+ console.log(`Boundary risk: ${summary.intent.boundaryRisk}`);
3147
+ }
3148
+ printHumanList("Next:", summary.nextSteps);
3149
+ }
3150
+ function printIntentClose(summary) {
3151
+ console.log("Ripple intent closed");
3152
+ console.log(`Intent: ${summary.intent.id}`);
3153
+ console.log(`Task: ${summary.intent.task}`);
3154
+ console.log(`Target: ${summary.intent.targetFile}`);
3155
+ console.log(`Closed by: ${summary.closedBy}`);
3156
+ console.log(`Reason: ${summary.reason}`);
3157
+ console.log(`Archived: ${summary.archivePath}`);
3158
+ console.log(`Closed marker: ${summary.intentPath}`);
3159
+ printHumanList("Next:", summary.nextSteps);
3160
+ }
2422
3161
  function approvalStatusOutput(intent, status) {
2423
3162
  return {
2424
3163
  ...status,
@@ -2478,32 +3217,60 @@ async function buildCheckSummaryForFiles(input) {
2478
3217
  engine.dispose();
2479
3218
  }
2480
3219
  }
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>");
3220
+ function selectedChangeMode(options) {
3221
+ if (options.changed) {
3222
+ return "changed";
2484
3223
  }
2485
- if (options.staged && options.changed) {
2486
- throw new Error("Choose one check mode: --staged or --changed");
3224
+ if (options.worktree) {
3225
+ return "worktree";
3226
+ }
3227
+ return "staged";
3228
+ }
3229
+ function selectedChangeModeCount(options) {
3230
+ return [options.staged, options.changed, options.worktree].filter(Boolean).length;
3231
+ }
3232
+ function listFilesForChangeMode(workspaceRoot, mode, baseRef) {
3233
+ if (mode === "changed") {
3234
+ return (0, core_1.listGitChangedFiles)(workspaceRoot, baseRef);
3235
+ }
3236
+ if (mode === "worktree") {
3237
+ return (0, core_1.listGitWorktreeFiles)(workspaceRoot);
3238
+ }
3239
+ return (0, core_1.listGitStagedFiles)(workspaceRoot);
3240
+ }
3241
+ async function checkCommand(options) {
3242
+ if (selectedChangeModeCount(options) !== 1) {
3243
+ throw new Error("Choose one check mode: --staged, --worktree, or --changed --base <ref>");
2487
3244
  }
2488
3245
  const workspaceRoot = resolveWorkspaceRoot(".");
2489
3246
  const baseRef = options.base ?? "HEAD";
2490
- const checkFiles = options.changed
2491
- ? (0, core_1.listGitChangedFiles)(workspaceRoot, baseRef)
2492
- : (0, core_1.listGitStagedFiles)(workspaceRoot);
3247
+ const mode = selectedChangeMode(options);
3248
+ let loadedIntent;
3249
+ if (options.intent) {
3250
+ try {
3251
+ loadedIntent = (0, core_1.loadChangeIntent)(workspaceRoot, options.intent);
3252
+ }
3253
+ catch (err) {
3254
+ if (!options.strict && !options.githubAnnotations) {
3255
+ throw err;
3256
+ }
3257
+ }
3258
+ }
3259
+ const checkFiles = listFilesForChangeMode(workspaceRoot, mode, baseRef);
2493
3260
  const engine = createFastCheckEngine(workspaceRoot);
2494
3261
  try {
2495
3262
  await runWithQuietEngine(() => engine.fastCheckScan(fastCheckCandidateFiles(checkFiles)));
2496
3263
  const stagedSummary = (0, core_1.buildStagedCheckSummary)(engine, {
2497
3264
  workspaceRoot,
2498
3265
  stagedFiles: checkFiles,
2499
- mode: options.changed ? "changed" : "staged",
2500
- baseRef: options.changed ? baseRef : undefined,
3266
+ mode,
3267
+ baseRef: mode === "changed" ? baseRef : undefined,
2501
3268
  tokenBudget: options.budget,
2502
3269
  });
2503
3270
  let summary = stagedSummary;
2504
3271
  if (options.intent) {
2505
3272
  try {
2506
- const intent = (0, core_1.loadChangeIntent)(workspaceRoot, options.intent);
3273
+ const intent = loadedIntent ?? (0, core_1.loadChangeIntent)(workspaceRoot, options.intent);
2507
3274
  summary = (0, core_1.validateStagedCheckAgainstIntent)(stagedSummary, intent, {
2508
3275
  currentPolicyExplanation: currentPolicyExplanationForIntent(workspaceRoot, intent),
2509
3276
  currentReadinessSnapshot: currentReadinessSnapshotForEngine(workspaceRoot, engine),
@@ -2571,18 +3338,16 @@ async function checkCommand(options) {
2571
3338
  }
2572
3339
  }
2573
3340
  async function buildAuditFromCliOptions(options) {
2574
- if (options.staged && options.changed) {
2575
- throw new Error("Choose one gate/audit mode: --staged or --changed");
3341
+ if (selectedChangeModeCount(options) > 1) {
3342
+ throw new Error("Choose one gate/audit mode: --staged, --worktree, or --changed --base <ref>");
2576
3343
  }
2577
3344
  const workspaceRoot = resolveWorkspaceRoot(".");
2578
3345
  const intentRef = options.intent ?? "latest";
2579
- const mode = options.changed ? "changed" : "staged";
3346
+ const mode = selectedChangeMode(options);
2580
3347
  const baseRef = options.base ?? "HEAD";
2581
- const files = mode === "changed"
2582
- ? (0, core_1.listGitChangedFiles)(workspaceRoot, baseRef)
2583
- : (0, core_1.listGitStagedFiles)(workspaceRoot);
2584
3348
  const intent = (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
2585
3349
  const currentPolicyExplanation = currentPolicyExplanationForIntent(workspaceRoot, intent);
3350
+ const files = listFilesForChangeMode(workspaceRoot, mode, baseRef);
2586
3351
  return buildAuditForFiles({
2587
3352
  workspaceRoot,
2588
3353
  files,
@@ -2610,6 +3375,36 @@ async function auditCommand(options) {
2610
3375
  applyStrictExit(options.strict && strictAuditShouldFail(audit));
2611
3376
  }
2612
3377
  async function gateCommand(options) {
3378
+ if (selectedChangeModeCount(options) > 1) {
3379
+ throw new Error("Choose one gate/audit mode: --staged, --worktree, or --changed --base <ref>");
3380
+ }
3381
+ const workspaceRoot = resolveWorkspaceRoot(".");
3382
+ const intentRef = options.intent ?? "latest";
3383
+ const mode = selectedChangeMode(options);
3384
+ const baseRef = options.base ?? "HEAD";
3385
+ try {
3386
+ (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
3387
+ }
3388
+ catch (err) {
3389
+ const block = (0, core_1.buildRippleGateIntentBlockSummary)({
3390
+ workspaceRoot,
3391
+ mode,
3392
+ baseRef: mode === "changed" ? baseRef : undefined,
3393
+ intentRef,
3394
+ error: err,
3395
+ });
3396
+ if (options.json) {
3397
+ printJson(block);
3398
+ }
3399
+ else if (options.agent) {
3400
+ printAgentGateIntentBlock(block);
3401
+ }
3402
+ else {
3403
+ printGateIntentBlock(block);
3404
+ }
3405
+ applyStrictExit(true);
3406
+ return;
3407
+ }
2613
3408
  const audit = await buildAuditFromCliOptions(options);
2614
3409
  const gate = (0, core_1.buildRippleGateSummary)(audit);
2615
3410
  if (options.json) {
@@ -2626,6 +3421,9 @@ async function gateCommand(options) {
2626
3421
  function approveCommand(options) {
2627
3422
  const workspaceRoot = resolveWorkspaceRoot(".");
2628
3423
  const intentRef = options.intent ?? "latest";
3424
+ if (!options.reason || options.reason.trim().length === 0) {
3425
+ throw new Error("Approval requires --reason explaining why this boundary is approved.");
3426
+ }
2629
3427
  const intent = (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
2630
3428
  const approval = (0, core_1.recordRippleApproval)(workspaceRoot, intent, {
2631
3429
  gate: options.gate,
@@ -2642,6 +3440,171 @@ function approveCommand(options) {
2642
3440
  }
2643
3441
  printApprovalRecord(approval);
2644
3442
  }
3443
+ function executeVerificationCommand(workspaceRoot, command, note) {
3444
+ const startedAt = Date.now();
3445
+ const result = (0, child_process_1.spawnSync)(command, {
3446
+ cwd: workspaceRoot,
3447
+ shell: true,
3448
+ encoding: "utf8",
3449
+ maxBuffer: 1024 * 1024,
3450
+ windowsHide: true,
3451
+ });
3452
+ const durationMs = Math.max(0, Date.now() - startedAt);
3453
+ const exitCode = typeof result.status === "number" ? result.status : 1;
3454
+ const stderr = [
3455
+ typeof result.stderr === "string" ? result.stderr : "",
3456
+ result.error ? result.error.message : "",
3457
+ ].filter(Boolean).join("\n");
3458
+ return {
3459
+ command,
3460
+ status: exitCode === 0 ? "passed" : "failed",
3461
+ exitCode,
3462
+ durationMs,
3463
+ stdoutTail: outputTail(typeof result.stdout === "string" ? result.stdout : ""),
3464
+ stderrTail: outputTail(stderr),
3465
+ note,
3466
+ };
3467
+ }
3468
+ function outputTail(value, maxLength = 4000) {
3469
+ const normalized = value.replace(/\r\n/g, "\n").trim();
3470
+ if (normalized.length === 0) {
3471
+ return undefined;
3472
+ }
3473
+ return normalized.length > maxLength ? normalized.slice(-maxLength) : normalized;
3474
+ }
3475
+ function currentChangeSnapshotForVerification(workspaceRoot, intent) {
3476
+ const scope = verificationSnapshotScope(intent);
3477
+ const snapshot = (changedFiles, diff, changeMode) => ({
3478
+ changedFiles: changedFiles
3479
+ .map(normalizeProjectPath)
3480
+ .filter(core_1.isRippleSourceFile)
3481
+ .filter((file) => scope.size === 0 || scope.has(file)),
3482
+ changeMode,
3483
+ changeFingerprint: (0, core_1.fingerprintRippleChangeDiff)(diff),
3484
+ });
3485
+ try {
3486
+ const diff = (0, core_1.listGitChangedDiff)(workspaceRoot, "HEAD");
3487
+ const changedFiles = (0, core_1.listGitChangedFiles)(workspaceRoot, "HEAD");
3488
+ const changedSnapshot = snapshot(changedFiles, diff, "changed");
3489
+ if (changedSnapshot.changedFiles.length > 0) {
3490
+ return changedSnapshot;
3491
+ }
3492
+ }
3493
+ catch {
3494
+ // Repositories without a HEAD commit fall back to staged/worktree snapshots.
3495
+ }
3496
+ try {
3497
+ const diff = (0, core_1.listGitStagedDiff)(workspaceRoot);
3498
+ const stagedSnapshot = snapshot((0, core_1.listGitStagedFiles)(workspaceRoot), diff, "staged");
3499
+ if (stagedSnapshot.changedFiles.length > 0) {
3500
+ return stagedSnapshot;
3501
+ }
3502
+ }
3503
+ catch {
3504
+ // Fall through to worktree or empty coverage.
3505
+ }
3506
+ try {
3507
+ return snapshot((0, core_1.listGitWorktreeFiles)(workspaceRoot), (0, core_1.listGitWorktreeDiff)(workspaceRoot), "worktree");
3508
+ }
3509
+ catch {
3510
+ return { changedFiles: [], changeMode: "staged" };
3511
+ }
3512
+ }
3513
+ function verificationSnapshotScope(intent) {
3514
+ return new Set([
3515
+ ...intent.editableFiles,
3516
+ ...intent.expectedFiles,
3517
+ intent.targetFile,
3518
+ ].map(normalizeProjectPath));
3519
+ }
3520
+ function verifyCommand(options) {
3521
+ const workspaceRoot = process.cwd();
3522
+ const intentRef = options.intent ?? "latest";
3523
+ const reportedCommand = options.verificationCommand?.trim();
3524
+ const runCommand = options.verificationRunCommand?.trim();
3525
+ if (reportedCommand && runCommand) {
3526
+ throw new Error("Use either --run for Ripple-executed evidence or --command/--status for reported evidence, not both.");
3527
+ }
3528
+ if (!reportedCommand && !runCommand) {
3529
+ 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]");
3530
+ }
3531
+ if (runCommand && options.verificationStatus) {
3532
+ throw new Error("--status is only valid with --command. Use --run to let Ripple compute passed/failed from the exit code.");
3533
+ }
3534
+ const intent = (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
3535
+ const executed = runCommand
3536
+ ? executeVerificationCommand(workspaceRoot, runCommand, options.note)
3537
+ : undefined;
3538
+ const changeSnapshot = currentChangeSnapshotForVerification(workspaceRoot, intent);
3539
+ const updatedIntent = (0, core_1.appendRippleVerificationEvidence)(intent, {
3540
+ command: executed?.command ?? reportedCommand ?? "",
3541
+ status: executed?.status ?? options.verificationStatus ?? "unknown",
3542
+ source: executed ? "executed" : "reported",
3543
+ changedFiles: changeSnapshot.changedFiles,
3544
+ changeMode: changeSnapshot.changeMode,
3545
+ changeFingerprint: changeSnapshot.changeFingerprint,
3546
+ exitCode: executed?.exitCode,
3547
+ durationMs: executed?.durationMs,
3548
+ stdoutTail: executed?.stdoutTail,
3549
+ stderrTail: executed?.stderrTail,
3550
+ note: executed?.note ?? options.note,
3551
+ });
3552
+ const intentPath = (0, core_1.saveChangeIntent)(workspaceRoot, updatedIntent, intentRef);
3553
+ const evidence = updatedIntent.verificationEvidence[updatedIntent.verificationEvidence.length - 1];
3554
+ const evidenceSourceLabel = evidence.source === "executed"
3555
+ ? "Ripple executed this command and recorded its exit code."
3556
+ : "Ripple recorded reported evidence only; it did not independently run this command.";
3557
+ const output = {
3558
+ protocol: "ripple-verification-evidence",
3559
+ version: 1,
3560
+ workspace: workspaceRoot,
3561
+ intentPath,
3562
+ intentId: updatedIntent.id,
3563
+ evidence,
3564
+ totalEvidence: updatedIntent.verificationEvidence.length,
3565
+ nextSteps: [
3566
+ "Run ripple gate --intent latest --json to include this evidence in the review packet.",
3567
+ evidence.status === "failed"
3568
+ ? "Fix the failing verification, rerun ripple verify --run, then run ripple gate again."
3569
+ : "Run ripple gate again before handoff so the continue/stop decision includes this evidence.",
3570
+ evidenceSourceLabel,
3571
+ ],
3572
+ };
3573
+ if (options.json) {
3574
+ printJson(output);
3575
+ return;
3576
+ }
3577
+ console.log("Ripple verification evidence");
3578
+ console.log(`Intent: ${output.intentId}`);
3579
+ console.log(`Intent path: ${path.relative(workspaceRoot, output.intentPath) || output.intentPath}`);
3580
+ console.log(`Status: ${output.evidence.status}`);
3581
+ console.log(`Command: ${output.evidence.command}`);
3582
+ console.log(`Source: ${output.evidence.source}`);
3583
+ if (typeof output.evidence.exitCode === "number") {
3584
+ console.log(`Exit code: ${output.evidence.exitCode}`);
3585
+ }
3586
+ if (typeof output.evidence.durationMs === "number") {
3587
+ console.log(`Duration ms: ${output.evidence.durationMs}`);
3588
+ }
3589
+ if (output.evidence.changedFiles) {
3590
+ printHumanList("Changed files covered:", output.evidence.changedFiles);
3591
+ }
3592
+ if (output.evidence.changeMode) {
3593
+ console.log(`Change mode: ${output.evidence.changeMode}`);
3594
+ }
3595
+ if (output.evidence.changeFingerprint) {
3596
+ console.log(`Change fingerprint: ${output.evidence.changeFingerprint.slice(0, 12)}`);
3597
+ }
3598
+ console.log(`Recorded at: ${output.evidence.recordedAt}`);
3599
+ if (output.evidence.note) {
3600
+ console.log(`Note: ${output.evidence.note}`);
3601
+ }
3602
+ if (output.evidence.stderrTail) {
3603
+ console.log("Stderr tail:");
3604
+ console.log(output.evidence.stderrTail);
3605
+ }
3606
+ console.log(`Note: ${evidenceSourceLabel}`);
3607
+ }
2645
3608
  function approvalCommand(options) {
2646
3609
  const workspaceRoot = resolveWorkspaceRoot(".");
2647
3610
  const intentRef = options.intent ?? "latest";
@@ -2660,9 +3623,69 @@ function approvalCommand(options) {
2660
3623
  async function ciCommand(options) {
2661
3624
  const workspaceRoot = resolveWorkspaceRoot(".");
2662
3625
  const baseRef = options.base ?? defaultCiBaseRef();
3626
+ const hasExplicitIntent = Boolean(options.intent);
2663
3627
  const intentRef = options.intent ?? "latest";
2664
3628
  const files = (0, core_1.listGitChangedFiles)(workspaceRoot, baseRef);
2665
3629
  const emitGithubAnnotations = shouldEmitGithubAnnotations(options);
3630
+ if (!hasExplicitIntent) {
3631
+ const summary = await buildCheckSummaryForFiles({
3632
+ workspaceRoot,
3633
+ files,
3634
+ mode: "changed",
3635
+ baseRef,
3636
+ tokenBudget: options.budget,
3637
+ });
3638
+ const policySync = buildPolicySyncSummary(workspaceRoot);
3639
+ if (options.json) {
3640
+ printJson({
3641
+ ...summary,
3642
+ protocol: "ripple-ci-policy-audit",
3643
+ version: 1,
3644
+ auditMode: true,
3645
+ blocking: false,
3646
+ intentRequired: false,
3647
+ policySync,
3648
+ });
3649
+ }
3650
+ else if (options.agent) {
3651
+ printAgentStagedCheckSummary(summary);
3652
+ console.log("");
3653
+ console.log("ci_mode: policy-audit");
3654
+ console.log("blocking: false");
3655
+ console.log("intent_required: false");
3656
+ console.log(`policy_sync: ${policySync.status}`);
3657
+ if (policySync.missingRules.length > 0) {
3658
+ console.log("policy_sync_missing_rules:");
3659
+ policySync.missingRules.slice(0, 12).forEach((rule) => {
3660
+ console.log(`- ${rule.paths.join(", ")} risk=${rule.risk ?? "medium"}`);
3661
+ });
3662
+ }
3663
+ console.log("next_required_action: Review policy-risk findings and policy-sync warnings before merge. Use --intent latest --strict only when you want an intent-bound hard gate.");
3664
+ }
3665
+ else {
3666
+ console.log("Ripple CI policy audit");
3667
+ console.log("Status: audit");
3668
+ console.log("Blocking: false");
3669
+ console.log("Intent: none (local intents are not required in CI audit mode)");
3670
+ console.log(`Policy sync: ${policySync.status}`);
3671
+ if (policySync.missingRules.length > 0) {
3672
+ console.log("");
3673
+ console.log("Policy may be missing risky repo surfaces:");
3674
+ policySync.missingRules.slice(0, 12).forEach((rule) => {
3675
+ console.log(`- ${rule.paths.join(", ")} risk=${rule.risk ?? "medium"}`);
3676
+ });
3677
+ }
3678
+ console.log("");
3679
+ printStagedCheckSummary(summary);
3680
+ console.log("");
3681
+ console.log("Next action: Review policy-risk findings and policy-sync warnings before merge. Use --intent latest --strict only when you want an intent-bound hard gate.");
3682
+ }
3683
+ if (emitGithubAnnotations && !options.json) {
3684
+ printGithubPolicyAuditAnnotations(summary, policySync);
3685
+ }
3686
+ writeGithubPolicyAuditStepSummary(summary, policySync);
3687
+ return;
3688
+ }
2666
3689
  let intent;
2667
3690
  try {
2668
3691
  intent = (0, core_1.loadChangeIntent)(workspaceRoot, intentRef);
@@ -2709,7 +3732,7 @@ async function ciCommand(options) {
2709
3732
  printGithubIntentLoadError(message);
2710
3733
  }
2711
3734
  writeGithubStepSummary({ summary, intentLoadError: message });
2712
- process.exitCode = 1;
3735
+ applyStrictExit(options.strict);
2713
3736
  return;
2714
3737
  }
2715
3738
  const audit = await buildAuditForFiles({
@@ -2737,7 +3760,7 @@ async function ciCommand(options) {
2737
3760
  printGithubAuditAnnotations(audit);
2738
3761
  }
2739
3762
  writeGithubAuditStepSummary(audit);
2740
- applyStrictExit(strictAuditShouldFail(audit));
3763
+ applyStrictExit(options.strict && strictAuditShouldFail(audit));
2741
3764
  }
2742
3765
  async function doctorCommand(options) {
2743
3766
  const workspaceRoot = resolveWorkspaceRoot(".");
@@ -2745,14 +3768,19 @@ async function doctorCommand(options) {
2745
3768
  try {
2746
3769
  await runWithQuietEngine(() => engine.initialScan());
2747
3770
  const summary = (0, core_1.buildRippleReadinessSummary)(workspaceRoot, engine);
3771
+ const policySync = buildPolicySyncSummary(workspaceRoot);
3772
+ const output = {
3773
+ ...summary,
3774
+ policySync,
3775
+ };
2748
3776
  if (options.json) {
2749
- printJson(summary);
3777
+ printJson(output);
2750
3778
  }
2751
3779
  else if (options.agent) {
2752
- printAgentDoctorSummary(summary);
3780
+ printAgentDoctorSummary(output);
2753
3781
  }
2754
3782
  else {
2755
- printDoctorSummary(summary);
3783
+ printDoctorSummary(output);
2756
3784
  }
2757
3785
  applyStrictExit(options.strict && summary.status !== "ready");
2758
3786
  }
@@ -2762,7 +3790,7 @@ async function doctorCommand(options) {
2762
3790
  }
2763
3791
  async function initCommand(options) {
2764
3792
  const workspaceRoot = resolveWorkspaceRoot(".");
2765
- const policy = (0, core_1.defaultRipplePolicy)();
3793
+ const { policy } = (0, core_1.buildSmartRipplePolicy)(workspaceRoot);
2766
3794
  const policyContents = (0, core_1.formatRipplePolicy)(policy);
2767
3795
  const workflow = githubActionsWorkflow();
2768
3796
  const gitignoreBlock = rippleGitIgnoreBlock();
@@ -2785,6 +3813,32 @@ async function initCommand(options) {
2785
3813
  },
2786
3814
  ];
2787
3815
  if (options.print) {
3816
+ const agentFiles = agentSetupFiles(workspaceRoot);
3817
+ const agentSetup = buildAgentSetupSummary(workspaceRoot, agentFiles.map((file) => ({
3818
+ path: file.path,
3819
+ status: "printed",
3820
+ written: false,
3821
+ overwritten: false,
3822
+ content: file.content,
3823
+ })));
3824
+ const preCommitContent = ripplePreCommitHookScript();
3825
+ const postCommitContent = ripplePostCommitHookScript();
3826
+ const hookPath = preferredHookPath(workspaceRoot, "pre-commit");
3827
+ const postCommitHookPath = preferredHookPath(workspaceRoot, "post-commit");
3828
+ const hooks = {
3829
+ protocol: "ripple-hook-install",
3830
+ version: 1,
3831
+ workspace: workspaceRoot,
3832
+ path: normalizeHookPathForOutput(workspaceRoot, hookPath),
3833
+ postCommitPath: normalizeHookPathForOutput(workspaceRoot, postCommitHookPath),
3834
+ status: "printed",
3835
+ written: false,
3836
+ overwritten: false,
3837
+ content: [preCommitContent, postCommitContent].join("\n--- ripple-post-commit ---\n"),
3838
+ preCommitContent,
3839
+ postCommitContent,
3840
+ nextSteps: ["Review the hook scripts, then run ripple init to write the full local setup."],
3841
+ };
2788
3842
  const summary = {
2789
3843
  protocol: "ripple-init",
2790
3844
  version: 1,
@@ -2796,20 +3850,30 @@ async function initCommand(options) {
2796
3850
  overwritten: false,
2797
3851
  content: file.content,
2798
3852
  })),
3853
+ agentSetup,
3854
+ hooks,
2799
3855
  nextSteps: defaultInitNextSteps(),
2800
3856
  };
2801
3857
  if (options.json) {
2802
3858
  printJson(summary);
2803
3859
  return;
2804
3860
  }
2805
- process.stdout.write(files
2806
- .flatMap((file) => [`# ${file.path}`, file.content.trimEnd(), ""])
2807
- .join("\n"));
3861
+ process.stdout.write([
3862
+ ...files.flatMap((file) => [`# ${file.path}`, file.content.trimEnd(), ""]),
3863
+ ...agentFiles.flatMap((file) => [`# ${file.path}`, file.content.trimEnd(), ""]),
3864
+ "# .git hooks",
3865
+ preCommitContent.trimEnd(),
3866
+ "--- ripple-post-commit ---",
3867
+ postCommitContent.trimEnd(),
3868
+ "",
3869
+ ].join("\n"));
2808
3870
  return;
2809
3871
  }
2810
3872
  const writtenFiles = files.map((file) => file.merge
2811
3873
  ? writeRippleGitIgnoreFile(file.absolutePath)
2812
3874
  : writeInitFile(file, options.force));
3875
+ const agentSetup = buildAgentSetupSummary(workspaceRoot, agentSetupFiles(workspaceRoot).map((file) => writeAgentSetupFile(file, options.force)));
3876
+ const hooks = installRippleHooks(workspaceRoot);
2813
3877
  const engine = createCliEngine(workspaceRoot);
2814
3878
  try {
2815
3879
  await runWithQuietEngine(() => engine.initialScan());
@@ -2819,6 +3883,8 @@ async function initCommand(options) {
2819
3883
  version: 1,
2820
3884
  workspace: workspaceRoot,
2821
3885
  files: writtenFiles,
3886
+ agentSetup,
3887
+ hooks,
2822
3888
  readiness,
2823
3889
  nextSteps: defaultInitNextSteps(readiness),
2824
3890
  };
@@ -2833,6 +3899,65 @@ async function initCommand(options) {
2833
3899
  engine.dispose();
2834
3900
  }
2835
3901
  }
3902
+ function writeAgentSetupFile(file, force) {
3903
+ const existed = fs.existsSync(file.absolutePath);
3904
+ const nextSection = normalizeLf(file.content);
3905
+ if (!existed || force) {
3906
+ fs.mkdirSync(path.dirname(file.absolutePath), { recursive: true });
3907
+ fs.writeFileSync(file.absolutePath, ensureTrailingLf(nextSection), "utf8");
3908
+ return {
3909
+ path: file.path,
3910
+ status: existed ? "overwritten" : "written",
3911
+ written: true,
3912
+ overwritten: existed,
3913
+ };
3914
+ }
3915
+ const existing = fs.readFileSync(file.absolutePath, "utf8");
3916
+ const updated = mergeRippleManagedSection(existing, nextSection);
3917
+ if (updated.content === existing) {
3918
+ return {
3919
+ path: file.path,
3920
+ status: "exists",
3921
+ written: false,
3922
+ overwritten: false,
3923
+ };
3924
+ }
3925
+ fs.writeFileSync(file.absolutePath, updated.content, "utf8");
3926
+ return {
3927
+ path: file.path,
3928
+ status: updated.action,
3929
+ written: true,
3930
+ overwritten: false,
3931
+ };
3932
+ }
3933
+ function mergeRippleManagedSection(existing, nextSection) {
3934
+ const normalizedExisting = normalizeLf(existing);
3935
+ const start = normalizedExisting.indexOf(RIPPLE_AGENT_SECTION_START);
3936
+ const end = normalizedExisting.indexOf(RIPPLE_AGENT_SECTION_END);
3937
+ if (start !== -1 && end !== -1 && end > start) {
3938
+ const afterEnd = end + RIPPLE_AGENT_SECTION_END.length;
3939
+ const withoutOldSection = `${normalizedExisting.slice(0, start)}${normalizedExisting.slice(afterEnd)}`;
3940
+ return {
3941
+ content: appendRippleSectionAtBottom(withoutOldSection, nextSection),
3942
+ action: "updated",
3943
+ };
3944
+ }
3945
+ return {
3946
+ content: appendRippleSectionAtBottom(normalizedExisting, nextSection),
3947
+ action: "appended",
3948
+ };
3949
+ }
3950
+ function appendRippleSectionAtBottom(existing, nextSection) {
3951
+ const base = normalizeLf(existing).replace(/\n*$/, "");
3952
+ const separator = base.length === 0 ? "" : "\n\n";
3953
+ return `${base}${separator}${ensureTrailingLf(normalizeLf(nextSection))}`;
3954
+ }
3955
+ function normalizeLf(value) {
3956
+ return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
3957
+ }
3958
+ function ensureTrailingLf(value) {
3959
+ return value.endsWith("\n") ? value : `${value}\n`;
3960
+ }
2836
3961
  function writeInitFile(file, force) {
2837
3962
  const existed = fs.existsSync(file.absolutePath);
2838
3963
  if (existed && !force) {
@@ -2890,6 +4015,260 @@ function gitIgnoreContainsRippleCache(contents) {
2890
4015
  line === ".ripple/.cache" ||
2891
4016
  line === ".ripple/.cache/**");
2892
4017
  }
4018
+ const RIPPLE_PRE_COMMIT_HOOK_START = "# >>> ripple pre-commit hook";
4019
+ const RIPPLE_PRE_COMMIT_HOOK_END = "# <<< ripple pre-commit hook";
4020
+ const RIPPLE_POST_COMMIT_HOOK_START = "# >>> ripple post-commit hook";
4021
+ const RIPPLE_POST_COMMIT_HOOK_END = "# <<< ripple post-commit hook";
4022
+ function ripplePreCommitHookBlock() {
4023
+ return [
4024
+ RIPPLE_PRE_COMMIT_HOOK_START,
4025
+ `# Policy is permanent. Intent is local. Git staged diff is truth.
4026
+ ripple_previous_status=$?
4027
+ if [ "$ripple_previous_status" -ne 0 ]; then
4028
+ exit "$ripple_previous_status"
4029
+ fi
4030
+
4031
+ set +e
4032
+
4033
+ ripple_run() {
4034
+ ${rippleDirectRunnerHookLines().join("\n")}
4035
+ }
4036
+
4037
+ if [ -f ".ripple/intents/latest.json" ]; then
4038
+ echo "[Ripple] Active local intent found. Checking staged changes against approved boundary..."
4039
+ ripple_run gate --staged --intent latest --agent --strict
4040
+ status=$?
4041
+ if [ "$status" -ne 0 ]; then
4042
+ cat <<'EOF'
4043
+ [RIPPLE STOP] Commit blocked by Ripple active-intent boundary.
4044
+
4045
+ If you are an AI agent:
4046
+ - DO NOT retry the commit.
4047
+ - Repair the unauthorized change or ask the human to approve a wider scope.
4048
+
4049
+ If you are a human developer:
4050
+ - Review the Ripple output above.
4051
+ - To bypass this local hook intentionally, run: git commit --no-verify
4052
+ EOF
4053
+ exit $status
4054
+ fi
4055
+ else
4056
+ echo "[Ripple] No active local intent found. Running staged policy/contract awareness check..."
4057
+ ripple_run check --staged --agent
4058
+ status=$?
4059
+ if [ "$status" -ne 0 ]; then
4060
+ cat <<'EOF'
4061
+ [RIPPLE WARNING] Ripple could not complete the no-intent staged check.
4062
+
4063
+ If you are an AI agent:
4064
+ - Stop and ask the human before continuing.
4065
+
4066
+ If you are a human developer:
4067
+ - Review the error above.
4068
+ - To bypass this local hook intentionally, run: git commit --no-verify
4069
+ EOF
4070
+ exit $status
4071
+ fi
4072
+ fi`,
4073
+ RIPPLE_PRE_COMMIT_HOOK_END,
4074
+ "",
4075
+ ].join("\n");
4076
+ }
4077
+ function ripplePreCommitHookScript() {
4078
+ return [
4079
+ "#!/bin/sh",
4080
+ "# Ripple pre-commit hook - generated by `ripple hook install`.",
4081
+ ripplePreCommitHookBlock(),
4082
+ "exit 0",
4083
+ "",
4084
+ ].join("\n");
4085
+ }
4086
+ function ripplePostCommitHookBlock() {
4087
+ return [
4088
+ RIPPLE_POST_COMMIT_HOOK_START,
4089
+ `# Local intents are consumed after a successful commit to avoid ghost-intent blocks.
4090
+ set +e
4091
+
4092
+ cleared=0
4093
+ for intent_file in .ripple/.cache/latest-intent.json .ripple/intents/latest.json; do
4094
+ if [ -f "$intent_file" ]; then
4095
+ rm "$intent_file"
4096
+ cleared=1
4097
+ fi
4098
+ done
4099
+
4100
+ if [ "$cleared" -eq 1 ]; then
4101
+ echo "[Ripple] Consumed and cleared local intent."
4102
+ fi`,
4103
+ RIPPLE_POST_COMMIT_HOOK_END,
4104
+ "",
4105
+ ].join("\n");
4106
+ }
4107
+ function ripplePostCommitHookScript() {
4108
+ return [
4109
+ "#!/bin/sh",
4110
+ "# Ripple post-commit hook - generated by `ripple hook install`.",
4111
+ ripplePostCommitHookBlock(),
4112
+ "exit 0",
4113
+ "",
4114
+ ].join("\n");
4115
+ }
4116
+ function normalizeHookPathForOutput(workspaceRoot, hookPath) {
4117
+ return path.relative(workspaceRoot, hookPath).split(path.sep).join("/");
4118
+ }
4119
+ function preferredHookPath(workspaceRoot, hookName) {
4120
+ const huskyDir = path.join(workspaceRoot, ".husky");
4121
+ if (fs.existsSync(huskyDir) && fs.statSync(huskyDir).isDirectory()) {
4122
+ return path.join(huskyDir, hookName);
4123
+ }
4124
+ return path.join(workspaceRoot, ".git", "hooks", hookName);
4125
+ }
4126
+ function installRippleHookBlock(input) {
4127
+ const { hookPath, fullScript, block, marker } = input;
4128
+ if (!fs.existsSync(hookPath)) {
4129
+ fs.mkdirSync(path.dirname(hookPath), { recursive: true });
4130
+ fs.writeFileSync(hookPath, fullScript, { encoding: "utf8", mode: 0o755 });
4131
+ try {
4132
+ fs.chmodSync(hookPath, 0o755);
4133
+ }
4134
+ catch {
4135
+ // chmod is best-effort on Windows.
4136
+ }
4137
+ return "created";
4138
+ }
4139
+ const existing = fs.readFileSync(hookPath, "utf8");
4140
+ if (existing.includes(marker)) {
4141
+ return "already-present";
4142
+ }
4143
+ const separator = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
4144
+ fs.writeFileSync(hookPath, `${existing}${separator}\n${block}\n`, "utf8");
4145
+ try {
4146
+ fs.chmodSync(hookPath, 0o755);
4147
+ }
4148
+ catch {
4149
+ // chmod is best-effort on Windows.
4150
+ }
4151
+ return "appended";
4152
+ }
4153
+ function installRippleHooks(workspaceRoot) {
4154
+ if (!fs.existsSync(path.join(workspaceRoot, ".git"))) {
4155
+ throw new Error("Cannot install Ripple hook because .git was not found. Run this inside a Git worktree.");
4156
+ }
4157
+ const hookPath = preferredHookPath(workspaceRoot, "pre-commit");
4158
+ const postCommitHookPath = preferredHookPath(workspaceRoot, "post-commit");
4159
+ const content = ripplePreCommitHookScript();
4160
+ const postCommitContent = ripplePostCommitHookScript();
4161
+ const preCommitAction = installRippleHookBlock({
4162
+ hookPath,
4163
+ fullScript: content,
4164
+ block: ripplePreCommitHookBlock(),
4165
+ marker: RIPPLE_PRE_COMMIT_HOOK_START,
4166
+ });
4167
+ const postCommitAction = installRippleHookBlock({
4168
+ hookPath: postCommitHookPath,
4169
+ fullScript: postCommitContent,
4170
+ block: ripplePostCommitHookBlock(),
4171
+ marker: RIPPLE_POST_COMMIT_HOOK_START,
4172
+ });
4173
+ const wroteSomething = preCommitAction !== "already-present" || postCommitAction !== "already-present";
4174
+ return {
4175
+ protocol: "ripple-hook-install",
4176
+ version: 1,
4177
+ workspace: workspaceRoot,
4178
+ path: normalizeHookPathForOutput(workspaceRoot, hookPath),
4179
+ postCommitPath: normalizeHookPathForOutput(workspaceRoot, postCommitHookPath),
4180
+ status: wroteSomething ? "written" : "exists",
4181
+ written: wroteSomething,
4182
+ overwritten: false,
4183
+ preCommitAction,
4184
+ postCommitAction,
4185
+ nextSteps: [
4186
+ "Run ripple plan --file <file> --task \"<task>\" --mode file --agent --save before AI edits.",
4187
+ "Commit normally; Ripple will block active-intent drift and clear consumed local intents after successful commits.",
4188
+ ],
4189
+ };
4190
+ }
4191
+ function hookInstallCommand(subcommand, options) {
4192
+ if (subcommand !== "install") {
4193
+ throw new Error("Usage: ripple hook install [--print] [--force]");
4194
+ }
4195
+ const workspaceRoot = resolveWorkspaceRoot(".");
4196
+ const hookPath = preferredHookPath(workspaceRoot, "pre-commit");
4197
+ const postCommitHookPath = preferredHookPath(workspaceRoot, "post-commit");
4198
+ const relativeHookPath = normalizeHookPathForOutput(workspaceRoot, hookPath);
4199
+ const relativePostCommitHookPath = normalizeHookPathForOutput(workspaceRoot, postCommitHookPath);
4200
+ const content = ripplePreCommitHookScript();
4201
+ const postCommitContent = ripplePostCommitHookScript();
4202
+ if (options.print) {
4203
+ const summary = {
4204
+ protocol: "ripple-hook-install",
4205
+ version: 1,
4206
+ workspace: workspaceRoot,
4207
+ path: relativeHookPath,
4208
+ postCommitPath: relativePostCommitHookPath,
4209
+ status: "printed",
4210
+ written: false,
4211
+ overwritten: false,
4212
+ content: [content, postCommitContent].join("\n--- ripple-post-commit ---\n"),
4213
+ preCommitContent: content,
4214
+ postCommitContent,
4215
+ nextSteps: ["Review the hook scripts, then run ripple hook install to write them."],
4216
+ };
4217
+ if (options.json) {
4218
+ printJson(summary);
4219
+ }
4220
+ else {
4221
+ process.stdout.write(content);
4222
+ process.stdout.write("\n--- ripple-post-commit ---\n");
4223
+ process.stdout.write(postCommitContent);
4224
+ }
4225
+ return;
4226
+ }
4227
+ if (!fs.existsSync(path.join(workspaceRoot, ".git"))) {
4228
+ throw new Error("Cannot install Ripple hook because .git was not found. Run this inside a Git worktree.");
4229
+ }
4230
+ const preCommitAction = installRippleHookBlock({
4231
+ hookPath,
4232
+ fullScript: content,
4233
+ block: ripplePreCommitHookBlock(),
4234
+ marker: RIPPLE_PRE_COMMIT_HOOK_START,
4235
+ });
4236
+ const postCommitAction = installRippleHookBlock({
4237
+ hookPath: postCommitHookPath,
4238
+ fullScript: postCommitContent,
4239
+ block: ripplePostCommitHookBlock(),
4240
+ marker: RIPPLE_POST_COMMIT_HOOK_START,
4241
+ });
4242
+ const wroteSomething = preCommitAction !== "already-present" || postCommitAction !== "already-present";
4243
+ const summary = {
4244
+ protocol: "ripple-hook-install",
4245
+ version: 1,
4246
+ workspace: workspaceRoot,
4247
+ path: relativeHookPath,
4248
+ postCommitPath: relativePostCommitHookPath,
4249
+ status: wroteSomething ? "written" : "exists",
4250
+ written: wroteSomething,
4251
+ overwritten: false,
4252
+ preCommitAction,
4253
+ postCommitAction,
4254
+ nextSteps: [
4255
+ "Commit normally. Ripple will check staged changes before each commit.",
4256
+ "After a successful commit, Ripple clears consumed local intents to prevent ghost-intent blocks.",
4257
+ "Humans can intentionally bypass local hooks with git commit --no-verify.",
4258
+ ],
4259
+ };
4260
+ if (options.json) {
4261
+ printJson(summary);
4262
+ }
4263
+ else {
4264
+ console.log(wroteSomething ? "Ripple Git hooks integrated" : "Ripple Git hooks already integrated");
4265
+ console.log(`Pre-commit: ${relativeHookPath} (${preCommitAction})`);
4266
+ console.log(`Post-commit: ${relativePostCommitHookPath} (${postCommitAction})`);
4267
+ console.log("Active intent: blocks staged drift against latest local plan.");
4268
+ console.log("No intent: warns with staged policy/contract awareness and lets humans stay in control.");
4269
+ console.log("Post-commit: clears consumed local intents to prevent ghost-intent blocks.");
4270
+ }
4271
+ }
2893
4272
  function initCiCommand(options) {
2894
4273
  const workflow = githubActionsWorkflow();
2895
4274
  const workspaceRoot = resolveWorkspaceRoot(".");
@@ -2924,7 +4303,7 @@ function initCiCommand(options) {
2924
4303
  }
2925
4304
  console.log(existed ? "Ripple CI workflow overwritten" : "Ripple CI workflow written");
2926
4305
  console.log(`Path: ${relativeTargetPath}`);
2927
- console.log("Command: npx -y @getripple/cli@latest ci --base origin/${{ github.base_ref }} --intent latest --github-annotations");
4306
+ console.log(`Command: npx -y ${rippleCliPackageSpec()} ci --base origin/\${{ github.base_ref }} --github-annotations`);
2928
4307
  }
2929
4308
  function policyCommand(args, options) {
2930
4309
  const subcommand = args[0];
@@ -2932,15 +4311,19 @@ function policyCommand(args, options) {
2932
4311
  policyInitCommand(options);
2933
4312
  return;
2934
4313
  }
4314
+ if (subcommand === "sync") {
4315
+ policySyncCommand(options);
4316
+ return;
4317
+ }
2935
4318
  if (subcommand === "explain") {
2936
4319
  policyExplainCommand(options);
2937
4320
  return;
2938
4321
  }
2939
- throw new Error("Usage: ripple policy init [--print] [--force] or ripple policy explain --file <file>");
4322
+ throw new Error("Usage: ripple policy init [--print] [--force], ripple policy sync [--json], or ripple policy explain --file <file>");
2940
4323
  }
2941
4324
  function policyInitCommand(options) {
2942
- const policy = (0, core_1.defaultRipplePolicy)();
2943
4325
  const workspaceRoot = resolveWorkspaceRoot(".");
4326
+ const { policy, detections } = (0, core_1.buildSmartRipplePolicy)(workspaceRoot);
2944
4327
  const targetPath = (0, core_1.ripplePolicyPath)(workspaceRoot);
2945
4328
  const relativeTargetPath = core_1.RIPPLE_POLICY_PATH.split(path.sep).join("/");
2946
4329
  const contents = (0, core_1.formatRipplePolicy)(policy);
@@ -2949,6 +4332,7 @@ function policyInitCommand(options) {
2949
4332
  printJson({
2950
4333
  path: relativeTargetPath,
2951
4334
  policy,
4335
+ detections,
2952
4336
  written: false,
2953
4337
  });
2954
4338
  }
@@ -2969,6 +4353,7 @@ function policyInitCommand(options) {
2969
4353
  written: true,
2970
4354
  overwritten: existed,
2971
4355
  policy,
4356
+ detections,
2972
4357
  });
2973
4358
  return;
2974
4359
  }
@@ -2976,6 +4361,189 @@ function policyInitCommand(options) {
2976
4361
  console.log(`Path: ${relativeTargetPath}`);
2977
4362
  console.log(`Default mode: ${policy.defaultMode ?? "file"}`);
2978
4363
  console.log(`Risk rules: ${policy.riskRules?.length ?? 0}`);
4364
+ if (detections.length > 0) {
4365
+ console.log("Smart detections:");
4366
+ detections.forEach((detection) => {
4367
+ console.log(`- ${detection.kind}: ${detection.evidence.join(", ")}`);
4368
+ });
4369
+ }
4370
+ }
4371
+ function policySyncCommand(options) {
4372
+ const workspaceRoot = resolveWorkspaceRoot(".");
4373
+ const summary = buildPolicySyncSummary(workspaceRoot);
4374
+ if (options.json) {
4375
+ printJson(summary);
4376
+ return;
4377
+ }
4378
+ printPolicySyncSummary(summary);
4379
+ }
4380
+ function buildPolicySyncSummary(workspaceRoot) {
4381
+ const loadedPolicy = (0, core_1.loadRipplePolicy)(workspaceRoot);
4382
+ const { policy: smartPolicy, detections } = (0, core_1.buildSmartRipplePolicy)(workspaceRoot);
4383
+ const existingRules = loadedPolicy.policy.riskRules ?? [];
4384
+ const missingRules = [];
4385
+ const missingRuleKeys = new Set();
4386
+ const detectionSummaries = detections.map((detection) => {
4387
+ let missingForDetection = 0;
4388
+ detection.rules.forEach((rule) => {
4389
+ if (policyRuleIsCovered(rule, existingRules)) {
4390
+ return;
4391
+ }
4392
+ const key = policyRuleKey(rule);
4393
+ if (missingRuleKeys.has(key)) {
4394
+ return;
4395
+ }
4396
+ missingRuleKeys.add(key);
4397
+ missingForDetection += 1;
4398
+ missingRules.push({
4399
+ ...clonePolicyRule(rule),
4400
+ reason: `${detection.kind}: ${detection.evidence.join(", ")}`,
4401
+ });
4402
+ });
4403
+ return {
4404
+ kind: detection.kind,
4405
+ evidence: detection.evidence,
4406
+ missingRules: missingForDetection,
4407
+ };
4408
+ });
4409
+ if (!loadedPolicy.exists) {
4410
+ (smartPolicy.riskRules ?? []).forEach((rule) => {
4411
+ const key = policyRuleKey(rule);
4412
+ if (missingRuleKeys.has(key)) {
4413
+ return;
4414
+ }
4415
+ missingRuleKeys.add(key);
4416
+ missingRules.push({
4417
+ ...clonePolicyRule(rule),
4418
+ reason: "policy file missing",
4419
+ });
4420
+ });
4421
+ }
4422
+ const status = missingRules.length > 0 ? "update-available" : "up-to-date";
4423
+ return {
4424
+ protocol: "ripple-policy-sync",
4425
+ version: 1,
4426
+ workspace: workspaceRoot,
4427
+ policyPath: core_1.RIPPLE_POLICY_PATH.split(path.sep).join("/"),
4428
+ policyExists: loadedPolicy.exists,
4429
+ status,
4430
+ missingRules,
4431
+ detections: detectionSummaries,
4432
+ nextSteps: policySyncNextSteps(status, loadedPolicy.exists),
4433
+ };
4434
+ }
4435
+ function clonePolicyRule(rule) {
4436
+ return {
4437
+ paths: [...rule.paths],
4438
+ ...(rule.risk ? { risk: rule.risk } : {}),
4439
+ ...(rule.requireHumanBeforeEdit === true ? { requireHumanBeforeEdit: true } : {}),
4440
+ ...(rule.requireHumanBeforeMerge === true ? { requireHumanBeforeMerge: true } : {}),
4441
+ ...(rule.allowPrMode === true ? { allowPrMode: true } : {}),
4442
+ };
4443
+ }
4444
+ function policyRuleIsCovered(suggested, existingRules) {
4445
+ return existingRules.some((existing) => {
4446
+ if (!suggested.paths.every((suggestedPath) => existing.paths.some((existingPath) => policyPathPatternCovers(existingPath, suggestedPath)))) {
4447
+ return false;
4448
+ }
4449
+ if (suggested.risk && comparePolicyRisk(existing.risk, suggested.risk) < 0) {
4450
+ return false;
4451
+ }
4452
+ if (suggested.requireHumanBeforeEdit === true && existing.requireHumanBeforeEdit !== true) {
4453
+ return false;
4454
+ }
4455
+ if (suggested.requireHumanBeforeMerge === true && existing.requireHumanBeforeMerge !== true) {
4456
+ return false;
4457
+ }
4458
+ return true;
4459
+ });
4460
+ }
4461
+ function policyPathPatternCovers(existingPattern, suggestedPattern) {
4462
+ const existing = normalizePolicyPattern(existingPattern);
4463
+ const suggested = normalizePolicyPattern(suggestedPattern);
4464
+ if (existing === suggested) {
4465
+ return true;
4466
+ }
4467
+ if (existing === "**" || existing === "**/*") {
4468
+ return true;
4469
+ }
4470
+ if (existing.endsWith("/**")) {
4471
+ const base = existing.slice(0, -3);
4472
+ return suggested === base || suggested.startsWith(`${base}/`);
4473
+ }
4474
+ if (!suggested.includes("*") && policyGlobToRegExp(existing).test(suggested)) {
4475
+ return true;
4476
+ }
4477
+ return false;
4478
+ }
4479
+ function normalizePolicyPattern(pattern) {
4480
+ return pattern.replace(/\\\\/g, "/").replace(/^\.\//, "");
4481
+ }
4482
+ function policyGlobToRegExp(pattern) {
4483
+ const normalized = normalizePolicyPattern(pattern);
4484
+ let source = "";
4485
+ for (let index = 0; index < normalized.length; index += 1) {
4486
+ const char = normalized[index];
4487
+ const next = normalized[index + 1];
4488
+ if (char === "*" && next === "*") {
4489
+ source += ".*";
4490
+ index += 1;
4491
+ continue;
4492
+ }
4493
+ if (char === "*") {
4494
+ source += "[^/]*";
4495
+ continue;
4496
+ }
4497
+ source += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
4498
+ }
4499
+ return new RegExp(`^${source}$`);
4500
+ }
4501
+ function comparePolicyRisk(existing, suggested) {
4502
+ const levels = ["low", "medium", "high", "critical"];
4503
+ return levels.indexOf(existing ?? "low") - levels.indexOf(suggested ?? "low");
4504
+ }
4505
+ function policyRuleKey(rule) {
4506
+ return JSON.stringify({
4507
+ paths: rule.paths.map(normalizePolicyPattern).sort(),
4508
+ risk: rule.risk ?? "",
4509
+ requireHumanBeforeEdit: rule.requireHumanBeforeEdit === true,
4510
+ requireHumanBeforeMerge: rule.requireHumanBeforeMerge === true,
4511
+ allowPrMode: rule.allowPrMode === true,
4512
+ });
4513
+ }
4514
+ function policySyncNextSteps(status, policyExists) {
4515
+ if (!policyExists) {
4516
+ return [
4517
+ "Run ripple policy init to create .ripple/policy.json from the current repository shape.",
4518
+ "Review the suggested risk rules before committing the policy.",
4519
+ ];
4520
+ }
4521
+ if (status === "up-to-date") {
4522
+ return ["No policy update is required right now."];
4523
+ }
4524
+ return [
4525
+ "Review the suggested missing rules with a human maintainer.",
4526
+ "Update .ripple/policy.json only after approving the new trust boundaries.",
4527
+ ];
4528
+ }
4529
+ function printPolicySyncSummary(summary) {
4530
+ console.log("Ripple policy sync");
4531
+ console.log(`Policy: ${summary.policyPath}${summary.policyExists ? "" : " (missing)"}`);
4532
+ console.log(`Status: ${summary.status}`);
4533
+ if (summary.missingRules.length > 0) {
4534
+ console.log("");
4535
+ console.log("Detected risky paths not covered by policy:");
4536
+ summary.missingRules.forEach((rule) => {
4537
+ console.log(`- ${rule.paths.join(", ")} risk=${rule.risk ?? "medium"}${rule.requireHumanBeforeEdit ? " human-before-edit" : ""}${rule.requireHumanBeforeMerge ? " human-before-merge" : ""}`);
4538
+ console.log(` reason: ${rule.reason}`);
4539
+ });
4540
+ }
4541
+ else {
4542
+ console.log("Policy is up to date with current smart detections.");
4543
+ }
4544
+ console.log("");
4545
+ console.log("Next:");
4546
+ summary.nextSteps.forEach((step) => console.log(`- ${step}`));
2979
4547
  }
2980
4548
  function policyExplainCommand(options) {
2981
4549
  if (!options.file) {
@@ -3035,8 +4603,8 @@ function printAgentPolicyExplanation(explanation) {
3035
4603
  }
3036
4604
  async function repairCommand(options) {
3037
4605
  const workspaceRoot = resolveWorkspaceRoot(".");
3038
- const stagedFiles = (0, core_1.listGitStagedFiles)(workspaceRoot);
3039
4606
  const intent = (0, core_1.loadChangeIntent)(workspaceRoot, options.intent ?? "latest");
4607
+ const stagedFiles = (0, core_1.listGitStagedFiles)(workspaceRoot);
3040
4608
  const engine = createFastCheckEngine(workspaceRoot);
3041
4609
  try {
3042
4610
  await runWithQuietEngine(() => engine.fastCheckScan(fastCheckCandidateFiles(stagedFiles, intent)));
@@ -3299,6 +4867,13 @@ async function main() {
3299
4867
  return;
3300
4868
  }
3301
4869
  if (command === "agent") {
4870
+ if (arg === "setup") {
4871
+ agentSetupCommand(options);
4872
+ return;
4873
+ }
4874
+ if (arg && arg !== "setup") {
4875
+ throw new Error("Usage: ripple agent or ripple agent setup [--print] [--force]");
4876
+ }
3302
4877
  if (options.json) {
3303
4878
  printJson((0, core_1.getAgentWorkflowSummary)());
3304
4879
  }
@@ -3307,6 +4882,10 @@ async function main() {
3307
4882
  }
3308
4883
  return;
3309
4884
  }
4885
+ if (command === "hook") {
4886
+ hookInstallCommand(arg, options);
4887
+ return;
4888
+ }
3310
4889
  if (command === "init") {
3311
4890
  await initCommand(options);
3312
4891
  return;
@@ -3355,6 +4934,10 @@ async function main() {
3355
4934
  await planCommand(options);
3356
4935
  return;
3357
4936
  }
4937
+ if (command === "intent") {
4938
+ intentCommand(arg, options);
4939
+ return;
4940
+ }
3358
4941
  if (command === "check") {
3359
4942
  await checkCommand(options);
3360
4943
  return;
@@ -3367,6 +4950,10 @@ async function main() {
3367
4950
  await gateCommand(options);
3368
4951
  return;
3369
4952
  }
4953
+ if (command === "verify") {
4954
+ verifyCommand(options);
4955
+ return;
4956
+ }
3370
4957
  if (command === "approval") {
3371
4958
  approvalCommand(options);
3372
4959
  return;