@getripple/cli 1.0.5 → 1.0.7
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 +6 -0
- package/README.md +491 -69
- package/dist/index.js +296 -64
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ function usage() {
|
|
|
36
36
|
" ripple init [--force] [--json]",
|
|
37
37
|
" ripple doctor [--strict] [--agent]",
|
|
38
38
|
" ripple scan [path]",
|
|
39
|
+
" ripple workflow [--json]",
|
|
39
40
|
" ripple focus <file>",
|
|
40
41
|
" ripple blast <file>",
|
|
41
42
|
" ripple imports <file>",
|
|
@@ -82,6 +83,7 @@ function usage() {
|
|
|
82
83
|
"Examples:",
|
|
83
84
|
" ripple init",
|
|
84
85
|
" ripple doctor",
|
|
86
|
+
" ripple workflow",
|
|
85
87
|
" ripple doctor --agent",
|
|
86
88
|
" ripple agent",
|
|
87
89
|
" ripple agent --json",
|
|
@@ -420,12 +422,19 @@ function githubActionsWorkflow() {
|
|
|
420
422
|
"",
|
|
421
423
|
].join("\n");
|
|
422
424
|
}
|
|
425
|
+
function rippleGitIgnoreBlock() {
|
|
426
|
+
return [
|
|
427
|
+
"# Ripple machine cache - regenerated automatically",
|
|
428
|
+
core_1.RIPPLE_CACHE_GITIGNORE_ENTRY,
|
|
429
|
+
"",
|
|
430
|
+
].join("\n");
|
|
431
|
+
}
|
|
423
432
|
function defaultInitNextSteps(readiness) {
|
|
424
433
|
return uniqueLines([
|
|
425
434
|
...(readiness?.nextSteps ?? []),
|
|
426
435
|
"Run ripple plan --file <file> --task \"<task>\" --mode file --agent --save.",
|
|
427
436
|
"Run ripple doctor --agent --strict after saving the first intent.",
|
|
428
|
-
"Commit .ripple/policy.json, .
|
|
437
|
+
"Commit .ripple/policy.json, .ripple/intents/latest.json, approvals when needed, and .github/workflows/ripple.yml.",
|
|
429
438
|
]);
|
|
430
439
|
}
|
|
431
440
|
function uniqueLines(lines) {
|
|
@@ -682,6 +691,13 @@ function buildGithubAuditStepSummary(audit) {
|
|
|
682
691
|
lines.push("");
|
|
683
692
|
pushList(lines, "Fix now", gate.fixNow, 12);
|
|
684
693
|
lines.push("");
|
|
694
|
+
lines.push("### Risk", "", `- Level: ${gate.risk.level}`, `- Score: ${gate.risk.score}/100`, `- Summary: ${gate.risk.summary}`, "");
|
|
695
|
+
pushList(lines, "Why this is risky", compactGateRiskReasons(gate), 8);
|
|
696
|
+
lines.push("");
|
|
697
|
+
pushList(lines, "Risk evidence", compactGateRiskEvidence(gate), 12);
|
|
698
|
+
lines.push("");
|
|
699
|
+
pushList(lines, "Risk required actions", compactGateRiskActions(gate), 12);
|
|
700
|
+
lines.push("");
|
|
685
701
|
pushList(lines, "Ask human", gate.askHuman, 8);
|
|
686
702
|
lines.push("");
|
|
687
703
|
pushList(lines, "Gate commands", uniqueItems([
|
|
@@ -933,13 +949,51 @@ async function runWithQuietEngine(task) {
|
|
|
933
949
|
console.log = originalLog;
|
|
934
950
|
}
|
|
935
951
|
}
|
|
952
|
+
function createCliEngine(workspaceRoot, contextMode = "lean") {
|
|
953
|
+
const engine = new core_1.GraphEngine(workspaceRoot);
|
|
954
|
+
engine.setContextGenerationMode(contextMode);
|
|
955
|
+
return engine;
|
|
956
|
+
}
|
|
957
|
+
function createFastCheckEngine(workspaceRoot) {
|
|
958
|
+
const engine = createCliEngine(workspaceRoot, "lean");
|
|
959
|
+
return engine;
|
|
960
|
+
}
|
|
961
|
+
function fastCheckCandidateFiles(files, intent) {
|
|
962
|
+
const intentSymbolFiles = (intent?.allowedSymbols ?? [])
|
|
963
|
+
.map((symbolId) => symbolId.split("::")[0])
|
|
964
|
+
.filter(Boolean);
|
|
965
|
+
return uniqueItems([
|
|
966
|
+
...files,
|
|
967
|
+
...(intent
|
|
968
|
+
? [
|
|
969
|
+
intent.targetFile,
|
|
970
|
+
...intent.editableFiles,
|
|
971
|
+
...intent.allowedFiles,
|
|
972
|
+
...intent.expectedFiles,
|
|
973
|
+
...intent.contextFiles,
|
|
974
|
+
...intentSymbolFiles,
|
|
975
|
+
]
|
|
976
|
+
: []),
|
|
977
|
+
].filter((file) => Boolean(file)));
|
|
978
|
+
}
|
|
936
979
|
function printScanSummary(summary) {
|
|
937
980
|
console.log("Ripple scan complete");
|
|
938
981
|
console.log(`Workspace: ${summary.workspace}`);
|
|
939
982
|
console.log(`Files: ${summary.files}`);
|
|
940
983
|
console.log(`Symbols: ${summary.symbols}`);
|
|
941
984
|
console.log(`Call edges: ${summary.callEdges}`);
|
|
942
|
-
console.log(`
|
|
985
|
+
console.log(`Mode: ${summary.contextMode}`);
|
|
986
|
+
console.log(`Graph cache: ${summary.cacheGenerated ? summary.cachePath : "not generated"}`);
|
|
987
|
+
console.log(`Context bundle: ${summary.contextGenerated ? summary.contextPath : "not generated by lean scan"}`);
|
|
988
|
+
}
|
|
989
|
+
function printWorkflowSummary(summary) {
|
|
990
|
+
console.log("Ripple workflow generated");
|
|
991
|
+
console.log(`Workspace: ${summary.workspace}`);
|
|
992
|
+
console.log(`Path: ${summary.path}`);
|
|
993
|
+
console.log(`Context bundle: ${summary.contextGenerated ? "generated" : "missing"}`);
|
|
994
|
+
console.log(`Focus files: ${summary.focusFileCount}`);
|
|
995
|
+
console.log("");
|
|
996
|
+
printHumanList("Next:", summary.nextSteps);
|
|
943
997
|
}
|
|
944
998
|
function printDoctorSummary(summary) {
|
|
945
999
|
console.log("Ripple doctor");
|
|
@@ -951,6 +1005,7 @@ function printDoctorSummary(summary) {
|
|
|
951
1005
|
console.log(`Agent trust: ${summary.adapterSupport.primaryAdapter.agentPolicy.canTrust.join(", ")}`);
|
|
952
1006
|
console.log(`Graph: ${summary.checks.graph.ok ? "ok" : "missing"} - ${summary.checks.graph.detail}`);
|
|
953
1007
|
console.log(`Git: ${summary.checks.git.ok ? "ok" : "missing"} - ${summary.checks.git.detail}`);
|
|
1008
|
+
console.log(`Git ignore: ${summary.checks.gitIgnore.ok ? "ok" : "missing"} - ${summary.checks.gitIgnore.detail}`);
|
|
954
1009
|
console.log(`CI workflow: ${summary.checks.ciWorkflow.ok ? "ok" : "missing"} - ${summary.checks.ciWorkflow.detail}`);
|
|
955
1010
|
console.log(`Latest intent: ${summary.checks.latestIntent.ok ? "ok" : "missing"} - ${summary.checks.latestIntent.detail}`);
|
|
956
1011
|
console.log("");
|
|
@@ -993,7 +1048,10 @@ function printInitSummary(summary) {
|
|
|
993
1048
|
function printAgentDoctorSummary(summary) {
|
|
994
1049
|
console.log("RIPPLE_DOCTOR");
|
|
995
1050
|
console.log(`status: ${summary.status}`);
|
|
996
|
-
console.log(`
|
|
1051
|
+
console.log(`decision: ${summary.decision}`);
|
|
1052
|
+
console.log(`can_continue: ${summary.canContinue}`);
|
|
1053
|
+
console.log(`must_stop: ${summary.mustStop}`);
|
|
1054
|
+
console.log(`next_required_action: ${summary.nextRequiredAction}`);
|
|
997
1055
|
console.log(`workspace: ${summary.workspace}`);
|
|
998
1056
|
console.log(`adapter: ${summary.adapterSupport.primaryAdapter.id}`);
|
|
999
1057
|
console.log(`adapter_support: ${summary.adapterSupport.supportLevel}`);
|
|
@@ -1006,10 +1064,13 @@ function printAgentDoctorSummary(summary) {
|
|
|
1006
1064
|
console.log(`policy_detail: ${summary.enforcement.explicitPolicy.detail}`);
|
|
1007
1065
|
console.log(`graph: ${summary.checks.graph.ok ? "ok" : "missing"} - ${summary.checks.graph.detail}`);
|
|
1008
1066
|
console.log(`git: ${summary.checks.git.ok ? "ok" : "missing"} - ${summary.checks.git.detail}`);
|
|
1067
|
+
console.log(`git_ignore: ${summary.checks.gitIgnore.ok ? "ok" : "missing"} - ${summary.checks.gitIgnore.detail}`);
|
|
1009
1068
|
console.log(`ci_workflow: ${summary.checks.ciWorkflow.ok ? "ok" : "missing"} - ${summary.checks.ciWorkflow.detail}`);
|
|
1010
1069
|
console.log(`latest_intent: ${summary.checks.latestIntent.ok ? "ok" : "missing"} - ${summary.checks.latestIntent.detail}`);
|
|
1011
1070
|
console.log("");
|
|
1012
|
-
printAgentList("
|
|
1071
|
+
printAgentList("why", summary.why);
|
|
1072
|
+
console.log("");
|
|
1073
|
+
printAgentList("fix_now", summary.fixNow);
|
|
1013
1074
|
console.log("");
|
|
1014
1075
|
printAgentList("next_steps", summary.nextSteps);
|
|
1015
1076
|
}
|
|
@@ -1752,6 +1813,15 @@ function printAgentGateSummary(summary) {
|
|
|
1752
1813
|
console.log(`control_mode: ${summary.intent.controlMode}`);
|
|
1753
1814
|
console.log(`human_gate: ${summary.intent.humanGate}`);
|
|
1754
1815
|
console.log(`boundary_risk: ${summary.intent.boundaryRisk}`);
|
|
1816
|
+
console.log(`risk_level: ${summary.risk.level}`);
|
|
1817
|
+
console.log(`risk_score: ${summary.risk.score}`);
|
|
1818
|
+
console.log(`risk_summary: ${summary.risk.summary}`);
|
|
1819
|
+
console.log("");
|
|
1820
|
+
printAgentList("risk_reasons", compactGateRiskReasons(summary));
|
|
1821
|
+
console.log("");
|
|
1822
|
+
printAgentList("risk_evidence", compactGateRiskEvidence(summary));
|
|
1823
|
+
console.log("");
|
|
1824
|
+
printAgentList("risk_required_actions", compactGateRiskActions(summary));
|
|
1755
1825
|
console.log("");
|
|
1756
1826
|
printAgentList("why", summary.why);
|
|
1757
1827
|
console.log("");
|
|
@@ -1776,43 +1846,100 @@ function printAgentGateSummary(summary) {
|
|
|
1776
1846
|
printAgentList("commands_verify", summary.commands.verify);
|
|
1777
1847
|
}
|
|
1778
1848
|
function printGateSummary(summary) {
|
|
1779
|
-
|
|
1780
|
-
console.log(`
|
|
1849
|
+
const statusLabel = summary.canContinue ? "CONTINUE" : "STOP";
|
|
1850
|
+
console.log(`Ripple gate: ${statusLabel}`);
|
|
1851
|
+
console.log(gateHeadline(summary));
|
|
1852
|
+
console.log("");
|
|
1781
1853
|
console.log(`Decision: ${summary.decision}`);
|
|
1782
|
-
console.log(`Can continue: ${summary.canContinue}`);
|
|
1783
|
-
console.log(`Must stop: ${summary.mustStop}`);
|
|
1784
|
-
|
|
1785
|
-
console.log(`Next required phase: ${summary.nextRequiredPhase}`);
|
|
1786
|
-
console.log(`Next required action: ${summary.nextRequiredAction}`);
|
|
1787
|
-
console.log(`Summary: ${summary.summary}`);
|
|
1854
|
+
console.log(`Can continue: ${formatYesNo(summary.canContinue)}`);
|
|
1855
|
+
console.log(`Must stop: ${formatYesNo(summary.mustStop)}`);
|
|
1856
|
+
printGateRiskSummary(summary);
|
|
1788
1857
|
console.log("");
|
|
1789
1858
|
console.log("Intent:");
|
|
1790
|
-
console.log(`
|
|
1791
|
-
console.log(`
|
|
1792
|
-
console.log(`
|
|
1793
|
-
console.log(`
|
|
1794
|
-
console.log(`
|
|
1795
|
-
console.log(
|
|
1859
|
+
console.log(` Task: ${summary.intent.task}`);
|
|
1860
|
+
console.log(` Boundary: ${summary.intent.controlMode}`);
|
|
1861
|
+
console.log(` Target: ${summary.intent.targetFile}`);
|
|
1862
|
+
console.log(` Human gate: ${summary.intent.humanGate}`);
|
|
1863
|
+
console.log(` Approval: ${summary.approvalStatus}`);
|
|
1864
|
+
console.log("");
|
|
1865
|
+
printHumanList("Allowed:", gateAllowedItems(summary));
|
|
1866
|
+
const outsideBoundary = gateChangedOutsideItems(summary);
|
|
1867
|
+
printHumanList(outsideBoundary.length > 0 ? "Changed outside boundary:" : "Changed files:", outsideBoundary.length > 0 ? outsideBoundary : summary.changedFiles);
|
|
1868
|
+
printHumanList("Why:", compactGateReasons(summary));
|
|
1869
|
+
printHumanList("Fix now:", compactGateFixes(summary));
|
|
1870
|
+
if (summary.canContinue) {
|
|
1871
|
+
printHumanList("Verify:", summary.verificationTargets.slice(0, 8));
|
|
1872
|
+
}
|
|
1873
|
+
else {
|
|
1874
|
+
printHumanList("Commands:", compactGateCommands(summary));
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
function printGateRiskSummary(summary) {
|
|
1796
1878
|
console.log("");
|
|
1797
|
-
console.log(
|
|
1798
|
-
console.log(`
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1879
|
+
console.log(`Risk: ${summary.risk.level.toUpperCase()} ${summary.risk.score}/100`);
|
|
1880
|
+
console.log(`Risk summary: ${summary.risk.summary}`);
|
|
1881
|
+
printHumanList("Why this is risky:", compactGateRiskReasons(summary));
|
|
1882
|
+
printHumanList("Evidence:", compactGateRiskEvidence(summary));
|
|
1883
|
+
printHumanList("Required:", compactGateRiskActions(summary));
|
|
1884
|
+
}
|
|
1885
|
+
function compactGateRiskReasons(summary) {
|
|
1886
|
+
return summary.risk.reasons
|
|
1887
|
+
.map((reason) => `${reason.severity.toUpperCase()} ${reason.kind}: ${reason.message}`)
|
|
1888
|
+
.slice(0, 6);
|
|
1889
|
+
}
|
|
1890
|
+
function compactGateRiskEvidence(summary) {
|
|
1891
|
+
return uniqueItems(summary.risk.reasons.flatMap((reason) => reason.evidence)).slice(0, 10);
|
|
1892
|
+
}
|
|
1893
|
+
function compactGateRiskActions(summary) {
|
|
1894
|
+
return uniqueItems(summary.risk.requiredActions).slice(0, 8);
|
|
1895
|
+
}
|
|
1896
|
+
function gateHeadline(summary) {
|
|
1897
|
+
if (summary.canContinue) {
|
|
1898
|
+
return "Agent may continue after running the listed verification targets.";
|
|
1899
|
+
}
|
|
1900
|
+
if (summary.decision === "restore-readiness") {
|
|
1901
|
+
return "Agent must stop because Ripple readiness is weaker than the saved intent.";
|
|
1902
|
+
}
|
|
1903
|
+
if (summary.needsHuman) {
|
|
1904
|
+
return "Agent must stop and ask the human before continuing.";
|
|
1905
|
+
}
|
|
1906
|
+
return "Agent must stop and repair the staged change before continuing.";
|
|
1907
|
+
}
|
|
1908
|
+
function gateAllowedItems(summary) {
|
|
1909
|
+
if (summary.intent.controlMode === "brainstorm") {
|
|
1910
|
+
return ["no file edits"];
|
|
1911
|
+
}
|
|
1912
|
+
if (summary.allowedSymbols.length > 0) {
|
|
1913
|
+
return summary.allowedSymbols;
|
|
1914
|
+
}
|
|
1915
|
+
if (summary.allowedFiles.length > 0) {
|
|
1916
|
+
return summary.allowedFiles;
|
|
1917
|
+
}
|
|
1918
|
+
return [summary.intent.targetFile];
|
|
1919
|
+
}
|
|
1920
|
+
function gateChangedOutsideItems(summary) {
|
|
1921
|
+
return [
|
|
1922
|
+
...summary.changedOutsideBoundaryFiles.map((file) => `file: ${file}`),
|
|
1923
|
+
...summary.changedOutsideBoundarySymbols.map((symbol) => `symbol: ${symbol}`),
|
|
1924
|
+
];
|
|
1925
|
+
}
|
|
1926
|
+
function compactGateReasons(summary) {
|
|
1927
|
+
return uniqueItems(summary.why).slice(0, 6);
|
|
1928
|
+
}
|
|
1929
|
+
function compactGateFixes(summary) {
|
|
1930
|
+
return uniqueItems(summary.fixNow).slice(0, 6);
|
|
1931
|
+
}
|
|
1932
|
+
function compactGateCommands(summary) {
|
|
1933
|
+
return uniqueItems([
|
|
1934
|
+
...summary.commands.unstage,
|
|
1811
1935
|
...summary.commands.repair,
|
|
1812
1936
|
...summary.commands.approve,
|
|
1813
|
-
...summary.commands.
|
|
1814
|
-
|
|
1815
|
-
|
|
1937
|
+
...summary.commands.plan,
|
|
1938
|
+
...summary.commands.doctor,
|
|
1939
|
+
]).slice(0, 6);
|
|
1940
|
+
}
|
|
1941
|
+
function formatYesNo(value) {
|
|
1942
|
+
return value ? "yes" : "no";
|
|
1816
1943
|
}
|
|
1817
1944
|
function printAuditSummary(summary) {
|
|
1818
1945
|
const validation = summary.stagedCheck.intentValidation;
|
|
@@ -2232,7 +2359,7 @@ async function planCommand(options) {
|
|
|
2232
2359
|
throw new Error("Missing target file. Usage: ripple plan --file <file> --task <task> [--budget N]");
|
|
2233
2360
|
}
|
|
2234
2361
|
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
2235
|
-
const engine =
|
|
2362
|
+
const engine = createCliEngine(workspaceRoot);
|
|
2236
2363
|
try {
|
|
2237
2364
|
await runWithQuietEngine(() => engine.initialScan());
|
|
2238
2365
|
const summary = engine.planContext(options.task ?? "", options.file, options.budget);
|
|
@@ -2306,9 +2433,9 @@ function approvalStatusOutput(intent, status) {
|
|
|
2306
2433
|
};
|
|
2307
2434
|
}
|
|
2308
2435
|
async function buildAuditForFiles(input) {
|
|
2309
|
-
const engine =
|
|
2436
|
+
const engine = createFastCheckEngine(input.workspaceRoot);
|
|
2310
2437
|
try {
|
|
2311
|
-
await runWithQuietEngine(() => engine.
|
|
2438
|
+
await runWithQuietEngine(() => engine.fastCheckScan(fastCheckCandidateFiles(input.files, input.intent)));
|
|
2312
2439
|
const stagedCheck = (0, core_1.buildStagedCheckSummary)(engine, {
|
|
2313
2440
|
workspaceRoot: input.workspaceRoot,
|
|
2314
2441
|
stagedFiles: input.files,
|
|
@@ -2336,9 +2463,9 @@ async function buildAuditForFiles(input) {
|
|
|
2336
2463
|
}
|
|
2337
2464
|
}
|
|
2338
2465
|
async function buildCheckSummaryForFiles(input) {
|
|
2339
|
-
const engine =
|
|
2466
|
+
const engine = createFastCheckEngine(input.workspaceRoot);
|
|
2340
2467
|
try {
|
|
2341
|
-
await runWithQuietEngine(() => engine.
|
|
2468
|
+
await runWithQuietEngine(() => engine.fastCheckScan(fastCheckCandidateFiles(input.files)));
|
|
2342
2469
|
return (0, core_1.buildStagedCheckSummary)(engine, {
|
|
2343
2470
|
workspaceRoot: input.workspaceRoot,
|
|
2344
2471
|
stagedFiles: input.files,
|
|
@@ -2363,9 +2490,9 @@ async function checkCommand(options) {
|
|
|
2363
2490
|
const checkFiles = options.changed
|
|
2364
2491
|
? (0, core_1.listGitChangedFiles)(workspaceRoot, baseRef)
|
|
2365
2492
|
: (0, core_1.listGitStagedFiles)(workspaceRoot);
|
|
2366
|
-
const engine =
|
|
2493
|
+
const engine = createFastCheckEngine(workspaceRoot);
|
|
2367
2494
|
try {
|
|
2368
|
-
await runWithQuietEngine(() => engine.
|
|
2495
|
+
await runWithQuietEngine(() => engine.fastCheckScan(fastCheckCandidateFiles(checkFiles)));
|
|
2369
2496
|
const stagedSummary = (0, core_1.buildStagedCheckSummary)(engine, {
|
|
2370
2497
|
workspaceRoot,
|
|
2371
2498
|
stagedFiles: checkFiles,
|
|
@@ -2614,7 +2741,7 @@ async function ciCommand(options) {
|
|
|
2614
2741
|
}
|
|
2615
2742
|
async function doctorCommand(options) {
|
|
2616
2743
|
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
2617
|
-
const engine =
|
|
2744
|
+
const engine = createCliEngine(workspaceRoot);
|
|
2618
2745
|
try {
|
|
2619
2746
|
await runWithQuietEngine(() => engine.initialScan());
|
|
2620
2747
|
const summary = (0, core_1.buildRippleReadinessSummary)(workspaceRoot, engine);
|
|
@@ -2638,6 +2765,7 @@ async function initCommand(options) {
|
|
|
2638
2765
|
const policy = (0, core_1.defaultRipplePolicy)();
|
|
2639
2766
|
const policyContents = (0, core_1.formatRipplePolicy)(policy);
|
|
2640
2767
|
const workflow = githubActionsWorkflow();
|
|
2768
|
+
const gitignoreBlock = rippleGitIgnoreBlock();
|
|
2641
2769
|
const files = [
|
|
2642
2770
|
{
|
|
2643
2771
|
path: core_1.RIPPLE_POLICY_PATH.split(path.sep).join("/"),
|
|
@@ -2649,6 +2777,12 @@ async function initCommand(options) {
|
|
|
2649
2777
|
absolutePath: path.join(workspaceRoot, GITHUB_ACTIONS_WORKFLOW_PATH),
|
|
2650
2778
|
content: workflow,
|
|
2651
2779
|
},
|
|
2780
|
+
{
|
|
2781
|
+
path: core_1.RIPPLE_GITIGNORE_PATH,
|
|
2782
|
+
absolutePath: path.join(workspaceRoot, core_1.RIPPLE_GITIGNORE_PATH),
|
|
2783
|
+
content: gitignoreBlock,
|
|
2784
|
+
merge: true,
|
|
2785
|
+
},
|
|
2652
2786
|
];
|
|
2653
2787
|
if (options.print) {
|
|
2654
2788
|
const summary = {
|
|
@@ -2668,18 +2802,15 @@ async function initCommand(options) {
|
|
|
2668
2802
|
printJson(summary);
|
|
2669
2803
|
return;
|
|
2670
2804
|
}
|
|
2671
|
-
process.stdout.write(
|
|
2672
|
-
`# ${
|
|
2673
|
-
|
|
2674
|
-
"",
|
|
2675
|
-
`# ${files[1].path}`,
|
|
2676
|
-
files[1].content.trimEnd(),
|
|
2677
|
-
"",
|
|
2678
|
-
].join("\n"));
|
|
2805
|
+
process.stdout.write(files
|
|
2806
|
+
.flatMap((file) => [`# ${file.path}`, file.content.trimEnd(), ""])
|
|
2807
|
+
.join("\n"));
|
|
2679
2808
|
return;
|
|
2680
2809
|
}
|
|
2681
|
-
const writtenFiles = files.map((file) =>
|
|
2682
|
-
|
|
2810
|
+
const writtenFiles = files.map((file) => file.merge
|
|
2811
|
+
? writeRippleGitIgnoreFile(file.absolutePath)
|
|
2812
|
+
: writeInitFile(file, options.force));
|
|
2813
|
+
const engine = createCliEngine(workspaceRoot);
|
|
2683
2814
|
try {
|
|
2684
2815
|
await runWithQuietEngine(() => engine.initialScan());
|
|
2685
2816
|
const readiness = (0, core_1.buildRippleReadinessSummary)(workspaceRoot, engine);
|
|
@@ -2721,6 +2852,44 @@ function writeInitFile(file, force) {
|
|
|
2721
2852
|
overwritten: existed,
|
|
2722
2853
|
};
|
|
2723
2854
|
}
|
|
2855
|
+
function writeRippleGitIgnoreFile(targetPath) {
|
|
2856
|
+
const content = rippleGitIgnoreBlock();
|
|
2857
|
+
const existed = fs.existsSync(targetPath);
|
|
2858
|
+
if (!existed) {
|
|
2859
|
+
fs.writeFileSync(targetPath, content, "utf8");
|
|
2860
|
+
return {
|
|
2861
|
+
path: core_1.RIPPLE_GITIGNORE_PATH,
|
|
2862
|
+
status: "written",
|
|
2863
|
+
written: true,
|
|
2864
|
+
overwritten: false,
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
const existing = fs.readFileSync(targetPath, "utf8");
|
|
2868
|
+
if (gitIgnoreContainsRippleCache(existing)) {
|
|
2869
|
+
return {
|
|
2870
|
+
path: core_1.RIPPLE_GITIGNORE_PATH,
|
|
2871
|
+
status: "exists",
|
|
2872
|
+
written: false,
|
|
2873
|
+
overwritten: false,
|
|
2874
|
+
};
|
|
2875
|
+
}
|
|
2876
|
+
const separator = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
|
|
2877
|
+
fs.writeFileSync(targetPath, `${existing}${separator}${content}`, "utf8");
|
|
2878
|
+
return {
|
|
2879
|
+
path: core_1.RIPPLE_GITIGNORE_PATH,
|
|
2880
|
+
status: "updated",
|
|
2881
|
+
written: true,
|
|
2882
|
+
overwritten: false,
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
function gitIgnoreContainsRippleCache(contents) {
|
|
2886
|
+
return contents
|
|
2887
|
+
.split(/\r?\n/)
|
|
2888
|
+
.map((line) => line.trim().replace(/\\/g, "/").replace(/^\/+/, ""))
|
|
2889
|
+
.some((line) => line === core_1.RIPPLE_CACHE_GITIGNORE_ENTRY ||
|
|
2890
|
+
line === ".ripple/.cache" ||
|
|
2891
|
+
line === ".ripple/.cache/**");
|
|
2892
|
+
}
|
|
2724
2893
|
function initCiCommand(options) {
|
|
2725
2894
|
const workflow = githubActionsWorkflow();
|
|
2726
2895
|
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
@@ -2867,15 +3036,15 @@ function printAgentPolicyExplanation(explanation) {
|
|
|
2867
3036
|
async function repairCommand(options) {
|
|
2868
3037
|
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
2869
3038
|
const stagedFiles = (0, core_1.listGitStagedFiles)(workspaceRoot);
|
|
2870
|
-
const
|
|
3039
|
+
const intent = (0, core_1.loadChangeIntent)(workspaceRoot, options.intent ?? "latest");
|
|
3040
|
+
const engine = createFastCheckEngine(workspaceRoot);
|
|
2871
3041
|
try {
|
|
2872
|
-
await runWithQuietEngine(() => engine.
|
|
3042
|
+
await runWithQuietEngine(() => engine.fastCheckScan(fastCheckCandidateFiles(stagedFiles, intent)));
|
|
2873
3043
|
const stagedSummary = (0, core_1.buildStagedCheckSummary)(engine, {
|
|
2874
3044
|
workspaceRoot,
|
|
2875
3045
|
stagedFiles,
|
|
2876
3046
|
tokenBudget: options.budget,
|
|
2877
3047
|
});
|
|
2878
|
-
const intent = (0, core_1.loadChangeIntent)(workspaceRoot, options.intent ?? "latest");
|
|
2879
3048
|
const summary = (0, core_1.validateStagedCheckAgainstIntent)(stagedSummary, intent, {
|
|
2880
3049
|
currentPolicyExplanation: currentPolicyExplanationForIntent(workspaceRoot, intent),
|
|
2881
3050
|
currentReadinessSnapshot: currentReadinessSnapshotForEngine(workspaceRoot, engine),
|
|
@@ -2898,7 +3067,7 @@ async function repairCommand(options) {
|
|
|
2898
3067
|
}
|
|
2899
3068
|
async function historyCommand(options) {
|
|
2900
3069
|
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
2901
|
-
const engine =
|
|
3070
|
+
const engine = createCliEngine(workspaceRoot);
|
|
2902
3071
|
try {
|
|
2903
3072
|
await runWithQuietEngine(() => engine.initialScan());
|
|
2904
3073
|
const summary = engine.getRecentHistorySummary(options.last);
|
|
@@ -2921,7 +3090,7 @@ async function callersCommand(symbolId, options) {
|
|
|
2921
3090
|
throw new Error("Symbol id must use <file>::<symbol>, for example src/auth.ts::validateToken");
|
|
2922
3091
|
}
|
|
2923
3092
|
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
2924
|
-
const engine =
|
|
3093
|
+
const engine = createCliEngine(workspaceRoot);
|
|
2925
3094
|
try {
|
|
2926
3095
|
await runWithQuietEngine(() => engine.initialScan());
|
|
2927
3096
|
const summary = engine.getSymbolCallersSummary(symbolId);
|
|
@@ -2941,16 +3110,20 @@ async function callersCommand(symbolId, options) {
|
|
|
2941
3110
|
}
|
|
2942
3111
|
async function scanCommand(targetPath, options) {
|
|
2943
3112
|
const workspaceRoot = resolveWorkspaceRoot(targetPath);
|
|
2944
|
-
const engine =
|
|
3113
|
+
const engine = createCliEngine(workspaceRoot);
|
|
2945
3114
|
try {
|
|
2946
3115
|
await runWithQuietEngine(() => engine.initialScan());
|
|
3116
|
+
const cachePath = path.join(workspaceRoot, ".ripple", ".cache", "graph.cache.json");
|
|
2947
3117
|
const workflowPath = path.join(workspaceRoot, ".ripple", "WORKFLOW.md");
|
|
2948
3118
|
const summary = {
|
|
2949
3119
|
workspace: workspaceRoot,
|
|
2950
3120
|
files: engine.graph.files.size,
|
|
2951
3121
|
symbols: engine.graph.symbols.size,
|
|
2952
3122
|
callEdges: countCallEdges(engine),
|
|
2953
|
-
|
|
3123
|
+
contextMode: "lean",
|
|
3124
|
+
cacheGenerated: fs.existsSync(cachePath),
|
|
3125
|
+
cachePath,
|
|
3126
|
+
contextGenerated: false,
|
|
2954
3127
|
contextPath: workflowPath,
|
|
2955
3128
|
};
|
|
2956
3129
|
if (options.json) {
|
|
@@ -2964,12 +3137,66 @@ async function scanCommand(targetPath, options) {
|
|
|
2964
3137
|
engine.dispose();
|
|
2965
3138
|
}
|
|
2966
3139
|
}
|
|
3140
|
+
async function workflowCommand(options) {
|
|
3141
|
+
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
3142
|
+
const engine = createCliEngine(workspaceRoot, "full");
|
|
3143
|
+
try {
|
|
3144
|
+
await runWithQuietEngine(() => engine.initialScan());
|
|
3145
|
+
const workflowPath = path.join(workspaceRoot, ".ripple", "WORKFLOW.md");
|
|
3146
|
+
const contextDir = path.join(workspaceRoot, ".ripple", ".cache");
|
|
3147
|
+
const contextFiles = [
|
|
3148
|
+
".ripple/.cache/context.json",
|
|
3149
|
+
".ripple/.cache/context.files.json",
|
|
3150
|
+
".ripple/.cache/context.symbols.json",
|
|
3151
|
+
];
|
|
3152
|
+
const focusDir = path.join(contextDir, "focus");
|
|
3153
|
+
const focusFileCount = countJsonFiles(focusDir);
|
|
3154
|
+
const summary = {
|
|
3155
|
+
protocol: "ripple-workflow",
|
|
3156
|
+
version: 1,
|
|
3157
|
+
workspace: workspaceRoot,
|
|
3158
|
+
path: ".ripple/WORKFLOW.md",
|
|
3159
|
+
written: fs.existsSync(workflowPath),
|
|
3160
|
+
contextGenerated: contextFiles.every((filePath) => fs.existsSync(path.join(workspaceRoot, filePath))),
|
|
3161
|
+
contextFiles,
|
|
3162
|
+
focusFileCount,
|
|
3163
|
+
nextSteps: [
|
|
3164
|
+
"Copy .ripple/WORKFLOW.md into your agent instruction file if your agent does not use MCP.",
|
|
3165
|
+
"For MCP-capable agents, prefer ripple_plan_context and ripple_gate over reading generated files.",
|
|
3166
|
+
"Run ripple scan for a lean graph refresh when you do not need file-based agent instructions.",
|
|
3167
|
+
],
|
|
3168
|
+
};
|
|
3169
|
+
if (!summary.written) {
|
|
3170
|
+
throw new Error("WORKFLOW.md was not generated. Check that the workspace has supported source files.");
|
|
3171
|
+
}
|
|
3172
|
+
if (options.json) {
|
|
3173
|
+
printJson(summary);
|
|
3174
|
+
}
|
|
3175
|
+
else {
|
|
3176
|
+
printWorkflowSummary(summary);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
finally {
|
|
3180
|
+
engine.dispose();
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
function countJsonFiles(directoryPath) {
|
|
3184
|
+
try {
|
|
3185
|
+
return fs
|
|
3186
|
+
.readdirSync(directoryPath)
|
|
3187
|
+
.filter((fileName) => fileName.endsWith(".json"))
|
|
3188
|
+
.length;
|
|
3189
|
+
}
|
|
3190
|
+
catch {
|
|
3191
|
+
return 0;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
2967
3194
|
async function symbolsCommand(filePath, options) {
|
|
2968
3195
|
if (!filePath) {
|
|
2969
3196
|
throw new Error("Missing file path. Usage: ripple symbols <file>");
|
|
2970
3197
|
}
|
|
2971
3198
|
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
2972
|
-
const engine =
|
|
3199
|
+
const engine = createCliEngine(workspaceRoot);
|
|
2973
3200
|
try {
|
|
2974
3201
|
await runWithQuietEngine(() => engine.initialScan());
|
|
2975
3202
|
const summary = engine.getFileSymbolsSummary(filePath);
|
|
@@ -2992,7 +3219,7 @@ async function blastCommand(filePath, options) {
|
|
|
2992
3219
|
throw new Error("Missing file path. Usage: ripple blast <file>");
|
|
2993
3220
|
}
|
|
2994
3221
|
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
2995
|
-
const engine =
|
|
3222
|
+
const engine = createCliEngine(workspaceRoot);
|
|
2996
3223
|
try {
|
|
2997
3224
|
await runWithQuietEngine(() => engine.initialScan());
|
|
2998
3225
|
const summary = engine.getBlastRadiusSummary(filePath);
|
|
@@ -3015,7 +3242,7 @@ async function dependencyCommand(filePath, direction, options) {
|
|
|
3015
3242
|
throw new Error(`Missing file path. Usage: ripple ${direction} <file>`);
|
|
3016
3243
|
}
|
|
3017
3244
|
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
3018
|
-
const engine =
|
|
3245
|
+
const engine = createCliEngine(workspaceRoot);
|
|
3019
3246
|
try {
|
|
3020
3247
|
await runWithQuietEngine(() => engine.initialScan());
|
|
3021
3248
|
const summary = engine.getFileDependencySummary(filePath);
|
|
@@ -3041,13 +3268,14 @@ async function focusCommand(filePath, options) {
|
|
|
3041
3268
|
throw new Error("Missing file path. Usage: ripple focus <file>");
|
|
3042
3269
|
}
|
|
3043
3270
|
const workspaceRoot = resolveWorkspaceRoot(".");
|
|
3044
|
-
const engine =
|
|
3271
|
+
const engine = createCliEngine(workspaceRoot, "on-demand");
|
|
3045
3272
|
try {
|
|
3046
3273
|
await runWithQuietEngine(() => engine.initialScan());
|
|
3047
3274
|
const summary = engine.getFileFocusSummary(filePath);
|
|
3048
3275
|
if (!summary) {
|
|
3049
3276
|
throw new Error(`File is not in the Ripple graph: ${filePath}`);
|
|
3050
3277
|
}
|
|
3278
|
+
engine.writeFileFocus(filePath);
|
|
3051
3279
|
if (options.json) {
|
|
3052
3280
|
printJson(summary);
|
|
3053
3281
|
}
|
|
@@ -3087,6 +3315,10 @@ async function main() {
|
|
|
3087
3315
|
await scanCommand(arg, options);
|
|
3088
3316
|
return;
|
|
3089
3317
|
}
|
|
3318
|
+
if (command === "workflow") {
|
|
3319
|
+
await workflowCommand(options);
|
|
3320
|
+
return;
|
|
3321
|
+
}
|
|
3090
3322
|
if (command === "doctor") {
|
|
3091
3323
|
await doctorCommand(options);
|
|
3092
3324
|
return;
|