@moda-ai/cli 1.37.0 → 1.39.0

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/cli.js CHANGED
@@ -8,11 +8,13 @@ import {
8
8
  runPromptsCommand,
9
9
  runSkillsCommand,
10
10
  runStatusCommand
11
- } from "./cli-ert1sbbt.js";
11
+ } from "./cli-a7ypptsf.js";
12
12
  import {
13
13
  ApiError,
14
14
  HARNESS_REPORT_APPROVAL_PATH,
15
15
  HARNESS_REPORT_PATH,
16
+ SIGNALS_MANAGEMENT_SUBCOMMANDS,
17
+ assertFlags,
16
18
  callControlAPI,
17
19
  callDataAPI,
18
20
  getApiRequestCount,
@@ -24,12 +26,17 @@ import {
24
26
  renderMarkdownForTerminal,
25
27
  resetApiRequestCount,
26
28
  runHarnessCommand,
29
+ runProblemReopenCommand,
30
+ runSignalsManagementCommand,
31
+ runUserCommand,
32
+ runUsersCommand,
27
33
  shouldColorTerminalOutput,
28
34
  stripAnsi,
29
35
  summarizeHarness,
30
36
  terminalStyles,
37
+ unknownSignalsSubcommandError,
31
38
  validateHarnessReport
32
- } from "./cli-07tv75te.js";
39
+ } from "./cli-xsmsjny3.js";
33
40
  import"./cli-ssfq84k5.js";
