@getripple/cli 1.0.4 → 1.0.6

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) {
@@ -933,13 +942,51 @@ async function runWithQuietEngine(task) {
933
942
  console.log = originalLog;
934
943
  }
935
944
  }
945
+ function createCliEngine(workspaceRoot, contextMode = "lean") {
946
+ const engine = new core_1.GraphEngine(workspaceRoot);
947
+ engine.setContextGenerationMode(contextMode);
948
+ return engine;
949
+ }
950
+ function createFastCheckEngine(workspaceRoot) {
951
+ const engine = createCliEngine(workspaceRoot, "lean");
952
+ return engine;
953
+ }
954
+ function fastCheckCandidateFiles(files, intent) {
955
+ const intentSymbolFiles = (intent?.allowedSymbols ?? [])
956
+ .map((symbolId) => symbolId.split("::")[0])
957
+ .filter(Boolean);
958
+ return uniqueItems([
959
+ ...files,
960
+ ...(intent
961
+ ? [
962
+ intent.targetFile,
963
+ ...intent.editableFiles,
964
+ ...intent.allowedFiles,
965
+ ...intent.expectedFiles,
966
+ ...intent.contextFiles,
967
+ ...intentSymbolFiles,
968
+ ]
969
+ : []),
970
+ ].filter((file) => Boolean(file)));
971
+ }
936
972
  function printScanSummary(summary) {
937
973
  console.log("Ripple scan complete");
938
974
  console.log(`Workspace: ${summary.workspace}`);
939
975
  console.log(`Files: ${summary.files}`);
940
976
  console.log(`Symbols: ${summary.symbols}`);
941
977
  console.log(`Call edges: ${summary.callEdges}`);
942
- console.log(`Context: .ripple/ ${summary.contextGenerated ? "generated" : "not generated"}`);
978
+ console.log(`Mode: ${summary.contextMode}`);
979
+ console.log(`Graph cache: ${summary.cacheGenerated ? summary.cachePath : "not generated"}`);
980
+ console.log(`Context bundle: ${summary.contextGenerated ? summary.contextPath : "not generated by lean scan"}`);
981
+ }
982
+ function printWorkflowSummary(summary) {
983
+ console.log("Ripple workflow generated");
984
+ console.log(`Workspace: ${summary.workspace}`);
985
+ console.log(`Path: ${summary.path}`);
986
+ console.log(`Context bundle: ${summary.contextGenerated ? "generated" : "missing"}`);
987
+ console.log(`Focus files: ${summary.focusFileCount}`);
988
+ console.log("");
989
+ printHumanList("Next:", summary.nextSteps);
943
990
  }
