@moda-ai/cli 1.50.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.
@@ -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-x0x0n35w.js";
23
+ } from "./cli-9250ckvg.js";
17
24
  import {
18
25
  SKILL_SOURCE_HARNESS,
19
26
  SKILL_SOURCE_INTEGRATION_BUNDLED,
@@ -26,7 +33,7 @@ import {
26
33
  import {
27
34
  isAuthSessionValid,
28
35
  loadAuthSession
29
- } from "./cli-7kbzqact.js";
36
+ } from "./cli-kxwm7y62.js";
30
37
  import {
31
38
  CliInputError,
32
39
  EXIT_ERROR,
@@ -45,7 +52,7 @@ import {
45
52
  resolveTenantId,
46
53
  secretFilePathFromRef,
47
54
  validateConfig
48
- } from "./cli-xrs02c0t.js";
55
+ } from "./cli-ac3p2byq.js";
49
56
  import {
50
57
  __commonJS,
51
58
  __require,
@@ -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-7gr0xbtc.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
  }