@moda-ai/cli 1.52.0 → 1.54.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/README.md CHANGED
@@ -106,8 +106,9 @@ Legacy aliases (same behavior, kept for existing scripts): `moda conversations`
106
106
  Prompt management:
107
107
 
108
108
  ```bash
109
- moda prompts init # Create .moda/prompts.yml
109
+ moda prompts init # Create .moda/prompts.yml (version 2)
110
110
  moda prompts status # Read-only local prompt status
111
+ moda prompts migrate --dry-run # Preview a version 1 -> 2 manifest rewrite
111
112
  moda prompts sync # Upload changed prompt versions
112
113
  moda prompts promote support.triage --label=prod --version=pver_abc123
113
114
  ```
@@ -176,7 +177,10 @@ results on stdout remain unchanged; `--quiet` suppresses progress and
176
177
  `--watch` starts with analysis and then watches mapped definitions; re-run sync
177
178
  to discover new sources that do not affect an existing mapping. You can also
178
179
  run `harness analyze --experimental --concern=prompts` and import its report
179
- with `prompts init --from-harness=<report>`. Discovery reports partial coverage:
180
+ with `prompts init --from-harness=<report>`. Older reports are normalized and
181
+ prompts without a `promptSource` are mapped from `sourcePath` when unambiguous;
182
+ prompts that still do not resolve are returned in `skipped` with a reason, and the
183
+ import fails only when none resolve. Discovery reports partial coverage:
180
184
  finding a prompt fragment does not prove that every composition or model call
181
185
  has been covered.
182
186
 
@@ -239,6 +243,14 @@ sources:
239
243
  - path: config/policy.txt
