@moda-ai/cli 1.13.9 → 1.13.11

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/README.md CHANGED
@@ -58,9 +58,6 @@ moda tool-failures # Tool failure overview
58
58
  moda tool-failure-detail <tool_name> # Per-tool failure detail
59
59
  moda tool-failure-detail <tool_name> --include-window --window=1
60
60
 
61
- moda loops # Currently-active loop detections
62
- moda loop <detection_id> # One loop detail (signature + blast radius)
63
-
64
61
  # Prompt management
65
62
  moda prompts init # Create .moda/prompts.yml
66
63
  moda prompts status # Read-only local prompt status
@@ -75,7 +72,6 @@ All commands output JSON. Pipe to `jq` for filtering:
75
72
  ```bash
76
73
  moda overview | jq '.frustrations'
77
74
  moda frustrations | jq '.frustrations[].primary_cause'
78
- moda loops | jq '.loops | sort_by(-.cycle_visits) | .[0:5] | .[].loop_signature_human'
79
75
  ```
80
76
 
81
77
  ## Prompt Management
@@ -224,7 +224,7 @@ const productionEvidenceRefSchema = {
224
224
  id: { type: 'string' },
225
225
  kind: {
226
226
  type: 'string',
227
- enum: ['data_api', 'tool_failure', 'frustration', 'loop', 'run', 'harness', 'ask'],
227
+ enum: ['data_api', 'tool_failure', 'frustration', 'run', 'harness', 'ask'],
228
228
  },
229
229
  label: { type: 'string' },
230
230
  endpoint: { type: 'string' },
@@ -284,7 +284,6 @@ const productionFindingSchema = {
284
284
  type: 'string',
285
285
  enum: [
286
286
  'tool_failure',
287
- 'loop',
288
287
  'frustration',
289
288
  'partial_run',
290
289
  'prompt_regression',
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { z } from 'zod';
3
3
  import { validateConfig, VERSION, resolveApiKey } from './config.js';
4
4
  import { callDataAPI, ApiError, getApiRequestCount, resetApiRequestCount, } from './api-client.js';
5
- import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, SearchSchema, WorldStateSchema, ContextSchema, AuditSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, LoopsSchema, LoopDetailSchema, } from './schemas.js';
5
+ import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, SearchSchema, WorldStateSchema, ContextSchema, AuditSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, } from './schemas.js';
6
6
  import { withFrustrationAnchors, withToolFailureAnchors, embeddedWindowSize, asRecord, asString, asNumber } from './transforms.js';
7
7
  import { runPromptsCommand } from './prompts.js';
8
8
  // Coding-agent capture (shadow workspace / Shadow Spark / trace hooks) is temporarily
@@ -171,9 +171,6 @@ Commands:
171
171
  tool-failures Get tool failure overview
172
172
  tool-failure-detail <tool_name> Get per-tool failure detail
173
173
 
174
- loops Currently-active loop detections
175
- loop <detection_id> Single loop detail
176
-
177
174
  Prompt management:
178
175
  prompts init Create .moda/prompts.yml
179
176
  prompts status Show local prompt changes
@@ -306,8 +303,6 @@ Examples:
306
303
  moda context <conversation_id> --window=3
307
304
  moda audit <conversation_id|trace_id>
308
305
  moda audit <trace_id> --kind=trace --json
309
- moda loops --limit=10
310
- moda loop 12345678-1234-1234-1234-1234567890ab
311
306
  moda prompts status
312
307
  moda prompts sync
313
308
  moda prompts promote support.triage --label=prod --version=pver_abc123
@@ -617,27 +612,6 @@ export async function runCommand(command, positional, flags, positionals = posit
617
612
  await printWithOptionalWindows(withToolFailureAnchors(data), 'examples', params.include_window === true, params.window ?? 1, context.output);
618
613
  break;
619
614
  }
620
- case 'loops': {
621
- const args = flagsToArgs(flags);
622
- const params = LoopsSchema.parse(args);
623
- const query = new URLSearchParams();
624
- if (params.limit)
625
- query.set('limit', params.limit.toString());
626
- const queryString = query.toString() ? `?${query.toString()}` : '';
627
- const data = await callDataAPI(`/loops/active${queryString}`);
628
- context.output.writeData(data);
629
- break;
630
- }
631
- case 'loop': {
632
- if (!positional) {
633
- throw new CliInputError('<detection_id> is required', 'Usage: moda loop <detection_id>');
634
- }
635
- const args = flagsToArgs(flags, 'detection_id', positional);
636
- const params = LoopDetailSchema.parse(args);
637
- const data = await callDataAPI(`/loops/${params.detection_id}`);
638
- context.output.writeData(data);
639
- break;
640
- }
641
615
  default:
642
616
  throw new CliInputError(`Unknown command '${command}'`, 'Run "moda --help" for usage information.');
643
617
  }
