@moda-ai/cli 1.52.0 → 1.55.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.
@@ -0,0 +1,47 @@
1
+ // src/prompt-hash.ts
2
+ import { createHash } from "node:crypto";
3
+ var PROMPT_HASH_VERSION = 1;
4
+ function promptSourceHashPayload(definition) {
5
+ return {
6
+ kind: definition.kind,
7
+ source: definition.source,
8
+ ...["text", "messages"].includes(definition.kind) ? {
9
+ sourceHash: definition.sourceHash
10
+ } : ["builder", "template"].includes(definition.kind) ? {
11
+ sourceHash: definition.sourceHash,
12
+ dependencies: definition.dependencies,
13
+ coverage: definition.coverage,
14
+ unresolved: definition.unresolved
15
+ } : {}
16
+ };
17
+ }
18
+ function stableJson(value) {
19
+ return JSON.stringify(sortJson(value));
20
+ }
21
+ function sortJson(value) {
22
+ if (Array.isArray(value))
23
+ return value.map(sortJson);
24
+ if (value && typeof value === "object") {
25
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson(item)]));
26
+ }
27
+ return value;
28
+ }
29
+ function promptContentHashPayload(prompt) {
30
+ return stableJson({
31
+ key: prompt.key,
32
+ content: prompt.content,
33
+ systemPrompt: prompt.systemPrompt ?? null,
34
+ messages: prompt.messages ?? [],
35
+ variables: prompt.variables ?? [],
36
+ variablesSchema: prompt.variablesSchema ?? {},
37
+ modelConfig: prompt.modelConfig ?? {},
38
+ toolRefs: prompt.toolRefs ?? [],
39
+ responseSchema: prompt.responseSchema ?? null,
40
+ ...prompt.sourceDefinition ? { sourceDefinition: promptSourceHashPayload(prompt.sourceDefinition) } : {}
41
+ });
42
+ }
43
+ function computePromptContentHash(prompt) {
44
+ return createHash("sha256").update(promptContentHashPayload(prompt)).digest("hex");
45
+ }
46
+
47
+ export { PROMPT_HASH_VERSION, promptSourceHashPayload, stableJson, promptContentHashPayload, computePromptContentHash };
package/dist/cli.js CHANGED
@@ -8,7 +8,8 @@ import {
8
8
  runPromptsCommand,
9
9
  runSkillsCommand,
10
10
  runStatusCommand
11
- } from "./cli-ha40mgy1.js";
11
+ } from "./cli-a2jq9yd5.js";
12
+ import"./cli-mm48br00.js";
12
13
  import {
13
14
  ApiError,
14
15
  HARNESS_REPORT_APPROVAL_PATH,
@@ -37,7 +38,7 @@ import {
37
38
  terminalStyles,
38
39
  unknownSignalsSubcommandError,
39
40
  validateHarnessReport
40
- } from "./cli-he2wb4hd.js";
41
+ } from "./cli-9250ckvg.js";
41
42
  import"./cli-ssfq84k5.js";
