@moda-ai/cli 1.10.0 → 1.10.2

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,12 +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
- # Agent runs runtime (workflows = recurring class, run = one execution)
62
- moda workflows --time-range=7d # Recurring workflow catalog with health
63
- moda runs # Last 7d of workflow executions
64
- moda runs --time-range=24h --outcome=partial # Last 24h, partial completions only
65
- moda runs --class-id=cron:helpscout-triage-hourly # Scoped to one workflow class
66
- moda run <run_id> # One execution + its side-effect receipts
67
61
  moda loops # Currently-active loop detections
68
62
  moda loop <detection_id> # One loop detail (signature + blast radius)
69
63
 
@@ -81,9 +75,7 @@ All commands output JSON. Pipe to `jq` for filtering:
81
75
  ```bash
82
76
  moda overview | jq '.frustrations'
83
77
  moda frustrations | jq '.frustrations[].primary_cause'
84
- moda runs --outcome=partial | jq '.runs[] | {job_id, intent_summary, outcome_reason}'
85
78
  moda loops | jq '.loops | sort_by(-.cycle_visits) | .[0:5] | .[].loop_signature_human'
86
- moda workflows | jq '.workflows | sort_by(.health) | .[] | select(.health < 0.8)'
87
79
  ```
88
80
 
89
81
  ## Prompt Management
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, WorkflowsSchema, RunsSchema, RunDetailSchema, LoopsSchema, LoopDetailSchema, } from './schemas.js';
5
+ import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, SearchSchema, WorldStateSchema, ContextSchema, AuditSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, LoopsSchema, LoopDetailSchema, } 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
@@ -130,7 +130,6 @@ Commands:
130
130
  agent evidence <ref> Expand one evidence reference
131
131
  investigate Rank production issues with evidence
132
132
  failures Show production failures worth fixing first
133
- why-failed <run_id> Explain a failed or partial production run
134
133
  ask "question" Ask Moda a production/harness question
135
134
  sync Sync the local harness graph to Moda Cloud
136
135
  sync harness Sync the local harness graph to Moda Cloud
@@ -149,10 +148,6 @@ Commands:
149
148
  tool-failures Get tool failure overview
150
149
  tool-failure-detail <tool_name> Get per-tool failure detail
151
150
 
152
- Agent runs runtime:
153
- workflows Recurring workflow catalog (per-class 30d health)
154
- runs List workflow executions with classified outcomes
155
- run <run_id> Get single execution + side-effect receipts
156
151
  loops Currently-active loop detections
157
152
  loop <detection_id> Single loop detail
158
153
 
@@ -216,11 +211,6 @@ World-state flags:
216
211
  --summary-only Return segment summaries only (omit the event stream)
217
212
  --event-limit=N Max world-state events to return (1-5000, default 2000)
218
213
 
219
- Runs flags:
220
- --outcome=OUTCOME Filter by outcome (running|succeeded|partial|failed|loop|abandoned|unknown)
221
- --class-id=ID Filter to a single workflow class (e.g. cron:helpscout-triage-hourly)
222
- --user-id=ID Filter to a single user
223
-
224
214
  Init flags:
225
215
  --yes Accept all defaults (non-interactive)
226
216
  --analyst=auto|claude|codex|cursor|local-scan
@@ -256,7 +246,6 @@ Examples:
256
246
  moda agent evidence doctor:harness_report_approved_synced
257
247
  moda investigate
258
248
  moda failures
259
- moda why-failed 12345678-1234-1234-1234-1234567890ab
260
249
  moda ask "what should I fix first?"
261
250
  moda sync --dry-run
262
251
  moda sync harness --profile=prod
@@ -276,10 +265,6 @@ Examples:
276
265
  moda context <conversation_id> --window=3
277
266
  moda audit <conversation_id|trace_id>
278
267
  moda audit <trace_id> --kind=trace --json
279
- moda workflows --time-range=7d
280
- moda runs --time-range=24h --outcome=partial
281
- moda runs --class-id=cron:helpscout-triage-hourly --limit=20
282
- moda run 12345678-1234-1234-1234-1234567890ab
283
268
  moda loops --limit=10
284
269
  moda loop 12345678-1234-1234-1234-1234567890ab
285
270
  moda prompts status
@@ -405,7 +390,6 @@ export async function runCommand(command, positional, flags, positionals = posit
405
390
  }
406
391
  case 'investigate':
407
392
  case 'failures':
408
- case 'why-failed':
409
393
  case 'ask': {
410
394
  return (await runProductionCommand(context)).exitCode;
411
395
  }
@@ -594,51 +578,6 @@ export async function runCommand(command, positional, flags, positionals = posit
594
578
  await printWithOptionalWindows(withToolFailureAnchors(data), 'examples', params.include_window === true, params.window ?? 1, context.output);
595
579
  break;
596
580
  }
