geotechcli 0.4.77 → 0.4.79

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ai.d.ts","sourceRoot":"","sources":["../../src/commands/ai.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4nDpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmU5D;AAMD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwChE;AAMD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgDzD;AA4GD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuX3D;AAMD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0J1D;AAMD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuF5D"}
1
+ {"version":3,"file":"ai.d.ts","sourceRoot":"","sources":["../../src/commands/ai.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAw3DpC,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmU5D;AAMD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwChE;AAMD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAgDzD;AA4GD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuX3D;AAMD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0J1D;AAMD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAuF5D"}
@@ -3,7 +3,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } fr
3
3
  import { join, relative, resolve } from 'node:path';
4
4
  import chalk from 'chalk';
5
5
  import ora from 'ora';
6
- import { buildLLMConfig, DEFAULT_LLM_VISION_MODEL, analyzeCoreBox, classifyRMRFromImage, classifySoilFromDescription, ingestBoreholeLogDocument, inspectPdfDocument, queryGBRDocument, interpretSensorImage, runAgent, runSwarm, AgentConversation, loadProject, getProjectAgentContext, addAgentSession, addArtifact, addNote, saveNamedDataset, saveDerivedParameter, setActiveAnalysisContext, generateReport, generateReportFromCaseFile, buildProjectWorkflowReport, renderReportAsPdf, renderReportAsDocx, buildSwarmSessionProjectRecord, persistSwarmCaseFile, persistCaseFileEvidence, analyzeWorkspace, resolveWorkspaceRoot, buildProjectWorkflowRouterPrompt, generateText, routeProjectWorkflowRequest, runProjectWorkflow, } from '@geotechcli/core';
6
+ import { buildLLMConfig, DEFAULT_LLM_VISION_MODEL, analyzeCoreBox, classifyRMRFromImage, classifySoilFromDescription, ingestBoreholeLogDocument, inspectPdfDocument, queryGBRDocument, interpretSensorImage, runAgent, runSwarm, AgentConversation, loadProject, getProjectAgentContext, addAgentSession, addArtifact, addNote, saveNamedDataset, saveDerivedParameter, setActiveAnalysisContext, generateReport, generateReportFromCaseFile, buildProjectWorkflowReport, renderReportAsPdf, renderReportAsDocx, buildSwarmSessionProjectRecord, persistSwarmCaseFile, persistCaseFileEvidence, analyzeWorkspace, resolveWorkspaceRoot, buildProjectWorkflowRouterPrompt, generateText, routeProjectWorkflowRequest, runProjectWorkflow, analyzeSignalFile, } from '@geotechcli/core';
7
7
  import { heading, keyValue, renderJSON, renderRichText, success, error, warn, renderTable, info } from '../ui/terminal.js';
8
8
  import { addGlobalFlags, getGlobalFlags } from '../util/flags.js';
9
9
  import { estimateHostedBetaVisionBodyBytes, formatByteSize, HOSTED_BETA_REQUEST_LIMIT_BYTES, readVisionInput, readVisionPdfPageInputs, resolveStructuredOutputTarget, HOSTED_BETA_REQUEST_SAFE_BYTES, } from '../util/vision-output.js';
@@ -101,6 +101,11 @@ const PROJECT_AGENT_TASKS = [
101
101
  label: 'Preliminary recommendations',
102
102
  prompt: 'Prepare preliminary geotechnical recommendations from the attached workspace manifest. Clearly mark what is evidence-backed, assumption-bound, or blocked by missing inputs.',
103
103
  },