42
43
  import {
43
44
  authFetch,
@@ -409,6 +410,7 @@ var SearchSchema = z.object({
409
410
  q: z.string().min(1).max(500),
410
411
  mode: z.enum(SEARCH_MODES).default("hybrid").optional(),
411
412
  user_id: z.string().optional(),
413
+ conversation_id: z.string().min(1).max(256).optional(),
412
414
  time_range: z.enum(TIME_RANGES).default("all").optional(),
413
415
  limit: z.number().min(1).max(100).default(20).optional(),
414
416
  include_tool_io: boolFlag(),
@@ -2000,6 +2002,20 @@ function nextCommandsFor(fix) {
2000
2002
  }
2001
2003
  ];
2002
2004
  case "UNROUTED":
2005
+ return [
2006
+ {
2007
+ command: `moda fix ${fix.id} --packet`,
2008
+ purpose: "Read the investigation packet — this fix is deliverable as diagnosis.",
2009
+ mutability: "read",
2010
+ requires_approval: false
2011
+ },
2012
+ {
2013
+ command: "moda prompts status",
2014
+ purpose: "If the reason says evidence carries no prompt version: check the prompt lockfile, then pass prompt: lock.prompts['<key>'] to createThinkTelemetry / createAI7Telemetry (docs.moda.dev/prompt-management/attribution).",
2015
+ mutability: "read",
2016
+ requires_approval: false
2017
+ }
2018
+ ];
2003
2019
  case "NO_EVIDENCE":
2004
2020
  case "BLOCKED_COVERAGE":
2005
2021
  case "NO_HEADROOM":
@@ -7963,9 +7979,10 @@ Commands:
7963
7979
  --signal=conversations = --signal=traces. <conversation_id> is the trace ID.
7964
7980
 
7965
7981
  Prompt management:
7966
- prompts init Create .moda/prompts.yml
7967
- prompts status Show local prompt changes
7982
+ prompts init Create .moda/prompts.yml (version 2: prompt_paths + sources)
7983
+ prompts status Show local prompt changes; warns on globs that match nothing
7968
7984
  prompts diff Only changed/new/deleted prompts since last sync
7985
+ prompts migrate [--dry-run] Rewrite a version 1 .moda/prompts.yml to version 2
7969
7986
  prompts sync Analyze prompt sources and upload versions
7970
7987
  prompts sync --watch Upload again when prompt files change
7971
7988
  prompts list|show|proposals|usage|replay-runs|decide
@@ -8033,6 +8050,7 @@ Traces flags:
8033
8050
  Search flags:
8034
8051
  --mode=keyword|semantic|hybrid Search mode (default hybrid)
8035
8052
  --user-id=ID Filter to a single user
8053
+ --conversation-id=ID Search inside one trace only
8036
8054
  --time-range=RANGE Time range filter (all|1h|3d|7d|24h|30d|90d)
8037
8055
  --limit=N Maximum results to return (1-100, default 20)
8038
8056
 
@@ -8792,6 +8810,23 @@ function shellArg(value) {
8792
8810
  return `"${value}"`;
8793
8811
  return `'${value.replace(/'/g, `'\\''`)}'`;
8794
8812
  }
8813
+ function searchCoverageWarnings(coverage, timeRange) {
8814
+ const record = asRecord(coverage);
8815
+ if (!record)
8816
+ return [];
8817
+ const warnings = [];
8818
+ const freshness = asRecord(record.index_freshness);
8819
+ const originalUsed = record.original_messages_search === "used" || record.keyword_source === "original_messages";
8820
+ if (freshness?.window_extends_beyond_index === true) {
8821
+ const indexedAt = asString(freshness.latest_indexed_message_at) ?? "none in the last 7 days";
8822
+ const originalAt = asString(freshness.latest_original_message_at) ?? "unknown";
8823
+ warnings.push(`The search index trails ingestion for --time-range=${timeRange}: newest indexed message ${indexedAt}, newest message ${originalAt}. ` + (originalUsed ? "Literal matches were read from the original messages, but semantic ranking cannot see messages after the index cutoff." : "Messages after the index cutoff are missing from these results; retry with --mode=keyword for exact text."));
8824
+ }
8825
+ if (record.results_only_from_original_messages === true) {
8826
+ warnings.push("Every result came from the original-message fallback: the search index returned no literal match (it may lag ingestion or not tokenize this text). Scores are literal match counts.");
8827
+ }
8828
+ return warnings;
8829
+ }
8795
8830
  async function searchFallbackToTraces(params) {
8796
8831
  const limit = Math.min(params.limit ?? 20, 100);
8797
8832
  const offset = params.offset ?? 0;
@@ -9338,7 +9373,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
9338
9373
  }
9339
9374
  case "search": {
9340
9375
  if (!positional || positional.trim() === "") {
9341
- 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]');
9376
+ throw new CliInputError("<query> is required", 'Usage: moda search "<query>" [--mode=keyword|semantic|hybrid] [--user-id=ID] [--conversation-id=ID] [--time-range=RANGE] [--include-tool-io] [--limit=N]');
9342
9377
  }
9343
9378
  const args = withStringIdFlags(flagsToArgs(flags, "q", positional), flags);
9344
9379
  const params = parseCommandFlags(SearchSchema, args, command, { positionalKey: "q" });
@@ -9348,6 +9383,8 @@ async function runCommand(command, positional, flags, positionals = positional ?
9348
9383
  query2.set("mode", params.mode);
9349
9384
  if (params.user_id)
9350
9385
  query2.set("user_id", params.user_id);
9386
+ if (params.conversation_id)
9387
+ query2.set("conversation_id", params.conversation_id);
9351
9388
  if (params.time_range)
9352
9389
  query2.set("time_range", params.time_range);
9353
9390
  if (params.limit)
@@ -9365,11 +9402,12 @@ async function runCommand(command, positional, flags, positionals = positional ?
9365
9402
  } else if (params.mode && actualMode && actualMode !== params.mode && params.mode !== "hybrid") {
9366
9403
  warnings.push(`Search ran in ${actualMode} mode instead of the requested ${params.mode} mode.`);
9367
9404
  }
9405
+ warnings.push(...searchCoverageWarnings(searchRecord.coverage, params.time_range ?? "all"));
9368
9406
  const results = Array.isArray(searchRecord.results) ? searchRecord.results : [];
9369
9407
  if (results.length > 0 && (params.offset ?? 0) > 0) {
9370
9408
  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.");
9371
9409
  }
9372
- if (results.length === 0) {
9410
+ if (results.length === 0 && !params.conversation_id) {
9373
9411
  const fallback = await searchFallbackToTraces({ ...params, offset: params.offset });
9374
9412
  if (fallback?.kind === "notice") {
9375
9413
  warnings.push(fallback.warning);
@@ -10132,7 +10170,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
10132
10170
  function telemetryCommandName(definition) {
10133
10171
  return definition.telemetryCommand ?? definition.name;
10134
10172
  }
10135
- var OFFLINE_PROMPTS_SUBCOMMANDS = new Set(["init", "status", "diff"]);
10173
+ var OFFLINE_PROMPTS_SUBCOMMANDS = new Set(["init", "status", "diff", "migrate"]);
10136
10174
  function legacyCommand(metadata) {
10137
10175
  return {
10138
10176
  ...metadata,
@@ -10226,7 +10264,7 @@ var commandRegistry = createCommandRegistry([
10226
10264
  resetApiRequestCountBeforeRun: true,
10227
10265
  telemetry: "result",
10228
10266
  handler: async (context) => {
10229
- const { runInit } = await import("./index-pykaaezb.js");
10267
+ const { runInit } = await import("./index-hn9t95tr.js");
10230
10268
  if (context.outputMode === "agent-stream") {
10231
10269
  context.output.writeEvent({
10232
10270
  event: "started",
@@ -10980,18 +11018,18 @@ var commandRegistry = createCommandRegistry([
10980
11018
  legacyCommand({
10981
11019
  name: "prompts",
10982
11020
  description: "Manage prompt versions and labels",
10983
- examples: ["moda prompts init", "moda prompts status", "moda prompts sync", "moda prompts ab --baseline=old.md --candidate=new.md"],
11021
+ examples: ["moda prompts init", "moda prompts status", "moda prompts migrate --dry-run", "moda prompts sync", "moda prompts ab --baseline=old.md --candidate=new.md"],
10984
11022
  subcommands: [
10985
11023
  {
10986
11024
  name: "init",
10987
- description: "Create .moda/prompts.yml in the current directory (never overwrites)",
11025
+ description: "Create a version 2 .moda/prompts.yml in the current directory (never overwrites). prompt_paths are exact fast-glob patterns from the repository root; sources: maps prompts defined in code (key, path, selector.symbol|pointer, kind, dependencies).",
10988
11026
  usage: "moda prompts init [--from-harness=<report>]",
10989
- flags: [{ name: "--from-harness=<report>", description: "Register verified source mappings from a harness report; source files stay in place." }],
11027
+ flags: [{ name: "--from-harness=<report>", description: "Register verified source mappings from a harness report (older reports are normalized; prompts without a promptSource are mapped from sourcePath when unambiguous). Unresolvable prompts are returned in imported.skipped with reasons; source files stay in place." }],
10990
11028
  examples: ["moda prompts init", "cd /path/to/project && moda prompts init"]
10991
11029
  },
10992
11030
  {
10993
11031
  name: "status",
10994
- description: "Show prompt files that changed since the last sync",
11032
+ description: "Show prompt files that changed since the last sync. Also reports prompt_paths patterns that match no files (zeroMatchPatterns) and files only older CLIs discovered (legacyOnlyFiles), with a warning pointing at `moda prompts migrate`.",
10995
11033
  usage: "moda prompts status",
10996
11034
  examples: ["moda prompts status"]
10997
11035
  },
@@ -11001,6 +11039,16 @@ var commandRegistry = createCommandRegistry([
11001
11039
  usage: "moda prompts diff",
11002
11040
  examples: ["moda prompts diff"]
11003
11041
  },
11042
+ {
11043
+ name: "migrate",
11044
+ description: "Rewrite .moda/prompts.yml to version 2: keeps every pattern and source, adds explicit paths for prompt files only the older repo-wide discovery matched (so their lockfile keys keep resolving), and ensures a sources: list. Comments are kept; the write is atomic.",
11045
+ usage: "moda prompts migrate [--dry-run] [--discover]",
11046
+ flags: [
11047
+ { name: "--dry-run", description: "Print the migrated manifest without writing it." },
11048
+ { name: "--discover", description: "Also scan for code-defined prompts (deterministic, no agent calls). Adds only mappings that need no semantic verification; other candidates are listed under pendingSources with the command that verifies them." }
11049
+ ],
11050
+ examples: ["moda prompts migrate --dry-run", "moda prompts migrate", "moda prompts migrate --discover"]
11051
+ },
11004
11052
  {
11005
11053
  name: "sync",
11006
11054
  description: "Analyze prompt sources and upload verified versions to Moda Cloud",
@@ -11009,7 +11057,7 @@ var commandRegistry = createCommandRegistry([
11009
11057
  { name: "--analyst=local-scan|claude|codex|cursor|auto", description: "Defaults to auto: deterministic discovery, installed-agent extension, and independent verification. Use local-scan for no agent calls." },
11010
11058
  { name: "--assist=none", description: "Disable agent fallback, including with an explicit analyst." },
11011
11059
  { name: "--no-analyze", description: "Skip discovery and sync only configured sources." },
11012
- { name: "--from-harness=<report>", description: "Read and register harness prompt source mappings before sync." },
11060
+ { name: "--from-harness=<report>", description: "Read and register harness prompt source mappings before sync; unresolvable prompts are listed in harnessImport.skipped. Fails only when none resolve (regenerate with `moda harness analyze --experimental --concern=prompts`)." },
11013
11061
  { name: "--allow-untrack", description: "Explicitly remove missing keys from local tracking; registry history remains." },
11014
11062
  { name: "--watch", description: "Analyze before syncing; re-upload when mapped prompt files change." },
11015
11063
  { name: "--dry-run", description: "Preview without saving mappings or registry versions; deterministic unless an analyst is explicitly selected." }
@@ -11485,6 +11533,7 @@ if (isMain) {
11485
11533
  export {
11486
11534
  telemetryCommandName,
11487
11535
  stripLoneSurrogates,
11536
+ searchCoverageWarnings,
11488
11537
  runCommand,
11489
11538
  resolveTailSignal,
11490
11539
  printCliError,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildSourceSnapshot
3
- } from "./cli-he2wb4hd.js";
3
+ } from "./cli-9250ckvg.js";
4
4
  import"./cli-ac3p2byq.js";
5
5
  import"./cli-0v6na3yp.js";
6
6
 
@@ -42,7 +42,7 @@ import {
42
42
  writeHarnessAnalyzeResult,
43
43
  writeHarnessArtifacts,
44
44
  writeHumanProgress
45
- } from "./cli-he2wb4hd.js";
45
+ } from "./cli-9250ckvg.js";
46
46
  import"./cli-ac3p2byq.js";
47
47
  import"./cli-0v6na3yp.js";
48
48
  export {
@@ -3,7 +3,8 @@ import {
3
3
  initPrompts,
4
4
  runPromptSync,
5
5
  runSkillSync
6
- } from "./cli-ha40mgy1.js";
6
+ } from "./cli-a2jq9yd5.js";
7
+ import"./cli-mm48br00.js";
7
8
  import {
8
9
  codingAgentDisplayName,
9
10
  describeCodingAgentEvent,
@@ -22,7 +23,7 @@ import {
22
23
  runHarnessCommand,
23
24
  startRemoteAnalyze,
24
25
  stripAnsi
25
- } from "./cli-he2wb4hd.js";
26
+ } from "./cli-9250ckvg.js";
26
27
  import {
27
28
  bundledSkillsDir,
28
29
  installIntegrationSkill,
@@ -0,0 +1,36 @@
1
+ /** Version of the canonicalization below. Bump on ANY change to the hashed payload. */
2
+ export declare const PROMPT_HASH_VERSION: 1;
3
+ /** The typed-source definition fields that participate in the hash. */
4
+ export interface PromptHashSourceDefinition {
5
+ kind: string;
6
+ source: unknown;
7
+ sourceHash?: string;
8
+ dependencies?: unknown;
9
+ coverage?: unknown;
10
+ unresolved?: unknown;
11
+ }
12
+ /** Hashed prompt fields; anything else on the object is ignored. */
13
+ export interface PromptHashInput {
14
+ key: string;
15
+ content: string;
16
+ systemPrompt?: string | null;
17
+ messages?: unknown[] | null;
18
+ variables?: unknown[] | null;
19
+ variablesSchema?: Record<string, unknown> | null;
20
+ modelConfig?: Record<string, unknown> | null;
21
+ toolRefs?: unknown[] | null;
22
+ responseSchema?: Record<string, unknown> | null;
23
+ sourceDefinition?: PromptHashSourceDefinition | null;
24
+ }
25
+ /**
26
+ * The subset of a typed source definition that is hashed: text/messages hash
27
+ * their extracted source; builders/templates also hash dependency
28
+ * fingerprints and coverage, so a fragment edit mints a new version.
29
+ */
30
+ export declare function promptSourceHashPayload(definition: PromptHashSourceDefinition): Record<string, unknown>;
31
+ /** JSON.stringify with every object's keys sorted by localeCompare, recursively. */
32
+ export declare function stableJson(value: unknown): string;
33
+ /** The exact string that is hashed (exposed for debugging hash mismatches). */
34
+ export declare function promptContentHashPayload(prompt: PromptHashInput): string;
35
+ /** Lowercase hex SHA-256 of {@link promptContentHashPayload}: the lockfile `contentHash`. */
36
+ export declare function computePromptContentHash(prompt: PromptHashInput): string;
@@ -0,0 +1,15 @@
1
+ import {
2
+ PROMPT_HASH_VERSION,
3
+ computePromptContentHash,
4
+ promptContentHashPayload,
5
+ promptSourceHashPayload,
6
+ stableJson
7
+ } from "./cli-mm48br00.js";
8
+ import"./cli-0v6na3yp.js";
9
+ export {
10
+ stableJson,
11
+ promptSourceHashPayload,
12
+ promptContentHashPayload,
13
+ computePromptContentHash,
14
+ PROMPT_HASH_VERSION
15
+ };
@@ -5,7 +5,7 @@ import {
5
5
  inspectCodeSelectors,
6
6
  inspectPythonSelectors,
7
7
  safePromptEvidencePath
8
- } from "./cli-he2wb4hd.js";
8
+ } from "./cli-9250ckvg.js";
9
9
  import"./cli-ac3p2byq.js";
10
10
  import {
11
11
  __commonJS,
package/package.json CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.52.0",
3
+ "version": "1.55.0",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "cli": "./dist/cli.js",
8
8
  "moda": "./dist/cli.js"
9
9
  },
10
+ "exports": {
11
+ "./prompt-hash": {
12
+ "types": "./dist/prompt-hash.d.ts",
13
+ "import": "./dist/prompt-hash.js"
14
+ },
15
+ "./package.json": "./package.json",
16
+ "./*": "./*"
17
+ },
10
18
  "files": [
11
19
  "dist/**/*.js",
12
20
  "dist/**/*.d.ts",
@@ -21,7 +29,7 @@
21
29
  "scripts": {
22
30
  "bundle:skills": "bun scripts/bundle-skills.ts",
23
31
  "export:admin-manifest": "bun scripts/export-admin-command-manifest.ts",
24
- "build": "bun run bundle:skills && tsc --noEmit && bun build ./src/cli.ts ./src/prompt-source-mcp.ts --target=node --format=esm --outdir=dist --splitting --external @inquirer/prompts --external open --external zod",
32
+ "build": "bun run bundle:skills && tsc --noEmit && tsc -p tsconfig.prompt-hash.json && bun build ./src/cli.ts ./src/prompt-source-mcp.ts ./src/prompt-hash.ts --target=node --format=esm --outdir=dist --splitting --external @inquirer/prompts --external open --external zod",
25
33
  "dev": "tsc --watch",
26
34
  "typecheck": "tsc --noEmit",
27
35
  "test": "bun run bundle:skills && vitest run",
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "schema_version": "moda.skill_index.v1",
3
- "bundled_at": "2026-10-12T06:24:12.441Z",
4
- "cli_version": "1.52.0",
3
+ "bundled_at": "2026-10-12T08:12:12.808Z",
4
+ "cli_version": "1.55.0",
5
5
  "skills": [
6
6
  {
7
7
  "id": "integration-cloudflare-think",
8
- "version": "1.1.0",
8
+ "version": "1.1.1",
9
9
  "summary": "Instrument Think on Workers with AI SDK 7, prompt versions, and acknowledged JSON export.",
10
10
  "targets": {
11
11
  "language": "node",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  id: integration-cloudflare-think
3
3
  name: "Integrate Moda with Cloudflare Think"
4
- version: 1.1.0
4
+ version: 1.1.1
5
5
  category: integration
6
6
  targets:
7
7
  language: node
@@ -94,7 +94,10 @@ the correct definition was registered.
94
94
  Keep memory and variable substitutions separate from reusable template
95
95
  versions. Think adds instructions/tools: verify actual system instructions
96
96
  on each model span as well as the registry definition.
97
- Reference: https://docs.moda.dev/prompt-management/attribution
97
+ Reference: https://docs.moda.dev/prompt-management/attribution#cloudflare-think-and-ai-sdk-7
98
+ and, for `.moda/prompts.yml` `sources:` mappings, https://docs.moda.dev/prompt-management/workflow#map-prompts-defined-in-code.
99
+ Without the `prompt` option no trace carries a prompt version, so prompt fixes
100
+ land `UNROUTED` ("top share 0%").
98
101
 
99
102
  ## 5. Validate through the original application route
100
103
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: moda-cli
3
- version: 2.7.0
3
+ version: 2.8.0
4
4
  description: Query Moda's AI agent trace analytics and manage code-first prompt versions from the terminal — semantic/keyword/hybrid message search, overview KPIs, topic clusters, message context, user frustration detections, tool failures, custom signals, false-positive review of detections, and moda prompts status/sync/promote. Use when the user asks about moda, modaflows, trace or conversation analytics, prompt management, user frustrations, agent observability, tool failure debugging, custom signals, marking a detection as a false positive, wants to find traces or tool calls about a topic, or wants to investigate how their AI agent is performing.
5
5
  ---
6
6
 
@@ -506,6 +506,7 @@ moda search "customer asked for a refund" --mode=semantic
506
506
  moda search "SEARCH_NO_RESULTS" --mode=keyword
507
507
  moda search "checkout error" --time-range=7d --limit=10
508
508
  moda search "stripe.charges.create failed" --user-id=<id>
509
+ moda search "Иван" --conversation-id=<id> --mode=keyword # inside one trace
509
510
  ```
510
511
 
511
512
  Searches trace messages at message grain. Pass `--include-tool-io` to
@@ -514,9 +515,17 @@ output previews), so it finds where an agent *did* something, not just where
514
515
  it talked about it — tool-IO search is opt-in, not the default.
515
516
 
516
517
  Flags: `--mode=keyword|semantic|hybrid` (default `hybrid`), `--user-id`,
518
+ `--conversation-id` (search inside one trace; no trace fallback),
517
519
  `--time-range` (`all|1h|3d|7d|24h|30d|90d`, default `all`), `--limit` (1–100,
518
520
  default 20). The query is a positional arg (max 500 chars).
519
521
 
522
+ **Index freshness.** Keyword matches (and hybrid's keyword half) fall back to
523
+ the original messages when the search index returns too few literal matches,
524
+ so recent, Cyrillic and partial-word text is still found. `coverage` reports
525
+ which source served the keyword leg (`keyword_source`) and how far the index
526
+ trails ingestion (`index_freshness`); a warning says so when your
527
+ `--time-range` reaches past the newest indexed message.
528
+
520
529
  **Fallback.** If message-level search finds nothing but whole traces match
521
530
  (same as `moda traces --search`), those traces come back in `results` with
522
531
  `match_grain: "trace"`, plus `fallback.source: "traces_search"` and a warning.
@@ -773,17 +782,63 @@ dropped and `truncation.data.full_output_path` holds every example. Agent output
773
782
  Use these only when working on the code-first prompt registry.
774
783
 
775
784
  ```bash
776
- moda prompts init # create .moda/prompts.yml
777
- moda prompts status # read-only local status
785
+ moda prompts init # create .moda/prompts.yml (version 2)
786
+ moda prompts status # read-only local status + manifest warnings
778
787
  moda prompts diff # only changed/new/deleted since last sync
788
+ moda prompts migrate --dry-run # preview a version 1 -> 2 manifest rewrite
789
+ moda prompts migrate # apply it (atomic; comments kept)
790
+ moda prompts sync --dry-run # preview discovery + what would upload
779
791
  moda prompts sync # upload changed prompt versions
792
+ moda prompts sync --from-harness # register mappings from .moda/harness-report.json first
780
793
  moda prompts sync --watch # sync when local prompt hashes change
781
794
  moda prompts promote support.triage --label=prod --version=pver_abc123
782
795
  ```
783
796
 
784
- `status` and `diff` are read-only. `sync` uploads prompt versions and writes
785
- `.moda/prompts.lock.json`. `promote` moves a remote `dev`, `staging`, or
786
- `prod` label to an existing version.
797
+ `status`, `diff` and `migrate --dry-run` are read-only. `sync` uploads prompt
798
+ versions and writes `.moda/prompts.lock.json`. `promote` moves a remote `dev`,
799
+ `staging`, or `prod` label to an existing version.
800
+
801
+ `.moda/prompts.yml` (version 2) has two ways to point at prompts:
802
+
803
+ ```yaml
804
+ version: 2
805
+ prompt_paths: # exact fast-glob patterns from the repo root
806
+ - "prompts/**/*.prompt.md"
807
+ - "!prompts/archive/**"
808
+ sources: # prompts defined in code or config, mapped in place
809
+ - key: support.system # stable registry key (also the lockfile key)
810
+ path: src/agent.ts
811
+ selector: { symbol: SYSTEM_PROMPT } # or { pointer: /support/prompt } for JSON/YAML
812
+ - key: support.build
813
+ path: src/prompt.py
814
+ selector: { symbol: build_prompt }
815
+ kind: builder # text | messages | template | builder
816
+ dependencies:
817
+ - path: config/policy.txt
818
+ ```
819
+
820
+ - `prompts status` returns `zeroMatchPatterns` (globs that match nothing),
821
+ `legacyOnlyFiles` and `legacyManifest`. A non-empty `legacyOnlyFiles` means a
822
+ manifest from an older CLI: those files used to sync (old discovery matched
823
+ `*.prompt.*` anywhere) but match nothing now. Run `moda prompts migrate
824
+ --dry-run`, then `moda prompts migrate`; it adds them as explicit paths so
825
+ their keys and lockfile entries are unchanged (`--discover` also scans for
826
+ code-defined prompts, but lists unverified ones under `pendingSources`
827
+ instead of adding them; verify those with `moda harness analyze
828
+ --experimental --concern=prompts`, then `sync --from-harness`). `moda
829
+ doctor` reports the same condition on its `prompts_discovered` check.
830
+ - `sync --from-harness` registers what resolves and lists the rest in
831
+ `harnessImport.skipped` with reasons; it fails only when nothing resolves
832
+ (regenerate with `moda harness analyze --experimental --concern=prompts`).
833
+ - Lockfile `contentHash` is `computePromptContentHash` from
834
+ `@moda-ai/cli/prompt-hash` (`PROMPT_HASH_VERSION` 1). Don't reimplement it.
835
+
836
+ To attribute runtime calls, stamp the lock entry `{ key, promptId, versionId }`
837
+ on telemetry. With Cloudflare Think / AI SDK 7, pass
838
+ `prompt: lock.prompts['<key>']` to `createThinkTelemetry` (`moda-ai/think`) or
839
+ `createAI7Telemetry` (`moda-ai/ai7`). A `moda fix` that lands `UNROUTED` with
840
+ "top share 0%" means the evidence carries no prompt version: wire this up,
841
+ then check `moda prompts status`.
787
842
 
788
843
  `prompts ab` and `prompts propose --gate` run paid replays. Before sending
789
844
  anything they compute planned playouts (cases × seeds × 2 arms) and print it