34
41
  import {
35
42
  authFetch,
@@ -38,7 +45,7 @@ import {
38
45
  loadAuthSession,
39
46
  login,
40
47
  revokeCliSessionToken
41
- } from "./cli-strngwn3.js";
48
+ } from "./cli-dyeywy9d.js";
42
49
  import {
43
50
  AGENT_ERROR_CODES,
44
51
  CliAccessError,
@@ -56,10 +63,12 @@ import {
56
63
  MODA_BASE_URL,
57
64
  PROD_BASE_URL,
58
65
  VERSION,
66
+ agentEnvelopeFits,
59
67
  assertValidProfileName,
60
68
  clearStoredApiKeyCredentials,
61
69
  createCliOutput,
62
70
  createCommandContext,
71
+ describeBaseUrlSource,
63
72
  embeddedTenantIdFromApiKey,
64
73
  ensureSecretFilePermissions,
65
74
  getActiveProfile,
@@ -70,6 +79,7 @@ import {
70
79
  loadProfileConfig,
71
80
  resolveApiKey,
72
81
  resolveApiKeyFromActiveProfile,
82
+ resolveApiTenantId,
73
83
  resolveIngestUrl,
74
84
  resolveModaBaseUrl,
75
85
  resolveOutputMode,
@@ -84,7 +94,7 @@ import {
84
94
  validateConfig,
85
95
  wasOutputCancelled,
86
96
  writeSecretJsonFile
87
- } from "./cli-q79sq80a.js";
97
+ } from "./cli-rp912ypf.js";
88
98
  import {
89
99
  __require
90
100
  } from "./cli-0v6na3yp.js";
@@ -121,6 +131,7 @@ var ConversationsSchema = z.object({
121
131
  world_state: z.string().max(200).optional(),
122
132
  outcome: z.enum(["any", "positive", "negative"]).default("any").optional(),
123
133
  include_world_state: boolFlag(),
134
+ sort: z.enum(["recent", "longest", "slowest"]).optional(),
124
135
  limit: z.number().min(1).max(100).default(20).optional(),
125
136
  offset: z.number().min(0).default(0).optional()
126
137
  });
@@ -139,14 +150,19 @@ var WorldStateSchema = z.object({
139
150
  var ContextSchema = z.object({
140
151
  conversation_id: z.string(),
141
152
  msg_index: z.number().min(0).optional(),
142
- window: z.number().min(1).max(5).default(2).optional()
153
+ window: z.number().min(1).max(5).default(2).optional(),
154
+ all: boolFlag(),
155
+ from: z.number().int().min(0).optional(),
156
+ max_messages: z.number().int().min(1).max(5000).optional()
143
157
  });
144
158
  var FrustrationsSchema = z.object({
145
159
  days_back: z.number().min(1).max(90).default(7).optional(),
146
160
  limit: z.number().min(1).max(20).default(10).optional(),
147
161
  offset: z.number().min(0).default(0).optional(),
148
162
  include_window: boolFlag(),
149
- window: z.number().min(1).max(5).default(1).optional()
163
+ window: z.number().min(1).max(5).default(1).optional(),
164
+ user_id: z.string().min(1).max(256).optional(),
165
+ conversation_id: z.string().min(1).max(256).optional()
150
166
  });
151
167
  var ToolFailuresSchema = z.object({
152
168
  days_back: z.number().min(1).max(90).default(7).optional()
@@ -155,6 +171,21 @@ var ProblemsSchema = z.object({
155
171
  days_back: z.number().min(1).max(90).default(30).optional(),
156
172
  limit: z.number().min(1).max(25).default(25).optional()
157
173
  });
174
+ var REMAINDER_FAMILIES = [
175
+ "tool_failure",
176
+ "emotion",
177
+ "laziness",
178
+ "hallucination",
179
+ "prm_dip",
180
+ "custom_signal",
181
+ "conversation_reader"
182
+ ];
183
+ var ProblemsRemainderSchema = z.object({
184
+ days_back: z.number().min(1).max(90).default(30).optional(),
185
+ limit: z.number().min(1).max(25).default(25).optional(),
186
+ family: z.enum(REMAINDER_FAMILIES).optional(),
187
+ cursor: z.string().min(1).max(4096).optional()
188
+ });
158
189
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
159
190
  var ProblemImpactSchema = z.object({
160
191
  node_id: z.string().min(1).max(256).optional(),
@@ -179,13 +210,7 @@ var ProblemImpactSchema = z.object({
179
210
  }).refine((p) => !p.cursor || p.mode === "evidence" && p.to && p.run_id, {
180
211
  message: "An evidence cursor requires --mode=evidence, --to, and --run-id from the previous response"
181
212
  });
182
- var PROBLEM_FAMILIES = [
183
- "tool_failure",
184
- "emotion",
185
- "laziness",
186
- "hallucination",
187
- "prm_dip"
188
- ];
213
+ var PROBLEM_FAMILIES = REMAINDER_FAMILIES;
189
214
  var PROBLEM_DOORS = ["rubric_match", "local_causal_chain"];
190
215
  var ProblemSchema = z.object({
191
216
  id: z.string().min(1),
@@ -286,6 +311,7 @@ var FalsePositiveSchema = z.object({
286
311
  });
287
312
  var EMOTION_FAMILIES = [
288
313
  "frustration",
314
+ "disapproval",
289
315
  "sadness",
290
316
  "confusion",
291
317
  "anxiety",
@@ -296,7 +322,9 @@ var EmotionsSchema = z.object({
296
322
  days_back: z.number().min(1).max(90).default(7).optional(),
297
323
  limit: z.number().min(1).max(20).default(10).optional(),
298
324
  offset: z.number().min(0).max(1e4).optional(),
299
- family: z.enum(EMOTION_FAMILIES).optional()
325
+ family: z.enum(EMOTION_FAMILIES).optional(),
326
+ user_id: z.string().min(1).max(256).optional(),
327
+ conversation_id: z.string().min(1).max(256).optional()
300
328
  });
301
329
  var HallucinationsSchema = z.object({
302
330
  days_back: z.number().min(1).max(90).default(7).optional(),
@@ -305,12 +333,33 @@ var HallucinationsSchema = z.object({
305
333
  conversation_id: z.string().max(200).optional(),
306
334
  kind: z.enum(["contradicted", "verified"]).optional()
307
335
  });
336
+ var LAZINESS_FAMILIES = [
337
+ "omission_comment",
338
+ "omission_phrase",
339
+ "stub_body",
340
+ "refusal_prefix",
341
+ "deflection",
342
+ "data_quality_truncation",
343
+ "promise_no_delivery",
344
+ "capability_refusal_here",
345
+ "capability_refusal_verb",
346
+ "error_admission",
347
+ "irreversibility",
348
+ "stall_recheck",
349
+ "diy_handoff",
350
+ "tool_selection",
351
+ "completion",
352
+ "code_artifact",
353
+ "repeated_tool_call",
354
+ "target_overshoot",
355
+ "data_regression"
356
+ ];
308
357
  var LazinessSchema = z.object({
309
358
  days_back: z.number().min(1).max(90).default(7).optional(),
310
359
  limit: z.number().min(1).max(50).default(10).optional(),
311
360
  offset: z.number().min(0).max(1e4).optional(),
312
361
  conversation_id: z.string().max(200).optional(),
313
- family: z.string().regex(/^[a-z_]{1,64}$/, "Family must be a lowercase laziness family").optional()
362
+ family: z.enum(LAZINESS_FAMILIES).optional()
314
363
  });
315
364
  var StepScoresSchema = z.object({
316
365
  conversation_id: z.string().min(1)
@@ -331,6 +380,7 @@ var ToolFailureDetailSchema = z.object({
331
380
  limit: z.number().min(1).max(20).default(5).optional(),
332
381
  offset: z.number().min(0).default(0).optional(),
333
382
  include_window: boolFlag(),
383
+ compact: boolFlag(),
334
384
  window: z.number().min(1).max(5).default(1).optional()
335
385
  });
336
386
  var AuditSchema = z.object({
@@ -347,7 +397,8 @@ var SearchSchema = z.object({
347
397
  user_id: z.string().optional(),
348
398
  time_range: z.enum(TIME_RANGES).default("all").optional(),
349
399
  limit: z.number().min(1).max(100).default(20).optional(),
350
- include_tool_io: boolFlag()
400
+ include_tool_io: boolFlag(),
401
+ offset: z.number().min(0).max(1e4).optional()
351
402
  });
352
403
  var FEEDBACK_CATEGORIES = [
353
404
  "bad_cluster_label",
@@ -383,21 +434,27 @@ function asString(value) {
383
434
  function asNumber(value) {
384
435
  return typeof value === "number" && Number.isFinite(value) ? value : null;
385
436
  }
437
+ function frustrationAnchorIndex(row) {
438
+ if (!row)
439
+ return null;
440
+ const userQuotes = Array.isArray(row.user_quotes) ? row.user_quotes : [];
441
+ const firstQuote = asRecord(userQuotes[0]);
442
+ const resolved = asNumber(firstQuote?.msg_index) ?? asNumber(row.anchor_msg_index);
443
+ if (resolved !== null)
444
+ return resolved;
445
+ const keyTurns = Array.isArray(row.key_turns) ? row.key_turns : [];
446
+ const firstTurn = keyTurns.find((t) => typeof t === "number");
447
+ if (firstTurn !== undefined)
448
+ return firstTurn;
449
+ return asNumber(firstQuote?.turn);
450
+ }
386
451
  function buildFrustrationAnchor(row) {
387
452
  const conversation_id = asString(row.conversation_id) ?? "";
388
453
  const userQuotes = Array.isArray(row.user_quotes) ? row.user_quotes : [];
389
454
  const firstQuote = asRecord(userQuotes[0]);
390
455
  const keyTurnsRaw = Array.isArray(row.key_turns) ? row.key_turns : [];
391
456
  const all_turns = keyTurnsRaw.filter((t) => typeof t === "number");
392
- let msg_index = null;
393
- if (all_turns.length > 0) {
394
- msg_index = all_turns[0];
395
- }
396
- if (msg_index === null && firstQuote) {
397
- const turn = asNumber(firstQuote.turn);
398
- if (turn !== null)
399
- msg_index = turn;
400
- }
457
+ const msg_index = frustrationAnchorIndex(row);
401
458
  let signal = null;
402
459
  let quote_preview = null;
403
460
  if (firstQuote) {
@@ -431,6 +488,43 @@ function buildToolFailureAnchor(row, toolName) {
431
488
  no_anchor: msg_index === null
432
489
  };
433
490
  }
491
+ function frustrationsFromEmotions(response) {
492
+ if (!Array.isArray(response.detections) || Array.isArray(response.frustrations))
493
+ return response;
494
+ const summary = asRecord(response.summary) ?? {};
495
+ const pagination = asRecord(response.pagination) ?? {};
496
+ const byFamily = asRecord(summary.by_family) ?? {};
497
+ const totalAnalyzed = asNumber(summary.conversations_analyzed) ?? 0;
498
+ const frustrated = asNumber(byFamily.frustration) ?? 0;
499
+ const frustrations = response.detections.map((item) => asRecord(item)).filter((row) => row !== null && (row.family === undefined || row.family === "frustration")).map(({ conversation_context, score, is_detected, ...row }) => ({
500
+ ...row,
501
+ is_frustrated: is_detected === true,
502
+ frustration_score: score,
503
+ target: row.elicitor === "bot" || row.elicitor === "external" ? row.elicitor : "ambiguous",
504
+ conversation: conversation_context ?? null
505
+ }));
506
+ const atRisk = asNumber(pagination.at_risk) ?? Math.max(0, (asNumber(pagination.total) ?? 0) - frustrated);
507
+ const signalBreakdown = {};
508
+ for (const item of Array.isArray(response.signal_breakdown) ? response.signal_breakdown : []) {
509
+ const row = asRecord(item);
510
+ if (row?.family === "frustration" && typeof row.signal === "string") {
511
+ signalBreakdown[row.signal] = asNumber(row.count) ?? 0;
512
+ }
513
+ }
514
+ const { detections: _detections, ...rest } = response;
515
+ return {
516
+ ...rest,
517
+ summary: {
518
+ total_analyzed: totalAnalyzed,
519
+ frustrated_count: frustrated,
520
+ at_risk_count: atRisk,
521
+ frustration_rate_pct: totalAnalyzed > 0 ? Math.round(frustrated / totalAnalyzed * 1000) / 10 : 0
522
+ },
523
+ signal_breakdown: signalBreakdown,
524
+ frustrations,
525
+ source: "emotion_detections"
526
+ };
527
+ }
434
528
  function withFrustrationAnchors(response) {
435
529
  if (!Array.isArray(response.frustrations))
436
530
  return response;
@@ -443,6 +537,76 @@ function withFrustrationAnchors(response) {
443
537
  });
444
538
  return { ...response, frustrations };
445
539
  }
540
+ var TOOL_FAILURE_TEXT_LIMIT = 300;
541
+ var TOOL_FAILURE_CONTEXT_RADIUS = 2;
542
+ function shellWord(value) {
543
+ return /^[\w@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
544
+ }
545
+ function trimFailureWindow(value, radius, fallbackCenter) {
546
+ const conv = asRecord(value);
547
+ if (!conv)
548
+ return value ?? null;
549
+ const { coverage: _coverage, context, ...convRest } = conv;
550
+ const ctx = asRecord(context);
551
+ if (!ctx)
552
+ return convRest;
553
+ const center = asNumber(ctx.center_index) ?? fallbackCenter;
554
+ const messages = (Array.isArray(ctx.messages) ? ctx.messages : []).map((message) => asRecord(message)).filter((message) => {
555
+ const index = asNumber(message?.index);
556
+ return message !== null && (center === null || index === null || Math.abs(index - center) <= radius);
557
+ }).map(({ content_blocks, ...message }) => {
558
+ const covered = typeof message.content === "string" && message.content.length > 0 || Array.isArray(message.tool_calls) && message.tool_calls.length > 0 || Array.isArray(message.tool_results) && message.tool_results.length > 0;
559
+ return covered || content_blocks === undefined ? message : { ...message, content_blocks };
560
+ });
561
+ const indexes = messages.map((message) => asNumber(message.index)).filter((index) => index !== null);
562
+ return {
563
+ ...convRest,
564
+ context: {
565
+ ...ctx,
566
+ ...indexes.length > 0 ? { from_index: Math.min(...indexes), to_index: Math.max(...indexes) } : {},
567
+ messages
568
+ }
569
+ };
570
+ }
571
+ function trimToolFailureConversations(response, opts) {
572
+ if (!Array.isArray(response.examples))
573
+ return response;
574
+ const examples = response.examples.map((item) => {
575
+ const row = asRecord(item);
576
+ if (!row)
577
+ return item;
578
+ const { conversation, window, ...rest } = row;
579
+ const conversationId = asString(row.conversation_id);
580
+ const msgIndex = asNumber(asRecord(row.anchor)?.msg_index) ?? asNumber(row.msg_index);
581
+ return {
582
+ ...rest,
583
+ ...conversationId && msgIndex !== null ? { context_command: `moda context ${shellWord(conversationId)} --msg-index=${msgIndex} --window=${TOOL_FAILURE_CONTEXT_RADIUS}` } : {},
584
+ ...opts.drop ? {} : { conversation: trimFailureWindow(conversation, TOOL_FAILURE_CONTEXT_RADIUS, msgIndex) },
585
+ ..."window" in row ? { window: trimFailureWindow(window, opts.windowRadius ?? TOOL_FAILURE_CONTEXT_RADIUS, msgIndex) } : {}
586
+ };
587
+ });
588
+ return { ...response, examples };
589
+ }
590
+ function compactToolFailureExamples(response, maxChars = TOOL_FAILURE_TEXT_LIMIT) {
591
+ if (!Array.isArray(response.examples))
592
+ return { data: response, truncated: 0 };
593
+ let truncated = 0;
594
+ const cap = (value) => {
595
+ if (typeof value === "string") {
596
+ if (value.length <= maxChars)
597
+ return value;
598
+ truncated += 1;
599
+ return `${value.slice(0, maxChars)}… [truncated ${value.length - maxChars} chars]`;
600
+ }
601
+ if (Array.isArray(value))
602
+ return value.map(cap);
603
+ if (value && typeof value === "object") {
604
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, cap(child)]));
605
+ }
606
+ return value;
607
+ };
608
+ return { data: { ...response, examples: response.examples.map(cap) }, truncated };
609
+ }
446
610
  function withToolFailureAnchors(response) {
447
611
  if (!Array.isArray(response.examples))
448
612
  return response;
@@ -482,8 +646,11 @@ import {
482
646
  openSync,
483
647
  readFileSync,
484
648
  realpathSync,
649
+ renameSync,
650
+ writeFileSync,
485
651
  writeSync
486
652
  } from "node:fs";
653
+ import { createHash } from "node:crypto";
487
654
  import { dirname, isAbsolute, join, resolve, sep } from "node:path";
488
655
  var O_NOFOLLOW = fsConstants.O_NOFOLLOW ?? 0;
489
656
  var ADVANCE_TIMEOUT_MS = 300000;
@@ -495,22 +662,60 @@ var DRIVE_PASS_INTERVAL_MS = 20000;
495
662
  var DRIVE_LIST_LIMIT = 100;
496
663
  var DRIVE_LIST_MAX_PAGES = 10;
497
664
  var DRAFT_BATCH_LIMIT_MAX = 25;
665
+ var DRAFT_BATCH_LIMIT_DEFAULT = 5;
666
+ var DRIVE_MAX_FIXES_DEFAULT = 3;
667
+ var DRIVE_MAX_FIXES_MAX = 10;
668
+ var VERIFY_COOLDOWN_MS = 10 * 60 * 1000;
669
+ var REGATE_COOLDOWN_STATUSES = new Set(["VERIFIED", "GATE_FAILED", "GATE_INCONCLUSIVE"]);
498
670
  var DRAFT_BATCH_TIMEOUT_MS = 300000;
499
671
  var ADVANCING_STATUSES = new Set(["DRAFT", "SCOPING", "PROPOSING", "PROPOSED", "GATING"]);
500
672
  var FIX_USAGE = [
501
673
  "Usage: moda fixes [--status=S] [--limit=N] [--cursor=TOKEN]",
502
- " moda fixes draft-batch [--limit=N] (N is 1-25, default 10)",
503
- " moda fixes drive [--status=S] [--timeout=ms] [--pass-interval=ms]",
674
+ " moda fixes draft-batch [--limit=N] (N is 1-25, default 5)",
675
+ " moda fixes drive [--status=S] [--max-fixes=N] [--timeout=ms] [--pass-interval=ms] (N is 1-10, default 3)",
504
676
  " moda fix <fix_id> [--packet] [--wait]",
505
677
  " moda fix start <problem_id> [--type=auto|prompt] [--wait] [--timeout=ms]",
506
- " moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms]",
678
+ " moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms] [--yes]",
507
679
  " moda fix checkout <fix_id>",
508
680
  " moda fix submit <fix_id> --pr | --local-ref=<branch>",
509
681
  " moda fix mark-applied <fix_id> [--note=TEXT]",
510
682
  " moda fix dismiss <fix_id> --reason=TEXT"
511
683
  ].join(`
512
684
  `);
685
+ var FIXES_SUBCOMMAND_FLAGS = {
686
+ list: ["cursor", "limit", "status"],
687
+ "draft-batch": ["limit"],
688
+ drive: ["status", "timeout", "pass-interval", "max-fixes"]
689
+ };
690
+ var FIX_SUBCOMMAND_FLAGS = {
691
+ detail: ["packet", "timeout", "wait", "poll-interval"],
692
+ start: ["timeout", "type", "wait", "poll-interval", "problem-id"],
693
+ verify: ["timeout", "wait", "fix-id", "no-wait", "poll-interval", "prompt-file"],
694
+ checkout: ["fix-id"],
695
+ submit: ["pr", "fix-id", "local-ref"],
696
+ "mark-applied": ["note", "fix-id"],
697
+ dismiss: ["reason", "fix-id"]
698
+ };
699
+ function assertWaitFlags(flags) {
700
+ const maxByFlag = {
701
+ timeout: MAX_WAIT_TIMEOUT_MS,
702
+ "poll-interval": MAX_POLL_INTERVAL_MS,
703
+ "pass-interval": MAX_POLL_INTERVAL_MS
704
+ };
705
+ for (const [name, max] of Object.entries(maxByFlag)) {
706
+ const raw = flags[name];
707
+ if (raw === undefined)
708
+ continue;
709
+ const value = Number(raw);
710
+ if (!/^\d+$/.test(raw.trim()) || !Number.isSafeInteger(value) || value < 1 || value > max) {
711
+ throw new CliInputError(`--${name}=${raw} must be a whole number of milliseconds between 1 and ${max}, so nothing was sent.`, FIX_USAGE);
712
+ }
713
+ }
714
+ }
513
715
  async function runFixesCommand(context) {
716
+ const fixesSub = context.positional && FIXES_SUBCOMMAND_FLAGS[context.positional] ? context.positional : "list";
717
+ assertFlags(fixesSub === "list" ? "fixes" : `fixes ${fixesSub}`, context.flags, [...FIXES_SUBCOMMAND_FLAGS[fixesSub], "tenant-id"]);
718
+ assertWaitFlags(context.flags);
514
719
  validateConfig();
515
720
  const profileOptions = profileOptionsFor(context);
516
721
  const tenantId = requireTenantId(context.flags, profileOptions);
@@ -564,8 +769,8 @@ async function runFixesCommand(context) {
564
769
  return EXIT_OK;
565
770
  }
566
771
  async function runFixesDraftBatch(context, profileOptions, tenantId) {
567
- const usage = "Usage: moda fixes draft-batch [--limit=N] (N is 1-25, default 10)";
568
- let limit;
772
+ const usage = "Usage: moda fixes draft-batch [--limit=N] (N is 1-25, default 5)";
773
+ let limit = DRAFT_BATCH_LIMIT_DEFAULT;
569
774
  if (context.flags.limit) {
570
775
  const parsed = Number(context.flags.limit);
571
776
  if (!Number.isInteger(parsed) || parsed < 1 || parsed > DRAFT_BATCH_LIMIT_MAX) {
@@ -573,7 +778,7 @@ async function runFixesDraftBatch(context, profileOptions, tenantId) {
573
778
  }
574
779
  limit = parsed;
575
780
  }
576
- const result = await callControlAPI(`${fixesBase(tenantId)}/draft-batch`, { method: "POST", body: JSON.stringify(limit !== undefined ? { limit } : {}) }, profileOptions, { timeoutMs: DRAFT_BATCH_TIMEOUT_MS, retries: 0 });
781
+ const result = await callControlAPI(`${fixesBase(tenantId)}/draft-batch`, { method: "POST", body: JSON.stringify({ limit }) }, profileOptions, { timeoutMs: DRAFT_BATCH_TIMEOUT_MS, retries: 0 });
577
782
  const drafted = result.drafted ?? [];
578
783
  const skipped = result.skipped ?? [];
579
784
  if (context.outputMode === "human") {
@@ -618,10 +823,53 @@ async function runFixesDraftBatch(context, profileOptions, tenantId) {
618
823
  });
619
824
  return EXIT_OK;
620
825
  }
826
+ function driveStatePath(tenantId, profileOptions) {
827
+ const safeTenant = tenantId.replace(/[^A-Za-z0-9_-]/g, "_");
828
+ return join(getModaPaths(profileOptions).dir, `fixes-drive-last.${safeTenant}.json`);
829
+ }
830
+ function lastDrivenTimes(tenantId, profileOptions) {
831
+ try {
832
+ const raw = JSON.parse(readFileSync(driveStatePath(tenantId, profileOptions), "utf8"));
833
+ if (Array.isArray(raw)) {
834
+ return new Map(raw.filter((id) => typeof id === "string").map((id) => [id, Date.now()]));
835
+ }
836
+ const out = new Map;
837
+ for (const [id, at] of Object.entries(raw ?? {})) {
838
+ if (typeof at === "number" && Number.isFinite(at))
839
+ out.set(id, at);
840
+ }
841
+ return out;
842
+ } catch {
843
+ return new Map;
844
+ }
845
+ }
846
+ function recordDrivenFixIds(tenantId, ids, previous, profileOptions) {
847
+ try {
848
+ const now = Date.now();
849
+ const merged = new Map(previous);
850
+ for (const id of ids)
851
+ merged.set(id, now);
852
+ const entries = [...merged.entries()].sort((left, right) => right[1] - left[1]).slice(0, 500);
853
+ const path = driveStatePath(tenantId, profileOptions);
854
+ mkdirSync(dirname(path), { recursive: true });
855
+ const tmp = `${path}.${process.pid}.${now}.tmp`;
856
+ writeFileSync(tmp, `${JSON.stringify(Object.fromEntries(entries))}
857
+ `, { mode: 384 });
858
+ renameSync(tmp, path);
859
+ } catch {}
860
+ }
621
861
  async function runFixesDrive(context, profileOptions, tenantId) {
622
862
  const timeoutMs = parsePositiveInt(context.flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, MAX_WAIT_TIMEOUT_MS);
623
863
  const passIntervalMs = parsePositiveInt(context.flags["pass-interval"], DRIVE_PASS_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
624
864
  const deadline = Date.now() + timeoutMs;
865
+ let maxFixes = DRIVE_MAX_FIXES_DEFAULT;
866
+ if (context.flags["max-fixes"] !== undefined) {
867
+ const parsed = Number(context.flags["max-fixes"]);
868
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > DRIVE_MAX_FIXES_MAX) {
869
+ throw new CliInputError(`--max-fixes must be an integer between 1 and ${DRIVE_MAX_FIXES_MAX} (got ${context.flags["max-fixes"]}) — each driven fix can run hundreds of replay playouts`, FIX_USAGE);
870
+ }
871
+ maxFixes = parsed;
872
+ }
625
873
  const statusFilter = (context.flags.status ?? "").trim().toUpperCase();
626
874
  if (statusFilter && !ADVANCING_STATUSES.has(statusFilter)) {
627
875
  throw new CliInputError(`--status must be one of the advancing statuses (${[...ADVANCING_STATUSES].join(", ")}) — a resting fix has nothing to drive (got ${context.flags.status})`, FIX_USAGE);
@@ -669,10 +917,27 @@ async function runFixesDrive(context, profileOptions, tenantId) {
669
917
  }
670
918
  }
671
919
  const tracked = new Map;
672
- for (const fix of snapshot) {
673
- if (ADVANCING_STATUSES.has(fix.status) && !isRestingProposed(fix)) {
920
+ const skipped = [];
921
+ const advancing = snapshot.filter((fix) => ADVANCING_STATUSES.has(fix.status) && !isRestingProposed(fix));
922
+ const drivenAt = lastDrivenTimes(tenantId, profileOptions);
923
+ const rankOf = new Map(advancing.map((fix, index) => [fix.id, index]));
924
+ const byLeastRecent = [...advancing].sort((left, right) => {
925
+ const leftAt = drivenAt.get(left.id) ?? -1;
926
+ const rightAt = drivenAt.get(right.id) ?? -1;
927
+ return leftAt !== rightAt ? leftAt - rightAt : (rankOf.get(left.id) ?? 0) - (rankOf.get(right.id) ?? 0);
928
+ });
929
+ const picked = new Set(byLeastRecent.slice(0, maxFixes).map((fix) => fix.id));
930
+ for (const fix of advancing) {
931
+ if (picked.has(fix.id))
674
932
  tracked.set(fix.id, fix);
675
- }
933
+ else
934
+ skipped.push(fix);
935
+ }
936
+ recordDrivenFixIds(tenantId, [...picked], drivenAt, profileOptions);
937
+ const gatingCount = [...tracked.values()].filter((fix) => fix.status === "GATING").length;
938
+ if (skipped.length > 0) {
939
+ coverageTruncated = true;
940
+ snapshotWarnings.push(`Drove ${tracked.size} fix(es) (--max-fixes=${maxFixes}; ${gatingCount} already gating; least-recently-driven first); skipped ${skipped.length} advancing fix(es): ${skipped.map((fix) => `MODA-FIX-${fix.shortRef}`).join(", ")}. Re-run \`moda fixes drive\` to drive the next ones.`);
676
941
  }
677
942
  if (tracked.size === 0) {
678
943
  context.output.writeData({
@@ -753,6 +1018,7 @@ async function runFixesDrive(context, profileOptions, tenantId) {
753
1018
  statusReason: fix.statusReason ?? null
754
1019
  }));
755
1020
  const stragglers = [...tracked.values()].filter((fix) => !isRested(fix));
1021
+ const skippedRefs = skipped.map((fix) => fix.shortRef);
756
1022
  const warnings = [
757
1023
  ...snapshotWarnings,
758
1024
  ...[...errors.entries()].map(([fixId, message]) => {
@@ -779,7 +1045,9 @@ ${coverageTruncated ? `Rested all ${tracked.size} scanned fix(es)` : `All ${trac
779
1045
  rested: restedCount,
780
1046
  stragglers: stragglers.map((fix) => fix.shortRef),
781
1047
  timedOut,
782
- coverageTruncated
1048
+ coverageTruncated,
1049
+ maxFixes,
1050
+ skipped: skippedRefs
783
1051
  }, {
784
1052
  ...degraded ? { status: "degraded" } : {},
785
1053
  summary: {
@@ -788,27 +1056,38 @@ ${coverageTruncated ? `Rested all ${tracked.size} scanned fix(es)` : `All ${trac
788
1056
  },
789
1057
  ...warnings.length > 0 ? { warnings } : {},
790
1058
  nextCommands: [
791
- timedOut ? {
1059
+ ...timedOut ? [{
792
1060
  command: "moda fixes drive",
793
1061
  purpose: "Resume driving the stragglers — advance-on-poll picks up where it left off.",
794
1062
  mutability: "write",
795
1063
  requires_approval: false
796
- } : coverageTruncated ? {
797
- command: !scanTruncated ? "moda fixes drive" : statusFilter ? "moda fix <fix_id> --wait" : "moda fixes drive --status=SCOPING",
798
- purpose: !scanTruncated ? "The queue was re-ranked mid-page — re-run to pick up any fix that shifted past the boundary." : statusFilter ? `Status ${statusFilter} overflows the scan window — advance the older fixes individually.` : "Drive each advancing status separately to reach fixes outside the unfiltered scan window.",
799
- mutability: "write",
800
- requires_approval: false
801
- } : {
1064
+ }] : coverageTruncated ? [
1065
+ ...skipped.length > 0 || !scanTruncated ? [{
1066
+ command: "moda fixes drive",
1067
+ purpose: skipped.length > 0 ? `Drive the next ${Math.min(skipped.length, maxFixes)} of the ${skipped.length} skipped fix(es) (--max-fixes spend cap).` : "The queue was re-ranked mid-page — re-run to pick up any fix that shifted past the boundary.",
1068
+ mutability: "write",
1069
+ requires_approval: false
1070
+ }] : [],
1071
+ ...scanTruncated ? [{
1072
+ command: statusFilter ? "moda fix <fix_id> --wait" : "moda fixes drive --status=SCOPING",
1073
+ purpose: statusFilter ? `Status ${statusFilter} overflows the scan window — advance the older fixes individually.` : "Drive each advancing status separately to reach fixes outside the unfiltered scan window.",
1074
+ mutability: "write",
1075
+ requires_approval: false
1076
+ }] : []
1077
+ ] : [{
802
1078
  command: "moda fixes",
803
1079
  purpose: "Review where every fix landed (VERIFIED fixes are ready to deliver).",
804
1080
  mutability: "read",
805
1081
  requires_approval: false
806
- }
1082
+ }]
807
1083
  ]
808
1084
  });
809
1085
  return degraded ? EXIT_DEGRADED : EXIT_OK;
810
1086
  }
811
1087
  async function runFixCommand(context) {
1088
+ const fixSub = context.positional && FIX_SUBCOMMAND_FLAGS[context.positional] ? context.positional : "detail";
1089
+ assertFlags(fixSub === "detail" ? "fix" : `fix ${fixSub}`, context.flags, [...FIX_SUBCOMMAND_FLAGS[fixSub], "tenant-id"]);
1090
+ assertWaitFlags(context.flags);
812
1091
  validateConfig();
813
1092
  const profileOptions = profileOptionsFor(context);
814
1093
  const tenantId = requireTenantId(context.flags, profileOptions);
@@ -916,6 +1195,10 @@ async function runFixVerify(context, profileOptions, tenantId) {
916
1195
  throw new CliInputError("--prompt-file content is shorter than 40 characters — refusing to gate a truncated candidate");
917
1196
  }
918
1197
  }
1198
+ const candidateHash = candidateContent === undefined ? undefined : sha256(candidateContent.trim());
1199
+ if (!context.yes) {
1200
+ await assertRegateCooldown(tenantId, fixId, context, profileOptions, candidateHash);
1201
+ }
919
1202
  const enqueue = await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}/verify`, { method: "POST", body: JSON.stringify(candidateContent ? { candidateContent } : {}) }, profileOptions, { timeoutMs: 120000, retries: 0 });
920
1203
  const noWait = context.flags.wait === "false" || context.flags["no-wait"] === "true";
921
1204
  if (noWait) {
@@ -1267,7 +1550,7 @@ function profileOptionsFor(context) {
1267
1550
  return { profile: context.profile, env: context.env };
1268
1551
  }
1269
1552
  function requireTenantId(flags, profileOptions) {
1270
- const tenantId = flags["tenant-id"] || resolveTenantId(profileOptions);
1553
+ const tenantId = flags["tenant-id"] || resolveApiTenantId(profileOptions);
1271
1554
  if (!tenantId) {
1272
1555
  throw new CliInputError("Missing tenant id. Run `moda init` or pass --tenant-id=<id>.");
1273
1556
  }
@@ -1276,6 +1559,29 @@ function requireTenantId(flags, profileOptions) {
1276
1559
  function fixesBase(tenantId) {
1277
1560
  return `/tenants/${encodeURIComponent(tenantId)}/fixes`;
1278
1561
  }
1562
+ async function assertRegateCooldown(tenantId, fixId, context, profileOptions, candidateHash) {
1563
+ const current = await getFix(tenantId, fixId, profileOptions);
1564
+ if (candidateHash && current.candidateRef?.contentSha256 !== candidateHash)
1565
+ return;
1566
+ const gate = current.gateResult ?? null;
1567
+ if (!REGATE_COOLDOWN_STATUSES.has(current.status) || !gate?.verdict || gate.pending)
1568
+ return;
1569
+ const landedAt = typeof gate.completedAt === "string" ? Date.parse(gate.completedAt) : Number.NaN;
1570
+ if (Number.isNaN(landedAt))
1571
+ return;
1572
+ const ageMs = Date.now() - landedAt;
1573
+ if (ageMs >= VERIFY_COOLDOWN_MS)
1574
+ return;
1575
+ const waitMin = Math.ceil((VERIFY_COOLDOWN_MS - ageMs) / 60000);
1576
+ const rerun = ["moda", "fix", "verify", fixId, ...Object.entries(context.flags).filter(([key]) => key !== "yes" && key !== "fix-id").map(([key, value]) => value === "true" ? `--${key}` : `--${key}=${shellQuote(value)}`), "--yes"].join(" ");
1577
+ throw new CliInputError(`MODA-FIX-${current.shortRef} was gated ${Math.max(0, Math.floor(ageMs / 60000))} min ago (verdict ${gate.verdict}); re-gating within 10 minutes re-runs the replay. Nothing was sent.`, `Wait ~${waitMin} min, or re-run with --yes to gate now: ${rerun}`);
1578
+ }
1579
+ function sha256(content) {
1580
+ return createHash("sha256").update(content, "utf8").digest("hex");
1581
+ }
1582
+ function shellQuote(value) {
1583
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
1584
+ }
1279
1585
  async function getFix(tenantId, fixId, profileOptions) {
1280
1586
  return await callControlAPI(`${fixesBase(tenantId)}/${encodeURIComponent(fixId)}`, { method: "GET" }, profileOptions);
1281
1587
  }
@@ -1753,6 +2059,443 @@ async function runSyncCommand(context) {
1753
2059
  throw new CliInputError(`Unknown sync resource '${resource}'.`, "Usage: moda sync [harness|prompts|skills] [--dry-run] [--profile=NAME]");
1754
2060
  }
1755
2061
 
2062
+ // src/registry.ts
2063
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
2064
+ import { dirname as dirname2, isAbsolute as isAbsolute2, relative, resolve as resolve2 } from "node:path";
2065
+ var REGISTRY_SCHEMA = "moda_registry.v1";
2066
+ var DEFAULT_REGISTRY_PATH = ".moda/registry.json";
2067
+ var SKILL_KEY_PATTERN = /^[A-Za-z0-9_-]+$/;
2068
+ var MAX_ITEMS_PER_SECTION = 500;
2069
+ var REGISTRY_TEMPLATE = {
2070
+ schema: REGISTRY_SCHEMA,
2071
+ prompts: [
2072
+ {
2073
+ key: "support.triage",
2074
+ name: "Support triage system prompt",
2075
+ description: "Routes inbound support messages",
2076
+ content: "You are a support triage agent. Classify the request and ...",
2077
+ sourcePath: "src/agents/triage/prompt.ts"
2078
+ },
2079
+ {
2080
+ key: "support.reply",
2081
+ file: "prompts/reply.md"
2082
+ }
2083
+ ],
2084
+ skills: [
2085
+ {
2086
+ key: "refund-policy",
2087
+ description: "How to process refund requests",
2088
+ skillMd: `---
2089
+ name: refund-policy
2090
+ description: How to process refund requests
2091
+ ---
2092
+
2093
+ # Refund policy
2094
+ ...`
2095
+ }
2096
+ ],
2097
+ tools: [
2098
+ {
2099
+ schema: {
2100
+ name: "lookup_customer",
2101
+ description: "Find a customer by email",
2102
+ parameters: {
2103
+ type: "object",
2104
+ properties: { email: { type: "string" } },
2105
+ required: ["email"]
2106
+ }
2107
+ }
2108
+ }
2109
+ ]
2110
+ };
2111
+ var REGISTRY_SUBCOMMAND_FLAGS = {
2112
+ template: ["out", "dry-run"],
2113
+ validate: ["file", "manifest"],
2114
+ push: ["file", "manifest", "live", "dry-run"]
2115
+ };
2116
+ async function runRegistryCommand(context) {
2117
+ const subcommand = context.positionals[0] ?? "push";
2118
+ assertFlags(`registry ${subcommand}`, context.flags, REGISTRY_SUBCOMMAND_FLAGS[subcommand] ?? []);
2119
+ switch (subcommand) {
2120
+ case "template":
2121
+ return writeTemplate(context);
2122
+ case "validate": {
2123
+ const registry = loadRegistry(context);
2124
+ context.output.writeData({
2125
+ schema: REGISTRY_SCHEMA,
2126
+ manifest: displayPath(context, registry.manifestPath),
2127
+ valid: true,
2128
+ counts: countsOf(registry)
2129
+ });
2130
+ return { exitCode: 0 };
2131
+ }
2132
+ case "push":
2133
+ return pushRegistry(context);
2134
+ default:
2135
+ throw new CliInputError(`Unknown registry command '${subcommand}'.`, "Usage: moda registry template|validate|push [--file=.moda/registry.json] [--dry-run] [--live]");
2136
+ }
2137
+ }
2138
+ function writeTemplate(context) {
2139
+ const out = context.flags.out;
2140
+ if (out === "true") {
2141
+ throw new CliInputError("Missing output path for --out.", "Usage: moda registry template --out=.moda/registry.json");
2142
+ }
2143
+ if (!out) {
2144
+ context.output.writeData(REGISTRY_TEMPLATE);
2145
+ return { exitCode: 0 };
2146
+ }
2147
+ const target = resolve2(context.cwd, out);
2148
+ if (existsSync2(target) && !context.yes) {
2149
+ throw new CliInputError(`${out} already exists; refusing to overwrite.`, "Pass --yes to overwrite it.");
2150
+ }
2151
+ if (!context.dryRun) {
2152
+ mkdirSync2(dirname2(target), { recursive: true });
2153
+ writeFileSync2(target, `${JSON.stringify(REGISTRY_TEMPLATE, null, 2)}
2154
+ `);
2155
+ }
2156
+ context.output.writeData({ written: context.dryRun ? [] : [displayPath(context, target)], dryRun: context.dryRun });
2157
+ return { exitCode: 0 };
2158
+ }
2159
+ async function pushRegistry(context) {
2160
+ const registry = loadRegistry(context);
2161
+ const profileOptions = { profile: context.profile, env: context.env };
2162
+ const hasKey = Boolean(resolveApiKey(profileOptions));
2163
+ if (!hasKey && !context.dryRun) {
2164
+ throw new CliInputError("No Moda API key found for registry push.", "Set MODA_API_KEY, run `moda auth login --api-key=<key>`, or select a profile with an API key.");
2165
+ }
2166
+ const sections = [];
2167
+ const push = async (section, endpoint, count, body) => {
2168
+ if (count === 0) {
2169
+ sections.push({ section, status: "skipped", count, endpoint });
2170
+ return;
2171
+ }
2172
+ if (context.dryRun && !hasKey) {
2173
+ sections.push({ section, status: "validated", count, endpoint });
2174
+ return;
2175
+ }
2176
+ try {
2177
+ const response = await callControlAPI(endpoint, { method: "POST", body: JSON.stringify({ ...body, ...context.dryRun ? { dryRun: true } : {} }) }, profileOptions);
2178
+ sections.push({ section, status: context.dryRun ? "dry_run" : "synced", count, endpoint, response });
2179
+ } catch (error) {
2180
+ sections.push({
2181
+ section,
2182
+ status: "error",
2183
+ count,
2184
+ endpoint,
2185
+ error: error instanceof Error ? error.message : String(error)
2186
+ });
2187
+ }
2188
+ };
2189
+ await push("prompts", "/prompts/sync", registry.prompts.length, { prompts: registry.prompts });
2190
+ await push("skills", "/skills/sync", registry.skills.length, { skills: registry.skills });
2191
+ await push("tools", "/tool-definitions/sync", registry.tools.length, { tools: registry.tools });
2192
+ const failed = sections.filter((section) => section.status === "error");
2193
+ if (context.outputMode === "human" && !context.quiet) {
2194
+ for (const section of sections) {
2195
+ const detail = section.status === "error" ? ` — ${section.error}` : describeActions(section.response);
2196
+ console.error(` ${section.section}: ${section.status} (${section.count})${detail}`);
2197
+ }
2198
+ }
2199
+ context.output.writeData({
2200
+ schema: REGISTRY_SCHEMA,
2201
+ manifest: displayPath(context, registry.manifestPath),
2202
+ dryRun: context.dryRun,
2203
+ counts: countsOf(registry),
2204
+ sections
2205
+ }, {
2206
+ status: failed.length > 0 ? "error" : "success",
2207
+ ...failed.length > 0 ? {
2208
+ errors: failed.map((section) => ({
2209
+ code: "registry_section_failed",
2210
+ message: `${section.section}: ${section.error}`,
2211
+ blocking: false
2212
+ }))
2213
+ } : {}
2214
+ });
2215
+ return { exitCode: failed.length > 0 ? 1 : 0 };
2216
+ }
2217
+ function describeActions(response) {
2218
+ const synced = isRecord(response) && Array.isArray(response.synced) ? response.synced : [];
2219
+ const counts = new Map;
2220
+ for (const item of synced) {
2221
+ if (!isRecord(item))
2222
+ continue;
2223
+ const action = typeof item.action === "string" ? `${item.action}${typeof item.reason === "string" ? ` (${item.reason})` : ""}` : item.changed === false ? "unchanged" : item.policyChanged === true && item.changed === true ? "changed (live setting)" : item.changed === true ? "changed" : "synced";
2224
+ counts.set(action, (counts.get(action) ?? 0) + 1);
2225
+ }
2226
+ return counts.size > 0 ? ` — ${[...counts].map(([action, count]) => `${count} ${action}`).join(", ")}` : "";
2227
+ }
2228
+ function loadRegistry(context) {
2229
+ const fileFlag = context.flags.file ?? context.flags.manifest;
2230
+ if (fileFlag === "true") {
2231
+ throw new CliInputError("Missing path for --file.", "Usage: moda registry push --file=.moda/registry.json");
2232
+ }
2233
+ const manifestPath = resolve2(context.cwd, fileFlag ?? DEFAULT_REGISTRY_PATH);
2234
+ if (!existsSync2(manifestPath)) {
2235
+ throw new CliInputError(`Registry manifest not found: ${fileFlag ?? DEFAULT_REGISTRY_PATH}`, "Create one with `moda registry template --out=.moda/registry.json`, fill it in, then rerun.");
2236
+ }
2237
+ let parsed;
2238
+ try {
2239
+ parsed = JSON.parse(readFileSync2(manifestPath, "utf8"));
2240
+ } catch (error) {
2241
+ throw new CliInputError(`Registry manifest is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
2242
+ }
2243
+ return resolveRegistry(parsed, {
2244
+ manifestPath,
2245
+ cwd: context.cwd,
2246
+ forceLive: context.flags.live === "true"
2247
+ });
2248
+ }
2249
+ var PROMPT_FIELDS = {
2250
+ key: "string",
2251
+ name: "string",
2252
+ description: "string",
2253
+ category: "string",
2254
+ content: "string",
2255
+ systemPrompt: "string",
2256
+ messages: "array",
2257
+ variables: "array",
2258
+ variablesSchema: "object",
2259
+ modelConfig: "object",
2260
+ toolRefs: "array",
2261
+ responseSchema: "object",
2262
+ sourcePath: "string",
2263
+ file: "string"
2264
+ };
2265
+ var SKILL_FIELDS = {
2266
+ key: "string",
2267
+ name: "string",
2268
+ description: "string",
2269
+ skillMd: "string",
2270
+ sourcePath: "string",
2271
+ file: "string",
2272
+ live: "boolean"
2273
+ };
2274
+ var TOOL_FIELDS = {
2275
+ name: "string",
2276
+ description: "string",
2277
+ schema: "object",
2278
+ isActive: "boolean"
2279
+ };
2280
+ var MAX_SKILL_KEY_LENGTH = 256;
2281
+ var MAX_SKILL_BYTES = 1e6;
2282
+ function checkFields(label, item, spec, errors) {
2283
+ for (const [key, value] of Object.entries(item)) {
2284
+ const expected = spec[key];
2285
+ if (!expected) {
2286
+ errors.push(`${label}: unknown field "${key}" (allowed: ${Object.keys(spec).join(", ")})`);
2287
+ continue;
2288
+ }
2289
+ const actual = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
2290
+ if (actual !== expected)
2291
+ errors.push(`${label}.${key} must be ${expected === "array" || expected === "object" ? "an" : "a"} ${expected} (got ${actual})`);
2292
+ }
2293
+ }
2294
+ function resolveRegistry(input, options) {
2295
+ const errors = [];
2296
+ if (!isRecord(input)) {
2297
+ throw new CliInputError("Registry manifest must be a JSON object with prompts, skills, and/or tools arrays.");
2298
+ }
2299
+ if (input.schema !== undefined && input.schema !== REGISTRY_SCHEMA) {
2300
+ errors.push(`schema must be "${REGISTRY_SCHEMA}" (got ${JSON.stringify(input.schema)})`);
2301
+ }
2302
+ const known = new Set(["schema", "prompts", "skills", "tools"]);
2303
+ for (const key of Object.keys(input)) {
2304
+ if (!known.has(key))
2305
+ errors.push(`unknown top-level field "${key}"`);
2306
+ }
2307
+ const readRef = (label, file) => {
2308
+ if (typeof file !== "string" || file.trim() === "") {
2309
+ errors.push(`${label}.file must be a non-empty string`);
2310
+ return;
2311
+ }
2312
+ const target = resolve2(options.cwd, file);
2313
+ if (!isInside(options.cwd, target)) {
2314
+ errors.push(`${label}.file "${file}" resolves outside the project directory`);
2315
+ return;
2316
+ }
2317
+ if (!existsSync2(target)) {
2318
+ errors.push(`${label}.file "${file}" does not exist`);
2319
+ return;
2320
+ }
2321
+ if (!isInside(realpathSync2(options.cwd), realpathSync2(target))) {
2322
+ errors.push(`${label}.file "${file}" is a symlink to outside the project directory`);
2323
+ return;
2324
+ }
2325
+ if (!statSync(target).isFile()) {
2326
+ errors.push(`${label}.file "${file}" is not a file`);
2327
+ return;
2328
+ }
2329
+ return readFileSync2(target, "utf8");
2330
+ };
2331
+ const sourcePathFor = (file) => relative(options.cwd, resolve2(options.cwd, file)).split("\\").join("/");
2332
+ const prompts = arrayOf(input.prompts, "prompts", errors).map((raw, index) => {
2333
+ const label = `prompts[${index}]`;
2334
+ if (!isRecord(raw)) {
2335
+ errors.push(`${label} must be an object`);
2336
+ return;
2337
+ }
2338
+ checkFields(label, raw, PROMPT_FIELDS, errors);
2339
+ const { file, ...rest } = raw;
2340
+ if (!nonEmptyString(rest.key))
2341
+ errors.push(`${label}.key is required`);
2342
+ const prompt = { ...rest };
2343
+ if (file !== undefined) {
2344
+ if (rest.content !== undefined)
2345
+ errors.push(`${label}: set content or file, not both`);
2346
+ const content = readRef(label, file);
2347
+ if (content !== undefined)
2348
+ prompt.content = content;
2349
+ if (typeof file === "string" && !prompt.sourcePath)
2350
+ prompt.sourcePath = sourcePathFor(file);
2351
+ }
2352
+ if (!nonEmptyString(prompt.content) && !nonEmptyString(prompt.systemPrompt) && !(Array.isArray(prompt.messages) && prompt.messages.length > 0)) {
2353
+ errors.push(`${label}: one of content, file, systemPrompt, or messages is required`);
2354
+ }
2355
+ return prompt;
2356
+ });
2357
+ const skills = arrayOf(input.skills, "skills", errors).map((raw, index) => {
2358
+ const label = `skills[${index}]`;
2359
+ if (!isRecord(raw)) {
2360
+ errors.push(`${label} must be an object`);
2361
+ return;
2362
+ }
2363
+ checkFields(label, raw, SKILL_FIELDS, errors);
2364
+ const skill = raw;
2365
+ if (!nonEmptyString(skill.key)) {
2366
+ errors.push(`${label}.key is required`);
2367
+ } else if (!SKILL_KEY_PATTERN.test(skill.key)) {
2368
+ errors.push(`${label}.key "${skill.key}" may only contain letters, digits, "_" and "-"`);
2369
+ } else if (skill.key.length > MAX_SKILL_KEY_LENGTH) {
2370
+ errors.push(`${label}.key must be ${MAX_SKILL_KEY_LENGTH} characters or fewer`);
2371
+ }
2372
+ let skillMd = skill.skillMd;
2373
+ let sourcePath = skill.sourcePath;
2374
+ if (skill.file !== undefined) {
2375
+ if (skill.skillMd !== undefined)
2376
+ errors.push(`${label}: set skillMd or file, not both`);
2377
+ skillMd = readRef(label, skill.file);
2378
+ if (typeof skill.file === "string" && !sourcePath)
2379
+ sourcePath = sourcePathFor(skill.file);
2380
+ }
2381
+ if (!nonEmptyString(skillMd)) {
2382
+ errors.push(`${label}: skillMd or file is required`);
2383
+ } else if (Buffer.byteLength(skillMd, "utf8") > MAX_SKILL_BYTES) {
2384
+ errors.push(`${label}: skill content exceeds ${MAX_SKILL_BYTES} bytes`);
2385
+ }
2386
+ if (skill.live !== undefined && typeof skill.live !== "boolean")
2387
+ errors.push(`${label}.live must be a boolean`);
2388
+ const live = options.forceLive || skill.live === true;
2389
+ return {
2390
+ key: skill.key,
2391
+ ...skill.name !== undefined ? { name: skill.name } : {},
2392
+ ...skill.description !== undefined ? { description: skill.description } : {},
2393
+ skillMd: skillMd ?? "",
2394
+ ...sourcePath !== undefined ? { sourcePath } : {},
2395
+ ...live ? { surfacePolicy: "include" } : skill.live === false ? { surfacePolicy: "local_only" } : {}
2396
+ };
2397
+ });
2398
+ const toolNames = new Set;
2399
+ const tools = arrayOf(input.tools, "tools", errors).map((raw, index) => {
2400
+ const label = `tools[${index}]`;
2401
+ if (!isRecord(raw) || !isRecord(raw.schema)) {
2402
+ errors.push(`${label}.schema must be an object (OpenAI function schema)`);
2403
+ return;
2404
+ }
2405
+ checkFields(label, raw, TOOL_FIELDS, errors);
2406
+ const tool = raw;
2407
+ const inner = tool.schema.type === "function" && isRecord(tool.schema.function) ? tool.schema.function : tool.schema;
2408
+ const schemaName = typeof inner.name === "string" ? inner.name.trim() : undefined;
2409
+ const name = (typeof tool.name === "string" ? tool.name.trim() : "") || schemaName;
2410
+ if (typeof tool.name === "string" && tool.name.trim() && schemaName && tool.name.trim() !== schemaName) {
2411
+ errors.push(`${label}: name "${tool.name}" does not match schema.name "${schemaName}"`);
2412
+ }
2413
+ if (!nonEmptyString(name)) {
2414
+ errors.push(`${label}: name or schema.name is required`);
2415
+ } else if (toolNames.has(name)) {
2416
+ errors.push(`${label}: duplicate tool name "${name}"`);
2417
+ } else {
2418
+ toolNames.add(name);
2419
+ }
2420
+ if (inner.name !== undefined && typeof inner.name !== "string")
2421
+ errors.push(`${label}: schema.name must be a string`);
2422
+ if (inner.description !== undefined && typeof inner.description !== "string") {
2423
+ errors.push(`${label}: schema.description must be a string`);
2424
+ }
2425
+ if (inner.parameters !== undefined) {
2426
+ if (!isRecord(inner.parameters) || inner.parameters.type !== "object") {
2427
+ errors.push(`${label}: schema.parameters.type must be "object"`);
2428
+ } else if (inner.parameters.properties !== undefined && !isRecord(inner.parameters.properties)) {
2429
+ errors.push(`${label}: schema.parameters.properties must be an object`);
2430
+ }
2431
+ }
2432
+ return tool;
2433
+ });
2434
+ const promptKeys = new Set;
2435
+ for (const prompt of prompts) {
2436
+ if (prompt && nonEmptyString(prompt.key)) {
2437
+ if (promptKeys.has(prompt.key))
2438
+ errors.push(`duplicate prompt key "${prompt.key}"`);
2439
+ promptKeys.add(prompt.key);
2440
+ }
2441
+ }
2442
+ const skillKeys = new Set;
2443
+ for (const skill of skills) {
2444
+ if (skill && nonEmptyString(skill.key)) {
2445
+ if (skillKeys.has(skill.key))
2446
+ errors.push(`duplicate skill key "${skill.key}"`);
2447
+ skillKeys.add(skill.key);
2448
+ }
2449
+ }
2450
+ if (errors.length > 0) {
2451
+ throw new CliInputError(`Registry manifest is invalid:
2452
+ - ${errors.join(`
2453
+ - `)}`, "Fix the manifest and rerun `moda registry validate`. See `moda registry template` for the format.");
2454
+ }
2455
+ const resolved = {
2456
+ manifestPath: options.manifestPath,
2457
+ prompts: prompts.filter(isDefined),
2458
+ skills: skills.filter(isDefined),
2459
+ tools: tools.filter(isDefined)
2460
+ };
2461
+ if (resolved.prompts.length + resolved.skills.length + resolved.tools.length === 0) {
2462
+ throw new CliInputError("Registry manifest has no prompts, skills, or tools to push.");
2463
+ }
2464
+ return resolved;
2465
+ }
2466
+ function arrayOf(value, label, errors) {
2467
+ if (value === undefined)
2468
+ return [];
2469
+ if (!Array.isArray(value)) {
2470
+ errors.push(`${label} must be an array`);
2471
+ return [];
2472
+ }
2473
+ if (value.length > MAX_ITEMS_PER_SECTION) {
2474
+ errors.push(`${label} has ${value.length} entries; the limit is ${MAX_ITEMS_PER_SECTION} per push`);
2475
+ }
2476
+ return value;
2477
+ }
2478
+ function countsOf(registry) {
2479
+ return { prompts: registry.prompts.length, skills: registry.skills.length, tools: registry.tools.length };
2480
+ }
2481
+ function displayPath(context, path) {
2482
+ const rel = relative(context.cwd, path);
2483
+ return rel && !rel.startsWith("..") && !isAbsolute2(rel) ? rel.split("\\").join("/") : path;
2484
+ }
2485
+ function isInside(root, target) {
2486
+ const rel = relative(resolve2(root), target);
2487
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
2488
+ }
2489
+ function isRecord(value) {
2490
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2491
+ }
2492
+ function nonEmptyString(value) {
2493
+ return typeof value === "string" && value.trim() !== "";
2494
+ }
2495
+ function isDefined(value) {
2496
+ return value !== undefined;
2497
+ }
2498
+
1756
2499
  // src/telemetry.ts
1757
2500
  import { randomUUID } from "node:crypto";
1758
2501
  var TELEMETRY_TIMEOUT_MS = 500;
@@ -1897,7 +2640,7 @@ async function sendCliUsageTelemetry(input) {
1897
2640
  }
1898
2641
 
1899
2642
  // src/update-check.ts
1900
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
2643
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
1901
2644
  import { join as join2 } from "node:path";
1902
2645
  var UPDATE_CHECK_FLAG = "no-update-check";
1903
2646
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -1916,7 +2659,7 @@ function cachePath(options = {}) {
1916
2659
  }
1917
2660
  function readCache(options = {}) {
1918
2661
  try {
1919
- const parsed = JSON.parse(readFileSync2(cachePath(options), "utf8"));
2662
+ const parsed = JSON.parse(readFileSync3(cachePath(options), "utf8"));
1920
2663
  if (parsed && typeof parsed === "object" && typeof parsed.checked_at === "number" && typeof parsed.latest === "string") {
1921
2664
  return parsed;
1922
2665
  }
@@ -1925,8 +2668,8 @@ function readCache(options = {}) {
1925
2668
  }
1926
2669
  function writeCache(cache, options = {}) {
1927
2670
  try {
1928
- mkdirSync2(getModaDir(options), { recursive: true });
1929
- writeFileSync(cachePath(options), `${JSON.stringify(cache, null, 2)}
2671
+ mkdirSync3(getModaDir(options), { recursive: true });
2672
+ writeFileSync3(cachePath(options), `${JSON.stringify(cache, null, 2)}
1930
2673
  `, "utf8");
1931
2674
  } catch {}
1932
2675
  }
@@ -2003,6 +2746,597 @@ function createCommandRegistry(definitions) {
2003
2746
  };
2004
2747
  }
2005
2748
 
2749
+ // src/subcommand-help.ts
2750
+ var DRY_RUN = { name: "--dry-run", description: "Print the exact request and send nothing." };
2751
+ var TENANT_ID = { name: "--tenant-id=<id>", description: "Tenant to act on; must match the API key's tenant (default: the key's)." };
2752
+ var SIGNAL_FIELD_FLAGS = [
2753
+ { name: "--name=<text>", description: "Signal name." },
2754
+ { name: "--description=<text>", description: "What the signal catches." },
2755
+ { name: "--pass-criteria=<text>", description: "When a trace passes (the judge reads this)." },
2756
+ { name: "--fail-criteria=<text>", description: "When a trace fails (the judge reads this)." },
2757
+ { name: "--polarity=violation|expectation", description: "Whether a hit is something that should not happen, or something that should." },
2758
+ { name: "--locus=agent|tool_output|user_input", description: "Where in the trace the judge looks." },
2759
+ { name: "--tool-names=a,b", description: "Scope: only traces that call these tools." },
2760
+ { name: "--keyword-hints=a,b", description: "Scope: keywords that make a trace a candidate." },
2761
+ { name: "--use-case-id=<id>", description: "Scope: one use case (cluster id)." },
2762
+ { name: "--message-types=user,assistant,tool_input,tool_output", description: "Scope: message types the judge reads." },
2763
+ { name: "--note=<text>", description: "Free-text note stored with the signal." },
2764
+ { name: "--file=<body.json>", description: "JSON request body; flags given on the command line override its fields." }
2765
+ ];
2766
+ var SIGNALS_SUBCOMMAND_HELP = [
2767
+ {
2768
+ name: "draft",
2769
+ description: "AI-draft name and criteria from a plain description (one LLM call; nothing is saved)",
2770
+ usage: 'moda signals draft --note="<what to catch>" [--file=<body.json>] [--dry-run]',
2771
+ flags: [
2772
+ { name: "--note=<text>", description: "Required. What the signal should catch." },
2773
+ { name: "--file=<body.json>", description: "JSON request body (e.g. with seed/answers); --note overrides its note." },
2774
+ DRY_RUN,
2775
+ TENANT_ID
2776
+ ],
2777
+ examples: ['moda signals draft --note="agent promises a refund it cannot issue"']
2778
+ },
2779
+ {
2780
+ name: "create",
2781
+ description: "Create a custom signal (starts as a draft; activate it to run)",
2782
+ usage: "moda signals create --name=<text> --description=<text> --pass-criteria=<text> --fail-criteria=<text> [scope flags] [--file=<body.json>] [--dry-run]",
2783
+ flags: [...SIGNAL_FIELD_FLAGS, DRY_RUN, TENANT_ID],
2784
+ examples: [
2785
+ 'moda signals create --name="Refund promise" --description="…" --pass-criteria="…" --fail-criteria="…" --dry-run',
2786
+ "moda signals create --file=signal.json"
2787
+ ]
2788
+ },
2789
+ {
2790
+ name: "update",
2791
+ description: "Edit a signal's fields (pass at least one field flag or --file)",
2792
+ usage: "moda signals update <signal_id> [field flags] [--file=<body.json>] [--dry-run]",
2793
+ flags: [...SIGNAL_FIELD_FLAGS, DRY_RUN, TENANT_ID],
2794
+ examples: ['moda signals update <signal_id> --fail-criteria="…" --dry-run']
2795
+ },
2796
+ {
2797
+ name: "activate",
2798
+ description: "Start running a signal on new traces",
2799
+ usage: "moda signals activate <signal_id> [--dry-run]",
2800
+ flags: [DRY_RUN, TENANT_ID],
2801
+ examples: ["moda signals activate <signal_id>"]
2802
+ },
2803
+ {
2804
+ name: "pause",
2805
+ description: "Stop running a signal (keeps it and its detections)",
2806
+ usage: "moda signals pause <signal_id> [--dry-run]",
2807
+ flags: [DRY_RUN, TENANT_ID],
2808
+ examples: ["moda signals pause <signal_id>"]
2809
+ },
2810
+ {
2811
+ name: "delete",
2812
+ description: "Delete a draft signal (drafts only; archiving a live signal is dashboard-only, use pause)",
2813
+ usage: "moda signals delete <signal_id> [--dry-run]",
2814
+ flags: [DRY_RUN, TENANT_ID],
2815
+ examples: ["moda signals delete <signal_id> --dry-run"]
2816
+ },
2817
+ {
2818
+ name: "examples",
2819
+ description: "Add, relabel, or remove a labelled example trace on a signal",
2820
+ usage: "moda signals examples <signal_id> add|update|remove --conversation-id=<id> [flags] [--dry-run]",
2821
+ flags: [
2822
+ { name: "--conversation-id=<id>", description: "Required. The trace (conversation_id) the example points at." },
2823
+ { name: "--label=fits|does_not_fit", description: "add: required. update: optional new label." },
2824
+ { name: "--note=<text>", description: "add/update: why the trace fits or not." },
2825
+ { name: "--msg-index=<n>", description: "add: anchor message index in the trace." },
2826
+ { name: "--msg-dedup-token=<token>", description: "add: stable anchor message token (preferred over --msg-index)." },
2827
+ { name: "--criteria=pass|fail", description: "add: which criteria the example illustrates." },
2828
+ { name: "--verdict=accept|reject", description: "add: your verdict on a proposed match." },
2829
+ { name: "--blurb=<text>", description: "update: replace the example summary." },
2830
+ DRY_RUN,
2831
+ TENANT_ID
2832
+ ],
2833
+ examples: [
2834
+ 'moda signals examples <signal_id> add --conversation-id=<id> --label=does_not_fit --note="refund was approved"',
2835
+ "moda signals examples <signal_id> remove --conversation-id=<id>"
2836
+ ]
2837
+ },
2838
+ {
2839
+ name: "refine",
2840
+ description: "Propose a criteria edit from a plain request (one LLM call; review with `proposals`)",
2841
+ usage: 'moda signals refine <signal_id> --request="<change>" [--dry-run]',
2842
+ flags: [
2843
+ { name: "--request=<text>", description: "Required, 1-2000 characters. The change you want." },
2844
+ DRY_RUN,
2845
+ TENANT_ID
2846
+ ],
2847
+ examples: ['moda signals refine <signal_id> --request="ignore refunds the agent escalates to a human"']
2848
+ },
2849
+ {
2850
+ name: "revise",
2851
+ description: "Re-derive criteria from labelled example traces (LLM call; rate-limited)",
2852
+ usage: "moda signals revise <signal_id> --conversation-ids=a,b [--dry-run]",
2853
+ flags: [
2854
+ { name: "--conversation-ids=a,b", description: "Required. Labelled traces to learn from." },
2855
+ DRY_RUN,
2856
+ TENANT_ID
2857
+ ],
2858
+ examples: ["moda signals revise <signal_id> --conversation-ids=conv_1,conv_2"]
2859
+ },
2860
+ {
2861
+ name: "proposals",
2862
+ description: "List pending criteria proposals for a signal (read-only)",
2863
+ usage: "moda signals proposals <signal_id>",
2864
+ flags: [TENANT_ID],
2865
+ examples: ["moda signals proposals <signal_id>"]
2866
+ },
2867
+ {
2868
+ name: "accept",
2869
+ description: "Apply a criteria proposal to the signal",
2870
+ usage: "moda signals accept <signal_id> <proposal_id> [--dry-run]",
2871
+ flags: [DRY_RUN, TENANT_ID],
2872
+ examples: ["moda signals accept <signal_id> <proposal_id>"]
2873
+ },
2874
+ {
2875
+ name: "reject",
2876
+ description: "Discard a criteria proposal",
2877
+ usage: "moda signals reject <signal_id> <proposal_id> [--dry-run]",
2878
+ flags: [DRY_RUN, TENANT_ID],
2879
+ examples: ["moda signals reject <signal_id> <proposal_id>"]
2880
+ },
2881
+ {
2882
+ name: "search",
2883
+ description: "Search recent traces for matches with the judge (up to ~1,000 judge calls; one search per signal at a time)",
2884
+ usage: "moda signals search <signal_id> [--mode=wizard|full] [--exclude-conversation-ids=a,b] [--dry-run]",
2885
+ flags: [
2886
+ { name: "--mode=wizard|full", description: "full (default): the whole search. wizard: stops at about 6 matches (at most ~42 traces judged), for labelling." },
2887
+ { name: "--exclude-conversation-ids=a,b", description: "Traces to skip (e.g. already labelled)." },
2888
+ DRY_RUN,
2889
+ TENANT_ID
2890
+ ],
2891
+ examples: ["moda signals search <signal_id> --mode=wizard"]
2892
+ },
2893
+ {
2894
+ name: "search-status",
2895
+ description: "Poll a signal search (read-only)",
2896
+ usage: "moda signals search-status <signal_id> <search_id>",
2897
+ flags: [TENANT_ID],
2898
+ examples: ["moda signals search-status <signal_id> <search_id>"]
2899
+ },
2900
+ {
2901
+ name: "search-stop",
2902
+ description: "Stop a running signal search",
2903
+ usage: "moda signals search-stop <signal_id> <search_id> [--dry-run]",
2904
+ flags: [DRY_RUN, TENANT_ID],
2905
+ examples: ["moda signals search-stop <signal_id> <search_id>"]
2906
+ },
2907
+ {
2908
+ name: "preview",
2909
+ description: "Run a deterministic (non-judge) detector over recent traces and show what it matches; saves nothing",
2910
+ usage: "moda signals preview --file=<detector.json> [--conversation-ids=a,b] [--max-examples=N]",
2911
+ flags: [
2912
+ {
2913
+ name: "--file=<detector.json>",
2914
+ description: 'Required. JSON {"detector":{"type":…,"config":{…}},"scope"?:{…}}. Types: rule, tool_call, routing, metric, all ' + '(judge detectors preview through `search`). Rule config: {"pattern":"<JS regex>","flags":"i","target":"assistant|user|any|tool_input|tool_output"}; ' + 'regex flags go in "flags" (i, m, s, u), not inline like (?i).'
2915
+ },
2916
+ { name: "--conversation-ids=a,b", description: "Check these traces (at most 200) instead of the recent sample; every match comes back." },
2917
+ { name: "--max-examples=<n>", description: "Examples returned from the recent sample (1-50, default 5)." },
2918
+ TENANT_ID
2919
+ ],
2920
+ examples: [
2921
+ `echo '{"detector":{"type":"rule","config":{"pattern":"cancel(l)?ed","flags":"i","target":"assistant"}}}' > detector.json`,
2922
+ "moda signals preview --file=detector.json --max-examples=10",
2923
+ `echo '{"detector":{"type":"tool_call","config":{"toolName":"lookupCustomer","condition":"errors"}}}' > detector.json`
2924
+ ]
2925
+ }
2926
+ ];
2927
+ var SKILLS_SUBCOMMAND_HELP = [
2928
+ {
2929
+ name: "gen",
2930
+ description: "Generate skills from production traces (paid: clustering + LLM distillation; no dry-run)",
2931
+ usage: "moda skills gen [--source=all|sdk] [--max-sessions=N] [--lookback-hours=N] [--max-traces=N] [--improvement-rounds=1-3] [--start-at=ISO] [--end-at=ISO] [--replay] [--replay-set-id=ID] [--wait]",
2932
+ flags: [
2933
+ { name: "--source=all|sdk", description: "Which traces to learn from (default all)." },
2934
+ { name: "--max-sessions=<n>", description: "Sessions to process (server default 200). Must be 1 or more." },
2935
+ { name: "--lookback-hours=<n>", description: "Window to read (server default 720 = 30 days). Must be 1 or more." },
2936
+ { name: "--start-at=<iso>, --end-at=<iso>", description: "Explicit window instead of --lookback-hours." },
2937
+ { name: "--max-traces=<n>", description: "Cap on cluster traces loaded for distillation." },
2938
+ { name: "--improve-candidates", description: "Run improvement rounds on candidate skills." },
2939
+ { name: "--improvement-rounds=<1-3>", description: "Improvement rounds (default 2, max 3)." },
2940
+ { name: "--replay", description: "Replay candidates against a replay set (extra paid playouts)." },
2941
+ { name: "--replay-set-id=<id>", description: "Replay set for --replay." },
2942
+ { name: "--reprocess-segments", description: "Re-segment sessions that were already processed." },
2943
+ { name: "--force-reprocess", description: "Reprocess everything in the window (implies --reprocess-segments)." },
2944
+ { name: "--no-clustering", description: "Skip clustering; reuse --clustering-run-id." },
2945
+ { name: "--clustering-run-id=<id>", description: "Use an existing clustering run." },
2946
+ { name: "--wait", description: "Wait for the run to finish instead of returning once it is accepted." },
2947
+ TENANT_ID
2948
+ ],
2949
+ examples: ["moda skills gen", "moda skills gen --lookback-hours=168 --max-sessions=50 --wait"]
2950
+ },
2951
+ {
2952
+ name: "status",
2953
+ description: "Show a skill generation run (default: the latest) and its events",
2954
+ usage: "moda skills status [<run_id>] [--run-id=<id>]",
2955
+ examples: ["moda skills status", "moda skills status <run_id>"]
2956
+ },
2957
+ {
2958
+ name: "pull",
2959
+ description: "Write generated skills into the detected coding agents' skill directories",
2960
+ usage: "moda skills pull [--status=approved|proposed|all]",
2961
+ flags: [{ name: "--status=approved|proposed|all", description: "Which skills to pull (default approved)." }],
2962
+ examples: ["moda skills pull", "moda skills pull --status=all"]
2963
+ },
2964
+ {
2965
+ name: "sync",
2966
+ description: "Upload locally authored .claude/skills/**/SKILL.md files to Moda",
2967
+ usage: "moda skills sync [--dry-run]",
2968
+ flags: [{ name: "--dry-run", description: "Server-side preview; nothing is saved." }],
2969
+ examples: ["moda skills sync --dry-run", "moda skills sync"]
2970
+ },
2971
+ {
2972
+ name: "list",
2973
+ description: "List the tenant's skills (the dashboard skill library; read-only)",
2974
+ usage: "moda skills list",
2975
+ examples: ["moda skills list"]
2976
+ },
2977
+ {
2978
+ name: "show",
2979
+ description: "Show one skill with its content (read-only)",
2980
+ usage: "moda skills show <key_or_id>",
2981
+ examples: ["moda skills show refund-policy"]
2982
+ },
2983
+ {
2984
+ name: "policy",
2985
+ description: "Switch a skill between live (served to agents) and local-only",
2986
+ usage: "moda skills policy <key_or_id> --live|--local [--dry-run]",
2987
+ flags: [
2988
+ { name: "--live", description: "Include the skill in the served skill surface." },
2989
+ { name: "--local", description: "Keep the skill local-only (not served)." },
2990
+ DRY_RUN
2991
+ ],
2992
+ examples: ["moda skills policy refund-policy --live --dry-run"]
2993
+ },
2994
+ {
2995
+ name: "proposals",
2996
+ description: "List skill improvement proposals (read-only)",
2997
+ usage: "moda skills proposals list [--status=ready_for_pr|all] [--generation-run-id=<id>]",
2998
+ flags: [
2999
+ { name: "--status=<status>", description: "Proposal status to list (default ready_for_pr; all for every status)." },
3000
+ { name: "--generation-run-id=<id>", description: "Only proposals from one generation run." },
3001
+ TENANT_ID
3002
+ ],
3003
+ examples: ["moda skills proposals list"]
3004
+ },
3005
+ {
3006
+ name: "proposal",
3007
+ description: "Apply a skill proposal: write its SKILL.md locally and acknowledge it on the server",
3008
+ usage: "moda skills proposal apply <proposal_id>",
3009
+ flags: [TENANT_ID],
3010
+ examples: ["moda skills proposal apply <proposal_id>"]
3011
+ },
3012
+ {
3013
+ name: "install",
3014
+ description: "Install bundled integration guides into .claude/skills (offline; no API key)",
3015
+ usage: "moda skills install [--list] <id...>",
3016
+ flags: [{ name: "--list", description: "List the bundled guides." }],
3017
+ examples: ["moda skills install --list", "moda skills install integration-node-openai"]
3018
+ }
3019
+ ];
3020
+ var PROMPTS_REGISTRY_SUBCOMMAND_HELP = [
3021
+ {
3022
+ name: "list",
3023
+ description: "List the tenant's registered prompts (read-only)",
3024
+ usage: "moda prompts list [--label=prod|staging|dev|unlabeled] [--search=<text>]",
3025
+ flags: [
3026
+ { name: "--label=<label>", description: "Only prompts with this label set (prod, staging, dev), or unlabeled ones." },
3027
+ { name: "--search=<text>", description: "Case-insensitive match on key, name, and description." }
3028
+ ],
3029
+ examples: ["moda prompts list", "moda prompts list --label=prod"]
3030
+ },
3031
+ {
3032
+ name: "show",
3033
+ description: "Show one prompt with its versions and labels (read-only)",
3034
+ usage: "moda prompts show <key_or_id>",
3035
+ examples: ["moda prompts show support.triage"]
3036
+ },
3037
+ {
3038
+ name: "proposals",
3039
+ description: "List revision proposals for a prompt (read-only)",
3040
+ usage: "moda prompts proposals <key_or_id>",
3041
+ examples: ["moda prompts proposals support.triage"]
3042
+ },
3043
+ {
3044
+ name: "usage",
3045
+ description: "Per-version call, trace, and token counts for a prompt from traces (read-only)",
3046
+ usage: "moda prompts usage <key_or_id>",
3047
+ examples: ["moda prompts usage support.triage"]
3048
+ },
3049
+ {
3050
+ name: "replay-runs",
3051
+ description: "List replay (A/B) runs for a prompt (read-only)",
3052
+ usage: "moda prompts replay-runs <key_or_id>",
3053
+ examples: ["moda prompts replay-runs support.triage"]
3054
+ },
3055
+ {
3056
+ name: "decide",
3057
+ description: "Merge, reject, or reopen a prompt revision proposal",
3058
+ usage: "moda prompts decide <key_or_id> <proposal_id> --action=merge|reject|reopen [--dry-run]",
3059
+ flags: [
3060
+ { name: "--action=merge|reject|reopen", description: "Required. The decision." },
3061
+ DRY_RUN
3062
+ ],
3063
+ examples: ["moda prompts decide support.triage <proposal_id> --action=merge --dry-run"]
3064
+ }
3065
+ ];
3066
+ var HARNESS_PATH_FLAG = { name: "--path=<dir>", description: "Project root to read (default: the current directory)." };
3067
+ var HARNESS_REPORT_FLAG = { name: "--report=<path>", description: "Harness report to use (default .moda/harness-report.json)." };
3068
+ var HARNESS_SUBCOMMAND_HELP = [
3069
+ {
3070
+ name: "scan",
3071
+ description: "Deterministic local scan; writes .moda/harness.json (local, no API key)",
3072
+ usage: "moda harness scan [--path=<dir>] [--summary] [--dry-run] [--yes]",
3073
+ flags: [
3074
+ HARNESS_PATH_FLAG,
3075
+ { name: "--summary", description: "Also write .moda/HARNESS.md." },
3076
+ { name: "--dry-run", description: "Scan and print; write nothing." },
3077
+ { name: "--yes", description: "Approve a broad scan without the prompt." }
3078
+ ],
3079
+ examples: ["moda harness scan", "moda harness scan --path=services/agent --dry-run"]
3080
+ },
3081
+ {
3082
+ name: "sync",
3083
+ description: "Upload the harness graph (an approved report, or a fresh local scan) to Moda Cloud",
3084
+ usage: "moda harness sync [--from-report=<path>] [--path=<dir>] [--summary] [--allow-no-agents] [--dry-run]",
3085
+ flags: [
3086
+ { name: "--from-report=<path>", description: "Sync an approved harness report (see `harness approve`). Without it, syncs a fresh local scan." },
3087
+ HARNESS_PATH_FLAG,
3088
+ { name: "--summary", description: "Also write .moda/HARNESS.md." },
3089
+ { name: "--allow-no-agents", description: "Sync a graph with no runtime agents (normally refused)." },
3090
+ { name: "--dry-run", description: "Server-side preview with the same graph hash and changed flag the real sync would report; writes nothing." },
3091
+ { name: "--tenant-id=<id>", description: "Tenant to record on the graph (default: the configured tenant)." }
3092
+ ],
3093
+ examples: [
3094
+ "moda harness sync --dry-run",
3095
+ "moda harness sync --from-report .moda/harness-report.json"
3096
+ ]
3097
+ },
3098
+ {
3099
+ name: "delete",
3100
+ description: "Permanently delete a synced harness and all its versions from Moda Cloud",
3101
+ usage: "moda harness delete <harness_id_or_key> [--yes] [--dry-run]",
3102
+ flags: [
3103
+ { name: "--yes", description: "Confirm the deletion (required outside an interactive terminal)." },
3104
+ { name: "--dry-run", description: "Print what would be deleted; send nothing." }
3105
+ ],
3106
+ examples: ["moda harness delete my-harness-key --dry-run", "moda harness delete my-harness-key --yes"]
3107
+ },
3108
+ {
3109
+ name: "analyze",
3110
+ description: "Run Moda Analyst to produce a cited harness report (experimental; the GitHub App analyzes automatically)",
3111
+ usage: "moda harness analyze --experimental [--remote | --analyst=local-scan|claude|codex|cursor|auto] [--concern=prompts] [--path=<dir>] [--summary] [--dry-run] [--yes]",
3112
+ flags: [
3113
+ { name: "--experimental", description: "Required to run the CLI analyze (or set MODA_HARNESS_ANALYZE_CLI=1)." },
3114
+ { name: "--remote", description: "Run the analysis Moda-hosted (paid); fetch the result with `harness pull`." },
3115
+ { name: "--local, --no-remote", description: "Force a local run even when MODA_HARNESS_REMOTE is set." },
3116
+ { name: "--analyst=<adapter>", description: "local-scan (no LLM), claude, codex, cursor, or auto (an installed coding agent)." },
3117
+ { name: "--analyst-model=<model>", description: "Model for the local coding-agent analyst." },
3118
+ { name: "--shards=<n>", description: "Remote: split the analysis across n shards." },
3119
+ { name: "--concern=prompts", description: "Narrow the analysis to prompts (local coding-agent analyst only)." },
3120
+ { name: "--github-actions", description: "Run inside GitHub Actions with OIDC (no API key on the runner)." },
3121
+ HARNESS_PATH_FLAG,
3122
+ { name: "--summary", description: "Also write markdown summaries." },
3123
+ { name: "--dry-run", description: "Analyze and print; write nothing." },
3124
+ { name: "--yes", description: "Approve a broad scan without the prompt." }
3125
+ ],
3126
+ examples: ["moda harness analyze --experimental --summary", "moda harness analyze --experimental --remote"]
3127
+ },
3128
+ {
3129
+ name: "pull",
3130
+ description: "Fetch a Moda-hosted analysis run once; write its report when complete",
3131
+ usage: "moda harness pull [--run-id=<id>] [--sync]",
3132
+ flags: [
3133
+ { name: "--run-id=<id>", description: "Run to fetch (default: the one in .moda/harness-remote-run.json)." },
3134
+ { name: "--sync", description: "When complete, approve the report and sync it (like `moda init`)." }
3135
+ ],
3136
+ examples: ["moda harness pull", "moda harness pull --sync"]
3137
+ },
3138
+ {
3139
+ name: "validate-report",
3140
+ description: "Validate a harness report against the code and write the validation result (local)",
3141
+ usage: "moda harness validate-report [<report_path>] [--report=<path>] [--dry-run]",
3142
+ flags: [HARNESS_REPORT_FLAG, { name: "--dry-run", description: "Validate only; write nothing." }],
3143
+ examples: ["moda harness validate-report"]
3144
+ },
3145
+ {
3146
+ name: "approve",
3147
+ description: "Record your approval of a validated harness report so `sync --from-report` will upload it (local)",
3148
+ usage: "moda harness approve [<report_path>] [--report=<path>] [--yes]",
3149
+ flags: [HARNESS_REPORT_FLAG, { name: "--yes", description: "Approve without the interactive prompt." }],
3150
+ examples: ["moda harness approve --yes"]
3151
+ },
3152
+ {
3153
+ name: "candidates",
3154
+ description: "Static pre-pass listing likely agents, prompts, and tools (local; no LLM, no network)",
3155
+ usage: "moda harness candidates [--path=<dir>] [--out=<file>] [--dry-run] [--yes]",
3156
+ flags: [
3157
+ HARNESS_PATH_FLAG,
3158
+ { name: "--out=<file>", description: "Also write the candidates report to a file." },
3159
+ { name: "--dry-run", description: "Do not write --out." },
3160
+ { name: "--yes", description: "Approve a broad scan without the prompt." }
3161
+ ],
3162
+ examples: ["moda harness candidates --out=.moda/candidates.json"]
3163
+ },
3164
+ {
3165
+ name: "agents",
3166
+ description: "List runtime agents and families from .moda/harness.json (or a live scan) (local)",
3167
+ usage: "moda harness agents [--path=<dir>]",
3168
+ flags: [HARNESS_PATH_FLAG],
3169
+ examples: ["moda harness agents"]
3170
+ },
3171
+ {
3172
+ name: "graph",
3173
+ description: "Print the full local harness graph (local)",
3174
+ usage: "moda harness graph [--path=<dir>]",
3175
+ flags: [HARNESS_PATH_FLAG],
3176
+ examples: ["moda harness graph --json"]
3177
+ },
3178
+ {
3179
+ name: "explain",
3180
+ description: "Explain the harness, or one runtime agent (local)",
3181
+ usage: "moda harness explain [--agent-id=<id>] [--path=<dir>]",
3182
+ flags: [
3183
+ { name: "--agent-id=<id>, --agent=<id>", description: "Explain one runtime agent (ids from `harness agents`)." },
3184
+ HARNESS_PATH_FLAG
3185
+ ],
3186
+ examples: ["moda harness explain", "moda harness explain --agent-id=agent_support"]
3187
+ },
3188
+ {
3189
+ name: "write-summary",
3190
+ description: "Write .moda/HARNESS.md from the local harness graph (local)",
3191
+ usage: "moda harness write-summary [--path=<dir>]",
3192
+ flags: [HARNESS_PATH_FLAG],
3193
+ examples: ["moda harness write-summary"]
3194
+ },
3195
+ {
3196
+ name: "status",
3197
+ description: "Local harness state: graph hash, last sync, and whether the graph changed since (local)",
3198
+ usage: "moda harness status [--path=<dir>]",
3199
+ flags: [HARNESS_PATH_FLAG],
3200
+ examples: ["moda harness status"]
3201
+ },
3202
+ {
3203
+ name: "list",
3204
+ description: "List the tenant's synced harnesses (read-only)",
3205
+ usage: "moda harness list [--limit=N] [--offset=N]",
3206
+ flags: [
3207
+ { name: "--limit=<n>", description: "Page size." },
3208
+ { name: "--offset=<n>", description: "Rows to skip." }
3209
+ ],
3210
+ examples: ["moda harness list"]
3211
+ },
3212
+ {
3213
+ name: "show",
3214
+ description: "Show one synced harness, at its current or a given version (read-only)",
3215
+ usage: "moda harness show <harness_id_or_key> [--version=<version_id>]",
3216
+ flags: [{ name: "--version=<version_id>", description: "Show this version instead of the current one." }],
3217
+ examples: ["moda harness show my-harness-key"]
3218
+ },
3219
+ {
3220
+ name: "versions",
3221
+ description: "List a synced harness's versions (read-only)",
3222
+ usage: "moda harness versions <harness_id_or_key> [--limit=N] [--offset=N]",
3223
+ flags: [
3224
+ { name: "--limit=<n>", description: "Page size." },
3225
+ { name: "--offset=<n>", description: "Rows to skip." }
3226
+ ],
3227
+ examples: ["moda harness versions my-harness-key"]
3228
+ },
3229
+ {
3230
+ name: "diff",
3231
+ description: "Diff two versions of a synced harness (default: the latest against its parent) (read-only)",
3232
+ usage: "moda harness diff <harness_id_or_key> [--from=<version_id>] [--to=<version_id>]",
3233
+ flags: [
3234
+ { name: "--from=<version_id>", description: "Older version." },
3235
+ { name: "--to=<version_id>", description: "Newer version." }
3236
+ ],
3237
+ examples: ["moda harness diff my-harness-key --from=hver_a --to=hver_b"]
3238
+ }
3239
+ ];
3240
+ var PROBLEMS_SUBCOMMAND_HELP = [
3241
+ {
3242
+ name: "remainder",
3243
+ description: "Signals no Problem explains yet, grouped by family (read-only)",
3244
+ usage: "moda problems remainder [--family=<family>] [--days-back=N] [--limit=N] [--cursor=<token>]",
3245
+ flags: [
3246
+ { name: "--family=<family>", description: "One of tool_failure, emotion, laziness, hallucination, prm_dip, custom_signal, conversation_reader (frustration is under emotion)." },
3247
+ { name: "--days-back=<n>", description: "Window in days, 1-90 (default 30)." },
3248
+ { name: "--limit=<n>", description: "Groups per page, 1-25 (default 25)." },
3249
+ { name: "--cursor=<token>", description: "The next_cursor from the previous page, verbatim." }
3250
+ ],
3251
+ examples: ["moda problems remainder", "moda problems remainder --family=emotion --days-back=7"]
3252
+ }
3253
+ ];
3254
+ var FIX_TIMEOUT = { name: "--timeout=<ms>", description: "How long to wait, in milliseconds (max 86400000)." };
3255
+ var FIX_POLL = { name: "--poll-interval=<ms>", description: "Polling interval while waiting, in milliseconds." };
3256
+ var FIXES_SUBCOMMAND_HELP = [
3257
+ {
3258
+ name: "draft-batch",
3259
+ description: "Draft Fixes for the top unfixed Problems (no LLM call itself; spend happens when they are driven)",
3260
+ usage: "moda fixes draft-batch [--limit=N]",
3261
+ flags: [{ name: "--limit=N", description: "How many Problems to draft for (1-25, default 5)." }, TENANT_ID],
3262
+ examples: ["moda fixes draft-batch --limit=3"]
3263
+ },
3264
+ {
3265
+ name: "drive",
3266
+ description: "Advance active Fixes through scoping, proposing, and gating (paid: each fix runs LLM calls and replay playouts)",
3267
+ usage: "moda fixes drive [--status=S] [--max-fixes=N] [--timeout=ms] [--pass-interval=ms]",
3268
+ flags: [
3269
+ { name: "--max-fixes=N", description: "Spend cap: advance at most N fixes this run (1-10, default 3). GATING fixes count too; each run takes the least-recently-driven fixes first, so re-runs reach every fix." },
3270
+ { name: "--status=S", description: "Only drive fixes in this status." },
3271
+ FIX_TIMEOUT,
3272
+ { name: "--pass-interval=<ms>", description: "Pause between passes, in milliseconds." },
3273
+ TENANT_ID
3274
+ ],
3275
+ examples: ["moda fixes drive", "moda fixes drive --max-fixes=1 --status=SCOPING"]
3276
+ }
3277
+ ];
3278
+ var FIX_SUBCOMMAND_HELP = [
3279
+ {
3280
+ name: "start",
3281
+ description: "Start a Fix for a Problem (paid: scoping, a proposal, and a replay gate)",
3282
+ usage: "moda fix start <problem_id> [--type=auto|prompt] [--wait] [--timeout=ms]",
3283
+ flags: [
3284
+ { name: "--type=auto|prompt", description: "Fix type (default auto)." },
3285
+ { name: "--wait", description: "Wait for the gate verdict." },
3286
+ FIX_TIMEOUT,
3287
+ FIX_POLL,
3288
+ TENANT_ID
3289
+ ],
3290
+ examples: ["moda fix start <problem_id> --wait"]
3291
+ },
3292
+ {
3293
+ name: "verify",
3294
+ description: "Re-gate a Fix, optionally with a new candidate prompt (paid: replay playouts)",
3295
+ usage: "moda fix verify <fix_id> [--prompt-file=PATH] [--no-wait] [--timeout=ms] [--yes]",
3296
+ flags: [
3297
+ { name: "--prompt-file=PATH", description: "Candidate prompt to gate. New content skips the 10-minute re-gate cooldown; unchanged content does not." },
3298
+ { name: "--no-wait", description: "Return once the gate is enqueued." },
3299
+ { name: "--yes", description: "Re-gate inside the 10-minute cooldown anyway." },
3300
+ FIX_TIMEOUT,
3301
+ FIX_POLL,
3302
+ TENANT_ID
3303
+ ],
3304
+ examples: ["moda fix verify <fix_id> --prompt-file=prompts/agent.prompt.md"]
3305
+ },
3306
+ {
3307
+ name: "checkout",
3308
+ description: "Write the Fix's candidate into your working tree",
3309
+ usage: "moda fix checkout <fix_id>",
3310
+ flags: [TENANT_ID],
3311
+ examples: ["moda fix checkout <fix_id>"]
3312
+ },
3313
+ {
3314
+ name: "submit",
3315
+ description: "Open a PR for a verified Fix, or record a local branch",
3316
+ usage: "moda fix submit <fix_id> --pr | --local-ref=<branch>",
3317
+ flags: [
3318
+ { name: "--pr", description: "Open a pull request." },
3319
+ { name: "--local-ref=<branch>", description: "Record a branch you pushed yourself." },
3320
+ TENANT_ID
3321
+ ],
3322
+ examples: ["moda fix submit <fix_id> --pr"]
3323
+ },
3324
+ {
3325
+ name: "mark-applied",
3326
+ description: "Mark a Fix as applied outside Moda",
3327
+ usage: "moda fix mark-applied <fix_id> [--note=TEXT]",
3328
+ flags: [{ name: "--note=TEXT", description: "Where or how it was applied." }, TENANT_ID],
3329
+ examples: ['moda fix mark-applied <fix_id> --note="tool description updated in our agent config"']
3330
+ },
3331
+ {
3332
+ name: "dismiss",
3333
+ description: "Dismiss a Fix (no dry-run mode)",
3334
+ usage: "moda fix dismiss <fix_id> --reason=TEXT",
3335
+ flags: [{ name: "--reason=TEXT", description: "Required. Why it is dismissed." }, TENANT_ID],
3336
+ examples: ['moda fix dismiss <fix_id> --reason="not actionable"']
3337
+ }
3338
+ ];
3339
+
2006
3340
  // src/intelligence-schema.ts
2007
3341
  var INTELLIGENCE_SCHEMA_VERSION = "moda.intelligence.v1";
2008
3342
  var provenanceSchema = { type: "string", enum: ["measured", "inferred"] };
@@ -2534,15 +3868,9 @@ async function probeApiKey(key, options = {}) {
2534
3868
  }
2535
3869
 
2536
3870
  // src/keys.ts
2537
- import { createHash } from "node:crypto";
2538
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
3871
+ import { createHash as createHash2 } from "node:crypto";
3872
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
2539
3873
  import { join as join3 } from "node:path";
2540
- var KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS = {
2541
- list: "GET /api/cli/keys?profile=<profile>",
2542
- create: "POST /api/cli/keys",
2543
- rotate: "POST /api/cli/keys/<key_id>/rotate",
2544
- revoke: "POST /api/cli/keys/<key_id>/revoke"
2545
- };
2546
3874
  function keySecretRef(profile, keyId) {
2547
3875
  assertValidProfileName(profile);
2548
3876
  assertSafeKeyId(keyId);
@@ -2608,9 +3936,9 @@ function keyDescriptorStorePath(options = {}) {
2608
3936
  function loadLocalKeyDescriptors(options = {}) {
2609
3937
  const path = keyDescriptorStorePath(options);
2610
3938
  try {
2611
- if (!existsSync2(path))
3939
+ if (!existsSync3(path))
2612
3940
  return [];
2613
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
3941
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
2614
3942
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2615
3943
  return [];
2616
3944
  }
@@ -2648,7 +3976,7 @@ function upsertLocalKeyDescriptor(descriptor, options = {}) {
2648
3976
  return next;
2649
3977
  }
2650
3978
  function stableKeyId(profile, name, now) {
2651
- const hash = createHash("sha256").update(`${profile}\x00${name}\x00${now.toISOString()}`).digest("hex").slice(0, 12);
3979
+ const hash = createHash2("sha256").update(`${profile}\x00${name}\x00${now.toISOString()}`).digest("hex").slice(0, 12);
2652
3980
  return `key_${hash}`;
2653
3981
  }
2654
3982
  function assertSafeKeyId(keyId) {
@@ -2937,11 +4265,15 @@ async function runProfilesCommand(context) {
2937
4265
  switch (subcommand(context)) {
2938
4266
  case "list": {
2939
4267
  const config = loadProfileConfig({ env: context.env });
4268
+ const { name: effective, source } = effectiveProfile(context, config.active_profile);
4269
+ const missing = !config.profiles[effective];
2940
4270
  context.output.writeData({
2941
- active_profile: config.active_profile,
2942
- profiles: Object.values(config.profiles).map((profile) => serializeProfile(profile, config.active_profile)),
4271
+ active_profile: effective,
4272
+ active_profile_source: source,
4273
+ default_profile: config.active_profile,
4274
+ profiles: Object.values(config.profiles).map((profile) => serializeProfile(profile, effective)),
2943
4275
  migrations: config.migrations
2944
- });
4276
+ }, missing ? { warnings: [`The active profile '${effective}' (from ${source}) does not exist. Create it with \`moda profiles create ${effective}\` or select an existing profile.`] } : undefined);
2945
4277
  return { exitCode: 0 };
2946
4278
  }
2947
4279
  case "use": {
@@ -3011,36 +4343,55 @@ async function runProfilesCommand(context) {
3011
4343
  throw unknownSubcommandError("profiles", subcommand(context), ["list", "use", "create", "doctor"]);
3012
4344
  }
3013
4345
  }
4346
+ function effectiveProfile(context, storedDefault) {
4347
+ const envProfile = context.env.MODA_PROFILE?.trim();
4348
+ if (context.profile)
4349
+ return { name: context.profile, source: "flag" };
4350
+ if (envProfile)
4351
+ return { name: envProfile, source: "env:MODA_PROFILE" };
4352
+ return { name: storedDefault, source: "config" };
4353
+ }
3014
4354
  async function runConfigCommand(context) {
3015
4355
  if (subcommand(context) !== "show") {
3016
4356
  throw unknownSubcommandError("config", subcommand(context), ["show"]);
3017
4357
  }
3018
4358
  const config = loadProfileConfig({ env: context.env });
3019
4359
  const profile = getActiveProfile({ profile: context.profile, env: context.env });
4360
+ const options = { profile: profile.name, env: context.env };
4361
+ const envProfile = context.env.MODA_PROFILE?.trim();
4362
+ const apiKey = resolveApiKey(options);
4363
+ const baseUrl = resolveModaBaseUrl(options);
3020
4364
  context.output.writeData({
3021
4365
  moda_dir: getModaPaths({ env: context.env }).dir,
3022
4366
  active_profile: profile.name,
3023
- profile: serializeProfile(profile, config.active_profile),
4367
+ active_profile_source: context.profile ? "flag" : envProfile ? "env:MODA_PROFILE" : "config",
4368
+ default_profile: config.active_profile,
4369
+ profile: serializeProfile(profile, profile.name),
3024
4370
  resolved: {
3025
- api_key: redactSecret(resolveApiKeyFromActiveProfile({ profile: profile.name, env: context.env })),
3026
- base_url: profile.base_url,
3027
- ingest_url: profile.ingest_url,
3028
- tenant_id: profile.tenant_id,
4371
+ api_key: redactSecret(apiKey),
4372
+ api_key_source: context.env.MODA_API_KEY ? "env:MODA_API_KEY" : profile.api_key_ref === "legacy:config.api_key" && apiKey ? "legacy_config" : resolveApiKeyFromActiveProfile(options) ? "profile" : apiKey ? "legacy_config" : "none",
4373
+ base_url: baseUrl,
4374
+ base_url_source: describeBaseUrlSource(options),
4375
+ ingest_url: resolveIngestUrl(options),
4376
+ tenant_id: resolveApiTenantId(options),
3029
4377
  workspace_id: profile.workspace_id
3030
4378
  },
3031
4379
  migrations: config.migrations
3032
4380
  });
3033
4381
  return { exitCode: 0 };
3034
4382
  }
4383
+ var KEYS_LOCAL_ONLY_NOTE = "moda keys manages local key descriptors in ~/.moda only; it does not list or change your tenant's server-side API keys (see Settings → Ingestion keys in the dashboard).";
3035
4384
  async function runKeysCommand(context) {
3036
4385
  switch (subcommand(context)) {
3037
4386
  case "list": {
3038
- const profile = context.profile ?? loadProfileConfig({ env: context.env }).active_profile;
4387
+ const profile = context.profile ?? (context.env.MODA_PROFILE?.trim() || undefined) ?? loadProfileConfig({ env: context.env }).active_profile;
3039
4388
  const keys = loadLocalKeyDescriptors({ env: context.env }).filter((key) => !profile || key.profile === profile);
3040
4389
  context.output.writeData({
4390
+ scope: "local_descriptors",
3041
4391
  profile,
3042
- keys,
3043
- endpoint_requirements: KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS
4392
+ keys
4393
+ }, {
4394
+ warnings: [KEYS_LOCAL_ONLY_NOTE]
3044
4395
  });
3045
4396
  return { exitCode: 0 };
3046
4397
  }
@@ -3061,7 +4412,9 @@ async function runKeysCommand(context) {
3061
4412
  context.output.writeData({
3062
4413
  key: descriptor,
3063
4414
  secret_path: secretPath,
3064
- endpoint_requirements: KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS.create
4415
+ scope: "local_descriptors"
4416
+ }, {
4417
+ warnings: [KEYS_LOCAL_ONLY_NOTE]
3065
4418
  });
3066
4419
  return { exitCode: 0 };
3067
4420
  }
@@ -3069,7 +4422,7 @@ async function runKeysCommand(context) {
3069
4422
  case "revoke": {
3070
4423
  const action = subcommand(context);
3071
4424
  const keyId = requirePositional(context, 1, `Usage: moda keys ${action} <key_id>`);
3072
- throw new CliInputError(`moda keys ${action} is waiting on Moda Cloud key lifecycle support for ${keyId}.`, `Required endpoint: ${KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS[action]}`);
4425
+ throw new CliInputError(`moda keys ${action} is not supported: the CLI cannot ${action} server-side API keys (${keyId} was not changed).`, "Manage API keys in the dashboard at Settings → Ingestion keys.");
3073
4426
  }
3074
4427
  default:
3075
4428
  throw unknownSubcommandError("keys", subcommand(context), ["list", "create", "rotate", "revoke"]);
@@ -3077,8 +4430,8 @@ async function runKeysCommand(context) {
3077
4430
  }
3078
4431
 
3079
4432
  // src/agent.ts
3080
- import { existsSync as existsSync3, readFileSync as readFileSync4, statSync } from "node:fs";
3081
- import { join as join4, relative, resolve as resolve2 } from "node:path";
4433
+ import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync2 } from "node:fs";
4434
+ import { join as join4, relative as relative2, resolve as resolve3 } from "node:path";
3082
4435
  var MODA_DIR = ".moda";
3083
4436
  var HARNESS_PATH = join4(MODA_DIR, "harness.json");
3084
4437
  var HARNESS_REPORT_MARKDOWN_PATH = join4(MODA_DIR, "HARNESS_REPORT.md");
@@ -3093,25 +4446,8 @@ async function runAgentCommand(context) {
3093
4446
  return { exitCode: 0 };
3094
4447
  }
3095
4448
  case "investigate": {
3096
- const loaded = await buildAgentContext(context);
3097
- const question = context.positionals.slice(1).join(" ").trim() || context.flags.question;
3098
- context.output.writeData({
3099
- schemaVersion: "agent_investigation.v0.1",
3100
- generatedAt: loaded.payload.generatedAt,
3101
- mode: "local",
3102
- question: question || null,
3103
- status: loaded.payload.doctor.status,
3104
- findings: loaded.payload.doctor.topFindings,
3105
- recommendedActions: loaded.payload.recommendedActions,
3106
- evidenceRefs: loaded.payload.evidenceRefs,
3107
- limitations: [
3108
- ...loaded.payload.limitations,
3109
- "Production investigation is not yet implemented in P08; use P09/cloud commands for production-rooted analysis once shipped."
3110
- ]
3111
- }, agentWriteOptions(loaded, {
3112
- summary: summarizeAgentContext(loaded.payload, "investigation")
3113
- }));
3114
- return { exitCode: 0 };
4449
+ const passthrough = Object.entries(context.flags).filter(([key]) => ["days-back", "tool", "trace", "conversation"].includes(key)).map(([key, value]) => `--${key}=${value}`);
4450
+ throw new CliInputError("'moda agent investigate' has been removed. Use 'moda investigate' for the ranked production investigation.", `Run: ${["moda investigate", ...passthrough].join(" ")} (flags: --days-back, --tool, --trace). For local readiness use 'moda agent context'.`);
3115
4451
  }
3116
4452
  case "next-action":
3117
4453
  case "next-actions": {
@@ -3161,7 +4497,7 @@ async function runAgentCommand(context) {
3161
4497
  return { exitCode: 0 };
3162
4498
  }
3163
4499
  default:
3164
- throw new CliInputError(`Unknown agent subcommand '${subcommand2}'`, "Usage: moda agent context|investigate|next-action|evidence");
4500
+ throw new CliInputError(`Unknown agent subcommand '${subcommand2}'`, "Usage: moda agent context|next-action|evidence (for production issues use moda investigate)");
3165
4501
  }
3166
4502
  }
3167
4503
  async function buildAgentContext(context) {
@@ -3263,17 +4599,17 @@ function summarizeAgentContext(payload, noun) {
3263
4599
  }
3264
4600
  function readHarnessGraphIfPresent(cwd) {
3265
4601
  const path = join4(cwd, HARNESS_PATH);
3266
- if (!existsSync3(path))
4602
+ if (!existsSync4(path))
3267
4603
  return;
3268
4604
  try {
3269
- return JSON.parse(readFileSync4(path, "utf8"));
4605
+ return JSON.parse(readFileSync5(path, "utf8"));
3270
4606
  } catch {
3271
4607
  return;
3272
4608
  }
3273
4609
  }
3274
4610
  function readHarnessReportState(cwd) {
3275
4611
  const path = join4(cwd, HARNESS_REPORT_PATH);
3276
- if (!existsSync3(path)) {
4612
+ if (!existsSync4(path)) {
3277
4613
  return {
3278
4614
  state: {
3279
4615
  path: HARNESS_REPORT_PATH,
@@ -3318,10 +4654,10 @@ function readHarnessReportState(cwd) {
3318
4654
  }
3319
4655
  function readApproval(cwd) {
3320
4656
  const path = join4(cwd, HARNESS_REPORT_APPROVAL_PATH);
3321
- if (!existsSync3(path))
4657
+ if (!existsSync4(path))
3322
4658
  return;
3323
4659
  try {
3324
- return JSON.parse(readFileSync4(path, "utf8"));
4660
+ return JSON.parse(readFileSync5(path, "utf8"));
3325
4661
  } catch {
3326
4662
  return;
3327
4663
  }
@@ -3399,7 +4735,7 @@ function existingArtifacts(cwd) {
3399
4735
  [HARNESS_SUMMARY_PATH, "harness_summary", "Human-readable harness graph summary."],
3400
4736
  [join4(MODA_DIR, "sync-state.json"), "sync_state", "Last local harness sync state."],
3401
4737
  [join4(MODA_DIR, "prompts.yml"), "prompt_manifest", "Local prompt discovery manifest."]
3402
- ].flatMap(([path, kind, description]) => existsSync3(join4(cwd, path)) ? [{ path, kind, description }] : []);
4738
+ ].flatMap(([path, kind, description]) => existsSync4(join4(cwd, path)) ? [{ path, kind, description }] : []);
3403
4739
  }
3404
4740
  function expandEvidenceRef(ref, loaded, cwd) {
3405
4741
  if (ref.startsWith("doctor:")) {
@@ -3480,9 +4816,9 @@ function readCitationExcerpt(cwd, citation) {
3480
4816
  if (!citation.path || citation.type !== "file" && citation.type !== "generated_doc")
3481
4817
  return citation.excerpt;
3482
4818
  const absolute = safeResolve(cwd, citation.path);
3483
- if (!absolute || !existsSync3(absolute))
4819
+ if (!absolute || !existsSync4(absolute))
3484
4820
  return citation.excerpt;
3485
- const text = readFileSync4(absolute, "utf8");
4821
+ const text = readFileSync5(absolute, "utf8");
3486
4822
  if (citation.line && citation.line > 0) {
3487
4823
  return text.split(/\r?\n/)[citation.line - 1]?.slice(0, 1000) ?? citation.excerpt;
3488
4824
  }
@@ -3493,10 +4829,10 @@ function readBoundedModaArtifact(cwd, artifactPath) {
3493
4829
  throw new CliInputError(`Artifact '${artifactPath}' is outside .moda and cannot be expanded directly.`, "Use a citation evidence ref for source-file evidence.");
3494
4830
  }
3495
4831
  const absolute = safeResolve(cwd, artifactPath);
3496
- if (!absolute || !existsSync3(absolute))
4832
+ if (!absolute || !existsSync4(absolute))
3497
4833
  throw missingEvidence(`artifact:${artifactPath}`);
3498
- const size = statSync(absolute).size;
3499
- const text = readFileSync4(absolute, "utf8");
4834
+ const size = statSync2(absolute).size;
4835
+ const text = readFileSync5(absolute, "utf8");
3500
4836
  return {
3501
4837
  path: artifactPath,
3502
4838
  bytes: size,
@@ -3505,8 +4841,8 @@ function readBoundedModaArtifact(cwd, artifactPath) {
3505
4841
  };
3506
4842
  }
3507
4843
  function safeResolve(cwd, path) {
3508
- const absolute = resolve2(cwd, path);
3509
- const rel = relative(cwd, absolute);
4844
+ const absolute = resolve3(cwd, path);
4845
+ const rel = relative2(cwd, absolute);
3510
4846
  if (rel.startsWith("..") || rel === ".." || rel.startsWith("/"))
3511
4847
  return;
3512
4848
  return absolute;
@@ -3539,29 +4875,25 @@ function purposeForCommand(command) {
3539
4875
  }
3540
4876
 
3541
4877
  // src/production-intelligence.ts
3542
- import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
4878
+ import { existsSync as existsSync5, readFileSync as readFileSync6 } from "node:fs";
3543
4879
  import { join as join5 } from "node:path";
3544
4880
 
3545
4881
  // src/ask-stream.ts
3546
4882
  class AskStreamError extends Error {
3547
4883
  elapsedMs;
3548
4884
  toolEvents;
4885
+ accepted;
3549
4886
  constructor(message) {
3550
4887
  super(message);
3551
4888
  this.name = "AskStreamError";
3552
4889
  }
3553
4890
  }
3554
- var SUBSTANTIAL_STREAM_PROGRESS_MS = 60000;
3555
- var SUBSTANTIAL_STREAM_TOOL_EVENTS = 5;
3556
- function streamFallbackNotice(error) {
3557
- if (!(error instanceof AskStreamError))
3558
- return null;
3559
- const elapsedMs = error.elapsedMs ?? 0;
3560
- const toolEvents = error.toolEvents ?? 0;
3561
- if (elapsedMs < SUBSTANTIAL_STREAM_PROGRESS_MS && toolEvents <= SUBSTANTIAL_STREAM_TOOL_EVENTS) {
3562
- return null;
3563
- }
3564
- return `stream interrupted after ${Math.round(elapsedMs / 1000)}s; ` + "retrying via buffered request — this may take another minute or two.";
4891
+ function canFallBackToBufferedAsk(error) {
4892
+ return error instanceof AskStreamError && error.accepted !== true;
4893
+ }
4894
+ function askStreamInterruptedError(error) {
4895
+ const secs = Math.round((error.elapsedMs ?? 0) / 1000);
4896
+ return new Error(`The live ask stream dropped ${secs}s into a run the server had already accepted (${error.message}). ` + "Not re-running it, to avoid paying for a second agent run; the run may still complete server-side. " + 'Retry with `moda ask "<question>" --no-stream` in a minute (the buffered path has no live progress but survives drops).');
3565
4897
  }
3566
4898
 
3567
4899
  class SseParser {
@@ -3677,6 +5009,7 @@ class AskStreamCollector {
3677
5009
  lastPhase;
3678
5010
  toolNames = new Map;
3679
5011
  toolEvents = 0;
5012
+ events = 0;
3680
5013
  constructor(onProgress = () => {}) {
3681
5014
  this.onProgress = onProgress;
3682
5015
  }
@@ -3687,6 +5020,7 @@ class AskStreamCollector {
3687
5020
  if (!event || typeof event !== "object" || typeof event.type !== "string") {
3688
5021
  throw new AskStreamError("stream emitted a frame without an event type");
3689
5022
  }
5023
+ this.events += 1;
3690
5024
  switch (event.type) {
3691
5025
  case "phase": {
3692
5026
  const phase = typeof event.phase === "string" ? event.phase : "";
@@ -3752,6 +5086,9 @@ class AskStreamCollector {
3752
5086
  isTerminal() {
3753
5087
  return this.terminal;
3754
5088
  }
5089
+ get eventCount() {
5090
+ return this.events;
5091
+ }
3755
5092
  get toolEventCount() {
3756
5093
  return this.toolEvents;
3757
5094
  }
@@ -3848,6 +5185,7 @@ async function streamAsk(options) {
3848
5185
  const collector = new AskStreamCollector(onProgress);
3849
5186
  const ticker = new IdleTicker(onProgress);
3850
5187
  ticker.start();
5188
+ let accepted = false;
3851
5189
  try {
3852
5190
  const fetchImpl = options.fetchImpl ?? fetch;
3853
5191
  let response;
@@ -3877,6 +5215,7 @@ async function streamAsk(options) {
3877
5215
  if (!contentType.includes("text/event-stream")) {
3878
5216
  throw new AskStreamError(`stream endpoint returned unexpected content type '${contentType || "unknown"}'`);
3879
5217
  }
5218
+ accepted = true;
3880
5219
  if (!response.body) {
3881
5220
  throw new AskStreamError("stream response had no body");
3882
5221
  }
@@ -3893,6 +5232,8 @@ async function streamAsk(options) {
3893
5232
  error.elapsedMs = Date.now() - startedAt;
3894
5233
  if (error.toolEvents === undefined)
3895
5234
  error.toolEvents = collector.toolEventCount;
5235
+ if (error.accepted === undefined)
5236
+ error.accepted = accepted || collector.eventCount > 0;
3896
5237
  }
3897
5238
  throw error;
3898
5239
  } finally {
@@ -3905,8 +5246,10 @@ async function streamAsk(options) {
3905
5246
  var INVESTIGATION_SCHEMA_VERSION = "production_investigation.v0.1";
3906
5247
  var OVERVIEW_SCHEMA_VERSION = "production_overview.v0.1";
3907
5248
  var ASK_SCHEMA_VERSION = "production_ask.v0.1";
5249
+ var INVESTIGATION_PROBLEM_LIMIT = 3;
5250
+ var INVESTIGATION_PROBLEM_SLOTS = INVESTIGATION_PROBLEM_LIMIT;
3908
5251
  async function runProductionCommand(context) {
3909
- const daysBack = numberFlag(context.flags, "days-back", 7);
5252
+ const daysBack = daysBackFlag(context.flags);
3910
5253
  if (context.command === "overview") {
3911
5254
  const briefing = await loadOverviewBriefing({ daysBack, cwd: context.cwd });
3912
5255
  if (context.flags.raw === "true") {
@@ -3925,7 +5268,7 @@ async function runProductionCommand(context) {
3925
5268
  if (context.command === "ask") {
3926
5269
  const question = context.positionals.join(" ").trim();
3927
5270
  if (!question) {
3928
- throw new CliInputError("<question> is required", 'Usage: moda ask "what should I fix first?" [--days-back=N]');
5271
+ throw new CliInputError("<question> is required", 'Usage: moda ask "what should I fix first?" [--days-back=N] [--no-stream]');
3929
5272
  }
3930
5273
  const result = await askModaForContext(context, question, daysBack);
3931
5274
  if (context.outputMode === "human") {
@@ -4006,30 +5349,45 @@ async function investigateProduction(options) {
4006
5349
  const hints = loadHarnessHints(options.cwd);
4007
5350
  const overviewEndpoint = `/overview?days_back=${options.daysBack}`;
4008
5351
  const toolEndpoint = options.toolName ? `/tool-failures/${encodeURIComponent(options.toolName)}?days_back=${options.daysBack}&limit=5` : `/tool-failures?days_back=${options.daysBack}`;
4009
- const frustrationsEndpoint = `/frustrations?days_back=${options.daysBack}&limit=5`;
4010
- const [overview, tools, frustrations] = await Promise.all([
5352
+ const frustrationsEndpoint = `/emotions?family=frustration&days_back=${options.daysBack}&limit=5${options.conversationId ? `&conversation_id=${encodeURIComponent(options.conversationId)}` : ""}`;
5353
+ const problemsEndpoint = `/problems?days_back=${options.daysBack}&limit=${INVESTIGATION_PROBLEM_LIMIT}`;
5354
+ const skipped = { data: null, status: "ok" };
5355
+ const [overview, tools, frustrations, problems] = await Promise.all([
4011
5356
  safeData(overviewEndpoint),
4012
5357
  safeData(toolEndpoint),
4013
- safeData(frustrationsEndpoint)
5358
+ options.failureOnly ? Promise.resolve(skipped) : safeData(frustrationsEndpoint),
5359
+ options.failureOnly ? Promise.resolve(skipped) : safeData(problemsEndpoint)
4014
5360
  ]);
4015
- for (const [key, result] of Object.entries({ overview, tools, frustrations })) {
5361
+ const sources = options.failureOnly ? { overview, tools } : { overview, tools, frustrations, problems };
5362
+ for (const [key, result] of Object.entries(sources)) {
4016
5363
  sourceStatus[key] = result.status;
4017
5364
  if (result.warning)
4018
5365
  warnings.push(result.warning);
4019
5366
  }
5367
+ if (options.toolName && tools.status === "ok" && toolRows(tools.data, options.toolName).every((row) => row.count <= 0)) {
5368
+ warnings.push(`No failures found for tool "${options.toolName}" in the last ${options.daysBack} day(s) — the tool may have no failures in this window, or the name may not exist in this tenant. ` + "Run `moda tool-failures` to list tools with recorded failures, or widen --days-back.");
5369
+ }
5370
+ if (options.conversationId) {
5371
+ warnings.push(`Scoped to trace ${options.conversationId} for frustration evidence only; overview and tool failures are tenant-wide. ` + `Open the trace itself with \`moda audit ${options.conversationId}\`.`);
5372
+ }
4020
5373
  const evidenceRefs = [];
4021
5374
  const findings = [];
4022
5375
  addOverviewEvidence(overview.data, overviewEndpoint, evidenceRefs);
4023
5376
  findings.push(...toolFailureFindings(tools.data, options.daysBack, hints, evidenceRefs, options.toolName));
4024
5377
  if (!options.failureOnly) {
4025
5378
  findings.push(...frustrationFindings(frustrations.data, options.daysBack, evidenceRefs));
5379
+ findings.push(...problemFindings(problems.data, options.daysBack, evidenceRefs));
4026
5380
  }
4027
5381
  if (findings.length === 0) {
4028
5382
  findings.push(insufficientDataFinding(overview.data, evidenceRefs));
4029
5383
  }
4030
5384
  findings.sort(compareFindings);
4031
- const boundedFindings = findings.slice(0, 8);
4032
- const nextCommands = nextCommandsForFindings(boundedFindings, options.daysBack);
5385
+ const boundedFindings = capFindings(findings, 8, INVESTIGATION_PROBLEM_SLOTS);
5386
+ const nextCommands = nextCommandsForFindings(boundedFindings, options.daysBack, !options.failureOnly);
5387
+ const summary = summarizeInvestigation(boundedFindings);
5388
+ if (options.conversationId) {
5389
+ summary.text = `${summary.text} (Only frustration evidence is scoped to trace ${options.conversationId}.)`;
5390
+ }
4033
5391
  return {
4034
5392
  schemaVersion: INVESTIGATION_SCHEMA_VERSION,
4035
5393
  generatedAt: new Date().toISOString(),
@@ -4039,7 +5397,7 @@ async function investigateProduction(options) {
4039
5397
  ...options.toolName ? { toolName: options.toolName } : {},
4040
5398
  ...options.question ? { question: options.question } : {}
4041
5399
  },
4042
- summary: summarizeInvestigation(boundedFindings),
5400
+ summary,
4043
5401
  findings: boundedFindings,
4044
5402
  evidenceRefs: uniqueEvidenceRefs(evidenceRefs),
4045
5403
  nextCommands,
@@ -4140,11 +5498,10 @@ async function askModaForContext(context, question, daysBack) {
4140
5498
  } catch (error) {
4141
5499
  if (error instanceof CliAuthError)
4142
5500
  throw error;
4143
- const notice = streamFallbackNotice(error);
4144
- if (notice) {
4145
- safeWriteStderr(`[moda] ${notice}
4146
- `);
4147
- } else if (context.verbose) {
5501
+ if (error instanceof AskStreamError && !canFallBackToBufferedAsk(error)) {
5502
+ throw askStreamInterruptedError(error);
5503
+ }
5504
+ if (context.verbose) {
4148
5505
  safeWriteStderr(`[moda] live progress unavailable (${error instanceof Error ? error.message : String(error)}); using buffered ask.
4149
5506
  `);
4150
5507
  }
@@ -4330,7 +5687,7 @@ function toolFailureFindings(data, daysBack, hints, evidenceRefs, scopedTool) {
4330
5687
  id: `action_tool_detail_${slug(name)}`,
4331
5688
  title: `Inspect ${name} failure examples.`,
4332
5689
  rationale: "Examples include trace anchors and error subtypes.",
4333
- command: `moda tool-failure-detail ${name} --include-window`,
5690
+ command: `moda tool-failure-detail ${name} --days-back=${daysBack} --include-window`,
4334
5691
  mutability: "read",
4335
5692
  requiresApproval: false
4336
5693
  }]
@@ -4338,6 +5695,7 @@ function toolFailureFindings(data, daysBack, hints, evidenceRefs, scopedTool) {
4338
5695
  });
4339
5696
  }
4340
5697
  function frustrationFindings(data, daysBack, evidenceRefs) {
5698
+ data = frustrationsFromEmotions(asRecord(data) ?? {});
4341
5699
  const summary = asRecord(valueAt(data, ["summary"]));
4342
5700
  const frustrations = Array.isArray(valueAt(data, ["frustrations"])) ? valueAt(data, ["frustrations"]) : [];
4343
5701
  const count = numberValue(summary?.frustrated_count) || frustrations.length;
@@ -4347,12 +5705,13 @@ function frustrationFindings(data, daysBack, evidenceRefs) {
4347
5705
  const first = asRecord(frustrations[0]);
4348
5706
  const conversationId = asString(first?.conversation_id);
4349
5707
  const quote = firstQuote(first);
5708
+ const anchorIndex = frustrationAnchorIndex(first);
4350
5709
  const evidenceId = conversationId ? `frustration:${conversationId}` : "frustration:summary";
4351
5710
  evidenceRefs.push({
4352
5711
  id: evidenceId,
4353
5712
  kind: "frustration",
4354
5713
  label: quote ? `Frustration anchor: "${quote}"` : `${count} frustrated trace(s)`,
4355
- endpoint: `/frustrations?days_back=${daysBack}&limit=5`,
5714
+ endpoint: `/emotions?family=frustration&days_back=${daysBack}&limit=5`,
4356
5715
  ...conversationId ? { conversationId } : {},
4357
5716
  value: first ?? summary ?? data
4358
5717
  });
@@ -4371,12 +5730,78 @@ function frustrationFindings(data, daysBack, evidenceRefs) {
4371
5730
  id: "action_frustration_window",
4372
5731
  title: "Read the frustration window.",
4373
5732
  rationale: "The user quote and nearby turns reveal what confused or blocked the user.",
4374
- command: conversationId ? `moda context ${conversationId} --window=3` : `moda frustrations --days-back=${daysBack} --include-window`,
5733
+ command: conversationId ? `moda context ${conversationId}${anchorIndex !== null ? ` --msg-index=${anchorIndex}` : ""} --window=3` : `moda frustrations --days-back=${daysBack} --include-window`,
4375
5734
  mutability: "read",
4376
5735
  requiresApproval: false
4377
5736
  }]
4378
5737
  }];
4379
5738
  }
5739
+ function problemRows(data) {
5740
+ const record = asRecord(data);
5741
+ const rows = [];
5742
+ const push = (items, trust) => {
5743
+ if (!Array.isArray(items))
5744
+ return;
5745
+ for (const item of items) {
5746
+ const row = asRecord(item);
5747
+ const id = asString(row?.problem_id);
5748
+ if (!row || !id)
5749
+ continue;
5750
+ const lifecycle = asString(row.lifecycle) ?? "open";
5751
+ if (lifecycle === "resolved")
5752
+ continue;
5753
+ rows.push({
5754
+ id,
5755
+ name: asString(row.display_name) || id,
5756
+ cause: asString(row.cause_statement) || asString(row.display_description) || undefined,
5757
+ lifecycle,
5758
+ trust,
5759
+ verification: asString(row.verification_status) ?? undefined,
5760
+ affectedConversations: numberValue(row.affected_conversations) || numberValue(asRecord(row.evidence_support)?.attribution_count),
5761
+ raw: row
5762
+ });
5763
+ }
5764
+ };
5765
+ push(record?.problems, "trusted");
5766
+ push(record?.untrusted, "untrusted");
5767
+ push(record?.emerging, "emerging");
5768
+ return rows.slice(0, INVESTIGATION_PROBLEM_LIMIT);
5769
+ }
5770
+ function problemFindings(data, daysBack, evidenceRefs) {
5771
+ return problemRows(data).map((problem, index) => {
5772
+ const evidenceId = `problem:${problem.id}`;
5773
+ const trusted = problem.trust === "trusted";
5774
+ const trustNote = trusted ? "verified" : problem.trust === "emerging" ? "emerging, not yet ranked" : `untrusted (${problem.verification ?? "unverified"})`;
5775
+ evidenceRefs.push({
5776
+ id: evidenceId,
5777
+ kind: "problem",
5778
+ label: `Problem "${problem.name}" (${trustNote})`,
5779
+ endpoint: `/problems?days_back=${daysBack}&limit=${INVESTIGATION_PROBLEM_LIMIT}`,
5780
+ value: problem.raw
5781
+ });
5782
+ const affected = problem.affectedConversations;
5783
+ return {
5784
+ id: `finding_problem_${slug(problem.id)}`,
5785
+ kind: "problem",
5786
+ title: `Problem: ${problem.name}`,
5787
+ severity: trusted ? affected >= 10 ? "critical" : "high" : "medium",
5788
+ confidence: trusted ? "high" : "low",
5789
+ impactScore: (trusted ? 100 : 60) + Math.min(affected, 25) - index,
5790
+ rankReason: trusted ? `Rank #${index + 1} verified Problem in \`moda problems\`${affected > 0 ? `, affecting ${affected} trace(s)` : ""}.` : `Listed by \`moda problems\` as ${trustNote}; treat the cause as a hypothesis.`,
5791
+ summary: problem.cause ? `${problem.cause}${problem.lifecycle !== "open" ? ` (${problem.lifecycle})` : ""}` : `Problem ${problem.id} is ${problem.lifecycle} in the last ${daysBack} day(s).`,
5792
+ evidenceRefIds: [evidenceId],
5793
+ likelyLocations: [],
5794
+ recommendedActions: [{
5795
+ id: `action_problem_${slug(problem.id)}`,
5796
+ title: `Open the Problem dossier for ${problem.name}.`,
5797
+ rationale: "The dossier has the cause, evidence, affected traces and verification state.",
5798
+ command: `moda problem ${problem.id}`,
5799
+ mutability: "read",
5800
+ requiresApproval: false
5801
+ }]
5802
+ };
5803
+ });
5804
+ }
4380
5805
  function insufficientDataFinding(overview, evidenceRefs) {
4381
5806
  const evidenceId = "overview:data";
4382
5807
  if (!evidenceRefs.some((ref) => ref.id === evidenceId)) {
@@ -4879,11 +6304,14 @@ function toolRows(data, scopedTool) {
4879
6304
  const directToolName = asString(record?.tool_name) ?? scopedTool;
4880
6305
  const examples = Array.isArray(record?.examples) ? record.examples : [];
4881
6306
  if (directToolName) {
6307
+ const subtypes = (Array.isArray(record?.subtypes) ? record.subtypes : []).map((item) => asRecord(item)).filter((item) => item !== null && item !== undefined);
6308
+ const subtypeTotal = subtypes.reduce((sum, item) => sum + numberValue(item.count), 0);
6309
+ const top = [...subtypes].sort((left, right) => numberValue(right.count) - numberValue(left.count))[0];
4882
6310
  return [{
4883
6311
  name: directToolName,
4884
- count: numberValue(record?.failure_count) || numberValue(record?.total_failures) || examples.length,
4885
- conversations: numberValue(record?.failed_conversations) || numberValue(record?.conversation_count) || examples.length,
4886
- subtype: asString(record?.top_error_subtype) ?? asString(record?.error_subtype) ?? undefined,
6312
+ count: numberValue(record?.failure_count) || numberValue(record?.total_failures) || numberValue(asRecord(record?.pagination)?.total) || subtypeTotal || examples.length,
6313
+ conversations: numberValue(record?.failed_conversations) || numberValue(record?.conversation_count) || Math.max(0, ...subtypes.map((item) => numberValue(item.conversation_count))) || examples.length,
6314
+ subtype: asString(record?.top_error_subtype) ?? asString(record?.error_subtype) ?? asString(top?.subtype) ?? undefined,
4887
6315
  raw: data
4888
6316
  }];
4889
6317
  }
@@ -4961,11 +6389,11 @@ function loadHarnessHints(cwd) {
4961
6389
  entrypointPaths: []
4962
6390
  };
4963
6391
  const harnessPath = join5(cwd, ".moda", "harness.json");
4964
- if (!existsSync4(harnessPath))
6392
+ if (!existsSync5(harnessPath))
4965
6393
  return hints;
4966
6394
  hints.reportPath = ".moda/harness.json";
4967
6395
  try {
4968
- const parsed = JSON.parse(readFileSync5(harnessPath, "utf8"));
6396
+ const parsed = JSON.parse(readFileSync6(harnessPath, "utf8"));
4969
6397
  const graph = asRecord(parsed);
4970
6398
  const artifacts = Array.isArray(graph?.artifacts) ? graph.artifacts : [];
4971
6399
  for (const item of artifacts) {
@@ -4994,10 +6422,10 @@ function loadHarnessHints(cwd) {
4994
6422
  }
4995
6423
  function localHarnessSummary(cwd) {
4996
6424
  const path = join5(cwd, ".moda", "harness.json");
4997
- if (!existsSync4(path))
6425
+ if (!existsSync5(path))
4998
6426
  return null;
4999
6427
  try {
5000
- const parsed = JSON.parse(readFileSync5(path, "utf8"));
6428
+ const parsed = JSON.parse(readFileSync6(path, "utf8"));
5001
6429
  return {
5002
6430
  harnessId: parsed.harnessId,
5003
6431
  graphHash: parsed.graphHash,
@@ -5009,7 +6437,7 @@ function localHarnessSummary(cwd) {
5009
6437
  }
5010
6438
  function localSyncBrief(cwd, relativePath, label) {
5011
6439
  const path = join5(cwd, relativePath);
5012
- if (!existsSync4(path)) {
6440
+ if (!existsSync5(path)) {
5013
6441
  return { status: "missing", summary: `${label} state not found`, path: relativePath };
5014
6442
  }
5015
6443
  return { status: "present", summary: `${label} state exists locally`, path: relativePath };
@@ -5017,7 +6445,7 @@ function localSyncBrief(cwd, relativePath, label) {
5017
6445
  function localSkillBrief(cwd) {
5018
6446
  const claudeSkill = join5(cwd, ".claude", "skills");
5019
6447
  const cursorRules = join5(cwd, ".cursor", "rules");
5020
- if (existsSync4(claudeSkill) || existsSync4(cursorRules)) {
6448
+ if (existsSync5(claudeSkill) || existsSync5(cursorRules)) {
5021
6449
  return { status: "present", summary: "Local agent skill/rule directory exists" };
5022
6450
  }
5023
6451
  return { status: "missing", summary: "No local agent skill/rule directory found" };
@@ -5035,11 +6463,35 @@ function summarizeInvestigation(findings) {
5035
6463
  confidence: top.confidence
5036
6464
  };
5037
6465
  }
5038
- function nextCommandsForFindings(findings, daysBack) {
5039
- const commands = findings.flatMap((finding) => finding.recommendedActions.map((action) => action.command)).filter(Boolean);
5040
- commands.push(`moda overview --days-back=${daysBack}`);
5041
- commands.push("moda doctor --online");
5042
- return [...new Set(commands)].slice(0, 8);
6466
+ function nextCommandsForFindings(findings, daysBack, includeProblems = false) {
6467
+ const limit = 8;
6468
+ const primary = [...new Set(findings.map((finding) => finding.recommendedActions[0]?.command).filter(Boolean))];
6469
+ const generic = [
6470
+ ...includeProblems ? [`moda problems --days-back=${daysBack}`] : [],
6471
+ `moda overview --days-back=${daysBack}`,
6472
+ "moda doctor --online"
6473
+ ];
6474
+ const secondary = findings.flatMap((finding) => finding.recommendedActions.slice(1).map((action) => action.command)).filter(Boolean);
6475
+ const keep = new Set(primary);
6476
+ for (const command of [...generic, ...secondary]) {
6477
+ if (keep.size >= Math.max(limit, primary.length))
6478
+ break;
6479
+ keep.add(command);
6480
+ }
6481
+ const ordered = [
6482
+ ...findings.flatMap((finding) => finding.recommendedActions.map((action) => action.command)).filter(Boolean),
6483
+ ...generic
6484
+ ];
6485
+ return [...new Set(ordered)].filter((command) => keep.has(command));
6486
+ }
6487
+ function capFindings(sorted, limit, problemSlots) {
6488
+ const reserved = [
6489
+ ...sorted.filter((finding) => finding.kind === "problem").slice(0, problemSlots),
6490
+ ...sorted.filter((finding) => finding.kind === "frustration").slice(0, 1)
6491
+ ];
6492
+ const others = sorted.filter((finding) => !reserved.includes(finding)).slice(0, limit - reserved.length);
6493
+ const kept = new Set([...reserved, ...others]);
6494
+ return sorted.filter((finding) => kept.has(finding));
5043
6495
  }
5044
6496
  function compareFindings(a, b) {
5045
6497
  const severityDelta = severityWeight(b.severity) - severityWeight(a.severity);
@@ -5132,12 +6584,15 @@ function numberValue(value) {
5132
6584
  }
5133
6585
  return 0;
5134
6586
  }
5135
- function numberFlag(flags, key, fallback) {
5136
- const raw = flags[key];
5137
- const parsed = Number(raw);
5138
- if (!Number.isFinite(parsed) || parsed < 1)
6587
+ function daysBackFlag(flags, fallback = 7) {
6588
+ const raw = flags["days-back"];
6589
+ if (raw === undefined)
5139
6590
  return fallback;
5140
- return Math.min(Math.floor(parsed), 90);
6591
+ const parsed = Number(raw);
6592
+ if (raw.trim() === "" || raw === "true" || !Number.isInteger(parsed) || parsed < 1 || parsed > 90) {
6593
+ throw new CliInputError(`--days-back must be a whole number from 1 to 90; got '${raw}'.`, "Nothing was run. Example: --days-back=7 (the default).");
6594
+ }
6595
+ return parsed;
5141
6596
  }
5142
6597
  function normalizeConfidence(value) {
5143
6598
  if (value === "high" || value === "medium" || value === "low" || value === "unknown")
@@ -5148,7 +6603,7 @@ function capFallbackConfidence(confidence) {
5148
6603
  return confidence === "high" ? "medium" : confidence;
5149
6604
  }
5150
6605
  function evidenceKind(value) {
5151
- if (value === "data_api" || value === "tool_failure" || value === "frustration" || value === "harness" || value === "ask")
6606
+ if (value === "data_api" || value === "tool_failure" || value === "frustration" || value === "problem" || value === "harness" || value === "ask")
5152
6607
  return value;
5153
6608
  return "ask";
5154
6609
  }
@@ -5164,6 +6619,10 @@ function purposeForCommand2(command) {
5164
6619
  return "Open the ranked production investigation.";
5165
6620
  if (command.startsWith("moda tool-failure-detail"))
5166
6621
  return "Inspect tool failure examples and trace anchors.";
6622
+ if (command.startsWith("moda problems"))
6623
+ return "List the ranked Problems.";
6624
+ if (command.startsWith("moda problem "))
6625
+ return "Open the Problem dossier.";
5167
6626
  if (command.startsWith("moda context"))
5168
6627
  return "Read the relevant trace window.";
5169
6628
  if (command.startsWith("moda doctor"))
@@ -5290,6 +6749,7 @@ function buildManifest(commands) {
5290
6749
  streaming: command.outputModes.includes("agent-stream") || command.outputModes.includes("agent"),
5291
6750
  examples: command.examples,
5292
6751
  auth: command.auth,
6752
+ ...command.authNote ? { auth_note: command.authNote } : {},
5293
6753
  mutability: command.mutability,
5294
6754
  exit_codes: exitCodesForCommand(command.name)
5295
6755
  })),
@@ -5338,18 +6798,24 @@ function exitCodesForCommand(command) {
5338
6798
 
5339
6799
  // src/cli.ts
5340
6800
  import { fileURLToPath } from "url";
5341
- import { realpathSync as realpathSync2 } from "fs";
6801
+ import { realpathSync as realpathSync3 } from "fs";
5342
6802
  function parseArgs(argv) {
5343
6803
  const args = argv.slice(2);
5344
6804
  let command = "";
5345
6805
  const flags = {};
5346
6806
  const positionals = [];
6807
+ const shortFlags = [];
5347
6808
  let positional;
5348
6809
  let i = 0;
6810
+ let endOfOptions = false;
5349
6811
  while (i < args.length) {
5350
6812
  const arg = args[i];
5351
- if (arg.startsWith("--")) {
6813
+ if (!endOfOptions && arg === "--" && command !== "") {
6814
+ endOfOptions = true;
6815
+ } else if (!endOfOptions && arg.startsWith("--")) {
5352
6816
  i = parseFlag(args, i, flags, command === "");
6817
+ } else if (!endOfOptions && command !== "" && /^-[A-Za-z]/.test(arg)) {
6818
+ shortFlags.push(arg);
5353
6819
  } else if (command === "") {
5354
6820
  command = arg;
5355
6821
  } else if (positional === undefined) {
@@ -5360,8 +6826,10 @@ function parseArgs(argv) {
5360
6826
  }
5361
6827
  i++;
5362
6828
  }
5363
- return { command, positional, positionals, flags };
6829
+ return { command, positional, positionals, flags, ...shortFlags.length > 0 ? { shortFlags } : {} };
5364
6830
  }
6831
+ var DRY_RUN_TRUE = new Set(["true", "yes", "1", "on"]);
6832
+ var DRY_RUN_FALSE = new Set(["false", "no", "0", "off"]);
5365
6833
  var GLOBAL_BOOLEAN_FLAGS = new Set([
5366
6834
  "agent",
5367
6835
  "json",
@@ -5450,7 +6918,7 @@ Commands:
5450
6918
  profiles use <name> Switch active Moda profile
5451
6919
  profiles create <name> Create or update a local Moda profile
5452
6920
  profiles doctor [name] Validate a local Moda profile
5453
- keys list List local key descriptors
6921
+ keys list List local key descriptors (not server-side API keys)
5454
6922
  keys create Create a local key descriptor
5455
6923
  config show Show resolved Moda config
5456
6924
  harness analyze Produce a cited structured harness report
@@ -5461,14 +6929,14 @@ Commands:
5461
6929
  harness candidates Static (no-LLM) harness candidate pre-pass with file:line citations
5462
6930
  harness sync Sync the local harness graph to Moda Cloud
5463
6931
  harness delete <id-or-key> Permanently delete a synced harness from Moda Cloud
6932
+ harness list|show|versions|diff List synced harnesses, show one, its versions, or a version diff
5464
6933
  harness agents List detected runtime agents and families
5465
6934
  harness explain Explain the discovered harness graph
5466
6935
  agent context Emit local Moda context for coding agents
5467
- agent investigate Diagnose local Moda readiness for agents
5468
6936
  agent next-action Emit prioritized next commands for agents
5469
6937
  agent evidence <ref> Expand one evidence reference
5470
- investigate Rank production issues with evidence
5471
- failures Show production failures worth fixing first
6938
+ investigate Rank production issues (tool failures, frustrations, top Problems)
6939
+ failures Tool failures only, ranked (no frustrations or Problems)
5472
6940
  ask "question" Ask Moda a production/harness question
5473
6941
  sync Sync the local harness graph to Moda Cloud
5474
6942
  sync harness Sync the local harness graph to Moda Cloud
@@ -5480,20 +6948,25 @@ Commands:
5480
6948
  clusters Browse topic cluster hierarchy
5481
6949
  cluster-traces <node_id> List traces in a cluster
5482
6950
  traces Search and filter traces (agent runs)
6951
+ traces --sort=longest Sort traces: recent (default), longest, slowest
5483
6952
  search "<query>" Search trace messages (keyword/semantic/hybrid)
5484
6953
  world-state <conversation_id> Get a trace's world state (slots/threads/events)
5485
6954
  context <conversation_id> Get windowed trace context
6955
+ context <conversation_id> --all Whole transcript in order (first 500; --from=N --max-messages=N to continue)
5486
6956
  audit <conversation_id|trace_id> Raw span audit (spans, hierarchy, orphans, duplicates)
5487
- frustrations Get user frustration detections (legacy single-family; see emotions)
5488
- emotions Multi-family emotion detections (frustration, sadness, confusion, anxiety, trust, positive)
6957
+ frustrations Frustration detections (same data as emotions --family=frustration)
6958
+ emotions Multi-family emotion detections (frustration, disapproval, sadness, confusion, anxiety, trust, positive)
5489
6959
  hallucinations Grounding detections: contradicted/verified outputs with rule breakdown
5490
6960
  laziness Laziness detections per trace (repeated calls, premature handoffs, ...)
5491
6961
  tool-failures Get tool failure overview
5492
- tool-failure-detail <tool_name> Get per-tool failure detail
6962
+ tool-failure-detail <tool_name> Get per-tool failure detail (--limit=N examples, 1-20, default 5;
6963
+ long example text is cut to 300 chars)
5493
6964
  problems Rank cross-signal Problems by root cause (what to fix first)
6965
+ problems remainder Signals no Problem explains yet (--family, --cursor)
5494
6966
  problem-impact Live problem/use-case impact (--node-id, --problem-id, --mode, --days-back)
5495
6967
  problem <problem_id> Open one Problem: dossier, or --evidence/--reports/--traces/--feedback
5496
6968
  problem-feedback <problem_id> Mark a Problem fixed, dismiss, rename, or flag a bad attribution
6969
+ problem-reopen <problem_id> Reopen a Problem marked fixed (--reason)
5497
6970
  false-positive <kind> <target> Mark any detection as a false positive, optional --reason (problem, problem-detection, quote, tool, laziness, hallucination, custom-signal)
5498
6971
  false-positives [conv_id] Current false-positive marks: all (--kind, --all, --limit) or one trace
5499
6972
  problem-linear <problem_id> Show the Linear issue linked to a Problem (key, state, URL)
@@ -5502,6 +6975,10 @@ Commands:
5502
6975
  detection-review <problem_id> Mark a problem detection correct, a false positive, or clear the review
5503
6976
  signals List custom signals (name, criteria, status, 7-day stats)
5504
6977
  signal <signal_id> One custom signal, or --detections for its recent hits
6978
+ signals create|update|activate|pause|delete ...
6979
+ Manage custom signals (also draft, examples, refine, revise,
6980
+ proposals, accept, reject, search, preview); writes take --dry-run
6981
+ users / user <user_id> End users with health and activity (the dashboard Users page)
5505
6982
  trace-signals <conversation_id> Custom signal hits on one trace
5506
6983
  signal-label <signal_id> Label a trace fits / does_not_fit (false positive) for a signal
5507
6984
  step-scores <conversation_id> Graph-PRM step scores (per-segment curves, first bad step, rollup)
@@ -5515,9 +6992,11 @@ Commands:
5515
6992
  Prompt management:
5516
6993
  prompts init Create .moda/prompts.yml
5517
6994
  prompts status Show local prompt changes
5518
- prompts diff Show local prompt changes
6995
+ prompts diff Only changed/new/deleted prompts since last sync
5519
6996
  prompts sync Analyze prompt sources and upload versions
5520
6997
  prompts sync --watch Upload again when prompt files change
6998
+ prompts list|show|proposals|usage|replay-runs|decide
6999
+ Read the prompt registry; decide merges/rejects a proposal
5521
7000
  prompts promote <key> Move a dev/staging/prod label
5522
7001
  prompts ab Run a judged prompt A/B replay experiment
5523
7002
  prompts propose Revise a prompt from a run's failures (--gate to auto-A/B, --promote-on-win)
@@ -5530,15 +7009,19 @@ Commands:
5530
7009
  skills pull --status=proposed Include not-yet-promoted skills (default: approved)
5531
7010
  skills sync Push local .claude/skills/**/SKILL.md up to Moda
5532
7011
  skills sync --dry-run Show what would be pushed without writing
7012
+ skills list|show|policy List pushed skills, show one, or set --live/--local
7013
+ registry push Push prompts, skills, and tools from one JSON manifest
5533
7014
 
5534
7015
  Fixes (a fix with proof):
5535
7016
  fixes Ranked Fix queue (--status=S --limit=N --cursor=TOKEN)
5536
7017
  fixes draft-batch Campaign mode: batch-draft fixes for the top-ranked problems
5537
- (--limit=N, 1-25, default 10 — batches cap at 25: LLM spend guard)
7018
+ (--limit=N, 1-25, default 5 — batches cap at 25: LLM spend guard)
5538
7019
  fixes drive Round-robin one advance per active fix per pass until all rest
5539
7020
  (--status=S narrows the server-side scan window to one advancing
5540
- status, --timeout=ms default 2h, --pass-interval=ms default 20s;
5541
- exit 3 when the timeout lapses with fixes still advancing)
7021
+ status, --max-fixes=N 1-10 default 3 fixes per run (rest skipped;
7022
+ GATING fixes count toward N; least-recently-driven first),
7023
+ --timeout=ms default 2h, --pass-interval=ms default 20s;
7024
+ exit 3 when the timeout lapses, fixes were skipped, or still advancing)
5542
7025
  fix <fix_id> One Fix: status + gate result; --packet prints moda.fix_packet.v1
5543
7026
  (tool/skill candidates are also written under .moda/fixes/<ref>/);
5544
7027
  --wait drives a still-running pipeline to rest
@@ -5546,6 +7029,7 @@ Commands:
5546
7029
  scope -> propose -> gate and prints the packet
5547
7030
  fix verify <fix_id> Re-gate on the frozen holdout; --prompt-file=P gates local content
5548
7031
  (exit 0 pass / 1 fail with per-case findings / 3 degraded-inconclusive)
7032
+ Refuses a re-gate within 10 min of the last verdict unless --yes
5549
7033
  fix checkout <fix_id> Write the candidate at its target source path (never runs git)
5550
7034
  fix submit <fix_id> Deliver: --pr opens a draft PR | --local-ref=<branch> records your branch
5551
7035
  fix mark-applied <fix_id> Confirm a candidate you applied in your own stack (--note=TEXT);
@@ -5581,7 +7065,7 @@ Output auto-detection (no flags needed):
5581
7065
  --no-update-check Skip the daily new-version check (or MODA_CLI_UPDATE_CHECK=0)
5582
7066
 
5583
7067
  Traces flags:
5584
- --search=TEXT Substring match on the trace summary
7068
+ --search=TEXT Ranked trace search (hybrid when embeddings exist, else keyword)
5585
7069
  --world-state=KEYWORDS Match world-state content (slots + durable profile); comma = AND
5586
7070
  --outcome=any|positive|negative Filter by trace outcome (trajectory + frustration)
5587
7071
  --include-world-state Attach each result's world-state summary
@@ -5604,12 +7088,12 @@ World-state flags:
5604
7088
  --message-count=N Number of frames to fold (frames mode)
5605
7089
 
5606
7090
  Emotions flags:
5607
- --family=F frustration|sadness|confusion|anxiety|trust|positive
7091
+ --family=F frustration|disapproval|sadness|confusion|anxiety|trust|positive
5608
7092
  --days-back=N --limit=N Window 1-90 (default 7); page size 1-20 (default 10)
5609
7093
  --offset=N Skip N detections (0-10000)
5610
7094
 
5611
7095
  Laziness flags:
5612
- --family=F Narrow to one laziness family
7096
+ --family=F One laziness family (e.g. omission_phrase, stub_body, deflection, completion, tool_selection)
5613
7097
  --conversation-id=ID One trace's detections
5614
7098
  --days-back=N --limit=N Window 1-90 (default 7); page size 1-50 (default 10)
5615
7099
 
@@ -5622,7 +7106,7 @@ Problem flags (moda problem <id>):
5622
7106
  --evidence|--reports|--traces|--feedback
5623
7107
  Open one sub-resource page (at most one)
5624
7108
  --limit=N --cursor=TOKEN Keyset paging (1-50; pass next_cursor back verbatim)
5625
- --family=F --door=D Filter --traces (families: tool_failure|emotion|laziness|hallucination|prm_dip)
7109
+ --family=F --door=D Filter --traces (families: tool_failure|emotion|laziness|hallucination|prm_dip|custom_signal|conversation_reader)
5626
7110
 
5627
7111
  Problem-feedback flags:
5628
7112
  --action=A mark_fixed|dismiss|flag_attribution|rename (required)
@@ -5721,6 +7205,8 @@ Init flags:
5721
7205
  --no-sdk-integrate Skip SDK integration (alias: --no-sdk-guide)
5722
7206
  --verification-id=ID Tag the SDK integration's test trace with this id
5723
7207
  (the onb_ id from the Moda onboarding page)
7208
+ --connect-code=CODE Sign in with the one-time mcc_ code from the Moda
7209
+ onboarding page instead of the browser (10 min, single use)
5724
7210
  --ci-cd Create GitHub Actions Moda validation workflow
5725
7211
  --no-ci-cd Skip CI/CD workflow creation
5726
7212
  --autosync Create GitHub Actions workflow to sync prompts + skills to Moda on push to main
@@ -5829,6 +7315,9 @@ function printCommandHelp(definition) {
5829
7315
  if (definition.mutability === "write") {
5830
7316
  lines.push("This command can mutate state.");
5831
7317
  }
7318
+ for (const note of definition.helpNotes ?? []) {
7319
+ lines.push("", note);
7320
+ }
5832
7321
  if (definition.subcommands && definition.subcommands.length > 0) {
5833
7322
  lines.push("", "Subcommands:");
5834
7323
  const width = Math.max(...definition.subcommands.map((entry) => entry.name.length));
@@ -5884,7 +7373,8 @@ async function attachWindows(rows, windowSize) {
5884
7373
  }
5885
7374
  const conversation = asRecord(row.conversation);
5886
7375
  const embedded = conversation ? embeddedWindowSize(conversation) : null;
5887
- if (conversation && embedded !== null && embedded >= windowSize) {
7376
+ const embeddedCenter = asNumber(asRecord(conversation?.context)?.center_index);
7377
+ if (conversation && embedded !== null && embedded >= windowSize && embeddedCenter === msg_index) {
5888
7378
  return { ...row, window: conversation };
5889
7379
  }
5890
7380
  try {
@@ -5899,15 +7389,18 @@ async function attachWindows(rows, windowSize) {
5899
7389
  }
5900
7390
  }));
5901
7391
  }
5902
- async function printWithOptionalWindows(enriched, listKey, includeWindow, windowSize, output, warnings) {
5903
- const writeOptions = warnings && warnings.length > 0 ? { warnings } : undefined;
5904
- if (!includeWindow) {
5905
- output.writeData(enriched, writeOptions);
5906
- return;
7392
+ var TOOL_FAILURE_DETAIL_DEFAULT_LIMIT = 5;
7393
+ var TOOL_FAILURE_AGENT_SUBTYPES = 20;
7394
+ async function printWithOptionalWindows(enriched, listKey, includeWindow, windowSize, output, warnings, finalize) {
7395
+ const allWarnings = [...warnings ?? []];
7396
+ let data = enriched;
7397
+ if (includeWindow) {
7398
+ const list = Array.isArray(enriched[listKey]) ? enriched[listKey] : [];
7399
+ data = { ...enriched, [listKey]: await attachWindows(list, windowSize) };
5907
7400
  }
5908
- const list = Array.isArray(enriched[listKey]) ? enriched[listKey] : [];
5909
- const withWindows = await attachWindows(list, windowSize);
5910
- output.writeData({ ...enriched, [listKey]: withWindows }, writeOptions);
7401
+ if (finalize)
7402
+ data = finalize(data, allWarnings);
7403
+ output.writeData(data, allWarnings.length > 0 ? { warnings: allWarnings } : undefined);
5911
7404
  }
5912
7405
  function notFoundWarning(entity, id, isMissing) {
5913
7406
  if (!isMissing)
@@ -5916,6 +7409,23 @@ function notFoundWarning(entity, id, isMissing) {
5916
7409
  `No ${entity} found for id "${id}" — it may not exist in this tenant (the Data API returns an empty result rather than an error for unknown ids).`
5917
7410
  ];
5918
7411
  }
7412
+ var AUDIT_DEFAULT_LIMIT = 200;
7413
+ var AUDIT_MAX_LIMIT = 1000;
7414
+ function auditCapStatus(data, limit) {
7415
+ const record = asRecord(data);
7416
+ if (!record)
7417
+ return null;
7418
+ const summary = asRecord(record.summary) ?? {};
7419
+ const rows = (asNumber(summary.ingest_rows) ?? (Array.isArray(record.ingest) ? record.ingest.length : 0)) + (asNumber(record.parse_errors) ?? 0);
7420
+ const spans = asNumber(summary.total_spans) ?? (Array.isArray(record.spans) ? record.spans.length : 0);
7421
+ const hasMore = record.has_more === true || summary.has_more === true;
7422
+ const spansCapped = summary.truncated === true || record.truncated === true;
7423
+ const serverSaid = typeof record.has_more === "boolean" || typeof summary.has_more === "boolean";
7424
+ const rowsCapped = hasMore || !serverSaid && rows >= limit;
7425
+ if (!rowsCapped && !spansCapped)
7426
+ return null;
7427
+ return { rowsCapped, spansCapped, hasMoreKnown: hasMore, rows, spans };
7428
+ }
5919
7429
  var TAIL_SEEN_MAX = 5000;
5920
7430
  var EMOTIONS_TAIL_PAGE_SIZE = 20;
5921
7431
  var EMOTIONS_TAIL_MAX_PAGES = 25;
@@ -6097,7 +7607,7 @@ async function runTailCommand(params, context) {
6097
7607
  break;
6098
7608
  if (params.max_events !== undefined && emitted >= params.max_events)
6099
7609
  break;
6100
- await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
7610
+ await new Promise((resolve4) => setTimeout(resolve4, intervalMs));
6101
7611
  }
6102
7612
  }
6103
7613
  async function attachWorldState(rows) {
@@ -6195,7 +7705,435 @@ function linearApiError(error) {
6195
7705
  }
6196
7706
  return error;
6197
7707
  }
7708
+ var GLOBAL_FLAG_NAMES = new Set([
7709
+ ...[...GLOBAL_BOOLEAN_FLAGS].filter((flag2) => flag2 !== "dry-run"),
7710
+ "profile",
7711
+ "progress",
7712
+ "no-stream",
7713
+ "format",
7714
+ "no-telemetry",
7715
+ "no-update-check"
7716
+ ]);
7717
+ var OVERVIEW_PRODUCTION_FLAGS = ["raw"];
7718
+ var WINDOW_FLAG_HINTS = {
7719
+ days_back: "This command takes --time-range (e.g. --time-range=7d), not --days-back.",
7720
+ time_range: "This command takes --days-back=N (1-90), not --time-range."
7721
+ };
7722
+ function editDistance(a, b) {
7723
+ const row = Array.from({ length: b.length + 1 }, (_, index) => index);
7724
+ for (let i = 1;i <= a.length; i++) {
7725
+ let previous = row[0];
7726
+ row[0] = i;
7727
+ for (let j = 1;j <= b.length; j++) {
7728
+ const current = row[j];
7729
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
7730
+ previous = current;
7731
+ }
7732
+ }
7733
+ return row[b.length];
7734
+ }
7735
+ function schemaFlagKeys(schema) {
7736
+ let current = schema;
7737
+ while (current instanceof z2.ZodEffects)
7738
+ current = current.innerType();
7739
+ return current instanceof z2.ZodObject ? Object.keys(current.shape) : [];
7740
+ }
7741
+ function parseCommandFlags(schema, args, command, options = {}) {
7742
+ const schemaKeys = schemaFlagKeys(schema);
7743
+ const known = new Set([
7744
+ ...schemaKeys,
7745
+ ...[...GLOBAL_FLAG_NAMES, ...options.extraFlags ?? []].map((flag2) => flag2.replace(/-/g, "_"))
7746
+ ]);
7747
+ const unknown = Object.keys(args).filter((key) => !known.has(key));
7748
+ if (unknown.includes("dry_run")) {
7749
+ throw new CliInputError(`'moda ${command}' has no dry-run mode, so nothing was sent.`, `Remove --dry-run to run it for real. Commands with a real dry-run: sync, registry push, harness sync/delete, prompts sync/decide, skills sync/policy, signals <write subcommands>, problem-reopen.`);
7750
+ }
7751
+ if (unknown.length > 0) {
7752
+ const flagKeys = schemaKeys.filter((key) => key !== options.positionalKey);
7753
+ const valid = flagKeys.map((key) => `--${key.replace(/_/g, "-")}`);
7754
+ const hints = unknown.map((key) => {
7755
+ const windowAlternative = key === "days_back" ? "time_range" : key === "time_range" ? "days_back" : undefined;
7756
+ if (windowAlternative && WINDOW_FLAG_HINTS[key] && flagKeys.includes(windowAlternative))
7757
+ return WINDOW_FLAG_HINTS[key];
7758
+ if (windowAlternative)
7759
+ return `This command has no time-window flag.`;
7760
+ const close = flagKeys.map((candidate) => ({ candidate, distance: editDistance(key, candidate) })).filter(({ distance }) => distance <= 2).sort((left, right) => left.distance - right.distance)[0];
7761
+ return close ? `Did you mean --${close.candidate.replace(/_/g, "-")}?` : "";
7762
+ }).filter(Boolean);
7763
+ const names = unknown.map((key) => `--${key.replace(/_/g, "-")}`).join(", ");
7764
+ throw new CliInputError(`Unknown flag${unknown.length > 1 ? "s" : ""} ${names} for 'moda ${command}'.`, [...hints, `Valid flags: ${valid.join(", ") || "(none)"}. Run 'moda ${command} --help'.`].join(" "));
7765
+ }
7766
+ return schema.parse(args);
7767
+ }
7768
+ function withStringIdFlags(args, flags) {
7769
+ for (const flag2 of ["user-id", "conversation-id"]) {
7770
+ if (flags[flag2] !== undefined)
7771
+ args[flag2.replace(/-/g, "_")] = flags[flag2];
7772
+ }
7773
+ return args;
7774
+ }
7775
+ function shellArg(value) {
7776
+ if (/^[A-Za-z0-9_.,:@%+\/=-]+$/.test(value))
7777
+ return value;
7778
+ if (!/["$`\\!]/.test(value))
7779
+ return `"${value}"`;
7780
+ return `'${value.replace(/'/g, `'\\''`)}'`;
7781
+ }
7782
+ async function searchFallbackToTraces(params) {
7783
+ const limit = Math.min(params.limit ?? 20, 100);
7784
+ const offset = params.offset ?? 0;
7785
+ const query = new URLSearchParams;
7786
+ query.set("search", params.q);
7787
+ if (params.user_id)
7788
+ query.set("user_id", params.user_id);
7789
+ if (params.time_range)
7790
+ query.set("time_range", params.time_range);
7791
+ query.set("limit", String(limit));
7792
+ if (offset > 0)
7793
+ query.set("offset", String(offset));
7794
+ const tracesCommand = [
7795
+ `moda traces --search=${shellArg(params.q)}`,
7796
+ params.user_id ? `--user-id=${shellArg(params.user_id)}` : "",
7797
+ params.time_range ? `--time-range=${shellArg(params.time_range)}` : "",
7798
+ `--limit=${limit}`
7799
+ ].filter(Boolean).join(" ");
7800
+ let traces;
7801
+ try {
7802
+ traces = asRecord(await callDataAPI(`/conversations?${query.toString()}`)) ?? {};
7803
+ } catch (error) {
7804
+ const reason = error instanceof Error ? error.message : String(error);
7805
+ return {
7806
+ kind: "notice",
7807
+ warning: `Message-level search returned 0 results, and the trace-level fallback failed (${reason}). ` + "Zero results here does NOT mean the topic never came up. Retry, or run the trace search directly.",
7808
+ nextCommands: [{ command: tracesCommand, purpose: "Retry the trace-level search directly.", mutability: "read", requires_approval: false }]
7809
+ };
7810
+ }
7811
+ const rows = Array.isArray(traces.conversations) ? traces.conversations.map(asRecord).filter(isDefinedRecord) : [];
7812
+ if (rows.length === 0)
7813
+ return;
7814
+ const pagination = asRecord(traces.pagination);
7815
+ const total = asNumber(pagination?.total) ?? rows.length;
7816
+ const nextOffset = offset + rows.length;
7817
+ const hasMore = pagination?.has_more === true || (asNumber(pagination?.total) ?? 0) > nextOffset;
7818
+ const traceMode = asString(traces.search_mode) ?? "keyword";
7819
+ const literal = traceMode === "keyword";
7820
+ if (params.mode === "keyword" && !literal) {
7821
+ return {
7822
+ kind: "notice",
7823
+ warning: `Message-level keyword search returned 0 results. Trace-level search is only available ${traceMode}-ranked here ` + `(results may not contain the exact words), so none are substituted for --mode=keyword. ` + "Zero results here does NOT mean the words never appear; the message index may be incomplete.",
7824
+ nextCommands: [{ command: tracesCommand, purpose: "See ranked trace-level results instead.", mutability: "read", requires_approval: false }]
7825
+ };
7826
+ }
7827
+ const results = rows.map((row) => ({
7828
+ match_grain: "trace",
7829
+ conversation_id: row.conversation_id,
7830
+ summary: row.summary ?? null,
7831
+ message_count: row.message_count ?? null,
7832
+ first_timestamp: row.first_timestamp ?? null,
7833
+ last_timestamp: row.last_timestamp ?? null
7834
+ }));
7835
+ const first = asString(rows[0].conversation_id);
7836
+ const describe = literal ? "trace-level keyword matches (each trace contains the words)" : `trace-level ${traceMode}-ranked related traces (they may not contain the exact words; fallback.total is the candidate pool, not a match count)`;
7837
+ const nextCommands = [];
7838
+ if (first) {
7839
+ nextCommands.push({ command: `moda audit ${first}`, purpose: "Open the top trace to locate the relevant message.", mutability: "read", requires_approval: false });
7840
+ }
7841
+ if (hasMore) {
7842
+ nextCommands.push({ command: `${tracesCommand} --offset=${nextOffset}`, purpose: "Page through further trace-level results.", mutability: "read", requires_approval: false });
7843
+ }
7844
+ return {
7845
+ kind: "results",
7846
+ hasMore,
7847
+ data: {
7848
+ results,
7849
+ fallback: {
7850
+ source: "traces_search",
7851
+ reason: "message_index_empty",
7852
+ grain: "trace",
7853
+ search_mode: traceMode,
7854
+ total,
7855
+ total_kind: literal ? "matches" : "candidate_pool",
7856
+ has_more: hasMore,
7857
+ ...offset > 0 ? { offset } : {},
7858
+ ...hasMore ? { next_offset: nextOffset } : {}
7859
+ }
7860
+ },
7861
+ warning: `Message-level search returned 0 results, so these are ${describe}, from \`moda traces --search\`. ` + `Each result is a whole trace, not a message, with no score or message index. ` + `Open one with \`moda audit <conversation_id>\` or \`moda context <conversation_id>\` to find the matching turn.` + (hasMore ? " More exist: page with the suggested `moda traces --search ... --offset` command." : ""),
7862
+ nextCommands
7863
+ };
7864
+ }
7865
+ function capProblemsBlocks(data, limit) {
7866
+ const truncated = {};
7867
+ const payload = { ...data };
7868
+ if (Array.isArray(data.emerging) && data.emerging.length > limit) {
7869
+ truncated.emerging = data.emerging.length - limit;
7870
+ payload.emerging = data.emerging.slice(0, limit);
7871
+ }
7872
+ const hierarchy = asRecord(data.hierarchy);
7873
+ if (hierarchy && Array.isArray(hierarchy.macros) && hierarchy.macros.length > limit) {
7874
+ truncated.hierarchy_macros = hierarchy.macros.length - limit;
7875
+ payload.hierarchy = { ...hierarchy, macros: hierarchy.macros.slice(0, limit) };
7876
+ }
7877
+ return Object.keys(truncated).length > 0 ? { payload, truncated } : { payload };
7878
+ }
7879
+ function serverWarnings(data) {
7880
+ const list = data?.warnings;
7881
+ return Array.isArray(list) ? list.filter((item) => typeof item === "string") : [];
7882
+ }
7883
+ function isDefinedRecord(value) {
7884
+ return value !== undefined && value !== null;
7885
+ }
7886
+ var HAND_PARSED_COMMAND_FLAGS = {
7887
+ investigate: ["days-back", "tool", "trace", "conversation"],
7888
+ failures: ["days-back", "tool"],
7889
+ ask: ["days-back", "stream"],
7890
+ "step-scores": [],
7891
+ "trace-signals": [],
7892
+ signals: [],
7893
+ "problem-feedback": ["action", "actor", "reason", "attribution-id", "new-name"],
7894
+ "problem-linear": [],
7895
+ "problem-linear-file": ["issue", "priority", "title", "team-id"],
7896
+ "signal-label": ["label", "note", "conversation-id", "msg-dedup-token", "msg-index"],
7897
+ "detection-reviews": ["attribution-id"],
7898
+ "detection-review": ["note", "verdict", "attribution-id", "conversation-id"],
7899
+ "false-positive": [
7900
+ "family",
7901
+ "quote",
7902
+ "reason",
7903
+ "tool",
7904
+ "turn",
7905
+ "undo",
7906
+ "attribution-id",
7907
+ "conversation-id",
7908
+ "msg-dedup-token",
7909
+ "msg-index",
7910
+ "tool-use-id"
7911
+ ],
7912
+ "false-positives": ["all", "kind", "limit"],
7913
+ feedback: ["category", "severity", "cluster-id", "conversation-id", "run-id", "tool-name"],
7914
+ "problem-impact": ["mode", "node-id", "problem-id", "run-id", "to", "cursor", "conversation-ids", "days-back"]
7915
+ };
7916
+ var NO_POSITIONAL_COMMANDS = new Set([
7917
+ "overview",
7918
+ "investigate",
7919
+ "failures",
7920
+ "signals",
7921
+ "emotions",
7922
+ "frustrations",
7923
+ "laziness",
7924
+ "hallucinations",
7925
+ "tool-failures",
7926
+ "traces",
7927
+ "conversations",
7928
+ "clusters",
7929
+ "tail",
7930
+ "problem-impact"
7931
+ ]);
7932
+ function assertHandParsedFlags(command, flags, positionals) {
7933
+ if (command === "signals" && positionals[0]) {
7934
+ if (SIGNALS_MANAGEMENT_SUBCOMMANDS.has(positionals[0]))
7935
+ return;
7936
+ throw unknownSignalsSubcommandError(positionals[0], positionals.slice(1));
7937
+ }
7938
+ if (NO_POSITIONAL_COMMANDS.has(command) && positionals.length > 0) {
7939
+ throw new CliInputError(`'moda ${command}' takes no arguments, but got '${positionals.join(" ")}'.`, `Nothing was run. See 'moda ${command} --help' for its flags.`);
7940
+ }
7941
+ const allowed = HAND_PARSED_COMMAND_FLAGS[command];
7942
+ if (!allowed)
7943
+ return;
7944
+ const shape = z2.object(Object.fromEntries(allowed.map((flag2) => [flag2.replace(/-/g, "_"), z2.unknown()])));
7945
+ const args = Object.fromEntries(Object.keys(flags).map((flag2) => [flag2.replace(/-/g, "_"), true]));
7946
+ parseCommandFlags(shape.passthrough(), args, command);
7947
+ }
7948
+ var PROFILE_MANAGING_COMMANDS = new Set(["profiles", "auth", "init", "provision", "status", "config", "keys", "manifest", "agent", "doctor"]);
7949
+ async function runProblemsRemainder(flags, context) {
7950
+ const params = parseCommandFlags(ProblemsRemainderSchema, flagsToArgs(flags), "problems remainder");
7951
+ const query = new URLSearchParams;
7952
+ if (params.days_back)
7953
+ query.set("days_back", params.days_back.toString());
7954
+ if (params.limit)
7955
+ query.set("limit", params.limit.toString());
7956
+ if (params.family)
7957
+ query.set("family", params.family);
7958
+ if (params.cursor)
7959
+ query.set("cursor", params.cursor);
7960
+ const queryString = query.toString() ? `?${query.toString()}` : "";
7961
+ context.output.writeData(await callDataAPI(`/problems/remainder${queryString}`));
7962
+ }
7963
+ function processingDisabledWarning(data, family) {
7964
+ const processing = asRecord(asRecord(data)?.processing);
7965
+ const label = family === "emotion" ? "Emotion" : "Frustration";
7966
+ if (processing?.enabled === null) {
7967
+ return [`Could not confirm whether ${family} analysis is enabled for this tenant, so treat zero results with caution.`];
7968
+ }
7969
+ if (processing?.enabled !== false)
7970
+ return [];
7971
+ if (asString(processing.reason) === "dogfood_tenant") {
7972
+ return [`${label} analysis is skipped for this internal (dogfood) tenant, so zero results do NOT mean users are not frustrated.`];
7973
+ }
7974
+ const setting = asString(processing.setting) ?? "signals.frustration";
7975
+ return [
7976
+ `${family === "emotion" ? "Emotion" : "Frustration"} analysis is disabled for this tenant (${setting} = false), ` + "so zero results do NOT mean users are not frustrated. Enable it in Settings → Account → Signal processing; " + "only traces ingested after enabling are analyzed."
7977
+ ];
7978
+ }
7979
+ var FULL_TRANSCRIPT_DEFAULT_MAX_MESSAGES = 500;
7980
+ var CONTEXT_RANGE_PAGE_SIZE = 200;
7981
+ var LEGACY_WINDOW_MAX_MESSAGES = 100;
7982
+ var CONTEXT_MESSAGE_TRUNCATE_CHARS = 500;
7983
+ var LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g;
7984
+ function stripLoneSurrogates(value) {
7985
+ if (typeof value === "string")
7986
+ return value.replace(LONE_SURROGATE, "");
7987
+ if (Array.isArray(value))
7988
+ return value.map(stripLoneSurrogates);
7989
+ if (value && typeof value === "object") {
7990
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, stripLoneSurrogates(v)]));
7991
+ }
7992
+ return value;
7993
+ }
7994
+ function isUnknownRangeParamsError(error) {
7995
+ const e = error;
7996
+ if (e?.statusCode !== 400)
7997
+ return false;
7998
+ const text = `${typeof e.responseBody === "string" ? e.responseBody : ""} ${typeof e.message === "string" ? e.message : ""}`;
7999
+ return /property (from|count) should not exist/i.test(text);
8000
+ }
8001
+ function pageMessages(page) {
8002
+ const messages = asRecord(page.context)?.messages ?? page.messages;
8003
+ return (Array.isArray(messages) ? messages : []).map((m) => asRecord(m) ?? {});
8004
+ }
8005
+ async function readFullTranscript(conversationId, opts = {}) {
8006
+ const start = opts.from ?? 0;
8007
+ const requested = opts.maxMessages ?? FULL_TRANSCRIPT_DEFAULT_MAX_MESSAGES;
8008
+ const path = `/conversations/${encodeURIComponent(conversationId)}/context`;
8009
+ const warnings = [];
8010
+ const messages = [];
8011
+ let total = 0;
8012
+ let summary = null;
8013
+ let ordering;
8014
+ let nextFrom = null;
8015
+ let legacy = false;
8016
+ let cursor = start;
8017
+ for (;; ) {
8018
+ const count = Math.min(CONTEXT_RANGE_PAGE_SIZE, requested - messages.length);
8019
+ let page;
8020
+ try {
8021
+ page = asRecord(await callDataAPI(`${path}?from=${cursor}&count=${count}`)) ?? {};
8022
+ } catch (error) {
8023
+ if (cursor === start && isUnknownRangeParamsError(error)) {
8024
+ legacy = true;
8025
+ break;
8026
+ }
8027
+ throw error;
8028
+ }
8029
+ if (cursor === start) {
8030
+ total = asNumber(page.total_messages) ?? 0;
8031
+ summary = page.summary ?? null;
8032
+ ordering = asRecord(page.ordering) ?? undefined;
8033
+ }
8034
+ const batch = pageMessages(page);
8035
+ messages.push(...batch);
8036
+ const next = asNumber(page.next_from);
8037
+ nextFrom = page.has_more === true && next !== null && next !== undefined && next > cursor ? next : null;
8038
+ if (nextFrom === null || batch.length === 0 || messages.length >= requested)
8039
+ break;
8040
+ cursor = nextFrom;
8041
+ }
8042
+ if (legacy) {
8043
+ const limit = Math.min(requested, LEGACY_WINDOW_MAX_MESSAGES);
8044
+ const window = 5;
8045
+ const span = window * 2 + 1;
8046
+ const byIndex = new Map;
8047
+ for (let center = start + window;; center += span) {
8048
+ const page = asRecord(await callDataAPI(`${path}?msg_index=${center}&window=${window}`)) ?? {};
8049
+ if (center === start + window) {
8050
+ total = asNumber(page.total_messages) ?? 0;
8051
+ summary = page.summary ?? null;
8052
+ }
8053
+ for (const message of pageMessages(page)) {
8054
+ const index = asNumber(message.index);
8055
+ if (index !== null && index !== undefined && index >= start && index < start + limit)
8056
+ byIndex.set(index, message);
8057
+ }
8058
+ if (center + window + 1 >= Math.min(total, start + limit))
8059
+ break;
8060
+ }
8061
+ messages.push(...[...byIndex.entries()].sort(([a], [b]) => a - b).map(([, message]) => message));
8062
+ const end = start + messages.length;
8063
+ nextFrom = end < total && messages.length > 0 ? end : null;
8064
+ warnings.push(`This Moda server has no range reads for context (older backend), so --all fell back to windowed paging ` + `and stopped at ${LEGACY_WINDOW_MAX_MESSAGES} messages to keep it cheap.`);
8065
+ }
8066
+ const pageSize = legacy ? LEGACY_WINDOW_MAX_MESSAGES : requested;
8067
+ const tied = asNumber(ordering?.tied_timestamp_messages) ?? 0;
8068
+ const synthetic = asNumber(ordering?.synthetic_timestamp_messages) ?? 0;
8069
+ const atCap = (value) => {
8070
+ const text = typeof value === "string" ? value : value === undefined || value === null ? "" : JSON.stringify(value);
8071
+ return text.length >= CONTEXT_MESSAGE_TRUNCATE_CHARS;
8072
+ };
8073
+ const isTruncated = (record) => {
8074
+ const toolCalls = Array.isArray(record.tool_calls) ? record.tool_calls : [];
8075
+ const toolResults = Array.isArray(record.tool_results) ? record.tool_results : [];
8076
+ return atCap(record.content) || toolCalls.some((call) => atCap(asRecord(call)?.input)) || toolResults.some((result) => atCap(asRecord(result)?.content));
8077
+ };
8078
+ const build = (kept) => {
8079
+ const returned = messages.slice(0, kept);
8080
+ const out = [...notFoundWarning("trace", conversationId, total === 0), ...warnings];
8081
+ if (total > 0 && start >= total) {
8082
+ out.push(`--from=${start} is past the end of this trace (${total} messages, indexes 0-${total - 1}).`);
8083
+ }
8084
+ const nextCommands = [];
8085
+ const firstMissing = kept < messages.length ? asNumber(messages[kept]?.index) ?? start + kept : null;
8086
+ const next = firstMissing ?? nextFrom;
8087
+ if (next !== null) {
8088
+ const last = asNumber(returned[returned.length - 1]?.index) ?? next - 1;
8089
+ const nextMax = firstMissing !== null ? Math.min(pageSize, Math.max(kept * 2, 50)) : pageSize;
8090
+ const continueWith = `moda context ${shellArg(conversationId)} --all --from=${next} --max-messages=${nextMax}`;
8091
+ out.push(`Trace has ${total} messages; returned messages ${start}-${last}. ` + (firstMissing !== null ? `Stopped at ${kept} of the ${messages.length} messages read to stay under the agent output byte limit (--json has no limit). ` : "") + `Continue with: ${continueWith}`);
8092
+ nextCommands.push({
8093
+ command: continueWith,
8094
+ purpose: `Read the next messages of this trace, starting at message ${next}.`,
8095
+ mutability: "read",
8096
+ requires_approval: false
8097
+ });
8098
+ }
8099
+ if (tied > 0 || synthetic > 0) {
8100
+ out.push(`Message order may not be chronological: ${tied} of ${total} messages share a timestamp with another message ` + `(ordered by id, not time)${synthetic > 0 ? ` and ${synthetic} carry a synthetic pre-2022 timestamp` : ""}.`);
8101
+ }
8102
+ if (returned.some(isTruncated)) {
8103
+ out.push(`Message text, tool-call inputs and tool results longer than ${CONTEXT_MESSAGE_TRUNCATE_CHARS} characters are truncated by the context endpoint. ` + "For full raw content (prompts, tool arguments/results) use `moda audit <conversation_id> --include-raw`.");
8104
+ }
8105
+ return {
8106
+ data: stripLoneSurrogates({
8107
+ conversation_id: conversationId,
8108
+ total_messages: total,
8109
+ from: start,
8110
+ returned: returned.length,
8111
+ has_more: next !== null,
8112
+ next_from: next,
8113
+ summary,
8114
+ ...ordering ? { ordering } : {},
8115
+ messages: returned
8116
+ }),
8117
+ warnings: out,
8118
+ nextCommands
8119
+ };
8120
+ };
8121
+ const full = build(messages.length);
8122
+ if (!opts.fits || messages.length <= 1 || opts.fits(full))
8123
+ return full;
8124
+ let low = 1;
8125
+ let high = messages.length - 1;
8126
+ while (low < high) {
8127
+ const mid = Math.ceil((low + high) / 2);
8128
+ if (opts.fits(build(mid)))
8129
+ low = mid;
8130
+ else
8131
+ high = mid - 1;
8132
+ }
8133
+ return build(low);
8134
+ }
6198
8135
  async function runCommand(command, positional, flags, positionals = positional ? [positional] : [], context = createCommandContext({ command, positional, positionals, flags })) {
8136
+ assertHandParsedFlags(command, flags, positionals);
6199
8137
  switch (command) {
6200
8138
  case "prompts": {
6201
8139
  await runPromptsCommand(context);
@@ -6203,7 +8141,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
6203
8141
  }
6204
8142
  case "overview": {
6205
8143
  const args = flagsToArgs(flags);
6206
- OverviewSchema.parse(args);
8144
+ parseCommandFlags(OverviewSchema, args, command, { extraFlags: OVERVIEW_PRODUCTION_FLAGS });
6207
8145
  return (await runProductionCommand(context)).exitCode;
6208
8146
  }
6209
8147
  case "investigate":
@@ -6213,7 +8151,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
6213
8151
  }
6214
8152
  case "clusters": {
6215
8153
  const args = flagsToArgs(flags);
6216
- const params = ClustersSchema.parse(args);
8154
+ const params = parseCommandFlags(ClustersSchema, args, command);
6217
8155
  if (params.search !== undefined || params.node_id !== undefined) {
6218
8156
  const query2 = new URLSearchParams;
6219
8157
  if (params.search !== undefined)
@@ -6247,7 +8185,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
6247
8185
  throw new CliInputError("<node_id> is required", "Usage: moda cluster-traces <node_id> [--limit=N] [--offset=N]");
6248
8186
  }
6249
8187
  const args = flagsToArgs(flags, "node_id", positional);
6250
- const params = ClusterConversationsSchema.parse(args);
8188
+ const params = parseCommandFlags(ClusterConversationsSchema, args, command, { positionalKey: "node_id" });
6251
8189
  const query = new URLSearchParams;
6252
8190
  if (params.limit)
6253
8191
  query.set("limit", params.limit.toString());
@@ -6262,8 +8200,8 @@ async function runCommand(command, positional, flags, positionals = positional ?
6262
8200
  }
6263
8201
  case "traces":
6264
8202
  case "conversations": {
6265
- const args = flagsToArgs(flags);
6266
- const params = ConversationsSchema.parse(args);
8203
+ const args = withStringIdFlags(flagsToArgs(flags), flags);
8204
+ const params = parseCommandFlags(ConversationsSchema, args, command);
6267
8205
  const query = new URLSearchParams;
6268
8206
  if (params.search)
6269
8207
  query.set("search", params.search);
@@ -6279,20 +8217,40 @@ async function runCommand(command, positional, flags, positionals = positional ?
6279
8217
  query.set("world_state", params.world_state);
6280
8218
  if (params.outcome && params.outcome !== "any")
6281
8219
  query.set("outcome", params.outcome);
8220
+ if (params.sort)
8221
+ query.set("sort_by", params.sort);
6282
8222
  if (params.limit)
6283
8223
  query.set("limit", params.limit.toString());
6284
8224
  if (params.offset !== undefined)
6285
8225
  query.set("offset", params.offset.toString());
6286
8226
  const queryString = query.toString() ? `?${query.toString()}` : "";
6287
- const data = await callDataAPI(`/conversations${queryString}`);
6288
- recordConversationsTelemetry(context, data);
8227
+ const raw = await callDataAPI(`/conversations${queryString}`);
8228
+ recordConversationsTelemetry(context, raw);
8229
+ const searchWarnings = [];
8230
+ let data = raw;
8231
+ const rawRecord = asRecord(raw);
8232
+ if (params.search && rawRecord) {
8233
+ const traceMode = asString(rawRecord.search_mode) ?? "keyword";
8234
+ const literal = traceMode === "keyword";
8235
+ data = { ...rawRecord, total_kind: literal ? "matches" : "candidate_pool" };
8236
+ if (!literal) {
8237
+ searchWarnings.push(`--search ran ${traceMode}-ranked: results are related traces that may not contain "${params.search}", and pagination.total is the candidate pool, not a match count.`);
8238
+ }
8239
+ }
8240
+ if (params.sort === "slowest") {
8241
+ const rows = asRecord(data)?.conversations;
8242
+ if (Array.isArray(rows) && rows.length > 0 && rows.every((row) => asRecord(row)?.duration_ms == null)) {
8243
+ searchWarnings.push("--sort=slowest: no trace in this page has a known duration (synthetic timestamps), so the order is not by duration. Use --sort=longest (message count) instead.");
8244
+ }
8245
+ }
8246
+ const writeOptions = searchWarnings.length > 0 ? { warnings: searchWarnings } : undefined;
6289
8247
  if (params.include_world_state === true) {
6290
8248
  const enriched = asRecord(data) ?? {};
6291
8249
  const list = Array.isArray(enriched.conversations) ? enriched.conversations : [];
6292
8250
  const withState = await attachWorldState(list);
6293
- context.output.writeData({ ...enriched, conversations: withState });
8251
+ context.output.writeData({ ...enriched, conversations: withState }, writeOptions);
6294
8252
  } else {
6295
- context.output.writeData(data);
8253
+ context.output.writeData(data, writeOptions);
6296
8254
  }
6297
8255
  break;
6298
8256
  }
@@ -6300,8 +8258,8 @@ async function runCommand(command, positional, flags, positionals = positional ?
6300
8258
  if (!positional || positional.trim() === "") {
6301
8259
  throw new CliInputError("<query> is required", 'Usage: moda search "<query>" [--mode=keyword|semantic|hybrid] [--user-id=ID] [--time-range=RANGE] [--include-tool-io] [--limit=N]');
6302
8260
  }
6303
- const args = flagsToArgs(flags, "q", positional);
6304
- const params = SearchSchema.parse(args);
8261
+ const args = withStringIdFlags(flagsToArgs(flags, "q", positional), flags);
8262
+ const params = parseCommandFlags(SearchSchema, args, command, { positionalKey: "q" });
6305
8263
  const query = new URLSearchParams;
6306
8264
  query.set("q", params.q);
6307
8265
  if (params.mode)
@@ -6325,6 +8283,30 @@ async function runCommand(command, positional, flags, positionals = positional ?
6325
8283
  } else if (params.mode && actualMode && actualMode !== params.mode && params.mode !== "hybrid") {
6326
8284
  warnings.push(`Search ran in ${actualMode} mode instead of the requested ${params.mode} mode.`);
6327
8285
  }
8286
+ const results = Array.isArray(searchRecord.results) ? searchRecord.results : [];
8287
+ if (results.length > 0 && (params.offset ?? 0) > 0) {
8288
+ warnings.push(`--offset=${params.offset} was not applied: message-level search is not pageable (it returns the top --limit hits). ` + "--offset only pages the trace-level fallback. Narrow with --time-range/--user-id or raise --limit instead.");
8289
+ }
8290
+ if (results.length === 0) {
8291
+ const fallback = await searchFallbackToTraces({ ...params, offset: params.offset });
8292
+ if (fallback?.kind === "notice") {
8293
+ warnings.push(fallback.warning);
8294
+ context.output.writeData(data, { warnings, nextCommands: fallback.nextCommands });
8295
+ break;
8296
+ }
8297
+ if (fallback?.kind === "results") {
8298
+ warnings.push(fallback.warning);
8299
+ const fallbackResults = fallback.data.results;
8300
+ const merged = {
8301
+ ...searchRecord,
8302
+ ...fallback.data,
8303
+ pagination: { ...asRecord(searchRecord.pagination) ?? {}, returned: fallbackResults.length, has_more: fallback.hasMore }
8304
+ };
8305
+ recordSearchTelemetry(context, merged);
8306
+ context.output.writeData(merged, { warnings, nextCommands: fallback.nextCommands });
8307
+ break;
8308
+ }
8309
+ }
6328
8310
  context.output.writeData(data, warnings.length > 0 ? { warnings } : undefined);
6329
8311
  break;
6330
8312
  }
@@ -6333,7 +8315,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
6333
8315
  throw new CliInputError("<conversation_id> is required", "Usage: moda world-state <conversation_id> [--summary-only] [--event-limit=N]");
6334
8316
  }
6335
8317
  const args = flagsToArgs(flags, "conversation_id", positional);
6336
- const params = WorldStateSchema.parse(args);
8318
+ const params = parseCommandFlags(WorldStateSchema, args, command, { positionalKey: "conversation_id" });
6337
8319
  if (params.snapshot === true && params.replay === true) {
6338
8320
  throw new CliInputError("Choose either --snapshot or --replay, not both.", "Usage: moda world-state <conversation_id> [--snapshot --msg-index=N [--block-index=M] [--segment-index=K]] | [--replay --message-count=N [--format=frames|fold-inputs]]");
6339
8321
  }
@@ -6382,7 +8364,26 @@ async function runCommand(command, positional, flags, positionals = positional ?
6382
8364
  throw new CliInputError("<conversation_id> is required", "Usage: moda context <conversation_id> [--msg-index=N] [--window=N]");
6383
8365
  }
6384
8366
  const args = flagsToArgs(flags, "conversation_id", positional);
6385
- const params = ContextSchema.parse(args);
8367
+ const params = parseCommandFlags(ContextSchema, args, command, { positionalKey: "conversation_id" });
8368
+ if (params.all === true) {
8369
+ if (params.msg_index !== undefined || flags.window !== undefined) {
8370
+ throw new CliInputError("--all reads the whole trace; drop --msg-index/--window.", "Usage: moda context <conversation_id> --all [--from=N] [--max-messages=N]");
8371
+ }
8372
+ const agentMode = context.output.mode === "agent" || context.output.mode === "agent-stream";
8373
+ const full = await readFullTranscript(params.conversation_id, {
8374
+ from: params.from,
8375
+ maxMessages: params.max_messages,
8376
+ fits: agentMode ? (page) => agentEnvelopeFits(command, page.data, { warnings: page.warnings, nextCommands: page.nextCommands }) : undefined
8377
+ });
8378
+ context.output.writeData(full.data, {
8379
+ ...full.warnings.length > 0 ? { warnings: full.warnings } : {},
8380
+ ...full.nextCommands.length > 0 ? { nextCommands: full.nextCommands } : {}
8381
+ });
8382
+ break;
8383
+ }
8384
+ if (params.from !== undefined || params.max_messages !== undefined) {
8385
+ throw new CliInputError("--from and --max-messages page the whole trace and need --all.", "Usage: moda context <conversation_id> --all [--from=N] [--max-messages=N]");
8386
+ }
6386
8387
  const query = new URLSearchParams;
6387
8388
  if (params.msg_index !== undefined)
6388
8389
  query.set("msg_index", params.msg_index.toString());
@@ -6401,7 +8402,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
6401
8402
  throw new CliInputError("<conversation_id|trace_id> is required", "Usage: moda audit <conversation_id|trace_id> [--kind=auto|trace|conversation] [--include-raw] [--limit=N]");
6402
8403
  }
6403
8404
  const args = flagsToArgs(flags, "id", positional);
6404
- const params = AuditSchema.parse(args);
8405
+ const params = parseCommandFlags(AuditSchema, args, command, { positionalKey: "id" });
6405
8406
  const query = new URLSearchParams;
6406
8407
  if (params.kind && params.kind !== "auto")
6407
8408
  query.set("kind", params.kind);
@@ -6411,12 +8412,37 @@ async function runCommand(command, positional, flags, positionals = positional ?
6411
8412
  query.set("limit", params.limit.toString());
6412
8413
  const queryString = query.toString() ? `?${query.toString()}` : "";
6413
8414
  const data = await callDataAPI(`/audit/${encodeURIComponent(params.id)}${queryString}`);
6414
- context.output.writeData(data);
8415
+ const capped = auditCapStatus(data, params.limit ?? AUDIT_DEFAULT_LIMIT);
8416
+ const auditRecord = asRecord(data);
8417
+ if (!capped || !auditRecord) {
8418
+ context.output.writeData(auditRecord ? { ...auditRecord, truncated: false, has_more: false } : data);
8419
+ break;
8420
+ }
8421
+ const limit = params.limit ?? AUDIT_DEFAULT_LIMIT;
8422
+ const perTrace = "audit one OTLP trace at a time with `moda audit <trace_id> --kind=trace` (ids in summary.trace_ids)";
8423
+ const warnings = [];
8424
+ const nextCommands = [];
8425
+ if (capped.rowsCapped) {
8426
+ const bigger = Math.min(AUDIT_MAX_LIMIT, limit * 5);
8427
+ warnings.push(`Spans were capped: read ${capped.rows} ingest row(s), the --limit=${limit} maximum, so this trace ${capped.hasMoreKnown ? "has" : "may have"} more spans than the ${capped.spans} shown. ` + (limit < AUDIT_MAX_LIMIT ? `Rerun with --limit=${bigger} (max ${AUDIT_MAX_LIMIT}), or ${perTrace}.` : `${perTrace[0].toUpperCase()}${perTrace.slice(1)}.`));
8428
+ if (limit < AUDIT_MAX_LIMIT) {
8429
+ const rerun = [
8430
+ `moda ${command} ${shellArg(params.id)}`,
8431
+ params.kind && params.kind !== "auto" ? `--kind=${params.kind}` : "",
8432
+ params.include_raw === true ? "--include-raw" : "",
8433
+ `--limit=${bigger}`
8434
+ ].filter(Boolean).join(" ");
8435
+ nextCommands.push({ command: rerun, purpose: "Read more ingest rows of this trace.", mutability: "read", requires_approval: false });
8436
+ }
8437
+ } else {
8438
+ warnings.push(`Spans were capped: the server returned ${capped.spans} span(s) and marked the response truncated. ${perTrace[0].toUpperCase()}${perTrace.slice(1)}.`);
8439
+ }
8440
+ context.output.writeData({ ...auditRecord, truncated: true, has_more: capped.rowsCapped }, { warnings, ...nextCommands.length > 0 ? { nextCommands } : {} });
6415
8441
  break;
6416
8442
  }
6417
8443
  case "frustrations": {
6418
- const args = flagsToArgs(flags);
6419
- const params = FrustrationsSchema.parse(args);
8444
+ const args = withStringIdFlags(flagsToArgs(flags), flags);
8445
+ const params = parseCommandFlags(FrustrationsSchema, args, command);
6420
8446
  const query = new URLSearchParams;
6421
8447
  if (params.days_back)
6422
8448
  query.set("days_back", params.days_back.toString());
@@ -6424,14 +8450,18 @@ async function runCommand(command, positional, flags, positionals = positional ?
6424
8450
  query.set("limit", params.limit.toString());
6425
8451
  if (params.offset !== undefined)
6426
8452
  query.set("offset", params.offset.toString());
6427
- const queryString = query.toString() ? `?${query.toString()}` : "";
6428
- const data = await callDataAPI(`/frustrations${queryString}`);
6429
- await printWithOptionalWindows(withFrustrationAnchors(data), "frustrations", params.include_window === true, params.window ?? 1, context.output);
8453
+ if (params.user_id)
8454
+ query.set("user_id", params.user_id);
8455
+ if (params.conversation_id)
8456
+ query.set("conversation_id", params.conversation_id);
8457
+ query.set("family", "frustration");
8458
+ const data = frustrationsFromEmotions(await callDataAPI(`/emotions?${query.toString()}`));
8459
+ await printWithOptionalWindows(withFrustrationAnchors(data), "frustrations", params.include_window === true, params.window ?? 1, context.output, processingDisabledWarning(data, "frustration"));
6430
8460
  break;
6431
8461
  }
6432
8462
  case "tool-failures": {
6433
8463
  const args = flagsToArgs(flags);
6434
- const params = ToolFailuresSchema.parse(args);
8464
+ const params = parseCommandFlags(ToolFailuresSchema, args, command);
6435
8465
  const queryString = params.days_back ? `?days_back=${params.days_back}` : "";
6436
8466
  const data = await callDataAPI(`/tool-failures${queryString}`);
6437
8467
  context.output.writeData(data);
@@ -6439,17 +8469,27 @@ async function runCommand(command, positional, flags, positionals = positional ?
6439
8469
  }
6440
8470
  case "tool-failure-detail": {
6441
8471
  if (!positional) {
6442
- throw new CliInputError("<tool_name> is required", "Usage: moda tool-failure-detail <tool_name> [--subtype=S] [--days-back=N] [--limit=N] [--offset=N]");
8472
+ throw new CliInputError("<tool_name> is required", "Usage: moda tool-failure-detail <tool_name> [--subtype=S] [--days-back=N] [--limit=N (1-20, default 5)] [--offset=N] [--compact | --include-window [--window=N]]");
8473
+ }
8474
+ const { "no-include-window": noIncludeWindow, ...toolFlags } = flags;
8475
+ if (noIncludeWindow !== undefined && noIncludeWindow !== "false") {
8476
+ if (toolFlags["include-window"] !== undefined && toolFlags["include-window"] !== "false") {
8477
+ throw new CliInputError("Choose either --include-window or --no-include-window, not both.");
8478
+ }
8479
+ toolFlags["include-window"] = "false";
8480
+ }
8481
+ const args = flagsToArgs(toolFlags, "tool_name", positional);
8482
+ const params = parseCommandFlags(ToolFailureDetailSchema, args, command, { positionalKey: "tool_name" });
8483
+ const dropConversation = params.compact === true || params.include_window === false;
8484
+ if (params.compact === true && params.include_window === true) {
8485
+ throw new CliInputError("--compact drops the conversation window; drop --include-window, or drop --compact.");
6443
8486
  }
6444
- const args = flagsToArgs(flags, "tool_name", positional);
6445
- const params = ToolFailureDetailSchema.parse(args);
6446
8487
  const query = new URLSearchParams;
6447
8488
  if (params.subtype)
6448
8489
  query.set("subtype", params.subtype);
6449
8490
  if (params.days_back)
6450
8491
  query.set("days_back", params.days_back.toString());
6451
- if (params.limit)
6452
- query.set("limit", params.limit.toString());
8492
+ query.set("limit", (params.limit ?? TOOL_FAILURE_DETAIL_DEFAULT_LIMIT).toString());
6453
8493
  if (params.offset !== undefined)
6454
8494
  query.set("offset", params.offset.toString());
6455
8495
  const queryString = query.toString() ? `?${query.toString()}` : "";
@@ -6460,7 +8500,28 @@ async function runCommand(command, positional, flags, positionals = positional ?
6460
8500
  const warnings = subtypeCount === 0 && exampleCount === 0 ? [
6461
8501
  `No failures found for tool "${params.tool_name}" in the last ${params.days_back ?? 7} day(s) — the tool may have no failures in this window, or the name may not exist in this tenant. Run \`moda tool-failures\` to list tools with recorded failures, or widen --days-back.`
6462
8502
  ] : [];
6463
- await printWithOptionalWindows(withToolFailureAnchors(data), "examples", params.include_window === true, params.window ?? 1, context.output, warnings);
8503
+ const knownSubtypes = Array.isArray(tfRecord.subtypes) ? tfRecord.subtypes.map((row) => asString(asRecord(row)?.subtype)).filter((value) => Boolean(value)) : [];
8504
+ if (params.subtype && knownSubtypes.length > 0 && !knownSubtypes.includes(params.subtype)) {
8505
+ warnings.push(`--subtype=${params.subtype} is not a failure subtype of "${params.tool_name}" in this window, so no examples matched. ` + `Valid subtypes: ${knownSubtypes.slice(0, 10).join(", ")}${knownSubtypes.length > 10 ? `, … (${knownSubtypes.length - 10} more in \`subtypes\`)` : ""}.`);
8506
+ }
8507
+ await printWithOptionalWindows(withToolFailureAnchors(data), "examples", params.include_window === true, params.window ?? 1, context.output, warnings, (payload, out) => {
8508
+ let trimmed = trimToolFailureConversations(payload, {
8509
+ drop: dropConversation || params.include_window === true,
8510
+ windowRadius: params.window ?? 1
8511
+ });
8512
+ const agentMode = context.output.mode === "agent" || context.output.mode === "agent-stream";
8513
+ const subtypes = Array.isArray(trimmed.subtypes) ? trimmed.subtypes : [];
8514
+ if (agentMode && subtypes.length > TOOL_FAILURE_AGENT_SUBTYPES) {
8515
+ const ranked = [...subtypes].sort((a, b) => (asNumber(asRecord(b)?.count) ?? 0) - (asNumber(asRecord(a)?.count) ?? 0));
8516
+ trimmed = { ...trimmed, subtypes: ranked.slice(0, TOOL_FAILURE_AGENT_SUBTYPES), subtypes_total: subtypes.length };
8517
+ out.push(`Showing the ${TOOL_FAILURE_AGENT_SUBTYPES} most frequent of ${subtypes.length} failure subtypes; --json lists all of them.`);
8518
+ }
8519
+ const { data: compacted, truncated } = compactToolFailureExamples(trimmed);
8520
+ if (truncated > 0) {
8521
+ out.push(`${truncated} long text field(s) in examples were cut to ${TOOL_FAILURE_TEXT_LIMIT} characters to keep this output small. For the full text, run each example's \`context_command\` (\`moda context <conversation_id> --msg-index=<msg_index>\`).`);
8522
+ }
8523
+ return compacted;
8524
+ });
6464
8525
  break;
6465
8526
  }
6466
8527
  case "problem-impact": {
@@ -6489,12 +8550,20 @@ async function runCommand(command, positional, flags, positionals = positional ?
6489
8550
  link.set("useCase", params.node_id);
6490
8551
  const problem = asString(data.problemId) || params.problem_id;
6491
8552
  const path = problem ? `/dashboard/problems/${encodeURIComponent(problem)}` : params.node_id ? `/dashboard/use-cases/${encodeURIComponent(params.node_id)}` : "/dashboard/use-cases";
6492
- context.output.writeData({ ...data, dashboard_url: `${resolveModaBaseUrl()}${path}?${link}` });
8553
+ const impactWarnings = serverWarnings(data);
8554
+ context.output.writeData({ ...data, dashboard_url: `${resolveModaBaseUrl()}${path}?${link}` }, impactWarnings.length > 0 ? { warnings: impactWarnings } : undefined);
6493
8555
  break;
6494
8556
  }
6495
8557
  case "problems": {
8558
+ if (positional === "remainder") {
8559
+ await runProblemsRemainder(flags, context);
8560
+ break;
8561
+ }
8562
+ if (positional) {
8563
+ throw new CliInputError(`Unknown problems subcommand '${positional}'.`, "Usage: moda problems [--days-back=N] [--limit=N] | moda problems remainder [--family=F] [--days-back=N] [--limit=N] [--cursor=TOKEN]. For one Problem use 'moda problem <id>'.");
8564
+ }
6496
8565
  const args = flagsToArgs(flags);
6497
- const params = ProblemsSchema.parse(args);
8566
+ const params = parseCommandFlags(ProblemsSchema, args, command);
6498
8567
  const query = new URLSearchParams;
6499
8568
  if (params.days_back)
6500
8569
  query.set("days_back", params.days_back.toString());
@@ -6502,19 +8571,26 @@ async function runCommand(command, positional, flags, positionals = positional ?
6502
8571
  query.set("limit", params.limit.toString());
6503
8572
  const queryString = query.toString() ? `?${query.toString()}` : "";
6504
8573
  const data = await callDataAPI(`/problems${queryString}`);
8574
+ const { payload, truncated } = capProblemsBlocks(asRecord(data) ?? {}, params.limit ?? 25);
6505
8575
  const withLink = {
6506
- ...asRecord(data) ?? {},
8576
+ ...payload,
8577
+ ...truncated ? { truncated } : {},
6507
8578
  dashboard_url: `${MODA_BASE_URL}/dashboard/problems`
6508
8579
  };
6509
- context.output.writeData(withLink);
8580
+ const warnings = [...serverWarnings(payload), ...truncated ? [`--limit=${params.limit ?? 25} also capped ${Object.entries(truncated).map(([block, count]) => `${block} (${count} more)`).join(", ")}; raise --limit (max 25) or open dashboard_url for the rest.`] : []];
8581
+ context.output.writeData(withLink, warnings.length > 0 ? { warnings } : undefined);
6510
8582
  break;
6511
8583
  }
6512
8584
  case "problem": {
8585
+ if (positional === "remainder") {
8586
+ await runProblemsRemainder(flags, context);
8587
+ break;
8588
+ }
6513
8589
  if (!positional) {
6514
8590
  throw new CliInputError("<problem_id> is required", "Usage: moda problem <problem_id> [--evidence|--reports|--traces|--feedback] [--limit=N] [--cursor=TOKEN] [--family=F] [--door=D]");
6515
8591
  }
6516
8592
  const args = flagsToArgs(flags, "id", positional);
6517
- const params = ProblemSchema.parse(args);
8593
+ const params = parseCommandFlags(ProblemSchema, args, command, { positionalKey: "id" });
6518
8594
  const traceView = params.traces === true || params.conversations === true;
6519
8595
  const views = ["evidence", "reports", "conversations", "feedback"].filter((view2) => view2 === "conversations" ? traceView : params[view2] === true);
6520
8596
  if (views.length > 1) {
@@ -6662,6 +8738,9 @@ async function runCommand(command, positional, flags, positionals = positional ?
6662
8738
  break;
6663
8739
  }
6664
8740
  case "signals": {
8741
+ if (positional && SIGNALS_MANAGEMENT_SUBCOMMANDS.has(positional)) {
8742
+ return (await runSignalsManagementCommand(positional, context)).exitCode;
8743
+ }
6665
8744
  const data = await callDataAPI("/custom-signals");
6666
8745
  context.output.writeData(data);
6667
8746
  break;
@@ -6672,7 +8751,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
6672
8751
  }
6673
8752
  const args = flagsToArgs(flags, "id", positional);
6674
8753
  args.id = positional;
6675
- const params = SignalSchema.parse(args);
8754
+ const params = parseCommandFlags(SignalSchema, args, command, { positionalKey: "id" });
6676
8755
  const id = encodeURIComponent(params.id);
6677
8756
  if (params.detections !== true) {
6678
8757
  const data2 = await callDataAPI(`/custom-signals/${id}`);
@@ -6861,8 +8940,8 @@ async function runCommand(command, positional, flags, positionals = positional ?
6861
8940
  break;
6862
8941
  }
6863
8942
  case "emotions": {
6864
- const args = flagsToArgs(flags);
6865
- const params = EmotionsSchema.parse(args);
8943
+ const args = withStringIdFlags(flagsToArgs(flags), flags);
8944
+ const params = parseCommandFlags(EmotionsSchema, args, command);
6866
8945
  const query = new URLSearchParams;
6867
8946
  if (params.days_back)
6868
8947
  query.set("days_back", params.days_back.toString());
@@ -6872,9 +8951,14 @@ async function runCommand(command, positional, flags, positionals = positional ?
6872
8951
  query.set("offset", params.offset.toString());
6873
8952
  if (params.family)
6874
8953
  query.set("family", params.family);
8954
+ if (params.user_id)
8955
+ query.set("user_id", params.user_id);
8956
+ if (params.conversation_id)
8957
+ query.set("conversation_id", params.conversation_id);
6875
8958
  const queryString = query.toString() ? `?${query.toString()}` : "";
6876
8959
  const data = await callDataAPI(`/emotions${queryString}`);
6877
- context.output.writeData(data);
8960
+ const processingWarnings = processingDisabledWarning(data, "emotion");
8961
+ context.output.writeData(data, processingWarnings.length > 0 ? { warnings: processingWarnings } : undefined);
6878
8962
  break;
6879
8963
  }
6880
8964
  case "laziness": {
@@ -6882,7 +8966,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
6882
8966
  if (flags["conversation-id"] !== undefined) {
6883
8967
  args.conversation_id = flags["conversation-id"];
6884
8968
  }
6885
- const params = LazinessSchema.parse(args);
8969
+ const params = parseCommandFlags(LazinessSchema, args, command);
6886
8970
  const query = new URLSearchParams;
6887
8971
  if (params.days_back)
6888
8972
  query.set("days_back", params.days_back.toString());
@@ -6904,7 +8988,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
6904
8988
  if (flags["conversation-id"] !== undefined) {
6905
8989
  args.conversation_id = flags["conversation-id"];
6906
8990
  }
6907
- const params = HallucinationsSchema.parse(args);
8991
+ const params = parseCommandFlags(HallucinationsSchema, args, command);
6908
8992
  const query = new URLSearchParams;
6909
8993
  if (params.days_back)
6910
8994
  query.set("days_back", params.days_back.toString());
@@ -6938,7 +9022,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
6938
9022
  }
6939
9023
  case "tail": {
6940
9024
  const args = flagsToArgs(flags);
6941
- const params = TailSchema.parse(args);
9025
+ const params = parseCommandFlags(TailSchema, args, command);
6942
9026
  await runTailCommand(params, context);
6943
9027
  break;
6944
9028
  }
@@ -7084,7 +9168,7 @@ var commandRegistry = createCommandRegistry([
7084
9168
  resetApiRequestCountBeforeRun: true,
7085
9169
  telemetry: "result",
7086
9170
  handler: async (context) => {
7087
- const { runInit } = await import("./index-vmmfya6b.js");
9171
+ const { runInit } = await import("./index-qgc37efj.js");
7088
9172
  if (context.outputMode === "agent-stream") {
7089
9173
  context.output.writeEvent({
7090
9174
  event: "started",
@@ -7125,7 +9209,7 @@ var commandRegistry = createCommandRegistry([
7125
9209
  resetApiRequestCountBeforeRun: true,
7126
9210
  telemetry: "result-and-error",
7127
9211
  handler: async (context) => {
7128
- const { runProvision } = await import("./provision-spwn5t41.js");
9212
+ const { runProvision } = await import("./provision-gabtks29.js");
7129
9213
  return runProvision(context, {
7130
9214
  resumeCommand: buildResumeCommand("provision", context.rawFlags)
7131
9215
  });
@@ -7267,6 +9351,7 @@ var commandRegistry = createCommandRegistry([
7267
9351
  description: "Diagnose local Moda setup and data flow",
7268
9352
  examples: ["moda doctor", "moda doctor --json", "moda doctor --offline"],
7269
9353
  auth: "none",
9354
+ authNote: "Offline by default; --online checks the API key and data flow.",
7270
9355
  outputModes: ["human", "json", "agent"],
7271
9356
  mutability: "read",
7272
9357
  validateConfigBeforeRun: false,
@@ -7300,7 +9385,7 @@ var commandRegistry = createCommandRegistry([
7300
9385
  },
7301
9386
  {
7302
9387
  name: "keys",
7303
- description: "Manage local key descriptors and cloud key lifecycle requirements",
9388
+ description: "Local key descriptors in ~/.moda only (not your server-side API keys; manage those in the dashboard)",
7304
9389
  examples: ["moda keys list", "moda keys create --name=local-dev"],
7305
9390
  auth: "none",
7306
9391
  outputModes: ["human", "json", "agent"],
@@ -7322,7 +9407,9 @@ var commandRegistry = createCommandRegistry([
7322
9407
  "moda harness delete my-harness-key --yes",
7323
9408
  "moda harness explain --agent=agent_support"
7324
9409
  ],
7325
- auth: "none",
9410
+ subcommands: HARNESS_SUBCOMMAND_HELP,
9411
+ auth: "api-key",
9412
+ authNote: "sync, list, show, versions, diff, delete, pull, and analyze need the API key; scan, explain, agents, graph, approve, and validate-report are local.",
7326
9413
  outputModes: ["human", "json", "agent"],
7327
9414
  mutability: "write",
7328
9415
  validateConfigBeforeRun: false,
@@ -7332,10 +9419,9 @@ var commandRegistry = createCommandRegistry([
7332
9419
  },
7333
9420
  {
7334
9421
  name: "agent",
7335
- description: "Agent-native Moda context, investigations, next actions, and evidence",
9422
+ description: "Agent-native local Moda context, next actions, and evidence (production investigation: moda investigate)",
7336
9423
  examples: [
7337
9424
  "moda agent context",
7338
- "moda agent investigate",
7339
9425
  "moda agent next-action",
7340
9426
  "moda agent evidence doctor:api_key_resolves"
7341
9427
  ],
@@ -7355,7 +9441,7 @@ var commandRegistry = createCommandRegistry([
7355
9441
  "moda sync harness --from-report .moda/harness-report.json",
7356
9442
  "moda sync prompts"
7357
9443
  ],
7358
- auth: "none",
9444
+ auth: "api-key",
7359
9445
  outputModes: ["human", "json", "agent"],
7360
9446
  mutability: "write",
7361
9447
  validateConfigBeforeRun: false,
@@ -7363,6 +9449,92 @@ var commandRegistry = createCommandRegistry([
7363
9449
  telemetry: "none",
7364
9450
  handler: runSyncCommand
7365
9451
  },
9452
+ {
9453
+ name: "users",
9454
+ description: "List end users with health, frustration and activity (the dashboard Users page)",
9455
+ examples: [
9456
+ "moda users --window=30d --sort=health_score --order=asc",
9457
+ "moda users --cohort=frustrated --min-conversations=3"
9458
+ ],
9459
+ auth: "api-key",
9460
+ outputModes: ["human", "json", "agent"],
9461
+ mutability: "read",
9462
+ validateConfigBeforeRun: false,
9463
+ resetApiRequestCountBeforeRun: false,
9464
+ telemetry: "result-and-error",
9465
+ handler: runUsersCommand
9466
+ },
9467
+ {
9468
+ name: "user",
9469
+ description: "One end user's profile: health, traits, frustration trajectory, activity",
9470
+ examples: ["moda user u-123 --window=30d"],
9471
+ auth: "api-key",
9472
+ outputModes: ["human", "json", "agent"],
9473
+ mutability: "read",
9474
+ validateConfigBeforeRun: false,
9475
+ resetApiRequestCountBeforeRun: false,
9476
+ telemetry: "result-and-error",
9477
+ handler: runUserCommand
9478
+ },
9479
+ {
9480
+ name: "problem-reopen",
9481
+ description: "Reopen a problem that was marked fixed (resolved problems return only via re-detection)",
9482
+ examples: ['moda problem-reopen <problem_id> --reason="still happening"', "moda problem-reopen <problem_id> --dry-run"],
9483
+ auth: "api-key",
9484
+ outputModes: ["human", "json", "agent"],
9485
+ mutability: "write",
9486
+ validateConfigBeforeRun: false,
9487
+ resetApiRequestCountBeforeRun: false,
9488
+ telemetry: "result-and-error",
9489
+ handler: runProblemReopenCommand
9490
+ },
9491
+ {
9492
+ name: "registry",
9493
+ description: "Push prompts, skills, and tools to Moda from one agent-written JSON manifest (no repo scan)",
9494
+ examples: [
9495
+ "moda registry template --out=.moda/registry.json",
9496
+ "moda registry validate",
9497
+ "moda registry push --dry-run",
9498
+ "moda registry push --file=.moda/registry.json"
9499
+ ],
9500
+ subcommands: [
9501
+ {
9502
+ name: "template",
9503
+ description: "Print (or write with --out) an example moda_registry.v1 manifest",
9504
+ usage: "moda registry template [--out=<path>] [--yes]",
9505
+ flags: [
9506
+ { name: "--out=<path>", description: "Write the template to a file instead of stdout (refuses to overwrite without --yes)." }
9507
+ ],
9508
+ examples: ["moda registry template --out=.moda/registry.json"]
9509
+ },
9510
+ {
9511
+ name: "validate",
9512
+ description: "Validate a manifest locally without contacting Moda",
9513
+ usage: "moda registry validate [--file=<path>]",
9514
+ flags: [{ name: "--file=<path>", description: "Manifest path (default .moda/registry.json)." }],
9515
+ examples: ["moda registry validate --file=.moda/registry.json"]
9516
+ },
9517
+ {
9518
+ name: "push",
9519
+ description: "Upload the manifest's prompts, skills, and tools to the API key's tenant",
9520
+ usage: "moda registry push [--file=<path>] [--dry-run] [--live]",
9521
+ flags: [
9522
+ { name: "--file=<path>", description: "Manifest path (default .moda/registry.json)." },
9523
+ { name: "--dry-run", description: "Server-side preview; nothing is written. Without an API key, validates locally only." },
9524
+ { name: "--live", description: "Mark every pushed skill live (included in the served skill surface)." }
9525
+ ],
9526
+ examples: ["moda registry push --dry-run", "moda registry push --live"]
9527
+ }
9528
+ ],
9529
+ auth: "api-key",
9530
+ authNote: "template and validate are local; push needs the API key (push --dry-run without a key validates locally only).",
9531
+ outputModes: ["human", "json", "agent"],
9532
+ mutability: "write",
9533
+ validateConfigBeforeRun: false,
9534
+ resetApiRequestCountBeforeRun: false,
9535
+ telemetry: "none",
9536
+ handler: runRegistryCommand
9537
+ },
7366
9538
  legacyCommand({
7367
9539
  name: "overview",
7368
9540
  description: "Harness health briefing from production data",
@@ -7371,25 +9543,32 @@ var commandRegistry = createCommandRegistry([
7371
9543
  }),
7372
9544
  legacyCommand({
7373
9545
  name: "investigate",
7374
- description: "Rank production issues with evidence and next commands",
9546
+ description: "Rank production issues (tool failures, frustrations, top Problems) with evidence and next commands. --trace scopes only the frustration evidence.",
7375
9547
  examples: ["moda investigate", "moda investigate --tool lookupCustomer"],
7376
9548
  ...dataApiDefaults
7377
9549
  }),
7378
9550
  legacyCommand({
7379
9551
  name: "failures",
7380
- description: "Show production failures worth fixing first",
9552
+ description: "Tool failures only, ranked with evidence (no frustrations or Problems; use investigate or problems for those)",
7381
9553
  examples: ["moda failures", "moda failures --tool lookupCustomer"],
7382
9554
  ...dataApiDefaults
7383
9555
  }),
7384
9556
  legacyCommand({
7385
9557
  name: "ask",
7386
- description: "Ask Moda a production intelligence question",
9558
+ description: "Ask Moda a production intelligence question (one paid agent run per question)",
7387
9559
  examples: [
7388
9560
  'moda ask "what should I fix first?"',
7389
9561
  'moda ask "what should I fix first?" --no-stream',
9562
+ 'moda ask "what changed this week?" --days-back=7',
7390
9563
  'moda --agent ask "why did this run fail?"'
7391
9564
  ],
7392
- ...dataApiDefaults
9565
+ helpNotes: [
9566
+ "Cost: each question starts one agent run on Moda (LLM calls, up to about 200k tokens). Re-asking the same question pays again.",
9567
+ "Streaming: in a terminal, ask streams live progress. If the stream fails before the server accepts the run, ask retries once on the buffered endpoint. If it drops after the server accepted the run, ask does NOT re-run the question (that would pay for a second run): it fails, and the run may still finish server-side. Retry later with --no-stream.",
9568
+ "Flags: --days-back=N (window for the evidence), --no-stream (buffered request, no live progress; --json and --agent never stream)."
9569
+ ],
9570
+ ...dataApiDefaults,
9571
+ mutability: "write"
7393
9572
  }),
7394
9573
  legacyCommand({
7395
9574
  name: "clusters",
@@ -7409,7 +9588,7 @@ var commandRegistry = createCommandRegistry([
7409
9588
  name: "traces",
7410
9589
  aliases: ["conversations"],
7411
9590
  telemetryCommand: "conversations",
7412
- description: "Search and filter traces (agent runs)",
9591
+ description: "Search and filter traces (agent runs); --search is ranked (hybrid when embeddings exist, else keyword), see total_kind",
7413
9592
  examples: [
7414
9593
  'moda traces --search="error" --limit=5',
7415
9594
  'moda traces --world-state="enterprise" --outcome=positive',
@@ -7437,8 +9616,12 @@ var commandRegistry = createCommandRegistry([
7437
9616
  }),
7438
9617
  legacyCommand({
7439
9618
  name: "context",
7440
- description: "Get windowed trace context (messages around one turn)",
7441
- examples: ["moda context <conversation_id> --window=3"],
9619
+ description: "Get windowed trace context (messages around one turn), or the whole trace with --all",
9620
+ examples: [
9621
+ "moda context <conversation_id> --window=3",
9622
+ "moda context <conversation_id> --all",
9623
+ "moda context <conversation_id> --all --from=500 --max-messages=500"
9624
+ ],
7442
9625
  ...dataApiDefaults
7443
9626
  }),
7444
9627
  legacyCommand({
@@ -7448,13 +9631,14 @@ var commandRegistry = createCommandRegistry([
7448
9631
  examples: [
7449
9632
  "moda audit <conversation_id|trace_id>",
7450
9633
  "moda audit <trace_id> --kind=trace --json",
7451
- "moda audit <conversation_id> --include-raw"
9634
+ "moda audit <conversation_id> --include-raw",
9635
+ "moda audit <conversation_id> --limit=1000 # ingest rows to read (default 200, max 1000); warns when capped"
7452
9636
  ],
7453
9637
  ...dataApiDefaults
7454
9638
  }),
7455
9639
  legacyCommand({
7456
9640
  name: "frustrations",
7457
- description: "Get user frustration detections",
9641
+ description: "User frustration detections: the frustration family of `emotions` (same data as the dashboard), legacy field names",
7458
9642
  examples: ["moda frustrations --days-back=14 --include-window"],
7459
9643
  ...dataApiDefaults
7460
9644
  }),
@@ -7466,14 +9650,19 @@ var commandRegistry = createCommandRegistry([
7466
9650
  }),
7467
9651
  legacyCommand({
7468
9652
  name: "tool-failure-detail",
7469
- description: "Get per-tool failure detail",
7470
- examples: ["moda tool-failure-detail search --include-window"],
9653
+ description: "Get per-tool failure detail (each example embeds its anchor ±2 messages, long text cut to 300 chars)",
9654
+ examples: [
9655
+ "moda tool-failure-detail search",
9656
+ "moda tool-failure-detail search --compact # no embedded conversation (same as --include-window=false)",
9657
+ "moda tool-failure-detail search --include-window --window=3"
9658
+ ],
7471
9659
  ...dataApiDefaults
7472
9660
  }),
7473
9661
  legacyCommand({
7474
9662
  name: "problems",
7475
9663
  description: "Rank cross-signal Problems by root cause (what to fix first)",
7476
- examples: ["moda problems", "moda problems --days-back=7 --limit=10"],
9664
+ examples: ["moda problems", "moda problems --days-back=7 --limit=10", "moda problems remainder --family=emotion"],
9665
+ subcommands: PROBLEMS_SUBCOMMAND_HELP,
7477
9666
  ...dataApiDefaults
7478
9667
  }),
7479
9668
  legacyCommand({
@@ -7580,9 +9769,16 @@ var commandRegistry = createCommandRegistry([
7580
9769
  }),
7581
9770
  legacyCommand({
7582
9771
  name: "signals",
7583
- description: "List custom signals with criteria, status, and 7-day judged/matched stats",
7584
- examples: ["moda signals"],
7585
- ...dataApiDefaults
9772
+ description: "List and manage custom signals (create, update, activate/pause/delete, examples, refine, proposals, search)",
9773
+ examples: [
9774
+ "moda signals",
9775
+ 'moda signals draft --note="agent promises a refund it cannot issue"',
9776
+ 'moda signals create --name="Refund promise" --description=… --pass-criteria=… --fail-criteria=… --dry-run',
9777
+ "moda signals activate <signal_id>"
9778
+ ],
9779
+ subcommands: SIGNALS_SUBCOMMAND_HELP,
9780
+ ...dataApiDefaults,
9781
+ mutability: "write"
7586
9782
  }),
7587
9783
  legacyCommand({
7588
9784
  name: "signal",
@@ -7611,7 +9807,7 @@ var commandRegistry = createCommandRegistry([
7611
9807
  }),
7612
9808
  legacyCommand({
7613
9809
  name: "emotions",
7614
- description: "Multi-family emotion detections (frustration, sadness, confusion, anxiety, trust, positive)",
9810
+ description: "Multi-family emotion detections (frustration, disapproval, sadness, confusion, anxiety, trust, positive)",
7615
9811
  examples: [
7616
9812
  "moda emotions --days-back=7",
7617
9813
  "moda emotions --family=confusion --limit=20"
@@ -7684,7 +9880,7 @@ var commandRegistry = createCommandRegistry([
7684
9880
  },
7685
9881
  {
7686
9882
  name: "diff",
7687
- description: "Alias of status: show local prompt changes",
9883
+ description: "Only the prompts that changed, are new, or were deleted since the last sync (hash-level; status lists all)",
7688
9884
  usage: "moda prompts diff",
7689
9885
  examples: ["moda prompts diff"]
7690
9886
  },
@@ -7716,11 +9912,15 @@ var commandRegistry = createCommandRegistry([
7716
9912
  {
7717
9913
  name: "ab",
7718
9914
  description: "Run a judged prompt A/B replay experiment",
7719
- usage: "moda prompts ab --baseline=<path> --candidate=<path> --traces=<ids>",
9915
+ usage: "moda prompts ab --baseline=<path> --candidate=<path> [--traces=<ids>|--set-id=<id>|--auto-generate] [--yes]",
7720
9916
  flags: [
7721
9917
  { name: "--baseline=<path>", description: "Prompt file to treat as the control." },
7722
9918
  { name: "--candidate=<path>", description: "Prompt file to treat as the variant." },
7723
- { name: "--traces=<ids>", description: "Comma-separated trace ids (conversation_id values) to replay. Legacy alias: --conversations=<ids>." }
9919
+ { name: "--traces=<ids>", description: "Comma-separated trace ids (conversation_id values) to replay, at most 100. Legacy alias: --conversations=<ids>." },
9920
+ { name: "--cases=<n>", description: "Cases to auto-generate (default 5, max 100)." },
9921
+ { name: "--seeds=<n>", description: "Repeats per case per arm (default 3, max 10)." },
9922
+ { name: "--model=<slug>", description: "Assistant model for both arms; must be in the replay allowlist unless --allow-any-model." },
9923
+ { name: "--yes", description: "Confirm a run above 200 planned playouts (cases x seeds x 2 arms), or one whose case count is unknown." }
7724
9924
  ],
7725
9925
  examples: [
7726
9926
  "moda prompts ab --baseline=prompts/agent.prompt.md --candidate=prompts/agent-v2.prompt.md --traces=conv_1,conv_2"
@@ -7728,14 +9928,23 @@ var commandRegistry = createCommandRegistry([
7728
9928
  },
7729
9929
  {
7730
9930
  name: "propose",
7731
- description: "Revise a prompt from a run's failures",
7732
- usage: "moda prompts propose [--gate] [--promote-on-win]",
9931
+ description: "Revise a prompt from a run's failures (one LLM call; --gate adds paid replays)",
9932
+ usage: "moda prompts propose --prompt-key=<key> --from-run=<run_id> --set-id=<replay_set_id> [--max-dossiers=N] [--model=<slug>] [--out=<path>] [--gate [--seeds=N] [--yes]] [--promote-on-win]",
7733
9933
  flags: [
7734
- { name: "--gate", description: "Automatically A/B the proposed revision." },
9934
+ { name: "--prompt-key=<key>", description: "Required. Registered prompt to revise." },
9935
+ { name: "--from-run=<run_id>", description: "Required. The `prompts ab` run whose failures drive the revision." },
9936
+ { name: "--set-id=<replay_set_id>", description: "Required. The replay set that run used; --gate replays all of it. A set that does not exist fails before anything is sent." },
9937
+ { name: "--max-dossiers=<n>", description: "Failure dossiers to revise from (default 8, max 16)." },
9938
+ { name: "--model=<slug>", description: "Revision model (replay allowlist unless --allow-any-model)." },
9939
+ { name: "--out=<path>", description: "Write the candidate prompt to this file." },
9940
+ { name: "--seeds=<n>", description: "Gate: repeats per case per arm (default 3, max 10)." },
9941
+ { name: "--gate", description: "Automatically A/B the proposed revision over the full --set-id (planned playouts = cases x seeds x 2; above 200 needs --yes)." },
9942
+ { name: "--yes", description: "Confirm a gate above 200 planned playouts, or one whose case count is unknown." },
7735
9943
  { name: "--promote-on-win", description: "Promote the candidate if it wins the gate." }
7736
9944
  ],
7737
- examples: ["moda prompts propose --gate --promote-on-win"]
7738
- }
9945
+ examples: ["moda prompts propose --prompt-key=support.triage --from-run=<run_id> --set-id=<set_id> --gate --promote-on-win"]
9946
+ },
9947
+ ...PROMPTS_REGISTRY_SUBCOMMAND_HELP
7739
9948
  ],
7740
9949
  auth: "api-key",
7741
9950
  outputModes: ["human", "json", "agent"],
@@ -7755,7 +9964,9 @@ var commandRegistry = createCommandRegistry([
7755
9964
  "moda skills install --list",
7756
9965
  "moda skills install integration-node-openai"
7757
9966
  ],
7758
- auth: "none",
9967
+ subcommands: SKILLS_SUBCOMMAND_HELP,
9968
+ auth: "api-key",
9969
+ authNote: "install is local; gen, status, pull, sync, list, show, policy, and proposals need the API key.",
7759
9970
  outputModes: ["human", "json", "agent"],
7760
9971
  mutability: "write",
7761
9972
  validateConfigBeforeRun: false,
@@ -7771,13 +9982,14 @@ var commandRegistry = createCommandRegistry([
7771
9982
  examples: [
7772
9983
  "moda fixes",
7773
9984
  "moda fixes --status=VERIFIED",
7774
- "moda fixes draft-batch --limit=10",
9985
+ "moda fixes draft-batch --limit=5",
7775
9986
  "moda fixes drive",
7776
9987
  "moda fixes drive --status=SCOPING"
7777
9988
  ],
7778
- auth: "none",
9989
+ auth: "api-key",
7779
9990
  outputModes: ["human", "json", "agent"],
7780
9991
  mutability: "write",
9992
+ subcommands: FIXES_SUBCOMMAND_HELP,
7781
9993
  validateConfigBeforeRun: false,
7782
9994
  resetApiRequestCountBeforeRun: false,
7783
9995
  telemetry: "result",
@@ -7788,6 +10000,7 @@ var commandRegistry = createCommandRegistry([
7788
10000
  {
7789
10001
  name: "fix",
7790
10002
  description: 'Draft, verify, check out, submit, and dismiss Fixes ("a fix with proof")',
10003
+ subcommands: FIX_SUBCOMMAND_HELP,
7791
10004
  examples: [
7792
10005
  "moda fix start <problem_id> --wait",
7793
10006
  "moda fix <fix_id> --packet",
@@ -7797,7 +10010,7 @@ var commandRegistry = createCommandRegistry([
7797
10010
  'moda fix mark-applied <fix_id> --note="tool description updated in our agent config"',
7798
10011
  'moda fix dismiss <fix_id> --reason="not actionable"'
7799
10012
  ],
7800
- auth: "none",
10013
+ auth: "api-key",
7801
10014
  outputModes: ["human", "json", "agent"],
7802
10015
  mutability: "write",
7803
10016
  validateConfigBeforeRun: false,
@@ -7876,6 +10089,17 @@ function printAgentCliError(command, error, mode = "agent") {
7876
10089
  output.writeError(error);
7877
10090
  }
7878
10091
  async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
10092
+ const previousProfileEnv = process.env.MODA_PROFILE;
10093
+ try {
10094
+ return await dispatchParsedCommandInner(parsed, startedAt);
10095
+ } finally {
10096
+ if (previousProfileEnv === undefined)
10097
+ delete process.env.MODA_PROFILE;
10098
+ else
10099
+ process.env.MODA_PROFILE = previousProfileEnv;
10100
+ }
10101
+ }
10102
+ async function dispatchParsedCommandInner(parsed, startedAt) {
7879
10103
  const { command, flags } = parsed;
7880
10104
  const commandFlags = stripUpdateCheckFlag(stripTelemetryFlag(flags));
7881
10105
  const telemetryDisabled = isTelemetryDisabled(flags);
@@ -7907,19 +10131,50 @@ async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
7907
10131
  printHelp();
7908
10132
  return 1;
7909
10133
  }
7910
- const definition = commandRegistry.find(command);
7911
- if (!definition) {
7912
- const error = new CliInputError(`Unknown command '${command}'`, 'Run "moda --help" for usage information.');
10134
+ const failEarly = (error) => {
7913
10135
  const mode = resolveOutputMode(commandFlags, {
7914
10136
  isTTY: process.stdout.isTTY,
7915
10137
  env: process.env
7916
10138
  });
7917
- if (mode === "agent" || mode === "agent-stream") {
10139
+ if (mode === "agent" || mode === "agent-stream" || mode === "json") {
7918
10140
  printAgentCliError(command, error, mode);
7919
10141
  } else {
7920
10142
  printCliError(error);
7921
10143
  }
7922
10144
  return 1;
10145
+ };
10146
+ const definition = commandRegistry.find(command);
10147
+ if (!definition) {
10148
+ return failEarly(new CliInputError(`Unknown command '${command}'`, 'Run "moda --help" for usage information.'));
10149
+ }
10150
+ if (parsed.shortFlags?.length) {
10151
+ return failEarly(new CliInputError(`Short options are not supported (${parsed.shortFlags.join(", ")}), so nothing was run.`, `Use the long form, e.g. --dry-run, --json, --quiet. See 'moda ${command} --help'.`));
10152
+ }
10153
+ for (const target of [flags, commandFlags]) {
10154
+ const raw = target["dry-run"];
10155
+ if (raw === undefined)
10156
+ continue;
10157
+ const value = raw.trim().toLowerCase();
10158
+ if (DRY_RUN_TRUE.has(value))
10159
+ target["dry-run"] = "true";
10160
+ else if (DRY_RUN_FALSE.has(value))
10161
+ delete target["dry-run"];
10162
+ else {
10163
+ return failEarly(new CliInputError(`--dry-run=${raw} is not a yes/no value, so nothing was run.`, "Pass --dry-run (or --dry-run=true) to preview, or omit it to run for real."));
10164
+ }
10165
+ }
10166
+ if (commandFlags.profile === "true" || commandFlags.profile === "") {
10167
+ return failEarly(new CliInputError("--profile needs a value, e.g. --profile=prod.", "List profiles with 'moda profiles list'."));
10168
+ }
10169
+ const requestedProfile = commandFlags.profile ?? process.env.MODA_PROFILE?.trim();
10170
+ if (requestedProfile && !PROFILE_MANAGING_COMMANDS.has(definition.name)) {
10171
+ const known = Object.keys(loadProfileConfig({ env: process.env }).profiles);
10172
+ if (!known.includes(requestedProfile)) {
10173
+ const source = commandFlags.profile ? `--profile=${requestedProfile}` : `MODA_PROFILE=${requestedProfile}`;
10174
+ return failEarly(new CliInputError(`Moda profile '${requestedProfile}' does not exist (from ${source}), so nothing was run.`, `Known profiles: ${known.join(", ")}. Create it with 'moda profiles create ${requestedProfile}' or fix the name.`));
10175
+ }
10176
+ if (commandFlags.profile)
10177
+ process.env.MODA_PROFILE = commandFlags.profile;
7923
10178
  }
7924
10179
  if (definition.name === "agent" && commandFlags.json !== "true") {
7925
10180
  commandFlags.agent = "true";
@@ -8061,7 +10316,7 @@ function installSignalHandlers() {
8061
10316
  async function main() {
8062
10317
  process.exitCode = await dispatchParsedCommand(parseArgs(process.argv));
8063
10318
  }
8064
- var isMain = process.argv[1] && fileURLToPath(import.meta.url) === realpathSync2(process.argv[1]);
10319
+ var isMain = process.argv[1] && fileURLToPath(import.meta.url) === realpathSync3(process.argv[1]);
8065
10320
  if (isMain) {
8066
10321
  installSignalHandlers();
8067
10322
  main().catch((error) => {
@@ -8071,9 +10326,11 @@ if (isMain) {
8071
10326
  }
8072
10327
  export {
8073
10328
  telemetryCommandName,
10329
+ stripLoneSurrogates,
8074
10330
  runCommand,
8075
10331
  resolveTailSignal,
8076
10332
  printCliError,
10333
+ parseCommandFlags,
8077
10334
  parseArgs,
8078
10335
  initFlagsForContext,
8079
10336
  flagsToArgs,
@@ -8082,7 +10339,11 @@ export {
8082
10339
  describeLinearLink,
8083
10340
  commandRegistry,
8084
10341
  classifyCliError,
10342
+ capProblemsBlocks,
8085
10343
  buildResumeCommand,
10344
+ auditCapStatus,
8086
10345
  TAIL_LIMITS,
10346
+ LEGACY_WINDOW_MAX_MESSAGES,
10347
+ FULL_TRANSCRIPT_DEFAULT_MAX_MESSAGES,
8087
10348
  CliInputError
8088
10349
  };