104
+ {
105
+ task: 'signal-analysis',
106
+ label: 'Signal analysis',
107
+ prompt: 'Route monitoring, instrumentation, settlement, piezometer, inclinometer, vibration, and load-test files into deterministic geotech signal analyze commands. Summarize available sources, missing threshold assumptions, and review gates without inventing signal metrics.',
108
+ },
104
109
  {
105
110
  task: 'visualization',
106
111
  label: 'Visualizations and maps',
@@ -164,6 +169,26 @@ function normalizeProjectTask(value) {
164
169
  case 'fem-foundation-settlement':
165
170
  case 'fem-excavation':
166
171
  case 'fem-excavation-deformation':
172
+ case 'fem-tunnel':
173
+ case 'fem-tunnel-settlement':
174
+ case 'fem-tunnel-volume-loss-settlement':
175
+ case 'fem-shaft':
176
+ case 'fem-shaft-deformation':
177
+ case 'fem-pile-group':
178
+ case 'fem-pile-group-elastic-interaction':
179
+ case 'fem-slope':
180
+ case 'fem-slope-embankment':
181
+ case 'fem-slope-embankment-deformation':
182
+ case 'fem-embankment':
183
+ case 'fem-retaining-wall':
184
+ case 'fem-retaining-wall-excavation-support':
185
+ case 'fem-excavation-support':
186
+ case 'fem-seepage':
187
+ case 'fem-seepage-groundwater-coupling':
188
+ case 'fem-groundwater-coupling':
189
+ case 'fem-staged-settlement':
190
+ case 'fem-staged-settlement-consolidation':
191
+ case 'fem-consolidation':
167
192
  return 'calculation-readiness';
168
193
  case 'risk':
169
194
  case 'risk-analysis':
@@ -176,6 +201,24 @@ function normalizeProjectTask(value) {
176
201
  case 'recommendations':
177
202
  case 'foundation-recommendations':
178
203
  return 'recommendations';
204
+ case 'signal':
205
+ case 'signals':
206
+ case 'signal-analysis':
207
+ case 'signal-analytics':
208
+ case 'monitoring':
209
+ case 'monitoring-analysis':
210
+ case 'time-series':
211
+ case 'timeseries':
212
+ case 'instrumentation':
213
+ case 'piezometer':
214
+ case 'piezometers':
215
+ case 'inclinometer':
216
+ case 'inclinometers':
217
+ case 'vibration':
218
+ case 'load-test':
219
+ case 'load-tests':
220
+ case 'pile-load-test':
221
+ return 'signal-analysis';
179
222
  case 'viz':
180
223
  case 'visualize':
181
224
  case 'visualization':
@@ -227,10 +270,11 @@ function inferProjectIntent(rawPrompt, selectedTask) {
227
270
  };
228
271
  add('data-quality', /\b(?:data quality|inventory|missing data|quality report|duplicate)\b/);
229
272
  add('ground-model', /\b(?:ground model|interpret|strata|stratigraphy|lithology|hydrogeology)\b/);
230
- add('calculation-readiness', /\b(?:calculation readiness|calculation route|calculation routing|design readiness|design route|bearing(?: capacity| calculation| readiness)?|settlement(?: calculation| readiness)?|pile(?: capacity| calculation| readiness)?|liquefaction(?: calculation| readiness)?|slope(?: stability| calculation| readiness)?|fem(?: draft| readiness| foundation settlement| excavation deformation)?|ready for calculation|ready for design)\b/);
273
+ add('calculation-readiness', /\b(?:calculation readiness|calculation route|calculation routing|design readiness|design route|bearing(?: capacity| calculation| readiness)?|settlement(?: calculation| readiness| design| route| workflow)|pile(?: capacity| calculation| readiness)?|liquefaction(?: calculation| readiness)?|slope(?: stability| calculation| readiness)?|fem(?: draft| readiness| foundation settlement| excavation deformation)?|ready for calculation|ready for design)\b/);
231
274
  add('risk-analysis', /\b(?:risk|hazard|limitation|uncertainty|mitigation)\b/);
232
275
  add('anomaly-detection', /\b(?:anomal\w*|conflict|outlier|inconsistent|inconsistency)\b/);
233
276
  add('recommendations', /\b(?:recommend|foundation option|advice|next action)\b/);
277
+ add('signal-analysis', /\b(?:signal analysis|signal analytics|monitoring analysis|time[-\s]?series|instrumentation|piezometer|pore pressure|inclinometer|vibration|accelerometer|settlement monitoring|monitoring trend|threshold|trigger level|load[-\s]?test)\b/);
234
278
  add('visualization', /\b(?:visual\w*|map|plot|chart|section|profile|strip log)\b/);
235
279
  const uniqueTasks = [...new Set(tasks)];
236
280
  if (uniqueTasks.length === 0) {
@@ -254,6 +298,9 @@ function projectReadinessFromManifest(manifest) {
254
298
  const hasGroundModel = Boolean(manifest.groundModel && manifest.groundModel.stats.evidenceRefs > 0);
255
299
  const hasCoordinates = Boolean(manifest.groundModel?.map?.points?.length);
256
300
  const hasVerifier = Boolean(manifest.verifier);
301
+ const signalSources = (manifest.summary.datasetTypes['monitoring-time-series'] ?? 0)
302
+ + (manifest.summary.datasetTypes['signal-record'] ?? 0)
303
+ + (manifest.summary.datasetTypes['pile-load-test'] ?? 0);
257
304
  const calculationWorkflows = manifest.verifier?.calculationReadiness.workflows ?? [];
258
305
  const readyWorkflowCount = calculationWorkflows.filter((workflow) => workflow.status !== 'blocked').length;
259
306
  const verifierMissing = calculationWorkflows.flatMap((workflow) => workflow.missing).slice(0, 8);
@@ -313,6 +360,15 @@ function projectReadinessFromManifest(manifest) {
313
360
  : ['No downstream calculation workflow is ready yet.'],
314
361
  missing: verifierMissing,
315
362
  },
363
+ {
364
+ task: 'signal-analysis',
365
+ label: 'Signal analysis',
366
+ status: signalSources > 0 ? 'partially_ready' : 'blocked',
367
+ reasons: signalSources > 0
368
+ ? [`${signalSources} monitoring/signal/load-test source(s) can be routed into deterministic signal analysis.`]
369
+ : ['Signal analysis needs settlement, piezometer, inclinometer, vibration, load-test, or time-series data.'],
370
+ missing: signalSources > 0 ? ['project-specific thresholds / trigger levels'] : ['monitoring/signal CSV, TSV, or XLSX files'],
371
+ },
316
372
  {
317
373
  task: 'visualization',
318
374
  label: 'Visualizations and maps',
@@ -529,6 +585,177 @@ function relativeArtifactPath(rootPath, filePath) {
529
585
  const rel = relative(rootPath, filePath);
530
586
  return rel && !rel.startsWith('..') ? rel : filePath;
531
587
  }
588
+ const SIGNAL_WORKFLOW_DATASET_TYPES = new Set(['monitoring-time-series', 'signal-record', 'pile-load-test']);
589
+ function detectSignalWorkflowSources(manifest) {
590
+ const sources = [];
591
+ const seen = new Set();
592
+ for (const file of manifest.files) {
593
+ const schemas = file.schemas ?? [];
594
+ const matchingSchemas = schemas.filter((schema) => SIGNAL_WORKFLOW_DATASET_TYPES.has(schema.datasetType));
595
+ if (matchingSchemas.length > 0) {
596
+ for (const schema of matchingSchemas) {
597
+ const key = `${file.absolutePath}#${schema.sheetName ?? ''}`;
598
+ if (seen.has(key))
599
+ continue;
600
+ seen.add(key);
601
+ sources.push({
602
+ file,
603
+ label: schema.sheetName ? `${file.path}#${schema.sheetName}` : file.path,
604
+ signalType: inferSignalWorkflowType(file, schema),
605
+ sheetName: schema.sheetName,
606
+ });
607
+ }
608
+ continue;
609
+ }
610
+ if (SIGNAL_WORKFLOW_DATASET_TYPES.has(file.classification.datasetType)) {
611
+ const key = `${file.absolutePath}#`;
612
+ if (seen.has(key))
613
+ continue;
614
+ seen.add(key);
615
+ sources.push({
616
+ file,
617
+ label: file.path,
618
+ signalType: inferSignalWorkflowType(file),
619
+ });
620
+ }
621
+ }
622
+ return sources;
623
+ }
624
+ function inferSignalWorkflowType(file, schema) {
625
+ if (schema?.datasetType === 'pile-load-test' || file.classification.datasetType === 'pile-load-test')
626
+ return 'load-test';
627
+ const roles = new Set(schema?.columns.flatMap((column) => column.roles) ?? []);
628
+ if (roles.has('settlement'))
629
+ return 'settlement';
630
+ if (roles.has('pore_pressure'))
631
+ return 'piezometer';
632
+ if (roles.has('inclination'))
633
+ return 'inclinometer';
634
+ if (roles.has('vibration'))
635
+ return 'vibration';
636
+ const text = [file.path, file.classification.datasetType, ...file.classification.signals].join(' ').toLowerCase();
637
+ if (/\b(load[-_\s]?test|pile[-_\s]?load)\b/.test(text))
638
+ return 'load-test';
639
+ if (/\b(settlement|heave|subsidence)\b/.test(text))
640
+ return 'settlement';
641
+ if (/\b(piezometer|pore[-_\s]?pressure|groundwater|water[-_\s]?level)\b/.test(text))
642
+ return 'piezometer';
643
+ if (/\b(inclinometer|inclination|tilt|deflection)\b/.test(text))
644
+ return 'inclinometer';
645
+ if (/\b(vibration|accelerometer|seismic|fft|psd)\b/.test(text))
646
+ return 'vibration';
647
+ return 'unknown';
648
+ }
649
+ function signalArtifactSlug(value) {
650
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 64) || 'signal';
651
+ }
652
+ async function persistSignalAnalysisArtifacts(options) {
653
+ if (options.workflowRun.task !== 'signal-analysis')
654
+ return;
655
+ const sources = detectSignalWorkflowSources(options.manifest).slice(0, 12);
656
+ if (sources.length === 0)
657
+ return;
658
+ const signalDir = join(options.runDir, 'signals');
659
+ mkdirSync(signalDir, { recursive: true });
660
+ const indexEntries = [];
661
+ let successCount = 0;
662
+ let failureCount = 0;
663
+ for (const [index, source] of sources.entries()) {
664
+ const artifactPath = join(signalDir, `${String(index + 1).padStart(2, '0')}-${signalArtifactSlug(source.label)}.analysis.json`);
665
+ const analyzeOptions = {
666
+ ...(source.signalType !== 'unknown' ? { type: source.signalType } : {}),
667
+ ...(source.sheetName ? { sheetName: source.sheetName } : {}),
668
+ maxRows: 5000,
669
+ };
670
+ try {
671
+ const result = await analyzeSignalFile(source.file.absolutePath, analyzeOptions);
672
+ writeFileSync(artifactPath, JSON.stringify(result, null, 2), 'utf-8');
673
+ successCount += 1;
674
+ const relativePath = relativeArtifactPath(options.plan.workspace.rootPath, artifactPath);
675
+ const status = result.warnings.length > 0 ? 'review' : 'pass';
676
+ indexEntries.push({
677
+ source: source.file.path,
678
+ absolutePath: source.file.absolutePath,
679
+ sheetName: source.sheetName,
680
+ status,
681
+ signalType: result.signalType,
682
+ rowsAnalyzed: result.source.rowsAnalyzed,
683
+ rowsRejected: result.source.rowsRejected,
684
+ series: result.series.length,
685
+ thresholdFlags: result.thresholdFlags.length,
686
+ missingIntervals: result.missingIntervals.length,
687
+ warnings: result.warnings,
688
+ artifact: relativePath,
689
+ });
690
+ options.workflowRun.artifacts.push({
691
+ kind: 'json',
692
+ path: relativePath,
693
+ description: `Deterministic signal analysis for ${source.label}`,
694
+ });
695
+ options.workflowRun.trace.steps.push({
696
+ type: 'tool_call',
697
+ name: 'signal.analyze_file',
698
+ status,
699
+ detail: `Analyzed ${source.label} into ${relativePath}.`,
700
+ });
701
+ options.workflowRun.toolCalls.push({
702
+ type: 'tool_call',
703
+ tool: 'signal.analyze_file',
704
+ status,
705
+ summary: `Analyzed ${source.label}: ${result.source.rowsAnalyzed} rows, ${result.series.length} series, ${result.thresholdFlags.length} threshold flag(s), ${result.missingIntervals.length} missing interval(s).`,
706
+ });
707
+ }
708
+ catch (err) {
709
+ failureCount += 1;
710
+ const message = err instanceof Error ? err.message : String(err);
711
+ indexEntries.push({
712
+ source: source.file.path,
713
+ absolutePath: source.file.absolutePath,
714
+ sheetName: source.sheetName,
715
+ status: 'blocked',
716
+ signalType: source.signalType,
717
+ error: message,
718
+ });
719
+ options.workflowRun.trace.steps.push({
720
+ type: 'tool_call',
721
+ name: 'signal.analyze_file',
722
+ status: 'blocked',
723
+ detail: `Could not analyze ${source.label}: ${message}`,
724
+ });
725
+ options.workflowRun.toolCalls.push({
726
+ type: 'tool_call',
727
+ tool: 'signal.analyze_file',
728
+ status: 'blocked',
729
+ summary: `Could not analyze ${source.label}: ${message}`,
730
+ });
731
+ }
732
+ }
733
+ if (failureCount > 0) {
734
+ options.workflowRun.status = successCount > 0 && options.workflowRun.status !== 'blocked' ? 'review' : 'blocked';
735
+ }
736
+ const indexPath = join(signalDir, 'index.json');
737
+ const indexPayload = {
738
+ schemaVersion: 'geotech.signal-analysis-artifact-index.v1',
739
+ generatedAt: options.workflowRun.generatedAt,
740
+ runId: options.workflowRun.runId,
741
+ task: options.workflowRun.task,
742
+ sources: indexEntries,
743
+ };
744
+ writeFileSync(indexPath, JSON.stringify(indexPayload, null, 2), 'utf-8');
745
+ const relativeIndexPath = relativeArtifactPath(options.plan.workspace.rootPath, indexPath);
746
+ options.workflowRun.artifacts.push({
747
+ kind: 'json',
748
+ path: relativeIndexPath,
749
+ description: 'Deterministic signal analysis artifact index',
750
+ });
751
+ options.workflowRun.summary.push(`Signal analyses persisted: ${successCount}/${sources.length} source(s) under ${relativeArtifactPath(options.plan.workspace.rootPath, signalDir)}.`);
752
+ options.workflowRun.toolCalls.push({
753
+ type: 'tool_call',
754
+ tool: 'signal.analysis_artifacts',
755
+ status: failureCount > 0 ? 'review' : 'pass',
756
+ summary: `Persisted ${successCount}/${sources.length} deterministic signal analysis artifact(s).`,
757
+ });
758
+ }
532
759
  async function requestProjectWorkflowRouteProposal(options) {
533
760
  const routePrompt = buildProjectWorkflowRouterPrompt({
534
761
  prompt: options.prompt,
@@ -612,7 +839,7 @@ function renderProjectAwarePlan(plan, flags) {
612
839
  }
613
840
  console.log('');
614
841
  warn('No model-heavy workflow has run yet. Choose a task with --task, or ask a project question with --workspace.');
615
- console.log(chalk.gray(' Next actions: geotech agent --task data-quality | --task ground-model | --task calculation-readiness | --task risk-analysis | --task anomaly-detection | --task recommendations | --task visualization'));
842
+ console.log(chalk.gray(' Next actions: geotech agent --task data-quality | --task ground-model | --task calculation-readiness | --task risk-analysis | --task anomaly-detection | --task recommendations | --task signal-analysis | --task visualization'));
616
843
  success(`Project state written to ${relativeArtifactPath(plan.workspace.rootPath, join(plan.workspace.rootPath, '.geotech'))}`);
617
844
  }
618
845
  async function renderAndPersistProjectWorkflow(plan, manifest, task, flags) {
@@ -622,11 +849,12 @@ async function renderAndPersistProjectWorkflow(plan, manifest, task, flags) {
622
849
  runId: plan.runId,
623
850
  now: plan.project.generatedAt,
624
851
  });
625
- const report = buildProjectWorkflowReport(workflowRun);
626
852
  const runDir = join(plan.workspace.rootPath, '.geotech', 'runs', plan.runId);
627
853
  const resultPath = join(runDir, 'workflow_result.json');
628
854
  const reportPath = join(runDir, 'workflow_report.md');
629
855
  const tracePath = join(runDir, 'workflow_trace.json');
856
+ await persistSignalAnalysisArtifacts({ plan, manifest, workflowRun, runDir });
857
+ const report = buildProjectWorkflowReport(workflowRun);
630
858
  writeFileSync(resultPath, JSON.stringify(workflowRun, null, 2), 'utf-8');
631
859
  writeFileSync(reportPath, report.fullMarkdown, 'utf-8');
632
860
  writeFileSync(tracePath, JSON.stringify(workflowRun.trace, null, 2), 'utf-8');
@@ -699,12 +927,13 @@ async function renderAndPersistProjectWorkflowRoute(plan, manifest, route, flags
699
927
  runId: workflowRunId,
700
928
  now: plan.project.generatedAt,
701
929
  });
702
- const report = buildProjectWorkflowReport(workflowRun);
703
930
  const runDir = join(plan.workspace.rootPath, '.geotech', 'runs', workflowRun.runId);
704
931
  mkdirSync(runDir, { recursive: true });
705
932
  const resultPath = join(runDir, 'workflow_result.json');
706
933
  const reportPath = join(runDir, 'workflow_report.md');
707
934
  const tracePath = join(runDir, 'workflow_trace.json');
935
+ await persistSignalAnalysisArtifacts({ plan, manifest, workflowRun, runDir });
936
+ const report = buildProjectWorkflowReport(workflowRun);
708
937
  writeFileSync(resultPath, JSON.stringify(workflowRun, null, 2), 'utf-8');
709
938
  writeFileSync(reportPath, report.fullMarkdown, 'utf-8');
710
939
  writeFileSync(tracePath, JSON.stringify(workflowRun.trace, null, 2), 'utf-8');
@@ -1830,14 +2059,14 @@ function renderSwarmStep(step, json, quiet = false) {
1830
2059
  }
1831
2060
  export function registerAgentCommand(program) {
1832
2061
  const cmd = new Command('agent')
1833
- .description('Agentic AI - reasons about your problem and executes real calculations')
2062
+ .description('Agentic AI - routes evidence into deterministic tools; FEM execution remains human-invoked and experimental')
1834
2063
  .argument('[task...]', 'Engineering task in natural language')
1835
2064
  .option('--swarm', 'Use the role-based multi-agent swarm planner and specialist review loop')
1836
2065
  .option('--skills', 'Enable installed skill tools for this session')
1837
2066
  .option('--project <id>', 'Load and persist context to a stored project')
1838
2067
  .option('--workspace <dir>', 'Scan a local workspace and attach its manifest summary to the agent task')
1839
2068
  .option('--no-workspace', 'Disable automatic project-aware workspace discovery')
1840
- .option('--task <task>', 'Run a project-aware task: data-quality, ground-model, calculation-readiness, risk-analysis, anomaly-detection, recommendations, visualization')
2069
+ .option('--task <task>', 'Run a project-aware task: data-quality, ground-model, calculation-readiness, risk-analysis, anomaly-detection, recommendations, signal-analysis, visualization')
1841
2070
  .option('--route-with-model', 'Let the configured LLM propose a validated workflow route when deterministic routing needs selection')
1842
2071
  .option('--plan-only', 'Scan the workspace, write .geotech project state, and show workflow readiness without calling an LLM')
1843
2072
  .option('--refresh', 'Refresh the deterministic workspace manifest and .geotech project state')