@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/CHANGELOG.md +25 -0
- package/README.md +142 -415
- package/dist/index.js +1643 -56
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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]
|
|
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
|
|
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;
|
|
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 --
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1849
|
-
|
|
1850
|
-
console.log(`
|
|
1851
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
2482
|
-
if (
|
|
2483
|
-
|
|
3220
|
+
function selectedChangeMode(options) {
|
|
3221
|
+
if (options.changed) {
|
|
3222
|
+
return "changed";
|
|
2484
3223
|
}
|
|
2485
|
-
if (options.
|
|
2486
|
-
|
|
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
|
|
2491
|
-
|
|
2492
|
-
|
|
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
|
|
2500
|
-
baseRef:
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
3777
|
+
printJson(output);
|
|
2750
3778
|
}
|
|
2751
3779
|
else if (options.agent) {
|
|
2752
|
-
printAgentDoctorSummary(
|
|
3780
|
+
printAgentDoctorSummary(output);
|
|
2753
3781
|
}
|
|
2754
3782
|
else {
|
|
2755
|
-
printDoctorSummary(
|
|
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.
|
|
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(
|
|
2806
|
-
.flatMap((file) => [`# ${file.path}`, file.content.trimEnd(), ""])
|
|
2807
|
-
.
|
|
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(
|
|
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;
|