@moda-ai/cli 1.38.0 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/dist/{cli-3kcy61fr.js → cli-a7ypptsf.js} +515 -59
- package/dist/{cli-kegd748v.js → cli-dyeywy9d.js} +1 -1
- package/dist/{cli-q79sq80a.js → cli-rp912ypf.js} +163 -21
- package/dist/{cli-07tv75te.js → cli-xsmsjny3.js} +696 -139
- package/dist/{cli-k38j2fpq.js → cli-yy8fwg1a.js} +2 -2
- package/dist/cli.js +2522 -263
- package/dist/{harness-n2xgzz31.js → harness-191xt7bq.js} +2 -2
- package/dist/{harness-github-actions-qz5d22rz.js → harness-github-actions-1qt4sgxd.js} +2 -2
- package/dist/{index-hwfe7gs8.js → index-qgc37efj.js} +5 -5
- package/dist/prompt-source-mcp.js +2 -2
- package/dist/{provision-64c2n9aw.js → provision-gabtks29.js} +3 -3
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
- package/skills/moda-cli/SKILL.md +239 -20
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
$parseDocument,
|
|
3
3
|
$stringify,
|
|
4
|
+
assertFlags,
|
|
5
|
+
assertPositionals,
|
|
4
6
|
callControlAPI,
|
|
5
7
|
callDataAPI,
|
|
6
8
|
inventoryFiles,
|
|
@@ -11,7 +13,7 @@ import {
|
|
|
11
13
|
resolvePromptSource,
|
|
12
14
|
safePromptEvidencePath,
|
|
13
15
|
scanHarness
|
|
14
|
-
} from "./cli-
|
|
16
|
+
} from "./cli-xsmsjny3.js";
|
|
15
17
|
import {
|
|
16
18
|
SKILL_SOURCE_HARNESS,
|
|
17
19
|
SKILL_SOURCE_INTEGRATION_BUNDLED,
|
|
@@ -24,7 +26,7 @@ import {
|
|
|
24
26
|
import {
|
|
25
27
|
isAuthSessionValid,
|
|
26
28
|
loadAuthSession
|
|
27
|
-
} from "./cli-
|
|
29
|
+
} from "./cli-dyeywy9d.js";
|
|
28
30
|
import {
|
|
29
31
|
CliInputError,
|
|
30
32
|
createCommandContext,
|
|
@@ -34,13 +36,14 @@ import {
|
|
|
34
36
|
isLoopbackBaseUrl,
|
|
35
37
|
loadProfileConfig,
|
|
36
38
|
resolveApiKey,
|
|
39
|
+
resolveApiTenantId,
|
|
37
40
|
resolveIngestUrl,
|
|
38
41
|
resolveModaBaseUrl,
|
|
39
42
|
resolveSecretReference,
|
|
40
43
|
resolveTenantId,
|
|
41
44
|
secretFilePathFromRef,
|
|
42
45
|
validateConfig
|
|
43
|
-
} from "./cli-
|
|
46
|
+
} from "./cli-rp912ypf.js";
|
|
44
47
|
import {
|
|
45
48
|
__commonJS,
|
|
46
49
|
__require,
|
|
@@ -5564,54 +5567,87 @@ import {
|
|
|
5564
5567
|
import { dirname as dirname2, isAbsolute as isAbsolute3, join, relative, resolve as resolve4 } from "node:path";
|
|
5565
5568
|
|
|
5566
5569
|
// src/prompts-ab.ts
|
|
5567
|
-
import { existsSync } from "node:fs";
|
|
5570
|
+
import { accessSync, constants as fsConstants, existsSync, statSync } from "node:fs";
|
|
5568
5571
|
import { isAbsolute, resolve } from "node:path";
|
|
5569
5572
|
var DEFAULT_SEEDS_PER_CASE = 3;
|
|
5573
|
+
var DEFAULT_AUTO_GENERATE_CASES = 5;
|
|
5574
|
+
var MAX_AUTO_GENERATE_CASES = 100;
|
|
5575
|
+
var MAX_REPLAY_TRACES = 100;
|
|
5576
|
+
var PLAYOUT_CONFIRM_THRESHOLD = 200;
|
|
5577
|
+
var REPLAY_MODEL_ALLOWLIST = [
|
|
5578
|
+
"openai/gpt-5.6-luna",
|
|
5579
|
+
"openai/gpt-4o-mini",
|
|
5580
|
+
"openai/gpt-4o",
|
|
5581
|
+
"anthropic/claude-sonnet-4-5",
|
|
5582
|
+
"anthropic/claude-haiku-4-5",
|
|
5583
|
+
"google/gemini-2.5-flash",
|
|
5584
|
+
"google/gemini-2.5-pro"
|
|
5585
|
+
];
|
|
5570
5586
|
var DEFAULT_POLL_INTERVAL_MS = 15000;
|
|
5571
5587
|
var DEFAULT_WAIT_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
5572
5588
|
var TERMINAL_RUN_STATUSES = new Set(["completed", "skipped", "error"]);
|
|
5573
5589
|
async function runPromptAb(flags, profileOptions, context) {
|
|
5574
5590
|
validateConfig();
|
|
5575
|
-
const tenantId = flags["tenant-id"] ||
|
|
5591
|
+
const tenantId = flags["tenant-id"] || resolveApiTenantId(profileOptions);
|
|
5576
5592
|
if (!tenantId) {
|
|
5577
5593
|
throw new Error("Missing tenant id. Run `moda init` or pass --tenant-id=<id>.");
|
|
5578
5594
|
}
|
|
5579
5595
|
const baselineSource = flags.baseline || flags["baseline-file"] || flags["baseline-key"];
|
|
5580
5596
|
const candidateSource = flags.candidate || flags["candidate-file"] || flags["candidate-key"];
|
|
5581
5597
|
if (!baselineSource || !candidateSource) {
|
|
5582
|
-
throw new Error("Usage: moda prompts ab --baseline=<file|key> --candidate=<file|key> [--auto-generate|--set-id=ID|--traces=id1,id2] (legacy alias: --conversations=)");
|
|
5598
|
+
throw new Error("Usage: moda prompts ab --baseline=<file|key> --candidate=<file|key> [--auto-generate [--cases=1-100]|--set-id=ID|--traces=id1,id2 (max 100)] [--seeds=1-10] [--model=<slug>] [--yes] (legacy alias: --conversations=)");
|
|
5599
|
+
}
|
|
5600
|
+
assertArmSourceShape(baselineSource, "baseline");
|
|
5601
|
+
assertArmSourceShape(candidateSource, "candidate");
|
|
5602
|
+
const seedsPerCase = parsePositiveInt(flags.seeds ?? flags["seeds-per-case"], DEFAULT_SEEDS_PER_CASE, 10, flags.seeds !== undefined ? "--seeds" : "--seeds-per-case");
|
|
5603
|
+
parsePositiveInt(flags["lookback-days"], 30, 365, "--lookback-days");
|
|
5604
|
+
const timeoutMs = parsePositiveInt(flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, 24 * 60 * 60 * 1000, "--timeout");
|
|
5605
|
+
const pollIntervalMs = parsePositiveInt(flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, 120000, "--poll-interval");
|
|
5606
|
+
const assistantModel = assertAllowedModel(flags.model ?? flags["assistant-model"], flags, "--model");
|
|
5607
|
+
const plan = await planReplaySet(flags, tenantId, profileOptions);
|
|
5608
|
+
const plannedPlayouts = plan.cases === null ? null : plan.cases * seedsPerCase * 2;
|
|
5609
|
+
assertPlayoutBudget(plannedPlayouts, { seedsPerCase, cases: plan.cases }, context.yes, rerunWithYes("prompts ab", [], flags));
|
|
5610
|
+
if (context.outputMode === "human") {
|
|
5611
|
+
process.stderr.write(`${describePlayouts(plannedPlayouts, plan.cases, seedsPerCase)}
|
|
5612
|
+
`);
|
|
5583
5613
|
}
|
|
5584
5614
|
if (flags.sync === "true") {
|
|
5615
|
+
for (const [source, label] of [[baselineSource, "baseline"], [candidateSource, "candidate"]]) {
|
|
5616
|
+
try {
|
|
5617
|
+
resolvePromptArmSpec(source, label);
|
|
5618
|
+
} catch (error) {
|
|
5619
|
+
if (!(error instanceof UnresolvedPromptError))
|
|
5620
|
+
throw error;
|
|
5621
|
+
}
|
|
5622
|
+
}
|
|
5585
5623
|
await runPromptSync({ ...flags, watch: "false" }, profileOptions);
|
|
5586
5624
|
}
|
|
5587
5625
|
const promptArms = {
|
|
5588
5626
|
prod: resolvePromptArmSpec(baselineSource, "baseline"),
|
|
5589
5627
|
proposed: resolvePromptArmSpec(candidateSource, "candidate")
|
|
5590
5628
|
};
|
|
5591
|
-
const replaySetId = await ensureReplaySet(flags, tenantId, profileOptions);
|
|
5592
|
-
const seedsPerCase = parsePositiveInt(flags.seeds ?? flags["seeds-per-case"], DEFAULT_SEEDS_PER_CASE, 10);
|
|
5593
|
-
const assistantModel = (flags.model ?? flags["assistant-model"] ?? "").trim();
|
|
5629
|
+
const replaySetId = await ensureReplaySet(plan, flags, tenantId, profileOptions);
|
|
5594
5630
|
const enqueue = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(replaySetId)}/run`, {
|
|
5595
5631
|
method: "POST",
|
|
5596
5632
|
body: JSON.stringify({
|
|
5597
5633
|
promptArms,
|
|
5598
5634
|
seedsPerCase,
|
|
5599
5635
|
assistantModel: assistantModel || undefined,
|
|
5600
|
-
promotePrimary: flags["no-promote"] !== "true"
|
|
5636
|
+
promotePrimary: flags["no-promote"] !== "true",
|
|
5637
|
+
...plan.kind === "existing" ? replayCasePin(plan.caseIds) : {}
|
|
5601
5638
|
})
|
|
5602
|
-
}, profileOptions, { timeoutMs: 120000, retries:
|
|
5639
|
+
}, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
5603
5640
|
const noWait = flags.wait === "false" || flags["no-wait"] === "true";
|
|
5604
5641
|
if (noWait) {
|
|
5605
5642
|
context.output.writeData({
|
|
5606
5643
|
replaySetId,
|
|
5607
5644
|
runId: enqueue.runId,
|
|
5608
5645
|
status: enqueue.status,
|
|
5609
|
-
message: enqueue.message ?? "Replay comparison queued"
|
|
5646
|
+
message: enqueue.message ?? "Replay comparison queued",
|
|
5647
|
+
plannedPlayouts
|
|
5610
5648
|
});
|
|
5611
5649
|
return;
|
|
5612
5650
|
}
|
|
5613
|
-
const timeoutMs = parsePositiveInt(flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, 24 * 60 * 60 * 1000);
|
|
5614
|
-
const pollIntervalMs = parsePositiveInt(flags["poll-interval"], DEFAULT_POLL_INTERVAL_MS, 120000);
|
|
5615
5651
|
const deadline = Date.now() + timeoutMs;
|
|
5616
5652
|
let latest = null;
|
|
5617
5653
|
while (Date.now() < deadline) {
|
|
@@ -5632,6 +5668,7 @@ async function runPromptAb(flags, profileOptions, context) {
|
|
|
5632
5668
|
}
|
|
5633
5669
|
const result = {
|
|
5634
5670
|
replaySetId,
|
|
5671
|
+
plannedPlayouts,
|
|
5635
5672
|
runId: latest.run.runId,
|
|
5636
5673
|
status: latest.run.status,
|
|
5637
5674
|
verdict: formatVerdict(latest),
|
|
@@ -5643,6 +5680,48 @@ async function runPromptAb(flags, profileOptions, context) {
|
|
|
5643
5680
|
}
|
|
5644
5681
|
context.output.writeData(result);
|
|
5645
5682
|
}
|
|
5683
|
+
var FILE_ARM_RE = /[\\/]|\.(?:md|txt|json|ya?ml|jinja2?|j2|hbs|tmpl|prompt|[cm]?[jt]sx?|py)$/i;
|
|
5684
|
+
var PROMPT_KEY_RE = /^[^\s"'`\x00-\x1f\x7f]+$/;
|
|
5685
|
+
function assertArmSourceShape(source, label) {
|
|
5686
|
+
const trimmed = source.trim();
|
|
5687
|
+
if (!trimmed) {
|
|
5688
|
+
throw new CliInputError(`Empty prompt source for ${label}, so nothing was sent.`);
|
|
5689
|
+
}
|
|
5690
|
+
const absPath = isAbsolute(trimmed) ? trimmed : resolve(process.cwd(), trimmed);
|
|
5691
|
+
const exists = existsSync(absPath);
|
|
5692
|
+
if (!exists && FILE_ARM_RE.test(trimmed)) {
|
|
5693
|
+
if (isDiscoveredKey(trimmed))
|
|
5694
|
+
return;
|
|
5695
|
+
throw new CliInputError(`--${label} file not found: ${trimmed}, so nothing was sent.`);
|
|
5696
|
+
}
|
|
5697
|
+
if (exists) {
|
|
5698
|
+
if (!statSync(absPath).isFile()) {
|
|
5699
|
+
throw new CliInputError(`--${label} is not a file: ${trimmed}, so nothing was sent.`);
|
|
5700
|
+
}
|
|
5701
|
+
try {
|
|
5702
|
+
accessSync(absPath, fsConstants.R_OK);
|
|
5703
|
+
} catch {
|
|
5704
|
+
throw new CliInputError(`--${label} file is not readable: ${trimmed}, so nothing was sent.`);
|
|
5705
|
+
}
|
|
5706
|
+
if (/\.(?:[cm]?[jt]sx?|py)$/i.test(absPath)) {
|
|
5707
|
+
throw new CliInputError(`--${label}=${trimmed} is a source module; source modules cannot be replayed as instructions, so nothing was sent.`, "Use the registered prompt key for this code source.");
|
|
5708
|
+
}
|
|
5709
|
+
return;
|
|
5710
|
+
}
|
|
5711
|
+
if (!PROMPT_KEY_RE.test(trimmed)) {
|
|
5712
|
+
throw new CliInputError(`--${label}=${JSON.stringify(trimmed)} is neither an existing prompt file nor a valid prompt key, so nothing was sent.`, "Pass a .prompt.md path or a prompt key (no spaces or quotes).");
|
|
5713
|
+
}
|
|
5714
|
+
}
|
|
5715
|
+
function isDiscoveredKey(key) {
|
|
5716
|
+
try {
|
|
5717
|
+
return discoverPrompts().some((prompt) => prompt.key === key);
|
|
5718
|
+
} catch {
|
|
5719
|
+
return false;
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
|
|
5723
|
+
class UnresolvedPromptError extends Error {
|
|
5724
|
+
}
|
|
5646
5725
|
function resolvePromptArmSpec(source, label, opts = {}) {
|
|
5647
5726
|
const trimmed = source.trim();
|
|
5648
5727
|
if (!trimmed) {
|
|
@@ -5676,7 +5755,7 @@ function resolvePromptArmSpec(source, label, opts = {}) {
|
|
|
5676
5755
|
} else {
|
|
5677
5756
|
const match = fromDiscovery();
|
|
5678
5757
|
if (!match) {
|
|
5679
|
-
throw new
|
|
5758
|
+
throw new UnresolvedPromptError(`Could not resolve prompt '${trimmed}' — pass a .prompt.md path or a discovered prompt key`);
|
|
5680
5759
|
}
|
|
5681
5760
|
key = match.key;
|
|
5682
5761
|
content = match.content;
|
|
@@ -5689,20 +5768,86 @@ function resolvePromptArmSpec(source, label, opts = {}) {
|
|
|
5689
5768
|
prompt_label: label
|
|
5690
5769
|
};
|
|
5691
5770
|
}
|
|
5692
|
-
async function
|
|
5771
|
+
async function planReplaySet(flags, tenantId, profileOptions) {
|
|
5693
5772
|
const existingSetId = (flags["set-id"] || flags["replay-set-id"] || "").trim();
|
|
5694
5773
|
if (existingSetId) {
|
|
5695
|
-
|
|
5774
|
+
const snapshot = await fetchReplaySetCases(tenantId, existingSetId, profileOptions);
|
|
5775
|
+
return { kind: "existing", setId: existingSetId, cases: snapshot?.ids.length ?? null, caseIds: snapshot?.ids };
|
|
5696
5776
|
}
|
|
5697
5777
|
const conversations = parseCsv(flags.traces ?? flags.conversations ?? flags["conversation-ids"]);
|
|
5778
|
+
if (conversations.length > MAX_REPLAY_TRACES) {
|
|
5779
|
+
throw new CliInputError(`--traces has ${conversations.length} ids; the limit is ${MAX_REPLAY_TRACES} per replay set, so nothing was sent.`, "Split the traces across several runs, or use --auto-generate.");
|
|
5780
|
+
}
|
|
5698
5781
|
if (conversations.length) {
|
|
5699
|
-
return
|
|
5782
|
+
return { kind: "traces", traces: conversations, cases: conversations.length };
|
|
5700
5783
|
}
|
|
5701
|
-
if (flags["auto-generate"] === "false"
|
|
5784
|
+
if (flags["auto-generate"] === "false") {
|
|
5702
5785
|
throw new Error("Provide --set-id=, --traces= (legacy alias: --conversations=), or allow --auto-generate (default)");
|
|
5703
5786
|
}
|
|
5704
|
-
|
|
5705
|
-
|
|
5787
|
+
return {
|
|
5788
|
+
kind: "auto-generate",
|
|
5789
|
+
cases: parseCappedInt(flags.cases ?? flags["case-count"], DEFAULT_AUTO_GENERATE_CASES, MAX_AUTO_GENERATE_CASES, "--cases")
|
|
5790
|
+
};
|
|
5791
|
+
}
|
|
5792
|
+
async function fetchReplaySetCases(tenantId, setId, profileOptions) {
|
|
5793
|
+
try {
|
|
5794
|
+
const set = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(setId)}`, { method: "GET" }, profileOptions);
|
|
5795
|
+
if (!Array.isArray(set?.cases))
|
|
5796
|
+
return null;
|
|
5797
|
+
return {
|
|
5798
|
+
ids: set.cases.filter((c) => String(c?.scenario ?? "").trim()).map((c, index) => String(c?.id ?? `case_${index}`))
|
|
5799
|
+
};
|
|
5800
|
+
} catch (error) {
|
|
5801
|
+
if (error?.statusCode === 404) {
|
|
5802
|
+
throw new CliInputError(`Replay set '${setId}' not found for tenant ${tenantId}, so nothing was sent.`, "Check --set-id (list sets in the dashboard, or omit it to auto-generate).");
|
|
5803
|
+
}
|
|
5804
|
+
return null;
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
function replayCasePin(caseIds) {
|
|
5808
|
+
if (!caseIds)
|
|
5809
|
+
return {};
|
|
5810
|
+
return caseIds.length <= REPLAY_CASE_IDS_PIN_MAX ? { caseIds, expectedCases: caseIds.length } : { expectedCases: caseIds.length };
|
|
5811
|
+
}
|
|
5812
|
+
var REPLAY_CASE_IDS_PIN_MAX = 500;
|
|
5813
|
+
function describePlayouts(planned, cases, seeds) {
|
|
5814
|
+
return planned === null ? `Planned replay playouts: unknown (case count unavailable) × ${seeds} seeds × 2 arms` : `Planned replay playouts: ${planned} (${cases} cases × ${seeds} seeds × 2 arms)`;
|
|
5815
|
+
}
|
|
5816
|
+
function assertPlayoutBudget(planned, shape, yes, rerun) {
|
|
5817
|
+
if (yes)
|
|
5818
|
+
return;
|
|
5819
|
+
if (planned !== null && planned <= PLAYOUT_CONFIRM_THRESHOLD)
|
|
5820
|
+
return;
|
|
5821
|
+
const what = planned === null ? "an unknown number of replay playouts (the replay set's case count could not be read)" : `${planned} replay playouts (${shape.cases} cases × ${shape.seedsPerCase} seeds × 2 arms)`;
|
|
5822
|
+
throw new CliInputError(`This would run ${what}, above the ${PLAYOUT_CONFIRM_THRESHOLD}-playout limit that needs confirmation. Nothing was sent.`, `Re-run with --yes to confirm: ${rerun}`);
|
|
5823
|
+
}
|
|
5824
|
+
function assertAllowedModel(raw, flags, flagName) {
|
|
5825
|
+
const model = (raw ?? "").trim();
|
|
5826
|
+
if (!model || flags["allow-any-model"] === "true")
|
|
5827
|
+
return model;
|
|
5828
|
+
if (REPLAY_MODEL_ALLOWLIST.includes(model))
|
|
5829
|
+
return model;
|
|
5830
|
+
throw new CliInputError(`${flagName}=${model} is not in the replay model allowlist, so nothing was sent.`, `Allowed: ${REPLAY_MODEL_ALLOWLIST.join(", ")}. Pass --allow-any-model to use another OpenRouter slug.`);
|
|
5831
|
+
}
|
|
5832
|
+
function rerunWithYes(command, positionals, flags) {
|
|
5833
|
+
const quote = (v) => /^[\w@%+=:,./-]+$/.test(v) ? v : `'${v.replace(/'/g, `'\\''`)}'`;
|
|
5834
|
+
const parts = ["moda", ...command.split(" "), ...positionals.map(quote)];
|
|
5835
|
+
for (const [key, value] of Object.entries(flags)) {
|
|
5836
|
+
if (key === "yes")
|
|
5837
|
+
continue;
|
|
5838
|
+
parts.push(value === "true" ? `--${key}` : `--${key}=${quote(value)}`);
|
|
5839
|
+
}
|
|
5840
|
+
parts.push("--yes");
|
|
5841
|
+
return parts.join(" ");
|
|
5842
|
+
}
|
|
5843
|
+
async function ensureReplaySet(plan, flags, tenantId, profileOptions) {
|
|
5844
|
+
if (plan.kind === "existing")
|
|
5845
|
+
return plan.setId;
|
|
5846
|
+
if (plan.kind === "traces") {
|
|
5847
|
+
return createSetFromConversations(plan.traces, flags, tenantId, profileOptions);
|
|
5848
|
+
}
|
|
5849
|
+
const caseCount = plan.cases;
|
|
5850
|
+
const lookbackDays = parsePositiveInt(flags["lookback-days"], 30, 365, "--lookback-days");
|
|
5706
5851
|
const name = (flags.name ?? flags["set-name"] ?? `Prompt A/B ${new Date().toISOString().slice(0, 10)}`).trim();
|
|
5707
5852
|
const generated = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/auto-generate`, {
|
|
5708
5853
|
method: "POST",
|
|
@@ -5826,9 +5971,10 @@ async function enqueueAndPollComparison(args, profileOptions) {
|
|
|
5826
5971
|
promptArms: args.promptArms,
|
|
5827
5972
|
seedsPerCase: args.seedsPerCase ?? DEFAULT_SEEDS_PER_CASE,
|
|
5828
5973
|
assistantModel: args.assistantModel || undefined,
|
|
5829
|
-
promotePrimary: args.promotePrimary ?? false
|
|
5974
|
+
promotePrimary: args.promotePrimary ?? false,
|
|
5975
|
+
...replayCasePin(args.caseIds)
|
|
5830
5976
|
})
|
|
5831
|
-
}, profileOptions, { timeoutMs: 120000, retries:
|
|
5977
|
+
}, profileOptions, { timeoutMs: 120000, retries: 0 });
|
|
5832
5978
|
const timeoutMs = args.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
5833
5979
|
const pollIntervalMs = args.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
5834
5980
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -5848,13 +5994,21 @@ async function enqueueAndPollComparison(args, profileOptions) {
|
|
|
5848
5994
|
}
|
|
5849
5995
|
return latest;
|
|
5850
5996
|
}
|
|
5851
|
-
function parsePositiveInt(raw, fallback, max) {
|
|
5852
|
-
if (
|
|
5853
|
-
return fallback;
|
|
5854
|
-
const parsed = Number.parseInt(raw, 10);
|
|
5855
|
-
if (!Number.isFinite(parsed) || parsed < 1)
|
|
5997
|
+
function parsePositiveInt(raw, fallback, max, flagName) {
|
|
5998
|
+
if (raw === undefined)
|
|
5856
5999
|
return fallback;
|
|
5857
|
-
|
|
6000
|
+
const text = String(raw).trim();
|
|
6001
|
+
const parsed = Number(text);
|
|
6002
|
+
if (!/^\d+$/.test(text) || !Number.isSafeInteger(parsed) || parsed < 1) {
|
|
6003
|
+
throw new CliInputError(`${flagName}=${raw} must be a whole number of at least 1, so nothing was sent.`, `Use ${flagName}=N with 1 <= N <= ${max}.`);
|
|
6004
|
+
}
|
|
6005
|
+
if (parsed > max) {
|
|
6006
|
+
throw new CliInputError(`${flagName}=${parsed} is above the maximum of ${max}, so nothing was sent.`, `Use ${flagName}=${max} or less.`);
|
|
6007
|
+
}
|
|
6008
|
+
return parsed;
|
|
6009
|
+
}
|
|
6010
|
+
function parseCappedInt(raw, fallback, max, flagName) {
|
|
6011
|
+
return parsePositiveInt(raw, fallback, max, flagName);
|
|
5858
6012
|
}
|
|
5859
6013
|
function parseCsv(raw) {
|
|
5860
6014
|
if (!raw?.trim())
|
|
@@ -5875,10 +6029,31 @@ async function runPromptPropose(positionals, flags, profileOptions, context) {
|
|
|
5875
6029
|
const fromRunId = flags["from-run"] || flags["from-run-id"] || flags["run-id"];
|
|
5876
6030
|
const replaySetId = flags["set-id"] || flags["replay-set-id"];
|
|
5877
6031
|
if (!promptKey || !fromRunId || !replaySetId) {
|
|
5878
|
-
throw new Error("Usage: moda prompts propose --prompt-key=<key> --from-run=<run_id> --set-id=<replay_set_id> [--max-dossiers=8] [--model=<slug>] [--out=path.prompt.md] [--gate] [--promote-on-win]");
|
|
6032
|
+
throw new Error("Usage: moda prompts propose --prompt-key=<key> --from-run=<run_id> --set-id=<replay_set_id> [--max-dossiers=8] [--model=<slug>] [--out=path.prompt.md] [--gate [--seeds=1-10] [--assistant-model=<slug>] [--yes]] [--promote-on-win] [--allow-any-model]");
|
|
6033
|
+
}
|
|
6034
|
+
const maxDossiers = parsePositiveInt2(flags["max-dossiers"], 8, 16, "--max-dossiers");
|
|
6035
|
+
parsePositiveInt2(flags["gate-timeout"] ?? flags.timeout, 2 * 60 * 60 * 1000, 24 * 60 * 60 * 1000, flags["gate-timeout"] !== undefined ? "--gate-timeout" : "--timeout");
|
|
6036
|
+
parsePositiveInt2(flags["poll-interval"], 15000, 120000, "--poll-interval");
|
|
6037
|
+
const model = assertAllowedModel(flags.model ?? flags["revise-model"], flags, "--model");
|
|
6038
|
+
const wantGate = flags.gate === "true";
|
|
6039
|
+
let gatePlan;
|
|
6040
|
+
if (wantGate) {
|
|
6041
|
+
const tenantId = flags["tenant-id"] || resolveApiTenantId(profileOptions);
|
|
6042
|
+
if (!tenantId) {
|
|
6043
|
+
throw new Error("Missing tenant id for --gate. Run `moda init` or pass --tenant-id=<id>.");
|
|
6044
|
+
}
|
|
6045
|
+
const seedsPerCase = parsePositiveInt2(flags.seeds ?? flags["seeds-per-case"], 3, 10, flags.seeds !== undefined ? "--seeds" : "--seeds-per-case");
|
|
6046
|
+
const assistantModel = assertAllowedModel(flags["assistant-model"], flags, "--assistant-model");
|
|
6047
|
+
const snapshot = await fetchGateSetCases(tenantId, replaySetId, profileOptions);
|
|
6048
|
+
const cases = snapshot?.ids.length ?? null;
|
|
6049
|
+
const plannedPlayouts = cases === null ? null : cases * seedsPerCase * 2;
|
|
6050
|
+
assertPlayoutBudget(plannedPlayouts, { seedsPerCase, cases }, context.yes, rerunWithYes("prompts propose", positionals.slice(1), flags));
|
|
6051
|
+
if (context.outputMode === "human") {
|
|
6052
|
+
process.stderr.write(`Gate: ${describePlayouts(plannedPlayouts, cases, seedsPerCase)}
|
|
6053
|
+
`);
|
|
6054
|
+
}
|
|
6055
|
+
gatePlan = { tenantId, seedsPerCase, assistantModel, plannedPlayouts, cases, caseIds: snapshot?.ids };
|
|
5879
6056
|
}
|
|
5880
|
-
const maxDossiers = parsePositiveInt2(flags["max-dossiers"], 8, 16);
|
|
5881
|
-
const model = (flags.model ?? flags["revise-model"] ?? "").trim();
|
|
5882
6057
|
const result = await callControlAPI(`/prompts/${encodeURIComponent(promptKey)}/propose`, {
|
|
5883
6058
|
method: "POST",
|
|
5884
6059
|
body: JSON.stringify({ fromRunId, replaySetId, maxDossiers, model: model || undefined })
|
|
@@ -5888,10 +6063,9 @@ async function runPromptPropose(positionals, flags, profileOptions, context) {
|
|
|
5888
6063
|
outPath = isAbsolute2(flags.out) ? flags.out : resolve2(process.cwd(), flags.out);
|
|
5889
6064
|
writeFileSync(outPath, result.content, "utf8");
|
|
5890
6065
|
}
|
|
5891
|
-
const wantGate = flags.gate === "true";
|
|
5892
6066
|
const promoteOnWin = flags["promote-on-win"] === "true";
|
|
5893
6067
|
let gate;
|
|
5894
|
-
if (
|
|
6068
|
+
if (gatePlan) {
|
|
5895
6069
|
if (result.status !== "proposed" || !result.content) {
|
|
5896
6070
|
gate = {
|
|
5897
6071
|
ran: false,
|
|
@@ -5900,7 +6074,8 @@ async function runPromptPropose(positionals, flags, profileOptions, context) {
|
|
|
5900
6074
|
verdict: "skipped",
|
|
5901
6075
|
outcome: "no-run",
|
|
5902
6076
|
runId: null,
|
|
5903
|
-
promoted: false
|
|
6077
|
+
promoted: false,
|
|
6078
|
+
plannedPlayouts: gatePlan.plannedPlayouts
|
|
5904
6079
|
};
|
|
5905
6080
|
} else {
|
|
5906
6081
|
gate = await runGate({
|
|
@@ -5908,6 +6083,7 @@ async function runPromptPropose(positionals, flags, profileOptions, context) {
|
|
|
5908
6083
|
promptKey,
|
|
5909
6084
|
replaySetId,
|
|
5910
6085
|
flags,
|
|
6086
|
+
plan: gatePlan,
|
|
5911
6087
|
promoteOnWin,
|
|
5912
6088
|
profileOptions,
|
|
5913
6089
|
context
|
|
@@ -5923,12 +6099,25 @@ async function runPromptPropose(positionals, flags, profileOptions, context) {
|
|
|
5923
6099
|
...gate ? { gate } : {}
|
|
5924
6100
|
});
|
|
5925
6101
|
}
|
|
5926
|
-
async function
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
6102
|
+
async function fetchGateSetCases(tenantId, replaySetId, profileOptions) {
|
|
6103
|
+
let set;
|
|
6104
|
+
try {
|
|
6105
|
+
set = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(replaySetId)}`, { method: "GET" }, profileOptions);
|
|
6106
|
+
} catch (error) {
|
|
6107
|
+
if (error?.statusCode === 404) {
|
|
6108
|
+
throw new CliInputError(`Replay set '${replaySetId}' not found for tenant ${tenantId}, so nothing was sent.`, "Check --set-id: use the replay set the --from-run run was scored on (see `moda prompts replay-runs <key>`).");
|
|
6109
|
+
}
|
|
6110
|
+
return null;
|
|
5931
6111
|
}
|
|
6112
|
+
if (!Array.isArray(set?.cases))
|
|
6113
|
+
return null;
|
|
6114
|
+
return {
|
|
6115
|
+
ids: set.cases.filter((c) => String(c?.scenario ?? "").trim()).map((c, index) => String(c?.id ?? `case_${index}`))
|
|
6116
|
+
};
|
|
6117
|
+
}
|
|
6118
|
+
async function runGate(args) {
|
|
6119
|
+
const { result, promptKey, replaySetId, flags, plan, promoteOnWin, profileOptions, context } = args;
|
|
6120
|
+
const { tenantId, seedsPerCase, assistantModel } = plan;
|
|
5932
6121
|
const prodArm = resolvePromptArmSpec(promptKey, "baseline", { preferKey: true });
|
|
5933
6122
|
const proposedArm = {
|
|
5934
6123
|
content: result.content ?? "",
|
|
@@ -5936,10 +6125,8 @@ async function runGate(args) {
|
|
|
5936
6125
|
prompt_id: "",
|
|
5937
6126
|
prompt_label: "candidate"
|
|
5938
6127
|
};
|
|
5939
|
-
const
|
|
5940
|
-
const
|
|
5941
|
-
const timeoutMs = parsePositiveInt2(flags["gate-timeout"] ?? flags.timeout, 2 * 60 * 60 * 1000, 24 * 60 * 60 * 1000);
|
|
5942
|
-
const pollIntervalMs = parsePositiveInt2(flags["poll-interval"], 15000, 120000);
|
|
6128
|
+
const timeoutMs = parsePositiveInt2(flags["gate-timeout"] ?? flags.timeout, 2 * 60 * 60 * 1000, 24 * 60 * 60 * 1000, flags["gate-timeout"] !== undefined ? "--gate-timeout" : "--timeout");
|
|
6129
|
+
const pollIntervalMs = parsePositiveInt2(flags["poll-interval"], 15000, 120000, "--poll-interval");
|
|
5943
6130
|
const scopeNote = result.holdoutCaseIds.length ? `gated over the FULL replay set (${result.holdoutCaseIds.length} holdout case(s) could not be isolated — the run endpoint scores every case)` : "gated over the FULL replay set";
|
|
5944
6131
|
if (context.outputMode === "human") {
|
|
5945
6132
|
process.stderr.write(`
|
|
@@ -5953,6 +6140,7 @@ Gating candidate for '${promptKey}' — ${scopeNote}...
|
|
|
5953
6140
|
seedsPerCase,
|
|
5954
6141
|
assistantModel: assistantModel || undefined,
|
|
5955
6142
|
promotePrimary: false,
|
|
6143
|
+
caseIds: plan.caseIds,
|
|
5956
6144
|
timeoutMs,
|
|
5957
6145
|
pollIntervalMs,
|
|
5958
6146
|
onWaiting: context.outputMode === "human" ? (status) => process.stderr.write(`Waiting for gate run (${status})...
|
|
@@ -5966,7 +6154,8 @@ Gating candidate for '${promptKey}' — ${scopeNote}...
|
|
|
5966
6154
|
verdict: formatVerdict(payload),
|
|
5967
6155
|
outcome,
|
|
5968
6156
|
runId: payload.run?.runId ?? null,
|
|
5969
|
-
promoted: false
|
|
6157
|
+
promoted: false,
|
|
6158
|
+
plannedPlayouts: plan.plannedPlayouts
|
|
5970
6159
|
};
|
|
5971
6160
|
if (promoteOnWin) {
|
|
5972
6161
|
if (outcome === "candidate" && result.versionId) {
|
|
@@ -6005,6 +6194,8 @@ function printHuman(result, outPath, gate) {
|
|
|
6005
6194
|
w("Gate (automatic A/B of candidate vs baseline):");
|
|
6006
6195
|
w(` scope: ${gate.scopeNote}`);
|
|
6007
6196
|
w(` verdict: ${gate.verdict}`);
|
|
6197
|
+
if (gate.plannedPlayouts !== undefined)
|
|
6198
|
+
w(` playouts: ${gate.plannedPlayouts ?? "unknown"} planned`);
|
|
6008
6199
|
if (gate.runId)
|
|
6009
6200
|
w(` run id: ${gate.runId}`);
|
|
6010
6201
|
if (gate.promoted) {
|
|
@@ -6037,11 +6228,18 @@ function printHuman(result, outPath, gate) {
|
|
|
6037
6228
|
w(` # or: moda prompts propose ... --gate --promote-on-win (auto-A/B, promote on a strict win)`);
|
|
6038
6229
|
w(` moda prompts promote ${result.promptKey} --label=prod --version=${result.versionId}`);
|
|
6039
6230
|
}
|
|
6040
|
-
function parsePositiveInt2(raw, fallback, max) {
|
|
6041
|
-
|
|
6042
|
-
if (!Number.isFinite(n) || n <= 0)
|
|
6231
|
+
function parsePositiveInt2(raw, fallback, max, flagName) {
|
|
6232
|
+
if (raw === undefined)
|
|
6043
6233
|
return fallback;
|
|
6044
|
-
|
|
6234
|
+
const text = String(raw).trim();
|
|
6235
|
+
const parsed = Number(text);
|
|
6236
|
+
if (!/^\d+$/.test(text) || !Number.isSafeInteger(parsed) || parsed < 1) {
|
|
6237
|
+
throw new CliInputError(`${flagName}=${raw} must be a whole number of at least 1, so nothing was sent.`, `Use ${flagName}=N with 1 <= N <= ${max}.`);
|
|
6238
|
+
}
|
|
6239
|
+
if (parsed > max) {
|
|
6240
|
+
throw new CliInputError(`${flagName}=${parsed} is above the maximum of ${max}, so nothing was sent.`, `Use ${flagName}=${max} or less.`);
|
|
6241
|
+
}
|
|
6242
|
+
return parsed;
|
|
6045
6243
|
}
|
|
6046
6244
|
|
|
6047
6245
|
// src/prompt-attribution.ts
|
|
@@ -6179,6 +6377,77 @@ var DEFAULT_PROMPT_PATHS = [
|
|
|
6179
6377
|
"prompts/**/*.prompt.yaml",
|
|
6180
6378
|
"prompts/**/*.prompt.yml"
|
|
6181
6379
|
];
|
|
6380
|
+
var PROMPT_WRITE_SUBCOMMAND_FLAGS = {
|
|
6381
|
+
init: ["from-harness"],
|
|
6382
|
+
status: [],
|
|
6383
|
+
diff: [],
|
|
6384
|
+
sync: [
|
|
6385
|
+
"dry-run",
|
|
6386
|
+
"from-harness",
|
|
6387
|
+
"no-analyze",
|
|
6388
|
+
"allow-untrack",
|
|
6389
|
+
"watch",
|
|
6390
|
+
"interval",
|
|
6391
|
+
"analyst",
|
|
6392
|
+
"assist",
|
|
6393
|
+
"analyst-command",
|
|
6394
|
+
"analyst-model"
|
|
6395
|
+
],
|
|
6396
|
+
promote: ["label", "version", "version-id"],
|
|
6397
|
+
ab: [
|
|
6398
|
+
"baseline",
|
|
6399
|
+
"candidate",
|
|
6400
|
+
"cases",
|
|
6401
|
+
"conversations",
|
|
6402
|
+
"model",
|
|
6403
|
+
"name",
|
|
6404
|
+
"seeds",
|
|
6405
|
+
"sync",
|
|
6406
|
+
"timeout",
|
|
6407
|
+
"traces",
|
|
6408
|
+
"wait",
|
|
6409
|
+
"assistant-model",
|
|
6410
|
+
"auto-generate",
|
|
6411
|
+
"baseline-file",
|
|
6412
|
+
"baseline-key",
|
|
6413
|
+
"candidate-file",
|
|
6414
|
+
"candidate-key",
|
|
6415
|
+
"case-count",
|
|
6416
|
+
"conversation-ids",
|
|
6417
|
+
"lookback-days",
|
|
6418
|
+
"no-promote",
|
|
6419
|
+
"no-wait",
|
|
6420
|
+
"poll-interval",
|
|
6421
|
+
"replay-set-id",
|
|
6422
|
+
"seeds-per-case",
|
|
6423
|
+
"set-id",
|
|
6424
|
+
"set-name",
|
|
6425
|
+
"tenant-id",
|
|
6426
|
+
"allow-any-model"
|
|
6427
|
+
],
|
|
6428
|
+
propose: [
|
|
6429
|
+
"gate",
|
|
6430
|
+
"model",
|
|
6431
|
+
"out",
|
|
6432
|
+
"seeds",
|
|
6433
|
+
"timeout",
|
|
6434
|
+
"assistant-model",
|
|
6435
|
+
"from-run",
|
|
6436
|
+
"from-run-id",
|
|
6437
|
+
"gate-timeout",
|
|
6438
|
+
"max-dossiers",
|
|
6439
|
+
"poll-interval",
|
|
6440
|
+
"promote-on-win",
|
|
6441
|
+
"prompt-key",
|
|
6442
|
+
"replay-set-id",
|
|
6443
|
+
"revise-model",
|
|
6444
|
+
"run-id",
|
|
6445
|
+
"seeds-per-case",
|
|
6446
|
+
"set-id",
|
|
6447
|
+
"tenant-id",
|
|
6448
|
+
"allow-any-model"
|
|
6449
|
+
]
|
|
6450
|
+
};
|
|
6182
6451
|
async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
|
|
6183
6452
|
const context = Array.isArray(input) ? createCommandContext({
|
|
6184
6453
|
command: "prompts",
|
|
@@ -6194,6 +6463,9 @@ async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
|
|
|
6194
6463
|
const flags = context.flags;
|
|
6195
6464
|
const profileOptions = legacyProfileOptions ?? { profile: context.profile, env: context.env };
|
|
6196
6465
|
const subcommand = positionals[0] || "status";
|
|
6466
|
+
const writeFlags = PROMPT_WRITE_SUBCOMMAND_FLAGS[subcommand];
|
|
6467
|
+
if (writeFlags)
|
|
6468
|
+
assertFlags(`prompts ${subcommand}`, flags, writeFlags);
|
|
6197
6469
|
switch (subcommand) {
|
|
6198
6470
|
case "init": {
|
|
6199
6471
|
const imported = flags["from-harness"] ? importHarnessPromptSources(flags["from-harness"]) : undefined;
|
|
@@ -6219,9 +6491,27 @@ async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
|
|
|
6219
6491
|
case "status":
|
|
6220
6492
|
context.output.writeData(await getPromptStatus());
|
|
6221
6493
|
break;
|
|
6222
|
-
case "diff":
|
|
6223
|
-
|
|
6494
|
+
case "diff": {
|
|
6495
|
+
const status = await getPromptStatus();
|
|
6496
|
+
const differing = status.prompts.filter((row) => row.state !== "unchanged");
|
|
6497
|
+
const tracked = existsSync2(MANIFEST_PATH);
|
|
6498
|
+
const untrackedWarning = tracked ? [] : [
|
|
6499
|
+
`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\`.`
|
|
6500
|
+
];
|
|
6501
|
+
context.output.writeData({
|
|
6502
|
+
...status,
|
|
6503
|
+
unchanged: status.total - differing.length,
|
|
6504
|
+
prompts: differing,
|
|
6505
|
+
...tracked ? {} : { tracked: false }
|
|
6506
|
+
}, {
|
|
6507
|
+
summary: {
|
|
6508
|
+
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}).`,
|
|
6509
|
+
confidence: "high"
|
|
6510
|
+
},
|
|
6511
|
+
...untrackedWarning.length > 0 ? { warnings: untrackedWarning } : {}
|
|
6512
|
+
});
|
|
6224
6513
|
break;
|
|
6514
|
+
}
|
|
6225
6515
|
case "sync":
|
|
6226
6516
|
await syncPrompts(flags, profileOptions, context);
|
|
6227
6517
|
break;
|
|
@@ -6234,8 +6524,16 @@ async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
|
|
|
6234
6524
|
case "propose":
|
|
6235
6525
|
await runPromptPropose(positionals, flags, profileOptions, context);
|
|
6236
6526
|
break;
|
|
6527
|
+
case "list":
|
|
6528
|
+
case "show":
|
|
6529
|
+
case "proposals":
|
|
6530
|
+
case "usage":
|
|
6531
|
+
case "replay-runs":
|
|
6532
|
+
case "decide":
|
|
6533
|
+
await runPromptRegistryRead(subcommand, positionals, flags, profileOptions, context);
|
|
6534
|
+
break;
|
|
6237
6535
|
default:
|
|
6238
|
-
throw new Error(`Unknown prompts command '${subcommand}'. Use init, status, diff, sync, promote, ab, or
|
|
6536
|
+
throw new Error(`Unknown prompts command '${subcommand}'. Use init, status, diff, sync, promote, ab, propose, list, show, proposals, usage, replay-runs, or decide.`);
|
|
6239
6537
|
}
|
|
6240
6538
|
}
|
|
6241
6539
|
async function initPrompts(options = {}) {
|
|
@@ -6320,7 +6618,7 @@ async function runPromptSyncExclusive(flags, profileOptions, options) {
|
|
|
6320
6618
|
const analyzedDefinitions = new Map;
|
|
6321
6619
|
if (flags["no-analyze"] !== "true") {
|
|
6322
6620
|
options.onProgress?.("Analyzing prompt sources before sync.");
|
|
6323
|
-
const { analyzeHarnessPrompts } = await import("./harness-
|
|
6621
|
+
const { analyzeHarnessPrompts } = await import("./harness-191xt7bq.js");
|
|
6324
6622
|
const result = await analyzeHarnessPrompts({
|
|
6325
6623
|
rootDir: process.cwd(),
|
|
6326
6624
|
existing: established.flatMap((prompt) => prompt.sourceDefinition ? [prompt.sourceDefinition.source] : []),
|
|
@@ -6800,6 +7098,80 @@ function sha256(input) {
|
|
|
6800
7098
|
function printJson(value) {
|
|
6801
7099
|
console.log(JSON.stringify(value, null, 2));
|
|
6802
7100
|
}
|
|
7101
|
+
var PROMPT_LABELS = ["prod", "staging", "dev"];
|
|
7102
|
+
async function runPromptRegistryRead(subcommand, positionals, flags, profileOptions, context) {
|
|
7103
|
+
const allowed = {
|
|
7104
|
+
list: ["label", "search"],
|
|
7105
|
+
show: [],
|
|
7106
|
+
proposals: [],
|
|
7107
|
+
usage: [],
|
|
7108
|
+
"replay-runs": [],
|
|
7109
|
+
decide: ["action", "dry-run"]
|
|
7110
|
+
};
|
|
7111
|
+
assertFlags(`prompts ${subcommand}`, flags, allowed[subcommand] ?? []);
|
|
7112
|
+
const maxPositionals = subcommand === "list" ? 1 : subcommand === "decide" ? 3 : 2;
|
|
7113
|
+
assertPositionals(`prompts ${subcommand}`, positionals, maxPositionals, `moda prompts ${subcommand}${subcommand === "list" ? "" : " <key>"}${subcommand === "decide" ? " <proposal_id> --action=merge|reject|reopen" : ""}`);
|
|
7114
|
+
const keyOrId = positionals[1];
|
|
7115
|
+
const needKey = (usage) => {
|
|
7116
|
+
if (!keyOrId)
|
|
7117
|
+
throw new CliInputError("<prompt key or id> is required.", `Usage: ${usage}`);
|
|
7118
|
+
return encodeURIComponent(keyOrId);
|
|
7119
|
+
};
|
|
7120
|
+
switch (subcommand) {
|
|
7121
|
+
case "list": {
|
|
7122
|
+
const label = flags.label;
|
|
7123
|
+
if (label && label !== "unlabeled" && !PROMPT_LABELS.includes(label)) {
|
|
7124
|
+
throw new CliInputError(`--label must be one of ${PROMPT_LABELS.join(", ")}, or unlabeled.`);
|
|
7125
|
+
}
|
|
7126
|
+
const response = asRecordValue(await callControlAPI("/prompts", {}, profileOptions));
|
|
7127
|
+
const all = Array.isArray(response.prompts) ? response.prompts.map(asRecordValue) : [];
|
|
7128
|
+
const search = flags.search?.toLowerCase();
|
|
7129
|
+
const prompts = all.filter((prompt) => {
|
|
7130
|
+
if (search && !`${prompt.key ?? ""} ${prompt.name ?? ""} ${prompt.description ?? ""}`.toLowerCase().includes(search))
|
|
7131
|
+
return false;
|
|
7132
|
+
if (!label)
|
|
7133
|
+
return true;
|
|
7134
|
+
const labelField = (name) => name === "dev" ? prompt.currentVersion ?? prompt.current_version : prompt[`${name}Version`] ?? prompt[`${name}_version`];
|
|
7135
|
+
const labels = PROMPT_LABELS.filter((name) => labelField(name));
|
|
7136
|
+
return label === "unlabeled" ? labels.length === 0 : labels.includes(label);
|
|
7137
|
+
});
|
|
7138
|
+
context.output.writeData({ prompts, total: prompts.length, ...label || search ? { filtered_from: all.length } : {} });
|
|
7139
|
+
return;
|
|
7140
|
+
}
|
|
7141
|
+
case "show":
|
|
7142
|
+
context.output.writeData(await callControlAPI(`/prompts/${needKey("moda prompts show <key>")}`, {}, profileOptions));
|
|
7143
|
+
return;
|
|
7144
|
+
case "proposals":
|
|
7145
|
+
context.output.writeData(await callControlAPI(`/prompts/${needKey("moda prompts proposals <key>")}/proposals`, {}, profileOptions));
|
|
7146
|
+
return;
|
|
7147
|
+
case "usage":
|
|
7148
|
+
context.output.writeData(await callControlAPI(`/prompts/${needKey("moda prompts usage <key>")}/usage`, {}, profileOptions));
|
|
7149
|
+
return;
|
|
7150
|
+
case "replay-runs":
|
|
7151
|
+
context.output.writeData(await callControlAPI(`/prompts/${needKey("moda prompts replay-runs <key>")}/replay-runs`, {}, profileOptions));
|
|
7152
|
+
return;
|
|
7153
|
+
case "decide": {
|
|
7154
|
+
const usage = "moda prompts decide <key> <proposal_id> --action=merge|reject|reopen";
|
|
7155
|
+
const id = needKey(usage);
|
|
7156
|
+
const proposalId = positionals[2];
|
|
7157
|
+
const action = flags.action;
|
|
7158
|
+
if (!proposalId)
|
|
7159
|
+
throw new CliInputError("<proposal_id> is required.", `Usage: ${usage}`);
|
|
7160
|
+
if (!action || !["merge", "reject", "reopen"].includes(action)) {
|
|
7161
|
+
throw new CliInputError("--action must be merge, reject, or reopen.", `Usage: ${usage}`);
|
|
7162
|
+
}
|
|
7163
|
+
if (context.dryRun) {
|
|
7164
|
+
context.output.writeData({ dryRun: true, planned: { method: "PATCH", endpoint: `/api/prompts/${id}/proposals/${encodeURIComponent(proposalId)}`, body: { action } } });
|
|
7165
|
+
return;
|
|
7166
|
+
}
|
|
7167
|
+
context.output.writeData(await callControlAPI(`/prompts/${id}/proposals/${encodeURIComponent(proposalId)}`, { method: "PATCH", body: JSON.stringify({ action }) }, profileOptions, { retries: 0 }));
|
|
7168
|
+
return;
|
|
7169
|
+
}
|
|
7170
|
+
}
|
|
7171
|
+
}
|
|
7172
|
+
function asRecordValue(value) {
|
|
7173
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7174
|
+
}
|
|
6803
7175
|
|
|
6804
7176
|
// src/skills.ts
|
|
6805
7177
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
@@ -7339,6 +7711,38 @@ function escapeYamlString(value) {
|
|
|
7339
7711
|
function formatProposalRate(value) {
|
|
7340
7712
|
return `${(Number(value || 0) * 100).toFixed(1)}%`;
|
|
7341
7713
|
}
|
|
7714
|
+
var SKILLS_SUBCOMMAND_FLAGS = {
|
|
7715
|
+
gen: [
|
|
7716
|
+
"replay",
|
|
7717
|
+
"source",
|
|
7718
|
+
"wait",
|
|
7719
|
+
"clustering-poll-seconds",
|
|
7720
|
+
"clustering-run-id",
|
|
7721
|
+
"clustering-timeout-seconds",
|
|
7722
|
+
"end-at",
|
|
7723
|
+
"eval-set-id",
|
|
7724
|
+
"force-reprocess",
|
|
7725
|
+
"harness-url",
|
|
7726
|
+
"improve-candidates",
|
|
7727
|
+
"improvement-rounds",
|
|
7728
|
+
"lookback-hours",
|
|
7729
|
+
"max-cluster-pct",
|
|
7730
|
+
"max-sessions",
|
|
7731
|
+
"max-traces",
|
|
7732
|
+
"no-clustering",
|
|
7733
|
+
"no-replay",
|
|
7734
|
+
"replay-set-id",
|
|
7735
|
+
"reprocess-segments",
|
|
7736
|
+
"start-at",
|
|
7737
|
+
"tenant-id",
|
|
7738
|
+
"wait-for-completion"
|
|
7739
|
+
],
|
|
7740
|
+
status: ["run-id"],
|
|
7741
|
+
pull: ["status"],
|
|
7742
|
+
proposals: ["status", "generation-run-id", "tenant-id"],
|
|
7743
|
+
proposal: ["proposal-id", "tenant-id"],
|
|
7744
|
+
install: ["list"]
|
|
7745
|
+
};
|
|
7342
7746
|
async function runSkillsCommand(input) {
|
|
7343
7747
|
const context = "output" in input ? input : createCommandContext({
|
|
7344
7748
|
command: input.command,
|
|
@@ -7351,6 +7755,8 @@ async function runSkillsCommand(input) {
|
|
|
7351
7755
|
const sub = context.positional;
|
|
7352
7756
|
const positionals = context.positionals ?? (sub ? [sub] : []);
|
|
7353
7757
|
const flags = context.flags;
|
|
7758
|
+
if (sub && SKILLS_SUBCOMMAND_FLAGS[sub])
|
|
7759
|
+
assertFlags(`skills ${sub}`, flags, SKILLS_SUBCOMMAND_FLAGS[sub]);
|
|
7354
7760
|
if (sub === "gen") {
|
|
7355
7761
|
const result = await generateSkills(flags);
|
|
7356
7762
|
if (context.outputMode !== "human") {
|
|
@@ -7433,7 +7839,11 @@ async function runSkillsCommand(input) {
|
|
|
7433
7839
|
}
|
|
7434
7840
|
return 0;
|
|
7435
7841
|
}
|
|
7842
|
+
if (sub === "list" || sub === "show" || sub === "policy") {
|
|
7843
|
+
return runSkillRegistryCommand(sub, positionals, flags, context);
|
|
7844
|
+
}
|
|
7436
7845
|
if (sub === "sync") {
|
|
7846
|
+
assertFlags("skills sync", flags, ["dry-run"]);
|
|
7437
7847
|
const result = await runSkillSync(flags, { profile: context.profile, env: context.env });
|
|
7438
7848
|
context.output.writeData({
|
|
7439
7849
|
synced: result.synced,
|
|
@@ -7446,7 +7856,7 @@ async function runSkillsCommand(input) {
|
|
|
7446
7856
|
return runSkillsInstall(context, positionals.slice(1));
|
|
7447
7857
|
}
|
|
7448
7858
|
if (sub !== "pull") {
|
|
7449
|
-
throw new CliInputError(sub ? `Unknown skills subcommand '${sub}'` : "A skills subcommand is required", "Usage: moda skills gen [--source=all|sdk] [--max-sessions=N] [--start-at=ISO] [--end-at=ISO] [--replay] [--replay-set-id=ID] [--reprocess-segments] [--force-reprocess] [--wait] | moda skills status [run-id] | moda skills proposals list [--status=ready_for_pr] | moda skills proposal apply <proposal-id> | moda skills pull [--status=approved|proposed|all] | moda skills sync [--dry-run] | moda skills install [--list] <id...>");
|
|
7859
|
+
throw new CliInputError(sub ? `Unknown skills subcommand '${sub}'` : "A skills subcommand is required", "Usage: moda skills gen [--source=all|sdk] [--max-sessions=N (default 200)] [--lookback-hours=N (default 720)] [--max-traces=N] [--improvement-rounds=1-3] [--start-at=ISO] [--end-at=ISO] [--replay] [--replay-set-id=ID] [--reprocess-segments] [--force-reprocess] [--wait] | moda skills status [run-id] | moda skills proposals list [--status=ready_for_pr] | moda skills proposal apply <proposal-id> | moda skills pull [--status=approved|proposed|all] | moda skills sync [--dry-run] | moda skills install [--list] <id...>");
|
|
7450
7860
|
}
|
|
7451
7861
|
const status = (flags.status ?? "approved").toLowerCase();
|
|
7452
7862
|
if (!VALID_STATUSES.has(status)) {
|
|
@@ -7487,8 +7897,8 @@ async function generateSkills(flags) {
|
|
|
7487
7897
|
}
|
|
7488
7898
|
const body = compactObject({
|
|
7489
7899
|
tenant_id: tenantId || undefined,
|
|
7490
|
-
max_sessions:
|
|
7491
|
-
lookback_hours:
|
|
7900
|
+
max_sessions: parseScopeInt(flags["max-sessions"], "max-sessions"),
|
|
7901
|
+
lookback_hours: parseScopeInt(flags["lookback-hours"], "lookback-hours"),
|
|
7492
7902
|
start_at: flags["start-at"] || undefined,
|
|
7493
7903
|
end_at: flags["end-at"] || undefined,
|
|
7494
7904
|
clustering_run_id: flags["clustering-run-id"] || undefined,
|
|
@@ -7500,9 +7910,9 @@ async function generateSkills(flags) {
|
|
|
7500
7910
|
max_cluster_pct: parseOptionalFloat(flags["max-cluster-pct"]),
|
|
7501
7911
|
reprocess_segments: flags["reprocess-segments"] === "true" || forceReprocess,
|
|
7502
7912
|
run_replay: flags.replay === "true" && flags["no-replay"] !== "true",
|
|
7503
|
-
max_traces:
|
|
7913
|
+
max_traces: parseScopeInt(flags["max-traces"], "max-traces"),
|
|
7504
7914
|
improve_candidates: flags["improve-candidates"] === "true",
|
|
7505
|
-
improvement_rounds:
|
|
7915
|
+
improvement_rounds: parseScopeInt(flags["improvement-rounds"], "improvement-rounds", MAX_IMPROVEMENT_ROUNDS) ?? 2,
|
|
7506
7916
|
force_reprocess: forceReprocess,
|
|
7507
7917
|
wait_for_completion: waitForCompletion
|
|
7508
7918
|
});
|
|
@@ -7523,6 +7933,19 @@ async function generateSkills(flags) {
|
|
|
7523
7933
|
}
|
|
7524
7934
|
return await response.json();
|
|
7525
7935
|
}
|
|
7936
|
+
var MAX_IMPROVEMENT_ROUNDS = 3;
|
|
7937
|
+
function parseScopeInt(value, flag, max) {
|
|
7938
|
+
if (value === undefined)
|
|
7939
|
+
return;
|
|
7940
|
+
const parsed = /^\d+$/.test(value.trim()) ? Number.parseInt(value, 10) : Number.NaN;
|
|
7941
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
7942
|
+
throw new CliInputError(`--${flag}=${value} is not a positive integer, so nothing was sent.`, `0 would mean unlimited server-side; omit --${flag} for the server default, or pass a number of 1 or more.`);
|
|
7943
|
+
}
|
|
7944
|
+
if (max !== undefined && parsed > max) {
|
|
7945
|
+
throw new CliInputError(`--${flag}=${value} is above the maximum of ${max}, so nothing was sent.`);
|
|
7946
|
+
}
|
|
7947
|
+
return parsed;
|
|
7948
|
+
}
|
|
7526
7949
|
function parsePositiveInt3(value, fallback) {
|
|
7527
7950
|
if (!value)
|
|
7528
7951
|
return fallback;
|
|
@@ -7617,10 +8040,43 @@ async function runSkillsInstall(context, ids) {
|
|
|
7617
8040
|
}
|
|
7618
8041
|
return 0;
|
|
7619
8042
|
}
|
|
8043
|
+
var RESERVED_SKILL_PATHS = new Set(["inbox", "library", "observed", "runs", "sync", "surface", "families", "proposals"]);
|
|
8044
|
+
async function runSkillRegistryCommand(sub, positionals, flags, context) {
|
|
8045
|
+
const profileOptions = { profile: context.profile, env: context.env };
|
|
8046
|
+
assertFlags(`skills ${sub}`, flags, sub === "policy" ? ["live", "local", "dry-run"] : []);
|
|
8047
|
+
assertPositionals(`skills ${sub}`, positionals, sub === "list" ? 1 : 2, sub === "list" ? "moda skills list" : `moda skills ${sub} <key>${sub === "policy" ? " --live|--local" : ""}`);
|
|
8048
|
+
if (sub === "list") {
|
|
8049
|
+
context.output.writeData(await callControlAPI("/skills", {}, profileOptions));
|
|
8050
|
+
return 0;
|
|
8051
|
+
}
|
|
8052
|
+
const idOrKey = positionals[1];
|
|
8053
|
+
if (!idOrKey) {
|
|
8054
|
+
throw new CliInputError(`A skill key or id is required for skills ${sub}.`, sub === "show" ? "Usage: moda skills show <key>" : "Usage: moda skills policy <key> --live|--local");
|
|
8055
|
+
}
|
|
8056
|
+
if (RESERVED_SKILL_PATHS.has(idOrKey)) {
|
|
8057
|
+
throw new CliInputError(`'${idOrKey}' is a reserved path, so this skill must be addressed by its id.`, "Find the id (skill_…) with `moda skills list`, then pass that instead of the key.");
|
|
8058
|
+
}
|
|
8059
|
+
if (sub === "show") {
|
|
8060
|
+
context.output.writeData(await callControlAPI(`/skills/${encodeURIComponent(idOrKey)}`, {}, profileOptions));
|
|
8061
|
+
return 0;
|
|
8062
|
+
}
|
|
8063
|
+
const live = flags.live === "true";
|
|
8064
|
+
const local = flags.local === "true";
|
|
8065
|
+
if (live === local) {
|
|
8066
|
+
throw new CliInputError("Pass exactly one of --live or --local.", "Usage: moda skills policy <key> --live|--local");
|
|
8067
|
+
}
|
|
8068
|
+
const surfacePolicy = live ? "include" : "local_only";
|
|
8069
|
+
if (context.dryRun) {
|
|
8070
|
+
context.output.writeData({ dryRun: true, planned: { method: "PATCH", endpoint: `/api/skills/${encodeURIComponent(idOrKey)}/surface-policy`, body: { surfacePolicy } } });
|
|
8071
|
+
return 0;
|
|
8072
|
+
}
|
|
8073
|
+
context.output.writeData(await callControlAPI(`/skills/${encodeURIComponent(idOrKey)}/surface-policy`, { method: "PATCH", body: JSON.stringify({ surfacePolicy }) }, profileOptions, { retries: 0 }));
|
|
8074
|
+
return 0;
|
|
8075
|
+
}
|
|
7620
8076
|
|
|
7621
8077
|
// src/doctor.ts
|
|
7622
8078
|
import { createHash as createHash3 } from "node:crypto";
|
|
7623
|
-
import { existsSync as existsSync5, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
8079
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
|
|
7624
8080
|
import { join as join4 } from "node:path";
|
|
7625
8081
|
async function runDoctorCommand(context) {
|
|
7626
8082
|
const report = await buildDoctorReport(context, {
|