@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.
@@ -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-07tv75te.js";
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-kegd748v.js";
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-q79sq80a.js";
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"] || resolveTenantId(profileOptions);
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: 1 });
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 Error(`Could not resolve prompt '${trimmed}' — pass a .prompt.md path or a discovered prompt key`);
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 ensureReplaySet(flags, tenantId, profileOptions) {
5771
+ async function planReplaySet(flags, tenantId, profileOptions) {
5693
5772
  const existingSetId = (flags["set-id"] || flags["replay-set-id"] || "").trim();
5694
5773
  if (existingSetId) {
5695
- return existingSetId;
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 createSetFromConversations(conversations, flags, tenantId, profileOptions);
5782
+ return { kind: "traces", traces: conversations, cases: conversations.length };
5700
5783
  }
5701
- if (flags["auto-generate"] === "false" && !existingSetId) {
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
- const caseCount = parsePositiveInt(flags.cases ?? flags["case-count"], 5, 500);
5705
- const lookbackDays = parsePositiveInt(flags["lookback-days"], 30, 365);
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: 1 });
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 (!raw)
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
- return Math.min(parsed, max);
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 (wantGate) {
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 runGate(args) {
5927
- const { result, promptKey, replaySetId, flags, promoteOnWin, profileOptions, context } = args;
5928
- const tenantId = flags["tenant-id"] || resolveTenantId(profileOptions);
5929
- if (!tenantId) {
5930
- throw new Error("Missing tenant id for --gate. Run `moda init` or pass --tenant-id=<id>.");
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 seedsPerCase = parsePositiveInt2(flags.seeds ?? flags["seeds-per-case"], 3, 10);
5940
- const assistantModel = (flags["assistant-model"] ?? "").trim();
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
- const n = Number.parseInt(raw ?? "", 10);
6042
- if (!Number.isFinite(n) || n <= 0)
6231
+ function parsePositiveInt2(raw, fallback, max, flagName) {
6232
+ if (raw === undefined)
6043
6233
  return fallback;
6044
- return Math.min(n, max);
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
- context.output.writeData(await getPromptStatus());
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 propose.`);
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-n2xgzz31.js");
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: parsePositiveInt3(flags["max-sessions"], 0),
7491
- lookback_hours: parsePositiveInt3(flags["lookback-hours"], 0),
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: parsePositiveInt3(flags["max-traces"], 0),
7913
+ max_traces: parseScopeInt(flags["max-traces"], "max-traces"),
7504
7914
  improve_candidates: flags["improve-candidates"] === "true",
7505
- improvement_rounds: parsePositiveInt3(flags["improvement-rounds"], 2),
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, {