240
244
  ```
241
245
 
246
+ Manifests from older CLIs say `version: 1`. Those CLIs matched `.prompt.*` files
247
+ anywhere in the repository; patterns are now exact globs from the repository root.
248
+ `prompts status` reports `zeroMatchPatterns` and `legacyOnlyFiles` (files only the
249
+ old discovery found), and `moda prompts migrate [--dry-run] [--discover]` rewrites
250
+ the manifest to version 2, adding those files as explicit paths so their keys and
251
+ lockfile entries are unchanged. The lockfile `contentHash` is
252
+ `computePromptContentHash` from `@moda-ai/cli/prompt-hash` (`PROMPT_HASH_VERSION` 1).
253
+
242
254
  Selectors support named TypeScript/JavaScript/Python definitions and JSON Pointer
243
255
  selection in JSON/YAML. Python resolution requires `python3`; the helper parses
244
256
  ASTs without importing customer modules. Static literals and supported constant
@@ -182903,6 +182903,19 @@ function discoverStructuredPromptSources(rootDir, sourcePath) {
182903
182903
  walk(value, [], 0, []);
182904
182904
  return sources;
182905
182905
  }
182906
+ function backfillPromptSource(rootDir, artifact) {
182907
+ if (artifact.promptSource)
182908
+ return artifact.promptSource;
182909
+ if (!artifact.sourcePath)
182910
+ return;
182911
+ if (/\.(?:[cm]?[jt]sx?|py)$/.test(artifact.sourcePath)) {
182912
+ const candidates = discoverCodePromptSources(rootDir, artifact.sourcePath);
182913
+ const name = (artifact.name ?? "").replace(/[^a-z0-9]/gi, "").toLowerCase();
182914
+ const matching = candidates.filter((source) => source.selector?.symbol?.replace(/[^a-z0-9]/gi, "").toLowerCase() === name);
182915
+ return matching.length === 1 ? matching[0] : undefined;
182916
+ }
182917
+ return { key: artifact.id, path: artifact.sourcePath };
182918
+ }
182906
182919
  function discoverCodePromptSources(rootDir, sourcePath) {
182907
182920
  return discoverCodePromptCandidates(rootDir, sourcePath).map((candidate) => candidate.source);
182908
182921
  }
@@ -184857,7 +184870,7 @@ async function runHarnessCommand(context) {
184857
184870
  throw new CliInputError("Focused remote prompt analysis is not supported yet.", "Use local --analyst=claude|codex|cursor|local-scan, or drop --concern for a full remote report.");
184858
184871
  }
184859
184872
  if (context.flags["github-actions"] === "true") {
184860
- const { runGithubActionsAnalyze } = await import("./harness-github-actions-gbty3k66.js");
184873
+ const { runGithubActionsAnalyze } = await import("./harness-github-actions-075xzdvp.js");
184861
184874
  const result = await runGithubActionsAnalyze(rootDir, {
184862
184875
  writeReport: (report2) => {
184863
184876
  const normalized = normalizeHarnessReport(report2);
@@ -185501,19 +185514,10 @@ function repairExternalAnalystReport(report, rootDir, options = {}) {
185501
185514
  if (artifact.type !== "prompt")
185502
185515
  continue;
185503
185516
  try {
185504
- if (!artifact.promptSource && artifact.sourcePath) {
185505
- if (/\.(?:[cm]?[jt]sx?|py)$/.test(artifact.sourcePath)) {
185506
- const candidates = discoverCodePromptSources(rootDir, artifact.sourcePath);
185507
- const name = artifact.name.replace(/[^a-z0-9]/gi, "").toLowerCase();
185508
- const matching = candidates.filter((source2) => source2.selector?.symbol?.replace(/[^a-z0-9]/gi, "").toLowerCase() === name);
185509
- if (matching.length === 1)
185510
- artifact.promptSource = matching[0];
185511
- } else {
185512
- artifact.promptSource = { key: artifact.id, path: artifact.sourcePath };
185513
- }
185514
- }
185515
- if (!artifact.promptSource)
185517
+ const backfilled = backfillPromptSource(rootDir, artifact);
185518
+ if (!backfilled)
185516
185519
  continue;
185520
+ artifact.promptSource = backfilled;
185517
185521
  const source = resolvePromptSource(rootDir, artifact.promptSource);
185518
185522
  artifact.body = source.content;
185519
185523
  artifact.sourceDefinition = source.sourceDefinition;
@@ -188911,4 +188915,4 @@ function hashText(text) {
188911
188915
  return createHash4("sha256").update(text).digest("hex");
188912
188916
  }
188913
188917
 
188914
- export { ApiError, callDataAPI, callControlAPI, getApiRequestCount, resetApiRequestCount, $parseDocument, $stringify, require_typescript, inspectCodeSelectors, inspectPythonSelectors, resolvePromptSource, assertFlags, assertPositionals, runUsersCommand, runUserCommand, runProblemReopenCommand, runSignalsManagementCommand, SIGNALS_MANAGEMENT_SUBCOMMANDS, unknownSignalsSubcommandError, shouldColorTerminalOutput, terminalStyles, stripAnsi2 as stripAnsi, renderMarkdownForTerminal, HARNESS_REPORT_PATH, HARNESS_REPORT_APPROVAL_PATH, HARNESS_ANALYZE_CONCERNS, validateHarnessReport, readHarnessReport, hashHarnessReport, hashStableJson, startRemoteAnalyze, pollRemoteAnalyzeUntilDone, isRecoverableRemoteRun, checkPromptMapping, safePromptEvidencePath, discoverHarnessPromptSources, renderDoctorHuman, renderStatusHuman, renderInitBanner, renderInitStep, renderInitNote, renderInitSetupPlan, renderInitComplete, runHarnessCommand, writeHarnessAnalyzeResult, emitWrittenArtifacts, analyzeHarnessPrompts, repairExternalAnalystReport, deriveRelationshipsFromGraph, mergeDeterministicArtifacts, describeCodingAgentEvent, analystEventProgress, isRetryableExternalAnalystFailure, sleep, summarizeInvalidAnalystReport, isDeterministicLocalAnalyst, resolveHarnessAnalyzeConcern, detectAvailableCodingAgents, codingAgentDisplayName, runCodingAgent, resolveAnalystModel, renderExternalHarnessAnalystPrompt, CONCERN_CANDIDATE_KINDS, SEED_CANDIDATE_KIND_LABELS, renderAnalystCandidateLines, renderAnalystSeedLines, truncateText, scanHarness, summarizeHarness, buildSourceSnapshot, writeHumanProgress, readHarnessSyncState, describeSyncIdentity, describeSyncTenantLabel, findFresherHarnessReports, writeHarnessArtifacts, assertHarnessGraphSyncQuality, graphForSync, inventoryFiles, numericEnv, numericEnvAllowZero, computeDeterministicFloor, stableId2 as stableId, titleFromId2 as titleFromId, promptNameFromPath };
188918
+ export { ApiError, callDataAPI, callControlAPI, getApiRequestCount, resetApiRequestCount, $isSeq, $parseDocument, $stringify, require_typescript, inspectCodeSelectors, inspectPythonSelectors, resolvePromptSource, backfillPromptSource, HARNESS_REPORT_PATH, HARNESS_REPORT_APPROVAL_PATH, HARNESS_ANALYZE_CONCERNS, validateHarnessReport, readHarnessReport, normalizeHarnessReport, hashHarnessReport, hashStableJson, assertFlags, assertPositionals, runUsersCommand, runUserCommand, runProblemReopenCommand, runSignalsManagementCommand, SIGNALS_MANAGEMENT_SUBCOMMANDS, unknownSignalsSubcommandError, shouldColorTerminalOutput, terminalStyles, stripAnsi2 as stripAnsi, renderMarkdownForTerminal, startRemoteAnalyze, pollRemoteAnalyzeUntilDone, isRecoverableRemoteRun, checkPromptMapping, safePromptEvidencePath, discoverHarnessPromptSources, renderDoctorHuman, renderStatusHuman, renderInitBanner, renderInitStep, renderInitNote, renderInitSetupPlan, renderInitComplete, runHarnessCommand, writeHarnessAnalyzeResult, emitWrittenArtifacts, analyzeHarnessPrompts, repairExternalAnalystReport, deriveRelationshipsFromGraph, mergeDeterministicArtifacts, describeCodingAgentEvent, analystEventProgress, isRetryableExternalAnalystFailure, sleep, summarizeInvalidAnalystReport, isDeterministicLocalAnalyst, resolveHarnessAnalyzeConcern, detectAvailableCodingAgents, codingAgentDisplayName, runCodingAgent, resolveAnalystModel, renderExternalHarnessAnalystPrompt, CONCERN_CANDIDATE_KINDS, SEED_CANDIDATE_KIND_LABELS, renderAnalystCandidateLines, renderAnalystSeedLines, truncateText, scanHarness, summarizeHarness, buildSourceSnapshot, writeHumanProgress, readHarnessSyncState, describeSyncIdentity, describeSyncTenantLabel, findFresherHarnessReports, writeHarnessArtifacts, assertHarnessGraphSyncQuality, graphForSync, inventoryFiles, numericEnv, numericEnvAllowZero, computeDeterministicFloor, stableId2 as stableId, titleFromId2 as titleFromId, promptNameFromPath };
@@ -1,11 +1,18 @@
1
1
  import {
2
+ computePromptContentHash,
3
+ stableJson
4
+ } from "./cli-mm48br00.js";
5
+ import {
6
+ $isSeq,
2
7
  $parseDocument,
3
8
  $stringify,
4
9
  assertFlags,
5
10
  assertPositionals,
11
+ backfillPromptSource,
6
12
  callControlAPI,
7
13
  callDataAPI,
8
14
  inventoryFiles,
15
+ normalizeHarnessReport,
9
16
  readHarnessSyncState,
10
17
  renderDoctorHuman,
11
18
  renderStatusHuman,
@@ -13,7 +20,7 @@ import {
13
20
  resolvePromptSource,
14
21
  safePromptEvidencePath,
15
22
  scanHarness
16
- } from "./cli-he2wb4hd.js";
23
+ } from "./cli-9250ckvg.js";
17
24
  import {
18
25
  SKILL_SOURCE_HARNESS,
19
26
  SKILL_SOURCE_INTEGRATION_BUNDLED,
@@ -6463,6 +6470,7 @@ function checkPromptAttribution(rootDir, keys) {
6463
6470
  var MODA_DIR = ".moda";
6464
6471
  var MANIFEST_PATH = join(MODA_DIR, "prompts.yml");
6465
6472
  var LOCK_PATH = join(MODA_DIR, "prompts.lock.json");
6473
+ var DISCOVERY_IGNORE = ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/.env*", "**/*.pem", "**/*.key"];
6466
6474
  var DEFAULT_PROMPT_PATHS = [
6467
6475
  "prompts/**/*.prompt.md",
6468
6476
  "prompts/**/*.prompt.json",
@@ -6473,6 +6481,7 @@ var PROMPT_WRITE_SUBCOMMAND_FLAGS = {
6473
6481
  init: ["from-harness"],
6474
6482
  status: [],
6475
6483
  diff: [],
6484
+ migrate: ["dry-run", "discover"],
6476
6485
  sync: [
6477
6486
  "dry-run",
6478
6487
  "from-harness",
@@ -6577,18 +6586,41 @@ async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
6577
6586
  kind: "prompt_manifest",
6578
6587
  path: init.manifestPath,
6579
6588
  description: init.created ? "Prompt discovery manifest created by this run." : "Prompt discovery manifest already existed; left unchanged."
6580
- }]
6589
+ }],
6590
+ ...imported?.skipped.length ? { warnings: [harnessSkipWarning(imported)] } : {}
6581
6591
  });
6582
6592
  break;
6583
6593
  }
6584
- case "status":
6585
- context.output.writeData(await getPromptStatus());
6594
+ case "status": {
6595
+ const status = await getPromptStatus();
6596
+ const warnings = promptManifestWarnings(status);
6597
+ context.output.writeData(status, warnings.length > 0 ? { warnings } : undefined);
6586
6598
  break;
6599
+ }
6600
+ case "migrate": {
6601
+ assertPositionals("prompts migrate", positionals, 1, "moda prompts migrate [--dry-run] [--discover]");
6602
+ const result = await migratePromptManifest({
6603
+ dryRun: flags["dry-run"] === "true",
6604
+ discover: flags.discover === "true",
6605
+ env: profileOptions.env
6606
+ });
6607
+ context.output.writeData(result, {
6608
+ summary: {
6609
+ text: !result.changed ? `${MANIFEST_PATH} is already version 2 with nothing to add.` : `${result.dryRun ? "Would migrate" : "Migrated"} ${MANIFEST_PATH} from version ${result.fromVersion} to 2: ${result.addedPaths.length} legacy-discovered path(s), ${result.addedSources.length} discovered source(s) added.`,
6610
+ confidence: "high"
6611
+ },
6612
+ ...result.changed && !result.dryRun ? {
6613
+ artifacts: [{ kind: "prompt_manifest", path: result.manifestPath, description: "Prompt manifest rewritten to version 2." }]
6614
+ } : {},
6615
+ ...migrateWarnings(result).length > 0 ? { warnings: migrateWarnings(result) } : {}
6616
+ });
6617
+ break;
6618
+ }
6587
6619
  case "diff": {
6588
6620
  const status = await getPromptStatus();
6589
6621
  const differing = status.prompts.filter((row) => row.state !== "unchanged");
6590
6622
  const tracked = existsSync2(MANIFEST_PATH);
6591
- const untrackedWarning = tracked ? [] : [
6623
+ const diffWarnings = tracked ? promptManifestWarnings(status) : [
6592
6624
  `No ${MANIFEST_PATH} here, so diff has no last sync to compare against: it only covers prompts tracked by \`moda prompts sync\`${differing.length > 0 ? ", and every discovered prompt file lists as new" : ""}. For a registry manifest, preview with \`moda registry push --dry-run\`.`