944
991
  function printDoctorSummary(summary) {
945
992
  console.log("Ripple doctor");
@@ -951,6 +998,7 @@ function printDoctorSummary(summary) {
951
998
  console.log(`Agent trust: ${summary.adapterSupport.primaryAdapter.agentPolicy.canTrust.join(", ")}`);
952
999
  console.log(`Graph: ${summary.checks.graph.ok ? "ok" : "missing"} - ${summary.checks.graph.detail}`);
953
1000
  console.log(`Git: ${summary.checks.git.ok ? "ok" : "missing"} - ${summary.checks.git.detail}`);
1001
+ console.log(`Git ignore: ${summary.checks.gitIgnore.ok ? "ok" : "missing"} - ${summary.checks.gitIgnore.detail}`);
954
1002
  console.log(`CI workflow: ${summary.checks.ciWorkflow.ok ? "ok" : "missing"} - ${summary.checks.ciWorkflow.detail}`);
955
1003
  console.log(`Latest intent: ${summary.checks.latestIntent.ok ? "ok" : "missing"} - ${summary.checks.latestIntent.detail}`);
956
1004
  console.log("");
@@ -993,7 +1041,10 @@ function printInitSummary(summary) {
993
1041
  function printAgentDoctorSummary(summary) {
994
1042
  console.log("RIPPLE_DOCTOR");
995
1043
  console.log(`status: ${summary.status}`);
996
- console.log(`readiness_decision: ${summary.status === "ready" ? "continue" : "setup-required"}`);
1044
+ console.log(`decision: ${summary.decision}`);
1045
+ console.log(`can_continue: ${summary.canContinue}`);
1046
+ console.log(`must_stop: ${summary.mustStop}`);
1047
+ console.log(`next_required_action: ${summary.nextRequiredAction}`);
997
1048
  console.log(`workspace: ${summary.workspace}`);
998
1049
  console.log(`adapter: ${summary.adapterSupport.primaryAdapter.id}`);
999
1050
  console.log(`adapter_support: ${summary.adapterSupport.supportLevel}`);
@@ -1006,10 +1057,13 @@ function printAgentDoctorSummary(summary) {
1006
1057
  console.log(`policy_detail: ${summary.enforcement.explicitPolicy.detail}`);
1007
1058
  console.log(`graph: ${summary.checks.graph.ok ? "ok" : "missing"} - ${summary.checks.graph.detail}`);
1008
1059
  console.log(`git: ${summary.checks.git.ok ? "ok" : "missing"} - ${summary.checks.git.detail}`);
1060
+ console.log(`git_ignore: ${summary.checks.gitIgnore.ok ? "ok" : "missing"} - ${summary.checks.gitIgnore.detail}`);
1009
1061
  console.log(`ci_workflow: ${summary.checks.ciWorkflow.ok ? "ok" : "missing"} - ${summary.checks.ciWorkflow.detail}`);
1010
1062
  console.log(`latest_intent: ${summary.checks.latestIntent.ok ? "ok" : "missing"} - ${summary.checks.latestIntent.detail}`);
1011
1063
  console.log("");
1012
- printAgentList("gaps", summary.enforcement.gaps);
1064
+ printAgentList("why", summary.why);
1065
+ console.log("");
1066
+ printAgentList("fix_now", summary.fixNow);
1013
1067
  console.log("");
1014
1068
  printAgentList("next_steps", summary.nextSteps);
1015
1069
  }
@@ -1776,43 +1830,80 @@ function printAgentGateSummary(summary) {
1776
1830
  printAgentList("commands_verify", summary.commands.verify);
1777
1831
  }
1778
1832
  function printGateSummary(summary) {
1779
- console.log("Ripple gate");
1780
- console.log(`Status: ${summary.status}`);
1833
+ const statusLabel = summary.canContinue ? "CONTINUE" : "STOP";
1834
+ console.log(`Ripple gate: ${statusLabel}`);
1835
+ console.log(gateHeadline(summary));
1836
+ console.log("");
1781
1837
  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}`);
1838
+ console.log(`Can continue: ${formatYesNo(summary.canContinue)}`);
1839
+ console.log(`Must stop: ${formatYesNo(summary.mustStop)}`);
1788
1840
  console.log("");
1789
1841
  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}`);
1796
- 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,
1842
+ console.log(` Task: ${summary.intent.task}`);
1843
+ console.log(` Boundary: ${summary.intent.controlMode}`);
1844
+ console.log(` Target: ${summary.intent.targetFile}`);
1845
+ console.log(` Human gate: ${summary.intent.humanGate}`);
1846
+ console.log(` Approval: ${summary.approvalStatus}`);
1847
+ console.log("");
1848
+ printHumanList("Allowed:", gateAllowedItems(summary));
1849
+ const outsideBoundary = gateChangedOutsideItems(summary);
1850
+ printHumanList(outsideBoundary.length > 0 ? "Changed outside boundary:" : "Changed files:", outsideBoundary.length > 0 ? outsideBoundary : summary.changedFiles);
1851
+ printHumanList("Why:", compactGateReasons(summary));
1852
+ printHumanList("Fix now:", compactGateFixes(summary));
1853
+ if (summary.canContinue) {
1854
+ printHumanList("Verify:", summary.verificationTargets.slice(0, 8));
1855
+ }
1856
+ else {
1857
+ printHumanList("Commands:", compactGateCommands(summary));
1858
+ }
1859
+ }
1860
+ function gateHeadline(summary) {
1861
+ if (summary.canContinue) {
1862
+ return "Agent may continue after running the listed verification targets.";
1863
+ }
1864
+ if (summary.decision === "restore-readiness") {
1865
+ return "Agent must stop because Ripple readiness is weaker than the saved intent.";
1866
+ }
1867
+ if (summary.needsHuman) {
1868
+ return "Agent must stop and ask the human before continuing.";
1869
+ }
1870
+ return "Agent must stop and repair the staged change before continuing.";
1871
+ }
1872
+ function gateAllowedItems(summary) {
1873
+ if (summary.intent.controlMode === "brainstorm") {
1874
+ return ["no file edits"];
1875
+ }
1876
+ if (summary.allowedSymbols.length > 0) {
1877
+ return summary.allowedSymbols;
1878
+ }
1879
+ if (summary.allowedFiles.length > 0) {
1880
+ return summary.allowedFiles;
1881
+ }
1882
+ return [summary.intent.targetFile];
1883
+ }
1884
+ function gateChangedOutsideItems(summary) {
1885
+ return [
1886
+ ...summary.changedOutsideBoundaryFiles.map((file) => `file: ${file}`),
1887
+ ...summary.changedOutsideBoundarySymbols.map((symbol) => `symbol: ${symbol}`),
1888
+ ];
1889
+ }
1890
+ function compactGateReasons(summary) {
1891
+ return uniqueItems(summary.why).slice(0, 6);
1892
+ }
1893
+ function compactGateFixes(summary) {
1894
+ return uniqueItems(summary.fixNow).slice(0, 6);
1895
+ }
1896
+ function compactGateCommands(summary) {
1897
+ return uniqueItems([
1898
+ ...summary.commands.unstage,
1811
1899
  ...summary.commands.repair,
1812
1900
  ...summary.commands.approve,
1813
- ...summary.commands.unstage,
1814
- ];
1815
- printHumanList("Commands:", commands);
1901
+ ...summary.commands.plan,
1902
+ ...summary.commands.doctor,
1903
+ ]).slice(0, 6);
1904
+ }
1905
+ function formatYesNo(value) {
1906
+ return value ? "yes" : "no";
1816
1907
  }
1817
1908
  function printAuditSummary(summary) {
1818
1909
  const validation = summary.stagedCheck.intentValidation;
@@ -2232,7 +2323,7 @@ async function planCommand(options) {
2232
2323
  throw new Error("Missing target file. Usage: ripple plan --file <file> --task <task> [--budget N]");
2233
2324
  }
2234
2325
  const workspaceRoot = resolveWorkspaceRoot(".");
2235
- const engine = new core_1.GraphEngine(workspaceRoot);
2326
+ const engine = createCliEngine(workspaceRoot);
2236
2327
  try {
2237
2328
  await runWithQuietEngine(() => engine.initialScan());
2238
2329
  const summary = engine.planContext(options.task ?? "", options.file, options.budget);
@@ -2306,9 +2397,9 @@ function approvalStatusOutput(intent, status) {
2306
2397
  };
2307
2398
  }
2308
2399
  async function buildAuditForFiles(input) {
2309
- const engine = new core_1.GraphEngine(input.workspaceRoot);
2400
+ const engine = createFastCheckEngine(input.workspaceRoot);
2310
2401
  try {
2311
- await runWithQuietEngine(() => engine.initialScan());
2402
+ await runWithQuietEngine(() => engine.fastCheckScan(fastCheckCandidateFiles(input.files, input.intent)));
2312
2403
  const stagedCheck = (0, core_1.buildStagedCheckSummary)(engine, {
2313
2404
  workspaceRoot: input.workspaceRoot,
2314
2405
  stagedFiles: input.files,
@@ -2336,9 +2427,9 @@ async function buildAuditForFiles(input) {
2336
2427
  }
2337
2428
  }
2338
2429
  async function buildCheckSummaryForFiles(input) {
2339
- const engine = new core_1.GraphEngine(input.workspaceRoot);
2430
+ const engine = createFastCheckEngine(input.workspaceRoot);
2340
2431
  try {
2341
- await runWithQuietEngine(() => engine.initialScan());
2432
+ await runWithQuietEngine(() => engine.fastCheckScan(fastCheckCandidateFiles(input.files)));
2342
2433
  return (0, core_1.buildStagedCheckSummary)(engine, {
2343
2434
  workspaceRoot: input.workspaceRoot,
2344
2435
  stagedFiles: input.files,
@@ -2363,9 +2454,9 @@ async function checkCommand(options) {
2363
2454
  const checkFiles = options.changed
2364
2455
  ? (0, core_1.listGitChangedFiles)(workspaceRoot, baseRef)
2365
2456
  : (0, core_1.listGitStagedFiles)(workspaceRoot);
2366
- const engine = new core_1.GraphEngine(workspaceRoot);
2457
+ const engine = createFastCheckEngine(workspaceRoot);
2367
2458
  try {
2368
- await runWithQuietEngine(() => engine.initialScan());
2459
+ await runWithQuietEngine(() => engine.fastCheckScan(fastCheckCandidateFiles(checkFiles)));
2369
2460
  const stagedSummary = (0, core_1.buildStagedCheckSummary)(engine, {
2370
2461
  workspaceRoot,
2371
2462
  stagedFiles: checkFiles,
@@ -2614,7 +2705,7 @@ async function ciCommand(options) {
2614
2705
  }
2615
2706
  async function doctorCommand(options) {
2616
2707
  const workspaceRoot = resolveWorkspaceRoot(".");
2617
- const engine = new core_1.GraphEngine(workspaceRoot);
2708
+ const engine = createCliEngine(workspaceRoot);
2618
2709
  try {
2619
2710
  await runWithQuietEngine(() => engine.initialScan());
2620
2711
  const summary = (0, core_1.buildRippleReadinessSummary)(workspaceRoot, engine);
@@ -2638,6 +2729,7 @@ async function initCommand(options) {
2638
2729
  const policy = (0, core_1.defaultRipplePolicy)();
2639
2730
  const policyContents = (0, core_1.formatRipplePolicy)(policy);
2640
2731
  const workflow = githubActionsWorkflow();
2732
+ const gitignoreBlock = rippleGitIgnoreBlock();
2641
2733
  const files = [
2642
2734
  {
2643
2735
  path: core_1.RIPPLE_POLICY_PATH.split(path.sep).join("/"),
@@ -2649,6 +2741,12 @@ async function initCommand(options) {
2649
2741
  absolutePath: path.join(workspaceRoot, GITHUB_ACTIONS_WORKFLOW_PATH),
2650
2742
  content: workflow,
2651
2743
  },
2744
+ {
2745
+ path: core_1.RIPPLE_GITIGNORE_PATH,
2746
+ absolutePath: path.join(workspaceRoot, core_1.RIPPLE_GITIGNORE_PATH),
2747
+ content: gitignoreBlock,
2748
+ merge: true,
2749
+ },
2652
2750
  ];
2653
2751
  if (options.print) {
2654
2752
  const summary = {
@@ -2668,18 +2766,15 @@ async function initCommand(options) {
2668
2766
  printJson(summary);
2669
2767
  return;
2670
2768
  }
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"));
2769
+ process.stdout.write(files
2770
+ .flatMap((file) => [`# ${file.path}`, file.content.trimEnd(), ""])
2771
+ .join("\n"));
2679
2772
  return;
2680
2773
  }
2681
- const writtenFiles = files.map((file) => writeInitFile(file, options.force));
2682
- const engine = new core_1.GraphEngine(workspaceRoot);
2774
+ const writtenFiles = files.map((file) => file.merge
2775
+ ? writeRippleGitIgnoreFile(file.absolutePath)
2776
+ : writeInitFile(file, options.force));
2777
+ const engine = createCliEngine(workspaceRoot);
2683
2778
  try {
2684
2779
  await runWithQuietEngine(() => engine.initialScan());
2685
2780
  const readiness = (0, core_1.buildRippleReadinessSummary)(workspaceRoot, engine);
@@ -2721,6 +2816,44 @@ function writeInitFile(file, force) {
2721
2816
  overwritten: existed,
2722
2817
  };
2723
2818
  }
2819
+ function writeRippleGitIgnoreFile(targetPath) {
2820
+ const content = rippleGitIgnoreBlock();
2821
+ const existed = fs.existsSync(targetPath);
2822
+ if (!existed) {
2823
+ fs.writeFileSync(targetPath, content, "utf8");
2824
+ return {
2825
+ path: core_1.RIPPLE_GITIGNORE_PATH,
2826
+ status: "written",
2827
+ written: true,
2828
+ overwritten: false,
2829
+ };
2830
+ }
2831
+ const existing = fs.readFileSync(targetPath, "utf8");
2832
+ if (gitIgnoreContainsRippleCache(existing)) {
2833
+ return {
2834
+ path: core_1.RIPPLE_GITIGNORE_PATH,
2835
+ status: "exists",
2836
+ written: false,
2837
+ overwritten: false,
2838
+ };
2839
+ }
2840
+ const separator = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
2841
+ fs.writeFileSync(targetPath, `${existing}${separator}${content}`, "utf8");
2842
+ return {
2843
+ path: core_1.RIPPLE_GITIGNORE_PATH,
2844
+ status: "updated",
2845
+ written: true,
2846
+ overwritten: false,
2847
+ };
2848
+ }
2849
+ function gitIgnoreContainsRippleCache(contents) {
2850
+ return contents
2851
+ .split(/\r?\n/)
2852
+ .map((line) => line.trim().replace(/\\/g, "/").replace(/^\/+/, ""))
2853
+ .some((line) => line === core_1.RIPPLE_CACHE_GITIGNORE_ENTRY ||
2854
+ line === ".ripple/.cache" ||
2855
+ line === ".ripple/.cache/**");
2856
+ }
2724
2857
  function initCiCommand(options) {
2725
2858
  const workflow = githubActionsWorkflow();
2726
2859
  const workspaceRoot = resolveWorkspaceRoot(".");
@@ -2867,15 +3000,15 @@ function printAgentPolicyExplanation(explanation) {
2867
3000
  async function repairCommand(options) {
2868
3001
  const workspaceRoot = resolveWorkspaceRoot(".");
2869
3002
  const stagedFiles = (0, core_1.listGitStagedFiles)(workspaceRoot);
2870
- const engine = new core_1.GraphEngine(workspaceRoot);
3003
+ const intent = (0, core_1.loadChangeIntent)(workspaceRoot, options.intent ?? "latest");
3004
+ const engine = createFastCheckEngine(workspaceRoot);
2871
3005
  try {
2872
- await runWithQuietEngine(() => engine.initialScan());
3006
+ await runWithQuietEngine(() => engine.fastCheckScan(fastCheckCandidateFiles(stagedFiles, intent)));
2873
3007
  const stagedSummary = (0, core_1.buildStagedCheckSummary)(engine, {
2874
3008
  workspaceRoot,
2875
3009
  stagedFiles,
2876
3010
  tokenBudget: options.budget,
2877
3011
  });
2878
- const intent = (0, core_1.loadChangeIntent)(workspaceRoot, options.intent ?? "latest");
2879
3012
  const summary = (0, core_1.validateStagedCheckAgainstIntent)(stagedSummary, intent, {
2880
3013
  currentPolicyExplanation: currentPolicyExplanationForIntent(workspaceRoot, intent),
2881
3014
  currentReadinessSnapshot: currentReadinessSnapshotForEngine(workspaceRoot, engine),
@@ -2898,7 +3031,7 @@ async function repairCommand(options) {
2898
3031
  }
2899
3032
  async function historyCommand(options) {
2900
3033
  const workspaceRoot = resolveWorkspaceRoot(".");
2901
- const engine = new core_1.GraphEngine(workspaceRoot);
3034
+ const engine = createCliEngine(workspaceRoot);
2902
3035
  try {
2903
3036
  await runWithQuietEngine(() => engine.initialScan());
2904
3037
  const summary = engine.getRecentHistorySummary(options.last);
@@ -2921,7 +3054,7 @@ async function callersCommand(symbolId, options) {
2921
3054
  throw new Error("Symbol id must use <file>::<symbol>, for example src/auth.ts::validateToken");
2922
3055
  }
2923
3056
  const workspaceRoot = resolveWorkspaceRoot(".");
2924
- const engine = new core_1.GraphEngine(workspaceRoot);
3057
+ const engine = createCliEngine(workspaceRoot);
2925
3058
  try {
2926
3059
  await runWithQuietEngine(() => engine.initialScan());
2927
3060
  const summary = engine.getSymbolCallersSummary(symbolId);
@@ -2941,16 +3074,20 @@ async function callersCommand(symbolId, options) {
2941
3074
  }
2942
3075
  async function scanCommand(targetPath, options) {
2943
3076
  const workspaceRoot = resolveWorkspaceRoot(targetPath);
2944
- const engine = new core_1.GraphEngine(workspaceRoot);
3077
+ const engine = createCliEngine(workspaceRoot);
2945
3078
  try {
2946
3079
  await runWithQuietEngine(() => engine.initialScan());
3080
+ const cachePath = path.join(workspaceRoot, ".ripple", ".cache", "graph.cache.json");
2947
3081
  const workflowPath = path.join(workspaceRoot, ".ripple", "WORKFLOW.md");
2948
3082
  const summary = {
2949
3083
  workspace: workspaceRoot,
2950
3084
  files: engine.graph.files.size,
2951
3085
  symbols: engine.graph.symbols.size,
2952
3086
  callEdges: countCallEdges(engine),
2953
- contextGenerated: fs.existsSync(workflowPath),
3087
+ contextMode: "lean",
3088
+ cacheGenerated: fs.existsSync(cachePath),
3089
+ cachePath,
3090
+ contextGenerated: false,
2954
3091
  contextPath: workflowPath,
2955
3092
  };
2956
3093
  if (options.json) {
@@ -2964,12 +3101,66 @@ async function scanCommand(targetPath, options) {
2964
3101
  engine.dispose();
2965
3102
  }
2966
3103
  }
3104
+ async function workflowCommand(options) {
3105
+ const workspaceRoot = resolveWorkspaceRoot(".");
3106
+ const engine = createCliEngine(workspaceRoot, "full");
3107
+ try {
3108
+ await runWithQuietEngine(() => engine.initialScan());
3109
+ const workflowPath = path.join(workspaceRoot, ".ripple", "WORKFLOW.md");
3110
+ const contextDir = path.join(workspaceRoot, ".ripple", ".cache");
3111
+ const contextFiles = [
3112
+ ".ripple/.cache/context.json",
3113
+ ".ripple/.cache/context.files.json",
3114
+ ".ripple/.cache/context.symbols.json",
3115
+ ];
3116
+ const focusDir = path.join(contextDir, "focus");
3117
+ const focusFileCount = countJsonFiles(focusDir);
3118
+ const summary = {
3119
+ protocol: "ripple-workflow",
3120
+ version: 1,
3121
+ workspace: workspaceRoot,
3122
+ path: ".ripple/WORKFLOW.md",
3123
+ written: fs.existsSync(workflowPath),
3124
+ contextGenerated: contextFiles.every((filePath) => fs.existsSync(path.join(workspaceRoot, filePath))),
3125
+ contextFiles,
3126
+ focusFileCount,
3127
+ nextSteps: [
3128
+ "Copy .ripple/WORKFLOW.md into your agent instruction file if your agent does not use MCP.",
3129
+ "For MCP-capable agents, prefer ripple_plan_context and ripple_gate over reading generated files.",
3130
+ "Run ripple scan for a lean graph refresh when you do not need file-based agent instructions.",
3131
+ ],
3132
+ };
3133
+ if (!summary.written) {
3134
+ throw new Error("WORKFLOW.md was not generated. Check that the workspace has supported source files.");
3135
+ }
3136
+ if (options.json) {
3137
+ printJson(summary);
3138
+ }
3139
+ else {
3140
+ printWorkflowSummary(summary);
3141
+ }
3142
+ }
3143
+ finally {
3144
+ engine.dispose();
3145
+ }
3146
+ }
3147
+ function countJsonFiles(directoryPath) {
3148
+ try {
3149
+ return fs
3150
+ .readdirSync(directoryPath)
3151
+ .filter((fileName) => fileName.endsWith(".json"))
3152
+ .length;
3153
+ }
3154
+ catch {
3155
+ return 0;
3156
+ }
3157
+ }
2967
3158
  async function symbolsCommand(filePath, options) {
2968
3159
  if (!filePath) {
2969
3160
  throw new Error("Missing file path. Usage: ripple symbols <file>");
2970
3161
  }
2971
3162
  const workspaceRoot = resolveWorkspaceRoot(".");
2972
- const engine = new core_1.GraphEngine(workspaceRoot);
3163
+ const engine = createCliEngine(workspaceRoot);
2973
3164
  try {
2974
3165
  await runWithQuietEngine(() => engine.initialScan());
2975
3166
  const summary = engine.getFileSymbolsSummary(filePath);
@@ -2992,7 +3183,7 @@ async function blastCommand(filePath, options) {
2992
3183
  throw new Error("Missing file path. Usage: ripple blast <file>");
2993
3184
  }
2994
3185
  const workspaceRoot = resolveWorkspaceRoot(".");
2995
- const engine = new core_1.GraphEngine(workspaceRoot);
3186
+ const engine = createCliEngine(workspaceRoot);
2996
3187
  try {
2997
3188
  await runWithQuietEngine(() => engine.initialScan());
2998
3189
  const summary = engine.getBlastRadiusSummary(filePath);
@@ -3015,7 +3206,7 @@ async function dependencyCommand(filePath, direction, options) {
3015
3206
  throw new Error(`Missing file path. Usage: ripple ${direction} <file>`);
3016
3207
  }
3017
3208
  const workspaceRoot = resolveWorkspaceRoot(".");
3018
- const engine = new core_1.GraphEngine(workspaceRoot);
3209
+ const engine = createCliEngine(workspaceRoot);
3019
3210
  try {
3020
3211
  await runWithQuietEngine(() => engine.initialScan());
3021
3212
  const summary = engine.getFileDependencySummary(filePath);
@@ -3041,13 +3232,14 @@ async function focusCommand(filePath, options) {
3041
3232
  throw new Error("Missing file path. Usage: ripple focus <file>");
3042
3233
  }
3043
3234
  const workspaceRoot = resolveWorkspaceRoot(".");
3044
- const engine = new core_1.GraphEngine(workspaceRoot);
3235
+ const engine = createCliEngine(workspaceRoot, "on-demand");
3045
3236
  try {
3046
3237
  await runWithQuietEngine(() => engine.initialScan());
3047
3238
  const summary = engine.getFileFocusSummary(filePath);
3048
3239
  if (!summary) {
3049
3240
  throw new Error(`File is not in the Ripple graph: ${filePath}`);
3050
3241
  }
3242
+ engine.writeFileFocus(filePath);
3051
3243
  if (options.json) {
3052
3244
  printJson(summary);
3053
3245
  }
@@ -3087,6 +3279,10 @@ async function main() {
3087
3279
  await scanCommand(arg, options);
3088
3280
  return;
3089
3281
  }
3282
+ if (command === "workflow") {
3283
+ await workflowCommand(options);
3284
+ return;
3285
+ }
3090
3286
  if (command === "doctor") {
3091
3287
  await doctorCommand(options);
3092
3288
  return;