@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 +14 -2
- package/dist/{cli-he2wb4hd.js → cli-9250ckvg.js} +18 -14
- package/dist/{cli-ha40mgy1.js → cli-a2jq9yd5.js} +384 -81
- package/dist/cli-mm48br00.js +47 -0
- package/dist/cli.js +37 -11
- package/dist/{harness-github-actions-gbty3k66.js → harness-github-actions-075xzdvp.js} +1 -1
- package/dist/{harness-s13wvdxt.js → harness-sd44jsaw.js} +1 -1
- package/dist/{index-pykaaezb.js → index-hn9t95tr.js} +3 -2
- package/dist/prompt-hash.d.ts +36 -0
- package/dist/prompt-hash.js +15 -0
- package/dist/prompt-source-mcp.js +1 -1
- package/package.json +10 -2
- package/skills/integration/index.json +3 -3
- package/skills/integration/integration-cloudflare-think/SKILL.md +5 -2
- package/skills/moda-cli/SKILL.md +52 -6
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>`.
|
|
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-
|
|
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
|
-
|
|
185505
|
-
|
|
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,
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
-
...
|
|
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:
|
|
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 =
|
|
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-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
7231
|
+
contentHash: computePromptContentHash(prompt)
|
|
7017
7232
|
};
|
|
7018
7233
|
}
|
|
7019
|
-
function
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
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
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
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
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
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
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
9200
|
+
function sortJson(value) {
|
|
8898
9201
|
if (Array.isArray(value))
|
|
8899
|
-
return value.map((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,
|
|
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-
|
|
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-
|
|
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-
|
|
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." }
|
|
@@ -3,7 +3,8 @@ import {
|
|
|
3
3
|
initPrompts,
|
|
4
4
|
runPromptSync,
|
|
5
5
|
runSkillSync
|
|
6
|
-
} from "./cli-
|
|
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-
|
|
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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moda-ai/cli",
|
|
3
|
-
"version": "1.
|
|
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-
|
|
4
|
-
"cli_version": "1.
|
|
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.
|
|
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.
|
|
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
|
|
package/skills/moda-cli/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: moda-cli
|
|
3
|
-
version: 2.
|
|
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 `
|
|
785
|
-
`.moda/prompts.lock.json`. `promote` moves a remote `dev`,
|
|
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
|