597
- // ─── Workflows + Runs ──────────────────────────────────────
598
- case 'workflows': {
599
- const args = flagsToArgs(flags);
600
- const params = WorkflowsSchema.parse(args);
601
- const query = new URLSearchParams();
602
- if (params.time_range)
603
- query.set('time_range', params.time_range);
604
- if (params.user_id)
605
- query.set('user_id', params.user_id);
606
- const queryString = query.toString() ? `?${query.toString()}` : '';
607
- const data = await callDataAPI(`/workflows${queryString}`);
608
- context.output.writeData(data);
609
- break;
610
- }
611
- case 'runs': {
612
- const args = flagsToArgs(flags);
613
- const params = RunsSchema.parse(args);
614
- const query = new URLSearchParams();
615
- if (params.time_range)
616
- query.set('time_range', params.time_range);
617
- if (params.outcome)
618
- query.set('outcome', params.outcome);
619
- if (params.class_id)
620
- query.set('class_id', params.class_id);
621
- if (params.user_id)
622
- query.set('user_id', params.user_id);
623
- if (params.limit)
624
- query.set('limit', params.limit.toString());
625
- if (params.offset !== undefined)
626
- query.set('offset', params.offset.toString());
627
- const queryString = query.toString() ? `?${query.toString()}` : '';
628
- const data = await callDataAPI(`/runs${queryString}`);
629
- context.output.writeData(data);
630
- break;
631
- }
632
- case 'run': {
633
- if (!positional) {
634
- throw new CliInputError('<run_id> is required', 'Usage: moda run <run_id>');
635
- }
636
- const args = flagsToArgs(flags, 'run_id', positional);
637
- const params = RunDetailSchema.parse(args);
638
- const data = await callDataAPI(`/runs/${params.run_id}`);
639
- context.output.writeData(data);
640
- break;
641
- }
642
581
  case 'loops': {
643
582
  const args = flagsToArgs(flags);
644
583
  const params = LoopsSchema.parse(args);
@@ -927,12 +866,6 @@ export const commandRegistry = createCommandRegistry([
927
866
  examples: ['moda failures', 'moda failures --tool lookupCustomer'],
928
867
  ...dataApiDefaults,
929
868
  }),
930
- legacyCommand({
931
- name: 'why-failed',
932
- description: 'Explain a failed or partial production run',
933
- examples: ['moda why-failed 12345678-1234-1234-1234-1234567890ab'],
934
- ...dataApiDefaults,
935
- }),
936
869
  legacyCommand({
937
870
  name: 'ask',
938
871
  description: 'Ask Moda a production intelligence question',
@@ -1014,24 +947,6 @@ export const commandRegistry = createCommandRegistry([
1014
947
  examples: ['moda tool-failure-detail search --include-window'],
1015
948
  ...dataApiDefaults,
1016
949
  }),
1017
- legacyCommand({
1018
- name: 'workflows',
1019
- description: 'Recurring workflow catalog',
1020
- examples: ['moda workflows --time-range=7d'],
1021
- ...dataApiDefaults,
1022
- }),
1023
- legacyCommand({
1024
- name: 'runs',
1025
- description: 'List workflow executions',
1026
- examples: ['moda runs --time-range=24h --outcome=partial'],
1027
- ...dataApiDefaults,
1028
- }),
1029
- legacyCommand({
1030
- name: 'run',
1031
- description: 'Get a single workflow execution',
1032
- examples: ['moda run 12345678-1234-1234-1234-1234567890ab'],
1033
- ...dataApiDefaults,
1034
- }),
1035
950
  legacyCommand({
1036
951
  name: 'loops',
1037
952
  description: 'List currently-active loop detections',
@@ -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' | 'partial_run' | 'prompt_regression' | 'skill_gap' | 'insufficient_data';
8
+ type FindingKind = 'tool_failure' | 'loop' | 'frustration' | 'prompt_regression' | 'skill_gap' | 'insufficient_data';
9
9
  export interface ProductionEvidenceRef {
10
10
  id: string;
11
- kind: 'data_api' | 'tool_failure' | 'frustration' | 'loop' | 'run' | 'harness' | 'ask';
11
+ kind: 'data_api' | 'tool_failure' | 'frustration' | 'loop' | 'harness' | 'ask';
12
12
  label: string;
13
13
  endpoint?: string;
14
14
  conversationId?: string;
@@ -52,7 +52,6 @@ export interface ProductionInvestigation {
52
52
  generatedAt: string;
53
53
  daysBack: number;
54
54
  scope: {
55
- runId?: string;
56
55
  conversationId?: string;
57
56
  toolName?: string;
58
57
  loopId?: string;
@@ -136,7 +135,6 @@ export declare function loadOverviewBriefing(options: {
136
135
  export declare function investigateProduction(options: {
137
136
  daysBack: number;
138
137
  cwd: string;
139
- runId?: string;
140
138
  conversationId?: string;
141
139
  toolName?: string;
142
140
  loopId?: string;
@@ -50,15 +50,6 @@ export async function runProductionCommand(context) {
50
50
  // it from the exit code alone (in addition to `data.degraded` / status).
51
51
  return { exitCode: result.degraded ? EXIT_DEGRADED : EXIT_OK };
52
52
  }
53
- if (context.command === 'why-failed') {
54
- const runId = context.positional;
55
- if (!runId) {
56
- throw new CliInputError('<run_id> is required', 'Usage: moda why-failed <run_id>');
57
- }
58
- const investigation = await investigateProduction({ daysBack, cwd: context.cwd, runId });
59
- writeInvestigation(context, investigation);
60
- return { exitCode: 0 };
61
- }
62
53
  if (context.command === 'failures') {
63
54
  const investigation = await investigateProduction({
64
55
  daysBack,
@@ -73,7 +64,6 @@ export async function runProductionCommand(context) {
73
64
  const investigation = await investigateProduction({
74
65
  daysBack,
75
66
  cwd: context.cwd,
76
- runId: context.flags.run,
77
67
  conversationId: context.flags.conversation,
78
68
  toolName: context.flags.tool,
79
69
  loopId: context.flags.loop,
@@ -137,20 +127,16 @@ export async function investigateProduction(options) {
137
127
  ? `/tool-failures/${encodeURIComponent(options.toolName)}?days_back=${options.daysBack}&limit=5`
138
128
  : `/tool-failures?days_back=${options.daysBack}`;
139
129
  const frustrationsEndpoint = `/frustrations?days_back=${options.daysBack}&limit=5`;
140
- const runsEndpoint = options.runId
141
- ? `/runs/${encodeURIComponent(options.runId)}`
142
- : `/runs?time_range=${timeRangeForDays(options.daysBack)}&limit=20`;
143
130
  const loopsEndpoint = options.loopId
144
131
  ? `/loops/${encodeURIComponent(options.loopId)}`
145
132
  : '/loops/active?limit=20';
146
- const [overview, tools, frustrations, runs, loops] = await Promise.all([
133
+ const [overview, tools, frustrations, loops] = await Promise.all([
147
134
  safeData(overviewEndpoint),
148
135
  safeData(toolEndpoint),
149
136
  safeData(frustrationsEndpoint),
150
- safeData(runsEndpoint),
151
137
  safeData(loopsEndpoint),
152
138
  ]);
153
- for (const [key, result] of Object.entries({ overview, tools, frustrations, runs, loops })) {
139
+ for (const [key, result] of Object.entries({ overview, tools, frustrations, loops })) {
154
140
  sourceStatus[key] = result.status;
155
141
  if (result.warning)
156
142
  warnings.push(result.warning);
@@ -161,11 +147,9 @@ export async function investigateProduction(options) {
161
147
  findings.push(...toolFailureFindings(tools.data, options.daysBack, hints, evidenceRefs, options.toolName));
162
148
  if (!options.failureOnly) {
163
149
  findings.push(...frustrationFindings(frustrations.data, options.daysBack, evidenceRefs));
164
- findings.push(...runFindings(runs.data, evidenceRefs, options.runId));
165
150
  findings.push(...loopFindings(loops.data, evidenceRefs, options.loopId));
166
151
  }
167
152
  else {
168
- findings.push(...runFindings(runs.data, evidenceRefs, options.runId).filter((finding) => finding.kind === 'partial_run'));
169
153
  findings.push(...loopFindings(loops.data, evidenceRefs, options.loopId));
170
154
  }
171
155
  if (findings.length === 0) {
@@ -179,7 +163,6 @@ export async function investigateProduction(options) {
179
163
  generatedAt: new Date().toISOString(),
180
164
  daysBack: options.daysBack,
181
165
  scope: {
182
- ...(options.runId ? { runId: options.runId } : {}),
183
166
  ...(options.conversationId ? { conversationId: options.conversationId } : {}),
184
167
  ...(options.toolName ? { toolName: options.toolName } : {}),
185
168
  ...(options.loopId ? { loopId: options.loopId } : {}),
@@ -461,45 +444,6 @@ function frustrationFindings(data, daysBack, evidenceRefs) {
461
444
  }],
462
445
  }];
463
446
  }
464
- function runFindings(data, evidenceRefs, scopedRunId) {
465
- const rows = runRows(data);
466
- return rows.flatMap((run) => {
467
- const runId = run.id || scopedRunId;
468
- const outcome = run.outcome;
469
- if (!runId || !['failed', 'partial', 'abandoned', 'loop'].includes(outcome))
470
- return [];
471
- const evidenceId = `run:${runId}`;
472
- evidenceRefs.push({
473
- id: evidenceId,
474
- kind: 'run',
475
- label: `${run.agentName || 'Agent run'} ${outcome}`,
476
- endpoint: `/runs/${runId}`,
477
- runId,
478
- conversationId: run.conversationId,
479
- value: run.raw,
480
- });
481
- return [{
482
- id: `finding_run_${slug(runId)}`,
483
- kind: outcome === 'partial' ? 'partial_run' : 'tool_failure',
484
- title: `${run.agentName || 'Agent run'} ended as ${outcome}`,
485
- severity: outcome === 'failed' ? 'high' : 'medium',
486
- confidence: 'high',
487
- impactScore: outcome === 'failed' ? 88 : 76,
488
- rankReason: `Run outcome is ${outcome}${run.reason ? `: ${run.reason}` : ''}.`,
489
- summary: run.reason || 'Moda classified this run as incomplete or unhealthy.',
490
- evidenceRefIds: [evidenceId],
491
- likelyLocations: [unknownLocation('entrypoint', 'Map this run to a harness entrypoint after harness sync.')],
492
- recommendedActions: [{
493
- id: `action_run_${slug(runId)}`,
494
- title: 'Open the run detail.',
495
- rationale: 'Run detail includes receipts and missing effects.',
496
- command: `moda run ${runId}`,
497
- mutability: 'read',
498
- requiresApproval: false,
499
- }],
500
- }];
501
- });
502
- }
503
447
  function loopFindings(data, evidenceRefs, scopedLoopId) {
504
448
  const rows = loopRows(data);
505
449
  return rows.flatMap((loop) => {
@@ -1116,24 +1060,6 @@ function toolRows(data, scopedTool) {
1116
1060
  }];
1117
1061
  });
1118
1062
  }
1119
- function runRows(data) {
1120
- const record = asRecord(data);
1121
- const run = asRecord(record?.run);
1122
- const rows = Array.isArray(record?.runs) ? record.runs : run ? [run] : [];
1123
- return rows.flatMap((item) => {
1124
- const row = asRecord(item);
1125
- if (!row)
1126
- return [];
1127
- return [{
1128
- id: asString(row.job_id) ?? asString(row.run_id) ?? '',
1129
- outcome: asString(row.outcome) ?? 'unknown',
1130
- agentName: asString(row.agent_name) ?? undefined,
1131
- reason: asString(row.outcome_reason) ?? undefined,
1132
- conversationId: asString(row.conversation_id) ?? undefined,
1133
- raw: row,
1134
- }];
1135
- });
1136
- }
1137
1063
  function loopRows(data) {
1138
1064
  const record = asRecord(data);
1139
1065
  const rows = Array.isArray(record?.loops)
@@ -1397,17 +1323,6 @@ function numberFlag(flags, key, fallback) {
1397
1323
  return fallback;
1398
1324
  return Math.min(Math.floor(parsed), 90);
1399
1325
  }
1400
- function timeRangeForDays(days) {
1401
- if (days <= 1)
1402
- return '24h';
1403
- if (days <= 3)
1404
- return '3d';
1405
- if (days <= 7)
1406
- return '7d';
1407
- if (days <= 30)
1408
- return '30d';
1409
- return '90d';
1410
- }
1411
1326
  function normalizeConfidence(value) {
1412
1327
  if (value === 'high' || value === 'medium' || value === 'low' || value === 'unknown')
1413
1328
  return value;
@@ -1421,7 +1336,6 @@ function evidenceKind(value) {
1421
1336
  value === 'tool_failure' ||
1422
1337
  value === 'frustration' ||
1423
1338
  value === 'loop' ||
1424
- value === 'run' ||
1425
1339
  value === 'harness' ||
1426
1340
  value === 'ask')
1427
1341
  return value;
@@ -1443,8 +1357,6 @@ function purposeForCommand(command) {
1443
1357
  return 'Read the relevant conversation window.';
1444
1358
  if (command.startsWith('moda loop'))
1445
1359
  return 'Inspect loop detection details.';
1446
- if (command.startsWith('moda run'))
1447
- return 'Inspect one agent run and side-effect receipts.';
1448
1360
  if (command.startsWith('moda doctor'))
1449
1361
  return 'Validate Moda setup and data flow.';
1450
1362
  if (command.startsWith('moda overview'))
package/dist/schemas.d.ts CHANGED
@@ -185,49 +185,6 @@ export declare const SearchSchema: z.ZodObject<{
185
185
  limit?: number | undefined;
186
186
  user_id?: string | undefined;
187
187
  }>;
188
- /** moda workflows — recurring workflow catalog (read from agent_job_classes). */
189
- export declare const WorkflowsSchema: z.ZodObject<{
190
- time_range: z.ZodOptional<z.ZodDefault<z.ZodEnum<["all", "1h", "3d", "7d", "24h", "30d", "90d"]>>>;
191
- user_id: z.ZodOptional<z.ZodString>;
192
- }, "strip", z.ZodTypeAny, {
193
- time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
194
- user_id?: string | undefined;
195
- }, {
196
- time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
197
- user_id?: string | undefined;
198
- }>;
199
- /** moda runs — list workflow executions with classified outcomes. */
200
- export declare const RunsSchema: z.ZodObject<{
201
- time_range: z.ZodOptional<z.ZodDefault<z.ZodEnum<["all", "1h", "3d", "7d", "24h", "30d", "90d"]>>>;
202
- outcome: z.ZodOptional<z.ZodEnum<["running", "succeeded", "partial", "failed", "loop", "abandoned", "unknown"]>>;
203
- /** Filter to one workflow class id (e.g. cron:helpscout-triage-hourly). */
204
- class_id: z.ZodOptional<z.ZodString>;
205
- user_id: z.ZodOptional<z.ZodString>;
206
- limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
207
- offset: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
208
- }, "strip", z.ZodTypeAny, {
209
- time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
210
- limit?: number | undefined;
211
- offset?: number | undefined;
212
- user_id?: string | undefined;
213
- outcome?: "unknown" | "loop" | "running" | "failed" | "succeeded" | "partial" | "abandoned" | undefined;
214
- class_id?: string | undefined;
215
- }, {
216
- time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
217
- limit?: number | undefined;
218
- offset?: number | undefined;
219
- user_id?: string | undefined;
220
- outcome?: "unknown" | "loop" | "running" | "failed" | "succeeded" | "partial" | "abandoned" | undefined;
221
- class_id?: string | undefined;
222
- }>;
223
- /** moda run <runId> — single execution detail with side-effect receipts. */
224
- export declare const RunDetailSchema: z.ZodObject<{
225
- run_id: z.ZodString;
226
- }, "strip", z.ZodTypeAny, {
227
- run_id: string;
228
- }, {
229
- run_id: string;
230
- }>;
231
188
  /** moda loops — currently active loop detections. */
232
189
  export declare const LoopsSchema: z.ZodObject<{
233
190
  limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
package/dist/schemas.js CHANGED
@@ -78,13 +78,6 @@ export const AuditSchema = z.object({
78
78
  include_raw: boolFlag(),
79
79
  limit: z.number().min(1).max(1000).default(200).optional(),
80
80
  });
81
- // ─── Workflows + Runs (agent runs runtime) ──────────────────
82
- //
83
- // Naming model:
84
- // workflow = the recurring class definition (e.g. cron:helpscout-triage-hourly)
85
- // run = one execution of a workflow
86
- // `moda workflows` lists the catalog; `moda runs` lists executions.
87
- const RUN_OUTCOMES = ['running', 'succeeded', 'partial', 'failed', 'loop', 'abandoned', 'unknown'];
88
81
  const TIME_RANGES = ['all', '1h', '3d', '7d', '24h', '30d', '90d'];
89
82
  const SEARCH_MODES = ['keyword', 'semantic', 'hybrid'];
90
83
  /**
@@ -99,25 +92,6 @@ export const SearchSchema = z.object({
99
92
  time_range: z.enum(TIME_RANGES).default('all').optional(),
100
93
  limit: z.number().min(1).max(100).default(20).optional(),
101
94
  });
102
- /** moda workflows — recurring workflow catalog (read from agent_job_classes). */
103
- export const WorkflowsSchema = z.object({
104
- time_range: z.enum(TIME_RANGES).default('all').optional(),
105
- user_id: z.string().optional(),
106
- });
107
- /** moda runs — list workflow executions with classified outcomes. */
108
- export const RunsSchema = z.object({
109
- time_range: z.enum(TIME_RANGES).default('7d').optional(),
110
- outcome: z.enum(RUN_OUTCOMES).optional(),
111
- /** Filter to one workflow class id (e.g. cron:helpscout-triage-hourly). */
112
- class_id: z.string().optional(),
113
- user_id: z.string().optional(),
114
- limit: z.number().min(1).max(100).default(50).optional(),
115
- offset: z.number().min(0).default(0).optional(),
116
- });
117
- /** moda run <runId> — single execution detail with side-effect receipts. */
118
- export const RunDetailSchema = z.object({
119
- run_id: z.string().uuid({ message: 'run_id must be a UUID' }),
120
- });
121
95
  /** moda loops — currently active loop detections. */
122
96
  export const LoopsSchema = z.object({
123
97
  limit: z.number().min(1).max(200).default(25).optional(),
package/dist/skills.js CHANGED
@@ -27,6 +27,7 @@ import { CliInputError } from './errors.js';
27
27
  const SKILLS_REQUEST_TIMEOUT_MS = 15_000;
28
28
  const VALID_STATUSES = new Set(['approved', 'proposed', 'all']);
29
29
  const SKILLS_MANIFEST_PATH = join('.moda', 'skills.yml');
30
+ const SKILL_PROPOSALS_MANIFEST_PATH = join('.moda', 'skill-proposals.yml');
30
31
  /**
31
32
  * Fetch a tenant's generated skills from the ingest worker. Uses the persisted
32
33
  * API key + ingest URL (`~/.moda/config.json`), mirroring snapshot auth.
@@ -342,11 +343,96 @@ function getGitCommit(cwd) {
342
343
  return undefined;
343
344
  }
344
345
  }
346
+ async function fetchSkillProposals(flags, profileOptions = {}) {
347
+ const params = new URLSearchParams();
348
+ if (flags['generation-run-id'])
349
+ params.set('generationRunId', flags['generation-run-id']);
350
+ const status = flags.status ?? 'ready_for_pr';
351
+ if (status !== 'all')
352
+ params.set('status', status);
353
+ if (flags['tenant-id'])
354
+ params.set('tenantId', flags['tenant-id']);
355
+ const suffix = params.toString() ? `?${params.toString()}` : '';
356
+ const response = (await callControlAPI(`/skills/proposals${suffix}`, { method: 'GET' }, profileOptions));
357
+ return Array.isArray(response.proposals) ? response.proposals : [];
358
+ }
359
+ async function applySkillProposal(proposalId, flags, profileOptions = {}, options = {}) {
360
+ const cwd = options.cwd ?? process.cwd();
361
+ const params = new URLSearchParams();
362
+ if (flags['tenant-id'])
363
+ params.set('tenantId', flags['tenant-id']);
364
+ const suffix = params.toString() ? `?${params.toString()}` : '';
365
+ const proposal = (await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}${suffix}`, { method: 'GET' }, profileOptions));
366
+ if (!proposal.skillMd?.trim()) {
367
+ throw new CliInputError(`Proposal ${proposalId} does not include SKILL.md content`);
368
+ }
369
+ const safeId = safeSkillId(proposal.skillId);
370
+ const relativePath = `.claude/skills/${safeId}/SKILL.md`;
371
+ const writeStatus = writeIfChanged(join(cwd, relativePath), proposal.skillMd);
372
+ if (writeStatus === 'failed') {
373
+ throw new Error(`Failed to write ${relativePath}`);
374
+ }
375
+ writeSkillProposalManifest(cwd, proposal, relativePath);
376
+ let acknowledged = true;
377
+ let ackError;
378
+ try {
379
+ await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}/mark-applied${suffix}`, {
380
+ method: 'POST',
381
+ body: JSON.stringify({ appliedBy: 'moda-cli' }),
382
+ }, profileOptions);
383
+ }
384
+ catch (err) {
385
+ acknowledged = false;
386
+ ackError = err instanceof Error ? err.message : String(err);
387
+ }
388
+ return {
389
+ proposalId,
390
+ skillId: proposal.skillId,
391
+ path: relativePath,
392
+ status: writeStatus,
393
+ acknowledged,
394
+ ackError,
395
+ };
396
+ }
397
+ function safeSkillId(skillId) {
398
+ const safeId = basename(skillId || '');
399
+ if (!safeId || !/^[A-Za-z0-9_-]+$/.test(safeId)) {
400
+ throw new CliInputError(`Invalid skill_id in proposal: ${JSON.stringify(skillId).slice(0, 80)}`);
401
+ }
402
+ return safeId;
403
+ }
404
+ function writeSkillProposalManifest(cwd, proposal, relativePath) {
405
+ const manifestPath = join(cwd, SKILL_PROPOSALS_MANIFEST_PATH);
406
+ mkdirSync(dirname(manifestPath), { recursive: true });
407
+ const existing = existsSync(manifestPath) ? readFileSync(manifestPath, 'utf8') : 'applied_proposals:\n';
408
+ const entry = [
409
+ ` - proposal_id: "${escapeYamlString(proposal.proposalId)}"`,
410
+ ` skill_id: "${escapeYamlString(proposal.skillId)}"`,
411
+ ` skill_family_id: "${escapeYamlString(proposal.skillFamilyId)}"`,
412
+ ` skill_version: ${Number(proposal.skillVersion || 0)}`,
413
+ ` generation_run_id: "${escapeYamlString(proposal.generationRunId)}"`,
414
+ ` clustering_run_id: "${escapeYamlString(proposal.clusteringRunId)}"`,
415
+ ` topic_id: "${escapeYamlString(proposal.topicId)}"`,
416
+ ` replay_set_id: "${escapeYamlString(proposal.replaySetId)}"`,
417
+ ` delta_pass_rate: ${Number(proposal.deltaPassRate || 0)}`,
418
+ ` path: "${escapeYamlString(relativePath)}"`,
419
+ ` applied_at: "${new Date().toISOString()}"`,
420
+ ].join('\n');
421
+ const prefix = existing.endsWith('\n') ? existing : `${existing}\n`;
422
+ writeFileSync(manifestPath, `${prefix}${entry}\n`);
423
+ }
424
+ function escapeYamlString(value) {
425
+ return String(value ?? '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
426
+ }
427
+ function formatProposalRate(value) {
428
+ return `${(Number(value || 0) * 100).toFixed(1)}%`;
429
+ }
345
430
  /**
346
431
  * `moda skills <subcommand>` dispatcher.
347
432
  */
348
433
  export async function runSkillsCommand(args) {
349
434
  const sub = args.positional;
435
+ const positionals = args.positionals ?? (sub ? [sub] : []);
350
436
  if (sub === 'gen') {
351
437
  const result = await generateSkills(args.flags);
352
438
  if (args.flags.json === 'true') {
@@ -371,13 +457,63 @@ export async function runSkillsCommand(args) {
371
457
  }
372
458
  return Number(result.sessions_failed ?? 0) > 0 ? 1 : 0;
373
459
  }
460
+ if (sub === 'proposals' && positionals[1] === 'list') {
461
+ const proposals = await fetchSkillProposals(args.flags, { profile: args.flags.profile });
462
+ if (args.flags.json === 'true') {
463
+ console.log(JSON.stringify({ proposals }));
464
+ }
465
+ else if (proposals.length === 0) {
466
+ console.log('No skill proposals found.');
467
+ }
468
+ else {
469
+ for (const proposal of proposals) {
470
+ console.log([
471
+ proposal.proposalId,
472
+ proposal.proposalStatus,
473
+ `${proposal.skillId}@v${proposal.skillVersion}`,
474
+ `delta=${formatProposalRate(proposal.deltaPassRate)}`,
475
+ `baseline=${formatProposalRate(proposal.baselinePassRate)}`,
476
+ `candidate=${formatProposalRate(proposal.candidatePassRate)}`,
477
+ proposal.name,
478
+ ].filter(Boolean).join('\t'));
479
+ }
480
+ }
481
+ return 0;
482
+ }
483
+ if (sub === 'proposal' && positionals[1] === 'apply') {
484
+ const proposalId = positionals[2] || args.flags['proposal-id'];
485
+ if (!proposalId) {
486
+ throw new CliInputError('A proposal id is required', 'Usage: moda skills proposal apply <proposal-id>');
487
+ }
488
+ const result = await applySkillProposal(proposalId, args.flags, { profile: args.flags.profile });
489
+ if (args.flags.json === 'true') {
490
+ console.log(JSON.stringify(result));
491
+ }
492
+ else {
493
+ console.log([
494
+ `${result.status} ${result.path}`,
495
+ result.acknowledged ? 'acknowledged: true' : 'acknowledged: false',
496
+ ].join('\n'));
497
+ }
498
+ if (!result.acknowledged) {
499
+ // The local SKILL.md was written, but the server still lists this
500
+ // proposal as ready_for_pr. Warn (to stderr so JSON stdout stays clean)
501
+ // and exit non-zero so the failed acknowledgement isn't silently treated
502
+ // as success — re-running `apply` re-attempts the acknowledgement.
503
+ console.error(`Warning: wrote ${result.path} but failed to acknowledge proposal ${proposalId} on the server` +
504
+ (result.ackError ? ` (${result.ackError})` : '') +
505
+ '. The proposal is still listed as ready — re-run `moda skills proposal apply` to acknowledge it.');
506
+ return 1;
507
+ }
508
+ return 0;
509
+ }
374
510
  if (sub === 'sync') {
375
511
  const result = await runSkillSync(args.flags, { profile: args.flags.profile });
376
512
  console.log(JSON.stringify({ synced: result.synced, dryRun: result.dryRun }));
377
513
  return 0;
378
514
  }
379
515
  if (sub !== 'pull') {
380
- throw new CliInputError(sub ? `Unknown skills subcommand '${sub}'` : 'A skills subcommand is required', 'Usage: moda skills gen [--source=all|sdk|coding-agent] [--max-sessions=N] [--start-at=ISO] [--end-at=ISO] [--replay] [--reprocess-segments] [--force-reprocess] [--wait] | moda skills pull [--status=approved|proposed|all] | moda skills sync [--dry-run]');
516
+ throw new CliInputError(sub ? `Unknown skills subcommand '${sub}'` : 'A skills subcommand is required', 'Usage: moda skills gen [--source=all|sdk|coding-agent] [--max-sessions=N] [--start-at=ISO] [--end-at=ISO] [--replay] [--replay-set-id=ID] [--reprocess-segments] [--force-reprocess] [--wait] | moda skills proposals list [--status=ready_for_pr] | moda skills proposal apply <proposal-id> | moda skills pull [--status=approved|proposed|all] | moda skills sync [--dry-run]');
381
517
  }
382
518
  const status = (args.flags.status ?? 'approved').toLowerCase();
383
519
  if (!VALID_STATUSES.has(status)) {
@@ -418,6 +554,7 @@ async function generateSkills(flags) {
418
554
  start_at: flags['start-at'] || undefined,
419
555
  end_at: flags['end-at'] || undefined,
420
556
  clustering_run_id: flags['clustering-run-id'] || undefined,
557
+ replay_set_id: flags['replay-set-id'] || flags['eval-set-id'] || undefined,
421
558
  run_clustering: flags['no-clustering'] !== 'true',
422
559
  source: normalizeSkillSource(flags.source),
423
560
  clustering_timeout_seconds: parsePositiveInt(flags['clustering-timeout-seconds'], 0),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
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
  name: moda-cli
3
3
  version: 2.0.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, agent jobs, recurring workflows, 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, why an agent run partially completed or looped, 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, 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.
5
5
  ---
6
6
 
7
7
  # Moda CLI
@@ -369,42 +369,7 @@ Pass `--include-window` to attach a `window` field per row with the message
369
369
  slice centered on the anchor. `--window=N` (1–5, default `1`) is the
370
370
  half-width; `--window=1` ⇒ 3 messages total (anchor ± 1).
371
371
 
372
- ### 7. Workflow runs and outcomes
373
-
374
- Every triggered run of an agent (cron invocation, scheduled task, or
375
- interactive session) is classified `succeeded | partial | failed | loop |
376
- abandoned`. Use this layer to answer "did the job actually do its work?",
377
- which is structurally different from "did it error?" — most jobs that
378
- partially completed never threw.
379
-
380
- ```bash
381
- moda runs # last 7d, all outcomes
382
- moda runs --time-range=24h --outcome=partial # which runs only half-finished
383
- moda runs --outcome=loop # runs caught in active loops
384
- moda runs --class-id=cron:helpscout-triage-hourly --limit=20
385
- moda run <run_id> # one execution + its side-effect receipts
386
- ```
387
-
388
- `moda run <run_id>` returns the `agent_jobs` row plus every
389
- `side_effect_receipts` entry for the run — i.e. every tool call the agent
390
- made (read / write / send / spawn / compute), with success status and
391
- target system. This is the agent-as-employee log; use it instead of (or
392
- alongside) `moda context` when debugging "what did the agent actually
393
- DO".
394
-
395
- ### 8. Recurring workflows / cron status board
396
-
397
- ```bash
398
- moda workflows # health per recurring class
399
- moda workflows --time-range=7d # filter to last 7d of activity
400
- ```
401
-
402
- Each row is one job class (a cron slug, idea-widget id, or sourceType)
403
- with 30d rolling success / partial / failed / loop counts and a
404
- `health_score` (0–1). Sort by health to find degraded customer-facing
405
- crons that aren't crashing but aren't completing either.
406
-
407
- ### 9. Active loop investigation
372
+ ### 7. Active loop investigation
408
373
 
409
374
  ```bash
410
375
  moda loops # everything currently stuck
@@ -418,7 +383,7 @@ agent re-ran the same action 378 times. Use the `loop_signature_human`
418
383
  field to recognize cross-conversation patterns (one tool definition bug
419
384
  will produce identical signatures across many users).
420
385
 
421
- ### 10. Prompt management
386
+ ### 8. Prompt management
422
387
 
423
388
  Use these only when working on the code-first prompt registry.
424
389
 
@@ -505,19 +470,6 @@ moda clusters --parent-id=<node_id>
505
470
  moda cluster-conversations <node_id>
506
471
  ```
507
472
 
508
- ### Find degraded crons before customers do
509
-
510
- ```bash
511
- # Health-sorted workflow board
512
- moda workflows | jq '.workflows | sort_by(.health) | .[] | select(.runs >= 4 and .health < 0.85)'
513
-
514
- # What loops are eating tokens right now?
515
- moda loops --limit=20 | jq '.loops[] | {sig: .loop_signature_human, visits: .cycle_visits, conv: .conversation_id}'
516
-
517
- # Drill into a partial run
518
- moda runs --outcome=partial --limit=5 | jq -r '.runs[].job_id' | head -1 | xargs -I{} moda run {}
519
- ```
520
-
521
473
  ### One-liner chains with jq
522
474
 
523
475
  ```bash
@@ -596,9 +548,6 @@ npx: `npx -p @moda-ai/cli moda <command>`.
596
548
  | `moda frustrations` | User frustration detections with evidence |
597
549
  | `moda tool-failures` | Tool failure overview |
598
550
  | `moda tool-failure-detail <tool_name>` | Per-tool failure breakdown + examples |
599
- | `moda workflows` | Recurring workflow catalog with 30d health per class |
600
- | `moda runs` | Recent workflow executions with classified outcomes |
601
- | `moda run <run_id>` | One execution + its side-effect receipts |
602
551
  | `moda loops` | Currently-active loop detections |
603
552
  | `moda loop <detection_id>` | One loop signature with blast radius |
604
553
  | `moda prompts status` | Read-only local prompt status |
@@ -610,8 +559,7 @@ npx: `npx -p @moda-ai/cli moda <command>`.
610
559
  `--user-id`, `--time-range=<range>`, `--limit=N` (1–100, default 20).
611
560
 
612
561
  Common flags across most commands: `--days-back=N` (1–90), `--limit=N`,
613
- `--offset=N`, `--time-range=<range>`. Runs commands also accept
614
- `--outcome=<outcome>`, `--class-id=<workflow_class_id>`, and `--user-id=<id>`.
562
+ `--offset=N`, `--time-range=<range>`.
615
563
  `frustrations` and `tool-failure-detail` additionally accept
616
564
  `--include-window` (attach a `window` slice per row) and `--window=N` (1–5,
617
565
  default `1`; half-width, so `--window=1` ⇒ 3 messages).