@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/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, .github/workflows/ripple.yml, and the saved intent when you want CI to enforce the gate.",
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(`Context: .ripple/ ${summary.contextGenerated ? "generated" : "not generated"}`);
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(`readiness_decision: ${summary.status === "ready" ? "continue" : "setup-required"}`);
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("gaps", summary.enforcement.gaps);
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
- console.log("Ripple gate");
1780
- console.log(`Status: ${summary.status}`);
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
- console.log(`Needs human: ${summary.needsHuman}`);
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(` id: ${summary.intent.id}`);
1791
- console.log(` task: ${summary.intent.task}`);
1792
- console.log(` target: ${summary.intent.targetFile}`);
1793
- console.log(` control mode: ${summary.intent.controlMode}`);
1794
- console.log(` human gate: ${summary.intent.humanGate}`);
1795
- console.log(` boundary risk: ${summary.intent.boundaryRisk}`);
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("Audit:");
1798
- console.log(` status: ${summary.auditStatus}`);
1799
- console.log(` decision: ${summary.auditDecision}`);
1800
- console.log(` approval status: ${summary.approvalStatus}`);
1801
- console.log("");
1802
- printHumanList("Why:", summary.why);
1803
- printHumanList("Fix now:", summary.fixNow);
1804
- printHumanList("Ask human:", summary.askHuman);
1805
- printHumanList("Changed files:", summary.changedFiles);
1806
- printHumanList("Verify:", summary.verificationTargets);
1807
- const commands = [
1808
- ...summary.commands.doctor,
1809
- ...summary.commands.check,
1810
- ...summary.commands.audit,
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.unstage,
1814
- ];
1815
- printHumanList("Commands:", commands);
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 = new core_1.GraphEngine(workspaceRoot);
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 = new core_1.GraphEngine(input.workspaceRoot);
2436
+ const engine = createFastCheckEngine(input.workspaceRoot);
2310
2437
  try {
2311
- await runWithQuietEngine(() => engine.initialScan());
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 = new core_1.GraphEngine(input.workspaceRoot);
2466
+ const engine = createFastCheckEngine(input.workspaceRoot);
2340
2467
  try {
2341
- await runWithQuietEngine(() => engine.initialScan());
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 = new core_1.GraphEngine(workspaceRoot);
2493
+ const engine = createFastCheckEngine(workspaceRoot);
2367
2494
  try {
2368
- await runWithQuietEngine(() => engine.initialScan());
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 = new core_1.GraphEngine(workspaceRoot);
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
- `# ${files[0].path}`,
2673
- files[0].content.trimEnd(),
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) => writeInitFile(file, options.force));
2682
- const engine = new core_1.GraphEngine(workspaceRoot);
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 engine = new core_1.GraphEngine(workspaceRoot);
3039
+ const intent = (0, core_1.loadChangeIntent)(workspaceRoot, options.intent ?? "latest");
3040
+ const engine = createFastCheckEngine(workspaceRoot);
2871
3041
  try {
2872
- await runWithQuietEngine(() => engine.initialScan());
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 = new core_1.GraphEngine(workspaceRoot);
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 = new core_1.GraphEngine(workspaceRoot);
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 = new core_1.GraphEngine(workspaceRoot);
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
- contextGenerated: fs.existsSync(workflowPath),
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 = new core_1.GraphEngine(workspaceRoot);
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 = new core_1.GraphEngine(workspaceRoot);
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 = new core_1.GraphEngine(workspaceRoot);
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 = new core_1.GraphEngine(workspaceRoot);
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;