6593
6625
  ];
6594
6626
  context.output.writeData({
@@ -6601,7 +6633,7 @@ async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
6601
6633
  text: !tracked ? differing.length === 0 ? `Not a prompts-sync project (no ${MANIFEST_PATH}) and no prompt files found; diff has nothing to compare.` : `Not a prompts-sync project (no ${MANIFEST_PATH}): ${differing.length} discovered prompt file(s) are untracked and would be new on \`moda prompts sync\`.` : differing.length === 0 ? "No prompt differs from the last sync." : `${differing.length} prompt(s) differ from the last sync (changed ${status.changed}, new ${status.new}, deleted ${status.deleted}).`,
6602
6634
  confidence: "high"
6603
6635
  },
6604
- ...untrackedWarning.length > 0 ? { warnings: untrackedWarning } : {}
6636
+ ...diffWarnings.length > 0 ? { warnings: diffWarnings } : {}
6605
6637
  });
6606
6638
  break;
6607
6639
  }
@@ -6625,7 +6657,7 @@ async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
6625
6657
  await runPromptRegistryRead(subcommand, positionals, flags, profileOptions, context);
6626
6658
  break;
6627
6659
  default:
6628
- throw new Error(`Unknown prompts command '${subcommand}'. Use init, status, diff, sync, promote, ab, propose, list, show, proposals, usage, replay-runs, or decide.`);
6660
+ throw new Error(`Unknown prompts command '${subcommand}'. Use init, status, diff, migrate, sync, promote, ab, propose, list, show, proposals, usage, replay-runs, or decide.`);
6629
6661
  }
6630
6662
  return 0;
6631
6663
  }