@@ -1044,18 +1018,6 @@ export const commandRegistry = createCommandRegistry([
1044
1018
  examples: ['moda tool-failure-detail search --include-window'],
1045
1019
  ...dataApiDefaults,
1046
1020
  }),
1047
- legacyCommand({
1048
- name: 'loops',
1049
- description: 'List currently-active loop detections',
1050
- examples: ['moda loops --limit=10'],
1051
- ...dataApiDefaults,
1052
- }),
1053
- legacyCommand({
1054
- name: 'loop',
1055
- description: 'Get a single loop detection',
1056
- examples: ['moda loop 12345678-1234-1234-1234-1234567890ab'],
1057
- ...dataApiDefaults,
1058
- }),
1059
1021
  legacyCommand({
1060
1022
  name: 'prompts',
1061
1023
  description: 'Manage prompt versions and labels',
@@ -214,10 +214,18 @@ function hydrateArtifactBody(artifact, rootDir) {
214
214
  }
215
215
  const evidenceLine = firstEvidenceLineNumber(artifact.evidence);
216
216
  const evidencePath = firstEvidencePath(artifact);
217
- if (evidencePath && evidenceLine !== undefined) {
217
+ if (evidencePath) {
218
218
  const text = readRepoRelativeText(rootDir, evidencePath);
219
219
  if (text !== undefined) {
220
- return extractSnippetAroundLine(text, evidenceLine, artifact.type === 'tool' ? 30 : 80);
220
+ if (evidenceLine !== undefined) {
221
+ return extractSnippetAroundLine(text, evidenceLine, artifact.type === 'tool' ? 30 : 80);
222
+ }
223
+ // No line number (common for agent/hosted analyst citations): fall back to
224
+ // the whole evidence file so the body is still captured in full.
225
+ if (artifact.type === 'prompt' || isDedicatedToolFilePath(evidencePath))
226
+ return text;
227
+ if (artifact.type === 'tool')
228
+ return extractToolBodyFromText(text, artifact.name) ?? text;
221
229
  }
222
230
  }
223
231
  return undefined;
@@ -97,15 +97,34 @@ export async function runRemoteHarnessAnalyst(options) {
97
97
  });
98
98
  writeHumanProgress(context, `Moda Analyst (remote): analysis run ${runId} accepted; waiting for results...`);
99
99
  const final = await pollRemoteAnalyze({ ingestUrl, apiKey, runId, context });
100
- if (final.status !== 'completed' || !final.report) {
101
- const detail = final.status === 'failed'
102
- ? `Remote harness analysis failed: ${final.error ?? 'unknown error'}.`
103
- : `Remote harness analysis did not finish before the timeout (last status: ${final.status}).`;
104
- throw new CliInputError(detail, [
105
- `Retry \`moda harness analyze --remote\`${runId ? ` (run ${runId})` : ''}.`,
106
- 'Or run `moda harness analyze --analyst=local-scan` to analyze locally.',
107
- 'Increase MODA_HARNESS_REMOTE_TIMEOUT_MS if large repos need longer.',
108
- ].join('\n'));
100
+ // A run that returned no report at all is a hard failure (timeout, or the
101
+ // hosted agent crashed before producing anything) — nothing to recover.
102
+ if (!final.report) {
103
+ throw remoteAnalyzeHardFailure(final, runId);
104
+ }
105
+ // A `failed` run only qualifies for client-side recovery when the server tells
106
+ // us the failure was its own REPORT VALIDATION (it produced a report but the
107
+ // report-contract check failed). That is the one class the client can recover:
108
+ // the uploaded snapshot is truncated/sampled, so the hosted analyst commonly
109
+ // leaves prompt/tool `body` (or citation lines) empty, and the client — which
110
+ // has the full local repo — can backfill those source-dependent gaps and
111
+ // re-validate. Any other `failed` run (a crashed/aborted agent, which carries
112
+ // an `error` string and no structured validation verdict) stays a hard failure
113
+ // even if a partial report leaked through, so we never write or sync a report
114
+ // from a hosted run the server aborted for a reason the client cannot repair.
115
+ if (final.status === 'failed' && !serverFailedReportValidation(final)) {
116
+ throw remoteAnalyzeHardFailure(final, runId);
117
+ }
118
+ // The hosted analyst returned a report that failed SERVER-side validation —
119
+ // most often because it enumerated prompt/tool artifacts but left their `body`
120
+ // empty, or because the uploaded snapshot was truncated so the server never
121
+ // saw the source. The client has the full local repo, so `repairExternalAnalystReport`
122
+ // backfills those bodies from disk here and we re-validate locally before
123
+ // deciding the outcome (a report that stays invalid after repair still exits 1
124
+ // and is never synced). This lets `--remote` succeed even when the server
125
+ // could not complete the report.
126
+ if (final.status === 'failed') {
127
+ writeHumanProgress(context, 'Moda Analyst (remote): server validation incomplete; completing artifact bodies from local source...');
109
128
  }
110
129
  let report = repairExternalAnalystReport(normalizeHarnessReport(final.report), rootDir);
111
130
  if (snapshot.truncated) {
@@ -145,6 +164,33 @@ export async function runRemoteHarnessAnalyst(options) {
145
164
  });
146
165
  return { exitCode: validation.status === 'pass' ? 0 : 1 };
147
166
  }
167
+ /** Build the terminal error for a remote run that cannot be recovered client-side. */
168
+ function remoteAnalyzeHardFailure(final, runId) {
169
+ const detail = final.status === 'failed'
170
+ ? `Remote harness analysis failed: ${final.error ?? 'unknown error'}.`
171
+ : `Remote harness analysis did not finish before the timeout (last status: ${final.status}).`;
172
+ return new CliInputError(detail, [
173
+ `Retry \`moda harness analyze --remote\`${runId ? ` (run ${runId})` : ''}.`,
174
+ 'Or run `moda harness analyze --analyst=local-scan` to analyze locally.',
175
+ 'Increase MODA_HARNESS_REMOTE_TIMEOUT_MS if large repos need longer.',
176
+ ].join('\n'));
177
+ }
178
+ /**
179
+ * True when a `failed` hosted run failed specifically because its own report
180
+ * validation failed — the only server failure the client can recover. The hosted
181
+ * service attaches a structured validation verdict (`{ status: 'fail', ... }`)
182
+ * only when it produced a report that failed the report-contract check
183
+ * (`apps/moda-harness-analyze` server.ts); an operational failure (crashed agent)
184
+ * reports an `error` string with no validation object. Gating recovery on this
185
+ * verdict keeps crashed/aborted runs as hard failures, and the mandatory
186
+ * client-side re-validation still rejects any report that stays invalid after
187
+ * body/citation repair.
188
+ */
189
+ function serverFailedReportValidation(final) {
190
+ const { validation } = final;
191
+ return typeof validation === 'object' && validation !== null && !Array.isArray(validation)
192
+ && validation.status === 'fail';
193
+ }
148
194
  /** Map CLI flags to the optional server-side analyze options in the wire contract. */
149
195
  function buildRemoteAnalyzeOptions(flags) {
150
196
  const options = {};
package/dist/harness.js CHANGED
@@ -730,6 +730,10 @@ function parseHarnessReportCandidate(candidate) {
730
730
  export function repairExternalAnalystReport(report, rootDir) {
731
731
  const normalized = normalizeHarnessReport(report);
732
732
  repairCitationLines(normalized, rootDir);
733
+ // Agent/hosted analysts reliably enumerate prompt/tool artifacts but often omit
734
+ // the verbatim `body`. Backfill it from source the same way local-scan does so
735
+ // external reports satisfy the prompt/tool body requirement.
736
+ hydratePromptAndToolBodies(normalized.graph, rootDir);
733
737
  repairReportSummaryCounts(normalized);
734
738
  return normalized;
735
739
  }
@@ -5,10 +5,10 @@ declare const INVESTIGATION_SCHEMA_VERSION = "production_investigation.v0.1";
5
5
  declare const OVERVIEW_SCHEMA_VERSION = "production_overview.v0.1";
6
6
  declare const ASK_SCHEMA_VERSION = "production_ask.v0.1";
7
7
  type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info';
8
- type FindingKind = 'tool_failure' | 'loop' | 'frustration' | 'prompt_regression' | 'skill_gap' | 'insufficient_data';
8
+ type FindingKind = 'tool_failure' | 'frustration' | 'prompt_regression' | 'skill_gap' | 'insufficient_data';
9
9
  export interface ProductionEvidenceRef {
10
10
  id: string;
11
- kind: 'data_api' | 'tool_failure' | 'frustration' | 'loop' | 'harness' | 'ask';
11
+ kind: 'data_api' | 'tool_failure' | 'frustration' | 'harness' | 'ask';
12
12
  label: string;
13
13
  endpoint?: string;
14
14
  conversationId?: string;
@@ -54,7 +54,6 @@ export interface ProductionInvestigation {
54
54
  scope: {
55
55
  conversationId?: string;
56
56
  toolName?: string;
57
- loopId?: string;
58
57
  question?: string;
59
58
  };
60
59
  summary: {
@@ -137,7 +136,6 @@ export declare function investigateProduction(options: {
137
136
  cwd: string;
138
137
  conversationId?: string;
139
138
  toolName?: string;
140
- loopId?: string;
141
139
  question?: string;
142
140
  failureOnly?: boolean;
143
141
  }): Promise<ProductionInvestigation>;
@@ -66,7 +66,6 @@ export async function runProductionCommand(context) {
66
66
  cwd: context.cwd,
67
67
  conversationId: context.flags.conversation,
68
68
  toolName: context.flags.tool,
69
- loopId: context.flags.loop,
70
69
  });
71
70
  writeInvestigation(context, investigation);
72
71
  return { exitCode: 0 };
@@ -127,16 +126,12 @@ export async function investigateProduction(options) {
127
126
  ? `/tool-failures/${encodeURIComponent(options.toolName)}?days_back=${options.daysBack}&limit=5`
128
127
  : `/tool-failures?days_back=${options.daysBack}`;
129
128
  const frustrationsEndpoint = `/frustrations?days_back=${options.daysBack}&limit=5`;
130
- const loopsEndpoint = options.loopId
131
- ? `/loops/${encodeURIComponent(options.loopId)}`
132
- : '/loops/active?limit=20';
133
- const [overview, tools, frustrations, loops] = await Promise.all([
129
+ const [overview, tools, frustrations] = await Promise.all([
134
130
  safeData(overviewEndpoint),
135
131
  safeData(toolEndpoint),
136
132
  safeData(frustrationsEndpoint),
137
- safeData(loopsEndpoint),
138
133
  ]);
139
- for (const [key, result] of Object.entries({ overview, tools, frustrations, loops })) {
134
+ for (const [key, result] of Object.entries({ overview, tools, frustrations })) {
140
135
  sourceStatus[key] = result.status;
141
136
  if (result.warning)
142
137
  warnings.push(result.warning);
@@ -147,10 +142,6 @@ export async function investigateProduction(options) {
147
142
  findings.push(...toolFailureFindings(tools.data, options.daysBack, hints, evidenceRefs, options.toolName));
148
143
  if (!options.failureOnly) {
149
144
  findings.push(...frustrationFindings(frustrations.data, options.daysBack, evidenceRefs));
150
- findings.push(...loopFindings(loops.data, evidenceRefs, options.loopId));
151
- }
152
- else {
153
- findings.push(...loopFindings(loops.data, evidenceRefs, options.loopId));
154
145
  }
155
146
  if (findings.length === 0) {
156
147
  findings.push(insufficientDataFinding(overview.data, evidenceRefs));
@@ -165,7 +156,6 @@ export async function investigateProduction(options) {
165
156
  scope: {
166
157
  ...(options.conversationId ? { conversationId: options.conversationId } : {}),
167
158
  ...(options.toolName ? { toolName: options.toolName } : {}),
168
- ...(options.loopId ? { loopId: options.loopId } : {}),
169
159
  ...(options.question ? { question: options.question } : {}),
170
160
  },
171
161
  summary: summarizeInvestigation(boundedFindings),
@@ -444,44 +434,6 @@ function frustrationFindings(data, daysBack, evidenceRefs) {
444
434
  }],
445
435
  }];
446
436
  }
447
- function loopFindings(data, evidenceRefs, scopedLoopId) {
448
- const rows = loopRows(data);
449
- return rows.flatMap((loop) => {
450
- const id = loop.id || scopedLoopId;
451
- if (!id)
452
- return [];
453
- const evidenceId = `loop:${id}`;
454
- evidenceRefs.push({
455
- id: evidenceId,
456
- kind: 'loop',
457
- label: loop.signature || `${loop.type || 'Loop'} detected`,
458
- endpoint: `/loops/${id}`,
459
- detectionId: id,
460
- conversationId: loop.conversationId,
461
- value: loop.raw,
462
- });
463
- return [{
464
- id: `finding_loop_${slug(id)}`,
465
- kind: 'loop',
466
- title: loop.signature || `${loop.type || 'Loop'} detected`,
467
- severity: loop.blastRadius >= 5 ? 'high' : 'medium',
468
- confidence: 'high',
469
- impactScore: 84 + Math.min(loop.blastRadius, 10),
470
- rankReason: `Active loop detection${loop.blastRadius ? ` with blast radius ${loop.blastRadius}` : ''}.`,
471
- summary: loop.rootCause || 'The agent appears to repeat work or retry a failing path.',
472
- evidenceRefIds: [evidenceId],
473
- likelyLocations: [unknownLocation('prompt', 'Loops often come from retry policy, stop conditions, or tool handling.')],
474
- recommendedActions: [{
475
- id: `action_loop_${slug(id)}`,
476
- title: 'Inspect loop detail.',
477
- rationale: 'Loop detail includes signature, cycle length, and root-cause hypothesis.',
478
- command: `moda loop ${id}`,
479
- mutability: 'read',
480
- requiresApproval: false,
481
- }],
482
- }];
483
- });
484
- }
485
437
  function insufficientDataFinding(overview, evidenceRefs) {
486
438
  const evidenceId = 'overview:data';
487
439
  if (!evidenceRefs.some((ref) => ref.id === evidenceId)) {
@@ -1060,28 +1012,6 @@ function toolRows(data, scopedTool) {
1060
1012
  }];
1061
1013
  });
1062
1014
  }
1063
- function loopRows(data) {
1064
- const record = asRecord(data);
1065
- const rows = Array.isArray(record?.loops)
1066
- ? record.loops
1067
- : asRecord(record)
1068
- ? [record]
1069
- : [];
1070
- return rows.flatMap((item) => {
1071
- const row = asRecord(item);
1072
- if (!row)
1073
- return [];
1074
- return [{
1075
- id: asString(row.detection_id) ?? '',
1076
- type: asString(row.loop_type) ?? undefined,
1077
- signature: asString(row.loop_signature_human) ?? undefined,
1078
- blastRadius: numberValue(row.blast_radius),
1079
- conversationId: asString(row.conversation_id) ?? undefined,
1080
- rootCause: asString(row.root_cause_hypothesis) ?? undefined,
1081
- raw: row,
1082
- }];
1083
- }).filter((loop) => loop.id);
1084
- }
1085
1015
  function firstQuote(row) {
1086
1016
  const quotes = Array.isArray(row?.user_quotes) ? row.user_quotes : [];
1087
1017
  const first = asRecord(quotes[0]);
@@ -1335,7 +1265,6 @@ function evidenceKind(value) {
1335
1265
  if (value === 'data_api' ||
1336
1266
  value === 'tool_failure' ||
1337
1267
  value === 'frustration' ||
1338
- value === 'loop' ||
1339
1268
  value === 'harness' ||
1340
1269
  value === 'ask')
1341
1270
  return value;
@@ -1355,8 +1284,6 @@ function purposeForCommand(command) {
1355
1284
  return 'Inspect tool failure examples and conversation anchors.';
1356
1285
  if (command.startsWith('moda context'))
1357
1286
  return 'Read the relevant conversation window.';
1358
- if (command.startsWith('moda loop'))
1359
- return 'Inspect loop detection details.';
1360
1287
  if (command.startsWith('moda doctor'))
1361
1288
  return 'Validate Moda setup and data flow.';
1362
1289
  if (command.startsWith('moda overview'))
package/dist/schemas.d.ts CHANGED
@@ -185,20 +185,4 @@ export declare const SearchSchema: z.ZodObject<{
185
185
  limit?: number | undefined;
186
186
  user_id?: string | undefined;
187
187
  }>;
188
- /** moda loops — currently active loop detections. */
189
- export declare const LoopsSchema: z.ZodObject<{
190
- limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
191
- }, "strip", z.ZodTypeAny, {
192
- limit?: number | undefined;
193
- }, {
194
- limit?: number | undefined;
195
- }>;
196
- /** moda loop <detectionId> — single loop detection detail. */
197
- export declare const LoopDetailSchema: z.ZodObject<{
198
- detection_id: z.ZodString;
199
- }, "strip", z.ZodTypeAny, {
200
- detection_id: string;
201
- }, {
202
- detection_id: string;
203
- }>;
204
188
  //# sourceMappingURL=schemas.d.ts.map
package/dist/schemas.js CHANGED
@@ -92,12 +92,4 @@ export const SearchSchema = z.object({
92
92
  time_range: z.enum(TIME_RANGES).default('all').optional(),
93
93
  limit: z.number().min(1).max(100).default(20).optional(),
94
94
  });
95
- /** moda loops — currently active loop detections. */
96
- export const LoopsSchema = z.object({
97
- limit: z.number().min(1).max(200).default(25).optional(),
98
- });
99
- /** moda loop <detectionId> — single loop detection detail. */
100
- export const LoopDetailSchema = z.object({
101
- detection_id: z.string().uuid({ message: 'detection_id must be a UUID' }),
102
- });
103
95
  //# sourceMappingURL=schemas.js.map
package/dist/tui.js CHANGED
@@ -298,7 +298,6 @@ function overviewSignals(record) {
298
298
  ['Frustrations', valueAt(record, ['frustrations', 'total']) ?? record.frustrations],
299
299
  ['Runs', valueAt(record, ['runs', 'total']) ?? record.runs],
300
300
  ['Workflows', valueAt(record, ['workflows', 'total']) ?? record.workflows],
301
- ['Loops', valueAt(record, ['loops', 'active']) ?? valueAt(record, ['loops', 'total']) ?? record.loops],
302
301
  ];
303
302
  return candidates
304
303
  .map(([label, value]) => [label, metricText(value)])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.13.9",
3
+ "version": "1.13.11",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "moda.skill_index.v1",
3
- "bundled_at": "2026-07-08T05:27:07.614Z",
4
- "cli_version": "1.13.9",
3
+ "bundled_at": "2026-07-08T07:05:48.702Z",
4
+ "cli_version": "1.13.11",
5
5
  "skills": [
6
6
  {
7
7
  "id": "integration-node-anthropic",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: moda-cli
3
3
  version: 2.1.0
4
- description: Query Moda's AI agent conversation analytics and manage code-first prompt versions from the terminal — semantic/keyword/hybrid message search, overview KPIs, topic clusters, message context, user frustration detections, tool failures, active loop detections, and moda prompts status/sync/promote. Use when the user asks about moda, modaflows, conversation analytics, prompt management, user frustrations, agent observability, tool failure debugging, wants to find conversations or tool calls about a topic, or wants to investigate how their AI agent is performing.
4
+ description: Query Moda's AI agent conversation analytics and manage code-first prompt versions from the terminal — semantic/keyword/hybrid message search, overview KPIs, topic clusters, message context, user frustration detections, tool failures, and moda prompts status/sync/promote. Use when the user asks about moda, modaflows, conversation analytics, prompt management, user frustrations, agent observability, tool failure debugging, wants to find conversations or tool calls about a topic, or wants to investigate how their AI agent is performing.
5
5
  ---
6
6
 
7
7
  # Moda CLI
@@ -475,7 +475,7 @@ Data API fallback. Abbreviated envelope:
475
475
  ```
476
476
 
477
477
  `moda investigate` also accepts scoping flags (`--tool`, `--run`,
478
- `--conversation`, `--loop`) to focus the ranking on one signal.
478
+ `--conversation`) to focus the ranking on one signal.
479
479
 
480
480
  ### 3. Browse topic clusters
481
481
 
@@ -578,21 +578,7 @@ Pass `--include-window` to attach a `window` field per row with the message
578
578
  slice centered on the anchor. `--window=N` (1–5, default `1`) is the
579
579
  half-width; `--window=1` ⇒ 3 messages total (anchor ± 1).
580
580
 
581
- ### 7. Active loop investigation
582
-
583
- ```bash
584
- moda loops # everything currently stuck
585
- moda loops --limit=10
586
- moda loop <detection_id> # one loop signature in detail
587
- ```
588
-
589
- Loops are detected in real time by the conversation worker (tier 0/1) and
590
- the analytics container (tier 2). A row with `cycle_visits=378` means the
591
- agent re-ran the same action 378 times. Use the `loop_signature_human`
592
- field to recognize cross-conversation patterns (one tool definition bug
593
- will produce identical signatures across many users).
594
-
595
- ### 8. Prompt management
581
+ ### 7. Prompt management
596
582
 
597
583
  Use these only when working on the code-first prompt registry.
598
584
 
@@ -762,8 +748,6 @@ npx: `npx -p @moda-ai/cli moda <command>`.
762
748
  | `moda frustrations` | User frustration detections with evidence |
763
749
  | `moda tool-failures` | Tool failure overview |
764
750
  | `moda tool-failure-detail <tool_name>` | Per-tool failure breakdown + examples |
765
- | `moda loops` | Currently-active loop detections |
766
- | `moda loop <detection_id>` | One loop signature with blast radius |
767
751
  | `moda prompts status` | Read-only local prompt status |
768
752
  | `moda prompts diff` | Read-only local prompt diff/status |
769
753
  | `moda prompts sync` | Upload changed prompt versions and update lockfile |