@moda-ai/cli 1.22.1 → 1.23.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/dist/cli-c4se7a5d.js +762 -0
- package/dist/cli-mnqa4rp6.js +1563 -0
- package/dist/cli-stmjdgvy.js +2842 -0
- package/dist/cli-y55bgg14.js +6980 -0
- package/dist/cli-ysegdrm5.js +286 -0
- package/dist/cli.js +4779 -1159
- package/dist/harness-github-actions-7t3cncnt.js +95 -0
- package/dist/index-4ser01zr.js +2769 -0
- package/dist/provision-hm1ppwj6.js +74 -0
- package/package.json +3 -2
- package/skills/integration/index.json +2 -2
- package/dist/agent-schemas.d.ts +0 -3
- package/dist/agent-schemas.js +0 -358
- package/dist/agent.d.ts +0 -5
- package/dist/agent.js +0 -477
- package/dist/analyst-tui.d.ts +0 -81
- package/dist/analyst-tui.js +0 -314
- package/dist/api-client.d.ts +0 -46
- package/dist/api-client.js +0 -120
- package/dist/ask-stream.d.ts +0 -146
- package/dist/ask-stream.js +0 -473
- package/dist/auth-storage.d.ts +0 -35
- package/dist/auth-storage.js +0 -159
- package/dist/auth.d.ts +0 -50
- package/dist/auth.js +0 -694
- package/dist/cli-session.d.ts +0 -26
- package/dist/cli-session.js +0 -169
- package/dist/cli.d.ts +0 -44
- package/dist/command-context.d.ts +0 -62
- package/dist/command-context.js +0 -57
- package/dist/command-registry.d.ts +0 -22
- package/dist/command-registry.js +0 -15
- package/dist/config.d.ts +0 -60
- package/dist/config.js +0 -110
- package/dist/doctor.d.ts +0 -79
- package/dist/doctor.js +0 -1182
- package/dist/errors.d.ts +0 -50
- package/dist/errors.js +0 -64
- package/dist/exit-funnel.d.ts +0 -14
- package/dist/exit-funnel.js +0 -53
- package/dist/harness-artifact-bodies.d.ts +0 -28
- package/dist/harness-artifact-bodies.js +0 -276
- package/dist/harness-candidates.d.ts +0 -77
- package/dist/harness-candidates.js +0 -482
- package/dist/harness-remote.d.ts +0 -135
- package/dist/harness-remote.js +0 -540
- package/dist/harness-report.d.ts +0 -145
- package/dist/harness-report.js +0 -1579
- package/dist/harness-sync-limits.d.ts +0 -8
- package/dist/harness-sync-limits.js +0 -21
- package/dist/harness.d.ts +0 -463
- package/dist/harness.js +0 -4009
- package/dist/init/index.d.ts +0 -78
- package/dist/init/index.js +0 -1442
- package/dist/init/mcp.d.ts +0 -2
- package/dist/init/mcp.js +0 -27
- package/dist/init/progress-board.d.ts +0 -80
- package/dist/init/progress-board.js +0 -225
- package/dist/init/rules.d.ts +0 -6
- package/dist/init/rules.js +0 -171
- package/dist/init/sdk-catalog.d.ts +0 -10
- package/dist/init/sdk-catalog.js +0 -80
- package/dist/init/sdk-guide.d.ts +0 -19
- package/dist/init/sdk-guide.js +0 -185
- package/dist/init/sdk-integration.d.ts +0 -53
- package/dist/init/sdk-integration.js +0 -835
- package/dist/init/sdk-protocol.d.ts +0 -135
- package/dist/init/sdk-protocol.js +0 -152
- package/dist/init/sdk-verification.d.ts +0 -27
- package/dist/init/sdk-verification.js +0 -123
- package/dist/init/tenant.d.ts +0 -67
- package/dist/init/tenant.js +0 -421
- package/dist/intelligence-schema.d.ts +0 -349
- package/dist/intelligence-schema.js +0 -174
- package/dist/keys.d.ts +0 -47
- package/dist/keys.js +0 -136
- package/dist/manifest.d.ts +0 -5
- package/dist/manifest.js +0 -114
- package/dist/markdown-terminal.d.ts +0 -49
- package/dist/markdown-terminal.js +0 -212
- package/dist/output.d.ts +0 -191
- package/dist/output.js +0 -624
- package/dist/production-intelligence.d.ts +0 -175
- package/dist/production-intelligence.js +0 -1430
- package/dist/profile-commands.d.ts +0 -10
- package/dist/profile-commands.js +0 -342
- package/dist/profiles.d.ts +0 -112
- package/dist/profiles.js +0 -573
- package/dist/prompts-ab.d.ts +0 -71
- package/dist/prompts-ab.js +0 -313
- package/dist/prompts-propose.d.ts +0 -15
- package/dist/prompts-propose.js +0 -212
- package/dist/prompts.d.ts +0 -58
- package/dist/prompts.js +0 -418
- package/dist/provision.d.ts +0 -28
- package/dist/provision.js +0 -104
- package/dist/redact.d.ts +0 -2
- package/dist/redact.js +0 -90
- package/dist/schemas.d.ts +0 -225
- package/dist/schemas.js +0 -126
- package/dist/skills-bundled.d.ts +0 -35
- package/dist/skills-bundled.js +0 -157
- package/dist/skills-manifest.d.ts +0 -33
- package/dist/skills-manifest.js +0 -244
- package/dist/skills.d.ts +0 -57
- package/dist/skills.js +0 -783
- package/dist/sync.d.ts +0 -5
- package/dist/sync.js +0 -36
- package/dist/telemetry.d.ts +0 -56
- package/dist/telemetry.js +0 -167
- package/dist/transforms.d.ts +0 -25
- package/dist/transforms.js +0 -102
- package/dist/tui.d.ts +0 -48
- package/dist/tui.js +0 -393
- package/dist/update-check.d.ts +0 -31
- package/dist/update-check.js +0 -141
- package/dist/workspace.d.ts +0 -12
- package/dist/workspace.js +0 -1294
package/dist/prompts-ab.js
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
-
import { callControlAPI, callDataAPI } from './api-client.js';
|
|
4
|
-
import { resolveTenantId, validateConfig } from './config.js';
|
|
5
|
-
import { discoverPrompts, parsePromptFileForAb, readPromptLock, runPromptSync, } from './prompts.js';
|
|
6
|
-
const DEFAULT_SEEDS_PER_CASE = 3;
|
|
7
|
-
export const DEFAULT_POLL_INTERVAL_MS = 15_000;
|
|
8
|
-
export const DEFAULT_WAIT_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
9
|
-
export const TERMINAL_RUN_STATUSES = new Set(['completed', 'skipped', 'error']);
|
|
10
|
-
export async function runPromptAb(flags, profileOptions, context) {
|
|
11
|
-
validateConfig();
|
|
12
|
-
const tenantId = flags['tenant-id'] || resolveTenantId(profileOptions);
|
|
13
|
-
if (!tenantId) {
|
|
14
|
-
throw new Error('Missing tenant id. Run `moda init` or pass --tenant-id=<id>.');
|
|
15
|
-
}
|
|
16
|
-
const baselineSource = flags.baseline || flags['baseline-file'] || flags['baseline-key'];
|
|
17
|
-
const candidateSource = flags.candidate || flags['candidate-file'] || flags['candidate-key'];
|
|
18
|
-
if (!baselineSource || !candidateSource) {
|
|
19
|
-
throw new Error('Usage: moda prompts ab --baseline=<file|key> --candidate=<file|key> [--auto-generate|--set-id=ID|--conversations=id1,id2]');
|
|
20
|
-
}
|
|
21
|
-
if (flags.sync === 'true') {
|
|
22
|
-
await runPromptSync({ ...flags, watch: 'false' }, profileOptions);
|
|
23
|
-
}
|
|
24
|
-
const promptArms = {
|
|
25
|
-
prod: resolvePromptArmSpec(baselineSource, 'baseline'),
|
|
26
|
-
proposed: resolvePromptArmSpec(candidateSource, 'candidate'),
|
|
27
|
-
};
|
|
28
|
-
const replaySetId = await ensureReplaySet(flags, tenantId, profileOptions);
|
|
29
|
-
const seedsPerCase = parsePositiveInt(flags.seeds ?? flags['seeds-per-case'], DEFAULT_SEEDS_PER_CASE, 10);
|
|
30
|
-
const assistantModel = (flags.model ?? flags['assistant-model'] ?? '').trim();
|
|
31
|
-
const enqueue = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(replaySetId)}/run`, {
|
|
32
|
-
method: 'POST',
|
|
33
|
-
body: JSON.stringify({
|
|
34
|
-
promptArms,
|
|
35
|
-
seedsPerCase,
|
|
36
|
-
assistantModel: assistantModel || undefined,
|
|
37
|
-
promotePrimary: flags['no-promote'] !== 'true',
|
|
38
|
-
}),
|
|
39
|
-
}, profileOptions, { timeoutMs: 120_000, retries: 1 });
|
|
40
|
-
const noWait = flags.wait === 'false' || flags['no-wait'] === 'true';
|
|
41
|
-
if (noWait) {
|
|
42
|
-
context.output.writeData({
|
|
43
|
-
replaySetId,
|
|
44
|
-
runId: enqueue.runId,
|
|
45
|
-
status: enqueue.status,
|
|
46
|
-
message: enqueue.message ?? 'Replay comparison queued',
|
|
47
|
-
});
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
const timeoutMs = parsePositiveInt(flags.timeout, DEFAULT_WAIT_TIMEOUT_MS, 24 * 60 * 60 * 1000);
|
|
51
|
-
const pollIntervalMs = parsePositiveInt(flags['poll-interval'], DEFAULT_POLL_INTERVAL_MS, 120_000);
|
|
52
|
-
const deadline = Date.now() + timeoutMs;
|
|
53
|
-
let latest = null;
|
|
54
|
-
while (Date.now() < deadline) {
|
|
55
|
-
latest = await fetchLatestRun(tenantId, replaySetId, enqueue.runId, profileOptions);
|
|
56
|
-
const status = latest.run?.status ?? '';
|
|
57
|
-
const runId = latest.run?.runId ?? '';
|
|
58
|
-
if (TERMINAL_RUN_STATUSES.has(status) && runId === enqueue.runId) {
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
61
|
-
if (context.outputMode === 'human') {
|
|
62
|
-
process.stderr.write(`Waiting for replay run (${status || 'pending'})...\n`);
|
|
63
|
-
}
|
|
64
|
-
await sleep(pollIntervalMs);
|
|
65
|
-
}
|
|
66
|
-
if (!latest?.run
|
|
67
|
-
|| latest.run.runId !== enqueue.runId
|
|
68
|
-
|| !TERMINAL_RUN_STATUSES.has(latest.run.status)) {
|
|
69
|
-
throw new Error(`Timed out after ${timeoutMs}ms waiting for replay run ${enqueue.runId} to finish`);
|
|
70
|
-
}
|
|
71
|
-
const result = {
|
|
72
|
-
replaySetId,
|
|
73
|
-
runId: latest.run.runId,
|
|
74
|
-
status: latest.run.status,
|
|
75
|
-
verdict: formatVerdict(latest),
|
|
76
|
-
run: { ...latest.run },
|
|
77
|
-
cases: latest.cases.map((row) => ({ ...row })),
|
|
78
|
-
};
|
|
79
|
-
if (context.outputMode === 'human') {
|
|
80
|
-
printHumanVerdict(result);
|
|
81
|
-
}
|
|
82
|
-
context.output.writeData(result);
|
|
83
|
-
}
|
|
84
|
-
export function resolvePromptArmSpec(source, label, opts = {}) {
|
|
85
|
-
const trimmed = source.trim();
|
|
86
|
-
if (!trimmed) {
|
|
87
|
-
throw new Error(`Empty prompt source for ${label}`);
|
|
88
|
-
}
|
|
89
|
-
const fromDiscovery = () => {
|
|
90
|
-
const discovered = discoverPrompts();
|
|
91
|
-
const match = discovered.find((prompt) => prompt.key === trimmed)
|
|
92
|
-
?? discovered.find((prompt) => prompt.sourcePath === trimmed);
|
|
93
|
-
return match ? { key: match.key, content: match.content } : null;
|
|
94
|
-
};
|
|
95
|
-
const absPath = isAbsolute(trimmed) ? trimmed : resolve(process.cwd(), trimmed);
|
|
96
|
-
let key;
|
|
97
|
-
let content;
|
|
98
|
-
// When the caller knows `source` is a registry key (e.g. `propose --gate`'s
|
|
99
|
-
// `--prompt-key`), resolve by discovery first so a same-named file/dir in the
|
|
100
|
-
// cwd can't shadow the intended prompt; fall back to path resolution otherwise.
|
|
101
|
-
const preferred = opts.preferKey ? fromDiscovery() : null;
|
|
102
|
-
if (preferred) {
|
|
103
|
-
key = preferred.key;
|
|
104
|
-
content = preferred.content;
|
|
105
|
-
}
|
|
106
|
-
else if (opts.preferKey) {
|
|
107
|
-
// Key-first resolution must NOT fall through to path resolution — otherwise a
|
|
108
|
-
// cwd file named like the key would still shadow an unresolved registry key.
|
|
109
|
-
throw new Error(`Could not resolve prompt key '${trimmed}' — no discovered prompt matched this key`);
|
|
110
|
-
}
|
|
111
|
-
else if (existsSync(absPath)) {
|
|
112
|
-
const parsed = parsePromptFileForAb(absPath);
|
|
113
|
-
key = parsed.key;
|
|
114
|
-
content = parsed.content;
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
const match = fromDiscovery();
|
|
118
|
-
if (!match) {
|
|
119
|
-
throw new Error(`Could not resolve prompt '${trimmed}' — pass a .prompt.md path or a discovered prompt key`);
|
|
120
|
-
}
|
|
121
|
-
key = match.key;
|
|
122
|
-
content = match.content;
|
|
123
|
-
}
|
|
124
|
-
const locked = readPromptLock()?.prompts[key];
|
|
125
|
-
return {
|
|
126
|
-
content,
|
|
127
|
-
prompt_version_id: locked?.versionId ?? '',
|
|
128
|
-
prompt_id: locked?.promptId ?? '',
|
|
129
|
-
prompt_label: label,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
async function ensureReplaySet(flags, tenantId, profileOptions) {
|
|
133
|
-
const existingSetId = (flags['set-id'] || flags['replay-set-id'] || '').trim();
|
|
134
|
-
if (existingSetId) {
|
|
135
|
-
return existingSetId;
|
|
136
|
-
}
|
|
137
|
-
const conversations = parseCsv(flags.conversations ?? flags['conversation-ids']);
|
|
138
|
-
if (conversations.length) {
|
|
139
|
-
return createSetFromConversations(conversations, flags, tenantId, profileOptions);
|
|
140
|
-
}
|
|
141
|
-
if (flags['auto-generate'] === 'false' && !existingSetId) {
|
|
142
|
-
throw new Error('Provide --set-id=, --conversations=, or allow --auto-generate (default)');
|
|
143
|
-
}
|
|
144
|
-
const caseCount = parsePositiveInt(flags.cases ?? flags['case-count'], 5, 25);
|
|
145
|
-
const lookbackDays = parsePositiveInt(flags['lookback-days'], 30, 365);
|
|
146
|
-
const name = (flags.name ?? flags['set-name'] ?? `Prompt A/B ${new Date().toISOString().slice(0, 10)}`).trim();
|
|
147
|
-
const generated = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/auto-generate`, {
|
|
148
|
-
method: 'POST',
|
|
149
|
-
body: JSON.stringify({ name, caseCount, lookbackDays }),
|
|
150
|
-
}, profileOptions, { timeoutMs: 300_000, retries: 0 });
|
|
151
|
-
if (!generated?.id) {
|
|
152
|
-
throw new Error('Auto-generate did not return a replay set id');
|
|
153
|
-
}
|
|
154
|
-
return generated.id;
|
|
155
|
-
}
|
|
156
|
-
async function createSetFromConversations(conversationIds, flags, tenantId, profileOptions) {
|
|
157
|
-
const name = (flags.name ?? flags['set-name'] ?? `Prompt A/B ${conversationIds.length} conv`).trim();
|
|
158
|
-
const created = await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets`, {
|
|
159
|
-
method: 'POST',
|
|
160
|
-
body: JSON.stringify({ name, description: 'Created by moda prompts ab' }),
|
|
161
|
-
}, profileOptions);
|
|
162
|
-
let position = 0;
|
|
163
|
-
for (const conversationId of conversationIds) {
|
|
164
|
-
const scenario = await loadScenarioFromConversation(conversationId);
|
|
165
|
-
await callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(created.id)}/cases`, {
|
|
166
|
-
method: 'POST',
|
|
167
|
-
body: JSON.stringify({
|
|
168
|
-
title: conversationId,
|
|
169
|
-
scenario,
|
|
170
|
-
sourceConversationId: conversationId,
|
|
171
|
-
successCriteria: [
|
|
172
|
-
'The agent understands the user request',
|
|
173
|
-
'The agent uses tools appropriately when needed',
|
|
174
|
-
'The agent provides a helpful final answer',
|
|
175
|
-
],
|
|
176
|
-
position: position++,
|
|
177
|
-
}),
|
|
178
|
-
}, profileOptions);
|
|
179
|
-
}
|
|
180
|
-
return created.id;
|
|
181
|
-
}
|
|
182
|
-
async function loadScenarioFromConversation(conversationId) {
|
|
183
|
-
try {
|
|
184
|
-
const data = await callDataAPI(`/conversations/${encodeURIComponent(conversationId)}/context?msg_index=0&window=0`);
|
|
185
|
-
const firstUser = (data.messages ?? []).find((message) => message.role === 'user' && String(message.content ?? '').trim());
|
|
186
|
-
const opening = String(firstUser?.content ?? '').trim();
|
|
187
|
-
if (opening) {
|
|
188
|
-
return `The user says: "${opening}"\n\nReplay the conversation faithfully and help them accomplish their goal.`;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
// Fall through to generic scenario.
|
|
193
|
-
}
|
|
194
|
-
return `Replay conversation ${conversationId}: help the user accomplish their original goal using the available tools.`;
|
|
195
|
-
}
|
|
196
|
-
export async function fetchLatestRun(tenantId, replaySetId, runId, profileOptions) {
|
|
197
|
-
// Pin the poll to OUR run id — otherwise a concurrent comparison on the same
|
|
198
|
-
// set would make `/runs/latest` return the newer run forever and this command
|
|
199
|
-
// would stall until timeout even after its own run finished.
|
|
200
|
-
return callControlAPI(`/tenants/${encodeURIComponent(tenantId)}/replay-sets/${encodeURIComponent(replaySetId)}/runs/latest?runId=${encodeURIComponent(runId)}`, { method: 'GET' }, profileOptions, { timeoutMs: 60_000, retries: 2 });
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Strict outcome of a completed comparison for gating: 'candidate' only when the
|
|
204
|
-
* proposed arm passed strictly more cases than prod on a completed run. A tie,
|
|
205
|
-
* regression, skip, error, or missing run is never a candidate win.
|
|
206
|
-
*/
|
|
207
|
-
export function candidateOutcome(payload) {
|
|
208
|
-
const run = payload.run;
|
|
209
|
-
if (!run || run.status !== 'completed')
|
|
210
|
-
return 'no-run';
|
|
211
|
-
const delta = run.proposedPassCount - run.prodPassCount;
|
|
212
|
-
if (delta > 0)
|
|
213
|
-
return 'candidate';
|
|
214
|
-
if (delta < 0)
|
|
215
|
-
return 'baseline';
|
|
216
|
-
return 'tie';
|
|
217
|
-
}
|
|
218
|
-
export function formatVerdict(payload) {
|
|
219
|
-
const run = payload.run;
|
|
220
|
-
if (!run)
|
|
221
|
-
return 'no run';
|
|
222
|
-
if (run.status === 'skipped')
|
|
223
|
-
return `skipped: ${run.notes || 'no reason'}`;
|
|
224
|
-
if (run.status === 'error')
|
|
225
|
-
return `error: ${run.notes || 'unknown error'}`;
|
|
226
|
-
if (run.status !== 'completed')
|
|
227
|
-
return `status=${run.status}`;
|
|
228
|
-
const delta = run.proposedPassCount - run.prodPassCount;
|
|
229
|
-
const winner = delta > 0 ? 'candidate wins' : delta < 0 ? 'baseline wins' : 'tie on case passes';
|
|
230
|
-
const rates = `baseline ${run.prodPassCount}/${run.caseCount} (${pct(run.prodPassRate)}), candidate ${run.proposedPassCount}/${run.caseCount} (${pct(run.proposedPassRate)})`;
|
|
231
|
-
const notes = run.notes?.trim();
|
|
232
|
-
return `${winner}; ${rates}${notes ? `; ${notes}` : ''}`;
|
|
233
|
-
}
|
|
234
|
-
function printHumanVerdict(result) {
|
|
235
|
-
process.stderr.write('\nPrompt A/B replay verdict\n');
|
|
236
|
-
process.stderr.write('-------------------------\n');
|
|
237
|
-
process.stderr.write(`Replay set: ${result.replaySetId}\n`);
|
|
238
|
-
process.stderr.write(`Run id: ${result.runId}\n`);
|
|
239
|
-
process.stderr.write(`Status: ${result.status}\n`);
|
|
240
|
-
process.stderr.write(`Verdict: ${result.verdict}\n`);
|
|
241
|
-
if (result.cases.length) {
|
|
242
|
-
process.stderr.write('\nPer-case:\n');
|
|
243
|
-
for (const row of result.cases) {
|
|
244
|
-
const caseId = String(row.caseId ?? '');
|
|
245
|
-
const prod = formatPass(row.prodPass);
|
|
246
|
-
const proposed = formatPass(row.proposedPass);
|
|
247
|
-
process.stderr.write(` ${caseId}: baseline=${prod} candidate=${proposed}\n`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
process.stderr.write('\n');
|
|
251
|
-
}
|
|
252
|
-
function formatPass(value) {
|
|
253
|
-
if (value === null || value === undefined)
|
|
254
|
-
return 'abstained';
|
|
255
|
-
return value ? 'pass' : 'fail';
|
|
256
|
-
}
|
|
257
|
-
function pct(rate) {
|
|
258
|
-
return `${Math.round(Math.max(0, Math.min(1, rate)) * 100)}%`;
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Enqueue a prod-vs-proposed replay comparison and poll `/runs/latest` (pinned to
|
|
262
|
-
* OUR run id) until it reaches a terminal status or the deadline. Shared by
|
|
263
|
-
* `prompts ab` and the `prompts propose --gate` auto-gate. Returns the terminal
|
|
264
|
-
* comparison payload; throws on enqueue/poll timeout.
|
|
265
|
-
*/
|
|
266
|
-
export async function enqueueAndPollComparison(args, profileOptions) {
|
|
267
|
-
const enqueue = (await callControlAPI(`/tenants/${encodeURIComponent(args.tenantId)}/replay-sets/${encodeURIComponent(args.replaySetId)}/run`, {
|
|
268
|
-
method: 'POST',
|
|
269
|
-
body: JSON.stringify({
|
|
270
|
-
promptArms: args.promptArms,
|
|
271
|
-
seedsPerCase: args.seedsPerCase ?? DEFAULT_SEEDS_PER_CASE,
|
|
272
|
-
assistantModel: args.assistantModel || undefined,
|
|
273
|
-
promotePrimary: args.promotePrimary ?? false,
|
|
274
|
-
}),
|
|
275
|
-
}, profileOptions, { timeoutMs: 120_000, retries: 1 }));
|
|
276
|
-
const timeoutMs = args.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
277
|
-
const pollIntervalMs = args.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
278
|
-
const deadline = Date.now() + timeoutMs;
|
|
279
|
-
let latest = null;
|
|
280
|
-
while (Date.now() < deadline) {
|
|
281
|
-
latest = await fetchLatestRun(args.tenantId, args.replaySetId, enqueue.runId, profileOptions);
|
|
282
|
-
const status = latest.run?.status ?? '';
|
|
283
|
-
const runId = latest.run?.runId ?? '';
|
|
284
|
-
if (TERMINAL_RUN_STATUSES.has(status) && runId === enqueue.runId) {
|
|
285
|
-
return latest;
|
|
286
|
-
}
|
|
287
|
-
args.onWaiting?.(status || 'pending');
|
|
288
|
-
await sleep(pollIntervalMs);
|
|
289
|
-
}
|
|
290
|
-
if (!latest?.run
|
|
291
|
-
|| latest.run.runId !== enqueue.runId
|
|
292
|
-
|| !TERMINAL_RUN_STATUSES.has(latest.run.status)) {
|
|
293
|
-
throw new Error(`Timed out after ${timeoutMs}ms waiting for replay run ${enqueue.runId} to finish`);
|
|
294
|
-
}
|
|
295
|
-
return latest;
|
|
296
|
-
}
|
|
297
|
-
function parsePositiveInt(raw, fallback, max) {
|
|
298
|
-
if (!raw)
|
|
299
|
-
return fallback;
|
|
300
|
-
const parsed = Number.parseInt(raw, 10);
|
|
301
|
-
if (!Number.isFinite(parsed) || parsed < 1)
|
|
302
|
-
return fallback;
|
|
303
|
-
return Math.min(parsed, max);
|
|
304
|
-
}
|
|
305
|
-
function parseCsv(raw) {
|
|
306
|
-
if (!raw?.trim())
|
|
307
|
-
return [];
|
|
308
|
-
return raw.split(',').map((part) => part.trim()).filter(Boolean);
|
|
309
|
-
}
|
|
310
|
-
function sleep(ms) {
|
|
311
|
-
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
312
|
-
}
|
|
313
|
-
//# sourceMappingURL=prompts-ab.js.map
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { CommandContext } from './command-context.js';
|
|
2
|
-
import type { ProfileIOOptions } from './profiles.js';
|
|
3
|
-
/**
|
|
4
|
-
* `moda prompts propose` — turn a `prompts ab` run's failures into a revised
|
|
5
|
-
* candidate registered as a new (unlabeled) prompt version. Does NOT promote by
|
|
6
|
-
* default: the operator gates the candidate with a fresh `prompts ab` (ideally on
|
|
7
|
-
* the printed holdout cases the revision never saw) and promotes only if it wins.
|
|
8
|
-
*
|
|
9
|
-
* `--gate` chains that gate automatically: it A/Bs the fresh candidate (proposed
|
|
10
|
-
* arm) vs the on-disk baseline (prod arm) over the same replay set and prints the
|
|
11
|
-
* verdict. `--promote-on-win` then promotes the candidate to `prod` ONLY on a
|
|
12
|
-
* strict candidate win. `--model=<slug>` overrides the revision model.
|
|
13
|
-
*/
|
|
14
|
-
export declare function runPromptPropose(positionals: string[], flags: Record<string, string>, profileOptions: ProfileIOOptions, context: CommandContext): Promise<void>;
|
|
15
|
-
//# sourceMappingURL=prompts-propose.d.ts.map
|
package/dist/prompts-propose.js
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs';
|
|
2
|
-
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
-
import { callControlAPI } from './api-client.js';
|
|
4
|
-
import { resolveTenantId, validateConfig } from './config.js';
|
|
5
|
-
import { candidateOutcome, enqueueAndPollComparison, formatVerdict, resolvePromptArmSpec, } from './prompts-ab.js';
|
|
6
|
-
// Propose is a single OpenRouter revision behind the backend; give it room.
|
|
7
|
-
const PROPOSE_TIMEOUT_MS = 300_000;
|
|
8
|
-
/**
|
|
9
|
-
* `moda prompts propose` — turn a `prompts ab` run's failures into a revised
|
|
10
|
-
* candidate registered as a new (unlabeled) prompt version. Does NOT promote by
|
|
11
|
-
* default: the operator gates the candidate with a fresh `prompts ab` (ideally on
|
|
12
|
-
* the printed holdout cases the revision never saw) and promotes only if it wins.
|
|
13
|
-
*
|
|
14
|
-
* `--gate` chains that gate automatically: it A/Bs the fresh candidate (proposed
|
|
15
|
-
* arm) vs the on-disk baseline (prod arm) over the same replay set and prints the
|
|
16
|
-
* verdict. `--promote-on-win` then promotes the candidate to `prod` ONLY on a
|
|
17
|
-
* strict candidate win. `--model=<slug>` overrides the revision model.
|
|
18
|
-
*/
|
|
19
|
-
export async function runPromptPropose(positionals, flags, profileOptions, context) {
|
|
20
|
-
validateConfig();
|
|
21
|
-
const promptKey = flags['prompt-key'] || positionals[1];
|
|
22
|
-
const fromRunId = flags['from-run'] || flags['from-run-id'] || flags['run-id'];
|
|
23
|
-
const replaySetId = flags['set-id'] || flags['replay-set-id'];
|
|
24
|
-
if (!promptKey || !fromRunId || !replaySetId) {
|
|
25
|
-
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]');
|
|
26
|
-
}
|
|
27
|
-
const maxDossiers = parsePositiveInt(flags['max-dossiers'], 8, 16);
|
|
28
|
-
const model = (flags.model ?? flags['revise-model'] ?? '').trim();
|
|
29
|
-
const result = (await callControlAPI(`/prompts/${encodeURIComponent(promptKey)}/propose`, {
|
|
30
|
-
method: 'POST',
|
|
31
|
-
body: JSON.stringify({ fromRunId, replaySetId, maxDossiers, model: model || undefined }),
|
|
32
|
-
}, profileOptions, { timeoutMs: PROPOSE_TIMEOUT_MS, retries: 0 }));
|
|
33
|
-
let outPath;
|
|
34
|
-
if (result.status === 'proposed' && result.content && flags.out) {
|
|
35
|
-
outPath = isAbsolute(flags.out) ? flags.out : resolve(process.cwd(), flags.out);
|
|
36
|
-
writeFileSync(outPath, result.content, 'utf8');
|
|
37
|
-
}
|
|
38
|
-
// The gate needs the candidate content in-hand for the proposed arm.
|
|
39
|
-
const wantGate = flags.gate === 'true';
|
|
40
|
-
const promoteOnWin = flags['promote-on-win'] === 'true';
|
|
41
|
-
let gate;
|
|
42
|
-
if (wantGate) {
|
|
43
|
-
if (result.status !== 'proposed' || !result.content) {
|
|
44
|
-
gate = {
|
|
45
|
-
ran: false,
|
|
46
|
-
scope: 'full-set',
|
|
47
|
-
scopeNote: `no candidate to gate (${result.status})`,
|
|
48
|
-
verdict: 'skipped',
|
|
49
|
-
outcome: 'no-run',
|
|
50
|
-
runId: null,
|
|
51
|
-
promoted: false,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
gate = await runGate({
|
|
56
|
-
result,
|
|
57
|
-
promptKey,
|
|
58
|
-
replaySetId,
|
|
59
|
-
flags,
|
|
60
|
-
promoteOnWin,
|
|
61
|
-
profileOptions,
|
|
62
|
-
context,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (context.outputMode === 'human') {
|
|
67
|
-
printHuman(result, outPath, gate);
|
|
68
|
-
}
|
|
69
|
-
context.output.writeData({
|
|
70
|
-
...result,
|
|
71
|
-
...(outPath ? { outPath } : {}),
|
|
72
|
-
...(gate ? { gate } : {}),
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
async function runGate(args) {
|
|
76
|
-
const { result, promptKey, replaySetId, flags, promoteOnWin, profileOptions, context } = args;
|
|
77
|
-
const tenantId = flags['tenant-id'] || resolveTenantId(profileOptions);
|
|
78
|
-
if (!tenantId) {
|
|
79
|
-
throw new Error('Missing tenant id for --gate. Run `moda init` or pass --tenant-id=<id>.');
|
|
80
|
-
}
|
|
81
|
-
// prod arm = the on-disk/registry baseline. `promptKey` is always a registry
|
|
82
|
-
// key here, so resolve key-first (`preferKey`) — otherwise a same-named file or
|
|
83
|
-
// dir in the cwd could shadow it and gate the candidate against wrong content.
|
|
84
|
-
// proposed arm = the freshly proposed candidate content returned above.
|
|
85
|
-
const prodArm = resolvePromptArmSpec(promptKey, 'baseline', { preferKey: true });
|
|
86
|
-
const proposedArm = {
|
|
87
|
-
content: result.content ?? '',
|
|
88
|
-
prompt_version_id: result.versionId ?? '',
|
|
89
|
-
prompt_id: '',
|
|
90
|
-
prompt_label: 'candidate',
|
|
91
|
-
};
|
|
92
|
-
const seedsPerCase = parsePositiveInt(flags.seeds ?? flags['seeds-per-case'], 3, 10);
|
|
93
|
-
const assistantModel = (flags['assistant-model'] ?? '').trim();
|
|
94
|
-
const timeoutMs = parsePositiveInt(flags['gate-timeout'] ?? flags.timeout, 2 * 60 * 60 * 1000, 24 * 60 * 60 * 1000);
|
|
95
|
-
const pollIntervalMs = parsePositiveInt(flags['poll-interval'], 15_000, 120_000);
|
|
96
|
-
// The /run endpoint scores the WHOLE replay set — there is no per-case subset
|
|
97
|
-
// knob — so the gate cannot be scoped to just the holdout ids the revision
|
|
98
|
-
// never saw. Say so plainly so a win isn't read as overfitting-proof.
|
|
99
|
-
const scopeNote = result.holdoutCaseIds.length
|
|
100
|
-
? `gated over the FULL replay set (${result.holdoutCaseIds.length} holdout case(s) could not be isolated — the run endpoint scores every case)`
|
|
101
|
-
: 'gated over the FULL replay set';
|
|
102
|
-
if (context.outputMode === 'human') {
|
|
103
|
-
process.stderr.write(`\nGating candidate for '${promptKey}' — ${scopeNote}...\n`);
|
|
104
|
-
}
|
|
105
|
-
const payload = await enqueueAndPollComparison({
|
|
106
|
-
tenantId,
|
|
107
|
-
replaySetId,
|
|
108
|
-
promptArms: { prod: prodArm, proposed: proposedArm },
|
|
109
|
-
seedsPerCase,
|
|
110
|
-
assistantModel: assistantModel || undefined,
|
|
111
|
-
promotePrimary: false,
|
|
112
|
-
timeoutMs,
|
|
113
|
-
pollIntervalMs,
|
|
114
|
-
onWaiting: context.outputMode === 'human'
|
|
115
|
-
? (status) => process.stderr.write(`Waiting for gate run (${status})...\n`)
|
|
116
|
-
: undefined,
|
|
117
|
-
}, profileOptions);
|
|
118
|
-
const outcome = candidateOutcome(payload);
|
|
119
|
-
const gate = {
|
|
120
|
-
ran: true,
|
|
121
|
-
scope: 'full-set',
|
|
122
|
-
scopeNote,
|
|
123
|
-
verdict: formatVerdict(payload),
|
|
124
|
-
outcome,
|
|
125
|
-
runId: payload.run?.runId ?? null,
|
|
126
|
-
promoted: false,
|
|
127
|
-
};
|
|
128
|
-
if (promoteOnWin) {
|
|
129
|
-
if (outcome === 'candidate' && result.versionId) {
|
|
130
|
-
await callControlAPI(`/prompts/${encodeURIComponent(promptKey)}/labels`, { method: 'POST', body: JSON.stringify({ label: 'prod', versionId: result.versionId }) }, profileOptions);
|
|
131
|
-
gate.promoted = true;
|
|
132
|
-
}
|
|
133
|
-
else {
|
|
134
|
-
gate.promoteSkippedReason =
|
|
135
|
-
outcome === 'candidate'
|
|
136
|
-
? 'no candidate versionId to promote'
|
|
137
|
-
: `candidate did not strictly win (${outcome})`;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return gate;
|
|
141
|
-
}
|
|
142
|
-
function printHuman(result, outPath, gate) {
|
|
143
|
-
const w = (s) => process.stderr.write(s + '\n');
|
|
144
|
-
if (result.status !== 'proposed') {
|
|
145
|
-
w(`No candidate produced (${result.status}): ${result.note}`);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
w(`Proposed candidate for '${result.promptKey}'`);
|
|
149
|
-
w(` new version: ${result.versionId} (unlabeled — not promoted)`);
|
|
150
|
-
w(` base version: ${result.baseVersionId ?? '(none)'}`);
|
|
151
|
-
w(` from run: built from ${result.dossierCount} failure dossier(s)`);
|
|
152
|
-
if (result.changelog)
|
|
153
|
-
w(` changelog: ${result.changelog}`);
|
|
154
|
-
if (result.risks.length)
|
|
155
|
-
w(` risks: ${result.risks.map((r) => `- ${r}`).join('\n ')}`);
|
|
156
|
-
if (outPath)
|
|
157
|
-
w(` written to: ${outPath}`);
|
|
158
|
-
if (result.lowN) {
|
|
159
|
-
w('');
|
|
160
|
-
w(` ⚠ low sample: ${result.lowN}`);
|
|
161
|
-
}
|
|
162
|
-
w('');
|
|
163
|
-
if (gate?.ran) {
|
|
164
|
-
w('Gate (automatic A/B of candidate vs baseline):');
|
|
165
|
-
w(` scope: ${gate.scopeNote}`);
|
|
166
|
-
w(` verdict: ${gate.verdict}`);
|
|
167
|
-
if (gate.runId)
|
|
168
|
-
w(` run id: ${gate.runId}`);
|
|
169
|
-
if (gate.promoted) {
|
|
170
|
-
w(` promoted: candidate won — promoted version ${result.versionId} to label 'prod'`);
|
|
171
|
-
}
|
|
172
|
-
else if (gate.promoteSkippedReason) {
|
|
173
|
-
w(` promote: skipped (${gate.promoteSkippedReason})`);
|
|
174
|
-
}
|
|
175
|
-
w('');
|
|
176
|
-
if (!gate.promoted) {
|
|
177
|
-
w(` moda prompts promote ${result.promptKey} --label=prod --version=${result.versionId}`);
|
|
178
|
-
}
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
if (gate && !gate.ran) {
|
|
182
|
-
w(`Gate not run: ${gate.scopeNote}`);
|
|
183
|
-
w('');
|
|
184
|
-
}
|
|
185
|
-
w('Next — gate on holdout cases the revision never saw, then promote if it wins:');
|
|
186
|
-
if (result.holdoutCaseIds.length) {
|
|
187
|
-
w(` holdout cases (${result.holdoutCaseIds.length}): ${result.holdoutCaseIds.join(', ')}`);
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
w(' (too few failures to reserve a holdout — gate on a fresh --auto-generate set)');
|
|
191
|
-
}
|
|
192
|
-
if (outPath) {
|
|
193
|
-
// The candidate lives only in the registry as an unlabeled version; `prompts ab`
|
|
194
|
-
// resolves --candidate from local file content, so point it at the written --out
|
|
195
|
-
// file. Using --candidate-key=<key> would resolve to the baseline and compare the
|
|
196
|
-
// prompt against itself.
|
|
197
|
-
w(` moda prompts ab --baseline-key=${result.promptKey} --candidate=${outPath} --auto-generate`);
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
w(` # re-run with --out=<path.prompt.md> to materialize the candidate, then:`);
|
|
201
|
-
w(` moda prompts ab --baseline-key=${result.promptKey} --candidate=<path.prompt.md> --auto-generate`);
|
|
202
|
-
}
|
|
203
|
-
w(` # or: moda prompts propose ... --gate --promote-on-win (auto-A/B, promote on a strict win)`);
|
|
204
|
-
w(` moda prompts promote ${result.promptKey} --label=prod --version=${result.versionId}`);
|
|
205
|
-
}
|
|
206
|
-
function parsePositiveInt(raw, fallback, max) {
|
|
207
|
-
const n = Number.parseInt(raw ?? '', 10);
|
|
208
|
-
if (!Number.isFinite(n) || n <= 0)
|
|
209
|
-
return fallback;
|
|
210
|
-
return Math.min(n, max);
|
|
211
|
-
}
|
|
212
|
-
//# sourceMappingURL=prompts-propose.js.map
|
package/dist/prompts.d.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { type CommandContext } from './command-context.js';
|
|
2
|
-
import type { ProfileIOOptions } from './profiles.js';
|
|
3
|
-
interface PromptFile {
|
|
4
|
-
key: string;
|
|
5
|
-
name: string;
|
|
6
|
-
description?: string;
|
|
7
|
-
category?: string;
|
|
8
|
-
sourcePath: string;
|
|
9
|
-
content: string;
|
|
10
|
-
systemPrompt?: string;
|
|
11
|
-
messages?: unknown[];
|
|
12
|
-
variables?: unknown[];
|
|
13
|
-
variablesSchema?: Record<string, unknown>;
|
|
14
|
-
modelConfig?: Record<string, unknown>;
|
|
15
|
-
toolRefs?: unknown[];
|
|
16
|
-
responseSchema?: Record<string, unknown>;
|
|
17
|
-
contentHash: string;
|
|
18
|
-
}
|
|
19
|
-
interface PromptLock {
|
|
20
|
-
version: 1;
|
|
21
|
-
generatedAt: string;
|
|
22
|
-
prompts: Record<string, {
|
|
23
|
-
key: string;
|
|
24
|
-
sourcePath: string;
|
|
25
|
-
contentHash: string;
|
|
26
|
-
versionId?: string;
|
|
27
|
-
promptId?: string;
|
|
28
|
-
}>;
|
|
29
|
-
}
|
|
30
|
-
export declare function runPromptsCommand(input: CommandContext | string[], legacyFlags?: Record<string, string>, legacyProfileOptions?: ProfileIOOptions): Promise<void>;
|
|
31
|
-
export declare function initPrompts(options?: {
|
|
32
|
-
silent?: boolean;
|
|
33
|
-
}): Promise<void>;
|
|
34
|
-
export interface PromptSyncResult {
|
|
35
|
-
synced: Array<{
|
|
36
|
-
key: string;
|
|
37
|
-
promptId?: string;
|
|
38
|
-
versionId?: string;
|
|
39
|
-
contentHash: string;
|
|
40
|
-
sourcePath?: string;
|
|
41
|
-
}>;
|
|
42
|
-
dryRun: boolean;
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Sync the manifest's prompt files to the backend and update the lockfile.
|
|
46
|
-
* Returns the synced list without printing, so callers (e.g. `moda init`)
|
|
47
|
-
* control their own output. `moda prompts sync` wraps this with JSON output.
|
|
48
|
-
*/
|
|
49
|
-
export declare function runPromptSync(flags: Record<string, string>, profileOptions: ProfileIOOptions): Promise<PromptSyncResult>;
|
|
50
|
-
export declare function discoverPrompts(): PromptFile[];
|
|
51
|
-
export declare function readPromptLock(): PromptLock | null;
|
|
52
|
-
/** Parse a prompt file for A/B runs (body text + registry key). */
|
|
53
|
-
export declare function parsePromptFileForAb(absPath: string): {
|
|
54
|
-
key: string;
|
|
55
|
-
content: string;
|
|
56
|
-
};
|
|
57
|
-
export {};
|
|
58
|
-
//# sourceMappingURL=prompts.d.ts.map
|