@@ -6640,7 +6672,7 @@ async function initPrompts(options = {}) {
6640
6672
  if (!existsSync2(MANIFEST_PATH)) {
6641
6673
  assertContainedSource(cwd, MODA_DIR);
6642
6674
  try {
6643
- writeFileSync2(manifestPath, ["version: 1", "prompt_paths:", ...DEFAULT_PROMPT_PATHS.map((path) => ` - "${path}"`), ""].join(`
6675
+ writeFileSync2(manifestPath, ["version: 2", "prompt_paths:", ...DEFAULT_PROMPT_PATHS.map((path) => ` - "${path}"`), "sources: []", ""].join(`
6644
6676
  `), { flag: "wx" });
6645
6677
  created = true;
6646
6678
  } catch (error) {
@@ -6698,20 +6730,26 @@ async function runPromptSync(flags, profileOptions, options = {}) {
6698
6730
  }
6699
6731
  }
6700
6732
  async function runPromptSyncExclusive(flags, profileOptions, options) {
6733
+ let harnessImport;
6701
6734
  if (flags["from-harness"] && flags["dry-run"] !== "true")
6702
- importHarnessPromptSources(flags["from-harness"]);
6735
+ harnessImport = importHarnessPromptSources(flags["from-harness"]);
6703
6736
  if (flags["dry-run"] !== "true")
6704
6737
  await initPrompts({ silent: true });
6705
6738
  const manifest = readManifest();
6706
6739
  const originalManifestHash = hashFile(MANIFEST_PATH);
6707
- let extraSources = flags["from-harness"] && flags["dry-run"] === "true" ? readHarnessSources(flags["from-harness"]) : [];
6740
+ let extraSources = [];
6741
+ if (flags["from-harness"] && flags["dry-run"] === "true") {
6742
+ const read = readHarnessSources(flags["from-harness"]);
6743
+ extraSources = read.sources;
6744
+ harnessImport = read.result;
6745
+ }
6708
6746
  mergePromptSources(manifest.sources, extraSources);
6709
6747
  const established = discoverPrompts(extraSources);
6710
6748
  let discovery;
6711
6749
  const analyzedDefinitions = new Map;
6712
6750
  if (flags["no-analyze"] !== "true") {
6713
6751
  options.onProgress?.("Analyzing prompt sources before sync.");
6714
- const { analyzeHarnessPrompts } = await import("./harness-s13wvdxt.js");
6752
+ const { analyzeHarnessPrompts } = await import("./harness-sd44jsaw.js");
6715
6753
  const result = await analyzeHarnessPrompts({
6716
6754
  rootDir: process.cwd(),
6717
6755
  existing: established.flatMap((prompt) => prompt.sourceDefinition ? [prompt.sourceDefinition.source] : []),
@@ -6761,7 +6799,7 @@ async function runPromptSyncExclusive(flags, profileOptions, options) {
6761
6799
  if (prompts.length === 0) {
6762
6800
  if (!dryRun && flags["allow-untrack"] === "true")
6763
6801
  writeLock([], []);
6764
- return { synced: [], dryRun, discovery, attribution };
6802
+ return { synced: [], dryRun, discovery, attribution, ...harnessImport ? { harnessImport } : {} };
6765
6803
  }
6766
6804
  const signature = stableJson(prompts);
6767
6805
  const response = await callControlAPI("/prompts/sync", {
@@ -6793,7 +6831,7 @@ async function runPromptSyncExclusive(flags, profileOptions, options) {
6793
6831
  atomicWrite(MANIFEST_PATH, manifestUpdate);
6794
6832
  writeLock(prompts, synced);
6795
6833
  }
6796
- return { synced: response.synced ?? [], dryRun, discovery, attribution };
6834
+ return { synced: response.synced ?? [], dryRun, discovery, attribution, ...harnessImport ? { harnessImport } : {} };
6797
6835
  }
6798
6836
  function writePromptSyncProgress(message, context, quiet = false) {
6799
6837
  if (quiet || context?.quiet)
@@ -6813,10 +6851,12 @@ async function syncPromptsOnce(flags, profileOptions, context) {
6813
6851
  synced: result.synced,
6814
6852
  discovery: result.discovery,
6815
6853
  attribution: result.attribution,
6854
+ ...result.harnessImport ? { harnessImport: result.harnessImport } : {},
6816
6855
  lockfile: result.dryRun ? undefined : LOCK_PATH
6817
6856
  };
6857
+ const skippedWarning = result.harnessImport?.skipped.length ? [harnessSkipWarning(result.harnessImport)] : [];
6818
6858
  if (context)
6819
- context.output.writeData(payload);
6859
+ context.output.writeData(payload, skippedWarning.length ? { warnings: skippedWarning } : undefined);
6820
6860
  else
6821
6861
  printJson(payload);
6822
6862
  if (!result.dryRun && result.synced.length === 0 && result.discovery?.candidates?.length) {
@@ -6885,9 +6925,15 @@ async function getPromptStatus() {
6885
6925
  });
6886
6926
  }
6887
6927
  }
6928
+ const inspection = inspectPromptManifest();
6888
6929
  return {
6889
6930
  manifest: MANIFEST_PATH,
6890
6931
  lockfile: LOCK_PATH,
6932
+ manifestVersion: inspection.manifestVersion,
6933
+ legacyManifest: inspection.legacyManifest,
6934
+ zeroMatchPatterns: inspection.zeroMatchPatterns,
6935
+ legacyOnlyFiles: inspection.legacyOnlyFiles,
6936
+ sources: inspection.sources,
6891
6937
  total: rows.length,
6892
6938
  changed: rows.filter((row) => row.state === "changed").length,
6893
6939
  new: rows.filter((row) => row.state === "new").length,
@@ -6896,7 +6942,9 @@ async function getPromptStatus() {
6896
6942
  };
6897
6943
  }
6898
6944
  function discoverPrompts(extraSources = []) {
6899
- const manifest = readManifest();
6945
+ return discoverManifestPrompts(readManifest(), extraSources);
6946
+ }
6947
+ function discoverManifestPrompts(manifest, extraSources = []) {
6900
6948
  const root = process.cwd();
6901
6949
  for (const pattern of manifest.promptPaths) {
6902
6950
  const positive = pattern.startsWith("!") ? pattern.slice(1) : pattern;
@@ -6912,7 +6960,7 @@ function discoverPrompts(extraSources = []) {
6912
6960
  followSymbolicLinks: false,
6913
6961
  dot: true,
6914
6962
  unique: true,
6915
- ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/.env*", "**/*.pem", "**/*.key"]
6963
+ ignore: DISCOVERY_IGNORE
6916
6964
  }).sort();
6917
6965
  const sources = new Map(manifest.sources.map((source) => [source.key, source]));
6918
6966
  for (const source of extraSources)
@@ -6934,6 +6982,185 @@ function discoverPrompts(extraSources = []) {
6934
6982
  }
6935
6983
  return prompts.sort((a, b) => a.key.localeCompare(b.key));
6936
6984
  }
6985
+ var LEGACY_PROMPT_SUFFIXES = [".prompt.md", ".prompt.json", ".prompt.yaml", ".prompt.yml"];
6986
+ function repoPath(root, path) {
6987
+ return relative(root, resolve4(root, path)).replaceAll("\\", "/");
6988
+ }
6989
+ function globPromptFiles(root, patterns, ignore = []) {
6990
+ if (patterns.length === 0)
6991
+ return [];
6992
+ return import_fast_glob.default.sync(patterns, {
6993
+ cwd: root,
6994
+ onlyFiles: true,
6995
+ followSymbolicLinks: false,
6996
+ dot: true,
6997
+ unique: true,
6998
+ ignore: [...DISCOVERY_IGNORE, ...ignore]
6999
+ }).sort();
7000
+ }
7001
+ function inspectPromptManifest(root = process.cwd()) {
7002
+ const manifest = readManifest(root);
7003
+ const safe = (pattern) => !isAbsolute3(pattern) && !pattern.split(/[\\/]/).includes("..");
7004
+ const positives = manifest.promptPaths.filter((pattern) => !pattern.startsWith("!") && safe(pattern));
7005
+ const negations = manifest.promptPaths.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
7006
+ const patterns = manifest.promptPaths.filter((pattern) => !pattern.startsWith("!")).map((pattern) => ({
7007
+ pattern,
7008
+ matches: safe(pattern) ? globPromptFiles(root, [pattern], negations).length : 0
7009
+ }));
7010
+ const matched = new Set(globPromptFiles(root, positives, negations));
7011
+ const owned = new Set(manifest.sources.map((source) => repoPath(root, source.path)));
7012
+ const suffixes = [...new Set(manifest.promptPaths.filter((pattern) => pattern.includes(".prompt.")).flatMap((pattern) => LEGACY_PROMPT_SUFFIXES.filter((suffix) => pattern.endsWith(suffix))))];
7013
+ const legacySuffixes = suffixes.length ? suffixes : [".prompt.md", ".prompt.json"];
7014
+ const tracked = manifest.version === 2 ? lockedSourcePaths(root) : null;
7015
+ const legacyOnlyFiles = manifest.version === null ? [] : globPromptFiles(root, legacySuffixes.map((suffix) => `**/*${suffix}`), negations).filter((path) => !matched.has(path) && !owned.has(path) && (!tracked || tracked.has(path)));
7016
+ return {
7017
+ manifestExists: manifest.version !== null,
7018
+ manifestVersion: manifest.version,
7019
+ legacyManifest: manifest.version === 1,
7020
+ patterns,
7021
+ zeroMatchPatterns: patterns.filter((row) => row.matches === 0).map((row) => row.pattern),
7022
+ sources: manifest.sources.length,
7023
+ matchedFiles: matched.size,
7024
+ legacyOnlyFiles
7025
+ };
7026
+ }
7027
+ function lockedSourcePaths(root) {
7028
+ const lockPath = resolve4(root, LOCK_PATH);
7029
+ if (!existsSync2(lockPath))
7030
+ return new Set;
7031
+ try {
7032
+ const lock = JSON.parse(readFileSync(lockPath, "utf8"));
7033
+ return new Set(Object.values(lock.prompts ?? {}).filter((entry) => entry.sourcePath).map((entry) => repoPath(root, String(entry.sourcePath))));
7034
+ } catch {
7035
+ return new Set;
7036
+ }
7037
+ }
7038
+ function promptManifestWarnings(inspection) {
7039
+ if (inspection.manifestVersion === null)
7040
+ return [];
7041
+ const warnings = [];
7042
+ const legacyCount = inspection.legacyOnlyFiles.length;
7043
+ if (legacyCount > 0) {
7044
+ const sample = inspection.legacyOnlyFiles.slice(0, 3).join(", ");
7045
+ warnings.push(`${legacyCount} prompt file(s) (${sample}${legacyCount > 3 ? ", ..." : ""}) were discovered by older CLIs but match no ${MANIFEST_PATH} pattern now: patterns are exact globs from the repository root. Run \`moda prompts migrate --dry-run\`, then \`moda prompts migrate\`, to keep them tracked under their existing keys.`);
7046
+ }
7047
+ if (inspection.zeroMatchPatterns.length > 0) {
7048
+ warnings.push(`prompt_paths pattern(s) match no files: ${inspection.zeroMatchPatterns.join(", ")}. Fix the globs (relative to the repository root), map code-defined prompts under \`sources:\`, or preview discovery with \`moda prompts sync --dry-run\`.`);
7049
+ }
7050
+ if (inspection.legacyManifest && warnings.length > 0) {
7051
+ warnings.push(`${MANIFEST_PATH} is a version 1 manifest; \`moda prompts migrate\` rewrites it to version 2 (explicit prompt_paths plus a sources: list).`);
7052
+ }
7053
+ return warnings;
7054
+ }
7055
+ function migrateWarnings(result) {
7056
+ const warnings = [];
7057
+ if (result.zeroMatchPatterns.length > 0) {
7058
+ warnings.push(`Kept prompt_paths pattern(s) that match no files: ${result.zeroMatchPatterns.join(", ")}. Fix or remove them.`);
7059
+ }
7060
+ if (result.pendingSources.length > 0) {
7061
+ const sample = result.pendingSources.slice(0, 3).map((source) => source.key).join(", ");
7062
+ warnings.push(`${result.pendingSources.length} code-defined prompt candidate(s) (${sample}${result.pendingSources.length > 3 ? ", ..." : ""}) were found but not added: --discover never runs an analyst, and code mappings need independent verification. Run \`${HARNESS_REANALYZE_COMMAND}\`, then \`moda prompts sync --from-harness\`, or map them under \`sources:\` yourself. See pendingSources.`);
7063
+ }
7064
+ return warnings;
7065
+ }
7066
+ async function migratePromptManifest(options = {}) {
7067
+ const root = process.cwd();
7068
+ const manifestPath = resolve4(root, MANIFEST_PATH);
7069
+ if (!existsSync2(manifestPath)) {
7070
+ throw new CliInputError(`No ${MANIFEST_PATH} to migrate in ${root}.`, "Run `moda prompts init` to create a version 2 manifest.");
7071
+ }
7072
+ const original = readFileSync(manifestPath, "utf8");
7073
+ const manifest = readManifest(root);
7074
+ const inspection = inspectPromptManifest(root);
7075
+ const addedPaths = inspection.legacyOnlyFiles;
7076
+ const addedPatterns = addedPaths.map((path) => import_fast_glob.default.escapePath(path));
7077
+ const promptPaths = [...manifest.promptPaths, ...addedPatterns];
7078
+ let sources = manifest.sources;
7079
+ let addedSources = [];
7080
+ let pendingSources = [];
7081
+ if (options.discover) {
7082
+ const { analyzeHarnessPrompts } = await import("./harness-sd44jsaw.js");
7083
+ const result = await analyzeHarnessPrompts({
7084
+ rootDir: root,
7085
+ existing: manifest.sources,
7086
+ flags: { analyst: "local-scan" },
7087
+ env: options.env ?? process.env,
7088
+ dryRun: true
7089
+ });
7090
+ const negations = promptPaths.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
7091
+ const claimed = new Set([
7092
+ ...globPromptFiles(root, promptPaths.filter((pattern) => !pattern.startsWith("!"))),
7093
+ ...globPromptFiles(root, negations)
7094
+ ]);
7095
+ const existingKeys = new Set(sources.map((source) => source.key));
7096
+ const found = result.sources.filter((source) => !existingKeys.has(source.key) && !claimed.has(repoPath(root, source.path)));
7097
+ sources = mergePromptSources(sources, found);
7098
+ addedSources = found.map((source) => source.key).sort();
7099
+ pendingSources = (result.pending ?? []).filter((candidate) => !existingKeys.has(candidate.source.key) && !claimed.has(repoPath(root, candidate.source.path))).map((candidate) => ({
7100
+ key: candidate.source.key,
7101
+ path: candidate.source.path,
7102
+ ...candidate.source.selector?.symbol ? { symbol: candidate.source.selector.symbol } : {},
7103
+ reason: candidate.reason ?? "Awaiting independent semantic verification"
7104
+ })).sort((a, b) => a.key.localeCompare(b.key));
7105
+ }
7106
+ try {
7107
+ discoverManifestPrompts({ version: 2, promptPaths, sources });
7108
+ } catch (error) {
7109
+ throw new CliInputError(`Cannot migrate ${MANIFEST_PATH}: ${error instanceof Error ? error.message : String(error)}`, `Give the conflicting prompt a unique \`key:\` (frontmatter or ${MANIFEST_PATH} sources), then rerun \`moda prompts migrate --dry-run\`. Nothing was written.`);
7110
+ }
7111
+ const expected = stableJson({ version: 2, prompt_paths: promptPaths, sources });
7112
+ const serialize = (inPlace) => {
7113
+ const doc = $parseDocument(original, { uniqueKeys: true });
7114
+ doc.set("version", 2);
7115
+ const pathsNode = doc.get("prompt_paths", true);
7116
+ if (inPlace && $isSeq(pathsNode)) {
7117
+ for (const pattern of addedPatterns)
7118
+ pathsNode.items.push(doc.createNode(pattern));
7119
+ } else {
7120
+ doc.set("prompt_paths", promptPaths);
7121
+ }
7122
+ const existingKeys = new Set(manifest.sources.map((source) => source.key));
7123
+ const newSources = sources.filter((source) => !existingKeys.has(source.key));
7124
+ const sourcesNode = doc.get("sources", true);
7125
+ if (inPlace && $isSeq(sourcesNode)) {
7126
+ for (const source of newSources)
7127
+ sourcesNode.items.push(doc.createNode(source));
7128
+ } else if (!inPlace || newSources.length > 0 || sourcesNode === undefined) {
7129
+ doc.set("sources", sources);
7130
+ }
7131
+ return doc.toString();
7132
+ };
7133
+ const matchesExpected = (text) => {
7134
+ const parsed = $parseDocument(text, { uniqueKeys: true }).toJS({ maxAliasCount: 20 }) ?? {};
7135
+ return stableJson({ version: parsed.version, prompt_paths: parsed.prompt_paths ?? [], sources: parsed.sources ?? [] }) === expected;
7136
+ };
7137
+ let content = serialize(true);
7138
+ if (!matchesExpected(content))
7139
+ content = serialize(false);
7140
+ if (!matchesExpected(content)) {
7141
+ throw new CliInputError(`Cannot migrate ${MANIFEST_PATH}: the rewritten manifest does not round-trip (shared YAML anchors or aliases?). Nothing was written.`, `Replace anchors/aliases in prompt_paths and sources with plain lists, then rerun \`moda prompts migrate --dry-run\`.`);
7142
+ }
7143
+ const changed = content !== original;
7144
+ if (!options.dryRun && changed) {
7145
+ if (hashFile(MANIFEST_PATH) !== sha256(original))
7146
+ throw new Error(`${MANIFEST_PATH} changed during migration. Retry.`);
7147
+ atomicWrite(MANIFEST_PATH, content);
7148
+ }
7149
+ return {
7150
+ manifest: MANIFEST_PATH,
7151
+ manifestPath,
7152
+ dryRun: options.dryRun === true,
7153
+ changed,
7154
+ fromVersion: manifest.version ?? 2,
7155
+ toVersion: 2,
7156
+ addedPaths,
7157
+ addedSources,
7158
+ pendingSources,
7159
+ zeroMatchPatterns: inspection.zeroMatchPatterns,
7160
+ promptPaths,
7161
+ content
7162
+ };
7163
+ }
6937
7164
  function assertContainedSource(root, path) {
6938
7165
  const abs = resolve4(root, path);
6939
7166
  const rel = relative(realpathSync(root), realpathSync(abs));
@@ -6987,18 +7214,6 @@ function parsePromptFile(absPath) {
6987
7214
  });
6988
7215
  }
6989
7216
  function normalizePrompt(prompt) {
6990
- const normalized = stableJson({
6991
- key: prompt.key,
6992
- content: prompt.content,
6993
- systemPrompt: prompt.systemPrompt ?? null,
6994
- messages: prompt.messages ?? [],
6995
- variables: prompt.variables ?? [],
6996
- variablesSchema: prompt.variablesSchema ?? {},
6997
- modelConfig: prompt.modelConfig ?? {},
6998
- toolRefs: prompt.toolRefs ?? [],
6999
- responseSchema: prompt.responseSchema ?? null,
7000
- ...prompt.sourceDefinition ? { sourceDefinition: promptSourceHashPayload(prompt.sourceDefinition) } : {}
7001
- });
7002
7217
  return {
7003
7218
  key: prompt.key,
7004
7219
  name: prompt.name || prompt.key,
@@ -7013,28 +7228,14 @@ function normalizePrompt(prompt) {
7013
7228
  modelConfig: prompt.modelConfig,
7014
7229
  toolRefs: prompt.toolRefs,
7015
7230
  responseSchema: prompt.responseSchema,
7016
- contentHash: sha256(normalized)
7231
+ contentHash: computePromptContentHash(prompt)
7017
7232
  };
7018
7233
  }
7019
- function promptSourceHashPayload(definition) {
7020
- return {
7021
- kind: definition.kind,
7022
- source: definition.source,
7023
- ...["text", "messages"].includes(definition.kind) ? {
7024
- sourceHash: definition.sourceHash
7025
- } : ["builder", "template"].includes(definition.kind) ? {
7026
- sourceHash: definition.sourceHash,
7027
- dependencies: definition.dependencies,
7028
- coverage: definition.coverage,
7029
- unresolved: definition.unresolved
7030
- } : {}
7031
- };
7032
- }
7033
- function readManifest() {
7034
- if (!existsSync2(MANIFEST_PATH))
7035
- return { promptPaths: DEFAULT_PROMPT_PATHS, sources: [] };
7036
- assertContainedSource(process.cwd(), MANIFEST_PATH);
7037
- const doc = $parseDocument(readFileSync(MANIFEST_PATH, "utf8"), { uniqueKeys: true });
7234
+ function readManifest(root = process.cwd()) {
7235
+ if (!existsSync2(resolve4(root, MANIFEST_PATH)))
7236
+ return { version: null, promptPaths: DEFAULT_PROMPT_PATHS, sources: [] };
7237
+ assertContainedSource(root, MANIFEST_PATH);
7238
+ const doc = $parseDocument(readFileSync(resolve4(root, MANIFEST_PATH), "utf8"), { uniqueKeys: true });
7038
7239
  if (doc.errors.length)
7039
7240
  throw new Error(`Invalid ${MANIFEST_PATH}: ${doc.errors[0].message}`);
7040
7241
  const parsed = doc.toJS({ maxAliasCount: 20 });
@@ -7055,33 +7256,96 @@ function readManifest() {
7055
7256
  throw new Error(`Duplicate prompt source key '${source.key}'.`);
7056
7257
  seen.add(source.key);
7057
7258
  }
7058
- return { promptPaths: parsed.prompt_paths ?? (parsed.version === 1 ? DEFAULT_PROMPT_PATHS : []), sources };
7259
+ return { version: parsed.version, promptPaths: parsed.prompt_paths ?? (parsed.version === 1 ? DEFAULT_PROMPT_PATHS : []), sources };
7059
7260
  }
7261
+ var HARNESS_REANALYZE_COMMAND = "moda harness analyze --experimental --concern=prompts";
7060
7262
  function readHarnessSources(reportPath) {
7061
7263
  const selected = reportPath === "true" ? ".moda/harness-report.json" : reportPath;
7062
7264
  assertContainedSource(process.cwd(), selected);
7063
- const report = JSON.parse(readFileSync(resolve4(selected), "utf8"));
7064
- if (!Array.isArray(report?.graph?.artifacts))
7065
- throw new Error("Expected a harness report with graph.artifacts.");
7066
- const artifacts = report.graph.artifacts.filter((artifact) => artifact.type === "prompt");
7265
+ let raw;
7266
+ try {
7267
+ raw = JSON.parse(readFileSync(resolve4(selected), "utf8"));
7268
+ } catch (error) {
7269
+ throw new CliInputError(`Could not read harness report ${selected}: ${error instanceof Error ? error.message : String(error)}.`, `Regenerate it with \`${HARNESS_REANALYZE_COMMAND}\`.`);
7270
+ }
7271
+ const record = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
7272
+ const graph = record.graph && typeof record.graph === "object" ? record.graph : undefined;
7273
+ const schemaVersion = typeof record.schemaVersion === "string" ? record.schemaVersion : typeof graph?.schemaVersion === "string" ? graph.schemaVersion : typeof graph?.graphSchemaVersion === "string" ? graph.graphSchemaVersion : null;
7274
+ const schemaLabel = schemaVersion ?? "no schemaVersion";
7275
+ if (!graph) {
7276
+ throw new CliInputError(`${selected} is not a harness report with a graph (schema: ${schemaLabel}). Regenerate it with \`${HARNESS_REANALYZE_COMMAND}\`.`);
7277
+ }
7278
+ const report = normalizeHarnessReport(raw);
7279
+ const artifacts = (report.graph.artifacts ?? []).filter((artifact) => artifact.type === "prompt");
7067
7280
  const sources = [];
7281
+ const skipped = [];
7282
+ const tracked = [];
7283
+ const root = process.cwd();
7284
+ const manifest = readManifest(root);
7285
+ const negations = manifest.promptPaths.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
7286
+ const globbed = new Set(globPromptFiles(root, manifest.promptPaths.filter((pattern) => !pattern.startsWith("!")), negations));
7287
+ const ownedKeys = new Map(manifest.sources.map((source) => [repoPath(root, source.path), source.key]));
7068
7288
  for (const artifact of artifacts) {
7069
- if (!artifact.promptSource)
7070
- throw new Error(`Harness prompt '${artifact.name ?? artifact.id}' has no verified source mapping. Run harness analyze --concern=prompts; legacy body snippets cannot be registered.`);
7071
- const resolved = resolvePromptSource(process.cwd(), artifact.promptSource);
7072
- sources.push(resolved.sourceDefinition.source);
7289
+ const label = String(artifact.name ?? artifact.id ?? "unnamed prompt");
7290
+ let mapping;
7291
+ try {
7292
+ mapping = backfillPromptSource(process.cwd(), artifact);
7293
+ } catch (error) {
7294
+ skipped.push({ artifact: label, reason: `source mapping could not be derived: ${error instanceof Error ? error.message : String(error)}` });
7295
+ continue;
7296
+ }
7297
+ if (!mapping) {
7298
+ skipped.push({
7299
+ artifact: label,
7300
+ reason: artifact.sourcePath ? `no promptSource, and no single definition in ${artifact.sourcePath} matches its name` : "no promptSource or sourcePath (legacy body snippets cannot be registered)"
7301
+ });
7302
+ continue;
7303
+ }
7304
+ if (!artifact.promptSource && !mapping.selector) {
7305
+ const path = repoPath(root, mapping.path);
7306
+ const nativeFile = /\.prompt\.(md|json)$/i.test(path);
7307
+ const ownedKey = ownedKeys.get(path);
7308
+ if (ownedKey !== undefined || globbed.has(path)) {
7309
+ let key = ownedKey ?? keyFromPath(path);
7310
+ try {
7311
+ if (ownedKey === undefined && nativeFile)
7312
+ key = parsePromptFile(resolve4(root, path)).key;
7313
+ } catch {}
7314
+ tracked.push({ artifact: label, key, path });
7315
+ continue;
7316
+ }
7317
+ if (nativeFile) {
7318
+ skipped.push({
7319
+ artifact: label,
7320
+ reason: `${path} is a dedicated prompt file; add it to prompt_paths (\`moda prompts migrate\` adds files older CLIs discovered) so it keeps its own key and parser`
7321
+ });
7322
+ continue;
7323
+ }
7324
+ if (/\.prompt\.ya?ml$/i.test(path))
7325
+ mapping = { ...mapping, key: keyFromPath(path) };
7326
+ }
7327
+ try {
7328
+ sources.push(resolvePromptSource(process.cwd(), mapping).sourceDefinition.source);
7329
+ } catch (error) {
7330
+ skipped.push({ artifact: label, reason: error instanceof Error ? error.message : String(error) });
7331
+ }
7332
+ }
7333
+ if (!sources.length && !tracked.length) {
7334
+ const detail = skipped.length ? ` ${skipped.length} prompt artifact(s) skipped: ${skipped.slice(0, 5).map((skip) => `'${skip.artifact}' (${skip.reason})`).join("; ")}${skipped.length > 5 ? "; ..." : ""}.` : " It contains no prompt artifacts.";
7335
+ throw new CliInputError(`Harness report ${selected} (schema: ${schemaLabel}) has no prompt that maps to current source.${detail} Regenerate it with \`${HARNESS_REANALYZE_COMMAND}\`.`, `Or map prompts directly under \`sources:\` in ${MANIFEST_PATH}.`);
7073
7336
  }
7074
- if (!sources.length)
7075
- throw new Error("Harness report contains no mapped prompts to register.");
7076
- return sources;
7337
+ return { sources, result: { report: selected, schemaVersion, registered: sources.map((source) => source.key), skipped, tracked } };
7338
+ }
7339
+ function harnessSkipWarning(result) {
7340
+ return `${result.skipped.length} harness prompt(s) skipped (registered ${result.registered.length}): ${result.skipped.slice(0, 3).map((skip) => `'${skip.artifact}' (${skip.reason})`).join("; ")}${result.skipped.length > 3 ? "; ..." : ""}. See harnessImport.skipped; regenerate the report with \`${HARNESS_REANALYZE_COMMAND}\` to map them.`;
7077
7341
  }
7078
7342
  function importHarnessPromptSources(reportPath = ".moda/harness-report.json") {
7079
- const sources = readHarnessSources(reportPath);
7343
+ const { sources, result } = readHarnessSources(reportPath);
7080
7344
  const manifest = readManifest();
7081
7345
  const merged = mergePromptSources(manifest.sources, sources);
7082
7346
  discoverPrompts(sources);
7083
7347
  atomicWrite(MANIFEST_PATH, $stringify({ version: 2, prompt_paths: manifest.promptPaths, sources: merged }));
7084
- return { registered: sources.map((source) => source.key) };
7348
+ return result;
7085
7349
  }
7086
7350
  function mergePromptSources(existingSources, sources) {
7087
7351
  const merged = new Map(existingSources.map((source) => [source.key, source]));
@@ -7174,17 +7438,6 @@ function getGitCommit() {
7174
7438
  return;
7175
7439
  }
7176
7440
  }
7177
- function stableJson(value) {
7178
- return JSON.stringify(sortJson(value));
7179
- }
7180
- function sortJson(value) {
7181
- if (Array.isArray(value))
7182
- return value.map(sortJson);
7183
- if (value && typeof value === "object") {
7184
- return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson(item)]));
7185
- }
7186
- return value;
7187
- }
7188
7441
  function sha256(input) {
7189
7442
  return createHash("sha256").update(input).digest("hex");
7190
7443
  }
@@ -7304,6 +7557,9 @@ function renderFixesBlock() {
7304
7557
  "- Propose the change in the code, prompt, tool definition, or skill that",
7305
7558
  " causes the issue, and cite the Moda problem or trace ids it is based on",
7306
7559
  " in the PR description.",
7560
+ "- Prompts live in `.moda/prompts.yml` (version 2: `prompt_paths` globs plus",
7561
+ " `sources:` mappings for prompts defined in code); after `moda prompts sync`,",
7562
+ " pass `prompt: lock.prompts['<key>']` to createThinkTelemetry/createAI7Telemetry.",
7307
7563
  "- After the fix ships, record it with",
7308
7564
  " `moda problem-feedback <problem_id> --action=mark_fixed`; if the issue",
7309
7565
  " is detected again, the Problem comes back.",
@@ -8315,10 +8571,7 @@ async function buildDoctorReport(context, options) {
8315
8571
  });
8316
8572
  checks.push(harnessCurrentCheck(context.cwd, manifestPath, manifestExists, tenantId));
8317
8573
  checks.push(harnessSyncStateCheck(context.cwd));
8318
- checks.push(discoveryCheck("prompts_discovered", "harness", "Prompts are discovered", context.cwd, [
8319
- "prompts",
8320
- ".moda/prompts.yml"
8321
- ], "moda prompts init"));
8574
+ checks.push(promptsDiscoveredCheck(context.cwd));
8322
8575
  checks.push(discoveryCheck("skills_discovered", "harness", "Skills are discovered", context.cwd, [
8323
8576
  "skills",
8324
8577
  ".claude/skills",
@@ -8588,6 +8841,56 @@ function harnessSyncStateCheck(cwd) {
8588
8841
  ]
8589
8842
  };
8590
8843
  }
8844
+ function promptsDiscoveredCheck(cwd) {
8845
+ const base = { id: "prompts_discovered", group: "harness", title: "Prompts are discovered" };
8846
+ if (!existsSync5(join4(cwd, ".moda", "prompts.yml"))) {
8847
+ return discoveryCheck(base.id, base.group, base.title, cwd, ["prompts", ".moda/prompts.yml"], "moda prompts init");
8848
+ }
8849
+ let inspection;
8850
+ try {
8851
+ inspection = inspectPromptManifest(cwd);
8852
+ } catch (error) {
8853
+ return {
8854
+ ...base,
8855
+ status: "fail",
8856
+ severity: "warning",
8857
+ evidence: [{ label: "manifest", value: ".moda/prompts.yml" }, { label: "error", value: error instanceof Error ? error.message : String(error) }],
8858
+ remediation: "Fix .moda/prompts.yml (version, prompt_paths, sources).",
8859
+ nextCommand: "moda prompts status"
8860
+ };
8861
+ }
8862
+ const evidence = [
8863
+ { label: "manifest", value: `.moda/prompts.yml (version ${inspection.manifestVersion})` },
8864
+ { label: "matched_files", value: String(inspection.matchedFiles) },
8865
+ { label: "sources", value: String(inspection.sources) },
8866
+ ...inspection.zeroMatchPatterns.length > 0 ? [{ label: "zero_match_patterns", value: inspection.zeroMatchPatterns.join(", ") }] : [],
8867
+ ...inspection.legacyOnlyFiles.length > 0 ? [{
8868
+ label: "legacy_only_files",
8869
+ value: `${inspection.legacyOnlyFiles.length} (${inspection.legacyOnlyFiles.slice(0, 3).join(", ")}${inspection.legacyOnlyFiles.length > 3 ? ", ..." : ""})`
8870
+ }] : []
8871
+ ];
8872
+ if (inspection.legacyOnlyFiles.length > 0) {
8873
+ return {
8874
+ ...base,
8875
+ status: "warning",
8876
+ severity: "warning",
8877
+ evidence,
8878
+ remediation: "Prompt files found by older CLIs no longer match the manifest globs (they are exact paths from the repository root). Migrate the manifest to keep them tracked under their existing keys.",
8879
+ nextCommand: "moda prompts migrate --dry-run"
8880
+ };
8881
+ }
8882
+ if (inspection.matchedFiles === 0 && inspection.sources === 0) {
8883
+ return {
8884
+ ...base,
8885
+ status: "warning",
8886
+ severity: "warning",
8887
+ evidence,
8888
+ remediation: "The prompt manifest matches no files and maps no sources, so sync registers nothing. Fix prompt_paths or preview source discovery.",
8889
+ nextCommand: "moda prompts sync --dry-run"
8890
+ };
8891
+ }
8892
+ return { ...base, status: "pass", severity: "info", evidence };
8893
+ }
8591
8894
  function discoveryCheck(id, group, title, cwd, relativePaths, nextCommand) {
8592
8895
  const found = relativePaths.filter((path) => existsSync5(join4(cwd, path)));
8593
8896
  return {
@@ -8884,7 +9187,7 @@ function agentPurposeForCommand(command) {
8884
9187
  return "Inspect or repair Moda setup.";
8885
9188
  }
8886
9189
  function stableGraphHash(graph) {
8887
- return createHash3("sha256").update(JSON.stringify(sortJson2({
9190
+ return createHash3("sha256").update(JSON.stringify(sortJson({
8888
9191
  ...graph,
8889
9192
  scan: {
8890
9193
  ...graph.scan,
@@ -8894,11 +9197,11 @@ function stableGraphHash(graph) {
8894
9197
  }
8895
9198
  }))).digest("hex");
8896
9199
  }
8897
- function sortJson2(value) {
9200
+ function sortJson(value) {
8898
9201
  if (Array.isArray(value))
8899
- return value.map((item) => sortJson2(item));
9202
+ return value.map((item) => sortJson(item));
8900
9203
  if (value && typeof value === "object") {
8901
- return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson2(item)]));
9204
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, sortJson(item)]));
8902
9205
  }
8903
9206
  return value;
8904
9207
  }
@@ -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,
@@ -2000,6 +2001,20 @@ function nextCommandsFor(fix) {
2000
2001
  }
2001
2002
  ];
2002
2003
  case "UNROUTED":
2004
+ return [
2005
+ {
2006
+ command: `moda fix ${fix.id} --packet`,
2007
+ purpose: "Read the investigation packet — this fix is deliverable as diagnosis.",
2008
+ mutability: "read",
2009
+ requires_approval: false
2010
+ },
2011
+ {
2012
+ command: "moda prompts status",
2013
+ 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).",
2014
+ mutability: "read",
2015
+ requires_approval: false
2016
+ }
2017
+ ];
2003
2018
  case "NO_EVIDENCE":
2004
2019
  case "BLOCKED_COVERAGE":
2005
2020
  case "NO_HEADROOM":
@@ -7963,9 +7978,10 @@ Commands:
7963
7978
  --signal=conversations = --signal=traces. <conversation_id> is the trace ID.
7964
7979
 
7965
7980
  Prompt management:
7966
- prompts init Create .moda/prompts.yml
7967
- prompts status Show local prompt changes
7981
+ prompts init Create .moda/prompts.yml (version 2: prompt_paths + sources)
7982
+ prompts status Show local prompt changes; warns on globs that match nothing
7968
7983
  prompts diff Only changed/new/deleted prompts since last sync
7984
+ prompts migrate [--dry-run] Rewrite a version 1 .moda/prompts.yml to version 2
7969
7985
  prompts sync Analyze prompt sources and upload versions
7970
7986
  prompts sync --watch Upload again when prompt files change
7971
7987
  prompts list|show|proposals|usage|replay-runs|decide
@@ -10132,7 +10148,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
10132
10148
  function telemetryCommandName(definition) {
10133
10149
  return definition.telemetryCommand ?? definition.name;
10134
10150
  }
10135
- var OFFLINE_PROMPTS_SUBCOMMANDS = new Set(["init", "status", "diff"]);
10151
+ var OFFLINE_PROMPTS_SUBCOMMANDS = new Set(["init", "status", "diff", "migrate"]);
10136
10152
  function legacyCommand(metadata) {
10137
10153
  return {
10138
10154
  ...metadata,
@@ -10226,7 +10242,7 @@ var commandRegistry = createCommandRegistry([
10226
10242
  resetApiRequestCountBeforeRun: true,
10227
10243
  telemetry: "result",
10228
10244
  handler: async (context) => {
10229
- const { runInit } = await import("./index-pykaaezb.js");
10245
+ const { runInit } = await import("./index-hn9t95tr.js");
10230
10246
  if (context.outputMode === "agent-stream") {
10231
10247
  context.output.writeEvent({
10232
10248
  event: "started",
@@ -10980,18 +10996,18 @@ var commandRegistry = createCommandRegistry([
10980
10996
  legacyCommand({
10981
10997
  name: "prompts",
10982
10998
  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"],
10999
+ 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
11000
  subcommands: [
10985
11001
  {
10986
11002
  name: "init",
10987
- description: "Create .moda/prompts.yml in the current directory (never overwrites)",
11003
+ 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
11004
  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." }],
11005
+ 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
11006
  examples: ["moda prompts init", "cd /path/to/project && moda prompts init"]
10991
11007
  },
10992
11008
  {
10993
11009
  name: "status",
10994
- description: "Show prompt files that changed since the last sync",
11010
+ 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
11011
  usage: "moda prompts status",
10996
11012
  examples: ["moda prompts status"]
10997
11013
  },
@@ -11001,6 +11017,16 @@ var commandRegistry = createCommandRegistry([
11001
11017
  usage: "moda prompts diff",
11002
11018
  examples: ["moda prompts diff"]
11003
11019
  },
11020
+ {
11021
+ name: "migrate",
11022
+ 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.",
11023
+ usage: "moda prompts migrate [--dry-run] [--discover]",
11024
+ flags: [
11025
+ { name: "--dry-run", description: "Print the migrated manifest without writing it." },
11026
+ { 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." }
11027
+ ],
11028
+ examples: ["moda prompts migrate --dry-run", "moda prompts migrate", "moda prompts migrate --discover"]
11029
+ },
11004
11030
  {
11005
11031
  name: "sync",
11006
11032
  description: "Analyze prompt sources and upload verified versions to Moda Cloud",
@@ -11009,7 +11035,7 @@ var commandRegistry = createCommandRegistry([
11009
11035
  { 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
11036
  { name: "--assist=none", description: "Disable agent fallback, including with an explicit analyst." },
11011
11037
  { 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." },
11038
+ { 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
11039
  { name: "--allow-untrack", description: "Explicitly remove missing keys from local tracking; registry history remains." },
11014
11040
  { name: "--watch", description: "Analyze before syncing; re-upload when mapped prompt files change." },
11015
11041
  { name: "--dry-run", description: "Preview without saving mappings or registry versions; deterministic unless an analyst is explicitly selected." }
@@ -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.54.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-12T07:41:12.130Z",
4
+ "cli_version": "1.54.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
 
@@ -773,17 +773,63 @@ dropped and `truncation.data.full_output_path` holds every example. Agent output
773
773
  Use these only when working on the code-first prompt registry.
774
774
 
775
775
  ```bash
776
- moda prompts init # create .moda/prompts.yml
777
- moda prompts status # read-only local status
776
+ moda prompts init # create .moda/prompts.yml (version 2)
777
+ moda prompts status # read-only local status + manifest warnings
778
778
  moda prompts diff # only changed/new/deleted since last sync
779
+ moda prompts migrate --dry-run # preview a version 1 -> 2 manifest rewrite
780
+ moda prompts migrate # apply it (atomic; comments kept)
781
+ moda prompts sync --dry-run # preview discovery + what would upload
779
782
  moda prompts sync # upload changed prompt versions
783
+ moda prompts sync --from-harness # register mappings from .moda/harness-report.json first
780
784
  moda prompts sync --watch # sync when local prompt hashes change
781
785
  moda prompts promote support.triage --label=prod --version=pver_abc123
782
786
  ```
783
787
 
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.
788
+ `status`, `diff` and `migrate --dry-run` are read-only. `sync` uploads prompt
789
+ versions and writes `.moda/prompts.lock.json`. `promote` moves a remote `dev`,
790
+ `staging`, or `prod` label to an existing version.
791
+
792
+ `.moda/prompts.yml` (version 2) has two ways to point at prompts:
793
+
794
+ ```yaml
795
+ version: 2
796
+ prompt_paths: # exact fast-glob patterns from the repo root
797
+ - "prompts/**/*.prompt.md"
798
+ - "!prompts/archive/**"
799
+ sources: # prompts defined in code or config, mapped in place
800
+ - key: support.system # stable registry key (also the lockfile key)
801
+ path: src/agent.ts
802
+ selector: { symbol: SYSTEM_PROMPT } # or { pointer: /support/prompt } for JSON/YAML
803
+ - key: support.build
804
+ path: src/prompt.py
805
+ selector: { symbol: build_prompt }
806
+ kind: builder # text | messages | template | builder
807
+ dependencies:
808
+ - path: config/policy.txt
809
+ ```
810
+
811
+ - `prompts status` returns `zeroMatchPatterns` (globs that match nothing),
812
+ `legacyOnlyFiles` and `legacyManifest`. A non-empty `legacyOnlyFiles` means a
813
+ manifest from an older CLI: those files used to sync (old discovery matched
814
+ `*.prompt.*` anywhere) but match nothing now. Run `moda prompts migrate
815
+ --dry-run`, then `moda prompts migrate`; it adds them as explicit paths so
816
+ their keys and lockfile entries are unchanged (`--discover` also scans for
817
+ code-defined prompts, but lists unverified ones under `pendingSources`
818
+ instead of adding them; verify those with `moda harness analyze
819
+ --experimental --concern=prompts`, then `sync --from-harness`). `moda
820
+ doctor` reports the same condition on its `prompts_discovered` check.
821
+ - `sync --from-harness` registers what resolves and lists the rest in
822
+ `harnessImport.skipped` with reasons; it fails only when nothing resolves
823
+ (regenerate with `moda harness analyze --experimental --concern=prompts`).
824
+ - Lockfile `contentHash` is `computePromptContentHash` from
825
+ `@moda-ai/cli/prompt-hash` (`PROMPT_HASH_VERSION` 1). Don't reimplement it.
826
+
827
+ To attribute runtime calls, stamp the lock entry `{ key, promptId, versionId }`
828
+ on telemetry. With Cloudflare Think / AI SDK 7, pass
829
+ `prompt: lock.prompts['<key>']` to `createThinkTelemetry` (`moda-ai/think`) or
830
+ `createAI7Telemetry` (`moda-ai/ai7`). A `moda fix` that lands `UNROUTED` with
831
+ "top share 0%" means the evidence carries no prompt version: wire this up,
832
+ then check `moda prompts status`.
787
833
 
788
834
  `prompts ab` and `prompts propose --gate` run paid replays. Before sending
789
835
  anything they compute planned playouts (cases × seeds × 2 arms) and print it